ugly-game 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ export interface Instance {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ scale: number;
6
+ material: number;
7
+ bounds: number;
8
+ lod?: number;
9
+ }
10
+ export type Plane = readonly [number, number, number, number];
11
+ export declare class GpuDrivenRenderer {
12
+ private device;
13
+ private format;
14
+ private instBuf;
15
+ private argsBuf;
16
+ private visBuf;
17
+ private camBuf;
18
+ private colorBuf;
19
+ private readBuf;
20
+ private cullPipe;
21
+ private drawPipe;
22
+ private cullBG;
23
+ private drawBG;
24
+ private count;
25
+ private pipelines;
26
+ private capPerPipe;
27
+ private lodCount;
28
+ private get buckets();
29
+ lastDrawCalls: number;
30
+ constructor(device: GPUDevice, format: GPUTextureFormat);
31
+ /** Upload a scene: instances + a color per pipeline (material). capPerPipe bounds the
32
+ * visible-per-pipeline compaction region. */
33
+ setScene(instances: Instance[], colors: [number, number, number][], capPerPipe: number, lodCount?: number): void;
34
+ /** One frame: reset args, compute-cull, then one indirect draw per pipeline. */
35
+ frame(encoder: GPUCommandEncoder, view: GPUTextureView, depth: GPUTextureView, viewProj: Float32Array, right: [number, number, number], up: [number, number, number], planes: Plane[]): void;
36
+ raw: number[];
37
+ /** Read the GPU-computed visible instanceCount per pipeline (the compute cull's result). */
38
+ readVisibleCounts(): Promise<number[]>;
39
+ }
@@ -0,0 +1,197 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // GPU-DRIVEN render backend (WebGPU) — executes the render IR's DrawPlan on the GPU.
3
+ // The whole scene lives in a storage buffer; a COMPUTE shader frustum-culls every
4
+ // instance and writes, per pipeline (material), an atomic instanceCount + a compacted
5
+ // list of visible instance indices; then ONE indirect draw per pipeline renders all
6
+ // its visible instances. CPU draw calls = O(pipelines), independent of instance count.
7
+ //
8
+ // This is the real backend for the render IR (shared/renderIR.ts is the CPU reference
9
+ // oracle it's validated against). Instances are billboarded quads for now — the shape
10
+ // is not the point; the draw-call model is. See design/moonshot-open-world.md §5.
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ const INSTANCE_F32 = 8; // pos.xyz, scale | material, bounds, pad, pad
13
+ const ARG_U32 = 4; // drawIndirect: vertexCount, instanceCount, firstVertex, firstInstance
14
+ const WGSL = /* wgsl */ `
15
+ struct Inst { pos: vec4<f32>, attr: vec4<f32> }; // pos.xyz + scale(w); attr.x=pipeline, attr.y=bounds, attr.z=lod
16
+ struct Args { vc: u32, ic: atomic<u32>, fv: u32, fi: u32 };
17
+ struct Cam {
18
+ viewProj: mat4x4<f32>,
19
+ right: vec4<f32>, up: vec4<f32>,
20
+ planes: array<vec4<f32>, 6>,
21
+ counts: vec4<f32>, // x=count, y=capPerPipe, z=pipelines, w=lodCount
22
+ };
23
+
24
+ @group(0) @binding(0) var<storage, read> instances: array<Inst>;
25
+ @group(0) @binding(1) var<storage, read_write> args: array<Args>;
26
+ @group(0) @binding(2) var<storage, read_write> visible: array<u32>;
27
+ @group(0) @binding(3) var<uniform> cam: Cam;
28
+
29
+
30
+ @compute @workgroup_size(64)
31
+ fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
32
+ let i = gid.x;
33
+ if (i >= u32(cam.counts.x)) { return; }
34
+ let inst = instances[i];
35
+ let c = inst.pos.xyz; let r = inst.attr.y;
36
+ var vis = true;
37
+ for (var p = 0u; p < 6u; p++) { let pl = cam.planes[p]; if (dot(pl.xyz, c) + pl.w < -r) { vis = false; } }
38
+ if (!vis) { return; }
39
+ // compact into per-(pipeline × lod) buckets — O(materials×lods) draws, LOD selected per instance.
40
+ let bucket = u32(inst.attr.x) * u32(cam.counts.w) + u32(inst.attr.z);
41
+ let slot = atomicAdd(&args[bucket].ic, 1u);
42
+ let cap = u32(cam.counts.y);
43
+ if (slot < cap) { visible[bucket * cap + slot] = i; }
44
+ }
45
+
46
+ // ── render (billboarded quad per visible instance) ──
47
+ struct RInst { pos: vec4<f32>, attr: vec4<f32> };
48
+ @group(0) @binding(0) var<storage, read> rinstances: array<RInst>;
49
+ @group(0) @binding(1) var<storage, read> rvisible: array<u32>;
50
+ @group(0) @binding(2) var<uniform> rcam: Cam;
51
+ @group(0) @binding(3) var<uniform> colors: array<vec4<f32>, 8>;
52
+
53
+ struct VOut { @builtin(position) pos: vec4<f32>, @location(0) color: vec3<f32> };
54
+ const QUAD = array<vec2<f32>, 6>(vec2(-1.,-1.), vec2(1.,-1.), vec2(1.,1.), vec2(-1.,-1.), vec2(1.,1.), vec2(-1.,1.));
55
+
56
+ @vertex
57
+ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
58
+ let idx = rvisible[ii];
59
+ let inst = rinstances[idx];
60
+ let q = QUAD[vi];
61
+ let world = inst.pos.xyz + (rcam.right.xyz * q.x + rcam.up.xyz * q.y) * inst.pos.w;
62
+ var o: VOut;
63
+ o.pos = rcam.viewProj * vec4(world, 1.0);
64
+ o.color = colors[u32(inst.attr.x)].rgb;
65
+ return o;
66
+ }
67
+ @fragment
68
+ fn fs(i: VOut) -> @location(0) vec4<f32> { return vec4(i.color, 1.0); }
69
+ `;
70
+ export class GpuDrivenRenderer {
71
+ device;
72
+ format;
73
+ instBuf;
74
+ argsBuf;
75
+ visBuf;
76
+ camBuf;
77
+ colorBuf;
78
+ readBuf;
79
+ cullPipe;
80
+ drawPipe;
81
+ cullBG;
82
+ drawBG;
83
+ count = 0;
84
+ pipelines = 1;
85
+ capPerPipe = 0;
86
+ lodCount = 1;
87
+ get buckets() { return this.pipelines * this.lodCount; } // per-(pipeline × lod) draw groups
88
+ lastDrawCalls = 0;
89
+ constructor(device, format) {
90
+ this.device = device;
91
+ this.format = format;
92
+ }
93
+ /** Upload a scene: instances + a color per pipeline (material). capPerPipe bounds the
94
+ * visible-per-pipeline compaction region. */
95
+ setScene(instances, colors, capPerPipe, lodCount = 1) {
96
+ const d = this.device;
97
+ this.count = instances.length;
98
+ this.pipelines = colors.length;
99
+ this.capPerPipe = capPerPipe;
100
+ this.lodCount = lodCount;
101
+ // instance storage (attr.z carries the per-instance LOD; defaults to 0 when unused)
102
+ const idata = new Float32Array(this.count * INSTANCE_F32);
103
+ for (let i = 0; i < this.count; i++) {
104
+ const o = i * INSTANCE_F32, it = instances[i];
105
+ idata[o] = it.x;
106
+ idata[o + 1] = it.y;
107
+ idata[o + 2] = it.z;
108
+ idata[o + 3] = it.scale;
109
+ idata[o + 4] = it.material;
110
+ idata[o + 5] = it.bounds;
111
+ idata[o + 6] = it.lod ?? 0;
112
+ }
113
+ this.instBuf = d.createBuffer({ size: idata.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
114
+ d.queue.writeBuffer(this.instBuf, 0, idata);
115
+ // per-bucket (pipeline × lod) indirect args + visible compaction buffer
116
+ this.argsBuf = d.createBuffer({ size: this.buckets * ARG_U32 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
117
+ this.visBuf = d.createBuffer({ size: this.buckets * capPerPipe * 4, usage: GPUBufferUsage.STORAGE });
118
+ this.readBuf = d.createBuffer({ size: this.buckets * ARG_U32 * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
119
+ // camera uniform (mat4 64 + right 16 + up 16 + planes 96 + counts 16 = 208)
120
+ this.camBuf = d.createBuffer({ size: 208, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
121
+ // colors uniform (8 × vec4)
122
+ const cdata = new Float32Array(8 * 4);
123
+ for (let p = 0; p < this.pipelines; p++) {
124
+ cdata[p * 4] = colors[p][0];
125
+ cdata[p * 4 + 1] = colors[p][1];
126
+ cdata[p * 4 + 2] = colors[p][2];
127
+ cdata[p * 4 + 3] = 1;
128
+ }
129
+ this.colorBuf = d.createBuffer({ size: cdata.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
130
+ d.queue.writeBuffer(this.colorBuf, 0, cdata);
131
+ const mod = d.createShaderModule({ code: WGSL });
132
+ this.cullPipe = d.createComputePipeline({ layout: 'auto', compute: { module: mod, entryPoint: 'cull' } });
133
+ this.drawPipe = d.createRenderPipeline({ layout: 'auto', vertex: { module: mod, entryPoint: 'vs' }, fragment: { module: mod, entryPoint: 'fs', targets: [{ format: this.format }] }, primitive: { topology: 'triangle-list' }, depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' } });
134
+ this.cullBG = d.createBindGroup({ layout: this.cullPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.instBuf } }, { binding: 1, resource: { buffer: this.argsBuf } }, { binding: 2, resource: { buffer: this.visBuf } }, { binding: 3, resource: { buffer: this.camBuf } }] });
135
+ this.drawBG = d.createBindGroup({ layout: this.drawPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.instBuf } }, { binding: 1, resource: { buffer: this.visBuf } }, { binding: 2, resource: { buffer: this.camBuf } }, { binding: 3, resource: { buffer: this.colorBuf } }] });
136
+ }
137
+ /** One frame: reset args, compute-cull, then one indirect draw per pipeline. */
138
+ frame(encoder, view, depth, viewProj, right, up, planes) {
139
+ // camera uniform
140
+ const cam = new Float32Array(52);
141
+ cam.set(viewProj, 0);
142
+ cam[16] = right[0];
143
+ cam[17] = right[1];
144
+ cam[18] = right[2];
145
+ cam[20] = up[0];
146
+ cam[21] = up[1];
147
+ cam[22] = up[2];
148
+ for (let p = 0; p < 6; p++) {
149
+ cam[24 + p * 4] = planes[p][0];
150
+ cam[25 + p * 4] = planes[p][1];
151
+ cam[26 + p * 4] = planes[p][2];
152
+ cam[27 + p * 4] = planes[p][3];
153
+ }
154
+ cam[48] = this.count;
155
+ cam[49] = this.capPerPipe;
156
+ cam[50] = this.pipelines;
157
+ cam[51] = this.lodCount;
158
+ this.device.queue.writeBuffer(this.camBuf, 0, cam);
159
+ // reset indirect args per bucket: [vertexCount=6, instanceCount=0, firstVertex=0, firstInstance=bucket*cap]
160
+ const args = new Uint32Array(this.buckets * ARG_U32);
161
+ for (let b = 0; b < this.buckets; b++) {
162
+ args[b * 4] = 6;
163
+ args[b * 4 + 1] = 0;
164
+ args[b * 4 + 2] = 0;
165
+ args[b * 4 + 3] = b * this.capPerPipe;
166
+ }
167
+ this.device.queue.writeBuffer(this.argsBuf, 0, args);
168
+ // compute cull
169
+ const cp = encoder.beginComputePass();
170
+ cp.setPipeline(this.cullPipe);
171
+ cp.setBindGroup(0, this.cullBG);
172
+ cp.dispatchWorkgroups(Math.ceil(this.count / 64));
173
+ cp.end();
174
+ // render — ONE indirect draw per pipeline
175
+ const rp = encoder.beginRenderPass({ colorAttachments: [{ view, clearValue: { r: 0.043, g: 0.075, b: 0.114, a: 1 }, loadOp: 'clear', storeOp: 'store' }], depthStencilAttachment: { view: depth, depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'store' } });
176
+ rp.setPipeline(this.drawPipe);
177
+ rp.setBindGroup(0, this.drawBG);
178
+ for (let b = 0; b < this.buckets; b++)
179
+ rp.drawIndirect(this.argsBuf, b * ARG_U32 * 4);
180
+ rp.end();
181
+ this.lastDrawCalls = this.buckets;
182
+ // copy args → readback
183
+ encoder.copyBufferToBuffer(this.argsBuf, 0, this.readBuf, 0, this.buckets * ARG_U32 * 4);
184
+ }
185
+ raw = [];
186
+ /** Read the GPU-computed visible instanceCount per pipeline (the compute cull's result). */
187
+ async readVisibleCounts() {
188
+ await this.readBuf.mapAsync(GPUMapMode.READ);
189
+ const a = new Uint32Array(this.readBuf.getMappedRange().slice(0));
190
+ this.readBuf.unmap();
191
+ this.raw = Array.from(a);
192
+ const out = [];
193
+ for (let b = 0; b < this.buckets; b++)
194
+ out.push(a[b * ARG_U32 + 1]); // visible count per (pipeline×lod) bucket
195
+ return out;
196
+ }
197
+ }
@@ -0,0 +1,39 @@
1
+ export interface GIBox {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ sx: number;
6
+ sy: number;
7
+ sz: number;
8
+ r: number;
9
+ g: number;
10
+ b: number;
11
+ }
12
+ export interface Grid {
13
+ ox: number;
14
+ oy: number;
15
+ oz: number;
16
+ cell: number;
17
+ nx: number;
18
+ ny: number;
19
+ nz: number;
20
+ }
21
+ /** Bake the probe grid: for each probe, cast rays; a surface hit contributes the hit box's
22
+ * sun-lit albedo (one bounce, colour bleed), a miss contributes sky. Project into 6 axes.
23
+ * Returns a Float32Array of `nx*ny*nz*6` vec4 (rgb + pad), axis-major within each probe. */
24
+ export declare function bakeProbes(boxes: GIBox[], g: Grid, rays?: number): Float32Array;
25
+ export declare class GpuGIRenderer {
26
+ private device;
27
+ private uBuf;
28
+ private u;
29
+ private instBuf;
30
+ private probeBuf;
31
+ private count;
32
+ private grid;
33
+ private pipe;
34
+ private bgl;
35
+ private bg;
36
+ constructor(device: GPUDevice, format: GPUTextureFormat);
37
+ setScene(boxes: GIBox[], probes: Float32Array, grid: Grid): void;
38
+ frame(enc: GPUCommandEncoder, colorView: GPUTextureView, depthView: GPUTextureView, camVP: Float32Array, giOn: boolean, flatAmbient: number): void;
39
+ }
package/dist/gpuGI.js ADDED
@@ -0,0 +1,253 @@
1
+ // Render increment 8 — BAKED GLOBAL ILLUMINATION via an irradiance probe grid (DDGI-style).
2
+ // The last of GAME.md §3.6's named look gaps. Real-time path tracing is out; the tractable form
3
+ // is to PRECOMPUTE indirect light into a 3D grid of probes, then sample it per-pixel. Each probe
4
+ // stores a 6-axis AMBIENT CUBE (irradiance arriving along ±X/±Y/±Z — the Half-Life 2 trick):
5
+ // cheap to store, captures direction, so a red box bleeds red onto its neighbours and occluded
6
+ // nooks that see little sky go dark (soft AO). One bounce, baked once on the CPU.
7
+ //
8
+ // bakeProbes() is the CPU bake (ray-cast the scene per probe → ambient cubes). GpuGIRenderer
9
+ // renders the boxes with direct sun + the probe-sampled indirect term. `?gi=0` on the page swaps
10
+ // the probe term for a flat ambient so the difference is unmistakable.
11
+ import { CUBE } from './gpuShadow';
12
+ const AXES = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
13
+ const SUN = [-0.4, -0.85, -0.32]; // direction light travels
14
+ const SUN_COL = [1.15, 1.05, 0.9];
15
+ function norm3(x, y, z) { const l = Math.hypot(x, y, z) || 1; return [x / l, y / l, z / l]; }
16
+ const SUN_N = norm3(SUN[0], SUN[1], SUN[2]);
17
+ /** Deterministic near-uniform sphere directions (Fibonacci). */
18
+ function sphereDirs(n) {
19
+ const out = [], ga = Math.PI * (3 - Math.sqrt(5));
20
+ for (let i = 0; i < n; i++) {
21
+ const y = 1 - ((i + 0.5) / n) * 2, r = Math.sqrt(Math.max(0, 1 - y * y)), th = ga * i;
22
+ out.push([Math.cos(th) * r, y, Math.sin(th) * r]);
23
+ }
24
+ return out;
25
+ }
26
+ /** Ray vs AABB (slab). Returns entry distance + entry-face normal, or null. */
27
+ function rayBox(ox, oy, oz, dx, dy, dz, b) {
28
+ const o = [ox, oy, oz], d = [dx, dy, dz], mn = [b.x - b.sx / 2, b.y - b.sy / 2, b.z - b.sz / 2], mx = [b.x + b.sx / 2, b.y + b.sy / 2, b.z + b.sz / 2];
29
+ let tmin = -Infinity, tmax = Infinity, axis = 0, sign = 1;
30
+ for (let a = 0; a < 3; a++) {
31
+ if (Math.abs(d[a]) < 1e-9) {
32
+ if (o[a] < mn[a] || o[a] > mx[a])
33
+ return null;
34
+ continue;
35
+ }
36
+ const inv = 1 / d[a];
37
+ let t1 = (mn[a] - o[a]) * inv, t2 = (mx[a] - o[a]) * inv, s = -1;
38
+ if (t1 > t2) {
39
+ const tmp = t1;
40
+ t1 = t2;
41
+ t2 = tmp;
42
+ s = 1;
43
+ }
44
+ if (t1 > tmin) {
45
+ tmin = t1;
46
+ axis = a;
47
+ sign = s;
48
+ }
49
+ if (t2 < tmax)
50
+ tmax = t2;
51
+ if (tmin > tmax)
52
+ return null;
53
+ }
54
+ if (tmax < 0)
55
+ return null;
56
+ const t = tmin > 0 ? tmin : tmax;
57
+ if (t < 0)
58
+ return null;
59
+ const n = [0, 0, 0];
60
+ n[axis] = sign;
61
+ return { t, n };
62
+ }
63
+ function skyColor(dy) {
64
+ if (dy < 0)
65
+ return [0.03, 0.03, 0.04]; // below horizon → almost no light
66
+ const t = dy; // horizon → zenith. Kept DIM on purpose:
67
+ return [0.16 + 0.08 * t, 0.19 + 0.11 * t, 0.24 + 0.16 * t]; // a bright sky washes out the colour bounce
68
+ }
69
+ /** Bake the probe grid: for each probe, cast rays; a surface hit contributes the hit box's
70
+ * sun-lit albedo (one bounce, colour bleed), a miss contributes sky. Project into 6 axes.
71
+ * Returns a Float32Array of `nx*ny*nz*6` vec4 (rgb + pad), axis-major within each probe. */
72
+ export function bakeProbes(boxes, g, rays = 32) {
73
+ const dirs = sphereDirs(rays);
74
+ const N = g.nx * g.ny * g.nz;
75
+ const out = new Float32Array(N * 6 * 4);
76
+ for (let iz = 0; iz < g.nz; iz++)
77
+ for (let iy = 0; iy < g.ny; iy++)
78
+ for (let ix = 0; ix < g.nx; ix++) {
79
+ const px = g.ox + ix * g.cell, py = g.oy + iy * g.cell, pz = g.oz + iz * g.cell;
80
+ const cube = new Float64Array(18), wsum = new Float64Array(6); // 6 axes × rgb
81
+ for (const [dx, dy, dz] of dirs) {
82
+ let bestT = Infinity, hit = null;
83
+ for (const b of boxes) {
84
+ const r = rayBox(px, py, pz, dx, dy, dz, b);
85
+ if (r && r.t > 0.02 && r.t < bestT) {
86
+ bestT = r.t;
87
+ hit = { b, n: r.n };
88
+ }
89
+ }
90
+ let cr, cg, cb;
91
+ if (hit) {
92
+ const sl = Math.max(-(hit.n[0] * SUN_N[0] + hit.n[1] * SUN_N[1] + hit.n[2] * SUN_N[2]), 0); // face lit by sun?
93
+ cr = hit.b.r * (0.16 + SUN_COL[0] * sl);
94
+ cg = hit.b.g * (0.16 + SUN_COL[1] * sl);
95
+ cb = hit.b.b * (0.16 + SUN_COL[2] * sl);
96
+ }
97
+ else {
98
+ const s = skyColor(dy);
99
+ cr = s[0];
100
+ cg = s[1];
101
+ cb = s[2];
102
+ }
103
+ for (let a = 0; a < 6; a++) {
104
+ const ax = AXES[a];
105
+ const w = Math.max(dx * ax[0] + dy * ax[1] + dz * ax[2], 0);
106
+ if (w <= 0)
107
+ continue;
108
+ wsum[a] += w;
109
+ cube[a * 3] += cr * w;
110
+ cube[a * 3 + 1] += cg * w;
111
+ cube[a * 3 + 2] += cb * w;
112
+ }
113
+ }
114
+ const pi = (iz * g.ny + iy) * g.nx + ix;
115
+ for (let a = 0; a < 6; a++) {
116
+ const wv = Math.max(wsum[a], 1e-4), o = (pi * 6 + a) * 4;
117
+ out[o] = cube[a * 3] / wv;
118
+ out[o + 1] = cube[a * 3 + 1] / wv;
119
+ out[o + 2] = cube[a * 3 + 2] / wv;
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ const SCENE_WGSL = /* wgsl */ `
125
+ struct Inst { pos: vec4<f32>, scl: vec4<f32>, col: vec4<f32> };
126
+ struct GU {
127
+ camVP: mat4x4<f32>,
128
+ sun: vec4<f32>, // xyz dir, w = giOn
129
+ originCell: vec4<f32>, // xyz grid origin, w = cell size
130
+ dims: vec4<f32>, // xyz grid dims, w = flat ambient (gi off)
131
+ };
132
+ @group(0) @binding(0) var<uniform> gu: GU;
133
+ @group(0) @binding(1) var<storage, read> insts: array<Inst>;
134
+ @group(0) @binding(2) var<storage, read> probes: array<vec4<f32>>;
135
+ const CUBE_POS = array<vec3<f32>, 36>(${CUBE.pos});
136
+ const CUBE_NRM = array<vec3<f32>, 36>(${CUBE.nrm});
137
+
138
+ struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) col: vec3<f32> };
139
+ @vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
140
+ let it = insts[ii];
141
+ let world = it.pos.xyz + CUBE_POS[vi] * it.scl.xyz;
142
+ var o: VOut;
143
+ o.clip = gu.camVP * vec4(world, 1.0);
144
+ o.world = world; o.nrm = normalize(CUBE_NRM[vi] / it.scl.xyz); o.col = it.col.rgb;
145
+ return o;
146
+ }
147
+ fn probeAxis(pi: u32, a: u32) -> vec3<f32> { return probes[pi * 6u + a].rgb; }
148
+ // Trilinear-sample the probe grid, then reconstruct irradiance for normal N (ambient cube).
149
+ fn giAt(world: vec3<f32>, N: vec3<f32>) -> vec3<f32> {
150
+ let dims = gu.dims.xyz;
151
+ let g = (world - gu.originCell.xyz) / gu.originCell.w;
152
+ let gc = clamp(floor(g), vec3(0.0), dims - 1.0);
153
+ let f = clamp(g - gc, vec3(0.0), vec3(1.0));
154
+ var c0 = vec3(0.0); var c1 = vec3(0.0); var c2 = vec3(0.0); var c3 = vec3(0.0); var c4 = vec3(0.0); var c5 = vec3(0.0);
155
+ for (var k = 0u; k < 8u; k = k + 1u) {
156
+ let ox = f32(k & 1u); let oy = f32((k >> 1u) & 1u); let oz = f32((k >> 2u) & 1u);
157
+ let ix = clamp(gc.x + ox, 0.0, dims.x - 1.0); let iy = clamp(gc.y + oy, 0.0, dims.y - 1.0); let iz = clamp(gc.z + oz, 0.0, dims.z - 1.0);
158
+ let w = mix(1.0 - f.x, f.x, ox) * mix(1.0 - f.y, f.y, oy) * mix(1.0 - f.z, f.z, oz);
159
+ let pi = u32((iz * dims.y + iy) * dims.x + ix);
160
+ c0 = c0 + w * probeAxis(pi, 0u); c1 = c1 + w * probeAxis(pi, 1u); c2 = c2 + w * probeAxis(pi, 2u);
161
+ c3 = c3 + w * probeAxis(pi, 3u); c4 = c4 + w * probeAxis(pi, 4u); c5 = c5 + w * probeAxis(pi, 5u);
162
+ }
163
+ let n2 = N * N;
164
+ let cx = select(c1, c0, N.x > 0.0); let cy = select(c3, c2, N.y > 0.0); let cz = select(c5, c4, N.z > 0.0);
165
+ return n2.x * cx + n2.y * cy + n2.z * cz;
166
+ }
167
+ @fragment fn fs(v: VOut) -> @location(0) vec4<f32> {
168
+ let N = normalize(v.nrm);
169
+ let sl = max(dot(N, -normalize(gu.sun.xyz)), 0.0);
170
+ let direct = v.col * (sl * vec3(0.85, 0.8, 0.7)); // dimmer direct so indirect bleed reads
171
+ var indirect = v.col * gu.dims.w; // flat ambient (gi off)
172
+ if (gu.sun.w > 0.5) { indirect = v.col * giAt(v.world, N); } // probe-baked indirect (gi on)
173
+ return vec4(pow(clamp(direct + indirect, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0);
174
+ }
175
+ `;
176
+ export class GpuGIRenderer {
177
+ device;
178
+ uBuf;
179
+ u = new Float32Array(28); // mat4 + 3 vec4
180
+ instBuf;
181
+ probeBuf;
182
+ count = 0;
183
+ grid;
184
+ pipe;
185
+ bgl;
186
+ bg;
187
+ constructor(device, format) {
188
+ this.device = device;
189
+ this.uBuf = device.createBuffer({ size: this.u.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
190
+ const mod = device.createShaderModule({ code: SCENE_WGSL });
191
+ this.bgl = device.createBindGroupLayout({ entries: [
192
+ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
193
+ { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
194
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } }
195
+ ] });
196
+ this.pipe = device.createRenderPipeline({
197
+ layout: device.createPipelineLayout({ bindGroupLayouts: [this.bgl] }),
198
+ vertex: { module: mod, entryPoint: 'vs' },
199
+ fragment: { module: mod, entryPoint: 'fs', targets: [{ format }] },
200
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
201
+ depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' }
202
+ });
203
+ }
204
+ setScene(boxes, probes, grid) {
205
+ this.grid = grid;
206
+ this.count = boxes.length;
207
+ const data = new Float32Array(this.count * 12);
208
+ for (let i = 0; i < this.count; i++) {
209
+ const b = boxes[i], o = i * 12;
210
+ data[o] = b.x;
211
+ data[o + 1] = b.y;
212
+ data[o + 2] = b.z;
213
+ data[o + 4] = b.sx;
214
+ data[o + 5] = b.sy;
215
+ data[o + 6] = b.sz;
216
+ data[o + 8] = b.r;
217
+ data[o + 9] = b.g;
218
+ data[o + 10] = b.b;
219
+ }
220
+ this.instBuf = this.device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
221
+ this.device.queue.writeBuffer(this.instBuf, 0, data);
222
+ this.probeBuf = this.device.createBuffer({ size: probes.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
223
+ this.device.queue.writeBuffer(this.probeBuf, 0, new Float32Array(probes)); // ArrayBuffer-backed copy (one-time)
224
+ this.bg = this.device.createBindGroup({ layout: this.bgl, entries: [
225
+ { binding: 0, resource: { buffer: this.uBuf } }, { binding: 1, resource: { buffer: this.instBuf } }, { binding: 2, resource: { buffer: this.probeBuf } }
226
+ ] });
227
+ }
228
+ frame(enc, colorView, depthView, camVP, giOn, flatAmbient) {
229
+ const g = this.grid;
230
+ this.u.set(camVP, 0);
231
+ this.u[16] = SUN[0];
232
+ this.u[17] = SUN[1];
233
+ this.u[18] = SUN[2];
234
+ this.u[19] = giOn ? 1 : 0;
235
+ this.u[20] = g.ox;
236
+ this.u[21] = g.oy;
237
+ this.u[22] = g.oz;
238
+ this.u[23] = g.cell;
239
+ this.u[24] = g.nx;
240
+ this.u[25] = g.ny;
241
+ this.u[26] = g.nz;
242
+ this.u[27] = flatAmbient;
243
+ this.device.queue.writeBuffer(this.uBuf, 0, this.u);
244
+ const rp = enc.beginRenderPass({
245
+ colorAttachments: [{ view: colorView, clearValue: { r: 0.03, g: 0.04, b: 0.06, a: 1 }, loadOp: 'clear', storeOp: 'store' }],
246
+ depthStencilAttachment: { view: depthView, depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store' }
247
+ });
248
+ rp.setPipeline(this.pipe);
249
+ rp.setBindGroup(0, this.bg);
250
+ rp.draw(36, this.count);
251
+ rp.end();
252
+ }
253
+ }
@@ -0,0 +1,28 @@
1
+ import { type Graph } from './shaderGraph';
2
+ export interface GraphInstance {
3
+ x: number;
4
+ y: number;
5
+ z: number;
6
+ sx: number;
7
+ sy: number;
8
+ sz: number;
9
+ }
10
+ export declare class GpuGraphRenderer {
11
+ private device;
12
+ private uBuf;
13
+ private u;
14
+ private samp;
15
+ private texView;
16
+ private bgl;
17
+ private pipes;
18
+ private groups;
19
+ /** Per-graph compiled WGSL — exposed so the demo can show it's generated, not hand-written. */
20
+ readonly compiled: string[];
21
+ constructor(device: GPUDevice, format: GPUTextureFormat, graphs: Graph[], tex: GPUTexture);
22
+ /** groups: each is (which graph pipeline, the instances shaded by it). */
23
+ setGroups(groups: {
24
+ graph: number;
25
+ instances: GraphInstance[];
26
+ }[]): void;
27
+ frame(enc: GPUCommandEncoder, msaaView: GPUTextureView, resolveView: GPUTextureView, depthView: GPUTextureView, camVP: Float32Array, eye: [number, number, number], sunDir: [number, number, number], time: number): void;
28
+ }