坐标空间、MVP 与透视投影

坐标空间、MVP 与透视投影:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。

学习目标

  • 能绘制从「局部空间 → 世界空间 → 观察空间 → 裁剪空间 → 屏幕空间」的完整管线,并说出每一步使用的矩阵
  • 能推导 LookAt 矩阵的 front / right / up 三个正交轴,并写出摄像机位于 z 轴时的 View 矩阵
  • 能解释裁剪坐标、透视除法与 NDC 的先后关系,判断一个点在哪一步会被裁掉
  • 能比较正交投影与透视投影的可见体、w 分量和近大远小效果
  • 能回答 near、far、fov 与 aspect 分别怎样改变视锥体,并分析错误参数导致的消失或变形

为什么一个点画到屏幕要经过五张「地图」

前面你已经能把三角形画出来了,也学会了用矩阵把它放大、旋转、平移。但真实的三维场景远不止「掰一下」。一个模型师在建模软件里雕出来的角色、一座从网上下载的地形——它们是怎么变成屏幕上那一帧画面的?

想象你在用卫星地图:地球是三维的球面,你拿手机看的是二维平面。从球面到平面,中间要一步步换「看世界的角度」——经纬度是一张图、平面投影是另一张图、屏幕像素又是第三张图。三维渲染也一样:每个物体经过五次换坐标系,从「用自己最舒服的坐标画自己」开始,一步步换到「用户屏幕上的像素停在哪个位置」为止。每一步乘一个矩阵,一共三个矩阵(Model / View / Projection),串在一起叫 MVP。

没有这套坐标系流水线,模型就不知道自己在世界里的位置、摄像机就不知道从哪拍、远处的山和近处的树就一样大——你看到的就不是三维,而是一张死板的二维平面图。

从局部到屏幕:五个空间的旅途

先看全景。下面这张图把顶点坐标在整个渲染管线里走过五个空间、乘三个矩阵的整趟旅途摊开:

一条线段总结:gl_Position = projection * view * model * vec4(aPos, 1.0)。乘的顺序从右往左——先 Model 把模型摆到世界、再 View 把世界拉到摄像机面前、最后 Projection 把可见范围压进 NDC 立方体。下面逐个空间掰碎。

局部空间:模型以自己为世界中心

建模师在 Blender 或 Maya 里雕一只猫时,不会关心这只猫最终会被你摆在场景的哪个位置、离摄像机多远。他只需要以猫自己为中心——猫的头朝什么方向、四条腿在什么高度,所有顶点坐标都用猫自己的 来写。同一个 .obj 文件里顶点坐标总是固定不变的——局部空间保证了「模型定义」这件事跟「摆在哪」完全解耦。

为什么需要它?如果每个模型都直接按世界坐标写顶点,换个位置就得重新导出模型文件,换一次就要命。局部空间让模型定义一次,后面靠矩阵随便摆。

世界空间:把所有模型摆到同一场上

要靠 。这个矩阵和变换章 §4 推导的组合矩阵完全一样(TRST\cdot R\cdot S),它把模型从局部空间变换到世界空间——所有物体共用一个原点、同一套坐标轴。

这本该叫「世界空间」,但因为 Model 矩阵和世界空间的「概念」上一章已经讲透了,这里一句带过,把篇幅留给更核心的 View 和 Projection。

观察空间:把世界拉到摄像机面前

世界空间里所有物体都摆好了,但画面得从某个角度去「拍」。 就是把世界空间里所有东西,整体挪动和旋转——直到摄像机变成新原点,摄像机面朝的方向变成 -z 轴(OpenGL 里约定观察方向为 -z)。

这一步靠的是 LookAt 矩阵(View 矩阵),输入只有三个数:摄像机站在哪、看向谁、哪个方向是「上」。矩阵推导在 §4。

裁剪空间:框定摄像机到底能「看见」多大范围

摄像机摆在观察空间原点、面朝 -z,但它不是 360° 全可以看到。相机有「视野宽度」(广角 / 长焦),也只看得到从镜头跟前一段距离到远处某个范围内、落在「画框」里的东西。这个「看得见的范围」就要靠投影矩阵来定义。 就是把视锥体里能看见的东西压进一个 [1,1]3[-1,1]^3 的立方体,压不进去的一律裁掉不画。

