ugly-game 0.2.0 → 0.4.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.
Files changed (67) hide show
  1. package/dist/assets/anim.d.ts +13 -0
  2. package/dist/assets/anim.js +55 -0
  3. package/dist/assets/animMoat.d.ts +36 -0
  4. package/dist/assets/animMoat.js +55 -0
  5. package/dist/assets/archetype.d.ts +20 -0
  6. package/dist/assets/archetype.js +8 -0
  7. package/dist/assets/assemble.d.ts +26 -0
  8. package/dist/assets/assemble.js +92 -0
  9. package/dist/assets/genome.d.ts +33 -0
  10. package/dist/assets/genome.js +55 -0
  11. package/dist/assets/genomeExplore.d.ts +28 -0
  12. package/dist/assets/genomeExplore.js +100 -0
  13. package/dist/assets/geometry.d.ts +23 -0
  14. package/dist/assets/geometry.js +134 -0
  15. package/dist/assets/gpuSkin.d.ts +21 -0
  16. package/dist/assets/gpuSkin.js +148 -0
  17. package/dist/assets/lod.d.ts +2 -0
  18. package/dist/assets/lod.js +18 -0
  19. package/dist/assets/meshlet.d.ts +23 -0
  20. package/dist/assets/meshlet.js +53 -0
  21. package/dist/assets/morph.d.ts +5 -0
  22. package/dist/assets/morph.js +75 -0
  23. package/dist/assets/partkit.d.ts +28 -0
  24. package/dist/assets/partkit.js +21 -0
  25. package/dist/assets/pose.d.ts +26 -0
  26. package/dist/assets/pose.js +208 -0
  27. package/dist/assets/renderAssets.d.ts +37 -0
  28. package/dist/assets/renderAssets.js +54 -0
  29. package/dist/assets/residency.d.ts +13 -0
  30. package/dist/assets/residency.js +45 -0
  31. package/dist/assets/retarget.d.ts +11 -0
  32. package/dist/assets/retarget.js +37 -0
  33. package/dist/assets/rig.d.ts +13 -0
  34. package/dist/assets/rig.js +93 -0
  35. package/dist/assets/select.d.ts +10 -0
  36. package/dist/assets/select.js +24 -0
  37. package/dist/assets/stream.d.ts +12 -0
  38. package/dist/assets/stream.js +32 -0
  39. package/dist/assets/style.d.ts +10 -0
  40. package/dist/assets/style.js +17 -0
  41. package/dist/assets/ugm.d.ts +53 -0
  42. package/dist/assets/ugm.js +100 -0
  43. package/dist/gpuDriven.d.ts +39 -0
  44. package/dist/gpuDriven.js +197 -0
  45. package/dist/gpuGI.d.ts +39 -0
  46. package/dist/gpuGI.js +253 -0
  47. package/dist/gpuGraph.d.ts +28 -0
  48. package/dist/gpuGraph.js +128 -0
  49. package/dist/gpuLit.d.ts +40 -0
  50. package/dist/gpuLit.js +193 -0
  51. package/dist/gpuPost.d.ts +33 -0
  52. package/dist/gpuPost.js +162 -0
  53. package/dist/gpuShadowMap.d.ts +11 -0
  54. package/dist/gpuShadowMap.js +41 -0
  55. package/dist/gpuSpot.d.ts +39 -0
  56. package/dist/gpuSpot.js +213 -0
  57. package/dist/gpuTaa.d.ts +34 -0
  58. package/dist/gpuTaa.js +247 -0
  59. package/dist/gpuTerrain.d.ts +25 -0
  60. package/dist/gpuTerrain.js +154 -0
  61. package/dist/gpuTiled.d.ts +55 -0
  62. package/dist/gpuTiled.js +247 -0
  63. package/dist/gpuWorld.d.ts +49 -0
  64. package/dist/gpuWorld.js +309 -0
  65. package/dist/shaderGraph.d.ts +84 -0
  66. package/dist/shaderGraph.js +231 -0
  67. package/package.json +2 -1
