ugly-game 0.5.9 → 0.5.10

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,29 @@
1
+ export interface PropMesh {
2
+ positions: Float32Array;
3
+ normals: Float32Array;
4
+ colors: Float32Array;
5
+ indices: Uint32Array;
6
+ }
7
+ export interface PropInstance {
8
+ model: number;
9
+ mat: Float32Array;
10
+ }
11
+ export declare class GpuPropsRenderer {
12
+ private device;
13
+ private pipe;
14
+ private occPipe;
15
+ private uBuf;
16
+ private lightBuf;
17
+ private models;
18
+ private instBufs;
19
+ private bgs;
20
+ private occBGs;
21
+ private counts;
22
+ constructor(device: GPUDevice, format: GPUTextureFormat);
23
+ setModels(meshes: PropMesh[]): void;
24
+ /** Group instances by model and upload per-model transform buffers. */
25
+ setInstances(instances: PropInstance[]): void;
26
+ /** Occluder pass: draw all instanced props' depth into the shared shadow map from the sun's ortho view. */
27
+ shadowPass(pass: GPURenderPassEncoder, lightVP: Float32Array): void;
28
+ frame(enc: GPUCommandEncoder, colorView: GPUTextureView, depthView: GPUTextureView, camVP: Float32Array, sunDir: [number, number, number], sunCol: [number, number, number], amb: [number, number, number], camPos: [number, number, number], fog: [number, number, number], fogDensity: number): void;
29
+ }
@@ -0,0 +1,147 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // GPU PROPS — instanced static-mesh renderer for scattered nature props (PLATFORM). Draws each cooked
3
+ // model (tree/rock/bush/…) many times at per-instance world transforms, lit + fogged to match the
4
+ // terrain. Runs in a load pass after the terrain so props depth-test against the surface.
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ const SHADER = /* wgsl */ `
7
+ struct U { mvp: mat4x4<f32>, sun: vec4<f32>, sunCol: vec4<f32>, amb: vec4<f32>, cam: vec4<f32>, fog: vec4<f32> };
8
+ @group(0) @binding(0) var<uniform> u: U;
9
+ @group(0) @binding(1) var<storage,read> models: array<mat4x4<f32>>;
10
+ struct VO { @builtin(position) pos: vec4<f32>, @location(0) world: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec3<f32> };
11
+ @vertex fn vs(@location(0) p: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) col: vec3<f32>, @builtin(instance_index) ii: u32) -> VO {
12
+ let m = models[ii];
13
+ let world = (m * vec4(p, 1.0)).xyz;
14
+ var o: VO; o.pos = u.mvp * vec4(world, 1.0); o.world = world; o.n = (m * vec4(nrm, 0.0)).xyz; o.c = col; return o;
15
+ }
16
+ @fragment fn fs(i: VO) -> @location(0) vec4<f32> {
17
+ let d = clamp(dot(normalize(i.n), -normalize(u.sun.xyz)), 0.0, 1.0);
18
+ let lit = i.c * (u.amb.xyz + d * u.sunCol.xyz);
19
+ let dist = length(i.world - u.cam.xyz);
20
+ let f = clamp(1.0 - exp(-dist * u.fog.w), 0.0, 1.0);
21
+ return vec4(mix(lit, u.fog.xyz, f), 1.0);
22
+ }`;
23
+ // Depth-only occluder pass: instanced prop depth from the sun's ortho view (so trees/rocks cast shadows).
24
+ const OCCLUDER = /* wgsl */ `
25
+ @group(0) @binding(0) var<uniform> lightVP: mat4x4<f32>;
26
+ @group(0) @binding(1) var<storage,read> models: array<mat4x4<f32>>;
27
+ @vertex fn vs(@location(0) p: vec3<f32>, @builtin(instance_index) ii: u32) -> @builtin(position) vec4<f32> {
28
+ return lightVP * (models[ii] * vec4(p, 1.0));
29
+ }`;
30
+ export class GpuPropsRenderer {
31
+ device;
32
+ pipe;
33
+ occPipe;
34
+ uBuf;
35
+ lightBuf;
36
+ models = [];
37
+ instBufs = [];
38
+ bgs = [];
39
+ occBGs = [];
40
+ counts = [];
41
+ constructor(device, format) {
42
+ this.device = device;
43
+ const sm = device.createShaderModule({ code: SHADER });
44
+ this.pipe = device.createRenderPipeline({
45
+ layout: 'auto',
46
+ vertex: { module: sm, entryPoint: 'vs', buffers: [
47
+ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] },
48
+ { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x3' }] },
49
+ { arrayStride: 12, attributes: [{ shaderLocation: 2, offset: 0, format: 'float32x3' }] }
50
+ ] },
51
+ fragment: { module: sm, entryPoint: 'fs', targets: [{ format }] },
52
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
53
+ depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
54
+ });
55
+ const om = device.createShaderModule({ code: OCCLUDER });
56
+ this.occPipe = device.createRenderPipeline({
57
+ layout: 'auto',
58
+ vertex: { module: om, entryPoint: 'vs', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }] },
59
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
60
+ depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'less' },
61
+ });
62
+ this.uBuf = device.createBuffer({ size: 96 + 16 * 5, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
63
+ this.lightBuf = device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
64
+ }
65
+ setModels(meshes) {
66
+ const dev = this.device, U = GPUBufferUsage;
67
+ for (const p of this.models) {
68
+ p.posB.destroy();
69
+ p.nrmB.destroy();
70
+ p.colB.destroy();
71
+ p.idxB.destroy();
72
+ }
73
+ const mk = (d, u) => { const b = dev.createBuffer({ size: d.byteLength, usage: u }); dev.queue.writeBuffer(b, 0, d); return b; };
74
+ this.models = meshes.map((m) => ({ posB: mk(m.positions, U.VERTEX | U.COPY_DST), nrmB: mk(m.normals, U.VERTEX | U.COPY_DST), colB: mk(m.colors, U.VERTEX | U.COPY_DST), idxB: mk(m.indices, U.INDEX | U.COPY_DST), idxCount: m.indices.length }));
75
+ this.instBufs = meshes.map(() => null);
76
+ this.bgs = meshes.map(() => null);
77
+ this.occBGs = meshes.map(() => null);
78
+ this.counts = meshes.map(() => 0);
79
+ }
80
+ /** Group instances by model and upload per-model transform buffers. */
81
+ setInstances(instances) {
82
+ const dev = this.device;
83
+ const byModel = this.models.map(() => []);
84
+ const data = this.models.map(() => []);
85
+ for (const it of instances) {
86
+ if (it.model < 0 || it.model >= this.models.length)
87
+ continue;
88
+ byModel[it.model].push(0);
89
+ for (let i = 0; i < 16; i++)
90
+ data[it.model].push(it.mat[i]);
91
+ }
92
+ for (let m = 0; m < this.models.length; m++) {
93
+ this.instBufs[m]?.destroy();
94
+ const arr = new Float32Array(data[m]);
95
+ this.counts[m] = byModel[m].length;
96
+ if (arr.length === 0) {
97
+ this.instBufs[m] = null;
98
+ this.bgs[m] = null;
99
+ this.occBGs[m] = null;
100
+ continue;
101
+ }
102
+ const b = dev.createBuffer({ size: arr.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
103
+ dev.queue.writeBuffer(b, 0, arr);
104
+ this.instBufs[m] = b;
105
+ this.bgs[m] = dev.createBindGroup({ layout: this.pipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.uBuf } }, { binding: 1, resource: { buffer: b } }] });
106
+ this.occBGs[m] = dev.createBindGroup({ layout: this.occPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.lightBuf } }, { binding: 1, resource: { buffer: b } }] });
107
+ }
108
+ }
109
+ /** Occluder pass: draw all instanced props' depth into the shared shadow map from the sun's ortho view. */
110
+ shadowPass(pass, lightVP) {
111
+ this.device.queue.writeBuffer(this.lightBuf, 0, lightVP);
112
+ pass.setPipeline(this.occPipe);
113
+ for (let m = 0; m < this.models.length; m++) {
114
+ const bg = this.occBGs[m], mdl = this.models[m];
115
+ if (!bg || this.counts[m] === 0)
116
+ continue;
117
+ pass.setBindGroup(0, bg);
118
+ pass.setVertexBuffer(0, mdl.posB);
119
+ pass.setIndexBuffer(mdl.idxB, 'uint32');
120
+ pass.drawIndexed(mdl.idxCount, this.counts[m]);
121
+ }
122
+ }
123
+ frame(enc, colorView, depthView, camVP, sunDir, sunCol, amb, camPos, fog, fogDensity) {
124
+ const u = new Float32Array(24 + 20);
125
+ u.set(camVP, 0);
126
+ u.set([sunDir[0], sunDir[1], sunDir[2], 0], 16);
127
+ u.set([sunCol[0], sunCol[1], sunCol[2], 0], 20);
128
+ u.set([amb[0], amb[1], amb[2], 0], 24);
129
+ u.set([camPos[0], camPos[1], camPos[2], 0], 28);
130
+ u.set([fog[0], fog[1], fog[2], fogDensity], 32);
131
+ this.device.queue.writeBuffer(this.uBuf, 0, u);
132
+ const rp = enc.beginRenderPass({ colorAttachments: [{ view: colorView, loadOp: 'load', storeOp: 'store' }], depthStencilAttachment: { view: depthView, depthLoadOp: 'load', depthStoreOp: 'store' } });
133
+ rp.setPipeline(this.pipe);
134
+ for (let m = 0; m < this.models.length; m++) {
135
+ const bg = this.bgs[m], mdl = this.models[m];
136
+ if (!bg || this.counts[m] === 0)
137
+ continue;
138
+ rp.setBindGroup(0, bg);
139
+ rp.setVertexBuffer(0, mdl.posB);
140
+ rp.setVertexBuffer(1, mdl.nrmB);
141
+ rp.setVertexBuffer(2, mdl.colB);
142
+ rp.setIndexBuffer(mdl.idxB, 'uint32');
143
+ rp.drawIndexed(mdl.idxCount, this.counts[m]);
144
+ }
145
+ rp.end();
146
+ }
147
+ }
@@ -8,6 +8,19 @@ export interface CubeInstance {
8
8
  hue: number;
9
9
  emissive?: number;
10
10
  rot?: [number, number, number, number];
11
+ tile?: number;
12
+ up?: number;
13
+ }
14
+ /** Atlas layout: which grid tiles a material's top/side/bottom faces sample. Grid indices into a cols×rows atlas. */
15
+ export interface AtlasLayout {
16
+ tileSize: number;
17
+ cols: number;
18
+ rows: number;
19
+ tiles: Record<number, {
20
+ top: number;
21
+ side: number;
22
+ bottom: number;
23
+ }>;
11
24
  }
