從體繪制繞路過來的,三維重建,網格處理,滑鼠互動,開始找不到路,邊走邊問,最後到了光栅渲染器,應該是這裡吧?這個坑挖好久了,試試能填多少。這裡沒有公式和算法,網上和參考文獻中都有詳細的解釋,不再重複内容。 --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();
}
光栅渲染器關鍵詞:
- 讀取解析資料;
- 多邊形網格:點->線->三角形->面。兩點構成一線,三條線構成一個三角形,若幹個三角形連接配接成三角帶(面)。
- 紋理坐标,顔色
- Camera :平移,旋轉,縮放:平移和自轉是網格的矩陣變換,縮放是錄影機的矩陣變換
- KeyEvent,MouseEvent
- World Transform,View Transform ,Projection Transform
- 矢量運算,矩陣變換,頂點運算
- 齊次坐标,cvv,歸一化,初始化
- 光栅化:光栅化是将幾何資料經過一系列變換後最終轉換為像素,進而呈現在顯示裝置上的過程。光栅化的本質是坐标變換、幾何離散化。
- 剪裁,透視除法,背面剔除,視口轉換,掃描轉換
- 光照,陰影
- 左手坐标系:Z軸指向螢幕裡
第六步 理論問題若幹
基本結構
- 先定義一個4屬性的向量vector,并組織向量的數學運算:叉乘,點乘,減法,歸一化。
- 由向量派生空間點point。
- 由空間點建構頂點vertex。
- 将資料點集按照vertex的格式組織起來,構成點,線,三角形,面,體。
- 再定義一個4X4的矩陣matrix,組織該矩陣的數學運算:
- 由該矩陣産生3個對象:世界坐标變換world,攝影機坐标變換view,投影變換projection
- 由這3個對象産生一個變換矩陣:transform = world * view * projection
- 建構場景renderer,接管了繪制畫布的記憶體,事實上所有的繪制都在這部分,其他的都是抽象和封裝。
- 建構相機camera,相機的變化就是修改攝影機坐标變換view,然後重新整理變換矩陣transform = world * view * projection
- 物體的旋轉變化就是修改世界坐标變換world,
幾個問題
- 如何把物體放置在視圖的正中央
- 在視野中,哪些點可見,哪些點不可見
// 檢查齊次坐标同 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;
}
- 物體旋轉矩陣
// 旋轉矩陣
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;
}
- 相機旋轉矩陣
// 設定錄影機
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;
}
參考文獻:
-
想用C++實作一個軟體渲染器,類似DX和OpenGL,除了《3D遊戲程式設計大師技巧》,或者什麼網站推薦? - 知乎
https://www.zhihu.com/question/33712299/answer/58495947
-
渲染器 1 —— 基本繪圖 - 知乎
https://zhuanlan.zhihu.com/p/20140034
- 《3D數學基礎:圖形與遊戲開發》
- 《3D遊戲程式設計大師技巧》
- 《計算機圖形學與幾何造型導論》