CPU-Software-Renderer/Knowledge.md

373 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Step 1画一个三角形光栅化
### 布雷森汉姆直线算法(bresenham's line algorithm)
- 有两点 $V_0(x_0, y_0)$ 和 $V_1(x_1, y_1)$
- 定义决策变量 $p_k$ 并令 $p_0 = 2 \times dy - dx$,其中 $dy = y_1 - y_0$$dx = x_1 - x_0$
- 当 $p_k < 0$ $p_{k+1}=p_k+2\times dy$取下方像素点$y_{k+1}=y_k$
- $p_k \geq 0$ $p_{k+1}=p_k+2\times dy-2\times dx$取上方像素点$y_{k+1}=y_k+1$
```C++
void DrawLine(Vector2 v0, Vector2 v1, Color color)
{
int32_t dx = v1.x - v0.x;
int32_t dy = v1.y - v0.y;
int32_t p = 2 * dy - dx;
int32_t y = v0.y;
for (int x = v0.x; x <= v1.x; x++)
{
frameBuffer[x][y] = color;
if (p >= 0)
{
p -= 2 * dx;
y++;
}
p += 2 * dy;
}
}
```
### 包围盒:
- 对于一个三角形 ABC
- 取出其每个顶点的坐标的最大值和最小值得到 $V_0(x_{min},y_{min})$ 和 $V_1(x_{max},y_{max})$
- 这两个点所包起来的矩形区域就是三角形的包围盒
```C++
std::vector<Vector2> Bound(Vector2 v1, Vector2 v2, Vector2 v3)
{
return {
Vector2(std::min({v1.x, v2.x, v3.x}), std::min({v1.y, v2.y, v3.y})),
Vector2(std::max({v1.x, v2.x, v3.x}), std::max({v1.y, v2.y, v3.y}))
};
}
```
### 叉乘法判断点是否在三角形内:
- 三角形 ABC 有 3 条边向量 $\overrightarrow{AB}$、$\overrightarrow{BC}$、$\overrightarrow{CA}$
- 对于点 $P(x,y)$,与每个顶点结合可得到 3 条向量 $\overrightarrow{PA}$、$\overrightarrow{PB}$、$\overrightarrow{PC}$
- 对于每一组向量(例如 $\overrightarrow{AB}$ 与 $\overrightarrow{PA}$)做叉乘的结果的 $Z$ 轴分量如果正负性相同,则表示这个点在三角形里
- 计算 $Z$ 轴分量可以简单表述为 $AB_x \times PA_y - AB_y \times PA_y$
```C++
int32_t Cross(Vector2 v1, Vector2 v2, Vector2 p)
{
int64_t x1 = p2.x - p1.x;
int64_t y1 = p2.y - p1.y;
int64_t x2 = p3.x - p1.x;
int64_t y2 = p3.y - p1.y;
return x1 * y2 - y1 * x2;
}
bool IsInTriangle(std::vector<Vector2> triangle, Vector2 point)
{
int64_t c0 = cross(triangle[0], triangle[1], point);
int64_t c1 = cross(triangle[1], triangle[2], point);
int64_t c2 = cross(triangle[2], triangle[0], point);
bool hasNeg = (c0 < 0) || (c1 < 0) || (c2 < 0);
bool hasPos = (c0 > 0) || (c1 > 0) || (c2 > 0);
return !(hasNeg && hasPos);
}
```
---
## Step 2MVP 变换3D → 2D
### Model 变换
Model 变换用于将模型从其自身的坐标空间变换到世界坐标空间,一般来说这个过程是被省略的,因为一般在定义模型时都会再定义一个位置和旋转,这里的定义就已经完成了 Model 变换的操作。
### View 变换
View 变换用于将世界空间里的各个顶点坐标变换到相机坐标系,也可以说是我们规定了一个摄像机需要处于的位置(一般右手系定义目标位置是 $(0,0,0)$,目标朝向是 $(0,0,-1)$,也就是在世界原点看向 $-z$ 方向),但目前摄像机没在那个位置,所以我们就需要对摄像机做 View 变换,把它变换到那个属于它的位置,同时又要保持所有的物体相对不变。具体到变换矩阵上就是先平移回目标点,再旋转到目标方向。(**特别注意,右手系与左手系 View 矩阵结果不一致,这里是右手系下的结果**
$$M_{View}=M_{rotation}^{-1} \times M_{translation}^{-1}=\begin{bmatrix} right.x & right.y & right.z & -dot(right, position) \\ up.x & up.y & up.z & -dot(up, position) \\ back.x & back.y & back.z & -dot(back, position) \\ 0 & 0 & 0 & 1\end{bmatrix}$$
其中:
- $M_{translation}^{-1}$ 是由目标位置平移到当前摄像机位置的平移变换矩阵
- $M_{rotation}^{-1}$ 是由目标方向旋转到当前摄像机朝向的旋转变换矩阵
### Projection 变换
Projection 变换用于将摄像头视锥里的各个模型投影到一个平面上,这里分为**正交投影**与**透视投影**,透视投影会产生近小远大的效果,而正交投影不会。
核心矩阵:
```text
Model
View
Projection
```
流程:
```text
model space
world space
view space
clip space
screen space
```
公式:
```text
screen_pos = MVP * vertex
```
这一步完成后:
你可以画一个 **旋转的立方体**
---
## Step 3深度测试Z-buffer
解决遮挡问题。
没有深度测试会出现:
```text
后面的三角形覆盖前面的
```
实现:
```text
depth_buffer[x][y]
```
核心逻辑:
```text
if z < depth_buffer
更新像素
```
这一步完成后:
**遮挡关系正确。**
---
## Step 4重心插值属性插值
你已经有:
```text
α β γ
```
现在可以插值:
```text
颜色
深度
法线
纹理坐标
```
例如:
```text
color = α*c1 + β*c2 + γ*c3
```
这是 **GPU 的核心思想之一**
---
## Step 5Blinn-Phong 光照
实现经典光照模型:
公式:
```text
I = kaIa + kd(L·N) + ks(H·N)^n
```
组成:
```text
环境光 ambient
漫反射 diffuse
高光 specular
```
Blinn-Phong 用:
```text
H = normalize(L + V)
```
优点:
* 比 Phong 更高效
完成后:
你的模型会 **有光照效果**
---
## Step 6加载 OBJ 模型
支持真实模型。
实现:
```text
读取 obj 文件
解析:
v 顶点
vn 法线
f 面
```
然后:
```text
三角形 → renderer
```
推荐模型:
```text
teapot
bunny
```
这一步完成后:
你的 renderer 已经像一个 **小型渲染器**
---
## Step 7相机控制
实现一个简单 camera。
例如:
```text
鼠标旋转
WASD移动
```
核心:
```text
view matrix
```
这一步完成后:
renderer 就有 **交互能力**
---
## 完成后的项目结构
最终 renderer 大概有这些模块:
```text
Renderer
├─ Rasterizer
├─ Shader
├─ Camera
├─ Math (Vec/Matrix)
├─ OBJLoader
└─ ZBuffer
```
核心代码量:
```text
1500 - 3000 行
```
就已经很不错。
---
## 简历怎么写(重点)
千万不要写:
```text
实现了 Blinn-Phong 光照
```
要写成:
```text
实现一个 CPU Software Renderer
支持 MVP 变换、三角形光栅化、Z-buffer 深度测试,
并实现基于 Blinn-Phong 的光照模型。
```
如果再加一句:
```text
支持 OBJ 模型加载和相机交互
```
这个项目 **含金量就很高了**
---
## 一个非常重要的小技巧
很多人做 renderer 只做到:
```text
作业级
```
但如果你再加 **一个小扩展**
例如:
```text
MSAA 抗锯齿
```
或者:
```text
Texture Mapping
```
项目立刻就变成:
```text
进阶图形项目
```
面试官印象会非常好。
---
## 最后给你一个真实评价
如果你完成这个 renderer
你的图形学水平大概是:
```text
超过 90% 游戏客户端校招生
```
因为大部分人只:
```text
看课程
```
而没有 **实现 pipeline**
---
如果你愿意,我可以再给你一份 **“Renderer 项目 GitHub 结构模板”**(很多人照这个结构写,很容易做成一个漂亮的开源项目)。