ugly-game 0.7.6 → 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.
- package/dist/gpuShadow.d.ts +16 -2
- package/dist/gpuShadow.js +75 -10
- package/dist/loop/simCore.js +3 -2
- package/package.json +1 -1
package/dist/gpuShadow.d.ts
CHANGED
|
@@ -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
|
|
100
|
-
|
|
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 =
|
|
150
|
-
for (var 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
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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 (
|
|
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/dist/loop/simCore.js
CHANGED
|
@@ -59,9 +59,10 @@ export function coopStepBatch(opts) {
|
|
|
59
59
|
stepped++;
|
|
60
60
|
};
|
|
61
61
|
// STEP_BUDGET burst first (forced, deterministic — independent of the target AND of play/pause, so a harness can
|
|
62
|
-
// advance a paused world).
|
|
62
|
+
// advance a paused world). The FULL budget runs (not maxCatch-bounded): it's a caller-chosen finite count, so a
|
|
63
|
+
// replay/harness step(N) advances exactly N — dropping the remainder would make deterministic advance lossy.
|
|
63
64
|
let budget = opts.ctl.takeStepBudget();
|
|
64
|
-
while (budget-- > 0
|
|
65
|
+
while (budget-- > 0)
|
|
65
66
|
stepOnce();
|
|
66
67
|
// Then chase the target — but only while PLAYING. Pausing freezes the sim without touching the queue or budget.
|
|
67
68
|
if (opts.ctl.playing) {
|