第07章:几何变换——平移、旋转与缩放
布尔运算负责”组合”,几何变换负责”摆放”。要把一个圆柱旋转到竖直、把配重球平移到杆的两端、把整个零件缩放到目标尺寸,都离不开变换。本章讲透 Solid 上的 Translate/Scale/RotateX/Y/Z/Transform 方法、它们背后的 Matrix4x4、旋转的坐标系语义、镜像的正确姿势,以及变换内部的 Tag 缓存优化。
7.1 变换方法总览
Solid 提供了一组便捷变换方法,全部返回新实体(不可变),因此可以链式调用:
| 方法 | 重载 | 作用 |
|---|---|---|
Translate |
Translate(Vector3D) / Translate(x, y, z) |
平移 |
Scale |
Scale(double) / Scale(x, y, z) / Scale(Vector3D) |
缩放 |
RotateX |
RotateX(degrees) |
绕 X 轴旋转(角度) |
RotateY |
RotateY(degrees) |
绕 Y 轴旋转(角度) |
RotateZ |
RotateZ(degrees) |
绕 Z 轴旋转(角度) |
Transform |
Transform(Matrix4x4) |
施加任意 4×4 变换矩阵 |
前面五个方法本质上都是”构造对应的 Matrix4x4 再调用 Transform“的语法糖。理解这一点,你就明白所有变换最终都归结为矩阵乘法。
7.2 平移 Translate
平移最简单——把实体沿各轴移动指定距离:
var cube = Cube(size: 1, center: true);
var right = cube.Translate(x: 2); // 沿 +X 移动 2
var forward = cube.Translate(y: 2); // 沿 +Y 移动 2
var up = cube.Translate(z: 2); // 沿 +Z 移动 2
var diag = cube.Translate(1, 1, 1); // 三个方向同时移动
var byVec = cube.Translate(new Vector3D(5, 0, 0));
Translate(double x = 0, double y = 0, double z = 0) 的三个参数都有默认值 0,所以你可以只指定关心的轴(用命名参数 x:/y:/z:),其余保持不动。这在实践中非常常用,比如第 5 章的哑铃就用 weight.Translate(y: -0.9) 只沿 Y 移动。
7.3 旋转 RotateX/Y/Z
三个旋转方法分别绕 X、Y、Z 轴旋转,参数单位是角度(degree),不是弧度:
var cyl = Cylinder(r: 1, h: 5, center: true); // 默认沿 Y 轴
var alongZ = cyl.RotateX(90); // 绕 X 转 90° → 变成沿 Z 轴(竖直站立)
var alongX = cyl.RotateZ(90); // 绕 Z 转 90° → 变成沿 X 轴
var tilt = cyl.RotateY(45); // 绕 Y 转 45°
7.3.1 关键:旋转是绕”过原点的世界坐标轴”
这是旋转最重要、也最容易出错的语义:RotateX/Y/Z 是绕世界坐标系中过原点的对应轴旋转,而不是绕实体自身的中心。
这意味着:如果实体不在原点,旋转会同时改变它的朝向和位置(因为它绕着远处的原点轴”公转”)。例如:
// 一个远离原点的方块
var box = Cube(size: 1, center: true).Translate(x: 10);
// 绕 Z 轴转 90°——box 会以原点为圆心画弧,跑到 (0,10,0) 附近,而不是原地自转!
var rotated = box.RotateZ(90);
正确的”原地自转”姿势:先在原点完成旋转,最后再平移到目标位置。这就是为什么建模时应该尽量让形体在原点附近成型,把定位放到最后一步:
// 推荐:先在原点旋转成型,再平移就位
var part = Cube(size: 1, center: true)
.RotateZ(45) // 在原点原地自转
.Translate(x: 10); // 最后就位
7.3.2 组合旋转的顺序
多个旋转链式调用时,顺序会影响结果(三维旋转不满足交换律):
var a = solid.RotateX(90).RotateZ(90);
var b = solid.RotateZ(90).RotateX(90);
// a 与 b 通常不同!
链式调用是”从左到右依次施加”。如果结果不符合预期,试着调换旋转顺序,或用纸笔/坐标轴模型推演一下每一步的朝向。
7.4 缩放 Scale
缩放可以整体等比,也可以各轴不等比:
var half = solid.Scale(0.5); // 三轴同时缩小一半
var wide = solid.Scale(2, 1, 1); // 只在 X 方向拉伸 2 倍(变"扁长")
var byVec = solid.Scale(new Vector3D(1, 1, 3)); // Z 方向拉高 3 倍
同旋转一样,缩放也是相对世界原点的:不在原点的实体缩放后,其位置(到原点的距离)也会按比例变化。要”原地缩放”,同样先在原点缩放、再平移。
7.5 Transform 与自定义矩阵
当便捷方法不够用时(斜切、绕任意轴旋转、镜像),可以直接构造 Matrix4x4 并调用 Transform:
// 复合矩阵:先绕 Z 转 30°,再平移到 (5,0,0)
var m = Matrix4x4.Translation(new Vector3D(5, 0, 0))
* Matrix4x4.RotationZ(30);
var moved = solid.Transform(m);
矩阵的 * 复合遵循标准的”右侧先作用”约定:A * B 表示先施加 B 再施加 A。把多个变换预先复合成一个矩阵,再一次性 Transform,在批量变换时比逐个调用更高效。
7.6 镜像:需要显式设置 IsMirroring
镜像(对称翻转)是个特例,OpenCSG.NET 没有提供 Mirror 便捷方法,需要你自己构造矩阵。这里有一个必须注意的细节:
Matrix4x4 有一个公开字段 IsMirroring(默认 false)。当 Transform 检测到 IsMirroring == true 时,会做两件额外的事来保持实体的法线朝外、内外一致:
- 反转每个多边形的顶点顺序(
newvertices.Reverse()); - 翻转每个平面(
plane.Flipped())。
问题在于:Matrix4x4.Scaling(...) 工厂并不会自动把 IsMirroring 设为 true,即使你传入了负的缩放值。所以,直接 solid.Scale(-1, 1, 1) 做镜像,会得到一个法线方向不一致的实体(几何翻了,但内外没翻),后续布尔运算可能出问题。
正确的镜像做法是手动构造矩阵并设置 IsMirroring:
// 沿 YZ 平面镜像(X 取反)——正确保持法线一致
var mirror = Matrix4x4.Scaling(new Vector3D(-1, 1, 1));
mirror.IsMirroring = true; // 关键:显式声明这是镜像变换
var mirrored = solid.Transform(mirror);
记住这条:用负缩放做镜像时,一定要把 IsMirroring 设为 true,否则结果实体的内外朝向会错乱。
7.7 变换内部的 Tag 缓存优化
理解 Transform 的内部实现,能帮你写出更高效的代码,也是通往第 12 章源码剖析的桥梁。Transform 并不是简单地”遍历所有顶点逐个变换”,而是用了基于 Tag 的缓存:
- 它维护两个字典
transformedvertices和transformedplanes,键是顶点/平面的 Tag(第 4 章讲过的全局唯一整数)。 - 变换某个顶点前,先查缓存:如果这个顶点(按 Tag 识别)之前已经变换过,直接复用结果,不重复计算。
为什么这有意义?因为在一个实体里,相邻多边形会共享顶点——同一个顶点实例被多个多边形引用。没有缓存的话,一个被 3 个面共享的顶点会被变换 3 次;有了 Tag 缓存,只算一次。对高面数模型,这能显著减少浮点运算量。
此外,Transform 会把结果实体的 IsRetesselated / IsCanonicalized 标志继承自源实体——因为纯变换(平移/旋转/等比缩放)不会破坏”已规范化”的状态,无需重新规范化。这是又一处惰性优化。
7.8 综合示例:把变换和布尔结合起来
变换很少单独使用,它总是和布尔运算配合,把形体摆到正确位置再组合。下面用变换 + 布尔做一个”带四条腿的桌子”雏形:
using Csg;
using static Csg.Solids;
double topW = 20, topD = 12, topH = 1; // 桌面尺寸
double legH = 10, legR = 0.6; // 桌腿高与半径
double inset = 1.5; // 桌腿相对桌面边缘的内缩
// 桌面:一块扁长方体,顶面放在 z = 0,往下不重要,先居中再抬高
var top = Cube(new Vector3D(topW, topD, topH), center: true)
.Translate(z: legH + topH / 2); // 抬到桌腿之上
// 一条桌腿模板:圆柱默认沿 Y,转成沿 Z(竖直),底面放在 z=0
var legTemplate = Cylinder(r: legR, h: legH, center: false) // 从 y=0 到 y=legH
.RotateX(90); // 转成从 z=0 向上
// 四条腿:把模板平移到桌面四角内缩位置
double lx = topW / 2 - inset;
double ly = topD / 2 - inset;
var legs = Union(
legTemplate.Translate(x: +lx, y: +ly),
legTemplate.Translate(x: -lx, y: +ly),
legTemplate.Translate(x: +lx, y: -ly),
legTemplate.Translate(x: -lx, y: -ly)
);
// 桌面 + 桌腿
var table = Union(top, legs);
using var w = new StreamWriter("table.stl");
table.WriteStl("table", w);
这个例子把本章要点都用上了:用 RotateX(90) 修正圆柱朝向、用 Translate 把桌腿模板复制到四角、用 Translate(z: ...) 把桌面抬到正确高度、最后用 Union 组合。注意桌腿模板被复用了四次——不可变实体的又一次实践。
7.9 本章小结
Solid的Translate/Scale/RotateX/Y/Z都是构造Matrix4x4再调Transform的语法糖,全部返回新实体、可链式调用。- 旋转/缩放都是相对世界原点的:不在原点的实体会连位置一起改变。要”原地”变换,先在原点成型、最后平移就位。
- 旋转参数单位是角度;多个旋转顺序影响结果。
- 镜像要手动构造矩阵并设
IsMirroring = true,否则法线内外会错乱——Scaling工厂不会自动设置它。 Transform用 Tag 缓存避免重复变换共享顶点/平面,并继承规范化标志,是重要的性能优化。- 变换与布尔配合,才能完成真实建模(如桌子示例)。
下一章讲把成果落地——如何把 Solid 导出为 ASCII 或二进制 STL 文件,以及三角化的原理与常见坑。