一个人写出的 3D 几何引擎:Ara3D SDK 技术深度剖析

一个由单人维护、近零外部依赖、覆盖从底层 SIMD 到 AI 协议栈的 .NET 8 几何 SDK。本文从架构、设计哲学、代码生成系统、命令引擎到 AI 集成,逐层拆解它的精妙之处。


一、这个项目是什么?

Ara3D SDK 是一个面向建筑、工程与施工(AEC)领域的 C# 3D 几何引擎,发布在 NuGet 上。它不是什么庞然大物,而是一个 单人维护 的项目,却覆盖了从底层集合库、几何算法、文件格式、BIM 数据模型,到 Roslyn 动态编译引擎和 MCP(Model Context Protocol)AI 协议接口的完整链条。

来看看它的规模:src/ 下 30+ 个项目Plato 代码生成系统产出 100+ 类型Bowerbird 命令系统支撑 Studio 和 Revit 双端脚本vendor 了 Revit 2015-2025 全系列 API + 3ds Max + Maya + Navisworks + FBX 。而所有这些依赖的外部 NuGet 包,核心库(Collections、Geometry、Utils)一个都没有

这是一份值得每一个 .NET 开发者、几何算法爱好者、以及"单人项目如何做到长生命周期可持续"的思考者细读的技术样本。


二、核心设计哲学:FP + 不可变 + 接口扩展

翻阅项目的 AGENTS.md,两条原则凌驾于一切之上:

  1. 代码必须能工作。 正确性优先。一个能工作的简单版本胜过坏掉的复杂版本。
  2. 代码必须易于改进和重构。 所有其他原则服务于这个目标。

这是典型的 现实主义工程哲学。作者没有追求"企业级抽象"和"最佳实践"的教条,而是选择了一条更务实、也更具个人风格的路线。具体的代码准则包括:

不可变扩展函数 + 最小接口

这是整个 SDK 中最高频出现的模式。看看这个:

csharp 复制代码
// 小接口,行为通过扩展函数添加
public static bool InRange<T>(this IReadOnlyList<T> self, int n)
    => n >= 0 && n < self.Count;

// 几何操作也是如此
public static TriangleMesh3D Triangulate(this QuadMesh3D mesh)
    => new(mesh.Points, mesh.FaceIndices.QuadFacesToTriFaces());

接口只定义"是什么",扩展函数定义"能做什么"。这就意味着你可以自由地添加新行为而不破坏任何调用方,同时保持了纯函数的不可变性------每个操作返回新值,不修改原数据。

函数式风格渗透到底

  • var 大量使用,类型从右侧推演
  • 早期返回(early return)替代 else
  • map/filter/fold 优于 for 循环
  • readonly struct + [StructLayout(LayoutKind.Sequential)] 无处不在
  • Debug.Assert 广泛用于不变量验证

三、Plato:用代码生成器构建数学王国

这是 Ara3D 最独特的技术决策之一。Plato 是一个独立的代码生成器(有独立的仓库),它为 SDK 生成了所有数学和几何类型。打开 src/Plato.Generated/,你会看到 100+ 个 .g.cs 文件,全部标记着 // Autogenerated file: DO NOT EDIT

生成的不仅仅是对 System.Numerics 的封装

每个生成的类型都遵循严格的结构模板,而这套模板的设计异常精巧:

csharp 复制代码
// Vector3 的生成代码(节选)
[StructLayout(LayoutKind.Sequential, Pack=1)]
public partial struct Vector3 : IVector<Vector3>
{
    // 与 C# 元组的隐式双向转换
    public static implicit operator (Number, Number, Number)(Vector3 self) => ...;

    // 所有 6 个分量置换自动生成
    public Vector3 XZY { get => (this.X, this.Z, this.Y); }

    // 签名距离场函数直接生成在 Vector3 上!
    public Number SdSphere(Number r) => this.Length.Subtract(r);
    public Number SdBox(Vector3 b) { ... }

    // 贝塞尔曲线及其导数展开
    public Vector3 CubicBezier(Vector3 b, Vector3 c, Vector3 d, Number t) => ...;
}

类型的泛型层次

Plato 生成的接口层次是精心设计的:

复制代码
IValue → IVectorLike → INumerical → IVector → Vector2, Vector3, Vector4
                        └── IInterpolatable (所有类型支持 Lerp)
                        └── IScalarArithmetic (向量 × 标量泛型)

这意味着你可以写出这样的泛型算法:

csharp 复制代码
// 对所有 IVector<T> 类型都生效
public static T LerpToHalf<T>(this T a, T b) where T : IInterpolatable<T>
    => a.Lerp(b, 0.5f);

为什么要自建而不是直接用 System.Numerics?

作者的考量很清晰。System.Numerics 的问题在于:

  1. 类型不完整 --- 有 Vector3 但没有 Point3DTriangleMesh3D 等几何概念
  2. 缺乏统一接口 --- 没有办法对"所有支持加法"的类型写泛型代码
  3. 版本脆弱性 --- .NET 6/7/8 不同版本间行为变化不可控
  4. 功能不满足需求 --- 签名距离场、贝塞尔展开、分量置换等功能需要自行实现

Plato 通过代码生成实现了 类型一致 + 接口统一 + 版本锁定 。而且生成的 SdBoxSdTorus 等 SDF 函数直接以实例方法的形式挂在 Vector3 上------这种直觉式的 API 设计,不可能在 System.Numerics 中实现。


四、集合与内存:IReadOnlyList<T> 的一等公民地位

Ara3D 的集合库是整个 SDK 的"血管系统"------所有数据以 IReadOnlyList<T> 形式传递。它回避了 IEnumerable<T> / LINQ to Objects 的惰性求值范式,转而使用一种更激进的模式。

ReadOnlyList<T> 的设计精髓

ReadOnlyList<T> 不是 List<T>------它是一个 函数 + 计数的对偶

csharp 复制代码
// ReadOnlyList<T> 只是一个 Func<int, T> 和一个 Count
public readonly struct ReadOnlyList<T> : IReadOnlyList<T>
{
    readonly Func<int, T> _getter;
    public readonly int Count;
    public T this[int index] => _getter(index);
}

关键推导:当 Select 操作应用到这个结构上时,不会分配任何新数组,只创建一个新的函数组合:

csharp 复制代码
public static ReadOnlyList<U> Select<T, U>(this IReadOnlyList<T> self, Func<T, U> f)
    => self.Count.Select(i => f(self[i]));  // 只创建新的 Func<int, U>

这个模式带来了巨大的性能优势------链式变换(Select().Where().Take())没有中间分配,没有任何 IEnumerator 装箱,没有惰性迭代器的状态机。

延伸到多维和稀疏

Ara3D.Collections 还提供了:

  • IReadOnlyList2D<T> / IReadOnlyList3D<T> --- 通过行优先索引计算的零拷贝多维视图
  • CompressedSparseRow<T> --- CSR 格式稀疏矩阵,在 Topology.cs 中用于半边的邻接查询
  • IntegerRange --- 零分配的整数范围枚举

这个设计理念贯穿了所有 SDK 库------想象一下,一个包含数百万面片的 BIM 模型的几何数据也是以这种零拷贝的方式传递的。


五、几何引擎:从三角剖分到隐式曲面

Ara3D.Geometry 是 SDK 中最大的单一库,覆盖了网格操作、拓扑分析、隐式曲面、三角剖分、空间查询、体素化、形状特征等几乎所有核心几何算法。

精选算法一览

领域 算法 文件
三角剖分 Bowyer-Watson 2D Delaunay DelaunayTriangulator.cs
多边形三角化 Ear-clipping(支持带洞多边形) PolygonTriangulator.cs
等值面提取 Marching Cubes MarchingCubes.cs
隐式曲面 SDF 函数组合 + 逆变换 Sdf3D.cs
拓扑 Half-edge 数据结构 + CSR 邻接 Topology.cs
空间索引 AABB Tree AABBTree.cs
主成分分析 协方差矩阵 + 特征分解 PrincipalComponentAnalysis.cs
形状分类 >100 个衍生特征(杆状/板状/块状) MeshFeatures.cs

一个优雅的 FP 技巧:SDF 变换

csharp 复制代码
public class Sdf3D : ITransformable3D<Sdf3D>
{
    readonly Func<Point3D, Number> _func;

    public Number Eval(Point3D p) => _func(p);

    // 对 SDF 做变换 = 对采样点做逆变换
    public Sdf3D Transform(Transform3D t)
        => new(p => Eval(t.Invert.TransformPoint(p)));
}

不需要修改 SDF 函数本身------只需要对采样点应用逆变换。当你的 SDF 是一个函数组合堆栈时,这种数学上的优雅直接转化为代码的简洁。

半边拓扑 + CSR 矩阵

