@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
|
@@ -138,7 +138,7 @@ const GROUP_DEFINITIONS = Object.freeze({
|
|
|
138
138
|
fields: Object.freeze({
|
|
139
139
|
exposure: field({
|
|
140
140
|
defaultValue: 1,
|
|
141
|
-
description: '
|
|
141
|
+
description: 'Rock-specific direct-sun calibration. Base Color and indirect sky light remain unchanged.',
|
|
142
142
|
label: 'Exposure',
|
|
143
143
|
range: { max: 4, min: 0, step: 0.01 },
|
|
144
144
|
}),
|
|
@@ -148,6 +148,12 @@ const GROUP_DEFINITIONS = Object.freeze({
|
|
|
148
148
|
label: 'Ambient Floor',
|
|
149
149
|
range: { max: 0.4, min: 0, step: 0.005 },
|
|
150
150
|
}),
|
|
151
|
+
skyColorInfluence: field({
|
|
152
|
+
defaultValue: 0.35,
|
|
153
|
+
description: 'Fraction of active sky chroma admitted into the albedo-relative readability floor.',
|
|
154
|
+
label: 'Sky Color Influence',
|
|
155
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
156
|
+
}),
|
|
151
157
|
skyFillStrength: field({
|
|
152
158
|
defaultValue: 1,
|
|
153
159
|
description: 'Rock-specific strength of the scene sky probe; direct sunlight is unchanged.',
|
|
@@ -160,6 +166,18 @@ const GROUP_DEFINITIONS = Object.freeze({
|
|
|
160
166
|
label: 'Sky Fill Tint',
|
|
161
167
|
type: 'color',
|
|
162
168
|
}),
|
|
169
|
+
shadowFill: field({
|
|
170
|
+
defaultValue: 0,
|
|
171
|
+
description: 'Fraction of direct sun retained where the shared low-angle shadow mask occludes the rock. Normal-facing response is preserved.',
|
|
172
|
+
label: 'Direct Shadow Fill',
|
|
173
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
174
|
+
}),
|
|
175
|
+
shadowFillTint: field({
|
|
176
|
+
defaultValue: [1, 1, 1],
|
|
177
|
+
description: 'RGB weighting for retained direct light inside shared sun-shadow coverage.',
|
|
178
|
+
label: 'Direct Shadow Fill Tint',
|
|
179
|
+
type: 'color',
|
|
180
|
+
}),
|
|
163
181
|
}),
|
|
164
182
|
}),
|
|
165
183
|
shoreline: Object.freeze({
|
|
@@ -315,7 +333,11 @@ const GROUP_DEFINITIONS = Object.freeze({
|
|
|
315
333
|
defaultValue: 1.6,
|
|
316
334
|
description: 'Strength of the projected moss coverage pattern.',
|
|
317
335
|
label: 'Coverage Gain',
|
|
318
|
-
|
|
336
|
+
// Ceiling raised from 6. The shader squares this term against a moss
|
|
337
|
+
// albedo whose linear luminance is near 0.13 (D19-032), so 6 is not
|
|
338
|
+
// enough to reach full coverage on a surface where moss is the
|
|
339
|
+
// subject rather than incidental weathering. The default is unchanged.
|
|
340
|
+
range: { max: 24, min: 0, step: 0.01 },
|
|
319
341
|
}),
|
|
320
342
|
colorPower: field({
|
|
321
343
|
defaultValue: 1.25,
|
|
@@ -335,6 +357,152 @@ const GROUP_DEFINITIONS = Object.freeze({
|
|
|
335
357
|
label: 'High Color',
|
|
336
358
|
type: 'color',
|
|
337
359
|
}),
|
|
360
|
+
// --- moss as a material, not a tint --------------------------------
|
|
361
|
+
// New capability. Every default is the previous behaviour, so a style
|
|
362
|
+
// that does not set them renders exactly as before.
|
|
363
|
+
roughness: field({
|
|
364
|
+
defaultValue: -1,
|
|
365
|
+
description: 'Surface roughness under full moss; -1 keeps the stone value.',
|
|
366
|
+
label: 'Moss Roughness',
|
|
367
|
+
range: { max: 1, min: -1, step: 0.01 },
|
|
368
|
+
}),
|
|
369
|
+
relief: field({
|
|
370
|
+
defaultValue: 0,
|
|
371
|
+
description: 'Flattens the rock detail normal under moss, as a cushion would.',
|
|
372
|
+
label: 'Moss Relief Fill',
|
|
373
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
374
|
+
}),
|
|
375
|
+
fringe: field({
|
|
376
|
+
defaultValue: 0,
|
|
377
|
+
description: 'Ragged break-up of the moss coverage boundary.',
|
|
378
|
+
label: 'Edge Fringe',
|
|
379
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
380
|
+
}),
|
|
381
|
+
fringeScale: field({
|
|
382
|
+
defaultValue: 0.35,
|
|
383
|
+
// Now a fraction of PATCH scale, not of the moss projection size. The
|
|
384
|
+
// fringe field is world-space fBm rather than a resample of the moss
|
|
385
|
+
// albedo (D19-211), and what it has to break up is the patch boundary,
|
|
386
|
+
// so the patch period is the only period it can be meaningfully
|
|
387
|
+
// relative to. `fringe` defaults to 0, so nothing shipped observes the
|
|
388
|
+
// change.
|
|
389
|
+
description: 'Fringe noise period as a fraction of the colonisation patch scale.',
|
|
390
|
+
label: 'Fringe Scale',
|
|
391
|
+
range: { max: 1, min: 0.02, step: 0.01 },
|
|
392
|
+
}),
|
|
393
|
+
formDriven: field({
|
|
394
|
+
defaultValue: 0,
|
|
395
|
+
description: 'Blends coverage from the moss albedo toward the geometric moisture field.',
|
|
396
|
+
label: 'Form-Driven Coverage',
|
|
397
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
398
|
+
}),
|
|
399
|
+
// --- coverage mask -------------------------------------------------
|
|
400
|
+
// `coverage` is the switch: -1 keeps the shipped squared-clamp curve,
|
|
401
|
+
// whose transition band is so narrow that moss resolves as a horizontal
|
|
402
|
+
// waterline. 0..1 selects the patch-mask path, where this value IS the
|
|
403
|
+
// fraction of supported surface moss takes.
|
|
404
|
+
coverage: field({
|
|
405
|
+
defaultValue: -1,
|
|
406
|
+
description: 'Target moss coverage of supported surface; -1 keeps the legacy coverage curve.',
|
|
407
|
+
label: 'Coverage',
|
|
408
|
+
range: { max: 1, min: -1, step: 0.01 },
|
|
409
|
+
}),
|
|
410
|
+
band: field({
|
|
411
|
+
defaultValue: 0.18,
|
|
412
|
+
description: 'Width of the coverage transition. Narrow bands read as a dyed waterline.',
|
|
413
|
+
label: 'Transition Band',
|
|
414
|
+
range: { max: 0.5, min: 0.001, step: 0.001 },
|
|
415
|
+
}),
|
|
416
|
+
patchScale: field({
|
|
417
|
+
defaultValue: 0.6,
|
|
418
|
+
description: 'World-space period of the colonisation noise, metres.',
|
|
419
|
+
label: 'Patch Scale',
|
|
420
|
+
range: { max: 20, min: 0.01, step: 0.01 },
|
|
421
|
+
}),
|
|
422
|
+
patchStrength: field({
|
|
423
|
+
defaultValue: 0.8,
|
|
424
|
+
description: 'How strongly colonisation noise gates coverage. 0 gives an unbroken cap.',
|
|
425
|
+
label: 'Patch Strength',
|
|
426
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
427
|
+
}),
|
|
428
|
+
patchOctaves: field({
|
|
429
|
+
defaultValue: 3,
|
|
430
|
+
description: 'Octaves of colonisation noise.',
|
|
431
|
+
label: 'Patch Octaves',
|
|
432
|
+
range: { max: 6, min: 1, step: 1 },
|
|
433
|
+
}),
|
|
434
|
+
patchContrast: field({
|
|
435
|
+
defaultValue: 1.6,
|
|
436
|
+
description: 'Separates the noise into distinct cushions rather than a smooth cloud.',
|
|
437
|
+
label: 'Patch Contrast',
|
|
438
|
+
range: { max: 6, min: 0.1, step: 0.01 },
|
|
439
|
+
}),
|
|
440
|
+
// --- value, form and depth -----------------------------------------
|
|
441
|
+
// The four defects that made a working patch mask still read as
|
|
442
|
+
// camouflage rather than as moss (D19-210 … D19-213). Every default is
|
|
443
|
+
// inert: -1 and 0 reproduce the shipped render exactly.
|
|
444
|
+
//
|
|
445
|
+
// `patternFloor` / `patternCeiling` remap the moss map's own luminance
|
|
446
|
+
// range onto the colour ramp. The shipped path feeds the RAW rgb sample
|
|
447
|
+
// into `mix(lowColor, highColor)`, and the shipped moss albedo occupies a
|
|
448
|
+
// linear luminance of 0.092-0.279 with 99.3% of its texels inside a
|
|
449
|
+
// single decile. The ramp coordinate therefore never exceeds ~0.09: moss
|
|
450
|
+
// renders as `lowColor` everywhere, `highColor` is dead, and the five
|
|
451
|
+
// authored palette stops collapse to one very dark green. Setting a floor
|
|
452
|
+
// switches the ramp to a scalar remap of that measured range, which both
|
|
453
|
+
// reaches the light end of the palette and stops the per-channel sample
|
|
454
|
+
// from skewing hue. See D19-210.
|
|
455
|
+
patternFloor: field({
|
|
456
|
+
defaultValue: -1,
|
|
457
|
+
description: 'Moss-map luminance that maps to Low Color; -1 keeps the shipped per-channel ramp.',
|
|
458
|
+
label: 'Pattern Floor',
|
|
459
|
+
range: { max: 1, min: -1, step: 0.001 },
|
|
460
|
+
}),
|
|
461
|
+
patternCeiling: field({
|
|
462
|
+
defaultValue: 1,
|
|
463
|
+
description: 'Moss-map luminance that maps to High Color. Ignored when Pattern Floor is -1.',
|
|
464
|
+
label: 'Pattern Ceiling',
|
|
465
|
+
range: { max: 1, min: 0, step: 0.001 },
|
|
466
|
+
}),
|
|
467
|
+
// Moss and stone read at a close value in life because moss grows where
|
|
468
|
+
// the stone stays WET, and wet stone is darker than dry stone. Without
|
|
469
|
+
// this the only way to close the value gap is to lift the moss or drop
|
|
470
|
+
// the stone globally — the first fights the shared palette, the second is
|
|
471
|
+
// the scene's grade to make. This closes it from the physically correct
|
|
472
|
+
// side and leaves a damp halo just beyond each cushion. See D19-210.
|
|
473
|
+
damp: field({
|
|
474
|
+
defaultValue: 0,
|
|
475
|
+
description: 'Darkens the stone where the moss support field is wet, including just beyond the moss.',
|
|
476
|
+
label: 'Damp Stone',
|
|
477
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
478
|
+
}),
|
|
479
|
+
// Crowns, ridges and shoulders shed water and dry first, so they stay
|
|
480
|
+
// bare. The support term could express "hollow" via `rockCavity` but had
|
|
481
|
+
// no term for "exposed", and its slope half saturates across the whole
|
|
482
|
+
// upper hemisphere — leaving the colonisation noise as the only thing
|
|
483
|
+
// distinguishing one upper face from another. See D19-212.
|
|
484
|
+
exposure: field({
|
|
485
|
+
defaultValue: 0,
|
|
486
|
+
description: 'How strongly convex, weather-exposed form (crowns and ridges) suppresses moss.',
|
|
487
|
+
label: 'Exposure Penalty',
|
|
488
|
+
range: { max: 2, min: 0, step: 0.01 },
|
|
489
|
+
}),
|
|
490
|
+
// A moss cushion is a raised soft mass, not a decal. `relief` flattens the
|
|
491
|
+
// stone's own detail under the moss but nothing ever raised the moss
|
|
492
|
+
// itself, so the patch stayed geometrically identical to the rock it sat
|
|
493
|
+
// on. See D19-213.
|
|
494
|
+
cushion: field({
|
|
495
|
+
defaultValue: 0,
|
|
496
|
+
description: 'Raises the moss into a soft cushion by tilting the shading normal along the patch field.',
|
|
497
|
+
label: 'Cushion Depth',
|
|
498
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
499
|
+
}),
|
|
500
|
+
contact: field({
|
|
501
|
+
defaultValue: 0,
|
|
502
|
+
description: 'Contact shading where a cushion meets bare stone — the occlusion a raised mass casts.',
|
|
503
|
+
label: 'Contact Shade',
|
|
504
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
505
|
+
}),
|
|
338
506
|
}),
|
|
339
507
|
}),
|
|
340
508
|
layerMask: Object.freeze({
|
|
@@ -540,6 +708,42 @@ const GROUP_DEFINITIONS = Object.freeze({
|
|
|
540
708
|
label: 'Vertex AO Strength',
|
|
541
709
|
range: { max: 2, min: 0, step: 0.01 },
|
|
542
710
|
}),
|
|
711
|
+
regionTintStrength: field({
|
|
712
|
+
defaultValue: 0,
|
|
713
|
+
description: 'Influence of the fail-closed _TL_ROCK_REGION base, shaft, neck, and cap palette. Zero does not require the attribute.',
|
|
714
|
+
label: 'Geology Region Tint',
|
|
715
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
716
|
+
}),
|
|
717
|
+
regionBaseTint: field({
|
|
718
|
+
defaultValue: [1, 1, 1],
|
|
719
|
+
description: 'Multiplier applied to the geological support/base region.',
|
|
720
|
+
label: 'Base Region Tint',
|
|
721
|
+
type: 'color',
|
|
722
|
+
}),
|
|
723
|
+
regionShaftTint: field({
|
|
724
|
+
defaultValue: [1, 1, 1],
|
|
725
|
+
description: 'Multiplier applied to the main hoodoo shaft region.',
|
|
726
|
+
label: 'Shaft Region Tint',
|
|
727
|
+
type: 'color',
|
|
728
|
+
}),
|
|
729
|
+
regionNeckTint: field({
|
|
730
|
+
defaultValue: [1, 1, 1],
|
|
731
|
+
description: 'Independent overlay multiplier around the constricted neck.',
|
|
732
|
+
label: 'Neck Region Tint',
|
|
733
|
+
type: 'color',
|
|
734
|
+
}),
|
|
735
|
+
regionCapTint: field({
|
|
736
|
+
defaultValue: [1, 1, 1],
|
|
737
|
+
description: 'Multiplier applied to the resistant caprock region.',
|
|
738
|
+
label: 'Cap Region Tint',
|
|
739
|
+
type: 'color',
|
|
740
|
+
}),
|
|
741
|
+
regionNeckOverlayStrength: field({
|
|
742
|
+
defaultValue: 0,
|
|
743
|
+
description: 'How strongly the overlapping neck mask modifies the primary base/shaft/cap partition.',
|
|
744
|
+
label: 'Neck Overlay Strength',
|
|
745
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
746
|
+
}),
|
|
543
747
|
}),
|
|
544
748
|
}),
|
|
545
749
|
});
|
|
@@ -589,9 +793,12 @@ export const DEFAULT_ROCK_SHADER_SETTINGS = Object.freeze(Object.fromEntries(
|
|
|
589
793
|
export const CALL_ME_SENSEI_ROCK_SHADER_SETTINGS = Object.freeze({
|
|
590
794
|
projection: Object.freeze({
|
|
591
795
|
scale: 48,
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
796
|
+
// Geological maps already own lithology color and value structure. The
|
|
797
|
+
// previous 0.72 saturation/contrast pair washed every family toward the
|
|
798
|
+
// same pale grey before lighting, most visibly red sandstone.
|
|
799
|
+
saturation: 1,
|
|
800
|
+
contrast: 1,
|
|
801
|
+
brightness: 0,
|
|
595
802
|
projectionContrast: 2,
|
|
596
803
|
sideOnly: false,
|
|
597
804
|
nearDetailScale: 1.2,
|
|
@@ -599,10 +806,11 @@ export const CALL_ME_SENSEI_ROCK_SHADER_SETTINGS = Object.freeze({
|
|
|
599
806
|
nearDetailDistance: 70,
|
|
600
807
|
}),
|
|
601
808
|
material: Object.freeze({
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
|
|
809
|
+
// Geological Base Color stays neutral at the material boundary. Daylight,
|
|
810
|
+
// golden hour, weather, and night color belong to the shared scene sun and
|
|
811
|
+
// sky probe; baking a blue-white cast here washed warm rocks such as
|
|
812
|
+
// sandstone out even before they saw the active environment.
|
|
813
|
+
tint: Object.freeze([1, 1, 1]),
|
|
606
814
|
metallic: 0,
|
|
607
815
|
smoothness: 0.07,
|
|
608
816
|
useSmoothnessTexture: false,
|
|
@@ -610,14 +818,23 @@ export const CALL_ME_SENSEI_ROCK_SHADER_SETTINGS = Object.freeze({
|
|
|
610
818
|
emissiveStrength: 0,
|
|
611
819
|
}),
|
|
612
820
|
lighting: Object.freeze({
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
ambientFloor: 0.
|
|
821
|
+
// Preserve Base Color headroom under the package's intensity-8 daylight
|
|
822
|
+
// sun. This is material exposure, not a scene-local light override.
|
|
823
|
+
exposure: 0.95,
|
|
824
|
+
// Keep the readability floor small: the main shadow-side value and colour
|
|
825
|
+
// must come from the scene's live sky, not a neutral emissive wash.
|
|
826
|
+
ambientFloor: 0.04,
|
|
827
|
+
skyColorInfluence: 1,
|
|
828
|
+
// Restore the strong open-sky fill that gave the accepted rocks their blue
|
|
829
|
+
// daylight backs. Its colour is now dynamic rather than baked blue, so the
|
|
830
|
+
// same channel becomes pink/orange at sunset and deep blue at night.
|
|
619
831
|
skyFillStrength: 0.72,
|
|
620
|
-
skyFillTint: Object.freeze([
|
|
832
|
+
skyFillTint: Object.freeze([1, 1, 1]),
|
|
833
|
+
// Shadowed faces belong to the sky probe. Retaining any fraction of the
|
|
834
|
+
// sunset sun here paints orange into the cool side and destroys the
|
|
835
|
+
// warm-key/cool-fill separation.
|
|
836
|
+
shadowFill: 0,
|
|
837
|
+
shadowFillTint: Object.freeze([1, 1, 1]),
|
|
621
838
|
}),
|
|
622
839
|
shoreline: Object.freeze({
|
|
623
840
|
wetBandWidth: 1,
|
|
@@ -652,6 +869,19 @@ export const CALL_ME_SENSEI_ROCK_SHADER_SETTINGS = Object.freeze({
|
|
|
652
869
|
colorPower: 1.3,
|
|
653
870
|
lowColor: Object.freeze([0.24, 0.42, 0.12]),
|
|
654
871
|
highColor: Object.freeze([0.46, 0.68, 0.24]),
|
|
872
|
+
// New capability, neutral defaults. No authored value above is changed.
|
|
873
|
+
roughness: -1,
|
|
874
|
+
relief: 0,
|
|
875
|
+
fringe: 0,
|
|
876
|
+
fringeScale: 0.35,
|
|
877
|
+
formDriven: 0,
|
|
878
|
+
// Inert: `coverage: -1` keeps the shipped coverage curve.
|
|
879
|
+
coverage: -1,
|
|
880
|
+
band: 0.18,
|
|
881
|
+
patchScale: 0.6,
|
|
882
|
+
patchStrength: 0.8,
|
|
883
|
+
patchOctaves: 3,
|
|
884
|
+
patchContrast: 1.6,
|
|
655
885
|
}),
|
|
656
886
|
layerMask: Object.freeze({
|
|
657
887
|
useAssetMask: true,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const DEFAULT_EPSILON = 1e-6;
|
|
2
|
+
const DEFAULT_ORTHOGONALITY_TOLERANCE = 0.1;
|
|
3
|
+
|
|
4
|
+
function finite(value) {
|
|
5
|
+
return Number.isFinite(Number(value));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Audits a geometry tangent attribute before a normal-mapped material can
|
|
10
|
+
* consume it. A missing tangent attribute is valid: Three.js derives a
|
|
11
|
+
* derivative TBN frame. A present but zero/invalid attribute is not valid and
|
|
12
|
+
* produces literal black facets in WebGPU/TSL normal mapping.
|
|
13
|
+
*/
|
|
14
|
+
export function inspectRockGeometryTangents(geometry, {
|
|
15
|
+
epsilon = DEFAULT_EPSILON,
|
|
16
|
+
orthogonalityTolerance = DEFAULT_ORTHOGONALITY_TOLERANCE,
|
|
17
|
+
} = {}) {
|
|
18
|
+
const position = geometry?.getAttribute?.('position');
|
|
19
|
+
const normal = geometry?.getAttribute?.('normal');
|
|
20
|
+
const tangent = geometry?.getAttribute?.('tangent');
|
|
21
|
+
const vertices = position?.count ?? 0;
|
|
22
|
+
const report = {
|
|
23
|
+
vertices,
|
|
24
|
+
hasTangents: Boolean(tangent),
|
|
25
|
+
tangentVertices: tangent?.count ?? 0,
|
|
26
|
+
invalidVertices: 0,
|
|
27
|
+
zeroLength: 0,
|
|
28
|
+
nonFinite: 0,
|
|
29
|
+
invalidHandedness: 0,
|
|
30
|
+
nonOrthogonal: 0,
|
|
31
|
+
countMismatch: Boolean(tangent && tangent.count !== vertices),
|
|
32
|
+
itemSizeInvalid: Boolean(tangent && tangent.itemSize < 4),
|
|
33
|
+
valid: true,
|
|
34
|
+
};
|
|
35
|
+
if (!tangent) return report;
|
|
36
|
+
|
|
37
|
+
const epsilonSquared = Math.max(Number(epsilon) || DEFAULT_EPSILON, 1e-12) ** 2;
|
|
38
|
+
const orthogonality = Math.max(
|
|
39
|
+
Number(orthogonalityTolerance) || DEFAULT_ORTHOGONALITY_TOLERANCE,
|
|
40
|
+
1e-6,
|
|
41
|
+
);
|
|
42
|
+
const count = Math.min(tangent.count, vertices || tangent.count);
|
|
43
|
+
for (let index = 0; index < count; index += 1) {
|
|
44
|
+
const x = tangent.getX(index);
|
|
45
|
+
const y = tangent.getY(index);
|
|
46
|
+
const z = tangent.getZ(index);
|
|
47
|
+
const w = tangent.itemSize > 3 ? tangent.getW(index) : 0;
|
|
48
|
+
const lengthSquared = (x * x) + (y * y) + (z * z);
|
|
49
|
+
let invalid = false;
|
|
50
|
+
if (![x, y, z, w].every(finite)) {
|
|
51
|
+
report.nonFinite += 1;
|
|
52
|
+
invalid = true;
|
|
53
|
+
}
|
|
54
|
+
if (!finite(lengthSquared) || lengthSquared <= epsilonSquared) {
|
|
55
|
+
report.zeroLength += 1;
|
|
56
|
+
invalid = true;
|
|
57
|
+
}
|
|
58
|
+
if (!finite(w) || Math.abs(w) < 0.5) {
|
|
59
|
+
report.invalidHandedness += 1;
|
|
60
|
+
invalid = true;
|
|
61
|
+
}
|
|
62
|
+
if (normal && normal.count > index && finite(lengthSquared) && lengthSquared > epsilonSquared) {
|
|
63
|
+
const nx = normal.getX(index);
|
|
64
|
+
const ny = normal.getY(index);
|
|
65
|
+
const nz = normal.getZ(index);
|
|
66
|
+
const normalLengthSquared = (nx * nx) + (ny * ny) + (nz * nz);
|
|
67
|
+
if (normalLengthSquared > epsilonSquared) {
|
|
68
|
+
const cosine = Math.abs((x * nx) + (y * ny) + (z * nz))
|
|
69
|
+
/ Math.sqrt(lengthSquared * normalLengthSquared);
|
|
70
|
+
if (!finite(cosine) || cosine > orthogonality) {
|
|
71
|
+
report.nonOrthogonal += 1;
|
|
72
|
+
invalid = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (invalid) report.invalidVertices += 1;
|
|
77
|
+
}
|
|
78
|
+
if (report.countMismatch || report.itemSizeInvalid) {
|
|
79
|
+
report.invalidVertices = Math.max(report.invalidVertices, tangent.count);
|
|
80
|
+
}
|
|
81
|
+
report.valid = report.invalidVertices === 0
|
|
82
|
+
&& !report.countMismatch
|
|
83
|
+
&& !report.itemSizeInvalid;
|
|
84
|
+
return report;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Removes only invalid tangent attributes. Three.js then uses its derivative
|
|
89
|
+
* tangent frame, which is the safe fallback for degenerate UV islands. Vertex
|
|
90
|
+
* positions, normals, UVs, indices, materials, and texture files are untouched.
|
|
91
|
+
*/
|
|
92
|
+
export function sanitizeRockGeometryTangents(geometry, options = {}) {
|
|
93
|
+
const inspection = inspectRockGeometryTangents(geometry, options);
|
|
94
|
+
const removed = inspection.hasTangents && !inspection.valid;
|
|
95
|
+
if (removed) {
|
|
96
|
+
geometry.deleteAttribute('tangent');
|
|
97
|
+
geometry.userData ??= {};
|
|
98
|
+
geometry.userData.toonLabRemovedInvalidTangents = {
|
|
99
|
+
invalidVertices: inspection.invalidVertices,
|
|
100
|
+
tangentVertices: inspection.tangentVertices,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { ...inspection, removed };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Audits and repairs every mesh below a rock root. */
|
|
107
|
+
export function sanitizeRockTangents(root, options = {}) {
|
|
108
|
+
const issues = [];
|
|
109
|
+
const report = {
|
|
110
|
+
meshes: 0,
|
|
111
|
+
meshesWithTangents: 0,
|
|
112
|
+
repairedMeshes: 0,
|
|
113
|
+
vertices: 0,
|
|
114
|
+
tangentVertices: 0,
|
|
115
|
+
invalidVertices: 0,
|
|
116
|
+
issues,
|
|
117
|
+
};
|
|
118
|
+
root?.traverse?.((object) => {
|
|
119
|
+
if (!object?.isMesh || !object.geometry) return;
|
|
120
|
+
report.meshes += 1;
|
|
121
|
+
const result = sanitizeRockGeometryTangents(object.geometry, options);
|
|
122
|
+
report.vertices += result.vertices;
|
|
123
|
+
report.tangentVertices += result.tangentVertices;
|
|
124
|
+
report.invalidVertices += result.invalidVertices;
|
|
125
|
+
if (result.hasTangents) report.meshesWithTangents += 1;
|
|
126
|
+
if (result.removed) {
|
|
127
|
+
report.repairedMeshes += 1;
|
|
128
|
+
issues.push({
|
|
129
|
+
mesh: object.name || `mesh-${report.meshes}`,
|
|
130
|
+
invalidVertices: result.invalidVertices,
|
|
131
|
+
tangentVertices: result.tangentVertices,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return report;
|
|
136
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Integrity checks for textures handed to the rock shader.
|
|
2
|
+
//
|
|
3
|
+
// A detail map only earns its slot if it can actually carry detail. Published
|
|
4
|
+
// catalog artifacts sometimes ship a placeholder in a real texture slot — a
|
|
5
|
+
// 4x4 normal map, for instance — which is materially worse than shipping
|
|
6
|
+
// nothing: the shader's own deterministic fallback is a usable surface, and a
|
|
7
|
+
// placeholder silently displaces it. Every consumer that harvests maps off an
|
|
8
|
+
// imported material needs the same predicate, so it lives here rather than in
|
|
9
|
+
// any one call site.
|
|
10
|
+
//
|
|
11
|
+
// This is deliberately a *resolution* test, not a content test. Reading pixels
|
|
12
|
+
// back is not possible for GPU-compressed formats (KTX2/Basis) without a
|
|
13
|
+
// decode, and resolution alone is sufficient to catch the placeholder class.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimum edge length, in texels, for a map to be treated as carrying detail.
|
|
17
|
+
*
|
|
18
|
+
* A triplanar detail map is projected at metre scale, so its texel density is
|
|
19
|
+
* what produces fracture and erosion relief. Below 16x16 there is no spatial
|
|
20
|
+
* frequency left to project and the map is indistinguishable from a constant.
|
|
21
|
+
*/
|
|
22
|
+
export const MIN_DETAIL_MAP_EDGE_TEXELS = 16;
|
|
23
|
+
|
|
24
|
+
/** Texture slots treated as detail maps for integrity purposes. */
|
|
25
|
+
export const ROCK_DETAIL_MAP_SLOTS = Object.freeze([
|
|
26
|
+
'rockNormal',
|
|
27
|
+
'sandNormal',
|
|
28
|
+
'sourceNormal',
|
|
29
|
+
'stylizedNormal',
|
|
30
|
+
'smoothness',
|
|
31
|
+
'topMask',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Reads a texture's pixel dimensions across the shapes three.js uses:
|
|
36
|
+
* `DataTexture`/`Texture` carry `image`, compressed KTX2 textures carry
|
|
37
|
+
* `mipmaps[0]`, and loaders that went through `Source` carry `source.data`.
|
|
38
|
+
*
|
|
39
|
+
* @returns {{width: number, height: number} | null} null when undeterminable.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveTextureSize(texture) {
|
|
42
|
+
if (!texture?.isTexture) return null;
|
|
43
|
+
const candidates = [
|
|
44
|
+
texture.image,
|
|
45
|
+
texture.source?.data,
|
|
46
|
+
Array.isArray(texture.mipmaps) ? texture.mipmaps[0] : null,
|
|
47
|
+
];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const width = Number(candidate?.width);
|
|
50
|
+
const height = Number(candidate?.height);
|
|
51
|
+
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
|
52
|
+
return { width, height };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* True when a texture is too small to carry the detail its slot implies.
|
|
60
|
+
*
|
|
61
|
+
* Undeterminable sizes return `false` — this guard removes maps only when it
|
|
62
|
+
* can prove they are degenerate, so an unusual-but-valid texture is never
|
|
63
|
+
* silently dropped.
|
|
64
|
+
*/
|
|
65
|
+
export function isDegenerateDetailMap(texture, {
|
|
66
|
+
minEdgeTexels = MIN_DETAIL_MAP_EDGE_TEXELS,
|
|
67
|
+
} = {}) {
|
|
68
|
+
const size = resolveTextureSize(texture);
|
|
69
|
+
if (!size) return false;
|
|
70
|
+
return size.width < minEdgeTexels || size.height < minEdgeTexels;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Describes a texture for diagnostics without assuming it is loaded.
|
|
75
|
+
*/
|
|
76
|
+
export function describeTexture(texture) {
|
|
77
|
+
const size = resolveTextureSize(texture);
|
|
78
|
+
return {
|
|
79
|
+
name: texture?.name || null,
|
|
80
|
+
resolution: size ? `${size.width}x${size.height}` : 'unknown',
|
|
81
|
+
uuid: texture?.uuid ?? null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Removes degenerate detail maps from a texture set.
|
|
87
|
+
*
|
|
88
|
+
* Returns a new object plus the list of rejected slots so callers can report
|
|
89
|
+
* the substitution rather than hiding it — a dropped map changes the rendered
|
|
90
|
+
* result and should be visible in an asset report.
|
|
91
|
+
*
|
|
92
|
+
* @param {Record<string, unknown>} textures
|
|
93
|
+
* @param {{minEdgeTexels?: number, slots?: readonly string[]}} [options]
|
|
94
|
+
* @returns {{textures: Record<string, unknown>, rejected: Array<{slot: string, reason: string, texture: object}>}}
|
|
95
|
+
*/
|
|
96
|
+
export function withoutDegenerateDetailMaps(textures, {
|
|
97
|
+
minEdgeTexels = MIN_DETAIL_MAP_EDGE_TEXELS,
|
|
98
|
+
slots = ROCK_DETAIL_MAP_SLOTS,
|
|
99
|
+
} = {}) {
|
|
100
|
+
const result = { ...(textures ?? {}) };
|
|
101
|
+
const rejected = [];
|
|
102
|
+
for (const slot of slots) {
|
|
103
|
+
const texture = result[slot];
|
|
104
|
+
if (!texture?.isTexture) continue;
|
|
105
|
+
if (!isDegenerateDetailMap(texture, { minEdgeTexels })) continue;
|
|
106
|
+
const described = describeTexture(texture);
|
|
107
|
+
rejected.push({
|
|
108
|
+
reason: `below the ${minEdgeTexels}x${minEdgeTexels} minimum for a detail map`,
|
|
109
|
+
slot,
|
|
110
|
+
texture: described,
|
|
111
|
+
});
|
|
112
|
+
delete result[slot];
|
|
113
|
+
}
|
|
114
|
+
return { rejected, textures: result };
|
|
115
|
+
}
|
package/src/rockgen/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import { compileDocument } from './sdf/fieldCompiler.js';
|
|
|
24
24
|
export const ROCKGEN_PROJECT_DOCUMENT_TYPE = 'toonlab/rockgen-project';
|
|
25
25
|
|
|
26
26
|
/** Current schema version for rockgen project documents. */
|
|
27
|
-
export const ROCKGEN_PROJECT_SCHEMA_VERSION =
|
|
27
|
+
export const ROCKGEN_PROJECT_SCHEMA_VERSION = 6;
|
|
28
28
|
|
|
29
29
|
/** Sparse source-mesh edits are packed into the portable document so normal
|
|
30
30
|
* sculpting remains below the hosted creation-document limit. Runtime state
|
|
@@ -70,6 +70,21 @@ function lodTrianglesOption(value, targetTriangles, lodRatios) {
|
|
|
70
70
|
: [];
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function surfacePackageOption(value) {
|
|
74
|
+
if (!value || typeof value !== 'object') return null;
|
|
75
|
+
if (value.schema !== 'toonlab/c7-geology-surface' || Number(value.version) !== 1) return null;
|
|
76
|
+
const geology = String(value.geology ?? '').trim();
|
|
77
|
+
if (!geology) return null;
|
|
78
|
+
const mapResolution = Math.max(32, Math.min(2048, Math.round(Number(value.mapResolution) || 512)));
|
|
79
|
+
return {
|
|
80
|
+
geology,
|
|
81
|
+
mapResolution,
|
|
82
|
+
schema: 'toonlab/c7-geology-surface',
|
|
83
|
+
seed: Math.round(Number(value.seed) || 0) >>> 0,
|
|
84
|
+
version: 1,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
function bytesToBase64(bytes) {
|
|
74
89
|
let binary = '';
|
|
75
90
|
const chunkSize = 0x8000;
|
|
@@ -219,6 +234,7 @@ export function createRockReferenceIdentity(options = null) {
|
|
|
219
234
|
role: String(options.role ?? '').trim(),
|
|
220
235
|
series: String(options.series ?? '').trim(),
|
|
221
236
|
sourceMode: 'mesh-template',
|
|
237
|
+
surfacePackage: surfacePackageOption(options.surfacePackage),
|
|
222
238
|
surfaceMode: options.surfaceMode === 'generated' ? 'generated' : 'source',
|
|
223
239
|
targetTriangles: positiveInteger(options.targetTriangles, lodTriangles[0] ?? 0),
|
|
224
240
|
topFinish,
|
|
@@ -508,8 +524,9 @@ export function serializeRockDocument(document, { pretty = false } = {}) {
|
|
|
508
524
|
// v3 adds optional descriptor-only reference identity and its LOD contract.
|
|
509
525
|
// v4 adds sparse vertex deltas for editable source-mesh projects. v5 packs
|
|
510
526
|
// those deltas as float32 binary in portable JSON so normal sculpt sessions
|
|
511
|
-
// fit the hosted creation limit.
|
|
512
|
-
//
|
|
527
|
+
// fit the hosted creation limit. v6 adds the optional deterministic C7 surface
|
|
528
|
+
// package identity used by the 480 source-mesh catalog. Existing
|
|
529
|
+
// preset/custom projects remain reference-free.
|
|
513
530
|
const MIGRATIONS = Object.freeze([
|
|
514
531
|
(document) => ({
|
|
515
532
|
...document,
|
|
@@ -547,6 +564,13 @@ const MIGRATIONS = Object.freeze([
|
|
|
547
564
|
schemaVersion: 5,
|
|
548
565
|
};
|
|
549
566
|
},
|
|
567
|
+
(document) => ({
|
|
568
|
+
...document,
|
|
569
|
+
reference: document.reference && typeof document.reference === 'object'
|
|
570
|
+
? { ...document.reference, surfacePackage: document.reference.surfacePackage ?? null }
|
|
571
|
+
: null,
|
|
572
|
+
schemaVersion: 6,
|
|
573
|
+
}),
|
|
550
574
|
]);
|
|
551
575
|
|
|
552
576
|
/**
|