OpenGL逻辑学快速入门 卷四 空间与变换:坐标系链条的全部因果
卷四 空间与变换坐标系链条的全部因果难度★★☆视角[CPU][GPU]优先级P04.1~4.4, 4.6P14.5P24.7上一卷你看到一行gl_Position u_mvp * vec4(a_pos, 1.0)。这一卷把这一行展开——每个空间为什么存在每次乘法在做什么为什么必须按这个顺序。数学预备30 分钟够用集不是数学课。只补理解后续内容所需的最小集。向量与点向量方向 长度不依附于位置“向北 5 米”点空间位置“经度 116.4纬度 39.9”OpenGL 用 4 维齐次坐标统一表示点(x, y, z,1)向量(x, y, z,0)为什么第 4 维要区分平移变换对点有效对向量无效把方向向北 5 米平移没意义。第 4 维 0 时平移项被自动归零。这就是齐次坐标的核心收益。矩阵 × 向量4×4 矩阵乘 4 维列向量得 4 维列向量| a b c d | | x | | axbyczdw | | e f g h | × | y | | exfygzhw | | i j k l | | z | | ixjykzlw | | m n o p | | w | | mxnyozpw |矩阵 一种线性变换。一个矩阵代表一种把空间所有点统一映射到新位置的规则。矩阵乘法不可交换A·B ≠ B·A。变换的顺序很重要先转再平移 ≠ 先平移再转。复合变换(A·B)·v A·(B·v)——先用 B 变换 v再用 A 变换结果。OpenGL 中读法MVP·v “先 P再 V再 M”不是。是先 M再 V再 P。从右往左读数学约定。理解这个就够了。下面开始正题。4.1 为什么要这么多空间[CPU][图×1]视角[CPU]优先级P06 个空间一图速览┌──────────────┐ │ 模型空间 │ 顶点数据原始坐标以模型自身为原点 └──────┬───────┘ │ × Model 矩阵 ▼ ┌──────────────┐ │ 世界空间 │ 全场景的统一坐标系以世界原点为基准 └──────┬───────┘ │ × View 矩阵 摄像机的逆变换 ▼ ┌──────────────┐ │ 观察空间 │ 以摄像机为原点相机看 -z 方向 └──────┬───────┘ │ × Projection 矩阵 ▼ ┌──────────────┐ │ 裁剪空间 │ 齐次坐标系视锥被映射到 [-w, w]³ 立方体 └──────┬───────┘ │ ÷ w 透视除法硬件自动 ▼ ┌──────────────┐ │ NDC 空间 │ [-1,1]³ 标准立方体 └──────┬───────┘ │ × Viewport 变换 ▼ ┌──────────────┐ │ 屏幕空间 │ 像素坐标 (x,y) 深度 z∈[0,1] └──────────────┘每个空间存在的独特价值空间为什么需要模型空间让美术建模时不用关心模型摆在世界哪里世界空间让多个模型有统一坐标系能算彼此距离、光照观察空间让剔除背面、雾效等计算简单朝向相机的方向是固定的 -z裁剪空间让视锥变成简单立方体硬件秒杀裁剪测试NDC与具体屏幕分辨率解耦的标准化坐标屏幕空间实际像素位置光栅化要的就是它核心思想每多一个空间是为了让接下来某一类计算变简单。这是计算机科学里一个普适原则——用空间换简洁。为什么不能少几个合并模型和世界→ 一个模型摆在场景里 100 次得复制 100 份顶点数据。合并世界和观察→ 移动相机得改动所有顶点。跳过裁剪空间直接到屏幕→ 视锥裁剪成噩梦。每个空间砍掉都会让某个固定操作复杂化。这就是它存在的逻辑必然性。4.2 Model 矩阵[CPU][图×1]视角[CPU]优先级P0三种基本变换平移| 1 0 0 tx | | 0 1 0 ty | | 0 0 1 tz | | 0 0 0 1 |缩放| sx 0 0 0 | | 0 sy 0 0 | | 0 0 sz 0 | | 0 0 0 1 |旋转绕 z 轴 θ 角举例| cos -sin 0 0 | | sin cos 0 0 | | 0 0 1 0 | | 0 0 0 1 |复合顺序为何不可交换要把一个模型先放大 2 倍再绕 y 轴转 30°再平移到 (5, 0, 0)矩阵复合M T(5,0,0) · R_y(30°) · S(2)从右往左读先缩放再旋转再平移。为什么不能换顺序先平移再旋转物体会绕远离原点的位置画大圈 先旋转再平移物体在原地转好后挪到目标位置两种结果完全不同。这不是 OpenGL 的怪癖是矩阵代数本身的性质。经验法则99% 的情况都是T · R · S · v顺序——先缩放再旋转再平移。旋转的三种表示欧拉角pitch / yaw / roll优点直观、好懂、好存3 个数缺点万向锁Gimbal Lock——三个旋转轴在某些角度会重合丢失一个自由度适用相机控制、调试轴角axis angle优点直接对应绕某条轴转某个角度无万向锁缺点插值不自然适用单次旋转的描述四元数优点无万向锁、插值平滑slerp、紧凑4 个数缺点抽象、调试痛苦适用动画、骨骼蒙皮、相机平滑过渡——几乎所有长时间存储 平滑插值的场景结论表达 / 存储用四元数最终交给 GLSL 时转成矩阵。4.3 View 矩阵 摄像机的逆变换[CPU][图×1]视角[CPU]优先级P0等价性证明关键事实OpenGL 没有摄像机这个概念。你看到的相机视角是通过反向移动整个世界伪装出来的。设想相机在 (5, 0, 0) 看向原点。等价于把整个世界向 (-5, 0, 0) 平移然后假装相机在原点。数学上相机自己的世界变换C T·R先 R 再 T 把相机摆到位。看向相机视角 把世界用C⁻¹ R⁻¹·T⁻¹反着动。关键化简旋转矩阵是正交矩阵所以R⁻¹ Rᵀ——无需做矩阵求逆直接转置即可便宜。这就是 LookAt 实现里你看不到inverse()调用的原因。为什么这样设计渲染管线只需要一种原点视角——所有计算深度比较、雾效、光照都假设相机在原点让动态相机等价于反向变换世界——管线不需要新加任何处理View 矩阵 摄像机变换的逆。如果摄像机变换是C T(camera_pos) · R(camera_orientation)“摄像机从原点平移旋转到此处”则View C⁻¹。LookAt 矩阵的推导最常用的相机定义方式相机在哪 (eye)、看向哪 (center)、哪边算上 (up)。构造步骤算相机的本地 z 轴反视线方向因为 OpenGL 相机看 -zf normalize(center - eye) // 视线方向 z_axis -f // 相机 z 轴指向相机背后算本地 x 轴右方向x_axis normalize(cross(f, up))算本地 y 轴真正的上方向避免 up 不正交y_axis cross(x_axis, f) cross(z_axis, x_axis) 的反实际上y_axis cross(-z_axis, x_axis)cross(f, x_axis)摄像机变换C的旋转部分由(x_axis, y_axis, z_axis)三列构成、平移部分是eye。求逆。利用旋转矩阵的逆 转置平移部分变号View | x_axis.x x_axis.y x_axis.z -dot(x_axis, eye) | | y_axis.x y_axis.y y_axis.z -dot(y_axis, eye) | | z_axis.x z_axis.y z_axis.z -dot(z_axis, eye) | | 0 0 0 1 |不用背公式——理解推导思路就行相机变换是 T·R逆就是 R⁻¹·T⁻¹。GLM、Eigen 之类的库都有lookAt(eye, center, up)调即可。4.4 Projection 矩阵[CPU][图×2]视角[CPU]优先级P0两种投影正交投影视体是一个长方体保留平行性平行线投影后还是平行不产生近大远小用途CAD、UI、2D 游戏、阴影贴图透视投影视体是一个截头锥体frustum不保留平行性铁轨交于远点产生近大远小用途绝大多数 3D 渲染透视投影做了什么把视锥映射到立方体。这件事数学上叫同伦变换。视锥的特征近平面 z-near宽度 2·near·tan(fov/2)·aspect高度 2·near·tan(fov/2)远平面 z-far宽度 2·far·tan(fov/2)·aspect高度 2·far·tan(fov/2)远处更宽近处更窄立方体的特征均匀的 [-1, 1]³。要把远处的宽矩形压缩到和近处的小矩形一样宽——这就是近大远小的数学根源。透视矩阵GLM 风格P | 1/(aspect·tan(fov/2)) 0 0 0 | | 0 1/tan(fov/2) 0 0 | | 0 0 -(fn)/(f-n) -2fn/(f-n) | | 0 0 -1 0 |关键点第 4 行是(0, 0, -1, 0)——这一行让gl_Position.w -z观察空间 z。然后透视除法/w时所有 (x, y) 都被-z除——z 越大越远分母越大结果越小。这就是近大远小。为什么 z 不是线性映射把 (近 z -near, 远 z -far) 映射到 NDC 的 [-1, 1]最直觉的是线性映射。但 OpenGL 选了双曲映射NDC z (-(fn)/(f-n) · z - 2fn/(f-n)) / (-z)化简后NDC z对原始z是1/z的形式。为什么这么选精度分布1/z 形式让近处的 z 精度高、远处低。这恰好符合视觉需求——近的物体我们看得清远的差一点点没关系。硬件友好透视除法本来就要算 1/wz 用 1/z 形式刚好搭便车零额外开销。代价远处 z 精度差。两个相距 1cm 的远处物体深度 buffer 可能存成相同值发生Z-fighting深度撕裂。解决方案见 4.5。4.5 现代深度技巧Reverse-Z[GPU]视角[GPU]优先级P1问题标准 OpenGL近 z 映射到 NDC z -1远 z 映射到 1。深度 buffer 存 [0, 1]做映射(NDC z 1) / 2→ 近 0、远 1。由于上面1/z的精度分布最重要的近处区域反而精度集中在 z buffer 的小数末位——浮点数在小数附近精度高、在大数附近精度低正好和近精度高的需求反着来。Reverse-Z 的解法反过来让近 z 对应 buffer 的 1.0、远 z 对应 0.0。这样浮点精度在 1.0 附近最高 → 近处精度最高 ✅1/z分布让近处比例大 → 又一次集中在 1.0 附近 ✅两层近高远低的分布叠加→ Z-fighting 问题在大 far/near 比的场景下大幅缓解。Reverse-Z 不是改一个矩阵很多文章把 Reverse-Z 说成改投影矩阵就行——这不准确。它需要三处协同投影矩阵让 z 映射方向反转深度比较函数从GL_LESS更近 z 更小改为GL_GREATER更近 z 更大清屏深度值从glClearDepth(1.0)改为glClearDepth(0.0)漏一项就完全错。无限远平面把far设为无穷大避免在远处出现突然不见的裁剪。和 Reverse-Z 配套使用时深度 buffer 上的值由depth n / (-z_view)给出——z_view -n时 depth 1z_view → -∞时 depth → 0永不精确达 0但浮点下逼近足够近。常用于户外大场景、星空。具体矩阵推导——以下为 Reverse-Z 无限远 far 的复合形式有限 far 的纯 Reverse-Z 公式不同投影矩阵第 3 行 [0, 0, 0, n]第 4 行 [0, 0, -1, 0]。详细推导可参考 Sasha Willems / Reverse-Z 的经典文献。4.6 透视除法与视口变换[GPU][图×1]视角[GPU]优先级P0透视除法发生位置的统一澄清回到卷三 3.6 的图VS 输出 gl_Position裁剪空间齐次 (x,y,z,w) ↓ 图元装配仍在裁剪空间按齐次坐标做 ↓ 视锥裁剪在裁剪空间用 ±w 不等式 ↓ 透视除法 (x/w, y/w, z/w, 1) → NDC 空间 ← ★这里 ↓ 视口变换 NDC (x,y) → 屏幕像素坐标 ↓ 光栅化关键透视除法不在 VS 里不在图元装配里在裁剪之后。由硬件自动完成你写不到也改不了。齐次坐标 w 的全部使命gl_Position.w这个第 4 个分量承担了三件大事区分点和向量1 是点0 是向量齐次坐标基本约定携带透视信息投影矩阵让w -z_view透视除法时 (x, y) 都被 z 除——产生近大远小携带透视校正插值的权重FS 插值时用 1/w 重新加权卷三 3.7 的透视校正小节没有 w 的话这三件事都做不到。所以裁剪空间用齐次坐标不是炫技是必需。视口变换glViewport(x,y,width,height);把 NDC 的 [-1, 1] 映射到屏幕的 [x, xwidth] × [y, yheight]。改的是什么决定渲染画到屏幕的哪个矩形。多视口如分屏游戏就是不同 Draw Call 间切换 viewport。不改的是什么不改裁剪范围裁剪在裁剪空间已经做完了不改投影矩阵视口和投影是独立的坑viewport 默认 窗口大小。窗口大小变了用户拖窗口你必须手动重新glViewport否则渲染只填了原矩形那块。屏幕坐标的小细节OpenGL 的屏幕坐标左下角是 (0, 0)向右上为正。不是大多数 UI 库的左上角 (0, 0)、向右下。读glReadPixels、做截图、贴图坐标和屏幕坐标转换时容易出 y 翻转 bug。记住OpenGL 的世界y 朝上。新手雷图像加载时的 y 翻转几乎所有图像格式PNG、JPEG、BMP规定第一行像素 图像顶部——即 y 朝下。OpenGL 纹理则规定第一行像素 图像底部——即 y 朝上。如果你直接glTexImage2D(..., pixels)上传 stb_image 默认加载的数据得到的纹理在屏幕上上下颠倒。两种处理stbi_set_flip_vertically_on_load(true);// 加载时翻转一次最干净或者着色器里采样时翻转 vvec4 c texture(tex, vec2(uv.x, 1.0 - uv.y));。记住屏幕坐标、纹理坐标 OpenGL 都是 y 朝上外部图像是 y 朝下。这是新手最常见的纹理倒置 bug 根因。4.7 内存布局补丁列主序 vs 行主序[CPU]视角[CPU]优先级P2本节是查漏补缺。读完 4.1~4.6 已经能写代码本节解决为什么矩阵在内存里这么排。两种排法列主序OpenGL / GLSL 内存顺序: m00 m10 m20 m30 | m01 m11 m21 m31 | ... C 数组写法: float m[16] { col0..., col1..., col2..., col3... }; 行主序C 程序员习惯 / D3D 内存顺序: m00 m01 m02 m03 | m10 m11 m12 m13 | ...OpenGL 选列主序的原因数学习惯经典线性代数把向量当列向量矩阵乘列向量M·v。列主序内存布局让读取一列做点乘是连续访问。GLSL 一致性GLSL 里mat4 m; vec4 v; m * v;是矩阵×列向量与列主序自洽。代价C 程序员二维数组习惯mat[row][col]和列主序冲突。容易出现看着对的代码计算结果错。M * vvsv * MGLSL 中gl_Position M * v; // 矩阵 × 列向量标准用法不要写gl_Position v * M; // GLSL 会把 v 当行向量左乘 → 等价于 M^T * v实战建议存矩阵用 GLMglm::mat4。它内部就是列主序与 GLSL 直接对齐。上传时glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(m))第 3 个参数GL_FALSE “数据已是列主序不用转置”GL_TRUE时 OpenGL 会上传时转置行主序数据 → 内部列主序自己手写矩阵 → 一律按列填floatm[16]{1,0,0,0,// 第 1 列0,1,0,0,// 第 2 列0,0,1,0,// 第 3 列tx,ty,tz,1// 第 4 列包含平移};注意最后一列是平移不是最后一行。这就是列主序最容易混淆的地方。本卷自检读完本卷你应该能回答为什么 OpenGL 没有摄像机概念透视除法到底在哪一步发生由谁完成为什么 OpenGL 选列主序View 矩阵为什么是相机变换的逆Reverse-Z 需要改哪三处gl_Position.w承担了哪几个使命写一个把模型先缩放 2 倍、再绕 y 轴转 90°、再平移到 (5,0,0) 的复合矩阵顺序怎么写下一卷我们离开数学进入着色器语言 GLSL——同一个gl_Position行背后的 GLSL 是个怎样的语言、它和 C 的相似与不同从何而来。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2564455.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!