@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.
Files changed (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +234 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +739 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,385 @@
1
+ // Seeded, extensible biome style generation. Families are only starting
2
+ // domain bundles: recipes embed their complete domain tree and can replace
3
+ // every leaf, so generation is never capped by the built-in choices.
4
+
5
+ import {
6
+ createGeneratorRecipeDocument,
7
+ deepMerge,
8
+ parseGeneratorRecipeDocument,
9
+ resolveGeneratorRecipe,
10
+ serializeGeneratorRecipeDocument,
11
+ validateGeneratorRecipeDocument,
12
+ } from '../core/generation.js';
13
+
14
+ export const BIOME_GENERATOR_DOMAIN = 'biome';
15
+ export const BIOME_PRESET_DOCUMENT_TYPE = 'toonlab/biome-preset';
16
+ export const BIOME_PRESET_SCHEMA_VERSION = 1;
17
+
18
+ const range = (min, max, step, extra = {}) => ({ $type: 'range', min, max, step, ...extra });
19
+ const choice = (options) => ({ $type: 'choice', options });
20
+ const bool = (probability) => ({ $type: 'boolean', probability });
21
+ // Linked interpolation travels through a deliberately authored palette line;
22
+ // independent RGB sampling tends to create accidental mud/brown combinations.
23
+ const color = (from, to, linked = true) => ({ $type: 'color', from, linked, to });
24
+
25
+ export const DEFAULT_BIOME_SETTINGS = Object.freeze({
26
+ atmosphere: {
27
+ fogDensity: 0.00065,
28
+ fogFalloff: 14,
29
+ skyRadius: 420,
30
+ },
31
+ features: {
32
+ floatingIslands: false,
33
+ flowers: true,
34
+ grass: true,
35
+ sinkholes: false,
36
+ trees: true,
37
+ water: true,
38
+ },
39
+ terrain: {
40
+ archetype: 'rollingPlains',
41
+ aspect: 1,
42
+ depth: 42,
43
+ floatingIslandCount: 3,
44
+ morphology: {
45
+ continent: { amp: 86, bias: -0.49, freq: 0.00125 },
46
+ mountains: {
47
+ amp: 112, freq: 0.00082, maskHigh: 0.68, maskLow: 0.49,
48
+ ridgeExp: 1.7, ridgeFreq: 0.0072,
49
+ },
50
+ rim: { base: 44, ridged: 86 },
51
+ rolling: { amp: 18, freq: 0.0038 },
52
+ terraces: { blendHigh: 0.94, blendLow: 0.7, enabled: true, sharpness: 4, step: 16 },
53
+ },
54
+ palette: {
55
+ golden: [0.78, 0.62, 0.24],
56
+ haze: [0.62, 0.76, 0.92],
57
+ meadow: [0.25, 0.58, 0.24],
58
+ rock: [0.58, 0.64, 0.72],
59
+ sand: [0.83, 0.75, 0.49],
60
+ snow: [0.88, 0.92, 0.96],
61
+ },
62
+ sinkholeCount: 3,
63
+ size: 420,
64
+ waterCoverage: 0.18,
65
+ },
66
+ vegetation: {
67
+ flowerDensity: 0.28,
68
+ flowerRadius: 26,
69
+ grassBaseColor: [0.16, 0.42, 0.18],
70
+ grassDensity: 4.2,
71
+ grassRadius: 38,
72
+ grassTipColor: [0.45, 0.72, 0.28],
73
+ treeCanopyColor: [0.23, 0.55, 0.24],
74
+ treeKeepChance: 0.78,
75
+ treeRadius: 105,
76
+ treeSize: 2.8,
77
+ treeSpacing: 10,
78
+ windStrength: 0.18,
79
+ },
80
+ water: {
81
+ deepColor: [0.04, 0.28, 0.48],
82
+ shallowColor: [0.34, 0.82, 0.8],
83
+ },
84
+ });
85
+
86
+ export const DEFAULT_BIOME_GENERATOR_DOMAINS = Object.freeze({
87
+ atmosphere: {
88
+ // Height fog multiplies distance fog by exp(-height / falloff). Outdoor
89
+ // worlds need a short vertical falloff; values in the hundreds haze the
90
+ // entire mountain column and flatten nearly every generated palette.
91
+ fogDensity: range(0.00015, 0.0018, 0.00005, { distribution: 'log' }),
92
+ fogFalloff: range(6, 40, 1, { distribution: 'normal', mean: 14, deviation: 7 }),
93
+ skyRadius: range(280, 900, 10),
94
+ },
95
+ features: {
96
+ floatingIslands: bool(0.12),
97
+ flowers: bool(0.75),
98
+ grass: bool(0.94),
99
+ sinkholes: bool(0.24),
100
+ trees: bool(0.9),
101
+ water: bool(0.9),
102
+ },
103
+ terrain: {
104
+ archetype: choice([
105
+ { value: 'rollingPlains', weight: 3 },
106
+ { value: 'lakeland', weight: 2 },
107
+ { value: 'terracedKarst', weight: 2 },
108
+ { value: 'alpine', weight: 1 },
109
+ { value: 'archipelago', weight: 1 },
110
+ ]),
111
+ aspect: range(0.68, 1.48, 0.01, { distribution: 'normal', mean: 1, deviation: 0.18 }),
112
+ depth: range(18, 120, 1, { distribution: 'normal', mean: 48, deviation: 22 }),
113
+ floatingIslandCount: range(1, 8, 1, { integer: true }),
114
+ morphology: {
115
+ continent: {
116
+ amp: range(40, 190, 1),
117
+ bias: range(-0.7, -0.34, 0.005),
118
+ freq: range(0.00065, 0.0022, 0.00001, { distribution: 'log' }),
119
+ },
120
+ mountains: {
121
+ amp: range(28, 300, 1, { distribution: 'normal', mean: 126, deviation: 55 }),
122
+ freq: range(0.00045, 0.0018, 0.00001, { distribution: 'log' }),
123
+ maskHigh: range(0.56, 0.82, 0.005),
124
+ maskLow: range(0.32, 0.62, 0.005),
125
+ ridgeExp: range(1.15, 2.8, 0.01),
126
+ ridgeFreq: range(0.0035, 0.014, 0.0001, { distribution: 'log' }),
127
+ },
128
+ rim: {
129
+ base: range(24, 110, 1),
130
+ ridged: range(44, 210, 1),
131
+ },
132
+ rolling: {
133
+ amp: range(5, 42, 0.5),
134
+ freq: range(0.0018, 0.009, 0.0001, { distribution: 'log' }),
135
+ },
136
+ terraces: {
137
+ blendHigh: range(0.78, 1, 0.005),
138
+ blendLow: range(0.42, 0.84, 0.005),
139
+ enabled: bool(0.62),
140
+ sharpness: range(1.5, 8, 0.1),
141
+ step: range(5, 38, 0.5),
142
+ },
143
+ },
144
+ palette: {
145
+ golden: color([0.45, 0.28, 0.08], [0.98, 0.78, 0.35]),
146
+ haze: color([0.42, 0.56, 0.72], [0.82, 0.9, 1]),
147
+ meadow: color([0.08, 0.28, 0.1], [0.55, 0.76, 0.36]),
148
+ rock: color([0.32, 0.34, 0.38], [0.82, 0.78, 0.74]),
149
+ sand: color([0.58, 0.4, 0.2], [0.96, 0.88, 0.62]),
150
+ snow: color([0.76, 0.82, 0.9], [1, 0.98, 0.96]),
151
+ },
152
+ sinkholeCount: range(1, 9, 1, { integer: true }),
153
+ size: range(180, 1100, 10, { distribution: 'log' }),
154
+ waterCoverage: range(0.01, 0.58, 0.005, { distribution: 'normal', mean: 0.2, deviation: 0.15 }),
155
+ },
156
+ vegetation: {
157
+ flowerDensity: range(0.03, 1.2, 0.01, { distribution: 'log' }),
158
+ flowerRadius: range(12, 70, 1),
159
+ grassBaseColor: color([0.05, 0.16, 0.06], [0.38, 0.58, 0.22]),
160
+ grassDensity: range(0.8, 10, 0.1, { distribution: 'log' }),
161
+ grassRadius: range(18, 78, 1),
162
+ grassTipColor: color([0.22, 0.4, 0.1], [0.72, 0.86, 0.38]),
163
+ treeCanopyColor: color([0.06, 0.22, 0.08], [0.56, 0.74, 0.3]),
164
+ treeKeepChance: range(0.28, 1, 0.01),
165
+ treeRadius: range(48, 240, 2),
166
+ treeSize: range(1.4, 4.8, 0.05),
167
+ treeSpacing: range(5, 24, 0.5, { distribution: 'log' }),
168
+ windStrength: range(0.03, 0.55, 0.01),
169
+ },
170
+ water: {
171
+ deepColor: color([0.005, 0.08, 0.14], [0.13, 0.46, 0.54]),
172
+ shallowColor: color([0.16, 0.54, 0.52], [0.58, 0.96, 0.84]),
173
+ },
174
+ });
175
+
176
+ const FAMILY_DEFINITIONS = new Map([
177
+ ['living-landscape', {
178
+ description: 'Broad game-ready landscape domains with bounded vegetation budgets.',
179
+ domains: DEFAULT_BIOME_GENERATOR_DOMAINS,
180
+ label: 'Living Landscape',
181
+ }],
182
+ ['sky-realm', {
183
+ configuration: { features: { floatingIslands: true, water: true } },
184
+ domains: deepMerge(DEFAULT_BIOME_GENERATOR_DOMAINS, {
185
+ features: { floatingIslands: { $type: 'boolean', probability: 0.92 } },
186
+ terrain: {
187
+ floatingIslandCount: range(3, 12, 1, { integer: true }),
188
+ waterCoverage: range(0.18, 0.56, 0.005),
189
+ },
190
+ }),
191
+ label: 'Sky Realm',
192
+ }],
193
+ ]);
194
+
195
+ function clamp(value, min, max) {
196
+ return Math.min(Math.max(value, min), max);
197
+ }
198
+
199
+ function number(value, fallback, min = -Infinity, max = Infinity) {
200
+ return clamp(Number.isFinite(Number(value)) ? Number(value) : fallback, min, max);
201
+ }
202
+
203
+ function colorArray(value, fallback) {
204
+ if (!Array.isArray(value) || value.length < 3) return [...fallback];
205
+ return value.slice(0, 3).map((channel, index) => number(channel, fallback[index], 0, 1));
206
+ }
207
+
208
+ /** Normalizes a resolved biome into serializable runtime settings. */
209
+ export function sanitizeBiomeSettings(input = {}) {
210
+ const source = deepMerge(DEFAULT_BIOME_SETTINGS, input);
211
+ const terrain = source.terrain ?? {};
212
+ const morphology = terrain.morphology ?? {};
213
+ const mountains = morphology.mountains ?? {};
214
+ const terraces = morphology.terraces ?? {};
215
+ const maskLow = number(mountains.maskLow, 0.49, 0.05, 0.9);
216
+ const blendLow = number(terraces.blendLow, 0.7, 0, 0.98);
217
+ const output = {
218
+ atmosphere: {
219
+ fogDensity: number(source.atmosphere?.fogDensity, 0.00065, 0, 0.02),
220
+ fogFalloff: number(source.atmosphere?.fogFalloff, 14, 1, 2000),
221
+ skyRadius: number(source.atmosphere?.skyRadius, 420, 50, 4000),
222
+ },
223
+ features: Object.fromEntries(
224
+ Object.keys(DEFAULT_BIOME_SETTINGS.features).map((key) => [key, Boolean(source.features?.[key])]),
225
+ ),
226
+ terrain: {
227
+ archetype: String(terrain.archetype || 'rollingPlains'),
228
+ aspect: number(terrain.aspect, 1, 0.25, 4),
229
+ depth: number(terrain.depth, 42, 1, 500),
230
+ floatingIslandCount: Math.round(number(terrain.floatingIslandCount, 3, 1, 64)),
231
+ morphology: {
232
+ continent: {
233
+ amp: number(morphology.continent?.amp, 86, 1, 1000),
234
+ bias: number(morphology.continent?.bias, -0.49, -2, 1),
235
+ freq: number(morphology.continent?.freq, 0.00125, 0.00001, 1),
236
+ },
237
+ mountains: {
238
+ amp: number(mountains.amp, 112, 1, 1600),
239
+ freq: number(mountains.freq, 0.00082, 0.00001, 1),
240
+ maskHigh: number(mountains.maskHigh, 0.68, maskLow + 0.01, 1),
241
+ maskLow,
242
+ ridgeExp: number(mountains.ridgeExp, 1.7, 0.1, 12),
243
+ ridgeFreq: number(mountains.ridgeFreq, 0.0072, 0.00001, 1),
244
+ },
245
+ rim: {
246
+ base: number(morphology.rim?.base, 44, 0, 1000),
247
+ ridged: number(morphology.rim?.ridged, 86, 0, 1600),
248
+ },
249
+ rolling: {
250
+ amp: number(morphology.rolling?.amp, 18, 0, 500),
251
+ freq: number(morphology.rolling?.freq, 0.0038, 0.00001, 1),
252
+ },
253
+ terraces: {
254
+ blendHigh: number(terraces.blendHigh, 0.94, blendLow + 0.01, 1),
255
+ blendLow,
256
+ enabled: Boolean(terraces.enabled),
257
+ sharpness: number(terraces.sharpness, 4, 0.1, 30),
258
+ step: number(terraces.step, 16, 0.1, 500),
259
+ },
260
+ },
261
+ palette: Object.fromEntries(Object.entries(DEFAULT_BIOME_SETTINGS.terrain.palette).map(
262
+ ([key, fallback]) => [key, colorArray(terrain.palette?.[key], fallback)],
263
+ )),
264
+ sinkholeCount: Math.round(number(terrain.sinkholeCount, 3, 1, 64)),
265
+ size: number(terrain.size, 420, 40, 10000),
266
+ waterCoverage: number(terrain.waterCoverage, 0.18, 0, 0.6),
267
+ },
268
+ vegetation: {
269
+ flowerDensity: number(source.vegetation?.flowerDensity, 0.28, 0, 20),
270
+ flowerRadius: number(source.vegetation?.flowerRadius, 26, 1, 1000),
271
+ grassBaseColor: colorArray(source.vegetation?.grassBaseColor, DEFAULT_BIOME_SETTINGS.vegetation.grassBaseColor),
272
+ grassDensity: number(source.vegetation?.grassDensity, 4.2, 0, 30),
273
+ grassRadius: number(source.vegetation?.grassRadius, 38, 1, 1000),
274
+ grassTipColor: colorArray(source.vegetation?.grassTipColor, DEFAULT_BIOME_SETTINGS.vegetation.grassTipColor),
275
+ treeCanopyColor: colorArray(source.vegetation?.treeCanopyColor, DEFAULT_BIOME_SETTINGS.vegetation.treeCanopyColor),
276
+ treeKeepChance: number(source.vegetation?.treeKeepChance, 0.78, 0, 1),
277
+ treeRadius: number(source.vegetation?.treeRadius, 105, 1, 5000),
278
+ treeSize: number(source.vegetation?.treeSize, 2.8, 0.1, 30),
279
+ treeSpacing: number(source.vegetation?.treeSpacing, 10, 0.5, 200),
280
+ windStrength: number(source.vegetation?.windStrength, 0.18, 0, 4),
281
+ },
282
+ water: {
283
+ deepColor: colorArray(source.water?.deepColor, DEFAULT_BIOME_SETTINGS.water.deepColor),
284
+ shallowColor: colorArray(source.water?.shallowColor, DEFAULT_BIOME_SETTINGS.water.shallowColor),
285
+ },
286
+ };
287
+ return output;
288
+ }
289
+
290
+ export function registerBiomeGeneratorFamily(id, definition = {}, { overwrite = false } = {}) {
291
+ const key = String(id ?? '').trim();
292
+ if (!key) throw new Error('Biome generator family id is required.');
293
+ if (!overwrite && FAMILY_DEFINITIONS.has(key)) throw new Error(`Biome generator family "${key}" already exists.`);
294
+ FAMILY_DEFINITIONS.set(key, deepMerge(definition));
295
+ return key;
296
+ }
297
+
298
+ export function getBiomeGeneratorFamilyOptions() {
299
+ return [...FAMILY_DEFINITIONS.entries()].map(([id, definition]) => ({
300
+ description: definition.description ?? '', id, label: definition.label ?? id,
301
+ }));
302
+ }
303
+
304
+ export function createBiomeGeneratorRecipe(id = 'generated-biome', definition = {}) {
305
+ const family = FAMILY_DEFINITIONS.get(definition.family ?? 'living-landscape') ?? {};
306
+ return createGeneratorRecipeDocument(BIOME_GENERATOR_DOMAIN, id, {
307
+ basePreset: definition.basePreset ?? 'outdoorGameplay',
308
+ configuration: deepMerge(family.configuration, definition.configuration),
309
+ description: definition.description ?? '',
310
+ domains: deepMerge(family.domains ?? DEFAULT_BIOME_GENERATOR_DOMAINS, definition.domains),
311
+ label: definition.label ?? id,
312
+ locks: definition.locks ?? [],
313
+ seed: definition.seed ?? 1,
314
+ });
315
+ }
316
+
317
+ export function validateBiomeGeneratorRecipe(input) {
318
+ return validateGeneratorRecipeDocument(input, { domain: BIOME_GENERATOR_DOMAIN });
319
+ }
320
+
321
+ export function parseBiomeGeneratorRecipe(input) {
322
+ return parseGeneratorRecipeDocument(input, { domain: BIOME_GENERATOR_DOMAIN });
323
+ }
324
+
325
+ export function serializeBiomeGeneratorRecipe(idOrDocument, definition = {}, { pretty = true } = {}) {
326
+ return serializeGeneratorRecipeDocument(BIOME_GENERATOR_DOMAIN, idOrDocument, definition, { pretty });
327
+ }
328
+
329
+ const QUALITY_BUDGETS = Object.freeze({
330
+ mobile: { flowerDensity: 0.18, grassDensity: 2.5, grassRadius: 28, segments: 64, treeRadius: 72, treeSpacing: 13 },
331
+ balanced: { flowerDensity: 0.5, grassDensity: 6, grassRadius: 48, segments: 112, treeRadius: 130, treeSpacing: 8 },
332
+ cinematic: { flowerDensity: 1.2, grassDensity: 10, grassRadius: 78, segments: 192, treeRadius: 240, treeSpacing: 5 },
333
+ });
334
+
335
+ export function applyBiomeQualityBudget(input, quality = 'balanced') {
336
+ const settings = sanitizeBiomeSettings(input);
337
+ const budget = QUALITY_BUDGETS[quality] ?? QUALITY_BUDGETS.balanced;
338
+ settings.runtime = {
339
+ quality: QUALITY_BUDGETS[quality] ? quality : 'balanced',
340
+ terrainSegments: budget.segments,
341
+ };
342
+ settings.vegetation.flowerDensity = Math.min(settings.vegetation.flowerDensity, budget.flowerDensity);
343
+ settings.vegetation.grassDensity = Math.min(settings.vegetation.grassDensity, budget.grassDensity);
344
+ settings.vegetation.grassRadius = Math.min(settings.vegetation.grassRadius, budget.grassRadius);
345
+ settings.vegetation.treeRadius = Math.min(settings.vegetation.treeRadius, budget.treeRadius);
346
+ settings.vegetation.treeSpacing = Math.max(settings.vegetation.treeSpacing, budget.treeSpacing);
347
+ return settings;
348
+ }
349
+
350
+ /** Resolves a generator recipe into flat, budgeted runtime settings. */
351
+ export function resolveBiomeGeneratorRecipe(recipeInput, { quality = 'balanced' } = {}) {
352
+ const validation = recipeInput?.type
353
+ ? validateBiomeGeneratorRecipe(recipeInput)
354
+ : { ok: true, value: createBiomeGeneratorRecipe('generated-biome', recipeInput) };
355
+ if (!validation.ok) throw new Error(validation.errors.join(' '));
356
+ const settings = resolveGeneratorRecipe(validation.value, {
357
+ baseSettings: DEFAULT_BIOME_SETTINGS,
358
+ sanitizeSettings: sanitizeBiomeSettings,
359
+ });
360
+ return applyBiomeQualityBudget(settings, quality);
361
+ }
362
+
363
+ export function createGeneratedBiomePresetDocument(recipeInput, options = {}) {
364
+ const validation = recipeInput?.type
365
+ ? validateBiomeGeneratorRecipe(recipeInput)
366
+ : { ok: true, value: createBiomeGeneratorRecipe('generated-biome', recipeInput) };
367
+ if (!validation.ok) throw new Error(validation.errors.join(' '));
368
+ const recipe = validation.value;
369
+ const settings = resolveBiomeGeneratorRecipe(recipe, { quality: options.quality });
370
+ return {
371
+ basePreset: recipe.basePreset ?? 'outdoorGameplay',
372
+ description: `Resolved from generator seed ${recipe.seed}.`,
373
+ id: options.id ?? `${recipe.id}-resolved`,
374
+ label: options.label ?? `${recipe.label} · resolved`,
375
+ seed: recipe.seed,
376
+ settings,
377
+ source: {
378
+ recipeId: recipe.id,
379
+ recipeVersion: recipe.version,
380
+ seed: recipe.seed,
381
+ },
382
+ type: BIOME_PRESET_DOCUMENT_TYPE,
383
+ version: BIOME_PRESET_SCHEMA_VERSION,
384
+ };
385
+ }
@@ -0,0 +1,299 @@
1
+ import * as THREE from 'three';
2
+
3
+ import { createStylizedTerrain } from '../stylizedTerrain.js';
4
+ import { createStylizedWorld } from '../stylizedWorld.js';
5
+ import {
6
+ applyBiomeQualityBudget,
7
+ resolveBiomeGeneratorRecipe,
8
+ } from './biomeGenerator.js';
9
+
10
+ function colorValue(value) {
11
+ if (!Array.isArray(value)) return value;
12
+ return new THREE.Color().setRGB(
13
+ Number(value[0]) || 0,
14
+ Number(value[1]) || 0,
15
+ Number(value[2]) || 0,
16
+ );
17
+ }
18
+
19
+ function terrainOptionsFromSettings(settings, seed) {
20
+ const terrain = settings.terrain;
21
+ const morphology = terrain.morphology;
22
+ const aspectRoot = Math.sqrt(terrain.aspect);
23
+ return {
24
+ archetype: terrain.archetype,
25
+ depth: terrain.depth,
26
+ detailTexture: true,
27
+ floatingIslands: settings.features.floatingIslands
28
+ ? { count: terrain.floatingIslandCount }
29
+ : false,
30
+ morphology: {
31
+ continent: { ...morphology.continent },
32
+ mountains: {
33
+ amp: morphology.mountains.amp,
34
+ freq: morphology.mountains.freq,
35
+ mask: [morphology.mountains.maskLow, morphology.mountains.maskHigh],
36
+ ridgeExp: morphology.mountains.ridgeExp,
37
+ ridgeFreq: morphology.mountains.ridgeFreq,
38
+ },
39
+ rim: { ...morphology.rim },
40
+ rolling: { ...morphology.rolling },
41
+ terraces: morphology.terraces.enabled
42
+ ? {
43
+ blendOff: [morphology.terraces.blendLow, morphology.terraces.blendHigh],
44
+ sharpness: morphology.terraces.sharpness,
45
+ step: morphology.terraces.step,
46
+ }
47
+ : false,
48
+ },
49
+ palette: Object.fromEntries(
50
+ Object.entries(terrain.palette).map(([key, value]) => [key, colorValue(value)]),
51
+ ),
52
+ seed,
53
+ segments: settings.runtime.terrainSegments,
54
+ sinkholes: settings.features.sinkholes ? { count: terrain.sinkholeCount } : false,
55
+ size: {
56
+ x: terrain.size * aspectRoot,
57
+ z: terrain.size / aspectRoot,
58
+ },
59
+ waterCoverage: settings.features.water ? terrain.waterCoverage : 0,
60
+ };
61
+ }
62
+
63
+ function resolveRuntimeSettings(source, quality) {
64
+ if (source?.type === 'toonlab/biome-generator') {
65
+ return resolveBiomeGeneratorRecipe(source, { quality });
66
+ }
67
+ if (source?.type === 'toonlab/biome-preset') {
68
+ return applyBiomeQualityBudget(source.settings, quality);
69
+ }
70
+ return applyBiomeQualityBudget(source?.settings ?? source, quality);
71
+ }
72
+
73
+ function countRuntimeGeometry(roots) {
74
+ const seen = new Set();
75
+ let drawables = 0;
76
+ let instances = 0;
77
+ let triangles = 0;
78
+ let vertices = 0;
79
+ for (const root of roots) {
80
+ root?.traverse?.((object) => {
81
+ if (seen.has(object.uuid) || !object.geometry) return;
82
+ seen.add(object.uuid);
83
+ const multiplier = object.isInstancedMesh ? Math.max(Number(object.count) || 0, 1) : 1;
84
+ const positionCount = Number(object.geometry.attributes?.position?.count) || 0;
85
+ const triangleCount = object.geometry.index
86
+ ? object.geometry.index.count / 3
87
+ : positionCount / 3;
88
+ drawables += 1;
89
+ instances += multiplier;
90
+ triangles += triangleCount * multiplier;
91
+ vertices += positionCount * multiplier;
92
+ });
93
+ }
94
+ return {
95
+ drawables,
96
+ instances,
97
+ triangles: Math.round(triangles),
98
+ vertices: Math.round(vertices),
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Builds and owns a complete generated biome. The async runtime mirrors an
104
+ * editor's Generate button but is safe to ship: it applies a named quality
105
+ * budget, owns every allocation, and exposes update/regenerate/dispose.
106
+ * Pass a generator as `recipe`, a resolved document as `preset`, or legacy
107
+ * flat settings as `settings`; resolved presets retain their seed and world
108
+ * base so design-time and runtime builds stay identical.
109
+ */
110
+ export async function createBiomeRuntime({
111
+ camera = null,
112
+ followTarget = null,
113
+ quality = 'balanced',
114
+ preset = null,
115
+ recipe = null,
116
+ renderer,
117
+ scene,
118
+ seed = null,
119
+ settings = null,
120
+ weather = false,
121
+ worldOptions = {},
122
+ } = {}) {
123
+ if (!renderer || !scene) throw new Error('createBiomeRuntime needs { renderer, scene }.');
124
+
125
+ let disposed = false;
126
+ let generation = 0;
127
+ let terrain = null;
128
+ let world = null;
129
+ let resolvedSettings = null;
130
+ let source = recipe ?? preset ?? settings ?? {};
131
+ let activeQuality = quality;
132
+ let activeSeed = Number(seed ?? source?.seed ?? 1) >>> 0 || 1;
133
+
134
+ async function build(nextSource = source, options = {}) {
135
+ if (disposed) throw new Error('Cannot regenerate a disposed biome runtime.');
136
+ const buildId = ++generation;
137
+ const nextQuality = options.quality ?? activeQuality;
138
+ const nextSeed = Number(options.seed ?? nextSource?.seed ?? activeSeed) >>> 0 || 1;
139
+ const nextSettings = resolveRuntimeSettings(nextSource, nextQuality);
140
+ const nextTerrain = createStylizedTerrain(terrainOptionsFromSettings(nextSettings, nextSeed));
141
+ scene.add(nextTerrain.root);
142
+
143
+ let nextWorld;
144
+ try {
145
+ const vegetation = nextSettings.vegetation;
146
+ nextWorld = await createStylizedWorld({
147
+ ...worldOptions,
148
+ applyCamera: worldOptions.applyCamera ?? true,
149
+ camera,
150
+ environment: {
151
+ ...(worldOptions.environment ?? {}),
152
+ parameters: {
153
+ heightFogDensity: nextSettings.atmosphere.fogDensity,
154
+ heightFogFalloff: nextSettings.atmosphere.fogFalloff,
155
+ ...(worldOptions.environment?.parameters ?? {}),
156
+ },
157
+ },
158
+ flowers: nextSettings.features.flowers
159
+ ? {
160
+ ...(worldOptions.flowers ?? {}),
161
+ scatter: {
162
+ density: vegetation.flowerDensity,
163
+ radius: vegetation.flowerRadius,
164
+ seed: nextSeed + 307,
165
+ ...(worldOptions.flowers?.scatter ?? {}),
166
+ },
167
+ }
168
+ : false,
169
+ followTarget,
170
+ grass: nextSettings.features.grass
171
+ ? {
172
+ ...(worldOptions.grass ?? {}),
173
+ scatter: {
174
+ density: vegetation.grassDensity,
175
+ radius: vegetation.grassRadius,
176
+ seed: nextSeed + 211,
177
+ ...(worldOptions.grass?.scatter ?? {}),
178
+ },
179
+ settings: {
180
+ baseColor: vegetation.grassBaseColor,
181
+ tipColor: vegetation.grassTipColor,
182
+ windStrength: vegetation.windStrength,
183
+ ...(worldOptions.grass?.settings ?? {}),
184
+ },
185
+ }
186
+ : false,
187
+ preset: nextSource?.basePreset ?? worldOptions.preset ?? 'outdoorGameplay',
188
+ renderer,
189
+ scene,
190
+ shadows: {
191
+ interval: nextQuality === 'cinematic' ? 1 : nextQuality === 'mobile' ? 3 : 2,
192
+ ...(worldOptions.shadows ?? {}),
193
+ },
194
+ sky: {
195
+ radius: nextSettings.atmosphere.skyRadius,
196
+ ...(worldOptions.sky ?? {}),
197
+ },
198
+ terrain: {
199
+ heightAt: nextTerrain.heightAt,
200
+ root: nextTerrain.root,
201
+ size: nextTerrain.meshExtent,
202
+ },
203
+ trees: nextSettings.features.trees
204
+ ? {
205
+ ...(worldOptions.trees ?? {}),
206
+ canopyColors: [vegetation.treeCanopyColor],
207
+ lod: {
208
+ detailCount: nextQuality === 'mobile' ? 30 : nextQuality === 'cinematic' ? 120 : 72,
209
+ variants: nextQuality === 'mobile' ? 3 : nextQuality === 'cinematic' ? 8 : 5,
210
+ ...(worldOptions.trees?.lod ?? {}),
211
+ },
212
+ scatter: {
213
+ keepChance: vegetation.treeKeepChance,
214
+ radius: vegetation.treeRadius,
215
+ seed: nextSeed + 101,
216
+ spacing: vegetation.treeSpacing,
217
+ ...(worldOptions.trees?.scatter ?? {}),
218
+ },
219
+ settings: {
220
+ size: vegetation.treeSize,
221
+ ...(worldOptions.trees?.settings ?? {}),
222
+ },
223
+ }
224
+ : false,
225
+ water: nextSettings.features.water
226
+ ? {
227
+ ...(worldOptions.water ?? {}),
228
+ level: nextTerrain.waterLevel,
229
+ settings: {
230
+ deepColor: nextSettings.water.deepColor,
231
+ shallowColor: nextSettings.water.shallowColor,
232
+ ...(worldOptions.water?.settings ?? {}),
233
+ },
234
+ }
235
+ : false,
236
+ weather: worldOptions.weather ?? weather,
237
+ });
238
+ } catch (error) {
239
+ nextTerrain.dispose();
240
+ throw error;
241
+ }
242
+
243
+ if (disposed || buildId !== generation) {
244
+ nextWorld.dispose();
245
+ nextTerrain.dispose();
246
+ return false;
247
+ }
248
+
249
+ world?.dispose();
250
+ terrain?.dispose();
251
+ world = nextWorld;
252
+ terrain = nextTerrain;
253
+ resolvedSettings = nextSettings;
254
+ source = nextSource;
255
+ activeQuality = nextQuality;
256
+ activeSeed = nextSeed;
257
+ return true;
258
+ }
259
+
260
+ const api = {
261
+ dispose() {
262
+ if (disposed) return;
263
+ disposed = true;
264
+ generation += 1;
265
+ world?.dispose();
266
+ terrain?.dispose();
267
+ world = null;
268
+ terrain = null;
269
+ },
270
+ get settings() { return resolvedSettings; },
271
+ get terrain() { return terrain; },
272
+ get world() { return world; },
273
+ regenerate: build,
274
+ stats() {
275
+ const geometry = countRuntimeGeometry([
276
+ terrain?.root,
277
+ world?.sky,
278
+ world?.water,
279
+ world?.forest,
280
+ world?.grass,
281
+ world?.flowerField,
282
+ ]);
283
+ return {
284
+ disposed,
285
+ quality: activeQuality,
286
+ seed: activeSeed,
287
+ terrainAttempts: terrain?.stats?.attempts ?? 0,
288
+ treePlacements: world?.forest?._placements?.length ?? 0,
289
+ ...geometry,
290
+ };
291
+ },
292
+ update(delta = 0.016) {
293
+ if (!disposed) world?.update(delta);
294
+ },
295
+ };
296
+
297
+ await build(source, { quality: activeQuality, seed: activeSeed });
298
+ return api;
299
+ }
@@ -0,0 +1,2 @@
1
+ export * from './biomeGenerator.js';
2
+ export * from './biomeRuntime.js';