@zephyr3d/scene 0.9.24 → 0.9.26

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 (264) hide show
  1. package/dist/animation/hair/gpu_hair_simulation.js +1090 -0
  2. package/dist/animation/hair/gpu_hair_simulation.js.map +1 -0
  3. package/dist/animation/joint_dynamics/controller.js +7 -5
  4. package/dist/animation/joint_dynamics/controller.js.map +1 -1
  5. package/dist/animation/joint_dynamics/joint_dynamics_system.js +81 -5
  6. package/dist/animation/joint_dynamics/joint_dynamics_system.js.map +1 -1
  7. package/dist/animation/morphtrack.js +3 -4
  8. package/dist/animation/morphtrack.js.map +1 -1
  9. package/dist/animation/skeleton.js +58 -24
  10. package/dist/animation/skeleton.js.map +1 -1
  11. package/dist/animation/spring/multi_chain_spring_system.js +577 -179
  12. package/dist/animation/spring/multi_chain_spring_system.js.map +1 -1
  13. package/dist/animation/spring/spring_collider.js +4 -3
  14. package/dist/animation/spring/spring_collider.js.map +1 -1
  15. package/dist/animation/spring/spring_node_pose_tracker.js +40 -0
  16. package/dist/animation/spring/spring_node_pose_tracker.js.map +1 -0
  17. package/dist/animation/spring/spring_particle.js +2 -0
  18. package/dist/animation/spring/spring_particle.js.map +1 -1
  19. package/dist/animation/spring/spring_system.js +11 -3
  20. package/dist/animation/spring/spring_system.js.map +1 -1
  21. package/dist/animation/spring_modifier.js +10 -0
  22. package/dist/animation/spring_modifier.js.map +1 -1
  23. package/dist/app/engine.js +53 -43
  24. package/dist/app/engine.js.map +1 -1
  25. package/dist/app/scriptingsystem.js +119 -6
  26. package/dist/app/scriptingsystem.js.map +1 -1
  27. package/dist/app/scriptregistry.js +3 -2
  28. package/dist/app/scriptregistry.js.map +1 -1
  29. package/dist/asset/assetmanager.js +284 -67
  30. package/dist/asset/assetmanager.js.map +1 -1
  31. package/dist/asset/loaders/zhair/zhair_format.js +288 -0
  32. package/dist/asset/loaders/zhair/zhair_format.js.map +1 -0
  33. package/dist/asset/loaders/zhair/zhair_loader.js +221 -0
  34. package/dist/asset/loaders/zhair/zhair_loader.js.map +1 -0
  35. package/dist/asset/model.js +178 -67
  36. package/dist/asset/model.js.map +1 -1
  37. package/dist/camera/camera.js +193 -21
  38. package/dist/camera/camera.js.map +1 -1
  39. package/dist/camera/perspectivecamera.js +3 -1
  40. package/dist/camera/perspectivecamera.js.map +1 -1
  41. package/dist/index.d.ts +4040 -1290
  42. package/dist/index.js +16 -6
  43. package/dist/index.js.map +1 -1
  44. package/dist/material/eye.js +932 -0
  45. package/dist/material/eye.js.map +1 -0
  46. package/dist/material/hair.js +917 -0
  47. package/dist/material/hair.js.map +1 -0
  48. package/dist/material/hairstrand.js +842 -0
  49. package/dist/material/hairstrand.js.map +1 -0
  50. package/dist/material/hairstrand_data.js +162 -0
  51. package/dist/material/hairstrand_data.js.map +1 -0
  52. package/dist/material/lambert.js +5 -2
  53. package/dist/material/lambert.js.map +1 -1
  54. package/dist/material/material.js +338 -300
  55. package/dist/material/material.js.map +1 -1
  56. package/dist/material/meshmaterial.js +120 -19
  57. package/dist/material/meshmaterial.js.map +1 -1
  58. package/dist/material/mixins/albedocolor.js +1 -1
  59. package/dist/material/mixins/albedocolor.js.map +1 -1
  60. package/dist/material/mixins/lightmodel/blinnphong.js +5 -3
  61. package/dist/material/mixins/lightmodel/blinnphong.js.map +1 -1
  62. package/dist/material/mixins/lightmodel/lambert.js +5 -4
  63. package/dist/material/mixins/lightmodel/lambert.js.map +1 -1
  64. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js +3 -3
  65. package/dist/material/mixins/lightmodel/pbrblueprintmixin.js.map +1 -1
  66. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js +5 -5
  67. package/dist/material/mixins/lightmodel/pbrmetallicroughness.js.map +1 -1
  68. package/dist/material/mixins/lightmodel/pbrspecularglossness.js +3 -3
  69. package/dist/material/mixins/lightmodel/pbrspecularglossness.js.map +1 -1
  70. package/dist/material/mixins/lit.js +73 -18
  71. package/dist/material/mixins/lit.js.map +1 -1
  72. package/dist/material/mixins/pbr/common.js +4 -4
  73. package/dist/material/mixins/pbr/common.js.map +1 -1
  74. package/dist/material/mixins/texture.js +48 -11
  75. package/dist/material/mixins/texture.js.map +1 -1
  76. package/dist/material/mtoon.js +6 -5
  77. package/dist/material/mtoon.js.map +1 -1
  78. package/dist/material/pbrblueprint.js +146 -28
  79. package/dist/material/pbrblueprint.js.map +1 -1
  80. package/dist/material/pbrblueprint_instance.js +501 -0
  81. package/dist/material/pbrblueprint_instance.js.map +1 -0
  82. package/dist/material/pbrmr.js +15 -16
  83. package/dist/material/pbrmr.js.map +1 -1
  84. package/dist/material/shader/helper.js +766 -456
  85. package/dist/material/shader/helper.js.map +1 -1
  86. package/dist/material/shader/terrain_blend.js +55 -0
  87. package/dist/material/shader/terrain_blend.js.map +1 -0
  88. package/dist/material/skin.js +192 -32
  89. package/dist/material/skin.js.map +1 -1
  90. package/dist/material/spriteblueprint.js +16 -1
  91. package/dist/material/spriteblueprint.js.map +1 -1
  92. package/dist/material/subsurfaceprofile.js +86 -23
  93. package/dist/material/subsurfaceprofile.js.map +1 -1
  94. package/dist/material/water.js +3 -1
  95. package/dist/material/water.js.map +1 -1
  96. package/dist/posteffect/bloom.js +167 -7
  97. package/dist/posteffect/bloom.js.map +1 -1
  98. package/dist/posteffect/coloradjust.js +2 -1
  99. package/dist/posteffect/coloradjust.js.map +1 -1
  100. package/dist/posteffect/compositor.js.map +1 -1
  101. package/dist/posteffect/fxaa.js +2 -1
  102. package/dist/posteffect/fxaa.js.map +1 -1
  103. package/dist/posteffect/grayscale.js +3 -1
  104. package/dist/posteffect/grayscale.js.map +1 -1
  105. package/dist/posteffect/motionblur.js +4 -1
  106. package/dist/posteffect/motionblur.js.map +1 -1
  107. package/dist/posteffect/posteffect.js.map +1 -1
  108. package/dist/posteffect/skinsss.js +368 -69
  109. package/dist/posteffect/skinsss.js.map +1 -1
  110. package/dist/posteffect/ssgi.js +16 -9
  111. package/dist/posteffect/ssgi.js.map +1 -1
  112. package/dist/posteffect/ssr.js +3 -0
  113. package/dist/posteffect/ssr.js.map +1 -1
  114. package/dist/posteffect/sss.js +91 -34
  115. package/dist/posteffect/sss.js.map +1 -1
  116. package/dist/posteffect/taa.js.map +1 -1
  117. package/dist/posteffect/tonemap.js +44 -1
  118. package/dist/posteffect/tonemap.js.map +1 -1
  119. package/dist/render/abuffer_oit.js +45 -39
  120. package/dist/render/abuffer_oit.js.map +1 -1
  121. package/dist/render/cluster_light.js +4 -4
  122. package/dist/render/cluster_light.js.map +1 -1
  123. package/dist/render/cull_visitor.js +19 -0
  124. package/dist/render/cull_visitor.js.map +1 -1
  125. package/dist/render/depthpass.js +36 -2
  126. package/dist/render/depthpass.js.map +1 -1
  127. package/dist/render/drawable_mixin.js +26 -1
  128. package/dist/render/drawable_mixin.js.map +1 -1
  129. package/dist/render/dualdepthpeeling_oit.js +52 -51
  130. package/dist/render/dualdepthpeeling_oit.js.map +1 -1
  131. package/dist/render/envlight.js +1 -1
  132. package/dist/render/envlight.js.map +1 -1
  133. package/dist/render/fbm_wavegenerator.js +3 -0
  134. package/dist/render/fbm_wavegenerator.js.map +1 -1
  135. package/dist/render/gerstner_wavegenerator.js +3 -0
  136. package/dist/render/gerstner_wavegenerator.js.map +1 -1
  137. package/dist/render/globalbindgroup_allocator.js +3 -0
  138. package/dist/render/globalbindgroup_allocator.js.map +1 -1
  139. package/dist/render/oit.js +29 -0
  140. package/dist/render/oit.js.map +1 -0
  141. package/dist/render/render_queue.js +3 -0
  142. package/dist/render/render_queue.js.map +1 -1
  143. package/dist/render/rendergraph/device_pool_allocator.js.map +1 -1
  144. package/dist/render/rendergraph/executor.js +2 -0
  145. package/dist/render/rendergraph/executor.js.map +1 -1
  146. package/dist/render/rendergraph/forward_plus_builder.js +61 -1
  147. package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
  148. package/dist/render/rendergraph/rendergraph.js.map +1 -1
  149. package/dist/render/rendergraph/texture_affinity_cache.js +1 -1
  150. package/dist/render/rendergraph/texture_affinity_cache.js.map +1 -1
  151. package/dist/render/rendergraph/types.js.map +1 -1
  152. package/dist/render/shadow_mask_pass.js +73 -8
  153. package/dist/render/shadow_mask_pass.js.map +1 -1
  154. package/dist/render/sky.js +166 -126
  155. package/dist/render/sky.js.map +1 -1
  156. package/dist/render/weightedblended_oit.js +36 -35
  157. package/dist/render/weightedblended_oit.js.map +1 -1
  158. package/dist/scene/basesprite.js +8 -0
  159. package/dist/scene/basesprite.js.map +1 -1
  160. package/dist/scene/batchgroup.js +3 -0
  161. package/dist/scene/batchgroup.js.map +1 -1
  162. package/dist/scene/environment.js +4 -1
  163. package/dist/scene/environment.js.map +1 -1
  164. package/dist/scene/hair.js +963 -0
  165. package/dist/scene/hair.js.map +1 -0
  166. package/dist/scene/light.js +5 -1
  167. package/dist/scene/light.js.map +1 -1
  168. package/dist/scene/mesh.js +481 -47
  169. package/dist/scene/mesh.js.map +1 -1
  170. package/dist/scene/meshdrawable.js +6 -0
  171. package/dist/scene/meshdrawable.js.map +1 -1
  172. package/dist/scene/msdftext.js +3 -0
  173. package/dist/scene/msdftext.js.map +1 -1
  174. package/dist/scene/msdftextsprite.js +3 -0
  175. package/dist/scene/msdftextsprite.js.map +1 -1
  176. package/dist/scene/particlesys.js +6 -0
  177. package/dist/scene/particlesys.js.map +1 -1
  178. package/dist/scene/scene.js +3 -0
  179. package/dist/scene/scene.js.map +1 -1
  180. package/dist/scene/scene_node.js +6 -1
  181. package/dist/scene/scene_node.js.map +1 -1
  182. package/dist/scene/terrain-cm/grass.js +273 -249
  183. package/dist/scene/terrain-cm/grass.js.map +1 -1
  184. package/dist/scene/terrain-cm/grassmaterial.js +3 -0
  185. package/dist/scene/terrain-cm/grassmaterial.js.map +1 -1
  186. package/dist/scene/terrain-cm/terrain-cm.js +6 -0
  187. package/dist/scene/terrain-cm/terrain-cm.js.map +1 -1
  188. package/dist/scene/terrain-cm/texture_cache.js +309 -0
  189. package/dist/scene/terrain-cm/texture_cache.js.map +1 -0
  190. package/dist/scene/water.js +6 -0
  191. package/dist/scene/water.js.map +1 -1
  192. package/dist/shaders/atmosphere.js.map +1 -1
  193. package/dist/shaders/diffusion.js +54 -0
  194. package/dist/shaders/diffusion.js.map +1 -0
  195. package/dist/shaders/shadow.js +33 -21
  196. package/dist/shaders/shadow.js.map +1 -1
  197. package/dist/shaders/temporal.js +71 -33
  198. package/dist/shaders/temporal.js.map +1 -1
  199. package/dist/shadow/dom.js +546 -0
  200. package/dist/shadow/dom.js.map +1 -0
  201. package/dist/shadow/pcf_opt.js +3 -5
  202. package/dist/shadow/pcf_opt.js.map +1 -1
  203. package/dist/shadow/pcf_pd.js +1 -1
  204. package/dist/shadow/pcf_pd.js.map +1 -1
  205. package/dist/shadow/receiver_bias.js +41 -0
  206. package/dist/shadow/receiver_bias.js.map +1 -0
  207. package/dist/shadow/shader.js +20 -60
  208. package/dist/shadow/shader.js.map +1 -1
  209. package/dist/shadow/shadow_impl.js +89 -0
  210. package/dist/shadow/shadow_impl.js.map +1 -1
  211. package/dist/shadow/shadow_region.js +50 -50
  212. package/dist/shadow/shadow_region.js.map +1 -1
  213. package/dist/shadow/shadowmapper.js +150 -37
  214. package/dist/shadow/shadowmapper.js.map +1 -1
  215. package/dist/shapes/shape.js +11 -2
  216. package/dist/shapes/shape.js.map +1 -1
  217. package/dist/shapes/sphere.js +50 -9
  218. package/dist/shapes/sphere.js.map +1 -1
  219. package/dist/utility/blueprint/common/constants.js +112 -2
  220. package/dist/utility/blueprint/common/constants.js.map +1 -1
  221. package/dist/utility/blueprint/material/func.js +142 -49
  222. package/dist/utility/blueprint/material/func.js.map +1 -1
  223. package/dist/utility/blueprint/material/ir.js +490 -25
  224. package/dist/utility/blueprint/material/ir.js.map +1 -1
  225. package/dist/utility/blueprint/material/texture.js +898 -4
  226. package/dist/utility/blueprint/material/texture.js.map +1 -1
  227. package/dist/utility/serialization/manager.js +431 -100
  228. package/dist/utility/serialization/manager.js.map +1 -1
  229. package/dist/utility/serialization/scene/animation.js +370 -3
  230. package/dist/utility/serialization/scene/animation.js.map +1 -1
  231. package/dist/utility/serialization/scene/batch.js +4 -0
  232. package/dist/utility/serialization/scene/batch.js.map +1 -1
  233. package/dist/utility/serialization/scene/camera.js +133 -1
  234. package/dist/utility/serialization/scene/camera.js.map +1 -1
  235. package/dist/utility/serialization/scene/common.js +26 -5
  236. package/dist/utility/serialization/scene/common.js.map +1 -1
  237. package/dist/utility/serialization/scene/hair.js +1141 -0
  238. package/dist/utility/serialization/scene/hair.js.map +1 -0
  239. package/dist/utility/serialization/scene/light.js +84 -4
  240. package/dist/utility/serialization/scene/light.js.map +1 -1
  241. package/dist/utility/serialization/scene/material.js +1867 -166
  242. package/dist/utility/serialization/scene/material.js.map +1 -1
  243. package/dist/utility/serialization/scene/mesh.js +231 -14
  244. package/dist/utility/serialization/scene/mesh.js.map +1 -1
  245. package/dist/utility/serialization/scene/node.js +141 -7
  246. package/dist/utility/serialization/scene/node.js.map +1 -1
  247. package/dist/utility/serialization/scene/particle.js +4 -0
  248. package/dist/utility/serialization/scene/particle.js.map +1 -1
  249. package/dist/utility/serialization/scene/primitive.js +18 -0
  250. package/dist/utility/serialization/scene/primitive.js.map +1 -1
  251. package/dist/utility/serialization/scene/sprite.js +4 -0
  252. package/dist/utility/serialization/scene/sprite.js.map +1 -1
  253. package/dist/utility/serialization/scene/terrain.js +92 -69
  254. package/dist/utility/serialization/scene/terrain.js.map +1 -1
  255. package/dist/utility/serialization/scene/text.js +4 -0
  256. package/dist/utility/serialization/scene/text.js.map +1 -1
  257. package/dist/utility/serialization/scene/water.js +4 -0
  258. package/dist/utility/serialization/scene/water.js.map +1 -1
  259. package/dist/utility/serialization/types.js.map +1 -1
  260. package/dist/utility/shprojector.js +96 -95
  261. package/dist/utility/shprojector.js.map +1 -1
  262. package/dist/values.js +105 -58
  263. package/dist/values.js.map +1 -1
  264. package/package.json +3 -3
