three.js BoxGeometry 几何类完全解析:构造参数、六面分组构建与 JSON 序列化机制
本文围绕 three.js 官方文档页 docs/pages/BoxGeometry.html.md 讲解 BoxGeometry 类:如何创建以原点为中心的长方体几何、六个构造参数(含分段数)的含义与默认值、.parameters 属性与 copy() 的行为,并结合 src/geometries/BoxGeometry.js 源码深入剖析其六面构建算法、法线与 UV 生成、多材质分组机制,以及 toJSON()/fromJSON() 参数化序列化与“变换后降级”策略,帮助你在项目中正确使用并调试这一最基础的几何体。
概述与继承关系
BoxGeometry 是一个用于创建指定宽、高、深的长方体(rectangular cuboid)的几何类。根据 src/geometries/BoxGeometry.js 中的类注释与类声明,它在创建时以原点为中心(centred on the origin),且每条边都与某个坐标轴平行:
继承链:EventDispatcher → BufferGeometry → BoxGeometry
- 它直接继承自 src/core/BufferGeometry.js 中的
BufferGeometry,因此拥有attributes、index、groups、toJSON()、clone()等全部缓冲几何体能力; - 单元测试 test/unit/src/geometries/BoxGeometry.tests.js 明确验证了
new BoxGeometry() instanceof BufferGeometry为true,且实例的type字段恒为'BoxGeometry'(源码中this.type = 'BoxGeometry',见 src/geometries/BoxGeometry.js#L36)。
最小可用示例
官方文档给出的标准用法(同样出现在源码文件的 JSDoc 注释中):
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
仓库中官方文档页还内嵌了一个交互式场景查看器,可直接在浏览器中观察 BoxGeometry 的效果,入口见 docs/pages/BoxGeometry.html 中引用的 geometry-browser 场景(对应源码 @demo scenes/geometry-browser.html#BoxGeometry 标注)。
构造函数:六个参数与默认值
构造函数签名为:
new BoxGeometry(
width = 1, height = 1, depth = 1,
widthSegments = 1, heightSegments = 1, depthSegments = 1
)
| 参数 | 含义 | 默认值 |
|---|---|---|
width |
宽度,即平行于 X 轴的边长 | 1 |
height |
高度,即平行于 Y 轴的边长 | 1 |
depth |
深度,即平行于 Z 轴的边长 | 1 |
widthSegments |
沿宽度方向(侧面)的矩形分段数 | 1 |
heightSegments |
沿高度方向(侧面)的矩形分段数 | 1 |
depthSegments |
沿深度方向(侧面)的矩形分段数 | 1 |
一个细节来自源码:三个分段参数在构造内部都会经过 Math.floor() 截断(见 src/geometries/BoxGeometry.js#L58-L60),因此传入 2.7 等价于 2,分段数必须是正整数才会产生预期的网格划分。
widthSegments / heightSegments / depthSegments 控制的是面被划分成的网格数量,而非顶点数。例如 widthSegments: 2, heightSegments: 3 会把相应侧面向宽、高两个维度各切 2 份、3 份。分段数直接影响每面的顶点与三角面数量,也决定了 UV 网格密度——这对纹理细分、位移贴图(displacement map)表现很重要:只有分段数足够,曲面细节才能在长方体表面上采样出来。
构建原理:六次 buildPlane 与分组(groups)
BoxGeometry 的构造体由一次 super() 调用后的一系列 buildPlane 生成。源码按“px、nx、py、ny、pz、nz”六个面依次调用(见 src/geometries/BoxGeometry.js#L76-L81):
buildPlane( 'z', 'y', 'x', - 1, - 1, depth, height, width, depthSegments, heightSegments, 0 ); // px
buildPlane( 'z', 'y', 'x', 1, - 1, depth, height, - width, depthSegments, heightSegments, 1 ); // nx
buildPlane( 'x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2 ); // py
buildPlane( 'x', 'z', 'y', 1, - 1, width, depth, - height, widthSegments, depthSegments, 3 ); // ny
buildPlane( 'x', 'y', 'z', 1, - 1, width, height, depth, widthSegments, heightSegments, 4 ); // pz
buildPlane( 'x', 'y', 'z', - 1, - 1, width, height, - depth, widthSegments, heightSegments, 5 ); // nz
buildPlane( u, v, w, udir, vdir, width, height, depth, gridX, gridY, materialIndex ) 的参数含义可以从源码结构看:
u / v / w:该面所在平面上用于生成局部网格的两个轴名,以及法线所在的轴名;udir / vdir:局部坐标方向的翻转标志(-1表示反向),用来保证六个面的顶点绕序统一为外法线朝向,这也是为什么负方向面(nx、ny、nz)会传入相反尺寸(- width等);gridX / gridY:该面两个方向的网格段数;materialIndex:该面对应的材质索引(0~5)。
每个面的生成逻辑(见 src/geometries/BoxGeometry.js#L90-L190)分三部分:
- 顶点:在
(gridX+1) × (gridY+1)的顶点网格上,局部坐标x = ix * segmentWidth - widthHalf、y = iy * segmentHeight - heightHalf,再写入vector[u]、vector[v],而vector[w] = depthHalf,从而把网格贴在对应半深位置,实现“以原点为中心”; - 法线:每个面的法线统一沿
w轴,方向由depth > 0 ? 1 : - 1决定(负深度时反转,保证法线仍朝外); - UV:
uvs.push( ix / gridX, 1 - ( iy / gridY ) ),即每个面都完整覆盖[0,1]×[0,1]的 UV 空间,v 轴做了翻转。
索引与分组是理解多材质支持的关键。每个网格单元生成 6 个索引(2 个三角面 × 3 个顶点),累加进 groupCount;每个面完成后调用:
scope.addGroup( groupStart, groupCount, materialIndex );
groupStart += groupCount;
因此一个 BoxGeometry 天然带有 6 个 group,materialIndex 为 0~5,分别对应 px、nx、py、ny、pz、nz 面。源码注释明确写道:“this will ensure multi material support”。这意味着你可以直接给 Mesh 传入 6 个材质的数组,让每个面使用不同材质——这是 BoxGeometry 相对手写 BufferGeometry 的一大便利。
构造结束后,几何体通过 setIndex 与 setAttribute 装配四块数据(见 src/geometries/BoxGeometry.js#L85-L88):
this.setIndex( indices );
this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
可以推断:由于顶点不共享(每个面独立生成自己的顶点网格),六个面在棱边上各有独立顶点,法线才能保持面严格平直,不会因为共享顶点而被平滑。
.parameters 属性与 copy()
实例属性 .parameters : Object 保存构造时使用的六个参数:
this.parameters = {
width, height, depth,
widthSegments, heightSegments, depthSegments
};
官方文档强调:实例化之后再修改 parameters 不会改变已生成的几何数据,它只是一份“出生证明”,主要用于序列化(见下文)与调试。
copy( source ) 方法在调用基类拷贝后额外浅拷贝 parameters(见 src/geometries/BoxGeometry.js#L194-L202):
copy( source ) {
super.copy( source );
this.parameters = Object.assign( {}, source.parameters );
return this;
}
注意这里是 Object.assign 新对象,clone() 出来的几何体修改自己的 parameters 不会影响源几何体。
静态方法 .fromJSON() 与序列化机制
static fromJSON( data ) {
return new BoxGeometry(
data.width, data.height, data.depth,
data.widthSegments, data.heightSegments, data.depthSegments
);
}
(见 src/geometries/BoxGeometry.js#L211-L215)它是从 JSON 对象重建实例的工厂方法,通常配合 src/loaders/ObjectLoader.js / src/loaders/BufferGeometryLoader.js 在加载 .json 场景或几何文件时使用。
与 fromJSON 配对的 toJSON() 继承自 BufferGeometry,但 BoxGeometry 的 parameters 让序列化拥有了一个非常划算的优化路径。从 src/core/BufferGeometry.js#L1230-L1258 可以看到核心策略:
data.type = ( this.parameters !== undefined && this._transformed === true )
? 'BufferGeometry' : this.type;
...
if ( this.parameters !== undefined && this._transformed !== true ) {
// 仅把 parameters 逐项写入 JSON,直接 return,
// 不序列化 attributes 数据
return data;
}
即:
- 未变换的
BoxGeometry(_transformed === false):JSON 中只记录type: 'BoxGeometry'和六个参数,不写入顶点/法线/UV 数组,体积极小,加载时由fromJSON重新生成几何; - 发生过顶点变换后(如调用过
translate()、rotateX()等会置_transformed = true的方法,见 src/core/BufferGeometry.js#L422):type会“降级”为'BufferGeometry',并把全部 attribute 数据落盘——因为参数已经不能还原变换后的形状了。
单元测试 test/unit/src/geometries/BoxGeometry.tests.js 对这两条路径都有显式断言:
new BoxGeometry(10, 20, 30)的toJSON()中json.width === 10、json.data === undefined(参数化序列化,属性数据不写出);- 先
geometry.translate( 1, 2, 3 )再toJSON()时json.type === 'BufferGeometry'、json.width === undefined,且json.data.attributes.position存在; clone()的几何体_transformed标志会通过copy()传播,序列化行为保持一致。
此外,通用工具 runStdGeometryTests(见 test/unit/utils/qunit-utils.js#L188-L202)对默认参数、10×20×30、以及 10×20×30 + 2/3/4 分段 三组 BoxGeometry 执行 clone 一致性与 JSON 往返(round-trip)校验,覆盖了上述构建与序列化路径。
实战要点小结
- 以原点为中心:几何体顶点分布在
±width/2、±height/2、±depth/2,若需要底边贴地,可在 Mesh 上平移(注意对几何体调用translate会触发_transformed,导致序列化体积增大); - 多材质:利用内置的 6 个 group,
new THREE.Mesh( geometry, [mat0, mat1, mat2, mat3, mat4, mat5] )即可为六个面分别指定材质; - 分段数:只在需要位移贴图、逐面纹理细分或更密的逐顶点动画时才增大,分段是乘法级增长顶点数(每面
(gridX+1) × (gridY+1)个顶点); - 序列化:保持几何体“未变换”时 JSON 只含 6 个参数,
fromJSON依赖data.width等字段完整重建;如果你从 JSON 加载后得到的是BufferGeometry而非BoxGeometry,说明源数据发生过顶点变换,属于预期行为; - 参数只读:
.parameters仅用于记录与序列化,运行时修改它不会重建几何。
参考文件
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 StartedRust0630
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00