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,154 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // GPU TERRAIN — a smooth lit triangle-mesh renderer for the world surface (PLATFORM), replacing the
3
+ // boxy instanced-cube look. Draws an indexed mesh (per-vertex position / normal / colour) with a sun
4
+ // lambert term + ambient + distance fog + a translucent animated water plane. It BOTH casts (shadowPass:
5
+ // its depth into the shared sun shadow map) and RECEIVES sun shadows (PCF sample in the fragment), so
6
+ // hills and props darken the ground they occlude — which also makes the sun's direction legible.
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ import { SHADOW_WGSL } from './gpuShadowMap';
9
+ const TERRAIN = /* wgsl */ `
10
+ struct U { mvp: mat4x4<f32>, sun: vec4<f32>, sunCol: vec4<f32>, amb: vec4<f32>, cam: vec4<f32>, fog: vec4<f32>, lightVP: mat4x4<f32>, shadowP: vec4<f32> };
11
+ @group(0) @binding(0) var<uniform> u: U;
12
+ @group(0) @binding(1) var shadowMap: texture_depth_2d;
13
+ @group(0) @binding(2) var shadowSamp: sampler_comparison;
14
+ struct VO { @builtin(position) pos: vec4<f32>, @location(0) world: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec3<f32> };
15
+ @vertex fn vs(@location(0) p: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) col: vec3<f32>) -> VO {
16
+ var o: VO; o.pos = u.mvp * vec4(p, 1.0); o.world = p; o.n = nrm; o.c = col; return o;
17
+ }
18
+ ` + SHADOW_WGSL + /* wgsl */ `
19
+ @fragment fn fs(i: VO) -> @location(0) vec4<f32> {
20
+ let d = clamp(dot(normalize(i.n), -normalize(u.sun.xyz)), 0.0, 1.0);
21
+ let sh = shadowFactor(u.lightVP, i.world, d, u.shadowP.x, shadowMap, shadowSamp);
22
+ let lit = i.c * (u.amb.xyz + d * sh * u.sunCol.xyz);
23
+ let dist = length(i.world - u.cam.xyz);
24
+ let f = clamp(1.0 - exp(-dist * u.fog.w), 0.0, 1.0);
25
+ return vec4(mix(lit, u.fog.xyz, f), 1.0);
26
+ }`;
27
+ // Depth-only occluder pass: terrain depth from the sun's ortho view.
28
+ const OCCLUDER = /* wgsl */ `
29
+ @group(0) @binding(0) var<uniform> lightVP: mat4x4<f32>;
30
+ @vertex fn vs(@location(0) p: vec3<f32>) -> @builtin(position) vec4<f32> { return lightVP * vec4(p, 1.0); }
31
+ `;
32
+ // cam.w carries a render clock (seconds) so the surface animates; ripples perturb the up-normal, driving
33
+ // fresnel sky reflection (more reflective at grazing angles) + a sharp sun glint — a real water look.
34
+ const WATER = /* wgsl */ `
35
+ struct U { mvp: mat4x4<f32>, sun: vec4<f32>, sunCol: vec4<f32>, amb: vec4<f32>, cam: vec4<f32>, fog: vec4<f32> };
36
+ @group(0) @binding(0) var<uniform> u: U;
37
+ struct VO { @builtin(position) pos: vec4<f32>, @location(0) world: vec3<f32> };
38
+ @vertex fn vs(@location(0) p: vec3<f32>) -> VO { var o: VO; o.pos = u.mvp * vec4(p, 1.0); o.world = p; return o; }
39
+ @fragment fn fs(i: VO) -> @location(0) vec4<f32> {
40
+ let t = u.cam.w;
41
+ let view = normalize(u.cam.xyz - i.world);
42
+ let w1 = sin(i.world.x * 0.33 + t * 1.1) + sin(i.world.z * 0.27 - t * 0.9);
43
+ let w2 = sin(i.world.x * 0.12 - t * 0.55) + sin(i.world.z * 0.17 + t * 0.7);
44
+ let n = normalize(vec3(w1 * 0.05, 1.0, w2 * 0.05));
45
+ let fres = pow(clamp(1.0 - max(dot(view, n), 0.0), 0.0, 1.0), 4.0);
46
+ let deep = vec3(0.03, 0.13, 0.24);
47
+ let shallow = vec3(0.08, 0.30, 0.40);
48
+ var col = mix(shallow, deep, 0.55);
49
+ col = mix(col, u.fog.xyz, fres * 0.6);
50
+ let refl = reflect(normalize(u.sun.xyz), n);
51
+ let spec = pow(max(dot(view, refl), 0.0), 64.0);
52
+ col += u.sunCol.xyz * spec * 0.9;
53
+ let dist = length(i.world - u.cam.xyz);
54
+ let f = clamp(1.0 - exp(-dist * u.fog.w), 0.0, 1.0);
55
+ col = mix(col, u.fog.xyz, f);
56
+ return vec4(col, 0.80);
57
+ }`;
58
+ const U_FLOATS = 56; // mvp(16)+sun+sunCol+amb+cam+fog(20)+lightVP(16)+shadowP(4)
59
+ export class GpuTerrainRenderer {
60
+ device;
61
+ terrainPipe;
62
+ waterPipe;
63
+ occPipe;
64
+ uBuf;
65
+ uBG;
66
+ uBGw;
67
+ lightBuf;
68
+ occBG;
69
+ posB = null;
70
+ nrmB = null;
71
+ colB = null;
72
+ idxB = null;
73
+ idxCount = 0;
74
+ waterB;
75
+ texel;
76
+ constructor(device, format, shadow) {
77
+ this.device = device;
78
+ this.texel = shadow.texel;
79
+ const tm = device.createShaderModule({ code: TERRAIN });
80
+ const vbuf = [
81
+ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] },
82
+ { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x3' }] },
83
+ { arrayStride: 12, attributes: [{ shaderLocation: 2, offset: 0, format: 'float32x3' }] },
84
+ ];
85
+ this.terrainPipe = device.createRenderPipeline({ layout: 'auto', vertex: { module: tm, entryPoint: 'vs', buffers: vbuf }, fragment: { module: tm, entryPoint: 'fs', targets: [{ format }] }, primitive: { topology: 'triangle-list', cullMode: 'none' }, depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' } });
86
+ const wm = device.createShaderModule({ code: WATER });
87
+ this.waterPipe = device.createRenderPipeline({ layout: 'auto', vertex: { module: wm, entryPoint: 'vs', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }] }, fragment: { module: wm, entryPoint: 'fs', targets: [{ format, blend: { color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha' }, alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' } } }] }, primitive: { topology: 'triangle-list', cullMode: 'none' }, depthStencil: { format: 'depth24plus', depthWriteEnabled: false, depthCompare: 'less' } });
88
+ const om = device.createShaderModule({ code: OCCLUDER });
89
+ this.occPipe = device.createRenderPipeline({ layout: 'auto', vertex: { module: om, entryPoint: 'vs', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }] }, primitive: { topology: 'triangle-list', cullMode: 'none' }, depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less' } });
90
+ this.uBuf = device.createBuffer({ size: U_FLOATS * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
91
+ this.uBG = device.createBindGroup({ layout: this.terrainPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.uBuf } }, { binding: 1, resource: shadow.view }, { binding: 2, resource: shadow.sampler }] });
92
+ this.uBGw = device.createBindGroup({ layout: this.waterPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.uBuf } }] });
93
+ this.lightBuf = device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
94
+ this.occBG = device.createBindGroup({ layout: this.occPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.lightBuf } }] });
95
+ this.waterB = device.createBuffer({ size: 6 * 3 * 4, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
96
+ }
97
+ /** Replace the terrain mesh (positions/normals/colors are Nx3, indices triangle list). */
98
+ setTerrain(positions, normals, colors, indices) {
99
+ const dev = this.device, U = GPUBufferUsage;
100
+ this.posB?.destroy();
101
+ this.nrmB?.destroy();
102
+ this.colB?.destroy();
103
+ this.idxB?.destroy();
104
+ const mk = (d, u) => { const b = dev.createBuffer({ size: d.byteLength, usage: u }); dev.queue.writeBuffer(b, 0, d); return b; };
105
+ this.posB = mk(positions, U.VERTEX | U.COPY_DST);
106
+ this.nrmB = mk(normals, U.VERTEX | U.COPY_DST);
107
+ this.colB = mk(colors, U.VERTEX | U.COPY_DST);
108
+ this.idxB = mk(indices, U.INDEX | U.COPY_DST);
109
+ this.idxCount = indices.length;
110
+ }
111
+ /** Occluder pass: draw the terrain depth into the shared shadow map from the sun's ortho view. */
112
+ shadowPass(pass, lightVP) {
113
+ if (!this.idxB || !this.posB)
114
+ return;
115
+ this.device.queue.writeBuffer(this.lightBuf, 0, lightVP);
116
+ pass.setPipeline(this.occPipe);
117
+ pass.setBindGroup(0, this.occBG);
118
+ pass.setVertexBuffer(0, this.posB);
119
+ pass.setIndexBuffer(this.idxB, 'uint32');
120
+ pass.drawIndexed(this.idxCount);
121
+ }
122
+ frame(enc, colorView, depthView, camVP, sunDir, sunCol, amb, camPos, fog, fogDensity, sea, extent, center, lightVP, time = 0, load = false) {
123
+ const u = new Float32Array(U_FLOATS);
124
+ u.set(camVP, 0);
125
+ u.set([sunDir[0], sunDir[1], sunDir[2], 0], 16);
126
+ u.set([sunCol[0], sunCol[1], sunCol[2], 0], 20);
127
+ u.set([amb[0], amb[1], amb[2], 0], 24);
128
+ u.set([camPos[0], camPos[1], camPos[2], time], 28);
129
+ u.set([fog[0], fog[1], fog[2], fogDensity], 32);
130
+ u.set(lightVP, 36);
131
+ u.set([this.texel, 0, 0, 0], 52);
132
+ this.device.queue.writeBuffer(this.uBuf, 0, u);
133
+ const [cx, cz] = center, e = extent;
134
+ this.device.queue.writeBuffer(this.waterB, 0, new Float32Array([cx - e, sea, cz - e, cx + e, sea, cz - e, cx + e, sea, cz + e, cx - e, sea, cz - e, cx + e, sea, cz + e, cx - e, sea, cz + e]));
135
+ const rp = enc.beginRenderPass({
136
+ colorAttachments: [{ view: colorView, ...(load ? { loadOp: 'load' } : { clearValue: { r: fog[0], g: fog[1], b: fog[2], a: 1 }, loadOp: 'clear' }), storeOp: 'store' }],
137
+ depthStencilAttachment: { view: depthView, ...(load ? { depthLoadOp: 'load' } : { depthClearValue: 1, depthLoadOp: 'clear' }), depthStoreOp: 'store' },
138
+ });
139
+ if (this.idxB && this.posB && this.nrmB && this.colB) {
140
+ rp.setPipeline(this.terrainPipe);
141
+ rp.setBindGroup(0, this.uBG);
142
+ rp.setVertexBuffer(0, this.posB);
143
+ rp.setVertexBuffer(1, this.nrmB);
144
+ rp.setVertexBuffer(2, this.colB);
145
+ rp.setIndexBuffer(this.idxB, 'uint32');
146
+ rp.drawIndexed(this.idxCount);
147
+ }
148
+ rp.setPipeline(this.waterPipe);
149
+ rp.setBindGroup(0, this.uBGw);
150
+ rp.setVertexBuffer(0, this.waterB);
151
+ rp.draw(6);
152
+ rp.end();
153
+ }
154
+ }
@@ -0,0 +1,55 @@
1
+ export interface Instance {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ scale: number;
6
+ material: number;
7
+ bounds: number;
8
+ }
9
+ export interface Light {
10
+ x: number;
11
+ y: number;
12
+ z: number;
13
+ radius: number;
14
+ r: number;
15
+ g: number;
16
+ b: number;
17
+ }
18
+ export type Plane = readonly [number, number, number, number];
19
+ export declare const TILE = 24, MAX_PER_TILE = 256;
20
+ export declare class GpuTiledRenderer {
21
+ private device;
22
+ private format;
23
+ private instBuf;
24
+ private argsBuf;
25
+ private visBuf;
26
+ private camBuf;
27
+ private lightBuf;
28
+ private tileCountBuf;
29
+ private tileListBuf;
30
+ private readBuf;
31
+ private cullPipe;
32
+ private lightPipe;
33
+ private drawPipe;
34
+ private cullBG;
35
+ private lightBG;
36
+ private drawBG;
37
+ private count;
38
+ private capPerPipe;
39
+ private lightCount;
40
+ ntx: number;
41
+ nty: number;
42
+ numTiles: number;
43
+ constructor(device: GPUDevice, format: GPUTextureFormat);
44
+ setScene(instances: Instance[], capPerPipe: number, maxLights: number, w: number, h: number): void;
45
+ setLights(lights: Light[]): void;
46
+ frame(encoder: GPUCommandEncoder, view: GPUTextureView, depth: GPUTextureView, viewProj: Float32Array, right: number[], up: number[], fwd: number[], planes: Plane[], sun: number[], sunCol: number[], ambient: number, w: number, h: number): void;
47
+ /** Queue a copy of the tile-count buffer for readback (call on measure frames only). */
48
+ copyTileCounts(encoder: GPUCommandEncoder): void;
49
+ /** Read tile light counts → { avg, max } over non-empty tiles (proves O(lights-per-tile)). */
50
+ readTileStats(): Promise<{
51
+ avg: number;
52
+ max: number;
53
+ nonEmpty: number;
54
+ }>;
55
+ }
@@ -0,0 +1,247 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Tiled (forward+) light culling — scales the many-dynamic-lights renderer from
3
+ // hundreds to THOUSANDS of lights. A COMPUTE pass bins each light into the screen
4
+ // tiles its projected sphere overlaps; the fragment shader then shades only its
5
+ // tile's lights → O(lights-per-tile) instead of O(all-lights). Same story as the
6
+ // instance brute→grid cull, applied to lights. Builds on the GPU-driven lit backend
7
+ // (shared/gpuLit). See design/moonshot-open-world.md §5 (phase 4), GAME.md §3.6.
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ const INSTANCE_F32 = 8, ARG_U32 = 4, LIGHT_F32 = 8, CAM_F32 = 72;
10
+ export const TILE = 24, MAX_PER_TILE = 256;
11
+ const WGSL = /* wgsl */ `
12
+ struct Inst { pos: vec4<f32>, attr: vec4<f32> };
13
+ struct Args { vc: u32, ic: atomic<u32>, fv: u32, fi: u32 };
14
+ struct Light { posr: vec4<f32>, color: vec4<f32> };
15
+ struct Cam {
16
+ viewProj: mat4x4<f32>,
17
+ right: vec4<f32>, up: vec4<f32>, fwd: vec4<f32>,
18
+ planes: array<vec4<f32>, 6>,
19
+ counts: vec4<f32>, // count, capPerPipe, pipelines, lightCount
20
+ sun: vec4<f32>, sunCol: vec4<f32>,
21
+ tile: vec4<f32>, // tileSize, numTilesX, numTilesY, maxPerTile
22
+ screen: vec4<f32>, // w, h
23
+ };
24
+
25
+ // ── instance frustum cull → per-pipeline indirect args ──
26
+ @group(0) @binding(0) var<storage, read> instances: array<Inst>;
27
+ @group(0) @binding(1) var<storage, read_write> args: array<Args>;
28
+ @group(0) @binding(2) var<storage, read_write> visible: array<u32>;
29
+ @group(0) @binding(3) var<uniform> cam: Cam;
30
+ @compute @workgroup_size(64)
31
+ fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
32
+ let i = gid.x; if (i >= u32(cam.counts.x)) { return; }
33
+ let inst = instances[i]; let c = inst.pos.xyz; let r = inst.attr.y;
34
+ for (var p = 0u; p < 6u; p++) { let pl = cam.planes[p]; if (dot(pl.xyz, c) + pl.w < -r) { return; } }
35
+ let pipe = u32(inst.attr.x); let slot = atomicAdd(&args[pipe].ic, 1u); let cap = u32(cam.counts.y);
36
+ if (slot < cap) { visible[pipe * cap + slot] = i; }
37
+ }
38
+
39
+ // ── tiled light culling: bin each light into the screen tiles it overlaps ──
40
+ @group(0) @binding(0) var<storage, read> lc_lights: array<Light>;
41
+ @group(0) @binding(1) var<storage, read_write> tileCount: array<atomic<u32>>;
42
+ @group(0) @binding(2) var<storage, read_write> tileList: array<u32>;
43
+ @group(0) @binding(3) var<uniform> lc_cam: Cam;
44
+ @compute @workgroup_size(64)
45
+ fn cullLights(@builtin(global_invocation_id) gid: vec3<u32>) {
46
+ let li = gid.x; if (li >= u32(lc_cam.counts.w)) { return; }
47
+ let L = lc_lights[li];
48
+ let clip = lc_cam.viewProj * vec4(L.posr.xyz, 1.0);
49
+ if (clip.w <= 0.0) { return; } // behind camera (approx)
50
+ let inv = 1.0 / clip.w;
51
+ let sx = (clip.x * inv * 0.5 + 0.5) * lc_cam.screen.x;
52
+ let sy = (0.5 - clip.y * inv * 0.5) * lc_cam.screen.y;
53
+ let clipR = lc_cam.viewProj * vec4(L.posr.xyz + lc_cam.right.xyz * L.posr.w, 1.0);
54
+ let srx = (clipR.x / clipR.w * 0.5 + 0.5) * lc_cam.screen.x;
55
+ let sr = max(abs(srx - sx), 2.0); // screen-space radius (px)
56
+ let ts = lc_cam.tile.x; let ntx = i32(lc_cam.tile.y); let nty = i32(lc_cam.tile.z); let mpt = u32(lc_cam.tile.w);
57
+ let minx = clamp(i32((sx - sr) / ts), 0, ntx - 1); let maxx = clamp(i32((sx + sr) / ts), 0, ntx - 1);
58
+ let miny = clamp(i32((sy - sr) / ts), 0, nty - 1); let maxy = clamp(i32((sy + sr) / ts), 0, nty - 1);
59
+ for (var ty = miny; ty <= maxy; ty++) { for (var tx = minx; tx <= maxx; tx++) {
60
+ let tile = u32(ty * ntx + tx); let slot = atomicAdd(&tileCount[tile], 1u);
61
+ if (slot < mpt) { tileList[tile * mpt + slot] = li; }
62
+ } }
63
+ }
64
+
65
+ // ── lit render: impostor spheres shaded by sun + the pixel's TILE lights ──
66
+ @group(0) @binding(0) var<storage, read> rinstances: array<Inst>;
67
+ @group(0) @binding(1) var<storage, read> rvisible: array<u32>;
68
+ @group(0) @binding(2) var<uniform> rcam: Cam;
69
+ @group(0) @binding(3) var<storage, read> rlights: array<Light>;
70
+ @group(0) @binding(4) var<storage, read> rTileCount: array<u32>;
71
+ @group(0) @binding(5) var<storage, read> rTileList: array<u32>;
72
+
73
+ struct VOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>, @location(1) center: vec3<f32>, @location(2) scale: f32 };
74
+ const QUAD = array<vec2<f32>, 6>(vec2(-1.,-1.), vec2(1.,-1.), vec2(1.,1.), vec2(-1.,-1.), vec2(1.,1.), vec2(-1.,1.));
75
+ @vertex
76
+ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
77
+ let inst = rinstances[rvisible[ii]]; let q = QUAD[vi];
78
+ let world = inst.pos.xyz + (rcam.right.xyz * q.x + rcam.up.xyz * q.y) * inst.pos.w;
79
+ var o: VOut; o.pos = rcam.viewProj * vec4(world, 1.0); o.uv = q; o.center = inst.pos.xyz; o.scale = inst.pos.w; return o;
80
+ }
81
+ @fragment
82
+ fn fs(v: VOut) -> @location(0) vec4<f32> {
83
+ let r2 = dot(v.uv, v.uv); if (r2 > 1.0) { discard; }
84
+ let nz = sqrt(1.0 - r2);
85
+ let N = normalize(rcam.right.xyz * v.uv.x + rcam.up.xyz * v.uv.y - rcam.fwd.xyz * nz);
86
+ let wp = v.center + N * v.scale;
87
+ let base = vec3<f32>(0.5, 0.52, 0.58);
88
+ var lit = base * rcam.sunCol.a + base * rcam.sunCol.rgb * max(dot(N, -rcam.sun.xyz), 0.0);
89
+ let ntx = u32(rcam.tile.y); let mpt = u32(rcam.tile.w);
90
+ let tx = min(u32(v.pos.x / rcam.tile.x), ntx - 1u); let ty = min(u32(v.pos.y / rcam.tile.x), u32(rcam.tile.z) - 1u);
91
+ let tile = ty * ntx + tx; let cnt = min(rTileCount[tile], mpt);
92
+ for (var j = 0u; j < cnt; j++) {
93
+ let L = rlights[rTileList[tile * mpt + j]];
94
+ let d3 = L.posr.xyz - wp; let d = length(d3); let rad = L.posr.w;
95
+ if (d < rad) { let a = 1.0 - d / rad; lit += base * L.color.rgb * max(dot(N, d3 / d), 0.0) * a * a; }
96
+ }
97
+ return vec4(lit, 1.0);
98
+ }
99
+ `;
100
+ export class GpuTiledRenderer {
101
+ device;
102
+ format;
103
+ instBuf;
104
+ argsBuf;
105
+ visBuf;
106
+ camBuf;
107
+ lightBuf;
108
+ tileCountBuf;
109
+ tileListBuf;
110
+ readBuf;
111
+ cullPipe;
112
+ lightPipe;
113
+ drawPipe;
114
+ cullBG;
115
+ lightBG;
116
+ drawBG;
117
+ count = 0;
118
+ capPerPipe = 0;
119
+ lightCount = 0;
120
+ ntx = 0;
121
+ nty = 0;
122
+ numTiles = 0;
123
+ constructor(device, format) {
124
+ this.device = device;
125
+ this.format = format;
126
+ }
127
+ setScene(instances, capPerPipe, maxLights, w, h) {
128
+ const d = this.device;
129
+ this.count = instances.length;
130
+ this.capPerPipe = capPerPipe;
131
+ this.ntx = Math.ceil(w / TILE);
132
+ this.nty = Math.ceil(h / TILE);
133
+ this.numTiles = this.ntx * this.nty;
134
+ const idata = new Float32Array(this.count * INSTANCE_F32);
135
+ for (let i = 0; i < this.count; i++) {
136
+ const o = i * INSTANCE_F32, it = instances[i];
137
+ idata[o] = it.x;
138
+ idata[o + 1] = it.y;
139
+ idata[o + 2] = it.z;
140
+ idata[o + 3] = it.scale;
141
+ idata[o + 4] = it.material;
142
+ idata[o + 5] = it.bounds;
143
+ }
144
+ this.instBuf = d.createBuffer({ size: idata.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
145
+ d.queue.writeBuffer(this.instBuf, 0, idata);
146
+ this.argsBuf = d.createBuffer({ size: ARG_U32 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST });
147
+ this.visBuf = d.createBuffer({ size: capPerPipe * 4, usage: GPUBufferUsage.STORAGE });
148
+ this.camBuf = d.createBuffer({ size: CAM_F32 * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
149
+ this.lightBuf = d.createBuffer({ size: maxLights * LIGHT_F32 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
150
+ this.tileCountBuf = d.createBuffer({ size: this.numTiles * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
151
+ this.tileListBuf = d.createBuffer({ size: this.numTiles * MAX_PER_TILE * 4, usage: GPUBufferUsage.STORAGE });
152
+ this.readBuf = d.createBuffer({ size: this.numTiles * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
153
+ const mod = d.createShaderModule({ code: WGSL });
154
+ this.cullPipe = d.createComputePipeline({ layout: 'auto', compute: { module: mod, entryPoint: 'cull' } });
155
+ this.lightPipe = d.createComputePipeline({ layout: 'auto', compute: { module: mod, entryPoint: 'cullLights' } });
156
+ 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' } });
157
+ 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 } }] });
158
+ this.lightBG = d.createBindGroup({ layout: this.lightPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.lightBuf } }, { binding: 1, resource: { buffer: this.tileCountBuf } }, { binding: 2, resource: { buffer: this.tileListBuf } }, { binding: 3, resource: { buffer: this.camBuf } }] });
159
+ 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.lightBuf } }, { binding: 4, resource: { buffer: this.tileCountBuf } }, { binding: 5, resource: { buffer: this.tileListBuf } }] });
160
+ }
161
+ setLights(lights) {
162
+ this.lightCount = lights.length;
163
+ const data = new Float32Array(lights.length * LIGHT_F32);
164
+ for (let i = 0; i < lights.length; i++) {
165
+ const o = i * LIGHT_F32, L = lights[i];
166
+ data[o] = L.x;
167
+ data[o + 1] = L.y;
168
+ data[o + 2] = L.z;
169
+ data[o + 3] = L.radius;
170
+ data[o + 4] = L.r;
171
+ data[o + 5] = L.g;
172
+ data[o + 6] = L.b;
173
+ }
174
+ this.device.queue.writeBuffer(this.lightBuf, 0, data);
175
+ }
176
+ frame(encoder, view, depth, viewProj, right, up, fwd, planes, sun, sunCol, ambient, w, h) {
177
+ const cam = new Float32Array(CAM_F32);
178
+ cam.set(viewProj, 0);
179
+ cam[16] = right[0];
180
+ cam[17] = right[1];
181
+ cam[18] = right[2];
182
+ cam[20] = up[0];
183
+ cam[21] = up[1];
184
+ cam[22] = up[2];
185
+ cam[24] = fwd[0];
186
+ cam[25] = fwd[1];
187
+ cam[26] = fwd[2];
188
+ for (let p = 0; p < 6; p++) {
189
+ cam[28 + p * 4] = planes[p][0];
190
+ cam[29 + p * 4] = planes[p][1];
191
+ cam[30 + p * 4] = planes[p][2];
192
+ cam[31 + p * 4] = planes[p][3];
193
+ }
194
+ cam[52] = this.count;
195
+ cam[53] = this.capPerPipe;
196
+ cam[54] = 1;
197
+ cam[55] = this.lightCount;
198
+ cam[56] = sun[0];
199
+ cam[57] = sun[1];
200
+ cam[58] = sun[2];
201
+ cam[60] = sunCol[0];
202
+ cam[61] = sunCol[1];
203
+ cam[62] = sunCol[2];
204
+ cam[63] = ambient;
205
+ cam[64] = TILE;
206
+ cam[65] = this.ntx;
207
+ cam[66] = this.nty;
208
+ cam[67] = MAX_PER_TILE;
209
+ cam[68] = w;
210
+ cam[69] = h;
211
+ this.device.queue.writeBuffer(this.camBuf, 0, cam);
212
+ this.device.queue.writeBuffer(this.argsBuf, 0, new Uint32Array([6, 0, 0, 0]));
213
+ this.device.queue.writeBuffer(this.tileCountBuf, 0, new Uint32Array(this.numTiles)); // reset tile counts to 0
214
+ const cp = encoder.beginComputePass();
215
+ cp.setPipeline(this.cullPipe);
216
+ cp.setBindGroup(0, this.cullBG);
217
+ cp.dispatchWorkgroups(Math.ceil(this.count / 64));
218
+ cp.setPipeline(this.lightPipe);
219
+ cp.setBindGroup(0, this.lightBG);
220
+ cp.dispatchWorkgroups(Math.ceil(this.lightCount / 64));
221
+ cp.end();
222
+ const rp = encoder.beginRenderPass({ colorAttachments: [{ view, clearValue: { r: 0.02, g: 0.03, b: 0.05, a: 1 }, loadOp: 'clear', storeOp: 'store' }], depthStencilAttachment: { view: depth, depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'store' } });
223
+ rp.setPipeline(this.drawPipe);
224
+ rp.setBindGroup(0, this.drawBG);
225
+ rp.drawIndirect(this.argsBuf, 0);
226
+ rp.end();
227
+ }
228
+ /** Queue a copy of the tile-count buffer for readback (call on measure frames only). */
229
+ copyTileCounts(encoder) { encoder.copyBufferToBuffer(this.tileCountBuf, 0, this.readBuf, 0, this.numTiles * 4); }
230
+ /** Read tile light counts → { avg, max } over non-empty tiles (proves O(lights-per-tile)). */
231
+ async readTileStats() {
232
+ await this.readBuf.mapAsync(GPUMapMode.READ);
233
+ const a = new Uint32Array(this.readBuf.getMappedRange().slice(0));
234
+ this.readBuf.unmap();
235
+ let sum = 0, max = 0, ne = 0;
236
+ for (let i = 0; i < this.numTiles; i++) {
237
+ const c = Math.min(a[i], MAX_PER_TILE);
238
+ if (c > 0) {
239
+ sum += c;
240
+ ne++;
241
+ if (c > max)
242
+ max = c;
243
+ }
244
+ }
245
+ return { avg: ne ? sum / ne : 0, max, nonEmpty: ne };
246
+ }
247
+ }
@@ -0,0 +1,49 @@
1
+ import type { CubeInstance } from './gpuShadow';
2
+ /** A dynamic instance that also knows where it was last frame (for its motion vector). */
3
+ export interface WorldInstance extends CubeInstance {
4
+ px: number;
5
+ py: number;
6
+ pz: number;
7
+ }
8
+ export declare class GpuWorldRenderer {
9
+ private device;
10
+ private w;
11
+ private h;
12
+ private instBuf;
13
+ private scratch;
14
+ private count;
15
+ private uBuf;
16
+ private rBuf;
17
+ private u;
18
+ private r;
19
+ private lightScratch;
20
+ private lightBuf;
21
+ private shadowTex;
22
+ private shadowView;
23
+ private shadowSamp;
24
+ private histSamp;
25
+ private shadowPipe;
26
+ private scenePipe;
27
+ private resolvePipe;
28
+ private shadowBGL;
29
+ private sceneBGL;
30
+ private resolveBGL;
31
+ private shadowBG;
32
+ private sceneBG;
33
+ private colorTex?;
34
+ private colorView;
35
+ private motionTex?;
36
+ private motionView;
37
+ private depthTex?;
38
+ private depthView;
39
+ private hist;
40
+ private histView;
41
+ private resolveBG;
42
+ private ping;
43
+ constructor(device: GPUDevice, format: GPUTextureFormat);
44
+ resize(w: number, h: number): void;
45
+ setCapacity(cap: number): void;
46
+ updateInstances(list: WorldInstance[]): void;
47
+ /** currVP/prevVP/lightVP: Float32Array(16). jitter: clip sub-pixel. */
48
+ frame(enc: GPUCommandEncoder, swapView: GPUTextureView, currVP: Float32Array, prevVP: Float32Array, lightVP: Float32Array, jitter: [number, number], sunDir: [number, number, number], ambient: number, taaOn: boolean, blend: number): void;
49
+ }