1. 为什么我们要绕开矩阵谈3D变换第一次接触WebGL的3D变换时90%的教程都会直接甩出矩阵运算。但当我真正开始教学时发现很多初学者其实是被矩阵这个数学包装吓退的。今天我们就用最原始的数学方法手把手实现三角形的旋转、平移和缩放。重要提示本文方法虽然避开了矩阵但最终效果与矩阵变换完全等效。理解这些基础原理后再学习矩阵会事半功倍。2. 基础概念3D空间中的点2.1 坐标系定义在WebGL中我们使用右手坐标系X轴向右Y轴向上Z轴指向屏幕外一个三维点可以表示为(x, y, z)例如三角形的三个顶点就是三个这样的坐标点。2.2 原始三角形数据假设我们有一个等腰直角三角形const vertices [ // x, y, z 0.0, 0.5, 0.0, // 顶点A -0.5, -0.5, 0.0, // 顶点B 0.5, -0.5, 0.0 // 顶点C ];3. 平移变换最直观的移动3.1 平移原理平移就是给每个顶点的x、y、z坐标分别加上固定的偏移量新x 原x Δx 新y 原y Δy 新z 原z Δz3.2 JavaScript实现function translate(vertices, dx, dy, dz) { return vertices.map((v, i) { if (i % 3 0) return v dx; // x坐标 if (i % 3 1) return v dy; // y坐标 return v dz; // z坐标 }); } // 向右移动0.2向上移动0.1 const translated translate(vertices, 0.2, 0.1, 0);实操技巧在顶点着色器中直接实现平移会更高效attribute vec3 aPosition; uniform vec3 uTranslation; void main() { gl_Position vec4(aPosition uTranslation, 1.0); }4. 缩放变换改变大小4.1 缩放原理缩放就是对每个坐标值乘以比例系数新x 原x × scaleX 新y 原y × scaleY 新z 原z × scaleZ4.2 均匀缩放 vs 非均匀缩放均匀缩放scaleX scaleY scaleZ非均匀缩放各轴比例不同function scale(vertices, sx, sy, sz) { return vertices.map((v, i) { if (i % 3 0) return v * sx; if (i % 3 1) return v * sy; return v * sz; }); } // 放大1.5倍 const scaled scale(vertices, 1.5, 1.5, 1.5);5. 旋转变换最复杂的部分5.1 二维旋转绕Z轴我们先从最简单的绕Z轴旋转开始相当于在XY平面旋转新x 原x × cosθ - 原y × sinθ 新y 原x × sinθ 原y × cosθ 新z 原z 不变5.2 三维旋转分解三维旋转可以分解为绕三个轴的旋转绕X轴旋转俯仰角绕Y轴旋转偏航角绕Z轴旋转滚转角5.3 JavaScript实现function rotateZ(vertices, angle) { const rad angle * Math.PI / 180; const cos Math.cos(rad); const sin Math.sin(rad); return vertices.map((v, i) { if (i % 3 0) { // x return vertices[i] * cos - vertices[i1] * sin; } if (i % 3 1) { // y return vertices[i-1] * sin vertices[i] * cos; } return v; // z不变 }); } // 旋转45度 const rotated rotateZ(vertices, 45);6. 组合变换顺序很重要6.1 变换顺序的影响变换顺序不同会导致完全不同的结果先平移后旋转物体会绕世界坐标系原点旋转先旋转后平移物体会绕自身中心旋转6.2 组合实现function transform(vertices, tx, ty, tz, sx, sy, sz, angle) { let result [...vertices]; // 注意顺序缩放 → 旋转 → 平移 result scale(result, sx, sy, sz); result rotateZ(result, angle); result translate(result, tx, ty, tz); return result; }7. 性能优化为什么最终还是要用矩阵7.1 直接计算的缺点虽然上面的方法直观但存在两个问题多次遍历顶点数据组合变换需要多次计算7.2 矩阵的优势矩阵可以将所有变换合并为一个计算uniform mat4 uModelMatrix; void main() { gl_Position uModelMatrix * vec4(aPosition, 1.0); }7.3 从基础到矩阵的过渡现在理解了基本变换原理后再看矩阵就会明白平移矩阵将偏移量放在最后一列缩放矩阵将对角线设为缩放因子旋转矩阵使用sin/csin构造旋转部分8. 常见问题排查8.1 物体不见了可能原因平移距离太大移出视口缩放系数为0旋转后超出裁剪空间解决方案检查变换参数是否合理添加调试输出显示顶点坐标8.2 旋转中心不对默认是绕坐标系原点旋转。要绕物体中心旋转需要先将物体中心移到原点执行旋转移回原位置// 绕自身中心旋转 result translate(result, -centerX, -centerY, -centerZ); result rotateZ(result, angle); result translate(result, centerX, centerY, centerZ);9. 进阶三维旋转的实现理解了二维旋转后三维旋转就是三个二维旋转的组合9.1 绕X轴旋转function rotateX(vertices, angle) { const rad angle * Math.PI / 180; const cos Math.cos(rad); const sin Math.sin(rad); return vertices.map((v, i) { if (i % 3 1) { // y return vertices[i] * cos - vertices[i1] * sin; } if (i % 3 2) { // z return vertices[i-1] * sin vertices[i] * cos; } return v; // x不变 }); }9.2 绕Y轴旋转function rotateY(vertices, angle) { const rad angle * Math.PI / 180; const cos Math.cos(rad); const sin Math.sin(rad); return vertices.map((v, i) { if (i % 3 0) { // x return vertices[i] * cos vertices[i2] * sin; } if (i % 3 2) { // z return -vertices[i-2] * sin vertices[i] * cos; } return v; // y不变 }); }10. 实际项目中的应用建议学习阶段建议先用本文方法手动实现变换加深理解生产环境还是应该使用矩阵运算性能更好调试技巧当矩阵变换出现问题时可以用本文方法验证单个变换是否正确我在教学中发现先让学生用这种方法实现变换再引入矩阵概念理解深度会完全不同。很多之前觉得矩阵神秘的同学在明白这些基础原理后突然就开窍了。