@instruktlabs/kiln 1.2.2 → 1.2.3
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +74 -0
- package/README.md +7 -0
- package/dist/agent-run.mjs +6392 -1477
- package/dist/build.json +23 -23
- package/dist/cli.mjs +8453 -3407
- package/dist/evaluator-worker.mjs +16608 -12387
- package/dist/mcp-engine.mjs +6746 -1725
- package/dist/mcp-server.mjs +4 -4
- package/dist/viewer/app.js +203 -199
- package/dist/viewer/chat.html +209 -205
- package/dist/viewer/style.css +10 -0
- package/docs/atlas-provenance.md +23 -0
- package/docs/community-exporter.md +27 -0
- package/docs/export-profiles.md +20 -0
- package/docs/material-library.md +9 -5
- package/docs/procedural-surfaces-v3.md +276 -0
- package/docs/projects-and-live-review.md +1 -1
- package/docs/rendering.md +41 -0
- package/docs/sdk.md +53 -0
- package/docs/tools-operations.md +2 -2
- package/docs/tools.md +3 -3
- package/lib/asset-materials-node.d.ts +3 -3
- package/lib/asset-materials-node.js +29 -33
- package/lib/assets-node.d.ts +1 -1
- package/lib/assets-node.js +11 -4
- package/lib/assets.d.ts +4 -2
- package/lib/assets.js +61 -16
- package/lib/atlas-provenance.d.ts +86 -0
- package/lib/atlas-provenance.js +448 -0
- package/lib/character.d.ts +11 -2
- package/lib/character.js +90 -2
- package/lib/community-exporter.d.ts +3 -2
- package/lib/community-exporter.js +139 -3
- package/lib/discovery/helper-contracts.js +161 -7
- package/lib/discovery/helper-specs.js +49 -4
- package/lib/discovery/shapes.js +20 -7
- package/lib/engine-identity.d.ts +1 -1
- package/lib/engine-identity.js +1 -1
- package/lib/evaluator/authoring-diagnostic.d.ts +1 -1
- package/lib/evaluator/authoring-diagnostic.js +12 -0
- package/lib/evaluator/protocol.d.ts +2 -2
- package/lib/evaluator/protocol.js +16 -6
- package/lib/evaluator/subprocess.js +2 -2
- package/lib/exporter-node.js +9 -1
- package/lib/geometry-budget.d.ts +7 -0
- package/lib/geometry-budget.js +8 -0
- package/lib/gltf-texture-binding.d.ts +6 -0
- package/lib/gltf-texture-binding.js +54 -0
- package/lib/material-authoring-node.d.ts +6 -0
- package/lib/material-authoring-node.js +49 -0
- package/lib/material-authoring-schema.d.ts +871 -0
- package/lib/material-authoring-schema.js +310 -0
- package/lib/material-export.d.ts +5 -0
- package/lib/material-export.js +21 -0
- package/lib/material-library-node.d.ts +34 -8
- package/lib/material-library-node.js +691 -11
- package/lib/material-library.d.ts +3704 -4
- package/lib/material-library.js +272 -0
- package/lib/mcp-core.d.ts +1 -1
- package/lib/pinned-material-map.d.ts +33 -0
- package/lib/pinned-material-map.js +1 -0
- package/lib/portable-material-runtime.d.ts +5 -0
- package/lib/portable-material-runtime.js +160 -0
- package/lib/portable-sampler-runtime.d.ts +4 -0
- package/lib/portable-sampler-runtime.js +26 -0
- package/lib/portable-sampler.d.ts +11 -0
- package/lib/portable-sampler.js +42 -0
- package/lib/primitives.d.ts +15 -3
- package/lib/primitives.js +24 -4
- package/lib/procedural-material-v2.d.ts +2 -0
- package/lib/procedural-material-v2.js +2 -1
- package/lib/procedural-material-v3.d.ts +72 -0
- package/lib/procedural-material-v3.js +162 -0
- package/lib/procedural-texture-v2-compiler.d.ts +31 -0
- package/lib/procedural-texture-v2-compiler.js +172 -0
- package/lib/procedural-texture-v3-contract.d.ts +125 -0
- package/lib/procedural-texture-v3-contract.js +333 -0
- package/lib/procedural-texture-v3.d.ts +12 -0
- package/lib/procedural-texture-v3.js +165 -0
- package/lib/procedural-texture.d.ts +9 -31
- package/lib/procedural-texture.js +43 -170
- package/lib/project-bundle-node.d.ts +1 -1
- package/lib/project-bundle-node.js +70 -13
- package/lib/project-bundle.d.ts +91 -3
- package/lib/project-bundle.js +48 -7
- package/lib/qa/atlas.d.ts +21 -0
- package/lib/qa/atlas.js +474 -0
- package/lib/qa/index.d.ts +1 -0
- package/lib/qa/index.js +1 -0
- package/lib/qa/material.js +2 -0
- package/lib/qa/skin-integrity.d.ts +18 -0
- package/lib/qa/skin-integrity.js +221 -0
- package/lib/render-service-host.js +4 -7
- package/lib/render.d.ts +3 -3
- package/lib/render.js +108 -16
- package/lib/skin-binding.d.ts +20 -0
- package/lib/skin-binding.js +144 -0
- package/lib/texture-atlas-contract.d.ts +97 -0
- package/lib/texture-atlas-contract.js +414 -0
- package/lib/texture-atlas-pixels.d.ts +73 -0
- package/lib/texture-atlas-pixels.js +290 -0
- package/lib/texture-atlas.d.ts +10 -0
- package/lib/texture-atlas.js +143 -0
- package/lib/texture-bake.d.ts +3 -0
- package/lib/texture-bake.js +66 -11
- package/lib/texture-resolver.d.ts +22 -0
- package/lib/texture-resolver.js +24 -0
- package/lib/tools/materials.js +8 -7
- package/lib/tools/registry.js +5 -16
- package/lib/tools/review.js +1 -1
- package/lib/uv-data.d.ts +32 -0
- package/lib/uv-data.js +172 -0
- package/lib/uv-shapes.js +3 -0
- package/lib/uv.d.ts +9 -3
- package/lib/uv.js +53 -82
- package/lib/views/camera.d.ts +3 -3
- package/lib/views/camera.js +32 -28
- package/lib/views/face-policy.d.ts +9 -0
- package/lib/views/face-policy.js +16 -0
- package/lib/views/glb.js +218 -24
- package/lib/views/pose.d.ts +7 -1
- package/lib/views/pose.js +63 -12
- package/lib/views/raster.d.ts +6 -2
- package/lib/views/raster.js +49 -37
- package/lib/views/scene-copy.d.ts +6 -0
- package/lib/views/scene-copy.js +73 -0
- package/lib/views/scene-raster.js +2 -1
- package/lib/views/triangle-coverage.d.ts +8 -0
- package/lib/views/triangle-coverage.js +58 -0
- package/lib/workspace-node.js +7 -3
- package/lib/workspace.d.ts +2 -2
- package/package.json +3 -1
- package/plugin.json +1 -1
- package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
- package/plugins/kiln-engine/README.md +3 -3
- package/plugins/kiln-engine/package-provenance.json +6 -6
- package/plugins/kiln-engine/plugin.json +1 -1
- package/plugins/kiln-engine/runtime.json +1 -1
- package/skills/kiln-author-asset/references/geometry-recipes.md +124 -0
- package/skills/kiln-author-asset/references/projects-and-materials.md +32 -2
- package/skills/kiln-compose-scene/references/runtime-scenes.md +19 -0
- package/skills/kiln-qa-asset/references/integration-checks.md +37 -2
- package/src/asset-materials-node.ts +38 -45
- package/src/assets-node.ts +22 -5
- package/src/assets.ts +61 -20
- package/src/atlas-provenance.ts +522 -0
- package/src/character.ts +107 -2
- package/src/community-exporter.ts +143 -3
- package/src/discovery/helper-contracts.ts +188 -7
- package/src/discovery/helper-specs.ts +69 -4
- package/src/discovery/shapes.ts +25 -9
- package/src/engine-identity.ts +1 -1
- package/src/evaluator/authoring-diagnostic.ts +19 -1
- package/src/evaluator/protocol.ts +19 -9
- package/src/evaluator/subprocess.ts +2 -2
- package/src/exporter-node.ts +12 -1
- package/src/generated/mcp-manifest.json +3 -3
- package/src/geometry-budget.ts +8 -0
- package/src/gltf-texture-binding.ts +60 -0
- package/src/material-authoring-node.ts +68 -0
- package/src/material-authoring-schema.ts +343 -0
- package/src/material-export.ts +25 -0
- package/src/material-library-node.ts +840 -16
- package/src/material-library.ts +348 -5
- package/src/pinned-material-map.ts +43 -0
- package/src/portable-material-runtime.ts +200 -0
- package/src/portable-sampler-runtime.ts +40 -0
- package/src/portable-sampler.ts +54 -0
- package/src/primitives.ts +98 -4
- package/src/procedural-material-v2.ts +2 -1
- package/src/procedural-material-v3.ts +224 -0
- package/src/procedural-texture-v2-compiler.ts +234 -0
- package/src/procedural-texture-v3-contract.ts +437 -0
- package/src/procedural-texture-v3.ts +220 -0
- package/src/procedural-texture.ts +61 -229
- package/src/project-bundle-node.ts +106 -14
- package/src/project-bundle.ts +61 -18
- package/src/project-cli.ts +29 -17
- package/src/qa/atlas.ts +597 -0
- package/src/qa/index.ts +1 -0
- package/src/qa/material.ts +2 -0
- package/src/qa/skin-integrity.ts +291 -0
- package/src/render-service-host.ts +4 -6
- package/src/render.ts +133 -15
- package/src/skin-binding.ts +225 -0
- package/src/texture-atlas-contract.ts +558 -0
- package/src/texture-atlas-pixels.ts +456 -0
- package/src/texture-atlas.ts +185 -0
- package/src/texture-bake.ts +88 -10
- package/src/texture-resolver.ts +55 -0
- package/src/tools/materials.ts +15 -11
- package/src/tools/registry.ts +5 -15
- package/src/tools/review.ts +1 -1
- package/src/uv-data.ts +238 -0
- package/src/uv-shapes.ts +3 -0
- package/src/uv.ts +73 -128
- package/src/viewer/app.ts +37 -0
- package/src/viewer/live-panel.ts +57 -1
- package/src/viewer/material-preview-panel.ts +140 -0
- package/src/viewer/material-preview.ts +138 -0
- package/src/viewer/materials-panel.ts +34 -5
- package/src/viewer/navigation.ts +2 -0
- package/src/views/camera.ts +37 -40
- package/src/views/face-policy.ts +20 -0
- package/src/views/glb.ts +294 -17
- package/src/views/pose.ts +71 -13
- package/src/views/raster.ts +73 -46
- package/src/views/scene-copy.ts +86 -0
- package/src/views/scene-raster.ts +2 -1
- package/src/views/triangle-coverage.ts +78 -0
- package/src/workspace-node.ts +11 -3
- package/src/workspace.ts +2 -2
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
const MAX_FINDINGS = 64;
|
|
3
|
+
const WEIGHT_TOLERANCE = 1e-5;
|
|
4
|
+
function validAttribute(a, count, size) {
|
|
5
|
+
if (!a || a.itemSize !== size || a.count !== count || !Number.isSafeInteger(count) || count < 1)
|
|
6
|
+
return false;
|
|
7
|
+
if ('isInterleavedBufferAttribute' in a) {
|
|
8
|
+
return (Number.isSafeInteger(a.data.stride) &&
|
|
9
|
+
a.data.stride >= size &&
|
|
10
|
+
Number.isSafeInteger(a.offset) &&
|
|
11
|
+
a.offset >= 0 &&
|
|
12
|
+
a.offset + size <= a.data.stride &&
|
|
13
|
+
(count - 1) * a.data.stride + a.offset + size <= a.array.length);
|
|
14
|
+
}
|
|
15
|
+
return count * size <= a.array.length;
|
|
16
|
+
}
|
|
17
|
+
function finiteAffine(matrix) {
|
|
18
|
+
const e = matrix?.elements;
|
|
19
|
+
return (!!e &&
|
|
20
|
+
e.length === 16 &&
|
|
21
|
+
[...e].every((v) => Number.isFinite(v) && Number.isFinite(Math.fround(v))) &&
|
|
22
|
+
// Inverting valid TRS matrices can introduce double-precision roundoff.
|
|
23
|
+
// Judge the affine row at the Float32 precision used by the exported data.
|
|
24
|
+
Math.fround(e[3]) === 0 &&
|
|
25
|
+
Math.fround(e[7]) === 0 &&
|
|
26
|
+
Math.fround(e[11]) === 0 &&
|
|
27
|
+
Math.fround(e[15]) === 1);
|
|
28
|
+
}
|
|
29
|
+
/** Read logical attributes and transforms without updating or repairing the caller's scene. */
|
|
30
|
+
export function inspectSkinIntegrity(root) {
|
|
31
|
+
const result = {
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
findings: [],
|
|
34
|
+
meshesChecked: 0,
|
|
35
|
+
verticesChecked: 0,
|
|
36
|
+
truncated: false,
|
|
37
|
+
bounds: [],
|
|
38
|
+
};
|
|
39
|
+
const paths = new Map();
|
|
40
|
+
const pending = [[root, '', 0]];
|
|
41
|
+
while (pending.length) {
|
|
42
|
+
const [node, parent, sibling] = pending.pop();
|
|
43
|
+
if (paths.has(node))
|
|
44
|
+
continue;
|
|
45
|
+
const segment = `${(node.name || node.type).slice(0, 80)}[${sibling}]`;
|
|
46
|
+
const path = (parent ? `${parent}/${segment}` : segment).slice(-800);
|
|
47
|
+
paths.set(node, path);
|
|
48
|
+
for (let i = node.children.length - 1; i >= 0; i--)
|
|
49
|
+
pending.push([node.children[i], path, i]);
|
|
50
|
+
}
|
|
51
|
+
const worlds = new Map();
|
|
52
|
+
function world(node) {
|
|
53
|
+
const chain = [], seen = new Set();
|
|
54
|
+
let cursor = node;
|
|
55
|
+
while (cursor && !worlds.has(cursor)) {
|
|
56
|
+
if (seen.has(cursor))
|
|
57
|
+
return undefined;
|
|
58
|
+
seen.add(cursor);
|
|
59
|
+
chain.push(cursor);
|
|
60
|
+
cursor = cursor.parent;
|
|
61
|
+
}
|
|
62
|
+
while (chain.length) {
|
|
63
|
+
const n = chain.pop();
|
|
64
|
+
const local = n.matrixAutoUpdate
|
|
65
|
+
? new THREE.Matrix4().compose(n.position, n.quaternion, n.scale)
|
|
66
|
+
: n.matrix.clone();
|
|
67
|
+
const parent = n.parent ? worlds.get(n.parent) : undefined;
|
|
68
|
+
const value = n.matrixWorldAutoUpdate === false
|
|
69
|
+
? n.matrixWorld.clone()
|
|
70
|
+
: parent
|
|
71
|
+
? new THREE.Matrix4().multiplyMatrices(parent, local)
|
|
72
|
+
: local;
|
|
73
|
+
worlds.set(n, value);
|
|
74
|
+
}
|
|
75
|
+
return worlds.get(node);
|
|
76
|
+
}
|
|
77
|
+
for (const [node, nodePath] of paths) {
|
|
78
|
+
const mesh = node;
|
|
79
|
+
if (!mesh.isSkinnedMesh)
|
|
80
|
+
continue;
|
|
81
|
+
if (result.findings.length >= MAX_FINDINGS) {
|
|
82
|
+
result.truncated = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
result.meshesChecked++;
|
|
86
|
+
const start = result.findings.length;
|
|
87
|
+
const add = (code, message, attribute, measurement) => {
|
|
88
|
+
if (result.findings.length >= MAX_FINDINGS) {
|
|
89
|
+
result.truncated = true;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
result.findings.push({
|
|
93
|
+
code,
|
|
94
|
+
disposition: 'block',
|
|
95
|
+
dimension: 'exportIntegrity',
|
|
96
|
+
profile: 'skin.source-integrity.v1',
|
|
97
|
+
message,
|
|
98
|
+
affected: { node: mesh.name.slice(0, 160), nodePath, ...(attribute ? { attribute } : {}) },
|
|
99
|
+
...(measurement ? { measurement } : {}),
|
|
100
|
+
repairText: 'Rebuild the named binding at its intended rest pose. Keep bone order, inverse bind matrices and four influence lanes aligned with every final vertex; unwrap or change topology before binding. Do not repair invalid indices by normalizing weights.',
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
const skeleton = mesh.skeleton, bones = skeleton?.bones;
|
|
104
|
+
if (!bones?.length || !Array.isArray(bones)) {
|
|
105
|
+
add('SKIN_SKELETON', 'Skinned mesh requires a nonempty skeleton in the exported hierarchy.');
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const unique = new Set();
|
|
109
|
+
for (let joint = 0; joint < bones.length; joint++) {
|
|
110
|
+
const bone = bones[joint];
|
|
111
|
+
if (!bone?.isObject3D || !paths.has(bone) || unique.has(bone)) {
|
|
112
|
+
add('SKIN_JOINT_MEMBERSHIP', `Joint ${joint} (${JSON.stringify(bone?.name?.slice(0, 160) ?? 'missing')}) must be a distinct node inside the exported hierarchy.`, `joints[${joint}]`);
|
|
113
|
+
}
|
|
114
|
+
if (bone)
|
|
115
|
+
unique.add(bone);
|
|
116
|
+
if (!finiteAffine(skeleton.boneInverses?.[joint])) {
|
|
117
|
+
add('SKIN_INVERSE_BIND', `Joint ${joint} (${JSON.stringify(bone?.name?.slice(0, 160) ?? 'missing')}) requires a finite affine inverse bind matrix.`, `boneInverses[${joint}]`);
|
|
118
|
+
}
|
|
119
|
+
if (result.truncated)
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (!finiteAffine(mesh.bindMatrix) ||
|
|
123
|
+
!Number.isFinite(mesh.bindMatrix.determinant()) ||
|
|
124
|
+
mesh.bindMatrix.determinant() === 0 ||
|
|
125
|
+
![THREE.AttachedBindMode, THREE.DetachedBindMode].includes(mesh.bindMode)) {
|
|
126
|
+
add('SKIN_BIND_MATRIX', 'Mesh requires a finite invertible affine bind matrix and an attached or detached bind mode.', 'bindMatrix');
|
|
127
|
+
}
|
|
128
|
+
const position = mesh.geometry?.getAttribute('position'), count = position?.count ?? 0;
|
|
129
|
+
const indices = mesh.geometry?.getAttribute('skinIndex'), weights = mesh.geometry?.getAttribute('skinWeight');
|
|
130
|
+
if (!validAttribute(position, count, 3) ||
|
|
131
|
+
!validAttribute(indices, count, 4) ||
|
|
132
|
+
indices.normalized ||
|
|
133
|
+
!validAttribute(weights, count, 4)) {
|
|
134
|
+
add('SKIN_ATTRIBUTES', 'Skin requires xyz positions and four non-normalized joint indices and four weights per final vertex, backed by complete buffers.', 'skinIndex/skinWeight');
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const invalid = new Map();
|
|
138
|
+
const bad = (code, vertex, attribute) => {
|
|
139
|
+
const row = invalid.get(code);
|
|
140
|
+
if (row) {
|
|
141
|
+
if (row.last !== vertex)
|
|
142
|
+
row.count++;
|
|
143
|
+
row.last = vertex;
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
invalid.set(code, { count: 1, first: vertex, last: vertex, attribute });
|
|
147
|
+
};
|
|
148
|
+
for (let vertex = 0; vertex < count; vertex++) {
|
|
149
|
+
let sum = 0;
|
|
150
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
151
|
+
const index = indices.getComponent(vertex, lane), weight = weights.getComponent(vertex, lane);
|
|
152
|
+
if (!Number.isInteger(index) || index < 0 || index >= bones.length)
|
|
153
|
+
bad('SKIN_JOINT_INDEX', vertex, 'skinIndex');
|
|
154
|
+
if (!Number.isFinite(weight) || weight < 0 || weight > 1)
|
|
155
|
+
bad('SKIN_WEIGHT_VALUE', vertex, 'skinWeight');
|
|
156
|
+
sum += weight;
|
|
157
|
+
}
|
|
158
|
+
if (!Number.isFinite(sum) || Math.abs(sum - 1) > WEIGHT_TOLERANCE)
|
|
159
|
+
bad('SKIN_WEIGHT_SUM', vertex, 'skinWeight');
|
|
160
|
+
}
|
|
161
|
+
result.verticesChecked += count;
|
|
162
|
+
for (const [code, row] of invalid) {
|
|
163
|
+
const why = code === 'SKIN_JOINT_INDEX'
|
|
164
|
+
? 'indices must address the skeleton, including zero-weight lanes'
|
|
165
|
+
: code === 'SKIN_WEIGHT_SUM'
|
|
166
|
+
? `weights must sum to one within ${WEIGHT_TOLERANCE}`
|
|
167
|
+
: 'weights must be finite values in [0,1]';
|
|
168
|
+
add(code, `Invalid ${row.attribute} at vertex range ${row.first}..${row.last} (${row.count} affected vertices): ${why}.`, row.attribute, {
|
|
169
|
+
name: 'invalidSkinVertices',
|
|
170
|
+
actual: row.count,
|
|
171
|
+
expected: 0,
|
|
172
|
+
breakdown: { firstVertex: row.first, lastVertex: row.last },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (result.findings.length !== start)
|
|
176
|
+
continue;
|
|
177
|
+
const meshWorld = world(mesh);
|
|
178
|
+
if (!finiteAffine(meshWorld)) {
|
|
179
|
+
add('SKIN_POSE_TRANSFORM', 'Current mesh world transform must be finite and affine.');
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const inverse = mesh.bindMode === THREE.AttachedBindMode
|
|
183
|
+
? meshWorld.clone().invert()
|
|
184
|
+
: mesh.bindMatrix.clone().invert();
|
|
185
|
+
const matrices = [];
|
|
186
|
+
for (let joint = 0; joint < bones.length; joint++) {
|
|
187
|
+
const boneWorld = world(bones[joint]);
|
|
188
|
+
if (!finiteAffine(boneWorld)) {
|
|
189
|
+
add('SKIN_POSE_TRANSFORM', `Current transform for joint ${joint} must be finite and affine.`, `joints[${joint}]`);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
matrices.push(new THREE.Matrix4()
|
|
193
|
+
.multiplyMatrices(boneWorld, skeleton.boneInverses[joint])
|
|
194
|
+
.multiply(mesh.bindMatrix));
|
|
195
|
+
}
|
|
196
|
+
if (result.findings.length !== start)
|
|
197
|
+
continue;
|
|
198
|
+
const base = new THREE.Vector3(), posed = new THREE.Vector3(), temp = new THREE.Vector3(), box = new THREE.Box3();
|
|
199
|
+
for (let vertex = 0; vertex < count; vertex++) {
|
|
200
|
+
// Mesh's implementation evaluates morphs but does not invoke the unsafe skin path.
|
|
201
|
+
THREE.Mesh.prototype.getVertexPosition.call(mesh, vertex, base);
|
|
202
|
+
posed.set(0, 0, 0);
|
|
203
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
204
|
+
const weight = weights.getComponent(vertex, lane);
|
|
205
|
+
if (weight !== 0)
|
|
206
|
+
posed.addScaledVector(temp.copy(base).applyMatrix4(matrices[indices.getComponent(vertex, lane)]), weight);
|
|
207
|
+
}
|
|
208
|
+
posed.applyMatrix4(inverse).applyMatrix4(meshWorld);
|
|
209
|
+
if (!Number.isFinite(Math.fround(posed.x)) ||
|
|
210
|
+
!Number.isFinite(Math.fround(posed.y)) ||
|
|
211
|
+
!Number.isFinite(Math.fround(posed.z))) {
|
|
212
|
+
add('SKIN_POSED_VERTEX', `Current deformed vertex ${vertex} is not finite and Float32-representable. Check positions, morphs and weighted joint transforms.`, `position[${vertex}]`);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
box.expandByPoint(posed);
|
|
216
|
+
}
|
|
217
|
+
if (result.findings.length === start)
|
|
218
|
+
result.bounds.push({ nodePath, min: box.min.toArray(), max: box.max.toArray() });
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
@@ -207,7 +207,6 @@ export async function startLocalRenderService(dir = renderServiceDir(), environm
|
|
|
207
207
|
stderr = `${stderr}${chunk.toString()}`.slice(-2_000);
|
|
208
208
|
});
|
|
209
209
|
const deadline = Date.now() + budget;
|
|
210
|
-
let exitedAt;
|
|
211
210
|
try {
|
|
212
211
|
while (Date.now() < deadline) {
|
|
213
212
|
const fresh = await inspectLocalRenderService(url, dir, 1_000);
|
|
@@ -218,12 +217,10 @@ export async function startLocalRenderService(dir = renderServiceDir(), environm
|
|
|
218
217
|
const exited = windowsLaunch
|
|
219
218
|
? windowsLaunch.hasExited()
|
|
220
219
|
: running.exitCode !== null || running.signalCode !== null;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
// A failed spawn has no such race; absent/unknown never count as success.
|
|
226
|
-
if (spawnError || (exitedAt !== undefined && Date.now() - exitedAt >= 1500))
|
|
220
|
+
// A competing child can win the socket while this one exits after losing
|
|
221
|
+
// its bind. An unknown listener gets the same startup deadline as our child;
|
|
222
|
+
// only verified health permits joining. An exited child with no listener fails.
|
|
223
|
+
if (spawnError || (exited && fresh.kind === 'absent'))
|
|
227
224
|
throw new Error(`render service exited during startup${spawnError ? `: ${spawnError.message}` : ''}${stderr.trim() ? `: ${stderr.trim()}` : windowsLaunch ? '; launch the renderer manually to inspect native-driver errors' : ''}`);
|
|
228
225
|
await new Promise((done) => setTimeout(done, 250));
|
|
229
226
|
}
|
package/lib/render.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { type MaterialMetricsV1 } from "./material-metrics.js";
|
|
|
24
24
|
import { type MaterialRecipeApplicationProvenanceV1 } from "./material-recipe-runtime.js";
|
|
25
25
|
import { type MaterialResourceProvenanceV1 } from "./material-resources.js";
|
|
26
26
|
import { type TextureResolver } from "./texture-resolver.js";
|
|
27
|
-
import type {
|
|
27
|
+
import type { MaterialLibraryPayload, MaterialManifest } from "./material-library.js";
|
|
28
28
|
import { type RebuildOptions } from "./rebuild-options.js";
|
|
29
29
|
import { type RigidMergeSummary } from "./rigid-merge.js";
|
|
30
30
|
import { type KitPackOptions, type KitPackSummary } from "./kit.js";
|
|
@@ -146,7 +146,7 @@ export interface RenderResult {
|
|
|
146
146
|
materialResourceProvenance?: MaterialResourceProvenanceV1[];
|
|
147
147
|
/** Exact host-approved input dependencies, including licenses and procedural recipes.
|
|
148
148
|
* These are declared build inputs, not proof that every supplied map was used. */
|
|
149
|
-
materialLibraryDependencies?:
|
|
149
|
+
materialLibraryDependencies?: MaterialManifest[];
|
|
150
150
|
/** Textures baked into the returned GLB, including bounded procedural lineage. */
|
|
151
151
|
bakedTextures?: BakedTextureProvenanceV1[];
|
|
152
152
|
integrationManifest: IntegrationManifestV1;
|
|
@@ -338,7 +338,7 @@ export interface RenderGlbOptions {
|
|
|
338
338
|
/** Trusted evaluator dependency; never derived from generated code. */
|
|
339
339
|
textureResolver?: TextureResolver;
|
|
340
340
|
/** Host-approved portable material maps, transported as data to evaluator workers. */
|
|
341
|
-
materialResources?:
|
|
341
|
+
materialResources?: MaterialLibraryPayload;
|
|
342
342
|
/** In-process host diagnostic sink; not serializable to evaluator workers. */
|
|
343
343
|
diagnosticConsole?: DiagnosticConsole;
|
|
344
344
|
}
|
package/lib/render.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { assertNoLegacyRuntimePolicy, resolveRequirementsContext, } from "./requirements-context.js";
|
|
2
2
|
import { runRequirementsSceneQa, collectRequirementsSceneEvidence, appendRequirementsFinalQa, } from "./qa/requirements-run.js";
|
|
3
|
+
import { createRequirementsQaReport } from "./qa/requirements-report.js";
|
|
4
|
+
import { inspectSkinIntegrity } from "./qa/skin-integrity.js";
|
|
3
5
|
import { AuthoringDiagnosticError, rethrowAuthoringError } from "./evaluator/authoring-diagnostic.js";
|
|
4
6
|
/**
|
|
5
7
|
* Headless Kiln GLB Renderer
|
|
@@ -18,6 +20,8 @@ import { collectLodSets } from "./lod.js";
|
|
|
18
20
|
import { applyLodChains, summarizeLodChains } from "./lod-export.js";
|
|
19
21
|
import { applyNodeVisibility, drawnSceneBounds, summarizeHiddenNodes } from "./node-visibility.js";
|
|
20
22
|
import { communitySceneDocument, resolveGltfExporter } from "./community-exporter.js";
|
|
23
|
+
import { collectAtlasProvenance, mergeAtlasDocumentProvenance, refreshAtlasDocumentProvenance, validateAtlasDocumentProvenance, } from "./atlas-provenance.js";
|
|
24
|
+
import { assertPortableMaterialSides } from "./material-export.js";
|
|
21
25
|
import { isCubicSplineTrack } from "./animation-spline.js";
|
|
22
26
|
import { applyIndexPolicy, resolveIndexPolicy, } from "./index-policy.js";
|
|
23
27
|
import { openShellExtras } from "./open-shell.js";
|
|
@@ -255,7 +259,42 @@ function bridgeEmissive(doc, target, source) {
|
|
|
255
259
|
.createEmissiveStrength()
|
|
256
260
|
.setEmissiveStrength(peak));
|
|
257
261
|
}
|
|
258
|
-
|
|
262
|
+
const GLTF_MIN_FILTER = {
|
|
263
|
+
[THREE.NearestFilter]: 9728,
|
|
264
|
+
[THREE.LinearFilter]: 9729,
|
|
265
|
+
[THREE.NearestMipmapNearestFilter]: 9984,
|
|
266
|
+
[THREE.LinearMipmapNearestFilter]: 9985,
|
|
267
|
+
[THREE.NearestMipmapLinearFilter]: 9986,
|
|
268
|
+
[THREE.LinearMipmapLinearFilter]: 9987,
|
|
269
|
+
};
|
|
270
|
+
const GLTF_MAG_FILTER = { [THREE.NearestFilter]: 9728, [THREE.LinearFilter]: 9729 };
|
|
271
|
+
const GLTF_WRAP = {
|
|
272
|
+
[THREE.ClampToEdgeWrapping]: 33071,
|
|
273
|
+
[THREE.MirroredRepeatWrapping]: 33648,
|
|
274
|
+
[THREE.RepeatWrapping]: 10497,
|
|
275
|
+
};
|
|
276
|
+
/** Samplers belong to each texture use, not the cached glTF image. */
|
|
277
|
+
function bridgeTextureSampler(source, target, atlas) {
|
|
278
|
+
const min = GLTF_MIN_FILTER[source.minFilter];
|
|
279
|
+
const mag = GLTF_MAG_FILTER[source.magFilter];
|
|
280
|
+
const s = GLTF_WRAP[source.wrapS];
|
|
281
|
+
const t = GLTF_WRAP[source.wrapT];
|
|
282
|
+
for (const [property, value] of [
|
|
283
|
+
['minFilter', min],
|
|
284
|
+
['magFilter', mag],
|
|
285
|
+
['wrapS', s],
|
|
286
|
+
['wrapT', t],
|
|
287
|
+
]) {
|
|
288
|
+
if (typeof source[property] !== 'number' || value === undefined) {
|
|
289
|
+
throw new TypeError(`${source.name || '(unnamed texture)'}: unsupported ${property} ${source[property]}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
target.setMinFilter(min).setMagFilter(mag).setWrapS(s).setWrapT(t);
|
|
293
|
+
const ref = atlas?.texture(source);
|
|
294
|
+
if (ref)
|
|
295
|
+
target.setExtras({ ...target.getExtras(), kilnAtlasReference: ref });
|
|
296
|
+
}
|
|
297
|
+
function bridgeMaterial(doc, threeMat, cache, textureCache, authorExtras, atlas) {
|
|
259
298
|
const cached = cache.get(threeMat);
|
|
260
299
|
if (cached)
|
|
261
300
|
return cached;
|
|
@@ -286,13 +325,17 @@ function bridgeMaterial(doc, threeMat, cache, textureCache, authorExtras) {
|
|
|
286
325
|
// PBR texture slots (Wave 3B)
|
|
287
326
|
if (stdMat.map) {
|
|
288
327
|
const t = bridgeTexture(doc, stdMat.map, textureCache);
|
|
289
|
-
if (t)
|
|
328
|
+
if (t) {
|
|
290
329
|
mat.setBaseColorTexture(t);
|
|
330
|
+
bridgeTextureSampler(stdMat.map, mat.getBaseColorTextureInfo(), atlas);
|
|
331
|
+
}
|
|
291
332
|
}
|
|
292
333
|
if (stdMat.normalMap) {
|
|
293
334
|
const t = bridgeTexture(doc, stdMat.normalMap, textureCache);
|
|
294
|
-
if (t)
|
|
335
|
+
if (t) {
|
|
295
336
|
mat.setNormalTexture(t);
|
|
337
|
+
bridgeTextureSampler(stdMat.normalMap, mat.getNormalTextureInfo(), atlas);
|
|
338
|
+
}
|
|
296
339
|
}
|
|
297
340
|
// metallic + roughness live in one glTF texture (R=unused, G=rough, B=metal).
|
|
298
341
|
// If the agent used separate Three.js maps we emit the roughness one as
|
|
@@ -303,18 +346,24 @@ function bridgeMaterial(doc, threeMat, cache, textureCache, authorExtras) {
|
|
|
303
346
|
const mrSource = stdMat.roughnessMap ?? stdMat.metalnessMap;
|
|
304
347
|
if (mrSource) {
|
|
305
348
|
const t = bridgeTexture(doc, mrSource, textureCache);
|
|
306
|
-
if (t)
|
|
349
|
+
if (t) {
|
|
307
350
|
mat.setMetallicRoughnessTexture(t);
|
|
351
|
+
bridgeTextureSampler(mrSource, mat.getMetallicRoughnessTextureInfo(), atlas);
|
|
352
|
+
}
|
|
308
353
|
}
|
|
309
354
|
if (stdMat.emissiveMap) {
|
|
310
355
|
const t = bridgeTexture(doc, stdMat.emissiveMap, textureCache);
|
|
311
|
-
if (t)
|
|
356
|
+
if (t) {
|
|
312
357
|
mat.setEmissiveTexture(t);
|
|
358
|
+
bridgeTextureSampler(stdMat.emissiveMap, mat.getEmissiveTextureInfo(), atlas);
|
|
359
|
+
}
|
|
313
360
|
}
|
|
314
361
|
if (stdMat.aoMap) {
|
|
315
362
|
const t = bridgeTexture(doc, stdMat.aoMap, textureCache);
|
|
316
|
-
if (t)
|
|
363
|
+
if (t) {
|
|
317
364
|
mat.setOcclusionTexture(t);
|
|
365
|
+
bridgeTextureSampler(stdMat.aoMap, mat.getOcclusionTextureInfo(), atlas);
|
|
366
|
+
}
|
|
318
367
|
mat.setOcclusionStrength(stdMat.aoMapIntensity ?? 1);
|
|
319
368
|
}
|
|
320
369
|
}
|
|
@@ -363,8 +412,10 @@ function bridgeMaterial(doc, threeMat, cache, textureCache, authorExtras) {
|
|
|
363
412
|
mat.setDoubleSided(true);
|
|
364
413
|
if (common.map && !mat.getBaseColorTexture()) {
|
|
365
414
|
const texture = bridgeTexture(doc, common.map, textureCache);
|
|
366
|
-
if (texture)
|
|
415
|
+
if (texture) {
|
|
367
416
|
mat.setBaseColorTexture(texture);
|
|
417
|
+
bridgeTextureSampler(common.map, mat.getBaseColorTextureInfo(), atlas);
|
|
418
|
+
}
|
|
368
419
|
}
|
|
369
420
|
cache.set(threeMat, mat);
|
|
370
421
|
return mat;
|
|
@@ -396,7 +447,7 @@ function bridgeTexture(doc, threeTex, cache) {
|
|
|
396
447
|
cache.set(threeTex, t);
|
|
397
448
|
return t;
|
|
398
449
|
}
|
|
399
|
-
function bridgeGeometry(doc, buf, geometry, material, meshName) {
|
|
450
|
+
function bridgeGeometry(doc, buf, geometry, material, meshName, atlas) {
|
|
400
451
|
if (!geometry.getAttribute('normal')) {
|
|
401
452
|
geometry.computeVertexNormals();
|
|
402
453
|
}
|
|
@@ -445,6 +496,9 @@ function bridgeGeometry(doc, buf, geometry, material, meshName) {
|
|
|
445
496
|
.setType(TYPE_SCALAR)
|
|
446
497
|
.setBuffer(buf));
|
|
447
498
|
}
|
|
499
|
+
const binding = atlas?.binding(geometry);
|
|
500
|
+
if (binding)
|
|
501
|
+
prim.setExtras({ kilnAtlasBinding: binding });
|
|
448
502
|
const mesh = doc.createMesh(meshName);
|
|
449
503
|
if (!Array.isArray(material))
|
|
450
504
|
return mesh.addPrimitive(prim);
|
|
@@ -462,12 +516,14 @@ function bridgeGeometry(doc, buf, geometry, material, meshName) {
|
|
|
462
516
|
.setArray(new IndexArray(indexValues.slice(group.start, group.start + group.count)))
|
|
463
517
|
.setType(TYPE_SCALAR)
|
|
464
518
|
.setBuffer(buf));
|
|
519
|
+
if (binding)
|
|
520
|
+
part.setExtras({ kilnAtlasBinding: binding });
|
|
465
521
|
mesh.addPrimitive(part);
|
|
466
522
|
}
|
|
467
523
|
prim.dispose();
|
|
468
524
|
return mesh;
|
|
469
525
|
}
|
|
470
|
-
function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache, authorExtras) {
|
|
526
|
+
function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache, authorExtras, atlas) {
|
|
471
527
|
const gtNode = doc.createNode(threeObj.name || undefined);
|
|
472
528
|
// Author userData reaches extras only through `collectAuthorExtras` (plain JSON, no
|
|
473
529
|
// `kiln*` key, bounded); the engine's own reserved payloads are promoted here only
|
|
@@ -534,7 +590,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
|
|
|
534
590
|
if (threeObj.isMesh) {
|
|
535
591
|
const threeMesh = threeObj;
|
|
536
592
|
const threeMats = Array.isArray(threeMesh.material) ? threeMesh.material : [threeMesh.material];
|
|
537
|
-
const gtMats = threeMats.map((material) => bridgeMaterial(doc, material, matCache, texCache, authorExtras));
|
|
593
|
+
const gtMats = threeMats.map((material) => bridgeMaterial(doc, material, matCache, texCache, authorExtras, atlas));
|
|
538
594
|
const gtMat = Array.isArray(threeMesh.material) ? gtMats : gtMats[0];
|
|
539
595
|
// Key the mesh cache by (geometry ref, material ref) so createInstance
|
|
540
596
|
// (same geo + mat as source) produces a GLB-level mesh instance: a
|
|
@@ -543,7 +599,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
|
|
|
543
599
|
const cacheKey = `${threeMesh.geometry.uuid}__${threeMats.map((material) => material.uuid).join('_')}`;
|
|
544
600
|
let gtMesh = meshCache.get(cacheKey);
|
|
545
601
|
if (!gtMesh) {
|
|
546
|
-
gtMesh = bridgeGeometry(doc, buf, threeMesh.geometry, gtMat, threeMesh.name || 'mesh');
|
|
602
|
+
gtMesh = bridgeGeometry(doc, buf, threeMesh.geometry, gtMat, threeMesh.name || 'mesh', atlas);
|
|
547
603
|
meshCache.set(cacheKey, gtMesh);
|
|
548
604
|
}
|
|
549
605
|
gtNode.setMesh(gtMesh);
|
|
@@ -551,7 +607,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
|
|
|
551
607
|
else if (threeObj.isSprite) {
|
|
552
608
|
const sprite = threeObj;
|
|
553
609
|
const threeMat = sprite.material;
|
|
554
|
-
const gtMat = bridgeMaterial(doc, threeMat, matCache, texCache, authorExtras);
|
|
610
|
+
const gtMat = bridgeMaterial(doc, threeMat, matCache, texCache, authorExtras, atlas);
|
|
555
611
|
// Unit XY quad matches Three.js Sprite scale semantics. Non-default center
|
|
556
612
|
// and material rotation are baked into the quad; camera-facing remains an
|
|
557
613
|
// explicit semantic runtime behavior on the exported node.
|
|
@@ -567,7 +623,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
|
|
|
567
623
|
}
|
|
568
624
|
if (rotation !== 0)
|
|
569
625
|
geometry.rotateZ(rotation);
|
|
570
|
-
gtMesh = bridgeGeometry(doc, buf, geometry, gtMat, sprite.name || 'sprite-quad');
|
|
626
|
+
gtMesh = bridgeGeometry(doc, buf, geometry, gtMat, sprite.name || 'sprite-quad', atlas);
|
|
571
627
|
meshCache.set(cacheKey, gtMesh);
|
|
572
628
|
}
|
|
573
629
|
gtNode.setMesh(gtMesh);
|
|
@@ -576,7 +632,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
|
|
|
576
632
|
nodeMap.set(threeObj.name, gtNode);
|
|
577
633
|
}
|
|
578
634
|
for (const child of threeObj.children) {
|
|
579
|
-
const childNode = bridgeNode(doc, buf, child, matCache, nodeMap, meshCache, texCache, authorExtras);
|
|
635
|
+
const childNode = bridgeNode(doc, buf, child, matCache, nodeMap, meshCache, texCache, authorExtras, atlas);
|
|
580
636
|
gtNode.addChild(childNode);
|
|
581
637
|
}
|
|
582
638
|
return gtNode;
|
|
@@ -1032,6 +1088,26 @@ export async function renderSceneToGLB(root, opts = {}) {
|
|
|
1032
1088
|
if (opts.geometryPolicy !== undefined && !['warn', 'strict'].includes(opts.geometryPolicy))
|
|
1033
1089
|
throw new Error('geometryPolicy must be warn or strict');
|
|
1034
1090
|
const exporter = resolveGltfExporter(opts.gltfExporter);
|
|
1091
|
+
// Mandatory source-data invariants precede any operation that dereferences bindings.
|
|
1092
|
+
// Reuse the structured QA transport without claiming that the full QA rules ran.
|
|
1093
|
+
// Like invalid geometry buffers, malformed skin data cannot be made safe by QA-warn
|
|
1094
|
+
// or derivative mode. No automatic weight normalization or rest-pose repair occurs.
|
|
1095
|
+
const skinIntegrity = inspectSkinIntegrity(root);
|
|
1096
|
+
if (skinIntegrity.findings.length) {
|
|
1097
|
+
throw new AssetQaBlockedError(createRequirementsQaReport(requirements, {
|
|
1098
|
+
findings: skinIntegrity.findings,
|
|
1099
|
+
executedRules: [],
|
|
1100
|
+
evaluatedDimensions: ['exportIntegrity'],
|
|
1101
|
+
metrics: {
|
|
1102
|
+
exportIntegrity: {
|
|
1103
|
+
skinMeshesChecked: skinIntegrity.meshesChecked,
|
|
1104
|
+
skinVerticesChecked: skinIntegrity.verticesChecked,
|
|
1105
|
+
skinInspectionTruncated: skinIntegrity.truncated,
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
}), 'scene');
|
|
1109
|
+
}
|
|
1110
|
+
assertPortableMaterialSides(root);
|
|
1035
1111
|
// A set of LOD tiers needs its declared thresholds before anything is written. A
|
|
1036
1112
|
// derivative re-serializes a review scene that has no declarations, so it is not judged.
|
|
1037
1113
|
const lodSets = opts.derivative === true ? [] : collectLodSets(root);
|
|
@@ -1062,6 +1138,7 @@ export async function renderSceneToGLB(root, opts = {}) {
|
|
|
1062
1138
|
// that will actually be written. A procedural DataTexture used to reach the
|
|
1063
1139
|
// bridge with no encoded bytes and be dropped without a word; now it is
|
|
1064
1140
|
// either baked into the GLB or warned about by name.
|
|
1141
|
+
const atlas = collectAtlasProvenance(root);
|
|
1065
1142
|
const bakedTextures = await bakeSceneTextures(root, warnings);
|
|
1066
1143
|
// A normal-mapped mesh without tangents makes every runtime invent its own
|
|
1067
1144
|
// tangent basis, so the same asset lights differently in different engines.
|
|
@@ -1085,7 +1162,7 @@ export async function renderSceneToGLB(root, opts = {}) {
|
|
|
1085
1162
|
const authorExtras = collectAuthorExtras(root);
|
|
1086
1163
|
warnings.push(...authorExtras.warnings);
|
|
1087
1164
|
const doc = exporter === 'three'
|
|
1088
|
-
? await communitySceneDocument(root, nativeClips, (await import("./exporter-node.js")).nodeExportPlatform, authorExtras)
|
|
1165
|
+
? await communitySceneDocument(root, nativeClips, (await import("./exporter-node.js")).nodeExportPlatform, authorExtras, atlas)
|
|
1089
1166
|
: new Document();
|
|
1090
1167
|
// `asset.generator` defaults to the serializer's own version string, which put
|
|
1091
1168
|
// `glTF-Transform v4.4.1` inside every artifact's bytes -- and therefore inside
|
|
@@ -1103,7 +1180,7 @@ export async function renderSceneToGLB(root, opts = {}) {
|
|
|
1103
1180
|
const meshCache = new Map();
|
|
1104
1181
|
const texCache = new Map();
|
|
1105
1182
|
const nodeMap = new Map();
|
|
1106
|
-
const rootNode = bridgeNode(doc, buf, root, matCache, nodeMap, meshCache, texCache, authorExtras);
|
|
1183
|
+
const rootNode = bridgeNode(doc, buf, root, matCache, nodeMap, meshCache, texCache, authorExtras, atlas);
|
|
1107
1184
|
const gltfScene = doc.createScene(opts.sceneName ?? 'Scene').addChild(rootNode);
|
|
1108
1185
|
doc.getRoot().setDefaultScene(gltfScene);
|
|
1109
1186
|
if (clips.length > 0) {
|
|
@@ -1124,6 +1201,10 @@ export async function renderSceneToGLB(root, opts = {}) {
|
|
|
1124
1201
|
scene.setExtras({ [REVIEW_CLIPS_EXTRAS_KEY]: review });
|
|
1125
1202
|
}
|
|
1126
1203
|
}
|
|
1204
|
+
if (atlas.table) {
|
|
1205
|
+
const scene = doc.getRoot().getDefaultScene();
|
|
1206
|
+
scene.setExtras({ ...scene.getExtras(), kilnAtlasProvenance: atlas.table });
|
|
1207
|
+
}
|
|
1127
1208
|
// Visibility flags and LOD chains are written before any pass merges or prunes nodes, so
|
|
1128
1209
|
// those passes see them and keep them; flags first, while every node is still in place.
|
|
1129
1210
|
applyNodeVisibility(root, doc);
|
|
@@ -1202,6 +1283,8 @@ export async function renderSceneToGLB(root, opts = {}) {
|
|
|
1202
1283
|
}
|
|
1203
1284
|
const io = engineIO();
|
|
1204
1285
|
ensureDefaultScene(doc);
|
|
1286
|
+
await refreshAtlasDocumentProvenance(doc);
|
|
1287
|
+
await validateAtlasDocumentProvenance(doc);
|
|
1205
1288
|
const bytes = await io.writeBinary(doc);
|
|
1206
1289
|
const artifactGlbSha256 = `sha256:${await sha256Hex(bytes)}`;
|
|
1207
1290
|
const boundBakedTextures = bakedTextures.map((entry) => ({
|
|
@@ -1415,6 +1498,7 @@ export async function optimizeGlbBytes(bytes, opts = {}) {
|
|
|
1415
1498
|
try {
|
|
1416
1499
|
const io = engineIO();
|
|
1417
1500
|
const doc = await io.readBinary(bytes);
|
|
1501
|
+
await validateAtlasDocumentProvenance(doc);
|
|
1418
1502
|
// GPU-instancing first (dedup already ran at bake; instance → palette is the
|
|
1419
1503
|
// canonical order). Defaults OFF here — this is the web-tier re-bake seam, and
|
|
1420
1504
|
// the caller opts in per-asset (Studio passes instance:'auto' + the record's
|
|
@@ -1443,6 +1527,8 @@ export async function optimizeGlbBytes(bytes, opts = {}) {
|
|
|
1443
1527
|
if (!summary && !instancing)
|
|
1444
1528
|
return undefined;
|
|
1445
1529
|
ensureDefaultScene(doc);
|
|
1530
|
+
await refreshAtlasDocumentProvenance(doc);
|
|
1531
|
+
await validateAtlasDocumentProvenance(doc);
|
|
1446
1532
|
const outBytes = await io.writeBinary(doc);
|
|
1447
1533
|
const gltfValidation = await assertFinalGlbValid(outBytes);
|
|
1448
1534
|
let report;
|
|
@@ -1516,6 +1602,7 @@ export async function snapGlbToPalette(bytes, slots, opts = {}) {
|
|
|
1516
1602
|
try {
|
|
1517
1603
|
const io = engineIO();
|
|
1518
1604
|
const doc = await io.readBinary(bytes);
|
|
1605
|
+
await validateAtlasDocumentProvenance(doc);
|
|
1519
1606
|
const before = collectGlbMetrics(doc);
|
|
1520
1607
|
const idx = buildSlotIndex(slots);
|
|
1521
1608
|
let snapped = 0;
|
|
@@ -1570,6 +1657,8 @@ export async function snapGlbToPalette(bytes, slots, opts = {}) {
|
|
|
1570
1657
|
drawsAfter: after.drawCalls,
|
|
1571
1658
|
};
|
|
1572
1659
|
ensureDefaultScene(doc);
|
|
1660
|
+
await refreshAtlasDocumentProvenance(doc);
|
|
1661
|
+
await validateAtlasDocumentProvenance(doc);
|
|
1573
1662
|
const outBytes = await io.writeBinary(doc);
|
|
1574
1663
|
const gltfValidation = await assertFinalGlbValid(outBytes);
|
|
1575
1664
|
let report;
|
|
@@ -1629,6 +1718,7 @@ export async function composeSceneGLB(parts, opts = {}) {
|
|
|
1629
1718
|
const part = parts[i];
|
|
1630
1719
|
try {
|
|
1631
1720
|
const src = await io.readBinary(part.bytes);
|
|
1721
|
+
await mergeAtlasDocumentProvenance(master, src);
|
|
1632
1722
|
if (opts.keepAnimations && opts.animationNaming === 'instance') {
|
|
1633
1723
|
for (const [clipIndex, animation] of src.getRoot().listAnimations().entries()) {
|
|
1634
1724
|
animation.setName(`${i}:${part.name ?? 'part'}/${clipIndex}:${animation.getName() || 'clip'}`);
|
|
@@ -1695,6 +1785,8 @@ export async function composeSceneGLB(parts, opts = {}) {
|
|
|
1695
1785
|
}
|
|
1696
1786
|
const metrics = collectGlbMetrics(master);
|
|
1697
1787
|
ensureDefaultScene(master);
|
|
1788
|
+
await refreshAtlasDocumentProvenance(master);
|
|
1789
|
+
await validateAtlasDocumentProvenance(master);
|
|
1698
1790
|
const bytes = await io.writeBinary(master);
|
|
1699
1791
|
const gltfValidation = await assertFinalGlbValid(bytes, 'scene.glb');
|
|
1700
1792
|
for (const issue of gltfValidation.issues.messages) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { type SemanticMetadataV1, type SemanticMetadataV1Input } from "./contracts/index.js";
|
|
3
|
+
export interface CreateSkinnedPartOptions {
|
|
4
|
+
/** Already assembled common hierarchy containing every bone, in the rest pose. */
|
|
5
|
+
parent: THREE.Object3D;
|
|
6
|
+
/** Explicit, stable index order. Bones remain owned by the caller's hierarchy. */
|
|
7
|
+
bones: readonly THREE.Bone[];
|
|
8
|
+
/** Four indices and weights per geometry vertex, including zero-weight lanes. */
|
|
9
|
+
indices: ArrayLike<number>;
|
|
10
|
+
weights: ArrayLike<number>;
|
|
11
|
+
/** Default false: require weights to sum to one. Never invent weights for zero sums. */
|
|
12
|
+
normalizeWeights?: boolean;
|
|
13
|
+
position?: [number, number, number];
|
|
14
|
+
/** Local Euler angles in degrees, matching createPart. */
|
|
15
|
+
rotation?: [number, number, number];
|
|
16
|
+
scale?: [number, number, number];
|
|
17
|
+
semantic?: SemanticMetadataV1 | SemanticMetadataV1Input;
|
|
18
|
+
}
|
|
19
|
+
/** Clone unchanged topology, attach explicit four-lane influences and bind at the current rest pose. */
|
|
20
|
+
export declare function createSkinnedPart(name: string, geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[], options: CreateSkinnedPartOptions): THREE.SkinnedMesh;
|