几何与缓冲布局
顶点数据进入 GPU 之前要经过三层:类型化数组、缓冲、布局声明。
v9 把 CPU 几何与 GPU 缓冲彻底分开,Geometry 只保存数据,
上传由 Model 或 GPUGeometry 完成。
从类型化数组到缓冲
device.createBuffer 直接接一个类型化数组时,会按数组长度分配,
并以顶点用途初始化。需要逐帧改写、作为索引或 uniform 使用时,
必须显式声明 usage。
// 一次性上传:紧凑写法
const positions = device.createBuffer(new Float32Array([
-0.6, -0.45, 0,
0.6, -0.45, 0,
0.0, 0.55, 0
]));
// 逐帧更新:声明可写
const dynamic = device.createBuffer({
data: new Float32Array(3 * 2048),
usage: luma.Buffer.VERTEX | luma.Buffer.COPY_DST
});
// 索引缓冲
const indices = device.createBuffer({
data: new Uint32Array([0, 1, 2]),
usage: luma.Buffer.INDEX,
indexType: 'uint32'
});
dynamic.write(newData); // 覆盖写入
console.log(dynamic.byteLength, dynamic.updateTimestamp);
| 用途常量 | 含义 | 缺少时的后果 |
|---|---|---|
Buffer.VERTEX | 顶点属性缓冲 | 能创建,但绑定到属性时行为未定义 |
Buffer.INDEX | 索引缓冲 | 索引绘制失败 |
Buffer.UNIFORM | uniform 缓冲 | 写入后读不到值 |
Buffer.STORAGE | 存储缓冲,供计算着色器 | WebGPU 上直接拒绝 |
Buffer.COPY_DST | 允许作为写入目标 | write() 报错或静默无效 |
Buffer.COPY_SRC | 允许被读取 | 回读失败 |
显式布局:一个缓冲一个属性
属性名、着色器里的 in 变量、布局里的 name
必须完全一致。格式只需与数组的分量数匹配:vec3 对应
float32x3。
const model = new luma.Model(device, {
vs: vsSource,
fs: fsSource,
attributes: {
positions: device.createBuffer(new Float32Array([...])),
colors: device.createBuffer(new Float32Array([...]))
},
bufferLayout: [
{ name: 'positions', stepMode: 'vertex', format: 'float32x3' },
{ name: 'colors', stepMode: 'vertex', format: 'float32x3' }
],
vertexCount: 3,
topology: 'triangle-list'
});
交错布局
位置、法线、颜色放在同一段内存里,需要用一个条目描述整条记录:
byteStride 是记录长度,每个成员给 byteOffset。
/* 每条记录 32 字节:位置 12 + 法线 12 + 颜色 8 */
const interleaved = new Float32Array([
// x, y, z, nx, ny, nz, r, g
0, 0, 0, 0, 0, 1, 1, 0,
1, 0, 0, 0, 0, 1, 0, 1,
0, 1, 0, 0, 0, 1, 0, 0
]);
const bufferLayout = [
{
name: 'vertexData',
stepMode: 'vertex',
byteStride: 32,
attributes: [
{ attribute: 'positions', format: 'float32x3', byteOffset: 0 },
{ attribute: 'normals', format: 'float32x3', byteOffset: 12 },
{ attribute: 'colors', format: 'float32x2', byteOffset: 24 }
]
}
];
偏移或跨度写错时不会抛错,画面整体错位:顶点位置被当成法线读, 几何体会以奇怪的方式扭曲。先减去其他属性,逐个验证偏移。
实例化的布局
stepMode: 'instance' 表示每个实例读一条记录,绘制调用仍然只有一次。
实例属性适合放偏移、缩放与颜色。
const instanceBuffer = device.createBuffer(new Float32Array([
// offsetX, offsetY, scale, alpha
-0.6, 0, 0.25, 1,
0.0, 0, 0.25, 0.7,
0.6, 0, 0.25, 0.4
]));
const model = new luma.Model(device, {
vs: instancedVertexShader,
fs: fsSource,
attributes: {
positions: quadPositions,
instanceData: instanceBuffer
},
bufferLayout: [
{ name: 'positions', stepMode: 'vertex', format: 'float32x2' },
{
name: 'instanceData',
stepMode: 'instance',
byteStride: 16,
attributes: [
{ attribute: 'instanceOffsets', format: 'float32x2', byteOffset: 0 },
{ attribute: 'instanceScale', format: 'float32', byteOffset: 8 },
{ attribute: 'instanceAlpha', format: 'float32', byteOffset: 12 }
]
}
],
vertexCount: 6,
instanceCount: 3,
topology: 'triangle-list'
});
Geometry 与 GPUGeometry
Geometry 是纯 CPU 容器,构造时不传设备;Model
接住它之后自动上传。GPUGeometry 包装已经创建好的缓冲,
适合多份模型共享同一批顶点。
const geometry = new luma.Geometry({
topology: 'triangle-list',
attributes: {
positions: new Float32Array([...]),
colors: new Float32Array([...])
},
indices: new Uint16Array([0, 1, 2])
});
const model = new luma.Model(device, {
geometry: geometry,
vs: vsSource,
fs: fsSource
});
console.log(geometry.getVertexCount()); // 3
注意事项
改动 CPU 数组后忘记同步
构造 Model 时属性被上传一次。之后改 Geometry 里的
类型化数组不会自动生效,要塞回 setGeometry,
或者直接对 Buffer 调 write()。
索引类型与顶点数不匹配
顶点超过 65535 仍用 Uint16Array 做索引,超出部分会回绕,
画面出现随机三角面。超过这个量级就改用 Uint32Array 配
indexType: 'uint32'。
逐帧新建数组
每帧 new Float32Array(count * 3) 会产生持续的压力,
触发 GC 抖动。点数固定时在外部复用一个数组,只改内容再
write()。
经纬志(2026)。《几何与缓冲布局》。https://www.jingweizhi.com/lumagl/tutorial/geometry-buffers.html(访问日期 2026-11-03)。