@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
package/src/render.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
appendRequirementsFinalQa,
|
|
12
12
|
} from './qa/requirements-run';
|
|
13
13
|
import type { AssetRequirementsQaReportV2 } from './qa/requirements-report';
|
|
14
|
+
import { createRequirementsQaReport } from './qa/requirements-report';
|
|
15
|
+
import { inspectSkinIntegrity } from './qa/skin-integrity';
|
|
14
16
|
import { AuthoringDiagnosticError, rethrowAuthoringError } from './evaluator/authoring-diagnostic';
|
|
15
17
|
/**
|
|
16
18
|
* Headless Kiln GLB Renderer
|
|
@@ -30,6 +32,14 @@ import { collectLodSets } from './lod';
|
|
|
30
32
|
import { applyLodChains, summarizeLodChains } from './lod-export';
|
|
31
33
|
import { applyNodeVisibility, drawnSceneBounds, summarizeHiddenNodes } from './node-visibility';
|
|
32
34
|
import { communitySceneDocument, resolveGltfExporter } from './community-exporter';
|
|
35
|
+
import {
|
|
36
|
+
collectAtlasProvenance,
|
|
37
|
+
mergeAtlasDocumentProvenance,
|
|
38
|
+
refreshAtlasDocumentProvenance,
|
|
39
|
+
validateAtlasDocumentProvenance,
|
|
40
|
+
type AtlasProvenanceCollection,
|
|
41
|
+
} from './atlas-provenance';
|
|
42
|
+
import { assertPortableMaterialSides } from './material-export';
|
|
33
43
|
import { isCubicSplineTrack } from './animation-spline';
|
|
34
44
|
import {
|
|
35
45
|
applyIndexPolicy,
|
|
@@ -47,7 +57,7 @@ import {
|
|
|
47
57
|
type GeometryExportPolicy,
|
|
48
58
|
} from './geometry-export';
|
|
49
59
|
import { createHash } from 'node:crypto';
|
|
50
|
-
import { Document, Logger } from '@gltf-transform/core';
|
|
60
|
+
import { Document, Logger, type TextureInfo } from '@gltf-transform/core';
|
|
51
61
|
import { KHRMaterialsEmissiveStrength } from '@gltf-transform/extensions';
|
|
52
62
|
import { dedup, instance, palette, weld, prune, mergeDocuments } from '@gltf-transform/functions';
|
|
53
63
|
|
|
@@ -98,7 +108,7 @@ import {
|
|
|
98
108
|
type MaterialResourceProvenanceV1,
|
|
99
109
|
} from './material-resources';
|
|
100
110
|
import { DEFAULT_TEXTURE_RESOLVER, type TextureResolver } from './texture-resolver';
|
|
101
|
-
import type {
|
|
111
|
+
import type { MaterialLibraryPayload, MaterialManifest } from './material-library';
|
|
102
112
|
import { FULL_OPTIMIZATION_PIPELINE, type RebuildOptions } from './rebuild-options';
|
|
103
113
|
import { rigidMerge, type RigidMergeSummary } from './rigid-merge';
|
|
104
114
|
import { assertGeneratedSourceSafe } from './validation';
|
|
@@ -464,12 +474,55 @@ function bridgeEmissive(
|
|
|
464
474
|
);
|
|
465
475
|
}
|
|
466
476
|
|
|
477
|
+
const GLTF_MIN_FILTER = {
|
|
478
|
+
[THREE.NearestFilter]: 9728,
|
|
479
|
+
[THREE.LinearFilter]: 9729,
|
|
480
|
+
[THREE.NearestMipmapNearestFilter]: 9984,
|
|
481
|
+
[THREE.LinearMipmapNearestFilter]: 9985,
|
|
482
|
+
[THREE.NearestMipmapLinearFilter]: 9986,
|
|
483
|
+
[THREE.LinearMipmapLinearFilter]: 9987,
|
|
484
|
+
} as const;
|
|
485
|
+
const GLTF_MAG_FILTER = { [THREE.NearestFilter]: 9728, [THREE.LinearFilter]: 9729 } as const;
|
|
486
|
+
const GLTF_WRAP = {
|
|
487
|
+
[THREE.ClampToEdgeWrapping]: 33071,
|
|
488
|
+
[THREE.MirroredRepeatWrapping]: 33648,
|
|
489
|
+
[THREE.RepeatWrapping]: 10497,
|
|
490
|
+
} as const;
|
|
491
|
+
|
|
492
|
+
/** Samplers belong to each texture use, not the cached glTF image. */
|
|
493
|
+
function bridgeTextureSampler(
|
|
494
|
+
source: THREE.Texture,
|
|
495
|
+
target: TextureInfo,
|
|
496
|
+
atlas?: AtlasProvenanceCollection,
|
|
497
|
+
): void {
|
|
498
|
+
const min = GLTF_MIN_FILTER[source.minFilter];
|
|
499
|
+
const mag = GLTF_MAG_FILTER[source.magFilter];
|
|
500
|
+
const s = GLTF_WRAP[source.wrapS];
|
|
501
|
+
const t = GLTF_WRAP[source.wrapT];
|
|
502
|
+
for (const [property, value] of [
|
|
503
|
+
['minFilter', min],
|
|
504
|
+
['magFilter', mag],
|
|
505
|
+
['wrapS', s],
|
|
506
|
+
['wrapT', t],
|
|
507
|
+
] as const) {
|
|
508
|
+
if (typeof source[property] !== 'number' || value === undefined) {
|
|
509
|
+
throw new TypeError(
|
|
510
|
+
`${source.name || '(unnamed texture)'}: unsupported ${property} ${source[property]}`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
target.setMinFilter(min).setMagFilter(mag).setWrapS(s).setWrapT(t);
|
|
515
|
+
const ref = atlas?.texture(source);
|
|
516
|
+
if (ref) target.setExtras({ ...target.getExtras(), kilnAtlasReference: ref });
|
|
517
|
+
}
|
|
518
|
+
|
|
467
519
|
function bridgeMaterial(
|
|
468
520
|
doc: Document,
|
|
469
521
|
threeMat: THREE.Material,
|
|
470
522
|
cache: Map<THREE.Material, GtMaterial>,
|
|
471
523
|
textureCache: Map<THREE.Texture, GtTexture>,
|
|
472
524
|
authorExtras?: AuthorExtras,
|
|
525
|
+
atlas?: AtlasProvenanceCollection,
|
|
473
526
|
): GtMaterial {
|
|
474
527
|
const cached = cache.get(threeMat);
|
|
475
528
|
if (cached) return cached;
|
|
@@ -505,11 +558,17 @@ function bridgeMaterial(
|
|
|
505
558
|
// PBR texture slots (Wave 3B)
|
|
506
559
|
if (stdMat.map) {
|
|
507
560
|
const t = bridgeTexture(doc, stdMat.map, textureCache);
|
|
508
|
-
if (t)
|
|
561
|
+
if (t) {
|
|
562
|
+
mat.setBaseColorTexture(t);
|
|
563
|
+
bridgeTextureSampler(stdMat.map, mat.getBaseColorTextureInfo()!, atlas);
|
|
564
|
+
}
|
|
509
565
|
}
|
|
510
566
|
if (stdMat.normalMap) {
|
|
511
567
|
const t = bridgeTexture(doc, stdMat.normalMap, textureCache);
|
|
512
|
-
if (t)
|
|
568
|
+
if (t) {
|
|
569
|
+
mat.setNormalTexture(t);
|
|
570
|
+
bridgeTextureSampler(stdMat.normalMap, mat.getNormalTextureInfo()!, atlas);
|
|
571
|
+
}
|
|
513
572
|
}
|
|
514
573
|
// metallic + roughness live in one glTF texture (R=unused, G=rough, B=metal).
|
|
515
574
|
// If the agent used separate Three.js maps we emit the roughness one as
|
|
@@ -522,15 +581,24 @@ function bridgeMaterial(
|
|
|
522
581
|
const mrSource = stdMat.roughnessMap ?? stdMat.metalnessMap;
|
|
523
582
|
if (mrSource) {
|
|
524
583
|
const t = bridgeTexture(doc, mrSource, textureCache);
|
|
525
|
-
if (t)
|
|
584
|
+
if (t) {
|
|
585
|
+
mat.setMetallicRoughnessTexture(t);
|
|
586
|
+
bridgeTextureSampler(mrSource, mat.getMetallicRoughnessTextureInfo()!, atlas);
|
|
587
|
+
}
|
|
526
588
|
}
|
|
527
589
|
if (stdMat.emissiveMap) {
|
|
528
590
|
const t = bridgeTexture(doc, stdMat.emissiveMap, textureCache);
|
|
529
|
-
if (t)
|
|
591
|
+
if (t) {
|
|
592
|
+
mat.setEmissiveTexture(t);
|
|
593
|
+
bridgeTextureSampler(stdMat.emissiveMap, mat.getEmissiveTextureInfo()!, atlas);
|
|
594
|
+
}
|
|
530
595
|
}
|
|
531
596
|
if (stdMat.aoMap) {
|
|
532
597
|
const t = bridgeTexture(doc, stdMat.aoMap, textureCache);
|
|
533
|
-
if (t)
|
|
598
|
+
if (t) {
|
|
599
|
+
mat.setOcclusionTexture(t);
|
|
600
|
+
bridgeTextureSampler(stdMat.aoMap, mat.getOcclusionTextureInfo()!, atlas);
|
|
601
|
+
}
|
|
534
602
|
mat.setOcclusionStrength(stdMat.aoMapIntensity ?? 1);
|
|
535
603
|
}
|
|
536
604
|
} else if (matFlags.isMeshLambertMaterial) {
|
|
@@ -580,7 +648,10 @@ function bridgeMaterial(
|
|
|
580
648
|
if (threeMat.side === THREE.DoubleSide) mat.setDoubleSided(true);
|
|
581
649
|
if (common.map && !mat.getBaseColorTexture()) {
|
|
582
650
|
const texture = bridgeTexture(doc, common.map, textureCache);
|
|
583
|
-
if (texture)
|
|
651
|
+
if (texture) {
|
|
652
|
+
mat.setBaseColorTexture(texture);
|
|
653
|
+
bridgeTextureSampler(common.map, mat.getBaseColorTextureInfo()!, atlas);
|
|
654
|
+
}
|
|
584
655
|
}
|
|
585
656
|
|
|
586
657
|
cache.set(threeMat, mat);
|
|
@@ -627,6 +698,7 @@ function bridgeGeometry(
|
|
|
627
698
|
geometry: THREE.BufferGeometry,
|
|
628
699
|
material: GtMaterial | GtMaterial[],
|
|
629
700
|
meshName: string,
|
|
701
|
+
atlas?: AtlasProvenanceCollection,
|
|
630
702
|
): GtMesh {
|
|
631
703
|
if (!geometry.getAttribute('normal')) {
|
|
632
704
|
geometry.computeVertexNormals();
|
|
@@ -697,6 +769,8 @@ function bridgeGeometry(
|
|
|
697
769
|
);
|
|
698
770
|
}
|
|
699
771
|
|
|
772
|
+
const binding = atlas?.binding(geometry);
|
|
773
|
+
if (binding) prim.setExtras({ kilnAtlasBinding: binding });
|
|
700
774
|
const mesh = doc.createMesh(meshName);
|
|
701
775
|
if (!Array.isArray(material)) return mesh.addPrimitive(prim);
|
|
702
776
|
const indexValues = indexAttr
|
|
@@ -715,6 +789,7 @@ function bridgeGeometry(
|
|
|
715
789
|
.setType(TYPE_SCALAR)
|
|
716
790
|
.setBuffer(buf),
|
|
717
791
|
);
|
|
792
|
+
if (binding) part.setExtras({ kilnAtlasBinding: binding });
|
|
718
793
|
mesh.addPrimitive(part);
|
|
719
794
|
}
|
|
720
795
|
prim.dispose();
|
|
@@ -730,6 +805,7 @@ function bridgeNode(
|
|
|
730
805
|
meshCache: Map<string, GtMesh>,
|
|
731
806
|
texCache: Map<THREE.Texture, GtTexture>,
|
|
732
807
|
authorExtras?: AuthorExtras,
|
|
808
|
+
atlas?: AtlasProvenanceCollection,
|
|
733
809
|
): GtNode {
|
|
734
810
|
const gtNode = doc.createNode(threeObj.name || undefined);
|
|
735
811
|
|
|
@@ -800,7 +876,7 @@ function bridgeNode(
|
|
|
800
876
|
const threeMesh = threeObj as THREE.Mesh;
|
|
801
877
|
const threeMats = Array.isArray(threeMesh.material) ? threeMesh.material : [threeMesh.material];
|
|
802
878
|
const gtMats = threeMats.map((material) =>
|
|
803
|
-
bridgeMaterial(doc, material, matCache, texCache, authorExtras),
|
|
879
|
+
bridgeMaterial(doc, material, matCache, texCache, authorExtras, atlas),
|
|
804
880
|
);
|
|
805
881
|
const gtMat = Array.isArray(threeMesh.material) ? gtMats : gtMats[0]!;
|
|
806
882
|
|
|
@@ -811,14 +887,14 @@ function bridgeNode(
|
|
|
811
887
|
const cacheKey = `${threeMesh.geometry.uuid}__${threeMats.map((material) => material.uuid).join('_')}`;
|
|
812
888
|
let gtMesh = meshCache.get(cacheKey);
|
|
813
889
|
if (!gtMesh) {
|
|
814
|
-
gtMesh = bridgeGeometry(doc, buf, threeMesh.geometry, gtMat, threeMesh.name || 'mesh');
|
|
890
|
+
gtMesh = bridgeGeometry(doc, buf, threeMesh.geometry, gtMat, threeMesh.name || 'mesh', atlas);
|
|
815
891
|
meshCache.set(cacheKey, gtMesh);
|
|
816
892
|
}
|
|
817
893
|
gtNode.setMesh(gtMesh);
|
|
818
894
|
} else if ((threeObj as THREE.Object3D & { isSprite?: boolean }).isSprite) {
|
|
819
895
|
const sprite = threeObj as THREE.Sprite;
|
|
820
896
|
const threeMat = sprite.material;
|
|
821
|
-
const gtMat = bridgeMaterial(doc, threeMat, matCache, texCache, authorExtras);
|
|
897
|
+
const gtMat = bridgeMaterial(doc, threeMat, matCache, texCache, authorExtras, atlas);
|
|
822
898
|
// Unit XY quad matches Three.js Sprite scale semantics. Non-default center
|
|
823
899
|
// and material rotation are baked into the quad; camera-facing remains an
|
|
824
900
|
// explicit semantic runtime behavior on the exported node.
|
|
@@ -833,7 +909,7 @@ function bridgeNode(
|
|
|
833
909
|
geometry.translate(0.5 - centerX, 0.5 - centerY, 0);
|
|
834
910
|
}
|
|
835
911
|
if (rotation !== 0) geometry.rotateZ(rotation);
|
|
836
|
-
gtMesh = bridgeGeometry(doc, buf, geometry, gtMat, sprite.name || 'sprite-quad');
|
|
912
|
+
gtMesh = bridgeGeometry(doc, buf, geometry, gtMat, sprite.name || 'sprite-quad', atlas);
|
|
837
913
|
meshCache.set(cacheKey, gtMesh);
|
|
838
914
|
}
|
|
839
915
|
gtNode.setMesh(gtMesh);
|
|
@@ -853,6 +929,7 @@ function bridgeNode(
|
|
|
853
929
|
meshCache,
|
|
854
930
|
texCache,
|
|
855
931
|
authorExtras,
|
|
932
|
+
atlas,
|
|
856
933
|
);
|
|
857
934
|
gtNode.addChild(childNode);
|
|
858
935
|
}
|
|
@@ -1070,7 +1147,7 @@ export interface RenderResult {
|
|
|
1070
1147
|
materialResourceProvenance?: MaterialResourceProvenanceV1[];
|
|
1071
1148
|
/** Exact host-approved input dependencies, including licenses and procedural recipes.
|
|
1072
1149
|
* These are declared build inputs, not proof that every supplied map was used. */
|
|
1073
|
-
materialLibraryDependencies?:
|
|
1150
|
+
materialLibraryDependencies?: MaterialManifest[];
|
|
1074
1151
|
/** Textures baked into the returned GLB, including bounded procedural lineage. */
|
|
1075
1152
|
bakedTextures?: BakedTextureProvenanceV1[];
|
|
1076
1153
|
integrationManifest: IntegrationManifestV1;
|
|
@@ -1539,6 +1616,29 @@ export async function renderSceneToGLB(
|
|
|
1539
1616
|
if (opts.geometryPolicy !== undefined && !['warn', 'strict'].includes(opts.geometryPolicy))
|
|
1540
1617
|
throw new Error('geometryPolicy must be warn or strict');
|
|
1541
1618
|
const exporter = resolveGltfExporter(opts.gltfExporter);
|
|
1619
|
+
// Mandatory source-data invariants precede any operation that dereferences bindings.
|
|
1620
|
+
// Reuse the structured QA transport without claiming that the full QA rules ran.
|
|
1621
|
+
// Like invalid geometry buffers, malformed skin data cannot be made safe by QA-warn
|
|
1622
|
+
// or derivative mode. No automatic weight normalization or rest-pose repair occurs.
|
|
1623
|
+
const skinIntegrity = inspectSkinIntegrity(root);
|
|
1624
|
+
if (skinIntegrity.findings.length) {
|
|
1625
|
+
throw new AssetQaBlockedError(
|
|
1626
|
+
createRequirementsQaReport(requirements, {
|
|
1627
|
+
findings: skinIntegrity.findings,
|
|
1628
|
+
executedRules: [],
|
|
1629
|
+
evaluatedDimensions: ['exportIntegrity'],
|
|
1630
|
+
metrics: {
|
|
1631
|
+
exportIntegrity: {
|
|
1632
|
+
skinMeshesChecked: skinIntegrity.meshesChecked,
|
|
1633
|
+
skinVerticesChecked: skinIntegrity.verticesChecked,
|
|
1634
|
+
skinInspectionTruncated: skinIntegrity.truncated,
|
|
1635
|
+
},
|
|
1636
|
+
},
|
|
1637
|
+
}),
|
|
1638
|
+
'scene',
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
assertPortableMaterialSides(root);
|
|
1542
1642
|
// A set of LOD tiers needs its declared thresholds before anything is written. A
|
|
1543
1643
|
// derivative re-serializes a review scene that has no declarations, so it is not judged.
|
|
1544
1644
|
const lodSets = opts.derivative === true ? [] : collectLodSets(root);
|
|
@@ -1569,6 +1669,7 @@ export async function renderSceneToGLB(
|
|
|
1569
1669
|
// that will actually be written. A procedural DataTexture used to reach the
|
|
1570
1670
|
// bridge with no encoded bytes and be dropped without a word; now it is
|
|
1571
1671
|
// either baked into the GLB or warned about by name.
|
|
1672
|
+
const atlas = collectAtlasProvenance(root);
|
|
1572
1673
|
const bakedTextures = await bakeSceneTextures(root, warnings);
|
|
1573
1674
|
// A normal-mapped mesh without tangents makes every runtime invent its own
|
|
1574
1675
|
// tangent basis, so the same asset lights differently in different engines.
|
|
@@ -1599,6 +1700,7 @@ export async function renderSceneToGLB(
|
|
|
1599
1700
|
nativeClips,
|
|
1600
1701
|
(await import('./exporter-node')).nodeExportPlatform,
|
|
1601
1702
|
authorExtras,
|
|
1703
|
+
atlas,
|
|
1602
1704
|
)
|
|
1603
1705
|
: new Document();
|
|
1604
1706
|
// `asset.generator` defaults to the serializer's own version string, which put
|
|
@@ -1627,6 +1729,7 @@ export async function renderSceneToGLB(
|
|
|
1627
1729
|
meshCache,
|
|
1628
1730
|
texCache,
|
|
1629
1731
|
authorExtras,
|
|
1732
|
+
atlas,
|
|
1630
1733
|
);
|
|
1631
1734
|
const gltfScene = doc.createScene(opts.sceneName ?? 'Scene').addChild(rootNode);
|
|
1632
1735
|
doc.getRoot().setDefaultScene(gltfScene);
|
|
@@ -1647,6 +1750,10 @@ export async function renderSceneToGLB(
|
|
|
1647
1750
|
scene.setExtras({ [REVIEW_CLIPS_EXTRAS_KEY]: review });
|
|
1648
1751
|
}
|
|
1649
1752
|
}
|
|
1753
|
+
if (atlas.table) {
|
|
1754
|
+
const scene = doc.getRoot().getDefaultScene()!;
|
|
1755
|
+
scene.setExtras({ ...scene.getExtras(), kilnAtlasProvenance: atlas.table });
|
|
1756
|
+
}
|
|
1650
1757
|
// Visibility flags and LOD chains are written before any pass merges or prunes nodes, so
|
|
1651
1758
|
// those passes see them and keep them; flags first, while every node is still in place.
|
|
1652
1759
|
applyNodeVisibility(root, doc);
|
|
@@ -1734,6 +1841,8 @@ export async function renderSceneToGLB(
|
|
|
1734
1841
|
|
|
1735
1842
|
const io = engineIO();
|
|
1736
1843
|
ensureDefaultScene(doc);
|
|
1844
|
+
await refreshAtlasDocumentProvenance(doc);
|
|
1845
|
+
await validateAtlasDocumentProvenance(doc);
|
|
1737
1846
|
const bytes = await io.writeBinary(doc);
|
|
1738
1847
|
const artifactGlbSha256 = `sha256:${await sha256Hex(bytes)}` as const;
|
|
1739
1848
|
const boundBakedTextures = bakedTextures.map((entry) => ({
|
|
@@ -1844,7 +1953,7 @@ export interface RenderGlbOptions {
|
|
|
1844
1953
|
/** Trusted evaluator dependency; never derived from generated code. */
|
|
1845
1954
|
textureResolver?: TextureResolver;
|
|
1846
1955
|
/** Host-approved portable material maps, transported as data to evaluator workers. */
|
|
1847
|
-
materialResources?:
|
|
1956
|
+
materialResources?: MaterialLibraryPayload;
|
|
1848
1957
|
/** In-process host diagnostic sink; not serializable to evaluator workers. */
|
|
1849
1958
|
diagnosticConsole?: DiagnosticConsole;
|
|
1850
1959
|
}
|
|
@@ -1874,7 +1983,7 @@ export async function renderGLBInProcess(
|
|
|
1874
1983
|
: {}),
|
|
1875
1984
|
instance: resolveInstance(opts.instance),
|
|
1876
1985
|
};
|
|
1877
|
-
let materialLibraryDependencies:
|
|
1986
|
+
let materialLibraryDependencies: MaterialManifest[] | undefined;
|
|
1878
1987
|
if (opts.materialResources) {
|
|
1879
1988
|
const { decodeMaterialLibraryPayload, createMaterialLibraryTextureResolver } = await import(
|
|
1880
1989
|
'./material-library-node'
|
|
@@ -2029,6 +2138,7 @@ export async function optimizeGlbBytes(
|
|
|
2029
2138
|
try {
|
|
2030
2139
|
const io = engineIO();
|
|
2031
2140
|
const doc = await io.readBinary(bytes);
|
|
2141
|
+
await validateAtlasDocumentProvenance(doc);
|
|
2032
2142
|
// GPU-instancing first (dedup already ran at bake; instance → palette is the
|
|
2033
2143
|
// canonical order). Defaults OFF here — this is the web-tier re-bake seam, and
|
|
2034
2144
|
// the caller opts in per-asset (Studio passes instance:'auto' + the record's
|
|
@@ -2053,6 +2163,8 @@ export async function optimizeGlbBytes(
|
|
|
2053
2163
|
if (mode) summary = await consolidateMaterials(doc, mode);
|
|
2054
2164
|
if (!summary && !instancing) return undefined;
|
|
2055
2165
|
ensureDefaultScene(doc);
|
|
2166
|
+
await refreshAtlasDocumentProvenance(doc);
|
|
2167
|
+
await validateAtlasDocumentProvenance(doc);
|
|
2056
2168
|
const outBytes = await io.writeBinary(doc);
|
|
2057
2169
|
const gltfValidation = await assertFinalGlbValid(outBytes);
|
|
2058
2170
|
let report: InstanceabilityReport | undefined;
|
|
@@ -2157,6 +2269,7 @@ export async function snapGlbToPalette(
|
|
|
2157
2269
|
try {
|
|
2158
2270
|
const io = engineIO();
|
|
2159
2271
|
const doc = await io.readBinary(bytes);
|
|
2272
|
+
await validateAtlasDocumentProvenance(doc);
|
|
2160
2273
|
const before = collectGlbMetrics(doc);
|
|
2161
2274
|
const idx = buildSlotIndex(slots as readonly SnapSlot[]);
|
|
2162
2275
|
let snapped = 0;
|
|
@@ -2209,6 +2322,8 @@ export async function snapGlbToPalette(
|
|
|
2209
2322
|
drawsAfter: after.drawCalls,
|
|
2210
2323
|
};
|
|
2211
2324
|
ensureDefaultScene(doc);
|
|
2325
|
+
await refreshAtlasDocumentProvenance(doc);
|
|
2326
|
+
await validateAtlasDocumentProvenance(doc);
|
|
2212
2327
|
const outBytes = await io.writeBinary(doc);
|
|
2213
2328
|
const gltfValidation = await assertFinalGlbValid(outBytes);
|
|
2214
2329
|
let report: InstanceabilityReport | undefined;
|
|
@@ -2322,6 +2437,7 @@ export async function composeSceneGLB(
|
|
|
2322
2437
|
const part = parts[i]!;
|
|
2323
2438
|
try {
|
|
2324
2439
|
const src = await io.readBinary(part.bytes);
|
|
2440
|
+
await mergeAtlasDocumentProvenance(master, src);
|
|
2325
2441
|
if (opts.keepAnimations && opts.animationNaming === 'instance') {
|
|
2326
2442
|
for (const [clipIndex, animation] of src.getRoot().listAnimations().entries()) {
|
|
2327
2443
|
animation.setName(
|
|
@@ -2393,6 +2509,8 @@ export async function composeSceneGLB(
|
|
|
2393
2509
|
|
|
2394
2510
|
const metrics = collectGlbMetrics(master);
|
|
2395
2511
|
ensureDefaultScene(master);
|
|
2512
|
+
await refreshAtlasDocumentProvenance(master);
|
|
2513
|
+
await validateAtlasDocumentProvenance(master);
|
|
2396
2514
|
const bytes = await io.writeBinary(master);
|
|
2397
2515
|
const gltfValidation = await assertFinalGlbValid(bytes, 'scene.glb');
|
|
2398
2516
|
for (const issue of gltfValidation.issues.messages) {
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { AuthoringDiagnosticError } from './evaluator/authoring-diagnostic';
|
|
3
|
+
import {
|
|
4
|
+
stampSemanticMetadataV1,
|
|
5
|
+
type SemanticMetadataV1,
|
|
6
|
+
type SemanticMetadataV1Input,
|
|
7
|
+
} from './contracts';
|
|
8
|
+
import { GEOMETRY_ALLOCATION_LIMITS as limits, assertFiniteTriple } from './geometry-budget';
|
|
9
|
+
|
|
10
|
+
export interface CreateSkinnedPartOptions {
|
|
11
|
+
/** Already assembled common hierarchy containing every bone, in the rest pose. */
|
|
12
|
+
parent: THREE.Object3D;
|
|
13
|
+
/** Explicit, stable index order. Bones remain owned by the caller's hierarchy. */
|
|
14
|
+
bones: readonly THREE.Bone[];
|
|
15
|
+
/** Four indices and weights per geometry vertex, including zero-weight lanes. */
|
|
16
|
+
indices: ArrayLike<number>;
|
|
17
|
+
weights: ArrayLike<number>;
|
|
18
|
+
/** Default false: require weights to sum to one. Never invent weights for zero sums. */
|
|
19
|
+
normalizeWeights?: boolean;
|
|
20
|
+
position?: [number, number, number];
|
|
21
|
+
/** Local Euler angles in degrees, matching createPart. */
|
|
22
|
+
rotation?: [number, number, number];
|
|
23
|
+
scale?: [number, number, number];
|
|
24
|
+
semantic?: SemanticMetadataV1 | SemanticMetadataV1Input;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function topRoot(node: THREE.Object3D): THREE.Object3D {
|
|
28
|
+
while (node.parent) node = node.parent;
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertMatrix(matrix: THREE.Matrix4, label: string): void {
|
|
33
|
+
if (
|
|
34
|
+
!matrix.elements.every(
|
|
35
|
+
(value) => Number.isFinite(value) && Number.isFinite(Math.fround(value)),
|
|
36
|
+
) ||
|
|
37
|
+
!Number.isFinite(matrix.determinant()) ||
|
|
38
|
+
matrix.determinant() === 0 ||
|
|
39
|
+
!matrix
|
|
40
|
+
.clone()
|
|
41
|
+
.invert()
|
|
42
|
+
.elements.every((value) => Number.isFinite(value) && Number.isFinite(Math.fround(value)))
|
|
43
|
+
)
|
|
44
|
+
throw new RangeError(`createSkinnedPart ${label} must have finite invertible bind transforms.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Clone unchanged topology, attach explicit four-lane influences and bind at the current rest pose. */
|
|
48
|
+
export function createSkinnedPart(
|
|
49
|
+
name: string,
|
|
50
|
+
geometry: THREE.BufferGeometry,
|
|
51
|
+
material: THREE.Material | THREE.Material[],
|
|
52
|
+
options: CreateSkinnedPartOptions,
|
|
53
|
+
): THREE.SkinnedMesh {
|
|
54
|
+
try {
|
|
55
|
+
return buildSkinnedPart(name, geometry, material, options);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof TypeError || error instanceof RangeError)
|
|
58
|
+
throw new AuthoringDiagnosticError('SKIN_BINDING_ARGUMENT', error.message);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildSkinnedPart(
|
|
64
|
+
name: string,
|
|
65
|
+
geometry: THREE.BufferGeometry,
|
|
66
|
+
material: THREE.Material | THREE.Material[],
|
|
67
|
+
options: CreateSkinnedPartOptions,
|
|
68
|
+
): THREE.SkinnedMesh {
|
|
69
|
+
if (
|
|
70
|
+
typeof name !== 'string' ||
|
|
71
|
+
!name.trim() ||
|
|
72
|
+
THREE.PropertyBinding.sanitizeNodeName(name) !== name
|
|
73
|
+
)
|
|
74
|
+
throw new TypeError(
|
|
75
|
+
'createSkinnedPart name must be nonempty and free of animation binding punctuation.',
|
|
76
|
+
);
|
|
77
|
+
if (!geometry?.isBufferGeometry)
|
|
78
|
+
throw new TypeError('createSkinnedPart requires BufferGeometry.');
|
|
79
|
+
const materials = Array.isArray(material) ? material : [material];
|
|
80
|
+
if (!materials.length || materials.some((value) => !value?.isMaterial))
|
|
81
|
+
throw new TypeError('createSkinnedPart requires a material or nonempty material array.');
|
|
82
|
+
if (!options?.parent?.isObject3D)
|
|
83
|
+
throw new TypeError('createSkinnedPart requires an existing parent in the bone hierarchy.');
|
|
84
|
+
const { bones, indices, weights, parent } = options;
|
|
85
|
+
if (!Array.isArray(bones) || bones.length < 1 || bones.length > limits.skinBones)
|
|
86
|
+
throw new RangeError(`createSkinnedPart bone limit is 1..${limits.skinBones}.`);
|
|
87
|
+
if (options.normalizeWeights !== undefined && typeof options.normalizeWeights !== 'boolean')
|
|
88
|
+
throw new TypeError('createSkinnedPart normalizeWeights must be boolean.');
|
|
89
|
+
const top = topRoot(parent),
|
|
90
|
+
names = new Set<string>(),
|
|
91
|
+
members = new Set<THREE.Bone>();
|
|
92
|
+
for (const bone of bones) {
|
|
93
|
+
if (!bone?.isBone || topRoot(bone) !== top || members.has(bone))
|
|
94
|
+
throw new TypeError(
|
|
95
|
+
'createSkinnedPart bones must be distinct Bones in the parent hierarchy.',
|
|
96
|
+
);
|
|
97
|
+
if (
|
|
98
|
+
!bone.name ||
|
|
99
|
+
THREE.PropertyBinding.sanitizeNodeName(bone.name) !== bone.name ||
|
|
100
|
+
names.has(bone.name)
|
|
101
|
+
)
|
|
102
|
+
throw new TypeError('createSkinnedPart bones need unique animation-safe names.');
|
|
103
|
+
members.add(bone);
|
|
104
|
+
names.add(bone.name);
|
|
105
|
+
}
|
|
106
|
+
// Collision with any other node makes name-based animation ambiguous.
|
|
107
|
+
top.traverse((node) => {
|
|
108
|
+
if (names.has(node.name) && !members.has(node as THREE.Bone))
|
|
109
|
+
throw new TypeError(`createSkinnedPart ambiguous bone name: ${node.name}.`);
|
|
110
|
+
});
|
|
111
|
+
const position = geometry.getAttribute('position'),
|
|
112
|
+
count = position?.count;
|
|
113
|
+
if (
|
|
114
|
+
position?.itemSize !== 3 ||
|
|
115
|
+
!Number.isSafeInteger(count) ||
|
|
116
|
+
count! < 1 ||
|
|
117
|
+
count! > limits.skinVertices
|
|
118
|
+
)
|
|
119
|
+
throw new RangeError(
|
|
120
|
+
`createSkinnedPart position vertex limit is 1..${limits.skinVertices} xyz vertices.`,
|
|
121
|
+
);
|
|
122
|
+
if (geometry.hasAttribute('skinIndex') || geometry.hasAttribute('skinWeight'))
|
|
123
|
+
throw new TypeError(
|
|
124
|
+
'createSkinnedPart geometry already contains binding attributes; provide unbound geometry.',
|
|
125
|
+
);
|
|
126
|
+
let bytes = count! * 4 * (2 + 4);
|
|
127
|
+
const attributes = [
|
|
128
|
+
geometry.getIndex(),
|
|
129
|
+
...Object.values(geometry.attributes),
|
|
130
|
+
...Object.values(geometry.morphAttributes).flat(),
|
|
131
|
+
];
|
|
132
|
+
for (const attribute of attributes) {
|
|
133
|
+
if (!attribute) continue;
|
|
134
|
+
bytes += attribute.array.byteLength;
|
|
135
|
+
if (!Number.isSafeInteger(bytes) || bytes > limits.skinAttributeBytes)
|
|
136
|
+
throw new RangeError(
|
|
137
|
+
`createSkinnedPart attribute allocation limit is ${limits.skinAttributeBytes} bytes.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (!indices || !weights || indices.length !== count! * 4 || weights.length !== count! * 4)
|
|
141
|
+
throw new RangeError('createSkinnedPart requires exactly four indices and weights per vertex.');
|
|
142
|
+
// First pass validates all input before cloning or allocating output attributes.
|
|
143
|
+
for (let vertex = 0; vertex < count!; vertex++) {
|
|
144
|
+
let sum = 0;
|
|
145
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
146
|
+
const i = vertex * 4 + lane,
|
|
147
|
+
index = indices[i]!,
|
|
148
|
+
weight = weights[i]!;
|
|
149
|
+
if (!Number.isInteger(index) || index < 0 || index >= bones.length)
|
|
150
|
+
throw new RangeError(
|
|
151
|
+
`createSkinnedPart vertex ${vertex} lane ${lane} has an invalid bone index.`,
|
|
152
|
+
);
|
|
153
|
+
if (!Number.isFinite(weight) || weight < 0)
|
|
154
|
+
throw new RangeError(
|
|
155
|
+
`createSkinnedPart vertex ${vertex} lane ${lane} needs a finite nonnegative weight.`,
|
|
156
|
+
);
|
|
157
|
+
if (!options.normalizeWeights && weight > 1)
|
|
158
|
+
throw new RangeError(
|
|
159
|
+
`createSkinnedPart vertex ${vertex} weights must be <=1 and sum to one; request normalizeWeights explicitly.`,
|
|
160
|
+
);
|
|
161
|
+
sum += weight;
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
!Number.isFinite(sum) ||
|
|
165
|
+
sum <= 0 ||
|
|
166
|
+
(!options.normalizeWeights && Math.abs(sum - 1) > 1e-5)
|
|
167
|
+
)
|
|
168
|
+
throw new RangeError(
|
|
169
|
+
`createSkinnedPart vertex ${vertex} has an invalid weight sum; require one or explicitly normalize a positive finite sum.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
for (const key of ['position', 'rotation', 'scale'] as const)
|
|
173
|
+
if (options[key] !== undefined) assertFiniteTriple(`createSkinnedPart ${key}`, options[key]);
|
|
174
|
+
const local = new THREE.Matrix4().compose(
|
|
175
|
+
new THREE.Vector3(...(options.position ?? [0, 0, 0])),
|
|
176
|
+
new THREE.Quaternion().setFromEuler(
|
|
177
|
+
new THREE.Euler(
|
|
178
|
+
...((options.rotation ?? [0, 0, 0]).map(THREE.MathUtils.degToRad) as [
|
|
179
|
+
number,
|
|
180
|
+
number,
|
|
181
|
+
number,
|
|
182
|
+
]),
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
new THREE.Vector3(...(options.scale ?? [1, 1, 1])),
|
|
186
|
+
);
|
|
187
|
+
top.updateWorldMatrix(true, true);
|
|
188
|
+
const bind = new THREE.Matrix4().multiplyMatrices(parent.matrixWorld, local);
|
|
189
|
+
assertMatrix(bind, 'mesh');
|
|
190
|
+
for (const bone of bones) {
|
|
191
|
+
assertMatrix(bone.matrixWorld, `bone ${bone.name}`);
|
|
192
|
+
}
|
|
193
|
+
const skinIndices = new Uint16Array(indices.length),
|
|
194
|
+
skinWeights = new Float32Array(weights.length);
|
|
195
|
+
for (let vertex = 0; vertex < count!; vertex++) {
|
|
196
|
+
const start = vertex * 4;
|
|
197
|
+
const sum = options.normalizeWeights
|
|
198
|
+
? weights[start]! + weights[start + 1]! + weights[start + 2]! + weights[start + 3]!
|
|
199
|
+
: 1;
|
|
200
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
201
|
+
skinIndices[start + lane] = indices[start + lane]!;
|
|
202
|
+
skinWeights[start + lane] = weights[start + lane]! / sum;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const owned = geometry.clone();
|
|
206
|
+
owned.setAttribute('skinIndex', new THREE.BufferAttribute(skinIndices, 4));
|
|
207
|
+
owned.setAttribute('skinWeight', new THREE.BufferAttribute(skinWeights, 4));
|
|
208
|
+
const mesh = new THREE.SkinnedMesh(owned, Array.isArray(material) ? [...material] : material);
|
|
209
|
+
mesh.name = `Mesh_${name}`;
|
|
210
|
+
if (options.position) mesh.position.fromArray(options.position);
|
|
211
|
+
if (options.rotation)
|
|
212
|
+
mesh.rotation.set(
|
|
213
|
+
...(options.rotation.map(THREE.MathUtils.degToRad) as [number, number, number]),
|
|
214
|
+
);
|
|
215
|
+
if (options.scale) mesh.scale.fromArray(options.scale);
|
|
216
|
+
if (options.semantic) stampSemanticMetadataV1(mesh, options.semantic);
|
|
217
|
+
const skeleton = new THREE.Skeleton(
|
|
218
|
+
[...bones],
|
|
219
|
+
bones.map((bone) => bone.matrixWorld.clone().invert()),
|
|
220
|
+
);
|
|
221
|
+
mesh.bind(skeleton, bind);
|
|
222
|
+
parent.add(mesh);
|
|
223
|
+
mesh.updateWorldMatrix(true, false);
|
|
224
|
+
return mesh;
|
|
225
|
+
}
|