投影分两种:

  • 一种是 :直接框一个长方体,里面东西平移到 NDC,没有近大远小(做 UI / 2D 常用)。
  • 另一种是 :框一个金字塔形的视锥体,远处的被压扁、近处的撑大——这就是「近大远小」的数学来源。

下面这张图把透视投影的视锥体和 NDC 的关系画出来:

NDC:管线里的「标准集装箱」

投影之后,所有顶点的坐标都落在 [1,1]3[-1,1]^3 这个立方体里。这就是 (归一化设备坐标)。

为什么需要它?不管你的屏幕是 1920×1080 还是 4K、不管摄像机看多远——GPU 需要一套统一的中间格式来做裁剪和深度判断。NDC 就是这个中间格式。透视投影做完后还不是 NDC——要再做一次「透视除法」(把 x/y/z 都除以 w)才真正落到 NDC,这一步 §4 细讲。

屏幕空间与深度缓冲

最后一步,NDC 里的 [1,1]3[-1,1]^3 坐标按屏幕分辨率缩放,映射成屏幕上的像素坐标——这叫视口变换(由 gl.viewport 自动搞定)。但还有一个重要问题:当一个像素上同时有多个物体投过来(比如一块玻璃前有棵树后面的树梢也被画到了同一个像素),谁在前面?

——GPU 在画每个像素前,先查这个像素上之前画过的最浅深度值。新来的要是更远就直接跳过,更近则画上并更新深度。这就是「后面的东西被前面的挡住」的机制。

数学推导:投影矩阵与 LookAt 矩阵

这是本章的重心——一切「空间变换」最终都得落到矩阵上。本节推导两个投影矩阵和 LookAt 矩阵。先列符号表:

  • l,r,b,tl, r, b, t:正交投影的左右下上
  • n,fn, f:近裁面和远裁面距离(绝对值,正数)
  • θ\theta:fov 视角(透视投影,按弧度)
  • AA:宽高比(aspect = 宽 / 高)
  • p\vec{p}:摄像机位置;t\vec{t}:目标点
  • f,r,u\vec{f}, \vec{r}, \vec{u}:摄像机三根正交轴(front / right / up)

正交投影矩阵

正交投影最简单:视野是一个长方体,左右 [l,r][l,r]、下上 [b,t][b,t]、近远 [n,f][n,f]。目标是把它平移 + 缩放进 [1,1]3[-1,1]^3

Portho=[2rl00r+lrl02tb0t+btb002fnf+nfn0001]P_{\text{ortho}} = \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & -\frac{2}{f-n} & -\frac{f+n}{f-n} \\ 0 & 0 & 0 & 1 \end{bmatrix}

这个式子在说:矩阵前三列对角线上放「缩放倍数」——把长方体的宽 / 高 / 深各自缩成 2([1,1][-1,1] 的长度)。最后一列放「平移量」——把长方体中心挪到原点。注意 z 分量处是负数,因为 OpenGL 里观察方向沿 z-z,近的 z 值比远的大(n>fn > f 按代数值),矩阵翻转符号让深度正确装盒。当 l=rl=-rb=tb=-t 时(左右对称、上下对称),前两个对角线退化为简单的 1/r1/r1/t1/t

透视投影矩阵

透视投影比正交复杂:要把一个金字塔形视锥体压进 NDC 立方体。关键在两步:透视除法(除以 w 实现远小近大)+ 装盒(缩放到 [1,1][-1,1])。

先看从视锥体「挤」成标准长方体的核心步骤。给定 fov θ\theta、宽高比 AA、近裁面 nn、远裁面 ff,矩阵(列主序,推导从相似三角形和 NDC 装盒两步得到):

Ppersp=[1Atan(θ/2)00001tan(θ/2)0000f+nfn2fnfn0010]P_{\text{persp}} = \begin{bmatrix} \frac{1}{A\tan(\theta/2)} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan(\theta/2)} & 0 & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & -\frac{2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix}

这个式子在说:第一行负责 x 的缩放——把视野宽度按 fov 和宽高比缩到 [1,1][-1,1];第二行同理缩放 y;第三行最核心——z 分量上,1-1 在右下角之上一个格(这就是「把 z-z 塞进 w」),让透视除法时 ww 等于观察空间里的 z-z(即点到摄像机的距离)。最后一行的 1-1 正是这个「把距离写进 w」的设置——乘完后 w 分量变成 z-z,透视除法时 x/w、y/w 就自动实现了「离摄像机越远,x 和 y 被除数越大 → 越缩越小」的近大远小。

