03 坐标空间、颜色与调试

前置知识:第 1 章的矩阵和第 2 章的向量。
本章示例:坐标空间可视化,完整源码。

学完本章能做什么

你将能跟踪一个顶点从模型到屏幕的坐标变换,辨认一个值处于局部、世界还是观察空间,并用颜色可视化中间结果排查 shader 问题。

先看现象

打开本章示例,先选“局部空间”,移动模型:立方体移到新位置,但附着在它各个表面的颜色关系仍然相同。再选“世界空间”并移动模型:同一个局部点到了新的世界位置,颜色也跟着改变。最后选“观察空间”并旋转相机:相机改变了观察坐标,颜色会改变。

这个实验提醒我们:同一个顶点可以同时拥有不同空间下的位置。不能只看变量类型 vec3 就认为它们可直接相加或相减。

1. 顶点如何走到屏幕

局部空间 position
  │ modelMatrix
  ▼
世界空间 worldPosition
  │ viewMatrix
  ▼
观察空间 viewPosition
  │ projectionMatrix
  ▼
裁剪空间 gl_Position(齐次 vec4)
  │ GPU 裁剪、除以 w、映射到视口
  ▼
屏幕上的片元

本章示例显式写出每一步:

vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
vec4 viewPosition = viewMatrix * worldPosition;
gl_Position = projectionMatrix * viewPosition;

对普通网格,第二行也可写成 modelViewMatrix * vec4(position, 1.0)。矩阵乘法次序不能随意交换:先进行模型变换,再进行相机变换,最后投影。

局部空间

position 来自几何体,以物体自己的原点和轴为参照。物体在世界中移动,原本的局部坐标不会因此改变。

世界空间

worldPosition 用场景坐标系描述点的位置。多个物体要比较彼此位置、或计算到世界空间 cameraPosition 的方向时,先把值放到同一世界空间。

观察空间

viewPosition 以相机为参照。对于常见的相机朝向,相机前方的物体通常具有负的观察空间 Z 值。移动相机,即使物体没有动,观察空间位置仍会改变。

裁剪空间与屏幕

gl_Position 是 vec4,仍处于裁剪空间;它的 xyz 不能直接当成像素坐标。GPU 会按 w 做透视除法,得到标准化设备坐标(NDC),再映射到视口。透视投影中,不同深度的物体因此呈现不同的屏幕大小。

片元着色器的 gl_FragCoord.xy 表示当前片元在渲染目标中的窗口坐标,单位通常是物理像素;画布使用了 setPixelRatio() 时,不要直接拿 CSS 像素尺寸与它比较。后续讨论屏幕比例时会统一处理分辨率。

2. UV 不是上述空间变换的一个阶段

uv 是附着在几何体顶点上的二维参数坐标,通常用来找纹理上的位置或画二维图案。移动相机不会改变某个顶点原有的 UV;模型空间位置和 UV 也没有必须一一对应的数学关系。例如,同一顶点可以拥有 (1.0, 0.0) 的 UV,却在世界空间的任何位置。

顶点着色器把 uv 放入 vUv 后,光栅化器在三角形内部对它插值。片元着色器因此得到每个片元的 UV。透视下的插值会考虑深度;无需自己把顶点 UV 按屏幕像素线性平均。

3. 用颜色查看位置和方向

屏幕颜色通道通常按 0.0~1.0 来表达。位置可能是负数,也可能远大于 1.0,所以调试时先把感兴趣的范围映射进颜色范围。本章示例采用:

vec3 color = positionToShow * 0.2 + 0.5;
gl_FragColor = vec4( color, 1.0 );
#include <colorspace_fragment>

此映射把 0.0 显示在约 0.5;-2.5 映射到 0.0,2.5 映射到 1.0。它只是调试时选的观察范围,不是坐标变换本身。超出范围的值会在显示时被限制,导致大片区域颜色相同;此时缩小倍率或改变偏移。

法线是方向,常见分量约在 -1.0~1.0。把它显示成颜色时,更合适的映射是:

vec3 normalColor = normalize( viewNormal ) * 0.5 + 0.5;

顶点法线经 varying 插值后,长度可能不再为 1,所以在片元着色器中使用前应重新 normalize。对有非等比缩放的模型,局部法线先用 normalMatrix 变到观察空间。第 15、16 章会讨论表面被 noise 改动后的法线。

4. 颜色数值与显示颜色

本教程的 shader 先按线性 RGB 计算颜色,再通过 #include <colorspace_fragment> 转成渲染器的输出色彩空间。示例将 renderer.outputColorSpace 设为 THREE.SRGBColorSpace。因此 shader 中的数值 0.5 与截图里看起来“正好一半亮”的直觉不一定一致。

这一步尤其影响渐变、混合与 RGB 叠加的视觉结果。第 8 章会再用具体图形解释颜色如何叠加;第 11 章讨论图片纹理自身的色彩空间。

5. 排查一个画面错误的顺序

  1. 先看浏览器控制台。 若 shader 编译失败,确认报错落在顶点还是片元阶段,并检查类型、变量名和括号。
  2. 确认物体确实在相机视野中。 先用最小位置公式:projectionMatrix * modelViewMatrix * vec4(position, 1.0)。
  3. 将中间值显示成颜色。 先显示 vUv,再显示局部、世界或观察位置。每次只换一个来源。
  4. 检查坐标空间是否匹配。 例如世界空间相机位置必须与世界空间顶点位置相减。
  5. 检查颜色映射范围。 若多数位置都超出 0.0~1.0,屏幕可能只有单一颜色,但计算本身未必错。
  6. 检查几何体属性与分段。 缺少 UV 会影响 UV 图案;顶点位移如果分段太少,波浪等形状变化可能看不出来。片元着色器画出的渐变通常不依赖高分段网格。

本章小结

位置的数值必须连同坐标空间一起理解。modelMatrix、viewMatrix、projectionMatrix 将局部顶点依次带到屏幕;UV 是独立的几何体参数坐标。调试时先把中间数据映射到可见颜色,再逐步恢复完整计算。

本章练习