Skip to content

Commit 29da05f

Browse files
authored
docs: Clouds example (#1655)
1 parent ecbe55d commit 29da05f

8 files changed

Lines changed: 515 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as d from 'typegpu/data';
2+
3+
export const FOV_FACTOR = 1;
4+
5+
export const SUN_DIRECTION = d.vec3f(1.0, 0.0, 0.0);
6+
export const SUN_BRIGHTNESS = 0.9;
7+
export const LIGHT_ABSORPTION = 0.88;
8+
9+
export const CLOUD_COVERAGE = 0.7;
10+
export const CLOUD_AMPLITUDE = 1.0;
11+
export const CLOUD_FREQUENCY = 1.4;
12+
export const WIND_SPEED = 1.0;
13+
14+
export const FBM_OCTAVES = 3;
15+
export const FBM_PERSISTENCE = 0.5;
16+
export const FBM_LACUNARITY = 2.0;
17+
18+
export const CLOUD_BRIGHT = d.vec3f(1.0, 1.0, 1.0);
19+
export const CLOUD_DARK = d.vec3f(0.2, 0.2, 0.2);
20+
export const SKY_AMBIENT = d.vec3f(0.6, 0.45, 0.75);
21+
export const SUN_COLOR = d.vec3f(1.0, 0.7, 0.3);
22+
export const SKY_HORIZON = d.vec3f(0.75, 0.66, 0.9);
23+
export const SKY_ZENITH_TINT = d.vec3f(1.0, 0.7, 0.43);
24+
export const SUN_GLOW = d.vec3f(1.0, 0.37, 0.17);
25+
26+
export const NOISE_Z_OFFSET = d.vec2f(37.0, 239.0);
27+
export const NOISE_TEXTURE_SIZE = 256;
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: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import tgpu from 'typegpu';
2+
import * as d from 'typegpu/data';
3+
import * as std from 'typegpu/std';
4+
import { fullScreenTriangle } from 'typegpu/common';
5+
import {
6+
FOV_FACTOR,
7+
NOISE_TEXTURE_SIZE,
8+
SKY_HORIZON,
9+
SKY_ZENITH_TINT,
10+
SUN_BRIGHTNESS,
11+
SUN_DIRECTION,
12+
SUN_GLOW,
13+
WIND_SPEED,
14+
} from './consts.ts';
15+
import { raymarch } from './utils.ts';
16+
import { cloudsLayout, CloudsParams } from './types.ts';
17+
import { randf } from '@typegpu/noise';
18+
19+
const canvas = document.querySelector('canvas') as HTMLCanvasElement;
20+
const context = canvas.getContext('webgpu') as GPUCanvasContext;
21+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
22+
23+
const root = await tgpu.init();
24+
25+
context.configure({
26+
device: root.device,
27+
format: presentationFormat,
28+
alphaMode: 'premultiplied',
29+
});
30+
31+
const paramsUniform = root.createUniform(CloudsParams, {
32+
time: 0,
33+
maxSteps: 50,
34+
maxDistance: 10.0,
35+
});
36+
const resolutionUniform = root.createUniform(
37+
d.vec2f,
38+
d.vec2f(canvas.width, canvas.height),
39+
);
40+
41+
const noiseData = new Uint8Array(NOISE_TEXTURE_SIZE * NOISE_TEXTURE_SIZE * 4);
42+
for (let i = 0; i < noiseData.length; i += 4) {
43+
const value = Math.random() * 255;
44+
noiseData[i] = value;
45+
noiseData[i + 1] = Math.random() * 255;
46+
noiseData[i + 2] = value;
47+
noiseData[i + 3] = 255;
48+
}
49+
50+
const sampler = root['~unstable'].createSampler({
51+
magFilter: 'linear',
52+
minFilter: 'linear',
53+
addressModeU: 'repeat',
54+
addressModeV: 'repeat',
55+
});
56+
57+
const noiseTexture = root['~unstable']
58+
.createTexture({
59+
size: [NOISE_TEXTURE_SIZE, NOISE_TEXTURE_SIZE],
60+
format: 'rgba8unorm',
61+
})
62+
.$usage('sampled', 'render');
63+
64+
root.device.queue.writeTexture(
65+
{ texture: root.unwrap(noiseTexture) },
66+
noiseData,
67+
{ bytesPerRow: NOISE_TEXTURE_SIZE * 4 },
68+
{ width: NOISE_TEXTURE_SIZE, height: NOISE_TEXTURE_SIZE },
69+
);
70+
71+
const bindGroup = root.createBindGroup(cloudsLayout, {
72+
params: paramsUniform.buffer,
73+
noiseTexture,
74+
sampler,
75+
});
76+
77+
const mainFragment = tgpu['~unstable'].fragmentFn({
78+
in: { uv: d.vec2f },
79+
out: d.vec4f,
80+
})(({ uv }) => {
81+
randf.seed2(uv.mul(cloudsLayout.$.params.time));
82+
const screenRes = resolutionUniform.$;
83+
const aspect = screenRes.x / screenRes.y;
84+
85+
let screenPos = std.mul(std.sub(uv, 0.5), 2.0);
86+
screenPos = d.vec2f(
87+
screenPos.x * std.max(aspect, 1.0),
88+
screenPos.y * std.max(1.0 / aspect, 1.0),
89+
);
90+
91+
const sunDir = std.normalize(SUN_DIRECTION);
92+
const time = cloudsLayout.$.params.time;
93+
const rayOrigin = d.vec3f(
94+
std.sin(time * 0.6) * 0.5,
95+
std.cos(time * 0.8) * 0.5 - 1,
96+
time * WIND_SPEED,
97+
);
98+
const rayDir = std.normalize(d.vec3f(screenPos.x, screenPos.y, FOV_FACTOR));
99+
100+
const sunDot = std.clamp(std.dot(rayDir, sunDir), 0.0, 1.0);
101+
const sunGlow = std.pow(
102+
sunDot,
103+
1.0 / (SUN_BRIGHTNESS * SUN_BRIGHTNESS * SUN_BRIGHTNESS),
104+
);
105+
106+
let skyCol = std.sub(SKY_HORIZON, std.mul(SKY_ZENITH_TINT, rayDir.y * 0.35));
107+
skyCol = std.add(skyCol, std.mul(SUN_GLOW, sunGlow));
108+
109+
const cloudCol = raymarch(rayOrigin, rayDir, sunDir);
110+
const finalCol = std.add(std.mul(skyCol, 1.1 - cloudCol.w), cloudCol.xyz);
111+
112+
return d.vec4f(finalCol, 1.0);
113+
});
114+
115+
const pipeline = root['~unstable']
116+
.withVertex(fullScreenTriangle)
117+
.withFragment(mainFragment, { format: presentationFormat })
118+
.createPipeline();
119+
120+
const resizeObserver = new ResizeObserver(() => {
121+
resolutionUniform.write(d.vec2f(canvas.width, canvas.height));
122+
});
123+
resizeObserver.observe(canvas);
124+
125+
let frameId: number;
126+
127+
function render() {
128+
paramsUniform.writePartial({ time: (performance.now() / 1000) % 500 });
129+
130+
pipeline
131+
.with(bindGroup)
132+
.withColorAttachment({
133+
view: context.getCurrentTexture().createView(),
134+
clearValue: [0, 0, 0, 1],
135+
loadOp: 'clear',
136+
storeOp: 'store',
137+
})
138+
.draw(6);
139+
140+
frameId = requestAnimationFrame(render);
141+
}
142+
143+
frameId = requestAnimationFrame(render);
144+
145+
const qualityOptions = {
146+
'very high': {
147+
maxSteps: 150,
148+
maxDistance: 13.0,
149+
},
150+
high: {
151+
maxSteps: 100,
152+
maxDistance: 12.0,
153+
},
154+
medium: {
155+
maxSteps: 50,
156+
maxDistance: 10.0,
157+
},
158+
low: {
159+
maxSteps: 30,
160+
maxDistance: 6.0,
161+
},
162+
'very low': {
163+
maxSteps: 15,
164+
maxDistance: 4.0,
165+
},
166+
} as Record<string, Partial<d.Infer<typeof CloudsParams>>>;
167+
168+
export const controls = {
169+
Quality: {
170+
initial: 'medium',
171+
options: ['very high', 'high', 'medium', 'low', 'very low'],
172+
onSelectChange(value: string) {
173+
paramsUniform.writePartial(qualityOptions[value]);
174+
},
175+
},
176+
};
177+
178+
export function onCleanup() {
179+
cancelAnimationFrame(frameId);
180+
resizeObserver.disconnect();
181+
root.destroy();
182+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"title": "Clouds",
3+
"category": "rendering",
4+
"tags": ["experimental", "3d", "ray marching"]
5+
}
1.4 MB
Loading
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import tgpu from 'typegpu';
2+
import * as d from 'typegpu/data';
3+
4+
export const CloudsParams = d.struct({
5+
time: d.f32,
6+
maxSteps: d.i32,
7+
maxDistance: d.f32,
8+
});
9+
10+
export const cloudsLayout = tgpu.bindGroupLayout({
11+
params: { uniform: CloudsParams },
12+
noiseTexture: { texture: d.texture2d() },
13+
sampler: { sampler: 'filtering' },
14+
});
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import tgpu from 'typegpu';
2+
import * as d from 'typegpu/data';
3+
import * as std from 'typegpu/std';
4+
import { randf } from '@typegpu/noise';
5+
import {
6+
CLOUD_AMPLITUDE,
7+
CLOUD_BRIGHT,
8+
CLOUD_COVERAGE,
9+
CLOUD_DARK,
10+
CLOUD_FREQUENCY,
11+
FBM_LACUNARITY,
12+
FBM_OCTAVES,
13+
FBM_PERSISTENCE,
14+
LIGHT_ABSORPTION,
15+
NOISE_TEXTURE_SIZE,
16+
NOISE_Z_OFFSET,
17+
SKY_AMBIENT,
18+
SUN_BRIGHTNESS,
19+
SUN_COLOR,
20+
} from './consts.ts';
21+
import { cloudsLayout } from './types.ts';
22+
23+
const sampleDensity = tgpu.fn([d.vec3f], d.f32)((pos) => {
24+
const coverage = CLOUD_COVERAGE - std.abs(pos.y) * 0.25;
25+
return std.saturate(fbm(pos) + coverage) - 0.5;
26+
});
27+
28+
const sampleDensityCheap = tgpu.fn([d.vec3f], d.f32)((pos) => {
29+
const noise = noise3d(std.mul(pos, CLOUD_FREQUENCY)) * CLOUD_AMPLITUDE;
30+
return std.clamp(noise + CLOUD_COVERAGE - 0.5, 0.0, 1.0);
31+
});
32+
33+
export const raymarch = tgpu.fn([d.vec3f, d.vec3f, d.vec3f], d.vec4f)(
34+
(rayOrigin, rayDir, sunDir) => {
35+
let accum = d.vec4f();
36+
37+
const params = cloudsLayout.$.params;
38+
const maxSteps = params.maxSteps;
39+
const maxDepth = params.maxDistance;
40+
41+
const stepSize = 1 / maxSteps;
42+
let dist = randf.sample() * stepSize;
43+
44+
for (let i = 0; i < maxSteps; i++) {
45+
const samplePos = std.add(rayOrigin, std.mul(rayDir, dist * maxDepth));
46+
const cloudDensity = sampleDensity(samplePos);
47+
48+
if (cloudDensity > 0.0) {
49+
const shadowPos = std.add(samplePos, sunDir);
50+
const shadowDensity = sampleDensityCheap(shadowPos);
51+
const shadow = std.clamp(cloudDensity - shadowDensity, 0.0, 1.0);
52+
const lightVal = std.mix(0.3, 1.0, shadow);
53+
54+
const light = std.add(
55+
std.mul(SKY_AMBIENT, 1.1),
56+
std.mul(SUN_COLOR, lightVal * SUN_BRIGHTNESS),
57+
);
58+
const color = std.mix(CLOUD_BRIGHT, CLOUD_DARK, cloudDensity);
59+
const lit = std.mul(color, light);
60+
61+
const contrib = std.mul(
62+
d.vec4f(lit, 1),
63+
cloudDensity * (LIGHT_ABSORPTION - accum.w),
64+
);
65+
accum = std.add(accum, contrib);
66+
67+
if (accum.w >= LIGHT_ABSORPTION - 0.001) {
68+
break;
69+
}
70+
}
71+
dist += stepSize;
72+
}
73+
return accum;
74+
},
75+
);
76+
77+
const fbm = tgpu.fn([d.vec3f], d.f32)((pos) => {
78+
let sum = d.f32();
79+
let amp = d.f32(CLOUD_AMPLITUDE);
80+
let freq = d.f32(CLOUD_FREQUENCY);
81+
82+
for (let i = 0; i < FBM_OCTAVES; i++) {
83+
sum += noise3d(std.mul(pos, freq)) * amp;
84+
amp *= FBM_PERSISTENCE;
85+
freq *= FBM_LACUNARITY;
86+
}
87+
return sum;
88+
});
89+
90+
const noise3d = tgpu.fn([d.vec3f], d.f32)((pos) => {
91+
const idx = std.floor(pos);
92+
const frac = std.fract(pos);
93+
const smooth = std.mul(std.mul(frac, frac), std.sub(3.0, std.mul(2.0, frac)));
94+
95+
const texCoord0 = std.fract(
96+
std.div(
97+
std.add(std.add(idx.xy, frac.xy), std.mul(NOISE_Z_OFFSET, idx.z)),
98+
NOISE_TEXTURE_SIZE,
99+
),
100+
);
101+
const texCoord1 = std.fract(
102+
std.div(
103+
std.add(
104+
std.add(idx.xy, frac.xy),
105+
std.mul(NOISE_Z_OFFSET, std.add(idx.z, 1.0)),
106+
),
107+
NOISE_TEXTURE_SIZE,
108+
),
109+
);
110+
111+
const val0 = std.textureSampleLevel(
112+
cloudsLayout.$.noiseTexture,
113+
cloudsLayout.$.sampler,
114+
texCoord0,
115+
0.0,
116+
).x;
117+
118+
const val1 = std.textureSampleLevel(
119+
cloudsLayout.$.noiseTexture,
120+
cloudsLayout.$.sampler,
121+
texCoord1,
122+
0.0,
123+
).x;
124+
125+
return std.mix(val0, val1, smooth.z) * 2.0 - 1.0;
126+
});

0 commit comments

Comments
 (0)