12
25
  /** The 36-vertex cube as WGSL array-literal bodies (pos + per-face normal). Shared by shadow modules. */
13
26
  export declare const CUBE: {
@@ -29,6 +42,10 @@ export declare class GpuShadowRenderer {
29
42
  private shadowSampler;
30
43
  private count;
31
44
  private cam;
45
+ private atlasView;
46
+ private atlasSampler;
47
+ private tileRectBuf;
48
+ private useAtlas;
32
49
  private alphaPipe;
33
50
  private overlayBuf;
34
51
  private overlayBG;
@@ -70,6 +87,10 @@ export declare class GpuShadowRenderer {
70
87
  private scratch;
71
88
  private fill;
72
89
  private makeBindGroups;
90
+ /** Bind a block-texture atlas + its material→tile layout. Each material's top/side/bottom faces sample distinct
91
+ * atlas tiles; a `CubeInstance.tile` of that material id + its `up`-face code selects the right tile in-shader.
92
+ * Call once after the atlas texture is uploaded; instances with `tile < 0` still use the flat hue path. */
93
+ setAtlas(tex: GPUTexture, layout: AtlasLayout): void;
73
94
  /** Static scene: sizes the buffer to exactly these instances. */
74
95
  setScene(instances: CubeInstance[]): void;
75
96
  /** Dynamic scene: pre-allocate for `cap` instances; call updateInstances() each frame. */
@@ -86,7 +107,7 @@ export declare class GpuShadowRenderer {
86
107
  depth: GPUTextureView;
87
108
  w: number;
88
109
  h: number;
89
- }): void;
110
+ }, tint?: [number, number, number, number], extraShadow?: (sp: GPURenderPassEncoder, lightVP: Float32Array) => void): void;
90
111
  /** Read back the last frame's GPU cull result (camera-visible, light-visible). Does its OWN copy+submit so it's
91
112
  * safe to call from a live render loop (no per-frame readback cost, no race with the ongoing frames). IR/tests. */
