天天看點

光栅渲染器基礎知識

從體繪制繞路過來的,三維重建,網格處理,滑鼠互動,開始找不到路,邊走邊問,最後到了光栅渲染器,應該是這裡吧?這個坑挖好久了,試試能填多少。這裡沒有公式和算法,網上和參考文獻中都有詳細的解釋,不再重複内容。 --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
xaxis.y yaxis.y zaxis.y
xaxis.z yaxis.z zaxis.z
-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. 《計算機圖形學與幾何造型導論》

繼續閱讀