Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/typegpu-docs/src/components/ExampleShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const ADVANCED_EXAMPLE_KEYS = [
'rendering--clouds',
'rendering--cubemap-reflection',
'rendering--function-visualizer',
'advanced--jelly-knob',
'rendering--jelly-slider',
'rendering--jelly-switch',
'rendering--os-awards',
Expand Down
36 changes: 36 additions & 0 deletions apps/typegpu-docs/src/examples/advanced/jelly-knob/blueNoise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* GPU-side blue noise, vendored from `packages/core/src/gpu/kit/noise.ts` in
* https://github.com/shader-effects-inc/shaders (commit b669017), written by Simon Le Marchant.
* The hash comes from the Fi-hash noise family by @lumiey.
*
* Copyright (c) 2026 Shader Effects Inc. Licensed under the MIT License.
*/
import { d, std } from 'typegpu';

// float(~0u), which rounds to 2^32 in f32. Used to normalize hashes into [0, 1]
const U32_MAX = 2 ** 32;

/**
* White noise, vec2 → [0, 1]. Reinterprets the float bits of `p` as integers,
* relying on wrapping u32 multiplication. GPU-only, as JS numbers don't wrap like u32 does.
*/
export function hash12(p: d.v2f): number {
'use gpu';
const u = std.bitcast(d.vec2f, d.vec2u)(p * d.vec2f(141421356, 2718281828));
return d.f32((u.x ^ u.y) * d.u32(3141592653)) / U32_MAX;
}

/**
* Pixel-space blue noise, made by high-pass filtering white noise over the 3×3 neighborhood.
* Expects floored pixel coordinates, returns values in roughly [0, 1].
*/
export function blue12(p: d.v2f): number {
'use gpu';
let sum = d.f32(0);
for (let y = -1; y <= 1; y++) {
for (let x = -1; x <= 1; x++) {
sum += hash12(p + d.vec2f(x, y));
}
}
return 0.9 * (1.125 * hash12(p) - sum / 8) + 0.5;
}
118 changes: 118 additions & 0 deletions apps/typegpu-docs/src/examples/advanced/jelly-knob/camera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { d, type TgpuRoot, type TgpuUniform } from 'typegpu';
import * as m from 'wgpu-matrix';

export const Camera = d.struct({
view: d.mat4x4f,
proj: d.mat4x4f,
viewInv: d.mat4x4f,
projInv: d.mat4x4f,
});

function halton(index: number, base: number) {
let result = 0;
let f = 1 / base;
let i = index;
while (i > 0) {
result += f * (i % base);
i = Math.floor(i / base);
f = f / base;
}
return result;
}

function* haltonSequence(base: number) {
let index = 1;
while (true) {
yield halton(index, base);
index++;
}
}

export class CameraController {
#uniform: TgpuUniform<typeof Camera>;
#view: d.m4x4f;
#viewInv: d.m4x4f;
#baseProj: d.m4x4f;
#baseProjInv: d.m4x4f;
#haltonX: Generator<number>;
#haltonY: Generator<number>;
#width: number;
#height: number;

constructor(
root: TgpuRoot,
position: d.v3f,
target: d.v3f,
up: d.v3f,
fov: number,
width: number,
height: number,
near = 0.1,
far = 10,
) {
this.#width = width;
this.#height = height;

this.#view = m.mat4.lookAt(position, target, up, d.mat4x4f());
this.#baseProj = m.mat4.perspective(fov, width / height, near, far, d.mat4x4f());

this.#viewInv = m.mat4.invert(this.#view, d.mat4x4f());
this.#baseProjInv = m.mat4.invert(this.#baseProj, d.mat4x4f());

this.#uniform = root.createUniform(Camera, {
view: this.#view,
proj: this.#baseProj,
viewInv: this.#viewInv,
projInv: this.#baseProjInv,
});

this.#haltonX = haltonSequence(2);
this.#haltonY = haltonSequence(3);
}

jitter() {
const [jx, jy] = [this.#haltonX.next().value, this.#haltonY.next().value] as [number, number];

const jitterX = ((jx - 0.5) * 2.0) / this.#width;
const jitterY = ((jy - 0.5) * 2.0) / this.#height;

const jitterMatrix = m.mat4.identity(d.mat4x4f());
jitterMatrix[12] = jitterX; // x translation in NDC
jitterMatrix[13] = jitterY; // y translation in NDC

const jitteredProj = m.mat4.mul(jitterMatrix, this.#baseProj, d.mat4x4f());
const jitteredProjInv = m.mat4.invert(jitteredProj, d.mat4x4f());

this.#uniform.patch({
proj: jitteredProj,
projInv: jitteredProjInv,
});
}

updateView(position: d.v3f, target: d.v3f, up: d.v3f) {
this.#view = m.mat4.lookAt(position, target, up, d.mat4x4f());
this.#viewInv = m.mat4.invert(this.#view, d.mat4x4f());

this.#uniform.patch({
view: this.#view,
viewInv: this.#viewInv,
});
}

updateProjection(fov: number, width: number, height: number, near = 0.1, far = 10) {
this.#width = width;
this.#height = height;

this.#baseProj = m.mat4.perspective(fov, width / height, near, far, d.mat4x4f());
this.#baseProjInv = m.mat4.invert(this.#baseProj, d.mat4x4f());

this.#uniform.patch({
proj: this.#baseProj,
projInv: this.#baseProjInv,
});
}

get cameraUniform() {
return this.#uniform;
}
}
54 changes: 54 additions & 0 deletions apps/typegpu-docs/src/examples/advanced/jelly-knob/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { d, std } from 'typegpu';
import type { SpringProperties } from './spring.ts';

// Rendering constants
export const MAX_STEPS = 64;
export const MAX_DIST = 10;
export const SURF_DIST = 0.001;

// Fixed camera, looking at the origin with +Y up
export const CAMERA_POSITION = d.vec3f(0, 2.7, 0.8);

// Ground material constants
// Linear blue-gray, tuned for the example page's dark #1b1f2c background.
export const GROUND_ALBEDO = d.vec3f(0.025, 0.032, 0.054);
export const LIGHT_GROUND_ALBEDO = d.vec3f(0.65, 0.7, 0.79);
export const EXPOSURE = 1.5;
export const METER_TICKS = 16;

export const GroundParams = {
groundThickness: 0.01,
groundRoundness: 0.01,
jellyCutoutRadius: 0.38,
meterCutoutRadius: 0.6,
meterCutoutGirth: 0.04,
};

// Lighting constants
export const AMBIENT_COLOR = d.vec3f(0.6);
export const AMBIENT_INTENSITY = 0.6;
export const SPECULAR_POWER = 10;
export const SPECULAR_INTENSITY = 0.6;
export const LIGHT_DIR = std.normalize(d.vec3f(-0.5, -0.14, -0.8));
// Light travels from above and behind the knob toward the camera (+Z).
export const LIGHT_MODE_LIGHT_DIR = std.normalize(d.vec3f(-0.12, -0.75, 0.7));

// Jelly material constants
export const JELLY_IOR = 1.42;
export const JELLY_SCATTER_STRENGTH = 3;

// Ambient occlusion constants
export const AO_STEPS = 3;
export const AO_RADIUS = 0.1;
export const AO_INTENSITY = 0.5;
export const AO_BIAS = SURF_DIST * 5;

// Jelly constants
export const JELLY_HALFSIZE = d.vec3f(0.3, 0.3, 0.3);

// Spring dynamics constants
export const twistProperties: SpringProperties = {
mass: 4,
stiffness: 700,
damping: 50,
};
70 changes: 70 additions & 0 deletions apps/typegpu-docs/src/examples/advanced/jelly-knob/dataTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { tgpu, d, type TgpuUniform } from 'typegpu';
import type { KnobBehavior } from './knob.ts';
import type { Camera } from './camera.ts';

export const DirectionalLight = d.struct({
direction: d.vec3f,
color: d.vec3f,
});

export const ObjectType = {
JELLY: 1,
BACKGROUND: 3,
} as const;

export const HitInfo = d.struct({
distance: d.f32,
objectType: d.i32,
});

export const BoxIntersection = d.struct({
hit: d.bool,
tMin: d.f32,
tMax: d.f32,
});

export const Ray = d.struct({
origin: d.vec3f,
direction: d.vec3f,
});

export const RayMarchResult = d.struct({
point: d.vec3f,
color: d.vec3f,
});

export type BoundingBox = d.Infer<typeof BoundingBox>;
export const BoundingBox = d.struct({
min: d.vec3f,
max: d.vec3f,
});

export const KnobState = d.struct({
topDisplacement: d.vec3f,
topProgress: d.f32,
bottomProgress: d.f32,
});

export const taaResolveLayout = tgpu.bindGroupLayout({
currentTexture: {
texture: d.texture2d(),
},
historyTexture: {
texture: d.texture2d(),
},
outputTexture: {
storageTexture: d.textureStorage2d('rgba16float', 'write-only'),
},
});

export const sampleLayout = tgpu.bindGroupLayout({
currentTexture: {
texture: d.texture2d(),
},
});

export const knobBehaviorSlot = tgpu.slot<KnobBehavior>();
export const cameraUniformSlot = tgpu.slot<TgpuUniform<typeof Camera>>();
export const lightUniformSlot = tgpu.slot<TgpuUniform<typeof DirectionalLight>>();
export const jellyColorUniformSlot = tgpu.slot<TgpuUniform<typeof d.vec4f>>();
export const darkModeUniformSlot = tgpu.slot<TgpuUniform<typeof d.u32>>();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<canvas></canvas>
Loading
Loading