代码分析说明:
import { assert, makeSample, SampleInit } from '../../components/SampleLayout';
import animometerWGSL from './animometer.wgsl';
const init: SampleInit = async ({ canvas, pageState, gui }) => {
const adapter = await navigator.gpu.requestAdapter();
assert(adapter, 'requestAdapter returned null');
const device = await adapter.requestDevice();
if (!pageState.active) return;
const perfDisplayContainer = document.createElement('div');
perfDisplayContainer.style.color = 'white';
perfDisplayContainer.style.background = 'black';
perfDisplayContainer.style.position = 'absolute';
perfDisplayContainer.style.top = '10px';
perfDisplayContainer.style.left = '10px';
const perfDisplay = document.createElement('pre');
perfDisplayContainer.appendChild(perfDisplay);
if (canvas.parentNode) {
canvas.parentNode.appendChild(perfDisplayContainer);
} else {
console.error('canvas.parentNode is null');
}
// 新建一组URL参数
const params = new URLSearchParams(window.location.search);
const settings = {
numTriangles: Number(params.get('numTriangles')) || 20000,
renderBundles: Boolean(params.get('renderBundles')),
dynamicOffsets: Boolean(params.get('dynamicOffsets')),
};
const context = canvas.getContext('webgpu') as GPUCanvasContext;
const devicePixelRatio = window.devicePixelRatio;
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: presentationFormat,
alphaMode: 'premultiplied',
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
});
const timeBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: {
type: 'uniform',
minBindingSize: 4,
},
},
],
});
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: {
type: 'uniform',
minBindingSize: 20,
},
},
],
});
const dynamicBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: {
type: 'uniform',
hasDynamicOffset: true,
minBindingSize: 20,
},
},
],
});
const vec4Size = 4 * Float32Array.BYTES_PER_ELEMENT;
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [timeBindGroupLayout, bindGroupLayout],
});
const dynamicPipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [timeBindGroupLayout, dynamicBindGroupLayout],
});
const shaderModule = device.createShaderModule({
code: animometerWGSL,
});
const pipelineDesc: GPURenderPipelineDescriptor = {
layout: 'auto',
vertex: {
module: shaderModule,
entryPoint: 'vert_main',
buffers: [
{
// vertex buffer
arrayStride: 2 * vec4Size,
stepMode: 'vertex',
attributes: [
{
// vertex positions
shaderLocation: 0,
offset: 0,
format: 'float32x4',
},
{
// vertex colors
shaderLocation: 1,
offset: vec4Size,
format: 'float32x4',
},
],
},
],
},
fragment: {
module: shaderModule,
entryPoint: 'frag_main',
targets: [
{
format: presentationFormat,
},
],
},
primitive: {
topology: 'triangle-list',
frontFace: 'ccw',
cullMode: 'none',
},
};
const pipeline = device.createRenderPipeline({
...pipelineDesc,
layout: pipelineLayout,
});
const dynamicPipeline = device.createRenderPipeline({
...pipelineDesc,
layout: dynamicPipelineLayout,
});
const vertexBuffer = device.createBuffer({
size: 2 * 3 * vec4Size,
usage: GPUBufferUsage.VERTEX,
mappedAtCreation: true,
});
// prettier-ignore
new Float32Array(vertexBuffer.getMappedRange()).set([
// position data /**/ color data
0, 0.1, 0, 1, /**/ 1, 0, 0, 1,
-0.1, -0.1, 0, 1, /**/ 0, 1, 0, 1,
0.1, -0.1, 0, 1, /**/ 0, 0, 1, 1,
]);
vertexBuffer.unmap();
function configure() {
const numTriangles = settings.numTriangles;
const uniformBytes = 5 * Float32Array.BYTES_PER_ELEMENT;
const alignedUniformBytes = Math.ceil(uniformBytes / 256) * 256;
const alignedUniformFloats =
alignedUniformBytes / Float32Array.BYTES_PER_ELEMENT;
const uniformBuffer = device.createBuffer({
size: numTriangles * alignedUniformBytes + Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM,
});
const uniformBufferData = new Float32Array(
numTriangles * alignedUniformFloats
);
const bindGroups = new Array(numTriangles);
for (let i = 0; i < numTriangles; ++i) {
uniformBufferData[alignedUniformFloats * i + 0] =
Math.random() * 0.2 + 0.2; // scale
uniformBufferData[alignedUniformFloats * i + 1] =
0.9 * 2 * (Math.random() - 0.5); // offsetX
uniformBufferData[alignedUniformFloats * i + 2] =
0.9 * 2 * (Math.random() - 0.5); // offsetY
uniformBufferData[alignedUniformFloats * i + 3] =
Math.random() * 1.5 + 0.5; // scalar
uniformBufferData[alignedUniformFloats * i + 4] = Math.random() * 10; // scalarOffset
bindGroups[i] = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer,
offset: i * alignedUniformBytes,
size: 6 * Float32Array.BYTES_PER_ELEMENT,
},
},
],
});
}
const dynamicBindGroup = device.createBindGroup({
layout: dynamicBindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer,
offset: 0,
size: 6 * Float32Array.BYTES_PER_ELEMENT,
},
},
],
});
const timeOffset = numTriangles * alignedUniformBytes;
const timeBindGroup = device.createBindGroup({
layout: timeBindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer,
offset: timeOffset,
size: Float32Array.BYTES_PER_ELEMENT,
},
},
],
});
// writeBuffer too large may OOM. TODO: The browser should internally chunk uploads.
const maxMappingLength =
(14 * 1024 * 1024) / Float32Array.BYTES_PER_ELEMENT;
for (
let offset = 0;
offset < uniformBufferData.length;
offset += maxMappingLength
) {
const uploadCount = Math.min(
uniformBufferData.length - offset,
maxMappingLength
);
device.queue.writeBuffer(
uniformBuffer,
offset * Float32Array.BYTES_PER_ELEMENT,
uniformBufferData.buffer,
uniformBufferData.byteOffset + offset * Float32Array.BYTES_PER_ELEMENT,
uploadCount * Float32Array.BYTES_PER_ELEMENT
);
}
function recordRenderPass(
passEncoder: GPURenderBundleEncoder | GPURenderPassEncoder
) {
if (settings.dynamicOffsets) {
passEncoder.setPipeline(dynamicPipeline);
} else {
passEncoder.setPipeline(pipeline);
}
passEncoder.setVertexBuffer(0, vertexBuffer);
passEncoder.setBindGroup(0, timeBindGroup);
const dynamicOffsets = [0];
for (let i = 0; i < numTriangles; ++i) {
if (settings.dynamicOffsets) {
dynamicOffsets[0] = i * alignedUniformBytes;
// 使用动态渲染管线,每一次设置都设置不同的dynamicOffsets
passEncoder.setBindGroup(1, dynamicBindGroup, dynamicOffsets);
} else {
// 不适用动态的管线
passEncoder.setBindGroup(1, bindGroups[i]);
}
passEncoder.draw(3);
}
}
let startTime: number | undefined = undefined;
const uniformTime = new Float32Array([0]);
const renderPassDescriptor = {
colorAttachments: [
{
view: undefined as GPUTextureView, // Assigned later
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear' as const,
storeOp: 'store' as const,
},
],
};
// 创建一个
const renderBundleEncoder = device.createRenderBundleEncoder({
colorFormats: [presentationFormat],
});
recordRenderPass(renderBundleEncoder);
const renderBundle = renderBundleEncoder.finish();
return function doDraw(timestamp: number) {
if (startTime === undefined) {
startTime = timestamp;
}
uniformTime[0] = (timestamp - startTime) / 1000;
device.queue.writeBuffer(uniformBuffer, timeOffset, uniformTime.buffer);
renderPassDescriptor.colorAttachments[0].view = context
.getCurrentTexture()
.createView();
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
if (settings.renderBundles) {
passEncoder.executeBundles([renderBundle]);
} else {
recordRenderPass(passEncoder);
}
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
};
}
let doDraw = configure();
const updateSettings = () => {
doDraw = configure();
};
if (gui === undefined) {
console.error('GUI not initialized');
} else {
gui
.add(settings, 'numTriangles', 0, 200000)
.step(1)
.onFinishChange(updateSettings);
gui.add(settings, 'renderBundles');
gui.add(settings, 'dynamicOffsets');
}
let previousFrameTimestamp: number | undefined = undefined;
let jsTimeAvg: number | undefined = undefined;
let frameTimeAvg: number | undefined = undefined;
let updateDisplay = true;
function frame(timestamp: number) {
// Sample is no longer the active page.
if (!pageState.active) return;
let frameTime = 0;
if (previousFrameTimestamp !== undefined) {
frameTime = timestamp - previousFrameTimestamp;
}
previousFrameTimestamp = timestamp;
const start = performance.now();
doDraw(timestamp);
const jsTime = performance.now() - start;
if (frameTimeAvg === undefined) {
frameTimeAvg = frameTime;
}
if (jsTimeAvg === undefined) {
jsTimeAvg = jsTime;
}
const w = 0.2;
frameTimeAvg = (1 - w) * frameTimeAvg + w * frameTime;
jsTimeAvg = (1 - w) * jsTimeAvg + w * jsTime;
if (updateDisplay) {
perfDisplay.innerHTML = `Avg Javascript: ${jsTimeAvg.toFixed(
2
)} ms\nAvg Frame: ${frameTimeAvg.toFixed(2)} ms`;
updateDisplay = false;
setTimeout(() => {
updateDisplay = true;
}, 100);
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
};
struct Time {
value : f32,
}
struct Uniforms {
scale : f32,
offsetX : f32,
offsetY : f32,
scalar : f32,
scalarOffset : f32,
}
@binding(0) @group(0) var<uniform> time : Time;
@binding(0) @group(1) var<uniform> uniforms : Uniforms;
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) v_color : vec4<f32>,
}
@vertex
fn vert_main(
@location(0) position : vec4<f32>,
@location(1) color : vec4<f32>
) -> VertexOutput {
var fade = (uniforms.scalarOffset + time.value * uniforms.scalar / 10.0) % 1.0;
if (fade < 0.5) {
fade = fade * 2.0;
} else {
fade = (1.0 - fade) * 2.0;
}
var xpos = position.x * uniforms.scale;
var ypos = position.y * uniforms.scale;
var angle = 3.14159 * 2.0 * fade;
var xrot = xpos * cos(angle) - ypos * sin(angle);
var yrot = xpos * sin(angle) + ypos * cos(angle);
xpos = xrot + uniforms.offsetX;
ypos = yrot + uniforms.offsetY;
var output : VertexOutput;
output.v_color = vec4(fade, 1.0 - fade, 0.0, 1.0) + color;
output.Position = vec4(xpos, ypos, 0.0, 1.0);
return output;
}
@fragment
fn frag_main(@location(0) v_color : vec4<f32>) -> @location(0) vec4<f32> {
return v_color;
}
总结步骤
- 创建了三个
BindGroupLayout
,分别是timeBindGroupLayout
、bindGroupLayout
、dynamicBindGroupLayout
区别是参数下面的minBindingSize
分别是4 、 20 、20 - 创建管线布局
PipelineLayout
, 第一个管线布局pipelineLayout
参数包含了[timeBindGroupLayout, bindGroupLayout]
,第二个管线布局dynamicPipelineLayout
参数包含了[timeBindGroupLayout, dynamicBindGroupLayout]
- 创建渲染管线
RenderPipeline
,第一个管线使用第一个布局pipelineLayout
,第二个管线使用第二个布局dynamicPipelineLayout
,其他的渲染参数都一致 - 使用
bindGroupLayout
创建多个绑定组,存放在bindGroups数组中多少个三角形就有多少个绑定组 - 使用
dynamicBindGroupLayout
创建绑定组,参数设置 offset 为 0 , size为 6 * Float32Array.BYTES_PER_ELEMENT - 使用
timeBindGroupLayout
创建绑定组,参数设置offset为每个顶点的数据大小,size为Float32Array.BYTES_PER_ELEMENT - 使用接口
writeBuffer
批量写入数据,为了防止内存不足一次吸入56M数据 -
settings.dynamicOffsets
使用这个参数动态切换管线 - 动态管线是接口渲染256个字节偏移,设置bindgroup,如
passEncoder.setBindGroup(1, dynamicBindGroup, dynamicOffsets);
- 非动态管线设置bindGroup是使用
passEncoder.setBindGroup(1, bindGroups[i]);
- 创建
bundleEncoder
编码器,返回捆绑渲染 - 在每帧的渲染中,场景普通编码器,如果选择
renderBundles
, 就执行passEncoder.executeBundles([renderBundle]);
,将编码器的内容采用renderBundles
的数据,否则按照正常的顺序 -
device.queue.submit([commandEncoder.finish()]);
提交编码器 - 本案例,渲染200000个三角形可以看出
正常渲染 | 单独采用动态Buffer | 单独使用bundle render | 同时使用 动态Buffer & bundle render |
---|---|---|---|
94ms | 55ms | 45ms | 45ms |
网友评论