@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/material-library.ts
CHANGED
|
@@ -4,6 +4,21 @@ import {
|
|
|
4
4
|
canonicalizeProceduralTextureSpecV2,
|
|
5
5
|
type CanonicalPortableMaterialSpecV2,
|
|
6
6
|
} from './procedural-material-v2';
|
|
7
|
+
import {
|
|
8
|
+
canonicalizeProceduralTextureSpecV3,
|
|
9
|
+
PROCEDURAL_TEXTURE_V3_COMPILER_ID,
|
|
10
|
+
} from './procedural-texture-v3-contract';
|
|
11
|
+
import { canonicalizePortableSamplerV1 } from './portable-sampler';
|
|
12
|
+
import type { CanonicalPortableMaterialSpecV3 } from './procedural-material-v3';
|
|
13
|
+
import {
|
|
14
|
+
canonicalizeTextureAtlasSpecV1,
|
|
15
|
+
validateTextureAtlasManifestV1,
|
|
16
|
+
TEXTURE_ATLAS_COMPILER_ID,
|
|
17
|
+
type CanonicalTextureAtlasSpecV1,
|
|
18
|
+
} from './texture-atlas-contract';
|
|
19
|
+
import type { TextureAtlasManifestV1 } from './texture-atlas-pixels';
|
|
20
|
+
import type { MaterialMapPinV1 } from './pinned-material-map';
|
|
21
|
+
import { makeMaterialAuthoringSchemas } from './material-authoring-schema';
|
|
7
22
|
|
|
8
23
|
export const MATERIAL_LIBRARY_LIMITS = Object.freeze({
|
|
9
24
|
maxMapBytes: 8 * 1024 * 1024,
|
|
@@ -301,9 +316,14 @@ export interface MaterialLibraryPayloadV1 {
|
|
|
301
316
|
records: { manifest: MaterialManifestV1; files: Record<string, string> }[];
|
|
302
317
|
}
|
|
303
318
|
export interface MaterialLibrary {
|
|
304
|
-
list(): Promise<
|
|
305
|
-
read(materialId: string, revisionId: string): Promise<
|
|
306
|
-
|
|
319
|
+
list(): Promise<MaterialManifest[]>;
|
|
320
|
+
read(materialId: string, revisionId: string): Promise<MaterialRecord>;
|
|
321
|
+
readManifest?(materialId: string, revisionId: string): Promise<MaterialManifest>;
|
|
322
|
+
readRecordFiles?(materialId: string, revisionId: string): Promise<MaterialRecord>;
|
|
323
|
+
import(
|
|
324
|
+
records: MaterialRecord[],
|
|
325
|
+
options?: { materials?: readonly MaterialRecord[] },
|
|
326
|
+
): Promise<MaterialManifest[]>;
|
|
307
327
|
}
|
|
308
328
|
export const materialLibraryPayloadSchema = z
|
|
309
329
|
.object({
|
|
@@ -455,7 +475,7 @@ export function validateMaterialRecordShape(record: MaterialRecordV1): MaterialM
|
|
|
455
475
|
return manifest;
|
|
456
476
|
}
|
|
457
477
|
export function materialLibraryResourceId(
|
|
458
|
-
manifest:
|
|
478
|
+
manifest: MaterialManifest,
|
|
459
479
|
slot: MaterialLibrarySlot,
|
|
460
480
|
): string {
|
|
461
481
|
materialLibraryHashSchema.parse(manifest.revisionId);
|
|
@@ -464,7 +484,35 @@ export function materialLibraryResourceId(
|
|
|
464
484
|
}
|
|
465
485
|
export function materialLibraryPortableSpec(
|
|
466
486
|
manifest: MaterialManifestV1,
|
|
467
|
-
): CanonicalPortableMaterialSpecV2
|
|
487
|
+
): CanonicalPortableMaterialSpecV2;
|
|
488
|
+
export function materialLibraryPortableSpec(
|
|
489
|
+
manifest: MaterialManifestV2,
|
|
490
|
+
): CanonicalPortableMaterialSpecV3;
|
|
491
|
+
export function materialLibraryPortableSpec(
|
|
492
|
+
manifest: MaterialManifest,
|
|
493
|
+
): CanonicalPortableMaterialSpecV2 | CanonicalPortableMaterialSpecV3;
|
|
494
|
+
export function materialLibraryPortableSpec(
|
|
495
|
+
manifest: MaterialManifest,
|
|
496
|
+
): CanonicalPortableMaterialSpecV2 | CanonicalPortableMaterialSpecV3 {
|
|
497
|
+
if (manifest.schemaVersion === 2) {
|
|
498
|
+
const modern = validateMaterialManifestAny(manifest) as MaterialManifestV2;
|
|
499
|
+
return {
|
|
500
|
+
schemaVersion: 3,
|
|
501
|
+
model: 'pbrMetallicRoughness',
|
|
502
|
+
name: modern.name,
|
|
503
|
+
...modern.parameters,
|
|
504
|
+
textures: Object.fromEntries(
|
|
505
|
+
modern.maps.map((map) => [
|
|
506
|
+
map.slot,
|
|
507
|
+
{
|
|
508
|
+
kind: 'resource' as const,
|
|
509
|
+
resourceId: materialLibraryResourceId(modern, map.slot),
|
|
510
|
+
sampler: { ...map.sampler },
|
|
511
|
+
},
|
|
512
|
+
]),
|
|
513
|
+
),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
468
516
|
manifest = validateMaterialManifest(manifest);
|
|
469
517
|
return {
|
|
470
518
|
schemaVersion: 2,
|
|
@@ -479,3 +527,298 @@ export function materialLibraryPortableSpec(
|
|
|
479
527
|
),
|
|
480
528
|
};
|
|
481
529
|
}
|
|
530
|
+
|
|
531
|
+
/** New versions are explicit; every exported V1 schema/type above stays V1. */
|
|
532
|
+
const authoringSchemas = makeMaterialAuthoringSchemas(materialProceduralSpecSchema);
|
|
533
|
+
const samplerSchema = authoringSchemas.portableSamplerSchema.refine((value) => {
|
|
534
|
+
try {
|
|
535
|
+
return (
|
|
536
|
+
canonicalMaterialJson(value) === canonicalMaterialJson(canonicalizePortableSamplerV1(value))
|
|
537
|
+
);
|
|
538
|
+
} catch {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
const v3SpecSchema = authoringSchemas.canonicalProceduralTextureSpecV3Schema.refine((value) => {
|
|
543
|
+
try {
|
|
544
|
+
return (
|
|
545
|
+
canonicalMaterialJson(value) ===
|
|
546
|
+
canonicalMaterialJson(canonicalizeProceduralTextureSpecV3(value))
|
|
547
|
+
);
|
|
548
|
+
} catch {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
const atlasSpecSchema: z.ZodType<CanonicalTextureAtlasSpecV1> =
|
|
553
|
+
authoringSchemas.canonicalTextureAtlasSpecV1Schema.refine((value) => {
|
|
554
|
+
try {
|
|
555
|
+
return (
|
|
556
|
+
canonicalMaterialJson(value) ===
|
|
557
|
+
canonicalMaterialJson(canonicalizeTextureAtlasSpecV1(value))
|
|
558
|
+
);
|
|
559
|
+
} catch {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
const atlasManifestSchema: z.ZodType<TextureAtlasManifestV1> =
|
|
564
|
+
authoringSchemas.textureAtlasManifestV1Schema;
|
|
565
|
+
export const materialMapPinSchema = z
|
|
566
|
+
.object({
|
|
567
|
+
resourceId: materialLibraryIdSchema,
|
|
568
|
+
revisionId: materialLibraryHashSchema,
|
|
569
|
+
sha256: materialLibraryHashSchema,
|
|
570
|
+
})
|
|
571
|
+
.strict()
|
|
572
|
+
.refine((pin) => pin.sha256 === pin.revisionId, 'Material pin hash must match revision');
|
|
573
|
+
const materialRecipeV2Schema = z.discriminatedUnion('kind', [
|
|
574
|
+
z
|
|
575
|
+
.object({
|
|
576
|
+
kind: z.literal('procedural'),
|
|
577
|
+
compilerId: z.enum(['kiln.procedural-texture.v2', PROCEDURAL_TEXTURE_V3_COMPILER_ID]),
|
|
578
|
+
spec: z.union([materialProceduralSpecSchema, v3SpecSchema]),
|
|
579
|
+
recipeHash: materialLibraryHashSchema,
|
|
580
|
+
pixelHash: materialLibraryHashSchema,
|
|
581
|
+
encoder: z.object({ name: z.literal('sharp'), version: text }).strict(),
|
|
582
|
+
derive: materialNormalDerivationSchema.optional(),
|
|
583
|
+
})
|
|
584
|
+
.strict(),
|
|
585
|
+
z
|
|
586
|
+
.object({
|
|
587
|
+
kind: z.literal('atlas'),
|
|
588
|
+
compilerId: z.literal(TEXTURE_ATLAS_COMPILER_ID),
|
|
589
|
+
spec: atlasSpecSchema,
|
|
590
|
+
manifest: atlasManifestSchema,
|
|
591
|
+
encoder: z.object({ name: z.literal('sharp'), version: text }).strict(),
|
|
592
|
+
})
|
|
593
|
+
.strict()
|
|
594
|
+
.superRefine((recipe, context) => {
|
|
595
|
+
try {
|
|
596
|
+
validateTextureAtlasManifestV1(recipe.manifest, recipe.spec);
|
|
597
|
+
} catch {
|
|
598
|
+
context.addIssue({ code: 'custom', message: 'Invalid stored atlas manifest' });
|
|
599
|
+
}
|
|
600
|
+
}),
|
|
601
|
+
]);
|
|
602
|
+
export const materialManifestV2Schema = materialManifestSchema
|
|
603
|
+
.omit({ schemaVersion: true, maps: true })
|
|
604
|
+
.extend({
|
|
605
|
+
schemaVersion: z.literal(2),
|
|
606
|
+
dependencies: z.array(materialMapPinSchema).max(16),
|
|
607
|
+
maps: z
|
|
608
|
+
.array(
|
|
609
|
+
materialManifestSchema.shape.maps.element
|
|
610
|
+
.omit({ procedural: true })
|
|
611
|
+
.extend({
|
|
612
|
+
sampler: samplerSchema,
|
|
613
|
+
pixelHash: materialLibraryHashSchema,
|
|
614
|
+
recipe: materialRecipeV2Schema.optional(),
|
|
615
|
+
})
|
|
616
|
+
.strict(),
|
|
617
|
+
)
|
|
618
|
+
.min(1)
|
|
619
|
+
.max(5),
|
|
620
|
+
})
|
|
621
|
+
.strict();
|
|
622
|
+
export type MaterialManifestV2 = z.infer<typeof materialManifestV2Schema>;
|
|
623
|
+
export type MaterialMapV2 = MaterialManifestV2['maps'][number];
|
|
624
|
+
export interface MaterialRecordV2 {
|
|
625
|
+
manifest: MaterialManifestV2;
|
|
626
|
+
files: Record<string, Uint8Array>;
|
|
627
|
+
}
|
|
628
|
+
export type MaterialManifest = MaterialManifestV1 | MaterialManifestV2;
|
|
629
|
+
export type MaterialRecord = MaterialRecordV1 | MaterialRecordV2;
|
|
630
|
+
export interface MaterialLibraryPayloadV2 {
|
|
631
|
+
schemaVersion: 2;
|
|
632
|
+
records: { manifest: MaterialManifest; files: Record<string, string> }[];
|
|
633
|
+
}
|
|
634
|
+
export type MaterialLibraryPayload = MaterialLibraryPayloadV1 | MaterialLibraryPayloadV2;
|
|
635
|
+
export const materialManifestAnySchema = z.discriminatedUnion('schemaVersion', [
|
|
636
|
+
materialManifestSchema,
|
|
637
|
+
materialManifestV2Schema,
|
|
638
|
+
]);
|
|
639
|
+
export const materialLibraryPayloadV2Schema = materialLibraryPayloadSchema
|
|
640
|
+
.extend({
|
|
641
|
+
schemaVersion: z.literal(2),
|
|
642
|
+
records: z
|
|
643
|
+
.array(
|
|
644
|
+
materialLibraryPayloadSchema.shape.records.element
|
|
645
|
+
.extend({ manifest: materialManifestAnySchema })
|
|
646
|
+
.strict(),
|
|
647
|
+
)
|
|
648
|
+
.max(MATERIAL_LIBRARY_LIMITS.maxPayloadRecords),
|
|
649
|
+
})
|
|
650
|
+
.strict();
|
|
651
|
+
export const materialLibraryPayloadAnySchema = z.discriminatedUnion('schemaVersion', [
|
|
652
|
+
materialLibraryPayloadSchema,
|
|
653
|
+
materialLibraryPayloadV2Schema,
|
|
654
|
+
]);
|
|
655
|
+
export function materialRecordDependencyPins(manifest: MaterialManifest): MaterialMapPinV1[] {
|
|
656
|
+
return manifest.schemaVersion === 1 ? [] : manifest.dependencies.map((pin) => ({ ...pin }));
|
|
657
|
+
}
|
|
658
|
+
/** Opt-in JSON-only standalone creation; external bytes remain host-only. */
|
|
659
|
+
export const proceduralMaterialDraftV2Schema = proceduralMaterialDraftSchema
|
|
660
|
+
.extend({
|
|
661
|
+
schemaVersion: z.literal(2),
|
|
662
|
+
maps: z
|
|
663
|
+
.array(
|
|
664
|
+
proceduralMaterialDraftSchema.shape.maps.element
|
|
665
|
+
.omit({ procedural: true })
|
|
666
|
+
.extend({
|
|
667
|
+
sampler: samplerSchema,
|
|
668
|
+
procedural: z
|
|
669
|
+
.union([
|
|
670
|
+
materialProceduralSpecSchema,
|
|
671
|
+
authoringSchemas.proceduralTextureSpecV3Schema.refine((value) => {
|
|
672
|
+
try {
|
|
673
|
+
canonicalizeProceduralTextureSpecV3(value);
|
|
674
|
+
return true;
|
|
675
|
+
} catch {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}),
|
|
679
|
+
])
|
|
680
|
+
.optional(),
|
|
681
|
+
atlas: authoringSchemas.textureAtlasSpecV1Schema
|
|
682
|
+
.refine((value) => {
|
|
683
|
+
try {
|
|
684
|
+
canonicalizeTextureAtlasSpecV1(value);
|
|
685
|
+
return true;
|
|
686
|
+
} catch {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
})
|
|
690
|
+
.optional(),
|
|
691
|
+
})
|
|
692
|
+
.strict()
|
|
693
|
+
.refine(
|
|
694
|
+
(map) => (map.procedural !== undefined) !== (map.atlas !== undefined),
|
|
695
|
+
'Supply exactly one procedural or atlas recipe',
|
|
696
|
+
),
|
|
697
|
+
)
|
|
698
|
+
.min(1)
|
|
699
|
+
.max(5),
|
|
700
|
+
})
|
|
701
|
+
.strict();
|
|
702
|
+
export type ProceduralMaterialDraftV2 = z.infer<typeof proceduralMaterialDraftV2Schema>;
|
|
703
|
+
export const proceduralMaterialDraftAnySchema = z.union([
|
|
704
|
+
proceduralMaterialDraftSchema,
|
|
705
|
+
proceduralMaterialDraftV2Schema,
|
|
706
|
+
]);
|
|
707
|
+
export function validateMaterialManifestAny(value: unknown): MaterialManifest {
|
|
708
|
+
assertMaterialJson(value);
|
|
709
|
+
if (
|
|
710
|
+
new TextEncoder().encode(JSON.stringify(value)).length >
|
|
711
|
+
MATERIAL_LIBRARY_LIMITS.maxManifestBytes
|
|
712
|
+
)
|
|
713
|
+
throw Error('Material manifest exceeds size limit');
|
|
714
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
715
|
+
throw Error('Invalid material manifest');
|
|
716
|
+
if ((value as Record<string, unknown>)['schemaVersion'] === 1)
|
|
717
|
+
return validateMaterialManifest(value);
|
|
718
|
+
const manifest = materialManifestV2Schema.parse(value);
|
|
719
|
+
if (
|
|
720
|
+
new Set(manifest.sources.map((s) => s.id)).size !== manifest.sources.length ||
|
|
721
|
+
new Set(manifest.maps.map((m) => m.slot)).size !== manifest.maps.length
|
|
722
|
+
)
|
|
723
|
+
throw Error('Duplicate material source or slot');
|
|
724
|
+
let bytes = 0,
|
|
725
|
+
pixels = 0;
|
|
726
|
+
const pins = new Map<string, MaterialMapPinV1>();
|
|
727
|
+
for (const map of manifest.maps) {
|
|
728
|
+
const source = manifest.sources.find((s) => s.id === map.sourceId);
|
|
729
|
+
if (
|
|
730
|
+
!source ||
|
|
731
|
+
map.file !== `${map.slot}.png` ||
|
|
732
|
+
map.usage !== MATERIAL_LIBRARY_USAGE[map.slot] ||
|
|
733
|
+
map.colorSpace !==
|
|
734
|
+
(map.slot === 'baseColor' || map.slot === 'emissive' ? 'srgb' : 'linear') ||
|
|
735
|
+
(map.slot === 'normal') !== (map.normalConvention === 'opengl') ||
|
|
736
|
+
(map.slot === 'metallicRoughness') !==
|
|
737
|
+
(map.channelPacking === 'r-occlusion-g-roughness-b-metallic')
|
|
738
|
+
)
|
|
739
|
+
throw Error('Material map source/usage/convention mismatch');
|
|
740
|
+
const recipe = map.recipe;
|
|
741
|
+
if (recipe) {
|
|
742
|
+
if (source.kind !== 'procedural' || map.originalFile)
|
|
743
|
+
throw Error('Procedural/atlas map source convention mismatch');
|
|
744
|
+
if (recipe.kind === 'procedural') {
|
|
745
|
+
const spec = recipe.spec;
|
|
746
|
+
if (spec.schemaVersion === 3) {
|
|
747
|
+
if (
|
|
748
|
+
recipe.compilerId !== PROCEDURAL_TEXTURE_V3_COMPILER_ID ||
|
|
749
|
+
recipe.derive ||
|
|
750
|
+
spec.width !== map.width ||
|
|
751
|
+
spec.height !== map.height
|
|
752
|
+
)
|
|
753
|
+
throw Error('V3 compiler/dimensions/derivation mismatch');
|
|
754
|
+
} else if (
|
|
755
|
+
recipe.compilerId !== 'kiln.procedural-texture.v2' ||
|
|
756
|
+
spec.size !== map.width ||
|
|
757
|
+
map.width !== map.height ||
|
|
758
|
+
canonicalMaterialJson(spec) !==
|
|
759
|
+
canonicalMaterialJson(canonicalizeProceduralTextureSpecV2(spec))
|
|
760
|
+
)
|
|
761
|
+
throw Error('V2 compiler/canonical recipe dimensions mismatch');
|
|
762
|
+
if (
|
|
763
|
+
spec.usage !== map.usage ||
|
|
764
|
+
recipe.pixelHash !== map.pixelHash ||
|
|
765
|
+
(recipe.derive && map.slot !== 'normal')
|
|
766
|
+
)
|
|
767
|
+
throw Error('Procedural recipe pixel/usage mismatch');
|
|
768
|
+
} else {
|
|
769
|
+
validateTextureAtlasManifestV1(recipe.manifest, recipe.spec);
|
|
770
|
+
if (
|
|
771
|
+
recipe.spec.width !== map.width ||
|
|
772
|
+
recipe.spec.height !== map.height ||
|
|
773
|
+
recipe.spec.usage !== map.usage ||
|
|
774
|
+
canonicalMaterialJson(recipe.spec.sampler) !== canonicalMaterialJson(map.sampler) ||
|
|
775
|
+
recipe.manifest.decodedPixelHash !== map.pixelHash
|
|
776
|
+
)
|
|
777
|
+
throw Error('Atlas recipe map mismatch');
|
|
778
|
+
for (const region of recipe.spec.regions)
|
|
779
|
+
if (region.source.kind === 'resource') {
|
|
780
|
+
const pin = region.source.pin;
|
|
781
|
+
pins.set(`${pin.resourceId}/${pin.revisionId}`, pin);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} else if (
|
|
785
|
+
source.kind !== 'external' ||
|
|
786
|
+
!source.originalFiles.some((f) => f.name === map.originalFile)
|
|
787
|
+
)
|
|
788
|
+
throw Error('External material source original file missing');
|
|
789
|
+
bytes += map.bytes;
|
|
790
|
+
pixels += map.width * map.height;
|
|
791
|
+
}
|
|
792
|
+
const actual = manifest.dependencies.map((p) => `${p.resourceId}/${p.revisionId}`);
|
|
793
|
+
if (
|
|
794
|
+
new Set(actual).size !== actual.length ||
|
|
795
|
+
actual.length !== pins.size ||
|
|
796
|
+
actual.some((k) => !pins.has(k))
|
|
797
|
+
)
|
|
798
|
+
throw Error('Material dependency pin inventory mismatch');
|
|
799
|
+
if (
|
|
800
|
+
bytes > MATERIAL_LIBRARY_LIMITS.maxRecordBytes ||
|
|
801
|
+
pixels > MATERIAL_LIBRARY_LIMITS.maxRecordPixels
|
|
802
|
+
)
|
|
803
|
+
throw Error('Material record exceeds byte/pixel budget');
|
|
804
|
+
return structuredClone(manifest);
|
|
805
|
+
}
|
|
806
|
+
export function validateMaterialRecordShapeAny(record: MaterialRecord): MaterialManifest {
|
|
807
|
+
assertMaterialJson(record, true);
|
|
808
|
+
if (record.manifest.schemaVersion === 1)
|
|
809
|
+
return validateMaterialRecordShape(record as MaterialRecordV1);
|
|
810
|
+
if (Object.keys(record).some((k) => k !== 'manifest' && k !== 'files'))
|
|
811
|
+
throw Error('Invalid material record');
|
|
812
|
+
const manifest = validateMaterialManifestAny(record.manifest);
|
|
813
|
+
if (!record.files || Object.keys(record.files).length !== manifest.maps.length)
|
|
814
|
+
throw Error('Material file inventory mismatch');
|
|
815
|
+
for (const map of manifest.maps) {
|
|
816
|
+
const bytes = record.files[map.file];
|
|
817
|
+
if (!(bytes instanceof Uint8Array) || bytes.length !== map.bytes)
|
|
818
|
+
throw Error('Material file inventory mismatch');
|
|
819
|
+
const size = materialPngDimensions(bytes);
|
|
820
|
+
if (size.width !== map.width || size.height !== map.height)
|
|
821
|
+
throw Error('Material PNG dimensions mismatch');
|
|
822
|
+
}
|
|
823
|
+
return manifest;
|
|
824
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Host-only atlas map capability. Authored code receives neither this port nor bytes. */
|
|
2
|
+
import type { TextureUsage } from './texture-contract';
|
|
3
|
+
export type MaterialMapSlotV1 =
|
|
4
|
+
| 'baseColor'
|
|
5
|
+
| 'normal'
|
|
6
|
+
| 'metallicRoughness'
|
|
7
|
+
| 'emissive'
|
|
8
|
+
| 'occlusion';
|
|
9
|
+
export type MaterialHashV1 = `sha256:${string}`;
|
|
10
|
+
export interface MaterialMapPinV1 {
|
|
11
|
+
resourceId: string;
|
|
12
|
+
revisionId: string;
|
|
13
|
+
sha256: string;
|
|
14
|
+
}
|
|
15
|
+
export interface VerifiedPinnedMaterialMapDescriptorV1 {
|
|
16
|
+
materialId: string;
|
|
17
|
+
revisionId: MaterialHashV1;
|
|
18
|
+
resourceId: string;
|
|
19
|
+
slot: MaterialMapSlotV1;
|
|
20
|
+
usage: TextureUsage;
|
|
21
|
+
colorSpace: 'srgb' | 'linear';
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
encodedByteLength: number;
|
|
25
|
+
imageSha256: MaterialHashV1;
|
|
26
|
+
decodedPixelHash?: MaterialHashV1;
|
|
27
|
+
}
|
|
28
|
+
export interface OwnedMaterialMapPixelsV1 {
|
|
29
|
+
descriptor: VerifiedPinnedMaterialMapDescriptorV1;
|
|
30
|
+
pixels: Uint8Array;
|
|
31
|
+
decodedPixelHash: MaterialHashV1;
|
|
32
|
+
}
|
|
33
|
+
export interface PinnedMaterialMapPortV1 {
|
|
34
|
+
describePinnedMaterialMap(
|
|
35
|
+
mapResourceId: unknown,
|
|
36
|
+
pin: MaterialMapPinV1,
|
|
37
|
+
): VerifiedPinnedMaterialMapDescriptorV1 | undefined;
|
|
38
|
+
resolvePinnedMaterialMapPixels(
|
|
39
|
+
mapResourceId: unknown,
|
|
40
|
+
pin: MaterialMapPinV1,
|
|
41
|
+
context?: { signal?: AbortSignal },
|
|
42
|
+
): Promise<OwnedMaterialMapPixelsV1>;
|
|
43
|
+
}
|
|
@@ -12,6 +12,16 @@ import {
|
|
|
12
12
|
} from './procedural-material-v2';
|
|
13
13
|
import { proceduralTexture } from './procedural-texture';
|
|
14
14
|
import { pbrMaterial, type PbrMaterialOptions, type TextureUsage } from './textures';
|
|
15
|
+
import { loadTexture } from './textures';
|
|
16
|
+
import {
|
|
17
|
+
admitPortableMaterialSpecV3,
|
|
18
|
+
PORTABLE_MATERIAL_V3_LIMITS,
|
|
19
|
+
PORTABLE_MATERIAL_SLOT_USAGE,
|
|
20
|
+
PortableMaterialV3Error,
|
|
21
|
+
type CanonicalPortableMaterialSpecV3,
|
|
22
|
+
} from './procedural-material-v3';
|
|
23
|
+
import { applyPortableSamplerV1 } from './portable-sampler-runtime';
|
|
24
|
+
import type { PreparedPortableTextureV3 } from './texture-resolver';
|
|
15
25
|
|
|
16
26
|
const SLOT_USAGE = {
|
|
17
27
|
baseColor: 'albedo',
|
|
@@ -107,3 +117,193 @@ export async function compilePortableMaterialSpecV2(
|
|
|
107
117
|
spec satisfies CanonicalPortableMaterialSpecV2;
|
|
108
118
|
return material;
|
|
109
119
|
}
|
|
120
|
+
|
|
121
|
+
/** New V3 path owns every mutable texture/source/pixel/encoded-byte binding. */
|
|
122
|
+
export async function compilePortableMaterialSpecV3(
|
|
123
|
+
input: unknown,
|
|
124
|
+
options: { resolver?: TextureResolver; signal?: AbortSignal } = {},
|
|
125
|
+
): Promise<THREE.MeshStandardMaterial> {
|
|
126
|
+
const owned = new Map<PortableSlot, THREE.Texture>();
|
|
127
|
+
try {
|
|
128
|
+
const admission = admitPortableMaterialSpecV3(input),
|
|
129
|
+
spec = admission.spec;
|
|
130
|
+
const resolver = options.resolver ?? DEFAULT_TEXTURE_RESOLVER;
|
|
131
|
+
const entries = Object.entries(spec.textures) as Array<
|
|
132
|
+
[PortableSlot, NonNullable<CanonicalPortableMaterialSpecV3['textures'][PortableSlot]>]
|
|
133
|
+
>;
|
|
134
|
+
// All slots are checked before even bounded encoded-byte preparation.
|
|
135
|
+
for (const [slot, ref] of entries)
|
|
136
|
+
if (ref.kind === 'resource') {
|
|
137
|
+
const descriptor = resolver.describeApprovedTexture?.(ref.resourceId);
|
|
138
|
+
if (
|
|
139
|
+
!descriptor?.allowedSlots.includes(slot) ||
|
|
140
|
+
descriptor.usage !== PORTABLE_MATERIAL_SLOT_USAGE[slot]
|
|
141
|
+
)
|
|
142
|
+
throw new ProceduralTextureError(
|
|
143
|
+
`${slot} requires an injected resource approved for its exact slot and usage.`,
|
|
144
|
+
);
|
|
145
|
+
if (!resolver.preparePortableTexture)
|
|
146
|
+
throw new ProceduralTextureError(
|
|
147
|
+
'V3 needs a verified-byte preparation resolver; keep V2 for a legacy resolver.',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const prepared = new Map<PortableSlot, PreparedPortableTextureV3>();
|
|
151
|
+
let encoded = 0,
|
|
152
|
+
texels = admission.texels,
|
|
153
|
+
resourcePixels = 0;
|
|
154
|
+
for (const [slot, ref] of entries)
|
|
155
|
+
if (ref.kind === 'resource') {
|
|
156
|
+
options.signal?.throwIfAborted();
|
|
157
|
+
const raw = await resolver.preparePortableTexture!(ref.resourceId, {
|
|
158
|
+
signal: options.signal,
|
|
159
|
+
});
|
|
160
|
+
const d = structuredClone(raw.descriptor);
|
|
161
|
+
if (
|
|
162
|
+
!(raw.bytes instanceof Uint8Array) ||
|
|
163
|
+
raw.bytes.length < 24 ||
|
|
164
|
+
raw.bytes.length > 8 * 1024 * 1024 ||
|
|
165
|
+
d.encodedByteLength !== raw.bytes.length ||
|
|
166
|
+
d.resourceId !== ref.resourceId ||
|
|
167
|
+
d.usage !== PORTABLE_MATERIAL_SLOT_USAGE[slot] ||
|
|
168
|
+
!d.allowedSlots.includes(slot) ||
|
|
169
|
+
d.colorSpace !== (slot === 'baseColor' || slot === 'emissive' ? 'srgb' : 'linear') ||
|
|
170
|
+
!Number.isInteger(d.width) ||
|
|
171
|
+
!Number.isInteger(d.height) ||
|
|
172
|
+
d.width < 1 ||
|
|
173
|
+
d.height < 1 ||
|
|
174
|
+
d.width > 4096 ||
|
|
175
|
+
d.height > 4096 ||
|
|
176
|
+
!/^sha256:[a-f0-9]{64}$/.test(d.imageSha256)
|
|
177
|
+
)
|
|
178
|
+
throw new ProceduralTextureError('Prepared texture descriptor/bytes are invalid.');
|
|
179
|
+
const pixels = d.width * d.height;
|
|
180
|
+
texels += pixels;
|
|
181
|
+
resourcePixels += pixels;
|
|
182
|
+
encoded += raw.bytes.length;
|
|
183
|
+
// Decoded RGBA write/copy plus encoded snapshot/hash/decode passes are
|
|
184
|
+
// charged beside the sole procedural admission owner's work estimate.
|
|
185
|
+
const resourceWork = resourcePixels * 12 + encoded * 8;
|
|
186
|
+
if (admission.estimatedWorkUnits + resourceWork > PORTABLE_MATERIAL_V3_LIMITS.maxWork)
|
|
187
|
+
throw new ProceduralTextureError(
|
|
188
|
+
'Aggregate material work budget exceeded before compilation or decoding.',
|
|
189
|
+
);
|
|
190
|
+
if (
|
|
191
|
+
texels > PORTABLE_MATERIAL_V3_LIMITS.maxTexels ||
|
|
192
|
+
encoded > PORTABLE_MATERIAL_V3_LIMITS.maxEncodedBytes
|
|
193
|
+
)
|
|
194
|
+
throw new ProceduralTextureError(
|
|
195
|
+
'Aggregate resource texel/encoded-byte budget exceeded before decoding.',
|
|
196
|
+
);
|
|
197
|
+
const snapshot = {
|
|
198
|
+
descriptor: structuredClone(d),
|
|
199
|
+
bytes: raw.bytes.slice(),
|
|
200
|
+
userData: structuredClone(raw.userData),
|
|
201
|
+
};
|
|
202
|
+
const header = new DataView(
|
|
203
|
+
snapshot.bytes.buffer,
|
|
204
|
+
snapshot.bytes.byteOffset,
|
|
205
|
+
snapshot.bytes.byteLength,
|
|
206
|
+
);
|
|
207
|
+
if (
|
|
208
|
+
![137, 80, 78, 71, 13, 10, 26, 10].every((n, i) => snapshot.bytes[i] === n) ||
|
|
209
|
+
String.fromCharCode(...snapshot.bytes.subarray(12, 16)) !== 'IHDR' ||
|
|
210
|
+
header.getUint32(16) !== d.width ||
|
|
211
|
+
header.getUint32(20) !== d.height
|
|
212
|
+
)
|
|
213
|
+
throw new ProceduralTextureError('Prepared texture PNG/header dimensions mismatch.');
|
|
214
|
+
const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', snapshot.bytes));
|
|
215
|
+
if (
|
|
216
|
+
`sha256:${Array.from(hash, (x) => x.toString(16).padStart(2, '0')).join('')}` !==
|
|
217
|
+
d.imageSha256
|
|
218
|
+
)
|
|
219
|
+
throw new ProceduralTextureError('Prepared texture encoded hash mismatch.');
|
|
220
|
+
prepared.set(slot, snapshot);
|
|
221
|
+
}
|
|
222
|
+
// Includes held outputs, decoder/copy allowances, encoded snapshots and generated mips.
|
|
223
|
+
const transient =
|
|
224
|
+
admission.conservativePixelAllocationBytes +
|
|
225
|
+
resourcePixels * 12 +
|
|
226
|
+
encoded * 3 +
|
|
227
|
+
Math.ceil((texels * 4) / 3);
|
|
228
|
+
if (transient > PORTABLE_MATERIAL_V3_LIMITS.maxTransientBytes)
|
|
229
|
+
throw new ProceduralTextureError(
|
|
230
|
+
'Aggregate material allocation budget exceeded before decoding.',
|
|
231
|
+
);
|
|
232
|
+
for (const [slot, ref] of entries) {
|
|
233
|
+
options.signal?.throwIfAborted();
|
|
234
|
+
let texture: THREE.DataTexture;
|
|
235
|
+
if (ref.kind === 'procedural') texture = proceduralTexture(ref.spec);
|
|
236
|
+
else {
|
|
237
|
+
const source = prepared.get(slot)!;
|
|
238
|
+
texture = await loadTexture(source.bytes.slice(), {
|
|
239
|
+
usage: source.descriptor.usage,
|
|
240
|
+
name: ref.resourceId,
|
|
241
|
+
});
|
|
242
|
+
owned.set(slot, texture);
|
|
243
|
+
if (
|
|
244
|
+
texture.image.width !== source.descriptor.width ||
|
|
245
|
+
texture.image.height !== source.descriptor.height ||
|
|
246
|
+
!texture.image.data ||
|
|
247
|
+
texture.image.data.length !== source.descriptor.width * source.descriptor.height * 4
|
|
248
|
+
)
|
|
249
|
+
throw new ProceduralTextureError('Prepared texture decoded dimensions mismatch.');
|
|
250
|
+
const actual = texture.userData['kilnTexture'];
|
|
251
|
+
texture.userData = { ...structuredClone(source.userData), ...texture.userData };
|
|
252
|
+
texture.userData['kilnTexture'] = {
|
|
253
|
+
...(source.userData['kilnTexture'] as Record<string, unknown>),
|
|
254
|
+
...actual,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
owned.set(slot, texture);
|
|
258
|
+
texture.flipY = false;
|
|
259
|
+
texture.premultiplyAlpha = false;
|
|
260
|
+
applyPortableSamplerV1(texture, ref.sampler);
|
|
261
|
+
texture.userData['kilnPortableBinding'] = {
|
|
262
|
+
schemaVersion: 3,
|
|
263
|
+
slot,
|
|
264
|
+
sampler: { ...ref.sampler },
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
options.signal?.throwIfAborted();
|
|
268
|
+
const material = pbrMaterial({
|
|
269
|
+
roughness: spec.roughness,
|
|
270
|
+
metalness: spec.metalness,
|
|
271
|
+
emissiveIntensity: spec.emissiveIntensity,
|
|
272
|
+
alphaMode: spec.alphaMode,
|
|
273
|
+
...(spec.alphaMode === 'mask' ? { alphaCutoff: spec.alphaCutoff } : {}),
|
|
274
|
+
doubleSided: spec.doubleSided,
|
|
275
|
+
...(owned.has('baseColor')
|
|
276
|
+
? { albedo: owned.get('baseColor') }
|
|
277
|
+
: spec.baseColor !== undefined
|
|
278
|
+
? { albedo: spec.baseColor }
|
|
279
|
+
: {}),
|
|
280
|
+
...(owned.has('normal') ? { normal: owned.get('normal') } : {}),
|
|
281
|
+
...(owned.has('metallicRoughness')
|
|
282
|
+
? { metallicRoughness: owned.get('metallicRoughness') }
|
|
283
|
+
: {}),
|
|
284
|
+
...(owned.has('emissive')
|
|
285
|
+
? { emissive: owned.get('emissive') }
|
|
286
|
+
: spec.emissive !== undefined
|
|
287
|
+
? { emissive: spec.emissive }
|
|
288
|
+
: {}),
|
|
289
|
+
...(owned.has('occlusion') ? { aoMap: owned.get('occlusion') } : {}),
|
|
290
|
+
});
|
|
291
|
+
if (spec.name) material.name = spec.name;
|
|
292
|
+
if (owned.has('baseColor') && spec.baseColor !== undefined)
|
|
293
|
+
material.color.setHex(spec.baseColor);
|
|
294
|
+
if (owned.has('emissive') && spec.emissive !== undefined)
|
|
295
|
+
material.emissive.setHex(spec.emissive);
|
|
296
|
+
material.userData['kilnPortableMaterial'] = spec;
|
|
297
|
+
return material;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
for (const texture of owned.values()) texture.dispose();
|
|
300
|
+
if (error instanceof PortableMaterialV3Error) throw error;
|
|
301
|
+
const wrapped = new PortableMaterialV3Error(
|
|
302
|
+
error instanceof ProceduralTextureError
|
|
303
|
+
? error.message
|
|
304
|
+
: 'Texture preparation or compilation failed; verify injected bytes, metadata and cancellation state.',
|
|
305
|
+
);
|
|
306
|
+
wrapped.cause = error;
|
|
307
|
+
throw wrapped;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Apply only after the caller has established independent texture ownership. */
|
|
2
|
+
import {
|
|
3
|
+
ClampToEdgeWrapping,
|
|
4
|
+
LinearFilter,
|
|
5
|
+
LinearMipmapLinearFilter,
|
|
6
|
+
LinearMipmapNearestFilter,
|
|
7
|
+
MirroredRepeatWrapping,
|
|
8
|
+
NearestFilter,
|
|
9
|
+
NearestMipmapLinearFilter,
|
|
10
|
+
NearestMipmapNearestFilter,
|
|
11
|
+
RepeatWrapping,
|
|
12
|
+
type Texture,
|
|
13
|
+
} from 'three';
|
|
14
|
+
import { canonicalizePortableSamplerV1, type CanonicalPortableSamplerV1 } from './portable-sampler';
|
|
15
|
+
const min = {
|
|
16
|
+
nearest: NearestFilter,
|
|
17
|
+
linear: LinearFilter,
|
|
18
|
+
'nearest-mipmap-nearest': NearestMipmapNearestFilter,
|
|
19
|
+
'linear-mipmap-nearest': LinearMipmapNearestFilter,
|
|
20
|
+
'nearest-mipmap-linear': NearestMipmapLinearFilter,
|
|
21
|
+
'linear-mipmap-linear': LinearMipmapLinearFilter,
|
|
22
|
+
} as const;
|
|
23
|
+
const wrap = {
|
|
24
|
+
repeat: RepeatWrapping,
|
|
25
|
+
'clamp-to-edge': ClampToEdgeWrapping,
|
|
26
|
+
'mirrored-repeat': MirroredRepeatWrapping,
|
|
27
|
+
} as const;
|
|
28
|
+
export function applyPortableSamplerV1<T extends Texture>(
|
|
29
|
+
ownedTexture: T,
|
|
30
|
+
sampler: CanonicalPortableSamplerV1,
|
|
31
|
+
): T {
|
|
32
|
+
const value = canonicalizePortableSamplerV1(sampler);
|
|
33
|
+
ownedTexture.minFilter = min[value.minFilter];
|
|
34
|
+
ownedTexture.magFilter = value.magFilter === 'nearest' ? NearestFilter : LinearFilter;
|
|
35
|
+
ownedTexture.wrapS = wrap[value.wrapS];
|
|
36
|
+
ownedTexture.wrapT = wrap[value.wrapT];
|
|
37
|
+
ownedTexture.generateMipmaps = value.minFilter.includes('mipmap');
|
|
38
|
+
ownedTexture.needsUpdate = true;
|
|
39
|
+
return ownedTexture;
|
|
40
|
+
}
|