Skip to content

Commit fe4b757

Browse files
committed
gpu time
1 parent aa56bf6 commit fe4b757

File tree

6 files changed

+490
-44
lines changed

6 files changed

+490
-44
lines changed

apps/typegpu-docs/src/content/examples/rendering/game-of-disco/dataTypes.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ export const Camera = d.struct({
88

99
export const Vertex = d.unstruct({
1010
position: d.float16x4,
11-
color: d.unorm8x4,
1211
normal: d.float16x4,
1312
});
1413

14+
export const ComputeVertex = d.struct({
15+
position: d.vec2u, // 2x16 float
16+
normal: d.vec2u, // 2x16 float
17+
});
18+
1519
export const CubeVertex = d.struct({
1620
position: d.vec4f,
1721
uv: d.vec2f,

apps/typegpu-docs/src/content/examples/rendering/game-of-disco/icosphere.ts

Lines changed: 323 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import tgpu, { type TgpuBuffer, type TgpuRoot, type VertexFlag } from 'typegpu';
12
import * as d from 'typegpu/data';
2-
import { Vertex } from './dataTypes';
3+
import * as std from 'typegpu/std';
4+
import { ComputeVertex, Vertex } from './dataTypes';
35

46
/**
57
* Creates an icosphere with the specified level of subdivision
@@ -144,11 +146,8 @@ function createVertex(
144146
position: d.v4f,
145147
normal: d.v4f,
146148
): ReturnType<typeof Vertex> {
147-
const color = d.vec4f(1, 1, 1, 1);
148-
149149
return Vertex({
150150
position,
151-
color,
152151
normal,
153152
});
154153
}
@@ -163,3 +162,323 @@ function normalizeSafely(v: d.v4f): d.v4f {
163162
}
164163
return d.vec4f(v.x / length, v.y / length, v.z / length, 1);
165164
}
165+
166+
//////// GPU TERRITORY ////////
167+
168+
function getVertexAmount(subdivisions: number): number {
169+
return 60 * 4 ** subdivisions;
170+
}
171+
172+
function createBaseIcosphere(smooth: boolean): d.Infer<typeof Vertex>[] {
173+
const goldenRatio = (1 + Math.sqrt(5)) / 2;
174+
175+
const initialVertices: d.v4f[] = [
176+
// Top group
177+
d.vec4f(-1, goldenRatio, 0, 1),
178+
d.vec4f(1, goldenRatio, 0, 1),
179+
d.vec4f(-1, -goldenRatio, 0, 1),
180+
d.vec4f(1, -goldenRatio, 0, 1),
181+
182+
// Middle group
183+
d.vec4f(0, -1, goldenRatio, 1),
184+
d.vec4f(0, 1, goldenRatio, 1),
185+
d.vec4f(0, -1, -goldenRatio, 1),
186+
d.vec4f(0, 1, -goldenRatio, 1),
187+
188+
// Bottom group
189+
d.vec4f(goldenRatio, 0, -1, 1),
190+
d.vec4f(goldenRatio, 0, 1, 1),
191+
d.vec4f(-goldenRatio, 0, -1, 1),
192+
d.vec4f(-goldenRatio, 0, 1, 1),
193+
].map((v) => {
194+
const length = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
195+
return d.vec4f(v.x / length, v.y / length, v.z / length, 1);
196+
});
197+
198+
// Define the 20 triangular faces of the icosahedron using vertex indices
199+
const faces: [number, number, number][] = [
200+
// 5 faces around vertex 0
201+
[0, 11, 5],
202+
[0, 5, 1],
203+
[0, 1, 7],
204+
[0, 7, 10],
205+
[0, 10, 11],
206+
// 5 adjacent faces
207+
[1, 5, 9],
208+
[5, 11, 4],
209+
[11, 10, 2],
210+
[10, 7, 6],
211+
[7, 1, 8],
212+
// 5 faces around vertex 3
213+
[3, 9, 4],
214+
[3, 4, 2],
215+
[3, 2, 6],
216+
[3, 6, 8],
217+
[3, 8, 9],
218+
// 5 adjacent faces
219+
[4, 9, 5],
220+
[2, 4, 11],
221+
[6, 2, 10],
222+
[8, 6, 7],
223+
[9, 8, 1],
224+
];
225+
226+
const vertices: d.Infer<typeof Vertex>[] = [];
227+
228+
for (const [i1, i2, i3] of faces) {
229+
const v1 = initialVertices[i1];
230+
const v2 = initialVertices[i2];
231+
const v3 = initialVertices[i3];
232+
233+
if (smooth) {
234+
vertices.push(createVertex(v1, v1));
235+
vertices.push(createVertex(v2, v2));
236+
vertices.push(createVertex(v3, v3));
237+
} else {
238+
const edge1 = d.vec4f(v2.x - v1.x, v2.y - v1.y, v2.z - v1.z, 0);
239+
const edge2 = d.vec4f(v3.x - v1.x, v3.y - v1.y, v3.z - v1.z, 0);
240+
const faceNormal = normalizeSafely(
241+
d.vec4f(
242+
edge1.y * edge2.z - edge1.z * edge2.y,
243+
edge1.z * edge2.x - edge1.x * edge2.z,
244+
edge1.x * edge2.y - edge1.y * edge2.x,
245+
0,
246+
),
247+
);
248+
vertices.push(createVertex(v1, faceNormal));
249+
vertices.push(createVertex(v2, faceNormal));
250+
vertices.push(createVertex(v3, faceNormal));
251+
}
252+
}
253+
254+
return vertices;
255+
}
256+
257+
const icoshpereCache = new Map<
258+
string,
259+
TgpuBuffer<d.Disarray<typeof Vertex>> & VertexFlag
260+
>();
261+
262+
export function createIcosphereShader(
263+
subdivisions: number,
264+
smooth: boolean,
265+
root: TgpuRoot,
266+
): TgpuBuffer<d.Disarray<typeof Vertex>> & VertexFlag {
267+
const key = `${subdivisions}-${smooth}`;
268+
const cached = icoshpereCache.get(key);
269+
if (cached) {
270+
return cached;
271+
}
272+
273+
const buffer = subdivide(subdivisions, smooth, root);
274+
icoshpereCache.set(key, buffer);
275+
return buffer;
276+
}
277+
278+
function subdivide(
279+
wantedSubdivisions: number,
280+
smooth: boolean,
281+
root: TgpuRoot,
282+
): TgpuBuffer<d.Disarray<typeof Vertex>> & VertexFlag {
283+
if (wantedSubdivisions === 0) {
284+
const key = `${wantedSubdivisions}-${smooth}`;
285+
const cached = icoshpereCache.get(key);
286+
if (cached) {
287+
return cached;
288+
}
289+
290+
const initialVertices = root
291+
.createBuffer(
292+
d.disarrayOf(Vertex, getVertexAmount(0)),
293+
createBaseIcosphere(smooth),
294+
)
295+
.$usage('vertex')
296+
.$addFlags(GPUBufferUsage.STORAGE);
297+
icoshpereCache.set(key, initialVertices);
298+
return initialVertices;
299+
}
300+
301+
const previousKey = `${wantedSubdivisions - 1}-${smooth}`;
302+
let previousVertices = icoshpereCache.get(previousKey);
303+
if (!previousVertices) {
304+
previousVertices = subdivide(wantedSubdivisions - 1, smooth, root);
305+
}
306+
307+
const nextBuffer = root
308+
.createBuffer(d.disarrayOf(Vertex, getVertexAmount(wantedSubdivisions)))
309+
.$usage('vertex')
310+
.$addFlags(GPUBufferUsage.STORAGE);
311+
312+
const currentComputeView = root
313+
.createBuffer(
314+
d.arrayOf(ComputeVertex, getVertexAmount(wantedSubdivisions - 1)),
315+
previousVertices.buffer,
316+
)
317+
.$usage('storage');
318+
const nextComputeView = root
319+
.createBuffer(
320+
d.arrayOf(ComputeVertex, getVertexAmount(wantedSubdivisions)),
321+
nextBuffer.buffer,
322+
)
323+
.$usage('storage');
324+
325+
const smoothBuffer = root
326+
.createBuffer(d.u32, smooth ? 1 : 0)
327+
.$usage('uniform');
328+
329+
const bindGroupLayout = tgpu.bindGroupLayout({
330+
prevVertices: {
331+
storage: (n: number) => d.arrayOf(ComputeVertex, n),
332+
access: 'readonly',
333+
},
334+
nextVertices: {
335+
storage: (n: number) => d.arrayOf(ComputeVertex, n),
336+
access: 'mutable',
337+
},
338+
smoothFlag: { uniform: d.u32 },
339+
});
340+
341+
const bindGroup = root.createBindGroup(bindGroupLayout, {
342+
prevVertices: currentComputeView,
343+
nextVertices: nextComputeView,
344+
smoothFlag: smoothBuffer,
345+
});
346+
347+
const { prevVertices, nextVertices, smoothFlag } = bindGroupLayout.bound;
348+
349+
const unpackVec2u = tgpu['~unstable'].fn([d.vec2u], d.vec4f).does((input) => {
350+
const xy = std.unpack2x16float(input.x);
351+
const zw = std.unpack2x16float(input.y);
352+
return d.vec4f(xy.x, xy.y, zw.x, zw.y);
353+
});
354+
355+
const packVec2u = tgpu['~unstable'].fn([d.vec4f], d.vec2u).does((input) => {
356+
const xy = std.pack2x16float(d.vec2f(input.x, input.y));
357+
const zw = std.pack2x16float(d.vec2f(input.z, input.w));
358+
return d.vec2u(xy, zw);
359+
});
360+
361+
const getNormal = tgpu['~unstable']
362+
.fn([d.vec4f, d.vec4f, d.vec4f, d.u32, d.vec4f], d.vec4f)
363+
.does((v1, v2, v3, smooth, vertexPos) => {
364+
if (smooth === 1) {
365+
// For smooth shading on a sphere, the normal is the same as the normalized position
366+
return vertexPos;
367+
}
368+
const edge1 = d.vec4f(v2.x - v1.x, v2.y - v1.y, v2.z - v1.z, 0);
369+
const edge2 = d.vec4f(v3.x - v1.x, v3.y - v1.y, v3.z - v1.z, 0);
370+
return std.normalize(
371+
d.vec4f(
372+
edge1.y * edge2.z - edge1.z * edge2.y,
373+
edge1.z * edge2.x - edge1.x * edge2.z,
374+
edge1.x * edge2.y - edge1.y * edge2.x,
375+
0,
376+
),
377+
);
378+
});
379+
380+
const calculateMidpoint = tgpu['~unstable']
381+
.fn([d.vec4f, d.vec4f], d.vec4f)
382+
.does((v1, v2) => {
383+
return d.vec4f(
384+
(v1.x + v2.x) * 0.5,
385+
(v1.y + v2.y) * 0.5,
386+
(v1.z + v2.z) * 0.5,
387+
1,
388+
);
389+
});
390+
391+
const normalizeSafely = tgpu['~unstable'].fn([d.vec4f], d.vec4f).does((v) => {
392+
const length = std.length(d.vec3f(v.x, v.y, v.z));
393+
const epsilon = 1e-8;
394+
if (length < epsilon) {
395+
return d.vec4f(0, 0, 1, 1);
396+
}
397+
return d.vec4f(v.x / length, v.y / length, v.z / length, 1);
398+
});
399+
400+
const computeFn = tgpu['~unstable']
401+
.computeFn({
402+
in: { gid: d.builtin.globalInvocationId },
403+
workgroupSize: [64, 1, 1],
404+
})
405+
.does((input) => {
406+
const triangleCount = std.arrayLength(prevVertices.value) / d.u32(3);
407+
// Calculate global triangleIndex from 2D dispatch
408+
const triangleIndex = input.gid.x + input.gid.y * d.u32(65535);
409+
if (triangleIndex >= triangleCount) {
410+
return;
411+
}
412+
413+
const baseIndexPrev = triangleIndex * d.u32(3);
414+
415+
// Read the 3 vertices of the triangle
416+
const v1 = unpackVec2u(prevVertices.value[baseIndexPrev].position);
417+
const v2 = unpackVec2u(
418+
prevVertices.value[baseIndexPrev + d.u32(1)].position,
419+
);
420+
const v3 = unpackVec2u(
421+
prevVertices.value[baseIndexPrev + d.u32(2)].position,
422+
);
423+
424+
// Calculate the midpoints of the edges and reproject them onto the unit sphere
425+
const v12 = normalizeSafely(calculateMidpoint(v1, v2));
426+
const v23 = normalizeSafely(calculateMidpoint(v2, v3));
427+
const v31 = normalizeSafely(calculateMidpoint(v3, v1));
428+
429+
const newVertices = [
430+
// Triangle A: [v1, v12, v31]
431+
v1,
432+
v12,
433+
v31,
434+
// Triangle B: [v2, v23, v12]
435+
v2,
436+
v23,
437+
v12,
438+
// Triangle C: [v3, v31, v23]
439+
v3,
440+
v31,
441+
v23,
442+
// Triangle D: [v12, v23, v31]
443+
v12,
444+
v23,
445+
v31,
446+
];
447+
448+
const baseIndexNext = triangleIndex * d.u32(12);
449+
// For each of the 12 new vertices, compute and store their values.
450+
for (let i = d.u32(0); i < 12; i++) {
451+
const reprojectedVertex = newVertices[i];
452+
453+
const triBase = i - (i % d.u32(3));
454+
const normal = getNormal(
455+
newVertices[triBase],
456+
newVertices[triBase + d.u32(1)],
457+
newVertices[triBase + d.u32(2)],
458+
smoothFlag.value,
459+
reprojectedVertex,
460+
);
461+
const outIndex = baseIndexNext + i;
462+
const nextVertex = nextVertices.value[outIndex];
463+
nextVertex.position = packVec2u(reprojectedVertex);
464+
nextVertex.normal = packVec2u(normal);
465+
nextVertices.value[outIndex] = nextVertex;
466+
}
467+
});
468+
469+
const pipeline = root['~unstable'].withCompute(computeFn).createPipeline();
470+
471+
// Calculate the appropriate workgroup dispatch dimensions, splitting across X and Y
472+
// when needed to stay within the 65535 limit
473+
const triangleCount = getVertexAmount(wantedSubdivisions - 1) / 3;
474+
const xGroups = Math.min(triangleCount, 65535);
475+
const yGroups = Math.ceil(triangleCount / 65535);
476+
477+
pipeline
478+
.with(bindGroupLayout, bindGroup)
479+
.dispatchWorkgroups(xGroups, yGroups, 1);
480+
481+
root['~unstable'].flush();
482+
483+
return nextBuffer;
484+
}

0 commit comments

Comments
 (0)