分开来看:乘完矩阵后,一个视锥体里的点 (xe,ye,ze,1)(x_e, y_e, z_e, 1) 变成了:

[xcyczcwc]=Ppersp[xeyeze1]=[xeAtan(θ/2)yetan(θ/2)f+nfnze2fnfnze]\begin{bmatrix} x_c \\ y_c \\ z_c \\ w_c \end{bmatrix} = P_{\text{persp}} \cdot \begin{bmatrix} x_e \\ y_e \\ z_e \\ 1 \end{bmatrix} = \begin{bmatrix} \frac{x_e}{A\tan(\theta/2)} \\ \frac{y_e}{\tan(\theta/2)} \\ -\frac{f+n}{f-n}z_e - \frac{2fn}{f-n} \\ -z_e \end{bmatrix}

这个式子在说:w 分量被填成了原始观察空间里的 ze-z_e(也就是点到摄像机的垂直距离)。接下来 GPU 自动做透视除法——把前三个分量都除以 w:

[xndcyndczndc]=[xc/wcyc/wczc/wc]=[xezeAtan(θ/2)yezetan(θ/2)f+nfn+2fn(fn)ze]\begin{bmatrix} x_{\text{ndc}} \\ y_{\text{ndc}} \\ z_{\text{ndc}} \end{bmatrix} = \begin{bmatrix} x_c / w_c \\ y_c / w_c \\ z_c / w_c \end{bmatrix} = \begin{bmatrix} \frac{x_e}{-z_e \cdot A\tan(\theta/2)} \\ \frac{y_e}{-z_e \cdot \tan(\theta/2)} \\ \frac{f+n}{f-n} + \frac{2fn}{(f-n)z_e} \end{bmatrix}

这就是透视的魔力。xe/(ze)x_e / (-z_e)ye/(ze)y_e / (-z_e) 正是「远处的东西除以更大的分母 → 结果更小 → 近大远小」。z 坐标经变换后不再是线性的——离近裁面近的点 z 精度高、离远裁面近的点 z 精度低(这正是后面「深度精度」那个坑的根因)。透视除法由 GPU 在顶点着色器之后、光栅化之前自动完成——你不需要手写。

LookAt 矩阵

View 矩阵的核心思路:把整个世界绕着摄像机平移再旋转,让摄像机变成新原点、面朝 z-z。构建它只需要三个数:摄像机位置 p\vec{p},看向的目标 t\vec{t},世界的「上」方向。

构造三根正交轴:

步骤固定——①用 target 减 position 得出摄像机面朝的方向(归一化得前向轴 f\vec{f});②拿世界 up 跟 f\vec{f} 叉乘算出右轴 r\vec{r};③再拿 r\vec{r}f\vec{f} 叉乘得到精确的上轴 u\vec{u}。三根轴拼成旋转矩阵,加上平移,组装成 4×4 LookAt 矩阵(先平移让摄像机到原点、再旋转让三轴对齐坐标轴):

V=[rxryrzrpuxuyuzupfxfyfzfp0001]V = \begin{bmatrix} r_x & r_y & r_z & -\vec{r}\cdot\vec{p} \\ u_x & u_y & u_z & -\vec{u}\cdot\vec{p} \\ -f_x & -f_y & -f_z & \vec{f}\cdot\vec{p} \\ 0 & 0 & 0 & 1 \end{bmatrix}

这个式子在说:矩阵左上 3×3 是旋转部分——把摄像机的右 / 上 / 前(取反,因为摄像机面朝 z-z)三轴分别对齐到世界 x / y / z 轴。最后一列是平移部分——把摄像机位置 p\vec{p} 投影到这三根轴上的负值,效果就是「把世界往反方向挪,让摄像机坐进原点」。所有元素纯由 p\vec{p}t\vec{t}、world up 三个输入算出来,一步到位。

动手:一步一步看一个点走完五张「地图」

下面是本章的主可视化——跟着一个模型局部空间里的顶点,一步步看它经 Model、View、Projection 三个矩阵变换后最终落在屏幕上的全过程:

分步1 / 5

第 0 步:局部空间

