计算机图形学笔记(二):光栅化渲染管线
这部分是整个图形学的核心,也是最实用的部分。做Assignment的时候深深感受到,理解渲染管线的每个步骤对写出正确的代码太重要了。从顶点数据到最终像素,每一步都有它存在的意义。
目录
- 图形渲染管线概述 - 现代GPU管线架构
- 顶点处理与变换 - MVP变换链与顶点着色器
- 图元装配与裁剪 - 视锥体裁剪算法详解
- 光栅化算法详解 - 三角形光栅化与重心坐标
- 深度测试与隐藏面消除 - Z-Buffer算法与优化
- 光照模型与着色 - Phong模型到PBR的演进
- 纹理映射技术 - Mipmap、法线贴图与环境映射
图形渲染管线概述
5.1 渲染管线的整体架构
5.1.1 管线阶段划分
应用阶段(Application Stage):
- 场景管理:构建场景图
- 视锥体裁剪:剔除不可见物体
- 细节层次控制:根据距离选择模型精度
- 动画更新:骨骼动画、变形动画
几何阶段(Geometry Stage):
- 顶点着色:变换顶点位置
- 投影:3D到2D的映射
- 裁剪:移除视锥体外的几何体
- 屏幕映射:NDC到屏幕坐标
光栅化阶段(Rasterization Stage):
- 三角形设置:计算边方程
- 三角形遍历:确定覆盖的像素
- 像素着色:计算每个像素的颜色
- 合并:深度测试、混合等
5.1.2 坐标系变换流程的数学推导
完整的变换链
坐标空间序列:
MVP变换的数学表示
复合变换矩阵:
其中:
- :模型变换矩阵(Model Matrix)
- :视图变换矩阵(View Matrix)
- :投影变换矩阵(Projection Matrix)
透视除法(Perspective Division)
齐次坐标到NDC的转换:
透视除法:
NDC范围:
视口变换(Viewport Transform)
从NDC到屏幕坐标:
视口变换矩阵 的结构:
- 第1行:
- 第2行:
- 第3行:
- 第4行:
其中:
- :屏幕宽度(像素)
- :屏幕高度(像素)
- :深度缓冲区范围
最终屏幕坐标:
对于NDC坐标 ,屏幕坐标为:
变换的几何意义
坐标系的右手/左手约定:
- OpenGL:右手坐标系,
- DirectX:左手坐标系,
深度值的非线性分布: 透视投影后,深度值在近平面附近密集,远平面附近稀疏:
5.2 可编程着色器架构
5.2.1 顶点着色器(Vertex Shader)
主要功能:
- 顶点位置变换
- 法向量变换
- 纹理坐标传递
- 光照计算(Gouraud着色)
典型顶点着色器:
# version 330 core
layout (location = 0) in vec3 aPos;layout (location = 1) in vec3 aNormal;layout (location = 2) in vec2 aTexCoord;
uniform mat4 model;uniform mat4 view;uniform mat4 projection;
out vec3 FragPos;out vec3 Normal;out vec2 TexCoord;
void main() { FragPos = vec3(model * vec4(aPos, 1.0)); Normal = mat3(transpose(inverse(model))) * aNormal; TexCoord = aTexCoord;
gl_Position = projection * view * vec4(FragPos, 1.0);}5.2.2 片段着色器(Fragment Shader)
主要功能:
- 像素颜色计算
- 纹理采样
- 光照计算(Phong着色)
- 特效处理
典型片段着色器:
# version 330 core
in vec3 FragPos;in vec3 Normal;in vec2 TexCoord;
uniform sampler2D texture_diffuse1;uniform vec3 lightPos;uniform vec3 viewPos;
out vec4 FragColor;
void main() { // 环境光 float ambientStrength = 0.1; vec3 ambient = ambientStrength * vec3(1.0);
// 漫反射 vec3 norm = normalize(Normal); vec3 lightDir = normalize(lightPos - FragPos); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * vec3(1.0);
// 镜面反射 float specularStrength = 0.5; vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); vec3 specular = specularStrength * spec * vec3(1.0);
vec3 result = (ambient + diffuse + specular) * texture(texture_diffuse1, TexCoord).rgb; FragColor = vec4(result, 1.0);}顶点处理与变换
6.1 顶点属性与数据结构
6.1.1 顶点数据组织
基本顶点属性:
struct Vertex { Vector3f position; // 位置 Vector3f normal; // 法向量 Vector2f texCoord; // 纹理坐标 Vector3f tangent; // 切向量 Vector3f bitangent; // 副切向量 Vector4f color; // 顶点颜色};顶点缓冲对象(VBO):
// 创建并绑定VBOunsigned int VBO;glGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW);6.1.2 顶点数组对象(VAO)
VAO的作用:
- 存储顶点属性配置
- 简化渲染调用
- 提高渲染效率
VAO配置示例:
unsigned int VAO;glGenVertexArrays(1, &VAO);glBindVertexArray(VAO);
// 位置属性glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);glEnableVertexAttribArray(0);
// 法向量属性glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));glEnableVertexAttribArray(1);
// 纹理坐标属性glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));glEnableVertexAttribArray(2);6.2 变换矩阵的计算与优化
6.2.1 法向量变换的数学推导
问题的提出
核心问题:法向量不能直接用模型变换矩阵进行变换,否则会破坏垂直关系。
数学推导过程
平面的隐式表示: 设平面方程为:
其中 是法向量, 是平面上的点, 是常数。
变换后的约束条件: 变换后的平面方程应为:
点的变换关系: 已知点的变换为:
法向量变换的推导: 为保持垂直关系,需要:
将其写成矩阵形式:
由于这对所有 都成立,因此:
转置得到:
解得:
法向量变换矩阵
法向量变换矩阵:
其中 是模型变换矩阵的左上角 子矩阵。
特殊情况:
- 正交变换:当 是正交矩阵时,,因此
- 均匀缩放:当 时,
工程实现
// 计算法向量变换矩阵Matrix3f compute_normal_matrix(const Matrix4f& model_matrix) { Matrix3f upper_left = model_matrix.block<3,3>(0,0); return upper_left.inverse().transpose();}
// 应用法向量变换Vector3f transform_normal(const Vector3f& normal, const Matrix4f& model_matrix) { Matrix3f normal_matrix = compute_normal_matrix(model_matrix); Vector3f transformed = normal_matrix * normal; return transformed.normalized(); // 重新归一化}6.2.2 变换矩阵的TRS分解
TRS分解的数学理论
分解定理: 任何非奇异的仿射变换矩阵 都可以唯一分解为:
其中:
- :平移矩阵(Translation)
- :旋转矩阵(Rotation)
- :缩放矩阵(Scaling)
分解算法的数学推导
矩阵结构分析:
变换矩阵的分块结构:
- 左上角 子矩阵 :包含旋转和缩放
- 右上角列向量 :平移向量
- 左下角行向量 :零向量
- 右下角标量:1
其中 是线性变换部分, 是平移向量。
第一步:提取平移
第二步:分解线性变换 对矩阵 进行极分解:
缩放因子计算:
旋转矩阵提取:
旋转矩阵通过归一化列向量获得:
其中每一列都是归一化后的旋转轴。
处理反射变换
行列式检查: 如果 ,说明包含反射变换:
完整的分解实现
struct Transform { Vector3f translation; Quaternionf rotation; Vector3f scale;};
Transform decompose_matrix(const Matrix4f& matrix) { Transform result;
// 1. 提取平移 result.translation = matrix.block<3,1>(0,3);
// 2. 提取线性变换部分 Matrix3f A = matrix.block<3,3>(0,0);
// 3. 计算缩放因子 result.scale.x() = A.col(0).norm(); result.scale.y() = A.col(1).norm(); result.scale.z() = A.col(2).norm();
// 4. 处理反射(负行列式) if (A.determinant() < 0) { result.scale.z() = -result.scale.z(); }
// 5. 提取旋转矩阵 Matrix3f R; R.col(0) = A.col(0) / result.scale.x(); R.col(1) = A.col(1) / result.scale.y(); R.col(2) = A.col(2) / result.scale.z();
// 6. 转换为四元数 result.rotation = Quaternionf(R);
return result;} rotation_matrix.col(1) = upper_left.col(1) / scale.y(); rotation_matrix.col(2) = upper_left.col(2) / scale.z();
// 转换为四元数 rotation = Quaternionf(rotation_matrix);}图元装配与裁剪
7.1 图元装配过程
7.1.1 图元类型
基本图元:
- 点(Points)
- 线段(Lines)
- 三角形(Triangles)
扩展图元:
- 线条带(Line Strip)
- 三角形带(Triangle Strip)
- 三角形扇(Triangle Fan)
7.1.2 索引缓冲对象(EBO)
作用:减少顶点数据冗余
示例:
// 顶点数据(4个顶点定义矩形)float vertices[] = { 0.5f, 0.5f, 0.0f, // 右上 0.5f, -0.5f, 0.0f, // 右下 -0.5f, -0.5f, 0.0f, // 左下 -0.5f, 0.5f, 0.0f // 左上};
// 索引数据(2个三角形)unsigned int indices[] = { 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形};
// 创建EBOunsigned int EBO;glGenBuffers(1, &EBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);7.2 裁剪算法
7.2.1 视锥体裁剪的数学理论
视锥体的数学定义
标准化设备坐标(NDC)中的视锥体: 在齐次裁剪坐标系中,视锥体由六个半空间定义:
六个裁剪平面:
- 左平面:
- 右平面:
- 下平面:
- 上平面:
- 近平面:
- 远平面:
其中 是齐次裁剪坐标。
点的裁剪测试
数学条件: 点 在视锥体内当且仅当:
几何意义: 这等价于透视除法后的NDC坐标满足:
工程实现
bool is_inside_frustum(const Vector4f& clip_pos) { float w = clip_pos.w();
// 检查w分量的有效性 if (w <= 0) return false; // 在相机后方
return (clip_pos.x() >= -w && clip_pos.x() <= w && clip_pos.y() >= -w && clip_pos.y() <= w && clip_pos.z() >= -w && clip_pos.z() <= w);}
// 计算点到裁剪平面的距离float distance_to_plane(const Vector4f& point, int plane_index) { switch (plane_index) { case 0: return point.w() + point.x(); // 左平面 case 1: return point.w() - point.x(); // 右平面 case 2: return point.w() + point.y(); // 下平面 case 3: return point.w() - point.y(); // 上平面 case 4: return point.w() + point.z(); // 近平面 case 5: return point.w() - point.z(); // 远平面 default: return 0.0f; }}7.2.2 Sutherland-Hodgman裁剪算法
算法的数学原理
基本思想: 对多边形逐个裁剪平面进行裁剪,每次裁剪产生一个新的多边形,直到所有裁剪平面都处理完毕。
数学模型: 设多边形的顶点序列为 ,裁剪平面方程为:
其中 是平面法向量, 是距离参数。
点与平面的位置关系
符号距离函数:
位置判断:
- :点在平面正侧(内部)
- :点在平面上
- :点在平面负侧(外部)
线段与平面的交点计算
参数方程: 线段 的参数方程为:
交点参数求解: 将参数方程代入平面方程:
解得:
交点坐标:
算法实现
class SutherlandHodgmanClipper {private: struct Plane { Vector3f normal; float d;
float distance(const Vector3f& point) const { return normal.dot(point) + d; }
bool is_inside(const Vector3f& point) const { return distance(point) >= 0; } };
public: std::vector<Vector3f> clip_polygon(const std::vector<Vector3f>& polygon, const std::vector<Plane>& planes) { std::vector<Vector3f> input = polygon;
for (const auto& plane : planes) { std::vector<Vector3f> output;
if (input.empty()) break;
Vector3f prev_vertex = input.back(); bool prev_inside = plane.is_inside(prev_vertex);
for (const auto& curr_vertex : input) { bool curr_inside = plane.is_inside(curr_vertex);
if (curr_inside) { if (!prev_inside) { // 从外部进入:添加交点 Vector3f intersection = compute_intersection(prev_vertex, curr_vertex, plane); output.push_back(intersection); } // 添加当前顶点 output.push_back(curr_vertex); } else if (prev_inside) { // 从内部离开:只添加交点 Vector3f intersection = compute_intersection(prev_vertex, curr_vertex, plane); output.push_back(intersection); } // 两点都在外部:不添加任何点
prev_vertex = curr_vertex; prev_inside = curr_inside; }
input = output; }
return input; }
private: Vector3f compute_intersection(const Vector3f& p1, const Vector3f& p2, const Plane& plane) { Vector3f direction = p2 - p1; float denominator = plane.normal.dot(direction);
if (std::abs(denominator) < 1e-6f) { return p1; // 线段平行于平面 }
float t = -plane.distance(p1) / denominator; return p1 + t * direction; }};光栅化算法详解
8.1 三角形光栅化基础
8.1.1 扫描线算法的数学原理
算法概述与数学基础
基本思想:扫描线算法通过逐行扫描的方式填充三角形,是经典的多边形光栅化方法。
核心数学原理:
- 直线方程:利用直线的参数方程或隐式方程计算交点
- 区间填充:在每条扫描线上填充左右边界之间的像素
- 增量计算:利用相邻扫描线的相关性优化计算
直线方程与交点计算
参数直线方程: 连接点 和 的直线:
参数方程():
扫描线交点计算: 给定水平扫描线 ,求解参数 :
代入x方程得交点:
增量形式优化: 相邻扫描线的x坐标增量:
因此:
边方程方法(Half-Space Test)
隐式直线方程: 对于直线 ,隐式方程为:
半空间测试: 点 相对于有向直线 的位置:
- :点在直线左侧
- :点在直线上
- :点在直线右侧
三角形内部判断: 对于逆时针方向的三角形 ,点 在三角形内当且仅当:
边函数定义:
优化的扫描线实现
void optimized_scanline_triangle(const Triangle& t) { auto vertices = t.toVector4();
// 按y坐标排序 std::sort(vertices.begin(), vertices.end(), [](const Vector4f& a, const Vector4f& b) { return a.y() < b.y(); });
Vector4f v0 = vertices[0], v1 = vertices[1], v2 = vertices[2];
// 计算边的增量 float dx01 = (v1.x() - v0.x()) / (v1.y() - v0.y()); float dx02 = (v2.x() - v0.x()) / (v2.y() - v0.y()); float dx12 = (v2.x() - v1.x()) / (v2.y() - v1.y());
// 上半部分三角形 (v0 到 v1) float x_left = v0.x(), x_right = v0.x(); for (int y = v0.y(); y <= v1.y(); y++) { fill_horizontal_line(x_left, x_right, y, t.getColor()); x_left += dx01; x_right += dx02; }
// 下半部分三角形 (v1 到 v2) x_left = v1.x(); // 重新设置左边界 for (int y = v1.y(); y <= v2.y(); y++) { fill_horizontal_line(x_left, x_right, y, t.getColor()); x_left += dx12; x_right += dx02; }}8.1.2 重心坐标系统(Barycentric Coordinates)
重心坐标的数学基础
定义:对于三角形 和平面内任意点 ,重心坐标是一组权重 ,使得:
几何解释:
- 分别表示点 相对于顶点 的”重量”
- 当 时,点 在三角形内部
- 重心坐标提供了三角形内任意点的唯一表示
重心坐标的计算方法
面积比方法: 重心坐标等于子三角形面积与总三角形面积的比值:
向量叉积计算: 利用叉积计算有向面积: 其中 是三角形的法向量。
GAMES101 Assignment 2实现:
// 基于面积的重心坐标计算static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector3f* v) { float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y()); float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y()); float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y()); return {c1, c2, c3};}
// 优化版本:避免重复计算Vector3f barycentric_coordinates(const Vector2f& A, const Vector2f& B, const Vector2f& C, const Vector2f& P) { Vector2f v0 = C - A; Vector2f v1 = B - A; Vector2f v2 = P - A;
float dot00 = v0.dot(v0); float dot01 = v0.dot(v1); float dot02 = v0.dot(v2); float dot11 = v1.dot(v1); float dot12 = v1.dot(v2);
float inv_denom = 1.0f / (dot00 * dot11 - dot01 * dot01); float u = (dot11 * dot02 - dot01 * dot12) * inv_denom; float v = (dot00 * dot12 - dot01 * dot02) * inv_denom;
return Vector3f(1.0f - u - v, v, u); // $(\alpha, \beta, \gamma)$}重心坐标的几何意义
面积解释:
- :点P到边BC的”距离权重”
- :点P到边AC的”距离权重”
- :点P到边AB的”距离权重”
边界情况:
- :
- :
- :
- 在边 上:,
重心坐标的应用
1. 点在三角形内部判断:
bool is_inside_triangle(float alpha, float beta, float gamma) { return alpha >= 0 && beta >= 0 && gamma >= 0;}2. 属性插值: 对于三角形顶点的任意属性(颜色、深度、纹理坐标等),可以用重心坐标进行插值:
// 深度插值float interpolated_depth = alpha * depth_A + beta * depth_B + gamma * depth_C;
// 颜色插值Vector3f interpolated_color = alpha * color_A + beta * color_B + gamma * color_C;
// 纹理坐标插值Vector2f interpolated_uv = alpha * uv_A + beta * uv_B + gamma * uv_C;8.2 属性插值
8.2.1 透视校正插值
问题:屏幕空间的线性插值在透视投影下不正确
解决方案:在齐次坐标空间进行插值
透视正确插值公式:
深度倒数插值:
纹理坐标u分量插值:
纹理坐标v分量插值:
最终纹理坐标:
代码实现:
Vector2f perspective_correct_interpolation(const Vector3f& bary, const Vector2f& uv1, float z1, const Vector2f& uv2, float z2, const Vector2f& uv3, float z3) { float inv_z = bary.x() / z1 + bary.y() / z2 + bary.z() / z3; float u_over_z = bary.x() * uv1.x() / z1 + bary.y() * uv2.x() / z2 + bary.z() * uv3.x() / z3; float v_over_z = bary.x() * uv1.y() / z1 + bary.y() * uv2.y() / z2 + bary.z() * uv3.y() / z3;
return Vector2f(u_over_z / inv_z, v_over_z / inv_z);}8.2.2 现代GPU并行光栅化算法
Tile-Based光栅化
基本思想: 现代GPU将屏幕分割成小的tile(通常8×8或16×16像素),每个tile并行处理。
算法流程:
- Tile分割:将屏幕分成规则的tile网格
- 三角形分配:确定每个三角形覆盖哪些tile
- 并行光栅化:每个tile独立进行光栅化
- 结果合并:将各tile的结果合并到最终图像
数学模型: 对于tile ,其屏幕坐标范围为:
Tile坐标计算:
三角形-Tile相交测试: 使用分离轴定理(SAT)或包围盒测试快速判断三角形是否与tile相交。
GPU Warp/Wavefront并行模型
SIMD执行模型: GPU以warp(NVIDIA)或wavefront(AMD)为单位执行,通常包含32个线程。
并行光栅化实现:
// GPU kernel伪代码__global__ void rasterize_triangle_kernel(Triangle* triangles, int num_triangles, float* framebuffer, float* depth_buffer, int width, int height) { int pixel_x = blockIdx.x * blockDim.x + threadIdx.x; int pixel_y = blockIdx.y * blockDim.y + threadIdx.y;
if (pixel_x >= width || pixel_y >= height) return;
float min_depth = FLT_MAX; Vector3f final_color(0, 0, 0);
// 遍历所有三角形 for (int i = 0; i < num_triangles; i++) { Triangle& tri = triangles[i];
// 计算重心坐标 auto [alpha, beta, gamma] = computeBarycentric2D(pixel_x, pixel_y, tri.vertices);
// 内部测试 if (alpha >= 0 && beta >= 0 && gamma >= 0) { // 插值深度 float depth = alpha * tri.vertices[0].z + beta * tri.vertices[1].z + gamma * tri.vertices[2].z;
// 深度测试 if (depth < min_depth) { min_depth = depth;
// 插值颜色 final_color = alpha * tri.colors[0] + beta * tri.colors[1] + gamma * tri.colors[2]; } } }
// 写入framebuffer int pixel_index = pixel_y * width + pixel_x; framebuffer[pixel_index * 3 + 0] = final_color.x; framebuffer[pixel_index * 3 + 1] = final_color.y; framebuffer[pixel_index * 3 + 2] = final_color.z; depth_buffer[pixel_index] = min_depth;}Early-Z优化技术
Z-Prepass: 在主渲染pass之前,先进行一次只写深度的pass:
// Z-Prepass阶段void z_prepass(const std::vector<Triangle>& triangles) { for (const auto& tri : triangles) { rasterize_triangle_depth_only(tri); }}
// 主渲染阶段void main_render_pass(const std::vector<Triangle>& triangles) { for (const auto& tri : triangles) { rasterize_triangle_with_early_z(tri); }}Early-Z测试: 在片段着色器执行前进行深度测试,可以大幅减少不必要的着色计算。
层次化Z-Buffer(Hi-Z)
基本原理: 构建深度缓冲区的mipmap层次结构,每个层级存储下一级的最小/最大深度值。
快速剔除:
bool hierarchical_z_test(int x, int y, float z, int level) { int tile_x = x >> level; int tile_y = y >> level;
float max_z = hi_z_buffer[level][tile_y * (width >> level) + tile_x];
return z <= max_z; // 如果当前深度大于tile最大深度,可以剔除}8.2.3 Mipmap与纹理过滤
Mipmap原理:预计算多级纹理,根据像素覆盖面积选择合适级别
级别计算:
float calculate_mipmap_level(const Vector2f& duv_dx, const Vector2f& duv_dy, int texture_width, int texture_height) { float du_dx = duv_dx.x() * texture_width; float dv_dx = duv_dx.y() * texture_height; float du_dy = duv_dy.x() * texture_width; float dv_dy = duv_dy.y() * texture_height;
float max_sqr = std::max(du_dx * du_dx + dv_dx * dv_dx, du_dy * du_dy + dv_dy * dv_dy);
return 0.5f * std::log2(max_sqr);}三线性过滤:
- 在两个相邻mipmap级别分别进行双线性过滤
- 在两个结果间进行线性插值
深度测试与隐藏面消除
9.1 Z-Buffer算法
9.1.1 算法原理
基本思想:为每个像素维护一个深度值,只绘制最近的表面
算法步骤:
// 初始化for each pixel (x, y): color_buffer[x][y] = background_color depth_buffer[x][y] = infinity
// 渲染for each triangle: for each pixel (x, y) in triangle: z = interpolated_depth(x, y) if z < depth_buffer[x][y]: depth_buffer[x][y] = z color_buffer[x][y] = shaded_color(x, y)9.1.2 深度值计算的数学分析
线性深度与非线性深度
线性深度(观察空间): 在观察空间中,深度值与距离成线性关系:
其中 是观察空间中的深度, 和 分别是近平面和远平面距离。
非线性深度(透视投影后): 经过透视投影和透视除法后,NDC空间中的深度为:
深度精度分析
精度分布函数: 深度缓冲区精度定义为:
关键观察:
- 精度与 成反比
- 近平面附近精度最高
- 远平面附近精度急剧下降
Z-Fighting的数学原因
浮点精度限制: 对于24位深度缓冲区,最小可分辨深度差为:
临界距离计算: 两个表面在距离 处可分辨的最小间距为:
深度精度优化策略
1. 近远平面比值优化: 最优比值关系:
2. 对数深度缓冲: 使用对数分布改善精度:
3. 反向Z缓冲: 将深度映射反转:
利用浮点数在0附近精度更高的特性。
工程实现
// 深度精度分析工具class DepthPrecisionAnalyzer {public: static float compute_precision(float z_eye, float near, float far) { return (2.0f * far * near) / (z_eye * z_eye * (far - near)); }
static float min_resolvable_distance(float z_eye, float near, float far, int depth_bits) { float depth_resolution = 1.0f / (1 << depth_bits); float precision = compute_precision(z_eye, near, far); return depth_resolution / precision; }
static void analyze_depth_distribution(float near, float far, int samples = 100) { std::cout << "Depth Precision Analysis:\n"; std::cout << "Near: " << near << ", Far: " << far << "\n"; std::cout << "Ratio: " << far/near << "\n\n";
for (int i = 0; i < samples; i++) { float t = static_cast<float>(i) / (samples - 1); float z_eye = near + t * (far - near); float precision = compute_precision(z_eye, near, far); float min_dist = min_resolvable_distance(z_eye, near, far, 24);
std::cout << "z=" << z_eye << ", precision=" << precision << ", min_dist=" << min_dist << "\n"; } }};
// 反向Z缓冲实现Matrix4f create_reverse_z_projection(float fov, float aspect, float near, float far) { Matrix4f proj = Matrix4f::Zero();
float tan_half_fov = std::tan(fov * 0.5f);
proj(0, 0) = 1.0f / (aspect * tan_half_fov); proj(1, 1) = 1.0f / tan_half_fov; proj(2, 2) = near / (far - near); // 反向映射 proj(2, 3) = (far * near) / (far - near); proj(3, 2) = 1.0f; // 注意:这里是+1而不是-1
return proj;}9.2 其他隐藏面消除算法
9.2.1 画家算法
原理:按深度从远到近绘制物体
优点:
- 简单易实现
- 支持透明度混合
缺点:
- 需要排序,复杂度高
- 无法处理循环遮挡
实现:
struct Triangle { Vector3f vertices[3]; float avg_depth; Color color;};
bool depth_compare(const Triangle& a, const Triangle& b) { return a.avg_depth > b.avg_depth; // 远到近排序}
void painters_algorithm(std::vector<Triangle>& triangles) { // 计算每个三角形的平均深度 for (auto& tri : triangles) { tri.avg_depth = (tri.vertices[0].z() + tri.vertices[1].z() + tri.vertices[2].z()) / 3.0f; }
// 排序并绘制 std::sort(triangles.begin(), triangles.end(), depth_compare);
for (const auto& tri : triangles) { rasterize_triangle(tri); }}9.2.2 BSP树算法
原理:用二叉空间分割树预处理场景
构建过程:
- 选择分割平面
- 将多边形分为前、后两组
- 递归构建子树
遍历渲染:
void render_bsp_tree(BSPNode* node, const Vector3f& view_pos) { if (!node) return;
float distance = node->plane.distance_to_point(view_pos);
if (distance > 0) { // 观察者在平面前方 render_bsp_tree(node->back, view_pos); // 先画后面 render_polygons(node->polygons); // 再画平面上的多边形 render_bsp_tree(node->front, view_pos); // 最后画前面 } else { // 观察者在平面后方 render_bsp_tree(node->front, view_pos); render_polygons(node->polygons); render_bsp_tree(node->back, view_pos); }}光照模型与着色
10.1 光照的物理基础理论
10.1.1 辐射度量学的数学框架
基本辐射量的定义
辐射能量(Radiant Energy): 表示电磁辐射携带的总能量。
辐射通量(Radiant Flux/Power): 表示单位时间内通过某个表面的辐射能量。
辐射强度(Radiant Intensity): 表示点光源在某个方向上单位立体角内的辐射通量。
辐照度(Irradiance): 表示单位面积接收到的辐射通量。
辐射度(Radiance):
这是最重要的辐射量,其中 是方向 与表面法向量的夹角。
辐射度的重要性质
1. 感知相关性: 辐射度直接对应于人眼或相机传感器接收到的光强。
2. 传播不变性: 在真空中,辐射度沿直线传播时保持不变:
3. 可加性: 多个光源的辐射度可以直接相加:
10.1.2 双向反射分布函数(BRDF)理论
BRDF的严格数学定义
微分形式定义:
其中:
- :反射辐射度
- :入射辐照度
- :入射辐射度
BRDF的物理约束条件
1. 非负性约束:
2. 能量守恒约束:
这确保反射的能量不超过入射能量。
3. 亥姆霍兹互易性:
这是基于光路可逆性的物理原理。
BRDF的几何解释
立体角微分:
其中 是投影面积, 是距离, 是与法向量的夹角。
半球积分: BRDF在上半球面上的积分给出了反射率:
10.2 经典光照模型的数学推导
10.2.1 Lambert漫反射模型
物理原理与假设
理想漫反射表面的特征:
- 各向同性:反射在所有方向上均匀分布
- 完全漫反射:没有镜面反射分量
- 表面粗糙:微观结构导致光线随机散射
Lambert BRDF的数学推导
基本假设:反射辐射度在所有方向上恒定
能量守恒约束:
其中 是漫反射率(albedo)。
半球积分计算:
Lambert BRDF推导: 由于 为常数,结合能量守恒:
因此:
Lambert余弦定律
反射辐射度计算:
对于平行光源:
物理意义:
- 项体现了投影面积效应
- 入射角越大,有效照射面积越小
工程实现
Vector3f lambert_diffuse(const Vector3f& light_dir, const Vector3f& normal, const Vector3f& light_color, const Vector3f& albedo) { // 计算入射角余弦值 float cos_theta = std::max(0.0f, normal.dot(light_dir));
// Lambert BRDF: $\frac{albedo}{\pi}$ // 最终颜色 = $BRDF \times 入射光 \times \cos(\theta) \times \pi$ (积分因子) // 简化为:$albedo \times light\_color \times \cos(\theta)$ return albedo * light_color * cos_theta;}
// 更严格的实现(包含$\pi$因子)Vector3f lambert_brdf_strict(const Vector3f& light_dir, const Vector3f& normal, const Vector3f& light_color, const Vector3f& albedo) { float cos_theta = std::max(0.0f, normal.dot(light_dir));
// 严格的Lambert BRDF Vector3f brdf = albedo / M_PI;
// 渲染方程的离散形式 return brdf * light_color * cos_theta * M_PI; // $\pi$来自立体角积分}10.2.2 Phong反射模型的数学推导
Phong模型的三分量结构
Phong模型将光照分解为三个独立的分量:
1. 环境光(Ambient): 模拟复杂的间接光照,是一个常数项。
2. 漫反射(Diffuse): 使用Lambert余弦定律。
3. 镜面反射(Specular): 模拟光滑表面的镜面高光。
镜面反射的数学推导
反射向量的计算: 根据反射定律,入射向量 关于法向量 的反射向量为:
推导过程: 设入射向量为 ,将其分解为法向分量和切向分量:
其中:
- (法向分量)
- (切向分量)
反射时,切向分量不变,法向分量反向:
整理得:
完整的Phong光照方程
其中:
- :材质系数
- :光泽度指数(shininess)
- :视线方向向量
工程实现
Vector3f phong_lighting(const Vector3f& position, const Vector3f& normal, const Vector3f& view_dir, const Vector3f& light_pos, const Vector3f& light_color, const Material& material) { // 环境光分量 Vector3f ambient = material.ambient * light_color;
// 计算光线方向 Vector3f light_dir = (light_pos - position).normalized();
// 漫反射分量(Lambert) float diff = std::max(normal.dot(light_dir), 0.0f); Vector3f diffuse = material.diffuse * light_color * diff;
// 计算反射向量 Vector3f reflect_dir = 2.0f * normal.dot(light_dir) * normal - light_dir; reflect_dir.normalize();
// 镜面反射分量 float spec = std::pow(std::max(view_dir.dot(reflect_dir), 0.0f), material.shininess); Vector3f specular = material.specular * light_color * spec;
return ambient + diffuse + specular;}10.2.3 Blinn-Phong模型的数学改进
Blinn-Phong的理论基础
改进动机: Phong模型在计算反射向量时存在效率和稳定性问题,Blinn-Phong通过引入半角向量来解决这些问题。
半角向量的数学定义:
其中 是光线方向, 是视线方向。
几何意义与物理解释
几何意义: 是光线方向和视线方向的角平分线,表示能够产生镜面反射的理想微表面法向量。
物理解释: 在微表面理论中,只有法向量与半角向量平行的微表面才会将光线从 方向反射到 方向。
Blinn-Phong镜面反射公式
修改后的镜面反射项:
指数关系: 为了获得与Phong模型相似的视觉效果,通常需要调整光泽度指数:
数学优势分析
1. 计算效率:
- Phong: (6次乘法,3次减法)
- Blinn-Phong: (3次加法,1次归一化)
2. 数值稳定性: 避免了反射向量在掠射角附近的数值不稳定问题。
3. 物理合理性: 更好地符合微表面理论的物理基础。
代码实现:
Vector3f blinn_phong_specular(const Vector3f& light_dir, const Vector3f& view_dir, const Vector3f& normal, const Vector3f& light_color, const Vector3f& specular_color, float shininess) { Vector3f half_dir = (light_dir + view_dir).normalized(); float spec = std::pow(std::max(normal.dot(half_dir), 0.0f), shininess); return specular_color * light_color * spec;}10.3 着色技术
10.3.1 平面着色(Flat Shading)
原理:每个三角形使用单一颜色
法向量计算:
Vector3f calculate_face_normal(const Vector3f& v1, const Vector3f& v2, const Vector3f& v3) { Vector3f edge1 = v2 - v1; Vector3f edge2 = v3 - v1; return edge1.cross(edge2).normalized();}特点:
- 计算简单
- 多面体外观明显
- 适合低多边形风格
10.3.2 Gouraud着色
原理:在顶点计算光照,三角形内部插值
算法步骤:
- 计算顶点法向量(相邻面法向量平均)
- 在顶点进行光照计算
- 在三角形内部插值颜色
顶点法向量计算:
void calculate_vertex_normals(Mesh& mesh) { // 初始化顶点法向量为零 for (auto& vertex : mesh.vertices) { vertex.normal = Vector3f::Zero(); }
// 累加相邻面的法向量 for (const auto& face : mesh.faces) { Vector3f face_normal = calculate_face_normal( mesh.vertices[face.v1].position, mesh.vertices[face.v2].position, mesh.vertices[face.v3].position );
mesh.vertices[face.v1].normal += face_normal; mesh.vertices[face.v2].normal += face_normal; mesh.vertices[face.v3].normal += face_normal; }
// 归一化 for (auto& vertex : mesh.vertices) { vertex.normal.normalize(); }}10.3.3 Phong着色
原理:插值法向量,在每个像素进行光照计算
算法步骤:
- 在顶点存储法向量
- 在三角形内部插值法向量
- 在每个像素进行光照计算
法向量插值:
Vector3f interpolate_normal(const Vector3f& bary, const Vector3f& n1, const Vector3f& n2, const Vector3f& n3) { Vector3f interpolated = bary.x() * n1 + bary.y() * n2 + bary.z() * n3; return interpolated.normalized();}质量对比:
- Flat < Gouraud < Phong(质量递增)
- Flat > Gouraud > Phong(性能递减)
纹理映射技术
11.1 纹理映射基础理论
11.1.1 纹理坐标系统的数学基础
UV坐标系的定义
标准化纹理坐标: 纹理坐标 定义在单位正方形内:
其中:
- 轴:水平方向,对应纹理的宽度
- 轴:垂直方向,对应纹理的高度
坐标变换
纹理坐标到像素坐标的映射:
最近邻采样的像素坐标:
其中 和 分别是纹理的宽度和高度(以像素为单位)。
连续坐标映射: 对于需要插值的情况:
连续坐标计算:
减去0.5是为了将采样点放在像素中心。
工程实现
struct TextureCoordinate { float u, v;
// 转换为像素坐标(整数) std::pair<int, int> to_pixel_coords(int width, int height) const { int x = static_cast<int>(u * (width - 1)); int y = static_cast<int>(v * (height - 1)); return {x, y}; }
// 转换为连续像素坐标(用于插值) std::pair<float, float> to_continuous_coords(int width, int height) const { float x = u * width - 0.5f; float y = v * height - 0.5f; return {x, y}; }};11.1.2 纹理采样理论
点采样(最近邻采样)
数学定义: 点采样选择距离采样点最近的纹理像素:
其中 表示纹理在像素 处的颜色值。
双线性过滤的数学推导
问题设定: 给定连续纹理坐标 ,计算对应的纹理值。
坐标变换:
连续坐标计算:
四个邻近像素:
像素坐标定义:
插值权重:
权重计算:
双线性插值公式:
分步计算:
展开形式:
其中 表示像素 的颜色值。
几何解释: 双线性插值等价于先在x方向进行两次线性插值,再在y方向进行一次线性插值:
分步插值过程:
x方向插值:
y方向插值:
工程实现
class TextureSampler {public: static Color sample_nearest(const Texture& texture, float u, float v) { int x = static_cast<int>(u * texture.width + 0.5f) % texture.width; int y = static_cast<int>(v * texture.height + 0.5f) % texture.height; return texture.get_pixel(x, y); }
static Color sample_bilinear(const Texture& texture, float u, float v) { float x = u * texture.width - 0.5f; float y = v * texture.height - 0.5f;
int x0 = static_cast<int>(std::floor(x)); int y0 = static_cast<int>(std::floor(y)); int x1 = x0 + 1; int y1 = y0 + 1;
float fx = x - x0; float fy = y - y0;
// 获取四个邻近像素(带边界处理) Color c00 = texture.get_pixel_wrap(x0, y0); Color c10 = texture.get_pixel_wrap(x1, y0); Color c01 = texture.get_pixel_wrap(x0, y1); Color c11 = texture.get_pixel_wrap(x1, y1);
// 双线性插值 Color c0 = lerp(c00, c10, fx); Color c1 = lerp(c01, c11, fx); return lerp(c0, c1, fy); }
private: static Color lerp(const Color& a, const Color& b, float t) { return a * (1.0f - t) + b * t; }};11.1.3 纹理寻址模式的数学定义
重复模式(Repeat/Wrap)
数学定义:
性质:
- 周期性: 对任意整数
- 值域:
镜像模式(Mirror/Reflect)
数学定义:
镜像映射函数:
当 时:
当 时:
其中 。
等价表示:
边缘拉伸模式(Clamp to Edge)
数学定义:
分段函数表示:
钳制映射函数:
当 时:
当 时:
当 时:
边界颜色模式(Border Color)
数学定义:
边界纹理映射函数:
当 时:
其他情况:
其中 是预定义的边界颜色。
工程实现
enum class WrapMode { REPEAT, MIRROR, CLAMP, BORDER};
class TextureAddressing {public: static float apply_wrap_mode(float coord, WrapMode mode) { switch (mode) { case WrapMode::REPEAT: return coord - std::floor(coord);
case WrapMode::MIRROR: { float t = (coord * 0.5f) - std::floor(coord * 0.5f); return (t <= 0.5f) ? (2.0f * t) : (2.0f * (1.0f - t)); }
case WrapMode::CLAMP: return std::max(0.0f, std::min(1.0f, coord));
case WrapMode::BORDER: return coord; // 边界检查在采样时处理
default: return coord; } }
static std::pair<float, float> apply_wrap_mode_2d(float u, float v, WrapMode u_mode, WrapMode v_mode) { return {apply_wrap_mode(u, u_mode), apply_wrap_mode(v, v_mode)}; }};11.2 高级纹理技术
11.2.1 Mipmap技术详解
Mipmap生成算法:
void generate_mipmaps(Texture& texture) { int width = texture.width; int height = texture.height; int level = 0;
while (width > 1 || height > 1) { int new_width = std::max(1, width / 2); int new_height = std::max(1, height / 2);
Texture mip_level(new_width, new_height);
for (int y = 0; y < new_height; ++y) { for (int x = 0; x < new_width; ++x) { // 2x2像素区域平均 Color sum(0, 0, 0, 0); int count = 0;
for (int dy = 0; dy < 2; ++dy) { for (int dx = 0; dx < 2; ++dx) { int src_x = x * 2 + dx; int src_y = y * 2 + dy;
if (src_x < width && src_y < height) { sum += texture.get_pixel(src_x, src_y); count++; } } }
mip_level.set_pixel(x, y, sum / count); } }
texture.mip_levels[++level] = mip_level; width = new_width; height = new_height; }}Mipmap级别计算:
float calculate_mipmap_level(const Vector2f& duv_dx, const Vector2f& duv_dy, int texture_width, int texture_height) { float du_dx = duv_dx.x() * texture_width; float dv_dx = duv_dx.y() * texture_height; float du_dy = duv_dy.x() * texture_width; float dv_dy = duv_dy.y() * texture_height;
float max_sqr = std::max(du_dx * du_dx + dv_dx * dv_dx, du_dy * du_dy + dv_dy * dv_dy);
return 0.5f * std::log2(max_sqr);}11.2.2 法线贴图(Normal Mapping)
切线空间法向量:
Vector3f sample_normal_map(const Texture& normal_map, float u, float v) { Color normal_color = sample_bilinear(normal_map, u, v);
// 从[0,1]映射到[-1,1] Vector3f normal; normal.x() = normal_color.r * 2.0f - 1.0f; normal.y() = normal_color.g * 2.0f - 1.0f; normal.z() = normal_color.b * 2.0f - 1.0f;
return normal.normalized();}TBN矩阵构建:
Matrix3f build_tbn_matrix(const Vector3f& normal, const Vector3f& tangent) { Vector3f N = normal.normalized(); Vector3f T = tangent.normalized();
// Gram-Schmidt正交化 T = (T - N.dot(T) * N).normalized(); Vector3f B = N.cross(T);
Matrix3f TBN; TBN.col(0) = T; // Tangent TBN.col(1) = B; // Bitangent TBN.col(2) = N; // Normal
return TBN;}切线空间到世界空间变换:
Vector3f transform_normal(const Vector3f& tangent_normal, const Matrix3f& TBN) { return (TBN * tangent_normal).normalized();}11.2.3 环境映射
球面映射:
Vector2f sphere_mapping(const Vector3f& direction) { Vector3f d = direction.normalized(); float u = 0.5f + std::atan2(d.z(), d.x()) / (2.0f * M_PI); float v = 0.5f - std::asin(d.y()) / M_PI; return Vector2f(u, v);}立方体映射:
struct CubemapSample { int face; Vector2f uv;};
CubemapSample cube_mapping(const Vector3f& direction) { Vector3f abs_dir = direction.cwiseAbs(); CubemapSample sample;
if (abs_dir.x() >= abs_dir.y() && abs_dir.x() >= abs_dir.z()) { // X面 sample.face = direction.x() > 0 ? 0 : 1; // +X : -X sample.uv.x() = direction.x() > 0 ? -direction.z() : direction.z(); sample.uv.y() = -direction.y(); sample.uv /= abs_dir.x(); } else if (abs_dir.y() >= abs_dir.z()) { // Y面 sample.face = direction.y() > 0 ? 2 : 3; // +Y : -Y sample.uv.x() = direction.x(); sample.uv.y() = direction.y() > 0 ? direction.z() : -direction.z(); sample.uv /= abs_dir.y(); } else { // Z面 sample.face = direction.z() > 0 ? 4 : 5; // +Z : -Z sample.uv.x() = direction.z() > 0 ? direction.x() : -direction.x(); sample.uv.y() = -direction.y(); sample.uv /= abs_dir.z(); }
// 转换到[0,1]范围 sample.uv = (sample.uv + Vector2f(1, 1)) * 0.5f; return sample;}