ugly-game 0.5.5 → 0.5.7
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 +29 -3
- package/dist/gpuShadow.js +157 -7
- package/dist/mat4.d.ts +1 -1
- package/dist/net/sharedClock.d.ts +4 -2
- package/dist/net/sharedClock.js +6 -2
- package/package.json +1 -1
package/dist/gpuShadow.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/** One cube instance: world position, per-axis scale, a hue (< 0 ⇒ neutral ground), and an optional
|
|
2
|
-
* emissive flag (> 0 ⇒ the cube self-glows at full brightness, bypassing sun/shadow/AO — used for the
|
|
3
|
-
* place ghost, destroy marker, projectiles, and sparkles so effect cubes read as distinct from terrain). */
|
|
4
1
|
export interface CubeInstance {
|
|
5
2
|
x: number;
|
|
6
3
|
y: number;
|
|
@@ -37,7 +34,27 @@ export declare class GpuShadowRenderer {
|
|
|
37
34
|
private overlayBG;
|
|
38
35
|
private overlayScratch;
|
|
39
36
|
private overlayCount;
|
|
37
|
+
private cullPipe;
|
|
38
|
+
private cullBGL;
|
|
39
|
+
private cap;
|
|
40
|
+
private camArgs;
|
|
41
|
+
private camVis;
|
|
42
|
+
private camCull;
|
|
43
|
+
private camCullBG;
|
|
44
|
+
private lightArgs;
|
|
45
|
+
private lightVis;
|
|
46
|
+
private lightCull;
|
|
47
|
+
private lightCullBG;
|
|
48
|
+
private statsBuf;
|
|
49
|
+
/** Last frame's visible counts (main-pass camera cull, shadow-pass light cull) + total — for IR/telemetry. */
|
|
50
|
+
lastCull: {
|
|
51
|
+
total: number;
|
|
52
|
+
camVisible: number;
|
|
53
|
+
lightVisible: number;
|
|
54
|
+
};
|
|
40
55
|
constructor(device: GPUDevice, format: GPUTextureFormat);
|
|
56
|
+
private makeCullBuf;
|
|
57
|
+
private makeCullResources;
|
|
41
58
|
private scratch;
|
|
42
59
|
private fill;
|
|
43
60
|
private makeBindGroups;
|
|
@@ -48,6 +65,15 @@ export declare class GpuShadowRenderer {
|
|
|
48
65
|
updateInstances(instances: CubeInstance[]): void;
|
|
49
66
|
/** Translucent overlay cubes drawn over the opaque scene each frame; `emissive` carries per-cube alpha. */
|
|
50
67
|
updateOverlay(instances: CubeInstance[]): void;
|
|
68
|
+
/** Write a frustum's cull uniform: 6 Gribb-Hartmann planes from `vp` + the live instance count. */
|
|
69
|
+
private writeCull;
|
|
51
70
|
/** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. */
|
|
52
71
|
frame(enc: GPUCommandEncoder, colorView: GPUTextureView, sceneDepth: GPUTextureView, camVP: Float32Array, lightVP: Float32Array, sunDir: [number, number, number], sunCol: [number, number, number], ambient: number, load?: boolean): void;
|
|
72
|
+
/** Read back the last frame's GPU cull result (camera-visible, light-visible). Does its OWN copy+submit so it's
|
|
73
|
+
* safe to call from a live render loop (no per-frame readback cost, no race with the ongoing frames). IR/tests. */
|
|
74
|
+
readCullStats(): Promise<{
|
|
75
|
+
total: number;
|
|
76
|
+
camVisible: number;
|
|
77
|
+
lightVisible: number;
|
|
78
|
+
}>;
|
|
53
79
|
}
|
package/dist/gpuShadow.js
CHANGED
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
// Everything is a cube instance — including the ground (a big flat cube) — so one geometry
|
|
9
9
|
// and one vertex path serve both passes. Per-instance non-uniform scale (vec3) lets the
|
|
10
10
|
// ground be flat while props are boxy.
|
|
11
|
+
/** One cube instance: world position, per-axis scale, a hue (< 0 ⇒ neutral ground), and an optional
|
|
12
|
+
* emissive flag (> 0 ⇒ the cube self-glows at full brightness, bypassing sun/shadow/AO — used for the
|
|
13
|
+
* place ghost, destroy marker, projectiles, and sparkles so effect cubes read as distinct from terrain). */
|
|
14
|
+
import { frustumPlanes } from './mat4';
|
|
11
15
|
const SHADOW_SIZE = 2048;
|
|
16
|
+
const CULL_WG = 64; // compute-cull workgroup size
|
|
12
17
|
const OVERLAY_CAP = 512; // max translucent overlay cubes per frame (the place ghost + headroom)
|
|
13
18
|
// Build the 36-vertex cube (6 faces × 2 tris) in JS and inject as WGSL const arrays —
|
|
14
19
|
// bulletproof + readable vs. bit-twiddling from vertex_index. cullMode is 'none', so winding
|
|
@@ -61,19 +66,60 @@ fn hsv(hh: f32) -> vec3<f32> {
|
|
|
61
66
|
if (h < 5.0) { return vec3(x, 0.0, 1.0); } return vec3(1.0, 0.0, x);
|
|
62
67
|
}
|
|
63
68
|
`;
|
|
69
|
+
// GPU-DRIVEN CULLING. The whole instance buffer stays resident; a compute pass frustum-culls it and compacts the
|
|
70
|
+
// survivors' indices into a `visible` list + an indirect draw's instanceCount. The draw passes then index
|
|
71
|
+
// insts[visible[ii]] and drawIndirect the compacted count — so the vertex shader + rasterizer only ever touch
|
|
72
|
+
// in-frustum cubes. Two culls per frame: the MAIN pass against the CAMERA frustum, the SHADOW pass against the
|
|
73
|
+
// LIGHT (ortho) frustum, so shadows from off-screen occluders stay correct while both passes shrink.
|
|
74
|
+
//
|
|
75
|
+
// The test is an ORIENTED-BOX vs plane separating test (NOT a bounding sphere). Each cube's world extent along a
|
|
76
|
+
// plane normal n is |n·bx|+|n·by|+|n·bz|, where bx/by/bz are the box's rotated semi-axes (0.5·scl rotated by the
|
|
77
|
+
// instance quaternion). This is exact for any size/aspect/rotation — critical because a bounding SPHERE from the
|
|
78
|
+
// centre wrongly culls a big flat cube (a ground plane whose centre sits far behind the camera while the ground
|
|
79
|
+
// underfoot is plainly in view). Mirrors gpuDriven.ts; validated against a CPU box-vs-plane oracle.
|
|
80
|
+
const CULL_WGSL = /* wgsl */ `
|
|
81
|
+
struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32> };
|
|
82
|
+
struct Args { vc: u32, ic: atomic<u32>, fv: u32, fi: u32 };
|
|
83
|
+
struct CullCam { planes: array<vec4<f32>, 6>, cnt: vec4<f32> }; // cnt.x = instance count
|
|
84
|
+
@group(0) @binding(0) var<storage, read> insts: array<Inst>;
|
|
85
|
+
@group(0) @binding(1) var<storage, read_write> args: Args;
|
|
86
|
+
@group(0) @binding(2) var<storage, read_write> visible: array<u32>;
|
|
87
|
+
@group(0) @binding(3) var<uniform> cc: CullCam;
|
|
88
|
+
fn qr(q: vec4<f32>, v: vec3<f32>) -> vec3<f32> { let t = 2.0 * cross(q.xyz, v); return v + q.w * t + cross(q.xyz, t); }
|
|
89
|
+
@compute @workgroup_size(${CULL_WG})
|
|
90
|
+
fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
91
|
+
let i = gid.x;
|
|
92
|
+
if (i >= u32(cc.cnt.x)) { return; }
|
|
93
|
+
let inst = insts[i];
|
|
94
|
+
let c = inst.pos.xyz;
|
|
95
|
+
let h = 0.5 * inst.scl.xyz;
|
|
96
|
+
let bx = qr(inst.rot, vec3<f32>(h.x, 0.0, 0.0)); // rotated box semi-axes
|
|
97
|
+
let by = qr(inst.rot, vec3<f32>(0.0, h.y, 0.0));
|
|
98
|
+
let bz = qr(inst.rot, vec3<f32>(0.0, 0.0, h.z));
|
|
99
|
+
for (var p = 0u; p < 6u; p++) {
|
|
100
|
+
let pl = cc.planes[p];
|
|
101
|
+
let e = abs(dot(pl.xyz, bx)) + abs(dot(pl.xyz, by)) + abs(dot(pl.xyz, bz)); // box half-width along the normal
|
|
102
|
+
if (dot(pl.xyz, c) + pl.w < -e) { return; }
|
|
103
|
+
}
|
|
104
|
+
let slot = atomicAdd(&args.ic, 1u);
|
|
105
|
+
visible[slot] = i;
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
64
108
|
const SHADOW_WGSL = COMMON_WGSL + /* wgsl */ `
|
|
109
|
+
@group(0) @binding(2) var<storage, read> vis: array<u32>;
|
|
65
110
|
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> @builtin(position) vec4<f32> {
|
|
66
|
-
return cam.lightVP * vec4(worldOf(insts[ii], vi), 1.0);
|
|
111
|
+
return cam.lightVP * vec4(worldOf(insts[vis[ii]], vi), 1.0);
|
|
67
112
|
}
|
|
68
113
|
`;
|
|
69
114
|
const MAIN_WGSL = COMMON_WGSL + /* wgsl */ `
|
|
70
115
|
@group(0) @binding(2) var shadowMap: texture_depth_2d;
|
|
71
116
|
@group(0) @binding(3) var shadowSamp: sampler_comparison;
|
|
117
|
+
@group(0) @binding(4) var<storage, read> vis: array<u32>;
|
|
72
118
|
|
|
73
119
|
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> };
|
|
74
120
|
|
|
75
121
|
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
|
|
76
|
-
let inst = insts[ii];
|
|
122
|
+
let inst = insts[vis[ii]];
|
|
77
123
|
let world = worldOf(inst, vi);
|
|
78
124
|
var o: VOut;
|
|
79
125
|
o.clip = cam.camVP * vec4(world, 1.0);
|
|
@@ -171,6 +217,21 @@ export class GpuShadowRenderer {
|
|
|
171
217
|
overlayBG;
|
|
172
218
|
overlayScratch = new Float32Array(OVERLAY_CAP * 12);
|
|
173
219
|
overlayCount = 0;
|
|
220
|
+
// GPU-driven culling: one compute pipeline; per-frustum (camera + light) indirect args + compacted visible lists.
|
|
221
|
+
cullPipe;
|
|
222
|
+
cullBGL;
|
|
223
|
+
cap = 0;
|
|
224
|
+
camArgs;
|
|
225
|
+
camVis;
|
|
226
|
+
camCull;
|
|
227
|
+
camCullBG;
|
|
228
|
+
lightArgs;
|
|
229
|
+
lightVis;
|
|
230
|
+
lightCull;
|
|
231
|
+
lightCullBG;
|
|
232
|
+
statsBuf; // readback: [camVisible, lightVisible] for the IR/tests
|
|
233
|
+
/** Last frame's visible counts (main-pass camera cull, shadow-pass light cull) + total — for IR/telemetry. */
|
|
234
|
+
lastCull = { total: 0, camVisible: 0, lightVisible: 0 };
|
|
174
235
|
constructor(device, format) {
|
|
175
236
|
this.device = device;
|
|
176
237
|
this.camBuf = device.createBuffer({ size: this.cam.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
@@ -181,6 +242,7 @@ export class GpuShadowRenderer {
|
|
|
181
242
|
const shadowBGL = device.createBindGroupLayout({ entries: [
|
|
182
243
|
{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
|
|
183
244
|
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
245
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }, // light-visible list
|
|
184
246
|
] });
|
|
185
247
|
this.shadowPipe = device.createRenderPipeline({
|
|
186
248
|
layout: device.createPipelineLayout({ bindGroupLayouts: [shadowBGL] }),
|
|
@@ -193,6 +255,7 @@ export class GpuShadowRenderer {
|
|
|
193
255
|
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
194
256
|
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
|
|
195
257
|
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
|
|
258
|
+
{ binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }, // camera-visible list
|
|
196
259
|
] });
|
|
197
260
|
this.mainPipe = device.createRenderPipeline({
|
|
198
261
|
layout: device.createPipelineLayout({ bindGroupLayouts: [mainBGL] }),
|
|
@@ -224,6 +287,44 @@ export class GpuShadowRenderer {
|
|
|
224
287
|
this.overlayBG = device.createBindGroup({ layout: alphaBGL, entries: [
|
|
225
288
|
{ binding: 0, resource: { buffer: this.camBuf } }, { binding: 1, resource: { buffer: this.overlayBuf } }
|
|
226
289
|
] });
|
|
290
|
+
// GPU-driven cull: compute pipeline shared by the camera + light dispatches (each with its own args/vis/uniform).
|
|
291
|
+
const cullMod = device.createShaderModule({ code: CULL_WGSL });
|
|
292
|
+
this.cullBGL = device.createBindGroupLayout({ entries: [
|
|
293
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // insts
|
|
294
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, // args (atomic ic)
|
|
295
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, // visible list
|
|
296
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // planes + count
|
|
297
|
+
] });
|
|
298
|
+
this.cullPipe = device.createComputePipeline({
|
|
299
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.cullBGL] }),
|
|
300
|
+
compute: { module: cullMod, entryPoint: 'cull' },
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
// One frustum's cull resources: indirect args (drawIndirect), a compacted visible-index list sized to the
|
|
304
|
+
// instance capacity, and a small uniform holding the 6 planes + the live instance count.
|
|
305
|
+
makeCullBuf() {
|
|
306
|
+
const d = this.device;
|
|
307
|
+
const args = d.createBuffer({ size: 4 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
|
|
308
|
+
const vis = d.createBuffer({ size: Math.max(1, this.cap) * 4, usage: GPUBufferUsage.STORAGE });
|
|
309
|
+
const cull = d.createBuffer({ size: 6 * 16 + 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
310
|
+
const bg = d.createBindGroup({ layout: this.cullBGL, entries: [
|
|
311
|
+
{ binding: 0, resource: { buffer: this.instBuf } }, { binding: 1, resource: { buffer: args } },
|
|
312
|
+
{ binding: 2, resource: { buffer: vis } }, { binding: 3, resource: { buffer: cull } }
|
|
313
|
+
] });
|
|
314
|
+
return { args, vis, cull, bg };
|
|
315
|
+
}
|
|
316
|
+
makeCullResources() {
|
|
317
|
+
const cam = this.makeCullBuf();
|
|
318
|
+
this.camArgs = cam.args;
|
|
319
|
+
this.camVis = cam.vis;
|
|
320
|
+
this.camCull = cam.cull;
|
|
321
|
+
this.camCullBG = cam.bg;
|
|
322
|
+
const lit = this.makeCullBuf();
|
|
323
|
+
this.lightArgs = lit.args;
|
|
324
|
+
this.lightVis = lit.vis;
|
|
325
|
+
this.lightCull = lit.cull;
|
|
326
|
+
this.lightCullBG = lit.bg;
|
|
327
|
+
this.statsBuf = this.device.createBuffer({ size: 4 * 4 * 2, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
227
328
|
}
|
|
228
329
|
scratch = new Float32Array(0);
|
|
229
330
|
fill(scratch, instances, n) {
|
|
@@ -244,17 +345,21 @@ export class GpuShadowRenderer {
|
|
|
244
345
|
}
|
|
245
346
|
}
|
|
246
347
|
makeBindGroups() {
|
|
348
|
+
this.makeCullResources(); // (re)create the cull buffers against the fresh instBuf/cap first — the draw passes bind their visible lists
|
|
247
349
|
this.shadowBG = this.device.createBindGroup({ layout: this.shadowBGL, entries: [
|
|
248
350
|
{ binding: 0, resource: { buffer: this.camBuf } }, { binding: 1, resource: { buffer: this.instBuf } },
|
|
351
|
+
{ binding: 2, resource: { buffer: this.lightVis } },
|
|
249
352
|
] });
|
|
250
353
|
this.mainBG = this.device.createBindGroup({ layout: this.mainBGL, entries: [
|
|
251
354
|
{ binding: 0, resource: { buffer: this.camBuf } }, { binding: 1, resource: { buffer: this.instBuf } },
|
|
252
355
|
{ binding: 2, resource: this.shadowView }, { binding: 3, resource: this.shadowSampler },
|
|
356
|
+
{ binding: 4, resource: { buffer: this.camVis } },
|
|
253
357
|
] });
|
|
254
358
|
}
|
|
255
359
|
/** Static scene: sizes the buffer to exactly these instances. */
|
|
256
360
|
setScene(instances) {
|
|
257
361
|
this.count = instances.length;
|
|
362
|
+
this.cap = this.count;
|
|
258
363
|
this.scratch = new Float32Array(this.count * 12);
|
|
259
364
|
this.fill(this.scratch, instances, this.count);
|
|
260
365
|
this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
@@ -263,6 +368,7 @@ export class GpuShadowRenderer {
|
|
|
263
368
|
}
|
|
264
369
|
/** Dynamic scene: pre-allocate for `cap` instances; call updateInstances() each frame. */
|
|
265
370
|
setCapacity(cap) {
|
|
371
|
+
this.cap = cap;
|
|
266
372
|
this.scratch = new Float32Array(cap * 12);
|
|
267
373
|
this.instBuf = this.device.createBuffer({ size: this.scratch.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
268
374
|
this.count = 0;
|
|
@@ -282,6 +388,19 @@ export class GpuShadowRenderer {
|
|
|
282
388
|
this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n * 12);
|
|
283
389
|
this.overlayCount = n;
|
|
284
390
|
}
|
|
391
|
+
/** Write a frustum's cull uniform: 6 Gribb-Hartmann planes from `vp` + the live instance count. */
|
|
392
|
+
writeCull(buf, vp) {
|
|
393
|
+
const planes = frustumPlanes(vp);
|
|
394
|
+
const u = new Float32Array(6 * 4 + 4);
|
|
395
|
+
for (let p = 0; p < 6; p++) {
|
|
396
|
+
u[p * 4] = planes[p][0];
|
|
397
|
+
u[p * 4 + 1] = planes[p][1];
|
|
398
|
+
u[p * 4 + 2] = planes[p][2];
|
|
399
|
+
u[p * 4 + 3] = planes[p][3];
|
|
400
|
+
}
|
|
401
|
+
u[24] = this.count;
|
|
402
|
+
this.device.queue.writeBuffer(buf, 0, u);
|
|
403
|
+
}
|
|
285
404
|
/** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. */
|
|
286
405
|
frame(enc, colorView, sceneDepth, camVP, lightVP, sunDir, sunCol, ambient, load = false) {
|
|
287
406
|
this.cam.set(camVP, 0);
|
|
@@ -295,27 +414,58 @@ export class GpuShadowRenderer {
|
|
|
295
414
|
this.cam[38] = sunCol[2];
|
|
296
415
|
this.cam[39] = SHADOW_SIZE;
|
|
297
416
|
this.device.queue.writeBuffer(this.camBuf, 0, this.cam);
|
|
298
|
-
//
|
|
417
|
+
// GPU-DRIVEN CULL: reset each frustum's indirect args ([vertexCount=36, instanceCount=0, 0, 0]), then compute-cull
|
|
418
|
+
// — main pass vs the camera frustum, shadow pass vs the light frustum. Each dispatch atomically compacts its
|
|
419
|
+
// survivors into a visible list + counts them in instanceCount, which the matching drawIndirect then consumes.
|
|
420
|
+
const resetArgs = new Uint32Array([36, 0, 0, 0]);
|
|
421
|
+
this.device.queue.writeBuffer(this.camArgs, 0, resetArgs);
|
|
422
|
+
this.device.queue.writeBuffer(this.lightArgs, 0, resetArgs);
|
|
423
|
+
this.writeCull(this.camCull, camVP);
|
|
424
|
+
this.writeCull(this.lightCull, lightVP);
|
|
425
|
+
if (this.count > 0) {
|
|
426
|
+
const cp = enc.beginComputePass();
|
|
427
|
+
cp.setPipeline(this.cullPipe);
|
|
428
|
+
cp.setBindGroup(0, this.camCullBG);
|
|
429
|
+
cp.dispatchWorkgroups(Math.ceil(this.count / CULL_WG));
|
|
430
|
+
cp.setBindGroup(0, this.lightCullBG);
|
|
431
|
+
cp.dispatchWorkgroups(Math.ceil(this.count / CULL_WG));
|
|
432
|
+
cp.end();
|
|
433
|
+
}
|
|
434
|
+
// Pass 1: occluder depth from the sun (light-frustum-culled).
|
|
299
435
|
const sp = enc.beginRenderPass({ colorAttachments: [], depthStencilAttachment: {
|
|
300
436
|
view: this.shadowView, depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store'
|
|
301
437
|
} });
|
|
302
438
|
sp.setPipeline(this.shadowPipe);
|
|
303
439
|
sp.setBindGroup(0, this.shadowBG);
|
|
304
|
-
sp.
|
|
440
|
+
sp.drawIndirect(this.lightArgs, 0);
|
|
305
441
|
sp.end();
|
|
306
|
-
// Pass 2: camera, shadow-sampled.
|
|
442
|
+
// Pass 2: camera, shadow-sampled (camera-frustum-culled).
|
|
307
443
|
const mp = enc.beginRenderPass({
|
|
308
444
|
colorAttachments: [{ view: colorView, ...(load ? { loadOp: 'load' } : { clearValue: { r: 0.04, g: 0.05, b: 0.07, a: 1 }, loadOp: 'clear' }), storeOp: 'store' }],
|
|
309
445
|
depthStencilAttachment: { view: sceneDepth, ...(load ? { depthLoadOp: 'load' } : { depthClearValue: 1.0, depthLoadOp: 'clear' }), depthStoreOp: 'store' }
|
|
310
446
|
});
|
|
311
447
|
mp.setPipeline(this.mainPipe);
|
|
312
448
|
mp.setBindGroup(0, this.mainBG);
|
|
313
|
-
mp.
|
|
449
|
+
mp.drawIndirect(this.camArgs, 0);
|
|
314
450
|
if (this.overlayCount > 0) {
|
|
315
451
|
mp.setPipeline(this.alphaPipe);
|
|
316
452
|
mp.setBindGroup(0, this.overlayBG);
|
|
317
453
|
mp.draw(36, this.overlayCount);
|
|
318
|
-
} // translucent overlay last
|
|
454
|
+
} // translucent overlay last (uncapped, no cull)
|
|
319
455
|
mp.end();
|
|
456
|
+
this.lastCull.total = this.count;
|
|
457
|
+
}
|
|
458
|
+
/** Read back the last frame's GPU cull result (camera-visible, light-visible). Does its OWN copy+submit so it's
|
|
459
|
+
* safe to call from a live render loop (no per-frame readback cost, no race with the ongoing frames). IR/tests. */
|
|
460
|
+
async readCullStats() {
|
|
461
|
+
const enc = this.device.createCommandEncoder();
|
|
462
|
+
enc.copyBufferToBuffer(this.camArgs, 0, this.statsBuf, 0, 16); // args[1] = instanceCount (the visible count)
|
|
463
|
+
enc.copyBufferToBuffer(this.lightArgs, 0, this.statsBuf, 16, 16);
|
|
464
|
+
this.device.queue.submit([enc.finish()]);
|
|
465
|
+
await this.statsBuf.mapAsync(GPUMapMode.READ);
|
|
466
|
+
const a = new Uint32Array(this.statsBuf.getMappedRange().slice(0));
|
|
467
|
+
this.statsBuf.unmap();
|
|
468
|
+
this.lastCull = { total: this.count, camVisible: a[1], lightVisible: a[5] };
|
|
469
|
+
return this.lastCull;
|
|
320
470
|
}
|
|
321
471
|
}
|
package/dist/mat4.d.ts
CHANGED
|
@@ -12,6 +12,6 @@ export declare function ortho(l: number, r: number, b: number, t: number, n: num
|
|
|
12
12
|
export declare function lightViewProjection(dir: Vec3, center: Vec3, extent: number, depth: number): Mat4;
|
|
13
13
|
export type Plane = readonly [number, number, number, number];
|
|
14
14
|
/** Extract + normalize the 6 frustum planes from a column-major view-proj (Gribb-Hartmann). */
|
|
15
|
-
export declare function frustumPlanes(m:
|
|
15
|
+
export declare function frustumPlanes(m: ArrayLike<number>): Plane[];
|
|
16
16
|
/** Sphere vs frustum: false only if the sphere is fully outside any plane. */
|
|
17
17
|
export declare function sphereVisible(planes: Plane[], x: number, y: number, z: number, radius: number): boolean;
|
|
@@ -7,8 +7,10 @@ export declare class SharedClock {
|
|
|
7
7
|
private pinned;
|
|
8
8
|
constructor(tickMs: number, // ms per tick (e.g. 1000/60)
|
|
9
9
|
now: () => number);
|
|
10
|
-
/** From the server `hello`.
|
|
11
|
-
|
|
10
|
+
/** From the server `hello`. Pass the server's `now` to seed a PROVISIONAL offset so `targetTick()` is roughly
|
|
11
|
+
* right immediately — the window between joining and the first ping/pong returning is exactly when lockstep
|
|
12
|
+
* clients stamp their first commands, and an offset of 0 there desyncs them. A real ping sample overrides it. */
|
|
13
|
+
setEpoch(epoch: number, serverNow?: number): void;
|
|
12
14
|
get hasEpoch(): boolean;
|
|
13
15
|
get epochMs(): number;
|
|
14
16
|
/** Feed a ping round-trip: `sentAt` = local ms we sent, `serverNow` = server ms in the pong. Keeps the sample
|
package/dist/net/sharedClock.js
CHANGED
|
@@ -17,10 +17,14 @@ export class SharedClock {
|
|
|
17
17
|
this.tickMs = tickMs;
|
|
18
18
|
this.now = now;
|
|
19
19
|
}
|
|
20
|
-
/** From the server `hello`.
|
|
21
|
-
|
|
20
|
+
/** From the server `hello`. Pass the server's `now` to seed a PROVISIONAL offset so `targetTick()` is roughly
|
|
21
|
+
* right immediately — the window between joining and the first ping/pong returning is exactly when lockstep
|
|
22
|
+
* clients stamp their first commands, and an offset of 0 there desyncs them. A real ping sample overrides it. */
|
|
23
|
+
setEpoch(epoch, serverNow) {
|
|
22
24
|
this.epoch = epoch;
|
|
23
25
|
this.pinned = true;
|
|
26
|
+
if (serverNow !== undefined && this.bestRtt === Infinity)
|
|
27
|
+
this.offset = serverNow - this.now();
|
|
24
28
|
}
|
|
25
29
|
get hasEpoch() {
|
|
26
30
|
return this.pinned;
|