Skip to content

3D 笔刷技术与游戏内世界雕塑

我们想让玩家去雕塑世界。不是在网格上摆放预制件,不是切换方块的开关,而是真正地重塑地形:开凿河道、隆起山脉、磨平崖面、挖出洞穴。就像 ZBrush 和 Blender 的雕刻模式为艺术家所做的那样,但要在多人浏览器游戏里以 60fps 运行。

这是一个困难的工程问题,跨越数据表示、网格提取、GPU 计算、笔刷数学和网络同步。本指南把我们找到的所有内容都记录下来。

地形表示的两个世界

每个雕塑系统都从地形数据如何存储这个选择开始。这个选择决定了可能进行的编辑类型、运行速度和占用内存。

高度图

高度图为每个网格点存储一个高度值。你可以把它想象成一张灰度图,亮度等于高程。我们当前的 world/client 地形就是这样工作的:noise.ts 通过 FBM 值噪声生成高度,每个 Chunk 存储一个 Float32Array 高度图,投影到 PlaneGeometry 上。

高度图很快。采样只需一次数组查找加双线性插值。LOD 很简单,因为你只需降低网格分辨率。基于 splat 的纹理混合直接映射到 UV 网格。物理碰撞简化为一次高度查询。

限制在于拓扑。高度图每个 (x, z) 坐标只能表示一个高度。没有洞穴,没有悬挑,没有拱门,没有隧道。如果玩家雕出一个回折的悬崖,高度图无法存储。对于以丘陵和山脉为主的地形,这没问题。但对于玩家能向下挖掘的自由式雕塑,这是死路。

体积(3D 标量场)

替代方案是为 3D 空间的每个点存储一个值。如果该值在固体材质内为负、在外为正(或反之),你就得到了一个 Signed Distance Field(有符号距离场,SDF)。如果该值只是一个密度(高于某阈值为固体,低于为空),你就得到了一个密度场。

体积表示可处理任意拓扑。洞穴、悬挑、漂浮岛屿、贯穿山体的隧道。代价是内存和复杂度。256^3 网格使用 32 位浮点要 64 MB。512^3 要 512 MB。而这只是单个 chunk。你需要稀疏数据结构(八叉树、brick map)才能实用。

网格提取这一步也不简单。你不能只设置顶点 Y 位置就完事。你需要一个算法读取标量场,生成一个三角形网格来逼近场值跨零的表面。

网格提取:Marching Cubes、Surface Nets 与 Dual Contouring

Marching Cubes

Marching Cubes 是最古老也最广泛实现的等值面提取算法。由 Lorensen 和 Cline 于 1987 年发表,它通过检查体素网格中每个立方体来工作,每个立方体的角点都有一个标量值。如果一些角点在表面内(负)、一些在外(正),就会在该立方体内放置一片三角网格。

每个立方体有 8 个角点,每个要么在内要么在外,产生 256 种可能配置(2^8)。通过对称性归约为 15 种独特情况。一张查找表将每种情况映射到一组三角形。边的交点通过沿符号变化的边线性插值得到。

最近的 GPU 实现让 Marching Cubes 快到足以支持实时雕塑。2025 年在 UE5 上的实现为每个 GPU 线程分配一个立方体,同时处理数千个。关键洞见是每个立方体的三角化与邻居无关,让算法尴尬地并行。

MCHex(arxiv 2511.02064,2025)把 Marching Cubes 扩展到自适应六面体网格生成,保证正雅可比值,改进了仿真网格的边界逼近。

rupMC 使用 CPU/GPU 异构架构,比串行实现快几十倍,比并行 DMC 变体快 4 倍。

主要限制:Marching Cubes 处理尖锐特征不好。90 度边缘会被磨成平滑曲线。对地形雕塑这通常可以接受(自然地形大多平滑),但对建筑特征就是个问题。

Surface Nets

Surface Nets 是一族较新的算法,从离散标量场产生更平滑的网格。它不是像 Marching Cubes 那样把顶点放在立方体边上,而是为每个包含表面的立方体放一个顶点,然后连接相邻顶点形成四边形。

结果天然更平滑。2024 年一篇论文(arxiv 2401.14906)演示了一个高性能并行 Surface Nets 实现,比串行算法快一到两个数量级。fast-surface-nets Rust 库在单核 2.5 GHz 上每秒生成约 2000 万三角形,使用小型查找表和 SIMD 加速。

bevy-sculpter(v0.18.0,2026 年 1 月)使用 Surface Nets 作为主要网格化策略。该库提供基于 SDF 的体积雕塑,有四种笔刷类型:硬 CSG(瞬时增/删)、平滑连续(用于持续输入)、模糊(表面平滑)和压平(设置为目标高度)。它还包括通过 Fast Sweeping Method 的 SDF 重定距离,在编辑后恢复正确的 SDF 属性。

Surface Nets 是 Marching Cubes(简单、快速,但二值数据上产生锯齿网格)和 Dual Contouring(保特征但复杂)之间的好中点。

Dual Contouring

Dual Contouring 保留 Marching Cubes 和 Surface Nets 无法保留的尖锐特征。它的做法是不仅使用每个角点的场符号,还使用边交点处的梯度(法线)。一个 QEF(Quadratic Error Function,二次误差函数)最小化把顶点放在每个单元内最能满足所有边交点约束的位置。

结果:提取的网格中保留尖锐边缘和角点。具有垂直面的立方体仍然是立方体。

代价是复杂度。QEF 求解有跨单元依赖,让 GPU 并行化变难。它可能产生非流形网格(边被两个以上多边形共享)。实现也比 Marching Cubes 更复杂,不过 Johannes Jendersie 指出,一个可用的 Dual Contouring 实现约 200 行代码,而稳健的 Marching Cubes 需要 500 行以上。

**Cubical Marching Squares(CMS)**被提出作为中间方案:跨单元独立(GPU 友好),同时仍提供一定的特征保留。

