@3driseai/3drise-viewer 0.1.41 → 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.
- package/dist/animationsV2/ClipsRuntime.d.ts +16 -1
- package/dist/animationsV2/ClipsRuntime.d.ts.map +1 -1
- package/dist/animationsV2/ClipsRuntime.js +144 -25
- package/dist/animationsV2/appliers.d.ts +9 -0
- package/dist/animationsV2/appliers.d.ts.map +1 -1
- package/dist/animationsV2/appliers.js +62 -2
- package/dist/animationsV2/clipUtils.d.ts +30 -0
- package/dist/animationsV2/clipUtils.d.ts.map +1 -1
- package/dist/animationsV2/clipUtils.js +163 -17
- package/dist/animationsV2/elementSettings.d.ts +5 -0
- package/dist/animationsV2/elementSettings.d.ts.map +1 -1
- package/dist/animationsV2/elementSettings.js +6 -3
- package/dist/animationsV2/sequenceUtils.js +4 -4
- package/dist/animationsV2/types.d.ts +23 -0
- package/dist/animationsV2/types.d.ts.map +1 -1
- package/dist/api/sceneHandle.js +2 -2
- package/dist/components/CreatedObject.d.ts +3 -0
- package/dist/components/CreatedObject.d.ts.map +1 -1
- package/dist/components/CreatedObject.js +6 -2
- package/dist/components/CustomPrimitive.d.ts.map +1 -1
- package/dist/components/CustomPrimitive.js +1 -10
- package/dist/components/EnvironmentManager.d.ts.map +1 -1
- package/dist/components/EnvironmentManager.js +11 -5
- package/dist/components/GeometryShape.d.ts +5 -2
- package/dist/components/GeometryShape.d.ts.map +1 -1
- package/dist/components/GeometryShape.js +71 -10
- package/dist/components/LandObject.d.ts.map +1 -1
- package/dist/components/LandObject.js +1 -1
- package/dist/components/LayoutInstances.d.ts +2 -1
- package/dist/components/LayoutInstances.d.ts.map +1 -1
- package/dist/components/LayoutInstances.js +39 -6
- package/dist/components/LightObject.d.ts.map +1 -1
- package/dist/components/LightObject.js +3 -0
- package/dist/components/ObjectNode.d.ts.map +1 -1
- package/dist/components/ObjectNode.js +62 -13
- package/dist/components/OceanBuilder.d.ts +2 -0
- package/dist/components/OceanBuilder.d.ts.map +1 -1
- package/dist/components/OceanBuilder.js +2 -1
- package/dist/components/RenderEffects.d.ts +8 -0
- package/dist/components/RenderEffects.d.ts.map +1 -0
- package/dist/components/RenderEffects.js +120 -0
- package/dist/components/SceneBuilder.d.ts.map +1 -1
- package/dist/components/SceneBuilder.js +18 -9
- package/dist/components/SportsObject.d.ts +9 -0
- package/dist/components/SportsObject.d.ts.map +1 -0
- package/dist/components/SportsObject.js +20 -0
- package/dist/components/UrbanObject.d.ts +9 -0
- package/dist/components/UrbanObject.d.ts.map +1 -0
- package/dist/components/UrbanObject.js +28 -0
- package/dist/components/VegetationObject.d.ts +9 -0
- package/dist/components/VegetationObject.d.ts.map +1 -0
- package/dist/components/VegetationObject.js +26 -0
- package/dist/components/WaterObject.d.ts +15 -0
- package/dist/components/WaterObject.d.ts.map +1 -0
- package/dist/components/WaterObject.js +29 -0
- package/dist/components/index.d.ts +7 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +6 -1
- package/dist/components/land/LandPatch.d.ts +3 -1
- package/dist/components/land/LandPatch.d.ts.map +1 -1
- package/dist/components/land/LandPatch.js +80 -7
- package/dist/components/layout/InstancedLayout.d.ts +19 -0
- package/dist/components/layout/InstancedLayout.d.ts.map +1 -0
- package/dist/components/layout/InstancedLayout.js +154 -0
- package/dist/components/layout/LayoutGrounds.d.ts +19 -0
- package/dist/components/layout/LayoutGrounds.d.ts.map +1 -0
- package/dist/components/layout/LayoutGrounds.js +104 -0
- package/dist/components/layout/LinkProxies.d.ts +5 -0
- package/dist/components/layout/LinkProxies.d.ts.map +1 -0
- package/dist/components/layout/LinkProxies.js +51 -0
- package/dist/components/layout/LinkedSource.d.ts +14 -0
- package/dist/components/layout/LinkedSource.d.ts.map +1 -0
- package/dist/components/layout/LinkedSource.js +129 -0
- package/dist/components/projector/Projector.d.ts +14 -0
- package/dist/components/projector/Projector.d.ts.map +1 -0
- package/dist/components/projector/Projector.js +44 -0
- package/dist/components/projector/index.d.ts +4 -0
- package/dist/components/projector/index.d.ts.map +1 -0
- package/dist/components/projector/index.js +2 -0
- package/dist/components/projector/projectorCore.d.ts +19 -0
- package/dist/components/projector/projectorCore.d.ts.map +1 -0
- package/dist/components/projector/projectorCore.js +545 -0
- package/dist/components/sports/SportsBall.d.ts +9 -0
- package/dist/components/sports/SportsBall.d.ts.map +1 -0
- package/dist/components/sports/SportsBall.js +23 -0
- package/dist/components/sports/SportsCourt.d.ts +9 -0
- package/dist/components/sports/SportsCourt.d.ts.map +1 -0
- package/dist/components/sports/SportsCourt.js +23 -0
- package/dist/components/sports/SportsEquipment.d.ts +9 -0
- package/dist/components/sports/SportsEquipment.d.ts.map +1 -0
- package/dist/components/sports/SportsEquipment.js +23 -0
- package/dist/components/sports/SportsGoal.d.ts +9 -0
- package/dist/components/sports/SportsGoal.d.ts.map +1 -0
- package/dist/components/sports/SportsGoal.js +23 -0
- package/dist/components/sports/core/ball.d.ts +166 -0
- package/dist/components/sports/core/ball.d.ts.map +1 -0
- package/dist/components/sports/core/ball.js +402 -0
- package/dist/components/sports/core/court.d.ts +442 -0
- package/dist/components/sports/core/court.d.ts.map +1 -0
- package/dist/components/sports/core/court.js +658 -0
- package/dist/components/sports/core/equipment.d.ts +103 -0
- package/dist/components/sports/core/equipment.d.ts.map +1 -0
- package/dist/components/sports/core/equipment.js +403 -0
- package/dist/components/sports/core/goal.d.ts +98 -0
- package/dist/components/sports/core/goal.d.ts.map +1 -0
- package/dist/components/sports/core/goal.js +44 -0
- package/dist/components/sports/index.d.ts +11 -0
- package/dist/components/sports/index.d.ts.map +1 -0
- package/dist/components/sports/index.js +8 -0
- package/dist/components/sports/sportsCore.d.ts +195 -0
- package/dist/components/sports/sportsCore.d.ts.map +1 -0
- package/dist/components/sports/sportsCore.js +758 -0
- package/dist/components/sports/useSportsInstance.d.ts +35 -0
- package/dist/components/sports/useSportsInstance.d.ts.map +1 -0
- package/dist/components/sports/useSportsInstance.js +47 -0
- package/dist/components/terrain/TerrainSurface.d.ts.map +1 -1
- package/dist/components/terrain/TerrainSurface.js +14 -0
- package/dist/components/urban/UrbanBlock.d.ts +9 -0
- package/dist/components/urban/UrbanBlock.d.ts.map +1 -0
- package/dist/components/urban/UrbanBlock.js +23 -0
- package/dist/components/urban/UrbanBuilding.d.ts +9 -0
- package/dist/components/urban/UrbanBuilding.d.ts.map +1 -0
- package/dist/components/urban/UrbanBuilding.js +23 -0
- package/dist/components/urban/UrbanJunction.d.ts +9 -0
- package/dist/components/urban/UrbanJunction.d.ts.map +1 -0
- package/dist/components/urban/UrbanJunction.js +23 -0
- package/dist/components/urban/UrbanPark.d.ts +9 -0
- package/dist/components/urban/UrbanPark.d.ts.map +1 -0
- package/dist/components/urban/UrbanPark.js +23 -0
- package/dist/components/urban/UrbanParkingLot.d.ts +9 -0
- package/dist/components/urban/UrbanParkingLot.d.ts.map +1 -0
- package/dist/components/urban/UrbanParkingLot.js +23 -0
- package/dist/components/urban/UrbanPlaza.d.ts +9 -0
- package/dist/components/urban/UrbanPlaza.d.ts.map +1 -0
- package/dist/components/urban/UrbanPlaza.js +23 -0
- package/dist/components/urban/UrbanRoad.d.ts +9 -0
- package/dist/components/urban/UrbanRoad.d.ts.map +1 -0
- package/dist/components/urban/UrbanRoad.js +23 -0
- package/dist/components/urban/UrbanSignage.d.ts +9 -0
- package/dist/components/urban/UrbanSignage.d.ts.map +1 -0
- package/dist/components/urban/UrbanSignage.js +23 -0
- package/dist/components/urban/UrbanSkyline.d.ts +9 -0
- package/dist/components/urban/UrbanSkyline.d.ts.map +1 -0
- package/dist/components/urban/UrbanSkyline.js +23 -0
- package/dist/components/urban/UrbanStreetFurniture.d.ts +9 -0
- package/dist/components/urban/UrbanStreetFurniture.d.ts.map +1 -0
- package/dist/components/urban/UrbanStreetFurniture.js +23 -0
- package/dist/components/urban/UrbanStreetGrid.d.ts +9 -0
- package/dist/components/urban/UrbanStreetGrid.d.ts.map +1 -0
- package/dist/components/urban/UrbanStreetGrid.js +34 -0
- package/dist/components/urban/UrbanTraffic.d.ts +9 -0
- package/dist/components/urban/UrbanTraffic.d.ts.map +1 -0
- package/dist/components/urban/UrbanTraffic.js +23 -0
- package/dist/components/urban/core/block.d.ts +119 -0
- package/dist/components/urban/core/block.d.ts.map +1 -0
- package/dist/components/urban/core/block.js +266 -0
- package/dist/components/urban/core/building.d.ts +128 -0
- package/dist/components/urban/core/building.d.ts.map +1 -0
- package/dist/components/urban/core/building.js +741 -0
- package/dist/components/urban/core/junction.d.ts +152 -0
- package/dist/components/urban/core/junction.d.ts.map +1 -0
- package/dist/components/urban/core/junction.js +617 -0
- package/dist/components/urban/core/park.d.ts +67 -0
- package/dist/components/urban/core/park.d.ts.map +1 -0
- package/dist/components/urban/core/park.js +176 -0
- package/dist/components/urban/core/parkingLot.d.ts +82 -0
- package/dist/components/urban/core/parkingLot.d.ts.map +1 -0
- package/dist/components/urban/core/parkingLot.js +284 -0
- package/dist/components/urban/core/plaza.d.ts +24 -0
- package/dist/components/urban/core/plaza.d.ts.map +1 -0
- package/dist/components/urban/core/plaza.js +243 -0
- package/dist/components/urban/core/road.d.ts +113 -0
- package/dist/components/urban/core/road.d.ts.map +1 -0
- package/dist/components/urban/core/road.js +347 -0
- package/dist/components/urban/core/signage.d.ts +83 -0
- package/dist/components/urban/core/signage.d.ts.map +1 -0
- package/dist/components/urban/core/signage.js +209 -0
- package/dist/components/urban/core/skyline.d.ts +100 -0
- package/dist/components/urban/core/skyline.d.ts.map +1 -0
- package/dist/components/urban/core/skyline.js +89 -0
- package/dist/components/urban/core/streetFurniture.d.ts +75 -0
- package/dist/components/urban/core/streetFurniture.d.ts.map +1 -0
- package/dist/components/urban/core/streetFurniture.js +193 -0
- package/dist/components/urban/core/streetGrid.d.ts +134 -0
- package/dist/components/urban/core/streetGrid.d.ts.map +1 -0
- package/dist/components/urban/core/streetGrid.js +422 -0
- package/dist/components/urban/core/traffic.d.ts +125 -0
- package/dist/components/urban/core/traffic.d.ts.map +1 -0
- package/dist/components/urban/core/traffic.js +305 -0
- package/dist/components/urban/index.d.ts +29 -0
- package/dist/components/urban/index.d.ts.map +1 -0
- package/dist/components/urban/index.js +16 -0
- package/dist/components/urban/textureSetResolver.d.ts +8 -0
- package/dist/components/urban/textureSetResolver.d.ts.map +1 -0
- package/dist/components/urban/textureSetResolver.js +16 -0
- package/dist/components/urban/urbanCore.d.ts +262 -0
- package/dist/components/urban/urbanCore.d.ts.map +1 -0
- package/dist/components/urban/urbanCore.js +928 -0
- package/dist/components/urban/useUrbanInstance.d.ts +53 -0
- package/dist/components/urban/useUrbanInstance.d.ts.map +1 -0
- package/dist/components/urban/useUrbanInstance.js +100 -0
- package/dist/components/vegetation/VegetationBush.d.ts +9 -0
- package/dist/components/vegetation/VegetationBush.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationBush.js +23 -0
- package/dist/components/vegetation/VegetationCrops.d.ts +9 -0
- package/dist/components/vegetation/VegetationCrops.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationCrops.js +23 -0
- package/dist/components/vegetation/VegetationFlowers.d.ts +9 -0
- package/dist/components/vegetation/VegetationFlowers.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationFlowers.js +23 -0
- package/dist/components/vegetation/VegetationForest.d.ts +9 -0
- package/dist/components/vegetation/VegetationForest.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationForest.js +23 -0
- package/dist/components/vegetation/VegetationFruit.d.ts +9 -0
- package/dist/components/vegetation/VegetationFruit.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationFruit.js +23 -0
- package/dist/components/vegetation/VegetationGrass.d.ts +9 -0
- package/dist/components/vegetation/VegetationGrass.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationGrass.js +23 -0
- package/dist/components/vegetation/VegetationHedge.d.ts +9 -0
- package/dist/components/vegetation/VegetationHedge.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationHedge.js +23 -0
- package/dist/components/vegetation/VegetationIvy.d.ts +9 -0
- package/dist/components/vegetation/VegetationIvy.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationIvy.js +23 -0
- package/dist/components/vegetation/VegetationTree.d.ts +9 -0
- package/dist/components/vegetation/VegetationTree.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationTree.js +23 -0
- package/dist/components/vegetation/VegetationVegetable.d.ts +9 -0
- package/dist/components/vegetation/VegetationVegetable.d.ts.map +1 -0
- package/dist/components/vegetation/VegetationVegetable.js +23 -0
- package/dist/components/vegetation/core/bush.d.ts +1256 -0
- package/dist/components/vegetation/core/bush.d.ts.map +1 -0
- package/dist/components/vegetation/core/bush.js +376 -0
- package/dist/components/vegetation/core/crops.d.ts +1033 -0
- package/dist/components/vegetation/core/crops.d.ts.map +1 -0
- package/dist/components/vegetation/core/crops.js +378 -0
- package/dist/components/vegetation/core/flowers.d.ts +648 -0
- package/dist/components/vegetation/core/flowers.d.ts.map +1 -0
- package/dist/components/vegetation/core/flowers.js +225 -0
- package/dist/components/vegetation/core/forest.d.ts +138 -0
- package/dist/components/vegetation/core/forest.d.ts.map +1 -0
- package/dist/components/vegetation/core/forest.js +310 -0
- package/dist/components/vegetation/core/fruit.d.ts +99 -0
- package/dist/components/vegetation/core/fruit.d.ts.map +1 -0
- package/dist/components/vegetation/core/fruit.js +77 -0
- package/dist/components/vegetation/core/grass.d.ts +103 -0
- package/dist/components/vegetation/core/grass.d.ts.map +1 -0
- package/dist/components/vegetation/core/grass.js +102 -0
- package/dist/components/vegetation/core/hedge.d.ts +193 -0
- package/dist/components/vegetation/core/hedge.d.ts.map +1 -0
- package/dist/components/vegetation/core/hedge.js +243 -0
- package/dist/components/vegetation/core/ivy.d.ts +213 -0
- package/dist/components/vegetation/core/ivy.d.ts.map +1 -0
- package/dist/components/vegetation/core/ivy.js +173 -0
- package/dist/components/vegetation/core/tree.d.ts +1117 -0
- package/dist/components/vegetation/core/tree.d.ts.map +1 -0
- package/dist/components/vegetation/core/tree.js +408 -0
- package/dist/components/vegetation/core/vegetable.d.ts +99 -0
- package/dist/components/vegetation/core/vegetable.d.ts.map +1 -0
- package/dist/components/vegetation/core/vegetable.js +76 -0
- package/dist/components/vegetation/index.d.ts +23 -0
- package/dist/components/vegetation/index.d.ts.map +1 -0
- package/dist/components/vegetation/index.js +14 -0
- package/dist/components/vegetation/produceCore.d.ts +929 -0
- package/dist/components/vegetation/produceCore.d.ts.map +1 -0
- package/dist/components/vegetation/produceCore.js +723 -0
- package/dist/components/vegetation/useVegetationInstance.d.ts +39 -0
- package/dist/components/vegetation/useVegetationInstance.d.ts.map +1 -0
- package/dist/components/vegetation/useVegetationInstance.js +45 -0
- package/dist/components/vegetation/vegetationCore.d.ts +412 -0
- package/dist/components/vegetation/vegetationCore.d.ts.map +1 -0
- package/dist/components/vegetation/vegetationCore.js +1728 -0
- package/dist/components/volumetricClouds/VolumetricCloudsPass.d.ts +5 -0
- package/dist/components/volumetricClouds/VolumetricCloudsPass.d.ts.map +1 -1
- package/dist/components/volumetricClouds/VolumetricCloudsPass.js +39 -13
- package/dist/components/water/OceanSurface.d.ts.map +1 -1
- package/dist/components/water/OceanSurface.js +3 -2
- package/dist/components/water/WaterBody.d.ts.map +1 -1
- package/dist/components/water/WaterBody.js +31 -10
- package/dist/components/water/WaterFountain.d.ts +9 -0
- package/dist/components/water/WaterFountain.d.ts.map +1 -0
- package/dist/components/water/WaterFountain.js +24 -0
- package/dist/components/water/WaterStream.d.ts +9 -0
- package/dist/components/water/WaterStream.d.ts.map +1 -0
- package/dist/components/water/WaterStream.js +24 -0
- package/dist/components/water/WaterWaterfall.d.ts +9 -0
- package/dist/components/water/WaterWaterfall.d.ts.map +1 -0
- package/dist/components/water/WaterWaterfall.js +24 -0
- package/dist/components/water/core/fountain.d.ts +70 -0
- package/dist/components/water/core/fountain.d.ts.map +1 -0
- package/dist/components/water/core/fountain.js +619 -0
- package/dist/components/water/core/stream.d.ts +72 -0
- package/dist/components/water/core/stream.d.ts.map +1 -0
- package/dist/components/water/core/stream.js +167 -0
- package/dist/components/water/core/waterfall.d.ts +78 -0
- package/dist/components/water/core/waterfall.d.ts.map +1 -0
- package/dist/components/water/core/waterfall.js +325 -0
- package/dist/components/water/index.d.ts +8 -0
- package/dist/components/water/index.d.ts.map +1 -1
- package/dist/components/water/index.js +7 -0
- package/dist/components/water/types.d.ts +3 -0
- package/dist/components/water/types.d.ts.map +1 -1
- package/dist/components/water/useWaterInstance.d.ts +43 -0
- package/dist/components/water/useWaterInstance.d.ts.map +1 -0
- package/dist/components/water/useWaterInstance.js +89 -0
- package/dist/components/water/waterCore.d.ts +179 -0
- package/dist/components/water/waterCore.d.ts.map +1 -0
- package/dist/components/water/waterCore.js +516 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +9 -1
- package/dist/hooks/index.d.ts +7 -2
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +5 -2
- package/dist/hooks/useActionPatch.d.ts +23 -0
- package/dist/hooks/useActionPatch.d.ts.map +1 -0
- package/dist/hooks/useActionPatch.js +38 -0
- package/dist/hooks/useCloudSun.d.ts.map +1 -1
- package/dist/hooks/useCloudSun.js +3 -2
- package/dist/hooks/useCommittedInstance.d.ts +6 -0
- package/dist/hooks/useCommittedInstance.d.ts.map +1 -0
- package/dist/hooks/useCommittedInstance.js +42 -0
- package/dist/hooks/useConfigLive.d.ts +1 -1
- package/dist/hooks/useConfigLive.d.ts.map +1 -1
- package/dist/hooks/useContinuousEffects.d.ts.map +1 -1
- package/dist/hooks/useContinuousEffects.js +36 -20
- package/dist/hooks/useGridLayout.d.ts +7 -0
- package/dist/hooks/useGridLayout.d.ts.map +1 -0
- package/dist/hooks/useGridLayout.js +10 -0
- package/dist/hooks/useInteractionEffects.d.ts.map +1 -1
- package/dist/hooks/useInteractionEffects.js +48 -9
- package/dist/hooks/useLightLive.d.ts.map +1 -1
- package/dist/hooks/useLightLive.js +2 -0
- package/dist/hooks/useLinkStatus.d.ts +7 -0
- package/dist/hooks/useLinkStatus.d.ts.map +1 -0
- package/dist/hooks/useLinkStatus.js +20 -0
- package/dist/hooks/useMaterialApplication.d.ts +6 -1
- package/dist/hooks/useMaterialApplication.d.ts.map +1 -1
- package/dist/hooks/useMaterialApplication.js +7 -2
- package/dist/hooks/useOceanMaterial.d.ts +11 -1
- package/dist/hooks/useOceanMaterial.d.ts.map +1 -1
- package/dist/hooks/useOceanMaterial.js +185 -27
- package/dist/hooks/usePathAnimation.d.ts +8 -0
- package/dist/hooks/usePathAnimation.d.ts.map +1 -1
- package/dist/hooks/usePathAnimation.js +140 -59
- package/dist/hooks/useResolvedLayout.d.ts +10 -0
- package/dist/hooks/useResolvedLayout.d.ts.map +1 -0
- package/dist/hooks/useResolvedLayout.js +17 -0
- package/dist/hooks/useTimeLighting.d.ts +36 -0
- package/dist/hooks/useTimeLighting.d.ts.map +1 -0
- package/dist/hooks/useTimeLighting.js +67 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/operations/catalogue/shaderLayers.d.ts.map +1 -1
- package/dist/operations/catalogue/shaderLayers.js +26 -3
- package/dist/render/renderLive.d.ts +11 -0
- package/dist/render/renderLive.d.ts.map +1 -0
- package/dist/render/renderLive.js +57 -0
- package/dist/shaders/glsl/oceanCore.glsl.d.ts +2 -2
- package/dist/shaders/glsl/oceanCore.glsl.d.ts.map +1 -1
- package/dist/shaders/glsl/oceanCore.glsl.js +35 -5
- package/dist/shaders/ocean/OceanSurfaceMaterial.d.ts +12 -0
- package/dist/shaders/ocean/OceanSurfaceMaterial.d.ts.map +1 -1
- package/dist/shaders/ocean/OceanSurfaceMaterial.js +55 -7
- package/dist/shaders/terrain/TerrainMaterial.d.ts.map +1 -1
- package/dist/shaders/terrain/TerrainMaterial.js +28 -0
- package/dist/triggers/kinds/change.js +1 -1
- package/dist/ui/UiLayers.d.ts +17 -1
- package/dist/ui/UiLayers.d.ts.map +1 -1
- package/dist/ui/UiLayers.js +76 -3
- package/dist/ui/index.d.ts +2 -2
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/utils/composerHostedPass.d.ts +74 -0
- package/dist/utils/composerHostedPass.d.ts.map +1 -0
- package/dist/utils/composerHostedPass.js +63 -0
- package/dist/utils/geometryScript.d.ts +17 -0
- package/dist/utils/geometryScript.d.ts.map +1 -0
- package/dist/utils/geometryScript.js +92 -0
- package/dist/utils/geometryScript.worker.d.ts +2 -0
- package/dist/utils/geometryScript.worker.d.ts.map +1 -0
- package/dist/utils/geometryScript.worker.js +42 -0
- package/dist/utils/groundRegistry.d.ts +11 -0
- package/dist/utils/groundRegistry.d.ts.map +1 -0
- package/dist/utils/groundRegistry.js +39 -0
- package/dist/utils/instancingSupport.d.ts +9 -0
- package/dist/utils/instancingSupport.d.ts.map +1 -0
- package/dist/utils/instancingSupport.js +13 -0
- package/dist/utils/landPatch.d.ts +26 -5
- package/dist/utils/landPatch.d.ts.map +1 -1
- package/dist/utils/landPatch.js +124 -9
- package/dist/utils/layoutPlacement.d.ts +30 -0
- package/dist/utils/layoutPlacement.d.ts.map +1 -0
- package/dist/utils/layoutPlacement.js +113 -0
- package/dist/utils/linkRegistry.d.ts +15 -0
- package/dist/utils/linkRegistry.d.ts.map +1 -0
- package/dist/utils/linkRegistry.js +11 -0
- package/dist/utils/shapes.d.ts +28 -2
- package/dist/utils/shapes.d.ts.map +1 -1
- package/dist/utils/shapes.js +32 -3
- package/dist/utils/sharedParts.d.ts +61 -0
- package/dist/utils/sharedParts.d.ts.map +1 -0
- package/dist/utils/sharedParts.js +268 -0
- package/dist/utils/svgAsset.d.ts +6 -0
- package/dist/utils/svgAsset.d.ts.map +1 -0
- package/dist/utils/svgAsset.js +46 -0
- package/dist/utils/waterLight.d.ts +29 -0
- package/dist/utils/waterLight.d.ts.map +1 -0
- package/dist/utils/waterLight.js +67 -0
- package/dist/utils/waterReflection.d.ts +185 -0
- package/dist/utils/waterReflection.d.ts.map +1 -0
- package/dist/utils/waterReflection.js +616 -0
- 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
|
+
}
|