回顾Games101 chapter 1 - 6
前言
本文只写回顾后重新加深认识的知识
透视除法的意义
经过MVP矩阵之后,将模型空间下某点的坐标,转换成了裁剪空间下的坐标,此时因为裁剪空间的范围是x∈[−W/2,W/2]和y∈[−H/2,H/2],所以经过以下两个变换,其中除以pz就是透视除法
透视矫正意义和做法:https://stackoverflow.com/questions/24441631/how-exactly-does-opengl-do-perspectively-correct-linear-interpolation
一:
−1≤2⋅(pxpz⋅near)w≤1−1≤2⋅(pypz⋅near)h≤1
二:
[xyzw]⎡⎢
⎢
⎢⎣100001000010ΔxΔyΔz1⎤⎥
⎥
⎥⎦=[x+Δx∗wy+Δy∗wz+Δz∗ww]
只有当W=1,这个三维坐标转换是等价的,才能保证位移的量是正确的,W=0时,则没有位移
只有当W=1时,三维坐标点转换成四维齐次坐标点才是等价的
坐标系变换和矩阵推导
坐标系变换理解不直观,倾向于101中闫老师所说的理解坐标系的转换通过矩阵进行的线性变换,将A坐标系下的点P,乘上矩阵得出B坐标系下的点P',以下是抛开常见的变换(如透视投影变换、正交投影变换等)如何得出变换矩阵M,通过矩阵变换(下文着重说明)
已知坐标系A和坐标系B
坐标系B的x,y,z轴在坐标系A下可表示为(ux,uy,vz,0)
(vx,vy,vz,0),(wx,wy,wz,0),坐标系B的原点在坐标系A下表示为(Qx,Qy,Qz,1)

则将坐标系B中一点P从坐标系B变换到坐标系A的变换矩阵为:(注意此处的例子是将源坐标系A变换到目标坐标系B下)
M=⎡⎢
⎢
⎢
⎢⎣uxuyuz0vxvyvz0wxwywz0QxQyQz1⎤⎥
⎥
⎥
⎥⎦
如之前所说,变换过程中点p在空间中的绝对位置没有发生改变,只是参考坐标系发生了改变,从B坐标系变到A坐标系。(缩放,旋转,平移变换只有在同一坐标系下才有意义)
矩阵变换是基于基向量组的结果
- 矩阵变换之于同一个坐标系,可以理解为坐标系不变,点的位置改变
- 矩阵变换之于不同坐标系,可以理解为点的绝对位置不变,坐标系改变
[x′y′]=B[xy]⇒[xy]=B−1[x′y′],B=[→b1→b2],且→b1,→b2是坐标系B的基向量
其中,矩阵B的各个列向量分别对应B坐标系的各个基向量,[xy]是向量−−→OP或者说点P在B坐标系的表示,[x′y′]则是向量−−→OP或者点P在A坐标系中的表示

