ugly-game 0.7.7 → 0.7.8

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.
@@ -47,10 +47,13 @@ export declare class GpuShadowRenderer {
47
47
  private tileRectBuf;
48
48
  private useAtlas;
49
49
  private alphaPipe;
50
+ private alphaBGL;
50
51
  private overlayBuf;
51
52
  private overlayBG;
53
+ private overlayCap;
52
54
  private overlayScratch;
53
55
  private overlayCount;
56
+ private overlayDeferred;
54
57
  private cullPipe;
55
58
  private cullBGL;
56
59
  private cap;
@@ -96,8 +99,19 @@ export declare class GpuShadowRenderer {
96
99
  /** Dynamic scene: pre-allocate for `cap` instances; call updateInstances() each frame. */
97
100
  setCapacity(cap: number): void;
98
101
  updateInstances(instances: CubeInstance[]): void;
99
- /** Translucent overlay cubes drawn over the opaque scene each frame; `emissive` carries per-cube alpha. */
100
- updateOverlay(instances: CubeInstance[]): void;
102
+ /** Translucent overlay cubes drawn over the opaque scene; `emissive` carries per-cube alpha (0..1). The buffer
103
+ * GROWS to fit (no hard cap). Pass `eye` to SORT the cubes back-to-front (farthest first) so overlapping
104
+ * translucent cubes blend in the correct order — required for anything denser than the place ghost. By default
105
+ * the cubes draw at the end of frame(); call deferOverlay(true) + overlayPass() to draw them after your own
106
+ * transparent passes (e.g. water) so those don't paint over them. */
107
+ updateOverlay(instances: CubeInstance[], eye?: [number, number, number]): void;
108
+ /** Defer the translucent overlay out of frame() so the caller draws it via overlayPass() AFTER its own
109
+ * transparent geometry (e.g. water). Otherwise those later passes paint over the overlay. */
110
+ deferOverlay(on: boolean): void;
111
+ /** Draw the translucent overlay in its own pass over `colorView`, depth-tested (read-only) against `depthView`
112
+ * — same pipeline as the in-frame draw, just callable after other transparent passes. Pairs with
113
+ * deferOverlay(true). No-op if the overlay is empty. */
114
+ overlayPass(enc: GPUCommandEncoder, colorView: GPUTextureView, depthView: GPUTextureView): void;
101
115
  /** Write a frustum's cull uniform: 6 planes + count + this frustum's VP + occlusion params (occ on the camera pass). */
102
116
  private writeCull;
103
117
  /** The sun shadow-map depth view + its comparison sampler + texel size. Feed these into GpuPropsRenderer/
package/dist/gpuShadow.js CHANGED
@@ -145,9 +145,18 @@ fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
145
145
  let t1 = vec2<i32>(floor(clamp(mx, vec2<f32>(0.0), vec2<f32>(1.0)) * dim));
146
146
  // only query a bounded footprint; a huge on-screen box just isn't occlusion-tested (kept)
147
147
  if (t1.x - t0.x <= 8 && t1.y - t0.y <= 8) {
148
+ // DILATE the query by 2 Hi-Z texels. hiz is LAST frame's depth; under camera motion a cube reprojects by
149
+ // a few source pixels between frames, so a cube sitting at an occluder's silhouette gets tested against
150
+ // stale NEAR depth from where the occluder used to be and is wrongly culled — it blinks out for a frame
151
+ // (worst on thin/sparse geometry, but a one-frame hole in dense terrain too). Taking MAX over a dilated
152
+ // footprint is strictly MORE conservative (it can only raise farOccluder ⇒ only ever KEEP more cubes), so it
153
+ // never hides real geometry; it just leaves a couple of deeply-hidden cubes unculled. 2×8px of slack.
154
+ let lim = vec2<i32>(i32(cc.occ.y) - 1, i32(cc.occ.z) - 1);
155
+ let q0 = max(t0 - vec2<i32>(2, 2), vec2<i32>(0, 0));
156
+ let q1 = min(t1 + vec2<i32>(2, 2), lim);
148
157
  var farOccluder = 0.0; // MAX over the footprint of the coarse max-depth
149
- for (var y = t0.y; y <= t1.y; y++) {
150
- for (var x = t0.x; x <= t1.x; x++) {
158
+ for (var y = q0.y; y <= q1.y; y++) {
159
+ for (var x = q0.x; x <= q1.x; x++) {
151
160
  farOccluder = max(farOccluder, textureLoad(hiz, vec2<i32>(x, y), 0).r);
152
161
  }
153
162
  }
@@ -341,10 +350,13 @@ export class GpuShadowRenderer {
341
350
  tileRectBuf;
342
351
  useAtlas = 0;
343
352
  alphaPipe;
353
+ alphaBGL; // kept so the overlay buffer can grow and rebind on demand
344
354
  overlayBuf;
345
355
  overlayBG;
356
+ overlayCap = OVERLAY_CAP; // grows to fit — the overlay is no longer hard-capped
346
357
  overlayScratch = new Float32Array(OVERLAY_CAP * INST_F);
347
358
  overlayCount = 0;
359
+ overlayDeferred = false; // when true, frame() does NOT draw the overlay — the caller draws it via overlayPass()
348
360
  // GPU-driven culling: one compute pipeline; per-frustum (camera + light) indirect args + compacted visible lists.
349
361
  cullPipe;
350
362
  cullBGL;
@@ -473,7 +485,7 @@ export class GpuShadowRenderer {
473
485
  });
474
486
  // translucent overlay: alpha-blended, depth-tested but not depth-writing (drawn after opaque)
475
487
  const alphaMod = device.createShaderModule({ code: ALPHA_WGSL });
476
- const alphaBGL = device.createBindGroupLayout({
488
+ const alphaBGL = (this.alphaBGL = device.createBindGroupLayout({
477
489
  entries: [
478
490
  {
479
491
  binding: 0,
@@ -486,7 +498,7 @@ export class GpuShadowRenderer {
486
498
  buffer: { type: 'read-only-storage' },
487
499
  },
488
500
  ],
489
- });
501
+ }));
490
502
  this.alphaPipe = device.createRenderPipeline({
491
503
  layout: device.createPipelineLayout({ bindGroupLayouts: [alphaBGL] }),
492
504
  vertex: { module: alphaMod, entryPoint: 'vs' },
@@ -791,14 +803,67 @@ export class GpuShadowRenderer {
791
803
  this.device.queue.writeBuffer(this.instBuf, 0, this.scratch, 0, n * INST_F);
792
804
  this.count = n;
793
805
  }
794
- /** Translucent overlay cubes drawn over the opaque scene each frame; `emissive` carries per-cube alpha. */
795
- updateOverlay(instances) {
796
- const n = Math.min(instances.length, OVERLAY_CAP);
797
- this.fill(this.overlayScratch, instances, n);
806
+ /** Translucent overlay cubes drawn over the opaque scene; `emissive` carries per-cube alpha (0..1). The buffer
807
+ * GROWS to fit (no hard cap). Pass `eye` to SORT the cubes back-to-front (farthest first) so overlapping
808
+ * translucent cubes blend in the correct order — required for anything denser than the place ghost. By default
809
+ * the cubes draw at the end of frame(); call deferOverlay(true) + overlayPass() to draw them after your own
810
+ * transparent passes (e.g. water) so those don't paint over them. */
811
+ updateOverlay(instances, eye) {
812
+ let list = instances;
813
+ if (eye && instances.length > 1) {
814
+ // back-to-front: a nearer translucent cube must blend OVER a farther one, so draw the farthest first
815
+ list = instances
816
+ .map((c) => ({
817
+ c,
818
+ d: (c.x - eye[0]) * (c.x - eye[0]) +
819
+ (c.y - eye[1]) * (c.y - eye[1]) +
820
+ (c.z - eye[2]) * (c.z - eye[2]),
821
+ }))
822
+ .sort((a, b) => b.d - a.d)
823
+ .map((o) => o.c);
824
+ }
825
+ const n = list.length;
826
+ if (n > this.overlayCap) {
827
+ this.overlayCap = Math.ceil(n * 1.5);
828
+ this.overlayScratch = new Float32Array(this.overlayCap * INST_F);
829
+ this.overlayBuf.destroy();
830
+ this.overlayBuf = this.device.createBuffer({
831
+ size: this.overlayScratch.byteLength,
832
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
833
+ });
834
+ this.overlayBG = this.device.createBindGroup({
835
+ layout: this.alphaBGL,
836
+ entries: [
837
+ { binding: 0, resource: { buffer: this.camBuf } },
838
+ { binding: 1, resource: { buffer: this.overlayBuf } },
839
+ ],
840
+ });
841
+ }
842
+ this.fill(this.overlayScratch, list, n);
798
843
  if (n > 0)
799
844
  this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n * INST_F);
800
845
  this.overlayCount = n;
801
846
  }
847
+ /** Defer the translucent overlay out of frame() so the caller draws it via overlayPass() AFTER its own
848
+ * transparent geometry (e.g. water). Otherwise those later passes paint over the overlay. */
849
+ deferOverlay(on) {
850
+ this.overlayDeferred = on;
851
+ }
852
+ /** Draw the translucent overlay in its own pass over `colorView`, depth-tested (read-only) against `depthView`
853
+ * — same pipeline as the in-frame draw, just callable after other transparent passes. Pairs with
854
+ * deferOverlay(true). No-op if the overlay is empty. */
855
+ overlayPass(enc, colorView, depthView) {
856
+ if (this.overlayCount === 0)
857
+ return;
858
+ const mp = enc.beginRenderPass({
859
+ colorAttachments: [{ view: colorView, loadOp: 'load', storeOp: 'store' }],
860
+ depthStencilAttachment: { view: depthView, depthReadOnly: true },
861
+ });
862
+ mp.setPipeline(this.alphaPipe);
863
+ mp.setBindGroup(0, this.overlayBG);
864
+ mp.draw(36, this.overlayCount);
865
+ mp.end();
866
+ }
802
867
  /** Write a frustum's cull uniform: 6 planes + count + this frustum's VP + occlusion params (occ on the camera pass). */
803
868
  writeCull(buf, vp, occlude) {
804
869
  const planes = frustumPlanes(vp);
@@ -914,11 +979,11 @@ export class GpuShadowRenderer {
914
979
  mp.setPipeline(this.mainPipe);
915
980
  mp.setBindGroup(0, this.mainBG);
916
981
  mp.drawIndirect(this.camArgs, 0);
917
- if (this.overlayCount > 0) {
982
+ if (!this.overlayDeferred && this.overlayCount > 0) {
918
983
  mp.setPipeline(this.alphaPipe);
919
984
  mp.setBindGroup(0, this.overlayBG);
920
985
  mp.draw(36, this.overlayCount);
921
- } // translucent overlay last (uncapped, no cull)
986
+ } // translucent overlay last (no cull) — unless deferred to overlayPass() (drawn after the caller's water etc.)
922
987
  mp.end();
923
988
  // Rebuild the coarse Hi-Z from the depth just written → next frame's cull uses it.
924
989
  if (occlusion) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-game",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {