@call-me-sensei/toonlab 0.2.0 → 0.3.0
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/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// Canonical settings, field metadata, and validation for the procedural
|
|
2
|
+
// texture generator. The field schema below is the single source of truth:
|
|
3
|
+
// defaults, the lab UI, recipe validation, and the AI parameter mapping all
|
|
4
|
+
// derive from it (same pattern as rockgen/debrisgen *_SETTING_FIELD_SCHEMA).
|
|
5
|
+
|
|
6
|
+
import { TEXTURE_GENERATORS, TEXTURE_GENERATOR_IDS } from './textureGenerators.js';
|
|
7
|
+
|
|
8
|
+
export const TEXTURE_DETAIL_BLENDS = Object.freeze(['overlay', 'add', 'multiply', 'screen', 'min', 'max', 'mix']);
|
|
9
|
+
export const TEXTURE_ACCENT_BLENDS = Object.freeze(['normal', 'multiply', 'overlay', 'screen']);
|
|
10
|
+
export const TEXTURE_EMISSIVE_SOURCES = Object.freeze(['crevices', 'peaks', 'band', 'accentA', 'accentB', 'everywhere']);
|
|
11
|
+
|
|
12
|
+
const GENERATOR_LABELS = Object.freeze(Object.fromEntries(
|
|
13
|
+
TEXTURE_GENERATOR_IDS.map((id) => [id, TEXTURE_GENERATORS[id].label]),
|
|
14
|
+
));
|
|
15
|
+
|
|
16
|
+
const num = (key, label, min, max, step, defaultValue, description, extra = {}) => Object.freeze({
|
|
17
|
+
defaultValue, description, key, label, range: Object.freeze({ max, min, step }), type: 'number', ...extra,
|
|
18
|
+
});
|
|
19
|
+
const bool = (key, label, defaultValue, description, extra = {}) => Object.freeze({
|
|
20
|
+
defaultValue, description, key, label, type: 'boolean', ...extra,
|
|
21
|
+
});
|
|
22
|
+
const sel = (key, label, options, optionLabels, defaultValue, description, extra = {}) => Object.freeze({
|
|
23
|
+
defaultValue, description, key, label, optionLabels, options, type: 'select', ...extra,
|
|
24
|
+
});
|
|
25
|
+
const col = (key, label, defaultValue, description, extra = {}) => Object.freeze({
|
|
26
|
+
defaultValue: Object.freeze(defaultValue), description, key, label, type: 'color', ...extra,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const generatorField = (defaultValue, description) => sel(
|
|
30
|
+
'generator', 'Pattern', TEXTURE_GENERATOR_IDS, GENERATOR_LABELS, defaultValue, description,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Layer parameters shared by base/detail/accent layers. Which ones a given
|
|
34
|
+
// generator reads is declared in TEXTURE_GENERATORS[id].uses — UIs filter
|
|
35
|
+
// on that so only meaningful sliders show.
|
|
36
|
+
const layerParamFields = ({ advancedShape = false } = {}) => [
|
|
37
|
+
num('scale', 'Scale', 1, 64, 1, 6, 'Feature cells across the tile. Higher = finer features.'),
|
|
38
|
+
bool('rotate90', 'Rotate 90°', false, 'Turns the pattern a quarter turn (planks run vertical, strata run horizontal). Tiling stays exact.', { advanced: true }),
|
|
39
|
+
num('detail', 'Detail octaves', 1, 8, 1, 4, 'Fractal octaves layered into the noise.'),
|
|
40
|
+
num('detailGain', 'Detail strength', 0.15, 0.85, 0.01, 0.5, 'How much each finer octave contributes.', { advanced: true }),
|
|
41
|
+
num('stretchX', 'Stretch X', 0.25, 8, 0.05, 1, 'Horizontal anisotropy (brushed metal, wood planks).', { advanced: true }),
|
|
42
|
+
num('stretchY', 'Stretch Y', 0.25, 8, 0.05, 1, 'Vertical anisotropy (drips, strata, fibers).', { advanced: true }),
|
|
43
|
+
num('warp', 'Warp', 0, 1, 0.01, 0, 'Domain warp: melts straight features into organic meanders.'),
|
|
44
|
+
num('warpScale', 'Warp scale', 1, 32, 1, 3, 'Frequency of the warp field.', { advanced: true }),
|
|
45
|
+
num('columns', 'Columns', 1, 64, 1, 4, 'Pattern cells across the tile.', { advanced: advancedShape }),
|
|
46
|
+
num('rows', 'Rows', 1, 64, 1, 8, 'Pattern cells down the tile.', { advanced: advancedShape }),
|
|
47
|
+
num('gap', 'Gap width', 0, 0.4, 0.005, 0.06, 'Mortar/groove width between pattern cells.', { advanced: advancedShape }),
|
|
48
|
+
num('bevel', 'Bevel', 0, 0.5, 0.005, 0.12, 'Edge ramp from groove up to the cell face.', { advanced: advancedShape }),
|
|
49
|
+
num('cellJitter', 'Cell jitter', 0, 1, 0.01, 1, 'Randomizes cell centers: 0 = perfect grid, 1 = organic.', { advanced: true }),
|
|
50
|
+
num('cellVariation', 'Cell variation', 0, 1, 0.01, 0.35, 'Per-cell brightness variance (brick tint shifts).'),
|
|
51
|
+
num('edgeWidth', 'Edge width', 0.01, 0.6, 0.005, 0.12, 'Width of cracks / caustic filaments / speckle chips.', { advanced: advancedShape }),
|
|
52
|
+
num('rings', 'Rings / veins', 1, 32, 1, 6, 'Ring or vein count across the tile (wood, marble).', { advanced: advancedShape }),
|
|
53
|
+
num('grain', 'Grain', 0, 1, 0.01, 0.5, 'Streak amount (wood) or vein sharpness (marble).', { advanced: advancedShape }),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const detailLayerFields = (defaults) => [
|
|
57
|
+
bool('enabled', 'Enabled', defaults.enabled, 'Toggles this detail layer.'),
|
|
58
|
+
generatorField(defaults.generator, 'Pattern blended over the base height.'),
|
|
59
|
+
sel('blend', 'Blend', TEXTURE_DETAIL_BLENDS, {
|
|
60
|
+
add: 'Add', max: 'Lighten', min: 'Darken', mix: 'Mix', multiply: 'Multiply', overlay: 'Overlay', screen: 'Screen',
|
|
61
|
+
}, defaults.blend, 'How this layer combines with the height underneath.'),
|
|
62
|
+
num('amount', 'Amount', 0, 1, 0.01, defaults.amount, 'Blend strength of this layer.'),
|
|
63
|
+
bool('invert', 'Invert', false, 'Flips the layer before blending.', { advanced: true }),
|
|
64
|
+
num('contrast', 'Contrast', -1, 1, 0.01, 0, 'Sharpens (+) or flattens (-) the layer.', { advanced: true }),
|
|
65
|
+
...layerParamFields({ advancedShape: true }).map((field) => (
|
|
66
|
+
field.key === 'scale' ? { ...field, defaultValue: defaults.scale } : field
|
|
67
|
+
)),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const accentLayerFields = (defaults) => [
|
|
71
|
+
bool('enabled', 'Enabled', defaults.enabled, 'Toggles this overlay.'),
|
|
72
|
+
generatorField(defaults.generator, 'Mask pattern deciding where the overlay lands.'),
|
|
73
|
+
col('color', 'Color', defaults.color, 'Overlay color where the mask is strongest.'),
|
|
74
|
+
col('colorB', 'Color B', defaults.colorB, 'Secondary overlay color for variation within the mask.'),
|
|
75
|
+
num('coverage', 'Coverage', 0, 1, 0.01, defaults.coverage, 'How much of the surface the overlay claims.'),
|
|
76
|
+
num('softness', 'Softness', 0.01, 0.6, 0.01, defaults.softness, 'Feather width of the overlay border.'),
|
|
77
|
+
num('creviceBias', 'Crevice bias', -1, 1, 0.01, defaults.creviceBias, '+1 pools into crevices (moss, grime); -1 caps ridges and peaks (snow, wear).'),
|
|
78
|
+
sel('blend', 'Blend', TEXTURE_ACCENT_BLENDS, {
|
|
79
|
+
multiply: 'Multiply', normal: 'Paint', overlay: 'Overlay', screen: 'Screen',
|
|
80
|
+
}, defaults.blend, 'How the overlay color mixes into the albedo.'),
|
|
81
|
+
num('roughnessShift', 'Roughness shift', -1, 1, 0.01, defaults.roughnessShift, 'Overlay area gets rougher (+) or glossier (-).'),
|
|
82
|
+
num('heightShift', 'Height shift', -0.5, 0.5, 0.01, defaults.heightShift, 'Overlay area rises (+) or sinks (-) in the height map.'),
|
|
83
|
+
num('metalShift', 'Metal shift', -1, 1, 0.01, defaults.metalShift, 'Overlay area gains (+) or loses (-) metalness — rust strips metal.', { advanced: true }),
|
|
84
|
+
num('contrast', 'Mask contrast', -1, 1, 0.01, 0, 'Sharpens (+) or flattens (-) the mask pattern.', { advanced: true }),
|
|
85
|
+
bool('invert', 'Invert mask', false, 'Flips the mask before thresholding.', { advanced: true }),
|
|
86
|
+
...layerParamFields({ advancedShape: true }).map((field) => (
|
|
87
|
+
field.key === 'scale' ? { ...field, defaultValue: defaults.scale }
|
|
88
|
+
: field.key === 'warp' ? { ...field, defaultValue: defaults.warp }
|
|
89
|
+
: field
|
|
90
|
+
)),
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const RAW_FIELD_SCHEMA = {
|
|
94
|
+
global: [
|
|
95
|
+
num('seed', 'Seed', 0, 99999, 1, 1337, 'Deterministic seed — every value is a different texture with the same recipe.'),
|
|
96
|
+
],
|
|
97
|
+
base: [
|
|
98
|
+
generatorField('fbm', 'Primary structure of the material: this drives height, color banding, and pattern cells.'),
|
|
99
|
+
num('contrast', 'Contrast', -1, 1, 0.01, 0, 'Sharpens (+) or flattens (-) the base pattern.'),
|
|
100
|
+
num('bias', 'Brightness bias', -0.5, 0.5, 0.01, 0, 'Shifts the whole pattern up or down the ramp.', { advanced: true }),
|
|
101
|
+
bool('invert', 'Invert', false, 'Flips the base pattern (crevices become ridges).'),
|
|
102
|
+
...layerParamFields(),
|
|
103
|
+
],
|
|
104
|
+
detailA: detailLayerFields({ amount: 0.35, blend: 'overlay', enabled: true, generator: 'fbm', scale: 18 }),
|
|
105
|
+
detailB: detailLayerFields({ amount: 0.2, blend: 'add', enabled: false, generator: 'speckle', scale: 24 }),
|
|
106
|
+
color: [
|
|
107
|
+
col('color0', 'Deepest', [0.16, 0.14, 0.13], 'Ramp stop at the darkest crevices.'),
|
|
108
|
+
col('color1', 'Low', [0.35, 0.31, 0.28], 'Ramp stop between crevices and the mid tone.'),
|
|
109
|
+
col('color2', 'Mid', [0.55, 0.5, 0.45], 'Ramp stop for the average surface.'),
|
|
110
|
+
col('color3', 'High', [0.72, 0.68, 0.62], 'Ramp stop approaching the ridges.'),
|
|
111
|
+
col('color4', 'Peak', [0.88, 0.85, 0.79], 'Ramp stop at the highest ridges.'),
|
|
112
|
+
num('pos1', 'Low position', 0.02, 0.98, 0.01, 0.25, 'Where the Low stop sits on the height ramp.', { advanced: true }),
|
|
113
|
+
num('pos2', 'Mid position', 0.02, 0.98, 0.01, 0.5, 'Where the Mid stop sits on the height ramp.', { advanced: true }),
|
|
114
|
+
num('pos3', 'High position', 0.02, 0.98, 0.01, 0.75, 'Where the High stop sits on the height ramp.', { advanced: true }),
|
|
115
|
+
num('rampSmooth', 'Band smoothness', 0, 1, 0.01, 1, '1 = smooth gradient, 0 = hard cel bands between the five stops.'),
|
|
116
|
+
num('jitterHue', 'Hue jitter', 0, 0.5, 0.005, 0.04, 'Painterly hue drift across the surface.'),
|
|
117
|
+
num('jitterValue', 'Value jitter', 0, 0.5, 0.005, 0.08, 'Painterly brightness drift across the surface.'),
|
|
118
|
+
num('jitterScale', 'Jitter scale', 2, 64, 1, 24, 'Frequency of the painterly drift.', { advanced: true }),
|
|
119
|
+
bool('jitterCells', 'Jitter per cell', false, 'Applies drift per pattern cell (per brick / plank / scale) instead of smoothly.'),
|
|
120
|
+
num('cavity', 'Cavity shading', 0, 1, 0.01, 0.35, 'Darkens crevices toward the cavity tint — the hand-painted occlusion read.'),
|
|
121
|
+
col('cavityTint', 'Cavity tint', [0.13, 0.09, 0.08], 'Color the crevices sink toward.', { advanced: true }),
|
|
122
|
+
num('sheen', 'Ridge sheen', 0, 1, 0.01, 0.18, 'Screens the sheen tint over ridges and edges — worn highlight.'),
|
|
123
|
+
col('sheenTint', 'Sheen tint', [1, 0.97, 0.88], 'Color of the ridge highlight.', { advanced: true }),
|
|
124
|
+
num('hueShift', 'Hue shift', -0.5, 0.5, 0.005, 0, 'Rotates the final palette hue.'),
|
|
125
|
+
num('saturation', 'Saturation', 0, 2, 0.01, 1, 'Final color saturation.'),
|
|
126
|
+
num('brightness', 'Brightness', 0.25, 1.75, 0.01, 1, 'Final brightness multiplier.'),
|
|
127
|
+
num('contrast', 'Color contrast', -1, 1, 0.01, 0, 'Final color contrast.'),
|
|
128
|
+
num('gamma', 'Gamma', 0.4, 2.5, 0.01, 1, 'Final gamma on the albedo.', { advanced: true }),
|
|
129
|
+
],
|
|
130
|
+
wear: [
|
|
131
|
+
num('damage', 'Damage', 0, 1, 0.01, 0, 'Universal wear macro: carves seeded scratches and chips into the surface and roughens them. One knob, many parameters.'),
|
|
132
|
+
num('dirt', 'Dirt', 0, 1, 0.01, 0, 'Grime macro: darkens crevices with pooled dirt and raises their roughness, independent of the overlay slots.'),
|
|
133
|
+
],
|
|
134
|
+
accentA: accentLayerFields({
|
|
135
|
+
blend: 'normal', color: [0.35, 0.48, 0.22], colorB: [0.52, 0.62, 0.28], coverage: 0.35,
|
|
136
|
+
creviceBias: 0.5, enabled: false, generator: 'fbm', heightShift: 0.05, metalShift: 0,
|
|
137
|
+
roughnessShift: 0.25, scale: 5, softness: 0.18, warp: 0.3,
|
|
138
|
+
}),
|
|
139
|
+
accentB: accentLayerFields({
|
|
140
|
+
blend: 'multiply', color: [0.16, 0.12, 0.09], colorB: [0.3, 0.24, 0.18], coverage: 0.3,
|
|
141
|
+
creviceBias: 0.6, enabled: false, generator: 'turbulence', heightShift: -0.03, metalShift: 0,
|
|
142
|
+
roughnessShift: 0.2, scale: 4, softness: 0.22, warp: 0.25,
|
|
143
|
+
}),
|
|
144
|
+
surface: [
|
|
145
|
+
num('heightScale', 'Height depth', 0, 1, 0.01, 0.5, 'Overall relief strength — feeds the normal map, AO, and displacement.'),
|
|
146
|
+
num('normalStrength', 'Normal strength', 0, 3, 0.01, 1, 'Extra multiplier on the derived normal map.'),
|
|
147
|
+
bool('invertHeight', 'Invert height', false, 'Flips the height map (grooves become ridges).'),
|
|
148
|
+
num('aoStrength', 'Occlusion', 0, 1, 0.01, 0.55, 'Baked ambient occlusion depth in the crevices.'),
|
|
149
|
+
num('roughness', 'Roughness', 0, 1, 0.01, 0.75, 'Base roughness: 0 = mirror gloss, 1 = fully matte.'),
|
|
150
|
+
num('roughnessContrast', 'Roughness contrast', -1, 1, 0.01, 0.35, '+1 = crevices rough & ridges polished; -1 = the reverse.'),
|
|
151
|
+
num('metalness', 'Metalness', 0, 1, 0.01, 0, 'Base metalness of the material.'),
|
|
152
|
+
],
|
|
153
|
+
emissive: [
|
|
154
|
+
bool('enabled', 'Enabled', false, 'Adds a glow map (lava cracks, sci-fi circuits, embers).'),
|
|
155
|
+
col('color', 'Glow color', [1, 0.45, 0.12], 'Emissive color.'),
|
|
156
|
+
num('intensity', 'Intensity', 0, 8, 0.05, 2, 'Emissive brightness (preview material intensity).'),
|
|
157
|
+
sel('source', 'Glow from', TEXTURE_EMISSIVE_SOURCES, {
|
|
158
|
+
accentA: 'Overlay A', accentB: 'Overlay B', band: 'Height band', crevices: 'Crevices', everywhere: 'Everywhere', peaks: 'Peaks',
|
|
159
|
+
}, 'crevices', 'Which part of the surface glows.'),
|
|
160
|
+
num('threshold', 'Level', 0, 1, 0.01, 0.5, 'Height level the glow hugs (band / crevices / peaks).'),
|
|
161
|
+
num('width', 'Width', 0.02, 0.8, 0.01, 0.25, 'Thickness of the glowing region.'),
|
|
162
|
+
num('softness', 'Softness', 0.01, 0.6, 0.01, 0.2, 'Feather on the glow border.'),
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const TEXTURE_SETTING_GROUPS = Object.freeze([
|
|
167
|
+
Object.freeze({ description: 'Deterministic seed shared by every layer.', id: 'global', label: 'Seed' }),
|
|
168
|
+
Object.freeze({ description: 'The primary structure: pattern, frequency, warp.', id: 'base', label: 'Base pattern' }),
|
|
169
|
+
Object.freeze({ description: 'Mid-frequency relief blended over the base.', id: 'detailA', label: 'Detail layer A' }),
|
|
170
|
+
Object.freeze({ description: 'Fine grain, pores, chips.', id: 'detailB', label: 'Detail layer B' }),
|
|
171
|
+
Object.freeze({ description: 'Five-stop height ramp, painterly jitter, cavity & sheen, final grade.', id: 'color', label: 'Color' }),
|
|
172
|
+
Object.freeze({ description: 'One-knob damage and dirt macros layered over everything.', id: 'wear', label: 'Wear & tear' }),
|
|
173
|
+
Object.freeze({ description: 'Masked colored overlay: moss, rust, dirt, snow, lichen…', id: 'accentA', label: 'Overlay A' }),
|
|
174
|
+
Object.freeze({ description: 'Second masked overlay: grime, stains, scorch, drips…', id: 'accentB', label: 'Overlay B' }),
|
|
175
|
+
Object.freeze({ description: 'PBR response: relief, occlusion, roughness, metalness.', id: 'surface', label: 'Surface' }),
|
|
176
|
+
Object.freeze({ description: 'Optional emissive map.', id: 'emissive', label: 'Glow' }),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
/** group id -> { fieldKey -> field descriptor } (UI SchemaGroup shape). */
|
|
180
|
+
export const TEXTURE_SETTING_FIELD_SCHEMA = Object.freeze(Object.fromEntries(
|
|
181
|
+
Object.entries(RAW_FIELD_SCHEMA).map(([group, fields]) => [
|
|
182
|
+
group,
|
|
183
|
+
Object.freeze(Object.fromEntries(fields.map((field) => [
|
|
184
|
+
field.key,
|
|
185
|
+
Object.freeze({ ...field, group, id: `${group}-${field.key}` }),
|
|
186
|
+
]))),
|
|
187
|
+
]),
|
|
188
|
+
));
|
|
189
|
+
|
|
190
|
+
export const DEFAULT_TEXTURE_SETTINGS = Object.freeze(Object.fromEntries(
|
|
191
|
+
Object.entries(TEXTURE_SETTING_FIELD_SCHEMA).map(([group, fields]) => [
|
|
192
|
+
group,
|
|
193
|
+
Object.freeze(Object.fromEntries(Object.values(fields).map((field) => [
|
|
194
|
+
field.key,
|
|
195
|
+
Array.isArray(field.defaultValue) ? Object.freeze([...field.defaultValue]) : field.defaultValue,
|
|
196
|
+
]))),
|
|
197
|
+
]),
|
|
198
|
+
));
|
|
199
|
+
|
|
200
|
+
/** '#rrggbb' or '#rgb' -> [r, g, b] in 0..1, or null when not parseable. */
|
|
201
|
+
export function hexToRgb01(hex) {
|
|
202
|
+
if (typeof hex !== 'string') return null;
|
|
203
|
+
const m = hex.trim().match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
204
|
+
if (!m) return null;
|
|
205
|
+
let s = m[1];
|
|
206
|
+
if (s.length === 3) s = s.split('').map((c) => c + c).join('');
|
|
207
|
+
return [0, 1, 2].map((i) => parseInt(s.slice(i * 2, i * 2 + 2), 16) / 255);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function rgb01ToHex(rgb) {
|
|
211
|
+
const channel = (v) => Math.round(Math.min(1, Math.max(0, Number(v) || 0)) * 255).toString(16).padStart(2, '0');
|
|
212
|
+
return `#${channel(rgb?.[0])}${channel(rgb?.[1])}${channel(rgb?.[2])}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function clampFieldValue(field, value, fallback) {
|
|
216
|
+
if (field.type === 'number') {
|
|
217
|
+
const v = Number(value);
|
|
218
|
+
if (!Number.isFinite(v)) return fallback;
|
|
219
|
+
const { min, max, step } = field.range;
|
|
220
|
+
const clamped = Math.min(max, Math.max(min, v));
|
|
221
|
+
return step >= 1 ? Math.round(clamped) : clamped;
|
|
222
|
+
}
|
|
223
|
+
if (field.type === 'boolean') {
|
|
224
|
+
if (typeof value === 'boolean') return value;
|
|
225
|
+
if (value === 'true' || value === 1) return true;
|
|
226
|
+
if (value === 'false' || value === 0) return false;
|
|
227
|
+
return fallback;
|
|
228
|
+
}
|
|
229
|
+
if (field.type === 'select') {
|
|
230
|
+
return field.options.includes(value) ? value : fallback;
|
|
231
|
+
}
|
|
232
|
+
if (field.type === 'color') {
|
|
233
|
+
const fromHex = hexToRgb01(value);
|
|
234
|
+
const rgb = fromHex ?? value;
|
|
235
|
+
if (Array.isArray(rgb) && rgb.length >= 3 && rgb.every((c) => Number.isFinite(Number(c)))) {
|
|
236
|
+
return rgb.slice(0, 3).map((c) => Math.min(1, Math.max(0, Number(c))));
|
|
237
|
+
}
|
|
238
|
+
return [...fallback];
|
|
239
|
+
}
|
|
240
|
+
return value === undefined ? fallback : value;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Builds a complete, clamped settings object. Unknown groups/keys in the
|
|
245
|
+
* overrides are ignored; colors accept hex strings or [r,g,b] triplets.
|
|
246
|
+
*/
|
|
247
|
+
export function createTextureSettings(overrides = {}) {
|
|
248
|
+
const result = {};
|
|
249
|
+
for (const [group, fields] of Object.entries(TEXTURE_SETTING_FIELD_SCHEMA)) {
|
|
250
|
+
const groupOverrides = overrides?.[group] ?? {};
|
|
251
|
+
const groupResult = {};
|
|
252
|
+
for (const field of Object.values(fields)) {
|
|
253
|
+
const fallback = Array.isArray(field.defaultValue) ? [...field.defaultValue] : field.defaultValue;
|
|
254
|
+
groupResult[field.key] = field.key in groupOverrides
|
|
255
|
+
? clampFieldValue(field, groupOverrides[field.key], fallback)
|
|
256
|
+
: fallback;
|
|
257
|
+
}
|
|
258
|
+
result[group] = groupResult;
|
|
259
|
+
}
|
|
260
|
+
result.global.seed = Math.max(0, Math.round(Number(result.global.seed) || 0)) % 100000;
|
|
261
|
+
|
|
262
|
+
// Optional image base layer — not schema-driven (no slider maps to a
|
|
263
|
+
// bitmap). Same shape/discipline as debrisgen's surface.customTexture:
|
|
264
|
+
// a data URL plus clamped derivation params, preserved through clones.
|
|
265
|
+
const image = overrides?.image;
|
|
266
|
+
const clampNum = (value, min, max, fallback) => {
|
|
267
|
+
const v = Number(value);
|
|
268
|
+
return Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : fallback;
|
|
269
|
+
};
|
|
270
|
+
result.image = image && typeof image.dataUrl === 'string' && image.dataUrl
|
|
271
|
+
? {
|
|
272
|
+
bands: Math.round(clampNum(image.bands, 0, 10, 0)),
|
|
273
|
+
dataUrl: image.dataUrl,
|
|
274
|
+
heightBase: clampNum(image.heightBase, 0, 1, 0.35),
|
|
275
|
+
heightDetail: clampNum(image.heightDetail, 0, 1, 0.65),
|
|
276
|
+
name: String(image.name ?? 'Image'),
|
|
277
|
+
seamless: image.seamless !== false,
|
|
278
|
+
}
|
|
279
|
+
: null;
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function cloneTextureSettings(settings) {
|
|
284
|
+
return createTextureSettings(settings);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Flattens settings to { 'group.key': value } — the AI patch space. */
|
|
288
|
+
export function flattenTextureSettings(settings) {
|
|
289
|
+
const flat = {};
|
|
290
|
+
for (const [group, fields] of Object.entries(TEXTURE_SETTING_FIELD_SCHEMA)) {
|
|
291
|
+
for (const field of Object.values(fields)) {
|
|
292
|
+
const value = settings?.[group]?.[field.key];
|
|
293
|
+
flat[`${group}.${field.key}`] = Array.isArray(value) ? rgb01ToHex(value) : value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return flat;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Applies { 'group.key': value } patches onto settings with schema
|
|
301
|
+
* clamping. Returns { settings, applied, ignored } — ignored lists the
|
|
302
|
+
* patch keys that named no known field.
|
|
303
|
+
*/
|
|
304
|
+
export function applyTextureSettingsPatch(settings, patch = {}) {
|
|
305
|
+
const next = createTextureSettings(settings);
|
|
306
|
+
const applied = [];
|
|
307
|
+
const ignored = [];
|
|
308
|
+
for (const [path, value] of Object.entries(patch)) {
|
|
309
|
+
const [group, key] = String(path).split('.');
|
|
310
|
+
const field = TEXTURE_SETTING_FIELD_SCHEMA[group]?.[key];
|
|
311
|
+
if (!field) {
|
|
312
|
+
ignored.push(path);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const fallback = next[group][key];
|
|
316
|
+
next[group][key] = clampFieldValue(field, value, fallback);
|
|
317
|
+
applied.push(path);
|
|
318
|
+
}
|
|
319
|
+
return { applied, ignored, settings: next };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export const TEXTURE_RECIPE_KIND = 'toonlab.textureRecipe';
|
|
323
|
+
|
|
324
|
+
export function createTextureRecipeDocument(settings, { name = 'Untitled texture' } = {}) {
|
|
325
|
+
return {
|
|
326
|
+
kind: TEXTURE_RECIPE_KIND,
|
|
327
|
+
name,
|
|
328
|
+
settings: createTextureSettings(settings),
|
|
329
|
+
version: 1,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function validateTextureRecipeDocument(document) {
|
|
334
|
+
const errors = [];
|
|
335
|
+
if (!document || typeof document !== 'object') errors.push('Recipe must be an object.');
|
|
336
|
+
else {
|
|
337
|
+
if (document.kind !== TEXTURE_RECIPE_KIND) errors.push('Unknown recipe kind.');
|
|
338
|
+
if (document.version !== 1) errors.push('Unsupported recipe version.');
|
|
339
|
+
if (!document.settings || typeof document.settings !== 'object') errors.push('Recipe settings are missing.');
|
|
340
|
+
}
|
|
341
|
+
return { errors, ok: errors.length === 0 };
|
|
342
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// THREE adapters for baked texture maps. Follows the repo texture
|
|
2
|
+
// conventions (src/debrisgen/debrisTextures.js): color-carrying maps are
|
|
3
|
+
// tagged SRGBColorSpace, data maps NoColorSpace, everything repeats and
|
|
4
|
+
// mipmaps. DataTexture needs an explicit needsUpdate on every write.
|
|
5
|
+
|
|
6
|
+
import * as THREE from 'three';
|
|
7
|
+
|
|
8
|
+
const MAP_COLOR_SPACES = Object.freeze({
|
|
9
|
+
albedo: 'srgb',
|
|
10
|
+
ao: 'data',
|
|
11
|
+
emissive: 'srgb',
|
|
12
|
+
heightBytes: 'data',
|
|
13
|
+
metalness: 'data',
|
|
14
|
+
normal: 'data',
|
|
15
|
+
orm: 'data',
|
|
16
|
+
roughness: 'data',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const TEXTURE_THREE_MAP_IDS = Object.freeze(Object.keys(MAP_COLOR_SPACES));
|
|
20
|
+
|
|
21
|
+
function configure(texture, kind) {
|
|
22
|
+
texture.colorSpace = kind === 'srgb' ? THREE.SRGBColorSpace : THREE.NoColorSpace;
|
|
23
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
24
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
25
|
+
texture.generateMipmaps = true;
|
|
26
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
27
|
+
texture.magFilter = THREE.LinearFilter;
|
|
28
|
+
texture.needsUpdate = true;
|
|
29
|
+
return texture;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wraps every byte buffer of an evaluateTextureMaps() result in a
|
|
34
|
+
* DataTexture. Pass a previous `textures` object to update in place —
|
|
35
|
+
* buffers of an unchanged size are re-tagged with needsUpdate; a size
|
|
36
|
+
* change disposes and recreates. Returns { textures, recreated }.
|
|
37
|
+
*/
|
|
38
|
+
export function syncTextureMapTextures(maps, textures = null) {
|
|
39
|
+
const sizeChanged = !textures || textures.size !== maps.size;
|
|
40
|
+
if (sizeChanged && textures) {
|
|
41
|
+
for (const id of TEXTURE_THREE_MAP_IDS) textures[id]?.dispose();
|
|
42
|
+
}
|
|
43
|
+
const next = sizeChanged ? { size: maps.size } : textures;
|
|
44
|
+
for (const [id, kind] of Object.entries(MAP_COLOR_SPACES)) {
|
|
45
|
+
if (sizeChanged) {
|
|
46
|
+
next[id] = configure(new THREE.DataTexture(maps[id], maps.size, maps.size, THREE.RGBAFormat), kind);
|
|
47
|
+
} else {
|
|
48
|
+
next[id].image.data = maps[id];
|
|
49
|
+
next[id].needsUpdate = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { recreated: sizeChanged, textures: next };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Disposes every texture created by syncTextureMapTextures. */
|
|
56
|
+
export function disposeTextureMapTextures(textures) {
|
|
57
|
+
if (!textures) return;
|
|
58
|
+
for (const id of TEXTURE_THREE_MAP_IDS) textures[id]?.dispose();
|
|
59
|
+
}
|
|
@@ -138,15 +138,21 @@ function rgbHex(rgb, shade = 1) {
|
|
|
138
138
|
return `#${rgb.map((channel) => channelHex(channel * shade)).join('')}`;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function paintGeometry(geometry, [r, g, b]) {
|
|
141
|
+
function paintGeometry(geometry, [r, g, b], role = 0) {
|
|
142
142
|
const count = geometry.getAttribute('position').count;
|
|
143
143
|
const colors = new Float32Array(count * 3);
|
|
144
|
+
const roles = new Float32Array(count);
|
|
144
145
|
for (let i = 0; i < count; i += 1) {
|
|
145
146
|
colors[i * 3] = r;
|
|
146
147
|
colors[i * 3 + 1] = g;
|
|
147
148
|
colors[i * 3 + 2] = b;
|
|
149
|
+
roles[i] = role;
|
|
148
150
|
}
|
|
149
151
|
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
152
|
+
// 0 = petal, 1 = center. The shared flower shader uses this semantic
|
|
153
|
+
// attribute instead of guessing from color, so recoloring never changes
|
|
154
|
+
// the center/petal lighting relationship.
|
|
155
|
+
geometry.setAttribute('flowerRole', new THREE.BufferAttribute(roles, 1));
|
|
150
156
|
return geometry;
|
|
151
157
|
}
|
|
152
158
|
|
|
@@ -199,7 +205,7 @@ export function createFlowerHeadGeometry({ species, color } = {}) {
|
|
|
199
205
|
.makeRotationZ(azimuth)
|
|
200
206
|
.multiply(new THREE.Matrix4().makeTranslation(0, petalBase, 0))
|
|
201
207
|
.multiply(new THREE.Matrix4().makeRotationX(lift)));
|
|
202
|
-
pieces.push(paintGeometry(petal, petalColor));
|
|
208
|
+
pieces.push(paintGeometry(petal, petalColor, 0));
|
|
203
209
|
}
|
|
204
210
|
}
|
|
205
211
|
|
|
@@ -207,7 +213,7 @@ export function createFlowerHeadGeometry({ species, color } = {}) {
|
|
|
207
213
|
const dome = new THREE.SphereGeometry(centerRadius, 10, 5, 0, Math.PI * 2, 0, Math.PI / 2);
|
|
208
214
|
dome.rotateX(Math.PI / 2);
|
|
209
215
|
dome.scale(1, 1, 0.45);
|
|
210
|
-
pieces.push(paintGeometry(dome, toRgb(spec.center.color)));
|
|
216
|
+
pieces.push(paintGeometry(dome, toRgb(spec.center.color), 1));
|
|
211
217
|
}
|
|
212
218
|
|
|
213
219
|
const merged = mergeGeometries(pieces);
|
|
@@ -256,5 +262,11 @@ export function createFlowerHeadTexture({ color, petal }) {
|
|
|
256
262
|
}
|
|
257
263
|
const texture = new THREE.CanvasTexture(canvas);
|
|
258
264
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
265
|
+
// Semantic asset metadata for composite flower-head materials. The shader
|
|
266
|
+
// uses this mask radius to shade center and petal roles independently; it
|
|
267
|
+
// must not infer the role from color because every palette is valid.
|
|
268
|
+
texture.userData.toonlabFlower = {
|
|
269
|
+
centerRadius: center ? center.radius / canvas.width : 0,
|
|
270
|
+
};
|
|
259
271
|
return texture;
|
|
260
272
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Curated grass material palettes. These are intentionally smaller than a
|
|
2
|
+
// grass preset: they repaint only the blade root, tip, and material shadow
|
|
3
|
+
// tone. Geometry, motion, shadow strength, scene light, and the IP-wide
|
|
4
|
+
// VegetationShaderProfile remain owned by their respective scopes.
|
|
5
|
+
|
|
6
|
+
const COLOR_KEYS = Object.freeze(['baseColor', 'tipColor', 'shadowTint']);
|
|
7
|
+
|
|
8
|
+
function color(value) {
|
|
9
|
+
if (Array.isArray(value) && value.length >= 3) {
|
|
10
|
+
return Object.freeze(value.slice(0, 3).map(Number));
|
|
11
|
+
}
|
|
12
|
+
const hex = String(value ?? '').replace(/^#/, '');
|
|
13
|
+
if (!/^[0-9a-f]{6}$/i.test(hex)) throw new Error(`Invalid grass palette color "${value}".`);
|
|
14
|
+
return Object.freeze([
|
|
15
|
+
parseInt(hex.slice(0, 2), 16) / 255,
|
|
16
|
+
parseInt(hex.slice(2, 4), 16) / 255,
|
|
17
|
+
parseInt(hex.slice(4, 6), 16) / 255,
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function definePalette({ id, label, description, baseColor, tipColor, shadowTint }) {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
baseColor: color(baseColor),
|
|
24
|
+
description,
|
|
25
|
+
id,
|
|
26
|
+
label,
|
|
27
|
+
shadowTint: color(shadowTint),
|
|
28
|
+
tipColor: color(tipColor),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Built-in grass paint palettes. Values are sRGB triplets because
|
|
34
|
+
* StylizedGrassField uploads grass colors with THREE.SRGBColorSpace.
|
|
35
|
+
*/
|
|
36
|
+
export const GRASS_COLOR_PALETTES = Object.freeze([
|
|
37
|
+
definePalette({
|
|
38
|
+
id: 'sensei_meadow',
|
|
39
|
+
label: 'Sensei Meadow',
|
|
40
|
+
description: 'The balanced green meadow used by the studio grass preset.',
|
|
41
|
+
baseColor: [0.42, 0.68, 0.24],
|
|
42
|
+
tipColor: [0.74, 0.9, 0.42],
|
|
43
|
+
shadowTint: [0.42, 0.47, 0.62],
|
|
44
|
+
}),
|
|
45
|
+
definePalette({
|
|
46
|
+
id: 'spring_lime',
|
|
47
|
+
label: 'Spring Lime',
|
|
48
|
+
description: 'Young yellow-green blades with a cool spring shadow.',
|
|
49
|
+
baseColor: '#5fa13e',
|
|
50
|
+
tipColor: '#b9e45f',
|
|
51
|
+
shadowTint: '#66718a',
|
|
52
|
+
}),
|
|
53
|
+
definePalette({
|
|
54
|
+
id: 'deep_forest',
|
|
55
|
+
label: 'Deep Forest',
|
|
56
|
+
description: 'Dense woodland green with a muted blue-green shadow.',
|
|
57
|
+
baseColor: '#245c35',
|
|
58
|
+
tipColor: '#61a857',
|
|
59
|
+
shadowTint: '#455969',
|
|
60
|
+
}),
|
|
61
|
+
definePalette({
|
|
62
|
+
id: 'sage_field',
|
|
63
|
+
label: 'Sage Field',
|
|
64
|
+
description: 'Soft desaturated greens for uplands and windswept fields.',
|
|
65
|
+
baseColor: '#6e8657',
|
|
66
|
+
tipColor: '#b9c98a',
|
|
67
|
+
shadowTint: '#687181',
|
|
68
|
+
}),
|
|
69
|
+
definePalette({
|
|
70
|
+
id: 'dry_prairie',
|
|
71
|
+
label: 'Dry Prairie',
|
|
72
|
+
description: 'Sun-dried ochre grass with a violet-brown shadow.',
|
|
73
|
+
baseColor: '#98713e',
|
|
74
|
+
tipColor: '#d9c476',
|
|
75
|
+
shadowTint: '#6f6274',
|
|
76
|
+
}),
|
|
77
|
+
definePalette({
|
|
78
|
+
id: 'autumn_amber',
|
|
79
|
+
label: 'Autumn Amber',
|
|
80
|
+
description: 'Warm russet and amber blades grounded by a plum shadow.',
|
|
81
|
+
baseColor: '#a4522c',
|
|
82
|
+
tipColor: '#e0a548',
|
|
83
|
+
shadowTint: '#745268',
|
|
84
|
+
}),
|
|
85
|
+
definePalette({
|
|
86
|
+
id: 'wisteria',
|
|
87
|
+
label: 'Wisteria',
|
|
88
|
+
description: 'Fantasy violet grass with lavender tips and a deep lilac shadow.',
|
|
89
|
+
baseColor: '#7254a5',
|
|
90
|
+
tipColor: '#b9a3ed',
|
|
91
|
+
shadowTint: '#62587e',
|
|
92
|
+
}),
|
|
93
|
+
definePalette({
|
|
94
|
+
id: 'moonlit_blue',
|
|
95
|
+
label: 'Moonlit Blue',
|
|
96
|
+
description: 'Cool blue grass for nocturnal, alpine, or magical biomes.',
|
|
97
|
+
baseColor: '#345f85',
|
|
98
|
+
tipColor: '#73b6c5',
|
|
99
|
+
shadowTint: '#4f5d7a',
|
|
100
|
+
}),
|
|
101
|
+
definePalette({
|
|
102
|
+
id: 'sakura_field',
|
|
103
|
+
label: 'Sakura Field',
|
|
104
|
+
description: 'Rose-pink blades with pale blossom tips and a mauve shadow.',
|
|
105
|
+
baseColor: '#a9567d',
|
|
106
|
+
tipColor: '#ed9fbf',
|
|
107
|
+
shadowTint: '#755775',
|
|
108
|
+
}),
|
|
109
|
+
definePalette({
|
|
110
|
+
id: 'crimson_field',
|
|
111
|
+
label: 'Crimson Field',
|
|
112
|
+
description: 'Deep red fantasy grass with coral tips and a cool wine shadow.',
|
|
113
|
+
baseColor: '#7d3041',
|
|
114
|
+
tipColor: '#d66963',
|
|
115
|
+
shadowTint: '#594e69',
|
|
116
|
+
}),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
/** Resolves a built-in palette by id, or accepts one of the catalog entries. */
|
|
120
|
+
export function resolveGrassColorPalette(paletteOrId) {
|
|
121
|
+
if (typeof paletteOrId === 'string') {
|
|
122
|
+
return GRASS_COLOR_PALETTES.find((entry) => entry.id === paletteOrId) ?? null;
|
|
123
|
+
}
|
|
124
|
+
if (paletteOrId && COLOR_KEYS.every((key) => Array.isArray(paletteOrId[key]))) {
|
|
125
|
+
return paletteOrId;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Returns a new settings object with only the coordinated paint trio replaced. */
|
|
131
|
+
export function applyGrassColorPalette(settings = {}, paletteOrId) {
|
|
132
|
+
const palette = resolveGrassColorPalette(paletteOrId);
|
|
133
|
+
if (!palette) throw new Error(`Unknown grass color palette "${String(paletteOrId)}".`);
|
|
134
|
+
return {
|
|
135
|
+
...settings,
|
|
136
|
+
baseColor: [...palette.baseColor],
|
|
137
|
+
shadowTint: [...palette.shadowTint],
|
|
138
|
+
tipColor: [...palette.tipColor],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sameColor(left, right, epsilon) {
|
|
143
|
+
return Array.isArray(left) && Array.isArray(right) && left.length >= 3 && right.length >= 3
|
|
144
|
+
&& left.slice(0, 3).every((channel, index) =>
|
|
145
|
+
Math.abs(Number(channel) - Number(right[index])) <= epsilon);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Infers the active palette from all three colors; no palette id is persisted. */
|
|
149
|
+
export function matchGrassColorPalette(settings, epsilon = 1e-5) {
|
|
150
|
+
return GRASS_COLOR_PALETTES.find((palette) =>
|
|
151
|
+
COLOR_KEYS.every((key) => sameColor(settings?.[key], palette[key], epsilon))) ?? null;
|
|
152
|
+
}
|
|
153
|
+
|
package/src/vegetation/index.js
CHANGED
|
@@ -10,3 +10,9 @@ export * from './treeRecipe.js';
|
|
|
10
10
|
export * from './treeExport.js';
|
|
11
11
|
export * from './scatter.js';
|
|
12
12
|
export * from './stylizedForest.js';
|
|
13
|
+
export * from './vegetationShaders.js';
|
|
14
|
+
export * from './grassPalettes.js';
|
|
15
|
+
export * from '../shaders-tsl/grass.js';
|
|
16
|
+
export * from '../shaders-tsl/tree-leaf.js';
|
|
17
|
+
export * from '../shaders-tsl/flower.js';
|
|
18
|
+
export * from '../shaders-tsl/woody-surface.js';
|
|
@@ -45,6 +45,7 @@ export class StylizedBush extends THREE.Group {
|
|
|
45
45
|
canopy = {},
|
|
46
46
|
canopyLayout = {},
|
|
47
47
|
foliage = {},
|
|
48
|
+
vegetationShader = null,
|
|
48
49
|
} = options;
|
|
49
50
|
this.name = 'StylizedBush';
|
|
50
51
|
// Kept for toJSON(); not cloned (see StylizedTree.config).
|
|
@@ -76,6 +77,7 @@ export class StylizedBush extends THREE.Group {
|
|
|
76
77
|
color: canopyColor,
|
|
77
78
|
palette: canopyPalette,
|
|
78
79
|
seed,
|
|
80
|
+
vegetationShader,
|
|
79
81
|
...foliage,
|
|
80
82
|
});
|
|
81
83
|
|