@woosh/meep-engine 3.53.0 → 3.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/package.json +1 -1
  2. package/src/core/math/noise/worley_noise_2d.d.ts +14 -0
  3. package/src/core/math/noise/worley_noise_2d.d.ts.map +1 -0
  4. package/src/core/math/noise/worley_noise_2d.js +74 -0
  5. package/src/engine/graphics/texture/atlas/CachingTextureAtlas.d.ts +6 -1
  6. package/src/engine/graphics/texture/atlas/CachingTextureAtlas.d.ts.map +1 -1
  7. package/src/engine/graphics/texture/atlas/CachingTextureAtlas.js +363 -325
  8. package/src/engine/graphics/texture/atlas/ManagedTextureAtlas.d.ts +93 -14
  9. package/src/engine/graphics/texture/atlas/ManagedTextureAtlas.d.ts.map +1 -1
  10. package/src/engine/graphics/texture/atlas/ManagedTextureAtlas.js +322 -183
  11. package/src/engine/graphics/texture/atlas/ReferencedTextureAtlas.d.ts +9 -0
  12. package/src/engine/graphics/texture/atlas/ReferencedTextureAtlas.d.ts.map +1 -1
  13. package/src/engine/graphics/texture/atlas/ReferencedTextureAtlas.js +110 -83
  14. package/src/engine/graphics/texture/atlas/SpriteAtlas.d.ts +42 -0
  15. package/src/engine/graphics/texture/atlas/SpriteAtlas.d.ts.map +1 -0
  16. package/src/engine/{graphics3/decal/DecalAtlas.js → graphics/texture/atlas/SpriteAtlas.js} +202 -194
  17. package/src/engine/graphics/texture/atlas/SpriteAtlasFill.d.ts +12 -0
  18. package/src/engine/graphics/texture/atlas/SpriteAtlasFill.d.ts.map +1 -0
  19. package/src/engine/graphics/texture/atlas/SpriteAtlasFill.js +30 -0
  20. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts +7 -0
  21. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
  22. package/src/engine/graphics/texture/atlas/TextureAtlas.js +11 -1
  23. package/src/engine/graphics/texture/atlas/sprite_atlas_border.d.ts +52 -0
  24. package/src/engine/graphics/texture/atlas/sprite_atlas_border.d.ts.map +1 -0
  25. package/src/engine/{graphics3/decal/decal_atlas_border.js → graphics/texture/atlas/sprite_atlas_border.js} +69 -69
  26. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  27. package/src/engine/graphics3/DecalSystem.js +447 -450
  28. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +9 -22
  29. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  30. package/src/engine/graphics3/GPUParticleEmitterSystem.js +892 -951
  31. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  32. package/src/generation/foliage/FOLIAGE_DESIGN.md +2 -1
  33. package/src/generation/foliage/config/BarkMaterialConfig.d.ts +4 -3
  34. package/src/generation/foliage/config/BarkMaterialConfig.d.ts.map +1 -1
  35. package/src/generation/foliage/config/BarkMaterialConfig.js +6 -6
  36. package/src/generation/foliage/config/FoliageBarkType.d.ts +11 -0
  37. package/src/generation/foliage/config/FoliageBarkType.d.ts.map +1 -0
  38. package/src/generation/foliage/config/FoliageBarkType.js +16 -0
  39. package/src/generation/foliage/config/FoliageMaterialConfig.d.ts +58 -5
  40. package/src/generation/foliage/config/FoliageMaterialConfig.d.ts.map +1 -1
  41. package/src/generation/foliage/config/FoliageMaterialConfig.js +279 -181
  42. package/src/generation/foliage/index.d.ts +2 -1
  43. package/src/generation/foliage/index.js +44 -43
  44. package/src/generation/foliage/material/make_bark_material.d.ts.map +1 -1
  45. package/src/generation/foliage/material/make_bark_material.js +23 -95
  46. package/src/generation/foliage/material/make_foliage_material.d.ts +29 -3
  47. package/src/generation/foliage/material/make_foliage_material.d.ts.map +1 -1
  48. package/src/generation/foliage/material/make_foliage_material.js +605 -359
  49. package/src/generation/foliage/mesh/branch_collar_project.d.ts.map +1 -1
  50. package/src/generation/foliage/mesh/branch_collar_project.js +205 -203
  51. package/src/generation/foliage/mesh/foliage_collect_attachments.d.ts +15 -0
  52. package/src/generation/foliage/mesh/foliage_collect_attachments.d.ts.map +1 -1
  53. package/src/generation/foliage/mesh/foliage_collect_attachments.js +569 -502
  54. package/src/generation/foliage/mesh/tree_mesh_build_bark.d.ts.map +1 -1
  55. package/src/generation/foliage/mesh/tree_mesh_build_bark.js +1049 -1047
  56. package/src/generation/foliage/mesh/tree_mesh_build_foliage.d.ts +2 -1
  57. package/src/generation/foliage/mesh/tree_mesh_build_foliage.d.ts.map +1 -1
  58. package/src/generation/foliage/mesh/tree_mesh_build_foliage.js +454 -415
  59. package/src/generation/foliage/presets/make_tree_species_preset.d.ts.map +1 -1
  60. package/src/generation/foliage/presets/make_tree_species_preset.js +870 -837
  61. package/src/generation/foliage/skeleton/crown_envelope_evaluate.js +121 -121
  62. package/src/generation/foliage/skeleton/tree_skeleton_compute_pipe_radii.d.ts +10 -7
  63. package/src/generation/foliage/skeleton/tree_skeleton_compute_pipe_radii.d.ts.map +1 -1
  64. package/src/generation/foliage/skeleton/tree_skeleton_compute_pipe_radii.js +206 -179
  65. package/src/generation/foliage/skeleton/tree_skeleton_grow.d.ts.map +1 -1
  66. package/src/generation/foliage/skeleton/tree_skeleton_grow.js +1871 -1104
  67. package/src/shade/playground/foliage_wind/foliage_world.d.ts +6 -0
  68. package/src/shade/playground/foliage_wind/foliage_world.d.ts.map +1 -1
  69. package/src/shade/playground/foliage_wind/foliage_world.js +602 -593
  70. package/src/shade/playground/foliage_wind/index.html +260 -254
  71. package/src/shade/playground/foliage_wind/main.js +275 -263
  72. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  73. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  74. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  75. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  76. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  77. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +1 -1
  78. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +3 -3
  79. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  80. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +350 -294
  81. package/src/shade/renderer/hiz/generate_downsampler_code.d.ts +0 -2
  82. package/src/shade/renderer/hiz/generate_downsampler_code.d.ts.map +1 -1
  83. package/src/shade/renderer/hiz/generate_downsampler_code.js +99 -100
  84. package/src/shade/renderer/hiz/hzb_pick_alignment.d.ts +15 -0
  85. package/src/shade/renderer/hiz/hzb_pick_alignment.d.ts.map +1 -0
  86. package/src/shade/renderer/hiz/hzb_pick_alignment.js +148 -0
  87. package/src/shade/renderer/hiz/hzb_plan_reduction_steps.d.ts +28 -0
  88. package/src/shade/renderer/hiz/hzb_plan_reduction_steps.d.ts.map +1 -0
  89. package/src/shade/renderer/hiz/hzb_plan_reduction_steps.js +58 -0
  90. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +5 -5
  91. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  92. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +57 -67
  93. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +3 -3
  94. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +1 -1
  95. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.js +111 -112
  96. package/src/shade/renderer/hiz/shader_hzb_spd.d.ts +5 -0
  97. package/src/shade/renderer/hiz/shader_hzb_spd.d.ts.map +1 -0
  98. package/src/shade/renderer/hiz/shader_hzb_spd.js +160 -0
  99. package/src/shade/renderer/light/GPULightCollection.d.ts +8 -0
  100. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  101. package/src/shade/renderer/light/GPULightCollection.js +67 -0
  102. package/src/shade/renderer/light/environment/FAR_FIELD_IRRADIANCE_SH_RESOURCE.d.ts +7 -0
  103. package/src/shade/renderer/light/environment/FAR_FIELD_IRRADIANCE_SH_RESOURCE.d.ts.map +1 -0
  104. package/src/shade/renderer/light/environment/FAR_FIELD_IRRADIANCE_SH_RESOURCE.js +13 -0
  105. package/src/shade/renderer/light/environment/shader_project_far_field_irradiance_sh.d.ts +10 -0
  106. package/src/shade/renderer/light/environment/shader_project_far_field_irradiance_sh.d.ts.map +1 -0
  107. package/src/shade/renderer/light/environment/shader_project_far_field_irradiance_sh.js +72 -0
  108. package/src/shade/renderer/light/world_cluster/WORLD_LIGHT_CLUSTER.d.ts +39 -0
  109. package/src/shade/renderer/light/world_cluster/WORLD_LIGHT_CLUSTER.d.ts.map +1 -0
  110. package/src/shade/renderer/light/world_cluster/WORLD_LIGHT_CLUSTER.js +90 -0
  111. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts +12 -0
  112. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts.map +1 -0
  113. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.js +56 -0
  114. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts +31 -0
  115. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts.map +1 -0
  116. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.js +196 -0
  117. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts +10 -0
  118. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts.map +1 -0
  119. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.js +163 -0
  120. package/src/shade/renderer/light/world_cluster/shader_world_cluster_cull_lights.d.ts +4 -0
  121. package/src/shade/renderer/light/world_cluster/shader_world_cluster_cull_lights.d.ts.map +1 -0
  122. package/src/shade/renderer/light/world_cluster/shader_world_cluster_cull_lights.js +137 -0
  123. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  124. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  125. package/src/shade/renderer/particles/GPUParticleSystem.js +4 -3
  126. package/src/shade/renderer/particles/graph_particles_avboit.d.ts +4 -3
  127. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  128. package/src/shade/renderer/particles/graph_particles_avboit.js +28 -11
  129. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +9 -13
  130. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
  131. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +25 -20
  132. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
  133. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +21 -38
  134. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  135. package/src/shade/renderer/shader/chunk/depth/chunk_query_depth_from_screen_space_bb.d.ts.map +1 -1
  136. package/src/shade/renderer/shader/chunk/depth/chunk_query_depth_from_screen_space_bb.js +62 -62
  137. package/src/shade/renderer/shader/chunk/random/lowbias/chunk_hash_lowbias32_vec2u.d.ts +6 -0
  138. package/src/shade/renderer/shader/chunk/random/lowbias/chunk_hash_lowbias32_vec2u.d.ts.map +1 -0
  139. package/src/shade/renderer/shader/chunk/random/lowbias/chunk_hash_lowbias32_vec2u.js +19 -0
  140. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_get_mip_resolution.d.ts.map +1 -1
  141. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_get_mip_resolution.js +16 -10
  142. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_hierarchical_raymarch.d.ts.map +1 -1
  143. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_hierarchical_raymarch.js +118 -117
  144. package/src/shade/renderer/texture/atlas/GPUTileAtlas3D.d.ts +41 -7
  145. package/src/shade/renderer/texture/atlas/GPUTileAtlas3D.d.ts.map +1 -1
  146. package/src/shade/renderer/texture/atlas/GPUTileAtlas3D.js +162 -58
  147. package/src/shade/renderer/volumetrics/GPUSceneVolumetrics.js +1 -1
  148. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +10 -12
  149. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  150. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +583 -606
  151. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_sample.d.ts +24 -0
  152. package/src/shade/renderer/volumetrics/chunk_volumetrics_froxel_sample.d.ts.map +1 -0
  153. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi_sample.d.ts.map +1 -1
  154. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +1 -1
  155. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +640 -640
  156. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts +113 -0
  157. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts.map +1 -0
  158. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.js +484 -0
  159. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP.d.ts +50 -0
  160. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP.d.ts.map +1 -0
  161. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP.js +60 -0
  162. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP_STRUCT.d.ts +25 -0
  163. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP_STRUCT.d.ts.map +1 -0
  164. package/src/shade/renderer/volumetrics/irradiance_clipmap/IRRADIANCE_CLIPMAP_STRUCT.js +47 -0
  165. package/src/shade/renderer/volumetrics/irradiance_clipmap/IrradianceCascadePlacement.d.ts +74 -0
  166. package/src/shade/renderer/volumetrics/irradiance_clipmap/IrradianceCascadePlacement.d.ts.map +1 -0
  167. package/src/shade/renderer/volumetrics/irradiance_clipmap/IrradianceCascadePlacement.js +173 -0
  168. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts +26 -0
  169. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts.map +1 -0
  170. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.js +83 -0
  171. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_local_light.d.ts +31 -0
  172. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_local_light.d.ts.map +1 -0
  173. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_local_light.js +113 -0
  174. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_sample.d.ts +121 -0
  175. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_sample.d.ts.map +1 -0
  176. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_sample.js +356 -0
  177. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.d.ts +16 -0
  178. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.d.ts.map +1 -0
  179. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.js +191 -0
  180. package/src/shade/renderer/volumetrics/shader_volumetrics_classify_froxels.d.ts +16 -0
  181. package/src/shade/renderer/volumetrics/shader_volumetrics_classify_froxels.d.ts.map +1 -0
  182. package/src/shade/renderer/volumetrics/shader_volumetrics_lighting_regimes.d.ts +23 -0
  183. package/src/shade/renderer/volumetrics/shader_volumetrics_lighting_regimes.d.ts.map +1 -0
  184. package/src/shade/renderer/wind/GPUSceneWind.d.ts +1 -1
  185. package/src/shade/renderer/wind/GPUSceneWind.js +1 -1
  186. package/src/engine/graphics3/decal/DecalAtlasFill.js +0 -32
  187. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.js +0 -49
  188. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +0 -50
  189. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi_sample.js +0 -105
  190. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +0 -80
  191. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi.js +0 -51
  192. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.js +0 -39
  193. package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.js +0 -121
  194. package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.js +0 -237
