ugly-game 0.3.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.
Files changed (43) hide show
  1. package/dist/assets/anim.d.ts +13 -0
  2. package/dist/assets/anim.js +55 -0
  3. package/dist/assets/animMoat.d.ts +36 -0
  4. package/dist/assets/animMoat.js +55 -0
  5. package/dist/assets/archetype.d.ts +20 -0
  6. package/dist/assets/archetype.js +8 -0
  7. package/dist/assets/assemble.d.ts +26 -0
  8. package/dist/assets/assemble.js +92 -0
  9. package/dist/assets/genome.d.ts +33 -0
  10. package/dist/assets/genome.js +55 -0
  11. package/dist/assets/genomeExplore.d.ts +28 -0
  12. package/dist/assets/genomeExplore.js +100 -0
  13. package/dist/assets/geometry.d.ts +23 -0
  14. package/dist/assets/geometry.js +134 -0
  15. package/dist/assets/gpuSkin.d.ts +21 -0
  16. package/dist/assets/gpuSkin.js +148 -0
  17. package/dist/assets/lod.d.ts +2 -0
  18. package/dist/assets/lod.js +18 -0
  19. package/dist/assets/meshlet.d.ts +23 -0
  20. package/dist/assets/meshlet.js +53 -0
  21. package/dist/assets/morph.d.ts +5 -0
  22. package/dist/assets/morph.js +75 -0
  23. package/dist/assets/partkit.d.ts +28 -0
  24. package/dist/assets/partkit.js +21 -0
  25. package/dist/assets/pose.d.ts +26 -0
  26. package/dist/assets/pose.js +208 -0
  27. package/dist/assets/renderAssets.d.ts +37 -0
  28. package/dist/assets/renderAssets.js +54 -0
  29. package/dist/assets/residency.d.ts +13 -0
  30. package/dist/assets/residency.js +45 -0
  31. package/dist/assets/retarget.d.ts +11 -0
  32. package/dist/assets/retarget.js +37 -0
  33. package/dist/assets/rig.d.ts +13 -0
  34. package/dist/assets/rig.js +93 -0
  35. package/dist/assets/select.d.ts +10 -0
  36. package/dist/assets/select.js +24 -0
  37. package/dist/assets/stream.d.ts +12 -0
  38. package/dist/assets/stream.js +32 -0
  39. package/dist/assets/style.d.ts +10 -0
  40. package/dist/assets/style.js +17 -0
  41. package/dist/assets/ugm.d.ts +53 -0
  42. package/dist/assets/ugm.js +100 -0
  43. package/package.json +2 -1