该用哪个

对于浏览器中面向玩家的地形雕塑:

Surface Nets 是最强候选。它产生平滑网格(看起来自然的地形),没有 Marching Cubes 在二值数据上的锯齿。它对实时重新网格化足够快。而且比 Dual Contouring 更容易实现。

Marching Cubes 在 GPU 并行优先(每个立方体独立)或需要最广泛的库支持时仍是稳妥之选。Reinder Nijhoff 的 WebGPU SDF Editor(2026 年 1 月)在其提取流水线中实现了 Marching Cubes 和 Surface Nets,全部在 GPU 上运行。

Dual Contouring 最适合建筑精度比性能更重要的场景。不适合浏览器环境中的实时地形雕塑。

Transvoxel 算法:解决 LOD 拼接

体素地形以不同分辨率(LOD0 靠近玩家,LOD2 远离)网格化时,边界处会出现裂缝。对于高度图,这是个简单问题:插值边缘顶点以匹配较低分辨率的邻居。我们当前的 chunk.tsstitchEdge() 中正是这样做的。

对于体积地形,问题要难得多。LOD0 的洞口可能在边界上产生 30 个三角形。同一区域在 LOD1 可能产生 8 个完全不同拓扑的三角形。没有简单办法在它们之间线性插值。

Eric Lengyel 的 Transvoxel 算法(2009)通过「过渡单元」解决了这个问题。在两个 LOD 等级的边界上,算法考虑 9 个高分辨率样本(而不是 8 个立方体角点),产生 512 种可能配置,归入 73 个等价类。每个类对应一个预定义的三角形图案,完美填补两个分辨率之间的间隙。

该算法在局部体素数据上运行,所以重新三角化修改区域很快。这对实时雕塑至关重要:当玩家在 LOD 边界附近编辑地形时,只需重建过渡单元。

存在一个 Rust 实现 transvoxel crate。原始查找表可在 transvoxel.org 获取。

笔刷数学

雕塑笔刷是一个函数,它修改目标点周围半径内的标量场值。从 Blender 到虚幻引擎到运行时游戏系统,所有实现中的数学都惊人地相似。

衰减函数

笔刷衰减决定编辑强度如何从中心到边缘下降。Blender 5.1 定义了这些标准曲线:

Smooth: f(d) = 3d^2 - 2d^3(Hermite 插值,与我们 noise.ts 中相同的 smoothstep)

Sphere: 中心强,边界附近陡降。近似为 f(d) = sqrt(1 - d^2)

Sharp: f(d) = (1 - d)^n,n > 2。形成尖锐点。

Linear: f(d) = 1 - d,其中 d 是距中心的归一化距离(中心为 0,边缘为 1)。

Constant: d < 1f(d) = 1,在笔刷边界处硬截断。

Inverse Square: smooth 与 sphere 的混合,带有自然的「黏土感」。

所有情况下,d = distance_to_center / brush_radius,截断到 [0, 1]。衰减值乘以笔刷强度产生每个点的实际场修改。

衰减空间

Blender 区分球形衰减(3D 世界空间中计算距离)和投影衰减(2D 屏幕空间中计算距离)。投影衰减意味着屏幕上看起来近的两个点会同等地影响彼此,即便它们在世界空间中深度差异很大。对于地形雕塑,3D 世界空间衰减通常更直观。

核心笔刷操作

Raise/Lower(位移): 在笔刷半径内按衰减加权地从标量场加减。对于高度图:height[i] += strength * falloff(d)。对于 SDF:sdf[i] -= strength * falloff(d)(减小让材质更实心,抬升表面)。