@@ -11,7 +11,10 @@ import '../../material/unlit.js';
11
11
  import '../../material/particle.js';
12
12
  import '../../material/subsurfaceprofile.js';
13
13
  import '../../material/skin.js';
14
+ import '../../material/eye.js';
14
15
  import '../../material/meshmaterial.js';
16
+ import '../../material/hair.js';
17
+ import '../../material/hairstrand.js';
15
18
  import '../../material/grassmaterial.js';
16
19
  import '../../material/terrain-cm.js';
17
20
  import '../../material/pbrmr.js';
@@ -31,30 +34,40 @@ import '../../shaders/atmosphere.js';
31
34
  import '../../render/rendergraph/scene_render_context.js';
32
35
  import { ClipmapGrassMaterial } from './grassmaterial.js';
33
36
 
34
- const MAX_INSTANCES_PER_NODE = 2048;
35
37
  const INSTANCE_BYTES = 4 * 4;
38
+ /** Number of placement cells along each axis of a grass tile */ const TILE_CELLS = 64;
39
+ /** Default number of placement cells per density map texel along each axis */ const DEFAULT_CELLS_PER_TEXEL = 2;
40
+ const MAX_CELLS_PER_TEXEL = 8;
36
41
  /**
37
- * Grass glade instance manager class
42
+ * Deterministic 2D integer hash, returns a value in [0, 1).
43
+ * Grass placement derives everything (jitter, survival threshold, rotation)
44
+ * from this hash so that blade positions are stable across edits and reloads.
45
+ */ function hashCell(x, z, seed) {
46
+ let h = Math.imul(x, 0x27d4eb2d) ^ Math.imul(z, 0x165667b1) ^ Math.imul(seed, 0x9e3779b9) | 0;
47
+ h = Math.imul(h ^ h >>> 15, 0x85ebca6b);
48
+ h = Math.imul(h ^ h >>> 13, 0xc2b2ae35);
49
+ h ^= h >>> 16;
50
+ return (h >>> 0) / 4294967296;
51
+ }
52
+ /**
53
+ * Grass blade instance buffer for a single tile
38
54
  * @internal
39
55
  */ class GrassInstances extends Disposable {
40
- _instances;
56
+ _numInstances;
41
57
  _baseVertexBuffer;
42
58
  _indexBuffer;
43
59
  _instanceBuffer;
44
60
  _primitive;
45
61
  constructor(baseVertexBuffer, indexBuffer){
46
62
  super();
47
- this._instances = [];
63
+ this._numInstances = 0;
48
64
  this._baseVertexBuffer = new DRef(baseVertexBuffer);
49
65
  this._indexBuffer = new DRef(indexBuffer);
50
66
  this._instanceBuffer = new DRef();
51
67
  this._primitive = new DRef();
52
68
  }
53
69
  get numInstances() {
54
- return this._instances.length;
55
- }
56
- get instanceBuffer() {
57
- return this._instanceBuffer.get();
70
+ return this._numInstances;
58
71
  }
59
72
  setBaseVertexBuffer(baseVertexBuffer) {
60
73
  if (baseVertexBuffer !== this._baseVertexBuffer.get()) {
@@ -62,8 +75,25 @@ const INSTANCE_BYTES = 4 * 4;
62
75
  this._primitive.dispose();
63
76
  }
64
77
  }
78
+ /**
79
+ * Sets the packed instance data (x, y, sin, cos per instance)
80
+ */ setData(data, count) {
81
+ this._numInstances = count;
82
+ if (count === 0) {
83
+ return;
84
+ }
85
+ const device = getDevice();
86
+ const bytesRequired = count * INSTANCE_BYTES;
87
+ let buffer = this._instanceBuffer.get();
88
+ if (!buffer || buffer.byteLength < bytesRequired) {
89
+ buffer = device.createVertexBuffer('tex1_f32x4', new Uint8Array(nextPowerOf2(bytesRequired)));
90
+ this._instanceBuffer.set(buffer);
91
+ this._primitive.dispose();
92
+ }
93
+ buffer.bufferSubData(0, data.subarray(0, count * 4));
94
+ }
65
95
  draw() {
66
- if (this._instances.length > 0) {
96
+ if (this._numInstances > 0) {
67
97
  if (!this._primitive.get()) {
68
98
  const primitive = new Primitive();
69
99
  primitive.setVertexBuffer(this._baseVertexBuffer.get());
@@ -74,58 +104,8 @@ const INSTANCE_BYTES = 4 * 4;
74
104
  primitive.indexCount = this._indexBuffer.get().length;
75
105
  this._primitive.set(primitive);
76
106
  }
77
- this._primitive.get().drawInstanced(this._instances.length);
78
- }
79
- }
80
- updateBuffers() {
81
- const device = getDevice();
82
- if (this._instances.length === 0) {
83
- this._instanceBuffer.set(null);
84
- return;
85
- }
86
- if (this._instances.length > 0 && !this._instanceBuffer.get()) {
87
- this._instanceBuffer.set(device.createVertexBuffer('tex1_f32x4', new Uint8Array(this._instances.length * INSTANCE_BYTES)));
88
- }
89
- let buffer = this._instanceBuffer.get();
90
- const currentBytes = buffer.byteLength;
91
- const bytesRequired = this._instances.length * INSTANCE_BYTES;
92
- if (currentBytes < bytesRequired) {
93
- buffer = device.createVertexBuffer('tex1_f32x4', new Uint8Array(nextPowerOf2(bytesRequired)));
94
- this._instanceBuffer.set(buffer);
95
- this._primitive.dispose();
96
- }
97
- const data = new Float32Array(4 * this._instances.length);
98
- for(let i = 0; i < this._instances.length; i++){
99
- data[i * 4 + 0] = this._instances[i].x;
100
- data[i * 4 + 1] = this._instances[i].y;
101
- data[i * 4 + 2] = Math.sin(this._instances[i].angle);
102
- data[i * 4 + 3] = Math.cos(this._instances[i].angle);
103
- }
104
- buffer.bufferSubData(0, data);
105
- }
106
- removeInstances(minX, minZ, maxX, maxZ, num) {
107
- if (num <= 0) {
108
- return 0;
109
- }
110
- let removed = 0;
111
- for(let i = this._instances.length - 1; i >= 0; i--){
112
- const instance = this._instances[i];
113
- if (instance.x >= minX && instance.x <= maxX && instance.y >= minZ && instance.y <= maxZ) {
114
- this._instances.splice(i, 1);
115
- removed++;
116
- if (removed === num) {
117
- break;
118
- }
119
- }
120
- }
121
- if (removed > 0) {
122
- this.updateBuffers();
107
+ this._primitive.get().drawInstanced(this._numInstances);
123
108
  }
124
- return removed;
125
- }
126
- addInstances(instances) {
127
- this._instances.push(...instances);
128
- this.updateBuffers();
129
109
  }
130
110
  onDispose() {
131
111
  super.onDispose();
@@ -137,34 +117,53 @@ const INSTANCE_BYTES = 4 * 4;
137
117
  }
138
118
  /**
139
119
  * Grass layer class
120
+ *
140
121
  * @public
141
122
  */ class GrassLayer extends Disposable {
142
123
  static _indexBuffer = new DRef();
124
+ static _cullAABB = new AABB();
125
+ static _visibleTiles = [];
126
+ static _instanceData = new Float32Array(TILE_CELLS * TILE_CELLS * 4);
143
127
  _material;
144
- _quadtree;
128
+ _seed;
145
129
  _bladeWidth;
146
130
  _bladeHeight;
147
131
  _baseVertexBuffer;
132
+ _densityWidth;
133
+ _densityHeight;
134
+ _cellsPerTexel;
135
+ _densityMap;
136
+ _tiles;
137
+ _tilesX;
138
+ _tilesZ;
139
+ _numBlades;
148
140
  /**
149
141
  * Creates an instance of GrassLayer
150
142
  * @param terrain - Clipmap terrain object
143
+ * @param seed - Seed for deterministic blade placement, usually the layer index
151
144
  * @param bladeWidth - Grass blade width
152
145
  * @param bladeHeight - Grass blade height
153
146
  * @param albedoMap - Albedo texture for the blade
154
- */ constructor(terrain, bladeWidth, bladeHeight, albedoMap){
147
+ */ constructor(terrain, seed, bladeWidth, bladeHeight, albedoMap){
155
148
  super();
156
149
  this._material = new DRef(new ClipmapGrassMaterial(terrain));
157
150
  this._material.get().albedoTexture = albedoMap ?? null;
158
151
  if (albedoMap) {
159
152
  this._material.get().setTextureSize(albedoMap.width, albedoMap.height);
160
153
  }
154
+ this._seed = seed;
161
155
  this._bladeWidth = bladeWidth;
162
156
  this._bladeHeight = bladeHeight;
163
157
  this._baseVertexBuffer = new DRef(this.createBaseVertexBuffer(this._bladeWidth, this._bladeHeight));
164
- this._quadtree = new DRef(new GrassQuadtreeNode(this._baseVertexBuffer.get(), GrassLayer._getIndexBuffer()));
165
- }
166
- /** @internal */ get quadtree() {
167
- return this._quadtree.get();
158
+ this._densityWidth = Math.max(1, terrain.sizeX);
159
+ this._densityHeight = Math.max(1, terrain.sizeZ);
160
+ this._cellsPerTexel = DEFAULT_CELLS_PER_TEXEL;
161
+ this._densityMap = new Uint8Array(this._densityWidth * this._densityHeight);
162
+ this._tiles = new Map();
163
+ this._tilesX = 0;
164
+ this._tilesZ = 0;
165
+ this._numBlades = 0;
166
+ this.updateTileGrid();
168
167
  }
169
168
  /** @internal */ updateMaterial() {
170
169
  this._material.get().uniformChanged();
@@ -184,21 +183,93 @@ const INSTANCE_BYTES = 4 * 4;
184
183
  */ getAlbedoMap() {
185
184
  return this._material.get().albedoTexture;
186
185
  }
186
+ /** How many grass blades are currently generated in this layer */ get numBlades() {
187
+ return this._numBlades;
188
+ }
189
+ /**
190
+ * Density map of this layer, one byte per texel (0 = no grass, 255 = full density).
191
+ * Texel (x, z) covers the normalized terrain region [x/w..(x+1)/w, z/h..(z+1)/h].
192
+ * After modifying the data, call {@link GrassLayer.updateDensityRegion} to
193
+ * regenerate the affected blade instances.
194
+ */ get densityMap() {
195
+ return this._densityMap;
196
+ }
197
+ /** Width of the density map in texels */ get densityMapWidth() {
198
+ return this._densityWidth;
199
+ }
200
+ /** Height of the density map in texels */ get densityMapHeight() {
201
+ return this._densityHeight;
202
+ }
203
+ /**
204
+ * Number of placement cells per density map texel along each axis.
205
+ * The maximum blade count per texel is the square of this value.
206
+ */ get cellsPerTexel() {
207
+ return this._cellsPerTexel;
208
+ }
209
+ set cellsPerTexel(val) {
210
+ val = Math.max(1, Math.min(MAX_CELLS_PER_TEXEL, val | 0));
211
+ if (val !== this._cellsPerTexel) {
212
+ this._cellsPerTexel = val;
213
+ this.rebuild();
214
+ }
215
+ }
216
+ /**
217
+ * Replaces the density map and regenerates all blade instances
218
+ * @param width - Density map width in texels
219
+ * @param height - Density map height in texels
220
+ * @param cellsPerTexel - Placement cells per texel along each axis
221
+ * @param data - Density data, one byte per texel
222
+ */ setDensityData(width, height, cellsPerTexel, data) {
223
+ if (width < 1 || height < 1 || width * height !== data.length) {
224
+ console.error('Invalid grass density data');
225
+ return;
226
+ }
227
+ this._densityWidth = width;
228
+ this._densityHeight = height;
229
+ this._cellsPerTexel = Math.max(1, Math.min(MAX_CELLS_PER_TEXEL, cellsPerTexel | 0));
230
+ this._densityMap = data;
231
+ this.rebuild();
232
+ }
187
233
  /**
188
- * Add grass blades to this layer in a region
189
- * @param instances - Grass blade instances to add
190
- */ addInstances(instances) {
191
- return this._quadtree.get().addInstances(instances);
234
+ * Regenerates blade instances for all tiles overlapping a density map region
235
+ * @param minTexelX - Minimum x texel of the region (inclusive)
236
+ * @param minTexelZ - Minimum z texel of the region (inclusive)
237
+ * @param maxTexelX - Maximum x texel of the region (exclusive)
238
+ * @param maxTexelZ - Maximum z texel of the region (exclusive)
239
+ */ updateDensityRegion(minTexelX, minTexelZ, maxTexelX, maxTexelZ) {
240
+ const k = this._cellsPerTexel;
241
+ // expand by one texel to cover the bilinear sampling footprint
242
+ const cx0 = Math.max(0, (minTexelX - 1) * k);
243
+ const cz0 = Math.max(0, (minTexelZ - 1) * k);
244
+ const cx1 = Math.min(this._densityWidth * k, (maxTexelX + 1) * k);
245
+ const cz1 = Math.min(this._densityHeight * k, (maxTexelZ + 1) * k);
246
+ if (cx1 <= cx0 || cz1 <= cz0) {
247
+ return;
248
+ }
249
+ const tx0 = Math.floor(cx0 / TILE_CELLS);
250
+ const tz0 = Math.floor(cz0 / TILE_CELLS);
251
+ const tx1 = Math.min(this._tilesX, Math.ceil(cx1 / TILE_CELLS));
252
+ const tz1 = Math.min(this._tilesZ, Math.ceil(cz1 / TILE_CELLS));
253
+ for(let tz = tz0; tz < tz1; tz++){
254
+ for(let tx = tx0; tx < tx1; tx++){
255
+ this.generateTile(tx, tz);
256
+ }
257
+ }
192
258
  }
193
259
  /**
194
- * Remove grass blades to this layer in a region
195
- * @param minX - Minimum x position of the region
196
- * @param minZ - Minimum z position of the region
197
- * @param maxX - Maximum x position of the region
198
- * @param maxZ - Maximum z position of the region
199
- * @param numInstances - How many grass blades to add
200
- */ removeInstances(minX, minZ, maxX, maxZ, numInstances) {
201
- this._quadtree.get().removeInstances(minX, minZ, maxX, maxZ, numInstances);
260
+ * Regenerates blade instances for the entire layer
261
+ */ rebuild() {
262
+ for (const tile of this._tiles.values()){
263
+ tile.dispose();
264
+ }
265
+ this._tiles.clear();
266
+ this._numBlades = 0;
267
+ this.updateTileGrid();
268
+ for(let tz = 0; tz < this._tilesZ; tz++){
269
+ for(let tx = 0; tx < this._tilesX; tx++){
270
+ this.generateTile(tx, tz);
271
+ }
272
+ }
202
273
  }
203
274
  /** Grass blade width in this layer */ get bladeWidth() {
204
275
  return this._bladeWidth;
@@ -221,7 +292,92 @@ const INSTANCE_BYTES = 4 * 4;
221
292
  this._bladeWidth = width;
222
293
  this._bladeHeight = height;
223
294
  this._baseVertexBuffer.set(this.createBaseVertexBuffer(this._bladeWidth, this._bladeHeight));
224
- this._quadtree.get().setBaseVertexBuffer(this._baseVertexBuffer.get());
295
+ for (const tile of this._tiles.values()){
296
+ tile.setBaseVertexBuffer(this._baseVertexBuffer.get());
297
+ }
298
+ }
299
+ }
300
+ /** @internal */ updateTileGrid() {
301
+ this._tilesX = Math.ceil(this._densityWidth * this._cellsPerTexel / TILE_CELLS);
302
+ this._tilesZ = Math.ceil(this._densityHeight * this._cellsPerTexel / TILE_CELLS);
303
+ }
304
+ /** @internal */ sampleDensity(u, v) {
305
+ const w = this._densityWidth;
306
+ const h = this._densityHeight;
307
+ const x = u * w - 0.5;
308
+ const z = v * h - 0.5;
309
+ const ix = Math.floor(x);
310
+ const iz = Math.floor(z);
311
+ const fx = x - ix;
312
+ const fz = z - iz;
313
+ const x0 = Math.min(Math.max(ix, 0), w - 1);
314
+ const x1 = Math.min(Math.max(ix + 1, 0), w - 1);
315
+ const z0 = Math.min(Math.max(iz, 0), h - 1);
316
+ const z1 = Math.min(Math.max(iz + 1, 0), h - 1);
317
+ const d = this._densityMap;
318
+ const a = d[z0 * w + x0] + (d[z0 * w + x1] - d[z0 * w + x0]) * fx;
319
+ const b = d[z1 * w + x0] + (d[z1 * w + x1] - d[z1 * w + x0]) * fx;
320
+ return (a + (b - a) * fz) * (1 / 255);
321
+ }
322
+ /** @internal */ regionIsEmpty(cx0, cz0, cx1, cz1) {
323
+ const k = this._cellsPerTexel;
324
+ const x0 = Math.max(0, Math.floor(cx0 / k) - 1);
325
+ const z0 = Math.max(0, Math.floor(cz0 / k) - 1);
326
+ const x1 = Math.min(this._densityWidth, Math.ceil(cx1 / k) + 1);
327
+ const z1 = Math.min(this._densityHeight, Math.ceil(cz1 / k) + 1);
328
+ for(let z = z0; z < z1; z++){
329
+ const rowBase = z * this._densityWidth;
330
+ for(let x = x0; x < x1; x++){
331
+ if (this._densityMap[rowBase + x] !== 0) {
332
+ return false;
333
+ }
334
+ }
335
+ }
336
+ return true;
337
+ }
338
+ /** @internal */ generateTile(tx, tz) {
339
+ const k = this._cellsPerTexel;
340
+ const cw = this._densityWidth * k;
341
+ const ch = this._densityHeight * k;
342
+ const cx0 = tx * TILE_CELLS;
343
+ const cz0 = tz * TILE_CELLS;
344
+ const cx1 = Math.min(cx0 + TILE_CELLS, cw);
345
+ const cz1 = Math.min(cz0 + TILE_CELLS, ch);
346
+ const key = tz * this._tilesX + tx;
347
+ let tile = this._tiles.get(key);
348
+ // fast path: nothing to generate if there is no tile yet and no density
349
+ if (!tile && this.regionIsEmpty(cx0, cz0, cx1, cz1)) {
350
+ return;
351
+ }
352
+ const data = GrassLayer._instanceData;
353
+ const seed = this._seed * 4;
354
+ let count = 0;
355
+ for(let cz = cz0; cz < cz1; cz++){
356
+ for(let cx = cx0; cx < cx1; cx++){
357
+ const u = (cx + hashCell(cx, cz, seed)) / cw;
358
+ const v = (cz + hashCell(cx, cz, seed + 1)) / ch;
359
+ if (this.sampleDensity(u, v) > hashCell(cx, cz, seed + 2)) {
360
+ const angle = hashCell(cx, cz, seed + 3) * Math.PI * 2;
361
+ data[count * 4 + 0] = u;
362
+ data[count * 4 + 1] = v;
363
+ data[count * 4 + 2] = Math.sin(angle);
364
+ data[count * 4 + 3] = Math.cos(angle);
365
+ count++;
366
+ }
367
+ }
368
+ }
369
+ this._numBlades += count - (tile?.numInstances ?? 0);
370
+ if (count === 0) {
371
+ if (tile) {
372
+ tile.dispose();
373
+ this._tiles.delete(key);
374
+ }
375
+ } else {
376
+ if (!tile) {
377
+ tile = new GrassInstances(this._baseVertexBuffer.get(), GrassLayer._getIndexBuffer());
378
+ this._tiles.set(key, tile);
379
+ }
380
+ tile.setData(data, count);
225
381
  }
226
382
  }
227
383
  /** @internal */ static _getIndexBuffer() {
@@ -323,16 +479,49 @@ const INSTANCE_BYTES = 4 * 4;
323
479
  ], vertices);
324
480
  }
325
481
  /** @internal */ draw(ctx, region, minY, maxY) {
482
+ const visible = GrassLayer._visibleTiles;
483
+ visible.length = 0;
484
+ const camera = ctx.camera;
485
+ const cullAABB = GrassLayer._cullAABB;
486
+ const cellsW = this._densityWidth * this._cellsPerTexel;
487
+ const cellsH = this._densityHeight * this._cellsPerTexel;
488
+ const rx = region.x;
489
+ const rz = region.y;
490
+ const rw = region.z - region.x;
491
+ const rh = region.w - region.y;
492
+ for (const [key, tile] of this._tiles){
493
+ const tx = key % this._tilesX;
494
+ const tz = (key - tx) / this._tilesX;
495
+ const u0 = tx * TILE_CELLS / cellsW;
496
+ const v0 = tz * TILE_CELLS / cellsH;
497
+ const u1 = Math.min(1, (tx + 1) * TILE_CELLS / cellsW);
498
+ const v1 = Math.min(1, (tz + 1) * TILE_CELLS / cellsH);
499
+ cullAABB.minPoint.setXYZ(rx + u0 * rw, minY, rz + v0 * rh);
500
+ cullAABB.maxPoint.setXYZ(rx + u1 * rw, maxY, rz + v1 * rh);
501
+ const clipState = camera.clipMask ? cullAABB.getClipStateWithFrustumMask(camera.frustum, camera.clipMask) : cullAABB.getClipStateWithFrustum(camera.frustum);
502
+ if (clipState !== ClipState.NOT_CLIPPED) {
503
+ visible.push(tile);
504
+ }
505
+ }
506
+ if (visible.length === 0) {
507
+ return;
508
+ }
326
509
  this._material.get().apply(ctx);
327
510
  for(let pass = 0; pass < this._material.get().numPasses; pass++){
328
511
  this._material.get().bind(ctx.device, pass);
329
- this._quadtree.get().draw(ctx.camera, region, minY, maxY, false);
512
+ for (const tile of visible){
513
+ tile.draw();
514
+ }
330
515
  }
516
+ visible.length = 0;
331
517
  }
332
518
  /** @internal */ onDispose() {
333
519
  super.onDispose();
334
520
  this._material.dispose();
335
- this._quadtree.dispose();
521
+ for (const tile of this._tiles.values()){
522
+ tile.dispose();
523
+ }
524
+ this._tiles.clear();
336
525
  this._baseVertexBuffer.dispose();
337
526
  }
338
527
  }
@@ -341,7 +530,6 @@ const INSTANCE_BYTES = 4 * 4;
341
530
  * @public
342
531
  */ class GrassRenderer extends Disposable {
343
532
  _terrain;
344
- _numGrassBlades;
345
533
  _layers;
346
534
  /**
347
535
  * Creates an instance of GrassRenderer
@@ -349,7 +537,6 @@ const INSTANCE_BYTES = 4 * 4;
349
537
  */ constructor(terrain){
350
538
  super();
351
539
  this._terrain = new DWeakRef(terrain);
352
- this._numGrassBlades = 0;
353
540
  this._layers = [];
354
541
  }
355
542
  /** @internal */ updateMaterial() {
@@ -358,7 +545,7 @@ const INSTANCE_BYTES = 4 * 4;
358
545
  }
359
546
  }
360
547
  /** How many grass blades */ get numGrassBlades() {
361
- return this._numGrassBlades;
548
+ return this._layers.reduce((sum, layer)=>sum + layer.numBlades, 0);
362
549
  }
363
550
  /** How many grass layers */ get numLayers() {
364
551
  return this._layers.length;
@@ -377,7 +564,7 @@ const INSTANCE_BYTES = 4 * 4;
377
564
  * @param albedoMap - Albedo texture of grass blades in this layer
378
565
  * @returns Index of the added grass layer
379
566
  */ addLayer(bladeWidth, bladeHeight, albedoMap) {
380
- const layer = new GrassLayer(this._terrain.get(), bladeWidth, bladeHeight, albedoMap);
567
+ const layer = new GrassLayer(this._terrain.get(), this._layers.length, bladeWidth, bladeHeight, albedoMap);
381
568
  this._layers.push(layer);
382
569
  return this._layers.length - 1;
383
570
  }
@@ -427,34 +614,6 @@ const INSTANCE_BYTES = 4 * 4;
427
614
  console.error(`Invalid grass layer: ${layer}`);
428
615
  }
429
616
  }
430
- /**
431
- * Add grass instances to the grass layer at given index
432
- * @param layer - Index of the grass layer
433
- * @param instances - Grass blade instances to add
434
- */ addInstances(layer, instances) {
435
- const grassLayer = this._layers[layer];
436
- if (!grassLayer) {
437
- console.error(`Invalid grass layer: ${layer}`);
438
- } else {
439
- this._numGrassBlades += grassLayer.addInstances(instances);
440
- }
441
- }
442
- /**
443
- * Remove grass instances from the grass layer at given index within a region
444
- * @param layer - Index of the grass layer
445
- * @param minX - Minimum x of the region
446
- * @param minZ - Minimum z of the region
447
- * @param maxX - Maximum x of the region
448
- * @param maxZ - Maximum z of the region
449
- * @param num - How many grass blades to remove
450
- */ removeInstances(layer, minX, minZ, maxX, maxZ, num) {
451
- const grassLayer = this._layers[layer];
452
- if (!grassLayer) {
453
- console.error(`Invalid grass layer: ${layer}`);
454
- } else {
455
- grassLayer.removeInstances(minX, minZ, maxX, maxZ, num);
456
- }
457
- }
458
617
  /** @internal */ draw(ctx) {
459
618
  const bv = this._terrain.get().getWorldBoundingVolume().toAABB();
460
619
  const minY = bv.minPoint.y;
@@ -471,141 +630,6 @@ const INSTANCE_BYTES = 4 * 4;
471
630
  }
472
631
  }
473
632
  }
