3 三维世界
1 准备顶点数据
   从 “示例框架” 的 “图形代码.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 用来存储经过变换后的顶点数据。
2 着色器
class MyShader :public IShader {
    SJ_SHADER_CONSTRUCT(MyShader);
    vec3& _normal = *(vec3*)&fBuf[4];
    vec2& _uv = *(vec2*)&fBuf[7];
    pImage& _tex = textureArr[0];
    
    vec3 u_lightDir;
    vec4 main() {
        vec4 texColor = _tex->sample(_uv);
        _normal.normalize();
        float light = dot(u_lightDir, _normal);
        light = (light + 1) * 0.5;
        return light * texColor;
    }
};
   该着色实现了简化的半兰伯特光照。它可以通过公有属性 u_lightDir 来接收平行光的方向。
3 DepthBuffer 对象、组装渲染过程
   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 函数中释放空间
4 三维图形变换
   接下来要把四棱锥绘制在窗口中,并让它不断旋转。需要在每帧把顶点从 _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 的插值算法。
   编译运行,得到一个旋转的四棱锥。
图片加载失败