Skip to content

Commit a677387

Browse files
committed
more work
1 parent 77561ca commit a677387

20 files changed

Lines changed: 1590 additions & 146 deletions

File tree

apps/typegpu-docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@typegpu/noise": "workspace:*",
3131
"@typegpu/three": "workspace:*",
3232
"@typegpu/sdf": "workspace:*",
33+
"@typegpu/radiance-cascades": "workspace:*",
3334
"three": "catalog:example",
3435
"@types/react": "^19.1.8",
3536
"@types/react-dom": "^19.1.6",

apps/typegpu-docs/src/examples/rendering/radiance-compute/drag-controller.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import type { AnySceneElement } from './scene.ts';
33
import { sceneElements } from './scene.ts';
44
import * as d from 'typegpu/data';
55

6-
export interface DragTarget {
7-
id: string;
8-
element: AnySceneElement;
9-
}
6+
type DragTarget = AnySceneElement;
107

118
export class DragController {
129
private isDragging = false;
@@ -28,34 +25,23 @@ export class DragController {
2825
}
2926

3027
private hitTestDisk(uv: d.v2f, center: d.v2f, radius: number): boolean {
31-
const dist = sdDisk(uv.sub(center), radius);
32-
return dist <= radius;
28+
return sdDisk(uv.sub(center), radius) <= 0;
3329
}
3430

3531
private hitTestBox(uv: d.v2f, center: d.v2f, size: d.v2f): boolean {
36-
const dist = sdBox2d(uv.sub(center), size.mul(1));
37-
return dist <= 0;
32+
return sdBox2d(uv.sub(center), size) <= 0;
3833
}
3934

4035
private hitTest(clientX: number, clientY: number): DragTarget | null {
4136
const uv = this.canvasToUV(clientX, clientY);
42-
43-
for (const element of sceneElements) {
44-
let hit = false;
45-
46-
if (element.type === 'disk') {
47-
const radius = element.size as number;
48-
hit = this.hitTestDisk(uv, element.position, radius);
49-
} else if (element.type === 'box') {
50-
const size = element.size as d.v2f;
51-
hit = this.hitTestBox(uv, element.position, size);
52-
}
53-
37+
for (const el of sceneElements) {
38+
const hit = el.type === 'disk'
39+
? this.hitTestDisk(uv, el.position, el.size as number)
40+
: this.hitTestBox(uv, el.position, el.size as d.v2f);
5441
if (hit) {
55-
return { id: element.id, element };
42+
return el;
5643
}
5744
}
58-
5945
return null;
6046
}
6147

apps/typegpu-docs/src/examples/rendering/radiance-compute/index.ts

Lines changed: 34 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import * as sdf from '@typegpu/sdf';
2-
import tgpu, {
3-
type SampledFlag,
4-
type StorageFlag,
5-
type TgpuTexture,
6-
} from 'typegpu';
2+
import tgpu from 'typegpu';
73
import { fullScreenTriangle } from 'typegpu/common';
84
import * as d from 'typegpu/data';
95
import * as std from 'typegpu/std';
@@ -50,24 +46,13 @@ const cascadeAmount = Math.ceil(
5046
Math.log2((maxIntervalStart * 3) / interval0 + 1) / 2,
5147
);
5248

53-
type CascadeTexture =
54-
& TgpuTexture<{
55-
size: [number, number, number];
56-
format: 'rgba16float';
57-
}>
58-
& StorageFlag
59-
& SampledFlag;
60-
61-
const cascadeTextures = Array.from(
62-
{ length: 2 },
63-
() =>
64-
root['~unstable']
65-
.createTexture({
66-
size: [cascadeDimX, cascadeDimY, cascadeAmount],
67-
format: 'rgba16float',
68-
})
69-
.$usage('storage', 'sampled'),
70-
) as [CascadeTexture, CascadeTexture];
49+
const cascadeTextures = Array.from({ length: 2 }, () =>
50+
root['~unstable']
51+
.createTexture({
52+
size: [cascadeDimX, cascadeDimY, cascadeAmount],
53+
format: 'rgba16float',
54+
})
55+
.$usage('storage', 'sampled'));
7156

7257
const radianceFieldTex = root['~unstable']
7358
.createTexture({
@@ -114,11 +99,6 @@ const cascadeProbesUniform = root.createUniform(
11499
const overlayEnabledUniform = root.createUniform(d.u32, 0);
115100
const overlayDebugCascadeUniform = root.createUniform(d.u32, 0);
116101

117-
const atlasPos = (dir: d.v2u, probe: d.v2u, probes: d.v2u) => {
118-
'use gpu';
119-
return dir.mul(probes).add(probe);
120-
};
121-
122102
const cascadePassBGL = tgpu.bindGroupLayout({
123103
upper: { texture: d.texture2d(d.f32) },
124104
upperSampler: { sampler: 'filtering' },
@@ -145,25 +125,18 @@ const cascadePassCompute = tgpu['~unstable'].computeFn({
145125
const probes = probesUniform.$;
146126
const cascadeProbes = cascadeProbesUniform.$;
147127

148-
// Decode atlas position to (direction, probe) using direction-first layout
149128
const dirStored = gid.xy.div(probes);
150129
const probe = std.mod(gid.xy, probes);
151-
152-
// Each stored texel = 2x2 actual rays; raysDimStored doubles per layer
153130
const raysDimStored = d.u32(2) << layer;
154131
const raysDimActual = raysDimStored * d.u32(2);
155132
const rayCountActual = raysDimActual * raysDimActual;
156133

157-
// Skip texels outside valid atlas region
158134
if (dirStored.x >= raysDimStored || dirStored.y >= raysDimStored) {
159135
std.textureStore(cascadePassBGL.$.dst, gid.xy, d.vec4f(0, 0, 0, 1));
160136
return;
161137
}
162138

163139
const probePos = d.vec2f(probe).add(0.5).div(d.vec2f(probes));
164-
165-
// Interval: each layer covers 4× the distance of the previous
166-
// Use min probe dimension for uniform spacing in both directions
167140
const cascadeProbesMinVal = d.f32(std.min(cascadeProbes.x, cascadeProbes.y));
168141
const interval0 = 1.0 / cascadeProbesMinVal;
169142
const pow4 = d.f32(d.u32(1) << (layer * d.u32(2)));
@@ -174,7 +147,6 @@ const cascadePassCompute = tgpu['~unstable'].computeFn({
174147

175148
let accum = d.vec4f();
176149

177-
// Cast 4 rays per stored texel (2x2 block) and average
178150
for (let i = 0; i < 4; i++) {
179151
const dirActual = dirStored
180152
.mul(d.u32(2))
@@ -187,8 +159,10 @@ const cascadePassCompute = tgpu['~unstable'].computeFn({
187159
let T = d.f32(1);
188160
let t = startUv;
189161

190-
for (let step = 0; step < 32; step++) {
191-
if (t > endUv) break;
162+
for (let step = 0; step < 64; step++) {
163+
if (t > endUv) {
164+
break;
165+
}
192166
const hit = sceneSDF(probePos.add(rayDir.mul(t)));
193167
if (hit.dist <= eps) {
194168
rgb = d.vec3f(hit.color);
@@ -198,7 +172,6 @@ const cascadePassCompute = tgpu['~unstable'].computeFn({
198172
t += std.max(hit.dist, minStep);
199173
}
200174

201-
// Merge with upper cascade if ray didn't hit anything
202175
if (layer < d.u32(cascadeAmount - 1) && T > 0.01) {
203176
const probesU = d.vec2u(
204177
std.max(probes.x >> d.u32(1), d.u32(1)),
@@ -219,7 +192,7 @@ const cascadePassCompute = tgpu['~unstable'].computeFn({
219192
0,
220193
);
221194
rgb = rgb.add(upper.xyz.mul(T));
222-
T = T * upper.w;
195+
T *= upper.w;
223196
}
224197

225198
accum = accum.add(d.vec4f(rgb, T));
@@ -233,7 +206,9 @@ const buildRadianceFieldCompute = tgpu['~unstable'].computeFn({
233206
in: { gid: d.builtin.globalInvocationId },
234207
})(({ gid }) => {
235208
const outputProbes = outputProbesUniform.$;
236-
if (gid.x >= outputProbes.x || gid.y >= outputProbes.y) return;
209+
if (gid.x >= outputProbes.x || gid.y >= outputProbes.y) {
210+
return;
211+
}
237212

238213
const cascadeProbes = cascadeProbesUniform.$;
239214
const cascadeDim = cascadeDimUniform.$;
@@ -250,43 +225,18 @@ const buildRadianceFieldCompute = tgpu['~unstable'].computeFn({
250225
const uvStride = d.vec2f(cascadeProbes).mul(invCascadeDim);
251226
const baseSampleUV = probePixel.mul(invCascadeDim);
252227

253-
// Sample Tile (0, 0)
254-
let sum = std.textureSampleLevel(
255-
buildRadianceFieldBGL.$.src,
256-
buildRadianceFieldBGL.$.srcSampler,
257-
baseSampleUV,
258-
0,
259-
).xyz;
260-
261-
// Sample Tile (1, 0)
262-
sum = sum.add(
263-
std.textureSampleLevel(
264-
buildRadianceFieldBGL.$.src,
265-
buildRadianceFieldBGL.$.srcSampler,
266-
baseSampleUV.add(d.vec2f(uvStride.x, 0.0)),
267-
0,
268-
).xyz,
269-
);
270-
271-
// Sample Tile (0, 1)
272-
sum = sum.add(
273-
std.textureSampleLevel(
274-
buildRadianceFieldBGL.$.src,
275-
buildRadianceFieldBGL.$.srcSampler,
276-
baseSampleUV.add(d.vec2f(0.0, uvStride.y)),
277-
0,
278-
).xyz,
279-
);
280-
281-
// Sample Tile (1, 1)
282-
sum = sum.add(
283-
std.textureSampleLevel(
284-
buildRadianceFieldBGL.$.src,
285-
buildRadianceFieldBGL.$.srcSampler,
286-
baseSampleUV.add(uvStride),
287-
0,
288-
).xyz,
289-
);
228+
let sum = d.vec3f();
229+
for (let i = d.u32(0); i < 4; i++) {
230+
const offset = d.vec2f(d.f32(i & 1), d.f32(i >> 1)).mul(uvStride);
231+
sum = sum.add(
232+
std.textureSampleLevel(
233+
buildRadianceFieldBGL.$.src,
234+
buildRadianceFieldBGL.$.srcSampler,
235+
baseSampleUV.add(offset),
236+
0,
237+
).xyz,
238+
);
239+
}
290240

291241
std.textureStore(
292242
buildRadianceFieldBGL.$.dst,
@@ -308,19 +258,6 @@ const ACESFilm = tgpu.fn(
308258
return std.saturate(res);
309259
});
310260

311-
const finalRadianceFieldFrag = tgpu['~unstable'].fragmentFn({
312-
in: { uv: d.vec2f },
313-
out: d.vec4f,
314-
})(({ uv }) => {
315-
const field = std.textureSample(
316-
radianceFieldView.$,
317-
radianceSampler.$,
318-
uv,
319-
).xyz;
320-
const outRgb = std.saturate(field);
321-
return d.vec4f(ACESFilm(outRgb), 1.0);
322-
});
323-
324261
const overlayDebugBGL = tgpu.bindGroupLayout({
325262
cascadeTex: { texture: d.texture2dArray(d.f32) },
326263
cascadeSampler: { sampler: 'filtering' },
@@ -344,19 +281,13 @@ const overlayFrag = tgpu['~unstable'].fragmentFn({
344281
std.max(cascadeProbes.x >> debugLayer, d.u32(1)),
345282
std.max(cascadeProbes.y >> debugLayer, d.u32(1)),
346283
);
347-
348-
// Ray dimensions: 2x2 stored at layer 0, doubling each layer
349284
const raysDimStored = d.u32(2) << debugLayer;
350285
const raysDimActual = raysDimStored * d.u32(2);
351286
const rayCountActual = raysDimActual * raysDimActual;
352-
353-
// Interval for ray visualization
354287
const cascadeProbesMinVal = d.f32(std.min(cascadeProbes.x, cascadeProbes.y));
355288
const interval0 = 1 / cascadeProbesMinVal;
356289
const pow4 = d.f32(d.u32(1) << (debugLayer * d.u32(2)));
357290
const endUv = (interval0 * (pow4 - 1)) / 3 + interval0 * pow4;
358-
359-
// Visual parameters
360291
const probeSpacing = std.min(1 / d.f32(probes.x), 1 / d.f32(probes.y));
361292
const probeRadius = std.max(probeSpacing * 0.08, 0.002);
362293
const rayThickness = std.max(probeSpacing * 0.03, 0.001);
@@ -367,7 +298,6 @@ const overlayFrag = tgpu['~unstable'].fragmentFn({
367298

368299
const centerProbe = d.vec2i(std.floor(uv.mul(d.vec2f(probes))));
369300

370-
// Check nearby probes (3x3 grid)
371301
for (let py = -1; py <= 1; py++) {
372302
for (let px = -1; px <= 1; px++) {
373303
const probeXY = centerProbe.add(d.vec2i(px, py));
@@ -411,7 +341,7 @@ const overlayFrag = tgpu['~unstable'].fragmentFn({
411341
);
412342
const sample = std.textureLoad(
413343
overlayDebugBGL.$.cascadeTex,
414-
d.vec2i(atlasPos(dirStored, probe, probes)),
344+
d.vec2i(dirStored.mul(probes).add(probe)),
415345
debugLayer,
416346
0,
417347
);
@@ -423,7 +353,6 @@ const overlayFrag = tgpu['~unstable'].fragmentFn({
423353
}
424354
}
425355

426-
// Visualize rays
427356
let overlayColor = d.vec3f();
428357
let overlayAlpha = d.f32(0);
429358
if (minRayDist < rayThickness) {
@@ -432,7 +361,6 @@ const overlayFrag = tgpu['~unstable'].fragmentFn({
432361
std.smoothstep(rayThickness * 1.5, rayThickness * 0.3, minRayDist) * 0.8;
433362
}
434363

435-
// Probe outline
436364
if (std.abs(minProbeDist) < probeRadius * 0.2) {
437365
const edgeAlpha = std.smoothstep(
438366
probeRadius * 0.3,
@@ -564,23 +492,13 @@ async function frame() {
564492
}
565493
frameId = requestAnimationFrame(frame);
566494

567-
function updateUniforms() {
495+
const onDrag = (id: string, position: d.v2f) => {
496+
updateElementPosition(id, position);
568497
sceneDataUniform.write(sceneData);
569-
}
498+
updateLighting();
499+
};
570500

571-
const dragController = new DragController(
572-
canvas,
573-
(id, position) => {
574-
updateElementPosition(id, position);
575-
updateUniforms();
576-
updateLighting();
577-
},
578-
(id, position) => {
579-
updateElementPosition(id, position);
580-
updateUniforms();
581-
updateLighting();
582-
},
583-
);
501+
const dragController = new DragController(canvas, onDrag, onDrag);
584502

585503
export function onCleanup() {
586504
dragController.destroy();

apps/typegpu-docs/src/examples/rendering/radiance-compute/scene.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ export interface SceneElement<T extends 'box' | 'disk'> {
88
position: d.v2f;
99
size: T extends 'box' ? d.v2f : number;
1010
emission?: d.v3f;
11-
dataIndex: number; // Index in sceneData.disks or sceneData.boxes
11+
dataIndex: number;
1212
}
1313
export type AnySceneElement = SceneElement<'box'> | SceneElement<'disk'>;
1414

1515
export const sceneElements: AnySceneElement[] = [
16-
// 3 RGB lights (disks with emission)
1716
{
1817
id: 'light-red',
1918
type: 'disk',
@@ -38,8 +37,6 @@ export const sceneElements: AnySceneElement[] = [
3837
size: 0.05,
3938
dataIndex: 2,
4039
},
41-
42-
// 3 occluders
4340
{
4441
id: 'box-1',
4542
type: 'box',
@@ -91,7 +88,6 @@ export function updateElementPosition(id: string, position: d.v2f): void {
9188
return;
9289
}
9390

94-
// Direct O(1) array access using stored index
9591
element.position = position;
9692
if (element.type === 'disk') {
9793
sceneData.disks[element.dataIndex].pos = position;
@@ -118,8 +114,8 @@ const BoxData = d.struct({
118114
});
119115

120116
export const SceneData = d.struct({
121-
disks: d.arrayOf(DiskData, 4),
122-
boxes: d.arrayOf(BoxData, 2),
117+
disks: d.arrayOf(DiskData, sceneData.disks.length),
118+
boxes: d.arrayOf(BoxData, sceneData.boxes.length),
123119
});
124120

125121
export const sceneDataAccess = tgpu['~unstable'].accessor(SceneData);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<canvas data-fit-to-container></canvas>

0 commit comments

Comments
 (0)