znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第04章:数学与几何基础类型

第 3 章我们理解了 CSG 与 BSP 树的原理。本章回到代码层,逐一拆解支撑整个库的底层数学与几何类型。这些类型定义在 Vector.csVertex.csPlane.csPolygon.cs 中,是所有形体、布尔运算、变换的构件。日常使用中你未必需要直接操作它们,但理解它们的语义能让你读懂源码、诊断精度问题、并在需要时构造自定义几何。

4.1 类型全景

类型 文件 种类 作用
Vector3D Vector.cs struct 三维向量/点(double 精度,带 NaN 校验)
Vector2D Vector.cs struct 二维向量/点(用于纹理坐标、二维轮廓)
Matrix4x4 Vector.cs class 4×4 变换矩阵(平移/缩放/旋转)
BoundingBox Vector.cs class 轴对齐包围盒(AABB)
BoundingSphere Vector.cs class 包围球
OrthoNormalBasis Vector.cs class(内部) 平面上的标准正交基(2D↔3D 投影)
Line2D Vector.cs struct 二维直线
Vertex Vertex.cs class 顶点 = 位置 + 纹理坐标 + Tag
Plane Plane.cs class 平面 = 法线 + 偏移,负责多边形分割
Polygon Polygon.cs class 平面凸多边形
PolygonShared Polygon.cs class 多边形的共享属性(颜色等)
Properties Polygon.cs class 键值属性字典(可随实体变换传递)

4.2 Vector3D:三维向量与点

Vector3D 是一个 struct(值类型),三个 double 字段 X, Y, Z。它既表示空间中的,也表示向量,二者在数学上共用同一套表示。

4.2.1 构造与 NaN 校验

var p = new Vector3D(1.0, 2.0, 3.0);

一个容易被忽视但非常重要的细节:构造函数会校验 NaN。如果 xyz 中任何一个是 double.NaN,构造函数立即抛出 ArgumentOutOfRangeException

// 抛出 ArgumentOutOfRangeException,而不是让 NaN 悄悄传播
var bad = new Vector3D(double.NaN, 0, 0);

这个”早失败”设计来自上游 Csg 分支。为什么重要?因为在几何计算里,一旦 NaN 混入(例如零向量求单位向量、除以零),它会像病毒一样污染后续所有运算,最终产生难以追踪的破面。把校验放在构造入口,能让错误在源头暴露,而不是在几百次运算之后。

4.2.2 常用运算

Vector3D 提供了完整的向量代数,既有属性也有运算符重载:

var a = new Vector3D(1, 0, 0);
var b = new Vector3D(0, 1, 0);

double len   = a.Length;         // 模长
double dot   = a.Dot(b);         // 点积 = 0(垂直)
Vector3D crs = a.Cross(b);       // 叉积 = (0,0,1)
Vector3D u   = a.Unit;           // 单位向量
Vector3D neg = a.Negated;        // 取反 = (-1,0,0)
Vector3D abs = new Vector3D(-1,-2,3).Abs;  // 逐分量绝对值 = (1,2,3)

Vector3D sum  = a + b;           // (1,1,0)
Vector3D diff = a - b;           // (1,-1,0)
Vector3D had  = a * b;           // 逐分量相乘(Hadamard 积)
Vector3D sc   = a * 2.0;         // 数乘 = (2,0,0)
Vector3D dv   = a / 2.0;         // 数除 = (0.5,0,0)

Vector3D lo = a.Min(b);          // 逐分量最小
Vector3D hi = a.Max(b);          // 逐分量最大
double d2  = a.DistanceToSquared(b);  // 距离平方(省一次开方,用于比较)

注意几个约定:

  • a * b 是逐分量相乘,不是点积或叉积。点积用 .Dot(),叉积用 .Cross()
  • Vector3D * Matrix4x4 也有重载(a * matrix),用于对点施加变换(详见 4.4)。
  • DistanceToSquared 返回距离的平方——在只需要比较远近时用它,省掉一次开方,是几何库常见的性能技巧。
  • RandomNonParallelVector() 返回一个与自身不平行的向量,供构造正交基时使用。

4.2.3 相等性:精确比较

Vector3D 实现了 IEquatable<Vector3D>,其 Equals逐分量精确相等==),不带容差。这一点务必牢记:

new Vector3D(0.1 + 0.2, 0, 0).Equals(new Vector3D(0.3, 0, 0));  // 可能为 false!

因为浮点误差,0.1 + 0.2 != 0.3。库内部凡是需要”近似相等”的地方(如规范化 Canonicalize),都不是用 Vector3D.Equals,而是用带 1e-5 容差的独立逻辑。你自己写比较时也应遵循同样原则——几何相等永远要带容差