以图中的两个向量→b1,→b2为基确定一个坐标系B,显然在B坐标系中−→b1B=[10],−→b2B=[01],接下来,将→b1,→b2定位到A坐标系中,得到−→b1A=[21],−→b2A=[−11]
∵−−→OP=2→b1+2→b2
∴−−→OP在B坐标系中的表示为[22],现在,将−−→OP用A坐标系描叙:
−−→OP=2→b1+2→b2=2−→b1A+2−→b2A=[−→b1A−→b2A][22]=[24]
现在,令矩阵B=[−→b1A−→b2A],P点是用B坐标系表示的任意一点(x,y)。
于是−−→OP在A坐标系中的表示[x′y′]=B[xy],显然,B是可逆的,于是就有了之前的结论
那么在这个例子当中,当我们需要知道某点在转换坐标系后的新坐标时,通过该例子也可以加深印象,比如在B坐标系下有点Q(3,4),即−−→OQ=(3,4),跟据刚才的例子可以看出它转换在A坐标系下的点
−−→OQ=2→b1+2→b2=2−→b1A+2−→b2A=[−→b1A−→b2A][34]=[2−111][54]=[69]
即转换到A坐标系下的点Q′的坐标为Q′(6,9)
虽然这里的讨论是基于二维的,但是,结论可以扩展到任意维度
阐述结论:
将B坐标系的基向量定位到A坐标系,然后将定位之后的基向量作为矩阵B的列向量,用矩阵B对B坐标系中的点P的坐标进行矩阵变换,将得到点P在A坐标系中的坐标。这个过程,就是从坐标系B到坐标系A的一个追溯过程
先将相机移到原点,然后进行分别对坐标轴进行旋转,用矩阵表示则是Mview=RviewTview
Tview=⎡⎢
⎢
⎢⎣100−xe010−ye001−ze0001⎤⎥
⎥
⎥⎦
- Rotategto−Z,ttoY,(g×t)ToX
g是相机看的方向(lookAt),t是相机向上的方向(Up),也就是相机的-Z轴和Y轴,两个向量叉积就是另一个坐标轴
R−1view=⎡⎢
⎢
⎢
⎢
⎢⎣xˆg׈txtx−g0yˆg׈tyty−g0zˆg׈tztz−g00001⎤⎥
⎥
⎥
⎥
⎥⎦
旋转矩阵是正交矩阵,所以旋转矩阵的逆就是旋转矩阵的转置
Rview=⎡⎢
⎢
⎢
⎢⎣xˆg׈tyˆg׈tzˆg׈t0xtytyt0x−gy−gz−g00001⎤⎥
⎥
⎥
⎥⎦
正交投影矩阵
无论是正交投影还是透视投影,都是要将x、y、z移到-1到1的范围内,先将中心点移到原点,然后缩放
Mortho=⎛⎜
⎜
⎜
⎜
⎜
⎜⎝2r−l00002t−b00002n−f00001⎞⎟
⎟
⎟
⎟
⎟
⎟⎠⎛⎜
⎜
⎜
⎜
⎜
⎜⎝100−r+l2010−t+b2001−n+f20001⎞⎟
⎟
⎟
⎟
⎟
⎟⎠
透视投影矩阵推导
首先先将frustum 转变为cuboid(n -> n,f -> f)(Mpersp−>ortho)
然后再做正交投影
整个投影变换包括两部分
- v = P(矩阵)*p
- v=vvw=vpz透视除法


以上大概推出等式这一步,接下来用公式展示更为直观
⎛⎜
⎜
⎜⎝m00m01m02m03m10m11m12m13m20m21m22m23m30m31m32m33⎞⎟
⎟
⎟⎠⎛⎜
⎜
⎜⎝xyz1⎞⎟
⎟
⎟⎠=⎛⎜
⎜
⎜
⎜
⎜
⎜
⎜⎝xz∗aspect∗tan(fov2)yz∗tan(fov2)z‘′1⎞⎟
⎟
⎟
⎟
⎟
⎟
⎟⎠
m00∗x+m01∗y+m02∗z+m03=xz∗aspect∗tan(fov2)
将右边的四维列向量表示的坐标每一项乘以z,所以有
⎛⎜
⎜
⎜⎝m00m01m02m03m10m11m12m13m20m21m22m23m30m31m32m33⎞⎟
⎟
⎟⎠∗⎛⎜
⎜
⎜⎝xyz1⎞⎟
⎟
⎟⎠=⎛⎜
⎜
⎜
⎜
⎜
⎜
⎜⎝xaspect∗tan(fov2)ytan(fov2)z∗z′′z⎞⎟
⎟
⎟
⎟
⎟
⎟
⎟⎠
所以求得矩阵为
⎛⎜
⎜
⎜
⎜
⎜
⎜
⎜⎝1aspect∗tan(fov2)00001tan(fov2)0000m22m230010⎞⎟
⎟
⎟
⎟
⎟
⎟
⎟⎠
m22∗z+m23=z∗z′′⇒m22+m23z=z′′
因为z=zNear时,z''=-1;z=zFar时,z''=1所以有以下等式
m22+m23zNear=−1m22+m23zFar=1
联立求得:
m22=−zFar−zNearzNear−zFarm23=2∗zFar∗zNearzNear−zFar
最后求得投影矩阵为
⎛⎜
⎜
⎜
⎜
⎜
⎜
⎜
⎜⎝1aspect∗tan(fov2)00001tan(fov2)0000−zFar−zNearzNear−zFar2∗zNear∗zFarzNear−zFar0010⎞⎟
⎟
⎟
⎟
⎟
⎟
⎟
⎟⎠
将这样得矩阵乘以视锥体内的一个顶点坐标,得到一个新的向量,再将这个向量的每个分量除以第四个分量(此步骤也被称为透视除法)(w),这样就可以得到顶点映射到规则立方观察体后的新的坐标
注意:z坐标的映射方式的获得,最后我们是为了方便矩阵乘法的操作方向求得了z坐标与cvv中的z坐标的映射方式:
m22+m23z=z′′
此时的映射并不是线性的,当z越大时,z的变化对z''的扰动越小
Canonical Cube to Screen
- Irrelevant to z
- Transform in xy plane : [-1, 1] to [0, width] × [0, height]
- Viewport transform matrix:
视口矩阵
Mviewport=⎡⎢
⎢
⎢
⎢
⎢⎣width200width20height20height200100001⎤⎥
⎥
⎥
⎥
⎥⎦
深度z的计算
前言
3D光栅化发生在图元被变换到Screen space之后,因为这里的Screen space与2D的Screen Space完全一致,所以2D的光栅化算法在这里依然适用。
然而由于图元经过了投影变换,且投影变换为非线性变换,所以不能用简单的线性插值获取fragment的属性