Topology.cs 从三角形网格构建完整的 half-edge 结构,使用 CSR 矩阵存储顶点-面-边的邻接关系。这种数据结构支持高效的边界环检测、流形判断和欧拉特征数计算------对于 BIM 元素的空间关系分析至关重要。


六、Bowerbird:运行时 C# 编译的命令系统

Bowerbird 是整个生态中最有创意的系统。它把"插件"这个概念从 编译时 搬到了 运行时

核心思路

复制代码
CommandsRoot/
├── ExportGeometry/
│   ├── ExportGeometry.manifest.json   ← JSON 描述文件
│   ├── ExportGeometryCommand.cs        ← 纯 C# 源码
│   └── bin/
│       └── ExportGeometry-20260715T120000.dll  ← Roslyn 编译产物

Bowerbird 的工作流程:

  1. CommandCatalog 扫描文件夹,读 JSON manifest(命令名、描述、类型名)
  2. CommandCompiler 调用 Microsoft.CodeAnalysis.CSharp 编译 .cs 源码为 DLL
  3. 通过 指纹缓存(源文件哈希 + 引用文件指纹)避免无变化的重复编译
  4. CommandLoaderAssembly.LoadFile 加载 DLL,反射创建 INamedCommand 实例
  5. CommandRunner 执行命令,返回结果

应用场景

  • Ara3D Studio 的脚本系统:用户写 C# 脚本 → 编译 → 执行,无需完整的 VS 项目
  • Revit 2025 插件BowerbirdExternalCommand 让 Revit 内部也能运行动态编译的 C# 命令
  • 命令行工具Ara3D.Bowerbird.Console 提供 CLI 入口
csharp 复制代码
// 一个典型的 Bowerbird 命令
public class ExportGeometryCommand : NamedCommand
{
    public override string Name => "export-geometry";
    public override void Execute(ICommandContext context)
    {
        // 获取当前 Revit 文档/3D 场景
        // 导出为 PLY/GLTF/BFAST
    }
}

这种设计允许终端用户(Revit 建筑师、Studio 使用者)在不重新编译任何东西的前提下扩展应用行为------可以做到"写完即用"。


七、BIM 数据模型与列式存储

BOS(BIM Open Schema)是 Ara3D 定义的工具无关 BIM 数据标准化格式,设计目标非常明确:面向分析优化,而不是面向显示优化。

列式几何存储

BimGeometry 将几何数据组织为 6 个平行数组(struct of arrays):

复制代码
Instance Table:   EntityIndex, MaterialIndex, MeshIndex, TransformIndex
Vertex Table:     float[] X, Y, Z (各列独立存储)
Index Table:      int[] 三角形索引
Mesh Table:       MeshVertexOffset[], MeshIndexOffset[]
Material Table:   Red[], Green[], Blue[], Alpha[], Roughness[], Metallic[]
Transform Table:  TX[], TY[], TZ[], QX[], QY[], QZ[], QW[], SX[], SY[], SZ[]

为什么用这种模式?因为列式存储直接适配 DuckDB / Apache Parquet 的查询引擎------你可以用 SQL 对几何数据做聚合分析,比如"统计所有 MEP 元素的总体积"或"找出每个楼层所有窗户的面积"。

EAV 参数模型 + 15 种关系

参数数据使用 Entity-Attribute-Value 模式------高度稀疏的 BIM 数据不适合扁平表,EAV 是最自然的建模方式。关系层面定义了 15 种语义关系(PartOf, HostedBy, Voids, Fills, ConnectsTo, BoundedBy 等),同时覆盖 Revit 和 IFC 的语义。


八、MCP:当 3D 工具遇见 AI

wip/Ara3D.MCP 中,我们发现了一个零外部依赖的 MCP(Model Context Protocol)实现。这可能是整个项目中最"前沿"的部分。

MCP 是什么,为什么在这个项目里?

MCP 是 Anthropic 提出的开放协议,允许 AI 助手通过标准化接口与外部工具交互。Ara3D 的 MCP 实现让 AI(Cursor、Claude 等)可以直接操控 3D 场景。

csharp 复制代码
var server = new McpServer("Ara3D Studio", "1.0.0", "Ara3D 3D Studio Tools");

// 注册一个工具
server.Tool("list_entities", "列出场景中所有实体",
    args => {
        var category = args.GetString("category");
        return Results.Ok(entities.Where(e => e.Category == category));
    });

