ugly-game 0.5.17 → 0.5.19
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.js +15 -3
- package/dist/assets/animMoat.js +20 -4
- package/dist/assets/assemble.js +50 -14
- package/dist/assets/genome.js +57 -13
- package/dist/assets/genomeExplore.js +24 -12
- package/dist/assets/geometry.js +14 -3
- package/dist/assets/gpuProps.d.ts +6 -1
- package/dist/assets/gpuProps.js +150 -26
- package/dist/assets/gpuSkin.d.ts +6 -1
- package/dist/assets/gpuSkin.js +162 -30
- package/dist/assets/meshlet.js +20 -6
- package/dist/assets/morph.js +3 -1
- package/dist/assets/pose.js +15 -4
- package/dist/assets/renderAssets.js +12 -5
- package/dist/assets/residency.js +13 -6
- package/dist/assets/retarget.js +6 -2
- package/dist/assets/rig.js +36 -8
- package/dist/assets/stream.js +11 -4
- package/dist/assets/style.js +1 -1
- package/dist/assets/ugm.js +31 -9
- package/dist/capture.d.ts +1 -3
- package/dist/capture.js +12 -2
- package/dist/determinismProbe.js +15 -3
- package/dist/dmath.js +38 -8
- package/dist/ecs.js +121 -25
- package/dist/frameProbe.js +28 -5
- package/dist/gpuDriven.js +83 -12
- package/dist/gpuGI.js +73 -17
- package/dist/gpuGraph.js +73 -16
- package/dist/gpuLit.js +75 -10
- package/dist/gpuPost.js +112 -27
- package/dist/gpuShadow.d.ts +7 -1
- package/dist/gpuShadow.js +343 -85
- package/dist/gpuShadowMap.js +19 -3
- package/dist/gpuSpot.js +119 -32
- package/dist/gpuSsao.js +14 -2
- package/dist/gpuTaa.js +136 -35
- package/dist/gpuTerrain.js +160 -18
- package/dist/gpuTiled.js +107 -16
- package/dist/gpuWorld.js +200 -55
- package/dist/input/ControlHints.js +105 -17
- package/dist/input/TouchControls.d.ts +1 -1
- package/dist/input/TouchControls.js +69 -10
- package/dist/input/browserBackend.js +83 -24
- package/dist/input/resolve.js +61 -19
- package/dist/ir.js +179 -34
- package/dist/kcc.js +30 -8
- package/dist/mat4.js +25 -4
- package/dist/moat.js +41 -8
- package/dist/net/commands.js +3 -1
- package/dist/net/lww.js +2 -1
- package/dist/net/reconcile.js +1 -1
- package/dist/net/roomCore.js +21 -6
- package/dist/net/roomSocket.js +25 -14
- package/dist/noise.js +8 -2
- package/dist/physics.js +15 -1
- package/dist/playSim.js +108 -24
- package/dist/playtestIR.d.ts +3 -2
- package/dist/playtestIR.js +48 -16
- package/dist/renderIR.js +21 -3
- package/dist/scheduler.js +84 -16
- package/dist/shaderGraph.js +135 -42
- package/dist/stateIR.js +24 -7
- package/dist/stateTree.js +66 -16
- package/dist/visualIR.js +18 -5
- package/package.json +8 -3
package/dist/assets/anim.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
const u16 = (v) => {
|
|
2
|
-
const
|
|
3
|
-
|
|
1
|
+
const u16 = (v) => {
|
|
2
|
+
const a = new Uint8Array(2);
|
|
3
|
+
new DataView(a.buffer).setUint16(0, v, true);
|
|
4
|
+
return a;
|
|
5
|
+
};
|
|
6
|
+
const u32 = (v) => {
|
|
7
|
+
const a = new Uint8Array(4);
|
|
8
|
+
new DataView(a.buffer).setUint32(0, v, true);
|
|
9
|
+
return a;
|
|
10
|
+
};
|
|
11
|
+
const i16 = (v) => {
|
|
12
|
+
const a = new Uint8Array(2);
|
|
13
|
+
new DataView(a.buffer).setInt16(0, v, true);
|
|
14
|
+
return a;
|
|
15
|
+
};
|
|
4
16
|
const f32s = (arr) => new Uint8Array(arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength));
|
|
5
17
|
const f32 = (v) => f32s(Float32Array.of(v));
|
|
6
18
|
export function packAnim(clip) {
|
package/dist/assets/animMoat.js
CHANGED
|
@@ -26,7 +26,12 @@ export function continuityScan(trace, factor = 8, floor = 0.02) {
|
|
|
26
26
|
violations.push({ tick: t, index: i, jump: a, ratio: a / base });
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
ticks: T,
|
|
31
|
+
components: C,
|
|
32
|
+
violations,
|
|
33
|
+
clean: violations.length === 0,
|
|
34
|
+
};
|
|
30
35
|
}
|
|
31
36
|
/** The animation IR: the deterministic per-tick bone-matrix palette for a clip sampled over `ticks`. */
|
|
32
37
|
export function animationPaletteTrace(skel, clip, ticks) {
|
|
@@ -44,12 +49,23 @@ export function scanAnimationContinuity(skel, clip, ticks = 120, factor = 8, flo
|
|
|
44
49
|
const r = continuityScan(animationPaletteTrace(skel, clip, ticks), factor, floor);
|
|
45
50
|
const seen = new Set();
|
|
46
51
|
const violations = [];
|
|
47
|
-
for (const v of r.violations) {
|
|
52
|
+
for (const v of r.violations) {
|
|
53
|
+
// dedupe to one entry per (tick,joint)
|
|
48
54
|
const joint = Math.floor(v.index / 16), key = `${v.tick}:${joint}`;
|
|
49
55
|
if (seen.has(key))
|
|
50
56
|
continue;
|
|
51
57
|
seen.add(key);
|
|
52
|
-
violations.push({
|
|
58
|
+
violations.push({
|
|
59
|
+
clipTime: ticks > 1 ? (v.tick / (ticks - 1)) * clip.duration : 0,
|
|
60
|
+
joint,
|
|
61
|
+
element: v.index % 16,
|
|
62
|
+
ratio: v.ratio,
|
|
63
|
+
});
|
|
53
64
|
}
|
|
54
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
clean: violations.length === 0,
|
|
67
|
+
ticks,
|
|
68
|
+
duration: clip.duration,
|
|
69
|
+
violations,
|
|
70
|
+
};
|
|
55
71
|
}
|
package/dist/assets/assemble.js
CHANGED
|
@@ -12,7 +12,11 @@ import { dsin, dcos } from '../dmath';
|
|
|
12
12
|
import { resolveKit } from './partkit';
|
|
13
13
|
import { resolveRole } from './style';
|
|
14
14
|
function applyPoint(M, x, y, z) {
|
|
15
|
-
return [
|
|
15
|
+
return [
|
|
16
|
+
M[0] * x + M[4] * y + M[8] * z + M[12],
|
|
17
|
+
M[1] * x + M[5] * y + M[9] * z + M[13],
|
|
18
|
+
M[2] * x + M[6] * y + M[10] * z + M[14],
|
|
19
|
+
];
|
|
16
20
|
}
|
|
17
21
|
/** Assemble a rigged creature mesh from a genome. Deterministic + style-conformant. */
|
|
18
22
|
export function assemble(arch, kit, genome, guide) {
|
|
@@ -20,28 +24,41 @@ export function assemble(arch, kit, genome, guide) {
|
|
|
20
24
|
const prop = genome.proportion;
|
|
21
25
|
// 1) Deform the shared rig's rest translations (bone lengths). Default per-joint scale = [girth,height,
|
|
22
26
|
// girth]; limb sockets add limbLength, head sockets add headScale. inverseBind stays ORIGINAL.
|
|
23
|
-
const sv = arch.restLocals.map(() => [
|
|
27
|
+
const sv = arch.restLocals.map(() => [
|
|
28
|
+
prop.girth,
|
|
29
|
+
prop.height,
|
|
30
|
+
prop.girth,
|
|
31
|
+
]);
|
|
24
32
|
for (const s of arch.sockets) {
|
|
25
33
|
const j = s.jointIndex;
|
|
26
34
|
if (j < 0 || j >= sv.length)
|
|
27
35
|
continue;
|
|
28
|
-
const f = s.region === 'limb'
|
|
36
|
+
const f = s.region === 'limb'
|
|
37
|
+
? prop.limbLength
|
|
38
|
+
: s.region === 'head'
|
|
39
|
+
? prop.headScale
|
|
40
|
+
: 1;
|
|
29
41
|
sv[j] = [sv[j][0] * f, sv[j][1] * f, sv[j][2] * f];
|
|
30
42
|
}
|
|
31
43
|
const restLocals = arch.restLocals.map((r, j) => ({
|
|
32
44
|
t: [r.t[0] * sv[j][0], r.t[1] * sv[j][1], r.t[2] * sv[j][2]],
|
|
33
|
-
q: [r.q[0], r.q[1], r.q[2], r.q[3]],
|
|
45
|
+
q: [r.q[0], r.q[1], r.q[2], r.q[3]],
|
|
46
|
+
s: [r.s[0], r.s[1], r.s[2]],
|
|
34
47
|
}));
|
|
35
48
|
// 2) Weld each selected part (deterministic socket order) with its per-part deform, concatenating
|
|
36
49
|
// positions/indices/joints/weights into one mesh sharing the archetype's parents + inverseBind.
|
|
37
50
|
const positions = [], indices = [], joints = [], weights = [], roles = [];
|
|
38
51
|
let minx = Infinity, miny = Infinity, minz = Infinity, maxx = -Infinity, maxy = -Infinity, maxz = -Infinity;
|
|
39
|
-
const sockets = [...arch.sockets].sort((a, b) =>
|
|
52
|
+
const sockets = [...arch.sockets].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
40
53
|
for (const s of sockets) {
|
|
41
54
|
const part = selected[s.id];
|
|
42
55
|
if (!part)
|
|
43
56
|
continue;
|
|
44
|
-
const d = genome.partDeform[s.id] ?? {
|
|
57
|
+
const d = genome.partDeform[s.id] ?? {
|
|
58
|
+
scale: [1, 1, 1],
|
|
59
|
+
stretch: [1, 1, 1],
|
|
60
|
+
twist: 0,
|
|
61
|
+
};
|
|
45
62
|
// socket joint bind position (= translation of the inverse of its inverseBind) → deform pivots here
|
|
46
63
|
const j = s.jointIndex, ib = new Float64Array(16);
|
|
47
64
|
for (let i = 0; i < 16; i++)
|
|
@@ -51,7 +68,11 @@ export function assemble(arch, kit, genome, guide) {
|
|
|
51
68
|
const half = d.twist / 2, qy = [0, dsin(half), 0, dcos(half)];
|
|
52
69
|
const Tp = composeTRS([bx, by, bz], [0, 0, 0, 1], [1, 1, 1]);
|
|
53
70
|
const R = composeTRS([0, 0, 0], qy, [1, 1, 1]);
|
|
54
|
-
const S = composeTRS([0, 0, 0], [0, 0, 0, 1], [
|
|
71
|
+
const S = composeTRS([0, 0, 0], [0, 0, 0, 1], [
|
|
72
|
+
d.scale[0] * d.stretch[0],
|
|
73
|
+
d.scale[1] * d.stretch[1],
|
|
74
|
+
d.scale[2] * d.stretch[2],
|
|
75
|
+
]);
|
|
55
76
|
const Tn = composeTRS([-bx, -by, -bz], [0, 0, 0, 1], [1, 1, 1]);
|
|
56
77
|
const W = mul(Tp, mul(R, mul(S, Tn)));
|
|
57
78
|
const base = positions.length / 3, pv = part.mesh.positions.length / 3;
|
|
@@ -75,18 +96,33 @@ export function assemble(arch, kit, genome, guide) {
|
|
|
75
96
|
ws += part.weights[v * 4 + k];
|
|
76
97
|
for (let k = 0; k < 4; k++) {
|
|
77
98
|
joints.push(part.joints[v * 4 + k]);
|
|
78
|
-
weights.push(ws > 0 ? part.weights[v * 4 + k] / ws :
|
|
99
|
+
weights.push(ws > 0 ? part.weights[v * 4 + k] / ws : k === 0 ? 1 : 0);
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
for (const idx of part.mesh.indices)
|
|
82
103
|
indices.push(idx + base);
|
|
83
|
-
roles.push({
|
|
104
|
+
roles.push({
|
|
105
|
+
first: base,
|
|
106
|
+
count: pv,
|
|
107
|
+
region: part.tag.region,
|
|
108
|
+
role: resolveRole(guide, part.tag.region),
|
|
109
|
+
});
|
|
84
110
|
}
|
|
85
111
|
return {
|
|
86
|
-
mesh: {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
112
|
+
mesh: {
|
|
113
|
+
positions: new Float32Array(positions),
|
|
114
|
+
indices: new Uint32Array(indices),
|
|
115
|
+
},
|
|
116
|
+
jointCount: arch.jointCount,
|
|
117
|
+
parents: arch.parents,
|
|
118
|
+
inverseBind: arch.inverseBind,
|
|
119
|
+
jointsPerVertex: new Uint16Array(joints),
|
|
120
|
+
weightsPerVertex: new Float32Array(weights),
|
|
121
|
+
restLocals,
|
|
122
|
+
rootWorld: arch.rootWorld,
|
|
123
|
+
roles,
|
|
124
|
+
bounds: positions.length
|
|
125
|
+
? [minx, miny, minz, maxx, maxy, maxz]
|
|
126
|
+
: [0, 0, 0, 0, 0, 0],
|
|
91
127
|
};
|
|
92
128
|
}
|
package/dist/assets/genome.js
CHANGED
|
@@ -11,13 +11,22 @@ import { partsForSocket } from './partkit';
|
|
|
11
11
|
/** Deterministic mulberry32 (local copy so the platform module has no game-code dependency). */
|
|
12
12
|
function rng(seed) {
|
|
13
13
|
let a = seed >>> 0;
|
|
14
|
-
return () => {
|
|
14
|
+
return () => {
|
|
15
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
16
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
17
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
18
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
19
|
+
};
|
|
15
20
|
}
|
|
16
21
|
const lerp = (r, lo, hi) => lo + (hi - lo) * r;
|
|
17
|
-
function hashStr(s) {
|
|
18
|
-
h
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
function hashStr(s) {
|
|
23
|
+
let h = 2166136261 >>> 0;
|
|
24
|
+
for (let i = 0; i < s.length; i++) {
|
|
25
|
+
h ^= s.charCodeAt(i);
|
|
26
|
+
h = Math.imul(h, 16777619);
|
|
27
|
+
}
|
|
28
|
+
return h >>> 0;
|
|
29
|
+
}
|
|
21
30
|
/** Sample a full genome from a seed: pick a part per socket, deform it, set global proportion + palette.
|
|
22
31
|
* Per-field seed splits keep draws independent (adding a field can't perturb earlier ones). */
|
|
23
32
|
export function sampleGenome(arch, kit, seed) {
|
|
@@ -29,27 +38,62 @@ export function sampleGenome(arch, kit, seed) {
|
|
|
29
38
|
if (opts.length)
|
|
30
39
|
parts[s.id] = opts[Math.floor(rp() * opts.length) % opts.length].tag.id;
|
|
31
40
|
partDeform[s.id] = {
|
|
32
|
-
scale: [
|
|
33
|
-
|
|
41
|
+
scale: [
|
|
42
|
+
lerp(rd(), 0.85, 1.15),
|
|
43
|
+
lerp(rd(), 0.85, 1.15),
|
|
44
|
+
lerp(rd(), 0.85, 1.15),
|
|
45
|
+
],
|
|
46
|
+
stretch: [
|
|
47
|
+
lerp(rd(), 0.9, 1.1),
|
|
48
|
+
lerp(rd(), 0.9, 1.1),
|
|
49
|
+
lerp(rd(), 0.9, 1.1),
|
|
50
|
+
],
|
|
34
51
|
twist: lerp(rd(), -0.3, 0.3),
|
|
35
52
|
};
|
|
36
53
|
}
|
|
37
54
|
const rprop = rng(seed ^ 0x3003);
|
|
38
|
-
const proportion = {
|
|
55
|
+
const proportion = {
|
|
56
|
+
height: lerp(rprop(), 0.8, 1.3),
|
|
57
|
+
girth: lerp(rprop(), 0.85, 1.2),
|
|
58
|
+
limbLength: lerp(rprop(), 0.85, 1.2),
|
|
59
|
+
headScale: lerp(rprop(), 0.85, 1.25),
|
|
60
|
+
};
|
|
39
61
|
const rpal = rng(seed ^ 0x4004);
|
|
40
62
|
const roleRamp = {};
|
|
41
63
|
for (const s of arch.sockets)
|
|
42
64
|
roleRamp[s.region] ??= Math.floor(rpal() * 6);
|
|
43
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
archetypeId: arch.id,
|
|
67
|
+
partKitVersion: kit.version,
|
|
68
|
+
seed,
|
|
69
|
+
parts,
|
|
70
|
+
partDeform,
|
|
71
|
+
proportion,
|
|
72
|
+
palette: { guideId: '', roleRamp },
|
|
73
|
+
};
|
|
44
74
|
}
|
|
45
75
|
/** Stable canonical string (sorted keys, fixed float precision) → the cache/replay identity. */
|
|
46
76
|
export function canonicalizeGenome(g) {
|
|
47
77
|
const fx = (n) => n.toFixed(6);
|
|
48
|
-
const parts = Object.keys(g.parts)
|
|
49
|
-
|
|
78
|
+
const parts = Object.keys(g.parts)
|
|
79
|
+
.sort()
|
|
80
|
+
.map((k) => `${k}:${g.parts[k]}`)
|
|
81
|
+
.join(',');
|
|
82
|
+
const def = Object.keys(g.partDeform)
|
|
83
|
+
.sort()
|
|
84
|
+
.map((k) => {
|
|
85
|
+
const d = g.partDeform[k];
|
|
86
|
+
return `${k}:${d.scale.map(fx)}|${d.stretch.map(fx)}|${fx(d.twist)}`;
|
|
87
|
+
})
|
|
88
|
+
.join(',');
|
|
50
89
|
const p = g.proportion, prop = [p.height, p.girth, p.limbLength, p.headScale].map(fx).join(',');
|
|
51
|
-
const ramp = Object.keys(g.palette.roleRamp)
|
|
90
|
+
const ramp = Object.keys(g.palette.roleRamp)
|
|
91
|
+
.sort()
|
|
92
|
+
.map((k) => `${k}:${g.palette.roleRamp[k]}`)
|
|
93
|
+
.join(',');
|
|
52
94
|
return `${g.archetypeId}#${g.partKitVersion}#${parts}#${def}#${prop}#${g.palette.guideId}:${ramp}`;
|
|
53
95
|
}
|
|
54
96
|
/** Content hash including the kit version (a kit change invalidates caches deterministically). */
|
|
55
|
-
export function genomeHash(g) {
|
|
97
|
+
export function genomeHash(g) {
|
|
98
|
+
return fnv1a(canonicalizeGenome(g) + '|' + String(g.partKitVersion));
|
|
99
|
+
}
|
|
@@ -10,21 +10,32 @@
|
|
|
10
10
|
import { sampleGenome } from './genome';
|
|
11
11
|
import { assemble } from './assemble';
|
|
12
12
|
import { composeTRS, forwardKinematics, buildPalette } from './pose';
|
|
13
|
-
function toMat(arr) {
|
|
14
|
-
m
|
|
13
|
+
function toMat(arr) {
|
|
14
|
+
const m = new Float64Array(16);
|
|
15
|
+
for (let i = 0; i < 16; i++)
|
|
16
|
+
m[i] = arr[i] ?? 0;
|
|
17
|
+
return m;
|
|
18
|
+
}
|
|
15
19
|
/** The largest VERTICAL gap between an assembled creature's parts (by role vertex-range Y-extents). A
|
|
16
20
|
* big gap means a body region has NO geometry — e.g. dropped mesh primitives (a multi-material sub-mesh
|
|
17
21
|
* silently skipped) or a missing socket. Structural validity + bind correctness both MISS this; it is
|
|
18
22
|
* the mesh-COMPLETENESS check the pipeline lacked (it hid the "floating head / no chest" render bug). */
|
|
19
23
|
export function verticalCoverageGap(a) {
|
|
20
24
|
const pos = a.mesh.positions;
|
|
21
|
-
const ranges = a.roles
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const ranges = a.roles
|
|
26
|
+
.map((r) => {
|
|
27
|
+
let lo = Infinity, hi = -Infinity;
|
|
28
|
+
for (let v = r.first; v < r.first + r.count; v++) {
|
|
29
|
+
const y = pos[v * 3 + 1];
|
|
30
|
+
if (y < lo)
|
|
31
|
+
lo = y;
|
|
32
|
+
if (y > hi)
|
|
33
|
+
hi = y;
|
|
34
|
+
}
|
|
35
|
+
return [lo, hi];
|
|
36
|
+
})
|
|
37
|
+
.filter((r) => r[0] <= r[1])
|
|
38
|
+
.sort((p, q) => p[0] - q[0]);
|
|
28
39
|
if (ranges.length < 2)
|
|
29
40
|
return 0;
|
|
30
41
|
let cover = ranges[0][1], maxGap = 0;
|
|
@@ -57,7 +68,7 @@ export function auditGenome(arch, kit, guide, g) {
|
|
|
57
68
|
const failures = [];
|
|
58
69
|
try {
|
|
59
70
|
for (const s of arch.sockets)
|
|
60
|
-
if (
|
|
71
|
+
if (!(s.id in g.parts))
|
|
61
72
|
failures.push(`socket ${s.id} unfilled`);
|
|
62
73
|
const a = assemble(arch, kit, g, guide);
|
|
63
74
|
const vc = a.mesh.positions.length / 3;
|
|
@@ -79,13 +90,14 @@ export function auditGenome(arch, kit, guide, g) {
|
|
|
79
90
|
failures.push('skin weights do not sum to 1');
|
|
80
91
|
if (a.roles.reduce((t, r) => t + r.count, 0) !== vc)
|
|
81
92
|
failures.push('roles do not cover all vertices');
|
|
82
|
-
if (a.roles.some((r) =>
|
|
93
|
+
if (a.roles.some((r) => !(r.role in guide.roles)))
|
|
83
94
|
failures.push('assigned role not in StyleGuide');
|
|
84
95
|
const gap = verticalCoverageGap(a), height = a.bounds[4] - a.bounds[1];
|
|
85
96
|
if (height > 0 && gap > 0.12 * height)
|
|
86
97
|
failures.push(`vertical geometry gap ${gap.toFixed(2)} of ${height.toFixed(2)} tall — a body region has no mesh (dropped primitive / missing part?)`);
|
|
87
98
|
const b = assemble(arch, kit, g, guide);
|
|
88
|
-
if (JSON.stringify([...a.mesh.positions]) !==
|
|
99
|
+
if (JSON.stringify([...a.mesh.positions]) !==
|
|
100
|
+
JSON.stringify([...b.mesh.positions]))
|
|
89
101
|
failures.push('non-deterministic assemble');
|
|
90
102
|
}
|
|
91
103
|
catch (e) {
|
package/dist/assets/geometry.js
CHANGED
|
@@ -9,7 +9,9 @@ import { MeshoptEncoder, MeshoptDecoder } from 'meshoptimizer';
|
|
|
9
9
|
const POS_STRIDE = 8; // 4 × uint16 (x,y,z + pad) — meshopt requires stride % 4 == 0; the constant
|
|
10
10
|
// pad channel costs ~nothing after encoding.
|
|
11
11
|
const AABB_BYTES = 24; // 6 × float32: min.xyz, max.xyz, prepended to the vtx payload.
|
|
12
|
-
const quant = (v, lo, hi) => hi > lo
|
|
12
|
+
const quant = (v, lo, hi) => hi > lo
|
|
13
|
+
? Math.max(0, Math.min(65535, Math.round(((v - lo) / (hi - lo)) * 65535)))
|
|
14
|
+
: 0;
|
|
13
15
|
function bounds3(p) {
|
|
14
16
|
let a = Infinity, b = Infinity, c = Infinity, d = -Infinity, e = -Infinity, f = -Infinity;
|
|
15
17
|
for (let i = 0; i < p.length; i += 3) {
|
|
@@ -51,7 +53,13 @@ export function compactMesh(positions, indices, uvs) {
|
|
|
51
53
|
}
|
|
52
54
|
outIdx[i] = n;
|
|
53
55
|
}
|
|
54
|
-
return uvs
|
|
56
|
+
return uvs
|
|
57
|
+
? {
|
|
58
|
+
positions: Float32Array.from(posOut),
|
|
59
|
+
indices: outIdx,
|
|
60
|
+
uvs: Float32Array.from(uvOut),
|
|
61
|
+
}
|
|
62
|
+
: { positions: Float32Array.from(posOut), indices: outIdx };
|
|
55
63
|
}
|
|
56
64
|
export async function encodeGeometry(m) {
|
|
57
65
|
await MeshoptEncoder.ready;
|
|
@@ -86,7 +94,10 @@ export async function decodeGeometry(enc) {
|
|
|
86
94
|
}
|
|
87
95
|
const idxBytes = new Uint8Array(enc.indexCount * 4);
|
|
88
96
|
MeshoptDecoder.decodeIndexBuffer(idxBytes, enc.indexCount, 4, enc.idx);
|
|
89
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
positions,
|
|
99
|
+
indices: new Uint32Array(idxBytes.buffer, idxBytes.byteOffset, enc.indexCount),
|
|
100
|
+
};
|
|
90
101
|
}
|
|
91
102
|
// ── UV (TEXCOORD_0): 16-bit quantized within its 2D bounds, meshopt-encoded, shared across LODs ──
|
|
92
103
|
const UV_STRIDE = 4; // 2 × uint16
|
|
@@ -19,11 +19,16 @@ export declare class GpuPropsRenderer {
|
|
|
19
19
|
private bgs;
|
|
20
20
|
private occBGs;
|
|
21
21
|
private counts;
|
|
22
|
+
private dummyShadowView;
|
|
23
|
+
private dummyShadowSamp;
|
|
24
|
+
private shadowBG;
|
|
25
|
+
private lastSV;
|
|
26
|
+
private lastSS;
|
|
22
27
|
constructor(device: GPUDevice, format: GPUTextureFormat);
|
|
23
28
|
setModels(meshes: PropMesh[]): void;
|
|
24
29
|
/** Group instances by model and upload per-model transform buffers. */
|
|
25
30
|
setInstances(instances: PropInstance[]): void;
|
|
26
31
|
/** Occluder pass: draw all instanced props' depth into the shared shadow map from the sun's ortho view. */
|
|
27
32
|
shadowPass(pass: GPURenderPassEncoder, lightVP: Float32Array): void;
|
|
28
|
-
frame(enc: GPUCommandEncoder, colorView: GPUTextureView, depthView: GPUTextureView, camVP: Float32Array, sunDir: [number, number, number], sunCol: [number, number, number], amb: [number, number, number], camPos: [number, number, number], fog: [number, number, number], fogDensity: number, up?: [number, number, number]): void;
|
|
33
|
+
frame(enc: GPUCommandEncoder, colorView: GPUTextureView, depthView: GPUTextureView, camVP: Float32Array, sunDir: [number, number, number], sunCol: [number, number, number], amb: [number, number, number], camPos: [number, number, number], fog: [number, number, number], fogDensity: number, up?: [number, number, number], lightVP?: Float32Array, shadowView?: GPUTextureView, shadowSamp?: GPUSampler): void;
|
|
29
34
|
}
|
package/dist/assets/gpuProps.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// terrain. Runs in a load pass after the terrain so props depth-test against the surface.
|
|
5
5
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
6
|
const SHADER = /* wgsl */ `
|
|
7
|
-
struct U { mvp: mat4x4<f32>, sun: vec4<f32>, sunCol: vec4<f32>, amb: vec4<f32>, cam: vec4<f32>, fog: vec4<f32>, up: vec4<f32> };
|
|
7
|
+
struct U { mvp: mat4x4<f32>, lightVP: mat4x4<f32>, sun: vec4<f32>, sunCol: vec4<f32>, amb: vec4<f32>, cam: vec4<f32>, fog: vec4<f32>, up: vec4<f32> };
|
|
8
8
|
@group(0) @binding(0) var<uniform> u: U;
|
|
9
9
|
@group(0) @binding(1) var<storage,read> models: array<mat4x4<f32>>;
|
|
10
|
+
@group(1) @binding(0) var shadowMap: texture_depth_2d; // the SAME sun shadow map the terrain cubes sample
|
|
11
|
+
@group(1) @binding(1) var shadowSamp: sampler_comparison;
|
|
10
12
|
struct VO { @builtin(position) pos: vec4<f32>, @location(0) world: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec3<f32> };
|
|
11
13
|
@vertex fn vs(@location(0) p: vec3<f32>, @location(1) nrm: vec3<f32>, @location(2) col: vec3<f32>, @builtin(instance_index) ii: u32) -> VO {
|
|
12
14
|
let m = models[ii];
|
|
@@ -22,11 +24,28 @@ fn fixedFill(N: vec3<f32>, up: vec3<f32>) -> f32 {
|
|
|
22
24
|
let L = normalize(up * 0.82 + t * 0.57);
|
|
23
25
|
return max(dot(N, L), 0.0);
|
|
24
26
|
}
|
|
27
|
+
// PCF 3×3 sun shadow — mirrors gpuShadow.shadowFactor so props sit in the SAME shadow as the terrain. 1 = lit, 0 = shadowed.
|
|
28
|
+
fn shadowFactor(world: vec3<f32>, ndl: f32) -> f32 {
|
|
29
|
+
let lp = u.lightVP * vec4(world, 1.0);
|
|
30
|
+
let proj = lp.xyz / lp.w;
|
|
31
|
+
let uv = proj.xy * vec2(0.5, -0.5) + vec2(0.5, 0.5);
|
|
32
|
+
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 || proj.z > 1.0 || proj.z < 0.0) { return 1.0; }
|
|
33
|
+
let bias = max(0.0006 * (1.0 - ndl), 0.0004);
|
|
34
|
+
let texel = 1.0 / 2048.0;
|
|
35
|
+
var s = 0.0;
|
|
36
|
+
for (var dy = -1; dy <= 1; dy = dy + 1) {
|
|
37
|
+
for (var dx = -1; dx <= 1; dx = dx + 1) {
|
|
38
|
+
s = s + textureSampleCompareLevel(shadowMap, shadowSamp, uv + vec2(f32(dx), f32(dy)) * texel, proj.z - bias);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return s / 9.0;
|
|
42
|
+
}
|
|
25
43
|
@fragment fn fs(i: VO) -> @location(0) vec4<f32> {
|
|
26
44
|
let N = normalize(i.n);
|
|
27
45
|
let ff = select(1.0, 0.5 + 0.85 * fixedFill(N, u.up.xyz), u.up.w > 0.5); // fixed fill (fall back to flat amb if no up)
|
|
28
46
|
let d = clamp(dot(N, -normalize(u.sun.xyz)), 0.0, 1.0);
|
|
29
|
-
let
|
|
47
|
+
let sh = mix(1.0, shadowFactor(i.world, d), u.sun.w); // u.sun.w = shadow-receive enable (0 ⇒ off, byte-identical for demo)
|
|
48
|
+
let lit = i.c * (u.amb.xyz * ff + d * u.sunCol.xyz * sh);
|
|
30
49
|
let dist = length(i.world - u.cam.xyz);
|
|
31
50
|
let f = clamp(1.0 - exp(-dist * u.fog.w), 0.0, 1.0);
|
|
32
51
|
return vec4(mix(lit, u.fog.xyz, f), 1.0);
|
|
@@ -49,29 +68,79 @@ export class GpuPropsRenderer {
|
|
|
49
68
|
bgs = [];
|
|
50
69
|
occBGs = [];
|
|
51
70
|
counts = [];
|
|
71
|
+
// shadow-receive (group 1): a dummy 1×1 depth map when no sun shadow is supplied (demo path), else the terrain's
|
|
72
|
+
// shared shadow map. The bind group is cached and only rebuilt when the supplied view/sampler identity changes.
|
|
73
|
+
dummyShadowView;
|
|
74
|
+
dummyShadowSamp;
|
|
75
|
+
shadowBG = null;
|
|
76
|
+
lastSV = null;
|
|
77
|
+
lastSS = null;
|
|
52
78
|
constructor(device, format) {
|
|
53
79
|
this.device = device;
|
|
54
80
|
const sm = device.createShaderModule({ code: SHADER });
|
|
55
81
|
this.pipe = device.createRenderPipeline({
|
|
56
82
|
layout: 'auto',
|
|
57
|
-
vertex: {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
83
|
+
vertex: {
|
|
84
|
+
module: sm,
|
|
85
|
+
entryPoint: 'vs',
|
|
86
|
+
buffers: [
|
|
87
|
+
{
|
|
88
|
+
arrayStride: 12,
|
|
89
|
+
attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
arrayStride: 12,
|
|
93
|
+
attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x3' }],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
arrayStride: 12,
|
|
97
|
+
attributes: [{ shaderLocation: 2, offset: 0, format: 'float32x3' }],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
62
101
|
fragment: { module: sm, entryPoint: 'fs', targets: [{ format }] },
|
|
63
102
|
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
64
|
-
depthStencil: {
|
|
103
|
+
depthStencil: {
|
|
104
|
+
format: 'depth24plus',
|
|
105
|
+
depthWriteEnabled: true,
|
|
106
|
+
depthCompare: 'less',
|
|
107
|
+
},
|
|
65
108
|
});
|
|
66
109
|
const om = device.createShaderModule({ code: OCCLUDER });
|
|
67
110
|
this.occPipe = device.createRenderPipeline({
|
|
68
111
|
layout: 'auto',
|
|
69
|
-
vertex: {
|
|
112
|
+
vertex: {
|
|
113
|
+
module: om,
|
|
114
|
+
entryPoint: 'vs',
|
|
115
|
+
buffers: [
|
|
116
|
+
{
|
|
117
|
+
arrayStride: 12,
|
|
118
|
+
attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
70
122
|
primitive: { topology: 'triangle-list', cullMode: 'none' },
|
|
71
|
-
depthStencil: {
|
|
123
|
+
depthStencil: {
|
|
124
|
+
format: 'depth32float',
|
|
125
|
+
depthWriteEnabled: true,
|
|
126
|
+
depthCompare: 'less',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
this.uBuf = device.createBuffer({
|
|
130
|
+
size: 128 + 16 * 6, // 2× mat4 (mvp + lightVP) + 6× vec4
|
|
131
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
132
|
+
});
|
|
133
|
+
this.lightBuf = device.createBuffer({
|
|
134
|
+
size: 64,
|
|
135
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
72
136
|
});
|
|
73
|
-
|
|
74
|
-
|
|
137
|
+
const dummy = device.createTexture({
|
|
138
|
+
size: [1, 1],
|
|
139
|
+
format: 'depth32float',
|
|
140
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
141
|
+
});
|
|
142
|
+
this.dummyShadowView = dummy.createView();
|
|
143
|
+
this.dummyShadowSamp = device.createSampler({ compare: 'less' });
|
|
75
144
|
}
|
|
76
145
|
setModels(meshes) {
|
|
77
146
|
const dev = this.device, U = GPUBufferUsage;
|
|
@@ -81,8 +150,18 @@ export class GpuPropsRenderer {
|
|
|
81
150
|
p.colB.destroy();
|
|
82
151
|
p.idxB.destroy();
|
|
83
152
|
}
|
|
84
|
-
const mk = (d, u) => {
|
|
85
|
-
|
|
153
|
+
const mk = (d, u) => {
|
|
154
|
+
const b = dev.createBuffer({ size: d.byteLength, usage: u });
|
|
155
|
+
dev.queue.writeBuffer(b, 0, d);
|
|
156
|
+
return b;
|
|
157
|
+
};
|
|
158
|
+
this.models = meshes.map((m) => ({
|
|
159
|
+
posB: mk(m.positions, U.VERTEX | U.COPY_DST),
|
|
160
|
+
nrmB: mk(m.normals, U.VERTEX | U.COPY_DST),
|
|
161
|
+
colB: mk(m.colors, U.VERTEX | U.COPY_DST),
|
|
162
|
+
idxB: mk(m.indices, U.INDEX | U.COPY_DST),
|
|
163
|
+
idxCount: m.indices.length,
|
|
164
|
+
}));
|
|
86
165
|
this.instBufs = meshes.map(() => null);
|
|
87
166
|
this.bgs = meshes.map(() => null);
|
|
88
167
|
this.occBGs = meshes.map(() => null);
|
|
@@ -110,11 +189,26 @@ export class GpuPropsRenderer {
|
|
|
110
189
|
this.occBGs[m] = null;
|
|
111
190
|
continue;
|
|
112
191
|
}
|
|
113
|
-
const b = dev.createBuffer({
|
|
192
|
+
const b = dev.createBuffer({
|
|
193
|
+
size: arr.byteLength,
|
|
194
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
195
|
+
});
|
|
114
196
|
dev.queue.writeBuffer(b, 0, arr);
|
|
115
197
|
this.instBufs[m] = b;
|
|
116
|
-
this.bgs[m] = dev.createBindGroup({
|
|
117
|
-
|
|
198
|
+
this.bgs[m] = dev.createBindGroup({
|
|
199
|
+
layout: this.pipe.getBindGroupLayout(0),
|
|
200
|
+
entries: [
|
|
201
|
+
{ binding: 0, resource: { buffer: this.uBuf } },
|
|
202
|
+
{ binding: 1, resource: { buffer: b } },
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
this.occBGs[m] = dev.createBindGroup({
|
|
206
|
+
layout: this.occPipe.getBindGroupLayout(0),
|
|
207
|
+
entries: [
|
|
208
|
+
{ binding: 0, resource: { buffer: this.lightBuf } },
|
|
209
|
+
{ binding: 1, resource: { buffer: b } },
|
|
210
|
+
],
|
|
211
|
+
});
|
|
118
212
|
}
|
|
119
213
|
}
|
|
120
214
|
/** Occluder pass: draw all instanced props' depth into the shared shadow map from the sun's ortho view. */
|
|
@@ -131,19 +225,45 @@ export class GpuPropsRenderer {
|
|
|
131
225
|
pass.drawIndexed(mdl.idxCount, this.counts[m]);
|
|
132
226
|
}
|
|
133
227
|
}
|
|
134
|
-
frame(enc, colorView, depthView, camVP, sunDir, sunCol, amb, camPos, fog, fogDensity, up
|
|
135
|
-
|
|
228
|
+
frame(enc, colorView, depthView, camVP, sunDir, sunCol, amb, camPos, fog, fogDensity, up,
|
|
229
|
+
// Optional sun shadow-receive: pass the terrain's lightVP + shadow-map view + comparison sampler and props sit
|
|
230
|
+
// in the same shadow. Omit all three (the demo path) → no shadowing, byte-identical to before.
|
|
231
|
+
lightVP, shadowView, shadowSamp) {
|
|
232
|
+
const receive = !!(lightVP && shadowView && shadowSamp);
|
|
233
|
+
const u = new Float32Array(56);
|
|
136
234
|
u.set(camVP, 0);
|
|
137
|
-
u.set(
|
|
138
|
-
u.set([
|
|
139
|
-
u.set([
|
|
140
|
-
u.set([
|
|
141
|
-
u.set([
|
|
235
|
+
u.set(lightVP ?? IDENTITY4, 16); // lightVP (identity when not receiving — result is gated off by sun.w)
|
|
236
|
+
u.set([sunDir[0], sunDir[1], sunDir[2], receive ? 1 : 0], 32); // sun.w = shadow-receive enable
|
|
237
|
+
u.set([sunCol[0], sunCol[1], sunCol[2], 0], 36);
|
|
238
|
+
u.set([amb[0], amb[1], amb[2], 0], 40);
|
|
239
|
+
u.set([camPos[0], camPos[1], camPos[2], 0], 44);
|
|
240
|
+
u.set([fog[0], fog[1], fog[2], fogDensity], 48);
|
|
142
241
|
if (up)
|
|
143
|
-
u.set([up[0], up[1], up[2], 1],
|
|
242
|
+
u.set([up[0], up[1], up[2], 1], 52); // up.w=1 → enable the fixed directional fill (matches the terrain)
|
|
144
243
|
this.device.queue.writeBuffer(this.uBuf, 0, u);
|
|
145
|
-
|
|
244
|
+
// group 1: the shadow map + comparison sampler (dummy when not receiving). Rebuild only when the view changes.
|
|
245
|
+
const sv = shadowView ?? this.dummyShadowView, ss = shadowSamp ?? this.dummyShadowSamp;
|
|
246
|
+
if (!this.shadowBG || sv !== this.lastSV || ss !== this.lastSS) {
|
|
247
|
+
this.shadowBG = this.device.createBindGroup({
|
|
248
|
+
layout: this.pipe.getBindGroupLayout(1),
|
|
249
|
+
entries: [
|
|
250
|
+
{ binding: 0, resource: sv },
|
|
251
|
+
{ binding: 1, resource: ss },
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
this.lastSV = sv;
|
|
255
|
+
this.lastSS = ss;
|
|
256
|
+
}
|
|
257
|
+
const rp = enc.beginRenderPass({
|
|
258
|
+
colorAttachments: [{ view: colorView, loadOp: 'load', storeOp: 'store' }],
|
|
259
|
+
depthStencilAttachment: {
|
|
260
|
+
view: depthView,
|
|
261
|
+
depthLoadOp: 'load',
|
|
262
|
+
depthStoreOp: 'store',
|
|
263
|
+
},
|
|
264
|
+
});
|
|
146
265
|
rp.setPipeline(this.pipe);
|
|
266
|
+
rp.setBindGroup(1, this.shadowBG);
|
|
147
267
|
for (let m = 0; m < this.models.length; m++) {
|
|
148
268
|
const bg = this.bgs[m], mdl = this.models[m];
|
|
149
269
|
if (!bg || this.counts[m] === 0)
|
|
@@ -158,3 +278,7 @@ export class GpuPropsRenderer {
|
|
|
158
278
|
rp.end();
|
|
159
279
|
}
|
|
160
280
|
}
|
|
281
|
+
// column-major identity mat4, used for lightVP when shadow-receive is off
|
|
282
|
+
const IDENTITY4 = new Float32Array([
|
|
283
|
+
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1,
|
|
284
|
+
]);
|