@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
|
@@ -118,6 +118,8 @@ export const TOONLAB_SURFACE_LIGHTING_CONTRACT = Object.freeze({
|
|
|
118
118
|
directDiffuse: 'radiance * BRDFData.diffuse (no 1/PI)',
|
|
119
119
|
indirectDiffuse: 'bakedGI * BRDFData.diffuse (no 1/PI)',
|
|
120
120
|
directSpecular: 'TOONLAB DirectBRDFSpecular optimized GGX',
|
|
121
|
+
shadowFill:
|
|
122
|
+
'authored fraction of the sun retained where the package sun-shadow pass reports occlusion (0 = TOONLAB literal)',
|
|
121
123
|
defaultInputAdapter: DEFAULT_INPUT_ADAPTER.id,
|
|
122
124
|
inputNormalization:
|
|
123
125
|
'direct radiance and cosine-convolved indirect irradiance are normalized independently',
|
|
@@ -149,6 +151,7 @@ const frozenColor = (value) => Object.freeze([...value]);
|
|
|
149
151
|
/** CPU oracle for the diffuse-only direct/indirect input decomposition. */
|
|
150
152
|
export function evaluateToonLabSurfaceDiffuseDecomposition({
|
|
151
153
|
brdfDiffuse = [1, 1, 1],
|
|
154
|
+
directStrength = 1,
|
|
152
155
|
directInput = [0, 0, 0],
|
|
153
156
|
indirectInput = [0, 0, 0],
|
|
154
157
|
inputAdapter = DEFAULT_INPUT_ADAPTER.id,
|
|
@@ -161,8 +164,9 @@ export function evaluateToonLabSurfaceDiffuseDecomposition({
|
|
|
161
164
|
const sourceCosine = Number(nDotL);
|
|
162
165
|
if (!Number.isFinite(sourceCosine)) throw new TypeError('nDotL must be finite.');
|
|
163
166
|
const cosine = Math.min(1, Math.max(0, sourceCosine));
|
|
167
|
+
const resolvedDirectStrength = Math.max(Number(directStrength) || 0, 0);
|
|
164
168
|
const directRadiance = direct.map(
|
|
165
|
-
(channel) => channel * adapter.directNormalization * cosine,
|
|
169
|
+
(channel) => channel * adapter.directNormalization * resolvedDirectStrength * cosine,
|
|
166
170
|
);
|
|
167
171
|
const indirectBakedGi = indirect.map(
|
|
168
172
|
(channel) => channel * adapter.indirectNormalization,
|
|
@@ -224,21 +228,46 @@ function toonLabBrdfInputs(workflow, specularF0Node, diffuseAlphaNode = float(1)
|
|
|
224
228
|
export class ToonLabSurfaceLightingModel extends PhysicalLightingModel {
|
|
225
229
|
constructor({
|
|
226
230
|
diffuseAlphaNode = float(1),
|
|
231
|
+
directStrength = 1,
|
|
227
232
|
indirectStrength = 1,
|
|
228
233
|
indirectTint = [1, 1, 1],
|
|
234
|
+
indirectTintNode = null,
|
|
235
|
+
indirectReplaceTintNode = null,
|
|
236
|
+
indirectTintBlendNode = null,
|
|
237
|
+
indirectTintMode = 'multiply',
|
|
229
238
|
inputAdapter = DEFAULT_INPUT_ADAPTER.id,
|
|
230
239
|
perceptualRoughnessNode = materialRoughness,
|
|
240
|
+
shadowFill = 0,
|
|
241
|
+
shadowFillTint = [1, 1, 1],
|
|
242
|
+
useSharedSunShadow = true,
|
|
231
243
|
specularF0Node = materialSpecularColor,
|
|
232
244
|
workflow = 'metallic',
|
|
233
245
|
} = {}) {
|
|
234
246
|
super(false, false, false, false, false, false);
|
|
235
247
|
this.diffuseAlphaNode = diffuseAlphaNode;
|
|
248
|
+
this.directStrength = float(Math.max(Number(directStrength) || 0, 0));
|
|
236
249
|
this.indirectStrength = float(Math.max(Number(indirectStrength) || 0, 0));
|
|
237
|
-
|
|
250
|
+
// A static tint is sufficient for authored indoor materials, but outdoor
|
|
251
|
+
// surfaces need the active sky colour to remain live as time-of-day and
|
|
252
|
+
// weather change. Accepting a node here lets callers bind that scene state
|
|
253
|
+
// without rebuilding the material or baking a permanent daylight-blue cast.
|
|
254
|
+
this.indirectTint = indirectTintNode
|
|
255
|
+
? vec3(indirectTintNode)
|
|
256
|
+
: vec3(...indirectTint);
|
|
257
|
+
this.indirectReplaceTint = indirectReplaceTintNode
|
|
258
|
+
? vec3(indirectReplaceTintNode)
|
|
259
|
+
: this.indirectTint;
|
|
260
|
+
this.indirectTintBlend = indirectTintBlendNode ?? float(0);
|
|
261
|
+
this.indirectTintMode = ['replace-chroma', 'sky-adaptive'].includes(indirectTintMode)
|
|
262
|
+
? indirectTintMode
|
|
263
|
+
: 'multiply';
|
|
238
264
|
this.inputAdapter = resolveToonLabSurfaceInputAdapter(inputAdapter);
|
|
239
265
|
this.perceptualRoughnessNode = perceptualRoughnessNode;
|
|
240
|
-
this.
|
|
266
|
+
this.shadowFill = Math.min(Math.max(Number(shadowFill) || 0, 0), 1);
|
|
267
|
+
this.shadowFillTint = shadowFillTint;
|
|
268
|
+
this.useSharedSunShadow = useSharedSunShadow !== false;
|
|
241
269
|
this.workflow = workflow === 'specular' ? 'specular' : 'metallic';
|
|
270
|
+
this.specularF0Node = specularF0Node;
|
|
242
271
|
}
|
|
243
272
|
|
|
244
273
|
direct({ lightDirection, lightColor, lightNode, reflectedLight }) {
|
|
@@ -248,12 +277,33 @@ export class ToonLabSurfaceLightingModel extends PhysicalLightingModel {
|
|
|
248
277
|
// TOONLAB's no-PI BRDF convention.
|
|
249
278
|
// Three can carry analytic light attenuation as RGBA for transmitted
|
|
250
279
|
// coloured shadows. TOONLAB's Light.color is RGB, so consume RGB explicitly.
|
|
251
|
-
|
|
280
|
+
// The package sun-shadow sample is a hard 0/1 mask. Applied raw it removes
|
|
281
|
+
// the ENTIRE direct term on an occluded fragment, and a Call Me Sensei rig
|
|
282
|
+
// has no ambient light behind it — only the SH sky probe, whose measured
|
|
283
|
+
// radiance is R:G:B ~ 1 : 2.2 : 5.3. A fully occluded ToonLab surface
|
|
284
|
+
// therefore renders as saturated navy with no value structure at all
|
|
285
|
+
// (D19-062). `shadowFill` keeps an authored, tintable fraction of the sun
|
|
286
|
+
// in shadow, which is the shadow-lift the environment node material has
|
|
287
|
+
// always had and this bridge did not. Default 0 — an existing caller
|
|
288
|
+
// resolves to exactly the previous expression.
|
|
289
|
+
const hasToonLabSunContract = Boolean(
|
|
290
|
+
lightNode?.light?.shadow?.toonLabLightingContract,
|
|
291
|
+
);
|
|
292
|
+
const sunVisibility = hasToonLabSunContract && this.useSharedSunShadow
|
|
252
293
|
? sampleEnvironmentSunShadow(positionWorld)
|
|
253
|
-
.mul(sampleEnvironmentCloudShadow(positionWorld, 1))
|
|
254
294
|
: float(1);
|
|
295
|
+
// Cloud transmittance remains shared even when a material relies on the
|
|
296
|
+
// native directional shadow. This avoids double/self-shadowing while every
|
|
297
|
+
// outdoor surface still sees the same moving cloud field.
|
|
298
|
+
const rawSunVisibility = hasToonLabSunContract
|
|
299
|
+
? sunVisibility.mul(sampleEnvironmentCloudShadow(positionWorld, 1))
|
|
300
|
+
: float(1);
|
|
301
|
+
const sharedSunVisibility = this.shadowFill > 0
|
|
302
|
+
? mix(float(this.shadowFill).mul(vec3(...this.shadowFillTint)), vec3(1), rawSunVisibility)
|
|
303
|
+
: rawSunVisibility;
|
|
255
304
|
const toonLabLightColor = vec3(lightColor)
|
|
256
305
|
.mul(this.inputAdapter.directNormalization)
|
|
306
|
+
.mul(this.directStrength)
|
|
257
307
|
.mul(sharedSunVisibility);
|
|
258
308
|
const nDotL = clamp(dot(normalView, lightDirection), 0, 1);
|
|
259
309
|
// Materialize the shadowed light once. CSM attenuation contains mutable
|
|
@@ -307,12 +357,24 @@ export class ToonLabSurfaceLightingModel extends PhysicalLightingModel {
|
|
|
307
357
|
this.specularF0Node,
|
|
308
358
|
this.diffuseAlphaNode,
|
|
309
359
|
);
|
|
360
|
+
const adaptedIrradiance = irradiance
|
|
361
|
+
.mul(this.inputAdapter.indirectNormalization)
|
|
362
|
+
.mul(this.indirectStrength);
|
|
363
|
+
// Outdoor probes already contain colour. Multiplying a cool sunset probe
|
|
364
|
+
// by an orange horizon collapses both toward dark purple. Rocks instead
|
|
365
|
+
// retain the probe's directional luminance and receive chroma from the live
|
|
366
|
+
// visible sky, which is the scene state the player actually sees.
|
|
367
|
+
const multipliedIrradiance = adaptedIrradiance.mul(this.indirectTint);
|
|
368
|
+
const replacedIrradiance = vec3(
|
|
369
|
+
dot(adaptedIrradiance, vec3(0.2126, 0.7152, 0.0722)),
|
|
370
|
+
).mul(this.indirectReplaceTint);
|
|
371
|
+
const tintedIrradiance = this.indirectTintMode === 'replace-chroma'
|
|
372
|
+
? replacedIrradiance
|
|
373
|
+
: this.indirectTintMode === 'sky-adaptive'
|
|
374
|
+
? mix(multipliedIrradiance, replacedIrradiance, this.indirectTintBlend)
|
|
375
|
+
: multipliedIrradiance;
|
|
310
376
|
reflectedLight.indirectDiffuse.addAssign(
|
|
311
|
-
|
|
312
|
-
.mul(this.inputAdapter.indirectNormalization)
|
|
313
|
-
.mul(this.indirectStrength)
|
|
314
|
-
.mul(this.indirectTint)
|
|
315
|
-
.mul(brdf.diffuse),
|
|
377
|
+
tintedIrradiance.mul(brdf.diffuse),
|
|
316
378
|
);
|
|
317
379
|
}
|
|
318
380
|
|
|
@@ -334,6 +396,9 @@ export function installToonLabSurfaceLighting(material, options = {}) {
|
|
|
334
396
|
const workflow = options.workflow ?? prior?.workflow ?? 'metallic';
|
|
335
397
|
const resolvedWorkflow = workflow === 'specular' ? 'specular' : 'metallic';
|
|
336
398
|
const diffuseAlphaNode = options.diffuseAlphaNode ?? float(1);
|
|
399
|
+
const directStrength = Math.max(Number(
|
|
400
|
+
options.directStrength ?? prior?.directStrength ?? 1,
|
|
401
|
+
) || 0, 0);
|
|
337
402
|
const indirectStrength = Math.max(Number(
|
|
338
403
|
options.indirectStrength ?? prior?.indirectStrength ?? 1,
|
|
339
404
|
) || 0, 0);
|
|
@@ -341,12 +406,37 @@ export function installToonLabSurfaceLighting(material, options = {}) {
|
|
|
341
406
|
options.indirectTint ?? prior?.indirectTint ?? [1, 1, 1],
|
|
342
407
|
'indirectTint',
|
|
343
408
|
);
|
|
409
|
+
const indirectTintNode = options.indirectTintNode ?? null;
|
|
410
|
+
const indirectReplaceTintNode = options.indirectReplaceTintNode ?? null;
|
|
411
|
+
const indirectTintBlendNode = options.indirectTintBlendNode ?? null;
|
|
412
|
+
const requestedIndirectTintMode = options.indirectTintMode ?? prior?.indirectTintMode;
|
|
413
|
+
const indirectTintMode = ['replace-chroma', 'sky-adaptive'].includes(requestedIndirectTintMode)
|
|
414
|
+
? requestedIndirectTintMode
|
|
415
|
+
: 'multiply';
|
|
416
|
+
const shadowFill = Math.min(Math.max(Number(
|
|
417
|
+
options.shadowFill ?? prior?.shadowFill ?? 0,
|
|
418
|
+
) || 0, 0), 1);
|
|
419
|
+
const shadowFillTint = finiteColor(
|
|
420
|
+
options.shadowFillTint ?? prior?.shadowFillTint ?? [1, 1, 1],
|
|
421
|
+
'shadowFillTint',
|
|
422
|
+
);
|
|
423
|
+
const useSharedSunShadow = options.useSharedSunShadow
|
|
424
|
+
?? prior?.useSharedSunShadow
|
|
425
|
+
?? true;
|
|
344
426
|
material.setupLightingModel = () => new ToonLabSurfaceLightingModel({
|
|
345
427
|
diffuseAlphaNode,
|
|
428
|
+
directStrength,
|
|
346
429
|
indirectStrength,
|
|
347
430
|
indirectTint,
|
|
431
|
+
indirectTintNode,
|
|
432
|
+
indirectReplaceTintNode,
|
|
433
|
+
indirectTintBlendNode,
|
|
434
|
+
indirectTintMode,
|
|
348
435
|
inputAdapter,
|
|
349
436
|
perceptualRoughnessNode: material.roughnessNode ?? materialRoughness,
|
|
437
|
+
shadowFill,
|
|
438
|
+
shadowFillTint,
|
|
439
|
+
useSharedSunShadow,
|
|
350
440
|
specularF0Node: material.specularColorNode ?? materialSpecularColor,
|
|
351
441
|
workflow: resolvedWorkflow,
|
|
352
442
|
});
|
|
@@ -354,9 +444,17 @@ export function installToonLabSurfaceLighting(material, options = {}) {
|
|
|
354
444
|
...TOONLAB_SURFACE_LIGHTING_CONTRACT,
|
|
355
445
|
inputAdapter: inputAdapter.id,
|
|
356
446
|
inputAdapterContract: { ...inputAdapter },
|
|
447
|
+
directStrength,
|
|
357
448
|
indirectStrength,
|
|
358
449
|
indirectTint: [...indirectTint],
|
|
450
|
+
indirectTintSource: indirectTintNode ? 'dynamic-node' : 'static',
|
|
451
|
+
indirectReplaceTintSource: indirectReplaceTintNode ? 'dynamic-node' : 'shared',
|
|
452
|
+
indirectTintBlendSource: indirectTintBlendNode ? 'dynamic-node' : 'static-zero',
|
|
453
|
+
indirectTintMode,
|
|
359
454
|
preserveSpecularAlpha: options.diffuseAlphaNode != null,
|
|
455
|
+
shadowFill,
|
|
456
|
+
shadowFillTint: [...shadowFillTint],
|
|
457
|
+
useSharedSunShadow: useSharedSunShadow !== false,
|
|
360
458
|
workflow: resolvedWorkflow,
|
|
361
459
|
};
|
|
362
460
|
material.needsUpdate = true;
|
|
@@ -203,6 +203,27 @@ function surfaceProfile(base, overrides) {
|
|
|
203
203
|
|
|
204
204
|
const SURFACE_PROFILES = Object.freeze({
|
|
205
205
|
...LEGACY_SURFACE_PROFILES,
|
|
206
|
+
// `bareMetal` is the weathered-scrap look: it discards the source albedo's
|
|
207
|
+
// hue AND value (both authority scales 0) and paints the surface from
|
|
208
|
+
// `bareMetalColor`, then piles on rust. That is right for an untextured or
|
|
209
|
+
// scanned prop, and wrong for an authored brushed-stainless map — the
|
|
210
|
+
// anisotropy an artist baked into the albedo simply never reached the frame,
|
|
211
|
+
// so `metal`/`brushed` rendered as one flat panel. `brushed` is its own
|
|
212
|
+
// finish in the contract and now gets its own profile: the metal response
|
|
213
|
+
// (role hue, fresnel, planar sheen, low light cap) is kept, the authored
|
|
214
|
+
// VALUE structure is honoured, and the scrapyard wear is dialled out.
|
|
215
|
+
// `raw`/`polished`/`anodized`/`mirror` still resolve to `bareMetal`.
|
|
216
|
+
brushedMetal: surfaceProfile(LEGACY_SURFACE_PROFILES.bareMetal, {
|
|
217
|
+
highlightScale: 0.16,
|
|
218
|
+
lightValueCap: 0.46,
|
|
219
|
+
normalScale: 0.9,
|
|
220
|
+
planarSheenScale: 0.5,
|
|
221
|
+
responseValueCap: 0.78,
|
|
222
|
+
sharpRustBoost: 0.5,
|
|
223
|
+
sourceHueAuthorityScale: 0,
|
|
224
|
+
sourceValueAuthorityScale: 1,
|
|
225
|
+
wearScale: 0.45,
|
|
226
|
+
}),
|
|
206
227
|
coatedPanel: LEGACY_SURFACE_PROFILES.lid,
|
|
207
228
|
genericDielectric: surfaceProfile(LEGACY_SURFACE_PROFILES.paintedMetal, {
|
|
208
229
|
colorLiftScale: 0.42,
|
|
@@ -345,6 +366,33 @@ const SURFACE_PROFILES = Object.freeze({
|
|
|
345
366
|
planarSheenScale: 0,
|
|
346
367
|
viewReflectionScale: 0.01,
|
|
347
368
|
}),
|
|
369
|
+
// A diffusing sheet — shoji paper, a paper lantern, a fabric shade — lit
|
|
370
|
+
// from the far side. `MANUFACTURED_RENDER_MODES` has carried `translucent`
|
|
371
|
+
// and `transmissive` since v1 and the manifest validates them, but nothing
|
|
372
|
+
// downstream did anything with them: a paper screen converted to the same
|
|
373
|
+
// opaque toon surface as a poster, and the single read that makes a teahouse
|
|
374
|
+
// look like a teahouse — warm interior light through paper, with the lattice
|
|
375
|
+
// dark against it — was not expressible. See D19-079.
|
|
376
|
+
//
|
|
377
|
+
// The response is deliberately NOT physical transmission. A diffuser is not
|
|
378
|
+
// a window: you do not see through it, you see it lit. So the term is a
|
|
379
|
+
// view-independent glow, tinted by `translucencyColor`, modulated by the
|
|
380
|
+
// paper's own albedo (a stain or a fibre inclusion must read darker when
|
|
381
|
+
// backlit, which is exactly what makes washi look like washi), softened
|
|
382
|
+
// toward grazing angles, and NOT gated by the sun shadow, because the light
|
|
383
|
+
// behind the screen is not the sun.
|
|
384
|
+
paperTranslucent: surfaceProfile(LEGACY_SURFACE_PROFILES.graphicPanel, {
|
|
385
|
+
fallbackMetalness: 0,
|
|
386
|
+
fresnelScale: 0.04,
|
|
387
|
+
highlightScale: 0.02,
|
|
388
|
+
lightValueCap: 1.16,
|
|
389
|
+
materialResponseScale: 0.08,
|
|
390
|
+
normalScale: 0.3,
|
|
391
|
+
planarSheenScale: 0,
|
|
392
|
+
translucencyScale: 1,
|
|
393
|
+
viewReflectionScale: 0.01,
|
|
394
|
+
wearScale: 0.04,
|
|
395
|
+
}),
|
|
348
396
|
composite: surfaceProfile(LEGACY_SURFACE_PROFILES.technicalSurface, {
|
|
349
397
|
fallbackMetalness: 0.08,
|
|
350
398
|
fresnelScale: 0.58,
|
|
@@ -737,6 +785,11 @@ export function createUrbanPropShaderControls(palette = 'source') {
|
|
|
737
785
|
sourceAuthorityEnabled: numberControl(1),
|
|
738
786
|
sourceAuthorityStrength: numberControl(1),
|
|
739
787
|
trimColor: colorControl(selected.trimColor),
|
|
788
|
+
// Diffusing-sheet response (shoji, paper lantern, fabric shade). Only
|
|
789
|
+
// profiles that declare `translucencyScale` read these.
|
|
790
|
+
translucencyColor: colorControl(0xffd7a0),
|
|
791
|
+
translucencyEnabled: numberControl(1),
|
|
792
|
+
translucencyStrength: numberControl(0.85),
|
|
740
793
|
viewReflectionEnabled: numberControl(1),
|
|
741
794
|
viewReflectionStrength: numberControl(0.62),
|
|
742
795
|
wearEnabled: numberControl(1),
|
|
@@ -1042,7 +1095,10 @@ export function resolveUrbanMaterialProfile(classification) {
|
|
|
1042
1095
|
}
|
|
1043
1096
|
switch (classification?.baseMaterial) {
|
|
1044
1097
|
case 'metal':
|
|
1045
|
-
|
|
1098
|
+
// Brushed is the one bare-metal finish that normally arrives with an
|
|
1099
|
+
// authored anisotropic map; it keeps that map's value structure.
|
|
1100
|
+
if (classification.finish === 'brushed') return 'brushedMetal';
|
|
1101
|
+
if (['raw', 'polished', 'anodized', 'mirror'].includes(
|
|
1046
1102
|
classification.finish,
|
|
1047
1103
|
)) {
|
|
1048
1104
|
return 'bareMetal';
|
|
@@ -1069,7 +1125,12 @@ export function resolveUrbanMaterialProfile(classification) {
|
|
|
1069
1125
|
case 'leather':
|
|
1070
1126
|
return 'leather';
|
|
1071
1127
|
case 'paper':
|
|
1072
|
-
|
|
1128
|
+
// A backlit diffusing sheet is a different surface from a poster, and
|
|
1129
|
+
// renderMode is where the contract already says so.
|
|
1130
|
+
return classification.renderMode === 'translucent'
|
|
1131
|
+
|| classification.renderMode === 'transmissive'
|
|
1132
|
+
? 'paperTranslucent'
|
|
1133
|
+
: 'paper';
|
|
1073
1134
|
case 'composite':
|
|
1074
1135
|
return 'composite';
|
|
1075
1136
|
case 'fluid':
|
|
@@ -2602,6 +2663,24 @@ export function createUrbanAnimePropMaterial(sourceMaterial, {
|
|
|
2602
2663
|
emissive: 0x020508,
|
|
2603
2664
|
gradientMap: createLockedGradientMap(),
|
|
2604
2665
|
});
|
|
2666
|
+
// WebGL counterpart of the WebGPU diffusing-sheet term. Without a node graph
|
|
2667
|
+
// the view easing is not available, so this is the flat form: the sheet's own
|
|
2668
|
+
// colour tinted by translucencyColor, added as emission.
|
|
2669
|
+
const translucencyScale = Number(profile.translucencyScale ?? 0);
|
|
2670
|
+
const applyTranslucency = () => {
|
|
2671
|
+
if (!(translucencyScale > 0)) return;
|
|
2672
|
+
const amount = THREE.MathUtils.clamp(
|
|
2673
|
+
Number(shared.translucencyEnabled?.value ?? 0)
|
|
2674
|
+
* Number(shared.translucencyStrength?.value ?? 0)
|
|
2675
|
+
* translucencyScale,
|
|
2676
|
+
0,
|
|
2677
|
+
2,
|
|
2678
|
+
);
|
|
2679
|
+
material.emissive
|
|
2680
|
+
.copy(shared.translucencyColor?.value ?? new THREE.Color(0xffd7a0))
|
|
2681
|
+
.multiply(material.color)
|
|
2682
|
+
.multiplyScalar(amount);
|
|
2683
|
+
};
|
|
2605
2684
|
material.name = `Locked urban · ${profileId} · ${source?.name ?? 'material'}`;
|
|
2606
2685
|
material.map = source?.map ?? null;
|
|
2607
2686
|
material.normalMap = source?.normalMap ?? null;
|
|
@@ -2637,6 +2716,7 @@ export function createUrbanAnimePropMaterial(sourceMaterial, {
|
|
|
2637
2716
|
material.gradientMap = shared.celLightingEnabled.value > 0.5
|
|
2638
2717
|
? createLockedGradientMap()
|
|
2639
2718
|
: createSmoothGradientMap();
|
|
2719
|
+
applyTranslucency();
|
|
2640
2720
|
};
|
|
2641
2721
|
syncLookControls();
|
|
2642
2722
|
material.onBeforeRender = syncLookControls;
|
|
@@ -2810,6 +2890,27 @@ export function createUrbanAnimePropNodeMaterial(sourceMaterial, {
|
|
|
2810
2890
|
castShadowAmount.mul(0.72),
|
|
2811
2891
|
);
|
|
2812
2892
|
|
|
2893
|
+
// Diffusing-sheet response. A backlit paper screen is not a window: the read
|
|
2894
|
+
// is the sheet GLOWING, not the room behind it, so this is a view-independent
|
|
2895
|
+
// lift rather than a transmission sample. It is modulated by the sheet's own
|
|
2896
|
+
// albedo so a fibre inclusion or a stain darkens when backlit — the thing
|
|
2897
|
+
// that makes washi read as washi — and eased off toward grazing angles,
|
|
2898
|
+
// where a real sheet presents more thickness and goes opaque. It is NOT
|
|
2899
|
+
// multiplied by `sceneShadow`, because the light behind a shoji is the
|
|
2900
|
+
// interior, not the sun, and a screen that stopped glowing when a cloud
|
|
2901
|
+
// crossed would be exactly wrong.
|
|
2902
|
+
const translucencyScale = Number(profile.translucencyScale ?? 0);
|
|
2903
|
+
const translucency = translucencyScale > 0
|
|
2904
|
+
? colorControlNode(shared.translucencyColor, 0xffd7a0)
|
|
2905
|
+
.mul(urbanColor)
|
|
2906
|
+
.mul(
|
|
2907
|
+
scalarControl(shared.translucencyEnabled)
|
|
2908
|
+
.mul(scalarControl(shared.translucencyStrength))
|
|
2909
|
+
.mul(translucencyScale)
|
|
2910
|
+
.mul(mix(0.35, 1, facing)),
|
|
2911
|
+
)
|
|
2912
|
+
: null;
|
|
2913
|
+
|
|
2813
2914
|
const material = new MeshToonNodeMaterial();
|
|
2814
2915
|
material.name = `Locked urban WebGPU · ${profileId} · ${
|
|
2815
2916
|
source?.name ?? 'material'
|
|
@@ -2819,6 +2920,7 @@ export function createUrbanAnimePropNodeMaterial(sourceMaterial, {
|
|
|
2819
2920
|
// Authored emission remains visible in shade; view-dependent manufactured
|
|
2820
2921
|
// highlights are direct-light cues and must disappear with the sun.
|
|
2821
2922
|
material.emissiveNode = sourceEmissionNode.add(stylizedResponse.mul(sceneShadow));
|
|
2923
|
+
if (translucency) material.emissiveNode = material.emissiveNode.add(translucency);
|
|
2822
2924
|
material.normalNode = sourceNormalNode;
|
|
2823
2925
|
material.gradientMap = createLockedGradientMap();
|
|
2824
2926
|
material.side = source?.side ?? THREE.FrontSide;
|
|
@@ -224,6 +224,7 @@ export function createLightingSystem({
|
|
|
224
224
|
let hour = clamp(finite(timeOfDay, 12), 0, 24);
|
|
225
225
|
let frame = sampleLightingStyle(styleSettings, hour);
|
|
226
226
|
let modulation = { ...DEFAULT_MODULATION };
|
|
227
|
+
let sunDirectionOverride = null;
|
|
227
228
|
let clock = 0;
|
|
228
229
|
let placementCounter = 0;
|
|
229
230
|
let disposed = false;
|
|
@@ -382,6 +383,15 @@ export function createLightingSystem({
|
|
|
382
383
|
const shadowFocusForward = new THREE.Vector3();
|
|
383
384
|
const shadowFocusTarget = new THREE.Vector3();
|
|
384
385
|
|
|
386
|
+
function effectiveSunSourceRatios() {
|
|
387
|
+
if (!sunDirectionOverride) return frame.sunSourceRatios;
|
|
388
|
+
return {
|
|
389
|
+
x: sunDirectionOverride[0],
|
|
390
|
+
y: sunDirectionOverride[1],
|
|
391
|
+
z: sunDirectionOverride[2],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
385
395
|
function updateOwnSunShadowFocus(nextCamera) {
|
|
386
396
|
const sun = attachment.ownSun;
|
|
387
397
|
const contract = CALL_ME_SENSEI_LIGHTING_CONTRACT.sun.shadow;
|
|
@@ -395,11 +405,8 @@ export function createLightingSystem({
|
|
|
395
405
|
// The style runtime cannot infer terrain height. Preserve the host/world
|
|
396
406
|
// target plane while following the camera horizontally.
|
|
397
407
|
shadowFocusTarget.y = sun.target.position.y;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
frame.sunSourceRatios.y,
|
|
401
|
-
frame.sunSourceRatios.z,
|
|
402
|
-
).normalize();
|
|
408
|
+
const sourceRatios = effectiveSunSourceRatios();
|
|
409
|
+
shadowFocusDirection.set(sourceRatios.x, sourceRatios.y, sourceRatios.z).normalize();
|
|
403
410
|
sun.target.position.copy(shadowFocusTarget);
|
|
404
411
|
sun.position.copy(shadowFocusTarget)
|
|
405
412
|
.addScaledVector(shadowFocusDirection, attachment.sunDistance);
|
|
@@ -440,7 +447,8 @@ export function createLightingSystem({
|
|
|
440
447
|
if (modulation.sunColorTint) {
|
|
441
448
|
for (let i = 0; i < 3; i += 1) sunColor[i] = clamp(sunColor[i] * modulation.sunColorTint[i], 0, 4);
|
|
442
449
|
}
|
|
443
|
-
const
|
|
450
|
+
const sourceRatios = effectiveSunSourceRatios();
|
|
451
|
+
const sunDirection = [sourceRatios.x, sourceRatios.y, sourceRatios.z];
|
|
444
452
|
|
|
445
453
|
if (attachment.sunRig?.setState) {
|
|
446
454
|
attachment.sunRig.setState({
|
|
@@ -450,15 +458,15 @@ export function createLightingSystem({
|
|
|
450
458
|
intensity: frame.sunIntensity * sunScale,
|
|
451
459
|
shaftOpacity: 0.1 * frame.accentScale * sunScale,
|
|
452
460
|
spillOpacity: 0.3 * frame.accentScale * sunScale,
|
|
453
|
-
...(attachment.driveSunPosition ? { sourceRatios
|
|
461
|
+
...(attachment.driveSunPosition ? { sourceRatios } : {}),
|
|
454
462
|
});
|
|
455
463
|
} else if (attachment.ownSun) {
|
|
456
464
|
attachment.ownSun.color.setRGB(sunColor[0], sunColor[1], sunColor[2]);
|
|
457
465
|
attachment.ownSun.intensity = frame.sunIntensity * sunScale;
|
|
458
466
|
attachment.ownSun.position.set(
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
467
|
+
sourceRatios.x,
|
|
468
|
+
sourceRatios.y,
|
|
469
|
+
sourceRatios.z,
|
|
462
470
|
).multiplyScalar(attachment.sunDistance);
|
|
463
471
|
attachment.ownSun.target.position.set(0, 0, 0);
|
|
464
472
|
attachment.ownSun.target.updateMatrixWorld();
|
|
@@ -543,6 +551,26 @@ export function createLightingSystem({
|
|
|
543
551
|
attachment.setSunDirection?.(sunDirection);
|
|
544
552
|
}
|
|
545
553
|
|
|
554
|
+
// Rock material color remains geological; this transient input colors its
|
|
555
|
+
// small readability floor from the same visible sky that drives the rest
|
|
556
|
+
// of the world. Traverse only when the lighting frame changes, not per
|
|
557
|
+
// render frame, and deduplicate shared materials.
|
|
558
|
+
if (scene?.traverse) {
|
|
559
|
+
const rockMaterials = new Set();
|
|
560
|
+
scene.traverse((object) => {
|
|
561
|
+
if (!object?.isMesh || !object.material) return;
|
|
562
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
563
|
+
for (const material of materials) {
|
|
564
|
+
if (material?.userData?.toonLabRockSceneState?.setSkyColor) {
|
|
565
|
+
rockMaterials.add(material);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
for (const material of rockMaterials) {
|
|
570
|
+
material.userData.toonLabRockSceneState.setSkyColor(skyFillColor);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
546
574
|
if (attachment.water) {
|
|
547
575
|
const waterFrame = {
|
|
548
576
|
skyHorizonColor: renderedSky?.horizonColor ?? frame.skyHorizonColor,
|
|
@@ -717,6 +745,29 @@ export function createLightingSystem({
|
|
|
717
745
|
return frame;
|
|
718
746
|
},
|
|
719
747
|
|
|
748
|
+
/**
|
|
749
|
+
* Overrides the scene-space direction toward the sun without replacing
|
|
750
|
+
* the active time-of-day palette. Pass null to return direction ownership
|
|
751
|
+
* to the day curve.
|
|
752
|
+
*/
|
|
753
|
+
setSunDirection(direction = null) {
|
|
754
|
+
if (direction == null) {
|
|
755
|
+
sunDirectionOverride = null;
|
|
756
|
+
} else {
|
|
757
|
+
const values = Array.isArray(direction)
|
|
758
|
+
? direction.slice(0, 3).map(Number)
|
|
759
|
+
: [Number(direction.x), Number(direction.y), Number(direction.z)];
|
|
760
|
+
if (values.length < 3 || !values.every(Number.isFinite)) {
|
|
761
|
+
throw new TypeError('Sun direction must contain three finite components.');
|
|
762
|
+
}
|
|
763
|
+
const length = Math.hypot(...values);
|
|
764
|
+
if (length < 1e-6) throw new RangeError('Sun direction cannot be zero length.');
|
|
765
|
+
sunDirectionOverride = values.map((value) => value / length);
|
|
766
|
+
}
|
|
767
|
+
applyFrame();
|
|
768
|
+
return api.sunDirection;
|
|
769
|
+
},
|
|
770
|
+
|
|
720
771
|
/** Advances the day cycle; use from update loops for live cycles. */
|
|
721
772
|
advanceTime(hoursDelta) {
|
|
722
773
|
return api.setTimeOfDay(hour + finite(hoursDelta, 0));
|
|
@@ -898,6 +949,10 @@ export function createLightingSystem({
|
|
|
898
949
|
},
|
|
899
950
|
|
|
900
951
|
get frame() { return frame; },
|
|
952
|
+
get sunDirection() {
|
|
953
|
+
const ratios = effectiveSunSourceRatios();
|
|
954
|
+
return [ratios.x, ratios.y, ratios.z];
|
|
955
|
+
},
|
|
901
956
|
get manager() { return manager; },
|
|
902
957
|
get quality() { return cloneJson(manager.quality); },
|
|
903
958
|
get skyLightProbe() { return skyLightProbe; },
|
package/src/renderer/index.js
CHANGED
|
@@ -7,3 +7,10 @@ export {
|
|
|
7
7
|
detectToonLabRendererBackend,
|
|
8
8
|
stabilizeToonLabWebGPUResourceLifetime,
|
|
9
9
|
} from './rendererConfiguration.js';
|
|
10
|
+
export {
|
|
11
|
+
STYLE_COMPARISON_AXES,
|
|
12
|
+
STYLE_COMPARISON_GENERATED_NODE_FLAGS,
|
|
13
|
+
captureComparisonFrameState,
|
|
14
|
+
createStyleComparison,
|
|
15
|
+
verifyStyleComparisonIdentity,
|
|
16
|
+
} from './styleComparison.js';
|