我们先从这里开始:在内存中开辟一块存储区,然后将它的内容以最简单的光栅格式写到文件中,也就是 PPM(Portable Pixel Map)格式。虽然 Windows 对这种格式并没有原生的支持,但很多图像浏览器和转换器都能使用这种格式,比如 IrfanView(www.irfanview.com)。所有 AGG 的控制台例子都使用了 P6 256 格式,也就是 RGB,每个字节代码一个颜色。现在假设我们将在下图所示的 RGB-buffer 内存区中工作:
这是第一个例子:
#include <stdio.h> #include <string.h> #include "agg_rendering_buffer.h" enum { frame_width = 320, frame_height = 200 }; // Writing the buffer to a .PPM file, assuming it has // RGB-structure, one byte per color component //-------------------------------------------------- bool write_ppm(const unsigned char* buf, unsigned width, unsigned height, const char* file_name) { FILE* fd = fopen(file_name, "wb"); if(fd) { fprintf(fd, "P6 %d %d 255 ", width, height); fwrite(buf, 1, width * height * 3, fd); fclose(fd); return true; } return false; } // Draw a black frame around the rendering buffer, assuming it has // RGB-structure, one byte per color component //-------------------------------------------------- void draw_black_frame(agg::rendering_buffer& rbuf) { unsigned i; for(i = 0; i < rbuf.height(); ++i) { unsigned char* p = rbuf.row_ptr(i); *p++ = 0; *p++ = 0; *p++ = 0; p += (rbuf.width() - 2) * 3; *p++ = 0; *p++ = 0; *p++ = 0; } memset(rbuf.row_ptr(0), 0, rbuf.width() * 3); memset(rbuf.row_ptr(rbuf.height() - 1), 0, rbuf.width() * 3); } int main() { // In the first example we do the following: //-------------------- // Allocate the buffer. // Clear the buffer, for now "manually" // Create the rendering buffer object // Do something simple, draw a diagonal line // Write the buffer to agg_test.ppm // Free memory unsigned char* buffer = new unsigned char[frame_width * frame_height * 3]; memset(buffer, 255, frame_width * frame_height * 3); agg::rendering_buffer rbuf(buffer, frame_width, frame_height, frame_width * 3); unsigned i; for(i = 0; i < rbuf.height()/2; ++i) { // Get the pointer to the beginning of the i-th row (Y-coordinate) // and shift it to the i-th position, that is, X-coordinate. //--------------- unsigned char* ptr = rbuf.row_ptr(i) + i * 3; // PutPixel, very sophisticated, huh? :) //------------- *ptr++ = 127; // R *ptr++ = 200; // G *ptr++ = 98; // B } draw_black_frame(rbuf); write_ppm(buffer, frame_width, frame_height, "agg_test.ppm"); delete [] buffer; return 0; }
在这个例子中,你甚至不需要链接任何的 AGG 的代码文件,你只需要在你的编译器命令行中设置好 AGG 的包含路径就行了。
编译并运行它,你会看到现图所示的结果:
这个例子中几乎所有东西都“手工打制”的,使用的唯一一个现成的类是 rendering_buffer。这个类本身并不知道关于内存中像素格式的任何信息,它只是保存了一个数组,数组中的元素分别指向每行(像素的开头)。为申请和释放这块存储区是使用者的责任,你可以使用任何可行的方式来申请和释放内存,比如使用系统提供的 API 函数,或是简单的用内存分配器(译注:应该是new、delete、malloc、free等),甚至是直接使用一个静态数组。在上面这个例子中,因为每个像素要占用3个字节,所以我们申请了 width*height*3 字节的内存,在实际内存中是不存在“行”这种布局方式的,但这样做可以提高程序的性能,而且有时候在使用 API 的时候需要。
包含文件: agg_rendering_buffer.h
rendering_buffer 这个类保存了指向每一行像素的指针,基本上这个类做的事就是这些了。看起来好像不是什么了不起的事,不过我们还是继续分析下去。这个类的接口和功能都很简单,它只是模板类 row_ptr_cache 的一个 typedef 而已:
typedef row_ptr_cache<int8u> rendering_buffer;
row_prt_cache 这个类的接口的功能如下:
template<class T> class row_ptr_cache { public: row_ptr_cache(); row_ptr_cache(T* buf, unsigned width, unsigned height, int stride); void attach(T* buf, unsigned width, unsigned height, int stride); T* buf(); const T* buf() const; unsigned width() const; unsigned height() const; int stride() const; unsigned stride_abs() const; T* row_ptr(int, int y, unsigned); T* row_ptr(int y); const T* row_ptr(int y) const; row_data row (int y) const; T const* const* rows() const; template<class RenBuf> void copy_from(const RenBuf& src); void clear(T value) };
这个类的实现里没有使用断言或是验证的措施,所以,使用者有责任在用这个类对象时正确地将它初始化到实际的内存块中,这可以在构造函数中完成,也可以使用 attach() 函数。它们的参数解释如下:
attach()函数会改变缓冲区或是它的参数,它自己会为“行指针”数组重新分配内存,所以你可以在任何时候调用它。当(且仅当)新的height值比之前使用过的最大的height值还要大时,它才会重新申请内存。
构造的这个对象的开销仅仅是初始化它的成员变量(设置为0),attach()的开销则是分配sizeof(ptr)*height个字节的内存,然后将这些指针指向对应的“行”。
最常使用的函数是 row_prt(y),这个函数只是简单地返回指向第y函数指针,这个指针指向的位置已经考虑到了Y轴的方向了。
注意:
渲染内存区(rendering buffer)并不管任何裁减或是边界检查的事,这是更高层的类的责任。
buf(), width(), height(), stride(), stride_abs() 这些函数的意义显而易见,就不解释了。
copy_from()函数会将其它内存的内容拷贝至“本”内存中。这个函数是安全的,如果(两者的)width和height值不相同,那它会尽可能拷贝大一些的区域。一般来讲都用于拷贝相同大小区域。
首先,在创建 rendering buffer 的对象时将 stride 取负值:
agg::rendering_buffer rbuf(buffer, frame_width, frame_height, -frame_width * 3);
然后,我们试下将 rendering buffer 附着(attach)到被分配的内存区的某部分。这个修改会使得 rendering buffer 两次附着在同一块内存区上,第一次是整个被分配的内存区域,第二次是其中的一部分:
int main() { unsigned char* buffer = new unsigned char[frame_width * frame_height * 3]; memset(buffer, 255, frame_width * frame_height * 3); agg::rendering_buffer rbuf(buffer, frame_width, frame_height, frame_width * 3); // Draw the outer black frame //------------------------ draw_black_frame(rbuf); // Attach to the part of the buffer, // with 20 pixel margins at each side. rbuf.attach(buffer + frame_width * 3 * 20 + // initial Y-offset 3 * 20, // initial X-offset frame_width - 40, frame_height - 40, frame_width * 3 // Note that the stride // remains the same ); // Draw a diagonal line //------------------------ unsigned i; for(i = 0; i < rbuf.height()/2; ++i) { // Get the pointer to the beginning of the i-th row (Y-coordinate) // and shift it to the i-th position, that is, X-coordinate. //--------------- unsigned char* ptr = rbuf.row_ptr(i) + i * 3; // PutPixel, very sophisticated, huh? :) //------------- *ptr++ = 127; // R *ptr++ = 200; // G *ptr++ = 98; // B } // Draw the inner black frame //------------------------ draw_black_frame(rbuf); // Write to a file //------------------------ write_ppm(buffer, frame_width, frame_height, "agg_test.ppm"); delete [] buffer; return 0; }
最后描画出来的结果是这样:
最后一处修改是:
// Attach to the part of the buffer, // with 20 pixel margins at each side and negative 'stride' rbuf.attach(buffer + frame_width * 3 * 20 + // initial Y-offset 3 * 20, // initial X-offset frame_width - 40, frame_height - 40, -frame_width * 3 // Negate the stride );
运行结果如下:
在最后的一个例子里,我们只是使 stride 取了负值,而指针则和上个例子一样,仍然指向内存区的起启处。
注意:Function
write_ppm()
writes the pixel map to a file. Hereafter it will be omited in this text, but duplicated when necessary in source code in theagg2/tutorial
directory.。
首先,我们创建一个更“文明”(译注:显得更高级一点)的例子:
#include <stdio.h> #include <string.h> #include "agg_pixfmt_rgb24.h" enum { frame_width = 320, frame_height = 200 }; // [...write_ppm is skipped...] // Draw a black frame around the rendering buffer //-------------------------------------------------- template<class Ren> void draw_black_frame(Ren& ren) { unsigned i; agg::rgba8 c(0,0,0); for(i = 0; i < ren.height(); ++i) { ren.copy_pixel(0, i, c); ren.copy_pixel(ren.width() - 1, i, c); } for(i = 0; i < ren.width(); ++i) { ren.copy_pixel(i, 0, c); ren.copy_pixel(i, ren.height() - 1, c); } } int main() { //-------------------- // Allocate the buffer. // Clear the buffer, for now "manually" // Create the rendering buffer object // Create the Pixel Format renderer // Do something simple, draw a diagonal line // Write the buffer to agg_test.ppm // Free memory unsigned char* buffer = new unsigned char[frame_width * frame_height * 3]; memset(buffer, 255, frame_width * frame_height * 3); agg::rendering_buffer rbuf(buffer, frame_width, frame_height, frame_width * 3); agg::pixfmt_rgb24 pixf(rbuf); unsigned i; for(i = 0; i < pixf.height()/2; ++i) { pixf.copy_pixel(i, i, agg::rgba8(127, 200, 98)); } draw_black_frame(pixf); write_ppm(buffer, frame_width, frame_height, "agg_test.ppm"); delete [] buffer; return 0; }
这个例子看起来和前面的没什么不一样的,但其实他们差别很大,看看这个声明:
agg::pixfmt_rgb24 pixf(rbuf);
这里我们创建了一个底层的像素渲染对象(pixel rendering object)并将它附着到渲染内存区(rendering buffer)上,它是这样定义的:
typedef pixel_formats_rgb24<order_rgb24> pixfmt_rgb24;
类模板 pixel_formats_rgb24 掌握了内存中具体的像素格式信息。唯一的模板参数可以是 order_rgb24 或是 order_rgb23,它们定义了颜色字节(color channels)的顺序。
与 rendering buffer 不同的是,这些类使用整型的像素坐标进行操作,因为它们知道怎么计算对于特定点 X 的偏移。你可能会说,如果在 rendering buffer 中保存像素的宽度值的话会更容易,但是在实践中会有很多限制。别忘了,像素宽度可能比一个字节还小,比如在打印机渲染高解析度的 B&W 图像的时候就是这样。因此,我们需要将这个功能分离出来,rendering_buffer 这个类就用于加速对“行”的访问,而 pixel format renderers 就负责如何解析“行”是什么。
现在,AGG 里下面这些文件实现了各种不同的像素格式:
像素格式的类定义了它们原始的颜色空间和颜色类型,像下面这样:
typedef rgba8 color_type;
对于 pixfmt_gray8_nnn,这些都是 gray8,这种机制允许你写自己的像素和颜色格式,举个例子,HSV、CMYK 等等,AGG 的其它部分可以完全无误地和你自己定义的新的像素格式一起工作。
注意
区分清楚 color type 和 buffer 代表的原始的颜色空间是非常重要的。比如说,你可以假设你正在使用 CMYK 进行工作,但使用的是 RGB 的 buffer (你只需要写一个简单的转换函数,就可以从 CMYK结构 中创建 rgba8 的对象)。但这种转换只是一个近似,可能会因此出现颜色上的失真,因为有些 CMYK 中的颜色无法用 RGB 来表示,反之亦然。如果想完全使用某种颜色空间的表现能力,你可能要写一个为这种颜色空间写一个可以避免中间转换的 pixel format renderer 。
重要!
像素格式相关的类并不进行任何的裁剪操作,也就是说直接使用这些类进行工作一般来说不太安全。裁剪是上层类的功能。采用这样设计的理由很简单:要让用户设计自定义的像素格式类越简单越好。像素格式可能会五花八门,但裁剪操作的代码一般都没有什么区别。
pixel_formats_rgb24(rendering_buffer& rb);
像素格式渲染器(pixecl format renderers)的构造函数需要一已经创建并良好初始化的rendering_buffer对象的引用。这个构建工作的开销很小,基本上只是初始化一个指针。
像素格式渲染器(pixecl format renderers)必须要实现以下这些接口:
unsigned width() const { return m_rbuf->width(); } unsigned height() const { return m_rbuf->height(); }
返回内存区的宽和高(以像素数来衡量)
color_type pixel(int x, int y);
返回(x,y)坐标处的像素的颜色
void copy_pixel(int x, int y, const color_type& c);
将带颜色的像素拷入缓存区中。如果是本身 RGB 像素格式,那么它就不考虑 rgba8 拷贝源中的存在的 alpha 通道。如果本身是 RGBA,那么它就简单地把所有值都拷贝过来,包括 R、G、B,以及 alpha 通道值。
void blend_pixel(int x, int y, const color_type& c, int8u cover);
这个函数将带颜色信息的像素 c 与缓存区(x,y)处的像素进行混合(blending)。现在我们来解释一下“混合”的概念。混合(blending)是抗锯齿(anti-aliasing)的关键特性。在 RGBA 的颜色空间中,我们使用 rgba8 结构体来代表颜色。这个结构体有一个数据成员 int8u a ,它就是 alpha 通道。不过,在这个函数里,我们还看到一个参数 cover ,表示像素的覆盖值大小,比如,这个像素被多边形所“覆盖”的部分的大小(译注:这涉及到亚像素精度,因为一个像素可以分为 256*256 份,所以这个像素并不一定全部被“覆盖”,详细可参考 AGG 对于亚像素的说明)。其实你可以把它看成是另一个 alpha(或者应该叫它Beta?:))。这么做有两个原因,首先,颜色类型(color type)不一定非要包含 alpha 值)。就算颜色类型带有 alpha 值,它的类型也不一定非要与抗锯齿算法中使用的颜色类型一致。假设你现在使用的是 "Hi-End" RGBA 颜色空间,这种颜色空间使用4个取值范围是[0,1]浮点型来表示,alpha 通道值也使用浮点数————对于这种情况来说,混合时使用一个byte实在太少了,但在去锯齿时却非常够用。所以,cover 值就是为去锯齿而使用的一个统一的备用 alpha 值。在大部分情况来说,用 cover_type 来定义它,但在光栅化处理器(rasterizers)中是直接显示地使用 int8u 类型的。这是故意这么定义的,因为如果需要加强 cover_type 的能力时,会使得所有已经存在的像素格式光栅化处理器(pixel format rasterizres)变得与 cover_type 不兼容。它们确实是不兼容的,在进行颜色混合时,如果中间值使用 32-bit 值来暂存的话,那么最大只能使用 8-bit 的覆盖值(coverage value)和 8-bit 的 alpha 值(alpha) 。如果使用 16-bit 的值的话,就要用 64-bit 的中间值暂存,这对于 32-bit 的平台来说会有非常昂贵的开销。
void copy_hline(int x, int y, unsigned len, const color_type& c); void copy_vline(int x, int y, unsigned len, const color_type& c);
使用某种颜色描画一条水平或是垂直的线。
void blend_hline(int x, int y, unsigned len, const color_type& c, int8u cover); void blend_vline(int x, int y, unsigned len, const color_type& c, int8u cover);
采用混合颜色的模式描画一带某种颜色的水平(或垂直线)线。之所以要分开 copy 和 blend 两个版本,是因为考虑到效率问题。虽然可以使用一个 if/else (其实在 blend 版的描画函数中就有)来区分,但对于某些场合,比如要描画很多小型标识(markers)时,这会很影响效率,这种场景在不同的散点图描画程序(scatter plot applicatioin)中常常遇到。
void blend_solid_hspan(int x, int y, unsigned len, const color_type& c, const int8u* covers); void blend_solid_vspan(int x, int y, unsigned len, const color_type& c, const int8u* covers);
混合描画一条水平或是垂直的 solid-color 的 span, Span与 hline 和 vline 几乎是一样的,但它拥有一个存有 coverage value 的数组。这两个函数在渲染实心的去锯齿多边形时会用到。
void blend_color_hspan(int x, int y, unsigned len, const color_type* colors, const int8u* covers); void blend_color_vspan(int x, int y, unsigned len, const color_type* colors, const int8u* covers);
混合描画水平或是垂直的颜色 span ,这两个函数用于不同的 span 产生器中,比如说 gradient,image,patterns,Gouraud interpolation 等等。函数接受一个颜色数组参数,这个颜色数组必须与所使用的像素格式兼容。比如说,所有 AGG 中已经有的 RGB 像素格式都与 rgb8 类型是兼容的。 covers 参数是一个 coverage value 的数组,这与 blend_solid_hspan 中的是一样的。这是参数可选,可以设置为 0 。
下面这个例子是描画阳光的光谱。rgba 类包含有 4 个浮点数的颜色部分(包括alpha),这个类有一个静态函数 from_wavelength ,以及相应的构造函数。rgba8 可以用 rgba 来构造(在 AGG 中这是一个常见的策略,也就是任何的颜色类型都可以用 rgba 来构造)。
#include <stdio.h> #include <string.h> #include "agg_pixfmt_rgb24.h" enum { frame_width = 320, frame_height = 200 }; // [...write_ppm is skipped...] int main() { //-------------------- // Allocate the buffer. // Clear the buffer, for now "manually" // Create the rendering buffer object // Create the Pixel Format renderer // Create one line (span) of type rgba8. // Fill the buffer using blend_color_span // Write the buffer to agg_test.ppm // Free memory unsigned char* buffer = new unsigned char[frame_width * frame_height * 3]; memset(buffer, 255, frame_width * frame_height * 3); agg::rendering_buffer rbuf(buffer, frame_width, frame_height, frame_width * 3); agg::pixfmt_rgb24 pixf(rbuf); agg::rgba8 span[frame_width]; unsigned i; for(i = 0; i < frame_width; ++i) { agg::rgba c(380.0 + 400.0 * i / frame_width, 0.8); span[i] = agg::rgba8(c); } for(i = 0; i < frame_height; ++i) { pixf.blend_color_hspan(0, i, frame_width, span, 0); } write_ppm(buffer, frame_width, frame_height, "agg_test.ppm"); delete [] buffer; return 0; }
运行结果如下: