@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,492 @@
1
+ // Procedural low-poly fauna bodies — seeded, palette-driven, no model files.
2
+ // Same philosophy as debrisgen pieces: geometry + vertex color carry the
3
+ // whole read. Bodies are silhouette-first (a bird must read at 100 m), with
4
+ // anime cel shading BAKED into the vertex colors (lit top, cool shaded
5
+ // underside): the meshes render UNLIT, exactly like the forest billboard
6
+ // impostors — distant creatures with no scene-light dependency never turn
7
+ // near-black against a bright sky, which is how modern anime layers its
8
+ // ambient life. Near-field hosts that want scene lighting can still rebuild
9
+ // with their own material; the geometry carries plain `color` attributes.
10
+ //
11
+ // Every geometry ships two animation attributes consumed by the fauna TSL
12
+ // vertex stage (see stylizedFauna.js):
13
+ // aWing — signed lateral extent from the spine in model meters (0 = body);
14
+ // drives the pseudo-rotation wing flap.
15
+ // aTail — 0 at the nose → 1 at the tail tip; drives the fish body sway.
16
+ //
17
+ // Model conventions: forward = +Z, up = +Y, wings along ±X, real-world
18
+ // meters at scale 1.
19
+
20
+ import * as THREE from 'three';
21
+
22
+ import { hashCombine } from './boids.js';
23
+ import { FAUNA_PALETTE_IDS } from './faunaSettings.js';
24
+
25
+ export { hashCombine };
26
+
27
+ function mulberry32(seed) {
28
+ let state = (Math.trunc(seed) || 1) >>> 0;
29
+ return () => {
30
+ state = (state + 0x6d2b79f5) >>> 0;
31
+ let t = state;
32
+ t = Math.imul(t ^ (t >>> 15), t | 1);
33
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
34
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
35
+ };
36
+ }
37
+
38
+ // Palette colors are authored in sRGB and stored linear (vertex colors feed
39
+ // the node material's working space, same as the tree-export bake).
40
+ const colorScratch = new THREE.Color();
41
+ function srgb(r, g, b) {
42
+ colorScratch.setRGB(r, g, b, THREE.SRGBColorSpace);
43
+ return [colorScratch.r, colorScratch.g, colorScratch.b];
44
+ }
45
+
46
+ // ---------------------------------------------------------------- palettes
47
+ //
48
+ // Each palette entry is a list of variant schemes; the scheme count is the
49
+ // species' variant count (2–4 per the fauna plan). Ids must match
50
+ // FAUNA_PALETTE_IDS in faunaSettings.js.
51
+
52
+ export const BIRD_PALETTES = Object.freeze({
53
+ swallow: [
54
+ { back: srgb(0.16, 0.2, 0.42), belly: srgb(0.96, 0.93, 0.84), throat: srgb(0.82, 0.42, 0.25), wing: srgb(0.12, 0.15, 0.34), tip: srgb(0.07, 0.09, 0.2) },
55
+ { back: srgb(0.2, 0.3, 0.52), belly: srgb(0.98, 0.97, 0.94), throat: srgb(0.9, 0.55, 0.3), wing: srgb(0.14, 0.2, 0.42), tip: srgb(0.08, 0.11, 0.24) },
56
+ { back: srgb(0.13, 0.14, 0.24), belly: srgb(0.88, 0.86, 0.82), throat: srgb(0.7, 0.32, 0.22), wing: srgb(0.1, 0.11, 0.2), tip: srgb(0.05, 0.06, 0.12) },
57
+ ],
58
+ egret: [
59
+ { back: srgb(0.97, 0.97, 0.95), belly: srgb(1.0, 1.0, 0.98), throat: srgb(0.98, 0.95, 0.88), wing: srgb(0.88, 0.9, 0.92), tip: srgb(0.28, 0.3, 0.36) },
60
+ { back: srgb(0.78, 0.82, 0.88), belly: srgb(0.95, 0.96, 0.97), throat: srgb(0.9, 0.9, 0.9), wing: srgb(0.6, 0.66, 0.76), tip: srgb(0.2, 0.24, 0.32) },
61
+ { back: srgb(0.92, 0.9, 0.86), belly: srgb(0.99, 0.98, 0.94), throat: srgb(0.95, 0.9, 0.8), wing: srgb(0.8, 0.78, 0.74), tip: srgb(0.32, 0.28, 0.26) },
62
+ ],
63
+ finch: [
64
+ { back: srgb(0.55, 0.38, 0.2), belly: srgb(0.95, 0.85, 0.62), throat: srgb(0.92, 0.62, 0.28), wing: srgb(0.4, 0.28, 0.16), tip: srgb(0.2, 0.14, 0.09) },
65
+ { back: srgb(0.72, 0.52, 0.22), belly: srgb(0.98, 0.92, 0.72), throat: srgb(0.95, 0.75, 0.3), wing: srgb(0.55, 0.38, 0.17), tip: srgb(0.26, 0.18, 0.1) },
66
+ { back: srgb(0.45, 0.42, 0.3), belly: srgb(0.9, 0.88, 0.74), throat: srgb(0.85, 0.6, 0.35), wing: srgb(0.34, 0.32, 0.24), tip: srgb(0.16, 0.15, 0.12) },
67
+ ],
68
+ });
69
+
70
+ export const BUTTERFLY_PALETTES = Object.freeze({
71
+ meadow: [
72
+ { wing: srgb(0.95, 0.52, 0.12), rim: srgb(0.16, 0.1, 0.07), accent: srgb(0.99, 0.78, 0.32), body: srgb(0.14, 0.1, 0.08) }, // monarch
73
+ { wing: srgb(0.25, 0.55, 0.98), rim: srgb(0.06, 0.1, 0.28), accent: srgb(0.6, 0.85, 1.0), body: srgb(0.08, 0.09, 0.16) }, // morpho
74
+ { wing: srgb(0.97, 0.97, 0.92), rim: srgb(0.35, 0.4, 0.3), accent: srgb(0.85, 0.92, 0.75), body: srgb(0.2, 0.22, 0.16) }, // cabbage white
75
+ { wing: srgb(0.98, 0.88, 0.25), rim: srgb(0.3, 0.2, 0.06), accent: srgb(1.0, 0.95, 0.55), body: srgb(0.18, 0.14, 0.06) }, // sulphur
76
+ ],
77
+ twilight: [
78
+ { wing: srgb(0.5, 0.3, 0.75), rim: srgb(0.12, 0.06, 0.2), accent: srgb(0.78, 0.55, 0.95), body: srgb(0.1, 0.07, 0.14) },
79
+ { wing: srgb(0.2, 0.62, 0.62), rim: srgb(0.05, 0.18, 0.18), accent: srgb(0.5, 0.9, 0.85), body: srgb(0.06, 0.14, 0.13) },
80
+ { wing: srgb(0.9, 0.86, 0.74), rim: srgb(0.42, 0.36, 0.26), accent: srgb(0.98, 0.95, 0.85), body: srgb(0.25, 0.21, 0.15) },
81
+ ],
82
+ });
83
+
84
+ export const DRAGONFLY_PALETTES = Object.freeze({
85
+ pond: [
86
+ { body: srgb(0.92, 0.24, 0.14), thorax: srgb(0.75, 0.18, 0.12), eye: srgb(0.55, 0.16, 0.12), wing: srgb(0.85, 0.9, 0.95) }, // akatombo
87
+ { body: srgb(0.25, 0.75, 0.9), thorax: srgb(0.16, 0.5, 0.64), eye: srgb(0.14, 0.32, 0.42), wing: srgb(0.85, 0.92, 0.97) },
88
+ { body: srgb(0.36, 0.72, 0.45), thorax: srgb(0.24, 0.5, 0.34), eye: srgb(0.2, 0.35, 0.25), wing: srgb(0.88, 0.93, 0.9) },
89
+ ],
90
+ ember: [
91
+ { body: srgb(0.95, 0.32, 0.12), thorax: srgb(0.72, 0.22, 0.1), eye: srgb(0.5, 0.16, 0.08), wing: srgb(0.95, 0.88, 0.8) },
92
+ { body: srgb(0.97, 0.65, 0.18), thorax: srgb(0.75, 0.46, 0.12), eye: srgb(0.5, 0.28, 0.1), wing: srgb(0.96, 0.92, 0.85) },
93
+ ],
94
+ });
95
+
96
+ export const FISH_PALETTES = Object.freeze({
97
+ koi: [
98
+ { base: srgb(1.0, 0.99, 0.95), patch: srgb(0.98, 0.3, 0.05), belly: srgb(1.0, 1.0, 0.98), fin: srgb(1.0, 0.9, 0.8), patchiness: 0.85 }, // kohaku
99
+ { base: srgb(1.0, 0.78, 0.18), patch: srgb(1.0, 0.92, 0.45), belly: srgb(1.0, 0.96, 0.72), fin: srgb(1.0, 0.85, 0.42), patchiness: 0.35 }, // yamabuki
100
+ // Benigoi (solid vermilion), not asagi: blue-grey koi vanish against a
101
+ // teal water body — warm hues are what read through the refraction.
102
+ { base: srgb(1.0, 0.42, 0.08), patch: srgb(0.85, 0.25, 0.04), belly: srgb(1.0, 0.72, 0.45), fin: srgb(1.0, 0.55, 0.25), patchiness: 0.3 },
103
+ { base: srgb(0.98, 0.97, 0.94), patch: srgb(0.14, 0.14, 0.18), belly: srgb(1.0, 0.99, 0.97), fin: srgb(0.95, 0.93, 0.9), patchiness: 0.6 }, // shiro bekko
104
+ ],
105
+ silver: [
106
+ { base: srgb(0.72, 0.78, 0.85), patch: srgb(0.45, 0.55, 0.68), belly: srgb(0.92, 0.95, 0.97), fin: srgb(0.62, 0.7, 0.8), patchiness: 0.0 },
107
+ { base: srgb(0.55, 0.65, 0.72), patch: srgb(0.35, 0.45, 0.55), belly: srgb(0.85, 0.9, 0.92), fin: srgb(0.5, 0.6, 0.68), patchiness: 0.0 },
108
+ { base: srgb(0.6, 0.68, 0.6), patch: srgb(0.4, 0.48, 0.42), belly: srgb(0.88, 0.92, 0.86), fin: srgb(0.55, 0.62, 0.55), patchiness: 0.0 },
109
+ ],
110
+ });
111
+
112
+ const SPECIES_PALETTES = Object.freeze({
113
+ birds: BIRD_PALETTES,
114
+ butterflies: BUTTERFLY_PALETTES,
115
+ dragonflies: DRAGONFLY_PALETTES,
116
+ fish: FISH_PALETTES,
117
+ });
118
+
119
+ function resolvePalette(species, paletteId) {
120
+ const palettes = SPECIES_PALETTES[species];
121
+ const fallback = FAUNA_PALETTE_IDS[species][0];
122
+ return palettes[paletteId] ?? palettes[fallback];
123
+ }
124
+
125
+ /** Variant count a species/palette pair provides (drives instancing splits). */
126
+ export function getFaunaVariantCount(species, paletteId) {
127
+ return resolvePalette(species, paletteId).length;
128
+ }
129
+
130
+ // ------------------------------------------------------------- mesh builder
131
+
132
+ class FaunaMeshBuilder {
133
+ constructor() {
134
+ this.positions = [];
135
+ this.colors = [];
136
+ this.wing = [];
137
+ this.tail = [];
138
+ this.indices = [];
139
+ }
140
+
141
+ vertex(x, y, z, color, wingExt = 0, tailWeight = 0) {
142
+ const index = this.positions.length / 3;
143
+ this.positions.push(x, y, z);
144
+ this.colors.push(color[0], color[1], color[2]);
145
+ this.wing.push(wingExt);
146
+ this.tail.push(tailWeight);
147
+ return index;
148
+ }
149
+
150
+ tri(a, b, c) {
151
+ this.indices.push(a, b, c);
152
+ }
153
+
154
+ quad(a, b, c, d) {
155
+ this.indices.push(a, b, c, a, c, d);
156
+ }
157
+
158
+ build(name) {
159
+ const geometry = new THREE.BufferGeometry();
160
+ geometry.name = name;
161
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(this.positions, 3));
162
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(this.colors, 3));
163
+ geometry.setAttribute('aWing', new THREE.Float32BufferAttribute(this.wing, 1));
164
+ geometry.setAttribute('aTail', new THREE.Float32BufferAttribute(this.tail, 1));
165
+ geometry.setIndex(this.indices);
166
+ return geometry;
167
+ }
168
+ }
169
+
170
+ // Anime cel bake: a soft two-tone by "how much this surface faces the sky",
171
+ // with the terminator pushed low so bodies stay bright and the underside
172
+ // carries a cool shadow tint (matches the environment shadowTint family).
173
+ function shadeTone(color, upness, lift = 1) {
174
+ const t = Math.min(1, Math.max(0, (upness + 0.55) / 1.05));
175
+ const band = t < 0.42 ? 0.0 : t < 0.58 ? (t - 0.42) / 0.16 : 1.0; // soft cel step
176
+ // `lift` raises the whole tone (shadow floor included) — tiny bodies like
177
+ // dragonflies otherwise read as black flecks from their shaded side.
178
+ const lit = (0.72 + 0.34 * band) * lift;
179
+ return [
180
+ color[0] * (lit * (band < 1 ? 0.96 : 1)) + (1 - band) * 0.015,
181
+ color[1] * lit + (1 - band) * 0.02,
182
+ color[2] * (lit * 1.02) + (1 - band) * 0.05, // shadow side drifts cool/blue
183
+ ];
184
+ }
185
+
186
+ function mixColor(a, b, t) {
187
+ return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
188
+ }
189
+
190
+ // Elliptical lathe around +Z with per-station color pickers. `colorAt(station,
191
+ // upness, angle)` returns the vertex color BEFORE the cel bake.
192
+ function lathe(builder, stations, { radial = 6, squashX = 1, colorAt, tailAt = null, shadeLift = 1 }) {
193
+ const rings = [];
194
+ for (let s = 0; s < stations.length; s += 1) {
195
+ const st = stations[s];
196
+ const ring = [];
197
+ for (let k = 0; k < radial; k += 1) {
198
+ const a = (k / radial) * Math.PI * 2;
199
+ const nx = Math.cos(a) * squashX;
200
+ const ny = Math.sin(a);
201
+ const base = colorAt(st, ny, a);
202
+ const color = shadeTone(base, ny, shadeLift);
203
+ ring.push(builder.vertex(
204
+ nx * st.r, st.y + ny * st.r, st.z, color, 0,
205
+ tailAt ? tailAt(st.z) : 0,
206
+ ));
207
+ }
208
+ rings.push(ring);
209
+ }
210
+ for (let s = 0; s < rings.length - 1; s += 1) {
211
+ for (let k = 0; k < radial; k += 1) {
212
+ const k2 = (k + 1) % radial;
213
+ builder.quad(rings[s][k], rings[s][k2], rings[s + 1][k2], rings[s + 1][k]);
214
+ }
215
+ }
216
+ return rings;
217
+ }
218
+
219
+ function capRing(builder, ring, z, y, color, tailWeight = 0) {
220
+ const center = builder.vertex(0, y, z, shadeTone(color, 0.4), 0, tailWeight);
221
+ for (let k = 0; k < ring.length; k += 1) {
222
+ builder.tri(ring[k], ring[(k + 1) % ring.length], center);
223
+ }
224
+ }
225
+
226
+ // ------------------------------------------------------------------- birds
227
+
228
+ /**
229
+ * Swallow-family silhouette: plump lathed body, swept pointed wings, forked
230
+ * tail. ~130 triangles — readable at 100 m by outline alone.
231
+ */
232
+ export function buildBirdGeometry({ seed = 1, variant = 0, palette = 'swallow' } = {}) {
233
+ const schemes = resolvePalette('birds', palette);
234
+ const scheme = schemes[variant % schemes.length];
235
+ const rng = mulberry32(hashCombine(seed, 0xb14d + variant));
236
+ const builder = new FaunaMeshBuilder();
237
+
238
+ const L = 0.36 * (0.92 + rng() * 0.2); // body length (m)
239
+ const plump = 0.9 + rng() * 0.28;
240
+ const span = 0.4 + rng() * 0.1; // half wingspan (m)
241
+ const sweep = 0.16 + rng() * 0.1;
242
+
243
+ // Body profile, tail → beak. yOff arches the back and lifts the head.
244
+ const stations = [
245
+ { z: -0.5 * L, r: 0.045 * L * plump, y: 0.045 * L, part: 'back' },
246
+ { z: -0.24 * L, r: 0.115 * L * plump, y: 0.012 * L, part: 'mid' },
247
+ { z: 0.02 * L, r: 0.15 * L * plump, y: 0.0, part: 'mid' },
248
+ { z: 0.26 * L, r: 0.135 * L * plump, y: 0.02 * L, part: 'throat' },
249
+ { z: 0.4 * L, r: 0.1 * L * plump, y: 0.07 * L, part: 'head' },
250
+ { z: 0.5 * L, r: 0.095 * L * plump, y: 0.095 * L, part: 'head' },
251
+ { z: 0.6 * L, r: 0.035 * L, y: 0.08 * L, part: 'beak' },
252
+ ];
253
+ const colorAt = (st, ny) => {
254
+ if (st.part === 'beak') return scheme.tip;
255
+ if (st.part === 'head') return ny > -0.25 ? scheme.back : scheme.throat;
256
+ if (st.part === 'throat') return ny > 0.1 ? scheme.back : scheme.throat;
257
+ if (st.part === 'back') return ny > -0.1 ? scheme.back : scheme.belly;
258
+ return ny > 0.2 ? scheme.back : scheme.belly;
259
+ };
260
+ const rings = lathe(builder, stations, { radial: 6, squashX: 0.88, colorAt });
261
+ capRing(builder, rings[rings.length - 1], 0.66 * L, 0.075 * L, scheme.tip);
262
+
263
+ // Wings: single-surface swept strips (double-sided material). Stations run
264
+ // shoulder → tip; leading/trailing z shape the pointed swallow outline.
265
+ const wingStations = [
266
+ { x: 0.08, lead: 0.16 * L, trail: -0.1 * L, y: 0.06 * L },
267
+ { x: span * 0.38, lead: 0.13 * L, trail: -0.16 * L, y: 0.085 * L },
268
+ { x: span * 0.7, lead: 0.06 * L - sweep * 0.35, trail: -0.17 * L - sweep * 0.5, y: 0.11 * L },
269
+ { x: span, lead: -sweep - 0.02, trail: -sweep - 0.055, y: 0.14 * L },
270
+ ];
271
+ for (const side of [1, -1]) {
272
+ let prev = null;
273
+ for (let i = 0; i < wingStations.length; i += 1) {
274
+ const st = wingStations[i];
275
+ const spanT = i / (wingStations.length - 1);
276
+ const top = mixColor(scheme.wing, scheme.tip, spanT * spanT);
277
+ const color = shadeTone(top, 0.5, 1 - spanT * 0.25);
278
+ const x = side * st.x;
279
+ const leadIdx = builder.vertex(x, st.y, st.lead, color, x, 0);
280
+ const trailIdx = builder.vertex(x, st.y - 0.01 * L, st.trail, shadeTone(top, -0.1), x, 0);
281
+ if (prev) builder.quad(prev.leadIdx, leadIdx, trailIdx, prev.trailIdx);
282
+ prev = { leadIdx, trailIdx };
283
+ }
284
+ }
285
+
286
+ // Forked tail: two angled prongs.
287
+ const forkLen = (0.28 + rng() * 0.14) * L;
288
+ for (const side of [1, -1]) {
289
+ const rootIn = builder.vertex(side * 0.012, 0.05 * L, -0.42 * L, shadeTone(scheme.back, 0.4), 0, 0);
290
+ const rootOut = builder.vertex(side * 0.05 * L, 0.05 * L, -0.48 * L, shadeTone(scheme.wing, 0.3), 0, 0);
291
+ const tipOut = builder.vertex(side * 0.1 * L, 0.06 * L, -0.5 * L - forkLen, shadeTone(scheme.tip, 0.2), 0, 0);
292
+ const tipIn = builder.vertex(side * 0.03 * L, 0.055 * L, -0.46 * L - forkLen * 0.72, shadeTone(scheme.tip, 0.25), 0, 0);
293
+ builder.quad(rootIn, rootOut, tipOut, tipIn);
294
+ }
295
+
296
+ return builder.build(`FaunaBird_${palette}_${variant}`);
297
+ }
298
+
299
+ // -------------------------------------------------------------- butterflies
300
+
301
+ /**
302
+ * Butterfly from 5 textureless quads: dark body sliver + fore/hind wing per
303
+ * side, silhouette shaped by the quad corners, rim darkening baked into the
304
+ * outer vertices. ~10 triangles.
305
+ */
306
+ export function buildButterflyGeometry({ seed = 1, variant = 0, palette = 'meadow' } = {}) {
307
+ const schemes = resolvePalette('butterflies', palette);
308
+ const scheme = schemes[variant % schemes.length];
309
+ const rng = mulberry32(hashCombine(seed, 0xbf17 + variant));
310
+ const builder = new FaunaMeshBuilder();
311
+
312
+ const span = 0.085 * (0.9 + rng() * 0.3); // half wingspan (m)
313
+ const lenScale = 0.9 + rng() * 0.25;
314
+
315
+ // Body: a thin vertical-diamond sliver so the side view isn't empty.
316
+ const bodyL = 0.055 * lenScale;
317
+ const b0 = builder.vertex(0, 0, bodyL * 0.7, shadeTone(scheme.body, 0.3), 0, 0);
318
+ const b1 = builder.vertex(0, 0.008, bodyL * 0.1, shadeTone(scheme.body, 0.5), 0, 0);
319
+ const b2 = builder.vertex(0, 0, -bodyL * 0.8, shadeTone(scheme.body, 0.2), 0, 0);
320
+ const b3 = builder.vertex(0, -0.008, bodyL * 0.1, shadeTone(scheme.body, -0.4), 0, 0);
321
+ builder.quad(b0, b1, b2, b3);
322
+
323
+ const rim = scheme.rim;
324
+ for (const side of [1, -1]) {
325
+ // Forewing: swept up-forward, pointed outer tip.
326
+ const f0 = builder.vertex(side * 0.006, 0, bodyL * 0.45, shadeTone(scheme.accent, 0.6), side * 0.006, 0);
327
+ const f1 = builder.vertex(side * span, 0, bodyL * 0.75 + span * 0.28, shadeTone(rim, 0.45), side * span, 0);
328
+ const f2 = builder.vertex(side * span * 0.82, 0, bodyL * 0.1, shadeTone(mixColor(scheme.wing, rim, 0.35), 0.5), side * span * 0.82, 0);
329
+ const f3 = builder.vertex(side * 0.006, 0, bodyL * 0.05, shadeTone(scheme.wing, 0.55), side * 0.006, 0);
330
+ builder.quad(f0, f1, f2, f3);
331
+ // Hindwing: rounder, tucked behind, slight overlap with the forewing.
332
+ const h0 = builder.vertex(side * 0.006, -0.001, bodyL * 0.12, shadeTone(scheme.wing, 0.5), side * 0.006, 0);
333
+ const h1 = builder.vertex(side * span * 0.72, -0.001, bodyL * 0.0, shadeTone(scheme.wing, 0.45), side * span * 0.72, 0);
334
+ const h2 = builder.vertex(side * span * 0.5, -0.001, -bodyL * 0.85, shadeTone(rim, 0.35), side * span * 0.5, 0);
335
+ const h3 = builder.vertex(side * 0.006, -0.001, -bodyL * 0.6, shadeTone(mixColor(scheme.wing, scheme.accent, 0.4), 0.5), side * 0.006, 0);
336
+ builder.quad(h0, h1, h2, h3);
337
+ }
338
+
339
+ return builder.build(`FaunaButterfly_${palette}_${variant}`);
340
+ }
341
+
342
+ // -------------------------------------------------------------- dragonflies
343
+
344
+ /** Needle body, four narrow wing blades, oversized eye ring. ~46 triangles. */
345
+ export function buildDragonflyGeometry({ seed = 1, variant = 0, palette = 'pond' } = {}) {
346
+ const schemes = resolvePalette('dragonflies', palette);
347
+ const scheme = schemes[variant % schemes.length];
348
+ const rng = mulberry32(hashCombine(seed, 0xd41f + variant));
349
+ const builder = new FaunaMeshBuilder();
350
+
351
+ const L = 0.14 * (0.9 + rng() * 0.25);
352
+ // Needle proportions: the abdomen is a sliver, the head a stylized bead —
353
+ // an oversized head ring reads as a blob, not an insect.
354
+ const stations = [
355
+ { z: -0.62 * L, r: 0.028 * L, y: 0, part: 'abdomen' },
356
+ { z: -0.2 * L, r: 0.042 * L, y: 0, part: 'abdomen' },
357
+ { z: 0.1 * L, r: 0.1 * L, y: 0, part: 'thorax' },
358
+ { z: 0.3 * L, r: 0.09 * L, y: 0.004, part: 'thorax' },
359
+ { z: 0.42 * L, r: 0.115 * L, y: 0.004, part: 'eye' },
360
+ ];
361
+ const colorAt = (st) => (st.part === 'eye' ? scheme.eye : st.part === 'thorax' ? scheme.thorax : scheme.body);
362
+ // Lifted shading: a head-on dragonfly is a few pixels of cross-section, and
363
+ // a dark one reads as a floating blob.
364
+ const rings = lathe(builder, stations, { colorAt, radial: 5, shadeLift: 1.3, squashX: 1 });
365
+ capRing(builder, rings[rings.length - 1], 0.5 * L, 0.004, scheme.eye);
366
+ capRing(builder, [...rings[0]].reverse(), -0.68 * L, 0, scheme.body);
367
+
368
+ // Two wing pairs, splayed slightly forward/back so the top view reads X.
369
+ const span = 0.09 * (0.9 + rng() * 0.2);
370
+ const wingPairs = [
371
+ { zRoot: 0.22 * L, splay: 0.35, chord: 0.02 },
372
+ { zRoot: 0.04 * L, splay: -0.3, chord: 0.018 },
373
+ ];
374
+ for (const pair of wingPairs) {
375
+ for (const side of [1, -1]) {
376
+ const wc = shadeTone(scheme.wing, 0.55);
377
+ const tipZ = pair.zRoot + side * 0 + pair.splay * span * 0.6;
378
+ const w0 = builder.vertex(side * 0.008, 0.006, pair.zRoot + pair.chord, wc, side * 0.008, 0);
379
+ const w1 = builder.vertex(side * span, 0.01, tipZ + pair.chord * 0.5, shadeTone(scheme.wing, 0.4), side * span, 0);
380
+ const w2 = builder.vertex(side * span * 0.96, 0.01, tipZ - pair.chord * 0.6, shadeTone(scheme.wing, 0.3), side * span * 0.96, 0);
381
+ const w3 = builder.vertex(side * 0.008, 0.006, pair.zRoot - pair.chord, shadeTone(scheme.wing, 0.45), side * 0.008, 0);
382
+ builder.quad(w0, w1, w2, w3);
383
+ }
384
+ }
385
+
386
+ return builder.build(`FaunaDragonfly_${palette}_${variant}`);
387
+ }
388
+
389
+ // -------------------------------------------------------------------- fish
390
+
391
+ // Blobby koi patches: a few seeded patch centers in (station, angle) space;
392
+ // vertices inside a center's radius take the patch color. Reads as classic
393
+ // koi mottling from above (the view the refraction pass shows).
394
+ function makePatchSampler(rng, patchiness) {
395
+ const count = patchiness <= 0 ? 0 : 2 + Math.floor(rng() * 3);
396
+ const patches = [];
397
+ for (let i = 0; i < count; i += 1) {
398
+ patches.push({
399
+ s: rng(), // 0..1 along the body
400
+ a: (rng() - 0.5) * 2.4, // radians around the top (0 = straight up)
401
+ r: 0.16 + rng() * 0.24 * patchiness,
402
+ });
403
+ }
404
+ return (s, angle, ny) => {
405
+ if (ny < -0.15) return false; // belly stays clean
406
+ for (const p of patches) {
407
+ const da = Math.atan2(Math.sin(angle - Math.PI / 2 - p.a), Math.cos(angle - Math.PI / 2 - p.a)) * 0.35;
408
+ const ds = s - p.s;
409
+ if (ds * ds + da * da < p.r * p.r) return true;
410
+ }
411
+ return false;
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Koi/shoal fish: laterally squashed lathe body, forked tail fan, dorsal
417
+ * ridge, pectoral pair. `aTail` ramps nose→tail for the GPU body sway.
418
+ * ~90 triangles.
419
+ */
420
+ export function buildFishGeometry({ seed = 1, variant = 0, palette = 'koi' } = {}) {
421
+ const schemes = resolvePalette('fish', palette);
422
+ const scheme = schemes[variant % schemes.length];
423
+ const rng = mulberry32(hashCombine(seed, 0xf1f + variant));
424
+ const builder = new FaunaMeshBuilder();
425
+
426
+ const L = 0.42 * (0.85 + rng() * 0.35);
427
+ const girth = 0.9 + rng() * 0.3;
428
+ const patchAt = makePatchSampler(rng, scheme.patchiness);
429
+ const tailAt = (z) => {
430
+ const t = (0.5 * L - z) / (1.1 * L);
431
+ return Math.max(0, Math.min(1, t)) ** 1.4;
432
+ };
433
+
434
+ const stations = [
435
+ { z: 0.5 * L, r: 0.03 * L, y: 0, s: 0 },
436
+ { z: 0.34 * L, r: 0.1 * L * girth, y: 0, s: 0.15 },
437
+ { z: 0.1 * L, r: 0.13 * L * girth, y: 0.005 * L, s: 0.4 },
438
+ { z: -0.14 * L, r: 0.105 * L * girth, y: 0.004 * L, s: 0.62 },
439
+ { z: -0.32 * L, r: 0.06 * L * girth, y: 0, s: 0.8 },
440
+ { z: -0.44 * L, r: 0.026 * L, y: 0, s: 0.92 },
441
+ ];
442
+ const colorAt = (st, ny, angle) => {
443
+ if (ny < -0.45) return scheme.belly;
444
+ return patchAt(st.s, angle, ny) ? scheme.patch : scheme.base;
445
+ };
446
+ const rings = lathe(builder, stations, { radial: 6, squashX: 0.62, colorAt, tailAt });
447
+ capRing(builder, rings[0], 0.56 * L, 0, scheme.base, tailAt(0.56 * L));
448
+
449
+ // Forked tail fan: two blades sweeping back from the peduncle.
450
+ const tailL = 0.24 * L * (0.9 + rng() * 0.3);
451
+ for (const side of [1, -1]) {
452
+ const up = side; // one blade up, one down — a vertical fork seen side-on
453
+ const fin = shadeTone(scheme.fin, 0.3 * up);
454
+ const t0 = builder.vertex(0, 0.0, -0.44 * L, fin, 0, tailAt(-0.44 * L));
455
+ const t1 = builder.vertex(0, up * 0.1 * L, -0.44 * L - tailL, shadeTone(scheme.fin, 0.15 * up), 0, 1);
456
+ const t2 = builder.vertex(0, up * 0.028 * L, -0.44 * L - tailL * 0.62, shadeTone(scheme.fin, 0.2 * up), 0, 1);
457
+ builder.tri(t0, t1, t2); // single face; the DoubleSide material shows both sides
458
+ }
459
+
460
+ // Dorsal fin: low triangle ridge.
461
+ const d0 = builder.vertex(0, 0.13 * L * girth, 0.08 * L, shadeTone(scheme.fin, 0.7), 0, tailAt(0.08 * L));
462
+ const d1 = builder.vertex(0, 0.2 * L * girth, -0.08 * L, shadeTone(scheme.fin, 0.6), 0, tailAt(-0.08 * L));
463
+ const d2 = builder.vertex(0, 0.11 * L * girth, -0.2 * L, shadeTone(scheme.fin, 0.55), 0, tailAt(-0.2 * L));
464
+ builder.tri(d0, d1, d2);
465
+
466
+ // Pectoral fins: small angled quads that sell the top-down koi read.
467
+ for (const side of [1, -1]) {
468
+ const px = side * 0.09 * L;
469
+ const p0 = builder.vertex(side * 0.055 * L, -0.02 * L, 0.22 * L, shadeTone(scheme.fin, 0.2), 0, 0.2);
470
+ const p1 = builder.vertex(px * 1.9, -0.045 * L, 0.12 * L, shadeTone(scheme.fin, 0.05), 0, 0.25);
471
+ const p2 = builder.vertex(px * 1.5, -0.04 * L, 0.02 * L, shadeTone(scheme.fin, 0.1), 0, 0.3);
472
+ builder.tri(p0, p1, p2);
473
+ }
474
+
475
+ return builder.build(`FaunaFish_${palette}_${variant}`);
476
+ }
477
+
478
+ // ------------------------------------------------------------------ generic
479
+
480
+ const BUILDERS = Object.freeze({
481
+ birds: buildBirdGeometry,
482
+ butterflies: buildButterflyGeometry,
483
+ dragonflies: buildDragonflyGeometry,
484
+ fish: buildFishGeometry,
485
+ });
486
+
487
+ /** Species-generic entry: `buildFaunaGeometry('birds', { seed, variant, palette })`. */
488
+ export function buildFaunaGeometry(species, options = {}) {
489
+ const builder = BUILDERS[species];
490
+ if (!builder) throw new Error(`Unknown fauna species "${species}".`);
491
+ return builder(options);
492
+ }
@@ -0,0 +1,52 @@
1
+ // Built-in fauna presets. Same convention as every cluster: `default` is the
2
+ // neutral starting point, `call_me_sensei` is the studio-managed look used
3
+ // across the reference worlds — swallow flocks over the treeline, koi in the
4
+ // lakes, monarch/morpho meadows, red dragonflies on the water margin.
5
+
6
+ import { createFaunaRecipeDocument } from './faunaSettings.js';
7
+
8
+ export const BUILT_IN_FAUNA_PRESETS = Object.freeze([
9
+ Object.freeze({
10
+ description: 'Neutral ambient life at library defaults.',
11
+ id: 'default',
12
+ label: 'Default',
13
+ species: Object.freeze({}),
14
+ settings: Object.freeze({}),
15
+ }),
16
+ Object.freeze({
17
+ description: 'The studio look: tight swallow flocks that perch and flush, koi schools under the refraction, saturated meadow butterflies, akatombo dragonflies skimming the shore.',
18
+ id: 'call_me_sensei',
19
+ label: 'Call Me Sensei',
20
+ species: Object.freeze({ birds: 48, butterflies: 70, dragonflies: 14, fish: 90 }),
21
+ settings: Object.freeze({
22
+ birds: Object.freeze({
23
+ alignment: 0.85,
24
+ altitudeMax: 30,
25
+ cohesion: 0.7,
26
+ neighborRadius: 12,
27
+ palette: 'swallow',
28
+ perchChance: 0.6,
29
+ }),
30
+ butterflies: Object.freeze({ flapHz: 9, palette: 'meadow', wanderRadius: 9 }),
31
+ dragonflies: Object.freeze({ dartChance: 0.55, hoverHeight: 0.55, palette: 'pond' }),
32
+ fish: Object.freeze({ cohesion: 1.0, neighborRadius: 4.5, palette: 'koi' }),
33
+ }),
34
+ }),
35
+ ]);
36
+
37
+ export function findFaunaPreset(id) {
38
+ return BUILT_IN_FAUNA_PRESETS.find((preset) => preset.id === id) ?? null;
39
+ }
40
+
41
+ /** Preset lookup used by createFauna({ preset }); unknown ids resolve to null. */
42
+ export function resolveFaunaPreset(id) {
43
+ if (!id || typeof id !== 'string') return null;
44
+ return findFaunaPreset(id);
45
+ }
46
+
47
+ /** Serializable recipe for a preset (see createFaunaRecipeDocument). */
48
+ export function createFaunaPresetRecipe(id, { seed = 1 } = {}) {
49
+ const preset = findFaunaPreset(id);
50
+ if (!preset) return null;
51
+ return createFaunaRecipeDocument({ seed, settings: preset.settings, species: preset.species });
52
+ }