投影变换不会保持相对距离不变性
如上图所示,view space中的线段v0v1上两点
p0(p0x,p0y,p0z,1),
p1(p1x,p1y,p1z,1)在near plane上的投影为点
s0(s0x,s0y),
s1(s1x,s1y)。
p0,
p1中间一点
v(vx,vy,vz,1)在near plane上的投影为点
q(qx,qy)。从图中可以看出点v到p0,p1的距离比值与点q到s0,s1的距离比值完全不同,投影变换不保持距离不变。
为了执行z-buffer算法,需要通过点q获取到v的深度值(z)
点v的深度值可以通过如下方法插值得到:
vz=1cp1z+(1−c)p0z
以下是推导的过程:
手写版:

文字版:
由于点q为点v在near plane上的投影,因此点q与点v的关系为:
- qx=vx⋅nearvz
且v位于p0p1之间,则
- vz=p0z+t⋅(p1z−p0z)=vx⋅nearqx
由点v在p0,p1之间,点q在s0,s1之间则有
- vx=p0x⋅(1−t)+p1x⋅t=p0x+t⋅(p1x−p0x)
- qx=s0x⋅(1−c)+s1x⋅c=s0x+c⋅(s1x−s0x)
代入式(1)可得
vz=vx⋅nearqx=(p0x+t⋅(p1x−p0x))⋅nears0x+c⋅(s1x−s0x)式(2)
又s0和s1分别为p0和p1在near plane上的投影,则:
- s0x=p0x⋅nearp1z
- s1x=p1x⋅nearp1z
代入式(2)可得:
vz=(p0x⋅s0xnear+t⋅(p1x⋅s1xnear−p0x⋅s0xnear))⋅nears0x+c⋅(s1x−s0x)
vz=(p0x⋅s0xnear+t⋅(p1x⋅s1xnear−p0x⋅s0xnear))⋅nears0x+c⋅(s1x−s0x)vz=(p0x⋅s0x+t⋅(p1x⋅s1x−p0x⋅s0x))s0x+c⋅(s1x−s0x)p0z+t⋅(p1z−p0z)=(p0x⋅s0x+t⋅(p1x⋅s1x−p0x⋅s0x))s0x+c⋅(s1x−s0x)(p0z+t⋅(p1z−p0z))⋅(s0x+c⋅(s1x−s0x))=p0x⋅s0x+t⋅(p1x⋅s1x−p0x⋅s0x)p0z⋅s0x+p0z⋅c⋅(s1x−s0x)+t⋅(p1z−p0z)⋅s0x+t⋅c⋅(p1z−p0z)⋅(s1x−s0x)=p0x⋅s0x+t⋅(p1x⋅s1x−p0x⋅s0x)
化简得:
t⋅(p1z−c⋅(p1z−p0z))=c⋅p0z
则:
t=c⋅p0zc⋅p0z+(1−c)⋅p1z
代入式(1)可得
vz=p0z+t⋅(p1z−p0z)
vz=p0z+c⋅p0zc⋅p0z+(1−c)⋅p1z⋅(p1z−p0z)
vz=1cp1z+(1−c)p0z
若View Space中三角形v0v1v2,变换到Screen Space后为三角形s0s1s2,v0v1v2内一点v在Screen Space的投影点s0s1s2内的点q,对三角形s0s1s2内的点(fragment)q,可以通过如下方法取得fragmentq在View Space中对应的深度值:
q.z=v.z=1λ0v0.z+λ1v1.z+λ2v2.z
λ0,λ1,λ2为点p在三角形s0s1s2内的重心坐标
引入结论:
对Screen Space三角形s0,s1,s2内一点p的任意属性插值的公式为:
Atribute(p)=z⋅(λ0⋅Atribute(v0)z0+λ1⋅Atribute(v1)z1+λ2⋅Atribute(v2)z2)
λ0,λ1,λ2为点p的重心坐标,z0,z1,z2,z分别为s0,s1,s2,p在view space中对应点的深度值,可以用这个方法插值得到p在NDC Space内对应点的深度值
此处贴一下Games101作业框架中关于深度的计算,与上述公式对应z=zinterpolated∗wreciprocal
auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated =
alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
if (depth_buf[get_index(x, y)] > z_interpolated) {
depth_buf[get_index(x, y)] = z_interpolated;
Eigen::Vector3f point;
point << static_cast<float>(x), static_cast<float>(y), z_interpolated;
set_pixel(point, t.getColor());
}
罗德里格斯旋转公式
指定任意轴k旋转α角得出旋转矩阵
字写得不好,在爬了...
手写版:

文字版:
首先先将→k处理成单位向量,这点很重要,关乎着下一步等式是否成立,有些博文写这里不需要处理单位向量,这是错的
→v⋅→k=|→v|⋅|→k|⋅cos<→v,→k>=|→v|⋅cos<→v,→k>
可得
→v||=|→v|⋅cos<→v,→k>⋅→k→v=−→v⊥+→v||−→v⊥=→v−→v||=→v−(→v⋅→k)→k
绕→k做旋转时,向下做垂线,可看作底部经过了类似半圆的旋转
要求得−−→vrot=→v||+−−−→vrot⊥,将−−−→vrot⊥作正交分解有−−−→vrot⊥=→a+→b,易得|→w|=|−→v⊥|,则有→w=→k×−→v⊥=→k×[→v−→v||]=→k×→v−→k×→v||=→k×→v−0=→k×→v
接下来求→a和→b
|→a|=|−−−→vrot⊥|⋅cos(θ−90)=|−−−→vrot⊥|⋅sin(θ)→a=→w|→w|⋅|→a|=→w|−−−→vrot⊥|⋅|−−−→vrot⊥|⋅sin(θ)=→w⋅sin(θ)
|→b|=|−−−→vrot⊥|⋅cos(180−θ)=|−−−→vrot⊥|⋅cos(θ)→b=−→v⊥|−→v⊥|⋅|→b|=−→v⊥|−→v⊥|⋅|−−−→vrot⊥|⋅cos(θ)=−→v⊥⋅cos(θ)注意|−→v⊥|=|−−−→vrot⊥|
−−−→vrot⊥=→a+→b=→w⋅sin(θ)+−→v⊥⋅cos(θ)=sin(θ)⋅(→k×→v)+cos(θ)(→v−(→v⋅→k)→k)−−→vrot=→v||+−−−→vrot⊥=(→v⋅→k)→k+sin(θ)⋅(→k×→v)+cos(θ)(→v−(→v⋅→k)→k)=cos(θ)→v+(1−cos(θ)(→v⋅→k)→k)+sin(θ)⋅(→k×→v)
把→k和→v分别写为列向量
→k=⎛⎜⎝kxkykz⎞⎟⎠
→v=⎛⎜⎝vxvyvz⎞⎟⎠
令−−→vrot=R⋅→v
两个式子
(→v⋅→k)→k=→k(→v⋅→k)=→k(−→kT⋅→v)
→k×→v=⎡⎢⎣kyvz−kzvykzvx−kxvzkxvy−kyvx⎤⎥⎦=⎡⎢⎣0−kzkykz0−kx−kykx0⎤⎥⎦⎡⎢⎣vxvyvz⎤⎥⎦
结合以上两个式子可得,其中I为3×3的单位矩阵
R=Icos(θ)+(1−cos(θ))⎛⎜⎝kxkykz⎞⎟⎠(kxkykz)+sin(θ)⎛⎜⎝0−kzkykz0−kx−kykx0⎞⎟⎠
以下是比较通用的表示方式
R(n,α)=cos(α)I+(1−cos(α))nnT+sin(α)⎛⎜⎝0−nznynz0−nx−nynx0⎞⎟⎠
部分引用的博文
https://blog.csdn.net/unclerunning/article/details/70948696#齐次坐标系与平移
https://zhuanlan.zhihu.com/p/45757899
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步