Skip to content

Commit 0dcbff9

Browse files
authored
feat: Render bundle, .with(pass) and .with(encoder) support (#2200)
1 parent 07cc3c0 commit 0dcbff9

20 files changed

Lines changed: 1837 additions & 236 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { d } from 'typegpu';
2+
import { Vertex } from './schemas.ts';
3+
4+
export function createCubeGeometry(): d.Infer<typeof Vertex>[] {
5+
const faces: [number[], number[]][] = [
6+
[
7+
[0, 0, 1],
8+
[0, 0, 1],
9+
],
10+
[
11+
[0, 0, -1],
12+
[0, 0, -1],
13+
],
14+
[
15+
[0, 1, 0],
16+
[0, 1, 0],
17+
],
18+
[
19+
[0, -1, 0],
20+
[0, -1, 0],
21+
],
22+
[
23+
[1, 0, 0],
24+
[1, 0, 0],
25+
],
26+
[
27+
[-1, 0, 0],
28+
[-1, 0, 0],
29+
],
30+
];
31+
32+
const verts: d.Infer<typeof Vertex>[] = [];
33+
34+
for (const [_offset, normal] of faces) {
35+
const n = d.vec3f(normal[0], normal[1], normal[2]);
36+
const absN = [
37+
Math.abs(normal[0]),
38+
Math.abs(normal[1]),
39+
Math.abs(normal[2]),
40+
];
41+
42+
let tangent: number[];
43+
let bitangent: number[];
44+
if (absN[2] === 1) {
45+
tangent = [1, 0, 0];
46+
bitangent = [0, 1, 0];
47+
} else if (absN[1] === 1) {
48+
tangent = [1, 0, 0];
49+
bitangent = [0, 0, 1];
50+
} else {
51+
tangent = [0, 0, 1];
52+
bitangent = [0, 1, 0];
53+
}
54+
55+
const center = normal.map((v) => v * 0.5);
56+
const corners = [
57+
[-1, -1],
58+
[1, -1],
59+
[1, 1],
60+
[-1, -1],
61+
[1, 1],
62+
[-1, 1],
63+
];
64+
65+
for (const [u, v] of corners) {
66+
verts.push({
67+
position: d.vec3f(
68+
center[0] + (tangent[0] * u + bitangent[0] * v) * 0.5,
69+
center[1] + (tangent[1] * u + bitangent[1] * v) * 0.5,
70+
center[2] + (tangent[2] * u + bitangent[2] * v) * 0.5,
71+
),
72+
normal: n,
73+
});
74+
}
75+
}
76+
77+
return verts;
78+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<canvas data-fit-to-container></canvas>
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { perlin2d } from '@typegpu/noise';
2+
import tgpu, { d } from 'typegpu';
3+
import * as m from 'wgpu-matrix';
4+
import { defineControls } from '../../common/defineControls.ts';
5+
import { setupOrbitCamera } from '../../common/setup-orbit-camera.ts';
6+
import { createCubeGeometry } from './geometry.ts';
7+
import {
8+
Camera,
9+
cameraLayout,
10+
Cube,
11+
cubeLayout,
12+
terrainLayout,
13+
TerrainParams,
14+
vertexLayout,
15+
} from './schemas.ts';
16+
import { fragmentFn, vertexFn } from './shaders.ts';
17+
18+
const CUBE_COUNTS = [
19+
1024,
20+
4096,
21+
8192,
22+
16384,
23+
32768,
24+
65536,
25+
131072,
26+
262144,
27+
524288,
28+
];
29+
const INITIAL_CUBE_COUNT = CUBE_COUNTS[0];
30+
const TERRAIN_SIZE = 50;
31+
const TERRAIN_HEIGHT = 6;
32+
const NOISE_SCALE = 0.3;
33+
34+
const root = await tgpu.init();
35+
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
36+
const context = root.configureContext({ canvas, alphaMode: 'premultiplied' });
37+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
38+
39+
const perlinCache = perlin2d.staticCache({ root, size: d.vec2u(16, 16) });
40+
41+
const cubeVerts = createCubeGeometry();
42+
const VERTS_PER_CUBE = cubeVerts.length;
43+
44+
const vertexBuffer = root
45+
.createBuffer(vertexLayout.schemaForCount(VERTS_PER_CUBE), cubeVerts)
46+
.$usage('vertex');
47+
48+
const cameraBuffer = root.createBuffer(Camera).$usage('uniform');
49+
50+
const terrainBuffer = root
51+
.createBuffer(TerrainParams, {
52+
terrainHeight: TERRAIN_HEIGHT,
53+
noiseScale: NOISE_SCALE,
54+
})
55+
.$usage('uniform');
56+
57+
const { cleanupCamera } = setupOrbitCamera(
58+
canvas,
59+
{
60+
initPos: d.vec4f(0, 10, 30, 1),
61+
target: d.vec4f(0, 0, 0, 1),
62+
minZoom: 5,
63+
maxZoom: 100,
64+
},
65+
(updates) => cameraBuffer.writePartial(updates),
66+
);
67+
68+
const cameraBindGroup = root.createBindGroup(cameraLayout, {
69+
camera: cameraBuffer,
70+
});
71+
72+
const terrainBindGroup = root.createBindGroup(terrainLayout, {
73+
terrain: terrainBuffer,
74+
});
75+
76+
const pipeline = root
77+
.pipe(perlinCache.inject())
78+
.createRenderPipeline({
79+
attribs: vertexLayout.attrib,
80+
vertex: vertexFn,
81+
fragment: fragmentFn,
82+
depthStencil: {
83+
format: 'depth24plus',
84+
depthWriteEnabled: true,
85+
depthCompare: 'less',
86+
},
87+
});
88+
89+
let depthTexture = root.device.createTexture({
90+
size: [canvas.width, canvas.height],
91+
format: 'depth24plus',
92+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
93+
});
94+
95+
let cubeCount = INITIAL_CUBE_COUNT;
96+
let cubeData: d.Infer<typeof Cube>[] = [];
97+
let cubeBuffer = root
98+
.createBuffer(d.arrayOf(Cube, INITIAL_CUBE_COUNT))
99+
.$usage('storage');
100+
let cubeBindGroup = root.createBindGroup(cubeLayout, { cubes: cubeBuffer });
101+
let renderBundle: GPURenderBundle;
102+
103+
let prepared = pipeline
104+
.with(cameraBindGroup)
105+
.with(cubeBindGroup)
106+
.with(terrainBindGroup)
107+
.with(vertexLayout, vertexBuffer);
108+
109+
function generateCubes(count: number) {
110+
cubeData = [];
111+
112+
const gridRes = Math.ceil(Math.sqrt(count));
113+
const cubeScale = TERRAIN_SIZE / gridRes;
114+
const half = TERRAIN_SIZE / 2;
115+
116+
for (let i = 0; i < count; i++) {
117+
const gx = i % gridRes;
118+
const gz = Math.floor(i / gridRes);
119+
120+
const x = gx * cubeScale - half + cubeScale * 0.5;
121+
const z = gz * cubeScale - half + cubeScale * 0.5;
122+
123+
const model = m.mat4.translation([x, 0, z], d.mat4x4f());
124+
m.mat4.scale(model, [cubeScale, cubeScale, cubeScale], model);
125+
126+
cubeData.push({ model });
127+
}
128+
}
129+
130+
function buildBundle(): GPURenderBundle {
131+
const bundleEncoder = root.device.createRenderBundleEncoder({
132+
colorFormats: [presentationFormat],
133+
depthStencilFormat: 'depth24plus',
134+
});
135+
136+
const withEncoder = prepared.with(bundleEncoder);
137+
for (let i = 0; i < cubeCount; i++) {
138+
withEncoder.draw(VERTS_PER_CUBE, 1, 0, i);
139+
}
140+
return bundleEncoder.finish();
141+
}
142+
143+
function setCubeCount(count: number) {
144+
cubeCount = count;
145+
generateCubes(count);
146+
147+
cubeBuffer.destroy();
148+
cubeBuffer = root
149+
.createBuffer(d.arrayOf(Cube, count), cubeData)
150+
.$usage('storage');
151+
cubeBindGroup = root.createBindGroup(cubeLayout, { cubes: cubeBuffer });
152+
153+
// Rebuild the prepared pipeline since cubeBindGroup changed.
154+
prepared = pipeline
155+
.with(cameraBindGroup)
156+
.with(cubeBindGroup)
157+
.with(terrainBindGroup)
158+
.with(vertexLayout, vertexBuffer);
159+
renderBundle = buildBundle();
160+
}
161+
162+
setCubeCount(INITIAL_CUBE_COUNT);
163+
164+
let useBundles = true;
165+
166+
let disposed = false;
167+
168+
function frame() {
169+
if (disposed) return;
170+
171+
if (
172+
depthTexture.width !== canvas.width ||
173+
depthTexture.height !== canvas.height
174+
) {
175+
depthTexture.destroy();
176+
depthTexture = root.device.createTexture({
177+
size: [canvas.width, canvas.height],
178+
format: 'depth24plus',
179+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
180+
});
181+
}
182+
183+
const encoder = root.device.createCommandEncoder();
184+
185+
const pass = encoder.beginRenderPass({
186+
colorAttachments: [
187+
{
188+
view: context.getCurrentTexture().createView(),
189+
clearValue: [1, 0.85, 0.74, 1] as const,
190+
loadOp: 'clear' as const,
191+
storeOp: 'store' as const,
192+
},
193+
],
194+
depthStencilAttachment: {
195+
view: depthTexture.createView(),
196+
depthClearValue: 1,
197+
depthLoadOp: 'clear' as const,
198+
depthStoreOp: 'store' as const,
199+
},
200+
});
201+
202+
if (useBundles) {
203+
pass.executeBundles([renderBundle]);
204+
} else {
205+
const withPass = prepared.with(pass);
206+
for (let i = 0; i < cubeCount; i++) {
207+
withPass.draw(VERTS_PER_CUBE, 1, 0, i);
208+
}
209+
}
210+
211+
pass.end();
212+
root.device.queue.submit([encoder.finish()]);
213+
214+
requestAnimationFrame(frame);
215+
}
216+
217+
requestAnimationFrame(frame);
218+
219+
// #region Example controls and cleanup
220+
221+
export const controls = defineControls({
222+
'cube count': {
223+
initial: INITIAL_CUBE_COUNT,
224+
options: CUBE_COUNTS,
225+
onSelectChange: (value: number) => {
226+
setCubeCount(value);
227+
},
228+
},
229+
'use render bundles': {
230+
initial: true,
231+
onToggleChange: (value: boolean) => {
232+
useBundles = value;
233+
},
234+
},
235+
});
236+
237+
export function onCleanup() {
238+
disposed = true;
239+
cleanupCamera();
240+
perlinCache.destroy();
241+
depthTexture.destroy();
242+
root.destroy();
243+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"title": "Render Bundles (.with API)",
3+
"category": "rendering",
4+
"tags": ["experimental", "3d", "rasterization", "performance"]
5+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import tgpu, { d } from 'typegpu';
2+
import { Camera } from '../../common/setup-orbit-camera.ts';
3+
4+
export { Camera };
5+
6+
export const Vertex = d.struct({
7+
position: d.vec3f,
8+
normal: d.vec3f,
9+
});
10+
11+
export const Cube = d.struct({
12+
model: d.mat4x4f,
13+
});
14+
15+
export const TerrainParams = d.struct({
16+
terrainHeight: d.f32,
17+
noiseScale: d.f32,
18+
});
19+
20+
export const vertexLayout = tgpu.vertexLayout(d.arrayOf(Vertex));
21+
22+
export const cameraLayout = tgpu.bindGroupLayout({
23+
camera: { uniform: Camera },
24+
});
25+
26+
export const cubeLayout = tgpu.bindGroupLayout({
27+
cubes: { storage: d.arrayOf(Cube), access: 'readonly' },
28+
});
29+
30+
export const terrainLayout = tgpu.bindGroupLayout({
31+
terrain: { uniform: TerrainParams },
32+
});

0 commit comments

Comments
 (0)