474
- /** @internal */ class GrassQuadtreeNode extends Disposable {
475
- static _cullAABB = new AABB();
476
- _grassInstances;
477
- _children;
478
- _baseVertexBuffer;
479
- _indexBuffer;
480
- _minX;
481
- _minZ;
482
- _maxX;
483
- _maxZ;
484
- constructor(baseVertexBuffer, indexBuffer){
485
- super();
486
- this._baseVertexBuffer = new DRef(baseVertexBuffer);
487
- this._indexBuffer = new DRef(indexBuffer);
488
- this._grassInstances = new DRef(new GrassInstances(baseVertexBuffer, indexBuffer));
489
- this._children = null;
490
- this._minX = 0;
491
- this._minZ = 0;
492
- this._maxX = 1;
493
- this._maxZ = 1;
494
- }
495
- get grassInstances() {
496
- return this._grassInstances.get();
497
- }
498
- get children() {
499
- return this._children;
500
- }
501
- draw(camera, region, minY, maxY, skipClipTest) {
502
- if (!skipClipTest) {
503
- const cullAABB = GrassQuadtreeNode._cullAABB;
504
- const x = region.x;
505
- const z = region.y;
506
- const dx = region.z - x;
507
- const dz = region.w - z;
508
- cullAABB.minPoint.setXYZ(x + this._minX * dx, minY, z + this._minZ * dz);
509
- cullAABB.maxPoint.setXYZ(x + this._maxX * dx, maxY, z + this._maxZ * dz);
510
- const clipState = camera.clipMask ? cullAABB.getClipStateWithFrustumMask(camera.frustum, camera.clipMask) : cullAABB.getClipStateWithFrustum(camera.frustum);
511
- if (clipState === ClipState.NOT_CLIPPED) {
512
- return;
513
- }
514
- skipClipTest = clipState === ClipState.A_INSIDE_B;
515
- }
516
- if (this._grassInstances.get().numInstances > 0) {
517
- this._grassInstances.get().draw();
518
- }
519
- if (this._children) {
520
- for (const child of this._children){
521
- child.draw(camera, region, minY, maxY, skipClipTest);
522
- }
523
- }
524
- }
525
- setBaseVertexBuffer(baseVertexBuffer) {
526
- if (baseVertexBuffer !== this._baseVertexBuffer.get()) {
527
- this._baseVertexBuffer.set(baseVertexBuffer);
528
- this._grassInstances.get().setBaseVertexBuffer(baseVertexBuffer);
529
- if (this._children) {
530
- for (const child of this._children){
531
- child.setBaseVertexBuffer(baseVertexBuffer);
532
- }
533
- }
534
- }
535
- }
536
- removeInstances(minX, minZ, maxX, maxZ, numInstances) {
537
- let n = Math.min(this._grassInstances.get().numInstances, numInstances);
538
- if (n <= 0) {
539
- return 0;
540
- }
541
- let removed = 0;
542
- if (this._children) {
543
- for (const child of this._children){
544
- if (child._minX < maxX && child._minZ < maxZ && child._maxX > minX && child._maxZ > minZ) {
545
- removed += child.removeInstances(minX, minZ, maxX, maxZ, n);
546
- n -= removed;
547
- }
548
- }
549
- }
550
- if (n > 0) {
551
- removed += this._grassInstances.get().removeInstances(minX, minZ, maxX, maxZ, n);
552
- }
553
- return removed;
554
- }
555
- addInstances(instances) {
556
- if (instances.length === 0) {
557
- return 0;
558
- }
559
- let n = Math.min(instances.length, MAX_INSTANCES_PER_NODE - this._grassInstances.get().numInstances);
560
- this._grassInstances.get().addInstances(instances.slice(0, n));
561
- if (n < instances.length) {
562
- if (!this._children) {
563
- this._children = [
564
- new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get()),
565
- new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get()),
566
- new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get()),
567
- new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get())
568
- ];
569
- this._children[0]._minX = this._minX;
570
- this._children[0]._minZ = this._minZ;
571
- this._children[0]._maxX = (this._minX + this._maxX) * 0.5;
572
- this._children[0]._maxZ = (this._minZ + this._maxZ) * 0.5;
573
- this._children[1]._minX = this._children[0]._maxX;
574
- this._children[1]._minZ = this._minZ;
575
- this._children[1]._maxX = this._maxX;
576
- this._children[1]._maxZ = this._children[0]._maxZ;
577
- this._children[2]._minX = this._minX;
578
- this._children[2]._minZ = this._children[0]._maxZ;
579
- this._children[2]._maxX = this._children[0]._maxX;
580
- this._children[2]._maxZ = this._maxZ;
581
- this._children[3]._minX = this._children[0]._maxX;
582
- this._children[3]._minZ = this._children[0]._maxZ;
583
- this._children[3]._maxX = this._maxX;
584
- this._children[3]._maxZ = this._maxZ;
585
- }
586
- instances = instances.slice(n);
587
- for (const child of this._children){
588
- if (instances.length > 0) {
589
- const childInstances = instances.filter((val)=>val.x >= child._minX && val.y >= child._minZ && val.x < child._maxX && val.y < child._maxZ);
590
- n += child.addInstances(childInstances);
591
- instances = instances.filter((val)=>!(val.x >= child._minX && val.y >= child._minZ && val.x < child._maxX && val.y < child._maxZ));
592
- }
593
- }
594
- }
595
- return n;
596
- }
597
- onDispose() {
598
- super.onDispose();
599
- this._baseVertexBuffer.dispose();
600
- this._indexBuffer.dispose();
601
- this._grassInstances.dispose();
602
- if (this._children) {
603
- for (const child of this._children){
604
- child.dispose();
605
- }
606
- }
607
- }
608
- }
609
633
 
610
- export { GrassInstances, GrassLayer, GrassQuadtreeNode, GrassRenderer };
634
+ export { GrassInstances, GrassLayer, GrassRenderer };
611
635
  //# sourceMappingURL=grass.js.map