七、WebGPU 贴图操作

62 阅读3分钟

接上文立方体,我们介绍了在WebGPU中操作三维物体的方法,学习到了除了在创建Buffer时可以填充数据以外,还可以利用device.queue.writeBuffer 这个API进行数据的写入。今天,让我们继续来操作这个立方体,今天要学习的内容是给立方体贴图。

创建一个图片对象

用 await 代表这个操作必须在 async 函数中,或者在 html 中提前做好 img 标签并加载纹理贴图

const textureUrl = 'texture.webp';
const res = await fetch(textureUrl)
const img = await res.blob()
// const img = document.createElement('img')
// img.src = textureUrl
// await img.decode()
const imageBitmap = await createImageBitmap(img);

创建一个纹理对象

  • dimension: 纹理可以是1d、2d、3d的,这里我们使用2d纹理就好。
  • size: 与dimension所对应,表示纹理的大小
  • format: 'rgba8unorm',这里对format的格式说明一下:r, g, b, a: 表示red, green, blue, alpha。unorm: 表示unsigned normalized,即表示是无符号的,归一化为 0~1范围的值。
  • usage: 这里可以认为是固定搭配,至少需要这三种usage。
const cubeTexture = device.createTexture({
  dimension: '2d',
  size: [imageBitmap.width, imageBitmap.height],
  format: 'rgba8unorm',
  usage:
    GPUTextureUsage.TEXTURE_BINDING |
    GPUTextureUsage.COPY_DST |
    GPUTextureUsage.RENDER_ATTACHMENT,
});

拷贝外部图像数据到纹理对象

  • source: GPUImageCopyExternalImage,其是一个对象,需要具有以下属性:只能是 ImageBitmap | HTMLCanvasElement | OffscreenCanvas 对象。
  • destination: GPUImageCopyTextureTagged,其中需要具有以下属性:必须具有texture属性,为 GPUTexture
  • copySize: 表示复制的纹理区域大小。
device.queue.copyExternalImageToTexture(
    { source: imageBitmap },
    { texture: cubeTexture },
    [imageBitmap.width, imageBitmap.height]
);

创建采样器--纹理

现在衣服制造完毕,并且也把快递发送给了GPU,但是这件衣服稍微有一点点的花里胡哨,可能GPU拿到这件新衣不知道应该怎样去穿,所以现在我们需要给GPU送去一封说明书。 而在WebGPU中,说明书却是单独发送的,这有一个好处,这提高了说明书的复用性,如果另一件相似的衣服我们传给GPU,那么它可以继续复用之前的说明书。

const sampler = device.createSampler({
    magFilter: 'linear',
    minFilter: 'linear',
  });

发送(采样器、纹理视图)到渲染通道

来到熟悉的环节,创建 BindGroup 来往渲染通道中发送数据。 要向渲染通道传递纹理和采样器,必须创建一个 “Pipeline布局” 对象。这个布局对象要对纹理对象、采样器对象进行绑定。在 WebGPU 中,将诸如 uniform变量、采样器、纹理对象 等资源统一打组,这个组叫 GPUBindGroup。

const uniformBindGroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      {
        binding: 0,
        resource: {
          buffer: uniformBuffer,
        },
      },
      {
        binding: 1,
        resource: sampler,
      },
      {
        binding: 2,
        resource: cubeTexture.createView(),
      },
    ],
  });

GPU绘制

发送完成后,现在来到GPU这边,GPU收到货之后,Shader开始运作了。

@binding(1) @group(0) var mySampler: sampler;
@binding(2) @group(0) var myTexture: texture_2d<f32>;

@fragment
fn main(@location(0) fragUV: vec2<f32>,@location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
  return textureSample(myTexture, mySampler, fragUV) * fragPosition;
}

sprites 贴图

// initPipeline 函数 /////////////////////////
//分割图片
const uvOffset = new Float32Array([0, 0, 1 / 3, 1 / 2]);
const uvBuffer = device.createBuffer({
    label: 'GPUBuffer store UV offset',
    size: 4 * 4, // 4 x uint32
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
device.queue.writeBuffer(uvBuffer, 0, uvOffset);
....
// createBindGroup 修改
....
    {
        binding: 2,
        resource: cubeTexture.createView(),
    },
    {
        binding: 3,
        resource: {
            buffer: uvBuffer
        }
    }
.....

return { ..., uvBuffer, uvOffset }



// frame 函数 //////////////////////////////////
  let count = 0;
  let uvOffset = pipelineObj.uvOffset;
  // start loop
  function frame() {
      count++;
      if (count % 30 === 0) {
          uvOffset[0] = uvOffset[0] >= 2 / 3 ? 0 : uvOffset[0] + 1 / 3
          if (count % 90 === 0)
              uvOffset[1] = uvOffset[1] >= 1 / 2 ? 0 : uvOffset[1] + 1 / 2
          device.queue.writeBuffer(pipelineObj.uvBuffer, 0, uvOffset)
      }
      ...


// 片源着色器 ///////////////////////////////////////////////////////
@binding(3) @group(0) var<uniform> uvOffset : vec4<f32>;
// main 修改
var uv = fragUV * vec2<f32>(uvOffset[2], uvOffset[3]) + vec2<f32>(uvOffset[0], uvOffset[1]);
  return textureSample(myTexture, mySampler, uv) * fragPosition;

canvas 贴图

const canvas2 = document.querySelector('canvas#canvas');
device.queue.copyExternalImageToTexture(
    { source: canvas2 },
    { texture: cubeTexture },
    [canvas2.width, canvas2.height]
)

video 贴图

// run 函数 ///////////////////////////
const video = document.createElement('video');
    video.loop = true
    video.autoplay = true
    video.muted = true
    video.src = videoUrl
    await video.play()
const sampler = device.createSampler({
    magFilter: 'linear',
    minFilter: 'linear',
});    
    ...

// frame 函数 ///////////////////////////
const videoGroup = device.createBindGroup({
    layout: pipelineObj.pipeline.getBindGroupLayout(1),
    entries: [
        {
            binding: 0,
            resource: sampler
        },
        {
            binding: 1,
            resource: device.importExternalTexture({
                source: video
            })
        }
    ]
})

draw(..., videoGroup)

//draw 函数 ////////////////////////////
passEncoder.setBindGroup(1, videoGroup)


// 片源着色器 ///////////////////////////////////////////////////////
@group(1) @binding(0) var Sampler: sampler;
@group(1) @binding(1) var Texture: texture_external;

@fragment
fn main(@location(0) fragUV: vec2<f32>,
        @location(1) fragPosition: vec4<f32>) -> @location(0) vec4<f32> {
  return textureSampleBaseClampToEdge(Texture, Sampler, fragUV) * fragPosition;
}