92
113
  readCullStats(): Promise<{
package/dist/gpuShadow.js CHANGED
@@ -14,6 +14,7 @@
14
14
  import { frustumPlanes } from './mat4';
15
15
  const SHADOW_SIZE = 2048;
16
16
  const CULL_WG = 64; // compute-cull workgroup size
17
+ const INST_F = 16; // floats per instance: pos.xyz+hue, scl.xyz+emissive, rot quat, aux (tile,up,_,_)
17
18
  const OVERLAY_CAP = 512; // max translucent overlay cubes per frame (the place ghost + headroom)
18
19
  // Build the 36-vertex cube (6 faces × 2 tris) in JS and inject as WGSL const arrays —
19
20
  // bulletproof + readable vs. bit-twiddling from vertex_index. cullMode is 'none', so winding
@@ -48,8 +49,10 @@ struct Cam {
48
49
  lightVP: mat4x4<f32>,
49
50
  sun: vec4<f32>, // xyz = direction light travels, w = ambient
50
51
  sunCol: vec4<f32>, // xyz = sun colour, w = shadow map size (texels)
52
+ tint: vec4<f32>, // rgb = per-planet tint · w = tint strength (0 = none)
53
+ atlas: vec4<f32>, // x = useAtlas (1/0)
51
54
  };
52
- struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32> }; // pos.w = hue (<0 ⇒ ground) · scl.w = emissive/alpha · rot = quaternion
55
+ struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32>, aux: vec4<f32> }; // pos.w = hue (<0 ⇒ ground) · scl.w = emissive/alpha · rot = quaternion · aux.x = atlas tile (<0 ⇒ hue) · aux.y = up-face code
53
56
  @group(0) @binding(0) var<uniform> cam: Cam;
