光栅渲染器基础知识

2021年9月26日 1点热度 0条评论 来源: winston600

从体绘制绕路过来的,三维重建,网格处理,鼠标交互,开始找不到路,边走边问,最后到了光栅渲染器,应该是这里吧?这个坑挖好久了,试试能填多少。这里没有公式和算法,网上和参考文献中都有详细的解释,不再重复内容。 --2020年4月9日
其实有点累了,有点想放弃,再坚持一下。 --2020年4月23日
这坑有点大,适当收敛一下。 --2020年4月24日
基本有个交代了,暂时就到这里。–2020年4月28日

第一步 将韦大的代码移植到gcc下,IDE用QtCreater。–2020年4月8日
#ifdef _MSC_VER
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "user32.lib")
#endif
//改成了
LIBS += libgdi32 \
        libuser32

既然windows下的Qt也是win32扩展,那么win32那一套在Qt下也是可用的,so,win32 api 和 Qt api 混用也可以。目前将msc平台下的代码移植到gcc下运行没问题,可移植的最大原因是代码本来就跨平台。

第二步 代码抽离封装,并增加一些功能。–2020年4月9日

  • 添加了圆柱
  • 添加了读取点集后线
  • 添加了读取中心线后管状物
第三步 代码封装管线机制,并添加鼠标交互 --2020年4月11日

封装完成后扩展性和易读性好了很多。
只是将原来的键盘交互改成了鼠标响应,并没有实质性的变动,离真正的轨迹球模式还有一段路要走。
鼠标响应借用了Qt的mouse event。

第四步 添加了轨迹球,光照,相机 --2020年4月21日

重写了相机。
将原来的2个方向扩展N个方向的轨迹球。
加入了一个实现不太好的光照。

第五步 3D线框渲染流水结构梳理

回到最初的代码,去掉封装,看3D线框变化原理

1、摄像机是一个4X4的矩阵

// 设置摄像机:创建左手坐标系的观察矩阵
void struct_demo::matrix_set_lookat(matrix_t *m, //相机坐标
									const vector_t *eye, //eye:相机所在的位置,
									const vector_t *at, //at:相机到目标的向量,默认{ 0, 0, 0, 1 }
									const vector_t *up) //up:向上的方向向量,这里用 { 0, 0, 1, 1 }
{ 
    vector_t xaxis, yaxis, zaxis;

    vector_sub(&zaxis, at, eye);//zaxis = at - eye
    vector_normalize(&xaxis);//zaxis归一化
    vector_crossproduct(&xaxis, up, &zaxis);// xaxis = up x zaxis
    vector_normalize(&xaxis);//xaxis归一化
    vector_crossproduct(&yaxis, &zaxis, &xaxis);//yaxis = zaxis x xaxis

    m->m[0][0] = xaxis.x;
    m->m[1][0] = xaxis.y;
    m->m[2][0] = xaxis.z;
    m->m[3][0] = -vector_dotproduct(&xaxis, eye);//xaxis * eye

    m->m[0][1] = yaxis.x;
    m->m[1][1] = yaxis.y;
    m->m[2][1] = yaxis.z;
    m->m[3][1] = -vector_dotproduct(&yaxis, eye);//yaxis * eye

    m->m[0][2] = zaxis.x;
    m->m[1][2] = zaxis.y;
    m->m[2][2] = zaxis.z;
    m->m[3][2] = -vector_dotproduct(&zaxis, eye);//zaxis * eye

    m->m[0][3] = 0.0f;
    m->m[1][3] = 0.0f; 
    m->m[2][3] = 0.0f; 
    m->m[3][3] = 1.0f;
}

左手坐标系的观察矩阵:
eye:相机所在的位置
at:相机到目标的向量
up:向上的方向向量,书中介绍时为[0,1,0],而很多地方用[0,-1,0]
观察坐标系的z轴为:zaxis = normal(at - eye)
观察坐标系的x轴为:xaxis = normal(cross(up,zaxis))
观察坐标系的z轴为:yaxis = cross(zaxis,xaxis)
其中:normal为使单位向量化,cross为求两向量的法向量(单位向量)
dot为:轴 * eye.x + 轴 * eye.y + 轴 * eye.z
创建的矩阵为:

观察矩阵
xaxis.x yaxis.x zaxis.x 0
xaxis.y yaxis.y zaxis.y 0
xaxis.z yaxis.z zaxis.z 0
-dot(xaxis,eye) -dot(yaxis,eye) -dot(zaxis,eye) 1

在物体视角变换的过程中,全程只用了一个camera_at_zero函数,而这个函数也只调整了一个参数eye,即相机所在的位置:

