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,128 @@
1
+ // Renders each shader GRAPH as its own pipeline: compileGraph() emits a `material()` WGSL fn that
2
+ // is spliced into a shared scene template (cube instances + Cook-Torrance PBR lighting). So the
3
+ // LIGHTING is engine-owned and identical; only the artist-authored material() differs per graph —
4
+ // exactly how a production shader-graph pipeline works. No hand-written per-material WGSL.
5
+ import { CUBE } from './gpuShadow';
6
+ import { compileGraph } from './shaderGraph';
7
+ function sceneWGSL(materialFn) {
8
+ return /* wgsl */ `
9
+ struct Inst { pos: vec4<f32>, scl: vec4<f32> };
10
+ struct U { camVP: mat4x4<f32>, eye: vec4<f32>, sun: vec4<f32>, misc: vec4<f32> }; // misc.x = time
11
+ @group(0) @binding(0) var<uniform> u: U;
12
+ @group(0) @binding(1) var<storage, read> insts: array<Inst>;
13
+ @group(0) @binding(2) var gsamp: sampler;
14
+ @group(0) @binding(3) var gtex: texture_2d_array<f32>;
15
+ const CUBE_POS = array<vec3<f32>, 36>(${CUBE.pos});
16
+ const CUBE_NRM = array<vec3<f32>, 36>(${CUBE.nrm});
17
+
18
+ struct Surface { albedo: vec3<f32>, rough: f32, metal: f32, emit: vec3<f32> };
19
+ fn hash21(p: vec2<f32>) -> f32 { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
20
+ fn gnoise(uv: vec2<f32>) -> f32 {
21
+ let i = floor(uv); let f = fract(uv); let w = f * f * (3.0 - 2.0 * f);
22
+ let a = hash21(i); let b = hash21(i + vec2(1.0, 0.0)); let c = hash21(i + vec2(0.0, 1.0)); let d = hash21(i + vec2(1.0, 1.0));
23
+ return mix(mix(a, b, w.x), mix(c, d, w.x), w.y);
24
+ }
25
+ fn gchecker(uv: vec2<f32>) -> f32 { return f32((i32(floor(uv.x)) + i32(floor(uv.y))) & 1); }
26
+
27
+ ${materialFn}
28
+
29
+ struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) uv: vec2<f32> };
30
+ @vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
31
+ let it = insts[ii]; let lp = CUBE_POS[vi]; let n = CUBE_NRM[vi];
32
+ var uv = lp.xy; if (abs(n.x) > 0.5) { uv = lp.zy; } else if (abs(n.y) > 0.5) { uv = lp.xz; }
33
+ var o: VOut; let world = it.pos.xyz + lp * it.scl.xyz;
34
+ o.clip = u.camVP * vec4(world, 1.0); o.world = world; o.nrm = normalize(n / it.scl.xyz); o.uv = (uv + 0.5) * 2.0;
35
+ return o;
36
+ }
37
+ const PI = 3.14159265;
38
+ fn ggx(nh: f32, r: f32) -> f32 { let a = r * r; let d = nh * nh * (a * a - 1.0) + 1.0; return a * a / max(PI * d * d, 1e-5); }
39
+ fn gs(nv: f32, nl: f32, r: f32) -> f32 { let k = (r + 1.0) * (r + 1.0) / 8.0; return (nv / (nv * (1.0 - k) + k)) * (nl / (nl * (1.0 - k) + k)); }
40
+ fn fres(ct: f32, f0: vec3<f32>) -> vec3<f32> { return f0 + (vec3(1.0) - f0) * pow(1.0 - ct, 5.0); }
41
+ fn aces(x: vec3<f32>) -> vec3<f32> { return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), vec3(0.0), vec3(1.0)); }
42
+
43
+ @fragment fn fs(v: VOut) -> @location(0) vec4<f32> {
44
+ let N = normalize(v.nrm); let V = normalize(u.eye.xyz - v.world);
45
+ let s = material(v.uv, N, V, u.misc.x); // ← artist-authored, compiled from the graph
46
+ let L = -normalize(u.sun.xyz); let H = normalize(V + L);
47
+ let nl = max(dot(N, L), 0.0); let nv = max(dot(N, V), 0.0);
48
+ let f0 = mix(vec3(0.04), s.albedo, s.metal);
49
+ let D = ggx(max(dot(N, H), 0.0), s.rough); let G = gs(nv, nl, s.rough); let F = fres(max(dot(H, V), 0.0), f0);
50
+ let spec = (D * G) * F / max(4.0 * nv * nl, 1e-4);
51
+ let kd = (vec3(1.0) - F) * (1.0 - s.metal);
52
+ let lit = (kd * s.albedo / PI + spec) * vec3(1.3, 1.2, 1.05) * nl + s.albedo * (0.11 + 0.06 * N.y) + s.emit;
53
+ return vec4(pow(aces(lit), vec3(0.4545)), 1.0);
54
+ }`;
55
+ }
56
+ export class GpuGraphRenderer {
57
+ device;
58
+ uBuf;
59
+ u = new Float32Array(28);
60
+ samp;
61
+ texView;
62
+ bgl;
63
+ pipes = [];
64
+ groups = [];
65
+ /** Per-graph compiled WGSL — exposed so the demo can show it's generated, not hand-written. */
66
+ compiled;
67
+ constructor(device, format, graphs, tex) {
68
+ this.device = device;
69
+ this.uBuf = device.createBuffer({ size: this.u.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
70
+ this.samp = device.createSampler({ magFilter: 'linear', minFilter: 'linear', mipmapFilter: 'linear', addressModeU: 'repeat', addressModeV: 'repeat' });
71
+ this.texView = tex.createView({ dimension: '2d-array' });
72
+ this.bgl = device.createBindGroupLayout({ entries: [
73
+ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
74
+ { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
75
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
76
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { viewDimension: '2d-array' } }
77
+ ] });
78
+ const layout = device.createPipelineLayout({ bindGroupLayouts: [this.bgl] });
79
+ this.compiled = graphs.map((g) => compileGraph(g));
80
+ this.pipes = this.compiled.map((mat) => {
81
+ const mod = device.createShaderModule({ code: sceneWGSL(mat) });
82
+ return device.createRenderPipeline({ layout, vertex: { module: mod, entryPoint: 'vs' }, fragment: { module: mod, entryPoint: 'fs', targets: [{ format }] },
83
+ primitive: { topology: 'triangle-list', cullMode: 'none' }, depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' }, multisample: { count: 4 } });
84
+ });
85
+ }
86
+ /** groups: each is (which graph pipeline, the instances shaded by it). */
87
+ setGroups(groups) {
88
+ this.groups = groups.map((grp) => {
89
+ const data = new Float32Array(grp.instances.length * 8);
90
+ for (let i = 0; i < grp.instances.length; i++) {
91
+ const c = grp.instances[i], o = i * 8;
92
+ data[o] = c.x;
93
+ data[o + 1] = c.y;
94
+ data[o + 2] = c.z;
95
+ data[o + 4] = c.sx;
96
+ data[o + 5] = c.sy;
97
+ data[o + 6] = c.sz;
98
+ }
99
+ const buf = this.device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
100
+ this.device.queue.writeBuffer(buf, 0, data);
101
+ const bg = this.device.createBindGroup({ layout: this.bgl, entries: [
102
+ { binding: 0, resource: { buffer: this.uBuf } }, { binding: 1, resource: { buffer: buf } }, { binding: 2, resource: this.samp }, { binding: 3, resource: this.texView }
103
+ ] });
104
+ return { pipe: grp.graph, buf, bg, count: grp.instances.length };
105
+ });
106
+ }
107
+ frame(enc, msaaView, resolveView, depthView, camVP, eye, sunDir, time) {
108
+ this.u.set(camVP, 0);
109
+ this.u[16] = eye[0];
110
+ this.u[17] = eye[1];
111
+ this.u[18] = eye[2];
112
+ this.u[20] = sunDir[0];
113
+ this.u[21] = sunDir[1];
114
+ this.u[22] = sunDir[2];
115
+ this.u[24] = time;
116
+ this.device.queue.writeBuffer(this.uBuf, 0, this.u);
117
+ const rp = enc.beginRenderPass({
118
+ colorAttachments: [{ view: msaaView, resolveTarget: resolveView, clearValue: { r: 0.05, g: 0.07, b: 0.11, a: 1 }, loadOp: 'clear', storeOp: 'store' }],
119
+ depthStencilAttachment: { view: depthView, depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store' }
120
+ });
121
+ for (const g of this.groups) {
122
+ rp.setPipeline(this.pipes[g.pipe]);
123
+ rp.setBindGroup(0, g.bg);
124
+ rp.draw(36, g.count);
125
+ }
126
+ rp.end();
127
+ }
128
+ }
@@ -0,0 +1,40 @@
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 class GpuLitRenderer {
20
+ private device;
21
+ private format;
22
+ private instBuf;
23
+ private argsBuf;
24
+ private visBuf;
25
+ private camBuf;
26
+ private lightBuf;
27
+ private cullPipe;
28
+ private drawPipe;
29
+ private cullBG;
30
+ private drawBG;
31
+ private count;
32
+ private pipelines;
33
+ private capPerPipe;
34
+ private lightCount;
35
+ lastDrawCalls: number;
36
+ constructor(device: GPUDevice, format: GPUTextureFormat);
37
+ setScene(instances: Instance[], pipelines: number, capPerPipe: number, maxLights: number): void;
38
+ setLights(lights: Light[]): void;
39
+ frame(encoder: GPUCommandEncoder, view: GPUTextureView, depth: GPUTextureView, viewProj: Float32Array, right: number[], up: number[], fwd: number[], planes: Plane[], sun: number[], sunCol: number[], ambient: number): void;
40
+ }
package/dist/gpuLit.js ADDED
@@ -0,0 +1,193 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // GPU-driven LIT renderer — the shading program begins here (GAME.md §3.6). Builds
3
+ // on the proven cull backend (compute frustum-cull → per-pipeline indirect draw) and
4
+ // adds real lighting: instances render as lit impostor SPHERES (a billboard quad whose
5
+ // fragment reconstructs a sphere normal), shaded by a directional sun + a buffer of
6
+ // MANY dynamic POINT LIGHTS. This is the many-dynamic-lights look the open-world axis
7
+ // needs; a later increment adds tiled/clustered light culling to scale the light count.
8
+ // See design/moonshot-open-world.md §5 (phase 4), design/engine-build-plan.md.
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ const INSTANCE_F32 = 8, ARG_U32 = 4, LIGHT_F32 = 8, CAM_F32 = 64;
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> }; // xyz + radius; rgb
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>, // sun dir xyz
21
+ sunCol: vec4<f32>, // rgb, ambient(a)
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
+ @compute @workgroup_size(64)
30
+ fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
31
+ let i = gid.x;
32
+ if (i >= u32(cam.counts.x)) { return; }
33
+ let inst = instances[i];
34
+ let c = inst.pos.xyz; let r = inst.attr.y;
35
+ for (var p = 0u; p < 6u; p++) { let pl = cam.planes[p]; if (dot(pl.xyz, c) + pl.w < -r) { return; } }
36
+ let pipe = u32(inst.attr.x);
37
+ let slot = atomicAdd(&args[pipe].ic, 1u);
38
+ let cap = u32(cam.counts.y);
39
+ if (slot < cap) { visible[pipe * cap + slot] = i; }
40
+ }
41
+
42
+ // ── lit render: impostor spheres shaded by sun + point lights ──
43
+ @group(0) @binding(0) var<storage, read> rinstances: array<Inst>;
44
+ @group(0) @binding(1) var<storage, read> rvisible: array<u32>;
45
+ @group(0) @binding(2) var<uniform> rcam: Cam;
46
+ @group(0) @binding(3) var<storage, read> lights: array<Light>;
47
+
48
+ struct VOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>, @location(1) center: vec3<f32>, @location(2) scale: f32 };
49
+ const QUAD = array<vec2<f32>, 6>(vec2(-1.,-1.), vec2(1.,-1.), vec2(1.,1.), vec2(-1.,-1.), vec2(1.,1.), vec2(-1.,1.));
50
+
51
+ @vertex
52
+ fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
53
+ let inst = rinstances[rvisible[ii]];
54
+ let q = QUAD[vi];
55
+ let world = inst.pos.xyz + (rcam.right.xyz * q.x + rcam.up.xyz * q.y) * inst.pos.w;
56
+ var o: VOut;
57
+ o.pos = rcam.viewProj * vec4(world, 1.0);
58
+ o.uv = q; o.center = inst.pos.xyz; o.scale = inst.pos.w;
59
+ return o;
60
+ }
61
+ @fragment
62
+ fn fs(v: VOut) -> @location(0) vec4<f32> {
63
+ let r2 = dot(v.uv, v.uv);
64
+ if (r2 > 1.0) { discard; }
65
+ let nz = sqrt(1.0 - r2);
66
+ let N = normalize(rcam.right.xyz * v.uv.x + rcam.up.xyz * v.uv.y - rcam.fwd.xyz * nz); // sphere normal toward camera
67
+ let wp = v.center + N * v.scale; // approx surface point
68
+ let base = vec3<f32>(0.55, 0.57, 0.62); // neutral albedo so light colors show
69
+ var lit = base * rcam.sunCol.a; // ambient
70
+ lit += base * rcam.sunCol.rgb * max(dot(N, -rcam.sun.xyz), 0.0); // sun
71
+ let n = u32(rcam.counts.w);
72
+ for (var i = 0u; i < n; i++) {
73
+ let L = lights[i].posr.xyz - wp; let d = length(L); let rad = lights[i].posr.w;
74
+ if (d < rad) { let a = 1.0 - d / rad; lit += base * lights[i].color.rgb * max(dot(N, L / d), 0.0) * a * a; }
75
+ }
76
+ return vec4(lit, 1.0);
77
+ }
78
+ `;
79
+ export class GpuLitRenderer {
80
+ device;
81
+ format;
82
+ instBuf;
83
+ argsBuf;
84
+ visBuf;
85
+ camBuf;
86
+ lightBuf;
87
+ cullPipe;
88
+ drawPipe;
89
+ cullBG;
90
+ drawBG;
91
+ count = 0;
92
+ pipelines = 1;
93
+ capPerPipe = 0;
94
+ lightCount = 0;
95
+ lastDrawCalls = 0;
96
+ constructor(device, format) {
97
+ this.device = device;
98
+ this.format = format;
99
+ }
100
+ setScene(instances, pipelines, capPerPipe, maxLights) {
101
+ const d = this.device;
102
+ this.count = instances.length;
103
+ this.pipelines = pipelines;
104
+ this.capPerPipe = capPerPipe;
105
+ const idata = new Float32Array(this.count * INSTANCE_F32);
106
+ for (let i = 0; i < this.count; i++) {
107
+ const o = i * INSTANCE_F32, it = instances[i];
108
+ idata[o] = it.x;
109
+ idata[o + 1] = it.y;
110
+ idata[o + 2] = it.z;
111
+ idata[o + 3] = it.scale;
112
+ idata[o + 4] = it.material;
113
+ idata[o + 5] = it.bounds;
114
+ }
115
+ this.instBuf = d.createBuffer({ size: idata.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
116
+ d.queue.writeBuffer(this.instBuf, 0, idata);
117
+ this.argsBuf = d.createBuffer({ size: pipelines * ARG_U32 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST });
118
+ this.visBuf = d.createBuffer({ size: pipelines * capPerPipe * 4, usage: GPUBufferUsage.STORAGE });
119
+ this.camBuf = d.createBuffer({ size: CAM_F32 * 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
120
+ this.lightBuf = d.createBuffer({ size: maxLights * LIGHT_F32 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
121
+ const mod = d.createShaderModule({ code: WGSL });
122
+ this.cullPipe = d.createComputePipeline({ layout: 'auto', compute: { module: mod, entryPoint: 'cull' } });
123
+ 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' } });
124
+ 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 } }] });
125
+ 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 } }] });
126
+ }
127
+ setLights(lights) {
128
+ this.lightCount = lights.length;
129
+ const data = new Float32Array(lights.length * LIGHT_F32);
130
+ for (let i = 0; i < lights.length; i++) {
131
+ const o = i * LIGHT_F32, L = lights[i];
132
+ data[o] = L.x;
133
+ data[o + 1] = L.y;
134
+ data[o + 2] = L.z;
135
+ data[o + 3] = L.radius;
136
+ data[o + 4] = L.r;
137
+ data[o + 5] = L.g;
138
+ data[o + 6] = L.b;
139
+ }
140
+ this.device.queue.writeBuffer(this.lightBuf, 0, data);
141
+ }
142
+ frame(encoder, view, depth, viewProj, right, up, fwd, planes, sun, sunCol, ambient) {
143
+ const cam = new Float32Array(CAM_F32);
144
+ cam.set(viewProj, 0);
145
+ cam[16] = right[0];
146
+ cam[17] = right[1];
147
+ cam[18] = right[2];
148
+ cam[20] = up[0];
149
+ cam[21] = up[1];
150
+ cam[22] = up[2];
151
+ cam[24] = fwd[0];
152
+ cam[25] = fwd[1];
153
+ cam[26] = fwd[2];
154
+ for (let p = 0; p < 6; p++) {
155
+ cam[28 + p * 4] = planes[p][0];
156
+ cam[29 + p * 4] = planes[p][1];
157
+ cam[30 + p * 4] = planes[p][2];
158
+ cam[31 + p * 4] = planes[p][3];
159
+ }
160
+ cam[52] = this.count;
161
+ cam[53] = this.capPerPipe;
162
+ cam[54] = this.pipelines;
163
+ cam[55] = this.lightCount;
164
+ cam[56] = sun[0];
165
+ cam[57] = sun[1];
166
+ cam[58] = sun[2];
167
+ cam[60] = sunCol[0];
168
+ cam[61] = sunCol[1];
169
+ cam[62] = sunCol[2];
170
+ cam[63] = ambient;
171
+ this.device.queue.writeBuffer(this.camBuf, 0, cam);
172
+ const args = new Uint32Array(this.pipelines * ARG_U32);
173
+ for (let p = 0; p < this.pipelines; p++) {
174
+ args[p * 4] = 6;
175
+ args[p * 4 + 1] = 0;
176
+ args[p * 4 + 2] = 0;
177
+ args[p * 4 + 3] = p * this.capPerPipe;
178
+ }
179
+ this.device.queue.writeBuffer(this.argsBuf, 0, args);
180
+ const cp = encoder.beginComputePass();
181
+ cp.setPipeline(this.cullPipe);
182
+ cp.setBindGroup(0, this.cullBG);
183
+ cp.dispatchWorkgroups(Math.ceil(this.count / 64));
184
+ cp.end();
185
+ 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' } });
186
+ rp.setPipeline(this.drawPipe);
187
+ rp.setBindGroup(0, this.drawBG);
188
+ for (let p = 0; p < this.pipelines; p++)
189
+ rp.drawIndirect(this.argsBuf, p * ARG_U32 * 4);
190
+ rp.end();
191
+ this.lastDrawCalls = this.pipelines;
192
+ }
193
+ }
@@ -0,0 +1,33 @@
1
+ export declare class PostChain {
2
+ private device;
3
+ private w;
4
+ private h;
5
+ private hw;
6
+ private hh;
7
+ private uMain;
8
+ private uMainBuf;
9
+ private uBlurH;
10
+ private uBlurV;
11
+ private samp;
12
+ private brightPipe;
13
+ private blurPipe;
14
+ private compPipe;
15
+ private brightBGL;
16
+ private blurBGL;
17
+ private compBGL;
18
+ private brightTex?;
19
+ private blurTmp?;
20
+ private bloomTex?;
21
+ private brightView;
22
+ private blurTmpView;
23
+ private bloomView;
24
+ private brightBG;
25
+ private blurHBG;
26
+ private blurVBG;
27
+ private compBG;
28
+ constructor(device: GPUDevice, format: GPUTextureFormat);
29
+ resize(w: number, h: number): void;
30
+ /** Bind the scene's HDR texture (rebuild when it's (re)created, e.g. on resize). */
31
+ setInput(hdr: GPUTexture): void;
32
+ process(enc: GPUCommandEncoder, swapView: GPUTextureView, sunUV: [number, number], sunVis: number): void;
33
+ }
@@ -0,0 +1,162 @@
1
+ // Stage 2 — a MULTI-STEP GPU POST-PROCESSING CHAIN over the HDR scene, the thing the earlier
2
+ // "few draw calls, analytic shading" demos conspicuously lacked. Five fullscreen passes:
3
+ // 1. BRIGHT extract (threshold the HDR → the bloom/flare source, at half res)
4
+ // 2+3. separable GAUSSIAN BLUR (H then V) → BLOOM
5
+ // 4. COMPOSITE: HDR + bloom + volumetric GOD-RAYS (radial march of the bright buffer toward the
6
+ // sun) + LENS-FLARE ghosts (offset samples of the bright buffer) → ACES tonemap → screen.
7
+ // This is exactly the "multi-step GPU processing" AAA rendering needs; the same skeleton extends
8
+ // to SSAO, SSR, motion blur, DOF, TAA, etc. Fed by gpuCinematic's textured-PBR HDR output.
9
+ const HDR = 'rgba16float';
10
+ const FS_VS = /* wgsl */ `@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4<f32> {
11
+ var p = array<vec2<f32>, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); return vec4(p[vi], 0.0, 1.0); }`;
12
+ const BRIGHT_WGSL = FS_VS + /* wgsl */ `
13
+ struct U { a: vec4<f32>, b: vec4<f32>, c: vec4<f32> }; // a=texelFull.xy,texelHalf.xy b=sunUV.xy,vis,exposure c=thresh,bloomI,godrayI,flareI
14
+ @group(0) @binding(0) var<uniform> u: U;
15
+ @group(0) @binding(1) var samp: sampler;
16
+ @group(0) @binding(2) var hdr: texture_2d<f32>;
17
+ @fragment fn fs(@builtin(position) fc: vec4<f32>) -> @location(0) vec4<f32> {
18
+ let uv = fc.xy * u.a.zw; // half-res target
19
+ let col = textureSampleLevel(hdr, samp, uv, 0.0).rgb;
20
+ return vec4(max(col - vec3(u.c.x), vec3(0.0)), 1.0);
21
+ }`;
22
+ const BLUR_WGSL = FS_VS + /* wgsl */ `
23
+ struct B { texel: vec4<f32> }; // xy = texel, zw = direction
24
+ @group(0) @binding(0) var<uniform> b: B;
25
+ @group(0) @binding(1) var samp: sampler;
26
+ @group(0) @binding(2) var src: texture_2d<f32>;
27
+ @fragment fn fs(@builtin(position) fc: vec4<f32>) -> @location(0) vec4<f32> {
28
+ let uv = fc.xy * b.texel.xy;
29
+ let d = b.texel.zw;
30
+ let w = array<f32, 5>(0.227, 0.194, 0.121, 0.054, 0.016);
31
+ var c = textureSampleLevel(src, samp, uv, 0.0).rgb * w[0];
32
+ for (var i = 1; i < 5; i = i + 1) {
33
+ let o = d * f32(i) * 1.4;
34
+ c = c + textureSampleLevel(src, samp, uv + o, 0.0).rgb * w[i];
35
+ c = c + textureSampleLevel(src, samp, uv - o, 0.0).rgb * w[i];
36
+ }
37
+ return vec4(c, 1.0);
38
+ }`;
39
+ const COMPOSITE_WGSL = FS_VS + /* wgsl */ `
40
+ struct U { a: vec4<f32>, b: vec4<f32>, c: vec4<f32> };
41
+ @group(0) @binding(0) var<uniform> u: U;
42
+ @group(0) @binding(1) var samp: sampler;
43
+ @group(0) @binding(2) var hdr: texture_2d<f32>;
44
+ @group(0) @binding(3) var bloom: texture_2d<f32>;
45
+ @group(0) @binding(4) var bright: texture_2d<f32>;
46
+ fn aces(x: vec3<f32>) -> vec3<f32> { return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), vec3(0.0), vec3(1.0)); }
47
+ @fragment fn fs(@builtin(position) fc: vec4<f32>) -> @location(0) vec4<f32> {
48
+ let uv = fc.xy * u.a.xy;
49
+ var col = textureSampleLevel(hdr, samp, uv, 0.0).rgb;
50
+ col = col + textureSampleLevel(bloom, samp, uv, 0.0).rgb * u.c.y;
51
+ // volumetric god-rays: march the bright buffer toward the sun's screen position
52
+ let sunUV = u.b.xy; let vis = u.b.z;
53
+ if (vis > 0.5) {
54
+ var gr = vec3(0.0); let delta = (sunUV - uv) * (1.0 / 48.0); var p = uv; var dec = 1.0;
55
+ for (var i = 0; i < 48; i = i + 1) { p = p + delta; gr = gr + textureSampleLevel(bloom, samp, p, 0.0).rgb * dec; dec = dec * 0.96; }
56
+ col = col + gr * (u.c.z / 48.0);
57
+ // lens-flare ghosts: mirrored samples of the bright buffer through screen centre
58
+ let gv = (vec2(0.5) - sunUV) * 0.32;
59
+ var fl = vec3(0.0);
60
+ for (var i = 1; i < 6; i = i + 1) { fl = fl + textureSampleLevel(bloom, samp, uv + gv * f32(i), 0.0).rgb * (0.7 / f32(i)); }
61
+ let halo = textureSampleLevel(bloom, samp, uv + normalize(gv + vec2(1e-5)) * 0.28, 0.0).rgb;
62
+ col = col + (fl + halo * 0.4) * u.c.w * vec3(1.0, 0.9, 0.75);
63
+ }
64
+ return vec4(pow(aces(col * u.b.w), vec3(0.4545)), 1.0);
65
+ }`;
66
+ export class PostChain {
67
+ device;
68
+ w = 0;
69
+ h = 0;
70
+ hw = 0;
71
+ hh = 0;
72
+ uMain = new Float32Array(12);
73
+ uMainBuf;
74
+ uBlurH;
75
+ uBlurV;
76
+ samp;
77
+ brightPipe;
78
+ blurPipe;
79
+ compPipe;
80
+ brightBGL;
81
+ blurBGL;
82
+ compBGL;
83
+ brightTex;
84
+ blurTmp;
85
+ bloomTex;
86
+ brightView;
87
+ blurTmpView;
88
+ bloomView;
89
+ brightBG;
90
+ blurHBG;
91
+ blurVBG;
92
+ compBG;
93
+ constructor(device, format) {
94
+ this.device = device;
95
+ this.uMainBuf = device.createBuffer({ size: this.uMain.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
96
+ this.uBlurH = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
97
+ this.uBlurV = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
98
+ this.samp = device.createSampler({ magFilter: 'linear', minFilter: 'linear', addressModeU: 'clamp-to-edge', addressModeV: 'clamp-to-edge' });
99
+ const bMod = device.createShaderModule({ code: BRIGHT_WGSL }), blMod = device.createShaderModule({ code: BLUR_WGSL }), cMod = device.createShaderModule({ code: COMPOSITE_WGSL });
100
+ this.brightBGL = device.createBindGroupLayout({ entries: [
101
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }
102
+ ] });
103
+ this.blurBGL = device.createBindGroupLayout({ entries: [
104
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }
105
+ ] });
106
+ this.compBGL = device.createBindGroupLayout({ entries: [
107
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }, { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
108
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, { binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: {} }, { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: {} }
109
+ ] });
110
+ const mk = (bgl, mod, fmt) => device.createRenderPipeline({
111
+ layout: device.createPipelineLayout({ bindGroupLayouts: [bgl] }), vertex: { module: mod, entryPoint: 'vs' }, fragment: { module: mod, entryPoint: 'fs', targets: [{ format: fmt }] }, primitive: { topology: 'triangle-list' }
112
+ });
113
+ this.brightPipe = mk(this.brightBGL, bMod, HDR);
114
+ this.blurPipe = mk(this.blurBGL, blMod, HDR);
115
+ this.compPipe = mk(this.compBGL, cMod, format);
116
+ }
117
+ resize(w, h) {
118
+ this.w = w;
119
+ this.h = h;
120
+ this.hw = Math.max(1, w >> 1);
121
+ this.hh = Math.max(1, h >> 1);
122
+ for (const t of [this.brightTex, this.blurTmp, this.bloomTex])
123
+ t?.destroy();
124
+ const mk = () => this.device.createTexture({ size: [this.hw, this.hh], format: HDR, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING });
125
+ this.brightTex = mk();
126
+ this.blurTmp = mk();
127
+ this.bloomTex = mk();
128
+ this.brightView = this.brightTex.createView();
129
+ this.blurTmpView = this.blurTmp.createView();
130
+ this.bloomView = this.bloomTex.createView();
131
+ this.device.queue.writeBuffer(this.uBlurH, 0, new Float32Array([1 / this.hw, 1 / this.hh, 1 / this.hw, 0]));
132
+ this.device.queue.writeBuffer(this.uBlurV, 0, new Float32Array([1 / this.hw, 1 / this.hh, 0, 1 / this.hh]));
133
+ this.blurHBG = this.device.createBindGroup({ layout: this.blurBGL, entries: [{ binding: 0, resource: { buffer: this.uBlurH } }, { binding: 1, resource: this.samp }, { binding: 2, resource: this.brightView }] });
134
+ this.blurVBG = this.device.createBindGroup({ layout: this.blurBGL, entries: [{ binding: 0, resource: { buffer: this.uBlurV } }, { binding: 1, resource: this.samp }, { binding: 2, resource: this.blurTmpView }] });
135
+ }
136
+ /** Bind the scene's HDR texture (rebuild when it's (re)created, e.g. on resize). */
137
+ setInput(hdr) {
138
+ const hv = hdr.createView();
139
+ this.brightBG = this.device.createBindGroup({ layout: this.brightBGL, entries: [{ binding: 0, resource: { buffer: this.uMainBuf } }, { binding: 1, resource: this.samp }, { binding: 2, resource: hv }] });
140
+ this.compBG = this.device.createBindGroup({ layout: this.compBGL, entries: [
141
+ { binding: 0, resource: { buffer: this.uMainBuf } }, { binding: 1, resource: this.samp },
142
+ { binding: 2, resource: hv }, { binding: 3, resource: this.bloomView }, { binding: 4, resource: this.brightView }
143
+ ] });
144
+ }
145
+ process(enc, swapView, sunUV, sunVis) {
146
+ this.uMain.set([1 / this.w, 1 / this.h, 1 / this.hw, 1 / this.hh], 0);
147
+ this.uMain.set([sunUV[0], sunUV[1], sunVis, 1.0], 4); // exposure 1.0
148
+ this.uMain.set([1.3, 0.85, 0.5, 0.22], 8); // threshold, bloomI, godrayI, flareI (softened)
149
+ this.device.queue.writeBuffer(this.uMainBuf, 0, this.uMain);
150
+ const pass = (view, pipe, bg) => {
151
+ const p = enc.beginRenderPass({ colorAttachments: [{ view, loadOp: 'clear', storeOp: 'store', clearValue: { r: 0, g: 0, b: 0, a: 1 } }] });
152
+ p.setPipeline(pipe);
153
+ p.setBindGroup(0, bg);
154
+ p.draw(3);
155
+ p.end();
156
+ };
157
+ pass(this.brightView, this.brightPipe, this.brightBG); // 1. bright extract
158
+ pass(this.blurTmpView, this.blurPipe, this.blurHBG); // 2. blur H
159
+ pass(this.bloomView, this.blurPipe, this.blurVBG); // 3. blur V → bloom
160
+ pass(swapView, this.compPipe, this.compBG); // 4. composite + godrays + flare + tonemap
161
+ }
162
+ }
@@ -0,0 +1,11 @@
1
+ export declare const SHADOW_SIZE = 2048;
2
+ export declare const SHADOW_WGSL = "\nfn shadowFactor(lightVP: mat4x4<f32>, world: vec3<f32>, ndl: f32, texel: f32,\n shadowMap: texture_depth_2d, shadowSamp: sampler_comparison) -> f32 {\n let lp = lightVP * vec4(world, 1.0);\n let proj = lp.xyz / lp.w;\n let uv = proj.xy * vec2(0.5, -0.5) + vec2(0.5, 0.5);\n if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 || proj.z > 1.0 || proj.z < 0.0) { return 1.0; }\n let bias = max(0.0025 * (1.0 - ndl), 0.0012);\n var s = 0.0;\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n s = s + textureSampleCompareLevel(shadowMap, shadowSamp, uv + vec2(f32(dx), f32(dy)) * texel, proj.z - bias);\n }\n }\n return s / 9.0;\n}";
3
+ export declare class GpuShadowMap {
4
+ readonly view: GPUTextureView;
5
+ readonly sampler: GPUSampler;
6
+ readonly texel: number;
7
+ private tex;
8
+ constructor(device: GPUDevice);
9
+ /** Begin the occluder depth pass (clears the map). Renderers draw their depth-only geometry into it. */
10
+ begin(enc: GPUCommandEncoder): GPURenderPassEncoder;
11
+ }
@@ -0,0 +1,41 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // GPU SHADOW MAP (PLATFORM) — a shared directional-sun depth map + PCF sampling helper, so the outdoor
3
+ // scene casts real shadows (the single biggest look upgrade after lighting; it also makes the sun's
4
+ // DIRECTION legible — long shadows at dawn/dusk, short at noon). Occluders (terrain, props) render their
5
+ // depth from the sun's ortho view into this one map; receivers (terrain) sample it with PCF. Reusable
6
+ // across renderers: each contributes a depth-only pass into `begin()`, and samples via SHADOW_WGSL.
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ export const SHADOW_SIZE = 2048;
9
+ // Drop-in WGSL: paste into a receiver fragment shader. Expects `shadowMap` (binding 2) + `shadowSamp`
10
+ // (binding 3) + a `lightVP` mat4 and texel size in scope. Returns 1 = lit, 0 = fully shadowed (PCF 3×3).
11
+ export const SHADOW_WGSL = /* wgsl */ `
12
+ fn shadowFactor(lightVP: mat4x4<f32>, world: vec3<f32>, ndl: f32, texel: f32,
13
+ shadowMap: texture_depth_2d, shadowSamp: sampler_comparison) -> f32 {
14
+ let lp = lightVP * vec4(world, 1.0);
15
+ let proj = lp.xyz / lp.w;
16
+ let uv = proj.xy * vec2(0.5, -0.5) + vec2(0.5, 0.5);
17
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 || proj.z > 1.0 || proj.z < 0.0) { return 1.0; }
18
+ let bias = max(0.0025 * (1.0 - ndl), 0.0012);
19
+ var s = 0.0;
20
+ for (var dy = -1; dy <= 1; dy = dy + 1) {
21
+ for (var dx = -1; dx <= 1; dx = dx + 1) {
22
+ s = s + textureSampleCompareLevel(shadowMap, shadowSamp, uv + vec2(f32(dx), f32(dy)) * texel, proj.z - bias);
23
+ }
24
+ }
25
+ return s / 9.0;
26
+ }`;
27
+ export class GpuShadowMap {
28
+ view;
29
+ sampler;
30
+ texel = 1 / SHADOW_SIZE;
31
+ tex;
32
+ constructor(device) {
33
+ this.tex = device.createTexture({ size: [SHADOW_SIZE, SHADOW_SIZE], format: 'depth32float', usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING });
34
+ this.view = this.tex.createView();
35
+ this.sampler = device.createSampler({ compare: 'less', magFilter: 'linear', minFilter: 'linear' });
36
+ }
37
+ /** Begin the occluder depth pass (clears the map). Renderers draw their depth-only geometry into it. */
38
+ begin(enc) {
39
+ return enc.beginRenderPass({ colorAttachments: [], depthStencilAttachment: { view: this.view, depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'store' } });
40
+ }
41
+ }
@@ -0,0 +1,39 @@
1
+ import type { CubeInstance } from './gpuShadow';
2
+ export type { CubeInstance } from './gpuShadow';
3
+ export interface Spot {
4
+ px: number;
5
+ py: number;
6
+ pz: number;
7
+ dx: number;
8
+ dy: number;
9
+ dz: number;
10
+ r: number;
11
+ g: number;
12
+ b: number;
13
+ range: number;
14
+ cosInner: number;
15
+ cosOuter: number;
16
+ }
17
+ export declare const NUM_SPOTS = 6;
18
+ export declare class GpuSpotRenderer {
19
+ private device;
20
+ private instBuf;
21
+ private uBuf;
22
+ private lightVPBufs;
23
+ private shadowTex;
24
+ private layerViews;
25
+ private shadowPipe;
26
+ private mainPipe;
27
+ private shadowBGL;
28
+ private mainBGL;
29
+ private shadowSampler;
30
+ private shadowBGs;
31
+ private mainBG;
32
+ private count;
33
+ private u;
34
+ private vpScratch;
35
+ constructor(device: GPUDevice, format: GPUTextureFormat);
36
+ setScene(instances: CubeInstance[]): void;
37
+ /** camVP: Float32Array(16). spotVPs: NUM_SPOTS × Float32Array(16) (perspective light views). */
38
+ frame(enc: GPUCommandEncoder, colorView: GPUTextureView, sceneDepth: GPUTextureView, camVP: Float32Array, spots: Spot[], spotVPs: Float32Array[], ambient: number): void;
39
+ }