@newkrok/three-particles 2.13.0 → 2.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -535,6 +535,10 @@ var InstancedParticleFragmentShader = `
535
535
  uniform bool discardBackgroundColor;
536
536
  uniform vec3 backgroundColor;
537
537
  uniform float backgroundColorTolerance;
538
+ uniform bool softParticlesEnabled;
539
+ uniform float softParticlesIntensity;
540
+ uniform sampler2D sceneDepthTexture;
541
+ uniform vec2 cameraNearFar;
538
542
 
539
543
  varying vec2 vUv;
540
544
  varying vec4 vColor;
@@ -542,10 +546,16 @@ var InstancedParticleFragmentShader = `
542
546
  varying float vStartLifetime;
543
547
  varying float vStartFrame;
544
548
  varying float vRotation;
549
+ varying float vViewZ;
545
550
 
546
551
  #include <common>
547
552
  #include <logdepthbuf_pars_fragment>
548
553
 
554
+ float linearizeDepth(float depthSample, float near, float far) {
555
+ float z_ndc = 2.0 * depthSample - 1.0;
556
+ return 2.0 * near * far / (far + near - z_ndc * (far - near));
557
+ }
558
+
549
559
  void main()
550
560
  {
551
561
  gl_FragColor = vColor;
@@ -588,6 +598,16 @@ var InstancedParticleFragmentShader = `
588
598
 
589
599
  if (discardBackgroundColor && abs(length(rotatedTexture.rgb - backgroundColor.rgb)) < backgroundColorTolerance) discard;
590
600
 
601
+ if (softParticlesEnabled) {
602
+ vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(sceneDepthTexture, 0));
603
+ float sceneDepthSample = texture2D(sceneDepthTexture, screenUV).r;
604
+ float sceneDepthLinear = linearizeDepth(sceneDepthSample, cameraNearFar.x, cameraNearFar.y);
605
+ float depthDiff = sceneDepthLinear - vViewZ;
606
+ float softFade = smoothstep(0.0, softParticlesIntensity, depthDiff);
607
+ gl_FragColor.a *= softFade;
608
+ if (gl_FragColor.a < 0.001) discard;
609
+ }
610
+
591
611
  #include <logdepthbuf_fragment>
592
612
  }
593
613
  `;
@@ -614,6 +634,7 @@ var InstancedParticleVertexShader = `
614
634
  varying float vStartLifetime;
615
635
  varying float vStartFrame;
616
636
  varying float vRotation;
637
+ varying float vViewZ;
617
638
 
618
639
  #include <common>
619
640
  #include <logdepthbuf_pars_vertex>
@@ -648,6 +669,7 @@ var InstancedParticleVertexShader = `
648
669
  // identical to the Points renderer).
649
670
  mvPosition.xy += position.xy * perspectiveSize;
650
671
 
672
+ vViewZ = -mvPosition.z;
651
673
  gl_Position = projectionMatrix * mvPosition;
652
674
 
653
675
  // Pass UV for texture sampling (quad ranges from -0.5..0.5, map to 0..1).
@@ -669,6 +691,10 @@ var MeshParticleFragmentShader = `
669
691
  uniform bool discardBackgroundColor;
670
692
  uniform vec3 backgroundColor;
671
693
  uniform float backgroundColorTolerance;
694
+ uniform bool softParticlesEnabled;
695
+ uniform float softParticlesIntensity;
696
+ uniform sampler2D sceneDepthTexture;
697
+ uniform vec2 cameraNearFar;
672
698
 
673
699
  varying vec4 vColor;
674
700
  varying float vLifetime;
@@ -677,10 +703,16 @@ var MeshParticleFragmentShader = `
677
703
  varying float vRotation;
678
704
  varying vec3 vNormal;
679
705
  varying vec2 vUv;
706
+ varying float vViewZ;
680
707
 
681
708
  #include <common>
682
709
  #include <logdepthbuf_pars_fragment>
683
710
 
711
+ float linearizeDepth(float depthSample, float near, float far) {
712
+ float z_ndc = 2.0 * depthSample - 1.0;
713
+ return 2.0 * near * far / (far + near - z_ndc * (far - near));
714
+ }
715
+
684
716
  void main()
685
717
  {
686
718
  gl_FragColor = vColor;
@@ -716,6 +748,16 @@ var MeshParticleFragmentShader = `
716
748
  float lightIntensity = 0.5 + 0.5 * max(dot(vNormal, vec3(0.0, 0.0, 1.0)), 0.0);
717
749
  gl_FragColor.rgb *= lightIntensity;
718
750
 
751
+ if (softParticlesEnabled) {
752
+ vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(sceneDepthTexture, 0));
753
+ float sceneDepthSample = texture2D(sceneDepthTexture, screenUV).r;
754
+ float sceneDepthLinear = linearizeDepth(sceneDepthSample, cameraNearFar.x, cameraNearFar.y);
755
+ float depthDiff = sceneDepthLinear - vViewZ;
756
+ float softFade = smoothstep(0.0, softParticlesIntensity, depthDiff);
757
+ gl_FragColor.a *= softFade;
758
+ if (gl_FragColor.a < 0.001) discard;
759
+ }
760
+
719
761
  #include <logdepthbuf_fragment>
720
762
  }
721
763
  `;
@@ -742,6 +784,7 @@ var MeshParticleVertexShader = `
742
784
  varying float vRotation;
743
785
  varying vec3 vNormal;
744
786
  varying vec2 vUv;
787
+ varying float vViewZ;
745
788
 
746
789
  #include <common>
747
790
  #include <logdepthbuf_pars_vertex>
@@ -769,6 +812,7 @@ var MeshParticleVertexShader = `
769
812
  vec3 worldPos = scaledPosition + instanceOffset;
770
813
 
771
814
  vec4 mvPosition = modelViewMatrix * vec4(worldPos, 1.0);
815
+ vViewZ = -mvPosition.z;
772
816
  gl_Position = projectionMatrix * mvPosition;
773
817
 
774
818
  // Transform normal by quaternion for lighting
@@ -792,16 +836,26 @@ var ParticleSystemFragmentShader = `
792
836
  uniform bool discardBackgroundColor;
793
837
  uniform vec3 backgroundColor;
794
838
  uniform float backgroundColorTolerance;
839
+ uniform bool softParticlesEnabled;
840
+ uniform float softParticlesIntensity;
841
+ uniform sampler2D sceneDepthTexture;
842
+ uniform vec2 cameraNearFar;
795
843
 
796
844
  varying vec4 vColor;
797
845
  varying float vLifetime;
798
846
  varying float vStartLifetime;
799
847
  varying float vRotation;
800
848
  varying float vStartFrame;
849
+ varying float vViewZ;
801
850
 
802
851
  #include <common>
803
852
  #include <logdepthbuf_pars_fragment>
804
853
 
854
+ float linearizeDepth(float depthSample, float near, float far) {
855
+ float z_ndc = 2.0 * depthSample - 1.0;
856
+ return 2.0 * near * far / (far + near - z_ndc * (far - near));
857
+ }
858
+
805
859
  void main()
806
860
  {
807
861
  gl_FragColor = vColor;
@@ -849,7 +903,17 @@ var ParticleSystemFragmentShader = `
849
903
  gl_FragColor = gl_FragColor * rotatedTexture;
850
904
 
851
905
  if (discardBackgroundColor && abs(length(rotatedTexture.rgb - backgroundColor.rgb)) < backgroundColorTolerance) discard;
852
-
906
+
907
+ if (softParticlesEnabled) {
908
+ vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(sceneDepthTexture, 0));
909
+ float sceneDepthSample = texture2D(sceneDepthTexture, screenUV).r;
910
+ float sceneDepthLinear = linearizeDepth(sceneDepthSample, cameraNearFar.x, cameraNearFar.y);
911
+ float depthDiff = sceneDepthLinear - vViewZ;
912
+ float softFade = smoothstep(0.0, softParticlesIntensity, depthDiff);
913
+ gl_FragColor.a *= softFade;
914
+ if (gl_FragColor.a < 0.001) discard;
915
+ }
916
+
853
917
  #include <logdepthbuf_fragment>
854
918
  }
