图形学新人常被模型视图矩阵绊倒,TaoToken 官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 上能拿到 API Key;把 Key 和 Base URL 填进 Codex 之后,我再让它对着老版 OpenGL 的 glulookat、glFrustum 把矩阵栈拆开,推导顺了,屏幕空间、切线空间矩阵的疑惑也一起解开。这不是让你换掉图形学教材,只是排障时多了一个随时能回答“这一步到底是哪个矩阵在乘”的助手。
老版固定管线把模型矩阵和视图矩阵合在GL_MODELVIEW里,glLoadIdentity()之后用gluLookAt摆摄像机,最后才画几何体。这种写法把矩阵连乘的顺序藏在了管线内部,所以很多人在 2.0 之后转到可编程管线就懵:为什么所有矩阵都要自己在程序里算好再传进 shader?模块视图矩阵到底先乘哪个、投影之后为什么还要除以 w,正是两个最容易卡住人的地方。
1. 模型视图矩阵:model 乘 view,还是 view 乘 model
一个顶点从模型本地坐标走到屏幕坐标,实际经历的是:
模型空间 -> 世界空间 -> 摄像机空间 -> 裁剪空间 -> 屏幕空间每一步对应一个矩阵,但现代 OpenGL 顶点着色器里只写一行:
uniform mat4 model; uniform mat4 view; uniform mat4 projection; layout(location = 0) in vec3 aPos; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); }这里从右往左读:model先作用到顶点上,把模型空间坐标变成世界空间坐标;view再把世界空间坐标变成摄像机空间坐标;projection最后把摄像机空间坐标压到裁剪空间。也就是说,顶点在传递过程中是从右边往左边依次经过矩阵的。如果你把顺序写成model * view * projection,等于让 model 矩阵作用于投影矩阵的结果,模型的位置和旋转会彻底错乱。
老版 OpenGL 之所以容易让人混淆,是因为GL_MODELVIEW这个名字把GL_MODELVIEW合成成了一个栈:在固定管线里,你调用glMatrixMode(GL_MODELVIEW)之后,后续的变换会左乘到当前矩阵上。当你在glLoadIdentity()之后调用gluLookAt,其实已经隐含了一层“模型矩阵在最内层、视图矩阵在它外层”的栈结构。可编程管线里没有这个隐式栈,uniform mat4 model和uniform mat4 view必须明确分开传,乘序一旦写反,物体会飞到摄像机身后,或者绕着世界原点乱转。
让 Codex 检查乘序时,我通常直接贴给它两段代码:一段是上面那个 GLSL 顶点着色器,另一段是固定管线的状态机代码,然后问它“这两者的矩阵乘法顺序如何对应”。它会告诉你:固定管线的glMatrixMode(GL_MODELVIEW)里实际维护的是view * model,顶点先被 model 变换,再被 view 变换;到了可编程管线,你要自己把这两个矩阵分别算出来,并按projection * view * model * vec4(pos, 1.0)的顺序传入。
这儿有个经常被忽略的细节:view矩阵本身也是“旋转 + 平移”,它的旋转部分由摄像机的前方向、上方向和右方向组成。当你写下gluLookAt(eye, center, up)时,OpenGL 并不是直接把这个函数参数当成矩阵元素存下来,而是先算出三个正交基向量,再把它们组装成一个 4x4 矩阵。这个组装过程才是视图矩阵推导里真正值得练手的地方。
2. 先把 Codex 指到 TaoToken:config.toml 里的 base_url 和 Key
要让 Codex 回答上面那些矩阵问题,第一步是让 Codex 背后的模型通道先通起来。TaoToken 在文章里扮演的就是这个兼容通道:它把统一的 API 地址暴露给 Codex,Codex 只需要知道一个 Base URL 和一个 API Key。
先去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册并创建 API Key,拿到之后,字符串一律以YOUR_API_KEY占位,不要写进任何会被提交到 Git 的文件里。接下来在本地编辑~/.codex/config.toml,把 Codex 的模型供应商指向 TaoToken:
# ~/.codex/config.toml model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"然后在当前 shell 里设置环境变量:
export TAOTOKEN_API_KEY=YOUR_API_KEY配置里最值得注意的地方是base_url。填进 Codex 的地址是https://taotoken.net/api,末尾不要加/v1,也不要顺手把前面网页链接里的utm_source参数带进来。官网落地页用来注册、创建 Key、看模型广场和看用量;工具配置文件里只需要纯接口地址。
model = "YOUR_MODEL_ID"里的模型 ID 不是随便编的,要去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的模型广场查当前可用的 ID。网上教程里那些带着日期后缀的命名很可能已经过期,直接复制到自己配置里只会得到 404。喜欢命令行的话,TaoToken 也提供一个快速验证通道:
npm install -g @taotoken/taotoken taotoken cc -k YOUR_API_KEY -u https://taotoken.net/api -m YOUR_MODEL_ID这条命令会启动一个终端问答会话,适合在写 Codex 配置之前先确认 Key 和模型 ID 是否有效。如果它能正常回复,说明 Codex 的config.toml只差最后一步:确认环境变量名和模型 ID 一致。
3. 拿 gluLookAt 当考题:让 Codex 把视图矩阵拆开
Codex 接通之后,第一道考题就用 glulookat。不要直接问“什么是视图矩阵”,那样得到的答案太笼统。把问题写具体一点:
请推导出 gluLookAt(eye, center, up) 返回的 4x4 矩阵,解释旋转部分和平移部分分别由什么组成,并指出为什么填充平移项时要取负的 dot(s, eye)。
Codex 会给出类似这样的拆解思路:先算出前方向f = normalize(center - eye),用f和up叉积得到右方向s = normalize(cross(f, up)),再用s和f叉积得到真正的上方向u = cross(s, f)。视图矩阵的旋转部分由s、u、-f三个行向量组成,平移部分是这三个轴在eye上的投影的负值。
如果你真的去手推,最容易出错的就是符号。f是center - eye的方向,但视图矩阵的旋转矩阵里第三行用的是-f,因为 OpenGL 摄像机默认看向-z方向。平移项为什么是负的?因为视图矩阵要把世界坐标的 eye 点移到原点,所以在旋转之后还要把 eye 的位置反向补回来。这一小段推导,自己算五分钟很容易乱,让 Codex 先给结论,再要求它把每一步的dot和cross写出来,比你盯着向量发呆要快得多。
验证 Codex 是否真的理解,而不是背答案,可以追加一个问题:
用 numpy 实现上面的 gluLookAt 推导,生成两个矩阵:一个用刚才的推导公式,另一个用我手写的实现,对比两者差值是否为 0。
让 Codex 生成一段本地脚本,你在自己的机器上跑,把输出贴回对话。这样它讲得对不对,一跑便知。
4. glFrustum 到齐次除法:投影后为什么必须除以 w
视图矩阵之后的变换是投影。老版 OpenGL 里这段代码:
glMatrixMode(GL_PROJECTION); glLoadIdentity(); glFrustum(left, right, bottom, top, near, far);glFrustum生成的是透视投影矩阵。透视的关键点是近处大、远处小,这个缩放关系被编码在矩阵第四行的w分量里。标准透视投影矩阵大致长这样:
xScale 0 a 0 0 yScale b 0 0 0 c d 0 0 -1 0其中xScale = 2 * near / (right - left),yScale = 2 * near / (top - bottom),a = (right + left) / (right - left),b = (top + bottom) / (top - bottom),c = -(far + near) / (far - near),d = -(2 * far * near) / (far - near)。
当顶点乘完这个矩阵后,得到的是裁剪空间坐标(x_clip, y_clip, z_clip, w_clip),而w_clip恰好等于原视点坐标中-z的镜像。透视投影让远处的物体坐标在齐次除法时被除一个更大的w,所以离摄像机越远,落到 NDC 里的坐标越趋近于 0,画面就自然呈现出近大远小。
如果不除以w,整个场景会以“正交拉伸”的方式铺在屏幕上,所有物体都没有远近纵深。很多读者卡在“投影矩阵已经算好了,为什么还要再做一步齐次除法”,其实是因为他们只看见了矩阵前半段,没看见后半段光栅化之前的透视除法。这一行除法发生在裁剪空间之后、NDC 空间之前,OpenGL 管线固定帮你做,你不需要在着色器里写gl_Position /= gl_Position.w,但推导时必须知道它存在。
从 NDC 到屏幕空间还有最后一步:x_ndc在[-1,1]区间,屏幕坐标screen_x = (x_ndc + 1) / 2 * viewport_width;y同理,但 OpenGL 的视口变换会让y方向翻转。原文里说“平移变化到(0,1)的屏幕空间”,指的就是把[-1,1]映射到[0,1]再乘以分辨率。Codex 在这儿能帮你做一件很实在的事:给定一组left/right/bottom/top/near/far,让它输出一个点从视锥内某个坐标一路到像素坐标的完整计算过程,然后你在点一下之前先猜一下结果,再对答案。
5. 切线空间矩阵 TBN:法线贴图不在模型空间里算
模型视图矩阵和投影矩阵处理的是顶点位置,法线贴图处理的则是“表面该怎么反光”。法线贴图里存的是切线空间的法线,取值范围从[0,1]映射到[-1,1],可它和顶点一样,也有一个“哪个空间”的问题。如果你直接拿贴图采样出来的法线和世界空间的光照方向做点积,会得到完全错误的结果。
要用好法线贴图,需要构造一个 TBN 矩阵,把切线空间的法线转到世界空间或视图空间。TBN 的三个列向量分别由切线 T、副切线 B、法线 N 组成:
vec3 N = normalize(vNormal); vec3 T = normalize(vTangent); vec3 B = normalize(cross(N, T)); mat3 TBN = mat3(T, B, N); vec3 tangentNormal = texture(normalMap, vUv).rgb * 2.0 - 1.0; vec3 worldNormal = normalize(TBN * tangentNormal);这里最常见的坑是B的方向。cross(N, T)得到的是“右手系”副切线,可很多建模软件的 UV 坐标系约定并不统一,有的引擎会直接取cross(T, N),或者乘一个-1。如果发现法线贴图的光照左右颠倒,八成就是这里的叉积顺序反了。
另一个容易翻车的地方是:TBN 矩阵本身来自模型空间的T和N,如果你的光照方向在视图空间计算,而法线被转到了世界空间,那两者就不在同一个坐标系里。要么让光照方向也走同一个view矩阵,要么把 TBN 转到视图空间后再变换法线。让 Codex 排查这类问题时,直接把着色器代码贴给它,并附上这句提示:
检查我的 TBN 变换矩阵和 lightDir、viewDir 是否处于同一个空间。如果是,请给出一个只在片元着色器里修改的最小例子。
Codex 会先定位lightDir是从哪来的,再看TBN * tangentNormal输出到哪个空间。这一步排查很值钱,因为画面出现不正常的亮斑时,人眼很难分辨到底是 TBN 叉积方向错,还是光照方向空间不统一。
原文里还提到平面投影阴影、模板阴影以及glClipPlane。这些技术同样依赖矩阵叠加,比如平面阴影会把一个投影矩阵乘到 model 变换之外,让几何体被压到一个平面上;模板阴影则是利用裁剪面控制哪些像素写入模板缓冲。理解了模型视图矩阵的乘序之后,再看这些用法会清晰很多:它们都是“在变换链的某一环插入一个额外矩阵”,插入位置不同,阴影效果完全不同。
6. 四个最容易翻车的位置
把这套矩阵变换从头到尾走了一遍之后,我把最常见的翻车点整理成一个对照表。它既包含图形学推导问题,也包含 Codex 接入 TaoToken 时可能遇到的问题:
| 现象 | 根源 | 检查方向 |
|---|---|---|
| 物体绕世界原点乱转,而不是绕自身中心旋转 | model 和 view 乘序写反,或 model 矩阵里的平移部分被 view 覆盖 | 确认projection * view * model * pos的从右到左顺序 |
| 场景没有近大远小,像被压平 | 顶点着色器里缺少齐次除法,或投影矩阵的 w 分量算错 | 确认 gl_Position.w 是否等于原视图空间-z |
| 法线贴图光照左右颠倒/出现对称亮斑 | TBN 的副切线方向用了错误的叉积顺序 | 检查cross(N, T)还是cross(T, N),并与建模工具约定对照 |
| Codex 返回 401 | 环境变量TAOTOKEN_API_KEY没有被替换成真实 Key,或 shell 没执行 export | 在终端 echo$TAOTOKEN_API_KEY确认值 |
| Codex 返回 404 | config.toml里的base_url多写了/v1 | 改成https://taotoken.net/api,不要加后缀 |
最后一行的 404 是配置时最典型的错误:很多人习惯性把 Base URL 写成/api/v1,但 TaoToken 的接入地址就是https://taotoken.net/api,Codex 会在请求时自动补齐具体路径。写配置时把官网和接口分开记:注册、看 Key、看用量去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,填配置文件用https://taotoken.net/api。
矩阵推导这件事,说到底是一个“把坐标系不断变换”的过程。Codex 走 TaoToken 的意义不在于替你写答案,而在于当你对着 glulookat 和 glFrustum 绕不出来时,它可以把你写的矩阵一笔一笔拆开,告诉你哪一列在做什么、哪一个符号为什么是负的。现在登录 TaoToken 控制台看看刚才那次 Codex 会话消耗了多少 Token,顺便核对一下模型广场里有没有更合适的模型 ID,再拿下一个矩阵变换去问它。