void struct_demo::camera_at_zero(device_t *device, float x, float y, float z) 
{ 
    point_t eye = {  x, y, z, 1 }, at = {  0, 0, 0, 1 }, up = {  0, 0, 1, 1 };
    matrix_set_lookat(&device->transform.view, &eye, &at, &up);
    //一旦调整相机位置,马上刷新观察矩阵。
    transform_update(&device->transform);
}

这里定义了坐标变换,最重要的是 transform = world * view * projection:

typedef struct { 
    matrix_t world;         // 世界坐标变换
    matrix_t view;          // 摄影机坐标变换
    matrix_t projection;    // 投影变换
    matrix_t transform;     // transform = world * view * projection
    float w, h;             // 屏幕大小
}	transform_t;

旋转是物体自传(如果是相机转呢?物体静止不动,细细想来,真是情况中两种情况都是存在的,应用场景不同),缩放是相机近大远小:

int main(int argc, char *argv[])
{ 
……

    device_t device;

    int indicator = 0;
    int kbhit = 0;
    float alpha = 1;
    float pos = 3.5;

……

    if (screen_init(800, 600, title))
        return -1;

    struct_demo *demo = new struct_demo;
    demo->device_init(&device, 800, 600, screen_fb);//给出一个初始化800*600大小的画布
    demo->camera_at_zero(&device, 3, 0, 0);//给出一个初始化的相机,不然看不到。其实就是虚拟与真实世界的抽象和封装。
    //demo->init_texture(&device);//给出一个初始化的纹理,如果不用纹理,可以注释
    device.render_state = RENDER_STATE_WIREFRAME;

    while (screen_exit == 0 && screen_keys[VK_ESCAPE] == 0) { 
        screen_dispatch();
        demo->device_clear(&device, 1);//清空整个窗口画布
        demo->camera_at_zero(&device, pos, 0, 0);//调整相机位置

        if (screen_keys[VK_UP]) pos -= 0.01f;//这部分全是键盘输入参数
        if (screen_keys[VK_DOWN]) pos += 0.01f;
        if (screen_keys[VK_LEFT]) alpha += 0.01f;
        if (screen_keys[VK_RIGHT]) alpha -= 0.01f;

        if (screen_keys[VK_SPACE]) { 
            if (kbhit == 0) { 
                kbhit = 1;
                if (++indicator >= 3) indicator = 0;
                device.render_state = RENDER_STATE_WIREFRAME;
            }
        }	else { 
            kbhit = 0;
        }

        demo->draw_box(&device, alpha);//刷新并绘制立方体
        screen_update();
        Sleep(1);
    }

    delete demo;
    return a.exec();
}

光栅渲染器关键词:

  1. 读取解析数据;
  2. 多边形网格:点->线->三角形->面。两点构成一线,三条线构成一个三角形,若干个三角形连接成三角带(面)。
  3. 纹理坐标,颜色
  4. Camera :平移,旋转,缩放:平移和自转是网格的矩阵变换,缩放是摄像机的矩阵变换
  5. KeyEvent,MouseEvent
  6. World Transform,View Transform ,Projection Transform
  7. 矢量运算,矩阵变换,顶点运算
  8. 齐次坐标,cvv,归一化,初始化
  9. 光栅化:光栅化是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上的过程。光栅化的本质是坐标变换、几何离散化。

  1. 剪裁,透视除法,背面剔除,视口转换,扫描转换
  2. 光照,阴影
  3. 左手坐标系:Z轴指向屏幕里

第六步 理论问题若干

基本结构

  1. 先定义一个4属性的向量vector,并组织向量的数学运算:叉乘,点乘,减法,归一化。
  2. 由向量派生空间点point。
  3. 由空间点构建顶点vertex。
  4. 将数据点集按照vertex的格式组织起来,构成点,线,三角形,面,体。
  5. 再定义一个4X4的矩阵matrix,组织该矩阵的数学运算:
  6. 由该矩阵产生3个对象:世界坐标变换world,摄影机坐标变换view,投影变换projection
  7. 由这3个对象产生一个变换矩阵:transform = world * view * projection
  8. 构建场景renderer,接管了绘制画布的内存,事实上所有的绘制都在这部分,其他的都是抽象和封装。
  9. 构建相机camera,相机的变化就是修改摄影机坐标变换view,然后刷新变换矩阵transform = world * view * projection
  10. 物体的旋转变化就是修改世界坐标变换world,

几个问题

  1. 如何把物体放置在视图的正中央
  2. 在视野中,哪些点可见,哪些点不可见
// 检查齐次坐标同 cvv 的边界用于视锥裁剪
int fishTransform::transform_check_cvv(const fishVector *v)
{ 
    float w = v->w;
    int check = 0;
    if (v->z < 0.0f) check |= 1;
    if (v->z >  w) check |= 2;
    if (v->x < -w) check |= 4;
    if (v->x >  w) check |= 8;
    if (v->y < -w) check |= 16;
    if (v->y >  w) check |= 32;
    return check;
}
  1. 物体旋转矩阵