855
919
  `;
@@ -873,6 +937,7 @@ var ParticleSystemVertexShader = `
873
937
  varying float vStartLifetime;
874
938
  varying float vRotation;
875
939
  varying float vStartFrame;
940
+ varying float vViewZ;
876
941
 
877
942
  #include <common>
878
943
  #include <logdepthbuf_pars_vertex>
@@ -887,6 +952,7 @@ var ParticleSystemVertexShader = `
887
952
 
888
953
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
889
954
  gl_PointSize = size * (100.0 / length(mvPosition.xyz));
955
+ vViewZ = -mvPosition.z;
890
956
  gl_Position = projectionMatrix * mvPosition;
891
957
 
892
958
  #include <logdepthbuf_vertex>
@@ -901,14 +967,24 @@ var TrailFragmentShader = `
901
967
  uniform bool discardBackgroundColor;
902
968
  uniform vec3 backgroundColor;
903
969
  uniform float backgroundColorTolerance;
970
+ uniform bool softParticlesEnabled;
971
+ uniform float softParticlesIntensity;
972
+ uniform sampler2D sceneDepthTexture;
973
+ uniform vec2 cameraNearFar;
904
974
 
905
975
  varying float vAlpha;
906
976
  varying vec4 vColor;
907
977
  varying vec2 vUv;
978
+ varying float vViewZ;
908
979
 
909
980
  #include <common>
910
981
  #include <logdepthbuf_pars_fragment>
911
982
 
983
+ float linearizeDepth(float depthSample, float near, float far) {
984
+ float z_ndc = 2.0 * depthSample - 1.0;
985
+ return 2.0 * near * far / (far + near - z_ndc * (far - near));
986
+ }
987
+
912
988
  void main()
