@instruktlabs/kiln 1.2.1 → 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 +86 -1
- package/README.md +18 -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/install.md +7 -6
- package/docs/material-library.md +9 -5
- package/docs/migration.md +16 -0
- 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/runtime.md +3 -3
- package/docs/sdk.md +55 -2
- 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/SKILL.md +1 -1
- package/skills/kiln-author-asset/references/camera-cli.md +13 -0
- package/skills/kiln-author-asset/references/geometry-recipes.md +134 -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,54 @@
|
|
|
1
|
+
/** Shared portable sampler contract; no renderer, loader or native dependencies. */
|
|
2
|
+
import { ProceduralTextureError } from './procedural-material-v2';
|
|
3
|
+
|
|
4
|
+
export const PORTABLE_MIN_FILTERS_V1 = [
|
|
5
|
+
'nearest',
|
|
6
|
+
'linear',
|
|
7
|
+
'nearest-mipmap-nearest',
|
|
8
|
+
'linear-mipmap-nearest',
|
|
9
|
+
'nearest-mipmap-linear',
|
|
10
|
+
'linear-mipmap-linear',
|
|
11
|
+
] as const;
|
|
12
|
+
export const PORTABLE_MAG_FILTERS_V1 = ['nearest', 'linear'] as const;
|
|
13
|
+
export const PORTABLE_WRAPS_V1 = ['repeat', 'clamp-to-edge', 'mirrored-repeat'] as const;
|
|
14
|
+
export interface PortableSamplerV1 {
|
|
15
|
+
minFilter: (typeof PORTABLE_MIN_FILTERS_V1)[number];
|
|
16
|
+
magFilter: (typeof PORTABLE_MAG_FILTERS_V1)[number];
|
|
17
|
+
wrapS: (typeof PORTABLE_WRAPS_V1)[number];
|
|
18
|
+
wrapT: (typeof PORTABLE_WRAPS_V1)[number];
|
|
19
|
+
}
|
|
20
|
+
export type CanonicalPortableSamplerV1 = PortableSamplerV1;
|
|
21
|
+
export function canonicalizePortableSamplerV1(input: unknown): CanonicalPortableSamplerV1 {
|
|
22
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
23
|
+
throw new ProceduralTextureError('portableSampler requires a complete JSON object.');
|
|
24
|
+
const prototype = Object.getPrototypeOf(input);
|
|
25
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
26
|
+
throw new ProceduralTextureError('portableSampler must be plain JSON data.');
|
|
27
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
28
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key)!;
|
|
29
|
+
if (typeof key !== 'string' || !descriptor.enumerable || !('value' in descriptor))
|
|
30
|
+
throw new ProceduralTextureError(
|
|
31
|
+
'portableSampler requires enumerable JSON data fields, never accessors.',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const value = input as Record<string, unknown>;
|
|
35
|
+
const allowed = {
|
|
36
|
+
minFilter: PORTABLE_MIN_FILTERS_V1,
|
|
37
|
+
magFilter: PORTABLE_MAG_FILTERS_V1,
|
|
38
|
+
wrapS: PORTABLE_WRAPS_V1,
|
|
39
|
+
wrapT: PORTABLE_WRAPS_V1,
|
|
40
|
+
};
|
|
41
|
+
if (Object.keys(value).some((key) => !Object.hasOwn(allowed, key)))
|
|
42
|
+
throw new ProceduralTextureError('portableSampler contains an unsupported field.');
|
|
43
|
+
for (const [key, options] of Object.entries(allowed))
|
|
44
|
+
if (!(options as readonly unknown[]).includes(value[key]))
|
|
45
|
+
throw new ProceduralTextureError(
|
|
46
|
+
`portableSampler.${key} requires one of ${options.join(', ')}.`,
|
|
47
|
+
);
|
|
48
|
+
return {
|
|
49
|
+
minFilter: value['minFilter'],
|
|
50
|
+
magFilter: value['magFilter'],
|
|
51
|
+
wrapS: value['wrapS'],
|
|
52
|
+
wrapT: value['wrapT'],
|
|
53
|
+
} as CanonicalPortableSamplerV1;
|
|
54
|
+
}
|
package/src/primitives.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
GEOMETRY_ALLOCATION_LIMITS,
|
|
20
20
|
} from './geometry-budget';
|
|
21
21
|
import { buildWallPanels } from './wall-panels';
|
|
22
|
-
import { createJointChain } from './character';
|
|
22
|
+
import { createBoneChain, createJointChain, createSkinnedPart } from './character';
|
|
23
23
|
import { describeAssembly, replicateAssembly } from './assembly';
|
|
24
24
|
import { defineLod } from './lod';
|
|
25
25
|
export { defineLod } from './lod';
|
|
@@ -57,8 +57,12 @@ import * as profile from './profile';
|
|
|
57
57
|
import * as solids from './solids';
|
|
58
58
|
import * as proceduralTextures from './procedural-texture';
|
|
59
59
|
import * as textures from './textures';
|
|
60
|
+
import { textureAtlas, remapUVToAtlas } from './texture-atlas';
|
|
60
61
|
import type { TextureResolver } from './texture-resolver';
|
|
61
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
compilePortableMaterialSpecV2,
|
|
64
|
+
compilePortableMaterialSpecV3,
|
|
65
|
+
} from './portable-material-runtime';
|
|
62
66
|
import { createVehicleFrame, createWheelAssembly, createWheelGeometrySet } from './vehicle';
|
|
63
67
|
import {
|
|
64
68
|
stampSemanticMetadataV1,
|
|
@@ -72,6 +76,36 @@ export { projectUV } from './uv-project';
|
|
|
72
76
|
export type { ProjectUVOptions } from './uv-project';
|
|
73
77
|
export { remapUV } from './uv-shapes';
|
|
74
78
|
export type { RemapUVOptions } from './uv-shapes';
|
|
79
|
+
export { textureAtlas, remapUVToAtlas } from './texture-atlas';
|
|
80
|
+
export type { AtlasUVOptionsV1 } from './texture-atlas';
|
|
81
|
+
export {
|
|
82
|
+
TextureAtlasError,
|
|
83
|
+
TEXTURE_ATLAS_COMPILER_ID,
|
|
84
|
+
TEXTURE_ATLAS_LIMITS,
|
|
85
|
+
canonicalizeTextureAtlasSpecV1,
|
|
86
|
+
canonicalTextureAtlasJsonV1,
|
|
87
|
+
hashTextureAtlasSpecV1,
|
|
88
|
+
} from './texture-atlas-contract';
|
|
89
|
+
export type {
|
|
90
|
+
AtlasRectangleV1,
|
|
91
|
+
AtlasEdgeModeV1,
|
|
92
|
+
AtlasResourceExpectationV1,
|
|
93
|
+
TextureAtlasSourceV1,
|
|
94
|
+
TextureAtlasRegionV1,
|
|
95
|
+
TextureAtlasSpecV1,
|
|
96
|
+
CanonicalTextureAtlasSourceV1,
|
|
97
|
+
CanonicalTextureAtlasRegionV1,
|
|
98
|
+
CanonicalTextureAtlasSpecV1,
|
|
99
|
+
} from './texture-atlas-contract';
|
|
100
|
+
export { compileTextureAtlasSpecV1 } from './texture-atlas-pixels';
|
|
101
|
+
export type {
|
|
102
|
+
TextureAtlasCompileOptionsV1,
|
|
103
|
+
TextureAtlasBudgetV1,
|
|
104
|
+
TextureAtlasSourceManifestV1,
|
|
105
|
+
TextureAtlasRegionManifestV1,
|
|
106
|
+
TextureAtlasManifestV1,
|
|
107
|
+
CompiledTextureAtlasV1,
|
|
108
|
+
} from './texture-atlas-pixels';
|
|
75
109
|
|
|
76
110
|
export {
|
|
77
111
|
createGableEndPanel,
|
|
@@ -86,8 +120,10 @@ export type {
|
|
|
86
120
|
RoofFaceFrame,
|
|
87
121
|
RoofSurfaceLayoutOptions,
|
|
88
122
|
} from './architecture';
|
|
89
|
-
export { createJointChain } from './character';
|
|
123
|
+
export { createBoneChain, createJointChain, createSkinnedPart } from './character';
|
|
90
124
|
export type {
|
|
125
|
+
BoneChainResultV1,
|
|
126
|
+
CreateSkinnedPartOptions,
|
|
91
127
|
CreateJointChainOptionsV1,
|
|
92
128
|
JointChainResultV1,
|
|
93
129
|
JointChainSegmentV1,
|
|
@@ -100,7 +136,50 @@ export {
|
|
|
100
136
|
export { materialRecipe } from './material-recipe-runtime';
|
|
101
137
|
export type { ApplyMaterialRecipeOptions } from './material-recipe-runtime';
|
|
102
138
|
export type { MaterialRecipeId, MaterialRecipeOverridesV1 } from './material-recipes';
|
|
103
|
-
export {
|
|
139
|
+
export {
|
|
140
|
+
compilePortableMaterialSpecV2,
|
|
141
|
+
compilePortableMaterialSpecV3,
|
|
142
|
+
} from './portable-material-runtime';
|
|
143
|
+
export {
|
|
144
|
+
canonicalizePortableMaterialSpecV3,
|
|
145
|
+
PortableMaterialV3Error,
|
|
146
|
+
PORTABLE_MATERIAL_V3_LIMITS,
|
|
147
|
+
} from './procedural-material-v3';
|
|
148
|
+
export type {
|
|
149
|
+
PortableMaterialSpecV3,
|
|
150
|
+
CanonicalPortableMaterialSpecV3,
|
|
151
|
+
PortableTextureRefV3,
|
|
152
|
+
CanonicalPortableTextureRefV3,
|
|
153
|
+
} from './procedural-material-v3';
|
|
154
|
+
export { canonicalizePortableSamplerV1 } from './portable-sampler';
|
|
155
|
+
export type { PortableSamplerV1, CanonicalPortableSamplerV1 } from './portable-sampler';
|
|
156
|
+
export {
|
|
157
|
+
compileProceduralTextureSpecV3,
|
|
158
|
+
canonicalizeProceduralTextureSpecV3,
|
|
159
|
+
canonicalProceduralTextureJsonV3,
|
|
160
|
+
hashProceduralTextureSpecV3,
|
|
161
|
+
PROCEDURAL_TEXTURE_V3_COMPILER_ID,
|
|
162
|
+
PROCEDURAL_TEXTURE_SPEC_VERSION_V3,
|
|
163
|
+
MAX_PROCEDURAL_V3_EDGE,
|
|
164
|
+
MAX_PROCEDURAL_V3_TEXELS,
|
|
165
|
+
MAX_PROCEDURAL_V3_OPERATIONS,
|
|
166
|
+
MAX_PROCEDURAL_V3_VERTICES,
|
|
167
|
+
MAX_PROCEDURAL_V3_STAMPS,
|
|
168
|
+
MAX_PROCEDURAL_V3_PALETTE,
|
|
169
|
+
MAX_PROCEDURAL_V3_WORK,
|
|
170
|
+
MAX_PROCEDURAL_V3_TRANSIENT_BYTES,
|
|
171
|
+
} from './procedural-texture-v3';
|
|
172
|
+
export type {
|
|
173
|
+
CompiledProceduralTextureV3,
|
|
174
|
+
RgbaV3,
|
|
175
|
+
PixelPointV3,
|
|
176
|
+
PixelRectangleV3,
|
|
177
|
+
ProceduralShapeV3,
|
|
178
|
+
ProceduralOperationV3,
|
|
179
|
+
CanonicalProceduralOperationV3,
|
|
180
|
+
ProceduralTextureSpecV3,
|
|
181
|
+
CanonicalProceduralTextureSpecV3,
|
|
182
|
+
} from './procedural-texture-v3';
|
|
104
183
|
export {
|
|
105
184
|
MAX_PORTABLE_MATERIAL_TEXELS,
|
|
106
185
|
MAX_PORTABLE_MATERIAL_TEXTURES,
|
|
@@ -2173,6 +2252,8 @@ export function buildSandboxGlobals(
|
|
|
2173
2252
|
replicateAssembly: wrap('replicateAssembly', replicateAssembly),
|
|
2174
2253
|
createPivot: wrap('createPivot', createPivot),
|
|
2175
2254
|
createJointChain: wrap('createJointChain', createJointChain),
|
|
2255
|
+
createBoneChain: wrap('createBoneChain', createBoneChain),
|
|
2256
|
+
createSkinnedPart: wrap('createSkinnedPart', createSkinnedPart),
|
|
2176
2257
|
createVehicleFrame: wrap('createVehicleFrame', createVehicleFrame),
|
|
2177
2258
|
createWheelGeometrySet: wrap('createWheelGeometrySet', createWheelGeometrySet),
|
|
2178
2259
|
createWheelAssembly: wrap('createWheelAssembly', createWheelAssembly),
|
|
@@ -2223,6 +2304,13 @@ export function buildSandboxGlobals(
|
|
|
2223
2304
|
);
|
|
2224
2305
|
return compilePortableMaterialSpecV2(args[0], { resolver: options.textureResolver });
|
|
2225
2306
|
}),
|
|
2307
|
+
compilePortableMaterialSpecV3: wrap('compilePortableMaterialSpecV3', (...args: unknown[]) => {
|
|
2308
|
+
if (args.length !== 1)
|
|
2309
|
+
return Promise.reject(
|
|
2310
|
+
new TypeError('compilePortableMaterialSpecV3 accepts exactly one material spec'),
|
|
2311
|
+
);
|
|
2312
|
+
return compilePortableMaterialSpecV3(args[0], { resolver: options.textureResolver });
|
|
2313
|
+
}),
|
|
2226
2314
|
basicMaterial: wrap('basicMaterial', basicMaterial),
|
|
2227
2315
|
glassMaterial: wrap('glassMaterial', glassMaterial),
|
|
2228
2316
|
lambertMaterial: wrap('lambertMaterial', lambertMaterial),
|
|
@@ -2275,6 +2363,7 @@ export function buildSandboxGlobals(
|
|
|
2275
2363
|
// Shape-aware unwraps (sync — preserve built-in directional UVs)
|
|
2276
2364
|
projectUV: wrap('projectUV', projectUV),
|
|
2277
2365
|
remapUV: wrap('remapUV', uvShapes.remapUV),
|
|
2366
|
+
remapUVToAtlas: wrap('remapUVToAtlas', remapUVToAtlas),
|
|
2278
2367
|
// Parametric primitives
|
|
2279
2368
|
gearGeo: wrapGeo('gearGeo', gears.gearGeo),
|
|
2280
2369
|
bladeGeo: wrapGeo('bladeGeo', gears.bladeGeo),
|
|
@@ -2283,6 +2372,11 @@ export function buildSandboxGlobals(
|
|
|
2283
2372
|
loadApprovedTexture: wrap('loadApprovedTexture', loadApprovedTexture),
|
|
2284
2373
|
// Procedural textures (sync — no I/O, so no await)
|
|
2285
2374
|
proceduralTexture: wrap('proceduralTexture', proceduralTextures.proceduralTexture),
|
|
2375
|
+
textureAtlas: wrap('textureAtlas', (...args: unknown[]) => {
|
|
2376
|
+
if (args.length !== 1)
|
|
2377
|
+
return Promise.reject(new TypeError('textureAtlas accepts exactly one atlas spec'));
|
|
2378
|
+
return textureAtlas(args[0], { resources: options.textureResolver?.pinnedMaterialMaps });
|
|
2379
|
+
}),
|
|
2286
2380
|
normalMapFromHeight: wrap('normalMapFromHeight', proceduralTextures.normalMapFromHeight),
|
|
2287
2381
|
pbrMaterial: wrap('pbrMaterial', textures.pbrMaterial),
|
|
2288
2382
|
foliageMaterial: wrap('foliageMaterial', textures.foliageMaterial),
|
|
@@ -519,7 +519,8 @@ const SHA256_K = new Uint32Array([
|
|
|
519
519
|
const rotateRight = (value: number, bits: number): number =>
|
|
520
520
|
(value >>> bits) | (value << (32 - bits));
|
|
521
521
|
|
|
522
|
-
|
|
522
|
+
/** Browser-safe SHA-256 shared by versioned recipe contracts. */
|
|
523
|
+
export function sha256Hex(text: string): string {
|
|
523
524
|
const source = new TextEncoder().encode(text);
|
|
524
525
|
const bitLength = source.length * 8;
|
|
525
526
|
const paddedLength = Math.ceil((source.length + 9) / 64) * 64;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/** Strict opt-in PBR material data. Pixel/byte loading is outside this module. */
|
|
2
|
+
import {
|
|
3
|
+
canonicalizePortableMaterialSpecV2,
|
|
4
|
+
ProceduralTextureError,
|
|
5
|
+
} from './procedural-material-v2';
|
|
6
|
+
import {
|
|
7
|
+
admitProceduralTextureSpecV3,
|
|
8
|
+
type CanonicalProceduralTextureSpecV3,
|
|
9
|
+
type ProceduralTextureSpecV3,
|
|
10
|
+
} from './procedural-texture-v3-contract';
|
|
11
|
+
import {
|
|
12
|
+
canonicalizePortableSamplerV1,
|
|
13
|
+
type CanonicalPortableSamplerV1,
|
|
14
|
+
type PortableSamplerV1,
|
|
15
|
+
} from './portable-sampler';
|
|
16
|
+
import type { MaterialMapSlotV1 } from './pinned-material-map';
|
|
17
|
+
|
|
18
|
+
export const PORTABLE_MATERIAL_V3_LIMITS = Object.freeze({
|
|
19
|
+
maxBindings: 5,
|
|
20
|
+
maxTexels: 4 * 1024 * 1024,
|
|
21
|
+
maxEncodedBytes: 16 * 1024 * 1024,
|
|
22
|
+
maxWork: 64_000_000,
|
|
23
|
+
maxTransientBytes: 64 * 1024 * 1024,
|
|
24
|
+
maxJsonBytes: 1024 * 1024,
|
|
25
|
+
});
|
|
26
|
+
export const PORTABLE_MATERIAL_SLOT_USAGE = {
|
|
27
|
+
baseColor: 'albedo',
|
|
28
|
+
normal: 'normal',
|
|
29
|
+
metallicRoughness: 'metallicRoughness',
|
|
30
|
+
emissive: 'emissive',
|
|
31
|
+
occlusion: 'occlusion',
|
|
32
|
+
} as const;
|
|
33
|
+
export type PortableTextureRefV3 =
|
|
34
|
+
| { kind: 'procedural'; spec: ProceduralTextureSpecV3; sampler: PortableSamplerV1 }
|
|
35
|
+
| { kind: 'resource'; resourceId: string; sampler: PortableSamplerV1 };
|
|
36
|
+
export type CanonicalPortableTextureRefV3 =
|
|
37
|
+
| {
|
|
38
|
+
kind: 'procedural';
|
|
39
|
+
spec: CanonicalProceduralTextureSpecV3;
|
|
40
|
+
sampler: CanonicalPortableSamplerV1;
|
|
41
|
+
}
|
|
42
|
+
| { kind: 'resource'; resourceId: string; sampler: CanonicalPortableSamplerV1 };
|
|
43
|
+
export interface PortableMaterialSpecV3 {
|
|
44
|
+
schemaVersion: 3;
|
|
45
|
+
model: 'pbrMetallicRoughness';
|
|
46
|
+
name?: string;
|
|
47
|
+
baseColor?: number;
|
|
48
|
+
roughness?: number;
|
|
49
|
+
metalness?: number;
|
|
50
|
+
emissive?: number;
|
|
51
|
+
emissiveIntensity?: number;
|
|
52
|
+
alphaMode?: 'opaque' | 'mask' | 'blend';
|
|
53
|
+
alphaCutoff?: number;
|
|
54
|
+
doubleSided?: boolean;
|
|
55
|
+
textures?: Partial<Record<MaterialMapSlotV1, PortableTextureRefV3>>;
|
|
56
|
+
}
|
|
57
|
+
export interface CanonicalPortableMaterialSpecV3
|
|
58
|
+
extends Omit<
|
|
59
|
+
PortableMaterialSpecV3,
|
|
60
|
+
| 'textures'
|
|
61
|
+
| 'roughness'
|
|
62
|
+
| 'metalness'
|
|
63
|
+
| 'emissiveIntensity'
|
|
64
|
+
| 'alphaMode'
|
|
65
|
+
| 'alphaCutoff'
|
|
66
|
+
| 'doubleSided'
|
|
67
|
+
> {
|
|
68
|
+
roughness: number;
|
|
69
|
+
metalness: number;
|
|
70
|
+
emissiveIntensity: number;
|
|
71
|
+
alphaMode: 'opaque' | 'mask' | 'blend';
|
|
72
|
+
alphaCutoff: number;
|
|
73
|
+
doubleSided: boolean;
|
|
74
|
+
textures: Partial<Record<MaterialMapSlotV1, CanonicalPortableTextureRefV3>>;
|
|
75
|
+
}
|
|
76
|
+
export class PortableMaterialV3Error extends ProceduralTextureError {
|
|
77
|
+
constructor(message: string) {
|
|
78
|
+
super(`portableMaterial V3: ${message}`, 'PORTABLE_MATERIAL_V3_CONTRACT');
|
|
79
|
+
this.name = 'PortableMaterialV3Error';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function fail(message: string): never {
|
|
83
|
+
throw new PortableMaterialV3Error(message);
|
|
84
|
+
}
|
|
85
|
+
/** Bound untrusted data before reading any accessor or making canonical copies. */
|
|
86
|
+
function assertJson(input: unknown): void {
|
|
87
|
+
let nodes = 0;
|
|
88
|
+
const visit = (value: unknown, depth: number): void => {
|
|
89
|
+
if (++nodes > 65536 || depth > 16) fail('JSON size/depth budget exceeded.');
|
|
90
|
+
if (value === null || typeof value === 'boolean') return;
|
|
91
|
+
if (typeof value === 'string') {
|
|
92
|
+
if (value.length > PORTABLE_MATERIAL_V3_LIMITS.maxJsonBytes) fail('string budget exceeded.');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === 'number' && Number.isFinite(value)) return;
|
|
96
|
+
if (!value || typeof value !== 'object') fail('only plain JSON data is supported.');
|
|
97
|
+
const array = Array.isArray(value),
|
|
98
|
+
proto = Object.getPrototypeOf(value);
|
|
99
|
+
if (array ? proto !== Array.prototype : proto !== Object.prototype && proto !== null)
|
|
100
|
+
fail('only plain JSON prototypes are supported.');
|
|
101
|
+
const descriptors = Object.getOwnPropertyDescriptors(value),
|
|
102
|
+
keys = Reflect.ownKeys(value);
|
|
103
|
+
if (keys.length > 1025) fail('field/array budget exceeded.');
|
|
104
|
+
const length = array ? (descriptors['length']?.value as number) : undefined;
|
|
105
|
+
if (array && (length! > 1024 || keys.length !== length! + 1))
|
|
106
|
+
fail('arrays must be bounded dense JSON data.');
|
|
107
|
+
for (const key of keys) {
|
|
108
|
+
if (array && key === 'length') continue;
|
|
109
|
+
if (typeof key !== 'string' || ['__proto__', 'prototype', 'constructor'].includes(key))
|
|
110
|
+
fail('forbidden JSON field.');
|
|
111
|
+
const d = descriptors[key]!;
|
|
112
|
+
if (!d.enumerable || !('value' in d)) fail('JSON accessors/hidden fields are unsupported.');
|
|
113
|
+
if (array && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= length!))
|
|
114
|
+
fail('invalid array key.');
|
|
115
|
+
visit(d.value, depth + 1);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
visit(input, 0);
|
|
119
|
+
if (
|
|
120
|
+
new TextEncoder().encode(JSON.stringify(input)).length >
|
|
121
|
+
PORTABLE_MATERIAL_V3_LIMITS.maxJsonBytes
|
|
122
|
+
)
|
|
123
|
+
fail('JSON byte budget exceeded.');
|
|
124
|
+
}
|
|
125
|
+
function record(value: unknown, keys: readonly string[]): Record<string, unknown> {
|
|
126
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail('expected a JSON object.');
|
|
127
|
+
const result = value as Record<string, unknown>;
|
|
128
|
+
if (Object.keys(result).some((key) => !keys.includes(key)))
|
|
129
|
+
fail('unknown field in material or texture reference.');
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
export interface PortableMaterialAdmissionV3 {
|
|
133
|
+
spec: CanonicalPortableMaterialSpecV3;
|
|
134
|
+
texels: number;
|
|
135
|
+
estimatedWorkUnits: number;
|
|
136
|
+
conservativePixelAllocationBytes: number;
|
|
137
|
+
}
|
|
138
|
+
function admitMaterial(input: unknown): PortableMaterialAdmissionV3 {
|
|
139
|
+
assertJson(input);
|
|
140
|
+
const scalarKeys = [
|
|
141
|
+
'baseColor',
|
|
142
|
+
'roughness',
|
|
143
|
+
'metalness',
|
|
144
|
+
'emissive',
|
|
145
|
+
'emissiveIntensity',
|
|
146
|
+
'alphaMode',
|
|
147
|
+
'alphaCutoff',
|
|
148
|
+
'doubleSided',
|
|
149
|
+
];
|
|
150
|
+
const r = record(input, ['schemaVersion', 'model', 'name', 'textures', ...scalarKeys]);
|
|
151
|
+
if (r['schemaVersion'] !== 3 || r['model'] !== 'pbrMetallicRoughness')
|
|
152
|
+
fail('requires schemaVersion 3 and model pbrMetallicRoughness.');
|
|
153
|
+
const name = r['name'];
|
|
154
|
+
if (name !== undefined && (typeof name !== 'string' || name.length < 1 || name.length > 200))
|
|
155
|
+
fail('name requires 1..200 characters.');
|
|
156
|
+
const common = canonicalizePortableMaterialSpecV2({
|
|
157
|
+
schemaVersion: 2,
|
|
158
|
+
model: 'pbrMetallicRoughness',
|
|
159
|
+
...Object.fromEntries(scalarKeys.filter((k) => r[k] !== undefined).map((k) => [k, r[k]])),
|
|
160
|
+
});
|
|
161
|
+
const refs = record(r['textures'] ?? {}, Object.keys(PORTABLE_MATERIAL_SLOT_USAGE));
|
|
162
|
+
const textures: CanonicalPortableMaterialSpecV3['textures'] = {};
|
|
163
|
+
let texels = 0,
|
|
164
|
+
work = 0,
|
|
165
|
+
bytes = 0;
|
|
166
|
+
for (const [key, value] of Object.entries(refs)) {
|
|
167
|
+
const slot = key as MaterialMapSlotV1;
|
|
168
|
+
const ref = record(value, ['kind', 'spec', 'resourceId', 'sampler']);
|
|
169
|
+
const sampler = canonicalizePortableSamplerV1(ref['sampler']);
|
|
170
|
+
if (ref['kind'] === 'procedural') {
|
|
171
|
+
if (ref['resourceId'] !== undefined)
|
|
172
|
+
fail('procedural references cannot carry a resource ID.');
|
|
173
|
+
const admitted = admitProceduralTextureSpecV3(ref['spec']);
|
|
174
|
+
if (admitted.spec.usage !== PORTABLE_MATERIAL_SLOT_USAGE[slot])
|
|
175
|
+
fail(`${slot} requires ${PORTABLE_MATERIAL_SLOT_USAGE[slot]} data.`);
|
|
176
|
+
texels += admitted.spec.width * admitted.spec.height;
|
|
177
|
+
work += admitted.estimatedWorkUnits;
|
|
178
|
+
bytes += admitted.conservativePixelAllocationBytes;
|
|
179
|
+
textures[slot] = { kind: 'procedural', spec: admitted.spec, sampler };
|
|
180
|
+
} else if (ref['kind'] === 'resource') {
|
|
181
|
+
if (
|
|
182
|
+
ref['spec'] !== undefined ||
|
|
183
|
+
typeof ref['resourceId'] !== 'string' ||
|
|
184
|
+
ref['resourceId'].length > 128 ||
|
|
185
|
+
!/^kiln\.[a-z0-9][a-z0-9._-]+$/.test(ref['resourceId'])
|
|
186
|
+
)
|
|
187
|
+
fail('resource requires only a bounded closed kiln.* ID and sampler.');
|
|
188
|
+
textures[slot] = { kind: 'resource', resourceId: ref['resourceId'], sampler };
|
|
189
|
+
} else fail('texture kind must be procedural or resource.');
|
|
190
|
+
}
|
|
191
|
+
if (
|
|
192
|
+
texels > PORTABLE_MATERIAL_V3_LIMITS.maxTexels ||
|
|
193
|
+
work > PORTABLE_MATERIAL_V3_LIMITS.maxWork ||
|
|
194
|
+
bytes > PORTABLE_MATERIAL_V3_LIMITS.maxTransientBytes
|
|
195
|
+
)
|
|
196
|
+
fail('aggregate texel/work/allocation budget exceeded.');
|
|
197
|
+
const { schemaVersion: _, textures: __, ...parameters } = common;
|
|
198
|
+
return {
|
|
199
|
+
spec: {
|
|
200
|
+
schemaVersion: 3,
|
|
201
|
+
...parameters,
|
|
202
|
+
...(name !== undefined ? { name: name as string } : {}),
|
|
203
|
+
textures,
|
|
204
|
+
},
|
|
205
|
+
texels,
|
|
206
|
+
estimatedWorkUnits: work,
|
|
207
|
+
conservativePixelAllocationBytes: bytes,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function admitPortableMaterialSpecV3(input: unknown): PortableMaterialAdmissionV3 {
|
|
211
|
+
try {
|
|
212
|
+
return admitMaterial(input);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof PortableMaterialV3Error) throw error;
|
|
215
|
+
throw new PortableMaterialV3Error(
|
|
216
|
+
error instanceof ProceduralTextureError ? error.message : 'Invalid bounded material data.',
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export function canonicalizePortableMaterialSpecV3(
|
|
221
|
+
input: unknown,
|
|
222
|
+
): CanonicalPortableMaterialSpecV3 {
|
|
223
|
+
return admitPortableMaterialSpecV3(input).spec;
|
|
224
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** Legacy V1/V2 pixel compiler. Extraction preserves arithmetic and recipe identity. */
|
|
2
|
+
import {
|
|
3
|
+
type CanonicalProceduralLayerV2,
|
|
4
|
+
type CanonicalProceduralTextureSpecV2,
|
|
5
|
+
type ProceduralBlend,
|
|
6
|
+
canonicalProceduralTextureJsonV2,
|
|
7
|
+
canonicalizeProceduralTextureSpecV2,
|
|
8
|
+
hashProceduralTextureSpecV2,
|
|
9
|
+
} from './procedural-material-v2';
|
|
10
|
+
|
|
11
|
+
// -----------------------------------------------------------------------------
|
|
12
|
+
// Deterministic value noise
|
|
13
|
+
// -----------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Integer hash of a lattice coordinate. Not a cryptographic hash — it only has
|
|
17
|
+
* to decorrelate neighbouring integers and return the same value forever.
|
|
18
|
+
*/
|
|
19
|
+
function hash2(x: number, y: number, seed: number): number {
|
|
20
|
+
let h = (Math.imul(x, 374761393) + Math.imul(y, 668265263) + Math.imul(seed, 1274126177)) | 0;
|
|
21
|
+
h = h ^ (h >>> 13);
|
|
22
|
+
h = Math.imul(h, 1274126177) | 0;
|
|
23
|
+
h = h ^ (h >>> 16);
|
|
24
|
+
return (h >>> 0) / 4294967296;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const smoothstep = (t: number): number => t * t * (3 - 2 * t);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Value noise on a lattice that wraps at `period`, so the result tiles.
|
|
31
|
+
* `period` is in lattice cells, and both axes use the same one.
|
|
32
|
+
*/
|
|
33
|
+
function valueNoise(u: number, v: number, period: number, seed: number): number {
|
|
34
|
+
const x = u * period;
|
|
35
|
+
const y = v * period;
|
|
36
|
+
const xi = Math.floor(x);
|
|
37
|
+
const yi = Math.floor(y);
|
|
38
|
+
const xf = x - xi;
|
|
39
|
+
const yf = y - yi;
|
|
40
|
+
// Wrapping the lattice index (not the sample point) is what makes it tile:
|
|
41
|
+
// the cell to the right of the last one is the first one again.
|
|
42
|
+
const wrap = (n: number): number => ((n % period) + period) % period;
|
|
43
|
+
const x0 = wrap(xi);
|
|
44
|
+
const y0 = wrap(yi);
|
|
45
|
+
const x1 = wrap(xi + 1);
|
|
46
|
+
const y1 = wrap(yi + 1);
|
|
47
|
+
const sx = smoothstep(xf);
|
|
48
|
+
const sy = smoothstep(yf);
|
|
49
|
+
const n00 = hash2(x0, y0, seed);
|
|
50
|
+
const n10 = hash2(x1, y0, seed);
|
|
51
|
+
const n01 = hash2(x0, y1, seed);
|
|
52
|
+
const n11 = hash2(x1, y1, seed);
|
|
53
|
+
const top = n00 + sx * (n10 - n00);
|
|
54
|
+
const bottom = n01 + sx * (n11 - n01);
|
|
55
|
+
return top + sy * (bottom - top);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fbm(u: number, v: number, period: number, octaves: number, seed: number): number {
|
|
59
|
+
let sum = 0;
|
|
60
|
+
let amplitude = 1;
|
|
61
|
+
let total = 0;
|
|
62
|
+
let p = period;
|
|
63
|
+
for (let o = 0; o < octaves; o++) {
|
|
64
|
+
sum += valueNoise(u, v, p, seed + o * 101) * amplitude;
|
|
65
|
+
total += amplitude;
|
|
66
|
+
amplitude *= 0.5;
|
|
67
|
+
// Doubling the period keeps every octave tileable at the base period.
|
|
68
|
+
p *= 2;
|
|
69
|
+
}
|
|
70
|
+
return total > 0 ? sum / total : 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// -----------------------------------------------------------------------------
|
|
74
|
+
// Layer evaluation
|
|
75
|
+
// -----------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
const rgbOf = (color: number): [number, number, number] => [
|
|
78
|
+
(color >> 16) & 0xff,
|
|
79
|
+
(color >> 8) & 0xff,
|
|
80
|
+
color & 0xff,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const lerp = (a: number, b: number, t: number): number => a + (b - a) * t;
|
|
84
|
+
|
|
85
|
+
/** Rotate normalized coords about the center so angled patterns stay centered. */
|
|
86
|
+
function project(u: number, v: number, angleDeg: number): number {
|
|
87
|
+
const a = (angleDeg * Math.PI) / 180;
|
|
88
|
+
return (u - 0.5) * Math.cos(a) + (v - 0.5) * Math.sin(a) + 0.5;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Evaluate one layer at a normalized coordinate, returning 0-255 RGB. */
|
|
92
|
+
function evalLayer(
|
|
93
|
+
layer: CanonicalProceduralLayerV2,
|
|
94
|
+
u: number,
|
|
95
|
+
v: number,
|
|
96
|
+
): [number, number, number] {
|
|
97
|
+
switch (layer.op) {
|
|
98
|
+
case 'solid':
|
|
99
|
+
return rgbOf(layer.color);
|
|
100
|
+
|
|
101
|
+
case 'checker': {
|
|
102
|
+
const cx = Math.floor(u * layer.squares);
|
|
103
|
+
const cy = Math.floor(v * layer.squares);
|
|
104
|
+
return rgbOf((cx + cy) % 2 === 0 ? layer.colorA : layer.colorB);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'stripes': {
|
|
108
|
+
const t = project(u, v, layer.angleDeg);
|
|
109
|
+
return rgbOf(Math.floor(t * layer.count) % 2 === 0 ? layer.colorA : layer.colorB);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'gradient': {
|
|
113
|
+
const t = Math.min(1, Math.max(0, project(u, v, layer.angleDeg)));
|
|
114
|
+
const from = rgbOf(layer.from);
|
|
115
|
+
const to = rgbOf(layer.to);
|
|
116
|
+
return [lerp(from[0], to[0], t), lerp(from[1], to[1], t), lerp(from[2], to[2], t)];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case 'bricks': {
|
|
120
|
+
const row = Math.floor(v * layer.rows);
|
|
121
|
+
// Offset alternate rows, wrapping into [0,1) so the pattern still tiles.
|
|
122
|
+
const shifted = (u + (row % 2 === 0 ? 0 : layer.stagger / layer.cols) + 1) % 1;
|
|
123
|
+
const inCol = (shifted * layer.cols) % 1;
|
|
124
|
+
const inRow = (v * layer.rows) % 1;
|
|
125
|
+
const isMortar =
|
|
126
|
+
inCol < layer.mortarWidth ||
|
|
127
|
+
inCol > 1 - layer.mortarWidth ||
|
|
128
|
+
inRow < layer.mortarWidth ||
|
|
129
|
+
inRow > 1 - layer.mortarWidth;
|
|
130
|
+
return rgbOf(isMortar ? layer.mortar : layer.brick);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'noise': {
|
|
134
|
+
const n = fbm(u, v, layer.scale, layer.octaves, layer.seed);
|
|
135
|
+
const a = rgbOf(layer.colorA);
|
|
136
|
+
const b = rgbOf(layer.colorB);
|
|
137
|
+
return [lerp(a[0], b[0], n), lerp(a[1], b[1], n), lerp(a[2], b[2], n)];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function blendChannel(mode: ProceduralBlend, base: number, top: number): number {
|
|
143
|
+
const b = base / 255;
|
|
144
|
+
const t = top / 255;
|
|
145
|
+
let out: number;
|
|
146
|
+
switch (mode) {
|
|
147
|
+
case 'multiply':
|
|
148
|
+
out = b * t;
|
|
149
|
+
break;
|
|
150
|
+
case 'screen':
|
|
151
|
+
out = 1 - (1 - b) * (1 - t);
|
|
152
|
+
break;
|
|
153
|
+
case 'overlay':
|
|
154
|
+
out = b < 0.5 ? 2 * b * t : 1 - 2 * (1 - b) * (1 - t);
|
|
155
|
+
break;
|
|
156
|
+
default:
|
|
157
|
+
out = t;
|
|
158
|
+
}
|
|
159
|
+
return out * 255;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// -----------------------------------------------------------------------------
|
|
163
|
+
// Public API
|
|
164
|
+
// -----------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build a tiling texture from a bounded, deterministic layer stack.
|
|
168
|
+
*
|
|
169
|
+
* The result is a plain RGBA `DataTexture`; `renderSceneToGLB` bakes it to PNG
|
|
170
|
+
* and embeds it, so nothing else has to be done to ship it.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* const bark = proceduralTexture({
|
|
174
|
+
* schemaVersion: 2,
|
|
175
|
+
* size: 256,
|
|
176
|
+
* usage: 'albedo',
|
|
177
|
+
* name: 'Bark',
|
|
178
|
+
* layers: [
|
|
179
|
+
* { op: 'solid', color: 0x5a4632 },
|
|
180
|
+
* { op: 'noise', colorA: 0x3d2f21, colorB: 0x7a6248, scale: 6, octaves: 4, blend: 'overlay' },
|
|
181
|
+
* { op: 'stripes', colorA: 0x000000, colorB: 0xffffff, count: 24, angleDeg: 90,
|
|
182
|
+
* blend: 'multiply', opacity: 0.15 },
|
|
183
|
+
* ],
|
|
184
|
+
* });
|
|
185
|
+
* const trunk = createPart('Trunk', cylinderGeo(0.3, 0.3, 3), pbrMaterial({ albedo: bark }), { parent: root });
|
|
186
|
+
*/
|
|
187
|
+
export interface CompiledProceduralTextureV2 {
|
|
188
|
+
spec: CanonicalProceduralTextureSpecV2;
|
|
189
|
+
pixels: Uint8Array;
|
|
190
|
+
canonicalJson: string;
|
|
191
|
+
recipeHash: `sha256:${string}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Validate, canonicalize, and compile one texture without constructing Three.js state. */
|
|
195
|
+
export function compileProceduralTextureSpecV2(input: unknown): CompiledProceduralTextureV2 {
|
|
196
|
+
const spec = canonicalizeProceduralTextureSpecV2(input);
|
|
197
|
+
const { size, layers } = spec;
|
|
198
|
+
const data = new Uint8Array(size * size * 4);
|
|
199
|
+
for (let y = 0; y < size; y++) {
|
|
200
|
+
// Sample at texel centers so a pattern with N divisions lands on exact
|
|
201
|
+
// boundaries instead of drifting half a texel.
|
|
202
|
+
const v = (y + 0.5) / size;
|
|
203
|
+
for (let x = 0; x < size; x++) {
|
|
204
|
+
const u = (x + 0.5) / size;
|
|
205
|
+
let r = 0;
|
|
206
|
+
let g = 0;
|
|
207
|
+
let b = 0;
|
|
208
|
+
for (const [i, layer] of layers.entries()) {
|
|
209
|
+
const [lr, lg, lb] = evalLayer(layer, u, v);
|
|
210
|
+
if (i === 0) {
|
|
211
|
+
r = lr;
|
|
212
|
+
g = lg;
|
|
213
|
+
b = lb;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
r = lerp(r, blendChannel(layer.blend, r, lr), layer.opacity);
|
|
217
|
+
g = lerp(g, blendChannel(layer.blend, g, lg), layer.opacity);
|
|
218
|
+
b = lerp(b, blendChannel(layer.blend, b, lb), layer.opacity);
|
|
219
|
+
}
|
|
220
|
+
const o = (y * size + x) * 4;
|
|
221
|
+
data[o] = Math.max(0, Math.min(255, Math.round(r)));
|
|
222
|
+
data[o + 1] = Math.max(0, Math.min(255, Math.round(g)));
|
|
223
|
+
data[o + 2] = Math.max(0, Math.min(255, Math.round(b)));
|
|
224
|
+
data[o + 3] = 255;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
spec,
|
|
230
|
+
pixels: data,
|
|
231
|
+
canonicalJson: canonicalProceduralTextureJsonV2(spec),
|
|
232
|
+
recipeHash: hashProceduralTextureSpecV2(spec),
|
|
233
|
+
};
|
|
234
|
+
}
|