经纬志
首页 / luma.gl / 教程

几何与缓冲布局

先看结论

顶点数据进入 GPU 之前要经过三层:类型化数组、缓冲、布局声明。 v9 把 CPU 几何与 GPU 缓冲彻底分开,Geometry 只保存数据, 上传由 Model 或 GPUGeometry 完成。

最后验证 2026-11-03 · luma.gl 9.4 · 更新 2026-10-29 · 引用本文

从类型化数组到缓冲

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.UNIFORMuniform 缓冲写入后读不到值
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)。