一个模型文件里的顶点(比如猫的左耳尖),坐标是 (0.5,2.0,0.0)(0.5, 2.0, 0.0)——用模型自己的坐标系写的。此时世界在哪里、摄像机在哪里,全不知道。

猜一猜:把透视投影的 fov 从 45° 调到 120°,画面里的物体会变多还是变少、变大还是变小?再把 near 从 0.1 慢慢调到 10——物体什么时候突然消失?为什么?(提示:想想视锥体的形状和「落在 near 和 far 之间才能被看见」这条规则。)

代码对照:在着色器里拼装 MVP

概念和数学讲完了,落地的代码其实很简洁。顶点着色器里只需一句乘法——MVP 矩阵整个作为 uniform 传入:

#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
  // 三个矩阵原地乘好,或分开乘——结果相同
  gl_Position = projection * view * model * vec4(aPos, 1.0);
}

CPU 侧负责构建三个矩阵。Model 用上一章的平移旋转缩放组合,View 用 LookAt,Projection 用透视投影:

// Model:把物体摆在 (0, 0, -5),绕 x 轴转 -55°
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, 0.0f, -5.0f));
model = glm::rotate(model, glm::radians(-55.0f),
                    glm::vec3(1.0f, 0.0f, 0.0f));
 
// View:摄像机在 (0,0,3),看向 (0,0,0)
glm::mat4 view = glm::lookAt(
  glm::vec3(0.0f, 0.0f, 3.0f),
  glm::vec3(0.0f, 0.0f, 0.0f),
  glm::vec3(0.0f, 1.0f, 0.0f));
 
// Projection:透视,fov 45°,宽高比,近 0.1 远 100
glm::mat4 projection = glm::perspective(
  glm::radians(45.0f),
  (float)width / (float)height, 0.1f, 100.0f);

两端思路一致:从单位矩阵起步,分别构建 Model / View / Projection,上传三个 uniform。注意三个关键 API 对应:

容易踩的坑

