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,208 @@
|
|
|
1
|
+
// Animation → skinning palette (design/asset-streaming.md §5.2). Samples an ANIM clip at a
|
|
2
|
+
// time, runs forward kinematics over the joint hierarchy, and builds the bone-matrix palette
|
|
3
|
+
// the (proven) WGSL skinning kernel consumes. Pure; column-major mat4 matching mat4.ts + WGSL.
|
|
4
|
+
import { mul } from '../mat4';
|
|
5
|
+
/** Column-major T·R·S from a translation, quaternion (xyzw), and scale. */
|
|
6
|
+
export function composeTRS(t, q, s) {
|
|
7
|
+
const [x, y, z, w] = q;
|
|
8
|
+
const [sx, sy, sz] = s;
|
|
9
|
+
const xx = x * x, yy = y * y, zz = z * z, xy = x * y, xz = x * z, yz = y * z, wx = w * x, wy = w * y, wz = w * z;
|
|
10
|
+
// rotation (row-major elements m<row><col>)
|
|
11
|
+
const m00 = 1 - 2 * (yy + zz), m01 = 2 * (xy - wz), m02 = 2 * (xz + wy);
|
|
12
|
+
const m10 = 2 * (xy + wz), m11 = 1 - 2 * (xx + zz), m12 = 2 * (yz - wx);
|
|
13
|
+
const m20 = 2 * (xz - wy), m21 = 2 * (yz + wx), m22 = 1 - 2 * (xx + yy);
|
|
14
|
+
const M = new Float64Array(16); // column-major: M[col*4 + row]
|
|
15
|
+
M[0] = m00 * sx;
|
|
16
|
+
M[1] = m10 * sx;
|
|
17
|
+
M[2] = m20 * sx;
|
|
18
|
+
M[3] = 0;
|
|
19
|
+
M[4] = m01 * sy;
|
|
20
|
+
M[5] = m11 * sy;
|
|
21
|
+
M[6] = m21 * sy;
|
|
22
|
+
M[7] = 0;
|
|
23
|
+
M[8] = m02 * sz;
|
|
24
|
+
M[9] = m12 * sz;
|
|
25
|
+
M[10] = m22 * sz;
|
|
26
|
+
M[11] = 0;
|
|
27
|
+
M[12] = t[0];
|
|
28
|
+
M[13] = t[1];
|
|
29
|
+
M[14] = t[2];
|
|
30
|
+
M[15] = 1;
|
|
31
|
+
return M;
|
|
32
|
+
}
|
|
33
|
+
function sampleVec(times, values, stride, time) {
|
|
34
|
+
const n = times.length;
|
|
35
|
+
if (n === 0)
|
|
36
|
+
return new Array(stride).fill(0);
|
|
37
|
+
if (n === 1 || time <= times[0])
|
|
38
|
+
return Array.from(values.subarray(0, stride));
|
|
39
|
+
if (time >= times[n - 1])
|
|
40
|
+
return Array.from(values.subarray((n - 1) * stride, n * stride));
|
|
41
|
+
let k = 0;
|
|
42
|
+
while (k < n - 1 && times[k + 1] < time)
|
|
43
|
+
k++;
|
|
44
|
+
const f = (time - times[k]) / (times[k + 1] - times[k]);
|
|
45
|
+
const a = values.subarray(k * stride, (k + 1) * stride), b = values.subarray((k + 1) * stride, (k + 2) * stride);
|
|
46
|
+
// quaternion channels (stride 4): take the SHORTEST path — negate b when dot(a,b)<0, else the
|
|
47
|
+
// interpolation rotates ~300° the wrong way at antipodally-stored keyframes (a periodic blink).
|
|
48
|
+
let sign = 1;
|
|
49
|
+
if (stride === 4) {
|
|
50
|
+
let dot = 0;
|
|
51
|
+
for (let i = 0; i < 4; i++)
|
|
52
|
+
dot += a[i] * b[i];
|
|
53
|
+
if (dot < 0)
|
|
54
|
+
sign = -1;
|
|
55
|
+
}
|
|
56
|
+
const out = [];
|
|
57
|
+
for (let i = 0; i < stride; i++)
|
|
58
|
+
out.push(a[i] + (b[i] * sign - a[i]) * f);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** Sample the clip at `time`, overriding each animated joint's rest T/R/S, → per-joint local mats. */
|
|
62
|
+
export function sampleClipToLocals(clip, time, rest) {
|
|
63
|
+
const trs = rest.map(r => ({ t: [...r.t], q: [...r.q], s: [...r.s] }));
|
|
64
|
+
for (const ch of clip.channels) {
|
|
65
|
+
if (ch.jointIndex < 0 || ch.jointIndex >= trs.length)
|
|
66
|
+
continue;
|
|
67
|
+
const stride = ch.path === 1 ? 4 : 3;
|
|
68
|
+
const v = sampleVec(ch.times, ch.values, stride, time);
|
|
69
|
+
const j = trs[ch.jointIndex];
|
|
70
|
+
if (ch.path === 0)
|
|
71
|
+
j.t = [v[0], v[1], v[2]];
|
|
72
|
+
else if (ch.path === 1) {
|
|
73
|
+
const len = Math.hypot(v[0], v[1], v[2], v[3]) || 1;
|
|
74
|
+
j.q = [v[0] / len, v[1] / len, v[2] / len, v[3] / len];
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
j.s = [v[0], v[1], v[2]];
|
|
78
|
+
}
|
|
79
|
+
return trs.map(r => composeTRS(r.t, r.q, r.s));
|
|
80
|
+
}
|
|
81
|
+
/** world[j] = world[parent] × local[j]; root joints (parent<0) are seeded with `rootSeed`
|
|
82
|
+
* (the armature/skeleton-root world matrix) so the hierarchy resolves in scene space.
|
|
83
|
+
* Recursive with memo (parent-order agnostic). */
|
|
84
|
+
export function forwardKinematics(locals, parents, rootSeed) {
|
|
85
|
+
const world = new Array(locals.length).fill(null);
|
|
86
|
+
const resolve = (j) => {
|
|
87
|
+
if (world[j])
|
|
88
|
+
return world[j];
|
|
89
|
+
const p = parents[j];
|
|
90
|
+
world[j] = p < 0 ? (rootSeed ? mul(rootSeed, locals[j]) : locals[j]) : mul(resolve(p), locals[j]);
|
|
91
|
+
return world[j];
|
|
92
|
+
};
|
|
93
|
+
for (let j = 0; j < locals.length; j++)
|
|
94
|
+
resolve(j);
|
|
95
|
+
return world;
|
|
96
|
+
}
|
|
97
|
+
/** Affine inverse of a column-major TRS matrix (assumes last row [0,0,0,1]). */
|
|
98
|
+
export function invert(m) {
|
|
99
|
+
// linear 3×3 part L (L_rc = m[c*4+r]) and translation t
|
|
100
|
+
const L00 = m[0], L10 = m[1], L20 = m[2], L01 = m[4], L11 = m[5], L21 = m[6], L02 = m[8], L12 = m[9], L22 = m[10];
|
|
101
|
+
const t0 = m[12], t1 = m[13], t2 = m[14];
|
|
102
|
+
const det = L00 * (L11 * L22 - L12 * L21) - L01 * (L10 * L22 - L12 * L20) + L02 * (L10 * L21 - L11 * L20);
|
|
103
|
+
const out = new Float64Array(16);
|
|
104
|
+
if (Math.abs(det) < 1e-20) {
|
|
105
|
+
out[0] = out[5] = out[10] = out[15] = 1;
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
const id = 1 / det;
|
|
109
|
+
const i00 = (L11 * L22 - L12 * L21) * id, i01 = (L02 * L21 - L01 * L22) * id, i02 = (L01 * L12 - L02 * L11) * id;
|
|
110
|
+
const i10 = (L12 * L20 - L10 * L22) * id, i11 = (L00 * L22 - L02 * L20) * id, i12 = (L02 * L10 - L00 * L12) * id;
|
|
111
|
+
const i20 = (L10 * L21 - L11 * L20) * id, i21 = (L01 * L20 - L00 * L21) * id, i22 = (L00 * L11 - L01 * L10) * id;
|
|
112
|
+
// column-major: columns are the inverse-linear columns; translation = -invL · t
|
|
113
|
+
out[0] = i00;
|
|
114
|
+
out[1] = i10;
|
|
115
|
+
out[2] = i20;
|
|
116
|
+
out[3] = 0;
|
|
117
|
+
out[4] = i01;
|
|
118
|
+
out[5] = i11;
|
|
119
|
+
out[6] = i21;
|
|
120
|
+
out[7] = 0;
|
|
121
|
+
out[8] = i02;
|
|
122
|
+
out[9] = i12;
|
|
123
|
+
out[10] = i22;
|
|
124
|
+
out[11] = 0;
|
|
125
|
+
out[12] = -(i00 * t0 + i01 * t1 + i02 * t2);
|
|
126
|
+
out[13] = -(i10 * t0 + i11 * t1 + i12 * t2);
|
|
127
|
+
out[14] = -(i20 * t0 + i21 * t1 + i22 * t2);
|
|
128
|
+
out[15] = 1;
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/** Decompose a column-major TRS matrix into translation / rotation-quaternion(xyzw) / scale.
|
|
132
|
+
* Assumes no shear (rigid + non-negative axis scale) — true for bind matrices. */
|
|
133
|
+
export function decomposeTRS(m) {
|
|
134
|
+
const t = [m[12], m[13], m[14]];
|
|
135
|
+
// scale = length of each basis column (column-major: col c = m[c*4 .. c*4+2])
|
|
136
|
+
const sx = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]) || 1;
|
|
137
|
+
const sy = Math.sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]) || 1;
|
|
138
|
+
const sz = Math.sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]) || 1;
|
|
139
|
+
// rotation matrix R(row,col) = m[col*4+row] / scale_col
|
|
140
|
+
const r00 = m[0] / sx, r10 = m[1] / sx, r20 = m[2] / sx;
|
|
141
|
+
const r01 = m[4] / sy, r11 = m[5] / sy, r21 = m[6] / sy;
|
|
142
|
+
const r02 = m[8] / sz, r12 = m[9] / sz, r22 = m[10] / sz;
|
|
143
|
+
const tr = r00 + r11 + r22;
|
|
144
|
+
let x, y, z, w;
|
|
145
|
+
if (tr > 0) {
|
|
146
|
+
const s = Math.sqrt(tr + 1) * 2;
|
|
147
|
+
w = 0.25 * s;
|
|
148
|
+
x = (r21 - r12) / s;
|
|
149
|
+
y = (r02 - r20) / s;
|
|
150
|
+
z = (r10 - r01) / s;
|
|
151
|
+
}
|
|
152
|
+
else if (r00 > r11 && r00 > r22) {
|
|
153
|
+
const s = Math.sqrt(1 + r00 - r11 - r22) * 2;
|
|
154
|
+
w = (r21 - r12) / s;
|
|
155
|
+
x = 0.25 * s;
|
|
156
|
+
y = (r01 + r10) / s;
|
|
157
|
+
z = (r02 + r20) / s;
|
|
158
|
+
}
|
|
159
|
+
else if (r11 > r22) {
|
|
160
|
+
const s = Math.sqrt(1 + r11 - r00 - r22) * 2;
|
|
161
|
+
w = (r02 - r20) / s;
|
|
162
|
+
x = (r01 + r10) / s;
|
|
163
|
+
y = 0.25 * s;
|
|
164
|
+
z = (r12 + r21) / s;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const s = Math.sqrt(1 + r22 - r00 - r11) * 2;
|
|
168
|
+
w = (r10 - r01) / s;
|
|
169
|
+
x = (r02 + r20) / s;
|
|
170
|
+
y = (r12 + r21) / s;
|
|
171
|
+
z = 0.25 * s;
|
|
172
|
+
}
|
|
173
|
+
return { t, q: [x, y, z, w], s: [sx, sy, sz] };
|
|
174
|
+
}
|
|
175
|
+
/** Derive each joint's REST local TRS from the inverseBind matrices, so FK over the result reproduces
|
|
176
|
+
* the BIND pose exactly (FK·inverseBind == identity at rest). Use this instead of a glTF's joint node
|
|
177
|
+
* TRS, which for some rigs is a non-bind pose — the cause of skinning displacement at rest. */
|
|
178
|
+
export function bindLocalsFromInverseBind(parents, inverseBind) {
|
|
179
|
+
const J = parents.length;
|
|
180
|
+
const globalBind = [];
|
|
181
|
+
for (let j = 0; j < J; j++) {
|
|
182
|
+
const ib = new Float64Array(16);
|
|
183
|
+
for (let i = 0; i < 16; i++)
|
|
184
|
+
ib[i] = inverseBind[j * 16 + i];
|
|
185
|
+
globalBind[j] = invert(ib);
|
|
186
|
+
}
|
|
187
|
+
const locals = [];
|
|
188
|
+
for (let j = 0; j < J; j++) {
|
|
189
|
+
const p = parents[j];
|
|
190
|
+
const local = p < 0 ? globalBind[j] : mul(invert(globalBind[p]), globalBind[j]);
|
|
191
|
+
locals[j] = decomposeTRS(local);
|
|
192
|
+
}
|
|
193
|
+
return locals;
|
|
194
|
+
}
|
|
195
|
+
/** palette[j] = world[j] × inverseBind[j], packed into a Float32Array (J×16, column-major) for GPU upload. */
|
|
196
|
+
export function buildPalette(worlds, inverseBind) {
|
|
197
|
+
const J = worlds.length;
|
|
198
|
+
const out = new Float32Array(J * 16);
|
|
199
|
+
for (let j = 0; j < J; j++) {
|
|
200
|
+
const ib = new Float64Array(16);
|
|
201
|
+
for (let i = 0; i < 16; i++)
|
|
202
|
+
ib[i] = inverseBind[j * 16 + i];
|
|
203
|
+
const m = mul(worlds[j], ib);
|
|
204
|
+
for (let i = 0; i < 16; i++)
|
|
205
|
+
out[j * 16 + i] = m[i];
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Budget, type Detail } from './select';
|
|
2
|
+
export interface AssetMeta {
|
|
3
|
+
lodCount: number;
|
|
4
|
+
mipCount: number;
|
|
5
|
+
residentFinestLod: number;
|
|
6
|
+
}
|
|
7
|
+
export interface InstanceView {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
z: number;
|
|
11
|
+
boundsRadius: number;
|
|
12
|
+
meshId: number;
|
|
13
|
+
}
|
|
14
|
+
export interface CameraView {
|
|
15
|
+
px: number;
|
|
16
|
+
py: number;
|
|
17
|
+
pz: number;
|
|
18
|
+
fov: number;
|
|
19
|
+
viewportPx: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function selectBatchDetail(insts: InstanceView[], cam: CameraView, budget: Budget, metaOf: (meshId: number) => AssetMeta): Detail[];
|
|
22
|
+
export interface LodDrawArg {
|
|
23
|
+
pipeline: number;
|
|
24
|
+
lod: number;
|
|
25
|
+
first: number;
|
|
26
|
+
count: number;
|
|
27
|
+
}
|
|
28
|
+
/** Compact visible instance indices grouped by (pipeline, lod); one draw per non-empty
|
|
29
|
+
* group. Draw count = O(pipelines×lods), independent of instance count — the LOD-aware
|
|
30
|
+
* extension of renderIR.ts's cull/batch that the gpuDriven.ts cull pass mirrors. */
|
|
31
|
+
export declare function batchByPipelineAndLod(visible: number[], pipelineOf: (i: number) => number, lodOf: (i: number) => number): {
|
|
32
|
+
order: Int32Array;
|
|
33
|
+
draws: LodDrawArg[];
|
|
34
|
+
};
|
|
35
|
+
/** Fetch order for the streaming loader (design/asset-streaming.md §6): instance indices
|
|
36
|
+
* sorted by descending screen coverage — near/large first, off-screen/small last. */
|
|
37
|
+
export declare function streamPriority(insts: InstanceView[], cam: CameraView): number[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Bridges cooked assets to the render path (design/asset-streaming.md §6). Per-instance
|
|
2
|
+
// LOD/mip/tier from screen-space error + device budget, clamped to resident LODs, then
|
|
3
|
+
// grouped into O(pipelines×lods) draws — the CPU reference for the gpuDriven.ts cull pass.
|
|
4
|
+
import { projectedError, selectDetail } from './select';
|
|
5
|
+
export function selectBatchDetail(insts, cam, budget, metaOf) {
|
|
6
|
+
return insts.map(it => {
|
|
7
|
+
const m = metaOf(it.meshId);
|
|
8
|
+
const dist = Math.hypot(it.x - cam.px, it.y - cam.py, it.z - cam.pz);
|
|
9
|
+
const err = projectedError(it.boundsRadius, dist, cam.fov, cam.viewportPx);
|
|
10
|
+
const d = selectDetail(err, m.lodCount, m.mipCount, budget);
|
|
11
|
+
const lodIndex = Math.max(d.lodIndex, m.residentFinestLod); // never finer than resident
|
|
12
|
+
return { ...d, lodIndex };
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** Compact visible instance indices grouped by (pipeline, lod); one draw per non-empty
|
|
16
|
+
* group. Draw count = O(pipelines×lods), independent of instance count — the LOD-aware
|
|
17
|
+
* extension of renderIR.ts's cull/batch that the gpuDriven.ts cull pass mirrors. */
|
|
18
|
+
export function batchByPipelineAndLod(visible, pipelineOf, lodOf) {
|
|
19
|
+
const keyOf = (i) => `${pipelineOf(i)}:${lodOf(i)}`;
|
|
20
|
+
const counts = new Map();
|
|
21
|
+
for (const i of visible)
|
|
22
|
+
counts.set(keyOf(i), (counts.get(keyOf(i)) ?? 0) + 1);
|
|
23
|
+
// deterministic group order: ascending pipeline, then lod
|
|
24
|
+
const keys = [...counts.keys()].sort((a, b) => { const [pa, la] = a.split(':').map(Number); const [pb, lb] = b.split(':').map(Number); return (pa - pb) || (la - lb); });
|
|
25
|
+
const start = new Map();
|
|
26
|
+
let cursor = 0;
|
|
27
|
+
const draws = [];
|
|
28
|
+
for (const k of keys) {
|
|
29
|
+
const [p, l] = k.split(':').map(Number);
|
|
30
|
+
const c = counts.get(k);
|
|
31
|
+
start.set(k, cursor);
|
|
32
|
+
draws.push({ pipeline: p, lod: l, first: cursor, count: c });
|
|
33
|
+
cursor += c;
|
|
34
|
+
}
|
|
35
|
+
const scatter = new Map(start);
|
|
36
|
+
const order = new Int32Array(visible.length);
|
|
37
|
+
for (const i of visible) {
|
|
38
|
+
const k = keyOf(i);
|
|
39
|
+
const at = scatter.get(k);
|
|
40
|
+
order[at] = i;
|
|
41
|
+
scatter.set(k, at + 1);
|
|
42
|
+
}
|
|
43
|
+
return { order, draws };
|
|
44
|
+
}
|
|
45
|
+
/** Fetch order for the streaming loader (design/asset-streaming.md §6): instance indices
|
|
46
|
+
* sorted by descending screen coverage — near/large first, off-screen/small last. */
|
|
47
|
+
export function streamPriority(insts, cam) {
|
|
48
|
+
const score = insts.map((it, i) => {
|
|
49
|
+
const dist = Math.hypot(it.x - cam.px, it.y - cam.py, it.z - cam.pz);
|
|
50
|
+
return { i, cov: projectedError(it.boundsRadius, dist, cam.fov, cam.viewportPx) };
|
|
51
|
+
});
|
|
52
|
+
score.sort((a, b) => b.cov - a.cov); // descending screen coverage
|
|
53
|
+
return score.map(s => s.i);
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class Residency {
|
|
2
|
+
private budgetBytes;
|
|
3
|
+
private order;
|
|
4
|
+
private size;
|
|
5
|
+
private pinned;
|
|
6
|
+
private used;
|
|
7
|
+
constructor(budgetBytes: number);
|
|
8
|
+
admit(key: string, bytes: number, base: boolean): void;
|
|
9
|
+
touch(key: string): void;
|
|
10
|
+
has(key: string): boolean;
|
|
11
|
+
residentBytes(): number;
|
|
12
|
+
private evictOne;
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// GPU-memory residency with LRU eviction (design/asset-streaming.md §7). Base-tier chunks
|
|
2
|
+
// are pinned (never evicted) so something always renders; fine chunks evict LRU under budget.
|
|
3
|
+
export class Residency {
|
|
4
|
+
budgetBytes;
|
|
5
|
+
order = []; // MRU at the end
|
|
6
|
+
size = new Map();
|
|
7
|
+
pinned = new Set();
|
|
8
|
+
used = 0;
|
|
9
|
+
constructor(budgetBytes) {
|
|
10
|
+
this.budgetBytes = budgetBytes;
|
|
11
|
+
}
|
|
12
|
+
admit(key, bytes, base) {
|
|
13
|
+
if (this.size.has(key)) {
|
|
14
|
+
this.touch(key);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
while (this.used + bytes > this.budgetBytes) {
|
|
18
|
+
if (!this.evictOne())
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
this.size.set(key, bytes);
|
|
22
|
+
this.order.push(key);
|
|
23
|
+
this.used += bytes;
|
|
24
|
+
if (base)
|
|
25
|
+
this.pinned.add(key);
|
|
26
|
+
}
|
|
27
|
+
touch(key) { const i = this.order.indexOf(key); if (i >= 0) {
|
|
28
|
+
this.order.splice(i, 1);
|
|
29
|
+
this.order.push(key);
|
|
30
|
+
} }
|
|
31
|
+
has(key) { return this.size.has(key); }
|
|
32
|
+
residentBytes() { return this.used; }
|
|
33
|
+
evictOne() {
|
|
34
|
+
for (let i = 0; i < this.order.length; i++) {
|
|
35
|
+
const k = this.order[i];
|
|
36
|
+
if (this.pinned.has(k))
|
|
37
|
+
continue;
|
|
38
|
+
this.order.splice(i, 1);
|
|
39
|
+
this.used -= this.size.get(k);
|
|
40
|
+
this.size.delete(k);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false; // nothing evictable (all pinned)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnimClip } from './anim';
|
|
2
|
+
import type { AssembledMesh } from './assemble';
|
|
3
|
+
import type { GlobalProportion } from './genome';
|
|
4
|
+
/** Sample `clip` at time `t` onto the assembled (deformed) rig → the joint palette (jointCount×16). */
|
|
5
|
+
export declare function poseAssembled(a: AssembledMesh, clip: AnimClip, t: number): Float32Array;
|
|
6
|
+
/** The bone palette for the (deformed) rest pose — no clip. Skinning with this reproduces the deformed
|
|
7
|
+
* creature standing at rest, which is what the Genome Garden shows. */
|
|
8
|
+
export declare function restPalette(a: AssembledMesh): Float32Array;
|
|
9
|
+
/** Pre-scale a clip's TRANSLATION channels (path 0) by the proportion, so a clip authored on the base
|
|
10
|
+
* rig tracks a resized rig (rotation channels are metric-free and pass through unchanged). */
|
|
11
|
+
export declare function retargetClip(clip: AnimClip, prop: GlobalProportion): AnimClip;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// RETARGET — plays a shared archetype animation clip on an assembled (deformed) creature (PLATFORM).
|
|
3
|
+
// Same-archetype retarget is EXACT + trivial: every creature shares the joint set/numbering; the genome
|
|
4
|
+
// only deforms rest translations, so a library clip samples straight onto the deformed rest and FK does
|
|
5
|
+
// the rest. retargetClip pre-scales a clip's translation channels by the proportion so absolute
|
|
6
|
+
// positions track the resized rig. poseAssembled returns the bone palette the WGSL skin kernel consumes.
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
import { sampleClipToLocals, forwardKinematics, buildPalette } from './pose';
|
|
9
|
+
function toMat(arr) { const m = new Float64Array(16); for (let i = 0; i < 16; i++)
|
|
10
|
+
m[i] = arr[i] ?? 0; return m; }
|
|
11
|
+
/** Sample `clip` at time `t` onto the assembled (deformed) rig → the joint palette (jointCount×16). */
|
|
12
|
+
export function poseAssembled(a, clip, t) {
|
|
13
|
+
const locals = sampleClipToLocals(clip, t, a.restLocals);
|
|
14
|
+
const worlds = forwardKinematics(locals, a.parents, toMat(a.rootWorld));
|
|
15
|
+
return buildPalette(worlds, a.inverseBind);
|
|
16
|
+
}
|
|
17
|
+
/** The bone palette for the (deformed) rest pose — no clip. Skinning with this reproduces the deformed
|
|
18
|
+
* creature standing at rest, which is what the Genome Garden shows. */
|
|
19
|
+
export function restPalette(a) {
|
|
20
|
+
return poseAssembled(a, { name: 'rest', duration: 0, channels: [] }, 0);
|
|
21
|
+
}
|
|
22
|
+
/** Pre-scale a clip's TRANSLATION channels (path 0) by the proportion, so a clip authored on the base
|
|
23
|
+
* rig tracks a resized rig (rotation channels are metric-free and pass through unchanged). */
|
|
24
|
+
export function retargetClip(clip, prop) {
|
|
25
|
+
const channels = clip.channels.map((ch) => {
|
|
26
|
+
if (ch.path !== 0)
|
|
27
|
+
return ch;
|
|
28
|
+
const v = new Float32Array(ch.values.length);
|
|
29
|
+
for (let i = 0; i + 2 < v.length; i += 3) {
|
|
30
|
+
v[i] = ch.values[i] * prop.girth;
|
|
31
|
+
v[i + 1] = ch.values[i + 1] * prop.height;
|
|
32
|
+
v[i + 2] = ch.values[i + 2] * prop.girth;
|
|
33
|
+
}
|
|
34
|
+
return { ...ch, values: v };
|
|
35
|
+
});
|
|
36
|
+
return { ...clip, channels };
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JointTRS } from './pose';
|
|
2
|
+
export interface SkelData {
|
|
3
|
+
jointCount: number;
|
|
4
|
+
vertexCount: number;
|
|
5
|
+
parents: Int16Array;
|
|
6
|
+
inverseBind: Float32Array;
|
|
7
|
+
jointsPerVertex: Uint16Array;
|
|
8
|
+
weightsPerVertex: Float32Array;
|
|
9
|
+
rootWorld?: number[];
|
|
10
|
+
restLocals?: JointTRS[];
|
|
11
|
+
}
|
|
12
|
+
export declare function packSkel(s: SkelData): Uint8Array;
|
|
13
|
+
export declare function unpackSkel(b: Uint8Array): Required<SkelData>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const align4 = (n) => (n + 3) & ~3;
|
|
2
|
+
const IDENT16 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
3
|
+
// Quantize 4 weights to u8 summing to exactly 255 (largest-remainder), so decode (÷255) sums to 1.
|
|
4
|
+
function quantWeights4(w0, w1, w2, w3) {
|
|
5
|
+
const sum = w0 + w1 + w2 + w3;
|
|
6
|
+
if (sum <= 0)
|
|
7
|
+
return [0, 0, 0, 0];
|
|
8
|
+
const scaled = [w0 / sum * 255, w1 / sum * 255, w2 / sum * 255, w3 / sum * 255];
|
|
9
|
+
const floor = scaled.map(Math.floor);
|
|
10
|
+
let rem = 255 - (floor[0] + floor[1] + floor[2] + floor[3]);
|
|
11
|
+
// hand the leftover units to the largest fractional parts
|
|
12
|
+
const order = [0, 1, 2, 3].sort((a, b) => (scaled[b] - floor[b]) - (scaled[a] - floor[a]));
|
|
13
|
+
for (let k = 0; k < rem; k++)
|
|
14
|
+
floor[order[k % 4]]++;
|
|
15
|
+
return [floor[0], floor[1], floor[2], floor[3]];
|
|
16
|
+
}
|
|
17
|
+
export function packSkel(s) {
|
|
18
|
+
const jc = s.jointCount, vc = s.vertexCount;
|
|
19
|
+
const jb = jc <= 255 ? 1 : 2; // joint-index bytes
|
|
20
|
+
const pParents = 12, pIB = align4(pParents + jc * 2), pJoints = pIB + jc * 16 * 4;
|
|
21
|
+
const pWeights = pJoints + vc * 4 * jb, pRoot = align4(pWeights + vc * 4), pRest = pRoot + 64;
|
|
22
|
+
const out = new Uint8Array(pRest + jc * 40);
|
|
23
|
+
const dv = new DataView(out.buffer);
|
|
24
|
+
dv.setUint32(0, jc, true);
|
|
25
|
+
dv.setUint32(4, vc, true);
|
|
26
|
+
dv.setUint8(8, jb);
|
|
27
|
+
for (let i = 0; i < jc; i++)
|
|
28
|
+
dv.setInt16(pParents + i * 2, s.parents[i], true);
|
|
29
|
+
for (let i = 0; i < jc * 16; i++)
|
|
30
|
+
dv.setFloat32(pIB + i * 4, s.inverseBind[i], true);
|
|
31
|
+
for (let i = 0; i < vc * 4; i++) {
|
|
32
|
+
const j = s.jointsPerVertex[i];
|
|
33
|
+
if (jb === 1)
|
|
34
|
+
dv.setUint8(pJoints + i, j);
|
|
35
|
+
else
|
|
36
|
+
dv.setUint16(pJoints + i * 2, j, true);
|
|
37
|
+
}
|
|
38
|
+
for (let v = 0; v < vc; v++) {
|
|
39
|
+
const q = quantWeights4(s.weightsPerVertex[v * 4], s.weightsPerVertex[v * 4 + 1], s.weightsPerVertex[v * 4 + 2], s.weightsPerVertex[v * 4 + 3]);
|
|
40
|
+
for (let k = 0; k < 4; k++)
|
|
41
|
+
dv.setUint8(pWeights + v * 4 + k, q[k]);
|
|
42
|
+
}
|
|
43
|
+
const root = s.rootWorld ?? IDENT16;
|
|
44
|
+
for (let i = 0; i < 16; i++)
|
|
45
|
+
dv.setFloat32(pRoot + i * 4, root[i] ?? IDENT16[i], true);
|
|
46
|
+
for (let j = 0; j < jc; j++) {
|
|
47
|
+
const r = s.restLocals?.[j];
|
|
48
|
+
const base = pRest + j * 40;
|
|
49
|
+
const t = r?.t ?? [0, 0, 0], q = r?.q ?? [0, 0, 0, 1], sc = r?.s ?? [1, 1, 1];
|
|
50
|
+
dv.setFloat32(base, t[0], true);
|
|
51
|
+
dv.setFloat32(base + 4, t[1], true);
|
|
52
|
+
dv.setFloat32(base + 8, t[2], true);
|
|
53
|
+
dv.setFloat32(base + 12, q[0], true);
|
|
54
|
+
dv.setFloat32(base + 16, q[1], true);
|
|
55
|
+
dv.setFloat32(base + 20, q[2], true);
|
|
56
|
+
dv.setFloat32(base + 24, q[3], true);
|
|
57
|
+
dv.setFloat32(base + 28, sc[0], true);
|
|
58
|
+
dv.setFloat32(base + 32, sc[1], true);
|
|
59
|
+
dv.setFloat32(base + 36, sc[2], true);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
export function unpackSkel(b) {
|
|
64
|
+
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
65
|
+
const jc = dv.getUint32(0, true), vc = dv.getUint32(4, true), jb = dv.getUint8(8) || 2;
|
|
66
|
+
const pParents = 12, pIB = align4(pParents + jc * 2), pJoints = pIB + jc * 16 * 4;
|
|
67
|
+
const pWeights = pJoints + vc * 4 * jb, pRoot = align4(pWeights + vc * 4), pRest = pRoot + 64;
|
|
68
|
+
const parents = new Int16Array(jc);
|
|
69
|
+
for (let i = 0; i < jc; i++)
|
|
70
|
+
parents[i] = dv.getInt16(pParents + i * 2, true);
|
|
71
|
+
const inverseBind = new Float32Array(jc * 16);
|
|
72
|
+
for (let i = 0; i < jc * 16; i++)
|
|
73
|
+
inverseBind[i] = dv.getFloat32(pIB + i * 4, true);
|
|
74
|
+
const jointsPerVertex = new Uint16Array(vc * 4);
|
|
75
|
+
for (let i = 0; i < vc * 4; i++)
|
|
76
|
+
jointsPerVertex[i] = jb === 1 ? dv.getUint8(pJoints + i) : dv.getUint16(pJoints + i * 2, true);
|
|
77
|
+
const weightsPerVertex = new Float32Array(vc * 4);
|
|
78
|
+
for (let i = 0; i < vc * 4; i++)
|
|
79
|
+
weightsPerVertex[i] = dv.getUint8(pWeights + i) / 255;
|
|
80
|
+
const rootWorld = [];
|
|
81
|
+
for (let i = 0; i < 16; i++)
|
|
82
|
+
rootWorld.push(dv.getFloat32(pRoot + i * 4, true));
|
|
83
|
+
const restLocals = [];
|
|
84
|
+
for (let j = 0; j < jc; j++) {
|
|
85
|
+
const base = pRest + j * 40;
|
|
86
|
+
restLocals.push({
|
|
87
|
+
t: [dv.getFloat32(base, true), dv.getFloat32(base + 4, true), dv.getFloat32(base + 8, true)],
|
|
88
|
+
q: [dv.getFloat32(base + 12, true), dv.getFloat32(base + 16, true), dv.getFloat32(base + 20, true), dv.getFloat32(base + 24, true)],
|
|
89
|
+
s: [dv.getFloat32(base + 28, true), dv.getFloat32(base + 32, true), dv.getFloat32(base + 36, true)],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return { jointCount: jc, vertexCount: vc, parents, inverseBind, jointsPerVertex, weightsPerVertex, rootWorld, restLocals };
|
|
93
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function projectedError(radius: number, distance: number, fov: number, viewportPx: number): number;
|
|
2
|
+
export interface Budget {
|
|
3
|
+
scalar: number;
|
|
4
|
+
}
|
|
5
|
+
export interface Detail {
|
|
6
|
+
lodIndex: number;
|
|
7
|
+
mipBase: number;
|
|
8
|
+
materialTier: 0 | 1;
|
|
9
|
+
}
|
|
10
|
+
export declare function selectDetail(errPx: number, lodCount: number, mipCount: number, budget: Budget): Detail;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The unifying selection driver (design/asset-streaming.md §6). One screen-error metric,
|
|
2
|
+
// scaled by a per-device budget, picks LOD + mip base + material tier. Pure & monotone.
|
|
3
|
+
export function projectedError(radius, distance, fov, viewportPx) {
|
|
4
|
+
const d = Math.max(distance, 1e-3);
|
|
5
|
+
// angular size → fraction of the vertical FOV → pixels
|
|
6
|
+
const angular = 2 * Math.atan(radius / d);
|
|
7
|
+
return (angular / fov) * viewportPx;
|
|
8
|
+
}
|
|
9
|
+
export function selectDetail(errPx, lodCount, mipCount, budget) {
|
|
10
|
+
const e = errPx * Math.max(budget.scalar, 1e-3); // weaker device shrinks effective error
|
|
11
|
+
// LOD: ≥256px ⇒ LOD0, halving thresholds down the ladder.
|
|
12
|
+
let lodIndex = 0;
|
|
13
|
+
for (let l = 0; l < lodCount; l++) {
|
|
14
|
+
if (e >= 256 / (1 << l)) {
|
|
15
|
+
lodIndex = l;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
lodIndex = l;
|
|
19
|
+
}
|
|
20
|
+
// mip base: finest (0) when large on screen; coarser as it shrinks.
|
|
21
|
+
const mipBase = Math.min(mipCount - 1, Math.max(0, Math.floor(Math.log2(Math.max(1, 512 / Math.max(e, 1))))));
|
|
22
|
+
const materialTier = e >= 64 ? 1 : 0;
|
|
23
|
+
return { lodIndex, mipBase, materialTier };
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type TocEntry } from './ugm';
|
|
2
|
+
export interface Transport {
|
|
3
|
+
range(start: number, end: number): Promise<Uint8Array>;
|
|
4
|
+
bytesFetched: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TierEvent {
|
|
7
|
+
detailLevel: number;
|
|
8
|
+
kinds: number[];
|
|
9
|
+
bytesFetchedSoFar: number;
|
|
10
|
+
decodeWrites: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function streamUgm(t: Transport, onTier: (e: TierEvent) => void, priority?: (e: TocEntry) => number): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Streaming loader (design/asset-streaming.md §8,§10-12). Reads the header (trivial JS
|
|
2
|
+
// DataView), fetches the base prefix first (emits a coarse tier), then refinement chunks
|
|
3
|
+
// by priority. Counts decode-writes to hold the §12 data-copy budget: one per chunk.
|
|
4
|
+
import { readHeader, readToc, HEADER_BYTES } from './ugm';
|
|
5
|
+
export async function streamUgm(t, onTier, priority = e => e.detailLevel) {
|
|
6
|
+
// 1. header (fixed 64B) — trivial JS parse
|
|
7
|
+
const head = await t.range(0, HEADER_BYTES);
|
|
8
|
+
const h = readHeader(head);
|
|
9
|
+
// 2. base prefix — one contiguous fetch containing header + TOC + base chunks (coarse render now)
|
|
10
|
+
const prefix = await t.range(0, h.basePayloadLen);
|
|
11
|
+
const toc = readToc(prefix, h);
|
|
12
|
+
const baseEntries = toc.filter(e => e.offset + e.length <= h.basePayloadLen);
|
|
13
|
+
emit(baseEntries, 0);
|
|
14
|
+
// 3. refinements grouped by detail level, in priority order
|
|
15
|
+
const refine = toc.filter(e => e.offset + e.length > h.basePayloadLen).sort((a, b) => priority(a) - priority(b));
|
|
16
|
+
const byLevel = new Map();
|
|
17
|
+
for (const e of refine) {
|
|
18
|
+
const g = byLevel.get(e.detailLevel) ?? [];
|
|
19
|
+
g.push(e);
|
|
20
|
+
byLevel.set(e.detailLevel, g);
|
|
21
|
+
}
|
|
22
|
+
for (const level of [...byLevel.keys()].sort((a, b) => a - b)) {
|
|
23
|
+
const group = byLevel.get(level);
|
|
24
|
+
for (const e of group)
|
|
25
|
+
await t.range(e.offset, e.offset + e.length);
|
|
26
|
+
emit(group, level);
|
|
27
|
+
}
|
|
28
|
+
function emit(entries, detailLevel) {
|
|
29
|
+
// one decode-write per chunk (meshopt/basis decode → upload-ready buffer); Phase 2 does the real GPU upload.
|
|
30
|
+
onTier({ detailLevel, kinds: entries.map(e => e.kind), bytesFetchedSoFar: t.bytesFetched, decodeWrites: entries.length });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type RGB = [number, number, number];
|
|
2
|
+
export interface StyleGuide {
|
|
3
|
+
id: string;
|
|
4
|
+
roles: Record<string, RGB>;
|
|
5
|
+
regionRole: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
/** The role a region resolves to under this guide (falls back to the first role if unmapped). */
|
|
8
|
+
export declare function resolveRole(guide: StyleGuide, region: string): string;
|
|
9
|
+
/** The rgb for a region under this guide. */
|
|
10
|
+
export declare function resolveRGB(guide: StyleGuide, region: string): RGB;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// STYLE GUIDE — the cohesion layer (PLATFORM, generic; see design/content-architecture.md Component 1).
|
|
3
|
+
// Content references color BY ROLE, never by raw value, so re-grading the guide re-grades every
|
|
4
|
+
// assembled creature at once — the mechanism that makes AI/procedural bulk read as one art direction.
|
|
5
|
+
// The assembler assigns each part's vertex range a role via resolveRole; the renderer looks up the rgb.
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
/** The role a region resolves to under this guide (falls back to the first role if unmapped). */
|
|
8
|
+
export function resolveRole(guide, region) {
|
|
9
|
+
const r = guide.regionRole[region];
|
|
10
|
+
if (r && guide.roles[r])
|
|
11
|
+
return r;
|
|
12
|
+
return Object.keys(guide.roles)[0] ?? 'default';
|
|
13
|
+
}
|
|
14
|
+
/** The rgb for a region under this guide. */
|
|
15
|
+
export function resolveRGB(guide, region) {
|
|
16
|
+
return guide.roles[resolveRole(guide, region)] ?? [0.7, 0.7, 0.7];
|
|
17
|
+
}
|