4.3 Vector2D、BoundingBox 与 OrthoNormalBasis

4.3.1 Vector2D

二维向量,字段 X, Y。主要用于纹理坐标(顶点的 Tex)和二维轮廓(型材截面,第 10 章)。除常规的 Length/Dot/Unit/Negated 外,有一个特别的属性:

var n = new Vector2D(1, 0).Normal;   // (0, -1) —— 顺时针旋转 90° 的法向

Normal 返回 (Y, -X),即把向量顺时针旋转 90°,用于在二维里求边的法向。

4.3.2 BoundingBox 与 BoundingSphere

BoundingBox轴对齐包围盒(AABB),只读的 Min/Max 两个角点,派生出:

Vector3D size   = box.Size;    // Max - Min
Vector3D center = box.Center;  // (Min + Max) / 2

包围盒在库里无处不在:布尔运算前用它快速判断两个实体是否可能相交MayOverlap),Union合并包围盒的中心做原点居中(第 3 章)。BoundingSphere 则是包围球(Center + Radius),Polygon 会缓存自己的包围球用于加速相交预判。

4.3.3 OrthoNormalBasis

OrthoNormalBasis(内部类)表示某个平面上的标准正交基,提供 To2D(Vector3D)To3D(Vector2D) 在平面局部二维坐标与世界三维坐标间互转。它是共面重划分(Retesselate) 的基础——把一堆共面三维多边形投影到二维、做扫描线合并、再投影回三维(第 12 章)。日常使用一般接触不到它。

4.4 Matrix4x4:变换矩阵

Matrix4x4 是一个 class,内部持有一个 16 元素的 double[](行主序,row-major)。它承载所有仿射变换。

4.4.1 三类基本变换的工厂方法

Matrix4x4 s  = Matrix4x4.Scaling(new Vector3D(2, 2, 2));   // 缩放
Matrix4x4 t  = Matrix4x4.Translation(new Vector3D(5, 0, 0)); // 平移
Matrix4x4 rx = Matrix4x4.RotationX(90);   // 绕 X 轴旋转 90 度(注意参数是角度,不是弧度)
Matrix4x4 ry = Matrix4x4.RotationY(45);   // 绕 Y 轴
Matrix4x4 rz = Matrix4x4.RotationZ(30);   // 绕 Z 轴

关键约定:旋转工厂方法的参数单位是角度(degree),不是弧度。库内部会自动转换为弧度。这与许多数学库不同,是 OpenCSG.NET 面向”建模易用性”的取舍。

4.4.2 矩阵复合与应用

矩阵可以用 * 复合,把多个变换合成一个:

// 先绕 Z 转 45°,再平移到 (5,0,0)
Matrix4x4 combined = Matrix4x4.Translation(new Vector3D(5, 0, 0))
                   * Matrix4x4.RotationZ(45);

// 对一个点施加变换
Vector3D p = new Vector3D(1, 0, 0);
Vector3D moved = p * combined;   // 通过 Vector3D * Matrix4x4 重载

在实体层面,你几乎不会直接摆弄矩阵——Solid 已经把常用变换封装成了 Translate/Scale/RotateX/Y/Z 等便捷方法(第 7 章)。但当你需要镜像斜切或其它自定义变换时,就要手工构造矩阵,再调用 solid.Transform(matrix)

4.5 Vertex:顶点

Vertex 是一个 class,表示多边形的一个角点:

public readonly Vector3D Pos;   // 空间位置
public readonly Vector2D Tex;   // 纹理坐标(默认为原点,STL 用不到,glTF/贴图场景才有意义)

两个字段都是只读的——顶点一经创建不可修改,这与整个库”不可变几何”的设计一致。要”修改”一个顶点,只能创建新的。

Vertex 还提供:

  • Flipped():返回一个”翻转”后的顶点(用于翻转多边形时同步翻转顶点顺序)。
  • Transform(Matrix4x4):施加变换,返回新顶点。
  • Tag:一个惰性生成的全局唯一整数(首次访问时通过 Solid.GetTag() 分配)。Tag 用于在布尔运算中缓存”这个顶点/平面变换后的结果”,避免对同一实例重复计算(第 12 章详解 Tag 缓存机制)。

4.6 Plane:平面与多边形分割

Plane 是 CSG 算法的心脏。它用法线 + 偏移表示一个无限平面:

public readonly Vector3D Normal;   // 单位法线
public readonly double W;          // 平面上任意点 p 满足 Normal · p = W

也就是说,平面方程是 Normal · p = WW 可以理解为”平面沿法线方向到原点的有符号距离”。

4.6.1 构造平面

// 由三点构造(按逆时针得到朝外法线)
Plane pl = Plane.FromVector3Ds(a, b, c);