@@ -0,0 +1,53 @@
1
+ export declare enum ChunkKind {
2
+ VTX = 1,
3
+ IDX = 2,
4
+ CLUSTER = 3,
5
+ MIP = 4,
6
+ MAT = 5,
7
+ SKEL = 6,
8
+ ANIM = 7,
9
+ MORPH = 8,
10
+ UV = 9,
11
+ KTX2 = 10
12
+ }
13
+ export interface ChunkIn {
14
+ kind: ChunkKind;
15
+ detailLevel: number;
16
+ targetId: number;
17
+ base: boolean;
18
+ bytes: Uint8Array;
19
+ }
20
+ export interface UgmMeta {
21
+ formatVersion: number;
22
+ renderIRVersion: number;
23
+ flags: number;
24
+ bounds: [number, number, number, number];
25
+ }
26
+ export declare const MAGIC = 843925333;
27
+ export declare const HEADER_BYTES = 64;
28
+ export declare const TOC_ENTRY_BYTES = 24;
29
+ /** fnv1a-32 over raw bytes → u32 content hash (deterministic; enables chunk dedup).
30
+ * Byte-wise (no String.fromCharCode spread — that overflows the stack on large chunks). */
31
+ export declare function hashBytes(b: Uint8Array): number;
32
+ export declare function packUgm(chunks: ChunkIn[], meta: UgmMeta): Uint8Array;
33
+ export interface TocEntry {
34
+ kind: ChunkKind;
35
+ detailLevel: number;
36
+ targetId: number;
37
+ offset: number;
38
+ length: number;
39
+ hash: number;
40
+ }
41
+ export interface UgmHeader {
42
+ formatVersion: number;
43
+ renderIRVersion: number;
44
+ flags: number;
45
+ bounds: [number, number, number, number];
46
+ chunkCount: number;
47
+ tocOffset: number;
48
+ basePayloadLen: number;
49
+ totalLen: number;
50
+ }
51
+ export declare function readHeader(buf: Uint8Array): UgmHeader;
52
+ export declare function readToc(buf: Uint8Array, h: UgmHeader): TocEntry[];
53
+ export declare function readChunk(buf: Uint8Array, e: TocEntry): Uint8Array;
@@ -0,0 +1,100 @@
1
+ // The cooked .ugm v2 container — streamable by construction (design/asset-streaming.md §2).
2
+ // Layout (little-endian): [64B header][TOC: chunkCount × 24B][chunk bytes …].
3
+ // Chunks are sorted base-first then ascending detailLevel, so a leading byte range
4
+ // (0 .. basePayloadLen) is a complete coarse-renderable asset.
5
+ export var ChunkKind;
6
+ (function (ChunkKind) {
7
+ ChunkKind[ChunkKind["VTX"] = 1] = "VTX";
8
+ ChunkKind[ChunkKind["IDX"] = 2] = "IDX";
9
+ ChunkKind[ChunkKind["CLUSTER"] = 3] = "CLUSTER";
10
+ ChunkKind[ChunkKind["MIP"] = 4] = "MIP";
11
+ ChunkKind[ChunkKind["MAT"] = 5] = "MAT";
12
+ ChunkKind[ChunkKind["SKEL"] = 6] = "SKEL";
13
+ ChunkKind[ChunkKind["ANIM"] = 7] = "ANIM";
14
+ ChunkKind[ChunkKind["MORPH"] = 8] = "MORPH";
15
+ ChunkKind[ChunkKind["UV"] = 9] = "UV";
16
+ ChunkKind[ChunkKind["KTX2"] = 10] = "KTX2";
17
+ })(ChunkKind || (ChunkKind = {}));
18
+ export const MAGIC = 0x324d4755; // "UGM2" little-endian
19
+ export const HEADER_BYTES = 64;
20
+ export const TOC_ENTRY_BYTES = 24; // kind1 detail1 targetId2 offset4 length4 hash4 depMask4 pad4
21
+ /** fnv1a-32 over raw bytes → u32 content hash (deterministic; enables chunk dedup).
22
+ * Byte-wise (no String.fromCharCode spread — that overflows the stack on large chunks). */
23
+ export function hashBytes(b) {
24
+ let h = 0x811c9dc5;
25
+ for (const byte of b) {
26
+ h ^= byte;
27
+ h = Math.imul(h, 0x01000193);
28
+ }
29
+ return h >>> 0;
30
+ }
31
+ export function packUgm(chunks, meta) {
32
+ // base-first, then ascending detail — this ordering IS the coarse→fine stream order.
33
+ const sorted = [...chunks].sort((a, b) => (Number(b.base) - Number(a.base)) || (a.detailLevel - b.detailLevel) || (a.kind - b.kind) || (a.targetId - b.targetId));
34
+ const tocBytes = sorted.length * TOC_ENTRY_BYTES;
35
+ const dataStart = HEADER_BYTES + tocBytes;
36
+ let cursor = dataStart, baseEnd = dataStart;
37
+ const offsets = [];
38
+ for (const c of sorted) {
39
+ offsets.push(cursor);
40
+ cursor += c.bytes.byteLength;
41
+ if (c.base)
42
+ baseEnd = cursor;
43
+ }
44
+ const total = cursor;
45
+ const out = new Uint8Array(total);
46
+ const dv = new DataView(out.buffer);
47
+ // header
48
+ dv.setUint32(0, MAGIC, true);
49
+ dv.setUint16(4, meta.formatVersion, true);
50
+ dv.setUint16(6, meta.renderIRVersion, true);
51
+ dv.setUint32(8, meta.flags, true);
52
+ dv.setFloat32(12, meta.bounds[0], true);
53
+ dv.setFloat32(16, meta.bounds[1], true);
54
+ dv.setUint32(20, sorted.length, true); // chunkCount
55
+ dv.setUint32(24, HEADER_BYTES, true); // tocOffset
56
+ dv.setUint32(28, baseEnd, true); // basePayloadLen
57
+ dv.setUint32(32, total, true); // totalLen
58
+ dv.setFloat32(36, meta.bounds[2], true);
59
+ dv.setFloat32(40, meta.bounds[3], true);
60
+ // TOC
61
+ let t = HEADER_BYTES;
62
+ for (let i = 0; i < sorted.length; i++) {
63
+ const c = sorted[i];
64
+ dv.setUint8(t, c.kind);
65
+ dv.setUint8(t + 1, c.detailLevel);
66
+ dv.setUint16(t + 2, c.targetId, true);
67
+ dv.setUint32(t + 4, offsets[i], true);
68
+ dv.setUint32(t + 8, c.bytes.byteLength, true);
69
+ dv.setUint32(t + 12, hashBytes(c.bytes), true);
70
+ dv.setUint32(t + 16, 0, true); // depMask reserved
71
+ t += TOC_ENTRY_BYTES;
72
+ }
73
+ // chunk bytes
74
+ for (let i = 0; i < sorted.length; i++)
75
+ out.set(sorted[i].bytes, offsets[i]);
76
+ return out;
77
+ }
78
+ function dvOf(buf) { return new DataView(buf.buffer, buf.byteOffset, buf.byteLength); }
79
+ export function readHeader(buf) {
80
+ const dv = dvOf(buf);
81
+ if (dv.getUint32(0, true) !== MAGIC)
82
+ throw new Error('.ugm: bad magic');
83
+ return {
84
+ formatVersion: dv.getUint16(4, true), renderIRVersion: dv.getUint16(6, true), flags: dv.getUint32(8, true),
85
+ bounds: [dv.getFloat32(12, true), dv.getFloat32(16, true), dv.getFloat32(36, true), dv.getFloat32(40, true)],
86
+ chunkCount: dv.getUint32(20, true), tocOffset: dv.getUint32(24, true),
87
+ basePayloadLen: dv.getUint32(28, true), totalLen: dv.getUint32(32, true),
88
+ };
89
+ }
90
+ export function readToc(buf, h) {
91
+ const dv = dvOf(buf);
92
+ const out = [];
93
+ for (let i = 0; i < h.chunkCount; i++) {
94
+ const t = h.tocOffset + i * TOC_ENTRY_BYTES;
95
+ out.push({ kind: dv.getUint8(t), detailLevel: dv.getUint8(t + 1), targetId: dv.getUint16(t + 2, true),
96
+ offset: dv.getUint32(t + 4, true), length: dv.getUint32(t + 8, true), hash: dv.getUint32(t + 12, true) });
97
+ }
98
+ return out;
99
+ }
100
+ export function readChunk(buf, e) { return buf.subarray(e.offset, e.offset + e.length); }
@@ -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
+ }