光栅化:对几何阶段传递过来的屏幕空间的顶点信息进行离散化处理
- 屏幕坐标
- 法向量
- 顶点颜色
- 纹理坐标
最终生成屏幕像素,渲染出图像。
相机坐标里的三角形经过透视变换,投影到屏幕坐标
主要问题(Visibility):
- 确定像素被哪些投影三角形覆盖
- 计算像素的最终颜色
这阶段的算法结果输出到FrameBuffer
Visibility Problem
光栅化算法
- 把一个空间三角形投影到屏幕坐标
v
′
.
x
=
n
∗
v
.
x
/
v
.
z
v
′
.
y
=
n
∗
v
.
y
/
v
.
z
v
′
.
z
=
v
.
z
v'.x=n*v.x/v.z\\ v'.y=n*v.y/v.z\\ v'.z=v.z
v′.x=n∗v.x/v.zv′.y=n∗v.y/v.zv′.z=v.z(简化为直接保留z)
2. 思路:对屏幕上每个像素,检查是否位于投影三角形内
考虑多个三角形有重叠的情形:Z-Buffer算法
- 维护FrameBuffer大小的二维数组Z-Buffer
- 对于被投影三角形覆盖的像素点,只更新离相机更近的点
光线投射:
- 对于屏幕每个像素生成一条射线
- 对场景里每个三角形,查找和射线相交的最近的三角形
Triangle Setup 三角形设置
已知图元顶点颜色和别的信息
- 组装图元(点,线,三角形)
- 需要计算三角形边方程(slope)
GPU内部采用三角形网格
- 任意多边形都可以拆分成三角形
- 三点共面
- 容易判断点在三角形内
- 三角形内容易插值
- 纹理映射会有问题
Triangle Traversal 三角形遍历
遍历三角形图元,检查覆盖的像素点
反锯齿(Anti-Alias)
光栅化算法
点光栅化
- 最近点:映射到距离最近的像素点,但动画时会跳动
- 取整点
- 取整点,保留4bit,看做Sub pixel。移动时计算保留位再取整
直线光栅化
DDA数值微分算法:
- y = k x + b y=kx+b y=kx+b
- 当 ∣ k ∣ < 1 |k|<1 ∣k∣<1时,从起点开始画起,每次 x ′ = x + 1 , y ′ = y + k x'=x+1, y'=y+k x′=x+1,y′=y+k,并将 y y y四舍五入,得到新的 x x x, y y y就是像素点应该画的地方。
- 当 ∣ k ∣ > 1 |k|>1 ∣k∣>1时,从起点开始画起,每次 y = y + 1 , x = x + 1 / k y=y+1, x=x+1/k y=y+1,x=x+1/k,并将 x x x四舍五入,得到新的 x x x, y y y就是像素点应该画的地方
缺点:每个点都需要浮点加
Bresenham中点画线算法:
- v 0 ( x 0 , y 0 ) → v 1 ( x 1 , y 1 ) v_0(x_0,y_0) \to v_1(x_1,y_1) v0(x0,y0)→v1(x1,y1)和斜率 k = y 1 − y 0 x 1 − x 0 < 1 k=\frac{y_1-y_0}{x_1-x_0}<1 k=x1−x0y1−y0<1
- 格点
x
x
x每次增加1,格点
y
y
y有可能增加1
高效实现(化为整数计算) - 线方程 f ( x , y ) = ( x 1 − x 0 ) y − ( y 1 − y 0 ) x + b ′ f(x,y)=(x_1-x_0)y-(y_1-y_0)x+b' f(x,y)=(x1−x0)y−(y1−y0)x+b′
- 判断
d
e
l
t
a
=
f
(
x
+
1
,
y
+
0.5
)
<
0
delta=f(x+1,y+0.5)<0
delta=f(x+1,y+0.5)<0
- 初始值 f ( x 0 + 1 , y 0 + 0.5 ) f(x_0+1,y_0+0.5) f(x0+1,y0+0.5)
- 情况一:
d
e
l
t
a
<
0
delta<0
delta<0
- y = y + 1 y=y+1 y=y+1
- d e l t a ′ = f ( x + 2 , y + 1.5 ) = d e l t a + ( x 1 − x 0 ) − ( y 1 − y 0 ) delta'=f(x+2,y+1.5)=delta+(x_1-x_0)-(y_1-y_0) delta′=f(x+2,y+1.5)=delta+(x1−x0)−(y1−y0)
- 情况二:
d
e
l
t
a
≥
0
delta\geq 0
delta≥0
-
d
e
l
t
a
′
=
f
(
x
+
2
,
y
+
0.5
)
=
d
e
l
t
a
−
(
y
1
−
y
0
)
delta'=f(x+2,y+0.5)=delta-(y_1-y_0)
delta′=f(x+2,y+0.5)=delta−(y1−y0)
-
d
e
l
t
a
′
=
f
(
x
+
2
,
y
+
0.5
)
=
d
e
l
t
a
−
(
y
1
−
y
0
)
delta'=f(x+2,y+0.5)=delta-(y_1-y_0)
delta′=f(x+2,y+0.5)=delta−(y1−y0)
曲线光栅化
圆光栅化
只需要计算八分之一圆弧,其余可对称
M点在圆弧外,则y-1,否则y不变
高效实现(化为整数计算)
圆方程:
f
(
x
,
y
)
=
y
2
+
x
2
−
r
2
f(x,y)=y^2+x^2-r^2
f(x,y)=y2+x2−r2
- 判断
d
e
l
t
a
=
f
(
x
+
1
,
y
−
0.5
)
≥
0
delta=f(x+1,y-0.5)\ge 0
delta=f(x+1,y−0.5)≥0
- 初始值 f ( 1 , r − 0.5 ) = 1.25 − r f(1,r-0.5)=1.25-r f(1,r−0.5)=1.25−r
- 情况一:
d
e
l
t
a
<
0
delta<0
delta<0
- d e l t a ′ = f ( x + 2 , y − 0.5 ) = d e l t a + 2 x + 3 delta'=f(x+2,y-0.5)=delta+2x+3 delta′=f(x+2,y−0.5)=delta+2x+3
- 情况二:
d
e
l
t
a
≥
0
delta\ge 0
delta≥0:
- y = y − 1 y=y-1 y=y−1
- d e l t a ′ = f ( x + 2 , y − 1.5 ) = d e l t a + 2 ( x − y ) + 5 delta'=f(x+2,y-1.5)=delta+2(x-y)+5 delta′=f(x+2,y−1.5)=delta+2(x−y)+5
插值算法
如何判断像素点是否在三角形图元内:
-
扫描线
覆盖范围内每行的最左、最右边界点
-
根据像素的中心位置,检查是否在三角形内
- 只看三角形的Bounding Box内的像素点
- 只画三角形的左边和上边
设计Edge Function
- 测试点 P 在线 V 0 V 1 的左(右)侧 测试点P在线V_0V_1的左(右)侧 测试点P在线V0V1的左(右)侧
- ∣ ∣ V 0 P → × V 0 V 1 → ∣ ∣ = ∣ ∣ V 0 P → ∣ ∣ ] ⋅ ∣ ∣ V 0 V 1 → ∣ ∣ ⋅ s i n θ ||\overrightarrow{V_0P}\times \overrightarrow{V_0V_1}||=||\overrightarrow{V_0P}||]\cdot||\overrightarrow{V_0V_1}||\cdot sin\theta ∣∣V0P×V0V1∣∣=∣∣V0P∣∣]⋅∣∣V0V1∣∣⋅sinθ
- 不考虑Z轴
(向量叉积的正负可以判断向量夹角是顺时针还是逆时针,也可以表示向量构成的平行四边形的面积)
- 按顺时针对三角形边测试,只有内部的点满足三次测试都是+
片元(像素)着色
- 所属图元的顶点信息
- 通过插值计算光照(阴影、明暗…)
- 纹理采样
重心坐标
根据三角形面积决定插值的参数
Edge Function
- 判断点是否在三角形内部只需要计算正负
- 可以计算面积,返回值是向量构成的三角形面积2倍
邻接三角形共同边容易被渲染两次
避免边界点渲染两次:
- 遵循Top-Left规则,只渲染左边和上边
遮挡剔除算法
渲染大量三角形图元,会有很多遮挡可能
- 靠近摄像机的物体被渲染
- 剔除被挡住的物体
- 视角变动后,遮挡关系变化
- GPU里通过Z-Buffer实现
Hidden Surface Removal算法
光纤投射 RayCasting
从每一个像素射出一条射线,找到最接近的物体
画家算法
先画后面的物体,再画前面的物体
无法处理:
沃诺克算法
- 分治法:四分屏幕空间
- 细分的子空间只存在简单的前后关系
- 使用画家算法
- 在曲面和抗锯齿中很有用
BSP Tree
分治法:用超平面递归细分空间
在每个存在前后关系的子空间里使用画家算法
可以处理带透明度的遮挡
不用每个像素点都计算Z并检测
Z-Buffer算法
从像素的细度考虑前后关系
- 创建和FrameBuffer大小一样的二维数组
- 每个元素代表对应像素的已知深度数据
- 每遍历一个三角形
- 获取覆盖像素的深度,比较Z-Buffer里对应的已知深度
- 如果小于,则同时更新深度值和颜色值
深度插值
正确的插值公式是:
Z
=
1
α
Z
0
+
β
Z
1
+
γ
Z
2
Z=\frac{1}{\frac{\alpha}{Z_0}+\frac{\beta}{Z_1}+\frac{\gamma}{Z_2}}
Z=Z0α+Z1β+Z2γ1
推导:
相当于对1/z插值
对投影三角形正确插值,需要透视纠正:重心坐标再除以对应的z值
z插值:
1
z
=
α
1
z
0
+
β
1
z
1
+
γ
1
z
2
\frac{1}{z}=\alpha\frac{1}{z_0}+\beta\frac{1}{z_1}+\gamma\frac{1}{z_2}
z1=αz01+βz11+γz21
别的属性插值,比如颜色、纹理坐标:
c
z
=
α
c
0
z
0
+
β
c
1
z
1
+
γ
c
2
z
2
\frac{c}{z}=\alpha\frac{c_0}{z_0}+\beta\frac{c_1}{z_1}+\gamma\frac{c_2}{z_2}
zc=αz0c0+βz1c1+γz2c2
Z-Buffer in OpenGL
- Z-Buffer初始值设为极大值
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT) - 绘制屏幕三角形时
- 比较覆盖像素的深度值和Z-Buffer中对应的深度值
- 如果新像素的深度值更小,则更新为新像素的颜色
- 使用Z-Buffer:
- 先激活深度测试
glutInitDisplayMode(GLUT_DEPTH | …);
glEnable(GL_DEPTH_TEST); - 每次绘制窗口时,先清理Z-Buffer数据
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
逐片元操作
- 片元(像素)可见性
- 深度测试Depth Test
- 透明度测试Alpha Test
- 模板测试Stencil Test
- 混合(Blending)
- 测试通过的片元和FrameBuffer里的值混合
- 最后通过FrameBuffer输出到屏幕
锯齿形边界Anti-Alias
采样:采样前先做模糊(或滤波)
采样
采样产生的问题:
-
锯齿问题(Jaggies)- sampling in space
-
摩尔纹问题(Moire)- 规律性细节采样不够
-
车轮效应 - 采样频率低
超采样Super Sampling
每个像素点细分为m*m像素点。以2*2为例
最后取每个像素块内所有像素点颜色均值
多重采样Multi-Sampling
- 避免每个采样点都计算颜色
- 颜色计算只按照中心点来
- 覆盖两个点则相当于50%
Multi-Sample Antialiasing in OpenGL
多重采样抗锯齿(MSAA)
- 设置glut
glutInitDisplayMode(GLUT_DOUBLE | GLUT_MULTISAMPLE) - 设置多采样
//Set multisampling
glHint(GL_MULTISAMPLE_FILTER_HINT_NV, GL_NICEST);
glEnable(GL_MULTISAMPLE);
Anti-Alias in OpenGL
- 激活混合
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - 设置反锯齿
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glEnable(GL_LINE_SMOOTH);