BufferAttribute 是用来存储几何体顶点数据的类。
BufferAttribute负责把几何体上的顶点数据(位置、法线、UV、颜色等)以 TypedArray 形式存储,并高效上传到 GPU。
基本用法如下:
ini
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array( [
- 1.0, - 1.0, 0.0,
1.0, - 1.0, 0.0,
0.0, 1.0, 0.0
] );
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
1-构造函数与核心属性
ini
constructor( array, itemSize, normalized = false ) {
// ...
this.isBufferAttribute = true;
this.array = array; // TypedArray 原始数据
this.itemSize = itemSize; // 每个顶点占几个分量(如 position=3, uv=2)
this.count = array.length / itemSize; // 顶点数量
this.normalized = normalized; // 整数类型是否归一化到 [0,1]
this.usage = StaticDrawUsage; // GPU 使用模式提示
this.updateRanges = []; // 局部更新范围
this.gpuType = FloatType; // GPU 上的数据类型
this.version = 0; // 数据变更版本号
}
| 属性 | 含义 |
|---|---|
name |
attribute 变量名 |
array |
底层 TypedArray(如 Float32Array) |
itemSize |
每个逻辑项的分量数(2=UV,3=位置/法线,4=颜色等) |
count |
项的数量 = array.length / itemSize |
normalized |
仅对整数数组有效;true 时 GPU 会把值映射到 [0, 1] |
usage |
向 WebGL 提示数据更新频率(静态/动态/流式) |
updateRanges |
只上传部分数据时的范围列表 |
version |
每次 needsUpdate = true 时自增,渲染器据此判断是否要重新上传attribute数据 |
注意:构造函数要求 array 必须是 TypedArray,普通 Array 会抛错(子类会帮你转换)。
2-BufferAttribute 核心属性与WebGL 原生 API 的对应关系
BufferAttribute 是 CPU 端对 Attribute 数据的封装;真
正调用 WebGL 的是 WebGLAttributes(上传缓冲)和 WebGLBindingStates(绑定顶点属性)。
对应关系如下。
总览:两条管线
VBO 相关
1.首次上传(WebGLAttributes.createBuffer):
ini
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // 顶点属性
// 或 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); // index
gl.bufferData(gl.ARRAY_BUFFER, attribute.array, attribute.usage);
attribute.usage 直接等于 WebGL 枚举值:
| Three.js | WebGL | 含义 |
|---|---|---|
StaticDrawUsage (35044) |
gl.STATIC_DRAW |
很少改,多次画 |
DynamicDrawUsage (35048) |
gl.DYNAMIC_DRAW |
频繁改,多次画 |
StreamDrawUsage (35040) |
gl.STREAM_DRAW |
改一次画几次 |
只影响 gl.bufferData(..., usage),创建后不能改(需新建 BufferAttribute)。
2.数据变更后(attribute.needsUpdate = true → version++):
c
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 无 updateRanges:全量更新
gl.bufferSubData(gl.ARRAY_BUFFER, 0, attribute.array);
// 有 updateRanges:局部更新
gl.bufferSubData(
gl.ARRAY_BUFFER,
range.start * array.BYTES_PER_ELEMENT,
array,
range.start,
range.count
);
array 的 TypedArray 类型 决定 WebGL 的 type 参数:
array 类型 |
WebGL type |
|---|---|
Float32Array |
gl.FLOAT |
Float16Array / Uint16Array(Float16 属性) |
gl.HALF_FLOAT |
Uint16Array |
gl.UNSIGNED_SHORT |
Int16Array |
gl.SHORT |
Uint32Array |
gl.UNSIGNED_INT |
Int32Array |
gl.INT |
Int8Array |
gl.BYTE |
Uint8Array / Uint8ClampedArray |
gl.UNSIGNED_BYTE |
VAO 相关
绘制前(WebGLBindingStates.setupVertexAttributes):
go
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(location);
// 普通浮点 / 可归一化整数
gl.vertexAttribPointer(
location,
attribute.itemSize, // size
type, // 由 array 类型推导
attribute.normalized, // normalized
stride, // itemSize * bytesPerElement(非交错时)
offset // 0(非交错时)
);
// 整数 attribute(gpuType === IntType 或 INT/UINT 数组)
gl.vertexAttribIPointer(
location,
attribute.itemSize,
type,
stride,
offset
);
绘制
count 不参与 bufferData / vertexAttribPointer,而是决定画多少:
arduino
// 无 index
gl.drawArrays(gl.TRIANGLES, 0, positionAttribute.count);
// 有 index(geometry.index 也是 BufferAttribute)
gl.drawElements(
gl.TRIANGLES,
indexAttribute.count, // 索引个数
gl.UNSIGNED_SHORT, // 由 index.array 类型决定
0
);
- 顶点 attribute 的
count= 顶点数 geometry.index的count= 索引数(drawElements的 count)
version
WebGL 没有 "version" 概念;Three 用它在 CPU 侧判断是否要重新上传:
ini
// 伪逻辑
if (cached.version < attribute.version) {
gl.bufferSubData(...);
cached.version = attribute.version;
}
局部更新
ini
attribute.addUpdateRange(start, count); // 以 array 元素为单位
attribute.needsUpdate = true;
映射为:
c
gl.bufferSubData(
gl.ARRAY_BUFFER,
start * array.BYTES_PER_ELEMENT, // 字节偏移
array,
start, // 源数组起始
count // 元素个数
);
上传后 Three 会 clearUpdateRanges()。
3-GPU 同步机制
arduino
set needsUpdate( value ) {
if ( value === true ) this.version ++;
}
修改 attribute.array 后需设置 attribute.needsUpdate = true,渲染器才会把新数据传到 GPU。
局部更新(避免全量上传):
javascript
addUpdateRange( start, count ) {
this.updateRanges.push( { start, count } );
}
clearUpdateRanges() {
this.updateRanges.length = 0;
}
上传完成后的回调(例如释放 CPU 端数据):
kotlin
onUpload( callback ) {
this.onUploadCallback = callback;
return this;
}
4-数据读写 API
按 顶点索引 访问,内部会处理 itemSize 偏移和 normalized 转换:
- 通用:
getComponent(index, component)/setComponent(index, component, value) - 分量:
getX/Y/Z/W、setX/Y/Z/W - 批量:
setXY、setXYZ、setXYZW - 整块:
set(value, offset)、copyArray(array)
normalized === true 时,读写会通过 normalize() / denormalize() 在逻辑值与存储值之间转换。例如 Uint8Array 存颜色 255 时,读取得到 1.0。
5-几何变换方法
对属性中每个向量批量应用矩阵(常用于变形、烘焙):
| 方法 | 适用场景 |
|---|---|
applyMatrix3(m) |
itemSize 为 2 或 3(如 UV、法线) |
applyMatrix4(m) |
itemSize 为 3(位置) |
applyNormalMatrix(m) |
法线变换(itemSize === 3) |
transformDirection(m) |
方向向量(不受平移影响) |
内部用模块级 _vector / _vector2 复用,避免循环中频繁 new Vector3()。
6-复制与序列化
copy(source):深拷贝数组并复制元数据copyAt(index1, attribute, index2):复制单个顶点数据clone():创建新实例toJSON():序列化为 JSON(含itemSize、type、array、normalized等)
7-子类(TypedArray 便捷封装)
文件末尾导出多个子类,构造时把普通数组转为对应 TypedArray:
| 类名 | 底层数组 | 典型用途 |
|---|---|---|
Float32BufferAttribute |
Float32Array |
位置、法线、UV(最常用) |
Float16BufferAttribute |
Uint16Array(半精度) |
省显存,读写时做 FP16 转换 |
Uint8BufferAttribute |
Uint8Array |
颜色(常配合 normalized: true) |
Uint8ClampedBufferAttribute |
Uint8ClampedArray |
颜色(自动 clamp 0--255) |
Int8/16/32、Uint16/32 |
对应整数数组 | 索引、骨骼权重、自定义数据 |
Float16BufferAttribute 特殊之处
浏览器对 Float16Array 支持不稳定,因此用 Uint16Array 存半精度,在 getX/setX 等方法里调用 fromHalfFloat() / toHalfFloat() 做转换,对外 API 与 Float32 一致。
8-在 Three.js 中的位置
less
BufferGeometry
├── attributes.position → Float32BufferAttribute (itemSize: 3)
├── attributes.normal → Float32BufferAttribute (itemSize: 3)
├── attributes.uv → Float32BufferAttribute (itemSize: 2)
└── index → Uint16/32BufferAttribute (itemSize: 1)
↓
WebGLRenderer 读取 BufferAttribute
↓
创建 WebGLBuffer,绑定到 shader attribute
9-使用要点
- 优先用子类(如
Float32BufferAttribute),不要直接new BufferAttribute([...], 3)。 - 改完数据要
needsUpdate = true。 itemSize必须和数据语义一致:位置/法线是 3,UV 是 2。- 大几何体、频繁局部更新 时用
addUpdateRange减少上传量。 - 颜色用
Uint8+normalized: true可省一半显存。