原理
背景
將三維空間中一個旋轉的三角形,顯示在屏幕上。
假設三角形初始點位為 v0(2.0, 0.0, ?2.0), v1(0.0, 2.0, ?2.0), v2(?2.0, 0.0, ?2.0), 你需要將這三個點的坐標變換為屏幕坐標并在屏幕上繪制出對應的線框三角形。
mvp
AI回答:
在計算機圖形領域,MVP指的是模型-視圖-投影矩陣(Model-View-Projection matrix)。它是將三維模型空間的點變換到二維屏幕上的關鍵變換矩陣鏈,包括以下幾個步驟:
- 模型變換(Model Transformation):將物體從其本地模型空間變換到世界空間,比如平移、旋轉、縮放。
- 視圖變換(View Transformation):將世界空間變換到相機(觀察者)空間,模擬相機位置和方向。
- 投影變換(Projection Transformation):將相機空間變換到裁剪空間,實現透視投影或正交投影。
總結:
MVP矩陣 = 投影矩陣 × 視圖矩陣 × 模型矩陣
如果一個運動的物體要實現“動起來”,通常需要對物體應用變換(Transformation),即:
- 平移(Translation):使物體移動到新位置
- 旋轉(Rotation):改變物體的方向
- 縮放(Scaling):改變物體大小
- 等
這些變換可以組合在一起,形成一個變換矩陣,然后乘到模型矩陣上,實現物體在空間中的運動。
世界空間(World Space)是三維計算機圖形中的一個坐標系,用來描述場景中的所有物體相對于一個統一的坐標系統。在這個空間里,場景中的每個物體都以其在整個場景中的位置、方向和大小進行定義。
簡單來說:
- 模型空間(Model Space):每個物體本地的坐標系統,描述物體的相對結構。
- 世界空間(World Space):所有物體的模型空間經過模型變換(如平移、旋轉、縮放)后,放在一個共同的坐標系中。
- 相機空間(View Space):從相機的視角來看場景。
- 裁剪空間/投影空間(Clip/Projection Space):經過投影變換后,用于最終渲染到屏幕。
一個運動的物體要“動起來”,通常需要修改模型矩陣,是因為模型矩陣控制著物體在模型空間到世界空間的變換。
具體來說:
- 模型矩陣包含平移、旋轉和縮放等變換,定義了物體在場景中的位置、方向和大小。
- 改變模型矩陣就相當于“移動”物體,或者調整它的朝向和比例,從而實現物體在空間中的運動。
通過不斷更新模型矩陣,物體就能在場景中“動起來”。
代碼架構
類
rasterizer
用于實現基本的線框和三角形光柵化(繪制)功能,主要用于軟件渲染管線中。核心內容包括加載頂點和索引數據、設置變換矩陣、繪制線條和三角形、以及管理幀緩沖和深度緩沖。
主要結構和功能簡介
1. 枚舉和類型定義
- Buffers:定義了緩沖區類型(顏色、深度),支持按位操作組合。
- Primitive:定義了圖元類型(線、三角形)。
- pos_buf_id、ind_buf_id:封裝了頂點位置緩沖和索引緩沖的ID,確保類型安全。
2. 類:rasterizer
-
成員變量:
- 變換矩陣(model、view、projection)
- 緩沖區(頂點位置、索引、幀緩沖、深度緩沖)
- 窗口寬度和高度
- 一個自動遞增的ID生成器
-
核心方法:
load_positions()
和load_indices()
:加載頂點和索引數據,返回對應的ID。set_model()
,set_view()
,set_projection()
:設置變換矩陣。clear()
:清空顏色緩沖和深度緩沖。draw()
:繪制三角形(支持線框輪廓作業通過rasterize_wireframe()
實現)。set_pixel()
:設置像素顏色到幀緩沖。frame_buffer()
:返回當前幀緩沖內容。
-
私有方法:
draw_line()
:實現了 Bresenham 線段繪制算法,用于繪制線段。rasterize_wireframe()
:繪制三角形的邊框線。get_index()
:根據像素點坐標計算緩沖區索引。
深度緩沖區作用
深度緩沖區(Depth Buffer,也叫Z緩沖區)用來管理和實現隱藏面消除,確保在渲染場景時,前面(離觀察者近)的像素遮擋后面(離觀察者遠)的像素。
- 隱藏面處理:在繪制每個像素時,比較當前像素的深度值(距離相機的遠近)與已存儲的深度值。
- 遮擋關系:只保留離觀察者更近的像素,屏蔽被遮擋的遠處的像素,從而實現正確的遮擋關系。
具體流程
- 當繪制一個像素時,計算其深度(z值)。
- 將該深度值與深度緩沖區中對應位置的值進行比較。
- 如果新像素更接近(深度值小),則更新顏色和深度緩沖區中的值。
- 如果遠離(深度值大),則跳過該像素,不更新。
代碼中的表現
在 clear()
函數中,深度緩沖區被初始化為無窮大(std::numeric_limits<float>::infinity()
),表示還沒有進行任何像素繪制,所有像素的深度都比任何實際像素都遠。
if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
{std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
}
處理流程
- 加載頂點和索引后,設置變換矩陣。
- 調用
draw()
方法,進行模型-視圖-投影變換,投影到屏幕空間。 - 裁剪、歸一化后,調用線段繪制或三角形光柵化函數。
- 圖元邊界由
draw_line()
或rasterize_wireframe()
實現。
注釋亮點
- 使用 Eigen 作為線性代數庫。
- 提供類型安全的緩沖區識別方式。
- 支持基礎的線段繪制(Bresenham算法)。
- 支持深度測試,啟用深度緩沖區。
- 簡單、直接、易擴展的設計架構。
基礎版
注:只顯示三角形,不旋轉
\\main.cpp 中只有下面的代碼完成基礎版while (key != 27) {r.clear(rst::Buffers::Color | rst::Buffers::Depth);r.set_model(get_model_matrix(angle));r.set_view(get_view_matrix(eye_pos));r.set_projection(get_projection_matrix(45, 1, 0.1, 50));r.draw(pos_id, ind_id, rst::Primitive::Triangle);cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());image.convertTo(image, CV_8UC3, 1.0f);cv::imshow("image", image);key = cv::waitKey(10);std::cout << "frame count: " << frame_count++ << '\n';if (key == 'a') {angle += 10;}else if (key == 'd') {angle -= 10;}}
我們根據代碼重新理解一下上面的知識。
- 模型變換(Model Transformation):將物體從其本地模型空間變換到世界空間。可以看到
get_model_matrix(angle)
的函數只是構建了一個旋轉矩陣。所以原本的三角形,即std::vector<Eigen::Vector3f> pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};
就是在世界坐標系下面表示的。旋轉三角形,即直接在世界坐標系下面旋轉,就可以做到在屏幕上旋轉 - 視圖變換(View Transformation):將世界空間變換到相機(觀察者)空間,模擬相機位置和方向。代碼在
get_view_matrix(eye_pos)
中,也可以看到相機的位置用世界坐標表示,但是經過視圖變換后坐標系將以相機為原點。 - 投影變換(Projection Transformation):將相機空間變換到裁剪空間,實現透視投影或正交投影。需要實現:
get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
輸入是 eyefov表示視野角度,aspect_ratio表示xy的比例。需要轉換一下才能套公式。
三個矩陣怎么計算的很多博主都寫過,我這里就不寫了
正交基之間的過渡矩陣非常好求解
xyz 對應的正交基是(1,0,0) (0,1,0),(0,0,1) 所以矩陣A是單位矩陣E
$\hat{g} \times \hat{t} , \hat{t}, \hat{g} $是另一組正交基,矩陣 B = ( g ^ × t ^ , t ^ , g ^ ) B=(\hat{g} \times \hat{t} , \hat{t}, \hat{g}) B=(g^?×t^,t^,g^?)
則, A P = B ? P = A ? 1 B = E B = B AP=B \Rightarrow P=A^{-1}B = EB=B AP=B?P=A?1B=EB=B
而B是正交基組成的,它逆矩陣是它的轉置
所以這個要求是給定的 t ^ , g ^ \hat{t}, \hat{g} t^,g^?是單位向量,且正交,才能推出老師給的公式
參考 :https://www.cnblogs.com/fijiisland/p/13993749.html
函數void rst::rasterizer::draw(...)
解析
傳入的參數是之前生成的“頂點坐標緩沖id”以及“頂點索引緩沖id”。
首先三角形有三個頂點
std::vector<Eigen::Vector3f> pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};
auto pos_id = r.load_positions(pos);
把頂點使用map存起來,并返回頂點的id
rst::pos_buf_id rst::rasterizer::load_positions(const std::vector<Eigen::Vector3f> &positions)
{auto id = get_next_id();//std::map<int, std::vector<Eigen::Vector3f>> pos_buf;pos_buf.emplace(id, positions);return {id};
}
然后這三個頂點有三個索引,同樣存在map里面
std::vector<Eigen::Vector3i> ind{{0, 1, 2}};
auto ind_id = r.load_indices(ind);rst::ind_buf_id rst::rasterizer::load_indices(const std::vector<Eigen::Vector3i> &indices)
{auto id = get_next_id();// std::map<int, std::vector<Eigen::Vector3i>> ind_buf;ind_buf.emplace(id, indices);return {id};
}
使用的時候,用id在map里取頂點與索引
// 向draw函數傳入之前生成的“頂點坐標緩沖id”以及“頂點索引緩沖id”
void rst::rasterizer::draw(rst::pos_buf_id pos_buffer, rst::ind_buf_id ind_buffer, rst::Primitive type)
{// 當前作業中,代碼支持的圖元(Primitive)類型僅為rst::Primitive::Triangle,即三角形if (type != rst::Primitive::Triangle){throw std::runtime_error("Drawing primitives other than triangle is not implemented yet!");}// 容易得出,auto推導出的類型為std::vector<Eigen::Vector3f>// buf取得對應的圖元頂點坐標vectorauto& buf = pos_buf[pos_buffer.pos_id];// ind取得對應的圖元頂點索引vectorauto& ind = ind_buf[ind_buffer.ind_id];// 下面會解釋f1、f2的含義float f1 = (100 - 0.1) / 2.0;float f2 = (100 + 0.1) / 2.0;// 最終的變換矩陣為投影、視圖、模型矩陣的點乘Eigen::Matrix4f mvp = projection * view * model;for (auto& i : ind){// 實例化一個TriangleTriangle t;// 構造一個元素為4行向量的數組v,向內插入mvp矩陣對頂點索引對應頂點坐標的變換點// 為了和mvp進行運算,將每個頂點坐標轉為一個Eigen::Vector4f,并規定w值為1Eigen::Vector4f v[] = {mvp * to_vec4(buf[i[0]], 1.0f),mvp * to_vec4(buf[i[1]], 1.0f),mvp * to_vec4(buf[i[2]], 1.0f)};// 透視除法for (auto& vec : v) {vec /= vec.w();}// 視口變換操作for (auto & vert : v){vert.x() = 0.5*width*(vert.x()+1.0);vert.y() = 0.5*height*(vert.y()+1.0);vert.z() = vert.z() * f1 + f2;}// 將變換好的頂點坐標傳入三角形實例tfor (int i = 0; i < 3; ++i){t.setVertex(i, v[i].head<3>());t.setVertex(i, v[i].head<3>());t.setVertex(i, v[i].head<3>());}// 根據頂點索引設置每個頂點的顏色t.setColor(0, 255.0, 0.0, 0.0);t.setColor(1, 0.0 ,255.0, 0.0);t.setColor(2, 0.0 , 0.0,255.0);rasterize_wireframe(t);}
}
三個頂點的位置都計算完畢,現在開始畫三角形:
rasterize_wireframe(t);
void rst::rasterizer::rasterize_wireframe(const Triangle& t)
{draw_line(t.c(), t.a());draw_line(t.c(), t.b());draw_line(t.b(), t.a());
}
這里使用的是Bresenham’s line drawing algorithm。就不多寫了。
計算出畫在屏幕上的點的位置之后使用函數:
void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)
{//old index: auto ind = point.y() + point.x() * width;if (point.x() < 0 || point.x() >= width ||point.y() < 0 || point.y() >= height) return;auto ind = (height-point.y())*width + point.x();frame_buf[ind] = color;
}
這里在圖像上對應的位置填上顏色。