Smooth(拉普拉斯): 把每个值替换为其邻居的加权平均,按衰减加权。这能擦除细节、降低噪声。拉普拉斯滤波器采样一个小核(高度图 3x3,体积 3x3x3)并向均值混合。HC(Humphrey's Classes)平滑是一种比原始拉普拉斯更好保留体积的变体。

Flatten: 把场值设为目标高度(或 SDF 空间中的距离),按衰减混合。目标通常在笔画开始时在笔刷中心采样,然后保持不变。这能形成平台。

Pinch/Inflate: 沿表面法线向内或向外移动顶点。在 SDF 空间中,这等同于沿梯度方向位移。

Grab: 平移场的一个区域,就像拉黏土。位移向量是鼠标增量投影到世界空间,应用于半径内的场值。

Noise: 在笔刷半径内向场添加程序化噪声。用于让平滑表面变粗糙。

Stamp: 把 2D 灰度图作为位移应用,投影到光标下的表面上。虚幻引擎的 Landscape 工具为地形笔刷支持这个。

自适应细分

sculpt-3D(React + Three.js 浏览器雕塑)实现了自适应细分:当笔刷在网格上移动时,笔刷中心附近的三角形会被细分,以提供更多顶点供形变。这避免了「低多边形拉伸」问题——粗糙网格被雕塑扭曲。细分使用对称分割来保持三角形质量均匀。

对于体积系统,并不需要同样方式的自适应细分,因为网格是从场重新生成的。相反,你可以在编辑附近局部增加体素分辨率(自适应八叉树)来达到同样效果。

SDF 雕塑:Dreams 的方法

Media Molecule 的 Dreams(PS4,2020)是迄今为止发布过的最雄心勃勃的游戏内雕塑系统。Alex Evans 在 SIGGRAPH 2015 上介绍了技术方法。

表示

Dreams 把几何存储为 83^3 fp16 体积纹理块中的复合 SDF 函数。每个雕塑是 1 到 100,000 个「编辑」组成的列表,每个编辑是一个 CSG 操作(加、减、上色),带一个原始形状(球体、立方体、圆柱、圆锥、椭球、环面等)和混合模式。

混合模式使用 soft-max 和 soft-min 函数。「软」混合产生原始体之间的圆角过渡(像挤在一起的黏土)。「硬」混合产生干净的布尔切割。混合半径可由用户控制。

渲染

Dreams 不提取三角网格。相反,它使用自定义点云渲染器(「flecks」)直接从 SDF 渲染。每个 fleck 是一个沿表面法线定向的小圆盘。SDF 被采样以找到表面,flecks 分布在它们上面。这完全避免了网格提取瓶颈,但需要自定义渲染器。

对于 Three.js/WebGL 世界,这种方法不能直接应用。我们需要提取网格。但 CSG 编辑列表概念对于撤销/重做和网络同步高度相关。

Mike Turitzin 的动态 SDF 引擎(2026)

Mike Turitzin 目前正在开发的一个游戏引擎使用动态 SDF 作为核心表示。该引擎支持:

游戏过程中的详细修改: 平滑地或带尖锐边缘地添加和移除物质。非破坏性更改,例如移动洞或在玩家身后创建临时隧道。

Brick map 和 brick atlas用于稀疏缓存。SDF 场不是存储在密集 3D 网格中,而是被分成「砖」(小的 3D 瓦片)。只为包含表面边界的砖分配空间。这能极大减少大量空旷或纯固体场景的内存。

Geometry clipmap(Losasso 与 Hoppe,SIGGRAPH 2004)用于 LOD。围绕相机位置的嵌套规则网格分辨率递增。最内层网格分辨率最细,外层逐渐变粗。这在支持广阔空间的同时大幅减少内存。Clipmap 随着相机移动增量更新,对流式开放世界很高效。

物理与碰撞直接对 SDF 工作。Sphere-tracing(用 SDF 距离作为步长的光线步进)提供高效的光线投射。碰撞检测使用 SDF 梯度作为表面法线,距离值作为穿透深度。

Teardown:规模化的体素破坏

Teardown(Voxagon)代表了光谱的另一端:世界中的每个物体都是一个体素体,可逐块破坏。

架构

物体以规则间距的体素网格存储。引擎不使用 Marching Cubes 或 SDF 渲染,而是在片元着色器中使用改良的 DDA(Digital Differential Analyzer)算法直接对体素光线追踪,构建在 OpenGL 3.3 之上。Mipmap 形成密集八叉树结构,加速光线相交中的空区域遍历。

对每个物体,引擎光栅化其朝向边界盒(OBB)并通过它光线追踪,找到体素相交。只渲染 OBB 的背面,允许相机剪入边界体内。

破坏同步(多人)

Teardown 2026 年 3 月的多人更新使用半确定性方法。结构性破坏(切洞、改变所有权、重新连接关节)通过在可靠网络流上使用定点整数数学处理。所有客户端执行相同的确定性命令,达到相同的世界状态。非结构性变化(碎片、粒子)使用不可靠的状态同步。

这对我们的多人世界是重要洞见:地形编辑必须是确定性的。如果玩家 A 雕出一座山,所有客户端必须从相同的场数据产生相同的网格。编辑命令(笔刷位置、半径、强度、操作类型)应当是权威数据,而不是结果网格。

ALICE-SDF:压缩与 CSG 树

ALICE-SDF(Adaptive Lightweight Implicit Compression Engine,v1.3.0,2026 年 3 月)提供了基于 SDF 的空间数据的 Rust 实现,相比多边形网格压缩 10-1000 倍。它支持:

126 个构建块: 72 个原始体、24 个操作、7 个变换和 23 个修饰符。平滑混合操作(并、差、交)、用于硬边斜角和阶梯式 CSG 过渡的 chamfer 和 stairs 混合。

CSG 树差异/补丁用于撤销/重做和网络同步。这是多人雕塑的关键功能:不发送整个场状态,而是发送两个 CSG 树之间的结构差异。客户端应用补丁重建新状态。这比对原始体素数据增量压缩高效得多。

CSG 树优化包括恒等变换移除、嵌套变换合并和修饰符降级。在编辑累积时保持树的紧凑。

网格生成通过 Marching Cubes 和 Dual Contouring 两者。物理碰撞检测直接对 SDF 工作。

WebAssembly 支持让浏览器集成成为可能。引擎用 Rust 写成,带 WASM 绑定,是 Three.js 应用的现实选项。

用于地形的 WebGPU Compute

WebGPU 自 2025 年末起在所有主流浏览器中可用。Chrome 113+、Edge 113+、Firefox 141+ 和 Safari 26+ 都默认启用。这开启了以前仅 GPU 才能做的 compute shader 流水线。

性能

WebGPU compute shader 通过大规模并行生成地形比 CPU 方法快约 100 倍。GPU 同时执行数千次计算,地形生成几乎完全并行(每个顶点/体素独立)。

工作分三个层级:Dispatch Level(GPU 上的工作量分配)、Workgroup Level(处理单元内的共享内存)和 Thread Level(单个计算)。WGSL(WebGPU Shading Language)是着色器语言。

实时地形雕塑流水线

WebGPU 雕塑流水线大概如下:

  1. 笔刷应用(compute shader):更新笔刷半径内的标量场值。每个线程处理一个体素。从 uniform buffer 读取笔刷参数(位置、半径、强度、衰减类型、操作)并应用修改。

  2. 网格提取(compute shader):在修改区域上运行 Surface Nets 或 Marching Cubes。Nijhoff 的 WebGPU SDF Editor 将其实现为多阶段流水线:跨 16,384 个单元的空间划分、基于八叉树的单元分割、然后是表面提取。

  3. 顶点缓冲区更新(GPU 侧):把提取的顶点直接写入渲染缓冲区,不需要往返 CPU 内存。

  4. 法线计算(compute shader):从网格或从 SDF 梯度计算顶点法线。

  5. 渲染(标准流水线):用标准 PBR 材质绘制网格。

步骤 1-4 都可以在 GPU 上运行,没有数据返回 JavaScript。CPU 每帧只需发送笔刷参数。

WebGPU SDF Editor

Reinder Nijhoff 的 WebGPU SDF Editor(2026 年 1 月)演示了这种方法在 Chrome 中运行。它支持六个原始体(cone、cylinder、capsule、torus、box、sphere)、三个混合操作(union、subtraction、intersection)带可配置的平滑混合,以及分层场景图。每个原始体在单个 GPU 缓冲区中占 112 字节。

渲染流水线使用 1,024 个阴影贴图做环境光遮蔽和时间反走样。这在高端 GPU 上以交互式帧率运行。

高度图雕塑:更简单的路径

如果不需要洞穴和悬挑,高度图雕塑可以避开整个体积流水线。这是大多数已发布游戏处理地形编辑的方式。

运行时实现模式

Unity Runtime Terrain(JohannHotzel,2026 年 1 月)展示了标准模式:

  1. 从相机通过鼠标位置光线投射,找到地形命中点。
  2. 将命中点映射到高度图坐标。
  3. 应用笔刷到附近的高度图值,按衰减加权。
  4. 更新网格,根据修改后的高度图设置顶点 Y 位置。
  5. 重建物理碰撞体以匹配新网格。

对我们的 Three.js 地形,步骤 1-4 直接映射到现有架构。Chunk 类已经存储高度图并从中构建网格。增加雕塑意味着:

  • 一个针对 chunk 网格的光线投射系统(Three.js Raycaster
  • 修改 chunk.heightmap 值的笔刷应用函数
  • 网格顶点更新(设置 Y 位置,重新计算法线)
  • 对受影响区域重新计算 splat map(让纹理混合反映新的坡度/高度)
  • 通过现有 WebSocket 协议把编辑(笔刷位置、半径、强度、操作)广播给其他客户端

Clipmap 方法

Landow.dev 描述了一种用于高度图地形的「wandering clipmap」:跟随玩家、具有可变细分密度的单个网格。不是把高度图分成不同 LOD 等级的独立网格(我们现在的做法),clipmap 是一个连续网格,靠近相机处密,远处稀。

这完全消除了 LOD 拼接。网格在需要的地方有更多三角形,不需要的地方更少。代价是雕塑需要更新单个大网格,而不是单个 chunk,对大编辑可能很昂贵。

非破坏性 SDF 高度图

Landow.dev 还描述了一种技术:高度图本身从 SDF 构成生成。形状实例(球、立方体、噪声函数)在 compute shader 中使用 CSG 操作组合,输出作为高度图采样。这提供非破坏性编辑(你可以在任何时候移动或删除任何形状实例),同时保持高度图渲染的简单性。

这是一个有吸引力的混合:数据表示是体积的(SDF CSG 树),但渲染路径是标准高度图网格。你从 SDF 一侧获得撤销/重做和网络友好的编辑操作,从高度图一侧获得简单渲染和物理。限制依然存在:没有洞穴或悬挑。

用于大型世界的稀疏体素八叉树

密集 3D 网格无法规模化。每边 1 km、分辨率 0.5m 的世界需要 80 亿体素。Sparse Voxel Octree(SVO)通过递归细分空间、仅为包含表面边界的八分体分配存储来解决。

SVO 天然提供分层 LOD:任意点的树深度决定有效分辨率。靠近玩家处树完全展开(最大细节)。远处在较粗等级截断。

对于渲染,SVO 可直接光线追踪(不需网格提取)。GPU ray marcher 在每个树层上与轴对齐盒子相交,完全跳过空子树。这消除了基于 chunk 渲染的过度绘制问题,并避免了 greedy meshing 的伪影。

AdamYuan 基于 Vulkan 的 SVO 构建器演示了显著性能:GTX 1660 Ti 上 Crytek Sponza 在 2^10 分辨率下 19ms 构建时间。

对于雕塑,SVO 修改高效:只需更新笔刷半径内的叶节点,树结构自然处理变化的分辨率。在玩家雕塑处添加细节(拆分节点到更高分辨率)、在他们平滑处移除细节(合并节点到更低分辨率),都是数据结构自然带来的。

浏览器部署的挑战在于 WebGL 不支持 compute shader,而虽然 WebGPU 支持,但 SVO 构建和遍历算法在 WGSL 中实现起来复杂。

多人雕塑的网络同步

我们的世界已经通过 Cloudflare Durable Objects(world-chunk-do.ts)实现了多人。增加雕塑意味着在所有连接客户端之间同步地形修改。

差量压缩

发送原始体素数据成本很高。奥卢大学 2024 年的一项研究通过组合 delta 编码与 DEFLATE 压缩,实现了 2-8 倍的载荷改进,体素更新打包到每个体素不到一字节。SDEC 编解码器演示了位打包 delta 编码,产生平均 259 字节的包,对比通用序列化的 1114 字节。

基于操作的同步(推荐)

不要同步场状态,而是同步操作。每个雕塑动作变成一条消息:

typescript
interface TerrainEditMsg {
  t: MsgType.TerrainEdit
  brush: {
    position: [number, number, number]
    radius: number
    strength: number
    falloff: 'smooth' | 'linear' | 'sharp' | 'constant'
    operation: 'raise' | 'lower' | 'smooth' | 'flatten' | 'noise'
    targetHeight?: number
  }
}

服务器把这个广播给所有客户端,每个客户端对其本地地形数据应用相同的确定性笔刷操作。这与 Teardown 用于结构性破坏的方法相同:可靠流上的确定性命令。

ALICE-SDF 的 CSG 树差异/补丁更进一步:不是单个笔画,差异表示整个 CSG 树的结构变化。这能在网络上高效地撤销/重做(发送逆向补丁),后加入的客户端可通过重放操作日志重建完整世界状态。

优先级与节流

靠近已连接玩家的地形编辑应是高优先级(立即广播)。远离任何玩家的编辑可批量低频发送。Enshrouded 的体素网络使用这种模式:玩家附近地形 60Hz 更新,背景区域 10Hz。

线上 ZSTD 压缩可把地形更新消息的包大小减少 60%。

协作雕塑:并发编辑

当多个玩家同时雕塑同一区域时,你需要冲突解决。cSculpt(CNR 视觉计算实验室,2016)用多分辨率合并算法解决了这一点。每个编辑在多个尺度上表示,并发的重叠编辑通过混合它们的多分辨率表示来合并。

对我们的目的,更简单的方法可行:服务器排序的 last-write-wins。Durable Object 给每个编辑加时间戳并按顺序广播。所有客户端按相同顺序应用编辑。由于笔画是小的、局部的、加/减性的,稍微重排序的并发编辑的视觉结果通常与「正确」顺序难以区分。

INST-Sculpt:神经 SDF 编辑(研究前沿)

INST-Sculpt(arxiv 2502.02891,2025 年 2 月)实现了对神经 SDF 的基于笔画的编辑。用户在表面上画笔画,系统沿笔画路径周围的管状邻域形变底层神经场。自定义笔刷剖面(可配置横截面)控制形变的形状。

这对 AI 生成地形很有意思:如果基础世界表示为神经 SDF(一个把 3D 坐标映射到有符号距离的小神经网络),雕塑修改网络权重而不是显式体素数据。表示极其紧凑(整个世界几兆字节),但评估比查找表昂贵。

这是研究阶段技术。神经 SDF 在消费级硬件上的推理成本对今日的实时游戏使用太高。但值得关注,特别是随着 WebGPU 着色器能力提升和模型推理加速。

World Creator 2026.3:商业地形的当前水平

World Creator(BiteTheBytes,2026 年 3 月)代表了地形创作工具的商业当前水平。2026.3 版增加了基于 GPU 的地形生成、自动地形适配(地形适应已放置的物体)、面向相机的物体分布以优化 LOD,以及真实世界高程数据导入(GeoTIFF、HGT、DTED)。

他们的方法对所有地形操作使用 GPU compute:侵蚀模拟、河道开凿、纹理绘制。笔刷工具是 GPU 加速的,带实时视口反馈。这与上面描述的 WebGPU compute 流水线一致,运行在桌面 GPU 上。

我们已经构建的:24 个 spike 和一个生产世界

world/spikes/ 目录包含 24 个自包含的原型。这些不是玩具演示,而是渐进式的研发流水线,每个 spike 解决一个具体问题,对照目标做基准测试,并影响下一个。雕塑系统建立在所有这些之上,不仅是后期的体积部分。

生产高度图地形(world/client/

实时世界使用 Three.js WebGL 中的分块高度图系统:

  • noise.ts 通过 FBM 值噪声生成地形高度(丘陵 5 个 octave,山脊 4 个,微细节 3 个),用确定性的 terrainHeight(wx, wz) 函数
  • chunk.tsFloat32Array 高度图构建 PlaneGeometry 网格,带 3 个 LOD 等级(每个 64 单位 chunk 32/8/4 段),通过种子随机放置和每物体碰撞体放置实例化的树/广告板
  • chunk-manager.ts 围绕玩家以环形流送 chunk(LOD0 半径 1,LOD1 半径 3,LOD2 半径 6),通过 stitchEdge() 中的线性插值做边缘拼接,并为物理层提供 getHeight()getNormal()resolveCollisions()
  • terrain-material.ts 通过 MeshStandardMaterial.onBeforeCompile 做 4 层基于 splat 的纹理混合(草/岩石/沙/土),带坡度和高度驱动的权重,以及每层法线贴图混合
  • character-controller.ts 每帧采样地形高度用于重力、接地和坡度拒绝(最大坡度 cos 50 度)。雕塑必须立即把修改的高度送进这个系统,否则玩家会穿过编辑过的地形
  • placement.ts 已有一个 Raycaster 击中 chunk 网格用于物体放置工具。笔刷工具应遵循这个模式,而不是从零构建光线投射
  • protocol.ts 定义了用 MessagePack 编码的消息,通过 world-chunk-do.ts Durable Object 做多人同步,目前处理 PlayerStatePlaceObjectRemoveObjectSnapshot 消息。地形编辑需要新消息类型
  • world-chunk-do.ts(Cloudflare Worker)把放置的物体持久化到 Durable Object 存储,以 50ms 间隔广播给连接的玩家。它目前还没有地形修改的概念

Spike 01-11:基础层

这些 spike 验证了雕塑将依赖的核心系统。跳过它们意味着错过雕塑系统必须遵守的约束。

Spike 01(地形 + 实例化): Three.js 中的第一个地形原型。确立了 PlaneGeometry + 高度图模式以及 chunk.ts 仍在使用的实例化物体放置。

Spike 02(Rapier 物理 Worker): Rapier 3D 在 Web Worker 中运行,使用 ColliderDesc.heightfield() 碰撞体。构建了带 autostep、坡度限制和贴地的运动学角色控制器。这个 spike 证明物理可在主线程之外针对高度场运行。如果我们雕塑地形,物理高度场必须重建,或对 MC chunk 替换为 trimesh 碰撞体。

Spike 05(LLM 行为): 与地形不直接相关,但确立了游戏对象的 JSON 行为模式。相关,因为雕塑出的地形特征可触发行为(例如雕出的河流催生水效果)。

Spike 06(Chunk 流送): 第一个 chunk 加载/交换系统,随玩家移动动态加载。确立了 chunk-manager.ts 使用的模式:加载和卸载的彩色区域。雕塑必须在 chunk 卸载和重新加载时保留编辑状态。

Spike 07(来自密度图的 GPU 植被): 通过采样地形高度和坡度的密度图放置实例化草和树。雕塑使植被放置失效:如果地形高度变了,树可能漂浮或被埋。必须为编辑过的 chunk 重新生成密度图。

Spike 08(地形材质着色器成本): 基准测试了 triplanar 投影、法线贴图和 4 层混合。测量了每个功能的精确 ms 成本。发现 triplanar + 法线 + 4 层在 45+ FPS 下保持在预算内。这个预算对雕塑出的地形很重要:如果我们为「编辑过的土壤」加第 5 层或为雕刻表面改变混合,我们能精确知道还有多少余量。

Spike 09(CSM 阴影预算): 3 个级联、1024^2 分辨率的级联阴影贴图。阴影成本测得约 1.5ms。雕塑出的地形改变阴影贴图,但无论地形形状如何成本恒定。

Spike 10(Geometry Clipmap + Geomorphing): LOD 等级之间带 geomorphing 的嵌套 clipmap 环,消除弹出。恒定的三角形数意味着可预测的 GPU 成本。Geomorphing 对雕塑很重要:当玩家在 LOD 边界附近雕塑时,LOD 等级之间的形变必须反映编辑。如果编辑只存在于高分辨率环中,geomorph 目标就是错的。

Spike 11(高度图 chunk 流送): 更高级的 chunk 流送,带可视化网格显示每个 LOD 等级的已加载/加载中/未加载状态。确立了流送预算:每帧最大加载 chunk 数,需要 LOD 升级的 chunk 的优先级。雕塑增加了新的优先级信号:玩家正在编辑的 chunk 永远不应卸载。

Spike 12-14:WebGPU + Three.js 集成

Spike 12(WebGPU Marching Cubes): 第一个体积 spike。四个 64^3 SDF chunk 带动画球洞,完全在 GPU 上运行。使用原生 WebGPU:用于 SDF 评估的 compute 管线、带 Twinklebare 情况表的 MC 提取(256 种配置,每个 16 项)、原子顶点计数器和间接绘制。性能目标是每 chunk <4ms,全部 4 个 <12ms。这验证了 GPU MC 在浏览器中对实时重新网格化足够快。每个后续的体积 spike 都重用这里定义的 MC 情况表和 WGSL 着色器。

Spike 13(从 Spike 12 重置基线): 把 Spike 12 的原生 WebGPU 绘制路径移植到运行在 Three.js 的 WebGPURenderer 中,直接访问后端的 device。渲染流水线仍使用原生 WebGPU(用 struct Vertex vec4+vec4 的 drawIndirect)。这证明自定义 compute 和 Three.js 场景渲染可以在同一 GPU 设备上共存。

Spike 14(Three.js WebGPU 增量加固): 把原生渲染流水线替换为 Three.js StorageBufferAttribute 用于位置和法线。MC compute 直接写入这些驻留 GPU 的缓冲区。drawIndirect 缓冲区控制 Three.js 绘制多少顶点。这是所有后续 spike 使用的模式:compute 保持原生 WebGPU,渲染通过 Three.js 场景图。这些 spike 中的 Three.js 版本从 0.170.0 演进到 0.172.0,因为 WebGPU 后端在稳定。

Spike 15-17:Transvoxel LOD 拼接

Spike 15(Transvoxel seam 脚手架): 增加了三区域架构:MC chunk(体积中心)、过渡条带(MC 边界与高度图之间的接缝)和地形环(围绕高度图)。三者共享一个材质 pass。过渡条带在这一阶段是占位网格,不是真正的 Transvoxel 单元。

Spike 16(带共享高度图的 Transvoxel +X 面): 一个 spike 里两个关键突破。第一,把 SDF 中的平坦地形面替换为共享 Perlin 高度图:一个 257x257 Float32Array 作为存储缓冲上传到 GPU,在 SDF compute shader 中通过双线性插值采样。MC 表面和高度图网格现在认同同一个真实值。第二,通过从 GitHub 获取 Eric Lengyel 的参考数据表(transitionCellClasstransitionVertexDatatransitionCellData)和 npm transvoxel-data 包,实现了 +X 面的真实 Transvoxel 过渡单元。CPU 评估 9 样本过渡单元(512 种配置,73 个等价类),通过在网格点插值 SDF 值放置顶点,并处理镜像情况的绕向反转。

Spike 17(Dual MC 1x/2x LOD): 两个 MC chunk 并排,分辨率不同。高分辨率:62 个 cell,cell_scale=1.0。低分辨率:31 个 cell,cell_scale=2.0。MC 着色器获得 cell_scalegrid_points uniform。引入了 transition_shrink:低分辨率 chunk 的 face-0 边界顶点被向内拉 15% 的 cell_scale,形成 Transvoxel 过渡单元填充的细缝,无 z-fighting。这是生产系统需要的 LOD 模型:近 chunk 全分辨率,远 chunk 半分辨率,每个边界 Transvoxel。

Spike 18-21:Transvoxel 角落情况与 GPU 加速

这四个 spike 各自解决了 Transvoxel 实现中的具体失败案例。把它们分组会掩盖不同的问题。

Spike 18(高度图 2:1 接缝): 把 Transvoxel 应用到纯高度图边界,一侧是另一侧两倍分辨率。不涉及 MC。62 cell 和 31 cell 高度图 chunk 之间的接缝由 Transvoxel 过渡表生成,带 15% 低面收缩。这验证了 Transvoxel 不仅在 MC 情况下工作,也能用于仅高度图情况。

Spike 19(64/32/32/16 角落网格): 最难的拼接情况:四个不同分辨率(64、32、32 和 16 cell)的 chunk 在角点相遇。接缝系统必须沿四条边(A-B、A-C、B-D、C-D)生成过渡单元,每个方向绕向正确。这个 spike 证明 Transvoxel 表能在不需自定义情况逻辑下处理多分辨率角落。

Spike 20(GPU Transvoxel 角落): 把 64/32/32/16 角落布局的 Transvoxel 过渡单元生成移到 GPU。当为动画地形每帧重新生成过渡单元时,CPU 是瓶颈。GPU compute 在与 MC 提取相同的 pass 中生成接缝顶点。

Spike 21(GPU MC + Transvoxel 角落): 把完整的 GPU MC 提取与 GPU Transvoxel 接缝生成组合到单个 compute dispatch 序列中。MC chunk 和所有四个接缝都在 GPU 上生成,顶点数通过原子计数器管理,用 drawIndirect 绘制。这是带无缝 LOD 过渡的多分辨率体积地形的完整 GPU 流水线。

Spike 22-24:混合架构

Spike 22(混合 MC/高度图策略): 关键架构 spike。chunk 默认是高度图。当动画形变球与某 chunk 的 AABB 相交时,该 chunk 切换到 MC 模式。其余保持为静态高度图网格。布局:64、32/32 和 16 cell chunk 不同分辨率。Transvoxel 接缝处理每个边界,包括 MC 到高度图的过渡。spike 跟踪 MC chunk 数 vs HM chunk 数和每帧顶点溢出。

Spike 23(策略驱动的 chunk 模式): 作为 Spike 22 上的补丁加载。增加了相机距离滞后(chunk 在相机靠近阈值时不在模式之间闪烁)和编辑掩码(被形变过的 chunk 即使形变源移开也保持 MC 模式)。这是雕塑需要的「粘性编辑」行为:一旦玩家雕出洞穴,那个 chunk 永远保持体积。

Spike 24(策略 + Clipmap 环): 最先进的 spike。结合 Spike 23 的近场策略系统与 Spike 10 的远场 geometry clipmap 环。升级到 Three.js 0.183.1。近场使用 HM/MC 混合,64/32/16 分辨率带 Transvoxel 接缝。远场使用跟随相机的静态中心 clipmap 环。这是完整的地形渲染架构:需要时分块体积雕塑,其他地方便宜的 clipmap 地形。

为什么是 Marching Cubes 而不是 Surface Nets

本指南的外部研究部分建议 Surface Nets 是浏览器地形雕塑的最强候选。但流水线中的每个 spike 都使用 Marching Cubes。这不是偶然。

MC 的决定性优势是尴尬地并行:每个立方体完全独立。Spike 12-24 中的 WGSL compute shader 为每个立方体派发一个线程,跨 cell 通信为零。原子计数器处理顶点分配。这完美映射到 GPU 工作组。

Surface Nets 为每个包含表面的 cell 放置一个顶点,然后连接邻居。这个邻居连接是跨 cell 依赖。fast-surface-nets crate 通过精心的迭代顺序在 CPU 上处理它。在 GPU 上,它需要两阶段方法(先找顶点,再连接)或工作组内共享内存。两者在 WebGPU 中都可能,但增加复杂度。

实际建议:雕塑流水线保留 Marching Cubes。它在我们的代码库中已被验证,WGSL 着色器存在并已基准测试,Transvoxel 接缝系统是围绕 MC 的基于边的顶点放置构建的。如果 MC 在二值数据上的锯齿成为明显问题,Surface Nets 值得重新考虑,但对值是平滑梯度的 SDF 地形,MC 产生干净结果。

雕塑的实用架构

spike 序列解决了渲染流水线。剩下的是笔刷系统、副作用在游戏系统中的级联和多人同步。这是计划,建立在每个 spike 之上。

阶段 1:高度图雕塑(最小变化,最大覆盖)

在生产 world/client/ 代码中加入修改 chunk 高度图的笔刷工具。这在现有 WebGL 渲染器中工作,不需要 WebGPU。

笔刷输入: 遵循 placement.ts 中的 PlacementTool 模式。它已经有 Raycaster 击中 chunkManager.getChunkMeshes(),并在命中点跟踪一个 ghost mesh。TerrainBrushTool 会做相同的光线投射,但修改 chunk 高度图而不是放置物体。World.onMouseDown 处理器已经根据工具状态分发。

Chunk 修改(Chunk.applyBrush): 把笔刷世界位置映射到高度图网格坐标。对笔刷半径内的每个网格点,计算衰减加权的位移,从高度图值加/减。然后更新网格:从修改的高度图设置顶点 Y 位置,通过中心差分重新计算法线(chunk.ts 第 155-158 行已用相同的 terrainHeight(wx +/- eps, wz) 模式),并通过 terrain-material.ts 中的 createSplatMap() 为受影响区域重新生成 splat map,让坡度驱动的纹理混合更新。

角色控制器: CharacterController.update() 每帧调用 getHeight() 做接地。ChunkManager.getHeight() 委托给 Chunk.sampleHeight(),它从 chunk 的 heightmap Float32Array 读。由于我们直接修改这个数组,角色控制器在下一帧自动拾起变化,无需额外接线。

物体失效: chunk.ts 中的树实例在生成时通过采样 terrainHeight() 放置。雕塑后,受影响区域的树可能高度错误。阶段 1 可以推迟这个(小编辑下树轻微漂浮)。阶段 2 需要一个 chunk.invalidateObjects(),重新采样高度并重建实例矩阵。resolveCollisions() 使用的碰撞体也一样。

Rapier 物理(如已集成): Spike 02 证明高度场碰撞体可行。如果 Rapier 处于活动状态,修改后的 chunk 的高度场碰撞体必须重建或打补丁。Rapier 的 ColliderDesc.heightfield() 接受一个平的 Float32Array,所以是直接替换。

网络同步:protocol.ts 增加 MsgType.TerrainEdit = 10

typescript
interface TerrainEditMsg {
  t: MsgType.TerrainEdit
  cx: number
  cz: number
  brush: {
    wx: number
    wz: number
    radius: number
    strength: number
    falloff: number
    operation: number
  }
}

WorldChunkDO 把这个广播给所有客户端,并附加到 Durable Object 存储的每个 chunk 编辑日志。后加入的客户端在 Snapshot 消息中接收编辑日志并重放来重建地形状态。所有客户端应用相同的确定性笔刷函数,所以它们收敛到相同高度图。

Chunk 卸载/重载: Spike 06 和 Spike 11 确立了流送模式。当 chunk 卸载稍后重载时,该 chunk 的编辑日志必须针对基础程序化高度图重放。编辑日志存储在服务端(Durable Object)并包含在 Snapshot 消息中。

阶段 2:使用 Spike 22-24 架构的体积雕塑

把 Spike 24 流水线移植到生产世界中。当玩家在表面以下雕塑(开凿洞穴、挖隧道)时,受影响的 chunk 从高度图模式过渡到 MC 模式。

WebGPU 渲染器迁移: Spike 13-14 证明 Three.js 的 WebGPURenderer 可在场景图旁托管自定义 compute。生产世界从 WebGLRenderer 迁移到 WebGPURenderer,MC chunk 使用 StorageBufferAttribute。当 WebGPU 不可用时回退到阶段 1 的纯高度图路径。

每 chunk SDF 分配: 遵循 Spike 22 的混合模式。每个 chunk 从高度图开始。在第一次体积笔画时,分配一个 64^3 Float32Array,通过采样高度图初始化(每个点的 SDF 值为 world.y - heightmap_value),并切换到 MC 渲染。Spike 23 的策略系统确保该 chunk 永久保持 MC 模式(来自编辑掩码的「粘性编辑」行为)。

SDF 着色器中的共享高度图: Spike 16 的 height_at() 函数。把 chunk 的高度图上传到 GPU 存储缓冲。SDF compute shader 评估 max(height_sdf, edit_sdf),其中 height_sdf = world.y - height_at(world.xz)edit_sdf 包含笔刷修改。MC 和高度图 chunk 在边界处认同同一真实值。

Transvoxel 接缝: Spike 15-21 的完整堆栈。MC 到高度图边界使用带收缩缝的过渡单元。不同分辨率的 MC 到 MC 边界使用 Spike 17 的双 LOD 模式。Spike 19 的角落情况处理 4 路交叉。Spike 21 的 GPU compute 在同一 dispatch 中生成所有接缝几何。

Clipmap 远场: Spike 24 的 clipmap 环用于雕塑范围之外的地形。雕塑从不触及这些环;它们采样基础程序化高度图。

Geomorphing: Spike 10 的 geomorphing 消除 LOD 过渡处的弹出。对编辑过的 chunk,geomorph 目标必须包括编辑。如果 chunk 在 LOD0 是 MC,它的 LOD1 邻居是高度图,geomorph 在两种表示之间混合。这需要即使在较低 LOD 也采样编辑日志。

材质预算: Spike 08 在 45+ FPS 下基准了 4 层 triplanar + 法线。MC chunk 需要相同的材质。splat map 可从 SDF 梯度生成(陡 = 岩石,平 = 草),而不是高度图坡度。这保持在 4 层预算内。

植被失效: Spike 07 的密度图植被依赖地形高度和坡度。当 chunk 过渡到 MC 模式时,必须通过采样 SDF 表面重新生成树实例。悬挑或洞内的树必须剔除。chunk.ts 的实例化网格矩阵从新表面重建。

阶段 3:用于撤销/重做和网络同步的 CSG 编辑树

把原始 SDF 变异替换为 CSG 操作树。每次笔画追加一个原始体(球、胶囊、盒子)和一个操作(加、减、平滑混合)。SDF 从树重新计算。

好处:

  • 非破坏性:可从树中移除任何编辑来撤销它
  • 网络高效:广播 CSG 操作,而不是原始场值
  • 确定性:所有客户端从相同操作序列构建相同 SDF
  • ALICE-SDF 的 CSG 树差异/补丁提供跨网络的带宽高效同步和撤销/重做

Durable Object 存储: 每 chunk 的编辑树替换阶段 1 的平坦编辑日志。WorldChunkDO 存储 CSG 树结构,而不是原始高度图增量。Snapshot 消息包含树,后加入的客户端评估它产生本地 SDF。

阶段 4:协作雕塑

通过服务器排序的操作重放增加并发编辑支持。Durable Object 给每个编辑加时间戳并按顺序广播。后加入的客户端接收操作日志并重建世界状态。现有 Snapshot 消息类型扩展为包含每 chunk 的地形编辑历史。

由于笔画是小的、局部的、加/减性的,稍微重排序的并发编辑的视觉结果通常与「正确」顺序难以区分。服务器排序的 last-write-wins 足够。Durable Object 的 tick() 函数(目前以 50ms 间隔运行用于玩家状态)把地形编辑广播加到同一循环中。

关键参考

算法:

  • Lorensen 与 Cline,「Marching Cubes」(1987)
  • Eric Lengyel,「Transvoxel Algorithm」(2009),transvoxel.org
  • Losasso 与 Hoppe,「Geometry Clipmaps」(SIGGRAPH 2004)
  • 「A High-Performance SurfaceNets Discrete Isocontouring Algorithm」(arxiv 2401.14906,2024)
  • MCHex(arxiv 2511.02064,2025)

实现:

  • bevy-sculpter v0.18.0(Rust,Surface Nets + SDF 笔刷)
  • fast-surface-nets(Rust,2000 万 tri/sec)
  • ALICE-SDF v1.3.0(Rust + WASM,CSG 树差异/补丁)
  • WebGPU SDF Editor(Nijhoff,2026 年 1 月)
  • SculptingPro(Unity 运行时雕塑 API)
  • TerraBrush(Godot 地形雕塑 GDExtension)

游戏:

  • Dreams(Media Molecule,SDF + 点云渲染,SIGGRAPH 2015)
  • Teardown(Voxagon,体素 DDA 光线追踪,确定性多人破坏)
  • Mike Turitzin 的 SDF 引擎(brick map + geometry clipmap,2026 年 1 月)

网络:

  • 「Optimizing payload size for voxel state synchronization」(Oulu,2024)
  • Teardown 多人(半确定性破坏同步,2026 年 3 月)
  • cSculpt(带多分辨率合并的协作网格雕塑)