@@ -0,0 +1,134 @@
1
+ // Geometry wire codec (design/asset-streaming.md §0/§1): positions + UVs are QUANTIZED to 16-bit
2
+ // within a per-mesh AABB, then run through the meshopt vertex codec. Quantizing first is the real
3
+ // size lever — the meshopt delta-coder compresses integers far better than float32 mantissas (whose
4
+ // low bits are high-entropy noise), and 16-bit across a model-scale AABB is sub-0.1mm, visually
5
+ // exact. A small float32 AABB prepends each payload so the chunk self-describes; decode dequantizes
6
+ // back to float32 so the GPU/render path is unchanged. Encode is offline, decode runs in-browser;
7
+ // both live here because meshoptimizer's WASM runs in Node and the browser.
8
+ import { MeshoptEncoder, MeshoptDecoder } from 'meshoptimizer';
9
+ const POS_STRIDE = 8; // 4 × uint16 (x,y,z + pad) — meshopt requires stride % 4 == 0; the constant
10
+ // pad channel costs ~nothing after encoding.
11
+ const AABB_BYTES = 24; // 6 × float32: min.xyz, max.xyz, prepended to the vtx payload.
12
+ const quant = (v, lo, hi) => hi > lo ? Math.max(0, Math.min(65535, Math.round((v - lo) / (hi - lo) * 65535))) : 0;
13
+ function bounds3(p) {
14
+ let a = Infinity, b = Infinity, c = Infinity, d = -Infinity, e = -Infinity, f = -Infinity;
15
+ for (let i = 0; i < p.length; i += 3) {
16
+ const x = p[i], y = p[i + 1], z = p[i + 2];
17
+ if (x < a)
18
+ a = x;
19
+ if (y < b)
20
+ b = y;
21
+ if (z < c)
22
+ c = z;
23
+ if (x > d)
24
+ d = x;
25
+ if (y > e)
26
+ e = y;
27
+ if (z > f)
28
+ f = z;
29
+ }
30
+ return [a, b, c, d, e, f];
31
+ }
32
+ // Compact a mesh (+ optional parallel per-vertex UV) to only the vertices its index buffer
33
+ // references, remapping indices to first-encounter order. A simplified LOD references a subset of
34
+ // the source vertices; without this each LOD re-stores the FULL vertex buffer (design/asset-
35
+ // streaming.md §3). Positions and UVs are remapped with the SAME table so they stay aligned. NOTE:
36
+ // this REORDERS vertices, so it must NOT be applied to a mesh whose per-vertex skin (SKEL) is kept
37
+ // separately — only compact LODs that carry no external per-vertex data (i.e. not the finest/skinned LOD).
38
+ export function compactMesh(positions, indices, uvs) {
39
+ const remap = new Int32Array(positions.length / 3).fill(-1);
40
+ const outIdx = new Uint32Array(indices.length);
41
+ const posOut = [], uvOut = [];
42
+ for (let i = 0; i < indices.length; i++) {
43
+ const o = indices[i];
44
+ let n = remap[o];
45
+ if (n < 0) {
46
+ n = posOut.length / 3;
47
+ remap[o] = n;
48
+ posOut.push(positions[o * 3], positions[o * 3 + 1], positions[o * 3 + 2]);
49
+ if (uvs)
50
+ uvOut.push(uvs[o * 2], uvs[o * 2 + 1]);
51
+ }
52
+ outIdx[i] = n;
53
+ }
54
+ return uvs ? { positions: Float32Array.from(posOut), indices: outIdx, uvs: Float32Array.from(uvOut) } : { positions: Float32Array.from(posOut), indices: outIdx };
55
+ }
56
+ export async function encodeGeometry(m) {
57
+ await MeshoptEncoder.ready;
58
+ const vertexCount = m.positions.length / 3;
59
+ const [minx, miny, minz, maxx, maxy, maxz] = bounds3(m.positions);
60
+ const q = new Uint16Array(vertexCount * 4);
61
+ for (let i = 0; i < vertexCount; i++) {
62
+ q[i * 4] = quant(m.positions[i * 3], minx, maxx);
63
+ q[i * 4 + 1] = quant(m.positions[i * 3 + 1], miny, maxy);
64
+ q[i * 4 + 2] = quant(m.positions[i * 3 + 2], minz, maxz);
65
+ }
66
+ const enc = MeshoptEncoder.encodeVertexBuffer(new Uint8Array(q.buffer), vertexCount, POS_STRIDE);
67
+ const vtx = new Uint8Array(AABB_BYTES + enc.length);
68
+ new Float32Array(vtx.buffer, 0, 6).set([minx, miny, minz, maxx, maxy, maxz]);
69
+ vtx.set(enc, AABB_BYTES);
70
+ const idx = MeshoptEncoder.encodeIndexBuffer(new Uint8Array(m.indices.buffer, m.indices.byteOffset, m.indices.byteLength), m.indices.length, 4);
71
+ return { vtx, idx, vertexCount, indexCount: m.indices.length };
72
+ }
73
+ export async function decodeGeometry(enc) {
74
+ await MeshoptDecoder.ready;
75
+ const aabb = new Float32Array(enc.vtx.slice(0, AABB_BYTES).buffer); // slice → aligned copy
76
+ const [minx, miny, minz, maxx, maxy, maxz] = aabb;
77
+ const qBytes = new Uint8Array(enc.vertexCount * POS_STRIDE);
78
+ MeshoptDecoder.decodeVertexBuffer(qBytes, enc.vertexCount, POS_STRIDE, enc.vtx.subarray(AABB_BYTES));
79
+ const q = new Uint16Array(qBytes.buffer);
80
+ const positions = new Float32Array(enc.vertexCount * 3);
81
+ const dx = (maxx - minx) / 65535, dy = (maxy - miny) / 65535, dz = (maxz - minz) / 65535;
82
+ for (let i = 0; i < enc.vertexCount; i++) {
83
+ positions[i * 3] = minx + q[i * 4] * dx;
84
+ positions[i * 3 + 1] = miny + q[i * 4 + 1] * dy;
85
+ positions[i * 3 + 2] = minz + q[i * 4 + 2] * dz;
86
+ }
87
+ const idxBytes = new Uint8Array(enc.indexCount * 4);
88
+ MeshoptDecoder.decodeIndexBuffer(idxBytes, enc.indexCount, 4, enc.idx);
89
+ return { positions, indices: new Uint32Array(idxBytes.buffer, idxBytes.byteOffset, enc.indexCount) };
90
+ }
91
+ // ── UV (TEXCOORD_0): 16-bit quantized within its 2D bounds, meshopt-encoded, shared across LODs ──
92
+ const UV_STRIDE = 4; // 2 × uint16
93
+ const UVB_BYTES = 16; // 4 × float32: minU, minV, maxU, maxV
94
+ export async function encodeUV(uvs) {
95
+ await MeshoptEncoder.ready;
96
+ const n = uvs.length / 2;
97
+ let minu = Infinity, minv = Infinity, maxu = -Infinity, maxv = -Infinity;
98
+ for (let i = 0; i < n; i++) {
99
+ const u = uvs[i * 2], v = uvs[i * 2 + 1];
100
+ if (u < minu)
101
+ minu = u;
102
+ if (v < minv)
103
+ minv = v;
104
+ if (u > maxu)
105
+ maxu = u;
106
+ if (v > maxv)
107
+ maxv = v;
108
+ }
109
+ const q = new Uint16Array(n * 2);
110
+ for (let i = 0; i < n; i++) {
111
+ q[i * 2] = quant(uvs[i * 2], minu, maxu);
112
+ q[i * 2 + 1] = quant(uvs[i * 2 + 1], minv, maxv);
113
+ }
114
+ const enc = MeshoptEncoder.encodeVertexBuffer(new Uint8Array(q.buffer), n, UV_STRIDE);
115
+ const out = new Uint8Array(UVB_BYTES + enc.length);
116
+ new Float32Array(out.buffer, 0, 4).set([minu, minv, maxu, maxv]);
117
+ out.set(enc, UVB_BYTES);
118
+ return out;
119
+ }
120
+ export async function decodeUV(bytes, vertexCount) {
121
+ await MeshoptDecoder.ready;
122
+ const b = new Float32Array(bytes.slice(0, UVB_BYTES).buffer);
123
+ const [minu, minv, maxu, maxv] = b;
124
+ const qBytes = new Uint8Array(vertexCount * UV_STRIDE);
125
+ MeshoptDecoder.decodeVertexBuffer(qBytes, vertexCount, UV_STRIDE, bytes.subarray(UVB_BYTES));
126
+ const q = new Uint16Array(qBytes.buffer);
127
+ const out = new Float32Array(vertexCount * 2);
128
+ const du = (maxu - minu) / 65535, dv = (maxv - minv) / 65535;
129
+ for (let i = 0; i < vertexCount; i++) {
130
+ out[i * 2] = minu + q[i * 2] * du;
131
+ out[i * 2 + 1] = minv + q[i * 2 + 1] * dv;
132
+ }
133
+ return out;
134
+ }
@@ -0,0 +1,21 @@
1
+ export interface SkinCreature {
2
+ rest: Float32Array;
3
+ normals: Float32Array;
4
+ joints: Uint16Array;
5
+ weights: Float32Array;
6
+ colors: Float32Array;
7
+ indices: Uint32Array;
8
+ palette: Float32Array;
9
+ model: Float32Array;
10
+ }
11
+ export declare class GpuSkinRenderer {
12
+ private device;
13
+ private skinPipe;
14
+ private drawPipe;
15
+ private packed;
16
+ constructor(device: GPUDevice, format: GPUTextureFormat);
17
+ setCreatures(list: SkinCreature[]): void;
18
+ /** Deform + draw every creature with the shared scene lighting. load=true composites over the world;
19
+ * otherwise clears to `clear`. sun/sunCol/amb/cam/fog/fogDensity match the terrain & prop renderers. */
20
+ 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, clear: [number, number, number], load?: boolean): void;
21
+ }
@@ -0,0 +1,148 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // GPU SKIN — a reusable WebGPU renderer for skinned creatures (PLATFORM). A compute pass deforms the
3
+ // rest mesh (AND its normals) by each creature's bone palette; the render pass draws them with the SAME
4
+ // lighting model as the terrain/prop renderers — sun lambert on smooth per-vertex normals + ambient +
5
+ // distance fog — so creatures sit in the scene consistently instead of looking flat/mismatched.
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+ const SKIN = /* wgsl */ `
8
+ @group(0) @binding(0) var<storage,read> rest: array<vec4f>;
9
+ @group(0) @binding(1) var<storage,read> joints: array<vec4u>;
10
+ @group(0) @binding(2) var<storage,read> weights: array<vec4f>;
11
+ @group(0) @binding(3) var<storage,read> palette: array<mat4x4f>;
12
+ @group(0) @binding(4) var<storage,read_write> deformed: array<vec4f>;
13
+ @group(0) @binding(5) var<uniform> P: vec4u;
14
+ @group(0) @binding(6) var<storage,read> restN: array<vec4f>;
15
+ @group(0) @binding(7) var<storage,read_write> defN: array<vec4f>;
16
+ fn rot(m: mat4x4f, v: vec3f) -> vec3f { return mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * v; }
17
+ @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) g: vec3u){
18
+ let i=g.x; if(i>=P.x){return;} let p=vec4f(rest[i].xyz,1.0); let j=joints[i]; let w=weights[i]; let nn=restN[i].xyz;
19
+ var a=vec4f(0.0); a+=w.x*(palette[j.x]*p); a+=w.y*(palette[j.y]*p); a+=w.z*(palette[j.z]*p); a+=w.w*(palette[j.w]*p);
20
+ var b=vec3f(0.0); b+=w.x*rot(palette[j.x],nn); b+=w.y*rot(palette[j.y],nn); b+=w.z*rot(palette[j.z],nn); b+=w.w*rot(palette[j.w],nn);
21
+ deformed[i]=vec4f(a.xyz,1.0); defN[i]=vec4f(normalize(b),0.0);
22
+ }`;
23
+ const DRAW = /* wgsl */ `
24
+ struct U { mvp: mat4x4<f32>, sun: vec4<f32>, sunCol: vec4<f32>, amb: vec4<f32>, cam: vec4<f32>, fog: vec4<f32> };
25
+ @group(0) @binding(0) var<uniform> u: U;
26
+ struct VOut { @builtin(position) pos: vec4<f32>, @location(0) world: vec3<f32>, @location(1) col: vec3<f32>, @location(2) n: vec3<f32> };
27
+ @vertex fn vs(@location(0) p: vec3<f32>, @location(1) c: vec3<f32>, @location(2) nrm: vec3<f32>) -> VOut { var o: VOut; o.pos=u.mvp*vec4(p,1.0); o.world=p; o.col=c; o.n=nrm; return o; }
28
+ @fragment fn fs(i: VOut) -> @location(0) vec4<f32> {
29
+ let d = clamp(dot(normalize(i.n), -normalize(u.sun.xyz)), 0.0, 1.0);
30
+ let lit = i.col * (u.amb.xyz + d * u.sunCol.xyz);
31
+ let dist = length(i.world - u.cam.xyz);
32
+ let f = clamp(1.0 - exp(-dist * u.fog.w), 0.0, 1.0);
33
+ return vec4(mix(lit, u.fog.xyz, f), 1.0);
34
+ }`;
35
+ export class GpuSkinRenderer {
36
+ device;
37
+ skinPipe;
38
+ drawPipe;
39
+ packed = [];
40
+ constructor(device, format) {
41
+ this.device = device;
42
+ const sm = device.createShaderModule({ code: SKIN });
43
+ this.skinPipe = device.createComputePipeline({ layout: 'auto', compute: { module: sm, entryPoint: 'main' } });
44
+ const dm = device.createShaderModule({ code: DRAW });
45
+ this.drawPipe = device.createRenderPipeline({
46
+ layout: 'auto',
47
+ vertex: { module: dm, entryPoint: 'vs', buffers: [
48
+ { arrayStride: 16, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] },
49
+ { arrayStride: 12, attributes: [{ shaderLocation: 1, offset: 0, format: 'float32x3' }] },
50
+ { arrayStride: 16, attributes: [{ shaderLocation: 2, offset: 0, format: 'float32x3' }] }
51
+ ] },
52
+ fragment: { module: dm, entryPoint: 'fs', targets: [{ format }] },
53
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
54
+ depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' },
55
+ });
56
+ }
57
+ setCreatures(list) {
58
+ const dev = this.device, U = GPUBufferUsage;
59
+ for (const p of this.packed) {
60
+ p.defB.destroy();
61
+ p.defNB.destroy();
62
+ p.colB.destroy();
63
+ p.idxB.destroy();
64
+ p.uBuf.destroy();
65
+ p.palB.destroy();
66
+ }
67
+ this.packed = list.map((c) => {
68
+ const n = c.rest.length / 3;
69
+ const rest = new Float32Array(n * 4), restN = new Float32Array(n * 4), joints = new Uint32Array(n * 4), weights = new Float32Array(n * 4);
70
+ for (let i = 0; i < n; i++) {
71
+ rest[i * 4] = c.rest[i * 3];
72
+ rest[i * 4 + 1] = c.rest[i * 3 + 1];
73
+ rest[i * 4 + 2] = c.rest[i * 3 + 2];
74
+ rest[i * 4 + 3] = 1;
75
+ restN[i * 4] = c.normals[i * 3];
76
+ restN[i * 4 + 1] = c.normals[i * 3 + 1];
77
+ restN[i * 4 + 2] = c.normals[i * 3 + 2];
78
+ }
79
+ for (let i = 0; i < n * 4; i++) {
80
+ joints[i] = c.joints[i];
81
+ weights[i] = c.weights[i];
82
+ }
83
+ const mk = (d, u) => { const b = dev.createBuffer({ size: d.byteLength, usage: u }); dev.queue.writeBuffer(b, 0, d); return b; };
84
+ const restB = mk(rest, U.STORAGE | U.COPY_DST), restNB = mk(restN, U.STORAGE | U.COPY_DST), jointsB = mk(joints, U.STORAGE | U.COPY_DST), weightsB = mk(weights, U.STORAGE | U.COPY_DST);
85
+ const colB = mk(c.colors, U.VERTEX | U.COPY_DST), idxB = mk(c.indices, U.INDEX | U.COPY_DST);
86
+ const palB = dev.createBuffer({ size: c.palette.length * 4, usage: U.STORAGE | U.COPY_DST });
87
+ dev.queue.writeBuffer(palB, 0, c.palette);
88
+ const defB = dev.createBuffer({ size: n * 16, usage: U.STORAGE | U.VERTEX }), defNB = dev.createBuffer({ size: n * 16, usage: U.STORAGE | U.VERTEX });
89
+ const pB = dev.createBuffer({ size: 16, usage: U.UNIFORM | U.COPY_DST });
90
+ dev.queue.writeBuffer(pB, 0, new Uint32Array([n, 0, 0, 0]));
91
+ const skinBG = dev.createBindGroup({ layout: this.skinPipe.getBindGroupLayout(0), entries: [
92
+ { binding: 0, resource: { buffer: restB } }, { binding: 1, resource: { buffer: jointsB } }, { binding: 2, resource: { buffer: weightsB } },
93
+ { binding: 3, resource: { buffer: palB } }, { binding: 4, resource: { buffer: defB } }, { binding: 5, resource: { buffer: pB } },
94
+ { binding: 6, resource: { buffer: restNB } }, { binding: 7, resource: { buffer: defNB } }
95
+ ] });
96
+ const uBuf = dev.createBuffer({ size: 96 + 16 * 5, usage: U.UNIFORM | U.COPY_DST });
97
+ const uBG = dev.createBindGroup({ layout: this.drawPipe.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: uBuf } }] });
98
+ return { defB, defNB, colB, idxB, idxCount: c.indices.length, skinBG, uBuf, uBG, palB, n, model: c.model };
99
+ });
100
+ }
101
+ /** Deform + draw every creature with the shared scene lighting. load=true composites over the world;
102
+ * otherwise clears to `clear`. sun/sunCol/amb/cam/fog/fogDensity match the terrain & prop renderers. */
103
+ frame(enc, colorView, depthView, camVP, sunDir, sunCol, amb, camPos, fog, fogDensity, clear, load = false) {
104
+ const cp = enc.beginComputePass();
105
+ cp.setPipeline(this.skinPipe);
106
+ for (const p of this.packed) {
107
+ cp.setBindGroup(0, p.skinBG);
108
+ cp.dispatchWorkgroups(Math.ceil(p.n / 64));
109
+ }
110
+ cp.end();
111
+ for (const p of this.packed) {
112
+ const u = new Float32Array(24 + 20);
113
+ u.set(mul16(camVP, p.model), 0);
114
+ u.set([sunDir[0], sunDir[1], sunDir[2], 0], 16);
115
+ u.set([sunCol[0], sunCol[1], sunCol[2], 0], 20);
116
+ u.set([amb[0], amb[1], amb[2], 0], 24);
117
+ u.set([camPos[0], camPos[1], camPos[2], 0], 28);
118
+ u.set([fog[0], fog[1], fog[2], fogDensity], 32);
119
+ this.device.queue.writeBuffer(p.uBuf, 0, u);
120
+ }
121
+ const rp = enc.beginRenderPass({
122
+ colorAttachments: [{ view: colorView, ...(load ? { loadOp: 'load' } : { clearValue: { r: clear[0], g: clear[1], b: clear[2], a: 1 }, loadOp: 'clear' }), storeOp: 'store' }],
123
+ depthStencilAttachment: { view: depthView, ...(load ? { depthLoadOp: 'load' } : { depthClearValue: 1, depthLoadOp: 'clear' }), depthStoreOp: 'store' },
124
+ });
125
+ rp.setPipeline(this.drawPipe);
126
+ for (const p of this.packed) {
127
+ rp.setBindGroup(0, p.uBG);
128
+ rp.setVertexBuffer(0, p.defB);
129
+ rp.setVertexBuffer(1, p.colB);
130
+ rp.setVertexBuffer(2, p.defNB);
131
+ rp.setIndexBuffer(p.idxB, 'uint32');
132
+ rp.drawIndexed(p.idxCount);
133
+ }
134
+ rp.end();
135
+ }
136
+ }
137
+ /** column-major 4×4 multiply (a·b). */
138
+ function mul16(a, b) {
139
+ const o = new Float32Array(16);
140
+ for (let c = 0; c < 4; c++)
141
+ for (let r = 0; r < 4; r++) {
142
+ let s = 0;
143
+ for (let k = 0; k < 4; k++)
144
+ s += a[k * 4 + r] * b[c * 4 + k];
145
+ o[c * 4 + r] = s;
146
+ }
147
+ return o;
148
+ }
@@ -0,0 +1,2 @@
1
+ import type { RawMesh } from './geometry';
2
+ export declare function buildLodLadder(m: RawMesh, ratios: number[]): Promise<RawMesh[]>;
@@ -0,0 +1,18 @@
1
+ // Discrete LOD ladder (design/asset-streaming.md §3 v1) via meshopt simplify.
2
+ // LOD0 is the source; each later LOD targets a fraction of the index count. The test
3
+ // oracle is the triangle count (strictly decreasing) + preserved bounds.
4
+ import { MeshoptSimplifier } from 'meshoptimizer';
5
+ export async function buildLodLadder(m, ratios) {
6
+ await MeshoptSimplifier.ready;
7
+ const out = [];
8
+ for (const r of ratios) {
9
+ if (r >= 1) {
10
+ out.push({ positions: m.positions, indices: m.indices });
11
+ continue;
12
+ }
13
+ const targetCount = Math.max(3, Math.floor((m.indices.length * r) / 3) * 3);
14
+ const [simplified] = MeshoptSimplifier.simplify(m.indices, m.positions, 3, targetCount, 0.05, ['LockBorder']);
15
+ out.push({ positions: m.positions, indices: Uint32Array.from(simplified) });
16
+ }
17
+ return out;
18
+ }
@@ -0,0 +1,23 @@
1
+ import type { RawMesh } from './geometry';
2
+ import type { Plane } from '../mat4';
3
+ export interface MeshletBound {
4
+ center: [number, number, number];
5
+ radius: number;
6
+ coneApex: [number, number, number];
7
+ coneAxis: [number, number, number];
8
+ coneCutoff: number;
9
+ }
10
+ export interface MeshletSet {
11
+ count: number;
12
+ bounds: MeshletBound[];
13
+ meshlets: Uint32Array;
14
+ vertices: Uint32Array;
15
+ triangles: Uint8Array;
16
+ }
17
+ export declare function buildMeshletSet(mesh: RawMesh, maxV?: number, maxT?: number): Promise<MeshletSet>;
18
+ /** Total triangles across all clusters (each meshlet's triangleCount is meshlets[i*4+3]). */
19
+ export declare function meshletTriangleCount(set: MeshletSet): number;
20
+ /** CPU cull oracle: a cluster is visible when its bounding sphere is inside the frustum AND its
21
+ * normal cone does not face entirely away from the camera. meshopt convention: the cluster is
22
+ * backfacing (cull) when dot(normalize(coneApex - camera), coneAxis) >= coneCutoff. */
23
+ export declare function cullMeshlets(set: MeshletSet, planes: Plane[], camera: readonly [number, number, number]): number[];
@@ -0,0 +1,53 @@
1
+ // Meshlets (design/asset-streaming.md §3 v2 — the cluster-DAG foundation). A mesh is partitioned
2
+ // into ~128-triangle clusters, each with a bounding sphere + a backface normal cone, so the GPU can
3
+ // cull at cluster granularity (a partly-offscreen or backfacing object only submits its visible
4
+ // clusters) — the precursor to per-cluster continuous LOD. buildMeshletSet wraps meshoptimizer's
5
+ // clusterizer; cullMeshlets is the CPU reference oracle the GPU cull path is validated against
6
+ // (same discipline as renderIR ↔ gpuDriven). This slice is cluster cull only; the LOD DAG (grouping
7
+ // + simplifying clusters into a monotonic-error hierarchy) is the next slice.
8
+ import { MeshoptClusterizer } from 'meshoptimizer';
9
+ const MAX_VERTS = 64, MAX_TRIS = 124, CONE_WEIGHT = 0.5;
10
+ export async function buildMeshletSet(mesh, maxV = MAX_VERTS, maxT = MAX_TRIS) {
11
+ await MeshoptClusterizer.ready;
12
+ const r = MeshoptClusterizer.buildMeshlets(mesh.indices, mesh.positions, 3, maxV, maxT, CONE_WEIGHT);
13
+ const raw = MeshoptClusterizer.computeMeshletBounds(r, mesh.positions, 3);
14
+ const bounds = raw.slice(0, r.meshletCount).map(b => ({
15
+ center: [b.centerX, b.centerY, b.centerZ], radius: b.radius,
16
+ coneApex: [b.coneApexX, b.coneApexY, b.coneApexZ], coneAxis: [b.coneAxisX, b.coneAxisY, b.coneAxisZ], coneCutoff: b.coneCutoff,
17
+ }));
18
+ return { count: r.meshletCount, bounds, meshlets: r.meshlets, vertices: r.vertices, triangles: r.triangles };
19
+ }
20
+ /** Total triangles across all clusters (each meshlet's triangleCount is meshlets[i*4+3]). */
21
+ export function meshletTriangleCount(set) {
22
+ let t = 0;
23
+ for (let i = 0; i < set.count; i++)
24
+ t += set.meshlets[i * 4 + 3];
25
+ return t;
26
+ }
27
+ /** CPU cull oracle: a cluster is visible when its bounding sphere is inside the frustum AND its
28
+ * normal cone does not face entirely away from the camera. meshopt convention: the cluster is
29
+ * backfacing (cull) when dot(normalize(coneApex - camera), coneAxis) >= coneCutoff. */
30
+ export function cullMeshlets(set, planes, camera) {
31
+ const vis = [];
32
+ for (let i = 0; i < set.count; i++) {
33
+ const b = set.bounds[i];
34
+ let inFrustum = true;
35
+ for (const pl of planes) {
36
+ if (pl[0] * b.center[0] + pl[1] * b.center[1] + pl[2] * b.center[2] + pl[3] < -b.radius) {
37
+ inFrustum = false;
38
+ break;
39
+ }
40
+ }
41
+ if (!inFrustum)
42
+ continue;
43
+ let dx = b.coneApex[0] - camera[0], dy = b.coneApex[1] - camera[1], dz = b.coneApex[2] - camera[2];
44
+ const l = Math.hypot(dx, dy, dz) || 1;
45
+ dx /= l;
46
+ dy /= l;
47
+ dz /= l;
48
+ if (dx * b.coneAxis[0] + dy * b.coneAxis[1] + dz * b.coneAxis[2] >= b.coneCutoff)
49
+ continue; // backfacing cone
50
+ vis.push(i);
51
+ }
52
+ return vis;
53
+ }
@@ -0,0 +1,5 @@
1
+ export declare function packMorph(name: string, deltas: Float32Array): Uint8Array;
2
+ export declare function unpackMorph(b: Uint8Array): {
3
+ name: string;
4
+ deltas: Float32Array;
5
+ };
@@ -0,0 +1,75 @@
1
+ // Morph-target blendshape (MORPH chunk, refinement tier — design/asset-streaming.md §5.2).
2
+ // One chunk per shape. A blendshape only displaces a SUBSET of vertices (a viseme moves the mouth;
3
+ // the rest are exactly 0), and the displacements are small, so the chunk is stored SPARSE +
4
+ // QUANTIZED: only moved vertices, each as u32 index + 3×u16 position-delta quantized within the
5
+ // shape's per-component delta bounds. unpack reconstructs the DENSE Float32Array (length 3*vc, zeros
6
+ // elsewhere) so consumers (the WGSL morph deform + its oracle) are unchanged. 16-bit over the delta
7
+ // range is sub-micron. All reads are element-wise DataView, so no alignment constraint.
8
+ // Layout LE: u16 nameLen, name utf8, u32 vertexCount, u32 movedCount, f32 bounds[6]
9
+ // (minX,maxX,minY,maxY,minZ,maxZ), then movedCount × { u32 index, u16 qx, u16 qy, u16 qz }.
10
+ const quant = (v, lo, hi) => hi > lo ? Math.max(0, Math.min(65535, Math.round((v - lo) / (hi - lo) * 65535))) : 0;
11
+ export function packMorph(name, deltas) {
12
+ const nm = new TextEncoder().encode(name);
13
+ const vc = deltas.length / 3;
14
+ const moved = [];
15
+ let minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity, minz = Infinity, maxz = -Infinity;
16
+ for (let i = 0; i < vc; i++) {
17
+ const x = deltas[i * 3], y = deltas[i * 3 + 1], z = deltas[i * 3 + 2];
18
+ if (x !== 0 || y !== 0 || z !== 0) {
19
+ moved.push(i);
20
+ if (x < minx)
21
+ minx = x;
22
+ if (x > maxx)
23
+ maxx = x;
24
+ if (y < miny)
25
+ miny = y;
26
+ if (y > maxy)
27
+ maxy = y;
28
+ if (z < minz)
29
+ minz = z;
30
+ if (z > maxz)
31
+ maxz = z;
32
+ }
33
+ }
34
+ if (moved.length === 0) {
35
+ minx = maxx = miny = maxy = minz = maxz = 0;
36
+ }
37
+ const mc = moved.length, p0 = 2 + nm.length;
38
+ const out = new Uint8Array(p0 + 8 + 24 + mc * 10);
39
+ const dv = new DataView(out.buffer);
40
+ dv.setUint16(0, nm.length, true);
41
+ out.set(nm, 2);
42
+ dv.setUint32(p0, vc, true);
43
+ dv.setUint32(p0 + 4, mc, true);
44
+ const bnd = [minx, maxx, miny, maxy, minz, maxz];
45
+ for (let i = 0; i < 6; i++)
46
+ dv.setFloat32(p0 + 8 + i * 4, bnd[i], true);
47
+ let o = p0 + 32;
48
+ for (const i of moved) {
49
+ dv.setUint32(o, i, true);
50
+ dv.setUint16(o + 4, quant(deltas[i * 3], minx, maxx), true);
51
+ dv.setUint16(o + 6, quant(deltas[i * 3 + 1], miny, maxy), true);
52
+ dv.setUint16(o + 8, quant(deltas[i * 3 + 2], minz, maxz), true);
53
+ o += 10;
54
+ }
55
+ return out;
56
+ }
57
+ export function unpackMorph(b) {
58
+ const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
59
+ const nameLen = dv.getUint16(0, true);
60
+ const name = new TextDecoder().decode(b.subarray(2, 2 + nameLen));
61
+ const p0 = 2 + nameLen;
62
+ const vc = dv.getUint32(p0, true), mc = dv.getUint32(p0 + 4, true);
63
+ const minx = dv.getFloat32(p0 + 8, true), maxx = dv.getFloat32(p0 + 12, true), miny = dv.getFloat32(p0 + 16, true), maxy = dv.getFloat32(p0 + 20, true), minz = dv.getFloat32(p0 + 24, true), maxz = dv.getFloat32(p0 + 28, true);
64
+ const dx = (maxx - minx) / 65535, dy = (maxy - miny) / 65535, dz = (maxz - minz) / 65535;
65
+ const deltas = new Float32Array(vc * 3);
66
+ let o = p0 + 32;
67
+ for (let r = 0; r < mc; r++) {
68
+ const i = dv.getUint32(o, true);
69
+ deltas[i * 3] = minx + dv.getUint16(o + 4, true) * dx;
70
+ deltas[i * 3 + 1] = miny + dv.getUint16(o + 6, true) * dy;
71
+ deltas[i * 3 + 2] = minz + dv.getUint16(o + 8, true) * dz;
72
+ o += 10;
73
+ }
74
+ return { name, deltas };
75
+ }
@@ -0,0 +1,28 @@
1
+ import type { JointTRS } from './pose';
2
+ import type { RawMesh } from './geometry';
3
+ /** Authoring tag: which archetype/socket/region a part fits, and the shared joints it skins to. */
4
+ export interface PartTag {
5
+ id: string;
6
+ archetypeId: string;
7
+ socket: string;
8
+ region: string;
9
+ jointRange: [number, number];
10
+ attach: JointTRS;
11
+ hasViseme?: boolean;
12
+ }
13
+ /** A cooked part: a positions-only mesh + 4-per-vertex skin (joints index the SHARED skeleton). */
14
+ export interface CookedPart {
15
+ tag: PartTag;
16
+ mesh: RawMesh;
17
+ joints: Uint16Array;
18
+ weights: Float32Array;
19
+ }
20
+ export interface PartKit {
21
+ version: number;
22
+ archetypeId: string;
23
+ parts: CookedPart[];
24
+ }
25
+ /** Every part that can fill a socket (matches socket id + one of its allowed regions). */
26
+ export declare function partsForSocket(kit: PartKit, socketId: string, allowedRegions: string[]): CookedPart[];
27
+ /** Resolve a genome's socket→partId choices to socket→CookedPart. */
28
+ export declare function resolveKit(kit: PartKit, parts: Record<string, string>): Record<string, CookedPart>;
@@ -0,0 +1,21 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // PART KIT — the modular library of body PARTS a genome draws from (PLATFORM, generic). Each part is a
3
+ // small skinned mesh authored to plug into ONE socket of ONE archetype: its per-vertex joints already
4
+ // index the shared skeleton (jointRange ⊆ that archetype), so the assembler just welds + concatenates —
5
+ // no runtime re-binding. A finite kit × continuous deformation × palette = infinite variety. The kit
6
+ // itself is showcase content; this module only defines the format + selection helpers.
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ /** Every part that can fill a socket (matches socket id + one of its allowed regions). */
9
+ export function partsForSocket(kit, socketId, allowedRegions) {
10
+ return kit.parts.filter((p) => p.tag.socket === socketId && allowedRegions.includes(p.tag.region));
11
+ }
12
+ /** Resolve a genome's socket→partId choices to socket→CookedPart. */
13
+ export function resolveKit(kit, parts) {
14
+ const out = {};
15
+ for (const [socket, partId] of Object.entries(parts)) {
16
+ const p = kit.parts.find((q) => q.tag.id === partId);
17
+ if (p)
18
+ out[socket] = p;
19
+ }
20
+ return out;
21
+ }
@@ -0,0 +1,26 @@
1
+ import { type Mat4 } from '../mat4';
2
+ import type { AnimClip } from './anim';
3
+ export interface JointTRS {
4
+ t: [number, number, number];
5
+ q: [number, number, number, number];
6
+ s: [number, number, number];
7
+ }
8
+ /** Column-major T·R·S from a translation, quaternion (xyzw), and scale. */
9
+ export declare function composeTRS(t: [number, number, number], q: [number, number, number, number], s: [number, number, number]): Mat4;
10
+ /** Sample the clip at `time`, overriding each animated joint's rest T/R/S, → per-joint local mats. */
11
+ export declare function sampleClipToLocals(clip: AnimClip, time: number, rest: JointTRS[]): Mat4[];
12
+ /** world[j] = world[parent] × local[j]; root joints (parent<0) are seeded with `rootSeed`
13
+ * (the armature/skeleton-root world matrix) so the hierarchy resolves in scene space.
14
+ * Recursive with memo (parent-order agnostic). */
15
+ export declare function forwardKinematics(locals: Mat4[], parents: Int16Array, rootSeed?: Mat4): Mat4[];
16
+ /** Affine inverse of a column-major TRS matrix (assumes last row [0,0,0,1]). */
17
+ export declare function invert(m: Mat4): Mat4;
18
+ /** Decompose a column-major TRS matrix into translation / rotation-quaternion(xyzw) / scale.
19
+ * Assumes no shear (rigid + non-negative axis scale) — true for bind matrices. */
20
+ export declare function decomposeTRS(m: Mat4): JointTRS;
21
+ /** Derive each joint's REST local TRS from the inverseBind matrices, so FK over the result reproduces
22
+ * the BIND pose exactly (FK·inverseBind == identity at rest). Use this instead of a glTF's joint node
23
+ * TRS, which for some rigs is a non-bind pose — the cause of skinning displacement at rest. */
24
+ export declare function bindLocalsFromInverseBind(parents: Int16Array, inverseBind: Float32Array): JointTRS[];
25
+ /** palette[j] = world[j] × inverseBind[j], packed into a Float32Array (J×16, column-major) for GPU upload. */
26
+ export declare function buildPalette(worlds: Mat4[], inverseBind: Float32Array): Float32Array;