913
989
  {
914
990
  // Soft edge: always fade near ribbon edges
@@ -929,6 +1005,16 @@ var TrailFragmentShader = `
929
1005
 
930
1006
  if (gl_FragColor.a < 0.001) discard;
931
1007
 
1008
+ if (softParticlesEnabled) {
1009
+ vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(sceneDepthTexture, 0));
1010
+ float sceneDepthSample = texture2D(sceneDepthTexture, screenUV).r;
1011
+ float sceneDepthLinear = linearizeDepth(sceneDepthSample, cameraNearFar.x, cameraNearFar.y);
1012
+ float depthDiff = sceneDepthLinear - vViewZ;
1013
+ float softFade = smoothstep(0.0, softParticlesIntensity, depthDiff);
1014
+ gl_FragColor.a *= softFade;
1015
+ if (gl_FragColor.a < 0.001) discard;
1016
+ }
1017
+
932
1018
  if (discardBackgroundColor && abs(length(gl_FragColor.rgb - backgroundColor.rgb)) < backgroundColorTolerance) discard;
933
1019
 
934
1020
  #include <logdepthbuf_fragment>
@@ -948,6 +1034,7 @@ var TrailVertexShader = `
948
1034
  varying float vAlpha;
949
1035
  varying vec4 vColor;
950
1036
  varying vec2 vUv;
1037
+ varying float vViewZ;
951
1038
 
952
1039
  #include <common>
953
1040
  #include <logdepthbuf_pars_vertex>
@@ -974,28 +1061,27 @@ var TrailVertexShader = `
974
1061
  float perpLen = length(perp);
975
1062
 
976
1063
  // When tangent is nearly parallel to view direction, the cross product
977
- // collapses and the ribbon becomes edge-on (invisible). Use a secondary
978
- // perpendicular from cross(tangent, up) and blend it in to guarantee
979
- // a minimum visible width.
980
- vec3 upPerp = cross(tangent, vec3(0.0, 1.0, 0.0));
981
- float upPerpLen = length(upPerp);
982
- if (upPerpLen < 0.0001) {
983
- upPerp = cross(tangent, vec3(1.0, 0.0, 0.0));
984
- upPerpLen = length(upPerp);
985
- }
986
- upPerp = upPerp / max(upPerpLen, 0.0001);
1064
+ // collapses and the ribbon becomes edge-on (invisible). Build a stable
1065
+ // fallback perpendicular from the camera's right axis \u2014 this keeps the
1066
+ // ribbon in screen-space and prevents it from flipping into an arbitrary
1067
+ // plane when viewed edge-on.
1068
+ vec3 camRight = vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
1069
+ vec3 fallbackPerp = normalize(camRight - tangent * dot(camRight, tangent));
987
1070
 
988
1071
  if (perpLen < 0.0001) {
989
- perp = upPerp;
1072
+ perp = fallbackPerp;
990
1073
  } else {
991
1074
  perp = perp / perpLen;
992
- // Blend in the secondary perp when billboard perp gets weak
993
- float blendFactor = smoothstep(0.0, 0.5, perpLen);
994
- perp = normalize(mix(upPerp, perp, blendFactor));
1075
+ // Smoothly blend toward the fallback when the billboard perp weakens.
1076
+ // The wide range (0..0.7) ensures a gradual transition so the ribbon
1077
+ // does not snap abruptly when rotating toward edge-on.
1078
+ float blendFactor = smoothstep(0.0, 0.7, perpLen);
1079
+ perp = normalize(mix(fallbackPerp, perp, blendFactor));
995
1080
  }
996
1081
 
997
1082
  vec3 offsetPos = position + perp * trailOffset * trailHalfWidth;
998
1083
  vec4 mvPosition = modelViewMatrix * vec4(offsetPos, 1.0);
1084
+ vViewZ = -mvPosition.z;
999
1085
  gl_Position = projectionMatrix * mvPosition;
1000
1086
 
1001
1087
  #include <logdepthbuf_vertex>
@@ -1168,7 +1254,11 @@ var DEFAULT_PARTICLE_SYSTEM_CONFIG = {
1168
1254
  backgroundColor: { r: 1, g: 1, b: 1 },
1169
1255
  transparent: true,
1170
1256
  depthTest: true,
1171
- depthWrite: false
1257
+ depthWrite: false,
1258
+ softParticles: {
1259
+ enabled: false,
1260
+ intensity: 1
1261
+ }
1172
1262
  },
1173
1263
  velocityOverLifetime: {
1174
1264
  isActive: false,
@@ -1598,7 +1688,13 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
1598
1688
  renderer.trail?.opacityOverTrail,
1599
1689
  defaultTrailCurve
1600
1690
  ),
1601
- colorOverTrail: renderer.trail?.colorOverTrail
1691
+ colorOverTrail: renderer.trail?.colorOverTrail,
1692
+ minVertexDistance: renderer.trail?.minVertexDistance ?? 0,
1693
+ maxTime: renderer.trail?.maxTime ?? 0,
1694
+ smoothing: renderer.trail?.smoothing ?? false,
1695
+ smoothingSubdivisions: renderer.trail?.smoothingSubdivisions ?? 3,
1696
+ twistPrevention: renderer.trail?.twistPrevention ?? false,
1697
+ ribbonId: renderer.trail?.ribbonId
1602
1698
  } : void 0;
1603
1699
  if (useTrail && trailConfig) {
1604
1700
  const trailLength = trailConfig.length;
@@ -1608,9 +1704,21 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
1608
1704
  );
1609
1705
  generalData.positionHistoryIndex = new Uint16Array(maxParticles);
1610
1706
  generalData.positionHistoryCount = new Uint16Array(maxParticles);
1707
+ if (trailConfig.minVertexDistance > 0) {
1708
+ generalData.trailLastSampledPosition = new Float32Array(maxParticles * 3);
1709
+ }
1710
+ if (trailConfig.maxTime > 0) {
1711
+ generalData.trailSampleTimes = new Float64Array(
1712
+ maxParticles * trailLength
1713
+ );
1714
+ }
1715
+ if (trailConfig.twistPrevention) {
1716
+ generalData.trailPrevNormal = new Float32Array(maxParticles * 3);
1717
+ }
1611
1718
  }
1612
1719
  const attr = (name) => useInstancedAttributes ? `instance${name.charAt(0).toUpperCase()}${name.slice(1)}` : name;
1613
1720
  const posAttr = useInstancedAttributes ? "instanceOffset" : "position";
1721
+ const softParticlesEnabled = !!(renderer.softParticles?.enabled && renderer.softParticles?.depthTexture);
1614
1722
  const sharedUniforms = {
1615
1723
  elapsed: { value: 0 },
1616
1724
  map: { value: particleMap },
@@ -1622,7 +1730,15 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
1622
1730
  backgroundColor: { value: renderer.backgroundColor },
1623
1731
  discardBackgroundColor: { value: renderer.discardBackgroundColor },
1624
1732
  backgroundColorTolerance: { value: renderer.backgroundColorTolerance },
1625
- ...useInstancing ? { viewportHeight: { value: 1 } } : {}
1733
+ ...useInstancing ? { viewportHeight: { value: 1 } } : {},
1734
+ softParticlesEnabled: { value: softParticlesEnabled },
1735
+ softParticlesIntensity: {
1736
+ value: Math.max(renderer.softParticles?.intensity ?? 1, 1e-3)
1737
+ },
1738
+ sceneDepthTexture: {
1739
+ value: renderer.softParticles?.depthTexture ?? null
1740
+ },
1741
+ cameraNearFar: { value: new THREE4.Vector2(0.1, 1e3) }
1626
1742
  };
1627
1743
  const getVertexShader = () => {
1628
1744
  if (useMesh) return mesh_particle_vertex_shader_glsl_default;
@@ -1820,6 +1936,18 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
1820
1936
  if (generalData.positionHistoryCount) {
1821
1937
  generalData.positionHistoryCount[particleIndex] = 0;
1822
1938
  generalData.positionHistoryIndex[particleIndex] = 0;
1939
+ if (generalData.trailLastSampledPosition) {
1940
+ const lsIdx = particleIndex * 3;
1941
+ generalData.trailLastSampledPosition[lsIdx] = 0;
1942
+ generalData.trailLastSampledPosition[lsIdx + 1] = 0;
1943
+ generalData.trailLastSampledPosition[lsIdx + 2] = 0;
1944
+ }
1945
+ if (generalData.trailPrevNormal) {
1946
+ const nIdx = particleIndex * 3;
1947
+ generalData.trailPrevNormal[nIdx] = 0;
1948
+ generalData.trailPrevNormal[nIdx + 1] = 0;
1949
+ generalData.trailPrevNormal[nIdx + 2] = 0;
1950
+ }
1823
1951
  }
1824
1952
  if (generalData.noise.offsets)
1825
1953
  generalData.noise.offsets[particleIndex] = Math.random() * 100;
@@ -2085,7 +2213,15 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
2085
2213
  useMap: { value: !!particleMap },
2086
2214
  discardBackgroundColor: { value: renderer.discardBackgroundColor },
2087
2215
  backgroundColor: { value: renderer.backgroundColor },
2088
- backgroundColorTolerance: { value: renderer.backgroundColorTolerance }
2216
+ backgroundColorTolerance: { value: renderer.backgroundColorTolerance },
2217
+ softParticlesEnabled: { value: softParticlesEnabled },
2218
+ softParticlesIntensity: {
2219
+ value: Math.max(renderer.softParticles?.intensity ?? 1, 1e-3)
2220
+ },
2221
+ sceneDepthTexture: {
2222
+ value: renderer.softParticles?.depthTexture ?? null
2223
+ },
2224
+ cameraNearFar: { value: new THREE4.Vector2(0.1, 1e3) }
2089
2225
  },
2090
2226
  vertexShader: trail_vertex_shader_glsl_default,
2091
2227
  fragmentShader: trail_fragment_shader_glsl_default,
@@ -2100,6 +2236,14 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
2100
2236
  const trailCameraPos = new THREE4.Vector3();
2101
2237
  trailMesh.onBeforeRender = (_renderer, _scene, camera) => {
2102
2238
  camera.getWorldPosition(trailCameraPos);
2239
+ if (softParticlesEnabled && camera.isPerspectiveCamera) {
2240
+ const perspCam = camera;
2241
+ const trailUniforms = trailMaterial.uniforms;
2242
+ trailUniforms.cameraNearFar.value.set(
2243
+ perspCam.near,
2244
+ perspCam.far
2245
+ );
2246
+ }
2103
2247
  };
2104
2248
  generalData.trailCameraPosition = trailCameraPos;
2105
2249
  trailWidthCurveFn = getCurveFunctionFromConfig(
@@ -2128,10 +2272,19 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
2128
2272
  }
2129
2273
  }
2130
2274
  let particleSystem = useInstancing || useMesh ? new THREE4.Mesh(geometry, material) : new THREE4.Points(geometry, material);
2131
- if (useInstancing) {
2132
- particleSystem.onBeforeRender = (glRenderer) => {
2133
- const size = glRenderer.getSize(new THREE4.Vector2());
2134
- sharedUniforms.viewportHeight.value = size.y * glRenderer.getPixelRatio();
2275
+ if (useInstancing || softParticlesEnabled) {
2276
+ particleSystem.onBeforeRender = (glRenderer, _scene, camera) => {
2277
+ if (useInstancing) {
2278
+ const size = glRenderer.getSize(new THREE4.Vector2());
2279
+ sharedUniforms.viewportHeight.value = size.y * glRenderer.getPixelRatio();
2280
+ }
2281
+ if (softParticlesEnabled && camera.isPerspectiveCamera) {
2282
+ const perspCam = camera;
2283
+ sharedUniforms.cameraNearFar.value.set(
2284
+ perspCam.near,
2285
+ perspCam.far
2286
+ );
2287
+ }
2135
2288
  };
2136
2289
  }
2137
2290
  if (useTrail && trailMesh) {
@@ -2227,7 +2380,13 @@ var createParticleSystem = (config = DEFAULT_PARTICLE_SYSTEM_CONFIG, externalNow
2227
2380
  trailColorOverTrailFns,
2228
2381
  trailConfig: {
2229
2382
  length: trailConfig.length,
2230
- width: trailConfig.width
2383
+ width: trailConfig.width,
2384
+ minVertexDistance: trailConfig.minVertexDistance,
2385
+ maxTime: trailConfig.maxTime,
2386
+ smoothing: trailConfig.smoothing,
2387
+ smoothingSubdivisions: trailConfig.smoothingSubdivisions,
2388
+ twistPrevention: trailConfig.twistPrevention,
2389
+ ribbonId: trailConfig.ribbonId
2231
2390
  }
2232
2391
  } : {}
2233
2392
  };
@@ -2489,10 +2648,84 @@ var updateParticleSystemInstance = (props, { now, delta, elapsed }) => {
2489
2648
  particleSystem
2490
2649
  });
2491
2650
  if (props.trailMesh) {
2492
- updateTrailGeometry(props);
2651
+ updateTrailGeometry(props, now);
2493
2652
  }
2494
2653
  };
2495
- var updateTrailGeometry = (props) => {
2654
+ var catmullRom = (out, outIdx, p0x, p0y, p0z, p1x, p1y, p1z, p2x, p2y, p2z, p3x, p3y, p3z, t) => {
2655
+ const t2 = t * t;
2656
+ const t3 = t2 * t;
2657
+ out[outIdx] = 0.5 * (2 * p1x + (-p0x + p2x) * t + (2 * p0x - 5 * p1x + 4 * p2x - p3x) * t2 + (-p0x + 3 * p1x - 3 * p2x + p3x) * t3);
2658
+ out[outIdx + 1] = 0.5 * (2 * p1y + (-p0y + p2y) * t + (2 * p0y - 5 * p1y + 4 * p2y - p3y) * t2 + (-p0y + 3 * p1y - 3 * p2y + p3y) * t3);
2659
+ out[outIdx + 2] = 0.5 * (2 * p1z + (-p0z + p2z) * t + (2 * p0z - 5 * p1z + 4 * p2z - p3z) * t2 + (-p0z + 3 * p1z - 3 * p2z + p3z) * t3);
2660
+ };
2661
+ var clearTrailVertex = (vIdx, cIdx, aIdx, uvIdx, trailPosArr, trailNextArr, trailHalfWidthArr, trailUVArr, trailAlphaArr, trailColorArr, fallbackX, fallbackY, fallbackZ) => {
2662
+ trailPosArr[vIdx] = fallbackX;
2663
+ trailPosArr[vIdx + 1] = fallbackY;
2664
+ trailPosArr[vIdx + 2] = fallbackZ;
2665
+ trailPosArr[vIdx + 3] = fallbackX;
2666
+ trailPosArr[vIdx + 4] = fallbackY;
2667
+ trailPosArr[vIdx + 5] = fallbackZ;
2668
+ trailNextArr[vIdx] = fallbackX;
2669
+ trailNextArr[vIdx + 1] = fallbackY;
2670
+ trailNextArr[vIdx + 2] = fallbackZ;
2671
+ trailNextArr[vIdx + 3] = fallbackX;
2672
+ trailNextArr[vIdx + 4] = fallbackY;
2673
+ trailNextArr[vIdx + 5] = fallbackZ;
2674
+ trailHalfWidthArr[aIdx] = 0;
2675
+ trailHalfWidthArr[aIdx + 1] = 0;
2676
+ trailUVArr[uvIdx] = 0;
2677
+ trailUVArr[uvIdx + 1] = 0;
2678
+ trailUVArr[uvIdx + 2] = 0;
2679
+ trailUVArr[uvIdx + 3] = 0;
2680
+ trailAlphaArr[aIdx] = 0;
2681
+ trailAlphaArr[aIdx + 1] = 0;
2682
+ trailColorArr[cIdx] = 0;
2683
+ trailColorArr[cIdx + 1] = 0;
2684
+ trailColorArr[cIdx + 2] = 0;
2685
+ trailColorArr[cIdx + 3] = 0;
2686
+ trailColorArr[cIdx + 4] = 0;
2687
+ trailColorArr[cIdx + 5] = 0;
2688
+ trailColorArr[cIdx + 6] = 0;
2689
+ trailColorArr[cIdx + 7] = 0;
2690
+ };
2691
+ var writeTrailVertex = (vIdx, cIdx, aIdx, uvIdx, hx, hy, hz, nx, ny, nz, halfWidth, t, alpha, fr, fg, fb, ca, trailPosArr, trailNextArr, trailHalfWidthArr, trailUVArr, trailAlphaArr, trailColorArr) => {
2692
+ trailPosArr[vIdx] = hx;
2693
+ trailPosArr[vIdx + 1] = hy;
2694
+ trailPosArr[vIdx + 2] = hz;
2695
+ trailPosArr[vIdx + 3] = hx;
2696
+ trailPosArr[vIdx + 4] = hy;
2697
+ trailPosArr[vIdx + 5] = hz;
2698
+ trailNextArr[vIdx] = nx;
2699
+ trailNextArr[vIdx + 1] = ny;
2700
+ trailNextArr[vIdx + 2] = nz;
2701
+ trailNextArr[vIdx + 3] = nx;
2702
+ trailNextArr[vIdx + 4] = ny;
2703
+ trailNextArr[vIdx + 5] = nz;
2704
+ trailHalfWidthArr[aIdx] = halfWidth;
2705
+ trailHalfWidthArr[aIdx + 1] = halfWidth;
2706
+ trailUVArr[uvIdx] = 0;
2707
+ trailUVArr[uvIdx + 1] = t;
2708
+ trailUVArr[uvIdx + 2] = 1;
2709
+ trailUVArr[uvIdx + 3] = t;
2710
+ trailAlphaArr[aIdx] = alpha;
2711
+ trailAlphaArr[aIdx + 1] = alpha;
2712
+ trailColorArr[cIdx] = fr;
2713
+ trailColorArr[cIdx + 1] = fg;
2714
+ trailColorArr[cIdx + 2] = fb;
2715
+ trailColorArr[cIdx + 3] = ca;
2716
+ trailColorArr[cIdx + 4] = fr;
2717
+ trailColorArr[cIdx + 5] = fg;
2718
+ trailColorArr[cIdx + 6] = fb;
2719
+ trailColorArr[cIdx + 7] = ca;
2720
+ };
2721
+ var _rawPoints = null;
2722
+ var _rawPointsSize = 0;
2723
+ var _smoothedPoints = null;
2724
+ var _smoothedPointsSize = 0;
2725
+ var _ribbonIndices = null;
2726
+ var _ribbonIndicesSize = 0;
2727
+ var _ribbonCount = 0;
2728
+ var updateTrailGeometry = (props, now) => {
2496
2729
  const {
2497
2730
  generalData,
2498
2731
  trailPositionAttr,
@@ -2510,13 +2743,23 @@ var updateTrailGeometry = (props) => {
2510
2743
  const positionHistory = generalData.positionHistory;
2511
2744
  const historyIndex = generalData.positionHistoryIndex;
2512
2745
  const historyCount = generalData.positionHistoryCount;
2746
+ const sampleTimes = generalData.trailSampleTimes;
2747
+ const lastSampledPos = generalData.trailLastSampledPosition;
2748
+ const prevNormal = generalData.trailPrevNormal;
2749
+ const minVertexDist = trailConfig.minVertexDistance;
2750
+ const minVertexDistSq = minVertexDist * minVertexDist;
2751
+ const maxTime = trailConfig.maxTime;
2752
+ const maxTimeMs = maxTime * 1e3;
2753
+ const useSmoothing = trailConfig.smoothing;
2754
+ const subdivisions = trailConfig.smoothingSubdivisions;
2755
+ const useTwistPrevention = trailConfig.twistPrevention;
2756
+ const ribbonId = trailConfig.ribbonId;
2513
2757
  const isActiveArr = ma.isActive.array;
2514
2758
  const positionArr = ma.position.array;
2515
2759
  const colorRArr = ma.colorR.array;
2516
2760
  const colorGArr = ma.colorG.array;
2517
2761
  const colorBArr = ma.colorB.array;
2518
2762
  const colorAArr = ma.colorA.array;
2519
- ma.size.array;
2520
2763
  const trailPosArr = trailPositionAttr.array;
2521
2764
  const trailAlphaArr = trailAlphaAttr.array;
2522
2765
  const trailColorArr = trailColorAttr.array;
@@ -2527,115 +2770,329 @@ var updateTrailGeometry = (props) => {
2527
2770
  const verticesPerParticle = trailLength * 2;
2528
2771
  const creationTimesLength = generalData.creationTimes.length;
2529
2772
  let hasUpdates = false;
2773
+ const useRibbon = ribbonId !== void 0;
2774
+ let ribbonLeader = -1;
2775
+ if (useRibbon) {
2776
+ if (!_ribbonIndices || _ribbonIndicesSize < creationTimesLength) {
2777
+ _ribbonIndices = new Uint16Array(creationTimesLength);
2778
+ _ribbonIndicesSize = creationTimesLength;
2779
+ }
2780
+ _ribbonCount = 0;
2781
+ for (let i = 0; i < creationTimesLength; i++) {
2782
+ if (isActiveArr[i]) _ribbonIndices[_ribbonCount++] = i;
2783
+ }
2784
+ for (let i = 1; i < _ribbonCount; i++) {
2785
+ const key = _ribbonIndices[i];
2786
+ const keyTime = generalData.creationTimes[key];
2787
+ let j = i - 1;
2788
+ while (j >= 0 && generalData.creationTimes[_ribbonIndices[j]] > keyTime) {
2789
+ _ribbonIndices[j + 1] = _ribbonIndices[j];
2790
+ j--;
2791
+ }
2792
+ _ribbonIndices[j + 1] = key;
2793
+ }
2794
+ if (_ribbonCount > 0) ribbonLeader = _ribbonIndices[0];
2795
+ }
2530
2796
  for (let index = 0; index < creationTimesLength; index++) {
2531
2797
  const vertBase = index * verticesPerParticle;
2532
2798
  if (isActiveArr[index]) {
2799
+ if (useRibbon && _ribbonCount >= 2 && index !== ribbonLeader) {
2800
+ const posIdx2 = index * 3;
2801
+ const px2 = positionArr[posIdx2];
2802
+ const py2 = positionArr[posIdx2 + 1];
2803
+ const pz2 = positionArr[posIdx2 + 2];
2804
+ const histBase = (index * trailLength + historyIndex[index]) * 3;
2805
+ positionHistory[histBase] = px2;
2806
+ positionHistory[histBase + 1] = py2;
2807
+ positionHistory[histBase + 2] = pz2;
2808
+ if (sampleTimes) {
2809
+ sampleTimes[index * trailLength + historyIndex[index]] = now;
2810
+ }
2811
+ historyIndex[index] = (historyIndex[index] + 1) % trailLength;
2812
+ if (historyCount[index] < trailLength) historyCount[index]++;
2813
+ continue;
2814
+ }
2533
2815
  hasUpdates = true;
2534
2816
  const posIdx = index * 3;
2535
2817
  const px = positionArr[posIdx];
2536
2818
  const py = positionArr[posIdx + 1];
2537
2819
  const pz = positionArr[posIdx + 2];
2538
- const histBase = (index * trailLength + historyIndex[index]) * 3;
2539
- positionHistory[histBase] = px;
2540
- positionHistory[histBase + 1] = py;
2541
- positionHistory[histBase + 2] = pz;
2542
- historyIndex[index] = (historyIndex[index] + 1) % trailLength;
2543
- if (historyCount[index] < trailLength) historyCount[index]++;
2544
- const count = historyCount[index];
2820
+ let shouldSample = true;
2821
+ if (minVertexDist > 0 && lastSampledPos && historyCount[index] > 0) {
2822
+ const lsIdx = index * 3;
2823
+ const dx = px - lastSampledPos[lsIdx];
2824
+ const dy = py - lastSampledPos[lsIdx + 1];
2825
+ const dz = pz - lastSampledPos[lsIdx + 2];
2826
+ if (dx * dx + dy * dy + dz * dz < minVertexDistSq) {
2827
+ shouldSample = false;
2828
+ }
2829
+ }
2830
+ if (shouldSample) {
2831
+ const histBase = (index * trailLength + historyIndex[index]) * 3;
2832
+ positionHistory[histBase] = px;
2833
+ positionHistory[histBase + 1] = py;
2834
+ positionHistory[histBase + 2] = pz;
2835
+ if (sampleTimes) {
2836
+ sampleTimes[index * trailLength + historyIndex[index]] = now;
2837
+ }
2838
+ historyIndex[index] = (historyIndex[index] + 1) % trailLength;
2839
+ if (historyCount[index] < trailLength) historyCount[index]++;
2840
+ if (lastSampledPos) {
2841
+ const lsIdx = index * 3;
2842
+ lastSampledPos[lsIdx] = px;
2843
+ lastSampledPos[lsIdx + 1] = py;
2844
+ lastSampledPos[lsIdx + 2] = pz;
2845
+ }
2846
+ }
2847
+ let rawCount = historyCount[index];
2848
+ let effectiveCount = rawCount;
2849
+ if (maxTime > 0 && sampleTimes && rawCount > 0) {
2850
+ const sampleBase = index * trailLength;
2851
+ effectiveCount = 0;
2852
+ for (let s = 0; s < rawCount; s++) {
2853
+ const sampleSlot = (historyIndex[index] - 1 - s + trailLength * 2) % trailLength;
2854
+ const age = now - sampleTimes[sampleBase + sampleSlot];
2855
+ if (age <= maxTimeMs) {
2856
+ effectiveCount++;
2857
+ } else {
2858
+ break;
2859
+ }
2860
+ }
2861
+ }
2862
+ const count = effectiveCount;
2545
2863
  const ribbonWidth = trailConfig.width;
2546
2864
  const cr = colorRArr[index];
2547
2865
  const cg = colorGArr[index];
2548
2866
  const cb = colorBArr[index];
2549
2867
  const ca = colorAArr[index];
2550
2868
  const ringOff = index * trailLength * 3;
2869
+ const rawPtsSize = count * 3;
2870
+ if (!_rawPoints || _rawPointsSize < rawPtsSize) {
2871
+ _rawPoints = new Float32Array(rawPtsSize);
2872
+ _rawPointsSize = rawPtsSize;
2873
+ }
2874
+ const rawPts = _rawPoints;
2875
+ for (let s = 0; s < count; s++) {
2876
+ const histSlot = (historyIndex[index] - 1 - s + trailLength * 2) % trailLength * 3 + ringOff;
2877
+ rawPts[s * 3] = positionHistory[histSlot];
2878
+ rawPts[s * 3 + 1] = positionHistory[histSlot + 1];
2879
+ rawPts[s * 3 + 2] = positionHistory[histSlot + 2];
2880
+ }
2881
+ let finalPts;
2882
+ let finalCount;
2883
+ if (useSmoothing && count >= 3) {
2884
+ const segmentCount = count - 1;
2885
+ finalCount = segmentCount * subdivisions + 1;
2886
+ const neededSize = finalCount * 3;
2887
+ if (!_smoothedPoints || _smoothedPointsSize < neededSize) {
2888
+ _smoothedPoints = new Float32Array(neededSize);
2889
+ _smoothedPointsSize = neededSize;
2890
+ }
2891
+ finalPts = _smoothedPoints;
2892
+ for (let seg = 0; seg < segmentCount; seg++) {
2893
+ const i0 = Math.max(0, seg - 1);
2894
+ const i1 = seg;
2895
+ const i2 = Math.min(count - 1, seg + 1);
2896
+ const i3 = Math.min(count - 1, seg + 2);
2897
+ const p0x = rawPts[i0 * 3], p0y = rawPts[i0 * 3 + 1], p0z = rawPts[i0 * 3 + 2];
2898
+ const p1x = rawPts[i1 * 3], p1y = rawPts[i1 * 3 + 1], p1z = rawPts[i1 * 3 + 2];
2899
+ const p2x = rawPts[i2 * 3], p2y = rawPts[i2 * 3 + 1], p2z = rawPts[i2 * 3 + 2];
2900
+ const p3x = rawPts[i3 * 3], p3y = rawPts[i3 * 3 + 1], p3z = rawPts[i3 * 3 + 2];
2901
+ for (let sub = 0; sub < subdivisions; sub++) {
2902
+ const t = sub / subdivisions;
2903
+ const outIdx = (seg * subdivisions + sub) * 3;
2904
+ catmullRom(
2905
+ finalPts,
2906
+ outIdx,
2907
+ p0x,
2908
+ p0y,
2909
+ p0z,
2910
+ p1x,
2911
+ p1y,
2912
+ p1z,
2913
+ p2x,
2914
+ p2y,
2915
+ p2z,
2916
+ p3x,
2917
+ p3y,
2918
+ p3z,
2919
+ t
2920
+ );
2921
+ }
2922
+ }
2923
+ const lastOutIdx = (finalCount - 1) * 3;
2924
+ finalPts[lastOutIdx] = rawPts[(count - 1) * 3];
2925
+ finalPts[lastOutIdx + 1] = rawPts[(count - 1) * 3 + 1];
2926
+ finalPts[lastOutIdx + 2] = rawPts[(count - 1) * 3 + 2];
2927
+ } else {
2928
+ finalPts = rawPts;
2929
+ finalCount = count;
2930
+ }
2931
+ if (finalCount > trailLength) finalCount = trailLength;
2932
+ if (useSmoothing && finalCount >= 2) {
2933
+ const MIN_SEG_DIST_SQ = 1e-4 * 1e-4;
2934
+ for (let d = 1; d < finalCount; d++) {
2935
+ const pi = (d - 1) * 3;
2936
+ const ci = d * 3;
2937
+ const dx = finalPts[ci] - finalPts[pi];
2938
+ const dy = finalPts[ci + 1] - finalPts[pi + 1];
2939
+ const dz = finalPts[ci + 2] - finalPts[pi + 2];
2940
+ if (dx * dx + dy * dy + dz * dz < MIN_SEG_DIST_SQ) {
2941
+ finalPts[ci] = finalPts[pi];
2942
+ finalPts[ci + 1] = finalPts[pi + 1];
2943
+ finalPts[ci + 2] = finalPts[pi + 2];
2944
+ }
2945
+ }
2946
+ }
2551
2947
  for (let s = 0; s < trailLength; s++) {
2552
2948
  const vIdx = (vertBase + s * 2) * 3;
2553
2949
  const cIdx = (vertBase + s * 2) * 4;
2554
2950
  const aIdx = vertBase + s * 2;
2555
- if (s >= count) {
2556
- trailPosArr[vIdx] = px;
2557
- trailPosArr[vIdx + 1] = py;
2558
- trailPosArr[vIdx + 2] = pz;
2559
- trailPosArr[vIdx + 3] = px;
2560
- trailPosArr[vIdx + 4] = py;
2561
- trailPosArr[vIdx + 5] = pz;
2562
- trailNextArr[vIdx] = px;
2563
- trailNextArr[vIdx + 1] = py;
2564
- trailNextArr[vIdx + 2] = pz;
2565
- trailNextArr[vIdx + 3] = px;
2566
- trailNextArr[vIdx + 4] = py;
2567
- trailNextArr[vIdx + 5] = pz;
2568
- trailHalfWidthArr[aIdx] = 0;
2569
- trailHalfWidthArr[aIdx + 1] = 0;
2570
- const uvIdx2 = (vertBase + s * 2) * 2;
2571
- trailUVArr[uvIdx2] = 0;
2572
- trailUVArr[uvIdx2 + 1] = 0;
2573
- trailUVArr[uvIdx2 + 2] = 0;
2574
- trailUVArr[uvIdx2 + 3] = 0;
2575
- trailAlphaArr[aIdx] = 0;
2576
- trailAlphaArr[aIdx + 1] = 0;
2577
- trailColorArr[cIdx] = 0;
2578
- trailColorArr[cIdx + 1] = 0;
2579
- trailColorArr[cIdx + 2] = 0;
2580
- trailColorArr[cIdx + 3] = 0;
2581
- trailColorArr[cIdx + 4] = 0;
2582
- trailColorArr[cIdx + 5] = 0;
2583
- trailColorArr[cIdx + 6] = 0;
2584
- trailColorArr[cIdx + 7] = 0;
2951
+ const uvIdxBase = (vertBase + s * 2) * 2;
2952
+ if (s >= finalCount) {
2953
+ clearTrailVertex(
2954
+ vIdx,
2955
+ cIdx,
2956
+ aIdx,
2957
+ uvIdxBase,
2958
+ trailPosArr,
2959
+ trailNextArr,
2960
+ trailHalfWidthArr,
2961
+ trailUVArr,
2962
+ trailAlphaArr,
2963
+ trailColorArr,
2964
+ px,
2965
+ py,
2966
+ pz
2967
+ );
2585
2968
  continue;
2586
2969
  }
2587
- const histIdx = (historyIndex[index] - 1 - s + trailLength * 2) % trailLength * 3 + ringOff;
2588
- const hx = positionHistory[histIdx];
2589
- const hy = positionHistory[histIdx + 1];
2590
- const hz = positionHistory[histIdx + 2];
2970
+ const hx = finalPts[s * 3];
2971
+ const hy = finalPts[s * 3 + 1];
2972
+ const hz = finalPts[s * 3 + 2];
2591
2973
  let nx, ny, nz;
2592
- if (s < count - 1) {
2593
- const ni = (historyIndex[index] - 2 - s + trailLength * 2) % trailLength * 3 + ringOff;
2594
- nx = positionHistory[ni];
2595
- ny = positionHistory[ni + 1];
2596
- nz = positionHistory[ni + 2];
2974
+ if (s > 0 && s < finalCount - 1) {
2975
+ const px2 = finalPts[(s - 1) * 3];
2976
+ const py2 = finalPts[(s - 1) * 3 + 1];
2977
+ const pz2 = finalPts[(s - 1) * 3 + 2];
2978
+ const nx2 = finalPts[(s + 1) * 3];
2979
+ const ny2 = finalPts[(s + 1) * 3 + 1];
2980
+ const nz2 = finalPts[(s + 1) * 3 + 2];
2981
+ const atx = nx2 - px2;
2982
+ const aty = ny2 - py2;
2983
+ const atz = nz2 - pz2;
2984
+ const atLen = Math.sqrt(atx * atx + aty * aty + atz * atz);
2985
+ if (atLen > 1e-4) {
2986
+ nx = hx + atx / atLen;
2987
+ ny = hy + aty / atLen;
2988
+ nz = hz + atz / atLen;
2989
+ } else {
2990
+ nx = finalPts[(s + 1) * 3];
2991
+ ny = finalPts[(s + 1) * 3 + 1];
2992
+ nz = finalPts[(s + 1) * 3 + 2];
2993
+ }
2994
+ } else if (s < finalCount - 1) {
2995
+ nx = finalPts[(s + 1) * 3];
2996
+ ny = finalPts[(s + 1) * 3 + 1];
2997
+ nz = finalPts[(s + 1) * 3 + 2];
2597
2998
  } else {
2598
2999
  nx = hx;
2599
3000
  ny = hy + 1e-3;
2600
3001
  nz = hz;
2601
3002
  }
2602
- const t = count > 1 ? s / (count - 1) : 0;
3003
+ const t = finalCount > 1 ? s / (finalCount - 1) : 0;
3004
+ let timeFade = 1;
3005
+ if (maxTime > 0 && sampleTimes && effectiveCount > 0) {
3006
+ const rawS = useSmoothing ? Math.min(
3007
+ Math.floor(s / Math.max(finalCount - 1, 1) * (rawCount - 1)),
3008
+ rawCount - 1
3009
+ ) : Math.min(s, rawCount - 1);
3010
+ const sampleSlot = (historyIndex[index] - 1 - rawS + trailLength * 2) % trailLength;
3011
+ const sampleBase = index * trailLength;
3012
+ const age = now - sampleTimes[sampleBase + sampleSlot];
3013
+ timeFade = 1 - Math.min(age / maxTimeMs, 1);
3014
+ }
2603
3015
  const widthScale = trailWidthCurveFn(t);
2604
3016
  const opacityScale = trailOpacityCurveFn(t);
2605
3017
  const halfWidth = ribbonWidth * widthScale * 0.5;
2606
- trailPosArr[vIdx] = hx;
2607
- trailPosArr[vIdx + 1] = hy;
2608
- trailPosArr[vIdx + 2] = hz;
2609
- trailPosArr[vIdx + 3] = hx;
2610
- trailPosArr[vIdx + 4] = hy;
2611
- trailPosArr[vIdx + 5] = hz;
2612
- trailNextArr[vIdx] = nx;
2613
- trailNextArr[vIdx + 1] = ny;
2614
- trailNextArr[vIdx + 2] = nz;
2615
- trailNextArr[vIdx + 3] = nx;
2616
- trailNextArr[vIdx + 4] = ny;
2617
- trailNextArr[vIdx + 5] = nz;
2618
- trailHalfWidthArr[aIdx] = halfWidth;
2619
- trailHalfWidthArr[aIdx + 1] = halfWidth;
2620
- const uvIdx = (vertBase + s * 2) * 2;
2621
- trailUVArr[uvIdx] = 0;
2622
- trailUVArr[uvIdx + 1] = t;
2623
- trailUVArr[uvIdx + 2] = 1;
2624
- trailUVArr[uvIdx + 3] = t;
2625
- const alpha = ca * opacityScale;
2626
- trailAlphaArr[aIdx] = alpha;
2627
- trailAlphaArr[aIdx + 1] = alpha;
3018
+ const alpha = ca * opacityScale * timeFade;
2628
3019
  const fr = trailColorOverTrailFns ? cr * trailColorOverTrailFns.r(t) : cr;
2629
3020
  const fg = trailColorOverTrailFns ? cg * trailColorOverTrailFns.g(t) : cg;
2630
3021
  const fb = trailColorOverTrailFns ? cb * trailColorOverTrailFns.b(t) : cb;
2631
- trailColorArr[cIdx] = fr;
2632
- trailColorArr[cIdx + 1] = fg;
2633
- trailColorArr[cIdx + 2] = fb;
2634
- trailColorArr[cIdx + 3] = ca;
2635
- trailColorArr[cIdx + 4] = fr;
2636
- trailColorArr[cIdx + 5] = fg;
2637
- trailColorArr[cIdx + 6] = fb;
2638
- trailColorArr[cIdx + 7] = ca;
3022
+ writeTrailVertex(
3023
+ vIdx,
3024
+ cIdx,
3025
+ aIdx,
3026
+ uvIdxBase,
3027
+ hx,
3028
+ hy,
3029
+ hz,
3030
+ nx,
3031
+ ny,
3032
+ nz,
3033
+ halfWidth,
3034
+ t,
3035
+ alpha,
3036
+ fr,
3037
+ fg,
3038
+ fb,
3039
+ ca,
3040
+ trailPosArr,
3041
+ trailNextArr,
3042
+ trailHalfWidthArr,
3043
+ trailUVArr,
3044
+ trailAlphaArr,
3045
+ trailColorArr
3046
+ );
3047
+ }
3048
+ if (useTwistPrevention && prevNormal && finalCount >= 2) {
3049
+ const nIdx = index * 3;
3050
+ const tx = finalPts[3] - finalPts[0];
3051
+ const ty = finalPts[4] - finalPts[1];
3052
+ const tz = finalPts[5] - finalPts[2];
3053
+ const tLen = Math.sqrt(tx * tx + ty * ty + tz * tz);
3054
+ if (tLen > 1e-4) {
3055
+ const ntx = tx / tLen;
3056
+ const nty = ty / tLen;
3057
+ const ntz = tz / tLen;
3058
+ let upx = 0, upy = 1, upz = 0;
3059
+ const dot = ntx * upx + nty * upy + ntz * upz;
3060
+ if (Math.abs(dot) > 0.999) {
3061
+ upx = 1;
3062
+ upy = 0;
3063
+ upz = 0;
3064
+ }
3065
+ let cnx = nty * upz - ntz * upy;
3066
+ let cny = ntz * upx - ntx * upz;
3067
+ let cnz = ntx * upy - nty * upx;
3068
+ const cnLen = Math.sqrt(cnx * cnx + cny * cny + cnz * cnz);
3069
+ if (cnLen > 1e-4) {
3070
+ cnx /= cnLen;
3071
+ cny /= cnLen;
3072
+ cnz /= cnLen;
3073
+ }
3074
+ const prevNx = prevNormal[nIdx];
3075
+ const prevNy = prevNormal[nIdx + 1];
3076
+ const prevNz = prevNormal[nIdx + 2];
3077
+ const hasPrev = prevNx !== 0 || prevNy !== 0 || prevNz !== 0;
3078
+ if (hasPrev) {
3079
+ const normalDot = cnx * prevNx + cny * prevNy + cnz * prevNz;
3080
+ if (normalDot < 0) {
3081
+ for (let s = 0; s < Math.min(finalCount, trailLength); s++) {
3082
+ const aIdx = vertBase + s * 2;
3083
+ const hw = trailHalfWidthArr[aIdx];
3084
+ trailHalfWidthArr[aIdx] = -hw;
3085
+ trailHalfWidthArr[aIdx + 1] = -hw;
3086
+ }
3087
+ cnx = -cnx;
3088
+ cny = -cny;
3089
+ cnz = -cnz;
3090
+ }
3091
+ }
3092
+ prevNormal[nIdx] = cnx;
3093
+ prevNormal[nIdx + 1] = cny;
3094
+ prevNormal[nIdx + 2] = cnz;
3095
+ }
2639
3096
  }
2640
3097
  } else if (historyCount[index] > 0) {
2641
3098
  hasUpdates = true;
@@ -2645,30 +3102,209 @@ var updateTrailGeometry = (props) => {
2645
3102
  const vIdx = (vertBase + s * 2) * 3;
2646
3103
  const cIdx = (vertBase + s * 2) * 4;
2647
3104
  const aIdx = vertBase + s * 2;
2648
- trailPosArr[vIdx] = 0;
2649
- trailPosArr[vIdx + 1] = 0;
2650
- trailPosArr[vIdx + 2] = 0;
2651
- trailPosArr[vIdx + 3] = 0;
2652
- trailPosArr[vIdx + 4] = 0;
2653
- trailPosArr[vIdx + 5] = 0;
2654
- trailNextArr[vIdx] = 0;
2655
- trailNextArr[vIdx + 1] = 0;
2656
- trailNextArr[vIdx + 2] = 0;
2657
- trailNextArr[vIdx + 3] = 0;
2658
- trailNextArr[vIdx + 4] = 0;
2659
- trailNextArr[vIdx + 5] = 0;
2660
- trailHalfWidthArr[aIdx] = 0;
2661
- trailHalfWidthArr[aIdx + 1] = 0;
2662
- trailAlphaArr[aIdx] = 0;
2663
- trailAlphaArr[aIdx + 1] = 0;
2664
- trailColorArr[cIdx] = 0;
2665
- trailColorArr[cIdx + 1] = 0;
2666
- trailColorArr[cIdx + 2] = 0;
2667
- trailColorArr[cIdx + 3] = 0;
2668
- trailColorArr[cIdx + 4] = 0;
2669
- trailColorArr[cIdx + 5] = 0;
2670
- trailColorArr[cIdx + 6] = 0;
2671
- trailColorArr[cIdx + 7] = 0;
3105
+ const uvIdxBase = (vertBase + s * 2) * 2;
3106
+ clearTrailVertex(
3107
+ vIdx,
3108
+ cIdx,
3109
+ aIdx,
3110
+ uvIdxBase,
3111
+ trailPosArr,
3112
+ trailNextArr,
3113
+ trailHalfWidthArr,
3114
+ trailUVArr,
3115
+ trailAlphaArr,
3116
+ trailColorArr,
3117
+ 0,
3118
+ 0,
3119
+ 0
3120
+ );
3121
+ }
3122
+ }
3123
+ }
3124
+ if (useRibbon && _ribbonCount >= 2 && _ribbonIndices) {
3125
+ hasUpdates = true;
3126
+ const leader = _ribbonIndices[0];
3127
+ const leaderVertBase = leader * verticesPerParticle;
3128
+ const controlCount = _ribbonCount;
3129
+ const filledCount = Math.min(
3130
+ trailLength,
3131
+ Math.max(controlCount * 4, controlCount)
3132
+ );
3133
+ const chainSize = filledCount * 3;
3134
+ if (!_rawPoints || _rawPointsSize < chainSize) {
3135
+ _rawPoints = new Float32Array(chainSize);
3136
+ _rawPointsSize = chainSize;
3137
+ }
3138
+ if (controlCount === 2) {
3139
+ const p0Idx = _ribbonIndices[0] * 3;
3140
+ const p1Idx = _ribbonIndices[1] * 3;
3141
+ for (let i = 0; i < filledCount; i++) {
3142
+ const t = i / (filledCount - 1);
3143
+ _rawPoints[i * 3] = positionArr[p0Idx] + t * (positionArr[p1Idx] - positionArr[p0Idx]);
3144
+ _rawPoints[i * 3 + 1] = positionArr[p0Idx + 1] + t * (positionArr[p1Idx + 1] - positionArr[p0Idx + 1]);
3145
+ _rawPoints[i * 3 + 2] = positionArr[p0Idx + 2] + t * (positionArr[p1Idx + 2] - positionArr[p0Idx + 2]);
3146
+ }
3147
+ } else {
3148
+ const segments = controlCount - 1;
3149
+ const ptsPerSeg = Math.max(1, Math.floor((filledCount - 1) / segments));
3150
+ let wi = 0;
3151
+ for (let seg = 0; seg < segments && wi < filledCount; seg++) {
3152
+ const i0 = Math.max(0, seg - 1);
3153
+ const i1 = seg;
3154
+ const i2 = Math.min(controlCount - 1, seg + 1);
3155
+ const i3 = Math.min(controlCount - 1, seg + 2);
3156
+ const p0i = _ribbonIndices[i0] * 3;
3157
+ const p1i = _ribbonIndices[i1] * 3;
3158
+ const p2i = _ribbonIndices[i2] * 3;
3159
+ const p3i = _ribbonIndices[i3] * 3;
3160
+ const subCount = seg === segments - 1 ? filledCount - wi : ptsPerSeg;
3161
+ for (let sub = 0; sub < subCount && wi < filledCount; sub++) {
3162
+ const t = sub / subCount;
3163
+ catmullRom(
3164
+ _rawPoints,
3165
+ wi * 3,
3166
+ positionArr[p0i],
3167
+ positionArr[p0i + 1],
3168
+ positionArr[p0i + 2],
3169
+ positionArr[p1i],
3170
+ positionArr[p1i + 1],
3171
+ positionArr[p1i + 2],
3172
+ positionArr[p2i],
3173
+ positionArr[p2i + 1],
3174
+ positionArr[p2i + 2],
3175
+ positionArr[p3i],
3176
+ positionArr[p3i + 1],
3177
+ positionArr[p3i + 2],
3178
+ t
3179
+ );
3180
+ wi++;
3181
+ }
3182
+ }
3183
+ if (wi > 0) {
3184
+ const lastPIdx = _ribbonIndices[controlCount - 1] * 3;
3185
+ _rawPoints[(wi - 1) * 3] = positionArr[lastPIdx];
3186
+ _rawPoints[(wi - 1) * 3 + 1] = positionArr[lastPIdx + 1];
3187
+ _rawPoints[(wi - 1) * 3 + 2] = positionArr[lastPIdx + 2];
3188
+ }
3189
+ }
3190
+ const leaderCr = colorRArr[leader];
3191
+ const leaderCg = colorGArr[leader];
3192
+ const leaderCb = colorBArr[leader];
3193
+ const leaderCa = colorAArr[leader];
3194
+ for (let s = 0; s < trailLength; s++) {
3195
+ const vIdx = (leaderVertBase + s * 2) * 3;
3196
+ const cIdx = (leaderVertBase + s * 2) * 4;
3197
+ const aIdx = leaderVertBase + s * 2;
3198
+ const uvIdxBase = (leaderVertBase + s * 2) * 2;
3199
+ if (s >= filledCount) {
3200
+ clearTrailVertex(
3201
+ vIdx,
3202
+ cIdx,
3203
+ aIdx,
3204
+ uvIdxBase,
3205
+ trailPosArr,
3206
+ trailNextArr,
3207
+ trailHalfWidthArr,
3208
+ trailUVArr,
3209
+ trailAlphaArr,
3210
+ trailColorArr,
3211
+ 0,
3212
+ 0,
3213
+ 0
3214
+ );
3215
+ continue;
3216
+ }
3217
+ const ptIdx = s * 3;
3218
+ const ptx = _rawPoints[ptIdx];
3219
+ const pty = _rawPoints[ptIdx + 1];
3220
+ const ptz = _rawPoints[ptIdx + 2];
3221
+ let nx, ny, nz;
3222
+ if (s > 0 && s < filledCount - 1) {
3223
+ const px2 = _rawPoints[(s - 1) * 3];
3224
+ const py2 = _rawPoints[(s - 1) * 3 + 1];
3225
+ const pz2 = _rawPoints[(s - 1) * 3 + 2];
3226
+ const nx2 = _rawPoints[(s + 1) * 3];
3227
+ const ny2 = _rawPoints[(s + 1) * 3 + 1];
3228
+ const nz2 = _rawPoints[(s + 1) * 3 + 2];
3229
+ const atx = nx2 - px2;
3230
+ const aty = ny2 - py2;
3231
+ const atz = nz2 - pz2;
3232
+ const atLen = Math.sqrt(atx * atx + aty * aty + atz * atz);
3233
+ if (atLen > 1e-4) {
3234
+ nx = ptx + atx / atLen;
3235
+ ny = pty + aty / atLen;
3236
+ nz = ptz + atz / atLen;
3237
+ } else {
3238
+ nx = _rawPoints[(s + 1) * 3];
3239
+ ny = _rawPoints[(s + 1) * 3 + 1];
3240
+ nz = _rawPoints[(s + 1) * 3 + 2];
3241
+ }
3242
+ } else if (s < filledCount - 1) {
3243
+ nx = _rawPoints[(s + 1) * 3];
3244
+ ny = _rawPoints[(s + 1) * 3 + 1];
3245
+ nz = _rawPoints[(s + 1) * 3 + 2];
3246
+ } else {
3247
+ nx = ptx;
3248
+ ny = pty + 1e-3;
3249
+ nz = ptz;
3250
+ }
3251
+ const t = filledCount > 1 ? s / (filledCount - 1) : 0;
3252
+ const widthScale = trailWidthCurveFn(t);
3253
+ const opacityScale = trailOpacityCurveFn(t);
3254
+ const halfWidth = trailConfig.width * widthScale * 0.5;
3255
+ const alpha = leaderCa * opacityScale;
3256
+ const fr = trailColorOverTrailFns ? leaderCr * trailColorOverTrailFns.r(t) : leaderCr;
3257
+ const fg = trailColorOverTrailFns ? leaderCg * trailColorOverTrailFns.g(t) : leaderCg;
3258
+ const fb = trailColorOverTrailFns ? leaderCb * trailColorOverTrailFns.b(t) : leaderCb;
3259
+ writeTrailVertex(
3260
+ vIdx,
3261
+ cIdx,
3262
+ aIdx,
3263
+ uvIdxBase,
3264
+ ptx,
3265
+ pty,
3266
+ ptz,
3267
+ nx,
3268
+ ny,
3269
+ nz,
3270
+ halfWidth,
3271
+ t,
3272
+ alpha,
3273
+ fr,
3274
+ fg,
3275
+ fb,
3276
+ leaderCa,
3277
+ trailPosArr,
3278
+ trailNextArr,
3279
+ trailHalfWidthArr,
3280
+ trailUVArr,
3281
+ trailAlphaArr,
3282
+ trailColorArr
3283
+ );
3284
+ }
3285
+ for (let ri = 1; ri < _ribbonCount; ri++) {
3286
+ const pIdx = _ribbonIndices[ri];
3287
+ const pVertBase = pIdx * verticesPerParticle;
3288
+ for (let s = 0; s < trailLength; s++) {
3289
+ const vIdx = (pVertBase + s * 2) * 3;
3290
+ const cIdx = (pVertBase + s * 2) * 4;
3291
+ const aIdx = pVertBase + s * 2;
3292
+ const uvIdxBase = (pVertBase + s * 2) * 2;
3293
+ clearTrailVertex(
3294
+ vIdx,
3295
+ cIdx,
3296
+ aIdx,
3297
+ uvIdxBase,
3298
+ trailPosArr,
3299
+ trailNextArr,
3300
+ trailHalfWidthArr,
3301
+ trailUVArr,
3302
+ trailAlphaArr,
3303
+ trailColorArr,
3304
+ 0,
3305
+ 0,
3306
+ 0
3307
+ );
2672
3308
  }
2673
3309
  }
2674
3310
  }