首页
/ three.js BufferGeometryUtils 几何工具全解析:合并、焊接、切线、法线与存储优化实战

three.js BufferGeometryUtils 几何工具全解析:合并、焊接、切线、法线与存储优化实战

2026-09-08 19:01:04作者:房伟宁

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 个方法:computeMikkTSpaceTangentsmergeGeometriesmergeAttributesdeepCloneAttributedeinterleaveAttributedeinterleaveGeometryinterleaveAttributesestimateBytesUsedmergeVerticestoTrianglesDrawModecomputeMorphedAttributesmergeGroupstoCreasedNormals(对应源码 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 就是它在数组中的下标——这样合并后的单一几何仍能配合多材质 Meshmaterial 数组渲染;默认 false 则整块使用同一材质。

仓库中的真实用法示例:

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 之前对模型做 mergeVerticesexamples/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();若要用多材质分组渲染,则给 mergeGeometriesuseGroups: true,并把 Meshmaterial 设为与分组一一对应的数组。

属性存储布局优化:交织、解交织、深拷贝与内存估算

这三个方法针对 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~getWsetX~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 = 4tangent 属性(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 ) 把以 TriangleStripDrawModeTriangleFanDrawMode 描述几何的三角形带回 TrianglesDrawMode(即 WebGL gl.TRIANGLES 图元),转换只重写索引、就地修改几何(源码 examples/jsm/utils/BufferGeometryUtils.js)。

  • drawMode 传入的是几何当前的绘制模式常量(three 核心常量定义见 src/constants.jsTrianglesDrawMode = 0TriangleStripDrawMode = 1TriangleFanDrawMode = 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

实现与入参说明:

  • objectMesh | Line | Points,要求其 geometry 至少带 position 与 normal;
  • 内部同时考虑:geometry.index(索引/非索引两种路径)、geometry.groupsgeometry.drawRange(material 为数组时按组裁剪遍历区间)、morphTargetsRelative(相对/绝对变形语义:相对模式取 morph - base 的差值加权)、非零 morphTargetInfluences(权重为 0 直接跳过);
  • 若物体是 SkinnedMesh,还会对每个三角形顶点调用 object.applyBoneTransform() 叠加骨骼矩阵;
  • 返回对象含四个成员:原始 positionAttributenormalAttribute 与重算出的 morphedPositionAttributemorphedNormalAttribute(均按 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 渲染器复用。

相关资源

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
521
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
392