54
57
  @group(0) @binding(1) var<storage, read> insts: array<Inst>;
55
58
 
@@ -85,7 +88,7 @@ fn hsv(hh: f32) -> vec3<f32> {
85
88
  // under-culling for one frame, never a false cull. occ.x toggles it (camera pass only; the shadow pass never
86
89
  // occludes — an off-screen occluder still casts a shadow into view). occ.yz = hiz dims, occ.w = pixels/texel.
87
90
  const CULL_WGSL = /* wgsl */ `
88
- struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32> };
91
+ struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32>, aux: vec4<f32> };
89
92
  struct Args { vc: u32, ic: atomic<u32>, fv: u32, fi: u32 };
90
93
  struct CullCam {
91
94
  planes: array<vec4<f32>, 6>, cnt: vec4<f32>, // cnt.x = instance count
@@ -181,8 +184,18 @@ const MAIN_WGSL = COMMON_WGSL + /* wgsl */ `
181
184
  @group(0) @binding(2) var shadowMap: texture_depth_2d;
182
185
  @group(0) @binding(3) var shadowSamp: sampler_comparison;
183
186
  @group(0) @binding(4) var<storage, read> vis: array<u32>;
187
+ @group(0) @binding(5) var atlasTex: texture_2d<f32>;
188
+ @group(0) @binding(6) var atlasSamp: sampler;
189
+ @group(0) @binding(7) var<storage, read> tileRects: array<vec4<f32>>; // [u0,v0,du,dv] per (material*3 + face-variant)
184
190
 
185
- struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) hue: f32, @location(3) emis: f32, @location(4) blk: vec3<f32>, @location(5) objN: vec3<f32> };
191
+ struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) hue: f32, @location(3) emis: f32, @location(4) blk: vec3<f32>, @location(5) objN: vec3<f32>, @location(6) tile: f32, @location(7) upc: f32 };
192
+
193
+ fn faceVec(code: f32) -> vec3<f32> {
194
+ let c = i32(code);
195
+ if (c == 0) { return vec3(1.,0.,0.); } if (c == 1) { return vec3(-1.,0.,0.); }
196
+ if (c == 2) { return vec3(0.,1.,0.); } if (c == 3) { return vec3(0.,-1.,0.); }
197
+ if (c == 4) { return vec3(0.,0.,1.); } return vec3(0.,0.,-1.);
198
+ }
186
199
 
187
200
  @vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
188
201
  let inst = insts[vis[ii]];
