纹理与采样器
纹理是 GPU 上的像素容器,采样器是读取这些像素的规则。 v9 把两者拆成独立资源:采样器可以挂在纹理上作为默认值, 也可以在不重建纹理的情况下替换。
采样器的默认值是最近邻
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 / minFilter | nearest | 放大与缩小过滤,linear 才平滑 |
mipmapFilter | none | none、nearest、linear |
addressModeU/V | clamp-to-edge | 平铺用 repeat |
maxAnisotropy | 1 | 斜视角下的清晰度,需要后端支持 |
type | color-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)。