// 旋转矩阵
void fishMatrix::matrixSetRotate(matrix_t *m, float x, float y, float z, float theta)
{ 
    float qsin = (float)sin(theta * 0.5f);
    float qcos = (float)cos(theta * 0.5f);

    fishVector vec;
    vec.x = x;
    vec.y = y;
    vec.z = z;
    vec.w = 1.0f;

    float w = qcos;
   // m_vector->vectorNormalize(&vec);
    vec.vectorNormalize();
    x = vec.x * qsin;
    y = vec.y * qsin;
    z = vec.z * qsin;

    m->m[0][0] = 1 - 2 * y * y - 2 * z * z;
    m->m[1][0] = 2 * x * y - 2 * w * z;
    m->m[2][0] = 2 * x * z + 2 * w * y;
    m->m[0][1] = 2 * x * y + 2 * w * z;
    m->m[1][1] = 1 - 2 * x * x - 2 * z * z;
    m->m[2][1] = 2 * y * z - 2 * w * x;
    m->m[0][2] = 2 * x * z - 2 * w * y;
    m->m[1][2] = 2 * y * z + 2 * w * x;
    m->m[2][2] = 1 - 2 * x * x - 2 * y * y;
    m->m[0][3] = m->m[1][3] = m->m[2][3] = 0.0f;
    m->m[3][0] = m->m[3][1] = m->m[3][2] = 0.0f;
    m->m[3][3] = 1.0f;
}
//cube
void fishBlocks::drawCube(float theta)
{ 
    matrix_t m;
	//计算旋转矩阵
    m_renderer->GetfishMatrix()->matrixSetRotate(&m, 1, 0, 0, theta);//Z
    //m_renderer->GetfishMatrix()->matrixSetRotate(&m, 0, 1, 0, theta);//X
    //m_renderer->GetfishMatrix()->matrixSetRotate(&m, 0, 0, 1, theta);//Y
    //m_renderer->GetfishMatrix()->matrixSetRotate(&m, -1, -0.5, 1, theta);
 	
 	//世界坐标变换 
    m_renderer->GetfishDevice()->GetFishTransform()->world = m;
    m_renderer->GetfishTransform()->transform_update();
    
    drawPlane(0, 1, 2, 3);
    drawPlane(4, 5, 6, 7);
    drawPlane(0, 4, 5, 1);
    drawPlane(1, 5, 6, 2);
    drawPlane(2, 6, 7, 3);
    drawPlane(3, 7, 4, 0);

    // qDebug()<<"fishRenderer::draw_cube";
    // std::cout<<"fishRenderer::draw_cube done"<<std::endl;
}
  1. 相机旋转矩阵
// 设置摄像机
void fishCamera::matrixSetLookat(matrix_t *m, const fishVector *eye, const fishVector *at, const fishVector *up)
{ 
    fishVector xaxis, yaxis, zaxis;
    zaxis.vectorSub(at,eye);
    zaxis.vectorNormalize();
    xaxis.vectorCrossproduct(up,&zaxis);
    xaxis.vectorNormalize();
    yaxis.vectorCrossproduct(&zaxis,&xaxis);

    m->m[0][0] = xaxis.x;
    m->m[1][0] = xaxis.y;
    m->m[2][0] = xaxis.z;
    m->m[3][0] = -m_renderer->GetfishVector()->vectorDotproduct(&xaxis, eye);

    m->m[0][1] = yaxis.x;
    m->m[1][1] = yaxis.y;
    m->m[2][1] = yaxis.z;
    m->m[3][1] = -m_renderer->GetfishVector()->vectorDotproduct(&yaxis, eye);

    m->m[0][2] = zaxis.x;
    m->m[1][2] = zaxis.y;
    m->m[2][2] = zaxis.z;
    m->m[3][2] = -m_renderer->GetfishVector()->vectorDotproduct(&zaxis, eye);

    m->m[0][3] = m->m[1][3] = m->m[2][3] = 0.0f;
    m->m[3][3] = 1.0f;
}

参考文献:

  1. 想用C++实现一个软件渲染器,类似DX和OpenGL,除了《3D游戏编程大师技巧》,或者什么网站推荐? - 知乎
    https://www.zhihu.com/question/33712299/answer/58495947
  2. 渲染器 1 —— 基本绘图 - 知乎
    https://zhuanlan.zhihu.com/p/20140034
  3. 《3D数学基础:图形与游戏开发》
  4. 《3D游戏编程大师技巧》
  5. 《计算机图形学与几何造型导论》
    原文作者:winston600
    原文地址: https://blog.csdn.net/rabbitbride/article/details/105403202
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。