@@ -1,359 +1,605 @@
1
- import { clamp } from "../../../core/math/clamp.js";
2
- import { lerp } from "../../../core/math/lerp.js";
3
- import { sdnoise3_array } from "../../../core/math/noise/sdnoise.js";
4
- import { seededRandom } from "../../../core/math/random/seededRandom.js";
5
- import { smoothStep } from "../../../core/math/smoothStep.js";
6
- import { Sampler2D } from "../../../engine/graphics/texture/sampler/Sampler2D.js";
7
- import { ShadeDrawSide } from "../../../shade/renderer/material/ShadeDrawSide.js";
8
- import { StandardShadeMaterial } from "../../../shade/renderer/material/StandardShadeMaterial.js";
9
- import { TransparencyMode } from "../../../shade/renderer/material/TransparencyMode.js";
10
- import { ColorSpace } from "../../../shade/renderer/texture/ColorSpace.js";
11
- import { ShadeTexture } from "../../../shade/renderer/texture/ShadeTexture.js";
12
- import { TextureFilterType } from "../../../shade/renderer/texture/TextureFilterType.js";
13
- import { TextureWrapType } from "../../../shade/renderer/texture/TextureWrapType.js";
14
- import { AlphaMode } from "../../../shade/renderer/texture/source/AlphaMode.js";
15
- import { ShadeImage } from "../../../shade/renderer/texture/source/ShadeImage.js";
16
- import { FoliageMaterialConfig } from "../config/FoliageMaterialConfig.js";
17
-
18
- const SCRATCH_NOISE = new Float32Array(4);
19
-
20
- /**
21
- * Evaluates a single leaflet SDF + venation relief in local leaflet coordinates `(lx, ly)`
22
- * where `ly` in `[0, 1]` runs from petiole base to leaflet tip and `lx` in `[-0.5, 0.5]` runs across width.
23
- *
24
- * @param {number} lx
25
- * @param {number} ly
26
- * @param {string} shape `"ovate" | "lobed" | "serrated" | "palmate" | "needle" | "frond"`
27
- * @returns {{ mask: number, vein: number, height: number }}
28
- */
29
- function evaluate_leaflet(lx, ly, shape) {
30
- if (ly < -0.05 || ly > 1.05) {
31
- return { mask: 0.0, vein: 0.0, height: 0.0 };
32
- }
33
-
34
- const t = clamp(ly, 0.0, 1.0);
35
- const ax = Math.abs(lx);
36
-
37
- let half_w = 0.34 * Math.sin(Math.PI * Math.pow(t, 0.72));
38
-
39
- switch (shape) {
40
- case "lobed": {
41
- // Oak-like rounded lobes along margin
42
- const lobe = 1.0 + 0.28 * Math.sin(t * Math.PI * 7.0 - 0.6) * Math.sin(Math.PI * t);
43
- half_w *= lobe;
44
- break;
45
- }
46
- case "serrated": {
47
- // Birch/elm sawtooth serration
48
- const tooth = (t * 14.0) % 1.0;
49
- half_w *= (0.88 + 0.16 * (1.0 - tooth));
50
- break;
51
- }
52
- case "palmate": {
53
- // Maple-like broader lobed palmate blade
54
- const lobe = 1.0 + 0.32 * Math.cos(t * Math.PI * 5.0);
55
- half_w = 0.40 * Math.sin(Math.PI * Math.pow(t, 0.65)) * lobe;
56
- break;
57
- }
58
- case "needle": {
59
- // Slender conifer needle
60
- half_w = 0.065 * (1.0 - 0.65 * t * t);
61
- break;
62
- }
63
- case "frond": {
64
- // Long tapering willow/palm/fern pinna
65
- half_w = 0.19 * Math.sin(Math.PI * Math.pow(t, 0.6));
66
- break;
67
- }
68
- case "ovate":
69
- default:
70
- break;
71
- }
72
-
73
- if (ax > half_w + 0.04) {
74
- return { mask: 0.0, vein: 0.0, height: 0.0 };
75
- }
76
-
77
- const edge_dist = half_w - ax;
78
- const mask = smoothStep(-0.015, 0.018, edge_dist);
79
- if (mask <= 0.0) {
80
- return { mask: 0.0, vein: 0.0, height: 0.0 };
81
- }
82
-
83
- // Midrib + herringbone secondary veins
84
- const midrib = Math.exp(-ax * ax * 900.0) * (1.0 - 0.6 * t);
85
- const sec_coord = (t * 9.0 - ax * 6.5);
86
- const sec_frac = sec_coord - Math.floor(sec_coord);
87
- const sec_vein = Math.exp(-Math.pow(sec_frac - 0.5, 2.0) * 65.0) * smoothStep(0.0, half_w, half_w - ax * 0.85) * 0.45;
88
-
89
- const vein = clamp(midrib + sec_vein, 0.0, 1.0);
90
- // Lamina pillow camber across width + vein ridge
91
- const camber = Math.sqrt(Math.max(0.0, 1.0 - Math.pow(ax / Math.max(1e-3, half_w), 2.0))) * 0.65;
92
- const height = (camber + vein * 0.35) * mask;
93
-
94
- return { mask, vein, height };
95
- }
96
-
97
- /**
98
- * Synthesizes a turn-key procedural PBR cutout `StandardShadeMaterial` (`TransparencyMode.AlphaTested`,
99
- * `ShadeDrawSide.Double`) for botanical foliage cards, with dilated straight-alpha Albedo
100
- * (`AlphaMode.Straight`, `ColorSpace.SRGB`), 2-channel Normal map (`ColorSpace.LinearSRGB`),
101
- * and ORM (`ColorSpace.LinearSRGB`).
102
- *
103
- * @param {FoliageMaterialConfig|Object} [config]
104
- * @param {number} [seed=1]
105
- * @returns {StandardShadeMaterial}
106
- */
107
- export function make_foliage_material(config = new FoliageMaterialConfig(), seed = 1) {
108
- const cfg = config && config.isFoliageMaterialConfig ? config : FoliageMaterialConfig.from(config);
109
- const res = Math.max(16, cfg.resolution | 0);
110
- const rng = seededRandom((seed ^ 0x7F4A7C15) | 0);
111
- const ox = (rng() - 0.5) * 150.0;
112
- const oy = (rng() - 0.5) * 150.0;
113
-
114
- const albedo_sampler = Sampler2D.uint8(4, res, res);
115
- const normal_sampler = Sampler2D.uint8(2, res, res);
116
- const orm_sampler = Sampler2D.uint8(4, res, res);
117
-
118
- const albedo_data = albedo_sampler.data;
119
- const normal_data = normal_sampler.data;
120
- const orm_data = orm_sampler.data;
121
-
122
- const height_field = new Float32Array(res * res);
123
- const shape = cfg.leaf_shape || "lobed";
124
- const leaflet_count = Math.max(1, cfg.leaflets_per_cluster | 0);
125
- const base_rough = clamp(cfg.roughness, 0.1, 1.0);
126
- const vein_str = clamp(cfg.vein_strength, 0.0, 1.0);
127
- const autumn = clamp(cfg.autumn_tint, 0.0, 1.0);
128
-
129
- // Base leaf color (mixed with optional autumn golden-orange tint)
130
- const r1 = lerp(cfg.primary_color.r, 0.78, autumn * 0.7);
131
- const g1 = lerp(cfg.primary_color.g, 0.36, autumn * 0.5);
132
- const b1 = lerp(cfg.primary_color.b, 0.08, autumn * 0.7);
133
-
134
- const r2 = lerp(cfg.secondary_color.r, 0.88, autumn * 0.75);
135
- const g2 = lerp(cfg.secondary_color.g, 0.55, autumn * 0.5);
136
- const b2 = lerp(cfg.secondary_color.b, 0.12, autumn * 0.75);
137
-
138
- // Precompute leaflet placement descriptors on the `[0, 1]^2` card anchored at `(anchor_u, anchor_v)`
139
- const anchor_u = clamp(cfg.stem_anchor_u ?? 0.5, 0.1, 0.9);
140
- const anchor_v = clamp(cfg.stem_anchor_v ?? 0.0, 0.0, 0.25);
141
- const leaflets = [];
142
- if (shape === "palmate") {
143
- // Radiating palmate leaflets from basal hub above (anchor_u, anchor_v)
144
- const count = Math.max(3, leaflet_count | 1);
145
- for (let i = 0; i < count; i++) {
146
- const frac = (i / (count - 1)) * 2.0 - 1.0; // [-1, 1]
147
- const angle = frac * 1.05;
148
- const scale = (1.0 - 0.32 * Math.abs(frac)) * 0.76;
149
- leaflets.push({
150
- ox: anchor_u,
151
- oy: anchor_v + 0.08,
152
- dir_x: Math.sin(angle),
153
- dir_y: Math.cos(angle),
154
- length: scale,
155
- width: scale * 0.9
156
- });
157
- }
158
- } else if (shape === "needle") {
159
- // Fascicle of radiating conifer needles along central twig from anchor_v
160
- const pairs = Math.max(4, leaflet_count);
161
- for (let i = 0; i < pairs; i++) {
162
- const v_stem = anchor_v + 0.04 + (i / pairs) * 0.74;
163
- for (const side of [-1, 1]) {
164
- const angle = side * (0.45 + 0.25 * (1.0 - i / pairs));
165
- const len = 0.36 * (1.0 - 0.35 * (i / pairs));
166
- leaflets.push({
167
- ox: anchor_u,
168
- oy: v_stem,
169
- dir_x: Math.sin(angle),
170
- dir_y: Math.cos(angle),
171
- length: len,
172
- width: len
173
- });
174
- }
175
- }
176
- // Terminal needle tuft
177
- leaflets.push({ ox: anchor_u, oy: anchor_v + 0.72, dir_x: 0.0, dir_y: 1.0, length: 0.26, width: 0.26 });
178
- } else {
179
- // Pinnate twig cluster: pairs of lateral leaflets + 1 terminal apical leaflet starting near anchor_v
180
- const pairs = Math.max(1, Math.floor(leaflet_count / 2));
181
- for (let p = 0; p < pairs; p++) {
182
- const u_p = (p + 0.35) / (pairs + 0.35);
183
- const v_stem = anchor_v + 0.04 + u_p * 0.56;
184
- const angle_mag = lerp(0.95, 0.55, u_p);
185
- const len = lerp(0.40, 0.30, u_p);
186
- for (const side of [-1, 1]) {
187
- const angle = side * angle_mag;
188
- leaflets.push({
189
- ox: anchor_u,
190
- oy: v_stem,
191
- dir_x: Math.sin(angle),
192
- dir_y: Math.cos(angle),
193
- length: len,
194
- width: len * 0.95
195
- });
196
- }
197
- }
198
- // Terminal apical leaflet
199
- leaflets.push({
200
- ox: anchor_u,
201
- oy: anchor_v + 0.56,
202
- dir_x: 0.0,
203
- dir_y: 1.0,
204
- length: 0.42,
205
- width: 0.40
206
- });
207
- }
208
-
209
- const min_stem_half_w = Math.max(0.022, 1.5 / res);
210
- for (let y = 0; y < res; y++) {
211
- const v = (y + 0.5) / res;
212
- for (let x = 0; x < res; x++) {
213
- const u = (x + 0.5) / res;
214
- const p_idx = y * res + x;
215
-
216
- // Central woody twig / rachis along u = anchor_u, v in [anchor_v, anchor_v + 0.72]
217
- let stem_mask = 0.0;
218
- if (v >= anchor_v && v <= anchor_v + 0.72) {
219
- const u_stem = clamp((v - anchor_v) / 0.72, 0.0, 1.0);
220
- const stem_half_w = lerp(min_stem_half_w, 0.007, u_stem);
221
- const d_stem = stem_half_w - Math.abs(u - anchor_u);
222
- stem_mask = smoothStep(-0.008, 0.008, d_stem);
223
- }
224
-
225
- let best_mask = 0.0;
226
- let best_vein = 0.0;
227
- let best_h = 0.0;
228
- let best_tip = 0.0;
229
-
230
- for (let l = 0; l < leaflets.length; l++) {
231
- const lf = leaflets[l];
232
- const dx = u - lf.ox;
233
- const dy = v - lf.oy;
234
- // Project onto leaflet forward (dir_x, dir_y) and right (dir_y, -dir_x)
235
- const ly = (dx * lf.dir_x + dy * lf.dir_y) / lf.length;
236
- const lx = (dx * lf.dir_y - dy * lf.dir_x) / lf.width;
237
-
238
- const eval_res = evaluate_leaflet(lx, ly, shape);
239
- if (eval_res.mask > best_mask) {
240
- best_mask = eval_res.mask;
241
- best_vein = eval_res.vein;
242
- best_h = eval_res.height;
243
- best_tip = clamp(ly, 0.0, 1.0);
244
- }
245
- }
246
-
247
- const alpha = clamp(Math.max(best_mask, stem_mask), 0.0, 1.0);
248
- height_field[p_idx] = Math.max(best_h, stem_mask * 0.4);
249
-
250
- sdnoise3_array(SCRATCH_NOISE, u * 6.0 + ox, v * 6.0 + oy, 0.0);
251
- const n_var = SCRATCH_NOISE[0] * 0.5 + 0.5;
252
-
253
- // Compute straight RGB even when alpha === 0 (automatic infinite dilation for mipmap bleeding safety!)
254
- const mix_t = clamp(best_vein * vein_str * 0.65 + best_tip * 0.25 + n_var * 0.2, 0.0, 1.0);
255
- let cr = lerp(r1, r2, mix_t);
256
- let cg = lerp(g1, g2, mix_t);
257
- let cb = lerp(b1, b2, mix_t);
258
-
259
- if (stem_mask > best_mask) {
260
- cr = lerp(cr, 0.32, stem_mask * 0.75);
261
- cg = lerp(cg, 0.26, stem_mask * 0.75);
262
- cb = lerp(cb, 0.16, stem_mask * 0.75);
263
- }
264
-
265
- // Subtle basal petiole ambient darkening
266
- const base_shade = 0.82 + 0.18 * v;
267
- cr *= base_shade;
268
- cg *= base_shade;
269
- cb *= base_shade;
270
-
271
- const i4 = p_idx * 4;
272
- albedo_data[i4] = clamp(Math.round(cr * 255), 0, 255);
273
- albedo_data[i4 + 1] = clamp(Math.round(cg * 255), 0, 255);
274
- albedo_data[i4 + 2] = clamp(Math.round(cb * 255), 0, 255);
275
- albedo_data[i4 + 3] = clamp(Math.round(alpha * 255), 0, 255);
276
-
277
- const ao = clamp(0.75 + 0.25 * best_h, 0.25, 1.0);
278
- const rough = clamp(base_rough - best_vein * 0.08 + (1.0 - n_var) * 0.06, 0.15, 0.95);
279
- orm_data[i4] = clamp(Math.round(ao * 255), 0, 255);
280
- orm_data[i4 + 1] = clamp(Math.round(rough * 255), 0, 255);
281
- orm_data[i4 + 2] = 0;
282
- orm_data[i4 + 3] = 255;
283
- }
284
- }
285
-
286
- // Pass 2: Central-difference tangent-space normal map from leaf venation heightfield
287
- const norm_str = 1.6 * vein_str + 0.35;
288
- for (let y = 0; y < res; y++) {
289
- const y0 = Math.max(0, y - 1);
290
- const y1 = Math.min(res - 1, y + 1);
291
- for (let x = 0; x < res; x++) {
292
- const x0 = Math.max(0, x - 1);
293
- const x1 = Math.min(res - 1, x + 1);
294
-
295
- const dh_du = (height_field[y * res + x1] - height_field[y * res + x0]) * 0.5 * norm_str;
296
- const dh_dv = (height_field[y1 * res + x] - height_field[y0 * res + x]) * 0.5 * norm_str;
297
-
298
- let nx = -dh_du;
299
- let ny = -dh_dv;
300
- let nz = 1.0;
301
- const nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
302
- nx /= nl;
303
- ny /= nl;
304
-
305
- const i2 = (y * res + x) * 2;
306
- normal_data[i2] = clamp(Math.round((nx * 0.5 + 0.5) * 255), 0, 255);
307
- normal_data[i2 + 1] = clamp(Math.round((ny * 0.5 + 0.5) * 255), 0, 255);
308
- }
309
- }
310
-
311
- const albedo_image = ShadeImage.fromSampler2D(albedo_sampler);
312
- albedo_image.color_space = ColorSpace.SRGB;
313
- // CRITICAL: Keep AlphaMode.Straight so texture_write_to_gpu premultiplies once on upload
314
- // and G-buffer albedo_unpremultiply recovers clean dilated leaf color at cutout boundaries.
315
- albedo_image.alpha_mode = AlphaMode.Straight;
316
- const texture_albedo = ShadeTexture.from(albedo_image);
317
- texture_albedo.label = "foliage_leaf_albedo";
318
- texture_albedo.color_space = ColorSpace.SRGB;
319
- texture_albedo.mipmapGenerationFilter = TextureFilterType.MagicKernelSharp;
320
- texture_albedo.wrapS = TextureWrapType.ClampToEdge;
321
- texture_albedo.wrapT = TextureWrapType.ClampToEdge;
322
-
323
- const normal_image = ShadeImage.fromSampler2D(normal_sampler);
324
- normal_image.color_space = ColorSpace.None;
325
- normal_image.alpha_mode = AlphaMode.Opaque;
326
- const texture_normal = ShadeTexture.from(normal_image);
327
- texture_normal.label = "foliage_leaf_normal";
328
- texture_normal.color_space = ColorSpace.LinearSRGB;
329
- texture_normal.mipmapGenerationFilter = TextureFilterType.LinearNormal;
330
- texture_normal.wrapS = TextureWrapType.ClampToEdge;
331
- texture_normal.wrapT = TextureWrapType.ClampToEdge;
332
-
333
- const orm_image = ShadeImage.fromSampler2D(orm_sampler);
334
- orm_image.color_space = ColorSpace.None;
335
- orm_image.alpha_mode = AlphaMode.Opaque;
336
- const texture_orm = ShadeTexture.from(orm_image);
337
- texture_orm.label = "foliage_leaf_orm";
338
- texture_orm.color_space = ColorSpace.LinearSRGB;
339
- texture_orm.wrapS = TextureWrapType.ClampToEdge;
340
- texture_orm.wrapT = TextureWrapType.ClampToEdge;
341
-
342
- const material = new StandardShadeMaterial();
343
- material.name = "foliage_leaf_material";
344
- material.texture_albedo = texture_albedo;
345
- material.texture_normal = texture_normal;
346
- material.texture_orm = texture_orm;
347
- material.roughness_factor = 1.0;
348
- material.metallic_factor = 0.0;
349
- // CRITICAL: Keep transmission_factor = 0 on AlphaTested cards so transparent card corners
350
- // do not act as refractive glass in the path tracer!
351
- material.transmission_factor = 0.0;
352
- material.ambient_factors.a = 1.0;
353
- material.ambient_factors.b = 0.0;
354
- material.transparency_mode = TransparencyMode.AlphaTested;
355
- material.transparency = TransparencyMode.AlphaTested;
356
- material.draw_side = ShadeDrawSide.Double;
357
-
358
- return material;
359
- }
1
+ import AABB2 from "../../../core/geom/2d/aabb/AABB2.js";
2
+ import { pack_max_rectangles } from "../../../core/geom/packing/max-rect/pack_max_rectangles.js";
3
+ import { clamp } from "../../../core/math/clamp.js";
4
+ import { lerp } from "../../../core/math/lerp.js";
5
+ import { sdnoise3_array } from "../../../core/math/noise/sdnoise.js";
6
+ import { seededRandom } from "../../../core/math/random/seededRandom.js";
7
+ import { smoothStep } from "../../../core/math/smoothStep.js";
8
+ import { Sampler2D } from "../../../engine/graphics/texture/sampler/Sampler2D.js";
9
+ import { ShadeDrawSide } from "../../../shade/renderer/material/ShadeDrawSide.js";
10
+ import { StandardShadeMaterial } from "../../../shade/renderer/material/StandardShadeMaterial.js";
11
+ import { TransparencyMode } from "../../../shade/renderer/material/TransparencyMode.js";
12
+ import { ColorSpace } from "../../../shade/renderer/texture/ColorSpace.js";
13
+ import { ShadeTexture } from "../../../shade/renderer/texture/ShadeTexture.js";
14
+ import { TextureFilterType } from "../../../shade/renderer/texture/TextureFilterType.js";
15
+ import { TextureWrapType } from "../../../shade/renderer/texture/TextureWrapType.js";
16
+ import { AlphaMode } from "../../../shade/renderer/texture/source/AlphaMode.js";
17
+ import { ShadeImage } from "../../../shade/renderer/texture/source/ShadeImage.js";
18
+ import { FoliageMaterialConfig } from "../config/FoliageMaterialConfig.js";
19
+
20
+ const SCRATCH_NOISE = new Float32Array(4);
21
+
22
+ /**
23
+ * Packs `variant_count` square foliage card tiles of size `resolution x resolution` into a 2D atlas
24
+ * using the engine's `MaxRectanglesPacker` (`pack_max_rectangles`), returning the atlas dimensions,
25
+ * pixel-space `AABB2` boxes, and normalized `[u0, v0, u1, v1]` UV rectangles.
26
+ *
27
+ * @param {number} [resolution=128]
28
+ * @param {number} [variant_count=4]
29
+ * @returns {{
30
+ * resolution: number,
31
+ * variant_count: number,
32
+ * atlas_w: number,
33
+ * atlas_h: number,
34
+ * boxes: AABB2[],
35
+ * uv_rects: Float32Array
36
+ * }}
37
+ */
38
+ export function compute_foliage_variant_atlas_layout(resolution = 128, variant_count = 4) {
39
+ const res = Math.max(16, resolution | 0);
40
+ const count = clamp(variant_count | 0, 1, 16);
41
+ const cols = Math.max(1, Math.ceil(Math.sqrt(count)));
42
+ const rows = Math.max(1, Math.ceil(count / cols));
43
+ const atlas_w = cols * res;
44
+ const atlas_h = rows * res;
45
+
46
+ /** @type {AABB2[]} */
47
+ const boxes = new Array(count);
48
+ for (let k = 0; k < count; k++) {
49
+ boxes[k] = new AABB2(0, 0, res, res);
50
+ }
51
+
52
+ const packed = pack_max_rectangles(atlas_w, atlas_h, boxes);
53
+ if (!packed) {
54
+ // Deterministic grid fallback if custom non-uniform boxes are ever used
55
+ for (let k = 0; k < count; k++) {
56
+ const c = k % cols;
57
+ const r = Math.floor(k / cols);
58
+ boxes[k].set(c * res, r * res, (c + 1) * res, (r + 1) * res);
59
+ }
60
+ }
61
+
62
+ const uv_rects = new Float32Array(count * 4);
63
+ for (let k = 0; k < count; k++) {
64
+ const b = boxes[k];
65
+ const k4 = k * 4;
66
+ uv_rects[k4] = b.x0 / atlas_w;
67
+ uv_rects[k4 + 1] = b.y0 / atlas_h;
68
+ uv_rects[k4 + 2] = b.x1 / atlas_w;
69
+ uv_rects[k4 + 3] = b.y1 / atlas_h;
70
+ }
71
+
72
+ return {
73
+ resolution: res,
74
+ variant_count: count,
75
+ atlas_w,
76
+ atlas_h,
77
+ boxes,
78
+ uv_rects
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Evaluates a single leaflet SDF, ontogenetic morphology, and venation relief in local leaflet
84
+ * coordinates `(lx, ly)` where `ly` in `[0, 1]` runs from petiole base to leaflet tip and `lx`
85
+ * in `[-0.5, 0.5]` runs across width.
86
+ *
87
+ * @param {number} lx
88
+ * @param {number} ly
89
+ * @param {string} shape `"ovate" | "lobed" | "serrated" | "palmate" | "needle" | "frond"`
90
+ * @param {number} mu_k ontogenetic maturity in `[0, 1]` (`0` = juvenile, `1` = mature)
91
+ * @param {number} shape_var ontogenetic morphology variation strength `[0, 1]`
92
+ * @param {number} vein_contrast ontogenetic venation & bullation contrast `[0, 1]`
93
+ * @returns {{ mask: number, vein: number, height: number, edge_norm: number }}
94
+ */
95
+ function evaluate_leaflet(lx, ly, shape, mu_k, shape_var, vein_contrast) {
96
+ if (ly < -0.05 || ly > 1.05) {
97
+ return { mask: 0.0, vein: 0.0, height: 0.0, edge_norm: 0.0 };
98
+ }
99
+
100
+ const t = clamp(ly, 0.0, 1.0);
101
+ const ax = Math.abs(lx);
102
+
103
+ // Juvenile leaves expand longitudinally before full lateral blade expansion (narrower, more acuminate)
104
+ const width_scale = lerp(1.0 - 0.26 * shape_var, 1.0 + 0.08 * shape_var, mu_k);
105
+ const lobe_scale = lerp(1.0 - 0.45 * shape_var, 1.0 + 0.15 * shape_var, mu_k);
106
+ const profile_exp = lerp(0.84, 0.68, mu_k);
107
+
108
+ let half_w = 0.34 * width_scale * Math.sin(Math.PI * Math.pow(t, profile_exp));
109
+
110
+ switch (shape) {
111
+ case "lobed": {
112
+ // Oak-like rounded lobes along margin deepen as lamina matures
113
+ const lobe = 1.0 + 0.28 * lobe_scale * Math.sin(t * Math.PI * 7.0 - 0.6) * Math.sin(Math.PI * t);
114
+ half_w *= lobe;
115
+ break;
116
+ }
117
+ case "serrated": {
118
+ // Birch/elm sawtooth serration
119
+ const tooth = (t * 14.0) % 1.0;
120
+ half_w *= (1.0 - 0.12 * lobe_scale + 0.16 * lobe_scale * (1.0 - tooth));
121
+ break;
122
+ }
123
+ case "palmate": {
124
+ // Maple-like broader lobed palmate blade
125
+ const lobe = 1.0 + 0.32 * lobe_scale * Math.cos(t * Math.PI * 5.0);
126
+ half_w = 0.40 * width_scale * Math.sin(Math.PI * Math.pow(t, lerp(0.76, 0.62, mu_k))) * lobe;
127
+ break;
128
+ }
129
+ case "needle": {
130
+ // Slender conifer needle
131
+ half_w = 0.065 * lerp(0.85, 1.05, mu_k) * (1.0 - 0.65 * t * t);
132
+ break;
133
+ }
134
+ case "frond": {
135
+ // Long tapering willow/palm/fern pinna
136
+ half_w = 0.19 * width_scale * Math.sin(Math.PI * Math.pow(t, lerp(0.70, 0.56, mu_k)));
137
+ break;
138
+ }
139
+ case "ovate":
140
+ default:
141
+ break;
142
+ }
143
+
144
+ if (ax > half_w + 0.04) {
145
+ return { mask: 0.0, vein: 0.0, height: 0.0, edge_norm: 0.0 };
146
+ }
147
+
148
+ const edge_dist = half_w - ax;
149
+ const mask = smoothStep(-0.015, 0.018, edge_dist);
150
+ if (mask <= 0.0) {
151
+ return { mask: 0.0, vein: 0.0, height: 0.0, edge_norm: 0.0 };
152
+ }
153
+
154
+ const inv_hw = 1.0 / Math.max(1e-3, half_w);
155
+ const edge_norm = clamp(edge_dist * inv_hw, 0.0, 1.0);
156
+
157
+ // Midrib + herringbone secondary veins (sharper and more lignified on mature leaves)
158
+ const midrib_sharp = lerp(700.0, 1050.0, mu_k);
159
+ const midrib = Math.exp(-ax * ax * midrib_sharp) * (1.0 - 0.58 * t);
160
+ const sec_coord = (t * 9.0 - ax * 6.5);
161
+ const sec_frac = sec_coord - Math.floor(sec_coord);
162
+ const sec_sharp = lerp(36.0, 80.0, mu_k * vein_contrast + 0.25);
163
+ const sec_amp = lerp(0.28, 0.54, mu_k);
164
+ const sec_vein = Math.exp(-Math.pow(sec_frac - 0.5, 2.0) * sec_sharp)
165
+ * smoothStep(0.0, half_w, half_w - ax * 0.85)
166
+ * sec_amp;
167
+
168
+ const vein_relief = lerp(1.0 - 0.62 * vein_contrast, 1.0 + 0.34 * vein_contrast, mu_k);
169
+ const vein = clamp((midrib + sec_vein) * vein_relief, 0.0, 1.0);
170
+
171
+ // Lamina pillow camber across width + mature interveinal mesophyll bullation + vein ridge
172
+ const camber = Math.sqrt(Math.max(0.0, 1.0 - Math.pow(ax * inv_hw, 2.0))) * lerp(0.45, 0.68, mu_k);
173
+ const bullation = Math.sin(sec_frac * Math.PI)
174
+ * smoothStep(0.0, 0.65, edge_norm)
175
+ * (1.0 - midrib)
176
+ * lerp(0.02, 0.26, mu_k)
177
+ * vein_contrast;
178
+ const height = (camber + bullation + vein * 0.40) * mask;
179
+
180
+ return { mask, vein, height, edge_norm };
181
+ }
182
+
183
+ /**
184
+ * Synthesizes a turn-key procedural PBR cutout `StandardShadeMaterial` (`TransparencyMode.AlphaTested`,
185
+ * `ShadeDrawSide.Double`) for botanical foliage cards, packing `config.variant_count` ontogenetic and
186
+ * biochemically senescent leaf cluster variants into the texture atlas via `MaxRectanglesPacker`
187
+ * (`pack_max_rectangles`), with dilated straight-alpha Albedo (`AlphaMode.Straight`, `ColorSpace.SRGB`),
188
+ * 2-channel Normal map (`ColorSpace.LinearSRGB`), and ORM (`ColorSpace.LinearSRGB`).
189
+ *
190
+ * @param {FoliageMaterialConfig|Object} [config]
191
+ * @param {number} [seed=1]
192
+ * @returns {StandardShadeMaterial}
193
+ */
194
+ export function make_foliage_material(config = new FoliageMaterialConfig(), seed = 1) {
195
+ const cfg = config && config.isFoliageMaterialConfig ? config : FoliageMaterialConfig.from(config);
196
+ const layout = compute_foliage_variant_atlas_layout(cfg.resolution, cfg.variant_count);
197
+ const res = layout.resolution;
198
+ const variant_count = layout.variant_count;
199
+ const atlas_w = layout.atlas_w;
200
+ const atlas_h = layout.atlas_h;
201
+ const boxes = layout.boxes;
202
+
203
+ const rng = seededRandom((seed ^ 0x7F4A7C15) | 0);
204
+
205
+ const albedo_sampler = Sampler2D.uint8(4, atlas_w, atlas_h);
206
+ const normal_sampler = Sampler2D.uint8(2, atlas_w, atlas_h);
207
+ const orm_sampler = Sampler2D.uint8(4, atlas_w, atlas_h);
208
+
209
+ const albedo_data = albedo_sampler.data;
210
+ const normal_data = normal_sampler.data;
211
+ const orm_data = orm_sampler.data;
212
+
213
+ const height_field = new Float32Array(atlas_w * atlas_h);
214
+ const shape = cfg.leaf_shape || "lobed";
215
+ const leaflet_count = Math.max(1, cfg.leaflets_per_cluster | 0);
216
+ const base_rough = clamp(cfg.roughness, 0.1, 1.0);
217
+ const vein_str = clamp(cfg.vein_strength, 0.0, 1.0);
218
+
219
+ // Ontogeny & first-principles biochemical senescence parameters
220
+ const juv_shift = clamp(cfg.juvenile_color_shift ?? 0.38, 0.0, 1.0);
221
+ const juv_antho = clamp(cfg.juvenile_anthocyanin ?? 0.08, 0.0, 1.0);
222
+ const shape_var = clamp(cfg.ontogeny_shape_variation ?? 0.35, 0.0, 1.0);
223
+ const vein_contrast = clamp(cfg.ontogeny_vein_contrast ?? 0.55, 0.0, 1.0);
224
+
225
+ // Honour both first-principles `senescence` and legacy `autumn_tint`
226
+ const senescence = clamp(Math.max(cfg.senescence ?? 0.0, cfg.autumn_tint ?? 0.0), 0.0, 1.0);
227
+ const car_density = clamp(cfg.carotenoid_density ?? 0.75, 0.0, 1.0);
228
+ const antho_pot = clamp(cfg.anthocyanin_potential ?? 0.35, 0.0, 1.0);
229
+ const tannin_ox = clamp(cfg.tannin_oxidation ?? 0.25, 0.0, 1.0);
230
+
231
+ const p_r = cfg.primary_color.r;
232
+ const p_g = cfg.primary_color.g;
233
+ const p_b = cfg.primary_color.b;
234
+ const s_r = cfg.secondary_color.r;
235
+ const s_g = cfg.secondary_color.g;
236
+ const s_b = cfg.secondary_color.b;
237
+
238
+ // Pre-fill entire atlas with dilated base RGB (alpha = 0) and neutral normal/ORM so any
239
+ // unused rectangle cells (e.g., when variant_count = 3 or 5) still carry valid dilated RGB.
240
+ const fill_r = clamp(Math.round(Math.max(0.04, p_r) * 255), 1, 255);
241
+ const fill_g = clamp(Math.round(Math.max(0.04, p_g) * 255), 1, 255);
242
+ const fill_b = clamp(Math.round(Math.max(0.02, p_b) * 255), 1, 255);
243
+ const fill_rough = clamp(Math.round(base_rough * 255), 0, 255);
244
+ const total_texels = atlas_w * atlas_h;
245
+ for (let p = 0; p < total_texels; p++) {
246
+ const i4 = p * 4;
247
+ albedo_data[i4] = fill_r;
248
+ albedo_data[i4 + 1] = fill_g;
249
+ albedo_data[i4 + 2] = fill_b;
250
+ albedo_data[i4 + 3] = 0;
251
+
252
+ const i2 = p * 2;
253
+ normal_data[i2] = 128;
254
+ normal_data[i2 + 1] = 128;
255
+
256
+ orm_data[i4] = 255;
257
+ orm_data[i4 + 1] = fill_rough;
258
+ orm_data[i4 + 2] = 0;
259
+ orm_data[i4 + 3] = 255;
260
+ }
261
+
262
+ const anchor_u = clamp(cfg.stem_anchor_u ?? 0.5, 0.1, 0.9);
263
+ const anchor_v = clamp(cfg.stem_anchor_v ?? 0.0, 0.0, 0.25);
264
+ const min_stem_half_w = Math.max(0.022, 1.5 / res);
265
+
266
+ for (let v_idx = 0; v_idx < variant_count; v_idx++) {
267
+ const box = boxes[v_idx];
268
+ const tile_x0 = box.x0;
269
+ const tile_y0 = box.y0;
270
+
271
+ // Ontogenetic coordinate mu_k in [0, 1]: 0 = juvenile apical flush, 1 = mature basal foliage
272
+ const mu_k = variant_count > 1 ? v_idx / (variant_count - 1) : 0.65;
273
+ const ox = (rng() - 0.5) * 150.0 + v_idx * 37.3;
274
+ const oy = (rng() - 0.5) * 150.0 + v_idx * 53.7;
275
+
276
+ // Ontogenetic cluster geometry scaling: juvenile shoots are slightly more forward-cupped and compact
277
+ const angle_scale = lerp(1.0 - 0.22 * shape_var, 1.0 + 0.06 * shape_var, mu_k);
278
+ const len_scale = lerp(1.0 - 0.14 * shape_var, 1.0 + 0.04 * shape_var, mu_k);
279
+
280
+ const leaflets = [];
281
+ if (shape === "palmate") {
282
+ const count = Math.max(3, leaflet_count | 1);
283
+ for (let i = 0; i < count; i++) {
284
+ const frac = (i / (count - 1)) * 2.0 - 1.0; // [-1, 1]
285
+ const angle = frac * 1.05 * angle_scale;
286
+ const scale = (1.0 - 0.32 * Math.abs(frac)) * 0.76 * len_scale;
287
+ leaflets.push({
288
+ ox: anchor_u,
289
+ oy: anchor_v + 0.08,
290
+ dir_x: Math.sin(angle),
291
+ dir_y: Math.cos(angle),
292
+ length: scale,
293
+ width: scale * 0.9
294
+ });
295
+ }
296
+ } else if (shape === "needle") {
297
+ const pairs = Math.max(4, leaflet_count);
298
+ for (let i = 0; i < pairs; i++) {
299
+ const v_stem = anchor_v + 0.04 + (i / pairs) * 0.74;
300
+ for (const side of [-1, 1]) {
301
+ const angle = side * (0.45 + 0.25 * (1.0 - i / pairs)) * angle_scale;
302
+ const len = 0.36 * (1.0 - 0.35 * (i / pairs)) * len_scale;
303
+ leaflets.push({
304
+ ox: anchor_u,
305
+ oy: v_stem,
306
+ dir_x: Math.sin(angle),
307
+ dir_y: Math.cos(angle),
308
+ length: len,
309
+ width: len
310
+ });
311
+ }
312
+ }
313
+ leaflets.push({
314
+ ox: anchor_u,
315
+ oy: anchor_v + 0.72,
316
+ dir_x: 0.0,
317
+ dir_y: 1.0,
318
+ length: 0.26 * len_scale,
319
+ width: 0.26 * len_scale
320
+ });
321
+ } else {
322
+ const pairs = Math.max(1, Math.floor(leaflet_count / 2));
323
+ for (let p = 0; p < pairs; p++) {
324
+ const u_p = (p + 0.35) / (pairs + 0.35);
325
+ const v_stem = anchor_v + 0.04 + u_p * 0.56;
326
+ const angle_mag = lerp(0.95, 0.55, u_p) * angle_scale;
327
+ const len = lerp(0.40, 0.30, u_p) * len_scale;
328
+ for (const side of [-1, 1]) {
329
+ const angle = side * angle_mag;
330
+ leaflets.push({
331
+ ox: anchor_u,
332
+ oy: v_stem,
333
+ dir_x: Math.sin(angle),
334
+ dir_y: Math.cos(angle),
335
+ length: len,
336
+ width: len * 0.95
337
+ });
338
+ }
339
+ }
340
+ leaflets.push({
341
+ ox: anchor_u,
342
+ oy: anchor_v + 0.56,
343
+ dir_x: 0.0,
344
+ dir_y: 1.0,
345
+ length: 0.42 * len_scale,
346
+ width: 0.40 * len_scale
347
+ });
348
+ }
349
+
350
+ // Ontogenetic chlorophyll accumulation (juvenile protochlorophyllide lag -> lighter yellow-green)
351
+ const juv_factor = (1.0 - mu_k) * juv_shift;
352
+ // Staggered variant senescence progression across the atlas so autumn crowns exhibit natural leaf-to-leaf diversity
353
+ const sigma_k = senescence > 0.0
354
+ ? clamp(senescence * (0.80 + 0.36 * Math.abs(mu_k - 0.35)) + senescence * senescence * 0.12, 0.0, 1.0)
355
+ : 0.0;
356
+ const sun_variant_factor = lerp(1.18, 0.74, mu_k);
357
+ const rough_ontogeny_offset = lerp(-0.10 * vein_contrast, 0.07 * vein_contrast, mu_k);
358
+
359
+ for (let ly_px = 0; ly_px < res; ly_px++) {
360
+ const v = (ly_px + 0.5) / res;
361
+ const atlas_y = tile_y0 + ly_px;
362
+ const row_offset = atlas_y * atlas_w;
363
+ // Prevent top-edge vertical bleeding into adjacent stacked tiles while keeping v=0 stem anchor solid
364
+ const top_border_fade = smoothStep(1.0, 0.975, v);
365
+
366
+ for (let lx_px = 0; lx_px < res; lx_px++) {
367
+ const u = (lx_px + 0.5) / res;
368
+ const atlas_x = tile_x0 + lx_px;
369
+ const p_idx = row_offset + atlas_x;
370
+
371
+ // Central woody twig / rachis along u = anchor_u, v in [anchor_v, anchor_v + 0.72]
372
+ let stem_mask = 0.0;
373
+ if (v >= anchor_v && v <= anchor_v + 0.72) {
374
+ const u_stem = clamp((v - anchor_v) / 0.72, 0.0, 1.0);
375
+ const stem_half_w = lerp(min_stem_half_w, 0.007, u_stem);
376
+ const d_stem = stem_half_w - Math.abs(u - anchor_u);
377
+ stem_mask = smoothStep(-0.008, 0.008, d_stem);
378
+ }
379
+
380
+ let best_mask = 0.0;
381
+ let best_vein = 0.0;
382
+ let best_h = 0.0;
383
+ let best_tip = 0.0;
384
+ let best_edge_norm = 0.5;
385
+
386
+ for (let l = 0; l < leaflets.length; l++) {
387
+ const lf = leaflets[l];
388
+ const dx = u - lf.ox;
389
+ const dy = v - lf.oy;
390
+ const ly = (dx * lf.dir_x + dy * lf.dir_y) / lf.length;
391
+ const lx = (dx * lf.dir_y - dy * lf.dir_x) / lf.width;
392
+
393
+ const eval_res = evaluate_leaflet(lx, ly, shape, mu_k, shape_var, vein_contrast);
394
+ if (eval_res.mask > best_mask) {
395
+ best_mask = eval_res.mask;
396
+ best_vein = eval_res.vein;
397
+ best_h = eval_res.height;
398
+ best_tip = clamp(ly, 0.0, 1.0);
399
+ best_edge_norm = eval_res.edge_norm;
400
+ }
401
+ }
402
+
403
+ best_mask *= top_border_fade;
404
+ const alpha = clamp(Math.max(best_mask, stem_mask), 0.0, 1.0);
405
+ height_field[p_idx] = Math.max(best_h * top_border_fade, stem_mask * 0.4);
406
+
407
+ sdnoise3_array(SCRATCH_NOISE, u * 6.0 + ox, v * 6.0 + oy, 0.0);
408
+ const n_var = SCRATCH_NOISE[0] * 0.5 + 0.5;
409
+
410
+ // 1. Base photosynthetic chlorophyll + carotenoid lamina reflectance (with juvenile chartreuse shift)
411
+ const vein_mix_w = lerp(0.38, 0.72, mu_k) * vein_str;
412
+ const mix_t = clamp(best_vein * vein_mix_w + best_tip * 0.24 + n_var * 0.18, 0.0, 1.0);
413
+ let cr = lerp(p_r, s_r, mix_t);
414
+ let cg = lerp(p_g, s_g, mix_t);
415
+ let cb = lerp(p_b, s_b, mix_t);
416
+
417
+ // Juvenile protochlorophyllide lag: higher mesophyll reflectance + yellow-green chartreuse shift
418
+ if (juv_factor > 1e-4) {
419
+ const tip_boost = 0.75 + 0.25 * best_tip;
420
+ const j_w = juv_factor * tip_boost;
421
+ cr = clamp(cr + j_w * 0.22, 0.0, 1.0);
422
+ cg = clamp(cg + j_w * 0.28, 0.0, 1.0);
423
+ cb = clamp(cb + j_w * 0.04, 0.0, 1.0);
424
+ }
425
+
426
+ // Transient juvenile epidermal anthocyanin flush along expanding young leaf margins/tips
427
+ const w_ant_juv = juv_antho * Math.pow(1.0 - mu_k, 1.8)
428
+ * (0.30 + 0.70 * (1.0 - best_edge_norm * 0.65) * (0.35 + 0.65 * best_tip))
429
+ * (1.0 - sigma_k);
430
+ if (w_ant_juv > 1e-4) {
431
+ cr = lerp(cr, 0.58, w_ant_juv * 0.75);
432
+ cg = lerp(cg, 0.24, w_ant_juv * 0.65);
433
+ cb = lerp(cb, 0.14, w_ant_juv * 0.65);
434
+ }
435
+
436
+ // 2. First-principles autumn senescence (Chlorophyll catabolism -> Carotenoid unmasking -> Anthocyanin -> Tannin)
437
+ if (sigma_k > 1e-4) {
438
+ // Vascular bundles (midrib & secondary veins) and basal petiole retain chlorophyll longest
439
+ const vascular_shield = clamp(
440
+ 0.48 * best_vein + 0.34 * best_edge_norm * (1.0 - 0.55 * best_tip) + 0.18 * n_var,
441
+ 0.0,
442
+ 1.0
443
+ );
444
+ const r_chl_lin = clamp(1.0 - sigma_k * (1.36 - 0.68 * vascular_shield), 0.0, 1.0);
445
+ const r_chl = r_chl_lin * r_chl_lin;
446
+ const chl_loss = 1.0 - r_chl;
447
+
448
+ // (a) Unmask plastid carotenoids (xanthophyll / beta-carotene -> golden-yellow)
449
+ const car_r = lerp(0.76, 0.94, car_density);
450
+ const car_g = lerp(0.54, 0.74, car_density);
451
+ const car_b = lerp(0.14, 0.08, car_density);
452
+ const w_car = clamp(chl_loss * (0.38 + 0.62 * car_density), 0.0, 1.0);
453
+ cr = lerp(cr, car_r, w_car);
454
+ cg = lerp(cg, car_g, w_car);
455
+ cb = lerp(cb, car_b, w_car);
456
+
457
+ // (b) De novo vacuolar anthocyanin synthesis (high light + margin/tip sugar accumulation -> scarlet/crimson)
458
+ if (antho_pot > 1e-4) {
459
+ const margin_sugar = clamp(
460
+ 0.52 * (1.0 - best_edge_norm) + 0.34 * best_tip + 0.22 * (1.0 - best_vein) + 0.15 * (1.0 - n_var),
461
+ 0.0,
462
+ 1.0
463
+ );
464
+ const w_ant = clamp(
465
+ antho_pot * chl_loss * Math.pow(sigma_k, 0.72) * sun_variant_factor * (0.42 + 0.78 * margin_sugar),
466
+ 0.0,
467
+ 1.0
468
+ );
469
+ // Vacuolar anthocyanin strongly absorbs green (~530nm) and blue while transmitting red (~650nm)
470
+ cr = lerp(cr, 0.82, w_ant * 0.85);
471
+ cg *= lerp(1.0, 0.18, w_ant);
472
+ cb *= lerp(1.0, 0.24, w_ant);
473
+ }
474
+
475
+ // (c) Late-senescence condensed tannin / polyphenol oxidation into russet/copper-brown
476
+ if (tannin_ox > 1e-4 && sigma_k > 0.22) {
477
+ const w_tan = clamp(
478
+ tannin_ox
479
+ * chl_loss * chl_loss
480
+ * smoothStep(0.22, 0.95, sigma_k)
481
+ * lerp(0.68, 1.22, mu_k)
482
+ * (0.42 + 0.72 * (1.0 - best_edge_norm * 0.7)),
483
+ 0.0,
484
+ 1.0
485
+ );
486
+ cr = lerp(cr, 0.44, w_tan);
487
+ cg = lerp(cg, 0.24, w_tan);
488
+ cb = lerp(cb, 0.10, w_tan);
489
+ }
490
+ }
491
+
492
+ // 3. Petiole / rachis coloration: herbaceous green on juvenile shoots -> suberized woody brown on mature shoots
493
+ if (stem_mask > best_mask) {
494
+ const stem_r = lerp(lerp(p_r, 0.32, 0.55), 0.32, mu_k);
495
+ const stem_g = lerp(lerp(p_g, 0.38, 0.55), 0.26, mu_k);
496
+ const stem_b = lerp(lerp(p_b, 0.16, 0.55), 0.16, mu_k);
497
+ cr = lerp(cr, stem_r, stem_mask * 0.75);
498
+ cg = lerp(cg, stem_g, stem_mask * 0.75);
499
+ cb = lerp(cb, stem_b, stem_mask * 0.75);
500
+ }
501
+
502
+ // Subtle basal petiole ambient darkening
503
+ const base_shade = 0.82 + 0.18 * v;
504
+ cr = Math.max(0.01, cr * base_shade);
505
+ cg = Math.max(0.01, cg * base_shade);
506
+ cb = Math.max(0.005, cb * base_shade);
507
+
508
+ const i4 = p_idx * 4;
509
+ albedo_data[i4] = clamp(Math.round(cr * 255), 1, 255);
510
+ albedo_data[i4 + 1] = clamp(Math.round(cg * 255), 1, 255);
511
+ albedo_data[i4 + 2] = clamp(Math.round(cb * 255), 1, 255);
512
+ albedo_data[i4 + 3] = clamp(Math.round(alpha * 255), 0, 255);
513
+
514
+ const ao = clamp(0.75 + 0.25 * best_h, 0.25, 1.0);
515
+ const rough = clamp(
516
+ base_rough + rough_ontogeny_offset + sigma_k * 0.08 - best_vein * 0.08 + (1.0 - n_var) * 0.06,
517
+ 0.15,
518
+ 0.95
519
+ );
520
+ orm_data[i4] = clamp(Math.round(ao * 255), 0, 255);
521
+ orm_data[i4 + 1] = clamp(Math.round(rough * 255), 0, 255);
522
+ orm_data[i4 + 2] = 0;
523
+ orm_data[i4 + 3] = 255;
524
+ }
525
+ }
526
+
527
+ // Pass 2 (per-tile): Central-difference tangent-space normal map clamped to tile bounds
528
+ const tile_vein_scale = lerp(1.0 - 0.58 * vein_contrast, 1.0 + 0.35 * vein_contrast, mu_k);
529
+ const norm_str = (1.6 * vein_str + 0.35) * tile_vein_scale;
530
+ for (let ly_px = 0; ly_px < res; ly_px++) {
531
+ const y0 = tile_y0 + Math.max(0, ly_px - 1);
532
+ const y1 = tile_y0 + Math.min(res - 1, ly_px + 1);
533
+ const yc = tile_y0 + ly_px;
534
+
535
+ for (let lx_px = 0; lx_px < res; lx_px++) {
536
+ const x0 = tile_x0 + Math.max(0, lx_px - 1);
537
+ const x1 = tile_x0 + Math.min(res - 1, lx_px + 1);
538
+ const xc = tile_x0 + lx_px;
539
+
540
+ const dh_du = (height_field[yc * atlas_w + x1] - height_field[yc * atlas_w + x0]) * 0.5 * norm_str;
541
+ const dh_dv = (height_field[y1 * atlas_w + xc] - height_field[y0 * atlas_w + xc]) * 0.5 * norm_str;
542
+
543
+ let nx = -dh_du;
544
+ let ny = -dh_dv;
545
+ const nz = 1.0;
546
+ const nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
547
+ nx /= nl;
548
+ ny /= nl;
549
+
550
+ const i2 = (yc * atlas_w + xc) * 2;
551
+ normal_data[i2] = clamp(Math.round((nx * 0.5 + 0.5) * 255), 0, 255);
552
+ normal_data[i2 + 1] = clamp(Math.round((ny * 0.5 + 0.5) * 255), 0, 255);
553
+ }
554
+ }
555
+ }
556
+
557
+ const albedo_image = ShadeImage.fromSampler2D(albedo_sampler);
558
+ albedo_image.color_space = ColorSpace.SRGB;
559
+ // CRITICAL: Keep AlphaMode.Straight so texture_write_to_gpu premultiplies once on upload
560
+ // and G-buffer albedo_unpremultiply recovers clean dilated leaf color at cutout boundaries.
561
+ albedo_image.alpha_mode = AlphaMode.Straight;
562
+ const texture_albedo = ShadeTexture.from(albedo_image);
563
+ texture_albedo.label = "foliage_leaf_albedo";
564
+ texture_albedo.color_space = ColorSpace.SRGB;
565
+ texture_albedo.mipmapGenerationFilter = TextureFilterType.MagicKernelSharp;
566
+ texture_albedo.wrapS = TextureWrapType.ClampToEdge;
567
+ texture_albedo.wrapT = TextureWrapType.ClampToEdge;
568
+
569
+ const normal_image = ShadeImage.fromSampler2D(normal_sampler);
570
+ normal_image.color_space = ColorSpace.None;
571
+ normal_image.alpha_mode = AlphaMode.Opaque;
572
+ const texture_normal = ShadeTexture.from(normal_image);
573
+ texture_normal.label = "foliage_leaf_normal";
574
+ texture_normal.color_space = ColorSpace.LinearSRGB;
575
+ texture_normal.mipmapGenerationFilter = TextureFilterType.LinearNormal;
576
+ texture_normal.wrapS = TextureWrapType.ClampToEdge;
577
+ texture_normal.wrapT = TextureWrapType.ClampToEdge;
578
+
579
+ const orm_image = ShadeImage.fromSampler2D(orm_sampler);
580
+ orm_image.color_space = ColorSpace.None;
581
+ orm_image.alpha_mode = AlphaMode.Opaque;
582
+ const texture_orm = ShadeTexture.from(orm_image);
583
+ texture_orm.label = "foliage_leaf_orm";
584
+ texture_orm.color_space = ColorSpace.LinearSRGB;
585
+ texture_orm.wrapS = TextureWrapType.ClampToEdge;
586
+ texture_orm.wrapT = TextureWrapType.ClampToEdge;
587
+
588
+ const material = new StandardShadeMaterial();
589
+ material.name = "foliage_leaf_material";
590
+ material.texture_albedo = texture_albedo;
591
+ material.texture_normal = texture_normal;
592
+ material.texture_orm = texture_orm;
593
+ material.roughness_factor = 1.0;
594
+ material.metallic_factor = 0.0;
595
+ // CRITICAL: Keep transmission_factor = 0 on AlphaTested cards so transparent card corners
596
+ // do not act as refractive glass in the path tracer!
597
+ material.transmission_factor = 0.0;
598
+ material.ambient_factors.a = 1.0;
599
+ material.ambient_factors.b = 0.0;
600
+ material.transparency_mode = TransparencyMode.AlphaTested;
601
+ material.transparency = TransparencyMode.AlphaTested;
602
+ material.draw_side = ShadeDrawSide.Double;
603
+
604
+ return material;
605
+ }