ugly-game 0.5.8 → 0.5.9
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 +21 -3
- package/dist/gpuShadow.js +150 -19
- package/package.json +1 -1
package/dist/gpuShadow.d.ts
CHANGED
|
@@ -52,9 +52,21 @@ export declare class GpuShadowRenderer {
|
|
|
52
52
|
camVisible: number;
|
|
53
53
|
lightVisible: number;
|
|
54
54
|
};
|
|
55
|
+
private hizPipe;
|
|
56
|
+
private hizBGL;
|
|
57
|
+
private hizTex;
|
|
58
|
+
private hizView;
|
|
59
|
+
private hizParam;
|
|
60
|
+
private hizW;
|
|
61
|
+
private hizH;
|
|
62
|
+
private hizReady;
|
|
63
|
+
private occView;
|
|
55
64
|
constructor(device: GPUDevice, format: GPUTextureFormat);
|
|
56
65
|
private makeCullBuf;
|
|
66
|
+
private cullBG;
|
|
57
67
|
private makeCullResources;
|
|
68
|
+
/** (Re)create the coarse Hi-Z for a `w×h` depth target and rebind the cull groups to it. Idempotent per size. */
|
|
69
|
+
private ensureHiZ;
|
|
58
70
|
private scratch;
|
|
59
71
|
private fill;
|
|
60
72
|
private makeBindGroups;
|
|
@@ -65,10 +77,16 @@ export declare class GpuShadowRenderer {
|
|
|
65
77
|
updateInstances(instances: CubeInstance[]): void;
|
|
66
78
|
/** Translucent overlay cubes drawn over the opaque scene each frame; `emissive` carries per-cube alpha. */
|
|
67
79
|
updateOverlay(instances: CubeInstance[]): void;
|
|
68
|
-
/** Write a frustum's cull uniform: 6
|
|
80
|
+
/** Write a frustum's cull uniform: 6 planes + count + this frustum's VP + occlusion params (occ on the camera pass). */
|
|
69
81
|
private writeCull;
|
|
70
|
-
/** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction.
|
|
71
|
-
|
|
82
|
+
/** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. Pass `occlusion` (the depth target +
|
|
83
|
+
* its size) to enable Hi-Z occlusion culling on the camera pass — the renderer rebuilds a coarse Hi-Z from that
|
|
84
|
+
* depth each frame and the NEXT frame's cull uses it (hole-free: the first frame + any resize skip occlusion). */
|
|
85
|
+
frame(enc: GPUCommandEncoder, colorView: GPUTextureView, sceneDepth: GPUTextureView, camVP: Float32Array, lightVP: Float32Array, sunDir: [number, number, number], sunCol: [number, number, number], ambient: number, load?: boolean, occlusion?: {
|
|
86
|
+
depth: GPUTextureView;
|
|
87
|
+
w: number;
|
|
88
|
+
h: number;
|
|
89
|
+
}): void;
|
|
72
90
|
/** Read back the last frame's GPU cull result (camera-visible, light-visible). Does its OWN copy+submit so it's
|
|
73
91
|
* safe to call from a live render loop (no per-frame readback cost, no race with the ongoing frames). IR/tests. */
|
|
74
92
|
readCullStats(): Promise<{
|
package/dist/gpuShadow.js
CHANGED
|
@@ -77,14 +77,25 @@ fn hsv(hh: f32) -> vec3<f32> {
|
|
|
77
77
|
// instance quaternion). This is exact for any size/aspect/rotation — critical because a bounding SPHERE from the
|
|
78
78
|
// centre wrongly culls a big flat cube (a ground plane whose centre sits far behind the camera while the ground
|
|
79
79
|
// underfoot is plainly in view). Mirrors gpuDriven.ts; validated against a CPU box-vs-plane oracle.
|
|
80
|
+
// OCCLUSION CULLING (Hi-Z). After the frustum test, an in-frustum cube is ALSO dropped if it's fully hidden behind
|
|
81
|
+
// nearer geometry. `hiz` is a coarse MAX-depth buffer (each texel = the farthest visible NDC depth over its block)
|
|
82
|
+
// built from the PREVIOUS frame's depth. A cube is occluded iff its nearest corner is behind the farthest occluder
|
|
83
|
+
// across its whole screen footprint. Using last frame's depth + a coarse buffer makes this HOLE-FREE by
|
|
84
|
+
// construction: a newly-disoccluded area has far/cleared depth so nothing there is culled — the worst case is
|
|
85
|
+
// under-culling for one frame, never a false cull. occ.x toggles it (camera pass only; the shadow pass never
|
|
86
|
+
// occludes — an off-screen occluder still casts a shadow into view). occ.yz = hiz dims, occ.w = pixels/texel.
|
|
80
87
|
const CULL_WGSL = /* wgsl */ `
|
|
81
88
|
struct Inst { pos: vec4<f32>, scl: vec4<f32>, rot: vec4<f32> };
|
|
82
89
|
struct Args { vc: u32, ic: atomic<u32>, fv: u32, fi: u32 };
|
|
83
|
-
struct CullCam {
|
|
90
|
+
struct CullCam {
|
|
91
|
+
planes: array<vec4<f32>, 6>, cnt: vec4<f32>, // cnt.x = instance count
|
|
92
|
+
camVP: mat4x4<f32>, occ: vec4<f32>, // occ.x = occlusion on · occ.yz = hiz w,h · occ.w = downscale
|
|
93
|
+
};
|
|
84
94
|
@group(0) @binding(0) var<storage, read> insts: array<Inst>;
|
|
85
95
|
@group(0) @binding(1) var<storage, read_write> args: Args;
|
|
86
96
|
@group(0) @binding(2) var<storage, read_write> visible: array<u32>;
|
|
87
97
|
@group(0) @binding(3) var<uniform> cc: CullCam;
|
|
98
|
+
@group(0) @binding(4) var hiz: texture_2d<f32>;
|
|
88
99
|
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
100
|
@compute @workgroup_size(${CULL_WG})
|
|
90
101
|
fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
@@ -101,10 +112,65 @@ fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
101
112
|
let e = abs(dot(pl.xyz, bx)) + abs(dot(pl.xyz, by)) + abs(dot(pl.xyz, bz)); // box half-width along the normal
|
|
102
113
|
if (dot(pl.xyz, c) + pl.w < -e) { return; }
|
|
103
114
|
}
|
|
115
|
+
// ── Hi-Z occlusion (camera pass only) ──
|
|
116
|
+
if (cc.occ.x > 0.5) {
|
|
117
|
+
var mn = vec2<f32>(1e9, 1e9);
|
|
118
|
+
var mx = vec2<f32>(-1e9, -1e9);
|
|
119
|
+
var nearZ = 1e9;
|
|
120
|
+
var ok = true;
|
|
121
|
+
for (var ci = 0u; ci < 8u; ci++) {
|
|
122
|
+
let s = vec3<f32>(select(-1.0, 1.0, (ci & 1u) != 0u), select(-1.0, 1.0, (ci & 2u) != 0u), select(-1.0, 1.0, (ci & 4u) != 0u));
|
|
123
|
+
let corner = c + bx * s.x + by * s.y + bz * s.z;
|
|
124
|
+
let clip = cc.camVP * vec4<f32>(corner, 1.0);
|
|
125
|
+
if (clip.w <= 0.0) { ok = false; break; } // a corner at/behind the eye → don't occlusion-cull (keep)
|
|
126
|
+
let ndc = clip.xyz / clip.w;
|
|
127
|
+
let uv = ndc.xy * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5, 0.5);
|
|
128
|
+
mn = min(mn, uv); mx = max(mx, uv);
|
|
129
|
+
nearZ = min(nearZ, ndc.z);
|
|
130
|
+
}
|
|
131
|
+
if (ok && nearZ >= 0.0) {
|
|
132
|
+
let dim = vec2<f32>(cc.occ.y, cc.occ.z);
|
|
133
|
+
let t0 = vec2<i32>(floor(clamp(mn, vec2<f32>(0.0), vec2<f32>(1.0)) * dim));
|
|
134
|
+
let t1 = vec2<i32>(floor(clamp(mx, vec2<f32>(0.0), vec2<f32>(1.0)) * dim));
|
|
135
|
+
// only query a bounded footprint; a huge on-screen box just isn't occlusion-tested (kept)
|
|
136
|
+
if (t1.x - t0.x <= 8 && t1.y - t0.y <= 8) {
|
|
137
|
+
var farOccluder = 0.0; // MAX over the footprint of the coarse max-depth
|
|
138
|
+
for (var y = t0.y; y <= t1.y; y++) {
|
|
139
|
+
for (var x = t0.x; x <= t1.x; x++) {
|
|
140
|
+
farOccluder = max(farOccluder, textureLoad(hiz, vec2<i32>(x, y), 0).r);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (nearZ > farOccluder) { return; } // behind the farthest occluder everywhere → occluded
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
104
147
|
let slot = atomicAdd(&args.ic, 1u);
|
|
105
148
|
visible[slot] = i;
|
|
106
149
|
}
|
|
107
150
|
`;
|
|
151
|
+
// Hi-Z BUILD: max-reduce the previous frame's depth (a sampleable depth texture) into a coarse R32float buffer —
|
|
152
|
+
// each output texel = the FARTHEST (max) NDC depth over its `down`×`down` source block, so "cube behind this" is a
|
|
153
|
+
// safe occlusion test. depth24plus reads as a depth texture; textureLoad returns the stored [0,1] depth.
|
|
154
|
+
const HIZ_WGSL = /* wgsl */ `
|
|
155
|
+
@group(0) @binding(0) var srcDepth: texture_depth_2d;
|
|
156
|
+
@group(0) @binding(1) var dst: texture_storage_2d<r32float, write>;
|
|
157
|
+
struct P { dim: vec4<u32> }; // dim.xy = dst size, dim.zw = src size
|
|
158
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
159
|
+
@compute @workgroup_size(8, 8)
|
|
160
|
+
fn build(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
161
|
+
if (gid.x >= p.dim.x || gid.y >= p.dim.y) { return; }
|
|
162
|
+
let down = 8u;
|
|
163
|
+
var mx = 0.0;
|
|
164
|
+
for (var dy = 0u; dy < down; dy++) {
|
|
165
|
+
for (var dx = 0u; dx < down; dx++) {
|
|
166
|
+
let sx = gid.x * down + dx; let sy = gid.y * down + dy;
|
|
167
|
+
if (sx < p.dim.z && sy < p.dim.w) { mx = max(mx, textureLoad(srcDepth, vec2<i32>(i32(sx), i32(sy)), 0)); }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
textureStore(dst, vec2<i32>(i32(gid.x), i32(gid.y)), vec4<f32>(mx, 0.0, 0.0, 1.0));
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
const HIZ_DOWN = 8; // source pixels per Hi-Z texel (coarse ⇒ conservative, cheap)
|
|
108
174
|
const SHADOW_WGSL = COMMON_WGSL + /* wgsl */ `
|
|
109
175
|
@group(0) @binding(2) var<storage, read> vis: array<u32>;
|
|
110
176
|
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> @builtin(position) vec4<f32> {
|
|
@@ -232,6 +298,16 @@ export class GpuShadowRenderer {
|
|
|
232
298
|
statsBuf; // readback: [camVisible, lightVisible] for the IR/tests
|
|
233
299
|
/** Last frame's visible counts (main-pass camera cull, shadow-pass light cull) + total — for IR/telemetry. */
|
|
234
300
|
lastCull = { total: 0, camVisible: 0, lightVisible: 0 };
|
|
301
|
+
// Hi-Z OCCLUSION: a coarse max-depth buffer rebuilt from each frame's depth, sampled by next frame's camera cull.
|
|
302
|
+
hizPipe;
|
|
303
|
+
hizBGL;
|
|
304
|
+
hizTex;
|
|
305
|
+
hizView;
|
|
306
|
+
hizParam;
|
|
307
|
+
hizW = 0;
|
|
308
|
+
hizH = 0;
|
|
309
|
+
hizReady = false; // a Hi-Z from a prior frame exists → safe to occlusion-test (false on frame 1 + resize)
|
|
310
|
+
occView; // the texture bound at cull binding 4 (a 1×1 dummy until occlusion is used)
|
|
235
311
|
constructor(device, format) {
|
|
236
312
|
this.device = device;
|
|
237
313
|
this.camBuf = device.createBuffer({ size: this.cam.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
@@ -293,39 +369,71 @@ export class GpuShadowRenderer {
|
|
|
293
369
|
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // insts
|
|
294
370
|
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, // args (atomic ic)
|
|
295
371
|
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, // visible list
|
|
296
|
-
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // planes + count
|
|
372
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // planes + count + camVP + occ
|
|
373
|
+
{ binding: 4, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'unfilterable-float' } }, // Hi-Z (dummy until occlusion is used)
|
|
297
374
|
] });
|
|
298
375
|
this.cullPipe = device.createComputePipeline({
|
|
299
376
|
layout: device.createPipelineLayout({ bindGroupLayouts: [this.cullBGL] }),
|
|
300
377
|
compute: { module: cullMod, entryPoint: 'cull' },
|
|
301
378
|
});
|
|
379
|
+
// Hi-Z occlusion build pipeline + a 1×1 dummy Hi-Z (bound when occlusion is off, never sampled)
|
|
380
|
+
const hizMod = device.createShaderModule({ code: HIZ_WGSL });
|
|
381
|
+
this.hizBGL = device.createBindGroupLayout({ entries: [
|
|
382
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'depth' } },
|
|
383
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, storageTexture: { access: 'write-only', format: 'r32float' } },
|
|
384
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
|
|
385
|
+
] });
|
|
386
|
+
this.hizPipe = device.createComputePipeline({
|
|
387
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.hizBGL] }),
|
|
388
|
+
compute: { module: hizMod, entryPoint: 'build' },
|
|
389
|
+
});
|
|
390
|
+
this.hizParam = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
391
|
+
const dummy = device.createTexture({ size: [1, 1], format: 'r32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING });
|
|
392
|
+
this.occView = dummy.createView();
|
|
302
393
|
}
|
|
303
394
|
// One frustum's cull resources: indirect args (drawIndirect), a compacted visible-index list sized to the
|
|
304
|
-
// instance capacity, and a
|
|
395
|
+
// instance capacity, and a uniform holding the 6 planes + instance count + camVP + occlusion params.
|
|
305
396
|
makeCullBuf() {
|
|
306
397
|
const d = this.device;
|
|
307
398
|
const args = d.createBuffer({ size: 4 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
|
|
308
399
|
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
|
-
|
|
400
|
+
const cull = d.createBuffer({ size: 6 * 16 + 16 + 64 + 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); // planes+cnt+camVP+occ = 192
|
|
401
|
+
return { args, vis, cull };
|
|
402
|
+
}
|
|
403
|
+
cullBG(args, vis, cull) {
|
|
404
|
+
return this.device.createBindGroup({ layout: this.cullBGL, entries: [
|
|
311
405
|
{ binding: 0, resource: { buffer: this.instBuf } }, { binding: 1, resource: { buffer: args } },
|
|
312
|
-
{ binding: 2, resource: { buffer: vis } }, { binding: 3, resource: { buffer: cull } }
|
|
406
|
+
{ binding: 2, resource: { buffer: vis } }, { binding: 3, resource: { buffer: cull } },
|
|
407
|
+
{ binding: 4, resource: this.occView }
|
|
313
408
|
] });
|
|
314
|
-
return { args, vis, cull, bg };
|
|
315
409
|
}
|
|
316
410
|
makeCullResources() {
|
|
317
411
|
const cam = this.makeCullBuf();
|
|
318
412
|
this.camArgs = cam.args;
|
|
319
413
|
this.camVis = cam.vis;
|
|
320
414
|
this.camCull = cam.cull;
|
|
321
|
-
this.camCullBG = cam.bg;
|
|
322
415
|
const lit = this.makeCullBuf();
|
|
323
416
|
this.lightArgs = lit.args;
|
|
324
417
|
this.lightVis = lit.vis;
|
|
325
418
|
this.lightCull = lit.cull;
|
|
326
|
-
this.
|
|
419
|
+
this.camCullBG = this.cullBG(this.camArgs, this.camVis, this.camCull);
|
|
420
|
+
this.lightCullBG = this.cullBG(this.lightArgs, this.lightVis, this.lightCull);
|
|
327
421
|
this.statsBuf = this.device.createBuffer({ size: 4 * 4 * 2, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
328
422
|
}
|
|
423
|
+
/** (Re)create the coarse Hi-Z for a `w×h` depth target and rebind the cull groups to it. Idempotent per size. */
|
|
424
|
+
ensureHiZ(w, h) {
|
|
425
|
+
const hw = Math.max(1, Math.ceil(w / HIZ_DOWN)), hh = Math.max(1, Math.ceil(h / HIZ_DOWN));
|
|
426
|
+
if (this.hizTex && this.hizW === hw && this.hizH === hh)
|
|
427
|
+
return;
|
|
428
|
+
this.hizW = hw;
|
|
429
|
+
this.hizH = hh;
|
|
430
|
+
this.hizReady = false; // fresh texture is uninitialized → skip occlusion until it's built once
|
|
431
|
+
this.hizTex = this.device.createTexture({ size: [hw, hh], format: 'r32float', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING });
|
|
432
|
+
this.hizView = this.hizTex.createView();
|
|
433
|
+
this.occView = this.hizView; // the cull now samples the real Hi-Z
|
|
434
|
+
this.camCullBG = this.cullBG(this.camArgs, this.camVis, this.camCull);
|
|
435
|
+
this.lightCullBG = this.cullBG(this.lightArgs, this.lightVis, this.lightCull);
|
|
436
|
+
}
|
|
329
437
|
scratch = new Float32Array(0);
|
|
330
438
|
fill(scratch, instances, n) {
|
|
331
439
|
for (let i = 0; i < n; i++) {
|
|
@@ -388,10 +496,10 @@ export class GpuShadowRenderer {
|
|
|
388
496
|
this.device.queue.writeBuffer(this.overlayBuf, 0, this.overlayScratch, 0, n * 12);
|
|
389
497
|
this.overlayCount = n;
|
|
390
498
|
}
|
|
391
|
-
/** Write a frustum's cull uniform: 6
|
|
392
|
-
writeCull(buf, vp) {
|
|
499
|
+
/** Write a frustum's cull uniform: 6 planes + count + this frustum's VP + occlusion params (occ on the camera pass). */
|
|
500
|
+
writeCull(buf, vp, occlude) {
|
|
393
501
|
const planes = frustumPlanes(vp);
|
|
394
|
-
const u = new Float32Array(
|
|
502
|
+
const u = new Float32Array(48); // planes[24] + cnt[4] + camVP[16] + occ[4]
|
|
395
503
|
for (let p = 0; p < 6; p++) {
|
|
396
504
|
u[p * 4] = planes[p][0];
|
|
397
505
|
u[p * 4 + 1] = planes[p][1];
|
|
@@ -399,10 +507,17 @@ export class GpuShadowRenderer {
|
|
|
399
507
|
u[p * 4 + 3] = planes[p][3];
|
|
400
508
|
}
|
|
401
509
|
u[24] = this.count;
|
|
510
|
+
u.set(vp, 28); // camVP
|
|
511
|
+
u[44] = occlude ? 1 : 0;
|
|
512
|
+
u[45] = this.hizW;
|
|
513
|
+
u[46] = this.hizH;
|
|
514
|
+
u[47] = HIZ_DOWN;
|
|
402
515
|
this.device.queue.writeBuffer(buf, 0, u);
|
|
403
516
|
}
|
|
404
|
-
/** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction.
|
|
405
|
-
|
|
517
|
+
/** camVP/lightVP: column-major Float32Array(16). sunDir: travel direction. Pass `occlusion` (the depth target +
|
|
518
|
+
* its size) to enable Hi-Z occlusion culling on the camera pass — the renderer rebuilds a coarse Hi-Z from that
|
|
519
|
+
* 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) {
|
|
406
521
|
this.cam.set(camVP, 0);
|
|
407
522
|
this.cam.set(lightVP, 16);
|
|
408
523
|
this.cam[32] = sunDir[0];
|
|
@@ -414,14 +529,17 @@ export class GpuShadowRenderer {
|
|
|
414
529
|
this.cam[38] = sunCol[2];
|
|
415
530
|
this.cam[39] = SHADOW_SIZE;
|
|
416
531
|
this.device.queue.writeBuffer(this.camBuf, 0, this.cam);
|
|
532
|
+
if (occlusion)
|
|
533
|
+
this.ensureHiZ(occlusion.w, occlusion.h);
|
|
534
|
+
const occludeNow = !!occlusion && this.hizReady; // only once a Hi-Z from a prior frame exists (never on frame 1/resize)
|
|
417
535
|
// 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
|
|
419
|
-
// survivors into a visible list + counts them in instanceCount, which the matching drawIndirect
|
|
536
|
+
// — main pass vs the camera frustum (+ Hi-Z occlusion), shadow pass vs the light frustum. Each dispatch atomically
|
|
537
|
+
// compacts its survivors into a visible list + counts them in instanceCount, which the matching drawIndirect consumes.
|
|
420
538
|
const resetArgs = new Uint32Array([36, 0, 0, 0]);
|
|
421
539
|
this.device.queue.writeBuffer(this.camArgs, 0, resetArgs);
|
|
422
540
|
this.device.queue.writeBuffer(this.lightArgs, 0, resetArgs);
|
|
423
|
-
this.writeCull(this.camCull, camVP);
|
|
424
|
-
this.writeCull(this.lightCull, lightVP);
|
|
541
|
+
this.writeCull(this.camCull, camVP, occludeNow);
|
|
542
|
+
this.writeCull(this.lightCull, lightVP, false);
|
|
425
543
|
if (this.count > 0) {
|
|
426
544
|
const cp = enc.beginComputePass();
|
|
427
545
|
cp.setPipeline(this.cullPipe);
|
|
@@ -439,7 +557,7 @@ export class GpuShadowRenderer {
|
|
|
439
557
|
sp.setBindGroup(0, this.shadowBG);
|
|
440
558
|
sp.drawIndirect(this.lightArgs, 0);
|
|
441
559
|
sp.end();
|
|
442
|
-
// Pass 2: camera, shadow-sampled (camera-frustum-culled).
|
|
560
|
+
// Pass 2: camera, shadow-sampled (camera-frustum + Hi-Z-occlusion-culled).
|
|
443
561
|
const mp = enc.beginRenderPass({
|
|
444
562
|
colorAttachments: [{ view: colorView, ...(load ? { loadOp: 'load' } : { clearValue: { r: 0.04, g: 0.05, b: 0.07, a: 1 }, loadOp: 'clear' }), storeOp: 'store' }],
|
|
445
563
|
depthStencilAttachment: { view: sceneDepth, ...(load ? { depthLoadOp: 'load' } : { depthClearValue: 1.0, depthLoadOp: 'clear' }), depthStoreOp: 'store' }
|
|
@@ -453,6 +571,19 @@ export class GpuShadowRenderer {
|
|
|
453
571
|
mp.draw(36, this.overlayCount);
|
|
454
572
|
} // translucent overlay last (uncapped, no cull)
|
|
455
573
|
mp.end();
|
|
574
|
+
// Rebuild the coarse Hi-Z from the depth just written → next frame's cull uses it.
|
|
575
|
+
if (occlusion) {
|
|
576
|
+
this.device.queue.writeBuffer(this.hizParam, 0, new Uint32Array([this.hizW, this.hizH, occlusion.w, occlusion.h]));
|
|
577
|
+
const bg = this.device.createBindGroup({ layout: this.hizBGL, entries: [
|
|
578
|
+
{ binding: 0, resource: occlusion.depth }, { binding: 1, resource: this.hizView }, { binding: 2, resource: { buffer: this.hizParam } }
|
|
579
|
+
] });
|
|
580
|
+
const hp = enc.beginComputePass();
|
|
581
|
+
hp.setPipeline(this.hizPipe);
|
|
582
|
+
hp.setBindGroup(0, bg);
|
|
583
|
+
hp.dispatchWorkgroups(Math.ceil(this.hizW / 8), Math.ceil(this.hizH / 8));
|
|
584
|
+
hp.end();
|
|
585
|
+
this.hizReady = true;
|
|
586
|
+
}
|
|
456
587
|
this.lastCull.total = this.count;
|
|
457
588
|
}
|
|
458
589
|
/** Read back the last frame's GPU cull result (camera-visible, light-visible). Does its OWN copy+submit so it's
|