@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
|
@@ -45,7 +45,7 @@ export const DEFAULT_CHARACTER_ANIMATION_ROLES = Object.freeze([
|
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
47
|
function stage(callback, name, detail = {}) {
|
|
48
|
-
callback?.({ detail, stage: name });
|
|
48
|
+
return callback?.({ detail, stage: name });
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function throwIfAborted(signal) {
|
|
@@ -137,6 +137,116 @@ function prepareNeutralCharacterSource(root, renderer) {
|
|
|
137
137
|
return replacements.size;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
export const CHARACTER_MATERIAL_MODES = Object.freeze({
|
|
141
|
+
neutral: 'neutral',
|
|
142
|
+
toon: 'toon',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
function normalizeCharacterMaterialMode(mode) {
|
|
146
|
+
return mode === CHARACTER_MATERIAL_MODES.neutral
|
|
147
|
+
? CHARACTER_MATERIAL_MODES.neutral
|
|
148
|
+
: CHARACTER_MATERIAL_MODES.toon;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isToonHelperMesh(object) {
|
|
152
|
+
return object?.userData?.isToonOutline === true || object?.userData?.isToonFurShell === true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function collectCharacterBodyMeshes(root) {
|
|
156
|
+
const meshes = [];
|
|
157
|
+
root.traverse((object) => {
|
|
158
|
+
if (!object?.isMesh || !object.material || isToonHelperMesh(object)) return;
|
|
159
|
+
meshes.push(object);
|
|
160
|
+
});
|
|
161
|
+
return meshes;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Builds a second, neutral material set over an already-converted character so
|
|
166
|
+
* a single load can present both looks.
|
|
167
|
+
*
|
|
168
|
+
* `prepareNeutralCharacterSource` mounts its neutral materials destructively and
|
|
169
|
+
* disposes the imported ones, which is right for a one-look runtime and wrong for
|
|
170
|
+
* a comparison: §11-style shader wipes must share one skeleton, one mixer and one
|
|
171
|
+
* set of geometry buffers, so the two looks can only differ by which `mesh.material`
|
|
172
|
+
* reference is bound at draw time. This builds the neutral set from the retained
|
|
173
|
+
* imported materials without mounting or disposing anything, so both sets stay live
|
|
174
|
+
* and swapping is a reference assignment rather than a rebuild.
|
|
175
|
+
*/
|
|
176
|
+
function createCharacterMaterialModeState({
|
|
177
|
+
integration,
|
|
178
|
+
originalsByMesh,
|
|
179
|
+
renderer,
|
|
180
|
+
root,
|
|
181
|
+
}) {
|
|
182
|
+
const usesNodeMaterials = renderer?.isWebGPURenderer === true;
|
|
183
|
+
const neutralReplacements = new Map();
|
|
184
|
+
const neutralByMesh = new Map();
|
|
185
|
+
const toonByMesh = new Map();
|
|
186
|
+
const neutralBeforeRender = new Map();
|
|
187
|
+
const toonBeforeRender = new Map();
|
|
188
|
+
const helpers = [];
|
|
189
|
+
|
|
190
|
+
root.traverse((object) => {
|
|
191
|
+
if (isToonHelperMesh(object)) helpers.push(object);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
for (const [mesh, originalMaterial] of originalsByMesh) {
|
|
195
|
+
const replace = (source) => {
|
|
196
|
+
if (!source) return source;
|
|
197
|
+
if (!usesNodeMaterials) return source;
|
|
198
|
+
if (!neutralReplacements.has(source)) {
|
|
199
|
+
neutralReplacements.set(source, createNeutralCharacterMaterial(source));
|
|
200
|
+
}
|
|
201
|
+
return neutralReplacements.get(source);
|
|
202
|
+
};
|
|
203
|
+
neutralByMesh.set(mesh, Array.isArray(originalMaterial)
|
|
204
|
+
? originalMaterial.map(replace)
|
|
205
|
+
: replace(originalMaterial));
|
|
206
|
+
toonByMesh.set(mesh, mesh.material);
|
|
207
|
+
toonBeforeRender.set(mesh, mesh.onBeforeRender);
|
|
208
|
+
neutralBeforeRender.set(
|
|
209
|
+
mesh,
|
|
210
|
+
usesNodeMaterials && mesh.isSkinnedMesh
|
|
211
|
+
? () => updateToonStorageSkinning(mesh)
|
|
212
|
+
: THREE.Object3D.prototype.onBeforeRender,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let mode = CHARACTER_MATERIAL_MODES.toon;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
disposeNeutral() {
|
|
220
|
+
for (const material of neutralReplacements.values()) material.dispose?.();
|
|
221
|
+
neutralReplacements.clear();
|
|
222
|
+
},
|
|
223
|
+
get mode() {
|
|
224
|
+
return mode;
|
|
225
|
+
},
|
|
226
|
+
neutralMaterialCount: usesNodeMaterials ? neutralReplacements.size : originalsByMesh.size,
|
|
227
|
+
set(nextMode) {
|
|
228
|
+
const resolved = normalizeCharacterMaterialMode(nextMode);
|
|
229
|
+
if (resolved === mode) return mode;
|
|
230
|
+
const neutral = resolved === CHARACTER_MATERIAL_MODES.neutral;
|
|
231
|
+
const source = neutral ? neutralByMesh : toonByMesh;
|
|
232
|
+
const beforeRender = neutral ? neutralBeforeRender : toonBeforeRender;
|
|
233
|
+
for (const [mesh, material] of source) {
|
|
234
|
+
mesh.material = material;
|
|
235
|
+
mesh.onBeforeRender = beforeRender.get(mesh);
|
|
236
|
+
}
|
|
237
|
+
// Outline and fur shells are separate child meshes, never removed — the
|
|
238
|
+
// toon look owns them and the neutral look must not draw them.
|
|
239
|
+
for (const helper of helpers) helper.visible = !neutral;
|
|
240
|
+
mode = resolved;
|
|
241
|
+
// The depth prepass and self-shadow target bind concrete materials; they
|
|
242
|
+
// must follow the swap or the neutral half shades against toon buffers.
|
|
243
|
+
integration?.refresh?.();
|
|
244
|
+
return mode;
|
|
245
|
+
},
|
|
246
|
+
usesNodeMaterials,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
140
250
|
function attachCharacterStyleMetadata(carrier, {
|
|
141
251
|
managed,
|
|
142
252
|
styleTarget,
|
|
@@ -181,6 +291,7 @@ function attachCharacterStyleMetadata(carrier, {
|
|
|
181
291
|
export async function createCharacterRuntime({
|
|
182
292
|
animation = true,
|
|
183
293
|
carrier = null,
|
|
294
|
+
materialModes = false,
|
|
184
295
|
materialUrl = null,
|
|
185
296
|
name = 'ToonLab Character',
|
|
186
297
|
onStage = null,
|
|
@@ -203,12 +314,12 @@ export async function createCharacterRuntime({
|
|
|
203
314
|
characterCarrier.name ||= name;
|
|
204
315
|
let attached = false;
|
|
205
316
|
|
|
206
|
-
stage(onStage, CHARACTER_RUNTIME_STAGE.MODEL, { url });
|
|
317
|
+
await stage(onStage, CHARACTER_RUNTIME_STAGE.MODEL, { url });
|
|
207
318
|
const asset = await loadModelAsset(url, { materialUrl, renderer });
|
|
208
319
|
throwIfAborted(signal);
|
|
209
320
|
|
|
210
321
|
try {
|
|
211
|
-
stage(onStage, CHARACTER_RUNTIME_STAGE.TEXTURES, { asset, url });
|
|
322
|
+
await stage(onStage, CHARACTER_RUNTIME_STAGE.TEXTURES, { asset, url });
|
|
212
323
|
await waitForObjectTextures(asset.root);
|
|
213
324
|
throwIfAborted(signal);
|
|
214
325
|
setObjectTextureColorSpaces(asset.root);
|
|
@@ -218,7 +329,14 @@ export async function createCharacterRuntime({
|
|
|
218
329
|
: null;
|
|
219
330
|
bakeSolidBaseColorTextures(asset.root);
|
|
220
331
|
|
|
221
|
-
stage(onStage, CHARACTER_RUNTIME_STAGE.STYLE, { asset, bounds, url });
|
|
332
|
+
await stage(onStage, CHARACTER_RUNTIME_STAGE.STYLE, { asset, bounds, url });
|
|
333
|
+
// A dual-mode runtime keeps the imported materials alive so the neutral set
|
|
334
|
+
// can be rebuilt from them after conversion; the single-mode path stays
|
|
335
|
+
// exactly as before, including the destructive neutral swap.
|
|
336
|
+
const wantsMaterialModes = materialModes === true && toon !== false;
|
|
337
|
+
const originalsByMesh = wantsMaterialModes
|
|
338
|
+
? new Map(collectCharacterBodyMeshes(asset.root).map((mesh) => [mesh, mesh.material]))
|
|
339
|
+
: null;
|
|
222
340
|
const neutralSourceMaterialCount = toon === false
|
|
223
341
|
? prepareNeutralCharacterSource(asset.root, renderer)
|
|
224
342
|
: 0;
|
|
@@ -231,7 +349,7 @@ export async function createCharacterRuntime({
|
|
|
231
349
|
let animationSource = 'none';
|
|
232
350
|
|
|
233
351
|
if (animationOptions.enabled) {
|
|
234
|
-
stage(onStage, CHARACTER_RUNTIME_STAGE.ANIMATION, { asset, rig, targetMesh, url });
|
|
352
|
+
await stage(onStage, CHARACTER_RUNTIME_STAGE.ANIMATION, { asset, rig, targetMesh, url });
|
|
235
353
|
const nativeRoles = resolveNativeLocomotionClips(asset.clips) ?? {};
|
|
236
354
|
clipRoles = keepRequestedRoles(nativeRoles, animationOptions.roles);
|
|
237
355
|
animationSource = Object.keys(clipRoles).length ? 'native' : 'none';
|
|
@@ -243,6 +361,11 @@ export async function createCharacterRuntime({
|
|
|
243
361
|
retargetMode: animationOptions.retargetMode,
|
|
244
362
|
rootMotion: animationOptions.rootMotion,
|
|
245
363
|
sourceUrl: animationOptions.fallbackSourceUrl,
|
|
364
|
+
// This runtime owns one mixer rooted at the imported model scene,
|
|
365
|
+
// so packaged clips must bind named bone nodes just like native
|
|
366
|
+
// glTF clips. The lower-level loader keeps its skeleton-rooted
|
|
367
|
+
// default for callers that mix directly on a SkinnedMesh.
|
|
368
|
+
trackNameStyle: 'node',
|
|
246
369
|
});
|
|
247
370
|
throwIfAborted(signal);
|
|
248
371
|
for (const role of missingRoles) {
|
|
@@ -295,6 +418,15 @@ export async function createCharacterRuntime({
|
|
|
295
418
|
};
|
|
296
419
|
characterCarrier.userData.toonlabCharacterStyleIntegration = characterStyleIntegration;
|
|
297
420
|
|
|
421
|
+
const materialModeState = originalsByMesh
|
|
422
|
+
? createCharacterMaterialModeState({
|
|
423
|
+
integration: characterStyleIntegration,
|
|
424
|
+
originalsByMesh,
|
|
425
|
+
renderer,
|
|
426
|
+
root: asset.root,
|
|
427
|
+
})
|
|
428
|
+
: null;
|
|
429
|
+
|
|
298
430
|
const mixer = Object.keys(clipRoles).length
|
|
299
431
|
? new THREE.AnimationMixer(asset.root)
|
|
300
432
|
: null;
|
|
@@ -317,9 +449,32 @@ export async function createCharacterRuntime({
|
|
|
317
449
|
rig,
|
|
318
450
|
targetMesh,
|
|
319
451
|
toonState,
|
|
320
|
-
neutralSourceMaterialCount
|
|
452
|
+
neutralSourceMaterialCount: materialModeState
|
|
453
|
+
? materialModeState.neutralMaterialCount
|
|
454
|
+
: neutralSourceMaterialCount,
|
|
321
455
|
url,
|
|
322
456
|
|
|
457
|
+
/**
|
|
458
|
+
* `'neutral' | 'toon'` when the runtime was created with
|
|
459
|
+
* `materialModes: true`, otherwise `null`. Reading it is cheap; it is the
|
|
460
|
+
* authoritative record of which material set is currently bound.
|
|
461
|
+
*/
|
|
462
|
+
get materialMode() {
|
|
463
|
+
return materialModeState?.mode ?? null;
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Binds one of the two material sets over the same meshes, skeleton and
|
|
468
|
+
* mixer. Returns the resolved mode, or `null` on a single-mode runtime.
|
|
469
|
+
*
|
|
470
|
+
* Cheap enough to call between two `renderer.render` calls in one frame,
|
|
471
|
+
* which is what a §11-style scissor wipe needs.
|
|
472
|
+
*/
|
|
473
|
+
setMaterialMode(mode) {
|
|
474
|
+
if (disposed || !materialModeState) return null;
|
|
475
|
+
return materialModeState.set(mode);
|
|
476
|
+
},
|
|
477
|
+
|
|
323
478
|
applyToonSettings(settings) {
|
|
324
479
|
if (disposed || !toonState) return null;
|
|
325
480
|
return applyToonSettingsToMaterial(asset.root, settings);
|
|
@@ -327,6 +482,19 @@ export async function createCharacterRuntime({
|
|
|
327
482
|
|
|
328
483
|
dispose({ disposeResources = true } = {}) {
|
|
329
484
|
if (disposed) return;
|
|
485
|
+
// Rebind the toon set so the unmounted half is the one this method
|
|
486
|
+
// disposes explicitly and `disposeLoadedRoot` still sees a complete
|
|
487
|
+
// mounted material set.
|
|
488
|
+
materialModeState?.set(CHARACTER_MATERIAL_MODES.toon);
|
|
489
|
+
if (disposeResources && materialModeState) {
|
|
490
|
+
// The neutral bridge materials, then the imported ones they were built
|
|
491
|
+
// from. When the renderer needs no bridge the two are the same objects
|
|
492
|
+
// and the second pass is a no-op re-dispose.
|
|
493
|
+
materialModeState.disposeNeutral();
|
|
494
|
+
for (const material of originalsByMesh.values()) {
|
|
495
|
+
for (const entry of Array.isArray(material) ? material : [material]) entry?.dispose?.();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
330
498
|
disposed = true;
|
|
331
499
|
mixer?.stopAllAction();
|
|
332
500
|
mixer?.uncacheRoot(asset.root);
|
|
@@ -363,7 +531,7 @@ export async function createCharacterRuntime({
|
|
|
363
531
|
},
|
|
364
532
|
};
|
|
365
533
|
|
|
366
|
-
stage(onStage, CHARACTER_RUNTIME_STAGE.READY, { runtime, url });
|
|
534
|
+
await stage(onStage, CHARACTER_RUNTIME_STAGE.READY, { runtime, url });
|
|
367
535
|
return runtime;
|
|
368
536
|
} catch (error) {
|
|
369
537
|
if (attached) {
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
setEnvironmentDebugOutput,
|
|
34
34
|
} from './environmentShaderMaterials.js';
|
|
35
35
|
import { resolveEnvironmentPreset } from './environmentPresets.js';
|
|
36
|
+
import { installToonLabSurfaceLighting } from './toonLabSurfaceLighting.js';
|
|
36
37
|
import {
|
|
37
38
|
resolveEnvironmentTextureSet,
|
|
38
39
|
} from './environmentTextureResolver.js';
|
|
@@ -213,6 +214,23 @@ export async function applyEnvironmentShader(root, {
|
|
|
213
214
|
scenario = undefined,
|
|
214
215
|
settings = {},
|
|
215
216
|
shaderMode = 'anime',
|
|
217
|
+
// Surface-lighting model options (D19-150). A material this adapter converts
|
|
218
|
+
// now JOINS the ToonLab surface-lighting model instead of being left outside
|
|
219
|
+
// it. Before this, `applyEnvironmentShader` reported a material as converted
|
|
220
|
+
// while never setting `userData.toonLabSurfaceLighting` — the exact flag every
|
|
221
|
+
// scene guards on when adopting its own shadow fill:
|
|
222
|
+
//
|
|
223
|
+
// if (!material?.userData?.toonLabSurfaceLighting) continue; // missed it
|
|
224
|
+
//
|
|
225
|
+
// so a scene's shadow fill silently reached every first-party surface and
|
|
226
|
+
// skipped every dropped-in asset. Nothing warned; the adoption loop simply
|
|
227
|
+
// reported a lower count. The divergence grows with how much fill the scene
|
|
228
|
+
// adopts, which is what made it severe rather than cosmetic.
|
|
229
|
+
//
|
|
230
|
+
// Defaults are the installer's own defaults (shadowFill 0), so installing
|
|
231
|
+
// here changes no existing capture — it only makes the material present in
|
|
232
|
+
// the model, which is what lets a scene's adoption loop find it afterwards.
|
|
233
|
+
surfaceLighting = {},
|
|
216
234
|
openWindows = false,
|
|
217
235
|
vertexAoOptions = {},
|
|
218
236
|
} = {}) {
|
|
@@ -258,6 +276,8 @@ export async function applyEnvironmentShader(root, {
|
|
|
258
276
|
: 'anime';
|
|
259
277
|
const resolvedDebugMode = normalizeEnvironmentDebugMode(debugOutputMode);
|
|
260
278
|
let convertedMeshCount = 0;
|
|
279
|
+
// D19-150: how many converted materials actually joined the lighting model.
|
|
280
|
+
let surfaceLightingMaterialCount = 0;
|
|
261
281
|
let aoOverlayMeshCount = 0;
|
|
262
282
|
let windowCutoutMaterialCount = 0;
|
|
263
283
|
let shadowMeshCount = 0;
|
|
@@ -409,6 +429,15 @@ export async function applyEnvironmentShader(root, {
|
|
|
409
429
|
|
|
410
430
|
for (const mat of toMaterialArray(obj.material)) {
|
|
411
431
|
if (mat?.userData?.windowCutout) windowCutoutMaterialCount += 1;
|
|
432
|
+
// D19-150: enrol every converted environment material in the surface
|
|
433
|
+
// lighting model. `installToonLabSurfaceLighting` is a no-op on anything
|
|
434
|
+
// that is not a NodeMaterial and preserves prior options when re-run, so
|
|
435
|
+
// this is safe both for materials that already carry it and for the
|
|
436
|
+
// scene-side adoption pass that re-installs with an authored shadowFill.
|
|
437
|
+
if (mat?.isNodeMaterial && mat.userData?.environmentMaterial) {
|
|
438
|
+
installToonLabSurfaceLighting(mat, surfaceLighting);
|
|
439
|
+
if (mat.userData?.toonLabSurfaceLighting) surfaceLightingMaterialCount += 1;
|
|
440
|
+
}
|
|
412
441
|
}
|
|
413
442
|
if (toMaterialArray(obj.material).some((mat) => mat?.userData?.environmentMaterial)) {
|
|
414
443
|
obj.onBeforeRender = environmentNodeLightSync;
|
|
@@ -430,6 +459,7 @@ export async function applyEnvironmentShader(root, {
|
|
|
430
459
|
aoOverlayMeshCount,
|
|
431
460
|
classification,
|
|
432
461
|
convertedMeshCount,
|
|
462
|
+
surfaceLightingMaterialCount,
|
|
433
463
|
manifestWarnings: manifestResult?.warnings ?? [],
|
|
434
464
|
manufacturedAssetId: resolvedAssetId,
|
|
435
465
|
manufacturedObjectClass: resolvedObjectClass,
|
|
@@ -660,6 +660,96 @@ registerEnvironmentPreset('call_me_sensei', {
|
|
|
660
660
|
},
|
|
661
661
|
},
|
|
662
662
|
},
|
|
663
|
+
// The finish axis is the one an audience actually reads at a glance:
|
|
664
|
+
// brushed stainless next to charcoal powder coat is the same base material
|
|
665
|
+
// and must not look like the same surface. Without these profiles the
|
|
666
|
+
// finish was validated by the contract, stored on the material, and then
|
|
667
|
+
// had no effect on the frame, so `metal`/`brushed` converted to the same
|
|
668
|
+
// response as `mineral`/`raw`. Resolution order puts finishes AFTER
|
|
669
|
+
// baseMaterials, so a finish refines its base rather than fighting it.
|
|
670
|
+
finishes: {
|
|
671
|
+
anodized: {
|
|
672
|
+
parameters: {
|
|
673
|
+
specularShininess: 70,
|
|
674
|
+
specularSoftness: 0.3,
|
|
675
|
+
specularStrength: 0.34,
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
brushed: {
|
|
679
|
+
// Anisotropic metal: a broad, smeared highlight (low shininess, wide
|
|
680
|
+
// softness) rather than a point hit, plus a real sky pickup — that
|
|
681
|
+
// cool skylight in the mid-tones is what says "metal" in an anime
|
|
682
|
+
// frame, not the metalness channel the toon shader does not have.
|
|
683
|
+
parameters: {
|
|
684
|
+
skyTintStrength: 0.34,
|
|
685
|
+
specularColor: [0.86, 0.9, 1],
|
|
686
|
+
specularShininess: 26,
|
|
687
|
+
specularSoftness: 0.42,
|
|
688
|
+
specularStrength: 0.55,
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
clearCoated: {
|
|
692
|
+
parameters: {
|
|
693
|
+
specularShininess: 140,
|
|
694
|
+
specularSoftness: 0.14,
|
|
695
|
+
specularStrength: 0.42,
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
glazed: {
|
|
699
|
+
parameters: {
|
|
700
|
+
specularShininess: 120,
|
|
701
|
+
specularSoftness: 0.16,
|
|
702
|
+
specularStrength: 0.46,
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
matte: {
|
|
706
|
+
parameters: {
|
|
707
|
+
specularShininess: 10,
|
|
708
|
+
specularSoftness: 0.55,
|
|
709
|
+
specularStrength: 0.03,
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
mirror: {
|
|
713
|
+
parameters: {
|
|
714
|
+
skyTintStrength: 0.6,
|
|
715
|
+
specularShininess: 240,
|
|
716
|
+
specularSoftness: 0.08,
|
|
717
|
+
specularStrength: 0.9,
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
// Powder coat and enamel are dielectric films: a soft, broad sheen that
|
|
721
|
+
// must stay clearly below `brushed` so painted metal and bare metal
|
|
722
|
+
// separate at a glance.
|
|
723
|
+
painted: {
|
|
724
|
+
parameters: {
|
|
725
|
+
specularShininess: 34,
|
|
726
|
+
specularSoftness: 0.34,
|
|
727
|
+
specularStrength: 0.14,
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
polished: {
|
|
731
|
+
parameters: {
|
|
732
|
+
skyTintStrength: 0.45,
|
|
733
|
+
specularShininess: 180,
|
|
734
|
+
specularSoftness: 0.1,
|
|
735
|
+
specularStrength: 0.7,
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
raw: {
|
|
739
|
+
parameters: {
|
|
740
|
+
specularShininess: 12,
|
|
741
|
+
specularSoftness: 0.5,
|
|
742
|
+
specularStrength: 0.04,
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
varnished: {
|
|
746
|
+
parameters: {
|
|
747
|
+
specularShininess: 90,
|
|
748
|
+
specularSoftness: 0.2,
|
|
749
|
+
specularStrength: 0.3,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
},
|
|
663
753
|
objectClasses: {
|
|
664
754
|
buildingExterior: {
|
|
665
755
|
parameters: { normalMapStrength: 0.66 },
|
|
@@ -61,6 +61,13 @@ export function createEnvironmentSunShadowPass({ renderer, scene } = {}) {
|
|
|
61
61
|
const warmedCasterSources = new WeakMap();
|
|
62
62
|
let cachedSunLight = null;
|
|
63
63
|
let renderCount = 0;
|
|
64
|
+
// Real health, not "the object exists". D19-041 shipped a Gate 3 check
|
|
65
|
+
// written as `Boolean(runtime.shadowPass)`, which is true from construction
|
|
66
|
+
// and therefore cannot fail. These are the three things that actually have
|
|
67
|
+
// to be true for a receiver to see a cast shadow.
|
|
68
|
+
let lastCasterCount = 0;
|
|
69
|
+
let lastSkippedNonCasterCount = 0;
|
|
70
|
+
let lastFailure = 'pass has not run';
|
|
64
71
|
let casterDepthSamplePoints = Object.freeze([]);
|
|
65
72
|
let casterCoverage = Object.freeze({
|
|
66
73
|
byDomain: Object.freeze({}),
|
|
@@ -345,6 +352,8 @@ export function createEnvironmentSunShadowPass({ renderer, scene } = {}) {
|
|
|
345
352
|
if (!sun) {
|
|
346
353
|
environmentSunShadow.ready.value = false;
|
|
347
354
|
environmentSunShadow.farReady.value = false;
|
|
355
|
+
lastCasterCount = 0;
|
|
356
|
+
lastFailure = 'no visible DirectionalLight with castShadow in the scene';
|
|
348
357
|
return;
|
|
349
358
|
}
|
|
350
359
|
|
|
@@ -359,6 +368,7 @@ export function createEnvironmentSunShadowPass({ renderer, scene } = {}) {
|
|
|
359
368
|
if (needsPrimaryRenderWarmup) {
|
|
360
369
|
// The host's normal scene render follows this scheduled pass. Retain any
|
|
361
370
|
// existing map for one frame and include the new caster on the next one.
|
|
371
|
+
lastFailure = 'warming up a newly added caster';
|
|
362
372
|
return;
|
|
363
373
|
}
|
|
364
374
|
|
|
@@ -557,6 +567,9 @@ export function createEnvironmentSunShadowPass({ renderer, scene } = {}) {
|
|
|
557
567
|
renderer.shadowMap.enabled = previousShadowAutoUpdate;
|
|
558
568
|
scene.background = previousBackground;
|
|
559
569
|
|
|
570
|
+
lastCasterCount = materialRestores.length;
|
|
571
|
+
lastSkippedNonCasterCount = visibilityRestores.length;
|
|
572
|
+
lastFailure = lastCasterCount > 0 ? null : 'no visible castShadow mesh was drawn into the map';
|
|
560
573
|
for (const { material, mesh } of materialRestores) mesh.material = material;
|
|
561
574
|
for (const obj of visibilityRestores) obj.visible = true;
|
|
562
575
|
materialRestores.length = 0;
|
|
@@ -607,12 +620,41 @@ export function createEnvironmentSunShadowPass({ renderer, scene } = {}) {
|
|
|
607
620
|
get ready() {
|
|
608
621
|
return environmentSunShadow.ready.value === true;
|
|
609
622
|
},
|
|
623
|
+
/**
|
|
624
|
+
* Whether the pass is actually producing a usable shadow map, and why not
|
|
625
|
+
* when it is not. Hosts gating a review on shadows must read `ok` here
|
|
626
|
+
* rather than testing that the pass object exists (D19-041).
|
|
627
|
+
*/
|
|
628
|
+
get health() {
|
|
629
|
+
const ready = environmentSunShadow.ready.value === true;
|
|
630
|
+
const ok = ready && renderCount > 0 && lastCasterCount > 0;
|
|
631
|
+
return Object.freeze({
|
|
632
|
+
backend: isNodeBackend ? 'node' : 'classic-webgl-unused',
|
|
633
|
+
casterCount: lastCasterCount,
|
|
634
|
+
farReady: environmentSunShadow.farReady.value === true,
|
|
635
|
+
hiddenNonCasterCount: lastSkippedNonCasterCount,
|
|
636
|
+
ok,
|
|
637
|
+
ready,
|
|
638
|
+
reason: ok ? null : (lastFailure ?? (ready ? null : 'pass has not published a map')),
|
|
639
|
+
renderCount,
|
|
640
|
+
sunName: cachedSunLight?.name ?? null,
|
|
641
|
+
});
|
|
642
|
+
},
|
|
610
643
|
get renderCount() {
|
|
611
644
|
return renderCount;
|
|
612
645
|
},
|
|
613
646
|
get shadowTexture() {
|
|
614
647
|
return shadowTarget?.texture ?? null;
|
|
615
648
|
},
|
|
649
|
+
// Render targets, for diagnostics that need to read the depth the pass
|
|
650
|
+
// actually wrote (D19-041/D19-062). `inspectDepthContent` only samples
|
|
651
|
+
// labelled style targets; these let a caller probe any world point.
|
|
652
|
+
get nearShadowTarget() {
|
|
653
|
+
return shadowTarget;
|
|
654
|
+
},
|
|
655
|
+
get farShadowTarget() {
|
|
656
|
+
return farShadowTarget;
|
|
657
|
+
},
|
|
616
658
|
get shadowMatrix() {
|
|
617
659
|
return shadowMatrix;
|
|
618
660
|
},
|