08-BufferAttribute

15 阅读4分钟

源码:github.com/buglas/thre…

BufferAttribute 是用来存储几何体顶点数据的类。

BufferAttribute负责把几何体上的顶点数据(位置、法线、UV、颜色等)以 TypedArray 形式存储,并高效上传到 GPU。

基本用法如下:

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-构造函数与核心属性

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;             // 数据变更版本号
}
属性含义
nameattribute 变量名
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(绑定顶点属性)。

对应关系如下。

总览:两条管线

image-20260707232255374

VBO 相关

1.首次上传(WebGLAttributes.createBuffer):

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.jsWebGL含义
StaticDrawUsage (35044)gl.STATIC_DRAW很少改,多次画
DynamicDrawUsage (35048)gl.DYNAMIC_DRAW频繁改,多次画
StreamDrawUsage (35040)gl.STREAM_DRAW改一次画几次

只影响 gl.bufferData(..., usage),创建后不能改(需新建 BufferAttribute)。

2.数据变更后(attribute.needsUpdate = trueversion++):

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
Float32Arraygl.FLOAT
Float16Array / Uint16Array(Float16 属性)gl.HALF_FLOAT
Uint16Arraygl.UNSIGNED_SHORT
Int16Arraygl.SHORT
Uint32Arraygl.UNSIGNED_INT
Int32Arraygl.INT
Int8Arraygl.BYTE
Uint8Array / Uint8ClampedArraygl.UNSIGNED_BYTE

VAO 相关

绘制前(WebGLBindingStates.setupVertexAttributes):

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,而是决定画多少:

// 无 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.indexcount = 索引数(drawElements 的 count)

version

WebGL 没有 “version” 概念;Three 用它在 CPU 侧判断是否要重新上传:

// 伪逻辑
if (cached.version < attribute.version) {
  gl.bufferSubData(...);
  cached.version = attribute.version;
}

局部更新

attribute.addUpdateRange(start, count);  // 以 array 元素为单位
attribute.needsUpdate = true;

映射为:

gl.bufferSubData(
  gl.ARRAY_BUFFER,
  start * array.BYTES_PER_ELEMENT,  // 字节偏移
  array,
  start,                             // 源数组起始
  count                              // 元素个数
);

上传后 Three 会 clearUpdateRanges()

3-GPU 同步机制

set needsUpdate( value ) {
    if ( value === true ) this.version ++;
}

修改 attribute.array 后需设置 attribute.needsUpdate = true,渲染器才会把新数据传到 GPU。

局部更新(避免全量上传):

addUpdateRange( start, count ) {
    this.updateRanges.push( { start, count } );
}
clearUpdateRanges() {
    this.updateRanges.length = 0;
}

上传完成后的回调(例如释放 CPU 端数据):

onUpload( callback ) {
    this.onUploadCallback = callback;
    return this;
}

4-数据读写 API

按 顶点索引 访问,内部会处理 itemSize 偏移和 normalized 转换:

  • 通用:getComponent(index, component) / setComponent(index, component, value)
  • 分量:getX/Y/Z/WsetX/Y/Z/W
  • 批量:setXYsetXYZsetXYZW
  • 整块: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(含 itemSizetypearraynormalized 等)

7-子类(TypedArray 便捷封装)

文件末尾导出多个子类,构造时把普通数组转为对应 TypedArray:

类名底层数组典型用途
Float32BufferAttributeFloat32Array位置、法线、UV(最常用)
Float16BufferAttributeUint16Array(半精度)省显存,读写时做 FP16 转换
Uint8BufferAttributeUint8Array颜色(常配合 normalized: true
Uint8ClampedBufferAttributeUint8ClampedArray颜色(自动 clamp 0–255)
Int8/16/32Uint16/32对应整数数组索引、骨骼权重、自定义数据

Float16BufferAttribute 特殊之处

浏览器对 Float16Array 支持不稳定,因此用 Uint16Array 存半精度,在 getX/setX 等方法里调用 fromHalfFloat() / toHalfFloat() 做转换,对外 API 与 Float32 一致。

8-在 Three.js 中的位置

BufferGeometry
  ├── attributes.positionFloat32BufferAttribute (itemSize: 3)
  ├── attributes.normal    → Float32BufferAttribute (itemSize: 3)
  ├── attributes.uv        → Float32BufferAttribute (itemSize: 2)
  └── index                → Uint16/32BufferAttribute (itemSize: 1)
         ↓
    WebGLRenderer 读取 BufferAttribute
         ↓
    创建 WebGLBuffer,绑定到 shader attribute

9-使用要点

  1. 优先用子类(如 Float32BufferAttribute),不要直接 new BufferAttribute([...], 3)
  2. 改完数据要 needsUpdate = true
  3. itemSize 必须和数据语义一致:位置/法线是 3,UV 是 2。
  4. 大几何体、频繁局部更新 时用 addUpdateRange 减少上传量。
  5. 颜色用 Uint8 + normalized: true 可省一半显存。