@call-me-sensei/toonlab 0.4.19 → 0.4.21
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/NPM-LIBRARY.md +230 -0
- package/README.md +78 -36
- package/agents/PROMPTS.md +91 -0
- package/agents/README.md +9 -14
- package/agents/claude/CLAUDE.md +8 -0
- package/agents/codex/AGENTS.md +161 -0
- package/agents/cursor/toonlab.mdc +37 -0
- package/agents/references/mcp-asset-discovery.md +12 -9
- package/agents/references/runtime-entry-points.md +5 -4
- package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
- package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
- package/cli/toonlab.mjs +3 -2
- package/database/fal-patina.mjs +131 -0
- package/database/generation-service.mjs +108 -7
- package/database/migrations/0007_creation_revisions.sql +80 -0
- package/database/migrations/0008_creation_revision_hardening.sql +98 -0
- package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
- package/database/migrations/0010_external_catalog_assets.sql +28 -0
- package/database/providers.mjs +195 -16
- package/database/repository.mjs +518 -70
- package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
- package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
- package/mcp/public-catalog.mjs +4 -52
- package/mcp/server.mjs +175 -12
- package/mcp/vite-plugin.mjs +78 -2
- package/package.json +32 -12
- package/scripts/generate-catalog-seed.mjs +34 -7
- package/src/asset-policy/catalogLicenses.js +64 -6
- package/src/assetlib/loadImported.js +15 -1
- package/src/catalog/officialCatalogAssetRuntime.js +13 -2
- package/src/catalog/officialCatalogLod.js +34 -1
- package/src/catalog/officialCatalogPlacement.js +29 -14
- package/src/character/animationRetarget.js +2 -0
- package/src/character/characterRig.js +6 -1
- package/src/character/characterRuntime.js +175 -7
- package/src/environment/environmentMaterialAdapter.js +30 -0
- package/src/environment/environmentPresets.js +90 -0
- package/src/environment/environmentSunShadowPass.js +42 -0
- package/src/environment/toonLabSurfaceLighting.js +108 -10
- package/src/environment/urbanPropMaterial.js +104 -2
- package/src/lighting/lightingSystem.js +65 -10
- package/src/renderer/index.js +7 -0
- package/src/renderer/styleComparison.js +985 -0
- package/src/rock-shader/index.js +5 -0
- package/src/rock-shader/rockGeometryDetail.js +701 -0
- package/src/rock-shader/rockMaterial.js +782 -39
- package/src/rock-shader/rockRegionRuntime.js +185 -0
- package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
- package/src/rock-shader/rockShaderRuntime.js +354 -67
- package/src/rock-shader/rockShaderSettings.js +246 -16
- package/src/rock-shader/rockTangentIntegrity.js +136 -0
- package/src/rock-shader/rockTextureIntegrity.js +115 -0
- package/src/rockgen/index.js +1 -0
- package/src/rockgen/rockDocument.js +27 -3
- package/src/rockgen/surface/c7GeologySurface.js +372 -0
- package/src/shaders-tsl/water.js +8 -0
- package/src/sky/cloudShadow.js +25 -1
- package/src/sky/skySystem.js +9 -0
- package/src/styles/index.js +23 -0
- package/src/styles/neutralStylePresets.js +712 -0
- package/src/styles/sceneStyleRuntime.js +51 -4
- package/src/styles/styleAdapters.js +13 -2
- package/src/styles/styleBundle.js +1 -1
- package/src/texgen/evaluateTexture.js +30 -3
- package/src/texgen/textureGenerators.js +63 -31
- package/src/texgen/textureSettings.js +25 -8
- package/src/toon/toonSettings.js +96 -2
- package/src/vegetation/branchTree.js +327 -42
- package/src/vegetation/index.js +24 -2
- package/src/vegetation/scatter.js +372 -1
- package/src/vegetation/stylizedTree.js +312 -126
- package/src/vegetation/stylizedTreeFoliage.js +229 -7
- package/src/vegetation/treeSurfaceTextures.js +328 -0
- package/src/version.js +1 -1
- package/src/water/waterSettings.js +15 -7
- package/types/asset-policy/catalogLicenses.d.ts +2 -0
- package/types/asset-policy/index.d.ts +2 -0
- package/types/assetlib/loadImported.d.ts +3 -1
- package/types/catalog/officialCatalogLod.d.ts +63 -5
- package/types/character/characterRig.d.ts +59 -0
- package/types/character/characterRuntime.d.ts +1 -0
- package/types/environment/environmentMaterialAdapter.d.ts +3 -1
- package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
- package/types/index.d.ts +89 -7
- package/types/lighting/lightingSystem.d.ts +7 -0
- package/types/react/index.d.ts +20 -0
- package/types/renderer/index.d.ts +1 -0
- package/types/renderer/styleComparison.d.ts +409 -0
- package/types/rock-shader/index.d.ts +5 -0
- package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
- package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
- package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
- package/types/rock-shader/rockShaderSettings.d.ts +19 -5
- package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
- package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/rockDocument.d.ts +29 -1
- package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
- package/types/sky/cloudShadow.d.ts +1 -0
- package/types/styles/index.d.ts +1 -0
- package/types/styles/neutralStylePresets.d.ts +944 -0
- package/types/styles/sceneStyleRuntime.d.ts +20 -0
- package/types/vegetation/branchTree.d.ts +4 -0
- package/types/vegetation/index.d.ts +9 -7
- package/types/vegetation/scatter.d.ts +1 -0
- package/types/vegetation/stylizedTree.d.ts +1 -2
- package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
- package/types/version.d.ts +1 -1
- package/agents/references/geology-playbook.md +0 -118
- package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
- package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
- package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/src/vegetation/legacyTreePresets.js +0 -120
- package/types/vegetation/legacyTreePresets.d.ts +0 -22
|
@@ -3,12 +3,26 @@ import {
|
|
|
3
3
|
createToonRockMaterial,
|
|
4
4
|
normalizeToonLabRockProfile,
|
|
5
5
|
} from './rockMaterial.js';
|
|
6
|
+
import { applyRockGeometryDetail } from './rockGeometryDetail.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Weight of the concavity channel in the moss moisture term.
|
|
10
|
+
*
|
|
11
|
+
* `moisture = clamp(slope + cavity * strength)`. At 1.6 a fully concave crevice
|
|
12
|
+
* reaches saturation on its own, so moss grows in a vertical cleft that the
|
|
13
|
+
* slope term scores at zero, while an exposed convex face still depends on
|
|
14
|
+
* slope alone and stays bare.
|
|
15
|
+
*/
|
|
16
|
+
export const ROCK_MOSS_CAVITY_STRENGTH = 1.6;
|
|
6
17
|
import { createRockShaderSettings } from './rockShaderSettings.js';
|
|
7
18
|
import { markFactoryStyleMaterial } from '../styles/styleMetadata.js';
|
|
19
|
+
import { withoutDegenerateDetailMaps } from './rockTextureIntegrity.js';
|
|
20
|
+
import { inspectRockRegionBindings } from './rockRegionRuntime.js';
|
|
21
|
+
import { sanitizeRockTangents } from './rockTangentIntegrity.js';
|
|
8
22
|
|
|
9
23
|
const ORIGINAL_MATERIALS = new WeakMap();
|
|
10
24
|
const ORIGINAL_SHADOW_FLAGS = new WeakMap();
|
|
11
|
-
|
|
25
|
+
const textureSetsByVariation = new Map();
|
|
12
26
|
|
|
13
27
|
function clamp01(value) {
|
|
14
28
|
return Math.min(1, Math.max(0, Number(value) || 0));
|
|
@@ -77,16 +91,22 @@ function makeProceduralTexture({
|
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
94
|
+
* Offsets every fallback map's seed by a variation index.
|
|
95
|
+
*
|
|
96
|
+
* Two rocks that resolve to the same fallback maps render with identical
|
|
97
|
+
* surface breakup. That reads as one rock duplicated — the failure §13 calls a
|
|
98
|
+
* "repeated prop pattern" — so a scene placing several rocks from one family
|
|
99
|
+
* needs a way to decorrelate them without authoring bespoke textures for each.
|
|
100
|
+
* Seed 0 reproduces the reviewed first-party set exactly.
|
|
83
101
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
function variationSeed(base, variation) {
|
|
103
|
+
return base + (variation * 101);
|
|
104
|
+
}
|
|
86
105
|
|
|
87
|
-
|
|
106
|
+
function buildRockShaderTextureSet(variation) {
|
|
107
|
+
return Object.freeze({
|
|
88
108
|
rock: makeProceduralTexture({
|
|
89
|
-
seed: 3,
|
|
109
|
+
seed: variationSeed(3, variation),
|
|
90
110
|
// Measured sRGB mean of the captured first-party T_RockClassic_BC input
|
|
91
111
|
// (179.70, 179.92, 178.98). Keeping the deterministic fallback anchored
|
|
92
112
|
// to that source lets the shader produce its reviewed grey sun face and
|
|
@@ -97,61 +117,85 @@ export function createDefaultRockShaderTextureSet() {
|
|
|
97
117
|
variation: 0.18,
|
|
98
118
|
}),
|
|
99
119
|
rockNormal: makeProceduralTexture({
|
|
100
|
-
seed: 5,
|
|
120
|
+
seed: variationSeed(5, variation),
|
|
101
121
|
color: [0.5, 0.5, 1],
|
|
102
122
|
variation: 0,
|
|
103
123
|
colorSpace: THREE.NoColorSpace,
|
|
104
124
|
channel: 'normal',
|
|
105
125
|
}),
|
|
106
126
|
smoothness: makeProceduralTexture({
|
|
107
|
-
seed: 7,
|
|
127
|
+
seed: variationSeed(7, variation),
|
|
108
128
|
color: [0.33, 0.33, 0.33],
|
|
109
129
|
variation: 0.12,
|
|
110
130
|
colorSpace: THREE.NoColorSpace,
|
|
111
131
|
channel: 'mask',
|
|
112
132
|
}),
|
|
113
133
|
stripe: makeProceduralTexture({
|
|
114
|
-
seed: 13,
|
|
134
|
+
seed: variationSeed(13, variation),
|
|
115
135
|
color: [0.58, 0.55, 0.5],
|
|
116
136
|
variation: 0.2,
|
|
117
137
|
}),
|
|
118
138
|
moss: makeProceduralTexture({
|
|
119
|
-
seed: 17,
|
|
139
|
+
seed: variationSeed(17, variation),
|
|
120
140
|
color: [0.3, 0.42, 0.2],
|
|
121
141
|
variation: 0.16,
|
|
122
142
|
}),
|
|
123
143
|
grass: makeProceduralTexture({
|
|
124
|
-
seed: 19,
|
|
144
|
+
seed: variationSeed(19, variation),
|
|
125
145
|
color: [0.32, 0.48, 0.19],
|
|
126
146
|
variation: 0.14,
|
|
127
147
|
}),
|
|
128
148
|
snow: makeProceduralTexture({
|
|
129
|
-
seed: 23,
|
|
149
|
+
seed: variationSeed(23, variation),
|
|
130
150
|
color: [0.88, 0.91, 0.94],
|
|
131
151
|
variation: 0.06,
|
|
132
152
|
}),
|
|
133
153
|
sand: makeProceduralTexture({
|
|
134
|
-
seed: 29,
|
|
154
|
+
seed: variationSeed(29, variation),
|
|
135
155
|
color: [0.7, 0.58, 0.39],
|
|
136
156
|
variation: 0.1,
|
|
137
157
|
}),
|
|
138
158
|
sandNormal: makeProceduralTexture({
|
|
139
|
-
seed: 31,
|
|
159
|
+
seed: variationSeed(31, variation),
|
|
140
160
|
color: [0.5, 0.5, 1],
|
|
141
161
|
variation: 0,
|
|
142
162
|
colorSpace: THREE.NoColorSpace,
|
|
143
163
|
channel: 'normal',
|
|
144
164
|
}),
|
|
145
165
|
topMask: makeProceduralTexture({
|
|
146
|
-
seed: 37,
|
|
166
|
+
seed: variationSeed(37, variation),
|
|
147
167
|
color: [0.56, 0.56, 0.56],
|
|
148
168
|
variation: 0.3,
|
|
149
169
|
colorSpace: THREE.NoColorSpace,
|
|
150
170
|
channel: 'mask',
|
|
151
171
|
}),
|
|
152
172
|
});
|
|
173
|
+
}
|
|
153
174
|
|
|
154
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Creates the neutral, deterministic fallback texture set used by the
|
|
177
|
+
* Call Me Sensei rock shader. Applications can replace any map with authored
|
|
178
|
+
* or licensed textures without changing the shader preset.
|
|
179
|
+
*
|
|
180
|
+
* `variation` decorrelates the generated maps so several rocks sharing one
|
|
181
|
+
* catalog family do not render with identical surface breakup. Sets are
|
|
182
|
+
* memoized per variation index and are deterministic: the same index always
|
|
183
|
+
* yields the same maps. Index 0 is the reviewed first-party set.
|
|
184
|
+
*
|
|
185
|
+
* @param {{variation?: number}} [options]
|
|
186
|
+
*/
|
|
187
|
+
export function createRockShaderTextureSet({ variation = 0 } = {}) {
|
|
188
|
+
const index = Number.isFinite(Number(variation)) ? Math.trunc(Number(variation)) : 0;
|
|
189
|
+
const existing = textureSetsByVariation.get(index);
|
|
190
|
+
if (existing) return existing;
|
|
191
|
+
const set = buildRockShaderTextureSet(index);
|
|
192
|
+
textureSetsByVariation.set(index, set);
|
|
193
|
+
return set;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The reviewed first-party fallback set (`variation: 0`). */
|
|
197
|
+
export function createDefaultRockShaderTextureSet() {
|
|
198
|
+
return createRockShaderTextureSet({ variation: 0 });
|
|
155
199
|
}
|
|
156
200
|
|
|
157
201
|
function tintArray(value, fallback) {
|
|
@@ -211,60 +255,59 @@ export function rockShaderSettingsToProfile(input = {}) {
|
|
|
211
255
|
lighting: {
|
|
212
256
|
exposure: lighting.exposure,
|
|
213
257
|
ambientFloor: lighting.ambientFloor,
|
|
258
|
+
skyColorInfluence: lighting.skyColorInfluence,
|
|
214
259
|
skyFillStrength: lighting.skyFillStrength,
|
|
215
260
|
skyFillTint: tintArray(lighting.skyFillTint, [1, 1, 1]),
|
|
261
|
+
shadowFill: lighting.shadowFill,
|
|
262
|
+
shadowFillTint: tintArray(lighting.shadowFillTint, [1, 1, 1]),
|
|
216
263
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
},
|
|
222
|
-
normals: {
|
|
223
|
-
distance: normals.distance,
|
|
224
|
-
nearFlatten: normals.nearFlatten,
|
|
225
|
-
farFlatten: normals.farFlatten,
|
|
226
|
-
useSmoothed: normals.useSmoothed,
|
|
227
|
-
normalGreenSign: normals.normalGreenSign,
|
|
228
|
-
},
|
|
264
|
+
// SPREAD, not an allow-list — the same trap the moss group was carrying.
|
|
265
|
+
// These two settings groups map onto identically named profile groups, so
|
|
266
|
+
// naming each field bought nothing and cost a silent drop for any field
|
|
267
|
+
// added later. See the longer note on `moss` below.
|
|
268
|
+
shoreline: { ...shoreline },
|
|
269
|
+
normals: { ...normals },
|
|
229
270
|
moss: {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
271
|
+
// SPREAD, not an allow-list. This mapping used to name every field, so
|
|
272
|
+
// a field added to the settings schema was silently dropped here —
|
|
273
|
+
// between validation and the shader — with no error anywhere. Adding a
|
|
274
|
+
// moss capability then required edits in four separate files and failed
|
|
275
|
+
// invisibly if you missed one. The settings object is already
|
|
276
|
+
// schema-validated and clamped by `createRockShaderSettings`, so
|
|
277
|
+
// forwarding it wholesale is both safer and correct; the entries below
|
|
278
|
+
// exist only to coerce types the profile wants in a different shape.
|
|
279
|
+
// `scripts/verify-rock-shader.mjs` asserts schema/profile parity so this
|
|
280
|
+
// cannot silently regress.
|
|
281
|
+
...moss,
|
|
236
282
|
lowColor: tintArray(moss.lowColor, [0.18, 0.28, 0.09]),
|
|
237
283
|
highColor: tintArray(moss.highColor, [0.42, 0.55, 0.2]),
|
|
238
284
|
},
|
|
285
|
+
// The layer groups DO get renamed on the way through — the settings schema
|
|
286
|
+
// exposes `layerMask` / `grassLayer` / `snowLayer` / `sandLayer` and the
|
|
287
|
+
// profile wants `layers.{maskEnabled, sharpness, offset, grass, snow,
|
|
288
|
+
// sand}` — so the group names are mapped explicitly. The FIELDS inside each
|
|
289
|
+
// group are spread, because those names are shared and an allow-list there
|
|
290
|
+
// is the same silent-drop trap the moss group was carrying: a new sand or
|
|
291
|
+
// grass field would pass validation, be clamped, and then vanish before the
|
|
292
|
+
// shader with no error at any layer. Only `useAssetMask` -> `maskEnabled`
|
|
293
|
+
// is genuinely renamed, and the `tint` entries coerce a shape the profile
|
|
294
|
+
// wants differently. `scripts/verify-rock-shader.mjs` asserts parity for
|
|
295
|
+
// every group so this cannot silently regress again.
|
|
239
296
|
layers: {
|
|
240
|
-
maskEnabled: layerMask.useAssetMask,
|
|
241
297
|
sharpness: layerMask.sharpness,
|
|
242
298
|
offset: layerMask.offset,
|
|
299
|
+
maskEnabled: layerMask.useAssetMask,
|
|
243
300
|
grass: {
|
|
244
|
-
|
|
245
|
-
useGroundShader: grassLayer.useGroundShader,
|
|
246
|
-
scale: grassLayer.scale,
|
|
301
|
+
...grassLayer,
|
|
247
302
|
tint: tintArray(grassLayer.tint, [0.48, 0.65, 0.3]),
|
|
248
|
-
saturation: grassLayer.saturation,
|
|
249
|
-
emission: grassLayer.emission,
|
|
250
303
|
},
|
|
251
304
|
snow: {
|
|
252
|
-
|
|
253
|
-
scale: snowLayer.scale,
|
|
305
|
+
...snowLayer,
|
|
254
306
|
tint: tintArray(snowLayer.tint, [0.9, 0.93, 0.96]),
|
|
255
|
-
saturation: snowLayer.saturation,
|
|
256
|
-
emission: snowLayer.emission,
|
|
257
307
|
},
|
|
258
308
|
sand: {
|
|
259
|
-
|
|
260
|
-
useGroundShader: sandLayer.useGroundShader,
|
|
261
|
-
scale: sandLayer.scale,
|
|
309
|
+
...sandLayer,
|
|
262
310
|
tint: tintArray(sandLayer.tint, [0.72, 0.59, 0.4]),
|
|
263
|
-
saturation: sandLayer.saturation,
|
|
264
|
-
emission: sandLayer.emission,
|
|
265
|
-
normalScale: sandLayer.normalScale,
|
|
266
|
-
normalStrength: sandLayer.normalStrength,
|
|
267
|
-
normalRotationDegrees: sandLayer.normalRotationDegrees,
|
|
268
311
|
},
|
|
269
312
|
},
|
|
270
313
|
});
|
|
@@ -294,20 +337,91 @@ function disposeOwnedMaterial(material) {
|
|
|
294
337
|
}
|
|
295
338
|
|
|
296
339
|
function createMaterialForSource({
|
|
340
|
+
vertexCavityStrength = 0,
|
|
341
|
+
rockRegionEnabled = false,
|
|
297
342
|
sourceMaterial,
|
|
298
343
|
settings,
|
|
299
344
|
profile,
|
|
300
345
|
textures,
|
|
301
346
|
name,
|
|
347
|
+
variation = 0,
|
|
348
|
+
degenerateMaps = null,
|
|
349
|
+
surfaceState = 'source',
|
|
302
350
|
}) {
|
|
303
|
-
const
|
|
351
|
+
const semantic = sourceMaterial?.userData?.toonlabRockSemanticMaterial ?? null;
|
|
352
|
+
const fallback = semantic ? {} : createRockShaderTextureSet({ variation });
|
|
353
|
+
// A placeholder in a real slot is worse than an empty slot: it silently
|
|
354
|
+
// displaces the deterministic fallback, which is a usable surface. Drop
|
|
355
|
+
// degenerate maps here so every consumer of the shader is protected, not
|
|
356
|
+
// only the ones that remembered to filter before calling.
|
|
357
|
+
const provided = withoutDegenerateDetailMaps(textures);
|
|
358
|
+
if (degenerateMaps && provided.rejected.length > 0) {
|
|
359
|
+
degenerateMaps.push(...provided.rejected);
|
|
360
|
+
}
|
|
304
361
|
const textureSet = {
|
|
305
362
|
...fallback,
|
|
306
|
-
...textures,
|
|
363
|
+
...(semantic?.textures ?? {}),
|
|
364
|
+
...provided.textures,
|
|
307
365
|
};
|
|
308
366
|
|
|
367
|
+
let materialProfile = profile;
|
|
368
|
+
if (semantic) {
|
|
369
|
+
materialProfile = structuredClone(semantic.profile);
|
|
370
|
+
// Scene state and coordinated lighting remain style-owned; source material
|
|
371
|
+
// identity (projection, PBR, masks, and layer inputs) remains set-owned.
|
|
372
|
+
materialProfile.lighting = structuredClone(profile.lighting);
|
|
373
|
+
materialProfile.shoreline = structuredClone(profile.shoreline);
|
|
374
|
+
materialProfile.base = {
|
|
375
|
+
...materialProfile.base,
|
|
376
|
+
closeTintDistance: profile.base.closeTintDistance,
|
|
377
|
+
distantTint: profile.base.distantTint,
|
|
378
|
+
distantTintMix: profile.base.distantTintMix,
|
|
379
|
+
farTintDistance: profile.base.farTintDistance,
|
|
380
|
+
nearDetailDistance: profile.base.nearDetailDistance,
|
|
381
|
+
nearDetailScale: profile.base.nearDetailScale,
|
|
382
|
+
nearDetailStrength: profile.base.nearDetailStrength,
|
|
383
|
+
};
|
|
384
|
+
const selected = surfaceState === 'source'
|
|
385
|
+
? null
|
|
386
|
+
: ['none', 'grass', 'sand', 'snow'].includes(surfaceState)
|
|
387
|
+
? surfaceState
|
|
388
|
+
: (() => { throw new TypeError(`Unknown rock surface state: ${surfaceState}`); })();
|
|
389
|
+
if (selected !== null) {
|
|
390
|
+
for (const role of ['grass', 'sand', 'snow']) {
|
|
391
|
+
materialProfile.layers[role].enabled = selected === role;
|
|
392
|
+
}
|
|
393
|
+
if (selected !== 'none') {
|
|
394
|
+
// An explicit replacement state is a ToonLab-authored cap, not the
|
|
395
|
+
// source graph's often-disabled top-layer default. Keep all four mask
|
|
396
|
+
// signals active while using a production-visible coverage range.
|
|
397
|
+
materialProfile.layers.maskEnabled = true;
|
|
398
|
+
materialProfile.layers.offset = 0.2;
|
|
399
|
+
materialProfile.layers.sharpness = 4;
|
|
400
|
+
materialProfile.layers.heightStart = 0.15;
|
|
401
|
+
materialProfile.layers.heightEnd = 0.7;
|
|
402
|
+
materialProfile.layers.cavityStrength = Math.max(
|
|
403
|
+
0.2,
|
|
404
|
+
Number(materialProfile.layers.cavityStrength ?? 0),
|
|
405
|
+
);
|
|
406
|
+
materialProfile.layers.exposureStrength = Math.max(
|
|
407
|
+
0.2,
|
|
408
|
+
Number(materialProfile.layers.exposureStrength ?? 0),
|
|
409
|
+
);
|
|
410
|
+
materialProfile.layers.semanticMaskStrength = Math.max(
|
|
411
|
+
0.15,
|
|
412
|
+
Number(materialProfile.layers.semanticMaskStrength ?? 0),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (selected !== 'none' && !textureSet[selected]?.isTexture) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Rock material set ${semantic.materialSetId} has no faithful ${selected} layer input.`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
309
423
|
const sourceAlbedoMode = settings.assetIntegration.sourceAlbedoMode;
|
|
310
|
-
const sourceAlbedoStrength = sourceAlbedoMode === 'retain'
|
|
424
|
+
const sourceAlbedoStrength = semantic ? 0 : sourceAlbedoMode === 'retain'
|
|
311
425
|
? 1
|
|
312
426
|
: sourceAlbedoMode === 'blend'
|
|
313
427
|
? settings.assetIntegration.sourceAlbedoStrength
|
|
@@ -316,11 +430,27 @@ function createMaterialForSource({
|
|
|
316
430
|
textureSet.sourceRock = sourceMaterial.map;
|
|
317
431
|
}
|
|
318
432
|
if (settings.assetIntegration.sourceNormalStrength > 0 && sourceMaterial?.normalMap) {
|
|
319
|
-
|
|
433
|
+
const sourceNormals = withoutDegenerateDetailMaps({
|
|
434
|
+
sourceNormal: sourceMaterial.normalMap,
|
|
435
|
+
});
|
|
436
|
+
if (sourceNormals.textures.sourceNormal) {
|
|
437
|
+
textureSet.sourceNormal = sourceNormals.textures.sourceNormal;
|
|
438
|
+
} else if (degenerateMaps) {
|
|
439
|
+
degenerateMaps.push(...sourceNormals.rejected);
|
|
440
|
+
}
|
|
320
441
|
}
|
|
321
442
|
if (sourceAlbedoStrength > 0 && sourceMaterial?.roughnessMap) {
|
|
322
443
|
textureSet.sourceRoughness = sourceMaterial.roughnessMap;
|
|
323
444
|
}
|
|
445
|
+
if (sourceAlbedoStrength > 0 && sourceMaterial?.metalnessMap) {
|
|
446
|
+
textureSet.sourceMetalness = sourceMaterial.metalnessMap;
|
|
447
|
+
}
|
|
448
|
+
if (sourceAlbedoStrength > 0 && sourceMaterial?.specularIntensityMap) {
|
|
449
|
+
textureSet.sourceSpecular = sourceMaterial.specularIntensityMap;
|
|
450
|
+
}
|
|
451
|
+
if (sourceAlbedoStrength > 0 && sourceMaterial?.emissiveMap) {
|
|
452
|
+
textureSet.sourceEmissive = sourceMaterial.emissiveMap;
|
|
453
|
+
}
|
|
324
454
|
if (settings.assetIntegration.sourceAoStrength > 0 && sourceMaterial?.aoMap) {
|
|
325
455
|
textureSet.sourceAo = sourceMaterial.aoMap;
|
|
326
456
|
}
|
|
@@ -329,8 +459,34 @@ function createMaterialForSource({
|
|
|
329
459
|
assetIntegration: {
|
|
330
460
|
...settings.assetIntegration,
|
|
331
461
|
sourceAlbedoStrength,
|
|
462
|
+
sourceColor: sourceMaterial?.color?.toArray?.() ?? [1, 1, 1],
|
|
463
|
+
sourceEmissive: sourceMaterial?.emissive?.toArray?.() ?? [0, 0, 0],
|
|
464
|
+
sourceEmissiveIntensity: Number(sourceMaterial?.emissiveIntensity ?? 1),
|
|
465
|
+
sourceOpacity: Number(sourceMaterial?.opacity ?? 1),
|
|
466
|
+
sourceAlphaTest: Number(sourceMaterial?.alphaTest ?? 0),
|
|
467
|
+
sourceTransparent: Boolean(sourceMaterial?.transparent),
|
|
468
|
+
sourceDepthWrite: sourceMaterial?.depthWrite !== false,
|
|
469
|
+
sourceSide: Number.isInteger(sourceMaterial?.side) ? sourceMaterial.side : null,
|
|
470
|
+
sourceMetalnessFactor: Number(sourceMaterial?.metalness ?? 0),
|
|
471
|
+
sourceRoughnessFactor: Number(sourceMaterial?.roughness ?? 1),
|
|
472
|
+
sourceSpecularFactor: Number(sourceMaterial?.specularIntensity ?? 1),
|
|
473
|
+
primaryNormalStrength: Number(
|
|
474
|
+
semantic?.binding?.materialVariation?.normalGain ?? 1,
|
|
475
|
+
),
|
|
476
|
+
primarySpecularStrength: Number(
|
|
477
|
+
semantic?.binding?.materialVariation?.specularGain ?? 1,
|
|
478
|
+
),
|
|
479
|
+
semanticMaskAttributes: Boolean(semantic),
|
|
480
|
+
rockRegionAttributes: rockRegionEnabled,
|
|
481
|
+
regionTintStrength: settings.assetIntegration.regionTintStrength,
|
|
482
|
+
regionBaseTint: settings.assetIntegration.regionBaseTint,
|
|
483
|
+
regionShaftTint: settings.assetIntegration.regionShaftTint,
|
|
484
|
+
regionNeckTint: settings.assetIntegration.regionNeckTint,
|
|
485
|
+
regionCapTint: settings.assetIntegration.regionCapTint,
|
|
486
|
+
regionNeckOverlayStrength: settings.assetIntegration.regionNeckOverlayStrength,
|
|
487
|
+
vertexCavityStrength,
|
|
332
488
|
},
|
|
333
|
-
profile,
|
|
489
|
+
profile: materialProfile,
|
|
334
490
|
textures: textureSet,
|
|
335
491
|
name,
|
|
336
492
|
});
|
|
@@ -338,21 +494,78 @@ function createMaterialForSource({
|
|
|
338
494
|
rockMaterial.userData.toonLabRockShaderPreset = settings.preset;
|
|
339
495
|
rockMaterial.userData.toonLabRockTextureSource = textures?.rock
|
|
340
496
|
? 'provided'
|
|
341
|
-
:
|
|
497
|
+
: semantic
|
|
498
|
+
? 'shared-semantic-material-set'
|
|
499
|
+
: 'first-party-generated';
|
|
342
500
|
rockMaterial.userData.toonlabSourceTextureIds = [...new Set([
|
|
501
|
+
...Object.values(semantic?.textures ?? {}),
|
|
343
502
|
textureSet.sourceRock,
|
|
344
503
|
textureSet.sourceNormal,
|
|
345
504
|
textureSet.sourceRoughness,
|
|
505
|
+
textureSet.sourceMetalness,
|
|
506
|
+
textureSet.sourceSpecular,
|
|
507
|
+
textureSet.sourceEmissive,
|
|
346
508
|
textureSet.sourceAo,
|
|
347
509
|
].filter((texture) => texture?.isTexture && texture.uuid).map((texture) => texture.uuid))];
|
|
348
510
|
rockMaterial.userData.toonLabRockTextureComposition = {
|
|
349
|
-
base: textures?.rock
|
|
511
|
+
base: textures?.rock
|
|
512
|
+
? 'provided'
|
|
513
|
+
: semantic
|
|
514
|
+
? 'shared-semantic-material-set'
|
|
515
|
+
: 'first-party-generated',
|
|
350
516
|
sourceAlbedoMode,
|
|
351
517
|
sourceAlbedoStrength,
|
|
352
518
|
sourceNormalStrength: settings.assetIntegration.sourceNormalStrength,
|
|
353
519
|
sourceAoStrength: settings.assetIntegration.sourceAoStrength,
|
|
520
|
+
sourcePbrChannels: {
|
|
521
|
+
primaryAo: Boolean(textureSet.rockAo),
|
|
522
|
+
albedo: Boolean(textureSet.sourceRock),
|
|
523
|
+
alphaCutout: Boolean(textureSet.sourceRock && sourceMaterial?.alphaTest > 0),
|
|
524
|
+
emissive: Boolean(textureSet.sourceEmissive),
|
|
525
|
+
metallicRoughness: Boolean(textureSet.sourceMetalness || textureSet.sourceRoughness),
|
|
526
|
+
normal: Boolean(textureSet.sourceNormal),
|
|
527
|
+
occlusion: Boolean(textureSet.sourceAo),
|
|
528
|
+
specular: Boolean(textureSet.sourceSpecular),
|
|
529
|
+
},
|
|
354
530
|
sourceTextureCount: rockMaterial.userData.toonlabSourceTextureIds.length,
|
|
355
531
|
};
|
|
532
|
+
if (semantic) {
|
|
533
|
+
rockMaterial.userData.toonLabRockTextureComposition.materialSetId = semantic.materialSetId;
|
|
534
|
+
rockMaterial.userData.toonLabRockTextureComposition.surfaceState = surfaceState;
|
|
535
|
+
rockMaterial.userData.toonLabRockTextureComposition.sourcePbrChannels = {
|
|
536
|
+
albedo: Boolean(textureSet.rock),
|
|
537
|
+
normal: Boolean(textureSet.rockNormal),
|
|
538
|
+
smoothness: Boolean(textureSet.smoothness),
|
|
539
|
+
topMask: Boolean(textureSet.topMask),
|
|
540
|
+
grass: Boolean(textureSet.grass),
|
|
541
|
+
grassRoughness: Boolean(textureSet.grassRoughness),
|
|
542
|
+
sand: Boolean(textureSet.sand),
|
|
543
|
+
sandNormal: Boolean(textureSet.sandNormal),
|
|
544
|
+
snow: Boolean(textureSet.snow),
|
|
545
|
+
moss: Boolean(textureSet.moss),
|
|
546
|
+
};
|
|
547
|
+
const activeLayer = ['grass', 'snow', 'sand']
|
|
548
|
+
.filter((role) => materialProfile.layers?.[role]?.enabled)
|
|
549
|
+
.at(-1) ?? 'rock';
|
|
550
|
+
rockMaterial.userData.toonlabRockActiveSurfaceResponse = {
|
|
551
|
+
activeLayer,
|
|
552
|
+
color: activeLayer === 'rock' ? textureSet.rock?.uuid : textureSet[activeLayer]?.uuid,
|
|
553
|
+
normal: activeLayer === 'sand' && materialProfile.layers.sand.normalStrength > 0
|
|
554
|
+
? textureSet.sandNormal?.uuid
|
|
555
|
+
: activeLayer === 'rock'
|
|
556
|
+
? textureSet.rockNormal?.uuid
|
|
557
|
+
: null,
|
|
558
|
+
roughness: activeLayer === 'grass' && textureSet.grassRoughness
|
|
559
|
+
? textureSet.grassRoughness.uuid
|
|
560
|
+
: materialProfile.layers?.[activeLayer]?.roughness ?? null,
|
|
561
|
+
};
|
|
562
|
+
rockMaterial.userData.toonlabRockProjectionContract = {
|
|
563
|
+
normalBlend: 'whiteout-world-to-tangent',
|
|
564
|
+
primary: 'world-space-triplanar',
|
|
565
|
+
scaleCompensation: 'absolute-world-position',
|
|
566
|
+
texelDensityInvariantUnderNonUniformScale: true,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
356
569
|
rockMaterial.userData.environmentShaderExclude = true;
|
|
357
570
|
return markFactoryStyleMaterial(rockMaterial, 'RockSurface');
|
|
358
571
|
}
|
|
@@ -362,6 +575,8 @@ export function createRockShaderMaterial({
|
|
|
362
575
|
textures = {},
|
|
363
576
|
sourceMaterial = null,
|
|
364
577
|
name = 'Call Me Sensei Rock',
|
|
578
|
+
variation = 0,
|
|
579
|
+
surfaceState = 'source',
|
|
365
580
|
} = {}) {
|
|
366
581
|
const settings = createRockShaderSettings(input);
|
|
367
582
|
return createMaterialForSource({
|
|
@@ -369,6 +584,8 @@ export function createRockShaderMaterial({
|
|
|
369
584
|
settings,
|
|
370
585
|
profile: rockShaderSettingsToProfile(settings),
|
|
371
586
|
textures,
|
|
587
|
+
variation,
|
|
588
|
+
surfaceState,
|
|
372
589
|
name,
|
|
373
590
|
});
|
|
374
591
|
}
|
|
@@ -384,19 +601,65 @@ export function applyRockShader(root, input = {}, {
|
|
|
384
601
|
name = 'Call Me Sensei Rock',
|
|
385
602
|
castShadow = true,
|
|
386
603
|
receiveShadow = true,
|
|
604
|
+
variation = 0,
|
|
605
|
+
surfaceState = 'source',
|
|
606
|
+
detail = null,
|
|
387
607
|
} = {}) {
|
|
388
608
|
const settings = createRockShaderSettings(input);
|
|
609
|
+
const rockRegionEnabled = settings.assetIntegration.regionTintStrength > 0;
|
|
610
|
+
if (rockRegionEnabled && detail) {
|
|
611
|
+
throw new Error('Rock-region styling cannot run with geometry detail; rebuild semantic masks after geometry changes.');
|
|
612
|
+
}
|
|
613
|
+
const rockRegionInspection = rockRegionEnabled
|
|
614
|
+
? inspectRockRegionBindings(root, { requireEveryMesh: true, sampleValues: true })
|
|
615
|
+
: null;
|
|
616
|
+
// Geometry detail runs before anything reads or writes vertex attributes:
|
|
617
|
+
// subdivision replaces every buffer, so a color/AO attribute built first
|
|
618
|
+
// would be sized to the old vertex count. Opt-in — most placements are not
|
|
619
|
+
// close enough to need it, and it multiplies triangles by 4^subdivisions.
|
|
620
|
+
const geometryDetail = detail
|
|
621
|
+
? applyRockGeometryDetail(root, { variation, ...(detail === true ? {} : detail) })
|
|
622
|
+
: null;
|
|
623
|
+
// A present-but-zero tangent attribute is worse than no tangent attribute:
|
|
624
|
+
// TSL trusts it, builds an invalid TBN frame, and normal-mapped facets become
|
|
625
|
+
// literal black. Removing only invalid tangent attributes lets Three derive
|
|
626
|
+
// its guarded derivative frame while leaving geometry and UVs unchanged.
|
|
627
|
+
const tangentIntegrity = sanitizeRockTangents(root);
|
|
628
|
+
// Only bind the moss cavity channel when EVERY enriched mesh actually carries
|
|
629
|
+
// the attribute. A material sampling an attribute that some geometry lacks is
|
|
630
|
+
// a hard shader failure, so this stays off unless the write is complete.
|
|
631
|
+
const vertexCavityStrength = geometryDetail
|
|
632
|
+
&& geometryDetail.meshes > 0
|
|
633
|
+
&& geometryDetail.cavity === geometryDetail.meshes
|
|
634
|
+
? ROCK_MOSS_CAVITY_STRENGTH
|
|
635
|
+
: 0;
|
|
389
636
|
const profile = rockShaderSettingsToProfile(settings);
|
|
390
637
|
const retainedSourceTextureIds = new Set();
|
|
638
|
+
const degenerateMaps = [];
|
|
391
639
|
const report = {
|
|
392
640
|
preset: settings.preset,
|
|
393
641
|
matched: 0,
|
|
394
642
|
applied: 0,
|
|
395
643
|
skipped: 0,
|
|
396
644
|
textureSource: textures?.rock ? 'provided' : 'first-party-generated',
|
|
397
|
-
usedGeneratedTextures:
|
|
645
|
+
usedGeneratedTextures: false,
|
|
398
646
|
shadowDefaultsApplied: 0,
|
|
399
647
|
retainedSourceTextures: 0,
|
|
648
|
+
variation,
|
|
649
|
+
// Slots dropped because the supplied map was too small to carry detail.
|
|
650
|
+
// Surfaced rather than silenced: a dropped map changes the render.
|
|
651
|
+
rejectedTextures: degenerateMaps,
|
|
652
|
+
// Tessellation/displacement actually applied, or null when not requested.
|
|
653
|
+
// Reported because it changes the triangle budget by up to 64x.
|
|
654
|
+
geometryDetail,
|
|
655
|
+
tangentIntegrity,
|
|
656
|
+
rockRegions: rockRegionInspection ? {
|
|
657
|
+
meshCount: rockRegionInspection.meshCount,
|
|
658
|
+
totalVertices: rockRegionInspection.totalVertices,
|
|
659
|
+
totalBytes: rockRegionInspection.totalBytes,
|
|
660
|
+
attribute: '_tl_rock_region',
|
|
661
|
+
} : null,
|
|
662
|
+
surfaceState,
|
|
400
663
|
};
|
|
401
664
|
|
|
402
665
|
root?.traverse?.((object) => {
|
|
@@ -428,13 +691,24 @@ export function applyRockShader(root, input = {}, {
|
|
|
428
691
|
disposeOwnedMaterial(object.material);
|
|
429
692
|
const sourceMaterials = Array.isArray(originalMaterial) ? originalMaterial : [originalMaterial];
|
|
430
693
|
const nextMaterials = sourceMaterials.map((sourceMaterial, index) => createMaterialForSource({
|
|
694
|
+
degenerateMaps,
|
|
695
|
+
vertexCavityStrength,
|
|
696
|
+
rockRegionEnabled,
|
|
431
697
|
sourceMaterial,
|
|
432
698
|
settings,
|
|
433
699
|
profile,
|
|
434
700
|
textures,
|
|
701
|
+
variation,
|
|
702
|
+
surfaceState,
|
|
435
703
|
name: sourceMaterials.length > 1 ? `${name} ${index + 1}` : name,
|
|
436
704
|
}));
|
|
437
705
|
for (const material of nextMaterials) {
|
|
706
|
+
const baseSource = material.userData.toonLabRockTextureComposition?.base;
|
|
707
|
+
if (baseSource === 'first-party-generated') {
|
|
708
|
+
report.usedGeneratedTextures = true;
|
|
709
|
+
} else if (baseSource === 'shared-semantic-material-set') {
|
|
710
|
+
report.textureSource = 'shared-semantic-material-set';
|
|
711
|
+
}
|
|
438
712
|
for (const textureId of material.userData.toonlabSourceTextureIds ?? []) {
|
|
439
713
|
retainedSourceTextureIds.add(textureId);
|
|
440
714
|
}
|
|
@@ -472,7 +746,7 @@ export function restoreRockShader(root) {
|
|
|
472
746
|
}
|
|
473
747
|
|
|
474
748
|
/** Updates current scene inputs without changing the portable rock preset. */
|
|
475
|
-
export function setRockShaderSceneState(root, { waterLevel } = {}) {
|
|
749
|
+
export function setRockShaderSceneState(root, { skyColor, waterLevel } = {}) {
|
|
476
750
|
let updated = 0;
|
|
477
751
|
root?.traverse?.((object) => {
|
|
478
752
|
if (!object?.isMesh) return;
|
|
@@ -480,15 +754,28 @@ export function setRockShaderSceneState(root, { waterLevel } = {}) {
|
|
|
480
754
|
for (const material of materials) {
|
|
481
755
|
const state = material?.userData?.toonLabRockSceneState;
|
|
482
756
|
if (!state) continue;
|
|
757
|
+
if (skyColor !== undefined) state.setSkyColor(skyColor);
|
|
483
758
|
if (Number.isFinite(Number(waterLevel))) state.setWaterLevel(Number(waterLevel));
|
|
484
759
|
updated += 1;
|
|
485
760
|
}
|
|
486
761
|
});
|
|
487
|
-
|
|
762
|
+
const resolvedSkyColor = Array.isArray(skyColor) || ArrayBuffer.isView(skyColor)
|
|
763
|
+
? Array.from(skyColor).slice(0, 3).map(Number)
|
|
764
|
+
: skyColor?.isColor
|
|
765
|
+
? [skyColor.r, skyColor.g, skyColor.b]
|
|
766
|
+
: null;
|
|
767
|
+
return {
|
|
768
|
+
skyColor: resolvedSkyColor?.length === 3 && resolvedSkyColor.every(Number.isFinite)
|
|
769
|
+
? resolvedSkyColor
|
|
770
|
+
: null,
|
|
771
|
+
updated,
|
|
772
|
+
waterLevel: Number.isFinite(Number(waterLevel)) ? Number(waterLevel) : null,
|
|
773
|
+
};
|
|
488
774
|
}
|
|
489
775
|
|
|
490
776
|
export function disposeDefaultRockShaderTextures() {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
777
|
+
for (const set of textureSetsByVariation.values()) {
|
|
778
|
+
for (const texture of Object.values(set)) texture.dispose();
|
|
779
|
+
}
|
|
780
|
+
textureSetsByVariation.clear();
|
|
494
781
|
}
|