小结

  • 顶点从模型文件到屏幕要走五个空间:局部 → 世界(Model) → 观察(View) → 裁剪(Projection) → NDC → 屏幕,三个矩阵串成 MVP
  • View 矩阵(LookAt)靠摄像机位置 + 目标点 + world up 三个输入算出正交三轴,组装成「平移+旋转」矩阵,把世界拉到摄像机前面
  • 正交投影直直框长方体进 [1,1]3[-1,1]^3,远近一样大;透视投影把金字塔形视锥体挤压进 NDC 立方体,透视除法让「远的变小」——这就是近大远小的数学
  • 透视投影矩阵把观察空间的深度 ze-z_e 塞进 w 分量,GPU 自动除以 w 完成透视;z 缓冲精度非线性——near 设太小会吃掉远处的精度,导致 z-fighting
  • 着色器里 gl_Position = P · V · M · vec4(aPos, 1.0) 一行走完全程;代码端记得开深度测试(gl.enable(gl.DEPTH_TEST)

练习

问题 1(推导 / 手算题) 摄像机在 (0,0,3)(0, 0, 3),看向原点 (0,0,0)(0, 0, 0),world up 是 (0,1,0)(0, 1, 0)。①写出三个输入向量,手算出 front、right、upReal 三根轴。②按 §4 LookAt 矩阵公式写出完整的 4×4 View 矩阵。

问题 2(改 Demo 题) 在本章示例代码里做两件事:①把透视投影改成正交投影,观察场景里的物体大小变化——远方的物体和近处的物体大小还一样吗?②在正交投影下,把 near 参数从 0.1 改成 5,找出哪些物体会消失,解释为什么。

问题 3(裁剪排错题) 一个点经 MVP 后得到裁剪坐标 (2,0,0.5,1)(2, 0, 0.5, 1);另一个点得到 (2,0,0.5,4)(2, 0, 0.5, 4)。分别做透视除法,判断哪个点位于 NDC 的可见范围,并说明为什么不能在除以 w 之前直接拿 x 与 1 比较。

名词解释

本章出现的专业名词,用大白话再讲一遍。

局部空间

每个模型自己定义的坐标系。原点通常是模型中心或脚底,模型师在这个空间里造模型,完全不关心它在世界里的位置、朝向、大小。同一个模型文件不管摆在哪,局部坐标永远不变。详见本章「局部空间」一节。

世界空间

所有物体共用的全局坐标系。每个模型经 Model 矩阵变换后摆进这个世界,跟其他物体共享同一个原点和坐标轴。就像把玩具一个个摆到同一张桌子上。详见本章「世界空间」一节。

模型矩阵(Model)

把模型从自己的局部空间摆进世界空间的 4×4 矩阵,通常由缩放、旋转、平移按 T·R·S 组合而成。它只决定物体在世界中的大小、朝向和位置,不负责摄像机与投影。

观察空间

以摄像机为原点的坐标系。摄像机自己站在原点 (0,0,0),面朝的方向是 -z 轴(OpenGL 约定),右方是 +x,上方是 +y。这个空间里所有坐标表示「从摄像机眼里看,这个点在哪」。由 LookAt 矩阵(View)把世界变换过来。详见本章「观察空间」一节与「LookAt 矩阵」推导。

裁剪空间

投影矩阵把顶点变换到的空间。在这个空间里,看得见的顶点被装进 [-1,1]³ 的立方体——落在外面的部分被 GPU 自动裁掉不画。透视投影下,这一步还包含了透视除法的前置准备(把深度塞进 w)。详见本章「裁剪空间」一节。

NDC

Normalized Device Coordinates,归一化设备坐标。顶点经投影矩阵和透视除法后最终落入 [-1,1]³ 的立方体。GPU 在 NDC 里做裁剪、深度测试等操作,最后一步视口变换才映射到屏幕实际像素。详见本章「NDC」一节。

视锥体

透视投影的可见区域——一个从摄像机出发的金字塔形四棱台:近裁面(near)是金字塔小平顶、远裁面(far)是金字塔大底。落在视锥体棱线内的物体会被渲染,之外的被裁剪。fov(视角)决定了金字塔的张开程度。详见本章「裁剪空间」一节与 FrustumDiagram。

正交投影

一种平直投影——直接把一个长方体视野内的东西映射到 NDC 立方体。远近物体大小不变,没有「近大远小」。常用于 2D 渲染、UI、工程制图。矩阵比透视投影简单得多:就是缩放到 [-1,1]³ 加居中平移。详见本章「正交投影矩阵」推导。

透视投影

一种模拟人眼和相机镜头的投影——把一个金字塔形视锥体里的内容挤压进 NDC 立方体。投影矩阵把观察空间的深度 ze-z_e 塞进 w 分量,透视除法除以 w 后实现「离摄像机越远,x 和 y 被除数越大 → 越缩越小」的近大远小。这是三维真实感的核心。详见本章「透视投影矩阵」推导。

深度缓冲

和颜色缓冲同样大的隐藏缓冲,每个像素存着「这个像素上已画过的最浅深度值」。新片段如果更深(更远)就被丢弃不画;更浅(更近)则覆盖颜色同时更新深度。这就是「后面的东西被前面的挡住」的 GPU 机制。使用时需要手动 gl.enable(gl.DEPTH_TEST) 开启。详见本章「深度缓冲」一节与 §7 误区。

版本、来源与运行边界

本章以 Joey de Vries 的 LearnOpenGL 原章 为授权改编依据,教学运行基线是 OpenGL 3.3 Core Profile。Khronos 当前发布的规范参照是 OpenGL 4.6 Core Profile;这里用 4.6 规范核查术语和状态合同,但不把 4.6 API 偷偷倒填为原教程内容。GLFW、GLAD、Assimp 与驱动版本都属于运行环境,不能拿“编译通过”替代对 context、资源和 framebuffer 结果的验证。

正式概念与状态责任

  • model matrix:在“坐标空间、MVP 与透视投影”中由model/view/projection 三矩阵与固定功能 viewport/depth 映射负责解释其输入、受控状态和可观察结果;运行时以五空间坐标、clip.w、NDC、viewport、深度值与基准截图定位它的第一处变化。
  • view matrix:在“坐标空间、MVP 与透视投影”中由model/view/projection 三矩阵与固定功能 viewport/depth 映射负责解释其输入、受控状态和可观察结果;运行时以五空间坐标、clip.w、NDC、viewport、深度值与基准截图定位它的第一处变化。
  • projection matrix:在“坐标空间、MVP 与透视投影”中由model/view/projection 三矩阵与固定功能 viewport/depth 映射负责解释其输入、受控状态和可观察结果;运行时以五空间坐标、clip.w、NDC、viewport、深度值与基准截图定位它的第一处变化。
  • perspective:在“坐标空间、MVP 与透视投影”中由model/view/projection 三矩阵与固定功能 viewport/depth 映射负责解释其输入、受控状态和可观察结果;运行时以五空间坐标、clip.w、NDC、viewport、深度值与基准截图定位它的第一处变化。

章专属 OpenGL 状态实验

先预测“更新 MVP,执行透视除法,并把 NDC 映射到 framebuffer viewport”发生后,model/view/projection 三矩阵与固定功能 viewport/depth 映射应怎样改变对象姿态、相机坐标、frustum、clip.w、NDC 和窗口深度;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。

实验一:Context—资源—结果合同

选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。

Context · resource · observable result

坐标空间、MVP 与透视投影:状态合同

沿 model→world→view→clip→NDC→viewport 追踪一个顶点并核对透视参数

验证场景

官方教程正式概念

logl-07 · 基线帧

model matrix固定 context、资源内容与输入事件,执行“更新 MVP,执行透视除法,并把 NDC 映射到 framebuffer viewport”

状态所有者model/view/projection 三矩阵与固定功能 viewport/depth 映射
受控状态/资源对象姿态、相机坐标、frustum、clip.w、NDC 和窗口深度
触发命令更新 MVP,执行透视除法,并把 NDC 映射到 framebuffer viewport

冻结输入:model matrix

model/view/projection 三矩阵与固定功能 viewport/depth 映射记录对象姿态、相机坐标、frustum、clip.w、NDC 和窗口深度

结果:得到可重复的初始 GL 状态与资源身份

观测:五空间坐标、clip.w、NDC、viewport、深度值与基准截图中的初始快照

预期:model/view/projection 三矩阵与固定功能 viewport/depth 映射得到可复查结果,并持续满足“所有矩阵使用同一坐标/手性约定,aspect 来自当前 framebuffer 宽高”

实验二:CPU 命令到 GPU 结果的五段轨迹

逐段执行“更新 MVP,执行透视除法,并把 NDC 映射到 framebuffer viewport”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“所有矩阵使用同一坐标/手性约定,aspect 来自当前 framebuffer 宽高”。

CPU command · GL state · GPU result

坐标空间、MVP 与透视投影:五段轨迹

选择一段命令—资源—结果1 / 5

当前观测:五空间坐标、clip.w、NDC、viewport、深度值与基准截图中的初始快照

不变量:所有矩阵使用同一坐标/手性约定,aspect 来自当前 framebuffer 宽高

实验三:单故障与同输入恢复

注入“窗口横向放大后 projection 仍用旧 aspect,圆形被拉成椭圆”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有五空间坐标、clip.w、NDC、viewport、深度值与基准截图一起恢复才算修复。

Single fault · first divergence · replay

坐标空间、MVP 与透视投影:反例与恢复

故障:窗口横向放大后 projection 仍用旧 aspect,圆形被拉成椭圆

1. 冻结输入一致

第 1 次沿用同一 context、资源、uniform 与 draw 输入

2. 注入单故障一致

保持其余输入不变,仅注入“窗口横向放大后 projection 仍用旧 aspect,圆形被拉成椭圆”

3. 定位首差一致

所有矩阵使用同一坐标/手性约定,aspect 来自当前 framebuffer 宽高

4. 清理并重放一致

五空间坐标、clip.w、NDC、viewport、深度值与基准截图

最小可重放检查

unit: logl-07
owner: model/view/projection 三矩阵与固定功能 viewport/depth 映射
state_or_resource: 对象姿态、相机坐标、frustum、clip.w、NDC 和窗口深度
command: 更新 MVP,执行透视除法,并把 NDC 映射到 framebuffer viewport
pass_invariant: 所有矩阵使用同一坐标/手性约定,aspect 来自当前 framebuffer 宽高
single_fault: 窗口横向放大后 projection 仍用旧 aspect,圆形被拉成椭圆
required_evidence: 五空间坐标、clip.w、NDC、viewport、深度值与基准截图

复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。

出处声明

本文为改编重写,改编自 Joey de Vries 的 LearnOpenGL。原文:learnopengl.com

原作及译作以 CC BY-NC 4.0 协议授权,本改编版同样遵循该协议(署名—非商业性使用)。

讨论

评论区加载中…