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

纹理与采样器

先看结论

纹理是 GPU 上的像素容器,采样器是读取这些像素的规则。 v9 把两者拆成独立资源:采样器可以挂在纹理上作为默认值, 也可以在不重建纹理的情况下替换。

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

采样器的默认值是最近邻

createSampler 省略参数时,过滤方式是 nearest、 寻址是 clamp-to-edge。这意味着放大后能看到方块像素, UV 超出 0 到 1 时边缘被拉伸。要平滑过渡就得显式写 linear。

const sampler = device.createSampler({
  minFilter: 'linear',
  magFilter: 'linear',
  mipmapFilter: 'linear',      // 需要纹理带 mipmap
  addressModeU: 'repeat',
  addressModeV: 'repeat',
  maxAnisotropy: 4
});

console.log(sampler.props.addressModeU);   // 'repeat'
参数默认说明
magFilter / minFilternearest放大与缩小过滤,linear 才平滑
mipmapFilternonenone、nearest、linear
addressModeU/Vclamp-to-edge平铺用 repeat
maxAnisotropy1斜视角下的清晰度,需要后端支持
typecolor-sampler深度比较采样用 comparison-sampler

创建纹理并写入像素

data 参数已废弃,改成创建后调用 writeData。 usage 决定这张纹理能做什么:采样、作为渲染目标、作为拷贝目标。

const SIZE = 64;
const pixels = new Uint8Array(SIZE * SIZE * 4);

for (let y = 0; y < SIZE; y++) {
  for (let x = 0; x < SIZE; x++) {
    const i = (y * SIZE + x) * 4;
    const cell = ((x >> 3) + (y >> 3)) & 1;
    pixels[i] = cell ? 232 : 52;
    pixels[i + 1] = cell ? 186 : 118;
    pixels[i + 2] = cell ? 84 : 214;
    pixels[i + 3] = 255;
  }
}

const texture = device.createTexture({
  width: SIZE,
  height: SIZE,
  format: 'rgba8unorm',
  usage: luma.Texture.SAMPLE | luma.Texture.COPY_DST,
  sampler: sampler
});

texture.writeData(pixels);

writeData 省略区域参数时按整张 mip 写入,数据按行紧密排列即可, 后端需要的行对齐由它自己计算。指定区域时再传 byteOffset 与行列范围。

上传图片与视频帧

copyExternalImage 接受 ImageBitmap、HTMLImageElement 与视频帧。图片是异步资源,必须等加载完成后再上传。

const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'https://example.com/terrain.png';

img.onload = function () {
  const tex = device.createTexture({
    width: img.naturalWidth,
    height: img.naturalHeight,
    format: 'rgba8unorm',
    usage: luma.Texture.SAMPLE | luma.Texture.COPY_DST | luma.Texture.RENDER,
    sampler: { minFilter: 'linear', magFilter: 'linear', mipmapFilter: 'none' }
  });

  tex.copyExternalImage({ image: img });
  model.setBindings({ uTexture: tex });
};

跨域图片没有 crossOrigin 时,上传会被浏览器拒绝, 控制台提示 tainted canvas。这条限制与原生 WebGL 完全一致。

在着色器里采样

纹理通过 bindings 交给模型,键名与 GLSL 里的 sampler2D 同名。纹理单元由 luma.gl 自动分配, 不需要写 uniform1i。

const model = new luma.Model(device, {
  vs: [
    '#version 300 es',
    'in vec3 positions;',
    'in vec2 uvs;',
    'out vec2 vUV;',
    'void main() {',
    '  vUV = uvs;',
    '  gl_Position = vec4(positions, 1.0);',
    '}'
  ].join('\n'),
  fs: [
    '#version 300 es',
    'precision highp float;',
    'in vec2 vUV;',
    'uniform sampler2D uTexture;',
    'out vec4 fragColor;',
    'void main() {',
    '  fragColor = texture(uTexture, vUV);',
    '}'
  ].join('\n'),
  attributes: { positions: positions, uvs: uvs },
  bufferLayout: [
    { name: 'positions', stepMode: 'vertex', format: 'float32x3' },
    { name: 'uvs', stepMode: 'vertex', format: 'float32x2' }
  ],
  bindings: { uTexture: texture },
  vertexCount: 6,
  topology: 'triangle-list'
});

作为渲染目标

同一张纹理既可以采样也可以渲染,前提是创建时把两种用途都写全。 只给 RENDER 的纹理在 WebGL 上可能仍能采样成功, 换成 WebGPU 就会被判定不可采样。

const color = device.createTexture({
  width: 512,
  height: 512,
  format: 'rgba8unorm',
  usage: luma.Texture.SAMPLE | luma.Texture.RENDER | luma.Texture.COPY_DST,
  sampler: { minFilter: 'linear', magFilter: 'linear' }
});

const depth = device.createTexture({
  width: 512,
  height: 512,
  format: device.preferredDepthFormat,
  usage: luma.Texture.RENDER
});

const fbo = device.createFramebuffer({
  width: 512,
  height: 512,
  colorAttachments: [color],
  depthStencilAttachment: depth
});

注意事项

替换采样器时漏字段

texture.setSampler({...}) 传参数对象时,省略的字段会回到默认值, 而不是保留上一份设置。需要保留其余参数时,先把原 texture.sampler.props 展开,再覆盖目标字段。

尺寸不是 2 的幂

WebGL2 支持非 2 的幂纹理,但只有 clamp-to-edge 与无 mipmap 时才可靠。 用 repeat 或 mipmap 时,宽高先补齐到 2 的幂, 否则采样结果会被静默钳制。

每帧重建纹理

视频或动态数据每帧都 createTexture,会不断申请显存。 尺寸不变时复用同一张纹理,只调 writeData 或 copyExternalImage。

引用本文

经纬志(2026)。《纹理与采样器》。https://www.jingweizhi.com/lumagl/tutorial/textures-samplers.html(访问日期 2026-11-03)。