@woosh/meep-engine 2.43.9 → 2.43.12

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 (29) hide show
  1. package/core/binary/is_data_url.js +12 -0
  2. package/core/binary/url_to_data_url.js +28 -0
  3. package/core/collection/queue/Deque.js +2 -2
  4. package/core/geom/3d/shape/AbstractShape3D.js +9 -0
  5. package/core/geom/3d/shape/TransformedShape3D.js +8 -0
  6. package/core/geom/3d/shape/UnionShape3D.js +35 -0
  7. package/core/geom/3d/shape/UnitCubeShape3D.js +10 -0
  8. package/core/geom/3d/shape/UnitSphereShape3D.js +10 -0
  9. package/editor/ecs/component/editors/ImagePathEditor.js +15 -66
  10. package/editor/ecs/component/editors/LargeStrongEditor.js +107 -0
  11. package/engine/graphics/ecs/path/PathDisplaySystem.js +92 -4
  12. package/engine/graphics/ecs/path/tube/build/{GeometryOutput.js → StreamGeometryBuilder.js} +1 -1
  13. package/engine/graphics/ecs/path/tube/build/TubePathBuilder.js +16 -14
  14. package/engine/graphics/ecs/path/tube/build/build_geometry_catmullrom.spec.js +32 -0
  15. package/engine/graphics/ecs/path/tube/build/computeFrenetFrames.js +6 -1
  16. package/engine/graphics/ecs/path/tube/build/makeTubeGeometry.js +6 -4
  17. package/engine/graphics/ecs/path/tube/build/make_cap.js +10 -10
  18. package/engine/graphics/ecs/path/tube/build/make_ring_faces.js +1 -1
  19. package/engine/graphics/ecs/path/tube/build/make_ring_vertices.js +1 -1
  20. package/engine/graphics/impostors/octahedral/shader/ImpostorShaderV0.js +143 -27
  21. package/engine/graphics/micron/plugin/shaded_geometry/MicronShadedGeometryRenderAdapter.js +5 -0
  22. package/engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.d.ts +3 -1
  23. package/engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.js +148 -5
  24. package/engine/graphics/particles/particular/engine/utils/volume/SamplingFunctionKind.d.ts +4 -0
  25. package/engine/graphics/particles/particular/engine/utils/volume/SamplingFunctionKind.js +8 -0
  26. package/engine/graphics/particles/particular/engine/utils/volume/prototypeParticleVolume.js +7 -4
  27. package/engine/navigation/ecs/components/Path.spec.js +21 -0
  28. package/package.json +1 -1
  29. package/samples/terrain/from_image_2.js +33 -0
@@ -1,10 +1,10 @@
1
1
  import { BufferGeometry, Vector3 } from "three";
2
- import { GeometryOutput } from "./GeometryOutput.js";
2
+ import { StreamGeometryBuilder } from "./StreamGeometryBuilder.js";
3
3
  import { CapType } from "../CapType.js";
4
4
  import { make_ring_vertices } from "./make_ring_vertices.js";
5
5
  import { assert } from "../../../../../../core/assert.js";
6
6
  import { make_ring_faces } from "./make_ring_faces.js";
7
- import { compute_cap_geometry_size, make_cap } from "./make_cap.js";
7
+ import { append_compute_cap_geometry_size, make_cap } from "./make_cap.js";
8
8
  import { v3_angle_cos_between } from "../../../../../../core/geom/v3_angle_between.js";
9
9
 
10
10
 
@@ -12,6 +12,7 @@ const v4_array = new Float32Array(4);
12
12
 
13
13
  /**
14
14
  * @see https://github.com/mrdoob/three.js/blob/master/src/geometries/TubeGeometry.js
15
+ * @see https://github.com/hofk/THREEg.js/blob/488f1128a25321a76888aa1fa19db64750318444/THREEg.js#L3483
15
16
  * @param {Float32Array|number[]} in_positions
16
17
  * @param {Vector3[]} in_normals
17
18
  * @param {Vector3[]} in_binormals
@@ -34,7 +35,7 @@ export function makeTubeGeometry(
34
35
  assert.isNumber(shape_length, 'shape_length');
35
36
  assert.isArrayLike(shape, 'shape');
36
37
 
37
- const out = new GeometryOutput();
38
+ const out = new StreamGeometryBuilder();
38
39
 
39
40
  // helper variables
40
41
 
@@ -48,7 +49,7 @@ export function makeTubeGeometry(
48
49
  };
49
50
 
50
51
  if (!closed) {
51
- compute_cap_geometry_size(2, geometry_size, shape_length, cap_type);
52
+ append_compute_cap_geometry_size(2, geometry_size, shape_length, cap_type);
52
53
  }
53
54
 
54
55
  out.allocate(
@@ -57,6 +58,7 @@ export function makeTubeGeometry(
57
58
  );
58
59
 
59
60
  // create buffer data
61
+
60
62
  if (!closed) {
61
63
  // start cap
62
64
  make_cap(
@@ -50,7 +50,7 @@ function compute_shape_radius(shape, shape_length, cx = 0, cy = 0) {
50
50
  * @param {Vector3[]} in_normals
51
51
  * @param {Vector3[]} in_binormals
52
52
  * @param {Vector3[]} in_tangents
53
- * @param {GeometryOutput} out
53
+ * @param {StreamGeometryBuilder} out
54
54
  * @param {number[]} shape
55
55
  * @param {number[]|Float32Array} shape_normal
56
56
  * @param {number} shape_length
@@ -106,9 +106,9 @@ function make_cap_round(
106
106
 
107
107
  for (i = 0; i <= cap_segment_count; i++) {
108
108
 
109
- const j = direction>0? i:cap_segment_count-i;
109
+ const j = direction > 0 ? i : cap_segment_count - i;
110
110
 
111
- const angle_b = j * angular_step_i ;
111
+ const angle_b = j * angular_step_i;
112
112
 
113
113
 
114
114
  const cos_b = Math.cos(angle_b);
@@ -151,9 +151,9 @@ const v4_no_bend = new Float32Array([0, 1, 0, 0]);
151
151
  * @param {Vector3[]} in_normals
152
152
  * @param {Vector3[]} in_binormals
153
153
  * @param {Vector3[]} in_tangents
154
- * @param {GeometryOutput} out
154
+ * @param {StreamGeometryBuilder} out
155
155
  * @param {number[]} shape
156
- * @param {number[]|Float32Array} shape_normals
156
+ * @param {number[]|Float32Array} shape_normal
157
157
  * @param {number} shape_length
158
158
  * @param {number[]} shape_transform
159
159
  * @param {number} direction
@@ -211,7 +211,7 @@ function make_cap_flat(
211
211
  * @param {Float32Array|number[]} in_positions
212
212
  * @param {Vector3[]} in_normals
213
213
  * @param {Vector3[]} in_binormals
214
- * @param {GeometryOutput} out
214
+ * @param {StreamGeometryBuilder} out
215
215
  * @param {Vector3[]} in_tangents
216
216
  * @param {number[]} shape
217
217
  * @param {number[]|Float32Array} shape_normal
@@ -250,13 +250,13 @@ export function make_cap(
250
250
  }
251
251
 
252
252
  /**
253
- *
253
+ * Increases target geometry buffers to accommodate a given number of caps
254
254
  * @param {number} count how many caps
255
- * @param {{polygon_count:number, vertex_count:number}} out
256
- * @param {number} radial_segments
255
+ * @param {{polygon_count:number, vertex_count:number}} out where to increment
256
+ * @param {number} radial_segments number of lines that make up profile of the extruded shape
257
257
  * @param {CapType} type
258
258
  */
259
- export function compute_cap_geometry_size(count, out, radial_segments, type) {
259
+ export function append_compute_cap_geometry_size(count, out, radial_segments, type) {
260
260
 
261
261
  if (type === CapType.None) {
262
262
  // do nothing
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  *
3
- * @param {GeometryOutput} out
3
+ * @param {StreamGeometryBuilder} out
4
4
  * @param {number} index_offset
5
5
  * @param {number} path_segments
6
6
  * @param {number} profile_segments
@@ -4,7 +4,7 @@ import { v3_dot } from "../../../../../../core/geom/v3_dot.js";
4
4
 
5
5
  /**
6
6
  *
7
- * @param {GeometryOutput} out
7
+ * @param {StreamGeometryBuilder} out
8
8
  * @param {number} Px
9
9
  * @param {number} Py
10
10
  * @param {number} Pz
@@ -27,13 +27,28 @@ const shader_vx = `
27
27
  out vec3 local_ray_far;
28
28
 
29
29
  flat out vec2 frame1;
30
+ flat out vec3 frame1_normal;
30
31
  out vec2 uv_frame1;
31
32
  out vec2 xy_frame1;
32
33
 
34
+ flat out vec2 frame2;
35
+ flat out vec3 frame2_normal;
36
+ out vec2 uv_frame2;
37
+ out vec2 xy_frame2;
38
+
39
+ flat out vec2 frame3;
40
+ flat out vec3 frame3_normal;
41
+ out vec2 uv_frame3;
42
+ out vec2 xy_frame3;
43
+
44
+ out vec4 blend_weights;
45
+
33
46
  uniform mat4 modelViewMatrix;
34
47
  uniform mat4 projectionMatrix;
35
48
  uniform mat3 normalMatrix;
36
49
  uniform mat4 modelMatrix;
50
+ uniform mat4 viewMatrix;
51
+ uniform vec3 cameraPosition;
37
52
 
38
53
  uniform vec3 uOffset;
39
54
  uniform float uRadius;
@@ -143,6 +158,16 @@ const shader_vx = `
143
158
  return res;
144
159
  }
145
160
 
161
+ vec3 projectOnPlaneBasis(vec3 ray, vec3 plane_normal, vec3 plane_x, vec3 plane_y)
162
+ {
163
+ //reproject plane normal onto planeXY basos
164
+ return normalize(vec3(
165
+ dot(plane_x, ray),
166
+ dot(plane_y, ray),
167
+ dot(plane_normal, ray)
168
+ ));
169
+ }
170
+
146
171
  void calcuateXYbasis(vec3 plane_normal, out vec3 plane_x, out vec3 plane_y)
147
172
  {
148
173
  vec3 up = vec3(0,1,0);
@@ -155,14 +180,56 @@ const shader_vx = `
155
180
  plane_y = normalize(cross(plane_x, plane_normal));
156
181
  }
157
182
 
183
+ //this function works well in orthogonal projection. It works okeyish with further distances of perspective projection
184
+ vec2 virtualPlaneUV(vec3 plane_normal, vec3 plane_x, vec3 plane_y, vec3 pivotToCameraRay, vec3 vertexToCameraRay, float size)
185
+ {
186
+ plane_normal = normalize(plane_normal);
187
+ plane_x = normalize(plane_x);
188
+ plane_y = normalize(plane_y);
189
+
190
+ // plane_normal is normalized but pivotToCameraRay & vertexToCameraRay are NOT
191
+ // so what are we doing here ?
192
+ // We are calculating length of pivotToCameraRay projected onto plane_normal
193
+ // so pivotToCameraRay is vector to camera from CENTER OF object
194
+ // we are recalculting this distance taking into account new plane normal
195
+ float projectedNormalRayLength = dot(plane_normal, pivotToCameraRay);
196
+ // tihs is direction is almost the same as origin, but its to individual vertex
197
+ // not sure this is correct for perspective projection
198
+ float projectedVertexRayLength = dot(plane_normal, vertexToCameraRay);
199
+ // basically its length difference betwen center and vertex - "not precise"
200
+ // projectedVertexRayLength is bigger than projectedNormalRayLength when vertex is
201
+ // further than "main front facing billboard"
202
+ // so offsetLength is getting smaller, otherwise is getting bigger
203
+ float offsetLength = projectedNormalRayLength/projectedVertexRayLength;
204
+
205
+ // ok so offsetLength is just a length
206
+ // we want a vector so we multiply it by vertexToCameraRay to get this offset
207
+ // now what are we REALY doing is calculuating distance difference
208
+ // se are SUBSTRACTING pivotToCameraRay vector
209
+ // we would get difference between center of plane and vertex rotated
210
+ vec3 offsetVector = vertexToCameraRay * offsetLength - pivotToCameraRay;
211
+
212
+ // we got the offset of rotated vertex, but we need to offset it from correct plane axis
213
+ // so again we projecting length of intersection (offset of rotated vertex) onto plane_x
214
+ // and plane_y
215
+ vec2 duv = vec2(
216
+ dot(plane_x, offsetVector),
217
+ dot(plane_y, offsetVector)
218
+ );
219
+
220
+ //we are in space -1 to 1
221
+ duv /= 2.0 * size;
222
+ duv += 0.5;
223
+ return duv;
224
+ }
158
225
 
159
226
  void main() {
160
227
  vUv = uv;
161
228
 
162
229
  vec2 framesMinusOne = uFrames - vec2(1.0);
163
230
 
164
- vec3 cameraPos_WS = (projectionMatrix * vec4(vec3(0), 1.0)).xyz;
165
- vec3 cameraPos_OS = (inverse(modelMatrix) * vec4(cameraPos_WS, 1.0)).xyz;
231
+ vec3 cameraPos_WS = cameraPosition;
232
+ vec3 cameraPos_OS = (inverse(viewMatrix) * vec4(cameraPos_WS, 1.0)).xyz;
166
233
 
167
234
  //TODO: check if this is correct. We are using orho projected images, so
168
235
  // camera far away
@@ -206,7 +273,7 @@ const shader_vx = `
206
273
  //get 2D projection of this vertex in normalized device coordinates
207
274
  vec2 pos = gl_Position.xy/gl_Position.w;
208
275
 
209
- vec3 projected = position;
276
+ vec3 projected = object_scale*position;
210
277
  vec3 vertexToCameraRay = (pivotToCameraRay - (projected));
211
278
  vec3 vertexToCameraDir = normalize(vertexToCameraRay);
212
279
 
@@ -221,25 +288,44 @@ const shader_vx = `
221
288
  //
222
289
  vec3 view_direction = normalize(local_ray_near-local_ray_far);
223
290
 
224
- vec2 octahedral_uv = clamp(VectorToGrid(view_direction)*0.5 + 0.5, 0.0, 1.0);
225
- vec2 grid = octahedral_uv * vec2(uFrames - 1.0);
291
+ vec2 grid = VectorToGrid(-view_direction);
292
+ //bias and scale to 0 to 1
293
+ grid = clamp((grid + 1.0) * 0.5, vec2(0, 0), vec2(1, 1));
294
+ grid *= framesMinusOne;
295
+ grid = clamp(grid, vec2(0), vec2(framesMinusOne));
296
+ vec2 gridFloor = min(floor(grid), framesMinusOne);
297
+ vec2 gridFract = fract(grid);
226
298
 
227
- vec2 gridFrac = fract(grid);
228
- vec2 gridFloor = floor(grid);
299
+ blend_weights = TriangleInterpolate( gridFract );
229
300
 
230
- vec4 weights = TriangleInterpolate( gridFrac );
301
+ frame1 = gridFloor;
302
+ frame2 = gridFloor + mix(vec2(0,1),vec2(1,0),blend_weights.w);
303
+ frame3 = gridFloor + vec2(1.0,1.0);
231
304
 
232
- vec2 frame1 = gridFloor;
233
- vec2 frame2 = gridFloor + mix(vec2(0,1),vec2(1,0),weights.w);
234
- vec2 frame3 = gridFloor + vec2(1.0,1.0);
305
+ vec3 projectedQuadADir = FrameXYToRay(frame1, framesMinusOne);
306
+ vec3 projectedQuadBDir = FrameXYToRay(frame2, framesMinusOne);
307
+ vec3 projectedQuadCDir = FrameXYToRay(frame3, framesMinusOne);
308
+
309
+ frame1_normal = (modelViewMatrix *vec4(projectedQuadADir, 0)).xyz;
310
+ frame2_normal = (modelViewMatrix *vec4(projectedQuadBDir, 0)).xyz;
311
+ frame3_normal = (modelViewMatrix *vec4(projectedQuadCDir, 0)).xyz;
235
312
 
236
313
  vec3 plane_x1, plane_y1, plane_x2, plane_y2, plane_x3, plane_y3;
237
- vec3 projectedQuadADir = FrameXYToRay(frame1, framesMinusOne);
314
+
238
315
  calcuateXYbasis(projectedQuadADir, plane_x1, plane_y1);
239
316
 
240
- uv_frame1 = virtualPlaneUV(projectedQuadADir, plane_x1, plane_y1, pivotToCameraRay, vertexToCameraRay, scale);
317
+ uv_frame1 = virtualPlaneUV(projectedQuadADir, plane_x1, plane_y1, pivotToCameraRay, vertexToCameraRay, uRadius);
241
318
  xy_frame1 = projectOnPlaneBasis(-vertexToCameraDir, projectedQuadADir, plane_x1, plane_y1).xy;
319
+
320
+ calcuateXYbasis(projectedQuadBDir, plane_x2, plane_y2);
321
+ uv_frame2 = virtualPlaneUV(projectedQuadBDir, plane_x2, plane_y2, pivotToCameraRay, vertexToCameraRay, uRadius);
322
+ xy_frame2 = projectOnPlaneBasis(-vertexToCameraDir, projectedQuadBDir, plane_x2, plane_y2).xy;
242
323
 
324
+ calcuateXYbasis(projectedQuadCDir, plane_x3, plane_y3);
325
+ uv_frame3 = virtualPlaneUV(projectedQuadCDir, plane_x3, plane_y3, pivotToCameraRay, vertexToCameraRay, uRadius);
326
+ xy_frame3 = projectOnPlaneBasis(-vertexToCameraDir, projectedQuadCDir, plane_x3, plane_y3).xy;
327
+
328
+
243
329
  }
244
330
  `;
245
331
  const shader_fg = `
@@ -269,6 +355,23 @@ const shader_fg = `
269
355
  in vec3 local_ray_near;
270
356
  in vec3 local_ray_far;
271
357
 
358
+ flat in vec2 frame1;
359
+ flat in vec3 frame1_normal;
360
+ in vec2 uv_frame1;
361
+ in vec2 xy_frame1;
362
+
363
+ flat in vec2 frame2;
364
+ flat in vec3 frame2_normal;
365
+ in vec2 uv_frame2;
366
+ in vec2 xy_frame2;
367
+
368
+ flat in vec2 frame3;
369
+ flat in vec3 frame3_normal;
370
+ in vec2 uv_frame3;
371
+ in vec2 xy_frame3;
372
+
373
+ in vec4 blend_weights;
374
+
272
375
  struct Material{
273
376
  vec3 diffuse;
274
377
  vec3 normal;
@@ -394,14 +497,14 @@ const shader_fg = `
394
497
  ));
395
498
  }
396
499
 
397
- vec2 recompute_uv(vec2 frame, vec2 frame_uv, sampler2D gBuffer){
500
+ vec2 recompute_uv(vec2 frame, vec2 frame_uv, vec2 xy_f, sampler2D gBuffer){
398
501
  vec2 frame_size = vec2(1.0/ uFrames);
399
502
 
400
503
  vec2 source_uv = (frame + frame_uv)*frame_size;
401
504
 
402
505
  float n_depth = texture(gBuffer, source_uv).a;
403
506
 
404
- vec2 offset = clamp(length(frame_uv*2.0 - 1.0) * vec2(0.5-n_depth ) * depth_scale,0.0, 1.0);
507
+ vec2 offset = clamp(xy_f * vec2(0.5-n_depth ) * depth_scale,0.0, 1.0);
405
508
 
406
509
  vec2 uv_f = clamp(frame_uv+offset, 0.0, 1.0);
407
510
 
@@ -411,6 +514,21 @@ const shader_fg = `
411
514
  // return source_uv;
412
515
  }
413
516
 
517
+ vec2 recalculateUV(vec2 uv_f, vec2 frame, vec2 xy_f, vec2 frame_size, float d_scale, sampler2D depthTexture)
518
+ {
519
+ //clamp for parallax sampling
520
+ uv_f = clamp(uv_f, vec2(0.0), vec2(1.0));
521
+ vec2 uv_quad = frame_size * (frame + uv_f);
522
+ //paralax
523
+ vec4 n_depth = (texture( depthTexture, uv_quad));
524
+ uv_f = xy_f * (0.5-n_depth.a) * d_scale + uv_f;
525
+ //clamp parallax offset
526
+ uv_f = clamp(uv_f, vec2(0.0), vec2(1.0));
527
+ uv_f = frame_size * (frame + uv_f);
528
+ //clamped full UV
529
+ return clamp(uv_f, vec2(0.0), vec2(1.0));
530
+ }
531
+
414
532
  void main(){
415
533
  vec3 view_direction = normalize(local_ray_near-local_ray_far);
416
534
 
@@ -420,19 +538,17 @@ const shader_fg = `
420
538
  vec2 gridFrac = fract(grid);
421
539
  vec2 gridFloor = floor(grid);
422
540
 
423
- vec4 weights = TriangleInterpolate( gridFrac );
541
+ vec4 weights = blend_weights;
424
542
 
425
543
  vec2 frame_uv = vUv;
426
544
 
427
545
  //3 nearest frames
428
- vec2 frame0 = gridFloor;
429
- vec2 frame1 = gridFloor + mix(vec2(0,1),vec2(1,0),weights.w);
430
- vec2 frame2 = gridFloor + vec2(1.0,1.0);
431
-
432
- vec2 uv0 = recompute_uv(frame0, frame_uv, tGeometry);
433
- vec2 uv1 = recompute_uv(frame1, frame_uv, tGeometry);
434
- vec2 uv2 = recompute_uv(frame2, frame_uv, tGeometry);
435
-
546
+ vec2 quad_size = vec2(1.0) / uFrames;
547
+ vec2 uv_f1 = recalculateUV(uv_frame1, frame1, xy_frame1, quad_size, depth_scale, tGeometry);
548
+ vec2 uv_f2 = recalculateUV(uv_frame2, frame2, xy_frame2, quad_size, depth_scale, tGeometry);
549
+ vec2 uv_f3 = recalculateUV(uv_frame3, frame3, xy_frame3, quad_size, depth_scale, tGeometry);
550
+
551
+
436
552
  vec4 ddxy = vec4( dFdx(vUv.xy), dFdy(vUv.xy) );
437
553
 
438
554
  vec2 frame_size = vec2(1.0/ uFrames);
@@ -440,9 +556,9 @@ const shader_fg = `
440
556
  vec4 texel_color = ImposterBlendWeights(
441
557
  // vec4 texel_color = ImposterBlendWeightsNearest(
442
558
  tBase,
443
- uv0,
444
- uv1,
445
- uv2,
559
+ uv_f1,
560
+ uv_f2,
561
+ uv_f3,
446
562
  weights, ddxy
447
563
  );
448
564
 
@@ -139,6 +139,10 @@ export class MicronShadedGeometryRenderAdapter extends AbstractRenderAdapter {
139
139
 
140
140
  scratch_mvc.material = sg.material;
141
141
  scratch_mvc.spec = micron_geometry.vertex_spec;
142
+
143
+ // clear all flags
144
+ scratch_mvc.flags = 0;
145
+
142
146
  scratch_mvc.writeFlag(MaterialVertexSpecFlags.CastShadow, sg.getFlag(ShadedGeometryFlags.CastShadow));
143
147
  scratch_mvc.writeFlag(MaterialVertexSpecFlags.ReceiveShadow, sg.getFlag(ShadedGeometryFlags.ReceiveShadow));
144
148
 
@@ -175,6 +179,7 @@ export class MicronShadedGeometryRenderAdapter extends AbstractRenderAdapter {
175
179
  // clear context
176
180
  const material_contexts = this.ctx.contexts_array;
177
181
  const contexts_count = material_contexts.length;
182
+
178
183
  for (let i = 0; i < contexts_count; i++) {
179
184
  const mc = material_contexts[i];
180
185
 
@@ -1,13 +1,15 @@
1
1
  import EntityBuilder from "../../../../../../ecs/EntityBuilder";
2
2
  import {AbstractShape3D} from "../../../../../../../core/geom/3d/shape/AbstractShape3D";
3
3
  import {AttributeValue} from "./AttributeValue";
4
+ import {SamplingFunctionKind} from "./SamplingFunctionKind";
4
5
 
5
6
  interface Args {
6
7
  density?: number
7
8
  particle_size?: number
8
9
  shape?: AbstractShape3D
9
10
  sort?: boolean
10
- sprite?: string
11
+ sprite?: string,
12
+ sampling_function?: SamplingFunctionKind
11
13
  }
12
14
 
13
15
  interface Attributes {
@@ -12,6 +12,10 @@ import { PARTICULAR_PARTICLE_SPECIFICATION } from "../../emitter/PARTICULAR_PART
12
12
  import { SerializationMetadata } from "../../../../../../ecs/components/SerializationMetadata.js";
13
13
  import { UnitCubeShape3D } from "../../../../../../../core/geom/3d/shape/UnitCubeShape3D.js";
14
14
  import { shape_from_json } from "../../../../../../../core/geom/3d/shape/json/shape_from_json.js";
15
+ import { min2 } from "../../../../../../../core/math/min2.js";
16
+ import { max2 } from "../../../../../../../core/math/max2.js";
17
+ import { SamplingFunctionKind } from "./SamplingFunctionKind.js";
18
+ import { assert } from "../../../../../../../core/assert.js";
15
19
 
16
20
  const DEFAULT_ATTRIBUTE_VALUES = {
17
21
  color: [1, 1, 1, 1],
@@ -32,6 +36,12 @@ const EXPOSED_ATTRIBUTES = [
32
36
  "blending"
33
37
  ];
34
38
 
39
+ /**
40
+ * Maximum allowed number of particles per emitter, used as a sanity constraint to prevent renderer from crashing
41
+ * @type {number}
42
+ */
43
+ const LIMIT_PARTICLE_COUNT = 1000000;
44
+
35
45
  export class ParticleVolume {
36
46
 
37
47
  constructor() {
@@ -176,6 +186,13 @@ export class ParticleVolume {
176
186
  */
177
187
  this.__shape = new UnitCubeShape3D();
178
188
 
189
+ /**
190
+ *
191
+ * @type {SamplingFunctionKind|number}
192
+ * @private
193
+ */
194
+ this.__sampling_function = SamplingFunctionKind.Random;
195
+
179
196
  const eb = new EntityBuilder();
180
197
 
181
198
  eb.add(this.__emitter)
@@ -254,6 +271,7 @@ export class ParticleVolume {
254
271
  * @param {boolean} sort
255
272
  * @param {boolean} lighting
256
273
  * @param {string} [sprite]
274
+ * @param {SamplingFunctionKind|number} [sampling_function]
257
275
  */
258
276
  from({
259
277
  density = 1,
@@ -261,12 +279,17 @@ export class ParticleVolume {
261
279
  shape = new UnitCubeShape3D(),
262
280
  sort = false,
263
281
  lighting = true,
264
- sprite = "data/textures/particle/smokeparticle.png"
282
+ sprite = "data/textures/particle/smokeparticle.png",
283
+ sampling_function = SamplingFunctionKind.Random
265
284
  } = {}) {
266
285
 
286
+ assert.enum(sampling_function, SamplingFunctionKind, 'sampling_function');
287
+
267
288
  this.__density = density;
268
289
  this.__shape = shape;
269
290
 
291
+ this.__sampling_function = sampling_function;
292
+
270
293
  const emitter = this.__emitter;
271
294
 
272
295
  emitter.writeFlag(ParticleEmitterFlag.DepthSorting, sort);
@@ -289,6 +312,7 @@ export class ParticleVolume {
289
312
  * @param {boolean} [is_lit]
290
313
  * @param {string} sprite
291
314
  * @param {Object<Object>} [attributes]
315
+ * @param {string} [sampling_function]
292
316
  */
293
317
  fromJSON({
294
318
  density,
@@ -298,20 +322,28 @@ export class ParticleVolume {
298
322
  is_sorted = true,
299
323
  is_lit = true,
300
324
  sprite,
301
- attributes = {}
325
+ attributes = {},
326
+ sampling_function = 'Random'
302
327
  }) {
303
328
 
304
329
  const shape_object = shape_from_json(shape);
305
330
 
306
331
  const actual_density = is_density_size_adjusted ? density / particle_size : density;
307
332
 
333
+ const sf_type = SamplingFunctionKind[sampling_function];
334
+
335
+ if (sf_type === undefined) {
336
+ throw new Error(`Unsupported sampling function type '${sampling_function}', valid values:[${Object.keys(SamplingFunctionKind).join(', ')}]`);
337
+ }
338
+
308
339
  this.from({
309
340
  density: actual_density,
310
341
  shape: shape_object,
311
342
  particle_size,
312
343
  sort: is_sorted,
313
344
  lighting: is_lit,
314
- sprite
345
+ sprite,
346
+ sampling_function: sf_type
315
347
  });
316
348
 
317
349
  // parse attributes
@@ -347,12 +379,118 @@ export class ParticleVolume {
347
379
 
348
380
  }
349
381
 
382
+ __distribute_grid() {
383
+ const emitter = this.__emitter;
384
+ const particles = emitter.particles;
385
+
386
+ const layer = emitter.layers.get(0);
387
+
388
+ // build transform matrix
389
+ const m4_transform = new Matrix4();
390
+
391
+ const t = this.__transform;
392
+
393
+ composeMatrix4(m4_transform, t.position, t.rotation, t.scale);
394
+
395
+ const m4_layer = new Matrix4();
396
+
397
+ composeMatrix4(m4_layer, layer.position, new Quaternion(), layer.scale);
398
+
399
+ const v3_position = [];
400
+
401
+ const m4_final = new Matrix4();
402
+
403
+ m4_final.multiplyMatrices(m4_transform, m4_layer);
404
+
405
+
406
+ // cache variables to save a bit on de-referencing
407
+ const matrix_elements = m4_final.elements;
408
+ const shape = this.__shape;
409
+
410
+ const ts = shape;
411
+
412
+ const bounding_box = [];
413
+
414
+ ts.compute_bounding_box(bounding_box);
415
+
416
+ const density_step = 1 / Math.pow(this.__density, 1 / 3);
417
+
418
+ let particle_index = 0;
419
+
420
+ // apply margin to the space, this will shrink the grid spacing to align particles more evenly within the bounding volume
421
+ const density_margin = density_step * 0.5;
422
+
423
+ const mid_x = (bounding_box[3] + bounding_box[0]) * 0.5;
424
+ const mid_y = (bounding_box[4] + bounding_box[1]) * 0.5;
425
+ const mid_z = (bounding_box[5] + bounding_box[2]) * 0.5;
426
+
427
+ const x0 = min2(mid_x, bounding_box[0] + density_margin);
428
+ const x1 = max2(mid_x, bounding_box[3] - density_margin);
429
+
430
+ const y0 = min2(mid_y, bounding_box[1] + density_margin);
431
+ const y1 = max2(mid_y, bounding_box[4] - density_margin);
432
+
433
+ const z0 = min2(mid_z, bounding_box[2] + density_margin);
434
+ const z1 = max2(mid_z, bounding_box[5] - density_margin);
435
+
436
+ main_loop:for (let x = x0; x <= x1; x += density_step) {
437
+ for (let y = y0; y <= y1; y += density_step) {
438
+ for (let z = z0; z <= z1; z += density_step) {
439
+
440
+ v3_position[0] = x;
441
+ v3_position[1] = y;
442
+ v3_position[2] = z;
443
+
444
+ if (ts.contains_point(v3_position)) {
445
+
446
+ vec3.transformMat4(v3_position, v3_position, matrix_elements);
447
+
448
+ particles.writeAttributeVector3(particle_index, PARTICLE_ATTRIBUTE_POSITION, v3_position[0], v3_position[1], v3_position[2]);
449
+
450
+ particle_index++;
451
+
452
+ if (particle_index >= particles.capacity) {
453
+ break main_loop;
454
+ }
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ if (particle_index < particles.capacity) {
461
+ // crop out remaining particles in the pool
462
+ particles.occupancy.clearRange(particle_index, particles.capacity);
463
+ particles.update();
464
+ }
465
+
466
+ // request bounds update
467
+ emitter.setFlag(ParticleEmitterFlag.ParticleBoundsNeedUpdate);
468
+ emitter.computeBoundingBox();
469
+ }
470
+
471
+ /**
472
+ *
473
+ * @param {function} random
474
+ * @private
475
+ */
476
+ __distribute(random) {
477
+ const sf = this.__sampling_function;
478
+
479
+ if (sf === SamplingFunctionKind.Random) {
480
+ this.__distribute_random(random);
481
+ } else if (sf === SamplingFunctionKind.Grid) {
482
+ this.__distribute_grid();
483
+ } else {
484
+ throw new Error(`Unsupported sampling function '${sf}'`);
485
+ }
486
+ }
487
+
350
488
  /**
351
489
  * Distribute particles in the volume
352
490
  * @param {function():number} random
353
491
  * @private
354
492
  */
355
- __distribute(random) {
493
+ __distribute_random(random) {
356
494
  const emitter = this.__emitter;
357
495
  const particles = emitter.particles;
358
496
  const particle_count = particles.size();
@@ -436,8 +574,13 @@ export class ParticleVolume {
436
574
  const volume_value = this.__shape.volume;
437
575
 
438
576
  // total particle count
439
- const particle_count = Math.max(1, Math.ceil(volume_value * this.__density));
577
+ let particle_count = Math.max(1, Math.ceil(volume_value * this.__density));
440
578
 
579
+ if (particle_count > LIMIT_PARTICLE_COUNT) {
580
+ console.warn(`ParticleVolume exceeds maximum allowed particle count (limit=${LIMIT_PARTICLE_COUNT}, requested=${particle_count}), clamping to limit. Consider reducing density (current = ${this.__density} particles per unit of volume)`);
581
+
582
+ particle_count = LIMIT_PARTICLE_COUNT;
583
+ }
441
584
 
442
585
  this.__ensure_initialized();
443
586
 
@@ -0,0 +1,4 @@
1
+ export enum SamplingFunctionKind {
2
+ Random = 0,
3
+ Grid = 1
4
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @readonly
3
+ * @enum {number}
4
+ */
5
+ export const SamplingFunctionKind = {
6
+ Random: 0,
7
+ Grid: 1
8
+ };