Skip to content

Commit c21ee71

Browse files
committed
Merge branch 'main' into release
2 parents 060b99b + 4393222 commit c21ee71

124 files changed

Lines changed: 5259 additions & 1264 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/typegpu-docs/astro.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ export default defineConfig({
208208
label: '@typegpu/sdf',
209209
slug: 'ecosystem/typegpu-sdf',
210210
},
211+
{
212+
label: '@typegpu/radiance-cascades',
213+
slug: 'ecosystem/typegpu-radiance-cascades',
214+
},
211215
DEV && {
212216
label: '@typegpu/color',
213217
slug: 'ecosystem/typegpu-color',

apps/typegpu-docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@typegpu/color": "workspace:*",
3232
"@typegpu/geometry": "workspace:*",
3333
"@typegpu/noise": "workspace:*",
34+
"@typegpu/radiance-cascades": "workspace:*",
3435
"@typegpu/sdf": "workspace:*",
3536
"@typegpu/sort": "workspace:*",
3637
"@typegpu/three": "workspace:*",
413 KB
Loading
351 KB
Loading
377 KB
Loading
198 KB
Loading
82.4 KB
Loading
184 KB
Loading
331 KB
Loading
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
---
2+
title: "@typegpu/radiance-cascades"
3+
---
4+
5+
import { Image } from 'astro:assets';
6+
import basicPreview from '../../../assets/docs/typegpu-radiance-cascades-basic.png';
7+
import generatedSdfPreview from '../../../assets/docs/typegpu-radiance-cascades-generated-sdf.png';
8+
import customRayMarchingPreview from '../../../assets/docs/typegpu-radiance-cascades-custom-ray-marching.png';
9+
10+
The `@typegpu/radiance-cascades` package provides a small TypeGPU runner for computing 2D radiance cascades.
11+
It is designed for screen-space lighting where your scene can be described by:
12+
13+
- an SDF callback sampled in normalized UV space,
14+
- a color callback sampled at hit points,
15+
- an output texture that stores the resolved radiance field.
16+
17+
The runner owns the cascade textures and dispatches the compute passes in the right order. You provide the scene functions and call `run()` when the scene changes.
18+
19+
:::note
20+
The package currently focuses on 2D radiance fields. It expects distances in UV-space units, where the visible domain usually spans `[0, 1]` on both axes.
21+
:::
22+
23+
## Basic usage
24+
25+
<div class="my-4 flex justify-center">
26+
<Image src={basicPreview} alt="A simple radiance cascades scene lit from SDF surfaces" class="w-full max-w-[20rem] rounded-md" />
27+
</div>
28+
29+
```ts twoslash
30+
import * as rc from '@typegpu/radiance-cascades';
31+
import * as sdf from '@typegpu/sdf';
32+
import tgpu, { d, std } from 'typegpu';
33+
34+
const root = await tgpu.init();
35+
const previewSize = { width: 512, height: 512 };
36+
37+
const sceneSdf = tgpu.fn([d.vec2f], d.f32)((uv) => {
38+
'use gpu';
39+
const circle = sdf.sdDisk(uv - d.vec2f(0.5), 0.18);
40+
const wall = sdf.sdRoundedBox2d(uv - d.vec2f(0.5, 0.82), d.vec2f(0.42, 0.03), 0.01);
41+
return sdf.opUnion(circle, wall);
42+
});
43+
44+
const surfaceColor = tgpu.fn([d.vec2f], d.vec3f)((uv) => {
45+
'use gpu';
46+
return std.mix(d.vec3f(1, 0.82, 0.5), d.vec3f(0.28, 0.52, 1), uv.x);
47+
});
48+
49+
const runner = rc.createRadianceCascades({
50+
root,
51+
size: previewSize,
52+
sdfResolution: { width: 1024, height: 1024 },
53+
sdf: sceneSdf,
54+
color: surfaceColor,
55+
});
56+
57+
runner.run();
58+
59+
const radianceView = runner.output.createView(d.texture2d());
60+
const sampler = root.createSampler({ magFilter: 'linear', minFilter: 'linear' });
61+
62+
const renderPreview = tgpu.fn([d.vec2f], d.vec4f)((uv) => {
63+
'use gpu';
64+
const dist = sceneSdf(uv);
65+
const edge = std.max(std.fwidth(dist), 0.001);
66+
const surface = 1 - std.smoothstep(-edge, edge, dist);
67+
const radiance = std.textureSampleLevel(radianceView.$, sampler.$, uv, 0).xyz;
68+
69+
const bg = std.mix(d.vec3f(0.04, 0.05, 0.07), d.vec3f(0.11, 0.15, 0.22), uv.y);
70+
const lit = bg + radiance * 1.55;
71+
const color = std.mix(lit, surfaceColor(uv), surface);
72+
73+
return d.vec4f(std.min(color, d.vec3f(1)), 1);
74+
});
75+
```
76+
77+
`size` controls the output texture resolution when the runner creates the texture for you. `sdfResolution` should match the resolution of the SDF source you are sampling from, or the resolution you use to think about texel-sized marching thresholds.
78+
79+
## Using a generated SDF texture
80+
81+
A common setup is to generate an SDF texture with [`@typegpu/sdf`](/TypeGPU/ecosystem/typegpu-sdf/) and feed it into the radiance runner.
82+
83+
<div class="my-4 flex justify-center">
84+
<Image src={generatedSdfPreview} alt="A radiance cascades scene using a generated SDF texture" class="w-full max-w-[24rem] rounded-md" />
85+
</div>
86+
87+
```ts
88+
import * as rc from '@typegpu/radiance-cascades';
89+
import * as sdf from '@typegpu/sdf';
90+
import tgpu, { d, std } from 'typegpu';
91+
92+
const root = await tgpu.init();
93+
const floodSize = { width: 2048, height: 2048 };
94+
const previewSize = { width: 512, height: 512 };
95+
96+
const sourceTexture = root
97+
.createTexture({
98+
size: [floodSize.width, floodSize.height],
99+
format: 'rgba8unorm',
100+
})
101+
.$usage('storage', 'sampled');
102+
103+
// Draw or bake the source mask into sourceTexture first.
104+
const sourceLayout = tgpu.bindGroupLayout({
105+
source: { texture: d.texture2d() },
106+
});
107+
const sourceBindGroup = root.createBindGroup(sourceLayout, {
108+
source: sourceTexture.createView(),
109+
});
110+
111+
const floodRunner = sdf
112+
.createJumpFlood({
113+
root,
114+
size: floodSize,
115+
classify: (coord) => {
116+
'use gpu';
117+
return std.textureLoad(sourceLayout.$.source, coord, 0).w > 0.5;
118+
},
119+
getSdf: (_coord, size, signedDist) => {
120+
'use gpu';
121+
return signedDist / d.f32(std.min(size.x, size.y));
122+
},
123+
getColor: (_coord, _size, _signedDist, insidePx) => {
124+
'use gpu';
125+
const source = std.textureLoad(sourceLayout.$.source, insidePx, 0);
126+
return d.vec4f(source.xyz, 1);
127+
},
128+
})
129+
.with(sourceBindGroup);
130+
131+
floodRunner.run();
132+
133+
const floodSdfView = floodRunner.sdfOutput.createView();
134+
const floodColorView = floodRunner.colorOutput.createView();
135+
const sampler = root.createSampler({ magFilter: 'linear', minFilter: 'linear' });
136+
137+
const runner = rc.createRadianceCascades({
138+
root,
139+
size: previewSize,
140+
sdfResolution: floodSize,
141+
sdf: (uv) => {
142+
'use gpu';
143+
if (uv.x < 0 || uv.x > 1 || uv.y < 0 || uv.y > 1) {
144+
return 1;
145+
}
146+
return std.textureSampleLevel(floodSdfView.$, sampler.$, uv, 0).x;
147+
},
148+
color: (uv) => {
149+
'use gpu';
150+
return std.textureSampleLevel(floodColorView.$, sampler.$, uv, 0).xyz;
151+
},
152+
});
153+
154+
runner.run();
155+
```
156+
157+
This is the same shape used by the [Radiance Cascades (with drawing)](/TypeGPU/examples#example=rendering--radiance-cascades-drawing) example: draw into a texture, rebuild an SDF with jump flooding, then update the radiance field from that SDF.
158+
159+
## Output textures
160+
161+
If you pass no `output`, the runner creates an `rgba16float` texture with `storage` and `sampled` usage and exposes it as `runner.output`.
162+
163+
You can also pass your own output texture or storage texture view:
164+
165+
```ts
166+
const output = root
167+
.createTexture({
168+
size: [width, height],
169+
format: 'rgba16float',
170+
})
171+
.$usage('storage', 'sampled');
172+
173+
const runner = rc.createRadianceCascades({
174+
root,
175+
output,
176+
sdfResolution,
177+
sdf: sceneSdf,
178+
color: surfaceColor,
179+
});
180+
```
181+
182+
When a storage view cannot provide its size, pass `size` explicitly alongside `output`.
183+
184+
## Updating and cleanup
185+
186+
The executor has a small lifecycle:
187+
188+
- `runner.run()` dispatches all cascade passes and writes the current radiance field.
189+
- `runner.output` is the sampled/storage `rgba16float` result.
190+
- `runner.with(bindGroup)` returns an executor with an extra bind group attached to all internal passes.
191+
- `runner.destroy()` releases the cascade textures, and also releases the output texture if the runner created it.
192+
193+
Use `with(bindGroup)` when your SDF, color, or ray marching callback reads resources that are not captured through TypeGPU accessors.
194+
195+
## Custom ray marching
196+
197+
By default, the runner uses `defaultRayMarch`, which sphere-traces through the supplied SDF and samples `color` at the first hit.
198+
For custom attenuation, translucent surfaces, or non-SDF sources, pass a `rayMarch` callback.
199+
This example adds near-surface haze and lets part of the light continue after a hit.
200+
201+
<div class="my-4 flex justify-center">
202+
<Image src={customRayMarchingPreview} alt="A radiance cascades scene with custom ray marching haze" class="w-full max-w-[24rem] rounded-md" />
203+
</div>
204+
205+
```ts
206+
const runner = rc.createRadianceCascades({
207+
root,
208+
size,
209+
sdfResolution,
210+
sdf: sceneSdf,
211+
color: surfaceColor,
212+
rayMarch: (probePos, rayDir, startT, endT, eps, minStep, bias) => {
213+
'use gpu';
214+
let color = d.vec3f();
215+
let transmittance = d.f32(1);
216+
let t = startT;
217+
218+
for (let step = 0; step < 64; step++) {
219+
if (t > endT || transmittance < 0.02) {
220+
break;
221+
}
222+
223+
const pos = probePos + rayDir * t;
224+
if (std.any(std.lt(pos, d.vec2f(0))) || std.any(std.gt(pos, d.vec2f(1)))) {
225+
break;
226+
}
227+
228+
const signedDist = sceneSdf(pos);
229+
const stepSize = std.max(signedDist + bias, minStep);
230+
const haze = std.exp(-std.abs(signedDist) * 34) * stepSize * 18;
231+
const hazeColor = std.mix(d.vec3f(1, 0.38, 0.14), d.vec3f(0.22, 0.56, 1), pos.x);
232+
color += hazeColor * haze * transmittance;
233+
234+
if (signedDist + bias <= eps) {
235+
color += surfaceColor(pos) * transmittance * 0.65;
236+
transmittance *= 0.35;
237+
break;
238+
}
239+
240+
transmittance *= std.max(1 - haze * 0.08, 0.72);
241+
t += stepSize;
242+
}
243+
244+
return rc.RayMarchResult({
245+
color,
246+
transmittance,
247+
});
248+
},
249+
});
250+
```
251+
252+
Custom ray marchers should return `RayMarchResult`, where `color` is accumulated radiance and `transmittance` is how much light should continue to the next cascade (`1` means no hit, `0` means fully blocked).
253+
254+
## Advanced exports
255+
256+
Most users only need `createRadianceCascades`. The package also exports lower-level pieces for custom runners and experiments:
257+
258+
| Export | Purpose |
259+
| --- | --- |
260+
| `defaultRayMarch` | The built-in ray marcher used by the runner |
261+
| `RayMarchResult` | Return struct for custom ray marchers |
262+
| `getCascadeDim` | Computes internal cascade texture dimensions |
263+
| `sdfSlot`, `colorSlot`, `rayMarchSlot`, `sdfResolutionSlot` | Slots used by the internal cascade compute pass |
264+
265+
See the [package source](https://github.com/software-mansion/TypeGPU/tree/main/packages/typegpu-radiance-cascades/src) for details that are not yet covered here.

0 commit comments

Comments
 (0)