核心模塊:
rasterizer
:光柵化器,負責三角形遍歷和像素繪制Shader
:包含頂點著色器和多種片元著色器Texture
:紋理處理模塊
頂點著色器的計算量一般遠小于片元著色器。因為組成三角形的頂點相對有限,而片元需要基于頂點組成的三角形進行插值。
比如,一個100 * 100大小的矩形平面,有四個頂點,計算四次,但是片元著色器需要計算10000次。
也比較簡單,模型自帶了法線。注意,法線取值范圍[-1, 1],需要映射到[0,1],再轉換到[0, 255]
網格數據遍歷與三角形構建
- 外層循環
for(auto mesh:Loader.LoadedMeshes)
遍歷模型的所有網格(Mesh),每個網格代表一個獨立幾何體(如物體的一個部件)
內層循環for(int i=0; ... i+=3)
以步長3遍歷頂點,將每三個頂點組成一個三角形面片(每個三角形由3個頂點構成)
該代碼段是3D模型加載的核心邏輯,它將.obj
模型文件中存儲的頂點、法線、紋理坐標等原始數據轉換為渲染管線可處理的三角形對象集合。這些數據為后續的MVP矩陣變換、光照計算(依賴法線)、紋理貼圖(依賴紋理坐標)提供了輸入基礎
**newtri
**
- 這是經過MVP矩陣變換?(Model-View-Projection)和視口變換后的三角形對象,其頂點坐標已處于屏幕空間。
- 包含以下處理后的屬性:
- ?頂點坐標:通過MVP矩陣變換和齊次除法后,再經過視口變換映射到屏幕坐標系
法線向量:通過逆變換矩陣將模型空間法線轉換到視圖空間,用于后續光照計算
*viewspace_pos
**
- 表示三角形頂點在視圖空間(觀察空間)??中的坐標,即經過?
view * model
?變換后的位置。 - 用于光照計算中的視線方向計算(如Phong著色中的鏡面反射分量)
光照模擬機制
通過修改模型表面的法線方向,改變光線反射角度,使平面在渲染時呈現凹凸、劃痕等細節效果。例如:
- 當光線照射到法線貼圖標記的“凹陷”區域時,法線方向改變導致陰影和高光位置變化,從而欺騙人眼感知
雙線性插值作用于片段著色器階段的紋理采樣過程。當紋理坐標(u,v)
為浮點數時(非整數像素中心坐標),需通過插值計算顏色值
光照函數
每個函數的實現步驟大致如下:
- phong_fragment_shader:遍歷光源,計算環境、漫反射、高光,累加結果。
- texture_fragment_shader:采樣紋理顏色作為kd,其余同phong。
- bump_fragment_shader:計算TBN矩陣,擾動法線,更新法線后計算光照。
- displacement_fragment_shader:調整頂點位置,更新法線,再計算光照。
Blinn-Phong模型由環境光、漫反射和高光組成,其中高光部分使用半程向量(halfway vector)來計算。環境光是簡單的常數乘以光強,漫反射是法線方向與光線方向的點積,而高光則是半程向量與法線的點積的冪次方。我需要遍歷所有光源,計算每個光源的這三個分量,并累加到結果顏色中。要注意歸一化各個向量,例如光線方向、視線方向和半程向量。
texture_fragment_shader,需要在Blinn-Phong的基礎上將紋理顏色作為漫反射系數kd。
實現
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos)
{// 獲取三角形頂點坐標(屏幕空間)auto v = t.toVector4();// 計算包圍盒邊界(網頁1、網頁4)float min_x = std::min(v[0].x(), std::min(v[1].x(), v[2].x()));float max_x = std::max(v[0].x(), std::max(v[1].x(), v[2].x()));float min_y = std::min(v[0].y(), std::min(v[1].y(), v[2].y()));float max_y = std::max(v[0].y(), std::max(v[1].y(), v[2].y()));// 轉換為整數像素范圍(網頁4)int x_min = std::floor(min_x);int x_max = std::ceil(max_x);int y_min = std::floor(min_y);int y_max = std::ceil(max_y);// 遍歷包圍盒內所有像素(網頁4)for (int x = x_min; x <= x_max; ++x) {for (int y = y_min; y <= y_max; ++y) {// 檢查像素中心是否在三角形內(網頁3)if (insideTriangle(x + 0.5, y + 0.5, t.v)) {// 計算重心坐標(網頁1)auto [alpha, beta, gamma] = computeBarycentric2D(x + 0.5, y + 0.5, t.v);// 深度插值計算(網頁1、網頁4)float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());float zp = alpha * v[0].z()/v[0].w() + beta * v[1].z()/v[1].w() + gamma * v[2].z()/v[2].w();zp *= Z;// 深度測試(網頁4)if (zp < depth_buf[get_index(x, y)]) {// 更新深度緩沖區depth_buf[get_index(x, y)] = zp;// 屬性插值(網頁1、網頁2)auto interpolated_color = interpolate(alpha, beta, gamma, t.color[0], t.color[1], t.color[2], 1.0f);auto interpolated_normal = interpolate(alpha, beta, gamma,t.normal[0], t.normal[1], t.normal[2], 1.0f).normalized();auto interpolated_texcoords = interpolate(alpha, beta, gamma,t.tex_coords[0], t.tex_coords[1], t.tex_coords[2], 1.0f);auto interpolated_shadingcoords = interpolate(alpha, beta, gamma,view_pos[0], view_pos[1], view_pos[2], 1.0f);// 構造著色器輸入(網頁1)fragment_shader_payload payload(interpolated_color,interpolated_normal,interpolated_texcoords,texture ? &*texture : nullptr);payload.view_pos = interpolated_shadingcoords;// 調用片段著色器(網頁2)Eigen::Vector3f pixel_color = fragment_shader(payload);// 寫入像素顏色(網頁4)set_pixel(Eigen::Vector2i(x, y), pixel_color);}}}}
}
圖形渲染管線
?
處理邏輯
在shader當中,是在shader.hpp中定義了片著色器的payload,然后在渲染的時候,是把插值后的屬性傳遞給Payload,再把payload傳遞給一開始定義的r.set_fragment_shader(active_shader);
for(auto mesh:Loader.LoadedMeshes){for(int i=0;i<mesh.Vertices.size();i+=3){Triangle* t = new Triangle();for(int j=0;j<3;j++){t->setVertex(j,Vector4f(mesh.Vertices[i+j].Position.X,mesh.Vertices[i+j].Position.Y,mesh.Vertices[i+j].Position.Z,1.0));t->setNormal(j,Vector3f(mesh.Vertices[i+j].Normal.X,mesh.Vertices[i+j].Normal.Y,mesh.Vertices[i+j].Normal.Z));t->setTexCoord(j,Vector2f(mesh.Vertices[i+j].TextureCoordinate.X, mesh.Vertices[i+j].TextureCoordinate.Y));}TriangleList.push_back(t);}}
是在一開始讀取模型的時候,就按照網面設置好了三角形,并把三角形裝進TriangleList中了,然后在最后繪制的時候,就是去繪制這個三角形