ugly-game 0.2.0 → 0.4.0
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/assets/anim.d.ts +13 -0
- package/dist/assets/anim.js +55 -0
- package/dist/assets/animMoat.d.ts +36 -0
- package/dist/assets/animMoat.js +55 -0
- package/dist/assets/archetype.d.ts +20 -0
- package/dist/assets/archetype.js +8 -0
- package/dist/assets/assemble.d.ts +26 -0
- package/dist/assets/assemble.js +92 -0
- package/dist/assets/genome.d.ts +33 -0
- package/dist/assets/genome.js +55 -0
- package/dist/assets/genomeExplore.d.ts +28 -0
- package/dist/assets/genomeExplore.js +100 -0
- package/dist/assets/geometry.d.ts +23 -0
- package/dist/assets/geometry.js +134 -0
- package/dist/assets/gpuSkin.d.ts +21 -0
- package/dist/assets/gpuSkin.js +148 -0
- package/dist/assets/lod.d.ts +2 -0
- package/dist/assets/lod.js +18 -0
- package/dist/assets/meshlet.d.ts +23 -0
- package/dist/assets/meshlet.js +53 -0
- package/dist/assets/morph.d.ts +5 -0
- package/dist/assets/morph.js +75 -0
- package/dist/assets/partkit.d.ts +28 -0
- package/dist/assets/partkit.js +21 -0
- package/dist/assets/pose.d.ts +26 -0
- package/dist/assets/pose.js +208 -0
- package/dist/assets/renderAssets.d.ts +37 -0
- package/dist/assets/renderAssets.js +54 -0
- package/dist/assets/residency.d.ts +13 -0
- package/dist/assets/residency.js +45 -0
- package/dist/assets/retarget.d.ts +11 -0
- package/dist/assets/retarget.js +37 -0
- package/dist/assets/rig.d.ts +13 -0
- package/dist/assets/rig.js +93 -0
- package/dist/assets/select.d.ts +10 -0
- package/dist/assets/select.js +24 -0
- package/dist/assets/stream.d.ts +12 -0
- package/dist/assets/stream.js +32 -0
- package/dist/assets/style.d.ts +10 -0
- package/dist/assets/style.js +17 -0
- package/dist/assets/ugm.d.ts +53 -0
- package/dist/assets/ugm.js +100 -0
- package/dist/gpuDriven.d.ts +39 -0
- package/dist/gpuDriven.js +197 -0
- package/dist/gpuGI.d.ts +39 -0
- package/dist/gpuGI.js +253 -0
- package/dist/gpuGraph.d.ts +28 -0
- package/dist/gpuGraph.js +128 -0
- package/dist/gpuLit.d.ts +40 -0
- package/dist/gpuLit.js +193 -0
- package/dist/gpuPost.d.ts +33 -0
- package/dist/gpuPost.js +162 -0
- package/dist/gpuShadowMap.d.ts +11 -0
- package/dist/gpuShadowMap.js +41 -0
- package/dist/gpuSpot.d.ts +39 -0
- package/dist/gpuSpot.js +213 -0
- package/dist/gpuTaa.d.ts +34 -0
- package/dist/gpuTaa.js +247 -0
- package/dist/gpuTerrain.d.ts +25 -0
- package/dist/gpuTerrain.js +154 -0
- package/dist/gpuTiled.d.ts +55 -0
- package/dist/gpuTiled.js +247 -0
- package/dist/gpuWorld.d.ts +49 -0
- package/dist/gpuWorld.js +309 -0
- package/dist/shaderGraph.d.ts +84 -0
- package/dist/shaderGraph.js +231 -0
- package/package.json +2 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare enum ChunkKind {
|
|
2
|
+
VTX = 1,
|
|
3
|
+
IDX = 2,
|
|
4
|
+
CLUSTER = 3,
|
|
5
|
+
MIP = 4,
|
|
6
|
+
MAT = 5,
|
|
7
|
+
SKEL = 6,
|
|
8
|
+
ANIM = 7,
|
|
9
|
+
MORPH = 8,
|
|
10
|
+
UV = 9,
|
|
11
|
+
KTX2 = 10
|
|
12
|
+
}
|
|
13
|
+
export interface ChunkIn {
|
|
14
|
+
kind: ChunkKind;
|
|
15
|
+
detailLevel: number;
|
|
16
|
+
targetId: number;
|
|
17
|
+
base: boolean;
|
|
18
|
+
bytes: Uint8Array;
|
|
19
|
+
}
|
|
20
|
+
export interface UgmMeta {
|
|
21
|
+
formatVersion: number;
|
|
22
|
+
renderIRVersion: number;
|
|
23
|
+
flags: number;
|
|
24
|
+
bounds: [number, number, number, number];
|
|
25
|
+
}
|
|
26
|
+
export declare const MAGIC = 843925333;
|
|
27
|
+
export declare const HEADER_BYTES = 64;
|
|
28
|
+
export declare const TOC_ENTRY_BYTES = 24;
|
|
29
|
+
/** fnv1a-32 over raw bytes → u32 content hash (deterministic; enables chunk dedup).
|
|
30
|
+
* Byte-wise (no String.fromCharCode spread — that overflows the stack on large chunks). */
|
|
31
|
+
export declare function hashBytes(b: Uint8Array): number;
|
|
32
|
+
export declare function packUgm(chunks: ChunkIn[], meta: UgmMeta): Uint8Array;
|
|
33
|
+
export interface TocEntry {
|
|
34
|
+
kind: ChunkKind;
|
|
35
|
+
detailLevel: number;
|
|
36
|
+
targetId: number;
|
|
37
|
+
offset: number;
|
|
38
|
+
length: number;
|
|
39
|
+
hash: number;
|
|
40
|
+
}
|
|
41
|
+
export interface UgmHeader {
|
|
42
|
+
formatVersion: number;
|
|
43
|
+
renderIRVersion: number;
|
|
44
|
+
flags: number;
|
|
45
|
+
bounds: [number, number, number, number];
|
|
46
|
+
chunkCount: number;
|
|
47
|
+
tocOffset: number;
|
|
48
|
+
basePayloadLen: number;
|
|
49
|
+
totalLen: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function readHeader(buf: Uint8Array): UgmHeader;
|
|
52
|
+
export declare function readToc(buf: Uint8Array, h: UgmHeader): TocEntry[];
|
|
53
|
+
export declare function readChunk(buf: Uint8Array, e: TocEntry): Uint8Array;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// The cooked .ugm v2 container — streamable by construction (design/asset-streaming.md §2).
|
|
2
|
+
// Layout (little-endian): [64B header][TOC: chunkCount × 24B][chunk bytes …].
|
|
3
|
+
// Chunks are sorted base-first then ascending detailLevel, so a leading byte range
|
|
4
|
+
// (0 .. basePayloadLen) is a complete coarse-renderable asset.
|
|
5
|
+
export var ChunkKind;
|
|
6
|
+
(function (ChunkKind) {
|
|
7
|
+
ChunkKind[ChunkKind["VTX"] = 1] = "VTX";
|
|
8
|
+
ChunkKind[ChunkKind["IDX"] = 2] = "IDX";
|
|
9
|
+
ChunkKind[ChunkKind["CLUSTER"] = 3] = "CLUSTER";
|
|
10
|
+
ChunkKind[ChunkKind["MIP"] = 4] = "MIP";
|
|
11
|
+
ChunkKind[ChunkKind["MAT"] = 5] = "MAT";
|
|
12
|
+
ChunkKind[ChunkKind["SKEL"] = 6] = "SKEL";
|
|
13
|
+
ChunkKind[ChunkKind["ANIM"] = 7] = "ANIM";
|
|
14
|
+
ChunkKind[ChunkKind["MORPH"] = 8] = "MORPH";
|
|
15
|
+
ChunkKind[ChunkKind["UV"] = 9] = "UV";
|
|
16
|
+
ChunkKind[ChunkKind["KTX2"] = 10] = "KTX2";
|
|
17
|
+
})(ChunkKind || (ChunkKind = {}));
|
|
18
|
+
export const MAGIC = 0x324d4755; // "UGM2" little-endian
|
|
19
|
+
export const HEADER_BYTES = 64;
|
|
20
|
+
export const TOC_ENTRY_BYTES = 24; // kind1 detail1 targetId2 offset4 length4 hash4 depMask4 pad4
|
|
21
|
+
/** fnv1a-32 over raw bytes → u32 content hash (deterministic; enables chunk dedup).
|
|
22
|
+
* Byte-wise (no String.fromCharCode spread — that overflows the stack on large chunks). */
|
|
23
|
+
export function hashBytes(b) {
|
|
24
|
+
let h = 0x811c9dc5;
|
|
25
|
+
for (const byte of b) {
|
|
26
|
+
h ^= byte;
|
|
27
|
+
h = Math.imul(h, 0x01000193);
|
|
28
|
+
}
|
|
29
|
+
return h >>> 0;
|
|
30
|
+
}
|
|
31
|
+
export function packUgm(chunks, meta) {
|
|
32
|
+
// base-first, then ascending detail — this ordering IS the coarse→fine stream order.
|
|
33
|
+
const sorted = [...chunks].sort((a, b) => (Number(b.base) - Number(a.base)) || (a.detailLevel - b.detailLevel) || (a.kind - b.kind) || (a.targetId - b.targetId));
|
|
34
|
+
const tocBytes = sorted.length * TOC_ENTRY_BYTES;
|
|
35
|
+
const dataStart = HEADER_BYTES + tocBytes;
|
|
36
|
+
let cursor = dataStart, baseEnd = dataStart;
|
|
37
|
+
const offsets = [];
|
|
38
|
+
for (const c of sorted) {
|
|
39
|
+
offsets.push(cursor);
|
|
40
|
+
cursor += c.bytes.byteLength;
|
|
41
|
+
if (c.base)
|
|
42
|
+
baseEnd = cursor;
|
|
43
|
+
}
|
|
44
|
+
const total = cursor;
|
|
45
|
+
const out = new Uint8Array(total);
|
|
46
|
+
const dv = new DataView(out.buffer);
|
|
47
|
+
// header
|
|
48
|
+
dv.setUint32(0, MAGIC, true);
|
|
49
|
+
dv.setUint16(4, meta.formatVersion, true);
|
|
50
|
+
dv.setUint16(6, meta.renderIRVersion, true);
|
|
51
|
+
dv.setUint32(8, meta.flags, true);
|
|
52
|
+
dv.setFloat32(12, meta.bounds[0], true);
|
|
53
|
+
dv.setFloat32(16, meta.bounds[1], true);
|
|
54
|
+
dv.setUint32(20, sorted.length, true); // chunkCount
|
|
55
|
+
dv.setUint32(24, HEADER_BYTES, true); // tocOffset
|
|
56
|
+
dv.setUint32(28, baseEnd, true); // basePayloadLen
|
|
57
|
+
dv.setUint32(32, total, true); // totalLen
|
|
58
|
+
dv.setFloat32(36, meta.bounds[2], true);
|
|
59
|
+
dv.setFloat32(40, meta.bounds[3], true);
|
|
60
|
+
// TOC
|
|
61
|
+
let t = HEADER_BYTES;
|
|
62
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
63
|
+
const c = sorted[i];
|
|
64
|
+
dv.setUint8(t, c.kind);
|
|
65
|
+
dv.setUint8(t + 1, c.detailLevel);
|
|
66
|
+
dv.setUint16(t + 2, c.targetId, true);
|
|
67
|
+
dv.setUint32(t + 4, offsets[i], true);
|
|
68
|
+
dv.setUint32(t + 8, c.bytes.byteLength, true);
|
|
69
|
+
dv.setUint32(t + 12, hashBytes(c.bytes), true);
|
|
70
|
+
dv.setUint32(t + 16, 0, true); // depMask reserved
|
|
71
|
+
t += TOC_ENTRY_BYTES;
|
|
72
|
+
}
|
|
73
|
+
// chunk bytes
|
|
74
|
+
for (let i = 0; i < sorted.length; i++)
|
|
75
|
+
out.set(sorted[i].bytes, offsets[i]);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function dvOf(buf) { return new DataView(buf.buffer, buf.byteOffset, buf.byteLength); }
|
|
79
|
+
export function readHeader(buf) {
|
|
80
|
+
const dv = dvOf(buf);
|
|
81
|
+
if (dv.getUint32(0, true) !== MAGIC)
|
|
82
|
+
throw new Error('.ugm: bad magic');
|
|
83
|
+
return {
|
|
84
|
+
formatVersion: dv.getUint16(4, true), renderIRVersion: dv.getUint16(6, true), flags: dv.getUint32(8, true),
|
|
85
|
+
bounds: [dv.getFloat32(12, true), dv.getFloat32(16, true), dv.getFloat32(36, true), dv.getFloat32(40, true)],
|
|
86
|
+
chunkCount: dv.getUint32(20, true), tocOffset: dv.getUint32(24, true),
|
|
87
|
+
basePayloadLen: dv.getUint32(28, true), totalLen: dv.getUint32(32, true),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function readToc(buf, h) {
|
|
91
|
+
const dv = dvOf(buf);
|
|
92
|
+
const out = [];
|
|
93
|
+
for (let i = 0; i < h.chunkCount; i++) {
|
|
94
|
+
const t = h.tocOffset + i * TOC_ENTRY_BYTES;
|
|
95
|
+
out.push({ kind: dv.getUint8(t), detailLevel: dv.getUint8(t + 1), targetId: dv.getUint16(t + 2, true),
|
|
96
|
+
offset: dv.getUint32(t + 4, true), length: dv.getUint32(t + 8, true), hash: dv.getUint32(t + 12, true) });
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
export function readChunk(buf, e) { return buf.subarray(e.offset, e.offset + e.length); }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface Instance {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
z: number;
|
|
5
|
+
scale: number;
|
|
6
|
+
material: number;
|
|
7
|
+
bounds: number;
|
|
8
|
+
lod?: number;
|
|
9
|
+
}
|
|
10
|
+
export type Plane = readonly [number, number, number, number];
|
|
11
|
+
export declare class GpuDrivenRenderer {
|
|
12
|
+
private device;
|
|
13
|
+
private format;
|
|
14
|
+
private instBuf;
|
|
15
|
+
private argsBuf;
|
|
16
|
+
private visBuf;
|
|
17
|
+
private camBuf;
|
|
18
|
+
private colorBuf;
|
|
19
|
+
private readBuf;
|
|
20
|
+
private cullPipe;
|
|
21
|
+
private drawPipe;
|
|
22
|
+
private cullBG;
|
|
23
|
+
private drawBG;
|
|
24
|
+
private count;
|
|
25
|
+
private pipelines;
|
|
26
|
+
private capPerPipe;
|
|
27
|
+
private lodCount;
|
|
28
|
+
private get buckets();
|
|
29
|
+
lastDrawCalls: number;
|
|
30
|
+
constructor(device: GPUDevice, format: GPUTextureFormat);
|
|
31
|
+
/** Upload a scene: instances + a color per pipeline (material). capPerPipe bounds the
|
|
32
|
+
* visible-per-pipeline compaction region. */
|
|
33
|
+
setScene(instances: Instance[], colors: [number, number, number][], capPerPipe: number, lodCount?: number): void;
|
|
34
|
+
/** One frame: reset args, compute-cull, then one indirect draw per pipeline. */
|
|
35
|
+
frame(encoder: GPUCommandEncoder, view: GPUTextureView, depth: GPUTextureView, viewProj: Float32Array, right: [number, number, number], up: [number, number, number], planes: Plane[]): void;
|
|
36
|
+
raw: number[];
|
|
37
|
+
/** Read the GPU-computed visible instanceCount per pipeline (the compute cull's result). */
|
|
38
|
+
readVisibleCounts(): Promise<number[]>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// GPU-DRIVEN render backend (WebGPU) — executes the render IR's DrawPlan on the GPU.
|
|
3
|
+
// The whole scene lives in a storage buffer; a COMPUTE shader frustum-culls every
|
|
4
|
+
// instance and writes, per pipeline (material), an atomic instanceCount + a compacted
|
|
5
|
+
// list of visible instance indices; then ONE indirect draw per pipeline renders all
|
|
6
|
+
// its visible instances. CPU draw calls = O(pipelines), independent of instance count.
|
|
7
|
+
//
|
|
8
|
+
// This is the real backend for the render IR (shared/renderIR.ts is the CPU reference
|
|
9
|
+
// oracle it's validated against). Instances are billboarded quads for now — the shape
|
|
10
|
+
// is not the point; the draw-call model is. See design/moonshot-open-world.md §5.
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
const INSTANCE_F32 = 8; // pos.xyz, scale | material, bounds, pad, pad
|
|
13
|
+
const ARG_U32 = 4; // drawIndirect: vertexCount, instanceCount, firstVertex, firstInstance
|
|
14
|
+
const WGSL = /* wgsl */ `
|
|
15
|
+
struct Inst { pos: vec4<f32>, attr: vec4<f32> }; // pos.xyz + scale(w); attr.x=pipeline, attr.y=bounds, attr.z=lod
|
|
16
|
+
struct Args { vc: u32, ic: atomic<u32>, fv: u32, fi: u32 };
|
|
17
|
+
struct Cam {
|
|
18
|
+
viewProj: mat4x4<f32>,
|
|
19
|
+
right: vec4<f32>, up: vec4<f32>,
|
|
20
|
+
planes: array<vec4<f32>, 6>,
|
|
21
|
+
counts: vec4<f32>, // x=count, y=capPerPipe, z=pipelines, w=lodCount
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
@group(0) @binding(0) var<storage, read> instances: array<Inst>;
|
|
25
|
+
@group(0) @binding(1) var<storage, read_write> args: array<Args>;
|
|
26
|
+
@group(0) @binding(2) var<storage, read_write> visible: array<u32>;
|
|
27
|
+
@group(0) @binding(3) var<uniform> cam: Cam;
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@compute @workgroup_size(64)
|
|
31
|
+
fn cull(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
32
|
+
let i = gid.x;
|
|
33
|
+
if (i >= u32(cam.counts.x)) { return; }
|
|
34
|
+
let inst = instances[i];
|
|
35
|
+
let c = inst.pos.xyz; let r = inst.attr.y;
|
|
36
|
+
var vis = true;
|
|
37
|
+
for (var p = 0u; p < 6u; p++) { let pl = cam.planes[p]; if (dot(pl.xyz, c) + pl.w < -r) { vis = false; } }
|
|
38
|
+
if (!vis) { return; }
|
|
39
|
+
// compact into per-(pipeline × lod) buckets — O(materials×lods) draws, LOD selected per instance.
|
|
40
|
+
let bucket = u32(inst.attr.x) * u32(cam.counts.w) + u32(inst.attr.z);
|
|
41
|
+
let slot = atomicAdd(&args[bucket].ic, 1u);
|
|
42
|
+
let cap = u32(cam.counts.y);
|
|
43
|
+
if (slot < cap) { visible[bucket * cap + slot] = i; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── render (billboarded quad per visible instance) ──
|
|
47
|
+
struct RInst { pos: vec4<f32>, attr: vec4<f32> };
|
|
48
|
+
@group(0) @binding(0) var<storage, read> rinstances: array<RInst>;
|
|
49
|
+
@group(0) @binding(1) var<storage, read> rvisible: array<u32>;
|
|
50
|
+
@group(0) @binding(2) var<uniform> rcam: Cam;
|
|
51
|
+
@group(0) @binding(3) var<uniform> colors: array<vec4<f32>, 8>;
|
|
52
|
+
|
|
53
|
+
struct VOut { @builtin(position) pos: vec4<f32>, @location(0) color: vec3<f32> };
|
|
54
|
+
const QUAD = array<vec2<f32>, 6>(vec2(-1.,-1.), vec2(1.,-1.), vec2(1.,1.), vec2(-1.,-1.), vec2(1.,1.), vec2(-1.,1.));
|
|
55
|
+
|
|
56
|
+
@vertex
|
|
57
|
+
fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
|
|
58
|
+
let idx = rvisible[ii];
|
|
59
|
+
let inst = rinstances[idx];
|
|
60
|
+
let q = QUAD[vi];
|
|
61
|
+
let world = inst.pos.xyz + (rcam.right.xyz * q.x + rcam.up.xyz * q.y) * inst.pos.w;
|
|
62
|
+
var o: VOut;
|
|
63
|
+
o.pos = rcam.viewProj * vec4(world, 1.0);
|
|
64
|
+
o.color = colors[u32(inst.attr.x)].rgb;
|
|
65
|
+
return o;
|
|
66
|
+
}
|
|
67
|
+
@fragment
|
|
68
|
+
fn fs(i: VOut) -> @location(0) vec4<f32> { return vec4(i.color, 1.0); }
|
|
69
|
+
`;
|
|
70
|
+
export class GpuDrivenRenderer {
|
|
71
|
+
device;
|
|
72
|
+
format;
|
|
73
|
+
instBuf;
|
|
74
|
+
argsBuf;
|
|
75
|
+
visBuf;
|
|
76
|
+
camBuf;
|
|
77
|
+
colorBuf;
|
|
78
|
+
readBuf;
|
|
79
|
+
cullPipe;
|
|
80
|
+
drawPipe;
|
|
81
|
+
cullBG;
|
|
82
|
+
drawBG;
|
|
83
|
+
count = 0;
|
|
84
|
+
pipelines = 1;
|
|
85
|
+
capPerPipe = 0;
|
|
86
|
+
lodCount = 1;
|
|
87
|
+
get buckets() { return this.pipelines * this.lodCount; } // per-(pipeline × lod) draw groups
|
|
88
|
+
lastDrawCalls = 0;
|
|
89
|
+
constructor(device, format) {
|
|
90
|
+
this.device = device;
|
|
91
|
+
this.format = format;
|
|
92
|
+
}
|
|
93
|
+
/** Upload a scene: instances + a color per pipeline (material). capPerPipe bounds the
|
|
94
|
+
* visible-per-pipeline compaction region. */
|
|
95
|
+
setScene(instances, colors, capPerPipe, lodCount = 1) {
|
|
96
|
+
const d = this.device;
|
|
97
|
+
this.count = instances.length;
|
|
98
|
+
this.pipelines = colors.length;
|
|
99
|
+
this.capPerPipe = capPerPipe;
|
|
100
|
+
this.lodCount = lodCount;
|
|
101
|
+
// instance storage (attr.z carries the per-instance LOD; defaults to 0 when unused)
|
|
102
|
+
const idata = new Float32Array(this.count * INSTANCE_F32);
|
|
103
|
+
for (let i = 0; i < this.count; i++) {
|
|
104
|
+
const o = i * INSTANCE_F32, it = instances[i];
|
|
105
|
+
idata[o] = it.x;
|
|
106
|
+
idata[o + 1] = it.y;
|
|
107
|
+
idata[o + 2] = it.z;
|
|
108
|
+
idata[o + 3] = it.scale;
|
|
109
|
+
idata[o + 4] = it.material;
|
|
110
|
+
idata[o + 5] = it.bounds;
|
|
111
|
+
idata[o + 6] = it.lod ?? 0;
|
|
112
|
+
}
|
|
113
|
+
this.instBuf = d.createBuffer({ size: idata.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
114
|
+
d.queue.writeBuffer(this.instBuf, 0, idata);
|
|
115
|
+
// per-bucket (pipeline × lod) indirect args + visible compaction buffer
|
|
116
|
+
this.argsBuf = d.createBuffer({ size: this.buckets * ARG_U32 * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC });
|
|
117
|
+
this.visBuf = d.createBuffer({ size: this.buckets * capPerPipe * 4, usage: GPUBufferUsage.STORAGE });
|
|
118
|
+
this.readBuf = d.createBuffer({ size: this.buckets * ARG_U32 * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
119
|
+
// camera uniform (mat4 64 + right 16 + up 16 + planes 96 + counts 16 = 208)
|
|
120
|
+
this.camBuf = d.createBuffer({ size: 208, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
121
|
+
// colors uniform (8 × vec4)
|
|
122
|
+
const cdata = new Float32Array(8 * 4);
|
|
123
|
+
for (let p = 0; p < this.pipelines; p++) {
|
|
124
|
+
cdata[p * 4] = colors[p][0];
|
|
125
|
+
cdata[p * 4 + 1] = colors[p][1];
|
|
126
|
+
cdata[p * 4 + 2] = colors[p][2];
|
|
127
|
+
cdata[p * 4 + 3] = 1;
|
|
128
|
+
}
|
|
129
|
+
this.colorBuf = d.createBuffer({ size: cdata.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
130
|
+
d.queue.writeBuffer(this.colorBuf, 0, cdata);
|
|
131
|
+
const mod = d.createShaderModule({ code: WGSL });
|
|
132
|
+
this.cullPipe = d.createComputePipeline({ layout: 'auto', compute: { module: mod, entryPoint: 'cull' } });
|
|
133
|
+
this.drawPipe = d.createRenderPipeline({ layout: 'auto', vertex: { module: mod, entryPoint: 'vs' }, fragment: { module: mod, entryPoint: 'fs', targets: [{ format: this.format }] }, primitive: { topology: 'triangle-list' }, depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' } });
|
|
134
|
+
this.cullBG = d.createBindGroup({ layout: this.cullPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.instBuf } }, { binding: 1, resource: { buffer: this.argsBuf } }, { binding: 2, resource: { buffer: this.visBuf } }, { binding: 3, resource: { buffer: this.camBuf } }] });
|
|
135
|
+
this.drawBG = d.createBindGroup({ layout: this.drawPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: this.instBuf } }, { binding: 1, resource: { buffer: this.visBuf } }, { binding: 2, resource: { buffer: this.camBuf } }, { binding: 3, resource: { buffer: this.colorBuf } }] });
|
|
136
|
+
}
|
|
137
|
+
/** One frame: reset args, compute-cull, then one indirect draw per pipeline. */
|
|
138
|
+
frame(encoder, view, depth, viewProj, right, up, planes) {
|
|
139
|
+
// camera uniform
|
|
140
|
+
const cam = new Float32Array(52);
|
|
141
|
+
cam.set(viewProj, 0);
|
|
142
|
+
cam[16] = right[0];
|
|
143
|
+
cam[17] = right[1];
|
|
144
|
+
cam[18] = right[2];
|
|
145
|
+
cam[20] = up[0];
|
|
146
|
+
cam[21] = up[1];
|
|
147
|
+
cam[22] = up[2];
|
|
148
|
+
for (let p = 0; p < 6; p++) {
|
|
149
|
+
cam[24 + p * 4] = planes[p][0];
|
|
150
|
+
cam[25 + p * 4] = planes[p][1];
|
|
151
|
+
cam[26 + p * 4] = planes[p][2];
|
|
152
|
+
cam[27 + p * 4] = planes[p][3];
|
|
153
|
+
}
|
|
154
|
+
cam[48] = this.count;
|
|
155
|
+
cam[49] = this.capPerPipe;
|
|
156
|
+
cam[50] = this.pipelines;
|
|
157
|
+
cam[51] = this.lodCount;
|
|
158
|
+
this.device.queue.writeBuffer(this.camBuf, 0, cam);
|
|
159
|
+
// reset indirect args per bucket: [vertexCount=6, instanceCount=0, firstVertex=0, firstInstance=bucket*cap]
|
|
160
|
+
const args = new Uint32Array(this.buckets * ARG_U32);
|
|
161
|
+
for (let b = 0; b < this.buckets; b++) {
|
|
162
|
+
args[b * 4] = 6;
|
|
163
|
+
args[b * 4 + 1] = 0;
|
|
164
|
+
args[b * 4 + 2] = 0;
|
|
165
|
+
args[b * 4 + 3] = b * this.capPerPipe;
|
|
166
|
+
}
|
|
167
|
+
this.device.queue.writeBuffer(this.argsBuf, 0, args);
|
|
168
|
+
// compute cull
|
|
169
|
+
const cp = encoder.beginComputePass();
|
|
170
|
+
cp.setPipeline(this.cullPipe);
|
|
171
|
+
cp.setBindGroup(0, this.cullBG);
|
|
172
|
+
cp.dispatchWorkgroups(Math.ceil(this.count / 64));
|
|
173
|
+
cp.end();
|
|
174
|
+
// render — ONE indirect draw per pipeline
|
|
175
|
+
const rp = encoder.beginRenderPass({ colorAttachments: [{ view, clearValue: { r: 0.043, g: 0.075, b: 0.114, a: 1 }, loadOp: 'clear', storeOp: 'store' }], depthStencilAttachment: { view: depth, depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'store' } });
|
|
176
|
+
rp.setPipeline(this.drawPipe);
|
|
177
|
+
rp.setBindGroup(0, this.drawBG);
|
|
178
|
+
for (let b = 0; b < this.buckets; b++)
|
|
179
|
+
rp.drawIndirect(this.argsBuf, b * ARG_U32 * 4);
|
|
180
|
+
rp.end();
|
|
181
|
+
this.lastDrawCalls = this.buckets;
|
|
182
|
+
// copy args → readback
|
|
183
|
+
encoder.copyBufferToBuffer(this.argsBuf, 0, this.readBuf, 0, this.buckets * ARG_U32 * 4);
|
|
184
|
+
}
|
|
185
|
+
raw = [];
|
|
186
|
+
/** Read the GPU-computed visible instanceCount per pipeline (the compute cull's result). */
|
|
187
|
+
async readVisibleCounts() {
|
|
188
|
+
await this.readBuf.mapAsync(GPUMapMode.READ);
|
|
189
|
+
const a = new Uint32Array(this.readBuf.getMappedRange().slice(0));
|
|
190
|
+
this.readBuf.unmap();
|
|
191
|
+
this.raw = Array.from(a);
|
|
192
|
+
const out = [];
|
|
193
|
+
for (let b = 0; b < this.buckets; b++)
|
|
194
|
+
out.push(a[b * ARG_U32 + 1]); // visible count per (pipeline×lod) bucket
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
}
|
package/dist/gpuGI.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface GIBox {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
z: number;
|
|
5
|
+
sx: number;
|
|
6
|
+
sy: number;
|
|
7
|
+
sz: number;
|
|
8
|
+
r: number;
|
|
9
|
+
g: number;
|
|
10
|
+
b: number;
|
|
11
|
+
}
|
|
12
|
+
export interface Grid {
|
|
13
|
+
ox: number;
|
|
14
|
+
oy: number;
|
|
15
|
+
oz: number;
|
|
16
|
+
cell: number;
|
|
17
|
+
nx: number;
|
|
18
|
+
ny: number;
|
|
19
|
+
nz: number;
|
|
20
|
+
}
|
|
21
|
+
/** Bake the probe grid: for each probe, cast rays; a surface hit contributes the hit box's
|
|
22
|
+
* sun-lit albedo (one bounce, colour bleed), a miss contributes sky. Project into 6 axes.
|
|
23
|
+
* Returns a Float32Array of `nx*ny*nz*6` vec4 (rgb + pad), axis-major within each probe. */
|
|
24
|
+
export declare function bakeProbes(boxes: GIBox[], g: Grid, rays?: number): Float32Array;
|
|
25
|
+
export declare class GpuGIRenderer {
|
|
26
|
+
private device;
|
|
27
|
+
private uBuf;
|
|
28
|
+
private u;
|
|
29
|
+
private instBuf;
|
|
30
|
+
private probeBuf;
|
|
31
|
+
private count;
|
|
32
|
+
private grid;
|
|
33
|
+
private pipe;
|
|
34
|
+
private bgl;
|
|
35
|
+
private bg;
|
|
36
|
+
constructor(device: GPUDevice, format: GPUTextureFormat);
|
|
37
|
+
setScene(boxes: GIBox[], probes: Float32Array, grid: Grid): void;
|
|
38
|
+
frame(enc: GPUCommandEncoder, colorView: GPUTextureView, depthView: GPUTextureView, camVP: Float32Array, giOn: boolean, flatAmbient: number): void;
|
|
39
|
+
}
|
package/dist/gpuGI.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Render increment 8 — BAKED GLOBAL ILLUMINATION via an irradiance probe grid (DDGI-style).
|
|
2
|
+
// The last of GAME.md §3.6's named look gaps. Real-time path tracing is out; the tractable form
|
|
3
|
+
// is to PRECOMPUTE indirect light into a 3D grid of probes, then sample it per-pixel. Each probe
|
|
4
|
+
// stores a 6-axis AMBIENT CUBE (irradiance arriving along ±X/±Y/±Z — the Half-Life 2 trick):
|
|
5
|
+
// cheap to store, captures direction, so a red box bleeds red onto its neighbours and occluded
|
|
6
|
+
// nooks that see little sky go dark (soft AO). One bounce, baked once on the CPU.
|
|
7
|
+
//
|
|
8
|
+
// bakeProbes() is the CPU bake (ray-cast the scene per probe → ambient cubes). GpuGIRenderer
|
|
9
|
+
// renders the boxes with direct sun + the probe-sampled indirect term. `?gi=0` on the page swaps
|
|
10
|
+
// the probe term for a flat ambient so the difference is unmistakable.
|
|
11
|
+
import { CUBE } from './gpuShadow';
|
|
12
|
+
const AXES = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
|
|
13
|
+
const SUN = [-0.4, -0.85, -0.32]; // direction light travels
|
|
14
|
+
const SUN_COL = [1.15, 1.05, 0.9];
|
|
15
|
+
function norm3(x, y, z) { const l = Math.hypot(x, y, z) || 1; return [x / l, y / l, z / l]; }
|
|
16
|
+
const SUN_N = norm3(SUN[0], SUN[1], SUN[2]);
|
|
17
|
+
/** Deterministic near-uniform sphere directions (Fibonacci). */
|
|
18
|
+
function sphereDirs(n) {
|
|
19
|
+
const out = [], ga = Math.PI * (3 - Math.sqrt(5));
|
|
20
|
+
for (let i = 0; i < n; i++) {
|
|
21
|
+
const y = 1 - ((i + 0.5) / n) * 2, r = Math.sqrt(Math.max(0, 1 - y * y)), th = ga * i;
|
|
22
|
+
out.push([Math.cos(th) * r, y, Math.sin(th) * r]);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/** Ray vs AABB (slab). Returns entry distance + entry-face normal, or null. */
|
|
27
|
+
function rayBox(ox, oy, oz, dx, dy, dz, b) {
|
|
28
|
+
const o = [ox, oy, oz], d = [dx, dy, dz], mn = [b.x - b.sx / 2, b.y - b.sy / 2, b.z - b.sz / 2], mx = [b.x + b.sx / 2, b.y + b.sy / 2, b.z + b.sz / 2];
|
|
29
|
+
let tmin = -Infinity, tmax = Infinity, axis = 0, sign = 1;
|
|
30
|
+
for (let a = 0; a < 3; a++) {
|
|
31
|
+
if (Math.abs(d[a]) < 1e-9) {
|
|
32
|
+
if (o[a] < mn[a] || o[a] > mx[a])
|
|
33
|
+
return null;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const inv = 1 / d[a];
|
|
37
|
+
let t1 = (mn[a] - o[a]) * inv, t2 = (mx[a] - o[a]) * inv, s = -1;
|
|
38
|
+
if (t1 > t2) {
|
|
39
|
+
const tmp = t1;
|
|
40
|
+
t1 = t2;
|
|
41
|
+
t2 = tmp;
|
|
42
|
+
s = 1;
|
|
43
|
+
}
|
|
44
|
+
if (t1 > tmin) {
|
|
45
|
+
tmin = t1;
|
|
46
|
+
axis = a;
|
|
47
|
+
sign = s;
|
|
48
|
+
}
|
|
49
|
+
if (t2 < tmax)
|
|
50
|
+
tmax = t2;
|
|
51
|
+
if (tmin > tmax)
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (tmax < 0)
|
|
55
|
+
return null;
|
|
56
|
+
const t = tmin > 0 ? tmin : tmax;
|
|
57
|
+
if (t < 0)
|
|
58
|
+
return null;
|
|
59
|
+
const n = [0, 0, 0];
|
|
60
|
+
n[axis] = sign;
|
|
61
|
+
return { t, n };
|
|
62
|
+
}
|
|
63
|
+
function skyColor(dy) {
|
|
64
|
+
if (dy < 0)
|
|
65
|
+
return [0.03, 0.03, 0.04]; // below horizon → almost no light
|
|
66
|
+
const t = dy; // horizon → zenith. Kept DIM on purpose:
|
|
67
|
+
return [0.16 + 0.08 * t, 0.19 + 0.11 * t, 0.24 + 0.16 * t]; // a bright sky washes out the colour bounce
|
|
68
|
+
}
|
|
69
|
+
/** Bake the probe grid: for each probe, cast rays; a surface hit contributes the hit box's
|
|
70
|
+
* sun-lit albedo (one bounce, colour bleed), a miss contributes sky. Project into 6 axes.
|
|
71
|
+
* Returns a Float32Array of `nx*ny*nz*6` vec4 (rgb + pad), axis-major within each probe. */
|
|
72
|
+
export function bakeProbes(boxes, g, rays = 32) {
|
|
73
|
+
const dirs = sphereDirs(rays);
|
|
74
|
+
const N = g.nx * g.ny * g.nz;
|
|
75
|
+
const out = new Float32Array(N * 6 * 4);
|
|
76
|
+
for (let iz = 0; iz < g.nz; iz++)
|
|
77
|
+
for (let iy = 0; iy < g.ny; iy++)
|
|
78
|
+
for (let ix = 0; ix < g.nx; ix++) {
|
|
79
|
+
const px = g.ox + ix * g.cell, py = g.oy + iy * g.cell, pz = g.oz + iz * g.cell;
|
|
80
|
+
const cube = new Float64Array(18), wsum = new Float64Array(6); // 6 axes × rgb
|
|
81
|
+
for (const [dx, dy, dz] of dirs) {
|
|
82
|
+
let bestT = Infinity, hit = null;
|
|
83
|
+
for (const b of boxes) {
|
|
84
|
+
const r = rayBox(px, py, pz, dx, dy, dz, b);
|
|
85
|
+
if (r && r.t > 0.02 && r.t < bestT) {
|
|
86
|
+
bestT = r.t;
|
|
87
|
+
hit = { b, n: r.n };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let cr, cg, cb;
|
|
91
|
+
if (hit) {
|
|
92
|
+
const sl = Math.max(-(hit.n[0] * SUN_N[0] + hit.n[1] * SUN_N[1] + hit.n[2] * SUN_N[2]), 0); // face lit by sun?
|
|
93
|
+
cr = hit.b.r * (0.16 + SUN_COL[0] * sl);
|
|
94
|
+
cg = hit.b.g * (0.16 + SUN_COL[1] * sl);
|
|
95
|
+
cb = hit.b.b * (0.16 + SUN_COL[2] * sl);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const s = skyColor(dy);
|
|
99
|
+
cr = s[0];
|
|
100
|
+
cg = s[1];
|
|
101
|
+
cb = s[2];
|
|
102
|
+
}
|
|
103
|
+
for (let a = 0; a < 6; a++) {
|
|
104
|
+
const ax = AXES[a];
|
|
105
|
+
const w = Math.max(dx * ax[0] + dy * ax[1] + dz * ax[2], 0);
|
|
106
|
+
if (w <= 0)
|
|
107
|
+
continue;
|
|
108
|
+
wsum[a] += w;
|
|
109
|
+
cube[a * 3] += cr * w;
|
|
110
|
+
cube[a * 3 + 1] += cg * w;
|
|
111
|
+
cube[a * 3 + 2] += cb * w;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const pi = (iz * g.ny + iy) * g.nx + ix;
|
|
115
|
+
for (let a = 0; a < 6; a++) {
|
|
116
|
+
const wv = Math.max(wsum[a], 1e-4), o = (pi * 6 + a) * 4;
|
|
117
|
+
out[o] = cube[a * 3] / wv;
|
|
118
|
+
out[o + 1] = cube[a * 3 + 1] / wv;
|
|
119
|
+
out[o + 2] = cube[a * 3 + 2] / wv;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
const SCENE_WGSL = /* wgsl */ `
|
|
125
|
+
struct Inst { pos: vec4<f32>, scl: vec4<f32>, col: vec4<f32> };
|
|
126
|
+
struct GU {
|
|
127
|
+
camVP: mat4x4<f32>,
|
|
128
|
+
sun: vec4<f32>, // xyz dir, w = giOn
|
|
129
|
+
originCell: vec4<f32>, // xyz grid origin, w = cell size
|
|
130
|
+
dims: vec4<f32>, // xyz grid dims, w = flat ambient (gi off)
|
|
131
|
+
};
|
|
132
|
+
@group(0) @binding(0) var<uniform> gu: GU;
|
|
133
|
+
@group(0) @binding(1) var<storage, read> insts: array<Inst>;
|
|
134
|
+
@group(0) @binding(2) var<storage, read> probes: array<vec4<f32>>;
|
|
135
|
+
const CUBE_POS = array<vec3<f32>, 36>(${CUBE.pos});
|
|
136
|
+
const CUBE_NRM = array<vec3<f32>, 36>(${CUBE.nrm});
|
|
137
|
+
|
|
138
|
+
struct VOut { @builtin(position) clip: vec4<f32>, @location(0) world: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) col: vec3<f32> };
|
|
139
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VOut {
|
|
140
|
+
let it = insts[ii];
|
|
141
|
+
let world = it.pos.xyz + CUBE_POS[vi] * it.scl.xyz;
|
|
142
|
+
var o: VOut;
|
|
143
|
+
o.clip = gu.camVP * vec4(world, 1.0);
|
|
144
|
+
o.world = world; o.nrm = normalize(CUBE_NRM[vi] / it.scl.xyz); o.col = it.col.rgb;
|
|
145
|
+
return o;
|
|
146
|
+
}
|
|
147
|
+
fn probeAxis(pi: u32, a: u32) -> vec3<f32> { return probes[pi * 6u + a].rgb; }
|
|
148
|
+
// Trilinear-sample the probe grid, then reconstruct irradiance for normal N (ambient cube).
|
|
149
|
+
fn giAt(world: vec3<f32>, N: vec3<f32>) -> vec3<f32> {
|
|
150
|
+
let dims = gu.dims.xyz;
|
|
151
|
+
let g = (world - gu.originCell.xyz) / gu.originCell.w;
|
|
152
|
+
let gc = clamp(floor(g), vec3(0.0), dims - 1.0);
|
|
153
|
+
let f = clamp(g - gc, vec3(0.0), vec3(1.0));
|
|
154
|
+
var c0 = vec3(0.0); var c1 = vec3(0.0); var c2 = vec3(0.0); var c3 = vec3(0.0); var c4 = vec3(0.0); var c5 = vec3(0.0);
|
|
155
|
+
for (var k = 0u; k < 8u; k = k + 1u) {
|
|
156
|
+
let ox = f32(k & 1u); let oy = f32((k >> 1u) & 1u); let oz = f32((k >> 2u) & 1u);
|
|
157
|
+
let ix = clamp(gc.x + ox, 0.0, dims.x - 1.0); let iy = clamp(gc.y + oy, 0.0, dims.y - 1.0); let iz = clamp(gc.z + oz, 0.0, dims.z - 1.0);
|
|
158
|
+
let w = mix(1.0 - f.x, f.x, ox) * mix(1.0 - f.y, f.y, oy) * mix(1.0 - f.z, f.z, oz);
|
|
159
|
+
let pi = u32((iz * dims.y + iy) * dims.x + ix);
|
|
160
|
+
c0 = c0 + w * probeAxis(pi, 0u); c1 = c1 + w * probeAxis(pi, 1u); c2 = c2 + w * probeAxis(pi, 2u);
|
|
161
|
+
c3 = c3 + w * probeAxis(pi, 3u); c4 = c4 + w * probeAxis(pi, 4u); c5 = c5 + w * probeAxis(pi, 5u);
|
|
162
|
+
}
|
|
163
|
+
let n2 = N * N;
|
|
164
|
+
let cx = select(c1, c0, N.x > 0.0); let cy = select(c3, c2, N.y > 0.0); let cz = select(c5, c4, N.z > 0.0);
|
|
165
|
+
return n2.x * cx + n2.y * cy + n2.z * cz;
|
|
166
|
+
}
|
|
167
|
+
@fragment fn fs(v: VOut) -> @location(0) vec4<f32> {
|
|
168
|
+
let N = normalize(v.nrm);
|
|
169
|
+
let sl = max(dot(N, -normalize(gu.sun.xyz)), 0.0);
|
|
170
|
+
let direct = v.col * (sl * vec3(0.85, 0.8, 0.7)); // dimmer direct so indirect bleed reads
|
|
171
|
+
var indirect = v.col * gu.dims.w; // flat ambient (gi off)
|
|
172
|
+
if (gu.sun.w > 0.5) { indirect = v.col * giAt(v.world, N); } // probe-baked indirect (gi on)
|
|
173
|
+
return vec4(pow(clamp(direct + indirect, vec3(0.0), vec3(1.0)), vec3(0.4545)), 1.0);
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
export class GpuGIRenderer {
|
|
177
|
+
device;
|
|
178
|
+
uBuf;
|
|
179
|
+
u = new Float32Array(28); // mat4 + 3 vec4
|
|
180
|
+
instBuf;
|
|
181
|
+
probeBuf;
|
|
182
|
+
count = 0;
|
|
183
|
+
grid;
|
|
184
|
+
pipe;
|
|
185
|
+
bgl;
|
|
186
|
+
bg;
|
|
187
|
+
constructor(device, format) {
|
|
188
|
+
this.device = device;
|
|
189
|
+
this.uBuf = device.createBuffer({ size: this.u.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
190
|
+
const mod = device.createShaderModule({ code: SCENE_WGSL });
|
|
191
|
+
this.bgl = device.createBindGroupLayout({ entries: [
|
|
192
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
|
193
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
|
194
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'read-only-storage' } }
|
|
195
|
+
] });
|
|
196
|
+
this.pipe = device.createRenderPipeline({
|
|
197
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.bgl] }),
|
|
198
|
+
vertex: { module: mod, entryPoint: 'vs' },
|
|
199
|
+
fragment: { module: mod, entryPoint: 'fs', targets: [{ format }] },
|
|
200
|
+
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
201
|
+
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
setScene(boxes, probes, grid) {
|
|
205
|
+
this.grid = grid;
|
|
206
|
+
this.count = boxes.length;
|
|
207
|
+
const data = new Float32Array(this.count * 12);
|
|
208
|
+
for (let i = 0; i < this.count; i++) {
|
|
209
|
+
const b = boxes[i], o = i * 12;
|
|
210
|
+
data[o] = b.x;
|
|
211
|
+
data[o + 1] = b.y;
|
|
212
|
+
data[o + 2] = b.z;
|
|
213
|
+
data[o + 4] = b.sx;
|
|
214
|
+
data[o + 5] = b.sy;
|
|
215
|
+
data[o + 6] = b.sz;
|
|
216
|
+
data[o + 8] = b.r;
|
|
217
|
+
data[o + 9] = b.g;
|
|
218
|
+
data[o + 10] = b.b;
|
|
219
|
+
}
|
|
220
|
+
this.instBuf = this.device.createBuffer({ size: data.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
221
|
+
this.device.queue.writeBuffer(this.instBuf, 0, data);
|
|
222
|
+
this.probeBuf = this.device.createBuffer({ size: probes.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
|
223
|
+
this.device.queue.writeBuffer(this.probeBuf, 0, new Float32Array(probes)); // ArrayBuffer-backed copy (one-time)
|
|
224
|
+
this.bg = this.device.createBindGroup({ layout: this.bgl, entries: [
|
|
225
|
+
{ binding: 0, resource: { buffer: this.uBuf } }, { binding: 1, resource: { buffer: this.instBuf } }, { binding: 2, resource: { buffer: this.probeBuf } }
|
|
226
|
+
] });
|
|
227
|
+
}
|
|
228
|
+
frame(enc, colorView, depthView, camVP, giOn, flatAmbient) {
|
|
229
|
+
const g = this.grid;
|
|
230
|
+
this.u.set(camVP, 0);
|
|
231
|
+
this.u[16] = SUN[0];
|
|
232
|
+
this.u[17] = SUN[1];
|
|
233
|
+
this.u[18] = SUN[2];
|
|
234
|
+
this.u[19] = giOn ? 1 : 0;
|
|
235
|
+
this.u[20] = g.ox;
|
|
236
|
+
this.u[21] = g.oy;
|
|
237
|
+
this.u[22] = g.oz;
|
|
238
|
+
this.u[23] = g.cell;
|
|
239
|
+
this.u[24] = g.nx;
|
|
240
|
+
this.u[25] = g.ny;
|
|
241
|
+
this.u[26] = g.nz;
|
|
242
|
+
this.u[27] = flatAmbient;
|
|
243
|
+
this.device.queue.writeBuffer(this.uBuf, 0, this.u);
|
|
244
|
+
const rp = enc.beginRenderPass({
|
|
245
|
+
colorAttachments: [{ view: colorView, clearValue: { r: 0.03, g: 0.04, b: 0.06, a: 1 }, loadOp: 'clear', storeOp: 'store' }],
|
|
246
|
+
depthStencilAttachment: { view: depthView, depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store' }
|
|
247
|
+
});
|
|
248
|
+
rp.setPipeline(this.pipe);
|
|
249
|
+
rp.setBindGroup(0, this.bg);
|
|
250
|
+
rp.draw(36, this.count);
|
|
251
|
+
rp.end();
|
|
252
|
+
}
|
|
253
|
+
}
|