第 3 课:坐标系与 UV
同一个位置有几种表示法
做圆形遮罩、棋盘格、雷达扇形或图片贴图时,最容易混淆的是“位置”到底在哪个坐标系。本课先用正方形预览理解 UV,再故意改成宽画布,看圆形为什么被拉长。运行方式:在 doc/shader-tutorial/exercises/ 执行 http-server -p 1000,打开 http://localhost:1000/03-coordinates-uv/。
| 变量 | 所在空间 | 范围与单位 | 常见用途 |
|---|---|---|---|
position |
几何体局部空间 | 模型自定义单位 | 顶点形变与物体轮廓 |
uv / vUv |
几何体 UV 空间 | 通常 0–1,起点由几何体定义 | 贴图、重复图案、随物体移动的遮罩 |
gl_FragCoord.xy |
当前渲染目标的窗口空间 | 像素,左下为原点 | 屏幕空间图案、像素定位、分辨率相关效果 |
uResolution |
JS 传入的画布尺寸 | 物理像素 | 把 gl_FragCoord 归一化、宽高比修正 |
vUv 与 gl_FragCoord 不是同一个东西。平面的 UV 即使在屏幕上只占很小一块,仍可从 0 到 1;gl_FragCoord 始终是该片元在渲染目标中的像素坐标。CSS 像素与画布物理像素还受设备像素比影响。本系列的运行时限制像素比到 2;在 JS 中用 renderer.getDrawingBufferSize() 取得实际绘制尺寸,与 gl_FragCoord 保持一致。
从 UV 到中心距离
平面左下角的 vUv≈(0,0),右上角是 (1,1)。把 vUv.x 写入红通道、vUv.y 写入绿通道,会看到左下黑、右上黄。中心化用 p = vUv - vec2(0.5),此时中心为 (0,0),左右边界的 x 是 -0.5 和 0.5。length(p) 是到中心的欧氏距离:
vec2 p = vUv - vec2(0.5);
float d = length(p); // sqrt(p.x*p.x + p.y*p.y)
半径为 r 的实心圆可写 1.0 - step(r,d)。边界会在一个像素内直接跳变,放大时容易锯齿。柔边版用 1.0 - smoothstep(r-w,r+w,d),w 越大,边缘越宽。要做圆环,可用“外圆遮罩 − 内圆遮罩”;也可用 abs(d-r) 表示到目标半径的距离。遮罩组合是之后 SDF、选区和描边的基础。
注意:UV 中的数学圆不必然是屏幕上的圆。 如果平面在屏幕上宽高不同,同样的 UV 距离会对应不同数量的像素。前七题使用正方形预览,让 UV 的 x/y 单位在画面中等长。第八题刻意用宽画布,暴露拉伸问题。
宽高比修正的适用条件
第八题的平面覆盖整个画布,所以 vUv 与画布比例一一对应。设画布宽 W、高 H,则像素中的等距要求 p.x 乘以 W/H:
vec2 p = vUv - vec2(0.5);
p.x *= uResolution.x / uResolution.y;
float d = length(p);
页面开关在修正前后切换;调整浏览器宽度时圆应继续保持正圆。uResolution 来源是画布 drawing buffer 的物理像素,不是 window.innerWidth。这条公式只适用于当前全屏平面。如果材质贴在一个场景里的长方形物体上,画布宽高比和物体表面宽高比是两件事:要按物体的物理宽高或局部坐标修正,而不能盲目用画布 W/H。若使用 gl_FragCoord.xy 画屏幕空间圆,先除以 uResolution.y 来统一像素单位也很方便。
重复图案与极坐标
棋盘格把 vUv 乘以 8 拆成 8×8 个格子,再由 floor(vUv*8.0) 得到格子索引。fract(cellIndex*0.5) 在偶数索引时为 0、奇数索引时为 0.5;step(0.5, ...) 把它变成 0/1,横纵结果不同的格子涂白。这里用 fract + step 判断奇偶,而不是对每格内部再次分段,因此严格得到 8×8 棋盘。
极坐标用 r=length(p) 表示半径,用 θ=atan(p.y,p.x) 表示相对 X 正方向的角度,范围为 -π 到 π。扇形需同时满足 r < radius 和 abs(θ) < halfAngle。JS 滑块显示的是整段开口角度(度),传给 Shader 时先换成弧度,再除以二。把 90° 改成 180° 后,扇形应由四分之一圆变成半圆。
数据流、常规实践与排查
JS 建立平面与 ShaderMaterial;平面顶点的 uv attribute 经过顶点着色器成为 vUv,GPU 光栅化插值后,片元着色器逐片元计算半径、格子或角度。第八题额外从 JS 传入 uResolution,第九题传入 uHalfAngle。顶点 Shader 管位置,片元 Shader 管当前片元的颜色;以后让图案随物体变形时,UV 和局部坐标会跟随物体,而 gl_FragCoord 仍固定在屏幕。
屏幕上的圆变椭圆时,先检查预览区域是否正方形,再检查使用的是 UV、局部位置还是屏幕像素;不要先随意调半径。棋盘格数量不对时,区分“周期数”和“每周期内再次分段的数量”。扇形角度不对时,检查度与弧度转换、atan 参数顺序,以及传入的是整角还是半角。宽度或设备像素比变化后若形状跳动,检查 uResolution 是否来自当前 drawing buffer。
练习
| 难度 | 题目 | 可见验收 |
|---|---|---|
| ⭐ | 把 UV 映射为红绿 | 左下黑、右上黄 |
| ⭐ | UV 中心化与距离 | 中心黑、四周亮 |
| ⭐⭐ | Length 圆形渐变 | 中心亮、向外变暗 |
| ⭐⭐ | Step 实心圆 | 中心白色实心圆 |
| ⭐⭐ | Smoothstep 柔和圆边 | 白圆边缘柔和 |
| ⭐⭐⭐ | 两条边界组合成圆环 | 白色空心圆环 |
| ⭐⭐⭐ | Fract + Step 棋盘格 | 严格 8×8 黑白棋盘 |
| ⭐⭐⭐ | 宽画布圆形拉伸与修复 | 关闭时椭圆、开启时正圆 |
| ⭐⭐⭐⭐ | Atan + Length 可调扇形 | 滑块控制开口角度 |