核心模块:
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中了,然后在最后绘制的时候,就是去绘制这个三角形