server.Start("http://127.0.0.1:8766/mcp/");

与 Bowerbird 的深度绑定

ScriptMcpTools.Refresh() 会扫描 Bowerbird 加载的所有插件,为每个脚本自动注册一个 script_{name} MCP 工具。这就形成了完整的链路:

用户写 C# 脚本 → Bowerbird 编译 → MCP 暴露为 AI 工具 → Cursor/Claude 通过自然语言调用

Bowerbird 的 REFACTOR.md 中也明确写道:"We will use the system for compiling MCP tools"------这说明 MCP 正是 Bowerbird 的下一个演进方向。


九、单人维护的工程智慧

遍历整个项目后,有些工程实践值得单独提炼出来:

1. 技术债务看得见

项目中有一整套债务追踪系统:代码中的 // TODO: 标记 + docs/TECHNICAL_DEBT.md 的条目 + docs/TODO.md 的优先级排序。不藏污纳垢,也第一时间修所有东西------而是记录、排序、在合适的时机处理。这对于单人项目是至关重要的自我管理方法。

2. 分层测试,按需执行

bat 复制代码
test.bat fast        # 跳过 I/O 密集型测试,适合高频迭代
test.bat geometry    # 只跑几何模块
test.bat ifcmesher   # IFC 网格化专项
test.bat             # 完整运行(包含 Slow 测试)

还有一个 tests/Ara3D.SDK.KnownIssues.Tests 项目------专门存放"已知会失败但尚不阻塞开发"的测试用例。当 bug 修好时,测试从 KnownIssues 移回正常项目。这种明确的用测试承载技术债务的做法,在商业化项目中也少见。

3. 度量驱动的 IFC Mesher

wip/Ara3D.Ifc.Mesher 的开发方式很值得学习:每次改动都通过 parity score 与 web-ifc 的基准输出做对比:

  • T0: 微测试(单个几何操作)
  • T1: 快速评分(典型文件)
  • T2: 深度对比(全部几何细节)
  • T3: 全量目录扫描

这不是"先写完再修 bug"的模式,而是用度量导航开发。每个 WP(工作包)完成时都有一个可量化的质量指标。

4. 零依赖的价值被反复证明

核心库(Collections、Geometry、Utils)没有引入任何 NuGet 包------只有 .NET 8 BCL。这意味着不会出现"NuGet 整活导致整个 SDK 编译不过"的问题,也意味着每个文件都可以被独立编译和理解。


十、总结:一个"小而全"的标杆

Ara3D SDK 不是一个用来炫耀技术广度的玩具项目。它是一个已经跑在生产环境(NuGet 发布、Studio 使用、Revit 集成)的真实产品。它的存在证明了几个重要的命题:

  1. 代码生成不是过度工程------当领域模型足够复杂(100+ 类型、200+ 曲线/形状),代码生成器是唯一能保证一致性和可维护性的方案。

  2. IReadOnlyList<T> + 扩展方法 这种 FP 模式在 C# 中远比想象中强大------它提供了 LINQ 的表达力,却避免了 IEnumerable 的分配开销和惰性求值的副作用。

  3. 单人项目可以做到"大而全"------只要坚持零外部依赖、用代码生成降低维护成本、用度量驱动质量、并诚实地记录技术债务。

  4. MCP + Bowerbird 的组合展示了 3D 工具与 AI 的集成方向------用户写的 C# 脚本自动暴露为 AI 工具,这是自然语言操控 3D 软件的可行路径。

相关推荐
逝水无殇14 小时前
C# 异常处理详解
开发语言·后端·c#
玖玥拾15 小时前
C# 语言进阶(十五)C# 游戏服务端 MySQL 数据库
服务器·开发语言·网络·数据库·mysql·c#
逝水无殇17 小时前
C# 文件的输入与输出详解
开发语言·数据库·后端·c#
唐青枫1 天前
看懂 IL 的关键:C#.NET 栈机执行模型实战拆解
c#·.net
我才是银古1 天前
从零构建 Windows 桌面 RPA 框架:Go 与 .NET 的跨语言协奏
c#·go·rpa
逝水无殇1 天前
C# 正则表达式详解
开发语言·后端·正则表达式·c#
朱永博1 天前
使用Onnruntime实现Padim/Patchcore推理
开发语言·c#
吴可可1231 天前
C# CAD二次开发添加带按钮的窗口
c#
铅笔侠_小龙虾1 天前
Rust 学习(8)-切片类型
学习·rust·c#