// 翻转平面(法线反向、W 取反)——用于翻转实体内外
Plane flipped = pl.Flipped();

4.6.2 EPSILON 与 SplitPolygon

Plane 内部定义了 const double EPSILON = 1e-5,这是整个库几何比较的基准容差(第 3 章反复强调过)。核心方法 SplitPolygon 把一个多边形相对本平面分类,输出一个 SplitPolygonResult

public struct SplitPolygonResult
{
    public int Type;        // 0=共面前, 1=共面后, 2=前, 3=后, 4=横跨
    public Polygon Front;   // 落在前方的部分(横跨时为切出的前半)
    public Polygon Back;    // 落在后方的部分(横跨时为切出的后半)
    // ... 以及共面时的收集列表
}

这五种类型正是第 3 章讲的”平面与多边形的五种关系”。Type == 4(横跨)时,SplitPolygon 会在多边形与平面的交点处插入新顶点,把它切成 FrontBack 两片——这是布尔运算能产生精确新边界的根本。相等性方面,Plane 实现了 IEquatable<Plane>,判定为”法线相等且 W 相等”。

4.7 Polygon:凸多边形

Polygon 表示一个平面凸多边形,是构成实体表面的基本单元:

public readonly List<Vertex> Vertices;   // 有序顶点(逆时针,朝外)
public readonly Plane Plane;             // 所在平面
public readonly PolygonShared Shared;    // 共享属性(颜色等)

4.7.1 构造

var v0 = new Vertex(new Vector3D(0, 0, 0));
var v1 = new Vertex(new Vector3D(1, 0, 0));
var v2 = new Vertex(new Vector3D(1, 1, 0));
var v3 = new Vertex(new Vector3D(0, 1, 0));

// 平面可省略,会自动由前三个顶点推导
var quad = new Polygon(new List<Vertex> { v0, v1, v2, v3 });

如果不传 plane,构造函数会用 Plane.FromVector3Ds 从顶点自动推导所在平面。顶点顺序决定法线朝向,进而决定”哪边是实体内部”,所以手工构造多边形时顺序至关重要。

4.7.2 缓存与翻转

  • BoundingBox / BoundingSphere 都是惰性缓存的属性——首次访问时计算并缓存,供布尔运算做快速相交预判。因为多边形不可变,缓存永远有效。
  • Flipped() 返回一个顶点顺序反转、平面翻转的新多边形,用于 Invert 翻转实体内外。

4.7.3 PolygonShared 与 Properties

  • PolygonShared 保存多个多边形共享的数据(目前主要是颜色 object color),也带一个惰性 Tag。共享设计避免了重复存储、并让相同来源的多边形在布尔后仍能被识别。
  • Properties 是一个字符串到 object 的属性字典,可以 Merge(合并)和 Transform(随变换传递)。它让你能把任意元数据附着在实体上,随几何一起流转。

4.8 不可变性:贯穿始终的设计哲学

回顾本章所有类型,你会发现一条共同主线——几何对象基本都是不可变的Vector3D/Vector2D 是值类型,Vertex/Plane/Polygon 的核心字段都是 readonly。任何”修改”都通过返回新对象完成(Flipped()Transform()Translate()……)。

这带来三个实实在在的好处:

  1. 可安全复用:第 2 章我们把同一个”孔模板”平移到 4 个位置而互不影响,正是因为 Translate 返回新实体,原模板纹丝不动。
  2. 缓存永远有效:包围盒、包围球一旦算出就不会失效,因为对象不会变。
  3. 线程安全:不可变对象天然可在多线程间共享读取,无需加锁。

代价是频繁创建对象带来的分配压力——这也是第 14 章性能优化会讨论的话题(MemoryDiagnoser 基准正是为此)。

4.9 本章小结

  • Vector3D 是带 NaN 校验的三维向量/点,运算符 *逐分量相乘,点积/叉积用 .Dot()/.Cross();相等性是精确比较,几何近似必须自带容差。
  • Matrix4x4 承载平移/缩放/旋转变换,旋转参数单位是角度,可用 * 复合,用 Vector3D * Matrix4x4 施加于点。
  • Vertex = 位置 + 纹理坐标 + 惰性 TagPlane = 法线 + W,内含 EPSILON = 1e-5 与核心的 SplitPolygon(五类关系)。
  • Polygon平面凸多边形,顶点逆时针朝外,包围盒/包围球惰性缓存;PolygonShared/Properties 承载共享属性与元数据。
  • 整套类型奉行不可变设计,带来复用安全、缓存有效、线程安全三大好处。

理解了这些”零件”,下一章我们就用它们组装出最常用的三种基础形体——立方体、球体、圆柱体,并彻底讲清它们的重载、选项与分辨率。


← 上一章 目录 下一章 →