three.js BufferGeometryUtils 几何工具全解析:合并、焊接、切线、法线与存储优化实战
BufferGeometryUtils 是 three.js 官方的几何工具集合(addon),集中封装了顶点合并(merge/mergeVertices)、索引与绘制模式转换、MikkTSpace 切线计算、折痕法线生成、属性交织/解交织、内存估算与蒙皮/变形属性提取等高频 GPU 几何处理能力。本篇以 docs/pages/module-BufferGeometryUtils.html.md 的 API 文档为骨架,对照 examples/jsm/utils/BufferGeometryUtils.js 源码(约 1500 行)与仓库内真实调用点,逐一讲解每个方法的签名、默认值、内部原理、失败边界与典型使用场景,让读者拿到一份可直接用于 Draw Call 优化、法线贴图修复、LOD/体素化流水线的完整工具箱参考。
模块引入与依赖说明
BufferGeometryUtils 不是 three.js 核心包的一部分,而是需要显式引入的 addon,完整约定见仓库文档 docs/pages/module-BufferGeometryUtils.html.md 中标注的 import 方式:
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
该模块最终导出了 13 个方法:computeMikkTSpaceTangents、mergeGeometries、mergeAttributes、deepCloneAttribute、deinterleaveAttribute、deinterleaveGeometry、interleaveAttributes、estimateBytesUsed、mergeVertices、toTrianglesDrawMode、computeMorphedAttributes、mergeGroups 与 toCreasedNormals(对应源码 examples/jsm/utils/BufferGeometryUtils.js 的导出列表)。注意其中 computeMikkTSpaceTangents 需要一个额外的 MikkTSpace 依赖(见后文),其余方法都只依赖 three 核心。
本仓库自带的单元测试位于 test/unit/addons/utils/BufferGeometryUtils.tests.js,在 Addons > Utils > BufferGeometryUtils 测试分组下对工具方法进行回归验证。
顶点与几何合并:减少 Draw Call 的三大工具
把多个几何体/多个网格合并成一个 BufferGeometry,是减少渲染批次、配合 InstancedMesh 或单材质渲染的最常见手段。BufferGeometryUtils 提供了三个不同粒度的合并函数。
mergeGeometries(geometries, useGroups = false)
将一组几何合并为单个实例,要求所有几何必须拥有相互兼容的属性集合:
const merged = BufferGeometryUtils.mergeGeometries( geometries );
其执行流程与失败边界(源码 examples/jsm/utils/BufferGeometryUtils.js):
- 索引一致性:所有几何要么都带索引,要么都不带索引;同时属性名集合、属性数量必须完全一致,否则
console.error并返回null。 - 变形属性:
morphTargetsRelative标志必须在所有几何上一致;每个属性名对应的 morph 目标数量也必须一致,合并时按 "同名属性第 i 个 morph 目标" 逐一用mergeAttributes合并。 - 索引合并:带索引时会按每个几何的顶点数累积
indexOffset重写索引;组(group)边界用每个几何的顶点/索引数量推进 offset。 - 返回值:成功返回新的
BufferGeometry,任一步骤失败返回null(调用方需要判空)。
useGroups 参数决定每个子几何是否被保留为独立的 group。若为 true,函数会为第 i 个几何调用 mergedGeometry.addGroup( offset, count, i ),materialIndex 就是它在数组中的下标——这样合并后的单一几何仍能配合多材质 Mesh 的 material 数组渲染;默认 false 则整块使用同一材质。
仓库中的真实用法示例:
- examples/webgl_geometry_minecraft.html 将无数方块几何体
mergeGeometries成 chunk 级几何; - examples/webgl_loader_ifc.html 按"不透明/透明"分组后分别
mergeGeometries; - examples/webgl_instancing_performance.html 把场景中静态物体合并后配合实例化渲染;
- examples/webgpu_compute_particles_rain.html 在 WebGPU 计算管线中合并涟漪几何。
mergeVertices(geometry, tolerance = 1e-4)
将属性在容差范围内完全相同的顶点合并,返回一个新的、带有索引的几何。它等价于 3D 建模软件里的 "Weld Vertices",最典型的用途是把平滑模型转成可索引结构、为后续 computeVertexNormals/computeTangents 或 Decal 投影做准备。
实现要点(源码 examples/jsm/utils/BufferGeometryUtils.js):
- 容差被钳制为不小于
Number.EPSILON,并通过Math.log10(1 / tolerance)换算为"量化到多少位小数",从而把每个顶点的全属性拼成一个哈希键(源码中逐分量~~(value * hashMultiplier + hashAdditive)截断小数); - 所有属性(
attributeNames遍历到的每一项)与同名morphAttributes都会被同步压缩写入新数组,最后setIndex并裁剪多余缓冲; - 原始几何不会被修改,返回的是新几何。
该函数在本仓库使用频率极高:例如 examples/webgl_geometry_convex.html 在计算 ConvexHull 前焊接正十二面体顶点,examples/webgl_modifier_edgesplit.html 在 EdgeSplit 之前对模型做 mergeVertices,examples/webgpu_compute_particles_fluid.html 在 SPH 流体粒子上对二十面体调用 mergeVertices。单元测试 test/unit/addons/utils/BufferGeometryUtils.tests.js 验证了"带 morphAttributes 的几何执行 mergeVertices 不会崩溃且 morphAttributes 同步保留、结果带索引"。
提示:如果目标只是"三角形之间不共享顶点",直接使用
geometry.toNonIndexed()(见 src/core/BufferGeometry.js)即可,它是 mergeVertices 的逆操作。
mergeGroups(geometry)
把同一几何中相邻且 materialIndex 相同的 group 合并,从而减少绘制调用中的组切换。源码 examples/jsm/utils/BufferGeometryUtils.js 的行为:
- 若几何没有 group,直接警告返回;
- 对 group 按
materialIndex升序、再按start升序排序;若几何当前无索引会自动生成索引; - 按排序后的顺序重排索引,
geometry.dispose()强制 GPU 缓冲重建,然后把可合并的相邻同材质 group 折叠成一个(materialIndex不同的 group 保留)。
本仓库中 examples/jsm/utils/SceneUtils.js 就在 createMeshesFromInstancedMesh 等场景转换逻辑里通过 mergeGroups 化简几何。由于它会就地修改并 dispose() 旧缓冲,使用前请确认不再持有旧的 GPU 资源引用。
mergeAttributes(attributes)
mergeGeometries 的底层拼图:把一组属性拼接成单个 BufferAttribute,要求所有属性在底层 TypedArray 构造器、itemSize、normalized、gpuType 上完全一致(源码 examples/jsm/utils/BufferGeometryUtils.js),不满足时返回 null。文档注明 InterleavedBufferAttribute 实例不被支持,不过从当前实现看(L385-L398)对传入的交织属性仍有一段按 getComponent/setComponent 逐个拷贝分量的处理逻辑,等价于"解交织后再落盘"——实际使用时建议先显式解交织,避免依赖这一隐式行为。
合并方法之后,实践中的组合用法
合并几何常用于"合并前先焊接、合并后统一归一化"的流水线。参考仓库中 examples/webgl_custom_attributes_points2.html 的做法:
sphereGeometry = BufferGeometryUtils.mergeVertices( sphereGeometry );
boxGeometry = BufferGeometryUtils.mergeVertices( boxGeometry );
// 两个几何共享 position/color 等相同属性集才能合并
const combinedGeometry = BufferGeometryUtils.mergeGeometries( [ sphereGeometry, boxGeometry ] );
若合并后还需要法线,可调用 three 核心的 geometry.computeVertexNormals();若要用多材质分组渲染,则给 mergeGeometries 传 useGroups: true,并把 Mesh 的 material 设为与分组一一对应的数组。
属性存储布局优化:交织、解交织、深拷贝与内存估算
这三个方法针对 BufferAttribute / InterleavedBuffer 的底层存储布局做转换,GPU 顶点布局(Vertex Layout)中把 position/normal/uv 打包进一个 InterleavedBuffer 可获得更好的缓存局部性,而 CPU 侧逐属性处理时又往往需要解交织。
interleaveAttributes( attributes ):把一组兼容类型的属性合并到同一个InterleavedBuffer(stride = 各属性 itemSize 之和),返回一一对应的InterleavedBufferAttribute数组;类型不一致时打印错误并返回null。实现见 examples/jsm/utils/BufferGeometryUtils.js,它借助getters/setters数组(getX~getW、setX~setW)逐个分量搬迁数据。deinterleaveAttribute( attribute ):将单个交织属性解交织成独立的BufferAttribute;若传入的是InstancedInterleavedBufferAttribute,会保留meshPerAttribute还原为InstancedBufferAttribute(源码 examples/jsm/utils/BufferGeometryUtils.js)。deinterleaveGeometry( geometry ):批量解交织,处理范围不仅包括geometry.attributes中所有交织属性,还包括geometry.morphTargets,并用attrMap保证共享同一底层InterleavedBuffer的多个属性只转换一次(源码 examples/jsm/utils/BufferGeometryUtils.js)。deepCloneAttribute( attribute ):执行属性深拷贝。对InstancedInterleavedBufferAttribute/InterleavedBufferAttribute通过解交织得到普通属性;对InstancedBufferAttribute用.copy()保留实例语义;普通属性则新建BufferAttribute().copy()(源码 examples/jsm/utils/BufferGeometryUtils.js)。estimateBytesUsed( geometry ):估算几何在 CPU 侧占用的字节数——遍历所有属性累加count * itemSize * BYTES_PER_ELEMENT,并额外计入索引缓冲,公式见 examples/jsm/utils/BufferGeometryUtils.js。该值可用于内存预算、调试或webgl_test_memory类场景中的资源统计。
切线计算:computeMikkTSpaceTangents 与法线贴图
import * as MikkTSpace from 'three/addons/libs/mikktspace.module.js';
import { computeMikkTSpaceTangents } from 'three/addons/utils/BufferGeometryUtils.js';
computeMikkTSpaceTangents( geometry, MikkTSpace, negateSign = true );
该方法用 MikkTSpace 算法为几何计算逐顶点切线(tangent)。MikkTSpace 是行业事实标准:大多数建模软件与法线贴图烘焙工具都生成一致的切线。因此当材质使用法线贴图(尤其存在镜像 UV 接缝)时,务必用 MikkTSpace 切线,避免因切线不一致导致的法线贴图细微视觉错误。
对比:three 核心的 geometry.computeTangents()(自定义算法,见 src/core/BufferGeometry.js)生成的切线可能与外部软件不一致,但对自定义材质的一般用途已足够,且通常比 MikkTSpace 更快。
参数与行为细节(源码 examples/jsm/utils/BufferGeometryUtils.js):
| 参数 | 含义 |
|---|---|
geometry |
要计算切线的几何,必须同时拥有 position、normal、uv 三个属性,否则抛错 |
MikkTSpace |
examples/jsm/libs/mikktspace.module.js(本仓库已内置,路径 examples/jsm/libs/mikktspace.module.js)或 npm mikktspace 包实例;使用前必须 await MikkTSpace.ready,模块会校验 MikkTSpace.isReady,未就绪直接抛错 |
negateSign |
是否翻转每个切线 .w 分量(符号分量);glTF 等格式的法线贴图坐标约定需要翻转。默认 true |
- MikkTSpace 算法要求非索引输入,因此带索引的几何会被内部
toNonIndexed()摊平(源码 L87),结果再通过geometry.copy()回写,方法始终返回传入的同一几何; - 计算产物是
itemSize = 4的tangent属性(xyz为切向量,w为副切线方向符号)。negateSign = true时对每 4 个分量中的第 4 个取反(源码 L102-L110),源码注释明确说明这是为 glTF 的纹理坐标约定所做的修正。
完整异步调用模式可参考仓库示例 examples/webgpu_loader_materialx.html:遍历模型节点,跳过已有 tangent 属性或缺少 position/normal/uv 的几何,再 await MikkTSpace.ready 后调用 computeMikkTSpaceTangents( geometry, MikkTSpace )。
折痕法线:toCreasedNormals
toCreasedNormals( geometry, creaseAngle = Math.PI / 3 ) 让几何除了夹角大于 creaseAngle 的面(即"折痕")之外,其余法线保持平滑,非常适合需要"低多边形 + 硬边+软边混合"的卡通/机械风格渲染。
实现特征(源码 examples/jsm/utils/BufferGeometryUtils.js):
- 对带索引的几何会先
toNonIndexed()摊平(因为每个"硬边顶点"需要独立法线),对已非索引的几何则原地修改——因此返回值可能是新几何也可能是原几何,非索引输入时传入对象被就地改写; - 内部先逐三角面计算面法线,再用开放寻址哈希表按量化位置合并"同一顶点位置"的多个顶点(
hashMultiplier = (1 + 1e-10) * 1e2,相当于以约 0.01 的容差判定重合顶点),最后对每个顶点仅平均与其夹角余弦大于Math.cos(creaseAngle)(即法线点积大于creaseDot)的相邻面法线; - 最终以
BufferAttribute( normalArray, 3, false )替换normal属性。注意:折痕角阈值依赖的是"逐顶点汇聚的面法线之间的夹角",顶点需要足够"焊接"才能正确判定相邻关系。
绘制模式转换:toTrianglesDrawMode
toTrianglesDrawMode( geometry, drawMode ) 把以 TriangleStripDrawMode 或 TriangleFanDrawMode 描述几何的三角形带回 TrianglesDrawMode(即 WebGL gl.TRIANGLES 图元),转换只重写索引、就地修改几何(源码 examples/jsm/utils/BufferGeometryUtils.js)。
drawMode传入的是几何当前的绘制模式常量(three 核心常量定义见 src/constants.js:TrianglesDrawMode = 0、TriangleStripDrawMode = 1、TriangleFanDrawMode = 2);- 已是三角形模式时发出警告并直接返回;无索引则先按 position 数量生成
[0,1,2,…]索引; - 对 strip:按
i % 2 === 0/奇数分别正序/逆序输出三角形顶点,保证绕序(winding)正确;对 fan:以index.getX(0)为公共顶点逐个扇形展开; - 转换成功后调用
geometry.clearGroups()。
最有说服力的应用在官方 glTF 加载器内部:examples/jsm/loaders/GLTFLoader.js 导入该函数,并在遇到 glTF 的 TRIANGLE_STRIP / TRIANGLE_FAN 图元(对应源码 L3835-L3843)时通过 toTrianglesDrawMode( geometry, TriangleStripDrawMode / TriangleFanDrawMode ) 统一转换为三角形网格——这正是为什么你几乎不需要手动处理 strip/fan 格式的 glTF。
变形/蒙皮几何的属性提取:computeMorphedAttributes
const result = BufferGeometryUtils.computeMorphedAttributes( mesh );
// result.positionAttribute / result.normalAttribute 为原始属性
// result.morphedPositionAttribute / result.morphedNormalAttribute 为叠加了 morph 影响与骨骼变换后的结果
computeMorphedAttributes( object ) 计算"把当前 morphTargetInfluences 权重和骨骼动画烘焙进去之后"的几何属性快照(源码 examples/jsm/utils/BufferGeometryUtils.js)。
典型场景:光线追踪、Decal 贴花——例如把 DecalGeometry 贴到一个正在变形/蒙皮的物体上时,直接使用该物体原始的 BufferGeometry 会得到错误结果(变形被忽略);正确的做法是先给物体克隆出一个"影子"对象,对其调用本函数取得烘焙后的几何,再基于该结果生成 DecalGeometry。
实现与入参说明:
object为Mesh | Line | Points,要求其geometry至少带 position 与 normal;- 内部同时考虑:
geometry.index(索引/非索引两种路径)、geometry.groups与geometry.drawRange(material 为数组时按组裁剪遍历区间)、morphTargetsRelative(相对/绝对变形语义:相对模式取morph - base的差值加权)、非零morphTargetInfluences(权重为 0 直接跳过); - 若物体是
SkinnedMesh,还会对每个三角形顶点调用object.applyBoneTransform()叠加骨骼矩阵; - 返回对象含四个成员:原始
positionAttribute、normalAttribute与重算出的morphedPositionAttribute、morphedNormalAttribute(均按 3 分量Float32输出)。
注意该函数返回的是未写回场景的独立快照数据,通常需要自行构造临时 BufferGeometry 交给 Decal/Raycast 使用。
参数速查表与兼容性提醒
| 函数 | 默认值 | 返回 | 关键失败行为 |
|---|---|---|---|
computeMikkTSpaceTangents |
negateSign = true |
同一几何(索引几何会被摊平) | MikkTSpace 未就绪或缺 position/normal/uv 时抛错 |
mergeGeometries |
useGroups = false |
合并后新几何 / null |
索引策略、属性集合、morphTargetsRelative 不一致时返回 null |
mergeVertices |
tolerance = 1e-4 |
新索引几何 | 容差钳制不低于 Number.EPSILON |
mergeGroups |
— | 就地修改的原几何 | 无 group 时警告并返回原几何 |
toCreasedNormals |
creaseAngle = Math.PI / 3 |
原几何或摊平后的新几何 | 摊平基于约 0.01 的量化顶点合并 |
toTrianglesDrawMode |
— | 就地转换的几何 | 未知 drawMode 或缺失 position 时打印错误 |
estimateBytesUsed |
— | 数字(字节) | 含索引缓冲估算 |
额外提醒:mergeGeometries/mergeGroups/toTrianglesDrawMode 等会就地修改或替换 GPU 缓冲(mergeGroups 甚至调用 dispose()),操作前确认无共享缓冲引用;所有属性合并类函数对"属性类型一致性"要求严格,跨 TypedArray 类型(如 Float32Array 与 Uint16Array)的属性不能直接合并。合并后可用 estimateBytesUsed 快速验证内存收益,最终几何可交给 WebGLRenderer/WebGPU 渲染器复用。
相关资源
- API 参考:
docs/pages/module-BufferGeometryUtils.html.md,配套单个类文档 BufferGeometry 文档(含computeTangents与groups说明)、InterleavedBuffer 文档 与 InterleavedBufferAttribute 文档 - 核心源码:examples/jsm/utils/BufferGeometryUtils.js
- 单元测试:test/unit/addons/utils/BufferGeometryUtils.tests.js
- 依赖库:examples/jsm/libs/mikktspace.module.js
- 真实用例:
mergeGeometries(webgl_geometry_minecraft.html、webgl_instancing_performance.html)、mergeVertices(webgl_modifier_edgesplit.html、webgl_geometry_convex.html)、computeMikkTSpaceTangents(webgpu_loader_materialx.html)、toTrianglesDrawMode(GLTFLoader)、mergeGroups(SceneUtils)
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00