渲染到纹理又称为离屏渲染,这次我们将在纹理上绘制一个红色三角形并将纹理显示在屏幕上。在线代码
首先,准备将作为渲染目标的纹理。
const renderTargetTexture = g_device.createTexture({
size: [512, 512, 1],
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
format: 'rgba8unorm',
});
const renderTargetTextureView = renderTargetTexture.createView();
我们来使用 g_device.createTexture
函数创建纹理。这次,纹理的大小设置为 512x512。至于纹理的使用,我们指定两件事:将其用作渲染目标和将其用作纹理。要将其用作渲染目标,请指定 GPUTextureUsage.RENDER_ATTACHMENT
;要将其用作纹理,请指定 GPUTextureUsage.TEXTURE_BINDING
。此外,“rgba8unorm”被指定为纹理格式。
另外,为了使用纹理作为渲染目标,我们需要纹理的视图,因此我们通过renderTargetTexture.createView()
方法创建一个。
接下来,准备引用纹理时所需的采样器。
const sampler = g_device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
});
显示三角形的WGSL代码在《在WebGPU上绘制三角形》中介绍过,所以这边就省略了。
以下是用于在屏幕上显示纹理的 WGSL 代码。
const vertWGSL2 = `
struct VertexOutput {
@builtin(position) position: vec4,
@location(0) texCoord: vec2,
}
@vertex
fn main(
@builtin(vertex_index) VertexIndex : u32
) -> VertexOutput {
var output : VertexOutput;
let x = f32((VertexIndex & 1) << 2);
let y = f32((VertexIndex & 2) << 1);
output.texCoord.x = x * 0.5;
output.texCoord.y = y * 0.5;
output.position = vec4(x - 1.0, y - 1.0, 0, 1);
return output;
}
`;
const fragWGSL2 = `
@group(0) @binding(0) var texture: texture_2d;
@group(0) @binding(1) var textureSampler: sampler;
struct FragmentInput {
@location(0) texCoord: vec2,
}
@fragment
fn main(input: FragmentInput) -> @location(0) vec4 {
return textureSample(texture, textureSampler, input.texCoord);
}
`;
对于片元着色器非常简单,因为代码只引用纹理。
但是需要解释下在顶点着色器中所做的事情。这里有一些技巧代码,可以在着色器中即兴创作,并使用 UV 绘制一个覆盖屏幕的大三角形,而无需任何顶点数据。
准备一个渲染管线。绘制三角形的RenderPipeline与上次几乎相同,但有一个变化。在fragment.targets[0].format部分指定format:'rgba8unorm'
以匹配纹理的格式而不是presentationFormat。
对于绘制纹理时新创建的RenderPipeline,指定之前准备的新WGSL代码。
// 绘制三角形时的RenderPipeline
const pipeline = g_device.createRenderPipeline({
layout: 'auto',
vertex: {
module: g_device.createShaderModule({
code: vertWGSL,
}),
entryPoint: 'main',
},
fragment: {
module: g_device.createShaderModule({
code: fragWGSL,
}),
entryPoint: 'main',
targets: [
{
format: 'rgba8unorm', // 仅更改此处。匹配纹理格式
},
],
},
primitive: {
topology: 'triangle-list',
},
});
// 绘制纹理时的RenderPipeline
const pipeline2 = g_device.createRenderPipeline({
layout: 'auto',
vertex: {
module: g_device.createShaderModule({
code: vertWGSL2,
}),
entryPoint: 'main',
},
fragment: {
module: g_device.createShaderModule({
code: fragWGSL2,
}),
entryPoint: 'main',
targets: [
{
format: presentationFormat,
},
],
},
primitive: {
topology: 'triangle-list',
},
});
创建一个 BindGroup 来引用纹理。这部分指定了之前创建的纹理视图和纹理采样器,其方式与普通纹理映射相同。
const bindGroup = g_device.createBindGroup({
layout: pipeline2.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: renderTargetTextureView,
},
{
binding: 1,
resource: sampler,
},
],
});
接下来,创建渲染命令。它分为两个pass,第一遍(将三角形绘制到纹理)和第二遍(将纹理显示在屏幕上)。
每个都必须创建一个单独的 CommandEncoder。此时,在GPURenderPassDescriptor的colorAttachments中,指定了每一个pass使用的渲染目标。在第一个pass中,指定了本次提前创建的纹理视图renderTargetTextureView
;在第二个pass中,指定了context.getCurrentTexture().createView()
创建的屏幕纹理视图。
// first pass
const commandEncoder = g_device.createCommandEncoder();
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: [
{
view: renderTargetTextureView,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.draw(3, 1, 0, 0);
passEncoder.end();
// second pass
const commandEncoder2 = g_device.createCommandEncoder();
const renderPassDescriptor2: GPURenderPassDescriptor = {
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
};
const passEncoder = commandEncoder2.beginRenderPass(renderPassDescriptor2);
passEncoder.setPipeline(pipeline2);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.draw(3, 1, 0, 0);
passEncoder.end();
g_device.queue.submit([commandEncoder.finish(), commandEncoder2.finish()]);
第二个Pass指定之前创建的 RenderPipeline 和 BindGroup。
最后,我们组合两个 CommandEncoder 并将它们传递给 g_device.queue.submit
函数。
我们可以从屏幕上看到三角形被绘制到纹理上。
在WebGL中,需要生成一个FBO(Framebuffer对象)来设置渲染目标,并且FBO中的绑定操作也很复杂。WebGPU 上的纹理渲染给人的印象是编码变得更加直观。