Skip to content

Commit cfeb3c6

Browse files
authored
docs: Jelly slider (#1791)
1 parent a8aed2e commit cfeb3c6

16 files changed

Lines changed: 2390 additions & 5 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { TgpuRoot, TgpuUniform } from 'typegpu';
2+
import * as d from 'typegpu/data';
3+
import * as m from 'wgpu-matrix';
4+
5+
const Camera = d.struct({
6+
view: d.mat4x4f,
7+
proj: d.mat4x4f,
8+
viewInv: d.mat4x4f,
9+
projInv: d.mat4x4f,
10+
});
11+
12+
function halton(index: number, base: number) {
13+
let result = 0;
14+
let f = 1 / base;
15+
let i = index;
16+
while (i > 0) {
17+
result += f * (i % base);
18+
i = Math.floor(i / base);
19+
f = f / base;
20+
}
21+
return result;
22+
}
23+
24+
function* haltonSequence(base: number) {
25+
let index = 1;
26+
while (true) {
27+
yield halton(index, base);
28+
index++;
29+
}
30+
}
31+
32+
export class CameraController {
33+
#uniform: TgpuUniform<typeof Camera>;
34+
#view: d.m4x4f;
35+
#proj: d.m4x4f;
36+
#viewInv: d.m4x4f;
37+
#projInv: d.m4x4f;
38+
#baseProj: d.m4x4f;
39+
#baseProjInv: d.m4x4f;
40+
#haltonX: Generator<number>;
41+
#haltonY: Generator<number>;
42+
#width: number;
43+
#height: number;
44+
45+
constructor(
46+
root: TgpuRoot,
47+
position: d.v3f,
48+
target: d.v3f,
49+
up: d.v3f,
50+
fov: number,
51+
width: number,
52+
height: number,
53+
near = 0.1,
54+
far = 10,
55+
) {
56+
this.#width = width;
57+
this.#height = height;
58+
59+
this.#view = m.mat4.lookAt(position, target, up, d.mat4x4f());
60+
this.#baseProj = m.mat4.perspective(
61+
fov,
62+
width / height,
63+
near,
64+
far,
65+
d.mat4x4f(),
66+
);
67+
this.#proj = this.#baseProj;
68+
69+
this.#viewInv = m.mat4.invert(this.#view, d.mat4x4f());
70+
this.#baseProjInv = m.mat4.invert(this.#baseProj, d.mat4x4f());
71+
this.#projInv = this.#baseProjInv;
72+
73+
this.#uniform = root.createUniform(Camera, {
74+
view: this.#view,
75+
proj: this.#proj,
76+
viewInv: this.#viewInv,
77+
projInv: this.#projInv,
78+
});
79+
80+
this.#haltonX = haltonSequence(2);
81+
this.#haltonY = haltonSequence(3);
82+
}
83+
84+
jitter() {
85+
const [jx, jy] = [
86+
this.#haltonX.next().value,
87+
this.#haltonY.next().value,
88+
] as [
89+
number,
90+
number,
91+
];
92+
93+
const jitterX = ((jx - 0.5) * 2.0) / this.#width;
94+
const jitterY = ((jy - 0.5) * 2.0) / this.#height;
95+
96+
const jitterMatrix = m.mat4.identity(d.mat4x4f());
97+
jitterMatrix[12] = jitterX; // x translation in NDC
98+
jitterMatrix[13] = jitterY; // y translation in NDC
99+
100+
const jitteredProj = m.mat4.mul(jitterMatrix, this.#baseProj, d.mat4x4f());
101+
const jitteredProjInv = m.mat4.invert(jitteredProj, d.mat4x4f());
102+
103+
this.#uniform.writePartial({
104+
proj: jitteredProj,
105+
projInv: jitteredProjInv,
106+
});
107+
}
108+
109+
updateView(position: d.v3f, target: d.v3f, up: d.v3f) {
110+
this.#view = m.mat4.lookAt(position, target, up, d.mat4x4f());
111+
this.#viewInv = m.mat4.invert(this.#view, d.mat4x4f());
112+
113+
this.#uniform.writePartial({
114+
view: this.#view,
115+
viewInv: this.#viewInv,
116+
});
117+
}
118+
119+
updateProjection(
120+
fov: number,
121+
width: number,
122+
height: number,
123+
near = 0.1,
124+
far = 100,
125+
) {
126+
this.#width = width;
127+
this.#height = height;
128+
129+
this.#baseProj = m.mat4.perspective(
130+
fov,
131+
width / height,
132+
near,
133+
far,
134+
d.mat4x4f(),
135+
);
136+
this.#baseProjInv = m.mat4.invert(this.#baseProj, d.mat4x4f());
137+
138+
this.#uniform.writePartial({
139+
proj: this.#baseProj,
140+
projInv: this.#baseProjInv,
141+
});
142+
}
143+
144+
get cameraUniform() {
145+
return this.#uniform;
146+
}
147+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as d from 'typegpu/data';
2+
3+
// Rendering constants
4+
export const MAX_STEPS = 64;
5+
export const MAX_DIST = 10;
6+
export const SURF_DIST = 0.001;
7+
8+
// Ground material constants
9+
export const GROUND_ALBEDO = d.vec3f(1);
10+
11+
// Lighting constants
12+
export const AMBIENT_COLOR = d.vec3f(0.6);
13+
export const AMBIENT_INTENSITY = 0.6;
14+
export const SPECULAR_POWER = 120.0;
15+
export const SPECULAR_INTENSITY = 0.6;
16+
17+
// Jelly material constants
18+
export const JELLY_IOR = 1.42;
19+
export const JELLY_SCATTER_STRENGTH = 3;
20+
21+
// Ambient occlusion constants
22+
export const AO_STEPS = 3;
23+
export const AO_RADIUS = 0.1;
24+
export const AO_INTENSITY = 0.5;
25+
export const AO_BIAS = SURF_DIST * 5;
26+
27+
// Line/slider constants
28+
export const LINE_RADIUS = 0.024;
29+
export const LINE_HALF_THICK = 0.17;
30+
31+
// Mouse interaction constants
32+
export const MOUSE_SMOOTHING = 0.08;
33+
export const MOUSE_MIN_X = 0.45;
34+
export const MOUSE_MAX_X = 0.9;
35+
export const MOUSE_RANGE_MIN = 0.4;
36+
export const MOUSE_RANGE_MAX = 0.9;
37+
export const TARGET_MIN = -0.7;
38+
export const TARGET_MAX = 1.0;
39+
export const TARGET_OFFSET = -0.5;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import tgpu from 'typegpu';
2+
import * as d from 'typegpu/data';
3+
4+
export const DirectionalLight = d.struct({
5+
direction: d.vec3f,
6+
color: d.vec3f,
7+
});
8+
9+
export const ObjectType = {
10+
SLIDER: 1,
11+
BACKGROUND: 2,
12+
} as const;
13+
14+
export const HitInfo = d.struct({
15+
distance: d.f32,
16+
objectType: d.i32,
17+
t: d.f32,
18+
});
19+
20+
export const LineInfo = d.struct({
21+
t: d.f32,
22+
distance: d.f32,
23+
normal: d.vec2f,
24+
});
25+
26+
export const BoxIntersection = d.struct({
27+
hit: d.bool,
28+
tMin: d.f32,
29+
tMax: d.f32,
30+
});
31+
32+
export const Ray = d.struct({
33+
origin: d.vec3f,
34+
direction: d.vec3f,
35+
});
36+
37+
export const SdfBbox = d.struct({
38+
left: d.f32,
39+
right: d.f32,
40+
bottom: d.f32,
41+
top: d.f32,
42+
});
43+
44+
export const rayMarchLayout = tgpu.bindGroupLayout({
45+
backgroundTexture: { texture: d.texture2d(d.f32) },
46+
});
47+
48+
export const taaResolveLayout = tgpu.bindGroupLayout({
49+
currentTexture: {
50+
texture: d.texture2d(),
51+
},
52+
historyTexture: {
53+
texture: d.texture2d(),
54+
},
55+
outputTexture: {
56+
storageTexture: d.textureStorage2d('rgba8unorm', 'write-only'),
57+
},
58+
});
59+
60+
export const sampleLayout = tgpu.bindGroupLayout({
61+
currentTexture: {
62+
texture: d.texture2d(),
63+
},
64+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {
2+
MOUSE_MAX_X,
3+
MOUSE_MIN_X,
4+
MOUSE_RANGE_MAX,
5+
MOUSE_RANGE_MIN,
6+
MOUSE_SMOOTHING,
7+
TARGET_MAX,
8+
TARGET_MIN,
9+
TARGET_OFFSET,
10+
} from './constants.ts';
11+
12+
export class EventHandler {
13+
private canvas: HTMLCanvasElement;
14+
private mouseX = 1.0;
15+
private targetMouseX = 1.0;
16+
private isMouseDown = false;
17+
18+
constructor(canvas: HTMLCanvasElement) {
19+
this.canvas = canvas;
20+
this.setupEventListeners();
21+
}
22+
23+
private setupEventListeners() {
24+
// Mouse events
25+
this.canvas.addEventListener('mouseup', () => {
26+
this.isMouseDown = false;
27+
});
28+
29+
this.canvas.addEventListener('mouseleave', () => {
30+
this.isMouseDown = false;
31+
});
32+
33+
this.canvas.addEventListener('mousedown', (e) => {
34+
this.handlePointerDown(e.clientX);
35+
});
36+
37+
this.canvas.addEventListener('mousemove', (e) => {
38+
if (!this.isMouseDown) return;
39+
this.handlePointerMove(e.clientX);
40+
});
41+
42+
// Touch events
43+
this.canvas.addEventListener('touchstart', (e) => {
44+
e.preventDefault();
45+
const touch = e.touches[0];
46+
this.handlePointerDown(touch.clientX);
47+
});
48+
49+
this.canvas.addEventListener('touchmove', (e) => {
50+
e.preventDefault();
51+
if (!this.isMouseDown) return;
52+
const touch = e.touches[0];
53+
this.handlePointerMove(touch.clientX);
54+
});
55+
56+
this.canvas.addEventListener('touchend', (e) => {
57+
e.preventDefault();
58+
this.isMouseDown = false;
59+
});
60+
}
61+
62+
private handlePointerDown(clientX: number) {
63+
this.isMouseDown = true;
64+
this.updateTargetMouseX(clientX);
65+
}
66+
67+
private handlePointerMove(clientX: number) {
68+
this.updateTargetMouseX(clientX);
69+
}
70+
71+
private updateTargetMouseX(clientX: number) {
72+
const rect = this.canvas.getBoundingClientRect();
73+
const normalizedX = (clientX - rect.left) / rect.width;
74+
const clampedX = Math.max(MOUSE_MIN_X, Math.min(MOUSE_MAX_X, normalizedX));
75+
this.targetMouseX =
76+
((clampedX - MOUSE_RANGE_MIN) / (MOUSE_RANGE_MAX - MOUSE_RANGE_MIN)) *
77+
(TARGET_MAX - TARGET_MIN) + TARGET_OFFSET;
78+
}
79+
80+
update() {
81+
if (this.isMouseDown) {
82+
this.mouseX += (this.targetMouseX - this.mouseX) * MOUSE_SMOOTHING;
83+
}
84+
}
85+
86+
get currentMouseX() {
87+
return this.mouseX;
88+
}
89+
90+
get isPointerDown() {
91+
return this.isMouseDown;
92+
}
93+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<style>
2+
#attribution {
3+
opacity: 1;
4+
position: absolute;
5+
bottom: 0.625rem;
6+
left: 50%;
7+
transform: translateX(-50%);
8+
z-index: 2;
9+
background-color: rgba(0, 0, 0, 0.7);
10+
color: white;
11+
padding: 0.75rem 1rem;
12+
border-radius: 0.625rem;
13+
user-select: none;
14+
pointer-events: auto;
15+
transition: opacity 0.5s;
16+
font-size: 1rem;
17+
text-align: center;
18+
white-space: nowrap;
19+
}
20+
21+
#attribution a {
22+
color: #87ceeb;
23+
text-decoration: none;
24+
cursor: pointer;
25+
}
26+
27+
#attribution a:hover {
28+
text-decoration: underline;
29+
}
30+
</style>
31+
32+
<div id="attribution">
33+
Inspired by work of
34+
<a href="https://x.com/cerpow/status/1964953851603358112" target="_blank"
35+
>Voicu Apostol</a>
36+
</div>
37+
<canvas></canvas>

0 commit comments

Comments
 (0)