babylon.quarks 0.20.0 → 0.23.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.
@@ -28,7 +28,7 @@ import particle_vert_wgsl from './shaders/particle_vert.wgsl';
28
28
  import {registerShaders, shaderLanguageFor, ShaderSources} from './shaders/shaderLanguageSupport';
29
29
  import stretched_bb_particle_vert from './shaders/stretched_bb_particle_vert.glsl';
30
30
  import stretched_bb_particle_vert_wgsl from './shaders/stretched_bb_particle_vert.wgsl';
31
- import {RenderMode, VFXBatch} from './VFXBatch';
31
+ import {defaultMeshLighting, MeshLightingSettings, RenderMode, VFXBatch} from './VFXBatch';
32
32
 
33
33
  export class SpriteBatch extends VFXBatch {
34
34
  private static whiteTextureByScene = new WeakMap<Scene, RawTexture>();
@@ -52,6 +52,10 @@ export class SpriteBatch extends VFXBatch {
52
52
 
53
53
  /** Concretely typed handle on the batch material, see syncStretchedSpeedFactor. */
54
54
  private shaderMaterial: ShaderMaterial | null = null;
55
+ /** The `'fixed'` mesh light, as shader uniforms; see applyMeshLighting. */
56
+ private readonly fixedLightDirection = new BVector3();
57
+ private readonly fixedLightColor = new BVector3();
58
+ private readonly fixedAmbientColor = new BVector3();
55
59
 
56
60
  private offsetVB!: VertexBuffer;
57
61
  private rotationVB!: VertexBuffer;
@@ -62,6 +66,7 @@ export class SpriteBatch extends VFXBatch {
62
66
 
63
67
  constructor(settings: VFXBatchSettings, scene: Scene) {
64
68
  super(settings, scene);
69
+ this.storeFixedLighting(defaultMeshLighting());
65
70
  this.maxParticles = 1000;
66
71
  this.setupBuffers();
67
72
  this.rebuildMaterial();
@@ -213,6 +218,20 @@ export class SpriteBatch extends VFXBatch {
213
218
  if (pivot) {
214
219
  defines.push('PIVOT');
215
220
  }
221
+ const isMesh = this.settings.renderMode === RenderMode.Mesh;
222
+ const lighting = this.settings.lighting ?? 'fixed';
223
+ const lit = isMesh && lighting !== 'unlit';
224
+ if (isMesh && !lit) {
225
+ defines.push('MESH_UNLIT');
226
+ }
227
+ const rim = isMesh ? this.settings.rim : undefined;
228
+ if (rim) {
229
+ defines.push('MESH_RIM');
230
+ if (rim.mode === 'mix') {
231
+ defines.push('MESH_RIM_MIX');
232
+ }
233
+ }
234
+ const needsEyePosition = useEnvAtlas || !!rim;
216
235
 
217
236
  const shaderLanguage = shaderLanguageFor(this.scene.getEngine());
218
237
  registerShaders(shaderName, vertexShader, fragmentShader, shaderLanguage);
@@ -237,9 +256,15 @@ export class SpriteBatch extends VFXBatch {
237
256
  }
238
257
  if (useEnvAtlas) {
239
258
  samplers.push('envAtlas');
240
- uniforms.push('eyePosition');
241
259
  uniforms.push('reflectionLevel');
242
260
  }
261
+ if (needsEyePosition) {
262
+ uniforms.push('eyePosition');
263
+ }
264
+ if (rim) {
265
+ uniforms.push('rimColor');
266
+ uniforms.push('rimParams');
267
+ }
243
268
  if (this.settings.renderMode === RenderMode.StretchedBillBoard) {
244
269
  uniforms.push('speedFactor');
245
270
  }
@@ -254,7 +279,7 @@ export class SpriteBatch extends VFXBatch {
254
279
  if (pivot) {
255
280
  uniforms.push('pivot');
256
281
  }
257
- if (this.settings.renderMode === RenderMode.Mesh) {
282
+ if (lit) {
258
283
  uniforms.push('lightDirection');
259
284
  uniforms.push('lightColor');
260
285
  uniforms.push('ambientColor');
@@ -280,7 +305,14 @@ export class SpriteBatch extends VFXBatch {
280
305
  if (useEnvAtlas && atlas) {
281
306
  mat.setTexture('envAtlas', atlas);
282
307
  mat.setFloat('reflectionLevel', this.settings.reflectionLevel);
308
+ }
309
+ if (rim) {
310
+ mat.setVector3('rimColor', new BVector3(rim.color[0], rim.color[1], rim.color[2]));
311
+ mat.setVector2('rimParams', new BVector2(rim.power, rim.strength));
312
+ }
313
+ if (needsEyePosition) {
283
314
  const eyePosition = new BVector3();
315
+ mat.setVector3('eyePosition', eyePosition);
284
316
  mat.onBindObservable.add(() => {
285
317
  const camera = this.scene.activeCamera;
286
318
  if (!camera) {
@@ -322,10 +354,22 @@ export class SpriteBatch extends VFXBatch {
322
354
  if (pivot) {
323
355
  mat.setVector3('pivot', new BVector3(pivot.x, pivot.y, pivot.z));
324
356
  }
325
- if (this.settings.renderMode === RenderMode.Mesh) {
326
- mat.setVector3('lightDirection', new BVector3(0.4, -1, 0.6));
327
- mat.setVector3('lightColor', new BVector3(1, 1, 1));
328
- mat.setVector3('ambientColor', new BVector3(0.35, 0.35, 0.35));
357
+ if (lit) {
358
+ this.writeFixedLighting(mat);
359
+ if (lighting === 'scene') {
360
+ const direction = new BVector3();
361
+ const color = new BVector3();
362
+ const ambient = new BVector3();
363
+ mat.onBindObservable.add(() => {
364
+ if (SpriteBatch.readSceneLight(this.scene, direction, color, ambient)) {
365
+ mat.setVector3('lightDirection', direction);
366
+ mat.setVector3('lightColor', color);
367
+ mat.setVector3('ambientColor', ambient);
368
+ } else {
369
+ this.writeFixedLighting(mat);
370
+ }
371
+ });
372
+ }
329
373
  }
330
374
 
331
375
  mat.backFaceCulling = false;
@@ -343,6 +387,64 @@ export class SpriteBatch extends VFXBatch {
343
387
  this.shaderMaterial = mat;
344
388
  }
345
389
 
390
+ applyMeshLighting(lighting: MeshLightingSettings): void {
391
+ this.storeFixedLighting(lighting);
392
+ const material = this.shaderMaterial;
393
+ if (material !== null && this.settings.renderMode === RenderMode.Mesh && this.settings.lighting !== 'unlit') {
394
+ this.writeFixedLighting(material);
395
+ }
396
+ }
397
+
398
+ private storeFixedLighting(lighting: MeshLightingSettings): void {
399
+ this.fixedLightDirection.copyFrom(lighting.direction);
400
+ this.fixedLightColor.set(lighting.color.r, lighting.color.g, lighting.color.b);
401
+ this.fixedAmbientColor.set(lighting.ambient.r, lighting.ambient.g, lighting.ambient.b);
402
+ }
403
+
404
+ private writeFixedLighting(material: ShaderMaterial): void {
405
+ material.setVector3('lightDirection', this.fixedLightDirection);
406
+ material.setVector3('lightColor', this.fixedLightColor);
407
+ material.setVector3('ambientColor', this.fixedAmbientColor);
408
+ }
409
+
410
+ /**
411
+ * Reads the scene's key light for `'scene'` mesh lighting: the first
412
+ * enabled directional or hemispheric light, its diffuse colour times its
413
+ * intensity, plus the scene's ambient colour. A hemispheric light lights
414
+ * from its `direction` (which points at the sky) and adds its ground colour
415
+ * to the ambient, which approximates its sky-to-ground blend with the one
416
+ * directional term the shader has. False when there is no such light.
417
+ */
418
+ static readSceneLight(scene: Scene, direction: BVector3, color: BVector3, ambient: BVector3): boolean {
419
+ const lights = scene.lights;
420
+ for (let i = 0; i < lights.length; i++) {
421
+ const light = lights[i];
422
+ const kind = light.getClassName();
423
+ if ((kind !== 'DirectionalLight' && kind !== 'HemisphericLight') || !light.isEnabled()) {
424
+ continue;
425
+ }
426
+ const intensity = light.intensity;
427
+ const diffuse = light.diffuse;
428
+ const sceneAmbient = scene.ambientColor;
429
+ color.set(diffuse.r * intensity, diffuse.g * intensity, diffuse.b * intensity);
430
+ ambient.set(sceneAmbient.r, sceneAmbient.g, sceneAmbient.b);
431
+ const lightDirection = (light as unknown as {direction: BVector3}).direction;
432
+ if (kind === 'DirectionalLight') {
433
+ direction.copyFrom(lightDirection);
434
+ } else {
435
+ direction.copyFrom(lightDirection).scaleInPlace(-1);
436
+ const ground = (light as unknown as {groundColor?: {r: number; g: number; b: number}}).groundColor;
437
+ if (ground) {
438
+ ambient.x += ground.r * intensity;
439
+ ambient.y += ground.g * intensity;
440
+ ambient.z += ground.b * intensity;
441
+ }
442
+ }
443
+ return true;
444
+ }
445
+ return false;
446
+ }
447
+
346
448
  private vector_ = new Vector3();
347
449
  private vector2_ = new Vector3();
348
450
  private vector3_ = new Vector3();
@@ -438,7 +540,9 @@ export class SpriteBatch extends VFXBatch {
438
540
  const residual = system.simulationResidual ?? 0;
439
541
  const stepFraction = residual === 0 ? 0 : residual / (system.simulationStep ?? residual);
440
542
  if (copiedColor) {
441
- if (stepFraction !== 0 && particleNum > 0) {
543
+ // An aliased previous column means nothing changed since the
544
+ // step; the copy is then the carried value.
545
+ if (stepFraction !== 0 && particleNum > 0 && store!.previousColor !== store!.color) {
442
546
  SpriteBatch.continueColor(this.colorBuffer, index * 4, store!, particleNum, stepFraction);
443
547
  } else {
444
548
  this.colorBuffer.set(store!.color.subarray(0, particleNum * 4), index * 4);
@@ -451,7 +555,7 @@ export class SpriteBatch extends VFXBatch {
451
555
  } else {
452
556
  this.offsetBuffer.set(store.position.subarray(0, particleNum * 3), index * 3);
453
557
  }
454
- if (stepFraction !== 0 && particleNum > 0) {
558
+ if (stepFraction !== 0 && particleNum > 0 && store.previousSize !== store.size) {
455
559
  SpriteBatch.continueVector3(
456
560
  this.sizeBuffer,
457
561
  index * 3,
@@ -500,7 +604,7 @@ export class SpriteBatch extends VFXBatch {
500
604
  offsets[o + 2] = (m02 * px + m12 * py + m22 * pz + m32) * w;
501
605
  }
502
606
  const sizes = this.sizeBuffer;
503
- if (stepFraction !== 0) {
607
+ if (stepFraction !== 0 && store!.previousSize !== store!.size) {
504
608
  SpriteBatch.continueVector3(
505
609
  sizes,
506
610
  base,
@@ -521,122 +625,172 @@ export class SpriteBatch extends VFXBatch {
521
625
  }
522
626
 
523
627
  const firstIndex = index;
524
- for (let j = 0; j < particleNum; j++, index++) {
525
- const particle = particles[j] as SpriteParticle;
526
-
527
- if (isMeshRender) {
528
- // Turning behaviors leave the last step's turn on the
529
- // particle; a fraction of it carries the mesh the rest of
530
- // the way to now.
531
- let own = particle.rotation as Quaternion;
532
- const step = particle.angularVelocity;
533
- if (stepFraction !== 0 && step instanceof Quaternion) {
534
- own = this.quaternion4_
535
- .copy(own)
536
- .multiply(SpriteBatch.partialTurn(step, stepFraction, this.quaternion5_));
628
+ // Every particle of a system that emits for itself shares the emitter's transform and,
629
+ // outside mesh mode, rotates by an angle: its scalar attributes are then store columns
630
+ // and go to the buffers as ranges. Only meshes (quaternion rotations) and particles that
631
+ // carry their own parent transform still need to be visited one by one.
632
+ if (sharedTransform && !isMeshRender) {
633
+ const columns = store!;
634
+ this.uvTileBuffer.set(columns.uvTile.subarray(0, particleNum), firstIndex);
635
+ const rotations = this.rotationBuffer;
636
+ rotations.set(columns.rotation.subarray(0, particleNum), firstIndex);
637
+ if (residual !== 0) {
638
+ // Carry the spin on by the time since the last step, as the
639
+ // per-particle path does.
640
+ const rates = columns.angularVelocity;
641
+ for (let j = 0; j < particleNum; j++) {
642
+ rotations[firstIndex + j] += rates[j] * residual;
537
643
  }
538
- let q: Quaternion;
539
- if (systemWorldSpace) {
540
- q = own;
541
- } else {
542
- let parentQ: Quaternion;
543
- if (particle.parentMatrix) {
544
- parentQ = this.quaternion3_.setFromRotationMatrix(particle.parentMatrix);
644
+ }
645
+ if (isStretchedRender && this.velocityBuffer) {
646
+ SpriteBatch.writeVelocities(
647
+ this.velocityBuffer,
648
+ firstIndex * 4,
649
+ columns,
650
+ particleNum,
651
+ stepFraction,
652
+ systemWorldSpace ? null : this.rotationMat_,
653
+ speedFactor,
654
+ lengthFactor
655
+ );
656
+ }
657
+ index += particleNum;
658
+ } else if (sharedTransform && store!.rotationQuaternion.length > 0) {
659
+ // Meshes rotate by quaternion, kept in the store too: turn them in
660
+ // bulk, by the emitter's rotation when the system is local.
661
+ const columns = store!;
662
+ this.uvTileBuffer.set(columns.uvTile.subarray(0, particleNum), firstIndex);
663
+ SpriteBatch.writeQuaternions(
664
+ this.rotationBuffer,
665
+ firstIndex * 4,
666
+ columns,
667
+ particleNum,
668
+ stepFraction,
669
+ systemWorldSpace ? null : rotation
670
+ );
671
+ index += particleNum;
672
+ } else {
673
+ for (let j = 0; j < particleNum; j++, index++) {
674
+ const particle = particles[j] as SpriteParticle;
675
+
676
+ if (isMeshRender) {
677
+ // Turning behaviors leave the last step's turn on the
678
+ // particle; a fraction of it carries the mesh the rest of
679
+ // the way to now.
680
+ let own = particle.rotation as Quaternion;
681
+ const step = particle.angularVelocity;
682
+ if (stepFraction !== 0 && step instanceof Quaternion) {
683
+ own = this.quaternion4_
684
+ .copy(own)
685
+ .multiply(SpriteBatch.partialTurn(step, stepFraction, this.quaternion5_));
686
+ }
687
+ let q: Quaternion;
688
+ if (systemWorldSpace) {
689
+ q = own;
545
690
  } else {
546
- parentQ = rotation;
691
+ let parentQ: Quaternion;
692
+ if (particle.parentMatrix) {
693
+ parentQ = this.quaternion3_.setFromRotationMatrix(particle.parentMatrix);
694
+ } else {
695
+ parentQ = rotation;
696
+ }
697
+ q = this.quaternion_;
698
+ q.copy(parentQ).multiply(own);
547
699
  }
548
- q = this.quaternion_;
549
- q.copy(parentQ).multiply(own);
700
+ const ri = index * 4;
701
+ this.rotationBuffer[ri] = q.x;
702
+ this.rotationBuffer[ri + 1] = q.y;
703
+ this.rotationBuffer[ri + 2] = q.z;
704
+ this.rotationBuffer[ri + 3] = q.w;
705
+ } else {
706
+ const spin = particle.angularVelocity;
707
+ this.rotationBuffer[index] =
708
+ (particle.rotation as number) + (typeof spin === 'number' ? spin * residual : 0);
550
709
  }
551
- const ri = index * 4;
552
- this.rotationBuffer[ri] = q.x;
553
- this.rotationBuffer[ri + 1] = q.y;
554
- this.rotationBuffer[ri + 2] = q.z;
555
- this.rotationBuffer[ri + 3] = q.w;
556
- } else {
557
- const spin = particle.angularVelocity;
558
- this.rotationBuffer[index] =
559
- (particle.rotation as number) + (typeof spin === 'number' ? spin * residual : 0);
560
- }
561
710
 
562
- if (!copiedPositionAndSize) {
563
- const position = particle.position;
564
- const previous = particle.previousPosition;
565
- let px = position.x + (position.x - previous.x) * stepFraction;
566
- let py = position.y + (position.y - previous.y) * stepFraction;
567
- let pz = position.z + (position.z - previous.z) * stepFraction;
568
- if (!systemWorldSpace) {
569
- // Inlined point transform: this is the default (local space)
570
- // path and runs for every particle every frame.
571
- const me = (particle.parentMatrix ?? emitterMatrix).elements;
572
- const w = 1 / (me[3] * px + me[7] * py + me[11] * pz + me[15]);
573
- const tx = (me[0] * px + me[4] * py + me[8] * pz + me[12]) * w;
574
- const ty = (me[1] * px + me[5] * py + me[9] * pz + me[13]) * w;
575
- pz = (me[2] * px + me[6] * py + me[10] * pz + me[14]) * w;
576
- px = tx;
577
- py = ty;
578
- }
711
+ if (!copiedPositionAndSize) {
712
+ const position = particle.position;
713
+ const previous = particle.previousPosition;
714
+ let px = position.x + (position.x - previous.x) * stepFraction;
715
+ let py = position.y + (position.y - previous.y) * stepFraction;
716
+ let pz = position.z + (position.z - previous.z) * stepFraction;
717
+ if (!systemWorldSpace) {
718
+ // Inlined point transform: this is the default (local space)
719
+ // path and runs for every particle every frame.
720
+ const me = (particle.parentMatrix ?? emitterMatrix).elements;
721
+ const w = 1 / (me[3] * px + me[7] * py + me[11] * pz + me[15]);
722
+ const tx = (me[0] * px + me[4] * py + me[8] * pz + me[12]) * w;
723
+ const ty = (me[1] * px + me[5] * py + me[9] * pz + me[13]) * w;
724
+ pz = (me[2] * px + me[6] * py + me[10] * pz + me[14]) * w;
725
+ px = tx;
726
+ py = ty;
727
+ }
579
728
 
580
- const oi = index * 3;
581
- this.offsetBuffer[oi] = px;
582
- this.offsetBuffer[oi + 1] = py;
583
- this.offsetBuffer[oi + 2] = pz;
584
-
585
- const size = particle.size;
586
- const si = index * 3;
587
- // Particle size is already in world units when the system is in world
588
- // space or the particle carries its own parent transform.
589
- if (systemWorldSpace || particle.parentMatrix) {
590
- this.sizeBuffer[si] = size.x;
591
- this.sizeBuffer[si + 1] = size.y;
592
- this.sizeBuffer[si + 2] = size.z;
593
- } else {
594
- this.sizeBuffer[si] = size.x * absScaleX;
595
- this.sizeBuffer[si + 1] = size.y * absScaleY;
596
- this.sizeBuffer[si + 2] = size.z * absScaleZ;
729
+ const oi = index * 3;
730
+ this.offsetBuffer[oi] = px;
731
+ this.offsetBuffer[oi + 1] = py;
732
+ this.offsetBuffer[oi + 2] = pz;
733
+
734
+ const size = particle.size;
735
+ const si = index * 3;
736
+ // Particle size is already in world units when the system is in world
737
+ // space or the particle carries its own parent transform.
738
+ if (systemWorldSpace || particle.parentMatrix) {
739
+ this.sizeBuffer[si] = size.x;
740
+ this.sizeBuffer[si + 1] = size.y;
741
+ this.sizeBuffer[si + 2] = size.z;
742
+ } else {
743
+ this.sizeBuffer[si] = size.x * absScaleX;
744
+ this.sizeBuffer[si + 1] = size.y * absScaleY;
745
+ this.sizeBuffer[si + 2] = size.z * absScaleZ;
746
+ }
597
747
  }
598
- }
599
748
 
600
- if (!copiedColor) {
601
- const color = particle.color;
602
- const ci = index * 4;
603
- this.colorBuffer[ci] = color.x;
604
- this.colorBuffer[ci + 1] = color.y;
605
- this.colorBuffer[ci + 2] = color.z;
606
- this.colorBuffer[ci + 3] = color.w;
607
- }
749
+ if (!copiedColor) {
750
+ const color = particle.color;
751
+ const ci = index * 4;
752
+ this.colorBuffer[ci] = color.x;
753
+ this.colorBuffer[ci + 1] = color.y;
754
+ this.colorBuffer[ci + 2] = color.z;
755
+ this.colorBuffer[ci + 3] = color.w;
756
+ }
608
757
 
609
- this.uvTileBuffer[index] = particle.uvTile;
758
+ this.uvTileBuffer[index] = particle.uvTile;
610
759
 
611
- if (isStretchedRender && this.velocityBuffer) {
612
- // The streak points along the velocity and is as long as it,
613
- // so a velocity frozen between steps makes the sprite's
614
- // outline pop at the step rate while its position glides.
615
- const velocity = particle.velocity;
616
- const previousVelocity = particle.previousVelocity;
617
- const vx = velocity.x + (velocity.x - previousVelocity.x) * stepFraction;
618
- const vy = velocity.y + (velocity.y - previousVelocity.y) * stepFraction;
619
- const vz = velocity.z + (velocity.z - previousVelocity.z) * stepFraction;
620
- let vel: Vector3 = this.vector_;
621
- vel.set(vx, vy, vz);
622
- if (!systemWorldSpace) {
623
- if (particle.parentMatrix) {
624
- this.rotationMat2_.setFromMatrix4(particle.parentMatrix);
625
- vel.applyMatrix3(this.rotationMat2_);
626
- } else {
627
- vel.applyMatrix3(this.rotationMat_);
760
+ if (isStretchedRender && this.velocityBuffer) {
761
+ // The streak points along the velocity and is as long as it,
762
+ // so a velocity frozen between steps makes the sprite's
763
+ // outline pop at the step rate while its position glides.
764
+ const velocity = particle.velocity;
765
+ const previousVelocity = particle.previousVelocity;
766
+ const vx = velocity.x + (velocity.x - previousVelocity.x) * stepFraction;
767
+ const vy = velocity.y + (velocity.y - previousVelocity.y) * stepFraction;
768
+ const vz = velocity.z + (velocity.z - previousVelocity.z) * stepFraction;
769
+ let vel: Vector3 = this.vector_;
770
+ vel.set(vx, vy, vz);
771
+ if (!systemWorldSpace) {
772
+ if (particle.parentMatrix) {
773
+ this.rotationMat2_.setFromMatrix4(particle.parentMatrix);
774
+ vel.applyMatrix3(this.rotationMat2_);
775
+ } else {
776
+ vel.applyMatrix3(this.rotationMat_);
777
+ }
628
778
  }
779
+ const vi = index * 4;
780
+ this.velocityBuffer[vi] = vel.x * speedFactor;
781
+ this.velocityBuffer[vi + 1] = vel.y * speedFactor;
782
+ this.velocityBuffer[vi + 2] = vel.z * speedFactor;
783
+ this.velocityBuffer[vi + 3] = lengthFactor;
629
784
  }
630
- const vi = index * 4;
631
- this.velocityBuffer[vi] = vel.x * speedFactor;
632
- this.velocityBuffer[vi + 1] = vel.y * speedFactor;
633
- this.velocityBuffer[vi + 2] = vel.z * speedFactor;
634
- this.velocityBuffer[vi + 3] = lengthFactor;
635
785
  }
636
786
  }
637
787
 
638
788
  if (flipsSprites && system.flip !== undefined) {
639
- SpriteBatch.applyFlip(this.sizeBuffer, firstIndex * 3, particles, particleNum);
789
+ if (store !== undefined) {
790
+ SpriteBatch.applyFlip(this.sizeBuffer, firstIndex * 3, store.flip, particleNum);
791
+ } else {
792
+ SpriteBatch.applyFlipFromParticles(this.sizeBuffer, firstIndex * 3, particles, particleNum);
793
+ }
640
794
  }
641
795
  }
642
796
 
@@ -709,7 +863,23 @@ export class SpriteBatch extends VFXBatch {
709
863
  * quad being centred — the way Unity's renderer Flip mirrors U and V. The
710
864
  * stretched shader reads the sign and mirrors the texture coordinates.
711
865
  */
712
- private static applyFlip(sizes: Float32Array, base: number, particles: IParticle[], count: number): void {
866
+ private static applyFlip(sizes: Float32Array, base: number, flips: Uint8Array, count: number): void {
867
+ for (let j = 0, o = base; j < count; j++, o += 3) {
868
+ const flip = flips[j];
869
+ if (flip !== 0) {
870
+ if (flip & 1) sizes[o] = -sizes[o];
871
+ if (flip & 2) sizes[o + 1] = -sizes[o + 1];
872
+ }
873
+ }
874
+ }
875
+
876
+ /** The same for a system without a store, read off the particles. */
877
+ private static applyFlipFromParticles(
878
+ sizes: Float32Array,
879
+ base: number,
880
+ particles: IParticle[],
881
+ count: number
882
+ ): void {
713
883
  for (let j = 0, o = base; j < count; j++, o += 3) {
714
884
  const flip = (particles[j] as SpriteParticle).flip;
715
885
  if (flip !== 0) {
@@ -719,6 +889,113 @@ export class SpriteBatch extends VFXBatch {
719
889
  }
720
890
  }
721
891
 
892
+ /**
893
+ * Writes the mesh rotations for rows `[0, count)` of a store: each
894
+ * particle's quaternion, carried on by `fraction` of its last turn (see
895
+ * {@link partialTurn}) and, for a local-space system, turned by the
896
+ * emitter's rotation `parent`.
897
+ */
898
+ private static writeQuaternions(
899
+ target: Float32Array,
900
+ base: number,
901
+ store: ParticleStore,
902
+ count: number,
903
+ fraction: number,
904
+ parent: Quaternion | null
905
+ ): void {
906
+ const rotations = store.rotationQuaternion;
907
+ const turns = store.angularVelocityQuaternion;
908
+ const hasParent = parent !== null;
909
+ const px = hasParent ? parent.x : 0;
910
+ const py = hasParent ? parent.y : 0;
911
+ const pz = hasParent ? parent.z : 0;
912
+ const pw = hasParent ? parent.w : 1;
913
+ for (let i = 0, o = 0, t = base; i < count; i++, o += 4, t += 4) {
914
+ let x = rotations[o];
915
+ let y = rotations[o + 1];
916
+ let z = rotations[o + 2];
917
+ let w = rotations[o + 3];
918
+ if (fraction !== 0) {
919
+ // The fraction of the step's turn, as a normalised lerp from no
920
+ // rotation, then own = own * turn.
921
+ const sx = turns[o] * fraction;
922
+ const sy = turns[o + 1] * fraction;
923
+ const sz = turns[o + 2] * fraction;
924
+ const sw = 1 + (turns[o + 3] - 1) * fraction;
925
+ const inverseLength = 1 / Math.sqrt(sx * sx + sy * sy + sz * sz + sw * sw);
926
+ const tx = sx * inverseLength;
927
+ const ty = sy * inverseLength;
928
+ const tz = sz * inverseLength;
929
+ const tw = sw * inverseLength;
930
+ const nx = x * tw + w * tx + y * tz - z * ty;
931
+ const ny = y * tw + w * ty + z * tx - x * tz;
932
+ const nz = z * tw + w * tz + x * ty - y * tx;
933
+ w = w * tw - x * tx - y * ty - z * tz;
934
+ x = nx;
935
+ y = ny;
936
+ z = nz;
937
+ }
938
+ if (hasParent) {
939
+ // q = parent * own
940
+ const nx = px * w + pw * x + py * z - pz * y;
941
+ const ny = py * w + pw * y + pz * x - px * z;
942
+ const nz = pz * w + pw * z + px * y - py * x;
943
+ w = pw * w - px * x - py * y - pz * z;
944
+ x = nx;
945
+ y = ny;
946
+ z = nz;
947
+ }
948
+ target[t] = x;
949
+ target[t + 1] = y;
950
+ target[t + 2] = z;
951
+ target[t + 3] = w;
952
+ }
953
+ }
954
+
955
+ /**
956
+ * Writes the stretched billboards' velocity attribute for rows `[0, count)`
957
+ * of a store: the velocity carried on by `fraction` of the last step (see
958
+ * {@link extrapolate}), turned into world space by `rotation` when the
959
+ * system simulates locally, scaled by the speed factor, with the length
960
+ * factor in `w`.
961
+ */
962
+ private static writeVelocities(
963
+ target: Float32Array,
964
+ base: number,
965
+ store: ParticleStore,
966
+ count: number,
967
+ fraction: number,
968
+ rotation: Matrix3 | null,
969
+ speedFactor: number,
970
+ lengthFactor: number
971
+ ): void {
972
+ const velocities = store.velocity;
973
+ // Empty until the first stretched step has run; the velocity then has no history yet.
974
+ const previous = store.previousVelocity.length >= count * 3 ? store.previousVelocity : velocities;
975
+ const e = rotation !== null ? rotation.elements : null;
976
+ for (let i = 0, o = 0, t = base; i < count; i++, o += 3, t += 4) {
977
+ let vx = velocities[o];
978
+ let vy = velocities[o + 1];
979
+ let vz = velocities[o + 2];
980
+ if (fraction !== 0) {
981
+ vx += (vx - previous[o]) * fraction;
982
+ vy += (vy - previous[o + 1]) * fraction;
983
+ vz += (vz - previous[o + 2]) * fraction;
984
+ }
985
+ if (e !== null) {
986
+ const x = e[0] * vx + e[3] * vy + e[6] * vz;
987
+ const y = e[1] * vx + e[4] * vy + e[7] * vz;
988
+ vz = e[2] * vx + e[5] * vy + e[8] * vz;
989
+ vx = x;
990
+ vy = y;
991
+ }
992
+ target[t] = vx * speedFactor;
993
+ target[t + 1] = vy * speedFactor;
994
+ target[t + 2] = vz * speedFactor;
995
+ target[t + 3] = lengthFactor;
996
+ }
997
+ }
998
+
722
999
  /**
723
1000
  * The fraction `f` of a turn, as a normalised lerp from no rotation toward
724
1001
  * `step`, written into `out`.