光栅化软渲 Simple Renderer

序言

一个用 C++ 编写的简单的光栅化软渲染器,没想好起什么名字,不如就叫 Simple Renderer 吧。主要还是为了巩固一下图形学相关的基础知识,毕竟有很多问题不实际上手是不会遇到的。

基础库的选择:我不想将注意力放在渲染管线以外的部分上,所以数学和显示的部分直接使用了 Eigen 和 EasyX。Eigen 是一个非常优秀的线性代数库,EasyX 是一个足够轻便又不至于太过底层的图形库。当然我只允许自己使用其中的 putpixel() 函数。

功能

  • Bresenham 画线法
  • 扫描线算法光栅化三角形
  • 重心坐标插值
  • 透视矫正插值
  • mvp、viewport 投影矩阵
  • 深度测试
  • 背面剔除
  • Blinn-Phong 光照模型
  • Diffuse 贴图
  • 高光贴图
  • 法线贴图

结构

顶点数据封装在 Vertex 类中。投影变换在 VertexShader 类中实现。光栅化与片元着色器耦合在 Rasterizer。

历程

仓库

GitHub

参考

tinyrenderer
GAMES101 Assignment3

透视矫正插值

具体的推导在 CSDNlowk_persp_interp_techrep 里已经讲的很清楚了。
重要的结论是:

  • 在投影面上对属性 II 插值时,先对 IZ\frac{I}{Z} 插值,然后将结果除以对 1Z\frac{1}{Z} 插值的结果。这样就得到了透视正确的属性插值。

这里是 OpenGL 和我的渲染器里的实现:
StackOverflow
首先,ClipSpace 中的 w 分量等于 ViewSpace 中的 z 分量(符号由左右手坐标系决定,这将影响到后续的深度测试)。
然后将 ClipSpace 的 1/w 分量保存起来,避免多次的除法运算。

1
2
3
4
5
Eigen::Vector4f p = mvp * vertex[i].pos;
p[3] = 1.f / p[3];
p[0] *= p[3];
p[1] *= p[3];
p[2] *= p[3];

然后将重心坐标 (a, b, c) 调整为:
(a,b,c)=(a/pos[0].w,b/pos[1].w,c/pos[2].w)a/pos[0].w+b/pos[1].w+c/pos[2].w(a,b,c)=\frac{(a/pos[0].w, b/pos[1].w, c/pos[2].w)}{a/pos[0].w+b/pos[1].w+c/pos[2].w}
这里的 w 就是 ViewSpace 的 z ,这个很长的分母就是所谓对 1Z\frac{1}{Z} 插值的结果,三个分子就是将 II 提出后剩余的部分。

1
2
3
4
5
6
7
8
9
10
11
12
// 在投影平面上求得透视不正确的重心坐标。
Eigen::Vector3f tmpBC = BarycentricCoor(x + 0.5f, y + 0.5f, v);
// pos[3] 存储的是 ViewSpace 中的 1/z。
float a = tmpBC[0] * v[0].pos[3];
float b = tmpBC[1] * v[1].pos[3];
float c = tmpBC[2] * v[2].pos[3];
float div = 1.f / (a + b + c);
a *= div;
b *= div;
c *= div;
// 重心坐标插值。
float z = Interpolate(a, b, c, v[0].pos[2], v[1].pos[2], v[2].pos[2]);

切线空间与法线贴图

推导:CSDNLearnOpenGLShaderX
记录一点自己的理解,矩阵乘法可以看做坐标系的转换,例如 M * a 便是将定义在 M 坐标系下的 a 向量转换至 定义了 M 坐标系的坐标系 下。
在这个问题中,M 便是当前片元所在的切线空间在世界坐标系下的表示(就是 TBN 矩阵),a 便是法线贴图中的向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Eigen::Vector2f dUV1 = v[1].uv - v[0].uv;
Eigen::Vector2f dUV2 = v[2].uv - v[0].uv;
float inverse = 1.f / (dUV1[0] * dUV2[1] - dUV2[0] * dUV1[1]);

Eigen::Vector3f e1 = v[1].viewPos - v[0].viewPos;
Eigen::Vector3f e2 = v[2].viewPos - v[0].viewPos;

Eigen::Vector3f T(dUV2[1] * e1[0] - dUV1[1] * e2[0],
dUV2[1] * e1[1] - dUV1[1] * e2[1],
dUV2[1] * e1[2] - dUV1[1] * e2[2]);
Eigen::Vector3f B(dUV1[0] * e2[0] - dUV2[0] * e1[0],
dUV1[0] * e2[1] - dUV2[0] * e1[1],
dUV1[0] * e2[2] - dUV2[0] * e1[2]);
B *= inverse;
T *= inverse;
T.normalize();
B.normalize();

Eigen::Matrix3f TBN;
TBN << T[0], B[0], normal[0],
T[1], B[1], normal[1],
T[2], B[2], normal[2];
normal = (TBN * model->normalMap(uv)).normalized();

法向量修复

LearnOpenGL 评论区
法向量乘以 model 矩阵左上角 3x3 矩阵的逆的转置。