@@ -198,6 +211,7 @@ struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>,
198
211
  // by inst.scl so a merged LOD cube (scl>1) still shows its sub-blocks. objN = pre-rotation face normal.
199
212
  o.blk = (CUBE_POS[vi] + vec3(0.5, 0.5, 0.5)) * inst.scl.xyz;
200
213
  o.objN = CUBE_NRM[vi];
214
+ o.tile = inst.aux.x; o.upc = inst.aux.y;
201
215
  return o;
202
216
  }
203
217
 
@@ -232,7 +246,6 @@ fn blockShade(fx: f32, fz: f32) -> f32 {
232
246
  }
233
247
  let N = normalize(i.nrm);
234
248
  let ndl = max(dot(N, -normalize(cam.sun.xyz)), 0.0);
235
- let base = select(mix(vec3(0.34), hsv(i.hue), 0.62), vec3(0.6, 0.62, 0.66), i.hue < 0.0);
236
249
  let sh = shadowFactor(i.world, ndl);
237
250
  // in-block coords = the two OBJECT axes not along the (object-space, axis-aligned) face normal — so the AO
238
251
  // pattern is anchored to the model's own block grid and translates/rotates with the instance.
@@ -241,6 +254,16 @@ fn blockShade(fx: f32, fz: f32) -> f32 {
241
254
  if (an.y >= an.x && an.y >= an.z) { fx = fract(i.blk.x); fz = fract(i.blk.z); }
242
255
  else if (an.x >= an.z) { fx = fract(i.blk.y); fz = fract(i.blk.z); }
243
256
  else { fx = fract(i.blk.x); fz = fract(i.blk.y); }
257
+ var base = select(mix(vec3(0.34), hsv(i.hue), 0.62), vec3(0.6, 0.62, 0.66), i.hue < 0.0);
258
+ if (cam.atlas.x > 0.5 && i.tile >= 0.0) { // ATLAS: pick top/side/bottom tile from the face vs the up axis, tile the AO in-face coords
259
+ let d = dot(i.objN, faceVec(i.upc));
260
+ var variant = 1u; // side
261
+ if (d > 0.5) { variant = 0u; } else if (d < -0.5) { variant = 2u; }
262
+ let rect = tileRects[u32(i.tile) * 3u + variant];
263
+ let uv = rect.xy + fract(vec2(fx, fz)) * rect.zw;
264
+ let texc = textureSampleLevel(atlasTex, atlasSamp, uv, 0.0).rgb; // explicit LOD (early-return above makes control flow non-uniform)
265
+ base = mix(texc, texc * cam.tint.rgb, cam.tint.w); // per-planet tint
266
+ }
244
267
  let ao = blockShade(fx, fz);
245
268
  let lit = base * (cam.sun.w + cam.sunCol.xyz * ndl * sh) * ao;
246
269
  return vec4(pow(clamp(lit, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0); // gamma
@@ -277,11 +300,15 @@ export class GpuShadowRenderer {
277
300
  mainBGL;
278
301
  shadowSampler;
279
302
  count = 0;
280
- cam = new Float32Array(40); // 2×mat4 + 2×vec4
303
+ cam = new Float32Array(48); // 2×mat4 + sun + sunCol + tint + atlas
304
+ atlasView;
305
+ atlasSampler;
306
+ tileRectBuf;
307
+ useAtlas = 0;
281
308
  alphaPipe;
282
309
  overlayBuf;
283
310
  overlayBG;
284
- overlayScratch = new Float32Array(OVERLAY_CAP * 12);
311
+ overlayScratch = new Float32Array(OVERLAY_CAP * INST_F);
285
312
  overlayCount = 0;
286
313
  // GPU-driven culling: one compute pipeline; per-frustum (camera + light) indirect args + compacted visible lists.
287
314
  cullPipe;
@@ -332,6 +359,9 @@ export class GpuShadowRenderer {
332
359
  { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
333
360
  { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
334
361
  { binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }, // camera-visible list
362
+ { binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } }, // block atlas
363
+ { binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // atlas sampler
364
+ { binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } }, // per-tile UV rects
335
365
  ] });
336
366
  this.mainPipe = device.createRenderPipeline({
337
367
  layout: device.createPipelineLayout({ bindGroupLayouts: [mainBGL] }),
@@ -390,6 +420,12 @@ export class GpuShadowRenderer {
390
420
  this.hizParam = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
391
421
  const dummy = device.createTexture({ size: [1, 1], format: 'r32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING });
392
422
  this.occView = dummy.createView();
423
+ // atlas dummies (bound until setAtlas): a 1×1 white texture + a 1-entry rect buffer + a plain sampler
424
+ const white = device.createTexture({ size: [1, 1], format: 'rgba8unorm', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST });
425
+ device.queue.writeTexture({ texture: white }, new Uint8Array([255, 255, 255, 255]), {}, [1, 1]);
426
+ this.atlasView = white.createView();
427
+ this.atlasSampler = device.createSampler({ magFilter: 'nearest', minFilter: 'linear', mipmapFilter: 'linear' });
428
+ this.tileRectBuf = device.createBuffer({ size: 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
393
429
  }
394
430
  // One frustum's cull resources: indirect args (drawIndirect), a compacted visible-index list sized to the
395
431
  // instance capacity, and a uniform holding the 6 planes + instance count + camVP + occlusion params.
@@ -437,7 +473,7 @@ export class GpuShadowRenderer {
437
473
  scratch = new Float32Array(0);
438
474
  fill(scratch, instances, n) {
439
475
  for (let i = 0; i < n; i++) {
440
- const c = instances[i], o = i * 12, r = c.rot;
476
+ const c = instances[i], o = i * INST_F, r = c.rot;
441
477
  scratch[o] = c.x;
442
478
  scratch[o + 1] = c.y;
443
479
  scratch[o + 2] = c.z;
@@ -450,6 +486,10 @@ export class GpuShadowRenderer {
450
486
  scratch[o + 9] = r ? r[1] : 0;
451
487
  scratch[o + 10] = r ? r[2] : 0;
452
488
  scratch[o + 11] = r ? r[3] : 1; // quaternion, default identity
489
+ scratch[o + 12] = c.tile ?? -1;
490
+ scratch[o + 13] = c.up ?? 1;
491
+ scratch[o + 14] = 0;
492
+ scratch[o + 15] = 0; // aux: atlas tile id + up-face code
453
493
  }
454
494
  }
455
495
  makeBindGroups() {
@@ -462,13 +502,37 @@ export class GpuShadowRenderer {
462
502
  { binding: 0, resource: { buffer: this.camBuf } }, { binding: 1, resource: { buffer: this.instBuf } },
463
503
  { binding: 2, resource: this.shadowView }, { binding: 3, resource: this.shadowSampler },
464
504
  { binding: 4, resource: { buffer: this.camVis } },
505
+ { binding: 5, resource: this.atlasView }, { binding: 6, resource: this.atlasSampler }, { binding: 7, resource: { buffer: this.tileRectBuf } },
465
506
  ] });
466
507
  }
508
+ /** Bind a block-texture atlas + its material→tile layout. Each material's top/side/bottom faces sample distinct
509
+ * atlas tiles; a `CubeInstance.tile` of that material id + its `up`-face code selects the right tile in-shader.
510
+ * Call once after the atlas texture is uploaded; instances with `tile < 0` still use the flat hue path. */
511
+ setAtlas(tex, layout) {
512
+ this.atlasView = tex.createView();
513
+ const ids = Object.keys(layout.tiles).map(Number);
514
+ const maxMat = (ids.length ? Math.max(...ids) : 0) + 1;
515
+ const rects = new Float32Array(maxMat * 3 * 4);
516
+ const rectOf = (g) => {
517
+ const cx = g % layout.cols, cy = Math.floor(g / layout.cols);
518
+ return [cx / layout.cols, cy / layout.rows, 1 / layout.cols, 1 / layout.rows];
519
+ };
520
+ for (const [mat, f] of Object.entries(layout.tiles)) {
521
+ const b = Number(mat) * 3 * 4;
522
+ rects.set(rectOf(f.top), b);
523
+ rects.set(rectOf(f.side), b + 4);
524
+ rects.set(rectOf(f.bottom), b + 8);
525
+ }
526
+ this.tileRectBuf = this.device.createBuffer({ size: Math.max(16, rects.byteLength), usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
527
+ this.device.queue.writeBuffer(this.tileRectBuf, 0, rects);
528
+ this.useAtlas = 1;
529
+ this.makeBindGroups(); // rebind the main pass with the real atlas + rects
530
+ }
467
531
  /** Static scene: sizes the buffer to exactly these instances. */
468
532
  setScene(instances) {
469
533
  this.count = instances.length;
470
534
  this.cap = this.count;
471
- this.scratch = new Float32Array(this.count * 12);
535
+ this.scratch = new Float32Array(this.count * INST_F);
472
536
  this.fill(this.scratch, instances, this.count);
473
537
  this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
474
538
  this.device.queue.writeBuffer(this.instBuf, 0, this.scratch);
@@ -477,15 +541,15 @@ export class GpuShadowRenderer {
477
541
  /** Dynamic scene: pre-allocate for `cap` instances; call updateInstances() each frame. */
478
542
  setCapacity(cap) {
479
543
  this.cap = cap;
480
- this.scratch = new Float32Array(cap * 12);
544
+ this.scratch = new Float32Array(cap * INST_F);
481
545
  this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
482
546
  this.count = 0;
483
547
  this.makeBindGroups();
484
548
  }
485
549
  updateInstances(instances) {
486
- const n = Math.min(instances.length, this.scratch.length / 12);
550
+ const n = Math.min(instances.length, this.scratch.length / INST_F);
487
551
  this.fill(this.scratch, instances, n);
488
- this.device.queue.writeBuffer(this.instBuf, 0, this.scratch, 0, n * 12);
552
+ this.device.queue.writeBuffer(this.instBuf, 0, this.scratch, 0, n * INST_F);
489
553
  this.count = n;
490
554
  }
491
555
  /** Translucent overlay cubes drawn over the opaque scene each frame; `emissive` carries per-cube alpha. */
@@ -493,7 +557,7 @@ export class GpuShadowRenderer {
493
557
  const n = Math.min(instances.length, OVERLAY_CAP);
494
558
  this.fill(this.overlayScratch, instances, n);
495
559
  if (n > 0)
496
- this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n * 12);
560
+ this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n * INST_F);
497
561
  this.overlayCount = n;
498
562
  }
499
563
  /** Write a frustum's cull uniform: 6 planes + count + this frustum's VP + occlusion params (occ on the camera pass). */
@@ -517,7 +581,7 @@ export class GpuShadowRenderer {
517
581
  /** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. Pass `occlusion` (the depth target +
518
582
  * its size) to enable Hi-Z occlusion culling on the camera pass — the renderer rebuilds a coarse Hi-Z from that
519
583
  * depth each frame and the NEXT frame's cull uses it (hole-free: the first frame + any resize skip occlusion). */
520
- frame(enc, colorView, sceneDepth, camVP, lightVP, sunDir, sunCol, ambient, load = false, occlusion) {
584
+ frame(enc, colorView, sceneDepth, camVP, lightVP, sunDir, sunCol, ambient, load = false, occlusion, tint = [1, 1, 1, 0], extraShadow) {
521
585
  this.cam.set(camVP, 0);
522
586
  this.cam.set(lightVP, 16);
523
587
  this.cam[32] = sunDir[0];
@@ -528,6 +592,11 @@ export class GpuShadowRenderer {
528
592
  this.cam[37] = sunCol[1];
529
593
  this.cam[38] = sunCol[2];
530
594
  this.cam[39] = SHADOW_SIZE;
595
+ this.cam[40] = tint[0];
596
+ this.cam[41] = tint[1];
597
+ this.cam[42] = tint[2];
598
+ this.cam[43] = tint[3];
599
+ this.cam[44] = this.useAtlas;
531
600
  this.device.queue.writeBuffer(this.camBuf, 0, this.cam);
532
601
  if (occlusion)
533
602
  this.ensureHiZ(occlusion.w, occlusion.h);
@@ -556,6 +625,8 @@ export class GpuShadowRenderer {
556
625
  sp.setPipeline(this.shadowPipe);
557
626
  sp.setBindGroup(0, this.shadowBG);
558
627
  sp.drawIndirect(this.lightArgs, 0);
628
+ if (extraShadow)
629
+ extraShadow(sp, lightVP); // let the app cast extra occluders (mesh props/trees) into the same sun shadow map
559
630
  sp.end();
560
631
  // Pass 2: camera, shadow-sampled (camera-frustum + Hi-Z-occlusion-culled).
561
632
  const mp = enc.beginRenderPass({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-game",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {