从 “示例框架” 的 “图形代码.cpp” 开始编写程序。
SJR 0.0.1.a 只有 “片元着色器” 而没有 “顶点着色器”,可以借助 sjMath.h 实现顶点着色器的功能。
本节我们将绘制一个四棱锥,在它的表面贴上图片,然后接受简单的光照。因此
着色器对象需要的顶点数据包括:位置(四个浮点数)、法线(三个浮点数)、uv 坐标(两个浮点数)。现在定义顶点:
struct MyPoint {
float x, y, z, w, nx, ny, nz, u, v;
};
模型数据:
// 全局区:
const float n_h = 2. / sqrt(5.);
const float n_v = 1. / sqrt(5.);
const float h = 2;
const int vtxCount = 12;
MyPoint _data[] = {
{1,0,1, 1, n_h, n_v, 0, 1,0},
{1,0,-1, 1, n_h, n_v, 0, 0,0},
{0,h,0, 1, n_h, n_v, 0, 0.5,1},
{-1,0,1, 1, 0, n_v, n_h, 1,0},
{1,0,1, 1, 0, n_v, n_h, 0,0},
{0,h,0, 1, 0, n_v, n_h, 0.5,1},
{-1,0,-1, 1, -n_h,n_v, 0, 1,0},
{-1,0,1, 1, -n_h,n_v, 0, 0,0},
{0,h,0, 1, -n_h,n_v, 0, 0.5,1},
{1,0,-1, 1, 0, n_v,-n_h, 1,0},
{-1,0,-1, 1, 0, n_v,-n_h, 0,0},
{0,h,0, 1, 0, n_v,-n_h, 0.5,1},
};
MyPoint vertex[vtxCount];
这个四棱锥没有底面。
所有顶点位置的 w 分量全为 1,这是齐次坐标。最后的数组 vertex 用来存储经过变换后的顶点数据。
sjz::DepthBuffer 对象用来实现深度检测,它的创建需要 sjz::Canvas 对象。我们先组装其它部分。
// 全局区
Image* texture;
Canvas* canvas;
Background* bgr;
MyShader* shader;
TrianglePainter* painter;
// start 函数中:
texture = Image::readFromFile("img03.jpg", SJ_IMAGE_4F_BIT);
canvas = new Canvas(canvasWidth, canvasHeight, buffer);
bgr = new Background(canvas);
bgr->set(RGBA(0.2 * 255, 0.25 * 255, 0.3 * 255));
shader = new MyShader(sizeof(MyPoint) / sizeof(float));
shader->textureArr[0] = texture;
shader->u_lightDir = vec3(0, 1, 1); // 设置光源方向
shader->u_lightDir.normalize(); // 将三维向量单位化
painter = new TrianglePainter(canvas);
painter->setShader(shader);
painter->setFunc(SJ_CULL_FRONT_BIT);
// draw 函数中:
bgr->paint();
for (int i = 0; i < vtxCount; i += 3) {
painter->paint((float*)&vertex[i],
(float*)&vertex[i + 1], (float*)&vertex[i + 2]);
}
// 在 quit 函数释放空间
其中 painter->setFunc(SJ_CULL_FRONT_BIT); 的
SJ_CULL_FRONT_BIT 字面意思是剔除正面,但在本程序中剔除的是背面(剔除逆时针的)。不过 SJR 0.0.1.a 未使用标准设备坐标系,所以无法在 API 的层面规定怎样是顺/逆时针。
使用 DepthBuffer 对象:
DepthBuffer* depthBuf; // 全局区
...
// 在 start 函数中:
depthBuf = new DepthBuffer(canvas, 100);
// 将 painter->setFunc(SJ_CULL_FRONT_BIT); 改为:
painter->setFunc(SJ_CULL_FRONT_BIT | SJ_DEPTHTEST_ON_BIT);
painter->setDepthBuffer(depthBuf);
...
// 在 draw 函数的开始处:
depthBuf->clear();
...
// 在 quit 函数中释放空间
接下来要把四棱锥绘制在窗口中,并让它不断旋转。需要在每帧把顶点从 _data 数组变换并转移到 vertex 数组。在此不介绍数学原理。
我们的顶点数据包含位置、法线、UV 坐标。其中 UV 坐标不需要进行变换,所以可以只在程序开始时传递一次。
// 在 start 函数中:
for (int i = 0; i < vtxCount; ++i) {
vertex[i].u = _data[i].u; vertex[i].v = _data[i].v;
}
生成模型矩阵、观察矩阵,然后把它们相乘。下面的 “ M ” 是示例工程中的全局
sjm::Math4_r 对象,计算时使用行向量。
unsigned t = 0; // 全局区
// 在 draw 函数中:
t += sju_time::DELTA;
...
mat4 mat_model; M.rotateY(t * 0.05, mat_model);
vec3 eyePos(4, 4, 4), targ(0, 1, 0), up(0, 1, 0);
mat4 mat_view; M.view(eyePos, targ, up, mat_view);
mat4 mat_MV; M.dot(mat_model, mat_view, mat_MV);
进行图形变换:
// 全局区:
Project_Y projector(45, canvasHeight);
const float halfWidth = canvasWidth * .5;
const float halfHeight = canvasHeight * .5;
...
// draw 函数中,生成矩阵之后:
for (int i = 0; i < vtxCount; ++i) {
MyPoint& dv = _data[i];
vec4& posTarg = *(vec4*)&vertex[i].x;
M.dot(*(vec4*)&dv.x, mat_MV, posTarg);
projector.project(posTarg);
posTarg.x += halfWidth;
posTarg.y += halfHeight;
vec3& normTarg = *(vec3*)&vertex[i].nx;
vec4 normal(*(vec3*)&dv.nx, 0);
vec4 tvec; M.dot(normal, mat_model, tvec);
normTarg = tvec; // tvec 的第四个分量被舍弃
}
sjm::Project_Y 对象根据构造时传入的参数,将
sjm::vec4 进行投影变换(结果不是 NDC 坐标)。结果的 x、y 适合绘图区域的像素坐标,z 等于顶点在观察坐标系 z 值的相反数;w 与 z 相等,适合 TrianglePainter 的插值算法。
编译运行,得到一个旋转的四棱锥。