1 开始:绘制三角形
1 从示例框架开始编写程序
您可以下载 SJR 0.0.1.a 的 “示例框架” 。其中有 11 个文件:
stb_image.h
sjDefine.h
sjCanvas.h
sjCanvas.cpp
sjMath.h
sjMath.cpp
以上六个文件是 SJR 0.0.1.a 本体的源码。
main.h
main.cpp
frameRate.h
frameRate.cpp
这四个文件是 Windows 窗体程序的代码。作者使用的编译环境是 Visual Studio 2019 默认的预编译和字符集选项,所以您可能需要修改示例框架的一些代码。
入口函数是 int main(),而不是 Windows 图形界面常用的 WinMain 入口函数。
图形代码.cpp
本教程将在这个文件书写绘制图形的代码。
您可以下载 “示例程序”,其中有本教程使用的图片资源。还有三个 cpp 文件,它们对应三篇教程,每个都可以替换前面示例框架中的 “图形代码.cpp” 然后进行编译(还需要对应的图片资源,也在 “示例程序” 中)。
2 创建 Canvas 对象
此后若没有特别说明,所有代码都在“图形代码.cpp”文件中书写。
int wWidth = 900;
int wHeight = 600;
int pxSize = 2;
int canvasWidth = wWidth / pxSize;
int canvasHeight = wHeight / pxSize;
这段代码决定了窗口的大小和绘图区域的像素大小。main.cpp 中创建窗口和位图的指令将访问这些整数,并创建相应的窗口程序和位图。一个名叫 buffer 的空类型指针将指向可直接读写的位图数据缓冲区。
本教程将绘制 3 个并排的三角形,所以请将 wWidth 改为 1200,将 wHeight 改为 400。编译运行,你将看到一个细长的窗口。
现在创建
sjz::Canvas 对象。它是绘图区域的抽象,SJR 0.0.1.a 的绝大部分具有绘图功能的对象的创建都需要 Canvas 对象。当那些对象创建后,绘图时不再依赖 Canvas 对象,但也无法自动修改自己保存的绘图区域信息。
现在定义一个全局变量:
Canvas* canvas;
然后在 start 函数中创建画布:
void start() {
canvas = new Canvas(canvasWidth, canvasHeight, buffer);
}
不能直接构造全局的 Canvas 对象,因为在 start 函数执行前,绘制用的位图 buffer 才准备好。
然后在 quit 函数中释放空间:
void quit() {
delete canvas;
}
quit 函数在绘图窗口被关闭后运行,所以关闭程序时最好关闭绘图窗口而不是控制台窗口,以避免内存泄漏。
3 使用固定管线绘制三角形
我们将绘制三个三角形,前两个分别用两种固定管线,第三个用可编程管线。
现在创建
sjz::TrianglePainter_P2FC3F 对象。
TrianglePainter_P2FC3F* PAI_1; // 全局区
...
// 在 start 函数中:
PAI_1 = new TrianglePainter_P2FC3F(canvas);
// 然后在 quit 函数中释放空间
准备好数据,然后绘图:
// 在 start 函数中:
float p1_x = 100, p1_y = 150;
float p2_x = 45, p2_y = 50;
float p3_x = 155, p3_y = 50; // 这些数据后面还会用到
PAI_1->paint(P2FC3F(p1_x, p1_y, 1, 0, 0),
P2FC3F(p2_x, p2_y, 0, 1, 0),
P2FC3F(p3_x, p3_y, 0, 0, 1));
P2FC3F 是 TrianglePainter_P2FC3F 所用的顶点数据结构体,它包含两个表示位置的浮点数和三个表示颜色的浮点数。
SJR 0.0.1.a 的各种 TrianglePainter 所用的顶点位置的 x 与 y 指的是在绘图区域的像素位置。
编译运行,你将看到三角形。
接下来绘制贴有纹理的三角形。首先准备纹理:
Image* texture; // 全局区
...
// 在 start 函数中:
texture = Image::readFromFile("img03.jpg"); // 相对路径
// 然后在 quit 函数中释放空间
准备
TrianglePainter_P4FT2F 对象:
TrianglePainter_P4FT2F* PAI_2; // 全局区
...
// 在 start 函数中:
PAI_2 = new TrianglePainter_P4FT2F(canvas);
PAI_2->setTexture(texture); // 给 painter 对象设置纹理
// 然后在 quit 函数中释放空间
绘图:
// 在 start 函数中:
PAI_2->paint(
P4FT2F(p1_x + 200, p1_y, 1, 1, 0.5, 1),
P4FT2F(p2_x + 200, p2_y, 1, 1, 0, 0),
P4FT2F(p3_x + 200, p3_y, 1, 1, 1, 0)
);
P4FT2F 是 TrianglePainter_P4FT2F 所用的顶点数据结构体,它包含四个表示位置的浮点数和三个表示颜色的浮点数。位置的 z 和 w 是绘制 3D 图形,或有遮挡关系的图形需要的。z 需要大于
MIN_DEPTH 的值,您可以修改这个宏。w 影响插值结果,线性插值要把三个点的 w 设为相同的非零数。
编译运行,你将看到三角形。
4 使用可编程管线绘制三角形
接下来我们要绘制第三个三角形,通过着色器将顶点颜色和贴图相乘:
首先定义顶点数据结构体,它必须只含有若干个连续排列的四字节浮点数,浮点数的个数不能小于 4,不能大于
SJ_SHADER_FBUF_SIZE(您可以修改这个宏,它在 sjDefine.h)。前四个浮点数必须是顶点位置的 x、y、z、w。后面的数的含义由我们来决定。这里让两个浮点数表示 uv 坐标,三个浮点数表示顶点颜色。
struct MyPoint {
float x, y, z, w, u, v, r, g, b;
};
定义一个的着色器,通过继承
sjz::IShader 类:
class MyShader :public IShader {
SJ_SHADER_CONSTRUCT(MyShader)
virtual vec4 main() {
return vec4(1, 1, 1, 1);
}
};
SJ_SHADER_CONSTRUCT(MyShader) 实现了 MyShader 类的构造器,构造器的参数与 IShader 构造器的相同。MyShader 要重写 IShader 类的 main 函数,它返回的颜色将被绘制。这里先直接返回白色。我们先组装程序的其它部分。
创建 MyShader 对象:
MyShader* shader; // 全局区
...
// start 函数中:
shader = new MyShader(9);
// 然后在 quit 函数中释放空间
MyShader 构造时传入我们定义的顶点数据结构体 MyPoint 含有浮点数的数量。
创建
sjz::TrianglePainter 对象,并将 MyShader 对象设置为它的着色器:
TrianglePainter* PAI_3; // 全局区
...
// start 函数中:
PAI_3 = new TrianglePainter(canvas);
PAI_3->setShader(shader);
// 然后在 quit 函数中释放空间
绘制三角形:
// start 函数中:
MyPoint ARR_pt[] = {
{p1_x+400,p1_y,1,1, 0.5,1, 1,0.5,0.5},
{p2_x+400,p2_y,1,1, 0,0, 0.5,1,0.5},
{p3_x+400,p3_y,1,1, 1,0, 0.5,0.5,1},
};
PAI_3->paint((float*)&ARR_pt[0], (float*)&ARR_pt[1], (float*)&ARR_pt[2]);
TrianglePainter 的 paint 方法的参数是三个 float 指针。
编译运行:
回到 MyShader。继续使用第二个三角形所用的纹理。修改创建 texture 时使用的函数:
// 在 start 函数中:
// 将 texture = Image::readFromFile("img03.jpg");
// 改为:
texture = Image::readFromFile("img03.jpg",
SJ_IMAGE_4B_BIT | SJ_IMAGE_4F_BIT);
点击这里进入 sjz::Image 文档。
着色器的 textureArr 成员是个 Image 指针数组,您可以随意访问,因此可以借助它来使着色器的 main 函数中能访某个 Image 对象。
// start 函数中,着色器对象创建之后:
shader->textureArr[0] = texture;
现在完成 MyShader:
class MyShader :public IShader {
SJ_SHADER_CONSTRUCT(MyShader)
vec2& _uv = *(vec2*)&fBuf[4];
vec3& _color = *(vec3*)&fBuf[6];
pImage& _tex = textureArr[0];
vec4 main() {
vec4 texColor = _tex->sample(_uv);
return vec4(_color) * texColor;
}
};
TrianglePainter 的 paint 方法执行中,插值的结果将被写入着色对象中的 float 数组 fBuf。数据在 fBuf 的排列与我们之前定义的 MyPoint 中的相同。但默认情况下,不将顶点位置的插值结果(前四个浮点数)写入 fBuf。
这里为了方便书写,定义了三个引用。
sjm::vec2、
sjm::vec3、
sjm::vec4 的成员分别是两个、三个、四个 float,可以嵌合 fBuf。
两个 vec4 用 “ * ” 相乘是对应分量相乘。
编译运行。