@3driseai/3drise-viewer 0.1.42 → 0.1.43

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 (408) hide show
  1. package/dist/animationsV2/ClipsRuntime.d.ts +16 -1
  2. package/dist/animationsV2/ClipsRuntime.d.ts.map +1 -1
  3. package/dist/animationsV2/ClipsRuntime.js +144 -25
  4. package/dist/animationsV2/appliers.d.ts +9 -0
  5. package/dist/animationsV2/appliers.d.ts.map +1 -1
  6. package/dist/animationsV2/appliers.js +62 -2
  7. package/dist/animationsV2/clipUtils.d.ts +30 -0
  8. package/dist/animationsV2/clipUtils.d.ts.map +1 -1
  9. package/dist/animationsV2/clipUtils.js +163 -17
  10. package/dist/animationsV2/elementSettings.d.ts +5 -0
  11. package/dist/animationsV2/elementSettings.d.ts.map +1 -1
  12. package/dist/animationsV2/elementSettings.js +6 -3
  13. package/dist/animationsV2/sequenceUtils.js +4 -4
  14. package/dist/animationsV2/types.d.ts +23 -0
  15. package/dist/animationsV2/types.d.ts.map +1 -1
  16. package/dist/api/sceneHandle.js +2 -2
  17. package/dist/components/CreatedObject.d.ts +3 -0
  18. package/dist/components/CreatedObject.d.ts.map +1 -1
  19. package/dist/components/CreatedObject.js +6 -2
  20. package/dist/components/CustomPrimitive.d.ts.map +1 -1
  21. package/dist/components/CustomPrimitive.js +1 -10
  22. package/dist/components/EnvironmentManager.d.ts.map +1 -1
  23. package/dist/components/EnvironmentManager.js +11 -5
  24. package/dist/components/GeometryShape.d.ts +5 -2
  25. package/dist/components/GeometryShape.d.ts.map +1 -1
  26. package/dist/components/GeometryShape.js +71 -10
  27. package/dist/components/LandObject.d.ts.map +1 -1
  28. package/dist/components/LandObject.js +1 -1
  29. package/dist/components/LayoutInstances.d.ts +2 -1
  30. package/dist/components/LayoutInstances.d.ts.map +1 -1
  31. package/dist/components/LayoutInstances.js +39 -6
  32. package/dist/components/LightObject.d.ts.map +1 -1
  33. package/dist/components/LightObject.js +3 -0
  34. package/dist/components/ObjectNode.d.ts.map +1 -1
  35. package/dist/components/ObjectNode.js +62 -13
  36. package/dist/components/OceanBuilder.d.ts +2 -0
  37. package/dist/components/OceanBuilder.d.ts.map +1 -1
  38. package/dist/components/OceanBuilder.js +2 -1
  39. package/dist/components/RenderEffects.d.ts +8 -0
  40. package/dist/components/RenderEffects.d.ts.map +1 -0
  41. package/dist/components/RenderEffects.js +120 -0
  42. package/dist/components/SceneBuilder.d.ts.map +1 -1
  43. package/dist/components/SceneBuilder.js +18 -9
  44. package/dist/components/SportsObject.d.ts +9 -0
  45. package/dist/components/SportsObject.d.ts.map +1 -0
  46. package/dist/components/SportsObject.js +20 -0
  47. package/dist/components/UrbanObject.d.ts +9 -0
  48. package/dist/components/UrbanObject.d.ts.map +1 -0
  49. package/dist/components/UrbanObject.js +28 -0
  50. package/dist/components/VegetationObject.d.ts +9 -0
  51. package/dist/components/VegetationObject.d.ts.map +1 -0
  52. package/dist/components/VegetationObject.js +26 -0
  53. package/dist/components/WaterObject.d.ts +15 -0
  54. package/dist/components/WaterObject.d.ts.map +1 -0
  55. package/dist/components/WaterObject.js +29 -0
  56. package/dist/components/index.d.ts +7 -2
  57. package/dist/components/index.d.ts.map +1 -1
  58. package/dist/components/index.js +6 -1
  59. package/dist/components/land/LandPatch.d.ts +3 -1
  60. package/dist/components/land/LandPatch.d.ts.map +1 -1
  61. package/dist/components/land/LandPatch.js +80 -7
  62. package/dist/components/layout/InstancedLayout.d.ts +19 -0
  63. package/dist/components/layout/InstancedLayout.d.ts.map +1 -0
  64. package/dist/components/layout/InstancedLayout.js +154 -0
  65. package/dist/components/layout/LayoutGrounds.d.ts +19 -0
  66. package/dist/components/layout/LayoutGrounds.d.ts.map +1 -0
  67. package/dist/components/layout/LayoutGrounds.js +104 -0
  68. package/dist/components/layout/LinkProxies.d.ts +5 -0
  69. package/dist/components/layout/LinkProxies.d.ts.map +1 -0
  70. package/dist/components/layout/LinkProxies.js +51 -0
  71. package/dist/components/layout/LinkedSource.d.ts +14 -0
  72. package/dist/components/layout/LinkedSource.d.ts.map +1 -0
  73. package/dist/components/layout/LinkedSource.js +129 -0
  74. package/dist/components/projector/Projector.d.ts +14 -0
  75. package/dist/components/projector/Projector.d.ts.map +1 -0
  76. package/dist/components/projector/Projector.js +44 -0
  77. package/dist/components/projector/index.d.ts +4 -0
  78. package/dist/components/projector/index.d.ts.map +1 -0
  79. package/dist/components/projector/index.js +2 -0
  80. package/dist/components/projector/projectorCore.d.ts +19 -0
  81. package/dist/components/projector/projectorCore.d.ts.map +1 -0
  82. package/dist/components/projector/projectorCore.js +545 -0
  83. package/dist/components/sports/SportsBall.d.ts +9 -0
  84. package/dist/components/sports/SportsBall.d.ts.map +1 -0
  85. package/dist/components/sports/SportsBall.js +23 -0
  86. package/dist/components/sports/SportsCourt.d.ts +9 -0
  87. package/dist/components/sports/SportsCourt.d.ts.map +1 -0
  88. package/dist/components/sports/SportsCourt.js +23 -0
  89. package/dist/components/sports/SportsEquipment.d.ts +9 -0
  90. package/dist/components/sports/SportsEquipment.d.ts.map +1 -0
  91. package/dist/components/sports/SportsEquipment.js +23 -0
  92. package/dist/components/sports/SportsGoal.d.ts +9 -0
  93. package/dist/components/sports/SportsGoal.d.ts.map +1 -0
  94. package/dist/components/sports/SportsGoal.js +23 -0
  95. package/dist/components/sports/core/ball.d.ts +166 -0
  96. package/dist/components/sports/core/ball.d.ts.map +1 -0
  97. package/dist/components/sports/core/ball.js +402 -0
  98. package/dist/components/sports/core/court.d.ts +442 -0
  99. package/dist/components/sports/core/court.d.ts.map +1 -0
  100. package/dist/components/sports/core/court.js +658 -0
  101. package/dist/components/sports/core/equipment.d.ts +103 -0
  102. package/dist/components/sports/core/equipment.d.ts.map +1 -0
  103. package/dist/components/sports/core/equipment.js +403 -0
  104. package/dist/components/sports/core/goal.d.ts +98 -0
  105. package/dist/components/sports/core/goal.d.ts.map +1 -0
  106. package/dist/components/sports/core/goal.js +44 -0
  107. package/dist/components/sports/index.d.ts +11 -0
  108. package/dist/components/sports/index.d.ts.map +1 -0
  109. package/dist/components/sports/index.js +8 -0
  110. package/dist/components/sports/sportsCore.d.ts +195 -0
  111. package/dist/components/sports/sportsCore.d.ts.map +1 -0
  112. package/dist/components/sports/sportsCore.js +758 -0
  113. package/dist/components/sports/useSportsInstance.d.ts +35 -0
  114. package/dist/components/sports/useSportsInstance.d.ts.map +1 -0
  115. package/dist/components/sports/useSportsInstance.js +47 -0
  116. package/dist/components/terrain/TerrainSurface.d.ts.map +1 -1
  117. package/dist/components/terrain/TerrainSurface.js +14 -0
  118. package/dist/components/urban/UrbanBlock.d.ts +9 -0
  119. package/dist/components/urban/UrbanBlock.d.ts.map +1 -0
  120. package/dist/components/urban/UrbanBlock.js +23 -0
  121. package/dist/components/urban/UrbanBuilding.d.ts +9 -0
  122. package/dist/components/urban/UrbanBuilding.d.ts.map +1 -0
  123. package/dist/components/urban/UrbanBuilding.js +23 -0
  124. package/dist/components/urban/UrbanJunction.d.ts +9 -0
  125. package/dist/components/urban/UrbanJunction.d.ts.map +1 -0
  126. package/dist/components/urban/UrbanJunction.js +23 -0
  127. package/dist/components/urban/UrbanPark.d.ts +9 -0
  128. package/dist/components/urban/UrbanPark.d.ts.map +1 -0
  129. package/dist/components/urban/UrbanPark.js +23 -0
  130. package/dist/components/urban/UrbanParkingLot.d.ts +9 -0
  131. package/dist/components/urban/UrbanParkingLot.d.ts.map +1 -0
  132. package/dist/components/urban/UrbanParkingLot.js +23 -0
  133. package/dist/components/urban/UrbanPlaza.d.ts +9 -0
  134. package/dist/components/urban/UrbanPlaza.d.ts.map +1 -0
  135. package/dist/components/urban/UrbanPlaza.js +23 -0
  136. package/dist/components/urban/UrbanRoad.d.ts +9 -0
  137. package/dist/components/urban/UrbanRoad.d.ts.map +1 -0
  138. package/dist/components/urban/UrbanRoad.js +23 -0
  139. package/dist/components/urban/UrbanSignage.d.ts +9 -0
  140. package/dist/components/urban/UrbanSignage.d.ts.map +1 -0
  141. package/dist/components/urban/UrbanSignage.js +23 -0
  142. package/dist/components/urban/UrbanSkyline.d.ts +9 -0
  143. package/dist/components/urban/UrbanSkyline.d.ts.map +1 -0
  144. package/dist/components/urban/UrbanSkyline.js +23 -0
  145. package/dist/components/urban/UrbanStreetFurniture.d.ts +9 -0
  146. package/dist/components/urban/UrbanStreetFurniture.d.ts.map +1 -0
  147. package/dist/components/urban/UrbanStreetFurniture.js +23 -0
  148. package/dist/components/urban/UrbanStreetGrid.d.ts +9 -0
  149. package/dist/components/urban/UrbanStreetGrid.d.ts.map +1 -0
  150. package/dist/components/urban/UrbanStreetGrid.js +34 -0
  151. package/dist/components/urban/UrbanTraffic.d.ts +9 -0
  152. package/dist/components/urban/UrbanTraffic.d.ts.map +1 -0
  153. package/dist/components/urban/UrbanTraffic.js +23 -0
  154. package/dist/components/urban/core/block.d.ts +119 -0
  155. package/dist/components/urban/core/block.d.ts.map +1 -0
  156. package/dist/components/urban/core/block.js +266 -0
  157. package/dist/components/urban/core/building.d.ts +128 -0
  158. package/dist/components/urban/core/building.d.ts.map +1 -0
  159. package/dist/components/urban/core/building.js +741 -0
  160. package/dist/components/urban/core/junction.d.ts +152 -0
  161. package/dist/components/urban/core/junction.d.ts.map +1 -0
  162. package/dist/components/urban/core/junction.js +617 -0
  163. package/dist/components/urban/core/park.d.ts +67 -0
  164. package/dist/components/urban/core/park.d.ts.map +1 -0
  165. package/dist/components/urban/core/park.js +176 -0
  166. package/dist/components/urban/core/parkingLot.d.ts +82 -0
  167. package/dist/components/urban/core/parkingLot.d.ts.map +1 -0
  168. package/dist/components/urban/core/parkingLot.js +284 -0
  169. package/dist/components/urban/core/plaza.d.ts +24 -0
  170. package/dist/components/urban/core/plaza.d.ts.map +1 -0
  171. package/dist/components/urban/core/plaza.js +243 -0
  172. package/dist/components/urban/core/road.d.ts +113 -0
  173. package/dist/components/urban/core/road.d.ts.map +1 -0
  174. package/dist/components/urban/core/road.js +347 -0
  175. package/dist/components/urban/core/signage.d.ts +83 -0
  176. package/dist/components/urban/core/signage.d.ts.map +1 -0
  177. package/dist/components/urban/core/signage.js +209 -0
  178. package/dist/components/urban/core/skyline.d.ts +100 -0
  179. package/dist/components/urban/core/skyline.d.ts.map +1 -0
  180. package/dist/components/urban/core/skyline.js +89 -0
  181. package/dist/components/urban/core/streetFurniture.d.ts +75 -0
  182. package/dist/components/urban/core/streetFurniture.d.ts.map +1 -0
  183. package/dist/components/urban/core/streetFurniture.js +193 -0
  184. package/dist/components/urban/core/streetGrid.d.ts +134 -0
  185. package/dist/components/urban/core/streetGrid.d.ts.map +1 -0
  186. package/dist/components/urban/core/streetGrid.js +422 -0
  187. package/dist/components/urban/core/traffic.d.ts +125 -0
  188. package/dist/components/urban/core/traffic.d.ts.map +1 -0
  189. package/dist/components/urban/core/traffic.js +305 -0
  190. package/dist/components/urban/index.d.ts +29 -0
  191. package/dist/components/urban/index.d.ts.map +1 -0
  192. package/dist/components/urban/index.js +16 -0
  193. package/dist/components/urban/textureSetResolver.d.ts +8 -0
  194. package/dist/components/urban/textureSetResolver.d.ts.map +1 -0
  195. package/dist/components/urban/textureSetResolver.js +16 -0
  196. package/dist/components/urban/urbanCore.d.ts +262 -0
  197. package/dist/components/urban/urbanCore.d.ts.map +1 -0
  198. package/dist/components/urban/urbanCore.js +928 -0
  199. package/dist/components/urban/useUrbanInstance.d.ts +53 -0
  200. package/dist/components/urban/useUrbanInstance.d.ts.map +1 -0
  201. package/dist/components/urban/useUrbanInstance.js +100 -0
  202. package/dist/components/vegetation/VegetationBush.d.ts +9 -0
  203. package/dist/components/vegetation/VegetationBush.d.ts.map +1 -0
  204. package/dist/components/vegetation/VegetationBush.js +23 -0
  205. package/dist/components/vegetation/VegetationCrops.d.ts +9 -0
  206. package/dist/components/vegetation/VegetationCrops.d.ts.map +1 -0
  207. package/dist/components/vegetation/VegetationCrops.js +23 -0
  208. package/dist/components/vegetation/VegetationFlowers.d.ts +9 -0
  209. package/dist/components/vegetation/VegetationFlowers.d.ts.map +1 -0
  210. package/dist/components/vegetation/VegetationFlowers.js +23 -0
  211. package/dist/components/vegetation/VegetationForest.d.ts +9 -0
  212. package/dist/components/vegetation/VegetationForest.d.ts.map +1 -0
  213. package/dist/components/vegetation/VegetationForest.js +23 -0
  214. package/dist/components/vegetation/VegetationFruit.d.ts +9 -0
  215. package/dist/components/vegetation/VegetationFruit.d.ts.map +1 -0
  216. package/dist/components/vegetation/VegetationFruit.js +23 -0
  217. package/dist/components/vegetation/VegetationGrass.d.ts +9 -0
  218. package/dist/components/vegetation/VegetationGrass.d.ts.map +1 -0
  219. package/dist/components/vegetation/VegetationGrass.js +23 -0
  220. package/dist/components/vegetation/VegetationHedge.d.ts +9 -0
  221. package/dist/components/vegetation/VegetationHedge.d.ts.map +1 -0
  222. package/dist/components/vegetation/VegetationHedge.js +23 -0
  223. package/dist/components/vegetation/VegetationIvy.d.ts +9 -0
  224. package/dist/components/vegetation/VegetationIvy.d.ts.map +1 -0
  225. package/dist/components/vegetation/VegetationIvy.js +23 -0
  226. package/dist/components/vegetation/VegetationTree.d.ts +9 -0
  227. package/dist/components/vegetation/VegetationTree.d.ts.map +1 -0
  228. package/dist/components/vegetation/VegetationTree.js +23 -0
  229. package/dist/components/vegetation/VegetationVegetable.d.ts +9 -0
  230. package/dist/components/vegetation/VegetationVegetable.d.ts.map +1 -0
  231. package/dist/components/vegetation/VegetationVegetable.js +23 -0
  232. package/dist/components/vegetation/core/bush.d.ts +1256 -0
  233. package/dist/components/vegetation/core/bush.d.ts.map +1 -0
  234. package/dist/components/vegetation/core/bush.js +376 -0
  235. package/dist/components/vegetation/core/crops.d.ts +1033 -0
  236. package/dist/components/vegetation/core/crops.d.ts.map +1 -0
  237. package/dist/components/vegetation/core/crops.js +378 -0
  238. package/dist/components/vegetation/core/flowers.d.ts +648 -0
  239. package/dist/components/vegetation/core/flowers.d.ts.map +1 -0
  240. package/dist/components/vegetation/core/flowers.js +225 -0
  241. package/dist/components/vegetation/core/forest.d.ts +138 -0
  242. package/dist/components/vegetation/core/forest.d.ts.map +1 -0
  243. package/dist/components/vegetation/core/forest.js +310 -0
  244. package/dist/components/vegetation/core/fruit.d.ts +99 -0
  245. package/dist/components/vegetation/core/fruit.d.ts.map +1 -0
  246. package/dist/components/vegetation/core/fruit.js +77 -0
  247. package/dist/components/vegetation/core/grass.d.ts +103 -0
  248. package/dist/components/vegetation/core/grass.d.ts.map +1 -0
  249. package/dist/components/vegetation/core/grass.js +102 -0
  250. package/dist/components/vegetation/core/hedge.d.ts +193 -0
  251. package/dist/components/vegetation/core/hedge.d.ts.map +1 -0
  252. package/dist/components/vegetation/core/hedge.js +243 -0
  253. package/dist/components/vegetation/core/ivy.d.ts +213 -0
  254. package/dist/components/vegetation/core/ivy.d.ts.map +1 -0
  255. package/dist/components/vegetation/core/ivy.js +173 -0
  256. package/dist/components/vegetation/core/tree.d.ts +1117 -0
  257. package/dist/components/vegetation/core/tree.d.ts.map +1 -0
  258. package/dist/components/vegetation/core/tree.js +408 -0
  259. package/dist/components/vegetation/core/vegetable.d.ts +99 -0
  260. package/dist/components/vegetation/core/vegetable.d.ts.map +1 -0
  261. package/dist/components/vegetation/core/vegetable.js +76 -0
  262. package/dist/components/vegetation/index.d.ts +23 -0
  263. package/dist/components/vegetation/index.d.ts.map +1 -0
  264. package/dist/components/vegetation/index.js +14 -0
  265. package/dist/components/vegetation/produceCore.d.ts +929 -0
  266. package/dist/components/vegetation/produceCore.d.ts.map +1 -0
  267. package/dist/components/vegetation/produceCore.js +723 -0
  268. package/dist/components/vegetation/useVegetationInstance.d.ts +39 -0
  269. package/dist/components/vegetation/useVegetationInstance.d.ts.map +1 -0
  270. package/dist/components/vegetation/useVegetationInstance.js +45 -0
  271. package/dist/components/vegetation/vegetationCore.d.ts +412 -0
  272. package/dist/components/vegetation/vegetationCore.d.ts.map +1 -0
  273. package/dist/components/vegetation/vegetationCore.js +1728 -0
  274. package/dist/components/volumetricClouds/VolumetricCloudsPass.d.ts +5 -0
  275. package/dist/components/volumetricClouds/VolumetricCloudsPass.d.ts.map +1 -1
  276. package/dist/components/volumetricClouds/VolumetricCloudsPass.js +39 -13
  277. package/dist/components/water/OceanSurface.d.ts.map +1 -1
  278. package/dist/components/water/OceanSurface.js +3 -2
  279. package/dist/components/water/WaterBody.d.ts.map +1 -1
  280. package/dist/components/water/WaterBody.js +31 -10
  281. package/dist/components/water/WaterFountain.d.ts +9 -0
  282. package/dist/components/water/WaterFountain.d.ts.map +1 -0
  283. package/dist/components/water/WaterFountain.js +24 -0
  284. package/dist/components/water/WaterStream.d.ts +9 -0
  285. package/dist/components/water/WaterStream.d.ts.map +1 -0
  286. package/dist/components/water/WaterStream.js +24 -0
  287. package/dist/components/water/WaterWaterfall.d.ts +9 -0
  288. package/dist/components/water/WaterWaterfall.d.ts.map +1 -0
  289. package/dist/components/water/WaterWaterfall.js +24 -0
  290. package/dist/components/water/core/fountain.d.ts +70 -0
  291. package/dist/components/water/core/fountain.d.ts.map +1 -0
  292. package/dist/components/water/core/fountain.js +619 -0
  293. package/dist/components/water/core/stream.d.ts +72 -0
  294. package/dist/components/water/core/stream.d.ts.map +1 -0
  295. package/dist/components/water/core/stream.js +167 -0
  296. package/dist/components/water/core/waterfall.d.ts +78 -0
  297. package/dist/components/water/core/waterfall.d.ts.map +1 -0
  298. package/dist/components/water/core/waterfall.js +325 -0
  299. package/dist/components/water/index.d.ts +8 -0
  300. package/dist/components/water/index.d.ts.map +1 -1
  301. package/dist/components/water/index.js +7 -0
  302. package/dist/components/water/types.d.ts +3 -0
  303. package/dist/components/water/types.d.ts.map +1 -1
  304. package/dist/components/water/useWaterInstance.d.ts +43 -0
  305. package/dist/components/water/useWaterInstance.d.ts.map +1 -0
  306. package/dist/components/water/useWaterInstance.js +89 -0
  307. package/dist/components/water/waterCore.d.ts +179 -0
  308. package/dist/components/water/waterCore.d.ts.map +1 -0
  309. package/dist/components/water/waterCore.js +516 -0
  310. package/dist/constants.d.ts.map +1 -1
  311. package/dist/constants.js +9 -1
  312. package/dist/hooks/index.d.ts +7 -2
  313. package/dist/hooks/index.d.ts.map +1 -1
  314. package/dist/hooks/index.js +5 -2
  315. package/dist/hooks/useActionPatch.d.ts +23 -0
  316. package/dist/hooks/useActionPatch.d.ts.map +1 -0
  317. package/dist/hooks/useActionPatch.js +38 -0
  318. package/dist/hooks/useCloudSun.d.ts.map +1 -1
  319. package/dist/hooks/useCloudSun.js +3 -2
  320. package/dist/hooks/useCommittedInstance.d.ts +6 -0
  321. package/dist/hooks/useCommittedInstance.d.ts.map +1 -0
  322. package/dist/hooks/useCommittedInstance.js +42 -0
  323. package/dist/hooks/useConfigLive.d.ts +1 -1
  324. package/dist/hooks/useConfigLive.d.ts.map +1 -1
  325. package/dist/hooks/useContinuousEffects.d.ts.map +1 -1
  326. package/dist/hooks/useContinuousEffects.js +36 -20
  327. package/dist/hooks/useGridLayout.d.ts +7 -0
  328. package/dist/hooks/useGridLayout.d.ts.map +1 -0
  329. package/dist/hooks/useGridLayout.js +10 -0
  330. package/dist/hooks/useInteractionEffects.d.ts.map +1 -1
  331. package/dist/hooks/useInteractionEffects.js +48 -9
  332. package/dist/hooks/useLightLive.d.ts.map +1 -1
  333. package/dist/hooks/useLightLive.js +2 -0
  334. package/dist/hooks/useLinkStatus.d.ts +7 -0
  335. package/dist/hooks/useLinkStatus.d.ts.map +1 -0
  336. package/dist/hooks/useLinkStatus.js +20 -0
  337. package/dist/hooks/useMaterialApplication.d.ts +6 -1
  338. package/dist/hooks/useMaterialApplication.d.ts.map +1 -1
  339. package/dist/hooks/useMaterialApplication.js +7 -2
  340. package/dist/hooks/useOceanMaterial.d.ts +11 -1
  341. package/dist/hooks/useOceanMaterial.d.ts.map +1 -1
  342. package/dist/hooks/useOceanMaterial.js +185 -27
  343. package/dist/hooks/usePathAnimation.d.ts +8 -0
  344. package/dist/hooks/usePathAnimation.d.ts.map +1 -1
  345. package/dist/hooks/usePathAnimation.js +140 -59
  346. package/dist/hooks/useResolvedLayout.d.ts +10 -0
  347. package/dist/hooks/useResolvedLayout.d.ts.map +1 -0
  348. package/dist/hooks/useResolvedLayout.js +17 -0
  349. package/dist/hooks/useTimeLighting.d.ts +36 -0
  350. package/dist/hooks/useTimeLighting.d.ts.map +1 -0
  351. package/dist/hooks/useTimeLighting.js +67 -0
  352. package/dist/index.d.ts +4 -0
  353. package/dist/index.d.ts.map +1 -1
  354. package/dist/index.js +4 -0
  355. package/dist/operations/catalogue/shaderLayers.d.ts.map +1 -1
  356. package/dist/operations/catalogue/shaderLayers.js +26 -3
  357. package/dist/render/renderLive.d.ts +11 -0
  358. package/dist/render/renderLive.d.ts.map +1 -0
  359. package/dist/render/renderLive.js +57 -0
  360. package/dist/shaders/glsl/oceanCore.glsl.d.ts +2 -2
  361. package/dist/shaders/glsl/oceanCore.glsl.d.ts.map +1 -1
  362. package/dist/shaders/glsl/oceanCore.glsl.js +35 -5
  363. package/dist/shaders/ocean/OceanSurfaceMaterial.d.ts +12 -0
  364. package/dist/shaders/ocean/OceanSurfaceMaterial.d.ts.map +1 -1
  365. package/dist/shaders/ocean/OceanSurfaceMaterial.js +55 -7
  366. package/dist/shaders/terrain/TerrainMaterial.d.ts.map +1 -1
  367. package/dist/shaders/terrain/TerrainMaterial.js +28 -0
  368. package/dist/triggers/kinds/change.js +1 -1
  369. package/dist/utils/composerHostedPass.d.ts +74 -0
  370. package/dist/utils/composerHostedPass.d.ts.map +1 -0
  371. package/dist/utils/composerHostedPass.js +63 -0
  372. package/dist/utils/geometryScript.d.ts +17 -0
  373. package/dist/utils/geometryScript.d.ts.map +1 -0
  374. package/dist/utils/geometryScript.js +92 -0
  375. package/dist/utils/geometryScript.worker.d.ts +2 -0
  376. package/dist/utils/geometryScript.worker.d.ts.map +1 -0
  377. package/dist/utils/geometryScript.worker.js +42 -0
  378. package/dist/utils/groundRegistry.d.ts +11 -0
  379. package/dist/utils/groundRegistry.d.ts.map +1 -0
  380. package/dist/utils/groundRegistry.js +39 -0
  381. package/dist/utils/instancingSupport.d.ts +9 -0
  382. package/dist/utils/instancingSupport.d.ts.map +1 -0
  383. package/dist/utils/instancingSupport.js +13 -0
  384. package/dist/utils/landPatch.d.ts +26 -5
  385. package/dist/utils/landPatch.d.ts.map +1 -1
  386. package/dist/utils/landPatch.js +124 -9
  387. package/dist/utils/layoutPlacement.d.ts +30 -0
  388. package/dist/utils/layoutPlacement.d.ts.map +1 -0
  389. package/dist/utils/layoutPlacement.js +113 -0
  390. package/dist/utils/linkRegistry.d.ts +15 -0
  391. package/dist/utils/linkRegistry.d.ts.map +1 -0
  392. package/dist/utils/linkRegistry.js +11 -0
  393. package/dist/utils/shapes.d.ts +28 -2
  394. package/dist/utils/shapes.d.ts.map +1 -1
  395. package/dist/utils/shapes.js +32 -3
  396. package/dist/utils/sharedParts.d.ts +61 -0
  397. package/dist/utils/sharedParts.d.ts.map +1 -0
  398. package/dist/utils/sharedParts.js +268 -0
  399. package/dist/utils/svgAsset.d.ts +6 -0
  400. package/dist/utils/svgAsset.d.ts.map +1 -0
  401. package/dist/utils/svgAsset.js +46 -0
  402. package/dist/utils/waterLight.d.ts +29 -0
  403. package/dist/utils/waterLight.d.ts.map +1 -0
  404. package/dist/utils/waterLight.js +67 -0
  405. package/dist/utils/waterReflection.d.ts +185 -0
  406. package/dist/utils/waterReflection.d.ts.map +1 -0
  407. package/dist/utils/waterReflection.js +616 -0
  408. package/package.json +4 -3
@@ -0,0 +1,1728 @@
1
+ // Emitted from Claude outputs/vegetation-lab/common.mjs by gen_family.mjs — edit the lab module and re-emit.
2
+ // Shared core of the vegetation objects (lab). Inlined verbatim into every generated component.
3
+ //
4
+ // A vegetation object is a solid plant standing in the scene, not a medium: it is lit by the scene's
5
+ // lights and casts and receives shadows like a mesh. Every material is therefore a three.js lit
6
+ // material (MeshStandardMaterial) with the plant's own code injected through onBeforeCompile:
7
+ // vertex hierarchical wind (plant bend ∝ height², branch sway at the branch's own phase, leaf
8
+ // flutter), instanced leaf cards; the same code runs in the custom
9
+ // depth material so shadows move with the wind and are cut by the leaf mask.
10
+ // fragment procedural bark (ridges as a bump), leaf shapes as anti-aliased SDF masks, colour from
11
+ // quantities (per-leaf hash, canopy depth, season), translucent back-lighting.
12
+ //
13
+ // Size contract: a plant is built in the unit box (x/z in [-0.5, 0.5], y in [0, 1], the ground at
14
+ // y = 0) and the transform scale sizes it (a tree with scale [8, 12, 8] is 8 m wide and 12 m tall).
15
+ // Knobs describe the plant, never the box.
16
+ import * as THREE from 'three';
17
+ export const DEG_V = Math.PI / 180;
18
+ export const WIND_MS_VEG = 8; // m/s per unit of windStrength (same range as the environment family)
19
+ export function mulberry32(a) {
20
+ return function () {
21
+ a |= 0;
22
+ a = (a + 0x6d2b79f5) | 0;
23
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
24
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
25
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
26
+ };
27
+ }
28
+ export function setColor(uniform, v) {
29
+ if (v === undefined || v === null)
30
+ return;
31
+ if (v.isColor)
32
+ uniform.value.copy(v);
33
+ else
34
+ uniform.value.set(v);
35
+ }
36
+ // ---------------------------------------------------------------------------- uniforms / config
37
+ /** Uniforms every vegetation object has (shared by all of its materials). */
38
+ export function createCommonUniforms() {
39
+ return {
40
+ uTime: { value: 0 },
41
+ uWindPhase: { value: 0 }, // integrated wind distance (gust fronts travel with it)
42
+ uWindDir: { value: new THREE.Vector2(0, 1) },
43
+ uWindStrength: { value: 0.2 },
44
+ uTurb: { value: 0.3 },
45
+ uSpeed: { value: 1 },
46
+ uSway: { value: 1 },
47
+ uFlutter: { value: 0.5 },
48
+ uScale: { value: new THREE.Vector3(1, 1, 1) }, // the transform scale (metres per unit)
49
+ uMean: { value: 1 }, // its geometric mean: local shapes are sized by it
50
+ uOpacity: { value: 1 },
51
+ uColor: { value: new THREE.Color('#4f8a3a') },
52
+ uShadeColor: { value: new THREE.Color('#1f3d1a') },
53
+ uSeason: { value: 0 },
54
+ uFoliage: { value: 1 },
55
+ uLeafType: { value: 0 },
56
+ uLeafSize: { value: 1 },
57
+ uGrowth: { value: 1 }, // plants scale up from their roots (crops, ivy, flowers)
58
+ uSunDirView: { value: new THREE.Vector3(0, 1, 0) }, // sun direction in view space (translucency only)
59
+ // detail levels (see "detail levels" below): the main camera, as every draw of the plant sees it
60
+ uViewPos: { value: new THREE.Vector3(0, 0, 1e5) }, // camera position, world
61
+ uLodK: { value: 1 }, // 1 / tan(fov / 2) · zoom (perspective) or 1 / half-height (orthographic)
62
+ uLodOrtho: { value: 0 },
63
+ uLodBias: { value: 1 }, // the lod knob: auto 1, full 1e6, impostor 1e-6
64
+ uImpReady: { value: 0 }, // the impostor atlas is baked: the far cross-fade may start
65
+ uBake: { value: 0 }, // 1 albedo pass, 2 normal pass (impostor bake), 0 normal render
66
+ uBakeView: { value: new THREE.Matrix3() }, // bake camera: view space → plant space (normal pass)
67
+ uBakeDepth: { value: new THREE.Vector2(1, 1) }, // bake view: camera distance to the centre, half-depth of the box along the view
68
+ };
69
+ }
70
+ /** The base knobs every vegetation object shares (push point, part 1). */
71
+ export function applyCommonConfig(u, c) {
72
+ if (c.speed !== undefined)
73
+ u.uSpeed.value = c.speed;
74
+ if (c.opacity !== undefined)
75
+ u.uOpacity.value = c.opacity;
76
+ setColor(u.uColor, c.color);
77
+ setColor(u.uShadeColor, c.shadeColor);
78
+ if (c.windStrength !== undefined)
79
+ u.uWindStrength.value = c.windStrength;
80
+ if (c.windDirection !== undefined)
81
+ u.uWindDir.value.set(Math.sin(c.windDirection * DEG_V), Math.cos(c.windDirection * DEG_V));
82
+ if (c.turbulence !== undefined)
83
+ u.uTurb.value = c.turbulence;
84
+ if (c.sway !== undefined)
85
+ u.uSway.value = c.sway;
86
+ if (c.flutter !== undefined)
87
+ u.uFlutter.value = c.flutter;
88
+ if (c.seasonHue !== undefined)
89
+ u.uSeason.value = c.seasonHue;
90
+ if (c.foliage !== undefined)
91
+ u.uFoliage.value = c.foliage;
92
+ if (c.leafSize !== undefined)
93
+ u.uLeafSize.value = c.leafSize;
94
+ if (c.growth !== undefined)
95
+ u.uGrowth.value = c.growth;
96
+ if (c.leafType !== undefined)
97
+ u.uLeafType.value = Math.max(0, LEAF_TYPES.indexOf(c.leafType));
98
+ }
99
+ export const _vs = new THREE.Vector3(), _vq = new THREE.Quaternion(), _vsun = new THREE.Vector3(), _vm = new THREE.Matrix3();
100
+ /** Per-frame bookkeeping shared by every object: scale, integrated phases, sun into view space. */
101
+ export function updateCommon(u, object, camera, dt, sunWorld) {
102
+ camera.updateMatrixWorld();
103
+ object.updateWorldMatrix(true, false);
104
+ object.getWorldScale(_vs);
105
+ u.uScale.value.set(Math.max(_vs.x, 1e-3), Math.max(_vs.y, 1e-3), Math.max(_vs.z, 1e-3));
106
+ u.uMean.value = Math.cbrt(u.uScale.value.x * u.uScale.value.y * u.uScale.value.z);
107
+ const sp = u.uSpeed.value;
108
+ u.uTime.value += dt * sp;
109
+ u.uWindPhase.value += dt * sp * (0.4 + u.uWindStrength.value * WIND_MS_VEG);
110
+ camera.getWorldPosition(u.uViewPos.value);
111
+ if (camera.isPerspectiveCamera) {
112
+ u.uLodK.value = (camera.zoom || 1) / Math.tan(camera.fov * DEG_V * 0.5);
113
+ u.uLodOrtho.value = 0;
114
+ }
115
+ else if (camera.isOrthographicCamera) {
116
+ u.uLodK.value = 2 * (camera.zoom || 1) / Math.max(camera.top - camera.bottom, 1e-6);
117
+ u.uLodOrtho.value = 1;
118
+ }
119
+ if (sunWorld) {
120
+ _vm.setFromMatrix4(camera.matrixWorldInverse);
121
+ u.uSunDirView.value.copy(_vsun.copy(sunWorld).normalize()).applyMatrix3(_vm).normalize();
122
+ }
123
+ }
124
+ // ---------------------------------------------------------------------------- GLSL: shared pieces
125
+ export const LEAF_TYPES = ['broadleaf', 'lobed', 'needles', 'round', 'lanceolate', 'frond', 'heart', 'petal', 'blade', 'maple'];
126
+ export const VEG_UNIFORMS_GLSL = /* glsl */ `
127
+ uniform float uTime, uWindPhase, uWindStrength, uTurb, uSpeed, uSway, uFlutter, uOpacity, uSeason, uFoliage, uLeafType, uLeafSize, uGrowth;
128
+ uniform float uMean;
129
+ uniform vec2 uWindDir;
130
+ uniform vec3 uScale, uColor, uShadeColor, uSunDirView;
131
+ uniform vec3 uViewPos; uniform float uLodK, uLodOrtho, uLodBias, uImpReady, uBake; uniform mat3 uBakeView; uniform vec2 uBakeDepth;
132
+ // The plant's skeleton follows the (possibly non-uniform) transform scale; its local shapes (cards,
133
+ // fruit, blade widths, bark rings) must not: an offset in "mean-scale units" is divided back per axis
134
+ // so it stays isotropic in the world, and its normal is pre-multiplied by the scale so the normal
135
+ // matrix (which undoes the scale) lands on the isotropic normal.
136
+ vec3 isoOff(vec3 o) { return o * uMean / uScale; }
137
+ vec3 isoNrm(vec3 n) { return normalize(n * uScale); }
138
+ `;
139
+ export const NOISE_GLSL_V = /* glsl */ `
140
+ float vhash12(vec2 p) { vec3 q = fract(vec3(p.xyx) * 0.1031); q += dot(q, q.yzx + 33.33); return fract((q.x + q.y) * q.z); }
141
+ float vhash11(float p) { p = fract(p * 0.1031); p *= p + 33.33; p *= p + p; return fract(p); }
142
+ float vnoise2v(vec2 p) {
143
+ vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f);
144
+ return mix(mix(vhash12(i), vhash12(i + vec2(1, 0)), f.x), mix(vhash12(i + vec2(0, 1)), vhash12(i + vec2(1, 1)), f.x), f.y);
145
+ }
146
+ float fbm2v(vec2 p) { return (0.5 * vnoise2v(p) + 0.25 * vnoise2v(p * 2.03 + 7.1) + 0.125 * vnoise2v(p * 4.07 + 3.3)) / 0.875; }
147
+ float vbump(float x) { float t = clamp(1.0 - x * x, 0.0, 1.0); return t * t; }
148
+ vec3 qrot(vec4 q, vec3 v) { return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v); }
149
+ `;
150
+ /**
151
+ * The wind as one displacement field over the plant (unit space).
152
+ * gust low-frequency noise travelling downwind (uWindPhase), sharpened by turbulence into
153
+ * separate gust fronts, so the whole plant leans and recovers rather than oscillating.
154
+ * bend the plant leans with the gust, ∝ height² (a cantilever), losing a little height.
155
+ * sway each branch oscillates at its own frequency and phase (w.x) with its own flexibility
156
+ * (w.y), driven by the gust strength; sideways component so tips draw ellipses.
157
+ * w = (phase, flexibility). hf = height fraction of the attachment (0 ground, 1 top).
158
+ */
159
+ export const WIND_GLSL = /* glsl */ `
160
+ #ifndef VEG_M
161
+ #define VEG_M modelMatrix
162
+ #endif
163
+ float gustAt(vec2 xz) {
164
+ // metres: the point inside the plant plus where the plant stands (modelMatrix), so gust fronts
165
+ // travel across a group of plants and copies of one plant (layouts) do not sway in lockstep
166
+ vec2 q = (xz * uScale.xz + VEG_M[3].xz) * 0.06 - uWindDir * uWindPhase * 0.08;
167
+ float g = vnoise2v(q) * 0.65 + vnoise2v(q * 3.1 + uTime * 0.15) * 0.35;
168
+ g = mix(g, smoothstep(0.35, 0.75, g), uTurb);
169
+ return 0.25 + 1.5 * g;
170
+ }
171
+ vec3 plantDisplace(vec3 p, float hf, vec2 w) {
172
+ float W = uWindStrength * gustAt(p.xz);
173
+ float h2 = hf * hf;
174
+ // cantilever lean: a quarter of the height in a full gale on a flexible plant
175
+ vec2 lean = uWindDir * W * 0.25 * h2 * uSway;
176
+ // branch sway: own frequency and phase, elliptical tip path
177
+ float f = 1.3 + 1.6 * w.y;
178
+ float s = sin(uTime * f + w.x * 6.2832) * W * w.y * 0.08 * uSway * hf;
179
+ float c = cos(uTime * f * 0.71 + w.x * 4.1) * W * w.y * 0.03 * uSway * hf;
180
+ vec3 d = vec3(lean.x, -0.35 * length(lean) * hf, lean.y);
181
+ d += vec3(uWindDir.x * s - uWindDir.y * c, 0.25 * abs(s), uWindDir.y * s + uWindDir.x * c);
182
+ return d;
183
+ }
184
+ `;
185
+ /**
186
+ * Leaf shape as a signed distance in card space (x across, y along, base at y = -1, tip at y = 1),
187
+ * for every leaf type. Positive = outside. Anti-aliased by the caller with fwidth.
188
+ */
189
+ export const LEAF_SDF_GLSL = /* glsl */ `
190
+ // serrated margin: small teeth along the edge, finer toward the tip
191
+ float teeth(float t, float k, float amp) { return amp * abs(sin(t * k * 3.1416)) * (1.0 - 0.5 * t); }
192
+ float leafSdf(vec2 p, float type, float h) {
193
+ float t = (p.y + 1.0) * 0.5; // 0 base .. 1 tip
194
+ // asymmetry: one half a little wider, the midrib slightly bowed
195
+ float asym = 1.0 + 0.1 * (h - 0.5) * sign(p.x);
196
+ float ax = abs(p.x - 0.06 * (h - 0.5) * sin(3.1416 * t));
197
+ if (type < 0.5) { // broadleaf: ovate, pointed drip tip, fine serration
198
+ float w = (0.62 * pow(sin(3.1416 * clamp(t, 0.0, 1.0)), 0.7) * (1.0 - 0.25 * t) + 0.02 * (1.0 - t)) * asym;
199
+ w -= teeth(t, 14.0 + 6.0 * h, 0.025);
200
+ return max(ax - w, -p.y - 1.0);
201
+ } else if (type < 1.5) { // lobed (oak): rounded lobes with deep sinuses
202
+ float lob = 0.5 + 0.5 * cos(3.1416 * (t * 7.0 + 0.5 + 0.4 * h));
203
+ lob = smoothstep(0.0, 1.0, lob);
204
+ float w = (0.3 + 0.36 * lob) * pow(sin(3.1416 * clamp(t, 0.02, 1.0)), 0.45) * asym;
205
+ return max(max(ax - w, -p.y - 1.0), p.y - 1.0);
206
+ } else if (type < 2.5) { // needles: a tuft fanning from the base
207
+ float d = 1e3;
208
+ for (int i = 0; i < 7; i++) {
209
+ float a = (float(i) - 3.0) * 0.21 + 0.1 * (h - 0.5);
210
+ vec2 dir = vec2(sin(a), cos(a));
211
+ vec2 q = p - vec2(0.0, -1.0);
212
+ float s = clamp(dot(q, dir), 0.0, 1.9 - 0.15 * abs(float(i) - 3.0));
213
+ d = min(d, length(q - dir * s) - 0.035);
214
+ }
215
+ return d;
216
+ } else if (type < 3.5) { // round (box, privet): obovate with a notch at the tip
217
+ float w = 0.68 * pow(sin(3.1416 * clamp(t, 0.0, 1.0)), 0.55) * (0.85 + 0.3 * t) * asym;
218
+ float notch = smoothstep(0.08, 0.0, ax) * smoothstep(0.9, 1.0, t) * 0.12;
219
+ return max(ax - w, -p.y - 1.0 + 0.0) + notch;
220
+ } else if (type < 4.5) { // lanceolate (willow): long, narrow, finely toothed
221
+ float w = 0.2 * pow(sin(3.1416 * clamp(t, 0.0, 1.0)), 0.55) * asym - teeth(t, 22.0, 0.012);
222
+ return max(ax - w, -p.y - 1.0);
223
+ } else if (type < 5.5) { // frond: a rachis with leaflets
224
+ float rachis = ax - 0.04;
225
+ float seg = fract(t * 9.0 + 0.5) - 0.5;
226
+ float leaflet = max(abs(seg) - 0.28 * (1.0 - t * 0.6), ax - 0.95 * (1.0 - t * 0.85) * smoothstep(0.0, 0.05, t));
227
+ return max(min(rachis, leaflet), -p.y - 1.0);
228
+ } else if (type < 6.5) { // ivy: three to five lobes, cordate base
229
+ vec2 q = vec2(ax, p.y + 0.15);
230
+ float c = length(q - vec2(0.0, 0.1)) - 0.75;
231
+ float side = length(q - vec2(0.55, -0.25)) - 0.5;
232
+ float tip = max(ax * 1.6 - (1.0 - p.y) * 0.9, -p.y - 0.5);
233
+ float base = -(length(q - vec2(0.0, -0.95)) - 0.25); // the notch at the stalk
234
+ return max(min(min(c, side), tip), base);
235
+ } else if (type < 7.5) { // petal: a soft drop, widest near the tip, a wavy edge
236
+ float w = 0.55 * pow(sin(3.1416 * clamp(t, 0.0, 1.0)), 0.5) * (0.55 + 0.45 * t) * asym;
237
+ w += 0.03 * sin(t * 19.0 + h * 7.0) * t;
238
+ return max(ax - w, -p.y - 1.0);
239
+ } else if (type < 8.5) { // blade (grass): tapering strip
240
+ return max(ax - 0.12 * (1.0 - t * 0.9), -p.y - 1.0);
241
+ }
242
+ // maple: palmate, five pointed lobes from the base
243
+ vec2 q = p - vec2(0.0, -0.75);
244
+ float r = length(q), a = atan(q.x, q.y);
245
+ float lobes = 0.55 + 0.55 * pow(abs(cos(a * 2.5)), 0.35) * (1.0 - 0.35 * abs(a) / 3.1416);
246
+ float lim = smoothstep(2.6, 1.9, abs(a));
247
+ return r - 1.55 * lobes * lim * (1.0 - 0.1 * h);
248
+ }
249
+ `;
250
+ // ---------------------------------------------------------------------------- detail levels (constants + GLSL)
251
+ /**
252
+ * Distance detail for a plant and every copy of it (layout mirrors, linked instances). One quantity
253
+ * decides, s: the plant's bounding radius over the camera's half-height at its distance (≈ the share of
254
+ * half the screen it fills). The CPU (per draw: onBeforeRender / onBeforeShadow, from the drawn mesh's
255
+ * own matrixWorld, so each mirror decides for itself) and the GPU (vertex, from modelMatrix) evaluate
256
+ * the same s from the same shared uniforms (uViewPos / uLodK = the main camera, set in updateCommon).
257
+ * s > FULL every leaf, curled cards, every branch level
258
+ * FLAT..FULL the leaf count falls toward MIN, the cards grow (1/√share, ≤ GROW) so the cover holds
259
+ * s < FLAT flat cards (2 triangles instead of 16), the finest branch level dropped
260
+ * IMP·0.8 .. IMP·1.2 dithered cross-fade to the impostor (both drawn, complementary pixels)
261
+ * s < IMP·0.8 the impostor alone: one octahedral-impostor card
262
+ * Shadow pass: flat cards, SHADOW × the leaves, trunk + first branch level; the impostor casts below IMP.
263
+ */
264
+ export const LOD = { FULL: 0.45, FLAT: 0.25, IMP: 0.12, MIN: 0.3, SHADOW: 0.5, GROW: 1.8, FADE_IN: 1.2, FADE_OUT: 0.8 };
265
+ export const LOD_GLSL_V = /* glsl */ `
266
+ float vegLodS() {
267
+ if (uBake > 0.5) return 1e6;
268
+ mat4 M_ = VEG_M;
269
+ vec3 c = (M_ * vec4(0.0, 0.5, 0.0, 1.0)).xyz;
270
+ float R = 0.5 * sqrt(dot(M_[0].xyz, M_[0].xyz) + dot(M_[1].xyz, M_[1].xyz) + dot(M_[2].xyz, M_[2].xyz));
271
+ float d = uLodOrtho > 0.5 ? 1.0 : max(distance(c, uViewPos), 1e-3);
272
+ return R * uLodK / d * uLodBias;
273
+ }
274
+ float vegLodFrac(float s) { return mix(${LOD.MIN.toFixed(3)}, 1.0, smoothstep(${LOD.IMP.toFixed(3)}, ${LOD.FULL.toFixed(3)}, s)); }
275
+ // 0 near .. 1 far: the share of pixels the impostor takes over in the cross-fade band
276
+ float vegLodFade(float s) { return uImpReady * (1.0 - smoothstep(${(LOD.IMP * LOD.FADE_OUT).toFixed(4)}, ${(LOD.IMP * LOD.FADE_IN).toFixed(4)}, s)); }
277
+ `;
278
+ /** Interleaved gradient noise: the dither of the cross-fade (full detail keeps the pixels the impostor leaves). */
279
+ export const LOD_GLSL_F = /* glsl */ `
280
+ float vegIgn(vec2 p) { return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); }
281
+ `;
282
+ /** Appended to every plant material's fragment: the impostor bake passes (albedo, then plant-space normal). */
283
+ const BAKE_FRAG = /* glsl */ `
284
+ if (uBake > 0.5) {
285
+ vec3 bakeN = normalize(uBakeView * normal) * 0.5 + 0.5;
286
+ // coverage is binary per baked pixel (a leaf drew here or not): leaves smaller than a pixel would
287
+ // otherwise bake as faint alpha; the atlas mips average it into real coverage at distance
288
+ // the normal pass keeps the depth along the view in alpha (0 back .. 1 front): the impostor reprojects with it
289
+ float bakeD = clamp((uBakeDepth.x - vViewPosition.z) / (2.0 * uBakeDepth.y) + 0.5, 0.0, 1.0);
290
+ gl_FragColor = linearToOutputTexel(uBake < 1.5 ? vec4(diffuseColor.rgb, 1.0) : vec4(bakeN, 1.0));
291
+ if (uBake > 1.5) gl_FragColor.a = bakeD;
292
+ }`;
293
+ function bakeOut(shader) { shader.fragmentShader = shader.fragmentShader.replace('#include <dithering_fragment>', '#include <dithering_fragment>' + BAKE_FRAG); }
294
+ // ---------------------------------------------------------------------------- materials
295
+ /**
296
+ * The shared vertex injection: wind applied at the attachment point, shared across the
297
+ * bark (attribute aWind = hf, phase, flex) and the instanced cards (aInfoB).
298
+ */
299
+ const COMMON_VERT_PARS = /* glsl */ `
300
+ // the plant's model matrix: per instance when the mesh is GPU-instanced (a forest's impostors), else the object's
301
+ #ifdef USE_INSTANCING
302
+ #define VEG_M (modelMatrix * instanceMatrix)
303
+ #else
304
+ #define VEG_M modelMatrix
305
+ #endif
306
+ ${VEG_UNIFORMS_GLSL}
307
+ ${NOISE_GLSL_V}
308
+ ${WIND_GLSL}
309
+ ${LOD_GLSL_V}
310
+ `;
311
+ function injectUniforms(shader, u) { for (const k in u)
312
+ shader.uniforms[k] = u[k]; }
313
+ /**
314
+ * Bark: a lit material whose vertices follow the wind (aWind = hf, phase, flex; aRing = angle 0..1,
315
+ * length along the branch in metres-at-scale-1) and whose surface is procedural bark: vertical ridges
316
+ * (anisotropic fbm), fissures darkening with depth, lichen specks on the shaded side, as a bump map.
317
+ */
318
+ export function barkMaterial(u, opts) {
319
+ const o = Object.assign({ barkColor: '#5a4632', roughness: 0.95, ridges: 1, lod: false }, opts);
320
+ const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: o.roughness, metalness: 0, envMapIntensity: 0.15 });
321
+ const own = { uBarkColor: { value: new THREE.Color(o.barkColor) }, uRidges: { value: o.ridges } };
322
+ mat.userData.uniforms = own;
323
+ const vert = (shader) => {
324
+ injectUniforms(shader, u);
325
+ injectUniforms(shader, own);
326
+ shader.vertexShader = shader.vertexShader
327
+ .replace('#include <common>', `#include <common>\n${COMMON_VERT_PARS}\nattribute vec3 aWind; attribute vec3 aRing; attribute vec3 aRoot; attribute vec3 aCentre; attribute float aGrow; varying vec3 vRing; varying float vHf;`)
328
+ .replace('#include <beginnormal_vertex>', `vec3 objectNormal = isoNrm(normal);\n#ifdef USE_TANGENT\nvec3 objectTangent = vec3(tangent.xyz);\n#endif`)
329
+ .replace('#include <begin_vertex>', `
330
+ // growth: the plant scales up from its root; a vine's part beyond the growth front collapses onto its axis
331
+ vec3 pg = aCentre + isoOff(position - aCentre) * step(aGrow, uGrowth);
332
+ pg = aRoot + (pg - aRoot) * uGrowth;
333
+ vec3 transformed = pg + plantDisplace(pg, aWind.x, aWind.yz) * uGrowth;
334
+ vRing = aRing; vHf = aWind.x;${o.lod ? '\n vLodFade = vegLodFade(vegLodS());' : ''}`);
335
+ if (o.lod)
336
+ shader.vertexShader = shader.vertexShader.replace('varying float vHf;', 'varying float vHf; varying float vLodFade;');
337
+ };
338
+ mat.onBeforeCompile = (shader) => {
339
+ vert(shader);
340
+ shader.fragmentShader = shader.fragmentShader
341
+ .replace('#include <common>', `#include <common>\n${VEG_UNIFORMS_GLSL}\n${NOISE_GLSL_V}\nuniform vec3 uBarkColor; uniform float uRidges; varying vec3 vRing; varying float vHf;${o.lod ? ' varying float vLodFade;' : ''}
342
+ ${LOD_GLSL_F}
343
+ float barkH(vec2 q) {
344
+ // plates between a network of vertical fissures (ridged noise stretched along the branch), cut by
345
+ // slower horizontal cracks, with fine grain on the plates
346
+ // two ridged layers (coarse furrows, fine furrows between them), wandering along the branch
347
+ float w = (vnoise2v(vec2(q.x * 2.0, q.y * 0.4) + 9.0) - 0.5) * 0.15;
348
+ float f1 = 1.0 - abs(2.0 * fbm2v(vec2(q.x * 12.0 + w, q.y * 1.4)) - 1.0);
349
+ float f2 = 1.0 - abs(2.0 * fbm2v(vec2(q.x * 30.0 - w, q.y * 4.0) + 3.0) - 1.0);
350
+ float fiss = pow(f1, 4.0) * 0.8 + pow(f2, 3.0) * 0.45;
351
+ float cracks = smoothstep(0.62, 0.78, vnoise2v(vec2(q.x * 3.0, q.y * 9.0) + 5.0));
352
+ float plates = vnoise2v(vec2(q.x * 7.0, q.y * 2.5) + 2.0);
353
+ float grain = vnoise2v(vec2(q.x * 120.0, q.y * 24.0)) * 0.1;
354
+ return clamp(1.0 - fiss - cracks * 0.3 + plates * 0.25 + grain - 0.05, 0.0, 1.0);
355
+ }`)
356
+ .replace('#include <map_fragment>', `${o.lod ? '\n if (vLodFade > 0.0 && vegIgn(gl_FragCoord.xy) < vLodFade) discard;' : ''}
357
+ float vegRough = 0.9;
358
+ // bark coordinates in metres: arc length around the branch, length along it
359
+ vec2 q = vec2(vRing.x * 6.2832 * vRing.z * uMean, vRing.y * uScale.y);
360
+ float hgt = barkH(q);
361
+ // three tones: dark fissure floor, mid plate, pale weathered plate tops
362
+ vec3 bark = mix(uBarkColor * 0.4, uBarkColor * 1.1, smoothstep(0.1, 0.5, hgt));
363
+ bark = mix(bark, uBarkColor * vec3(1.35, 1.3, 1.25), smoothstep(0.75, 1.0, hgt) * 0.6);
364
+ bark *= 0.9 + 0.2 * vnoise2v(q * vec2(1.5, 0.3) + 7.0);
365
+ // moss and lichen: low on the trunk and in the fissures, mostly on the shaded side
366
+ float shade = 0.5 - 0.5 * dot(normalize(vNormal), normalize(uSunDirView));
367
+ float moss = smoothstep(0.6, 0.8, fbm2v(q * vec2(4.0, 1.5) + 11.0) + 0.2 * (1.0 - hgt)) * smoothstep(0.5, 0.0, vHf) * (0.2 + 0.8 * shade);
368
+ bark = mix(bark, vec3(0.36, 0.48, 0.2), moss * 0.6);
369
+ float lichen = smoothstep(0.74, 0.82, vnoise2v(q * vec2(20.0, 8.0) + 3.0)) * smoothstep(0.4, 0.8, hgt) * 0.35;
370
+ bark = mix(bark, vec3(0.62, 0.66, 0.5), lichen);
371
+ diffuseColor.rgb *= bark;
372
+ vegRough = mix(1.0, 0.88, hgt);`)
373
+ .replace('#include <roughnessmap_fragment>', '#include <roughnessmap_fragment>\n roughnessFactor = vegRough;')
374
+ .replace('#include <normal_fragment_maps>', `
375
+ {
376
+ // the ridge height as a bump (screen-space derivatives: no tangent frame needed)
377
+ vec2 q = vec2(vRing.x * 6.2832 * vRing.z * uMean, vRing.y * uScale.y);
378
+ float hgt = barkH(q) * 0.02 * uRidges * min(1.0, vRing.z * uMean * 8.0);
379
+ vec3 dpdx = dFdx(-vViewPosition), dpdy = dFdy(-vViewPosition);
380
+ float dhx = dFdx(hgt), dhy = dFdy(hgt);
381
+ vec3 r1 = cross(dpdy, normal), r2 = cross(normal, dpdx);
382
+ float det = dot(dpdx, r1);
383
+ vec3 grad = (r1 * dhx + r2 * dhy) / max(abs(det), 1e-6) * sign(det);
384
+ normal = normalize(normal - grad);
385
+ }`);
386
+ bakeOut(shader);
387
+ };
388
+ mat.customProgramCacheKey = () => 'veg-bark' + (o.lod ? '-lod' : '');
389
+ const depth = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking });
390
+ depth.onBeforeCompile = vert;
391
+ depth.customProgramCacheKey = () => 'veg-bark-depth' + (o.lod ? '-lod' : '');
392
+ return { material: mat, depth };
393
+ }
394
+ /**
395
+ * Leaf cards: one instanced draw. Per instance: aPos (attachment, unit space), aQuat (card frame:
396
+ * card x across, y along, z = normal), aInfoA = (size, hash, hf, ao), aInfoB = (phase, flex, order, kind).
397
+ * The card is a 2×2 quad; `uFoliage` keeps the leaves whose `order` is below it (the inner,
398
+ * late-season leaves go first). The fragment cuts the leaf shape (SDF, anti-aliased), colours from
399
+ * the hash, the canopy depth and the season, and adds translucent back-light.
400
+ */
401
+ export function leafMaterial(u, opts) {
402
+ const o = Object.assign({ roughness: 0.6, translucency: 0.55, veins: 0.5, autumnA: '#e2a21c', autumnB: '#b8441c', bloom: false, underside: 0.45, lod: false }, opts);
403
+ const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: o.roughness, metalness: 0, side: THREE.DoubleSide, alphaToCoverage: true, envMapIntensity: 0.4 });
404
+ const own = { uTranslucency: { value: o.translucency }, uVeins: { value: o.veins }, uAutumnA: { value: new THREE.Color(o.autumnA) }, uAutumnB: { value: new THREE.Color(o.autumnB) }, uBloom: { value: 1 }, uUnderside: { value: o.underside } };
405
+ mat.userData.uniforms = own;
406
+ const PARS = `#include <common>\n${COMMON_VERT_PARS}
407
+ uniform float uBloom;
408
+ attribute vec2 aCard; attribute vec3 aPos; attribute vec3 aRoot; attribute vec4 aQuat; attribute vec4 aInfoA; attribute vec4 aInfoB;
409
+ varying vec2 vCard; varying vec4 vLeaf;${o.lod ? ' varying float vLodFade;' : ''}`;
410
+ // the card's own flutter: a rock about its base axis, faster and wider in wind and under the cursor;
411
+ // a petal (bloom material) also folds up toward its normal while the bud is closed
412
+ const PRE = `
413
+ float W_ = uWindStrength * gustAt(aPos.xz);
414
+ float flut = sin(uTime * (5.0 + 3.0 * aInfoB.z) + aInfoB.x * 17.0) * (0.08 + 0.35 * W_) * uFlutter${o.bloom ? ' + (1.0 - smoothstep(aInfoA.w - 0.3, aInfoA.w + 0.1, uBloom)) * 1.35' : ''};
415
+ // the card's own shape: it curls along its length (drooping tip) and across (a gutter or a dome),
416
+ // amounts from the leaf's hash, so no two leaves are the same flat quad
417
+ float hh = aInfoA.y;
418
+ float curlY = (vhash11(hh * 13.0 + 0.3) - 0.35) * ${o.curl === undefined ? '0.9' : o.curl.toFixed(2)};
419
+ float curlX = (vhash11(hh * 17.0 + 0.7) - 0.5) * ${o.curl === undefined ? '0.8' : o.curl.toFixed(2)};
420
+ float ty = (aCard.y + 1.0) * 0.5;
421
+ float zc = curlY * ty * ty + curlX * aCard.x * aCard.x;
422
+ vec3 lp0 = vec3(aCard.x, aCard.y, zc);
423
+ // analytic normal of z = f(x, y)
424
+ vec3 ln0 = normalize(vec3(-2.0 * curlX * aCard.x, -curlY * ty, 1.0));
425
+ float cf = cos(flut), sf = sin(flut);
426
+ vec3 lp = vec3(lp0.x, lp0.y * cf - lp0.z * sf, lp0.y * sf + lp0.z * cf);
427
+ vec3 ln = vec3(ln0.x, ln0.y * cf - ln0.z * sf, ln0.y * sf + ln0.z * cf);`;
428
+ const NRM = `
429
+ vec3 objectNormal = isoNrm(qrot(aQuat, ln));
430
+ #ifdef USE_TANGENT
431
+ vec3 objectTangent = vec3(tangent.xyz);
432
+ #endif`;
433
+ // detail levels: fewer leaves drawn at distance (the CPU sets the count), each grown so the cover holds
434
+ const lodPos = (depth) => !o.lod ? '' : `
435
+ float lodS_ = vegLodS();
436
+ sz *= min(${LOD.GROW.toFixed(2)}, inversesqrt(vegLodFrac(lodS_)${depth ? ` * ${LOD.SHADOW.toFixed(2)}` : ''}));${depth ? '' : '\n vLodFade = vegLodFade(lodS_);'}`;
437
+ const POS = (depth) => `
438
+ // growth: the leaf's plant scales up from its root; a leaf past the growth front (aInfoB.w) is not there yet
439
+ float keep = step(aInfoB.z, uFoliage) * step(aInfoB.w, uGrowth);
440
+ vec3 ap = aRoot + (aPos - aRoot) * uGrowth;
441
+ float sz = aInfoA.x * uLeafSize * keep * uGrowth;${lodPos(depth)}
442
+ vec3 transformed = ap + isoOff(qrot(aQuat, (lp + vec3(0.0, 1.0, 0.0)) * sz)) + plantDisplace(ap, aInfoA.z, aInfoB.xy) * uGrowth;
443
+ vCard = aCard; vLeaf = vec4(aInfoA.y, aInfoA.w, aInfoB.w, aInfoA.z);`;
444
+ const vert = (shader, depth = false) => {
445
+ injectUniforms(shader, u);
446
+ injectUniforms(shader, own);
447
+ shader.vertexShader = depth
448
+ ? shader.vertexShader.replace('#include <common>', PARS).replace('#include <begin_vertex>', PRE + POS(true))
449
+ : shader.vertexShader.replace('#include <common>', PARS).replace('#include <beginnormal_vertex>', PRE + NRM).replace('#include <begin_vertex>', POS(false));
450
+ };
451
+ const fragMask = `
452
+ float lsd = leafSdf(vCard, uLeafType, vLeaf.x);
453
+ float aa = fwidth(lsd) * 0.9;
454
+ float cover = 1.0 - smoothstep(-aa, aa, lsd);
455
+ if (cover < 0.02) discard;
456
+ float vegRough = 0.5; float leafHgt = 0.0;`;
457
+ mat.onBeforeCompile = (shader) => {
458
+ vert(shader);
459
+ shader.fragmentShader = shader.fragmentShader
460
+ .replace('#include <common>', `#include <common>\n${VEG_UNIFORMS_GLSL}\n${NOISE_GLSL_V}\n${LEAF_SDF_GLSL}
461
+ uniform float uTranslucency, uVeins, uUnderside; uniform vec3 uAutumnA, uAutumnB;
462
+ varying vec2 vCard; varying vec4 vLeaf;${o.lod ? ' varying float vLodFade;' : ''}
463
+ ${LOD_GLSL_F}`)
464
+ .replace('#include <map_fragment>', `${o.lod ? '\n if (vLodFade > 0.0 && vegIgn(gl_FragCoord.xy) < vLodFade) discard;' : ''}${fragMask}
465
+ float h = vLeaf.x, ao = vLeaf.y;
466
+ float t = (vCard.y + 1.0) * 0.5;
467
+ // colour from quantities: per-leaf hue/value scatter, canopy depth (inner leaves in shade), season
468
+ vec3 base = uColor * (0.8 + 0.4 * vhash11(h * 7.0 + 1.0));
469
+ base = mix(base * vec3(0.92, 1.04, 0.78), base * vec3(0.98, 1.0, 1.04), vhash11(h * 3.0 + 2.0));
470
+ base = mix(uShadeColor, base, 0.15 + 0.85 * ao);
471
+ // young leaves (tip of the canopy) a touch yellower, the blade base paler
472
+ base = mix(base, base * vec3(1.08, 1.04, 0.75), 0.25 * (1.0 - t) );
473
+ vec3 autumn = mix(uAutumnA, uAutumnB, vhash11(h * 5.0 + 3.0));
474
+ float turn = smoothstep(h * 0.7 + 0.05, h * 0.7 + 0.45, uSeason);
475
+ base = mix(base, autumn, turn);
476
+ // dry brown margin as the season turns
477
+ float edge = smoothstep(-0.08, 0.0, lsd);
478
+ base = mix(base, vec3(0.42, 0.26, 0.1), edge * smoothstep(0.3, 0.9, turn) * 0.8);
479
+ // the vein network: a tapered midrib, secondaries leaving it at an angle and curving to the margin,
480
+ // a fine tertiary net; veins are paler and sit in shallow grooves (height field → bump)
481
+ float mid = 1.0 - smoothstep(0.0, 0.035 * (1.0 - 0.6 * t), abs(vCard.x));
482
+ float side = abs(vCard.x);
483
+ float sec = fract((t - side * 0.35) * (7.0 + 2.0 * vhash11(h * 2.0)) + 0.3);
484
+ float secL = 1.0 - smoothstep(0.0, 0.04, abs(sec - 0.5) - 0.0);
485
+ secL *= smoothstep(0.0, 0.06, side) * (1.0 - 0.5 * t);
486
+ float tert = smoothstep(0.55, 0.75, vnoise2v(vCard * vec2(14.0, 22.0) + h * 9.0)) * 0.3;
487
+ float vein = max(mid, max(secL * 0.55, tert * 0.6)) * uVeins;
488
+ base = mix(base, base * vec3(1.18, 1.12, 0.82), vein * 0.8);
489
+ // the underside is paler, bluer and matte
490
+ if (!gl_FrontFacing) base = mix(base, vec3(dot(base, vec3(0.33))) * vec3(0.85, 0.95, 0.85) + 0.08, uUnderside);
491
+ diffuseColor.rgb *= base;
492
+ diffuseColor.a *= cover * uOpacity;
493
+ vegRough = gl_FrontFacing ? mix(0.42, 0.62, vhash11(h * 4.0)) + 0.25 * turn : 0.85;
494
+ // bump: veins recessed, cells between the secondaries bulging, the edge rolled
495
+ leafHgt = (-0.5 * vein + 0.25 * (1.0 - secL) * (1.0 - mid) * uVeins) * 0.0025 + 0.004 * smoothstep(-0.12, 0.0, lsd);`)
496
+ .replace('#include <roughnessmap_fragment>', '#include <roughnessmap_fragment>\n roughnessFactor = vegRough;')
497
+ .replace('#include <normal_fragment_maps>', `#include <normal_fragment_maps>
498
+ {
499
+ vec3 dpdx = dFdx(-vViewPosition), dpdy = dFdy(-vViewPosition);
500
+ float dhx = dFdx(leafHgt), dhy = dFdy(leafHgt);
501
+ vec3 r1 = cross(dpdy, normal), r2 = cross(normal, dpdx);
502
+ float det = dot(dpdx, r1);
503
+ vec3 grad = (r1 * dhx + r2 * dhy) / max(abs(det), 1e-7) * sign(det);
504
+ normal = normalize(normal - grad);
505
+ }`)
506
+ .replace('#include <lights_fragment_end>', `#include <lights_fragment_end>
507
+ {
508
+ // translucency: sun through the blade when it is behind the leaf; veins and the midrib are thicker
509
+ float back = max(dot(-normal, uSunDirView), 0.0);
510
+ float thin = 1.0 - 0.6 * max(mid, secL * 0.7);
511
+ reflectedLight.indirectDiffuse += diffuseColor.rgb * vec3(1.05, 1.1, 0.7) * back * uTranslucency * thin * 1.1;
512
+ }`);
513
+ bakeOut(shader);
514
+ };
515
+ // every option that changes the shader text is in the key (leaf materials with different curls must not share a program)
516
+ const leafKey = 'veg-leaf:' + [o.bloom ? 1 : 0, o.curl === undefined ? 'd' : o.curl.toFixed(2), o.lod ? 1 : 0].join(',');
517
+ mat.customProgramCacheKey = () => leafKey;
518
+ const depth = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking, side: THREE.DoubleSide });
519
+ depth.onBeforeCompile = (shader) => {
520
+ vert(shader, true);
521
+ shader.fragmentShader = shader.fragmentShader
522
+ .replace('#include <common>', `#include <common>\n${VEG_UNIFORMS_GLSL}\n${LEAF_SDF_GLSL}\nvarying vec2 vCard; varying vec4 vLeaf;`)
523
+ .replace('#include <map_fragment>', fragMask);
524
+ };
525
+ depth.customProgramCacheKey = () => leafKey + ':depth';
526
+ return { material: mat, depth };
527
+ }
528
+ /** The leaf card: a 3×5 grid (so it can curl along and across) with instance buffers for `count` leaves. */
529
+ export const CARD_NX = 3, CARD_NY = 5;
530
+ /** Index counts: the curled grid, then the flat card that follows it. */
531
+ export const CARD_IDX = (CARD_NX - 1) * (CARD_NY - 1) * 6, FLAT_IDX = 6;
532
+ export function leafGeometry(count) {
533
+ const geo = new THREE.InstancedBufferGeometry();
534
+ const P = [], C = [], N = [], U = [], I = [];
535
+ const box = [[-0.5, 0, -0.5], [0.5, 0, 0.5], [0.5, 1, -0.5], [-0.5, 1, 0.5]];
536
+ let k = 0;
537
+ for (let j = 0; j < CARD_NY; j++)
538
+ for (let i = 0; i < CARD_NX; i++) {
539
+ // `position` holds only unit-box corners (what Box3.expandByObject reads); the card is aCard
540
+ const bp = box[k++ % 4];
541
+ P.push(bp[0], bp[1], bp[2]);
542
+ C.push(-1 + 2 * i / (CARD_NX - 1), -1 + 2 * j / (CARD_NY - 1));
543
+ N.push(0, 0, 1);
544
+ U.push(i / (CARD_NX - 1), j / (CARD_NY - 1));
545
+ }
546
+ for (let j = 0; j < CARD_NY - 1; j++)
547
+ for (let i = 0; i < CARD_NX - 1; i++) {
548
+ const a = j * CARD_NX + i, b = a + 1, c = a + CARD_NX, d = c + 1;
549
+ I.push(a, b, d, a, d, c);
550
+ }
551
+ // a flat card from the grid's four corners, after the grid (detail levels draw it at distance)
552
+ const c0 = 0, c1 = CARD_NX - 1, c2 = (CARD_NY - 1) * CARD_NX, c3 = CARD_NY * CARD_NX - 1;
553
+ I.push(c0, c1, c3, c0, c3, c2);
554
+ geo.setAttribute('position', new THREE.Float32BufferAttribute(P, 3));
555
+ geo.setAttribute('aCard', new THREE.Float32BufferAttribute(C, 2));
556
+ geo.setAttribute('normal', new THREE.Float32BufferAttribute(N, 3));
557
+ geo.setAttribute('uv', new THREE.Float32BufferAttribute(U, 2));
558
+ geo.setIndex(I);
559
+ geo.setDrawRange(0, CARD_IDX);
560
+ geo.setAttribute('aPos', new THREE.InstancedBufferAttribute(new Float32Array(count * 3), 3));
561
+ geo.setAttribute('aRoot', new THREE.InstancedBufferAttribute(new Float32Array(count * 3), 3));
562
+ geo.setAttribute('aQuat', new THREE.InstancedBufferAttribute(new Float32Array(count * 4), 4));
563
+ geo.setAttribute('aInfoA', new THREE.InstancedBufferAttribute(new Float32Array(count * 4), 4));
564
+ geo.setAttribute('aInfoB', new THREE.InstancedBufferAttribute(new Float32Array(count * 4), 4));
565
+ geo.instanceCount = count;
566
+ return geo;
567
+ }
568
+ const _q = new THREE.Quaternion(), _m = new THREE.Matrix4(), _x = new THREE.Vector3(), _y = new THREE.Vector3(), _z = new THREE.Vector3();
569
+ /** Writes leaf i: attachment, frame from (along = card +y, normal = card +z), size, hash, hf, ao, phase, flex, order, kind. */
570
+ export function setLeaf(geo, i, pos, along, normal, size, hash, hf, ao, phase, flex, order, kind) {
571
+ _y.copy(along).normalize();
572
+ _z.copy(normal).addScaledVector(_y, -normal.dot(_y)).normalize();
573
+ if (_z.lengthSq() < 1e-6)
574
+ _z.set(0, 0, 1).addScaledVector(_y, -_y.z).normalize();
575
+ _x.crossVectors(_y, _z).normalize();
576
+ _m.makeBasis(_x, _y, _z);
577
+ _q.setFromRotationMatrix(_m);
578
+ geo.attributes.aPos.setXYZ(i, pos.x, pos.y, pos.z);
579
+ geo.attributes.aQuat.setXYZW(i, _q.x, _q.y, _q.z, _q.w);
580
+ geo.attributes.aInfoA.setXYZW(i, size, hash, hf, ao);
581
+ geo.attributes.aInfoB.setXYZW(i, phase, flex, order, kind || 0);
582
+ }
583
+ /** The leaf's plant root (for growth). */
584
+ export function setLeafRoot(geo, i, root) { geo.attributes.aRoot.setXYZ(i, root.x, root.y, root.z); }
585
+ /** Bounds-only: the unit box, so Box3.expandByObject centres the gizmo on the plant (instanced attrs are not read). */
586
+ export function unitBounds(geo) {
587
+ geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0.5, 0), 0.9);
588
+ geo.boundingBox = new THREE.Box3(new THREE.Vector3(-0.5, 0, -0.5), new THREE.Vector3(0.5, 1, 0.5));
589
+ }
590
+ // ---------------------------------------------------------------------------- branching generator
591
+ /**
592
+ * A parametric branching model (after Weber & Penn): each level has a child count, length and
593
+ * radius ratios, a down angle from the parent, a phyllotactic rotation, curvature and gravity
594
+ * (droop). A branch is a polyline with tapering radii; the crown silhouette is a shape function of
595
+ * the attachment height. Deterministic for a seed. Lengths in unit space (height 1).
596
+ *
597
+ * params:
598
+ * levels number of branch levels after the trunk (0..3)
599
+ * trunk { length, radius, taper, curve, segments, splits? }
600
+ * level[i] { count, length, radius, downAngle, downVar, curve, curveVar, segments, start }
601
+ * shape(t) crown silhouette multiplier for first-level branch length by attachment t
602
+ * droop gravity on the branches (negative lifts them)
603
+ * twist extra random curvature
604
+ * leaves { perLength, size, sizeVar, spread, fromLevel, tuft }
605
+ * Returns { branches: [{ level, pts, radii, hf0, phase, flex }], leaves: [{ pos, along, normal, size, hf, ao, phase, flex, order }] }.
606
+ */
607
+ export function generatePlant(P, rnd) {
608
+ const branches = [], leaves = [];
609
+ const up = new THREE.Vector3(0, 1, 0);
610
+ const tmpA = new THREE.Vector3(), tmpB = new THREE.Vector3(), axis = new THREE.Vector3();
611
+ const lv = P.level || [];
612
+ const levels = Math.min(P.levels, lv.length);
613
+ const maxBranches = P.maxBranches || 2500;
614
+ function perpendicular(d, out) {
615
+ out.crossVectors(d, Math.abs(d.y) < 0.9 ? up : tmpB.set(1, 0, 0)).normalize();
616
+ return out;
617
+ }
618
+ function grow(level, start, dir, length, radius, hf0, phase, flex, parentFlex) {
619
+ if (branches.length > maxBranches)
620
+ return;
621
+ const L = level < 0 ? P.trunk : lv[level];
622
+ const segs = Math.max(2, L.segments || 5);
623
+ const pts = [start.clone()], radii = [radius];
624
+ const d = dir.clone().normalize(), p = start.clone();
625
+ const curve = (L.curve || 0) * DEG_V * (1 + (rnd() - 0.5) * (L.curveVar || 0));
626
+ perpendicular(d, axis);
627
+ const bendAxis = axis.clone().applyAxisAngle(d, rnd() * Math.PI * 2);
628
+ const droop = (P.droop || 0) * (level < 0 ? 0.15 : 0.5 + 0.5 * level / Math.max(levels, 1));
629
+ for (let i = 1; i <= segs; i++) {
630
+ d.applyAxisAngle(bendAxis, curve / segs);
631
+ // twist: random wander, more on thin branches
632
+ if (P.twist) {
633
+ perpendicular(d, axis).applyAxisAngle(d, rnd() * Math.PI * 2);
634
+ d.applyAxisAngle(axis, (rnd() - 0.5) * P.twist * 0.9 / segs * (1 + level));
635
+ }
636
+ // gravity and phototropism
637
+ d.y -= droop * 0.9 / segs * (i / segs);
638
+ d.y += (P.photo || 0) * 0.3 / segs;
639
+ d.normalize();
640
+ p.addScaledVector(d, length / segs);
641
+ pts.push(p.clone());
642
+ radii.push(radius * (1 - (L.taper === undefined ? 0.85 : L.taper) * i / segs));
643
+ }
644
+ const hfEnd = Math.min(1, Math.max(0, p.y));
645
+ branches.push({ level, pts, radii, hf0, phase, flex, tip: d.clone() });
646
+ const tipFlex = flex;
647
+ // forks: the trunk (or a limb) splits at its end into a few continuing limbs of nearly its size
648
+ if (L.splits && level + 1 < levels) {
649
+ const nS = L.splits, az0 = rnd() * Math.PI * 2;
650
+ for (let k = 0; k < nS; k++) {
651
+ const cd = d.clone();
652
+ perpendicular(cd, axis).applyAxisAngle(cd, az0 + k / nS * Math.PI * 2 + (rnd() - 0.5) * 0.6);
653
+ cd.applyAxisAngle(axis, (L.splitAngle || 28) * DEG_V * (0.7 + 0.6 * rnd())).normalize();
654
+ const rEnd = radii[radii.length - 1];
655
+ grow(level + 1, p.clone(), cd, length * (L.splitLength || 0.9) * (0.85 + 0.3 * rnd()), Math.max(rEnd * (L.splitRadius || 0.8), 0.0015), Math.min(1, Math.max(0, p.y)), rnd(), Math.min(1, tipFlex + 0.15), tipFlex);
656
+ }
657
+ }
658
+ // children
659
+ if (level + 1 < levels) {
660
+ const C = lv[level + 1];
661
+ let n = Math.round(C.count * (P.branchiness || 1) * (0.8 + 0.4 * rnd()));
662
+ if (level < 0 && P.whorls)
663
+ n = Math.round(C.count * (P.branchiness || 1));
664
+ const start = C.start === undefined ? 0.3 : C.start;
665
+ let az = rnd() * Math.PI * 2;
666
+ for (let k = 0; k < n; k++) {
667
+ let t;
668
+ if (P.whorls && level < 0) {
669
+ const w = Math.floor(k / P.whorls);
670
+ const nw = Math.ceil(n / P.whorls);
671
+ t = start + (1 - start) * (w + 0.5) / nw;
672
+ az = (k % P.whorls) / P.whorls * Math.PI * 2 + w * 0.5;
673
+ }
674
+ else {
675
+ t = start + (1 - start) * (k + rnd() * 0.8) / n;
676
+ az += 137.5 * DEG_V + (rnd() - 0.5) * (C.azVar || 0.6);
677
+ }
678
+ const seg = Math.min(segs - 1, Math.floor(t * segs)), f = t * segs - seg;
679
+ tmpA.lerpVectors(pts[seg], pts[seg + 1], f);
680
+ const tangent = tmpB.subVectors(pts[seg + 1], pts[seg]).normalize();
681
+ const shape = level < 0 && P.shape ? P.shape(t) : 1;
682
+ const childLen = length * C.length * shape * (0.8 + 0.4 * rnd());
683
+ if (childLen < 0.01)
684
+ continue;
685
+ const rAt = radii[seg] * (1 - f) + radii[seg + 1] * f;
686
+ const childR = Math.min(rAt * 0.9, rAt * (C.radius === undefined ? 0.7 : C.radius) * Math.pow(Math.max(shape, 0.2), 0.5));
687
+ const down = (C.downAngle + (rnd() - 0.5) * (C.downVar || 0)) * DEG_V;
688
+ const cd = tangent.clone();
689
+ perpendicular(cd, axis).applyAxisAngle(cd, az);
690
+ cd.applyAxisAngle(axis, down).normalize();
691
+ const cflex = Math.min(1, tipFlex + 0.25 + 0.15 * level) * (0.8 + 0.4 * rnd());
692
+ grow(level + 1, tmpA.clone(), cd, childLen, Math.max(childR, 0.0015), Math.min(1, Math.max(0, tmpA.y)), rnd(), cflex, tipFlex);
693
+ }
694
+ }
695
+ // leaves
696
+ const LF = P.leaves;
697
+ if (LF && level >= (LF.fromLevel === undefined ? levels - 1 : LF.fromLevel)) {
698
+ const nL = LF.perBranch ? LF.perBranch : Math.max(1, Math.round(length * LF.perLength * (LF.density || 1)));
699
+ const t0 = LF.start === undefined ? 0.2 : LF.start;
700
+ for (let k = 0; k < nL; k++) {
701
+ const t = t0 + (1 - t0) * (k + rnd()) / nL;
702
+ const seg = Math.min(segs - 1, Math.floor(t * segs)), f = t * segs - seg;
703
+ const pos = new THREE.Vector3().lerpVectors(pts[seg], pts[seg + 1], f);
704
+ const tangent = new THREE.Vector3().subVectors(pts[seg + 1], pts[seg]).normalize();
705
+ const tuft = LF.tuft || 1;
706
+ for (let j = 0; j < tuft; j++) {
707
+ // along: away from the twig, tilted toward the tip and upward (phototropism), scattered
708
+ const along = new THREE.Vector3().copy(perpendicular(tangent, axis)).applyAxisAngle(tangent, rnd() * Math.PI * 2);
709
+ along.multiplyScalar(LF.spread === undefined ? 0.8 : LF.spread).addScaledVector(tangent, 0.45 + rnd() * 0.5).add(up.clone().multiplyScalar(LF.lift === undefined ? 0.25 : LF.lift)).normalize();
710
+ // normal: mostly up/outward with scatter, so leaves face the light
711
+ const normal = new THREE.Vector3(rnd() - 0.5, 1.2 * (LF.faceUp === undefined ? 1 : LF.faceUp), rnd() - 0.5).addScaledVector(along, 0.3).normalize();
712
+ if (rnd() < 0.3)
713
+ normal.multiplyScalar(-1);
714
+ const size = LF.size * (1 + (rnd() - 0.5) * (LF.sizeVar || 0.4));
715
+ const off = (rnd() - 0.5) * 0.3 * size;
716
+ leaves.push({ pos: pos.clone().addScaledVector(along, off).addScaledVector(tangent, (rnd() - 0.5) * 0.02), along, normal, size, hf: Math.min(1, Math.max(0, pos.y)), ao: 0, phase: phase + rnd() * 0.2, flex: tipFlex * (0.8 + 0.4 * t), order: 0 });
717
+ }
718
+ }
719
+ }
720
+ }
721
+ const T = P.trunk;
722
+ grow(-1, new THREE.Vector3(0, 0, 0), new THREE.Vector3((rnd() - 0.5) * (T.lean || 0), 1, (rnd() - 0.5) * (T.lean || 0)).normalize(), T.length, T.radius, 0, rnd(), T.flex === undefined ? 0.1 : T.flex, 0);
723
+ // canopy occlusion: how many leaves sit around each leaf (a 3-D count grid) plus the depth below
724
+ // the canopy top — the outer, upper leaves are lit, the inner and lower ones in shade
725
+ if (leaves.length) {
726
+ const c = new THREE.Vector3();
727
+ let yMin = 1e9, yMax = -1e9;
728
+ for (const l of leaves) {
729
+ c.add(l.pos);
730
+ yMin = Math.min(yMin, l.pos.y);
731
+ yMax = Math.max(yMax, l.pos.y);
732
+ }
733
+ c.divideScalar(leaves.length);
734
+ let rMax = 1e-6;
735
+ for (const l of leaves)
736
+ rMax = Math.max(rMax, l.pos.distanceTo(c));
737
+ const cell = rMax / 7, grid = new Map();
738
+ const key = (p) => (Math.floor(p.x / cell) * 73856093) ^ (Math.floor(p.y / cell) * 19349663) ^ (Math.floor(p.z / cell) * 83492791);
739
+ for (const l of leaves) {
740
+ const k = key(l.pos);
741
+ grid.set(k, (grid.get(k) || 0) + 1);
742
+ }
743
+ let cMax = 1;
744
+ const counts = leaves.map((l) => { let n = 0; for (let dx = -1; dx <= 1; dx++)
745
+ for (let dy = -1; dy <= 1; dy++)
746
+ for (let dz = -1; dz <= 1; dz++) {
747
+ const k = (Math.floor(l.pos.x / cell + dx) * 73856093) ^ (Math.floor(l.pos.y / cell + dy) * 19349663) ^ (Math.floor(l.pos.z / cell + dz) * 83492791);
748
+ n += grid.get(k) || 0;
749
+ } if (n > cMax)
750
+ cMax = n; return n; });
751
+ leaves.forEach((l, i) => {
752
+ const dens = Math.min(1, counts[i] / (cMax * 0.6));
753
+ const up = (l.pos.y - yMin) / Math.max(yMax - yMin, 1e-6);
754
+ l.ao = Math.min(1, Math.max(0, 1.05 - 0.8 * dens * dens + 0.25 * up - 0.2));
755
+ l.order = Math.min(0.999, (1 - l.ao) * 0.6 + rnd() * 0.4);
756
+ });
757
+ }
758
+ return { branches, leaves };
759
+ }
760
+ /**
761
+ * Generalized cylinders for every branch, merged in one geometry: rings of `sides` vertices
762
+ * (fewer on thin branches) along the polyline, parallel-transport frames, a pointed cap.
763
+ * Attributes: position, normal, aRing (angle 0..1, length along), aWind (hf, phase, flex).
764
+ */
765
+ export function branchGeometry(branches, opts) {
766
+ const o = Object.assign({ sides: [10, 7, 5, 4, 3], minRadius: 0.0008, flare: null }, opts);
767
+ const pos = [], nrm = [], ring = [], wind = [], root = [], centre = [], grow = [], idx = [];
768
+ const n = new THREE.Vector3(), b = new THREE.Vector3(), t = new THREE.Vector3(), prevT = new THREE.Vector3();
769
+ const zero = new THREE.Vector3();
770
+ // sorted by level (trunk first) so a prefix of the index buffer is "trunk … level k" (detail levels)
771
+ const sorted = branches.slice().sort((a, b) => a.level - b.level);
772
+ const levelEnds = [];
773
+ for (const br of sorted) {
774
+ const sides = o.sides[Math.min(o.sides.length - 1, br.level + 1)];
775
+ const base = pos.length / 3;
776
+ let along = 0;
777
+ // initial frame
778
+ t.subVectors(br.pts[1], br.pts[0]).normalize();
779
+ n.crossVectors(t, Math.abs(t.y) < 0.9 ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0)).normalize();
780
+ b.crossVectors(t, n);
781
+ for (let i = 0; i < br.pts.length; i++) {
782
+ if (i > 0) {
783
+ prevT.copy(t);
784
+ if (i < br.pts.length - 1)
785
+ t.subVectors(br.pts[i + 1], br.pts[i - 1]).normalize();
786
+ else
787
+ t.subVectors(br.pts[i], br.pts[i - 1]).normalize();
788
+ // parallel transport: rotate the frame by the tangent change
789
+ const ax = new THREE.Vector3().crossVectors(prevT, t);
790
+ if (ax.lengthSq() > 1e-10) {
791
+ const ang = Math.acos(Math.min(1, Math.max(-1, prevT.dot(t))));
792
+ ax.normalize();
793
+ n.applyAxisAngle(ax, ang);
794
+ b.applyAxisAngle(ax, ang);
795
+ }
796
+ along += br.pts[i].distanceTo(br.pts[i - 1]);
797
+ }
798
+ const r = Math.max(br.radii[i], o.minRadius);
799
+ const hf = Math.min(1, Math.max(0, br.pts[i].y));
800
+ const flex = br.flex * Math.min(1, 0.3 + 0.7 * (i / (br.pts.length - 1)));
801
+ const rt = br.root || zero, gr = br.grow ? br.grow[i] : 0;
802
+ for (let s = 0; s <= sides; s++) {
803
+ const a = s / sides * Math.PI * 2, ca = Math.cos(a), sa = Math.sin(a);
804
+ // flare(branch, ringIndex, angle) → radius multiplier (root buttresses, a flared base)
805
+ const rr = o.flare ? r * o.flare(br, i, a) : r;
806
+ pos.push(br.pts[i].x + (n.x * ca + b.x * sa) * rr, br.pts[i].y + (n.y * ca + b.y * sa) * rr, br.pts[i].z + (n.z * ca + b.z * sa) * rr);
807
+ nrm.push(n.x * ca + b.x * sa, n.y * ca + b.y * sa, n.z * ca + b.z * sa);
808
+ ring.push(s / sides, along, br.radii[0]); // the base radius: a stable arc-length coordinate along the whole branch
809
+ wind.push(hf, br.phase, flex);
810
+ root.push(rt.x, rt.y, rt.z);
811
+ centre.push(br.pts[i].x, br.pts[i].y, br.pts[i].z);
812
+ grow.push(gr);
813
+ }
814
+ }
815
+ const rows = br.pts.length, cols = sides + 1;
816
+ for (let i = 0; i < rows - 1; i++)
817
+ for (let s = 0; s < sides; s++) {
818
+ const a = base + i * cols + s, c = a + cols;
819
+ idx.push(a, a + 1, c, a + 1, c + 1, c);
820
+ }
821
+ // cap
822
+ const tipI = pos.length / 3;
823
+ const tipP = br.pts[rows - 1].clone().addScaledVector(t, Math.max(br.radii[rows - 1], o.minRadius) * 1.5);
824
+ pos.push(tipP.x, tipP.y, tipP.z);
825
+ nrm.push(t.x, t.y, t.z);
826
+ ring.push(0.5, along, 0.001);
827
+ wind.push(Math.min(1, Math.max(0, tipP.y)), br.phase, br.flex);
828
+ {
829
+ const rt = br.root || zero;
830
+ root.push(rt.x, rt.y, rt.z);
831
+ centre.push(tipP.x, tipP.y, tipP.z);
832
+ grow.push(br.grow ? br.grow[rows - 1] : 0);
833
+ }
834
+ const last = base + (rows - 1) * cols;
835
+ for (let s = 0; s < sides; s++)
836
+ idx.push(last + s, last + s + 1, tipI);
837
+ levelEnds[br.level + 1] = idx.length; // the index count once this level is complete
838
+ }
839
+ const geo = new THREE.BufferGeometry();
840
+ geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
841
+ geo.setAttribute('normal', new THREE.Float32BufferAttribute(nrm, 3));
842
+ geo.setAttribute('aRing', new THREE.Float32BufferAttribute(ring, 3));
843
+ geo.setAttribute('aWind', new THREE.Float32BufferAttribute(wind, 3));
844
+ geo.setAttribute('aRoot', new THREE.Float32BufferAttribute(root, 3));
845
+ geo.setAttribute('aCentre', new THREE.Float32BufferAttribute(centre, 3));
846
+ geo.setAttribute('aGrow', new THREE.Float32BufferAttribute(grow, 1));
847
+ geo.setIndex(idx);
848
+ // [end of trunk, end of level 0, …]: holes (a level with no branches) take the previous end
849
+ for (let i = 0; i < levelEnds.length; i++)
850
+ if (levelEnds[i] === undefined)
851
+ levelEnds[i] = i ? levelEnds[i - 1] : 0;
852
+ geo.userData.levelEnds = levelEnds;
853
+ return geo;
854
+ }
855
+ /** Fills a leaf geometry from generatePlant's leaves (optionally the first `count`). */
856
+ export function fillLeaves(geo, leaves, kind = undefined) {
857
+ const n = Math.min(geo.instanceCount, leaves.length);
858
+ for (let i = 0; i < n; i++) {
859
+ const l = leaves[i];
860
+ setLeaf(geo, i, l.pos, l.along, l.normal, l.size, (i * 0.618033) % 1, l.hf, l.ao, l.phase, l.flex, l.order, l.grow || kind || 0);
861
+ if (l.root)
862
+ setLeafRoot(geo, i, l.root);
863
+ }
864
+ geo.instanceCount = n;
865
+ for (const k of ['aPos', 'aRoot', 'aQuat', 'aInfoA', 'aInfoB'])
866
+ geo.attributes[k].needsUpdate = true;
867
+ return n;
868
+ }
869
+ // ---------------------------------------------------------------------------- fruit on the plant
870
+ /**
871
+ * Hanging fruit: instanced low-poly spheroids at a subset of the leaf positions, shaped per kind in
872
+ * the vertex shader (oblate / prolate / dimple / taper), skinned per kind in the fragment (two
873
+ * colours by ripeness, waxy specular, pores / streaks), swinging with the branch.
874
+ * aPos, aInfo = (size, hash, hf, order), aWind = (phase, flex).
875
+ */
876
+ export const FRUIT_KINDS = ['none', 'apple', 'orange', 'lemon', 'cherry', 'pear', 'plum', 'berry', 'rosehip'];
877
+ // shape: elongation (y stretch), dimple (top/bottom indent), taper (narrower top), size (m at scale 1 tree ~ relative), colours
878
+ export const FRUIT_PROFILES = {
879
+ apple: { elong: 0.92, dimple: 0.35, taper: 0.1, size: 1.0, unripe: '#7fae3c', ripe: '#c8302a', gloss: 0.5, streaks: 0.6, pores: 0 },
880
+ orange: { elong: 0.95, dimple: 0.1, taper: 0.0, size: 1.0, unripe: '#7fa83a', ripe: '#ef8a1d', gloss: 0.25, streaks: 0, pores: 1 },
881
+ lemon: { elong: 1.35, dimple: -0.3, taper: 0.05, size: 0.85, unripe: '#8fb53c', ripe: '#f2d82a', gloss: 0.3, streaks: 0, pores: 0.8 },
882
+ cherry: { elong: 0.9, dimple: 0.4, taper: 0.0, size: 0.35, unripe: '#d8c060', ripe: '#8a1324', gloss: 0.9, streaks: 0, pores: 0 },
883
+ pear: { elong: 1.3, dimple: 0.1, taper: 0.55, size: 1.05, unripe: '#9cbd4a', ripe: '#cfc24a', gloss: 0.2, streaks: 0.3, pores: 0.3 },
884
+ plum: { elong: 1.05, dimple: 0.2, taper: 0.05, size: 0.65, unripe: '#8aa34a', ripe: '#4a1d4f', gloss: 0.7, streaks: 0, pores: 0 },
885
+ berry: { elong: 0.95, dimple: 0.3, taper: 0.0, size: 0.25, unripe: '#9fcf7a', ripe: '#3a2f6e', gloss: 0.6, streaks: 0, pores: 0 },
886
+ rosehip: { elong: 1.25, dimple: -0.2, taper: 0.1, size: 0.3, unripe: '#9fbf4a', ripe: '#d5361e', gloss: 0.8, streaks: 0, pores: 0 },
887
+ };
888
+ export function fruitMaterial(u) {
889
+ const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.45, metalness: 0 });
890
+ const own = {
891
+ uFruitShape: { value: new THREE.Vector4(1, 0, 0, 1) }, // elong, dimple, taper, size
892
+ uFruitSkin: { value: new THREE.Vector4(0.5, 0, 0, 0) }, // gloss, streaks, pores, -
893
+ uFruitUnripe: { value: new THREE.Color('#7fae3c') }, uFruitRipe: { value: new THREE.Color('#c8302a') },
894
+ uFruitAmount: { value: 0.5 }, uRipeness: { value: 1 }, uFruitTint: { value: new THREE.Color('#ffffff') },
895
+ };
896
+ mat.userData.uniforms = own;
897
+ const PARS = `#include <common>\n${COMMON_VERT_PARS}
898
+ uniform vec4 uFruitShape; uniform float uFruitAmount;
899
+ attribute vec3 aSph; attribute vec3 aPos; attribute vec3 aRoot; attribute vec4 aInfo; attribute vec2 aWind;
900
+ varying vec3 vFruit; varying vec2 vFruitUv;
901
+ vec3 fruitShape(vec3 p, float h) {
902
+ // a unit sphere shaped per kind: stretched along y, tapered toward the top, dimpled at the poles
903
+ float y = p.y;
904
+ float taper = 1.0 - uFruitShape.z * smoothstep(-0.2, 1.0, y);
905
+ vec3 q = vec3(p.x * taper, y * uFruitShape.x, p.z * taper);
906
+ float pole = pow(max(abs(y), 0.0), 6.0);
907
+ q.y -= sign(y) * uFruitShape.y * pole * 0.35 * (y > 0.0 ? 1.0 : 0.6);
908
+ // slight individual lumpiness
909
+ q *= 1.0 + 0.03 * (vhash11(h * 9.0 + floor(p.x * 3.0 + p.z * 5.0 + 7.0)) - 0.5);
910
+ return q;
911
+ }`;
912
+ const PRE = `
913
+ float keep = step(aInfo.w, uFruitAmount);
914
+ float sz = aInfo.x * uFruitShape.w * keep * 0.5 * uGrowth;
915
+ vec3 q0 = fruitShape(aSph, aInfo.y);
916
+ vec3 ap = aRoot + (aPos - aRoot) * uGrowth;`;
917
+ const NRM = `
918
+ vec3 objectNormal = isoNrm(normal / vec3(1.0, uFruitShape.x, 1.0));
919
+ #ifdef USE_TANGENT
920
+ vec3 objectTangent = vec3(tangent.xyz);
921
+ #endif`;
922
+ const POS = `
923
+ // hangs below its attachment and swings with the branch
924
+ vec3 hang = vec3(0.0, -sz * uFruitShape.x - 0.004, 0.0);
925
+ vec3 transformed = ap + isoOff(hang + q0 * sz) + plantDisplace(ap, aInfo.z, aWind) * uGrowth;
926
+ vFruit = vec3(aInfo.y, aInfo.z, aSph.y); vFruitUv = vec2(atan(aSph.z, aSph.x), aSph.y);`;
927
+ const vert = (shader, depth = false) => {
928
+ injectUniforms(shader, u);
929
+ injectUniforms(shader, own);
930
+ shader.vertexShader = depth
931
+ ? shader.vertexShader.replace('#include <common>', PARS).replace('#include <begin_vertex>', PRE + POS)
932
+ : shader.vertexShader.replace('#include <common>', PARS).replace('#include <beginnormal_vertex>', PRE + NRM).replace('#include <begin_vertex>', POS);
933
+ };
934
+ mat.onBeforeCompile = (shader) => {
935
+ vert(shader);
936
+ shader.fragmentShader = shader.fragmentShader
937
+ .replace('#include <common>', `#include <common>\n${VEG_UNIFORMS_GLSL}\n${NOISE_GLSL_V}
938
+ uniform vec4 uFruitSkin; uniform vec3 uFruitUnripe, uFruitRipe, uFruitTint; uniform float uRipeness;
939
+ varying vec3 vFruit; varying vec2 vFruitUv;`)
940
+ .replace('#include <roughnessmap_fragment>', '#include <roughnessmap_fragment>\n roughnessFactor = vegRough;')
941
+ .replace('#include <map_fragment>', `
942
+ float vegRough = 0.5;
943
+ float h = vFruit.x;
944
+ // ripeness per fruit: the knob with a spread, so a tree ripens over a while, sun side first
945
+ float ripe = smoothstep(h * 0.6 - 0.1, h * 0.6 + 0.35, uRipeness * 1.2);
946
+ vec3 skin = mix(uFruitUnripe, uFruitRipe, ripe);
947
+ // blush streaks along the meridians, pores as fine speckle
948
+ float streak = fbm2v(vec2(vFruitUv.x * 2.5 + h * 10.0, vFruitUv.y * 3.0));
949
+ skin = mix(skin, uFruitRipe * 1.15, uFruitSkin.y * ripe * smoothstep(0.45, 0.7, streak) * 0.7);
950
+ skin = mix(skin, uFruitUnripe, uFruitSkin.y * (1.0 - ripe) * 0.3 * smoothstep(0.5, 0.7, streak));
951
+ float pores = vnoise2v(vec2(vFruitUv.x * 40.0, vFruitUv.y * 30.0) + h * 3.0);
952
+ skin *= 1.0 - uFruitSkin.z * 0.12 * pores;
953
+ diffuseColor.rgb *= skin * uFruitTint * (0.9 + 0.2 * vhash11(h * 4.0));
954
+ vegRough = mix(0.7, 0.2, uFruitSkin.x);`);
955
+ bakeOut(shader);
956
+ };
957
+ mat.customProgramCacheKey = () => 'veg-fruit';
958
+ const depth = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking });
959
+ depth.onBeforeCompile = (shader) => vert(shader, true);
960
+ depth.customProgramCacheKey = () => 'veg-fruit-depth';
961
+ return { material: mat, depth };
962
+ }
963
+ export function applyFruitConfig(mat, c) {
964
+ const o = mat.userData.uniforms;
965
+ if (c.fruit !== undefined) {
966
+ const p = FRUIT_PROFILES[c.fruit];
967
+ if (p) {
968
+ o.uFruitShape.value.set(p.elong, p.dimple, p.taper, p.size);
969
+ o.uFruitSkin.value.set(p.gloss, p.streaks, p.pores, 0);
970
+ o.uFruitUnripe.value.set(p.unripe);
971
+ o.uFruitRipe.value.set(p.ripe);
972
+ }
973
+ mat.visible = !!p;
974
+ }
975
+ if (c.fruitAmount !== undefined)
976
+ o.uFruitAmount.value = c.fruitAmount;
977
+ if (c.ripeness !== undefined)
978
+ o.uRipeness.value = c.ripeness;
979
+ if (c.fruitColor !== undefined)
980
+ setColor(o.uFruitTint, c.fruitColor);
981
+ }
982
+ export function fruitGeometry(count, detail) {
983
+ const sph = new THREE.SphereGeometry(1, detail || 12, detail ? Math.round(detail * 0.7) : 8);
984
+ const geo = new THREE.InstancedBufferGeometry();
985
+ // `position` is bounds only (the sphere fitted in the unit box); the shape reads aSph
986
+ const bp = new Float32Array(sph.attributes.position.array.length);
987
+ for (let i = 0; i < bp.length; i += 3) {
988
+ bp[i] = sph.attributes.position.array[i] * 0.5;
989
+ bp[i + 1] = sph.attributes.position.array[i + 1] * 0.5 + 0.5;
990
+ bp[i + 2] = sph.attributes.position.array[i + 2] * 0.5;
991
+ }
992
+ geo.setAttribute('position', new THREE.Float32BufferAttribute(bp, 3));
993
+ geo.setAttribute('aSph', sph.attributes.position);
994
+ geo.setAttribute('normal', sph.attributes.normal);
995
+ geo.setAttribute('uv', sph.attributes.uv);
996
+ geo.setIndex(sph.index);
997
+ geo.setAttribute('aPos', new THREE.InstancedBufferAttribute(new Float32Array(count * 3), 3));
998
+ geo.setAttribute('aRoot', new THREE.InstancedBufferAttribute(new Float32Array(count * 3), 3));
999
+ geo.setAttribute('aInfo', new THREE.InstancedBufferAttribute(new Float32Array(count * 4), 4));
1000
+ geo.setAttribute('aWind', new THREE.InstancedBufferAttribute(new Float32Array(count * 2), 2));
1001
+ geo.instanceCount = count;
1002
+ return geo;
1003
+ }
1004
+ /** Fruit at a seeded subset of the outer leaves; `size` in unit space (fruit radius ≈ size/2 × profile size). */
1005
+ export function fillFruit(geo, leaves, rnd, size) {
1006
+ const outer = leaves.filter((l) => l.ao > 0.45);
1007
+ const n = geo.instanceCount;
1008
+ for (let i = 0; i < n; i++) {
1009
+ const l = outer.length ? outer[Math.floor(rnd() * outer.length)] : leaves[Math.floor(rnd() * leaves.length)];
1010
+ geo.attributes.aPos.setXYZ(i, l.pos.x, l.pos.y, l.pos.z);
1011
+ geo.attributes.aInfo.setXYZW(i, size * (0.8 + 0.4 * rnd()), rnd(), l.hf, rnd());
1012
+ geo.attributes.aWind.setXY(i, l.phase, l.flex);
1013
+ if (l.root)
1014
+ geo.attributes.aRoot.setXYZ(i, l.root.x, l.root.y, l.root.z);
1015
+ }
1016
+ for (const k of ['aPos', 'aRoot', 'aInfo', 'aWind'])
1017
+ geo.attributes[k].needsUpdate = true;
1018
+ }
1019
+ // ---------------------------------------------------------------------------- falling leaves
1020
+ /**
1021
+ * Leaves letting go: a small instanced card layer. Each card cycles analytically — released from a
1022
+ * canopy leaf's position, it falls with a side-to-side glide and a tumble, drifts with the wind,
1023
+ * and fades at the ground; then starts again. `uLeafFall` says how many are
1024
+ * in the air; the season releases more.
1025
+ */
1026
+ export function fallingMaterial(u) {
1027
+ const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.7, metalness: 0, side: THREE.DoubleSide, alphaToCoverage: true });
1028
+ const own = { uLeafFall: { value: 0 }, uAutumnA: { value: new THREE.Color('#e2a21c') }, uAutumnB: { value: new THREE.Color('#b8441c') } };
1029
+ mat.userData.uniforms = own;
1030
+ const PARS = `#include <common>\n${COMMON_VERT_PARS}
1031
+ uniform float uLeafFall;
1032
+ attribute vec2 aCard; attribute vec3 aPos; attribute vec4 aQuat; attribute vec4 aInfoA; attribute vec4 aInfoB;
1033
+ varying vec2 vCard; varying vec4 vLeaf;`;
1034
+ const PRE = `
1035
+ float h = aInfoA.y;
1036
+ float period = 6.0 + 6.0 * vhash11(h * 3.1);
1037
+ float ph = fract(uTime / period + h);
1038
+ // in the air when its order is under the fall rate (the season adds to it); fades in and out
1039
+ float rate = uLeafFall + 0.6 * smoothstep(0.5, 1.0, uSeason);
1040
+ float on = step(aInfoB.z, rate) * smoothstep(0.0, 0.08, ph) * (1.0 - smoothstep(0.88, 1.0, ph));
1041
+ // glide: sideways pendulum with a tumble
1042
+ float tumble = uTime * 2.5 + h * 20.0;
1043
+ float ct = cos(tumble), st = sin(tumble);
1044
+ vec3 lp = vec3(aCard.x * ct, aCard.y, aCard.x * st);
1045
+ vec3 ln = vec3(-st, 0.0, ct);`;
1046
+ const NRM = `
1047
+ vec3 objectNormal = isoNrm(qrot(aQuat, ln));
1048
+ #ifdef USE_TANGENT
1049
+ vec3 objectTangent = vec3(tangent.xyz);
1050
+ #endif`;
1051
+ const POS = `
1052
+ float fallH = aPos.y + 0.02;
1053
+ float y = aPos.y - ph * fallH;
1054
+ vec2 drift = uWindDir * uWindStrength * ph * fallH * 2.5;
1055
+ vec2 glide = vec2(sin(ph * 14.0 + h * 9.0), cos(ph * 11.0 + h * 5.0)) * 0.04 * ph;
1056
+ vec3 p = vec3(aPos.x + drift.x + glide.x, y, aPos.z + drift.y + glide.y);
1057
+ float sz = aInfoA.x * uLeafSize * on;
1058
+ vec3 transformed = p + isoOff(qrot(aQuat, lp * sz));
1059
+ vCard = aCard; vLeaf = vec4(h, 1.0, 0.0, aInfoA.z);`;
1060
+ const vert = (shader, depth = false) => {
1061
+ injectUniforms(shader, u);
1062
+ injectUniforms(shader, own);
1063
+ shader.vertexShader = depth
1064
+ ? shader.vertexShader.replace('#include <common>', PARS).replace('#include <begin_vertex>', PRE + POS)
1065
+ : shader.vertexShader.replace('#include <common>', PARS).replace('#include <beginnormal_vertex>', PRE + NRM).replace('#include <begin_vertex>', POS);
1066
+ };
1067
+ const fragMask = `
1068
+ float lsd = leafSdf(vCard, uLeafType, vLeaf.x);
1069
+ float aa = fwidth(lsd) * 0.9;
1070
+ float cover = 1.0 - smoothstep(-aa, aa, lsd);
1071
+ if (cover < 0.02) discard;`;
1072
+ mat.onBeforeCompile = (shader) => {
1073
+ vert(shader);
1074
+ shader.fragmentShader = shader.fragmentShader
1075
+ .replace('#include <common>', `#include <common>\n${VEG_UNIFORMS_GLSL}\n${NOISE_GLSL_V}\n${LEAF_SDF_GLSL}\nuniform vec3 uAutumnA, uAutumnB; varying vec2 vCard; varying vec4 vLeaf;`)
1076
+ .replace('#include <map_fragment>', `${fragMask}
1077
+ float h = vLeaf.x;
1078
+ // a leaf that lets go has turned: mostly autumn colours, a few still green
1079
+ vec3 autumn = mix(uAutumnA, uAutumnB, vhash11(h * 5.0 + 3.0));
1080
+ vec3 base = mix(uColor * 0.9, autumn, smoothstep(0.1, 0.4, vhash11(h * 2.0) + uSeason * 0.5));
1081
+ diffuseColor.rgb *= base;
1082
+ diffuseColor.a *= cover * uOpacity;`);
1083
+ bakeOut(shader);
1084
+ };
1085
+ mat.customProgramCacheKey = () => 'veg-fall';
1086
+ const depth = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking, side: THREE.DoubleSide });
1087
+ depth.onBeforeCompile = (shader) => {
1088
+ vert(shader, true);
1089
+ shader.fragmentShader = shader.fragmentShader
1090
+ .replace('#include <common>', `#include <common>\n${VEG_UNIFORMS_GLSL}\n${LEAF_SDF_GLSL}\nvarying vec2 vCard; varying vec4 vLeaf;`)
1091
+ .replace('#include <map_fragment>', fragMask);
1092
+ };
1093
+ depth.customProgramCacheKey = () => 'veg-fall-depth';
1094
+ return { material: mat, depth };
1095
+ }
1096
+ /** A shadow-casting mesh from a geometry and a { material, depth } pair; clicks pass through. */
1097
+ export function plantMesh(geo, pair, name) {
1098
+ const mesh = new THREE.Mesh(geo, pair.material);
1099
+ mesh.customDepthMaterial = pair.depth;
1100
+ mesh.castShadow = true;
1101
+ mesh.receiveShadow = true;
1102
+ mesh.frustumCulled = false;
1103
+ mesh.raycast = function () { };
1104
+ mesh.name = name || '';
1105
+ return mesh;
1106
+ }
1107
+ /** The quality table every plant uses: a multiplier on counts. */
1108
+ export const QUALITY_V = { low: 0.5, medium: 1, high: 1.8 };
1109
+ // ---------------------------------------------------------------------------- blades (grass, stems)
1110
+ /**
1111
+ * Bent blades: one instanced strip (2 × SEG+1 vertices) per blade. Per instance: aPos (root), aInfoA =
1112
+ * (height, width, yaw, hash), aInfoB = (bend, phase, order, kind). In the vertex the blade curves
1113
+ * along its bend direction (quadratic, tip bends most), the wind field bends it further ∝ t²
1114
+ * (plantDisplace with hf = t) plus a travelling ripple. The fragment
1115
+ * shades a base-to-tip gradient with per-blade variation and translucency.
1116
+ */
1117
+ export const BLADE_SEG = 6;
1118
+ export function bladeGeometry(count) {
1119
+ const geo = new THREE.InstancedBufferGeometry();
1120
+ const P = [], C = [], N = [], I = [];
1121
+ for (let i = 0; i <= BLADE_SEG; i++) {
1122
+ const t = i / BLADE_SEG;
1123
+ // bounds-only positions (a thin box in the unit box); the blade is aCard = (side, t), three across so it folds
1124
+ P.push(-0.5, t, -0.5, 0, t, 0, 0.5, t, 0.5);
1125
+ C.push(-1, t, 0, t, 1, t);
1126
+ N.push(0, 0, 1, 0, 0, 1, 0, 0, 1);
1127
+ if (i < BLADE_SEG) {
1128
+ const a = i * 3;
1129
+ I.push(a, a + 1, a + 3, a + 1, a + 4, a + 3, a + 1, a + 2, a + 4, a + 2, a + 5, a + 4);
1130
+ }
1131
+ }
1132
+ geo.setAttribute('position', new THREE.Float32BufferAttribute(P, 3));
1133
+ geo.setAttribute('aCard', new THREE.Float32BufferAttribute(C, 2));
1134
+ geo.setAttribute('normal', new THREE.Float32BufferAttribute(N, 3));
1135
+ geo.setIndex(I);
1136
+ geo.setAttribute('aPos', new THREE.InstancedBufferAttribute(new Float32Array(count * 3), 3));
1137
+ geo.setAttribute('aInfoA', new THREE.InstancedBufferAttribute(new Float32Array(count * 4), 4));
1138
+ geo.setAttribute('aInfoB', new THREE.InstancedBufferAttribute(new Float32Array(count * 4), 4));
1139
+ geo.instanceCount = count;
1140
+ unitBounds(geo);
1141
+ return geo;
1142
+ }
1143
+ export function setBlade(geo, i, pos, height, width, yaw, hash, bend, phase, order, kind) {
1144
+ geo.attributes.aPos.setXYZ(i, pos.x, pos.y, pos.z);
1145
+ geo.attributes.aInfoA.setXYZW(i, height, width, yaw, hash);
1146
+ geo.attributes.aInfoB.setXYZW(i, bend, phase, order, kind || 0);
1147
+ }
1148
+ export function bladeMaterial(u, opts) {
1149
+ const o = Object.assign({ roughness: 0.6, translucency: 0.5, tipColor: null, taper: 1 }, opts);
1150
+ const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: o.roughness, metalness: 0, side: THREE.DoubleSide, envMapIntensity: 0.5 });
1151
+ const own = { uTranslucency: { value: o.translucency }, uTaper: { value: o.taper }, uTipColor: { value: new THREE.Color(o.tipColor || '#9ccf5a') }, uTipMix: { value: o.tipColor ? 1 : 0 } };
1152
+ mat.userData.uniforms = own;
1153
+ const PARS = `#include <common>\n${COMMON_VERT_PARS}
1154
+ uniform float uTaper;
1155
+ attribute vec2 aCard; attribute vec3 aPos; attribute vec4 aInfoA; attribute vec4 aInfoB;
1156
+ varying vec2 vCard; varying vec4 vBlade;`;
1157
+ const PRE = `
1158
+ float t = aCard.y;
1159
+ float keep = step(aInfoB.z, uFoliage);
1160
+ float H = aInfoA.x * uGrowth * keep, Wd = aInfoA.y * uLeafSize * keep;
1161
+ float cy = cos(aInfoA.z), sy = sin(aInfoA.z);
1162
+ vec3 side = vec3(cy, 0.0, sy), fwd = vec3(-sy, 0.0, cy);
1163
+ // the blade's own curve (quadratic along fwd), width tapering to the tip
1164
+ float w = Wd * mix(1.0, 1.0 - 0.9 * t, uTaper) * (1.0 - 0.15 * t * t);
1165
+ // the blade folds along its midrib (a gutter), so each half catches the light differently
1166
+ float fold = 0.35 * (1.0 - aCard.x * aCard.x) * w;
1167
+ // the blade stands and curves over only near its tip (t³), so the sward reads upright, not combed flat
1168
+ float tc = t * t * t;
1169
+ // sideways offsets are heights laid over: they must be in the height's metric, not the footprint's
1170
+ // (a 6 m × 0.6 m box would otherwise turn a 30° lean into a 2 m sweep)
1171
+ vec3 hk = vec3(uScale.y / uScale.x, 1.0, uScale.y / uScale.z);
1172
+ vec3 p = aPos + vec3(0.0, H * t, 0.0) + fwd * aInfoB.x * H * tc * hk + isoOff(side * aCard.x * 0.5 * w - fwd * fold);
1173
+ // wind: the field at the root, applied ∝ t² (the base is rooted), with a travelling ripple
1174
+ vec3 d = plantDisplace(aPos + vec3(0.0, H, 0.0), 1.0, aInfoB.yy) * tc * 0.7 * hk;
1175
+ float rip = sin(uTime * 2.2 + dot(aPos.xz, uWindDir) * 14.0 * uScale.x * 0.05 + aInfoB.y * 3.0) * uWindStrength * 0.08 * H * t * t;
1176
+ p += d + vec3(uWindDir.x, 0.0, uWindDir.y) * rip * hk;
1177
+ // a bent blade loses height
1178
+ p.y -= length((d.xz + fwd.xz * aInfoB.x * H * tc * hk.xz) / hk.xz) * 0.5 * tc;
1179
+ vec3 nrm = normalize(cross(side, normalize(vec3(fwd.x * aInfoB.x * 3.0 * t * t * H + d.x, H, fwd.z * aInfoB.x * 3.0 * t * t * H + d.z))));
1180
+ nrm = normalize(nrm + side * aCard.x * 0.7);`;
1181
+ const NRM = `
1182
+ vec3 objectNormal = isoNrm(nrm);
1183
+ #ifdef USE_TANGENT
1184
+ vec3 objectTangent = vec3(tangent.xyz);
1185
+ #endif`;
1186
+ const POS = `
1187
+ vec3 transformed = p;
1188
+ vCard = aCard; vBlade = vec4(aInfoA.w, t, aInfoB.w, aPos.x + aPos.z * 1.7);`;
1189
+ const vert = (shader, depth = false) => {
1190
+ injectUniforms(shader, u);
1191
+ injectUniforms(shader, own);
1192
+ shader.vertexShader = depth
1193
+ ? shader.vertexShader.replace('#include <common>', PARS).replace('#include <begin_vertex>', PRE + POS)
1194
+ : shader.vertexShader.replace('#include <common>', PARS).replace('#include <beginnormal_vertex>', PRE + NRM).replace('#include <begin_vertex>', POS);
1195
+ };
1196
+ mat.onBeforeCompile = (shader) => {
1197
+ vert(shader);
1198
+ shader.fragmentShader = shader.fragmentShader
1199
+ .replace('#include <common>', `#include <common>\n${VEG_UNIFORMS_GLSL}\n${NOISE_GLSL_V}
1200
+ uniform float uTranslucency, uTipMix; uniform vec3 uTipColor;
1201
+ varying vec2 vCard; varying vec4 vBlade;`)
1202
+ .replace('#include <map_fragment>', `
1203
+ float h = vBlade.x, t = vBlade.y;
1204
+ // base in shade (the sward's own occlusion), tip in light; per-blade scatter; patches of yellower and
1205
+ // bluer grass across the field (vBlade.w carries the root's x+z); a few dead straws
1206
+ vec3 base = mix(uShadeColor, uColor, pow(smoothstep(0.0, 0.85, t), 1.4) * (0.75 + 0.45 * vhash11(h * 5.0)));
1207
+ base = mix(base, base * vec3(1.05, 1.0, 0.85), vhash11(h * 3.0 + 1.0) * 0.5);
1208
+ float ptch = vnoise2v(vec2(vBlade.w * 3.0, vBlade.w * 7.0 + h * 0.01));
1209
+ base = mix(base * vec3(0.9, 0.98, 1.08), base * vec3(1.12, 1.05, 0.78), ptch);
1210
+ float dead = step(0.96, vhash11(h * 11.0 + 2.0));
1211
+ base = mix(base, vec3(0.62, 0.52, 0.28) * (0.8 + 0.4 * t), dead);
1212
+ base = mix(base, uTipColor, uTipMix * smoothstep(0.6, 1.0, t));
1213
+ vec3 dry = vec3(0.78, 0.66, 0.3) * (0.8 + 0.4 * vhash11(h * 7.0));
1214
+ base = mix(base, dry, smoothstep(h * 0.6, h * 0.6 + 0.4, uSeason) * smoothstep(0.1, 0.9, t + uSeason * 0.5));
1215
+ // midrib
1216
+ base *= 1.0 + 0.1 * (1.0 - smoothstep(0.0, 0.25, abs(vCard.x)));
1217
+ diffuseColor.rgb *= base;
1218
+ diffuseColor.a *= uOpacity;`)
1219
+ .replace('#include <lights_fragment_end>', `#include <lights_fragment_end>
1220
+ {
1221
+ float back = max(dot(-normal, uSunDirView), 0.0);
1222
+ reflectedLight.indirectDiffuse += diffuseColor.rgb * back * uTranslucency * 0.8;
1223
+ }`);
1224
+ };
1225
+ mat.customProgramCacheKey = () => 'veg-blade';
1226
+ const depth = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking, side: THREE.DoubleSide });
1227
+ depth.onBeforeCompile = (shader) => vert(shader, true);
1228
+ depth.customProgramCacheKey = () => 'veg-blade-depth';
1229
+ return { material: mat, depth };
1230
+ }
1231
+ /** Seeded positions over the unit footprint with clumping: jittered grid, kept where a clump noise says so. */
1232
+ export function clumpedPositions(count, rnd, clumping, margin) {
1233
+ const out = [];
1234
+ const m = margin || 0;
1235
+ const side = Math.ceil(Math.sqrt(count * 1.6));
1236
+ const cells = [];
1237
+ for (let j = 0; j < side; j++)
1238
+ for (let i = 0; i < side; i++)
1239
+ cells.push([i, j]);
1240
+ // shuffle cells, then take the ones the clump field admits
1241
+ for (let i = cells.length - 1; i > 0; i--) {
1242
+ const j = Math.floor(rnd() * (i + 1));
1243
+ const t = cells[i];
1244
+ cells[i] = cells[j];
1245
+ cells[j] = t;
1246
+ }
1247
+ const f1 = 2 + rnd() * 2, f2 = 5 + rnd() * 3, o1 = rnd() * 10, o2 = rnd() * 10;
1248
+ for (const [i, j] of cells) {
1249
+ if (out.length >= count)
1250
+ break;
1251
+ const x = -0.5 + m + (1 - 2 * m) * (i + rnd()) / side, z = -0.5 + m + (1 - 2 * m) * (j + rnd()) / side;
1252
+ const nz = 0.5 + 0.5 * Math.sin(x * f1 * 6.28 + o1) * Math.cos(z * f1 * 6.28 + o2) + 0.25 * Math.sin((x + z) * f2 * 6.28 + o1);
1253
+ if (rnd() < 1 - clumping + clumping * nz)
1254
+ out.push(new THREE.Vector3(x, 0, z));
1255
+ }
1256
+ while (out.length < count)
1257
+ out.push(new THREE.Vector3(-0.5 + m + (1 - 2 * m) * rnd(), 0, -0.5 + m + (1 - 2 * m) * rnd()));
1258
+ return out;
1259
+ }
1260
+ // ---------------------------------------------------------------------------- flower heads on woody plants
1261
+ /**
1262
+ * Flower heads at branch tips (blossom on trees, flowers on shrubs): each head is a fan of petal cards
1263
+ * around the tip (a `head` of several florets scattered in a small dome for panicles), coloured by its
1264
+ * own uniforms. `uFoliage` of the head material is the flowers knob (0..1 keeps that share of heads).
1265
+ * spec: { petals, size, color, head?, type?, hang? }. Returns { geo, pair, U } (U.uFoliage / U.uColor).
1266
+ */
1267
+ export function flowerHeads(u, tips, rnd, spec, maxHeads, matOpts = undefined) {
1268
+ const petals = spec.petals || 5, head = spec.head || 1, type = spec.type || 'petal';
1269
+ const U = Object.assign({}, u, { uLeafType: { value: LEAF_TYPES.indexOf(type) }, uColor: { value: new THREE.Color(spec.color) }, uShadeColor: { value: new THREE.Color(spec.color).multiplyScalar(0.6) }, uFoliage: { value: 1 }, uSeason: { value: 0 }, uLeafSize: { value: 1 } });
1270
+ const nHeads = Math.min(tips.length, maxHeads);
1271
+ const geo = leafGeometry(Math.max(1, nHeads * petals * head));
1272
+ const up = new THREE.Vector3(0, 1, 0), along = new THREE.Vector3(), nrm = new THREE.Vector3(), perp = new THREE.Vector3(), pos = new THREE.Vector3();
1273
+ let fi = 0;
1274
+ for (let h = 0; h < nHeads; h++) {
1275
+ const b = tips[Math.floor(rnd() * tips.length)];
1276
+ const tipP = b.pts[b.pts.length - 1], t = spec.hang ? new THREE.Vector3(0, -1, 0) : (b.tip || new THREE.Vector3().subVectors(tipP, b.pts[Math.max(0, b.pts.length - 2)]).normalize());
1277
+ const order = h / nHeads;
1278
+ for (let k = 0; k < head; k++) {
1279
+ perp.crossVectors(t, Math.abs(t.y) < 0.9 ? up : new THREE.Vector3(1, 0, 0)).normalize().applyAxisAngle(t, rnd() * Math.PI * 2);
1280
+ pos.copy(tipP).addScaledVector(perp, head > 1 ? rnd() * spec.size * 2.5 : 0).addScaledVector(t, head > 1 ? rnd() * spec.size * 2 : spec.size * 0.3);
1281
+ for (let p = 0; p < petals; p++) {
1282
+ const a = (p + rnd() * 0.3) / petals * Math.PI * 2;
1283
+ along.copy(perp).applyAxisAngle(t, a).multiplyScalar(1 - (spec.cup || 0.3)).addScaledVector(t, spec.cup || 0.3).normalize();
1284
+ nrm.copy(t).addScaledVector(along, -0.3).normalize();
1285
+ setLeaf(geo, fi++, pos, along, nrm, spec.size * (0.8 + 0.4 * rnd()), rnd(), Math.min(1, pos.y), 0.9, b.phase, b.flex, order, 0);
1286
+ if (b.root)
1287
+ setLeafRoot(geo, fi - 1, b.root);
1288
+ }
1289
+ }
1290
+ }
1291
+ geo.instanceCount = fi;
1292
+ for (const k of ['aPos', 'aRoot', 'aQuat', 'aInfoA', 'aInfoB'])
1293
+ geo.attributes[k].needsUpdate = true;
1294
+ unitBounds(geo);
1295
+ const pair = leafMaterial(U, Object.assign({ translucency: 0.7, veins: 0, roughness: 0.9, underside: 0.05, curl: 0.5 }, matOpts));
1296
+ pair.material.envMapIntensity = 0.2;
1297
+ return { geo, pair, U };
1298
+ }
1299
+ // ---------------------------------------------------------------------------- detail levels (CPU) + impostor
1300
+ const _lsm = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
1301
+ /** The plant's screen size s from a drawn mesh's matrixWorld — the same formula as vegLodS (GLSL). */
1302
+ export function lodScreenSize(m, u) {
1303
+ if (u.uBake.value > 0.5)
1304
+ return 1e6;
1305
+ const e = m.elements;
1306
+ const cx = e[4] * 0.5 + e[12], cy = e[5] * 0.5 + e[13], cz = e[6] * 0.5 + e[14];
1307
+ const R = 0.5 * Math.sqrt(e[0] * e[0] + e[1] * e[1] + e[2] * e[2] + e[4] * e[4] + e[5] * e[5] + e[6] * e[6] + e[8] * e[8] + e[9] * e[9] + e[10] * e[10]);
1308
+ const v = u.uViewPos.value;
1309
+ const d = u.uLodOrtho.value > 0.5 ? 1 : Math.max(Math.hypot(cx - v.x, cy - v.y, cz - v.z), 1e-3);
1310
+ return R * u.uLodK.value / d * u.uLodBias.value;
1311
+ }
1312
+ /** The share of the leaves drawn at screen size s (vegLodFrac). */
1313
+ export function lodFrac(s) { return LOD.MIN + (1 - LOD.MIN) * _lsm(LOD.IMP, LOD.FULL, s); }
1314
+ /** The lod knob: auto (by distance), full (never reduce — a hero plant), impostor (always the far card). */
1315
+ export const LOD_MODES = ['auto', 'full', 'impostor'];
1316
+ export function applyLodMode(u, mode) { u.uLodBias.value = mode === 'full' ? 1e6 : mode === 'impostor' ? 1e-6 : 1; }
1317
+ /** Leaves in a random order (own stream, so the plant's other random draws are unchanged): drawing the first K is an even thinning. */
1318
+ export function lodOrder(list, seed) {
1319
+ const r = mulberry32((seed || 1) * 7919 + 13), a = list.slice();
1320
+ for (let i = a.length - 1; i > 0; i--) {
1321
+ const j = Math.floor(r() * (i + 1));
1322
+ const t = a[i];
1323
+ a[i] = a[j];
1324
+ a[j] = t;
1325
+ }
1326
+ return a;
1327
+ }
1328
+ /**
1329
+ * The per-draw decision for one part. Runs in onBeforeRender / onBeforeShadow of the part AND of every
1330
+ * mirror of it (layouts copy the callbacks), so it reads only the drawn object's matrixWorld and writes
1331
+ * only draw state of the shared geometry (instanceCount, drawRange), which the renderer reads right after.
1332
+ */
1333
+ function lodDrawFn(st, role, geo) {
1334
+ const full = geo.instanceCount || 0;
1335
+ const idxAll = geo.index ? geo.index.count : Infinity;
1336
+ const ends = geo.userData.levelEnds;
1337
+ return (obj, shadow) => {
1338
+ const s = lodScreenSize(obj.matrixWorld, st.u);
1339
+ if (s < LOD.IMP * 1.6)
1340
+ st.wanted = true;
1341
+ const dr = geo.drawRange;
1342
+ dr.start = 0;
1343
+ if (role === 'impostor') {
1344
+ dr.count = st.ready && s < LOD.IMP * (shadow ? 1 : LOD.FADE_IN) ? Infinity : 0;
1345
+ return;
1346
+ }
1347
+ if (st.ready && s < LOD.IMP * (shadow ? 1 : LOD.FADE_OUT)) {
1348
+ dr.count = 0;
1349
+ return;
1350
+ }
1351
+ if (role === 'leaves') {
1352
+ geo.instanceCount = Math.min(full, Math.ceil(full * lodFrac(s) * (shadow ? LOD.SHADOW : 1)));
1353
+ const flat = shadow || s < LOD.FLAT;
1354
+ dr.start = flat ? CARD_IDX : 0;
1355
+ dr.count = flat ? FLAT_IDX : CARD_IDX;
1356
+ }
1357
+ else if (role === 'bark' && ends && ends.length > 1) {
1358
+ dr.count = shadow ? ends[1] : s < LOD.FLAT ? ends[ends.length - 2] : idxAll;
1359
+ }
1360
+ else
1361
+ dr.count = Infinity;
1362
+ };
1363
+ }
1364
+ const LOD_SPHERE = new THREE.Sphere(new THREE.Vector3(0, 0.5, 0), 1.0); // the unit box with room for the wind
1365
+ const LOD_CULLED = new THREE.Sphere(new THREE.Vector3(), -1e9); // intersects no frustum
1366
+ /**
1367
+ * Installs the detail decision on a part and on every copy of it. A part whose tier needs nothing from it
1368
+ * is frustum-culled through a per-object bounding sphere (Frustum.intersectsObject reads
1369
+ * object.boundingSphere when the object has one), so the renderer never sets it up — an emptied draw
1370
+ * range still costs a program / uniform setup per draw. Inside the bands, onBeforeRender / onBeforeShadow
1371
+ * trim the shared geometry's draw range and instance count. Layout mirrors call userData.mirrorSetup.
1372
+ */
1373
+ function lodAttach(mesh, fn, st, role) {
1374
+ const install = (obj) => {
1375
+ obj.onBeforeRender = function () { fn(this, false); };
1376
+ obj.onBeforeShadow = function (renderer, object) { fn(object, true); };
1377
+ obj.frustumCulled = true;
1378
+ Object.defineProperty(obj, 'boundingSphere', {
1379
+ configurable: true,
1380
+ get() {
1381
+ if (!st.ready)
1382
+ return LOD_SPHERE;
1383
+ const s = lodScreenSize(this.matrixWorld, st.u);
1384
+ if (s < LOD.IMP * 1.6)
1385
+ st.wanted = true;
1386
+ const off = role === 'impostor' ? s >= LOD.IMP * LOD.FADE_IN : s < LOD.IMP * LOD.FADE_OUT;
1387
+ return off ? LOD_CULLED : LOD_SPHERE;
1388
+ },
1389
+ });
1390
+ };
1391
+ mesh.geometry.boundingSphere = LOD_SPHERE.clone();
1392
+ mesh.userData.mirrorSetup = install;
1393
+ install(mesh);
1394
+ }
1395
+ /**
1396
+ * The impostor card: one quad billboarded toward the camera in the vertex shader (toward the light in the
1397
+ * shadow pass — the depth material runs the same code with the shadow camera), textured from the 8×8
1398
+ * hemi-octahedral atlas: the view direction in the plant's own frame picks the three nearest baked views
1399
+ * and their weights; each pixel of the card is projected onto each view's plane (no parallax: fine at the
1400
+ * distances it is drawn) and the three samples are blended. Albedo + plant-space normal, so the scene's sun
1401
+ * lights it like the plant. `position` holds unit-box corners (gizmo bounds); the card is aCorner.
1402
+ */
1403
+ export const IMP_N = 8, IMP_FRAME = 128, IMP_CHUNK = 16, IMP_MARGIN = 1.04;
1404
+ function impostorGeometry() {
1405
+ const geo = new THREE.BufferGeometry();
1406
+ geo.setAttribute('position', new THREE.Float32BufferAttribute([-0.5, 0, -0.5, 0.5, 0, 0.5, 0.5, 1, -0.5, -0.5, 1, 0.5], 3));
1407
+ geo.setAttribute('aCorner', new THREE.Float32BufferAttribute([-1, -1, 1, -1, 1, 1, -1, 1], 2));
1408
+ geo.setAttribute('normal', new THREE.Float32BufferAttribute([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1], 3));
1409
+ geo.setIndex([0, 1, 2, 0, 2, 3]);
1410
+ return geo;
1411
+ }
1412
+ const IMP_PARS_V = /* glsl */ `
1413
+ uniform float uImpGrid, uImpR; uniform vec3 uImpScale;
1414
+ attribute vec2 aCorner;
1415
+ #ifdef USE_INSTANCING
1416
+ attribute float aImpSlot;
1417
+ #endif
1418
+ varying vec3 vImpP, vImpC, vAx0, vAx1, vAx2, vJit, vFW, vCamL, vToCamL; varying vec4 vF01; varying vec2 vF2; varying float vImpFade;`;
1419
+ const IMP_VERT = /* glsl */ `
1420
+ mat4 IM = VEG_M;
1421
+ vec3 ic = (IM * vec4(0.0, 0.5, 0.0, 1.0)).xyz;
1422
+ vec3 a0 = IM[0].xyz, a1 = IM[1].xyz, a2 = IM[2].xyz;
1423
+ vec3 al = vec3(length(a0), length(a1), length(a2));
1424
+ vJit = al / uImpScale; // this copy's scale over the baked one (layout jitter; 1 for the plant itself)
1425
+ vAx0 = a0 / al.x; vAx1 = a1 / al.y; vAx2 = a2 / al.z;
1426
+ float Rw = uImpR * max(vJit.x, max(vJit.y, vJit.z));
1427
+ vec3 camU = vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
1428
+ vec3 camB = vec3(viewMatrix[0][2], viewMatrix[1][2], viewMatrix[2][2]);
1429
+ vec3 toCam = isOrthographic ? camB : normalize(cameraPosition - ic);
1430
+ vec3 bu = camU - toCam * dot(camU, toCam);
1431
+ bu = dot(bu, bu) > 1e-8 ? normalize(bu) : normalize(cross(toCam, vec3(1.0, 0.0, 0.0)));
1432
+ vec3 br = normalize(cross(bu, toCam));
1433
+ vec3 P = ic + (br * aCorner.x + bu * aCorner.y) * Rw;
1434
+ // sway: the plant's own wind field at the card's height (unit space → world)
1435
+ float hfI = clamp(dot(P - IM[3].xyz, vAx1) / al.y, 0.0, 1.0);
1436
+ P += mat3(IM) * plantDisplace(vec3(0.0, hfI, 0.0), hfI, vec2(0.5, 0.35));
1437
+ vImpP = P; vImpC = ic;
1438
+ // the view direction in the plant's frame → hemi-octahedral grid → the three views around it, barycentric weights
1439
+ vec3 vd = vec3(dot(toCam, vAx0), max(dot(toCam, vAx1), 0.0), dot(toCam, vAx2));
1440
+ vd /= max(abs(vd.x) + abs(vd.y) + abs(vd.z), 1e-5);
1441
+ vec2 g = (vec2(vd.x - vd.z, -vd.x - vd.z) * 0.5 + 0.5) * (uImpGrid - 1.0);
1442
+ vec2 cl = clamp(floor(g), 0.0, uImpGrid - 2.0);
1443
+ vec2 fr = clamp(g - cl, 0.0, 1.0);
1444
+ if (fr.x + fr.y < 1.0) { vF01 = vec4(cl, cl + vec2(1.0, 0.0)); vF2 = cl + vec2(0.0, 1.0); vFW = vec3(1.0 - fr.x - fr.y, fr.x, fr.y); }
1445
+ else { vF01 = vec4(cl + vec2(1.0), cl + vec2(0.0, 1.0)); vF2 = cl + vec2(1.0, 0.0); vFW = vec3(fr.x + fr.y - 1.0, 1.0 - fr.x, 1.0 - fr.y); }
1446
+ // the camera in the plant's (baked) frame: the fragment casts the view ray from it to reproject per view
1447
+ vec3 cw = cameraPosition - ic;
1448
+ vCamL = vec3(dot(cw, vAx0), dot(cw, vAx1), dot(cw, vAx2)) / vJit;
1449
+ vToCamL = normalize(vec3(dot(toCam, vAx0), dot(toCam, vAx1), dot(toCam, vAx2)) / vJit);
1450
+ vImpFade = vegLodFade(vegLodS());
1451
+ #ifdef USE_INSTANCING
1452
+ // a forest: only the trees drawn in full detail this frame (aImpSlot = 1) cross-fade; every other one is the card
1453
+ if (aImpSlot < 0.5) vImpFade = 1.0;
1454
+ #endif
1455
+ vec3 transformed = (inverse(IM) * vec4(P, 1.0)).xyz;`;
1456
+ const IMP_PARS_F = /* glsl */ `
1457
+ uniform sampler2D uImpC, uImpN; uniform float uImpGrid, uImpR; uniform vec3 uImpScale;
1458
+ varying vec3 vImpP, vImpC, vAx0, vAx1, vAx2, vJit, vFW, vCamL, vToCamL; varying vec4 vF01; varying vec2 vF2; varying float vImpFade;
1459
+ // the baked view (i, j): direction (hemi-octahedral decode), image plane, framing (the box's half-extents in
1460
+ // that view, = the bake's) and half-depth along it
1461
+ struct ImpView { vec2 fr; vec3 r; vec3 up; vec3 d; float rf; float rd; };
1462
+ ImpView impView(vec2 fr) {
1463
+ vec2 c = fr / (uImpGrid - 1.0) * 2.0 - 1.0;
1464
+ vec3 d = vec3((c.x - c.y) * 0.5, 0.0, (-c.x - c.y) * 0.5);
1465
+ d.y = 1.0 - abs(d.x) - abs(d.z);
1466
+ d = normalize(d);
1467
+ vec3 r = normalize(cross(vec3(0.0, 1.0, 0.0), d)), up = cross(d, r);
1468
+ vec3 H = uImpScale * 0.5;
1469
+ return ImpView(fr, r, up, d, max(dot(abs(r), H), dot(abs(up), H)) * ${IMP_MARGIN.toFixed(3)}, dot(abs(d), H) * ${IMP_MARGIN.toFixed(3)});
1470
+ }
1471
+ // atlas uv in xy; z = 1 inside this view's frame, 0 outside (outside is empty — never the frame's border repeated)
1472
+ vec3 impAt(ImpView v, vec3 L) {
1473
+ vec2 q = vec2(dot(L, v.r), dot(L, v.up)) / (2.0 * v.rf) + 0.5;
1474
+ float inside = step(0.0, q.x) * step(q.x, 1.0) * step(0.0, q.y) * step(q.y, 1.0);
1475
+ return vec3((v.fr + clamp(q, 0.004, 0.996)) / uImpGrid, inside);
1476
+ }
1477
+ // parallax: the card pixel L is a point on the view ray, not the surface. Read this view's depth where L
1478
+ // lands, move along the ray to that depth, sample there — so the three views agree on where the surface is
1479
+ vec3 impReproject(ImpView v, vec3 L, vec3 ray) {
1480
+ float dd = (texture2D(uImpN, impAt(v, L).xy).a - 0.5) * 2.0 * v.rd;
1481
+ float k = dot(ray, v.d);
1482
+ float t = abs(k) > 0.02 ? clamp((dd - dot(L, v.d)) / k, -3.0 * v.rd, 3.0 * v.rd) : 0.0;
1483
+ return impAt(v, L + ray * t);
1484
+ }
1485
+ vec3 impLocal() { vec3 w = vImpP - vImpC; return vec3(dot(w, vAx0), dot(w, vAx1), dot(w, vAx2)) / vJit; }`;
1486
+ const IMP_ALPHA = /* glsl */ `
1487
+ vec3 impL = impLocal();
1488
+ vec3 impRay = isOrthographic ? -vToCamL : normalize(impL - vCamL);
1489
+ vec3 iu0 = impReproject(impView(vF01.xy), impL, impRay), iu1 = impReproject(impView(vF01.zw), impL, impRay), iu2 = impReproject(impView(vF2), impL, impRay);
1490
+ vec4 ic0 = texture2D(uImpC, iu0.xy), ic1 = texture2D(uImpC, iu1.xy), ic2 = texture2D(uImpC, iu2.xy);
1491
+ ic0.a *= iu0.z; ic1.a *= iu1.z; ic2.a *= iu2.z;
1492
+ // coverage: the weighted views (reprojected, so they agree), leaning a little toward the union so the
1493
+ // crown does not thin where a far view barely misses
1494
+ float impA = mix(ic0.a * vFW.x + ic1.a * vFW.y + ic2.a * vFW.z, max(ic0.a * vFW.x, max(ic1.a * vFW.y, ic2.a * vFW.z)) / max(max(vFW.x, vFW.y), vFW.z), 0.35);`;
1495
+ export function impostorMaterial(u) {
1496
+ const mat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.8, metalness: 0, alphaToCoverage: true, envMapIntensity: 0.4 });
1497
+ // the shadow card faces the light: three would draw a front-sided material back-faced in the shadow pass
1498
+ mat.shadowSide = THREE.DoubleSide;
1499
+ const own = { uImpC: { value: null }, uImpN: { value: null }, uImpGrid: { value: IMP_N }, uImpR: { value: 1 }, uImpScale: { value: new THREE.Vector3(1, 1, 1) } };
1500
+ mat.userData.uniforms = own;
1501
+ const vert = (shader) => {
1502
+ injectUniforms(shader, u);
1503
+ injectUniforms(shader, own);
1504
+ shader.vertexShader = shader.vertexShader
1505
+ .replace('#include <common>', `#include <common>\n${COMMON_VERT_PARS}\n${IMP_PARS_V}`)
1506
+ .replace('#include <begin_vertex>', IMP_VERT);
1507
+ };
1508
+ mat.onBeforeCompile = (shader) => {
1509
+ vert(shader);
1510
+ shader.fragmentShader = shader.fragmentShader
1511
+ .replace('#include <common>', `#include <common>\n${VEG_UNIFORMS_GLSL}\n${LOD_GLSL_F}\n${IMP_PARS_F}`)
1512
+ .replace('#include <map_fragment>', `${IMP_ALPHA}
1513
+ if (impA < 0.42) discard;
1514
+ if (vImpFade < 1.0 && vegIgn(gl_FragCoord.xy) >= vImpFade) discard;
1515
+ // the colour weighted by coverage too, so a view where this pixel is empty does not darken it
1516
+ vec3 iw = vec3(ic0.a * vFW.x, ic1.a * vFW.y, ic2.a * vFW.z);
1517
+ float iws = max(iw.x + iw.y + iw.z, 1e-4);
1518
+ vec3 impAlb = (ic0.rgb * iw.x + ic1.rgb * iw.y + ic2.rgb * iw.z) / iws;
1519
+ vec3 impNl = (texture2D(uImpN, iu0.xy).rgb * iw.x + texture2D(uImpN, iu1.xy).rgb * iw.y + texture2D(uImpN, iu2.xy).rgb * iw.z) / iws;
1520
+ impNl = normalize(impNl * 2.0 - 1.0);
1521
+ vec3 impNView = normalize((viewMatrix * vec4(vAx0 * impNl.x + vAx1 * impNl.y + vAx2 * impNl.z, 0.0)).xyz);
1522
+ diffuseColor.rgb *= impAlb;
1523
+ diffuseColor.a *= smoothstep(0.42, 0.62, impA) * uOpacity;`)
1524
+ .replace('#include <normal_fragment_maps>', '#include <normal_fragment_maps>\n normal = impNView;')
1525
+ .replace('#include <lights_fragment_end>', `#include <lights_fragment_end>
1526
+ // the leaves' translucency, approximated: sun through the crown when it is behind it
1527
+ reflectedLight.indirectDiffuse += diffuseColor.rgb * vec3(1.05, 1.1, 0.7) * max(dot(-normal, uSunDirView), 0.0) * 0.5;`);
1528
+ };
1529
+ mat.customProgramCacheKey = () => 'veg-impostor';
1530
+ const depth = new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking });
1531
+ depth.onBeforeCompile = (shader) => {
1532
+ vert(shader);
1533
+ shader.fragmentShader = shader.fragmentShader
1534
+ .replace('#include <common>', `#include <common>\n${VEG_UNIFORMS_GLSL}\n${IMP_PARS_F}`)
1535
+ .replace('#include <map_fragment>', `${IMP_ALPHA}\n if (impA < 0.5) discard;`);
1536
+ };
1537
+ depth.customProgramCacheKey = () => 'veg-impostor-depth';
1538
+ return { material: mat, depth, own };
1539
+ }
1540
+ /** Hemi-octahedral view (i, j) of an n×n grid: the unit direction toward the camera, y ≥ 0 (= impUV's decode). */
1541
+ function impFrameDir(i, j, n, out) {
1542
+ const cx = i / (n - 1) * 2 - 1, cy = j / (n - 1) * 2 - 1;
1543
+ const x = (cx - cy) * 0.5, z = (-cx - cy) * 0.5;
1544
+ return out.set(x, 1 - Math.abs(x) - Math.abs(z), z).normalize();
1545
+ }
1546
+ const _iUp = new THREE.Vector3(0, 1, 0), _iD = new THREE.Vector3(), _iR = new THREE.Vector3(), _iU = new THREE.Vector3(), _iC = new THREE.Vector3(), _iCol = new THREE.Color();
1547
+ /**
1548
+ * Detail levels for one plant: per-draw part reduction + the impostor (baked lazily, only once some draw
1549
+ * of the plant is far enough to want it; re-baked progressively — IMP_CHUNK views per frame — when the
1550
+ * plant's look or scale changes, at most every 1.5 s while it keeps changing).
1551
+ * parts.leaves leaf-card meshes (fewer, grown, flat at distance)
1552
+ * parts.bark branch meshes (finest level dropped at distance; trunk + level 0 in the shadow pass)
1553
+ * parts.other meshes drawn until the impostor takes over (fruit, falling leaves)
1554
+ * parts.bake the meshes the impostor shows (not the falling leaves)
1555
+ * parts.visible [mesh, () => boolean]: parts hidden when they would draw nothing (blossom at 0, …)
1556
+ * Returns { impostor, update(dt, renderer), applied(cfg), dispose() }.
1557
+ */
1558
+ const LOOK_IGNORE = new Set(['speed', 'windStrength', 'windDirection', 'turbulence', 'flutter', 'sway', 'lod', 'leafFall', 'opacity', 'enabled', 'quality', 'seed']);
1559
+ export function plantLod(u, object, parts) {
1560
+ const st = { u, ready: false, wanted: false, dirty: true, changedAt: 0, lastBake: -1e9, t: 0, frame: -1, key: '', bakeScale: new THREE.Vector3(), rtC: null, rtN: null };
1561
+ for (const m of parts.leaves || [])
1562
+ lodAttach(m, lodDrawFn(st, 'leaves', m.geometry), st, 'leaves');
1563
+ for (const m of parts.bark || [])
1564
+ lodAttach(m, lodDrawFn(st, 'bark', m.geometry), st, 'bark');
1565
+ for (const m of parts.other || [])
1566
+ lodAttach(m, lodDrawFn(st, 'other', m.geometry), st, 'other');
1567
+ const imp = impostorMaterial(u);
1568
+ const impGeo = impostorGeometry();
1569
+ const impostor = plantMesh(impGeo, imp, 'impostor');
1570
+ // casts (its card turns to the sun in the shadow pass) but does not receive: the card the camera sees and
1571
+ // the card the sun sees cross, so it would shadow itself along their intersection. Its baked normals still
1572
+ // give it a lit and a shaded side; at the distances it is drawn, other objects' shadows on it are not missed.
1573
+ impostor.receiveShadow = false;
1574
+ lodAttach(impostor, lodDrawFn(st, 'impostor', impGeo), st, 'impostor');
1575
+ object.add(impostor);
1576
+ // the bake scene: proxies sharing each part's geometry and material, drawn at full detail, unrotated, at the plant's scale
1577
+ const scene = new THREE.Scene();
1578
+ const proxies = (parts.bake || []).map((src) => {
1579
+ const g = src.geometry, full = g.instanceCount, card = !!g.isInstancedBufferGeometry && g.index && g.index.count === CARD_IDX + FLAT_IDX;
1580
+ const p = new THREE.Mesh(g, src.material);
1581
+ p.matrixAutoUpdate = false;
1582
+ p.frustumCulled = false;
1583
+ p.onBeforeRender = function () { if (card) {
1584
+ g.instanceCount = full;
1585
+ g.drawRange.start = 0;
1586
+ g.drawRange.count = CARD_IDX;
1587
+ }
1588
+ else {
1589
+ g.drawRange.start = 0;
1590
+ g.drawRange.count = Infinity;
1591
+ } };
1592
+ scene.add(p);
1593
+ return { p, src };
1594
+ });
1595
+ const cam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.01, 10);
1596
+ cam.matrixAutoUpdate = false;
1597
+ function markDirty() { st.dirty = true; st.changedAt = st.t; }
1598
+ function makeTarget() {
1599
+ const size = IMP_N * IMP_FRAME;
1600
+ const rt = new THREE.WebGLRenderTarget(size, size, { colorSpace: THREE.SRGBColorSpace, generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter, depthBuffer: true });
1601
+ return rt;
1602
+ }
1603
+ function bakeChunk(renderer) {
1604
+ if (!st.rtC) {
1605
+ st.rtC = makeTarget();
1606
+ st.rtN = makeTarget();
1607
+ // allocate with a mip chain now; mips are then generated once, after the last view
1608
+ if (renderer.initRenderTarget) {
1609
+ renderer.initRenderTarget(st.rtC);
1610
+ renderer.initRenderTarget(st.rtN);
1611
+ }
1612
+ imp.own.uImpC.value = st.rtC.texture;
1613
+ imp.own.uImpN.value = st.rtN.texture;
1614
+ }
1615
+ const S = st.bakeScale, R = 0.5 * S.length();
1616
+ if (st.frame === 0) {
1617
+ imp.own.uImpR.value = R;
1618
+ imp.own.uImpScale.value.copy(S);
1619
+ }
1620
+ const prevRT = renderer.getRenderTarget(), prevAuto = renderer.autoClear, prevAlpha = renderer.getClearAlpha();
1621
+ renderer.getClearColor(_iCol);
1622
+ const pr = _iCol.r, pg = _iCol.g, pb = _iCol.b;
1623
+ const prevShadowAuto = renderer.shadowMap.autoUpdate;
1624
+ const saved = [u.uWindStrength.value, u.uFlutter.value, u.uOpacity.value, u.uBake.value];
1625
+ u.uWindStrength.value = 0;
1626
+ u.uFlutter.value = 0;
1627
+ u.uOpacity.value = 1;
1628
+ renderer.shadowMap.autoUpdate = false;
1629
+ renderer.autoClear = true;
1630
+ for (const { p, src } of proxies) {
1631
+ p.matrix.makeScale(S.x, S.y, S.z);
1632
+ p.visible = src.visible;
1633
+ }
1634
+ scene.updateMatrixWorld(true);
1635
+ cam.near = 0.01 * R;
1636
+ cam.far = 4 * R;
1637
+ _iC.set(0, 0.5 * S.y, 0);
1638
+ const total = IMP_N * IMP_N, end = Math.min(st.frame + IMP_CHUNK, total);
1639
+ for (let f = st.frame; f < end; f++) {
1640
+ const i = f % IMP_N, j = Math.floor(f / IMP_N);
1641
+ impFrameDir(i, j, IMP_N, _iD);
1642
+ _iR.crossVectors(_iUp, _iD).normalize();
1643
+ _iU.crossVectors(_iD, _iR);
1644
+ const Rf = Math.max(Math.abs(_iR.x) * S.x + Math.abs(_iR.y) * S.y + Math.abs(_iR.z) * S.z, Math.abs(_iU.x) * S.x + Math.abs(_iU.y) * S.y + Math.abs(_iU.z) * S.z) * 0.5 * IMP_MARGIN;
1645
+ cam.left = -Rf;
1646
+ cam.right = Rf;
1647
+ cam.top = Rf;
1648
+ cam.bottom = -Rf;
1649
+ cam.updateProjectionMatrix();
1650
+ u.uBakeDepth.value.set(2 * R, (Math.abs(_iD.x) * S.x + Math.abs(_iD.y) * S.y + Math.abs(_iD.z) * S.z) * 0.5 * IMP_MARGIN);
1651
+ cam.matrix.makeBasis(_iR, _iU, _iD).setPosition(_iC.x + _iD.x * 2 * R, _iC.y + _iD.y * 2 * R, _iC.z + _iD.z * 2 * R);
1652
+ cam.updateMatrixWorld(true);
1653
+ u.uBakeView.value.setFromMatrix4(cam.matrixWorld);
1654
+ for (let pass = 1; pass <= 2; pass++) {
1655
+ const rt = pass === 1 ? st.rtC : st.rtN;
1656
+ // empty pixels carry a foliage colour / an upward normal, so filtering at the silhouette does not fringe dark
1657
+ if (pass === 1)
1658
+ renderer.setClearColor(u.uShadeColor.value, 0);
1659
+ else
1660
+ renderer.setClearColor(_iCol.setRGB(0.5, 1, 0.5), 0);
1661
+ rt.texture.generateMipmaps = f === total - 1;
1662
+ rt.viewport.set(i * IMP_FRAME, j * IMP_FRAME, IMP_FRAME, IMP_FRAME);
1663
+ rt.scissor.set(i * IMP_FRAME, j * IMP_FRAME, IMP_FRAME, IMP_FRAME);
1664
+ rt.scissorTest = true;
1665
+ u.uBake.value = pass;
1666
+ renderer.setRenderTarget(rt);
1667
+ renderer.render(scene, cam);
1668
+ }
1669
+ }
1670
+ [u.uWindStrength.value, u.uFlutter.value, u.uOpacity.value, u.uBake.value] = saved;
1671
+ renderer.shadowMap.autoUpdate = prevShadowAuto;
1672
+ renderer.autoClear = prevAuto;
1673
+ renderer.setClearColor(_iCol.setRGB(pr, pg, pb), prevAlpha);
1674
+ renderer.setRenderTarget(prevRT);
1675
+ st.frame = end >= total ? -1 : end;
1676
+ if (st.frame < 0) {
1677
+ st.ready = true;
1678
+ u.uImpReady.value = 1;
1679
+ st.lastBake = st.t;
1680
+ }
1681
+ }
1682
+ return {
1683
+ impostor,
1684
+ state: st,
1685
+ /** Per frame, after updateCommon: hides empty parts, bakes when wanted and stale. */
1686
+ update(dt, renderer) {
1687
+ st.t += dt || 0;
1688
+ for (const [m, test] of parts.visible || [])
1689
+ m.visible = !!test();
1690
+ const sc = u.uScale.value;
1691
+ if (st.ready && Math.abs(sc.x / st.bakeScale.x - 1) + Math.abs(sc.y / st.bakeScale.y - 1) + Math.abs(sc.z / st.bakeScale.z - 1) > 0.03)
1692
+ markDirty();
1693
+ if (renderer && st.frame < 0 && st.dirty && st.wanted && (st.t - st.changedAt > 0.3 || st.t - st.lastBake > 1.5)) {
1694
+ st.dirty = false;
1695
+ st.frame = 0;
1696
+ st.bakeScale.copy(sc);
1697
+ }
1698
+ if (renderer && st.frame >= 0)
1699
+ bakeChunk(renderer);
1700
+ st.wanted = false; // re-armed by this frame's draws
1701
+ },
1702
+ /** After every apply: the lod knob, and a re-bake when a knob that changes the look moved. */
1703
+ applied(cfg) {
1704
+ if (cfg.lod !== undefined)
1705
+ applyLodMode(u, cfg.lod);
1706
+ let key = '';
1707
+ for (const k in cfg)
1708
+ if (!LOOK_IGNORE.has(k)) {
1709
+ const v = cfg[k];
1710
+ key += k + ':' + (v && v.isColor ? v.getHexString() : typeof v === 'number' ? v.toFixed(3) : v) + ';';
1711
+ }
1712
+ if (key !== st.key) {
1713
+ st.key = key;
1714
+ markDirty();
1715
+ }
1716
+ },
1717
+ dispose() {
1718
+ impGeo.dispose();
1719
+ imp.material.dispose();
1720
+ imp.depth.dispose();
1721
+ if (st.rtC) {
1722
+ st.rtC.dispose();
1723
+ st.rtN.dispose();
1724
+ }
1725
+ u.uImpReady.value = 0;
1726
+ },
1727
+ };
1728
+ }