@polarfront-lab/ionian 1.5.0 → 1.7.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.
package/dist/ionian.js CHANGED
@@ -334,6 +334,7 @@ class MeshSurfaceSampler {
334
334
  }
335
335
  }
336
336
  class DataTextureService {
337
+ // Cache the current atlas
337
338
  /**
338
339
  * Creates a new DataTextureManager instance.
339
340
  * @param eventEmitter
@@ -343,6 +344,7 @@ class DataTextureService {
343
344
  __publicField(this, "textureSize");
344
345
  __publicField(this, "dataTextures");
345
346
  __publicField(this, "eventEmitter");
347
+ __publicField(this, "currentAtlas", null);
346
348
  this.eventEmitter = eventEmitter;
347
349
  this.textureSize = textureSize;
348
350
  this.dataTextures = /* @__PURE__ */ new Map();
@@ -353,6 +355,10 @@ class DataTextureService {
353
355
  this.textureSize = textureSize;
354
356
  this.dataTextures.forEach((texture) => texture.dispose());
355
357
  this.dataTextures.clear();
358
+ if (this.currentAtlas) {
359
+ this.currentAtlas.dispose();
360
+ this.currentAtlas = null;
361
+ }
356
362
  }
357
363
  /**
358
364
  * Prepares a mesh for sampling.
@@ -360,23 +366,76 @@ class DataTextureService {
360
366
  * @param asset The asset to prepare.
361
367
  */
362
368
  async getDataTexture(asset) {
363
- const texture = this.dataTextures.get(asset.name);
364
- if (texture) {
365
- return texture;
369
+ const cachedTexture = this.dataTextures.get(asset.uuid);
370
+ if (cachedTexture) {
371
+ return cachedTexture;
366
372
  }
367
373
  const meshData = parseMeshData(asset);
368
374
  const array = sampleMesh(meshData, this.textureSize);
369
375
  const dataTexture = createDataTexture(array, this.textureSize);
370
376
  dataTexture.name = asset.name;
377
+ this.dataTextures.set(asset.uuid, dataTexture);
371
378
  return dataTexture;
372
379
  }
373
380
  async dispose() {
381
+ this.dataTextures.forEach((texture) => texture.dispose());
374
382
  this.dataTextures.clear();
383
+ if (this.currentAtlas) {
384
+ this.currentAtlas.dispose();
385
+ this.currentAtlas = null;
386
+ }
375
387
  this.updateServiceState("disposed");
376
388
  }
377
389
  updateServiceState(serviceState) {
378
390
  this.eventEmitter.emit("serviceStateUpdated", { type: "data-texture", state: serviceState });
379
391
  }
392
+ /**
393
+ * Creates a Texture Atlas containing position data for a sequence of meshes.
394
+ * @param meshes An array of THREE.Mesh objects in the desired sequence.
395
+ * @param singleTextureSize The desired resolution (width/height) for each mesh's data within the atlas.
396
+ * @returns A Promise resolving to the generated DataTexture atlas.
397
+ */
398
+ async createSequenceDataTextureAtlas(meshes, singleTextureSize) {
399
+ this.updateServiceState("loading");
400
+ if (this.currentAtlas) {
401
+ this.currentAtlas.dispose();
402
+ this.currentAtlas = null;
403
+ }
404
+ const numMeshes = meshes.length;
405
+ if (numMeshes === 0) {
406
+ throw new Error("Mesh array cannot be empty.");
407
+ }
408
+ const atlasWidth = singleTextureSize * numMeshes;
409
+ const atlasHeight = singleTextureSize;
410
+ const atlasData = new Float32Array(atlasWidth * atlasHeight * 4);
411
+ try {
412
+ for (let i = 0; i < numMeshes; i++) {
413
+ const mesh = meshes[i];
414
+ const meshDataTexture = await this.getDataTexture(mesh);
415
+ const meshTextureData = meshDataTexture.image.data;
416
+ for (let y = 0; y < singleTextureSize; y++) {
417
+ for (let x = 0; x < singleTextureSize; x++) {
418
+ const sourceIndex = (y * singleTextureSize + x) * 4;
419
+ const targetX = x + i * singleTextureSize;
420
+ const targetIndex = (y * atlasWidth + targetX) * 4;
421
+ atlasData[targetIndex] = meshTextureData[sourceIndex];
422
+ atlasData[targetIndex + 1] = meshTextureData[sourceIndex + 1];
423
+ atlasData[targetIndex + 2] = meshTextureData[sourceIndex + 2];
424
+ atlasData[targetIndex + 3] = meshTextureData[sourceIndex + 3];
425
+ }
426
+ }
427
+ }
428
+ const atlasTexture = new THREE.DataTexture(atlasData, atlasWidth, atlasHeight, THREE.RGBAFormat, THREE.FloatType);
429
+ atlasTexture.needsUpdate = true;
430
+ atlasTexture.name = `atlas-${meshes.map((m) => m.name).join("-")}`;
431
+ this.currentAtlas = atlasTexture;
432
+ this.updateServiceState("ready");
433
+ return atlasTexture;
434
+ } catch (error) {
435
+ this.updateServiceState("error");
436
+ throw error;
437
+ }
438
+ }
380
439
  }
381
440
  function parseMeshData(mesh) {
382
441
  var _a;
@@ -722,32 +781,35 @@ class IntersectionService {
722
781
  * Creates a new IntersectionService instance.
723
782
  * @param eventEmitter The event emitter used for emitting events.
724
783
  * @param camera The camera used for raycasting.
725
- * @param originGeometry The origin geometry.
726
- * @param destinationGeometry The destination geometry.
727
784
  */
728
- constructor(eventEmitter, camera, originGeometry, destinationGeometry) {
785
+ constructor(eventEmitter, camera) {
729
786
  __publicField(this, "active", true);
730
787
  __publicField(this, "raycaster", new THREE.Raycaster());
731
788
  __publicField(this, "mousePosition", new THREE.Vector2());
732
789
  __publicField(this, "camera");
733
- __publicField(this, "originGeometry");
734
- __publicField(this, "destinationGeometry");
735
- __publicField(this, "progress", 0);
790
+ __publicField(this, "meshSequenceGeometries", []);
791
+ // ADDED: Store cloned geometries
792
+ __publicField(this, "meshSequenceUUIDs", []);
793
+ // ADDED: Track UUIDs to avoid redundant cloning
794
+ __publicField(this, "overallProgress", 0);
795
+ // ADDED: Store overall progress (0-1)
736
796
  __publicField(this, "intersectionMesh", new THREE.Mesh());
797
+ // Use a single mesh for intersection target
737
798
  __publicField(this, "geometryNeedsUpdate");
738
799
  __publicField(this, "eventEmitter");
739
800
  __publicField(this, "blendedGeometry");
801
+ // Keep for the final blended result
740
802
  __publicField(this, "intersection");
741
- __publicField(this, "lastKnownOriginMeshID");
742
- __publicField(this, "lastKnownDestinationMeshID");
743
803
  this.camera = camera;
744
- this.originGeometry = originGeometry;
745
804
  this.eventEmitter = eventEmitter;
746
- this.destinationGeometry = destinationGeometry;
747
805
  this.geometryNeedsUpdate = true;
748
806
  }
749
807
  setActive(active) {
750
808
  this.active = active;
809
+ if (!active) {
810
+ this.intersection = void 0;
811
+ this.eventEmitter.emit("interactionPositionUpdated", { position: { x: 0, y: 0, z: 0, w: 0 } });
812
+ }
751
813
  }
752
814
  getIntersectionMesh() {
753
815
  return this.intersectionMesh;
@@ -760,36 +822,40 @@ class IntersectionService {
760
822
  this.camera = camera;
761
823
  }
762
824
  /**
763
- * Set the origin geometry.
764
- * @param source
825
+ * Sets the sequence of meshes used for intersection calculations.
826
+ * Clones the geometries to avoid modifying originals.
827
+ * @param meshes An array of THREE.Mesh objects in sequence.
765
828
  */
766
- setOriginGeometry(source) {
767
- if (this.lastKnownOriginMeshID === source.uuid) return;
768
- if (this.originGeometry) this.originGeometry.dispose();
769
- this.lastKnownOriginMeshID = source.uuid;
770
- this.originGeometry = source.geometry.clone();
771
- this.originGeometry.applyMatrix4(source.matrixWorld);
772
- this.geometryNeedsUpdate = true;
773
- }
774
- /**
775
- * Set the destination geometry.
776
- * @param source
777
- */
778
- setDestinationGeometry(source) {
779
- if (this.lastKnownDestinationMeshID === source.uuid) return;
780
- if (this.destinationGeometry) this.destinationGeometry.dispose();
781
- this.lastKnownDestinationMeshID = source.uuid;
782
- this.destinationGeometry = source.geometry.clone();
783
- this.destinationGeometry.applyMatrix4(source.matrixWorld);
829
+ setMeshSequence(meshes) {
830
+ this.meshSequenceGeometries.forEach((geom) => geom.dispose());
831
+ this.meshSequenceGeometries = [];
832
+ this.meshSequenceUUIDs = [];
833
+ if (!meshes || meshes.length === 0) {
834
+ this.geometryNeedsUpdate = true;
835
+ return;
836
+ }
837
+ meshes.forEach((mesh) => {
838
+ if (mesh && mesh.geometry) {
839
+ const clonedGeometry = mesh.geometry.clone();
840
+ clonedGeometry.applyMatrix4(mesh.matrixWorld);
841
+ this.meshSequenceGeometries.push(clonedGeometry);
842
+ this.meshSequenceUUIDs.push(mesh.uuid);
843
+ } else {
844
+ console.warn("Invalid mesh provided to IntersectionService sequence.");
845
+ }
846
+ });
784
847
  this.geometryNeedsUpdate = true;
785
848
  }
786
849
  /**
787
- * Set the progress of the morphing animation.
788
- * @param progress
850
+ * Set the overall progress through the mesh sequence.
851
+ * @param progress Value between 0.0 (first mesh) and 1.0 (last mesh).
789
852
  */
790
- setProgress(progress) {
791
- this.progress = progress;
792
- this.geometryNeedsUpdate = true;
853
+ setOverallProgress(progress) {
854
+ const newProgress = THREE.MathUtils.clamp(progress, 0, 1);
855
+ if (this.overallProgress !== newProgress) {
856
+ this.overallProgress = newProgress;
857
+ this.geometryNeedsUpdate = true;
858
+ }
793
859
  }
794
860
  /**
795
861
  * Set the mouse position.
@@ -803,22 +869,46 @@ class IntersectionService {
803
869
  * @returns The intersection point or undefined if no intersection was found.
804
870
  */
805
871
  calculate(instancedMesh) {
806
- if (!this.active) return;
807
- this.updateIntersectionMesh(instancedMesh);
808
- if (!this.camera) return;
872
+ var _a, _b, _c;
873
+ if (!this.active || !this.camera || this.meshSequenceGeometries.length === 0) {
874
+ if (this.intersection) {
875
+ this.intersection = void 0;
876
+ this.eventEmitter.emit("interactionPositionUpdated", { position: { x: 0, y: 0, z: 0, w: 0 } });
877
+ }
878
+ return void 0;
879
+ }
809
880
  if (this.geometryNeedsUpdate) {
810
- this.geometryNeedsUpdate = false;
881
+ if (this.blendedGeometry && this.blendedGeometry !== this.intersectionMesh.geometry) {
882
+ this.blendedGeometry.dispose();
883
+ }
811
884
  this.blendedGeometry = this.getBlendedGeometry();
885
+ this.geometryNeedsUpdate = false;
886
+ if (this.blendedGeometry) {
887
+ if (this.intersectionMesh.geometry !== this.blendedGeometry) {
888
+ if (this.intersectionMesh.geometry) this.intersectionMesh.geometry.dispose();
889
+ this.intersectionMesh.geometry = this.blendedGeometry;
890
+ }
891
+ } else {
892
+ if (this.intersectionMesh.geometry) this.intersectionMesh.geometry.dispose();
893
+ this.intersectionMesh.geometry = new THREE.BufferGeometry();
894
+ }
812
895
  }
813
- if (this.blendedGeometry) {
814
- this.intersection = this.getFirstIntersection(this.camera, instancedMesh);
815
- } else {
816
- this.intersection = void 0;
896
+ this.intersectionMesh.matrixWorld.copy(instancedMesh.matrixWorld);
897
+ let newIntersection = void 0;
898
+ if (this.blendedGeometry && this.blendedGeometry.attributes.position) {
899
+ newIntersection = this.getFirstIntersection(this.camera, this.intersectionMesh);
817
900
  }
818
- if (this.intersection) {
819
- this.eventEmitter.emit("interactionPositionUpdated", { position: this.intersection });
820
- } else {
821
- this.eventEmitter.emit("interactionPositionUpdated", { position: { x: 0, y: 0, z: 0, w: 0 } });
901
+ const hasChanged = ((_a = this.intersection) == null ? void 0 : _a.x) !== (newIntersection == null ? void 0 : newIntersection.x) || ((_b = this.intersection) == null ? void 0 : _b.y) !== (newIntersection == null ? void 0 : newIntersection.y) || ((_c = this.intersection) == null ? void 0 : _c.z) !== (newIntersection == null ? void 0 : newIntersection.z) || this.intersection && !newIntersection || !this.intersection && newIntersection;
902
+ if (hasChanged) {
903
+ this.intersection = newIntersection;
904
+ if (this.intersection) {
905
+ const worldPoint = new THREE.Vector3(this.intersection.x, this.intersection.y, this.intersection.z);
906
+ const localPoint = instancedMesh.worldToLocal(worldPoint.clone());
907
+ this.intersection.set(localPoint.x, localPoint.y, localPoint.z, 1);
908
+ this.eventEmitter.emit("interactionPositionUpdated", { position: this.intersection });
909
+ } else {
910
+ this.eventEmitter.emit("interactionPositionUpdated", { position: { x: 0, y: 0, z: 0, w: 0 } });
911
+ }
822
912
  }
823
913
  return this.intersection;
824
914
  }
@@ -827,8 +917,13 @@ class IntersectionService {
827
917
  */
828
918
  dispose() {
829
919
  var _a;
830
- (_a = this.blendedGeometry) == null ? void 0 : _a.dispose();
831
- this.intersectionMesh.geometry.dispose();
920
+ this.meshSequenceGeometries.forEach((geom) => geom.dispose());
921
+ this.meshSequenceGeometries = [];
922
+ this.meshSequenceUUIDs = [];
923
+ if (this.blendedGeometry && this.blendedGeometry !== this.intersectionMesh.geometry) {
924
+ this.blendedGeometry.dispose();
925
+ }
926
+ (_a = this.intersectionMesh.geometry) == null ? void 0 : _a.dispose();
832
927
  }
833
928
  updateIntersectionMesh(instancedMesh) {
834
929
  if (this.blendedGeometry) {
@@ -842,29 +937,50 @@ class IntersectionService {
842
937
  this.intersectionMesh.matrixAutoUpdate = false;
843
938
  this.intersectionMesh.updateMatrixWorld(true);
844
939
  }
845
- getFirstIntersection(camera, instancedMesh) {
940
+ getFirstIntersection(camera, targetMesh) {
846
941
  this.raycaster.setFromCamera(this.mousePosition, camera);
847
- const intersection = this.raycaster.intersectObject(this.intersectionMesh, false)[0];
848
- if (intersection) {
849
- const worldPoint = intersection.point.clone();
850
- const localPoint = instancedMesh.worldToLocal(worldPoint);
851
- return new THREE.Vector4(localPoint.x, localPoint.y, localPoint.z, 1);
942
+ const intersects = this.raycaster.intersectObject(targetMesh, false);
943
+ if (intersects.length > 0 && intersects[0].point) {
944
+ const worldPoint = intersects[0].point;
945
+ return new THREE.Vector4(worldPoint.x, worldPoint.y, worldPoint.z, 1);
852
946
  }
947
+ return void 0;
853
948
  }
854
949
  getBlendedGeometry() {
855
- if (this.progress === 0) {
856
- return this.originGeometry;
950
+ const numGeometries = this.meshSequenceGeometries.length;
951
+ if (numGeometries === 0) {
952
+ return void 0;
857
953
  }
858
- if (this.progress === 1) {
859
- return this.destinationGeometry;
954
+ if (numGeometries === 1) {
955
+ return this.meshSequenceGeometries[0];
860
956
  }
861
- if (!this.originGeometry || !this.destinationGeometry) {
862
- return;
957
+ const totalSegments = numGeometries - 1;
958
+ const progressPerSegment = 1 / totalSegments;
959
+ const scaledProgress = this.overallProgress * totalSegments;
960
+ let indexA = Math.floor(scaledProgress);
961
+ let indexB = indexA + 1;
962
+ indexA = THREE.MathUtils.clamp(indexA, 0, totalSegments);
963
+ indexB = THREE.MathUtils.clamp(indexB, 0, totalSegments);
964
+ let localProgress = 0;
965
+ if (progressPerSegment > 0) {
966
+ localProgress = scaledProgress - indexA;
967
+ }
968
+ if (this.overallProgress >= 1) {
969
+ indexA = totalSegments;
970
+ indexB = totalSegments;
971
+ localProgress = 1;
972
+ }
973
+ localProgress = THREE.MathUtils.clamp(localProgress, 0, 1);
974
+ const geomA = this.meshSequenceGeometries[indexA];
975
+ const geomB = this.meshSequenceGeometries[indexB];
976
+ if (!geomA || !geomB) {
977
+ console.error("IntersectionService: Invalid geometries found for blending at indices", indexA, indexB);
978
+ return this.meshSequenceGeometries[0];
863
979
  }
864
- if (this.originGeometry === this.destinationGeometry) {
865
- return this.originGeometry;
980
+ if (indexA === indexB) {
981
+ return geomA;
866
982
  }
867
- return this.blendGeometry(this.originGeometry, this.destinationGeometry, this.progress);
983
+ return this.blendGeometry(geomA, geomB, localProgress);
868
984
  }
869
985
  blendGeometry(from, to, progress) {
870
986
  const blended = new THREE.BufferGeometry();
@@ -914,8 +1030,8 @@ class FullScreenQuad {
914
1030
  }
915
1031
  class GPUComputationRenderer {
916
1032
  /**
917
- * @param {Number} sizeX Computation problem size is always 2d: sizeX * sizeY elements.
918
- * @param {Number} sizeY Computation problem size is always 2d: sizeX * sizeY elements.
1033
+ * @param {number} sizeX Computation problem size is always 2d: sizeX * sizeY elements.
1034
+ * @param {number} sizeY Computation problem size is always 2d: sizeX * sizeY elements.
919
1035
  * @param {WebGLRenderer} renderer The renderer
920
1036
  */
921
1037
  constructor(sizeX, sizeY, renderer) {
@@ -1087,179 +1203,283 @@ class GPUComputationRenderer {
1087
1203
  }
1088
1204
  }
1089
1205
  }
1090
- const mixShader = `
1091
- uniform sampler2D uPositionA;
1092
- uniform sampler2D uPositionB;
1093
- uniform float uProgress;
1094
-
1095
- void main() {
1096
- vec2 uv = gl_FragCoord.xy / resolution.xy;
1097
- vec3 positionA = texture2D(uPositionA, uv).xyz;
1098
- vec3 positionB = texture2D(uPositionB, uv).xyz;
1099
- vec3 mixedPosition = mix(positionA, positionB, uProgress);
1100
- gl_FragColor = vec4(mixedPosition, 1.0);
1101
- }
1102
- `;
1103
1206
  const positionShader = `
1104
- uniform float uProgress;
1105
1207
  uniform vec4 uInteractionPosition;
1106
1208
  uniform float uTime;
1107
1209
  uniform float uTractionForce;
1210
+ uniform sampler2D uPositionAtlas;
1211
+ uniform float uOverallProgress; // (0.0 to 1.0)
1212
+ uniform int uNumMeshes;
1213
+ uniform float uSingleTextureSize;
1108
1214
 
1109
1215
  float rand(vec2 co) {
1110
1216
  return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
1111
1217
  }
1112
1218
 
1219
+ // Helper function to get position from atlas
1220
+ vec3 getAtlasPosition(vec2 uv, int meshIndex) {
1221
+ float atlasWidth = uSingleTextureSize * float(uNumMeshes);
1222
+ float atlasHeight = uSingleTextureSize; // Assuming height is single texture size
1223
+
1224
+ // Calculate UV within the specific mesh's section of the atlas
1225
+ float segmentWidthRatio = uSingleTextureSize / atlasWidth;
1226
+ vec2 atlasUV = vec2(
1227
+ uv.x * segmentWidthRatio + segmentWidthRatio * float(meshIndex),
1228
+ uv.y // Assuming vertical layout doesn't change y
1229
+ );
1230
+
1231
+ return texture2D(uPositionAtlas, atlasUV).xyz;
1232
+ }
1233
+
1113
1234
  void main() {
1235
+ // GPGPU UV calculation
1236
+ vec2 uv = gl_FragCoord.xy / resolution.xy; // resolution is the size of the *output* texture (e.g., 256x256)
1114
1237
 
1115
- // in GPGPU, we calculate the uv on each fragment shader, not using the static varying passed over from the v shader.
1116
- vec2 uv = gl_FragCoord.xy / resolution.xy;
1117
- float offset = rand(uv);
1238
+ vec3 currentPosition = texture2D(uCurrentPosition, uv).xyz;
1239
+ vec3 currentVelocity = texture2D(uCurrentVelocity, uv).xyz;
1118
1240
 
1119
- vec3 position = texture2D(uCurrentPosition, uv).xyz;
1120
- vec3 velocity = texture2D(uCurrentVelocity, uv).xyz;
1121
- vec3 mixedPosition = texture2D(uMixedPosition, uv).xyz;
1241
+ // --- Calculate Target Position from Atlas ---
1242
+ vec3 targetPosition;
1243
+ if (uNumMeshes <= 1) {
1244
+ targetPosition = getAtlasPosition(uv, 0);
1245
+ } else {
1246
+ float totalSegments = float(uNumMeshes - 1);
1247
+ float progressPerSegment = 1.0 / totalSegments;
1248
+ float scaledProgress = uOverallProgress * totalSegments;
1122
1249
 
1123
- // particle attraction to original position.
1124
- vec3 direction = normalize(mixedPosition - position); // direction vector
1125
- float dist = length ( mixedPosition - position ); // distance from where it was supposed to be, and currently are.
1250
+ int indexA = int(floor(scaledProgress));
1251
+ // Clamp indexB to avoid going out of bounds
1252
+ int indexB = min(indexA + 1, uNumMeshes - 1);
1126
1253
 
1127
- if (dist > 0.01) {
1128
- position = mix(position, mixedPosition, 0.1 * uTractionForce); // 0.1 ~ 0.001 (faster, slower)
1254
+ // Ensure indexA is also within bounds (important if uOverallProgress is exactly 1.0)
1255
+ indexA = min(indexA, uNumMeshes - 1);
1256
+
1257
+
1258
+ float localProgress = fract(scaledProgress);
1259
+
1260
+ // Handle edge case where progress is exactly 1.0
1261
+ if (uOverallProgress == 1.0) {
1262
+ indexA = uNumMeshes - 1;
1263
+ indexB = uNumMeshes - 1;
1264
+ localProgress = 1.0; // or 0.0 depending on how you want to handle it
1265
+ }
1266
+
1267
+
1268
+ vec3 positionA = getAtlasPosition(uv, indexA);
1269
+ vec3 positionB = getAtlasPosition(uv, indexB);
1270
+
1271
+ targetPosition = mix(positionA, positionB, localProgress);
1272
+ }
1273
+ // --- End Target Position Calculation ---
1274
+
1275
+ // Particle attraction to target position
1276
+ vec3 direction = normalize(targetPosition - currentPosition);
1277
+ float dist = length(targetPosition - currentPosition);
1278
+
1279
+ vec3 finalPosition = currentPosition;
1280
+
1281
+ // Apply attraction force (simplified mix)
1282
+ if (dist > 0.01) { // Only apply if significantly far
1283
+ finalPosition = mix(currentPosition, targetPosition, 0.1 * uTractionForce);
1129
1284
  }
1130
1285
 
1131
- position += velocity;
1132
- gl_FragColor = vec4(position, 1.0);
1286
+ finalPosition += currentVelocity;
1287
+ gl_FragColor = vec4(finalPosition, 1.0);
1133
1288
  }
1134
1289
  `;
1135
1290
  const velocityShader = `
1136
- uniform float uProgress;
1137
1291
  uniform vec4 uInteractionPosition;
1138
1292
  uniform float uTime;
1139
1293
  uniform float uTractionForce;
1140
1294
  uniform float uMaxRepelDistance;
1295
+ uniform sampler2D uPositionAtlas;
1296
+ uniform float uOverallProgress;
1297
+ uniform int uNumMeshes;
1298
+ uniform float uSingleTextureSize;
1141
1299
 
1142
1300
  float rand(vec2 co) {
1143
1301
  return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
1144
1302
  }
1145
1303
 
1304
+ // Helper function (same as in position shader)
1305
+ vec3 getAtlasPosition(vec2 uv, int meshIndex) {
1306
+ float atlasWidth = uSingleTextureSize * float(uNumMeshes);
1307
+ float atlasHeight = uSingleTextureSize;
1308
+ float segmentWidthRatio = uSingleTextureSize / atlasWidth;
1309
+ vec2 atlasUV = vec2(uv.x * segmentWidthRatio + segmentWidthRatio * float(meshIndex), uv.y);
1310
+ return texture2D(uPositionAtlas, atlasUV).xyz;
1311
+ }
1312
+
1146
1313
  void main() {
1147
- vec2 uv = gl_FragCoord.xy / resolution.xy;
1314
+ vec2 uv = gl_FragCoord.xy / resolution.xy;
1148
1315
  float offset = rand(uv);
1149
1316
 
1150
- vec3 position = texture2D(uCurrentPosition, uv).xyz;
1151
- vec3 velocity = texture2D(uCurrentVelocity, uv).xyz;
1152
- vec3 mixedPosition = texture2D(uMixedPosition, uv).xyz;
1317
+ vec3 currentPosition = texture2D(uCurrentPosition, uv).xyz;
1318
+ vec3 currentVelocity = texture2D(uCurrentVelocity, uv).xyz;
1319
+
1320
+ // --- Calculate Target Position from Atlas (same logic as position shader) ---
1321
+ vec3 targetPosition;
1322
+ if (uNumMeshes <= 1) {
1323
+ targetPosition = getAtlasPosition(uv, 0);
1324
+ } else {
1325
+ float totalSegments = float(uNumMeshes - 1);
1326
+ float progressPerSegment = 1.0 / totalSegments;
1327
+ float scaledProgress = uOverallProgress * totalSegments;
1328
+ int indexA = int(floor(scaledProgress));
1329
+ int indexB = min(indexA + 1, uNumMeshes - 1);
1330
+ indexA = min(indexA, uNumMeshes - 1);
1331
+ float localProgress = fract(scaledProgress);
1332
+ if (uOverallProgress == 1.0) {
1333
+ indexA = uNumMeshes - 1;
1334
+ indexB = uNumMeshes - 1;
1335
+ localProgress = 1.0;
1336
+ }
1337
+ vec3 positionA = getAtlasPosition(uv, indexA);
1338
+ vec3 positionB = getAtlasPosition(uv, indexB);
1339
+ targetPosition = mix(positionA, positionB, localProgress);
1340
+ }
1341
+ // --- End Target Position Calculation ---
1153
1342
 
1154
- velocity *= 0.9;
1343
+ vec3 finalVelocity = currentVelocity * 0.9; // Dampening
1155
1344
 
1156
- // particle traction
1157
- vec3 direction = normalize(mixedPosition - position); // direction vector
1158
- float dist = length ( mixedPosition - position ); // distance from where it was supposed to be, and currently are.
1345
+ // Particle traction force towards target (influences velocity)
1346
+ vec3 direction = normalize(targetPosition - currentPosition);
1347
+ float dist = length(targetPosition - currentPosition);
1159
1348
  if (dist > 0.01) {
1160
- position += direction * 0.1 * uTractionForce; // uTractionForce defaults to 0.1
1349
+ // Add force proportional to distance and traction setting
1350
+ finalVelocity += direction * dist * 0.01 * uTractionForce; // Adjust multiplier as needed
1161
1351
  }
1162
1352
 
1163
- // mouse repel force
1164
- float pointerDistance = distance(position, uInteractionPosition.xyz);
1165
- float mouseRepelModifier = clamp(uMaxRepelDistance - pointerDistance, 0.0, 1.0);
1166
- float normalizedDistance = pointerDistance / uMaxRepelDistance;
1167
- float repulsionStrength = (1.0 - normalizedDistance) * uInteractionPosition.w;
1168
- direction = normalize(position - uInteractionPosition.xyz);
1169
- velocity += (direction * 0.01 * repulsionStrength) * mouseRepelModifier;
1353
+ // Mouse repel force
1354
+ if (uInteractionPosition.w > 0.0) { // Check if interaction is active (w component)
1355
+ float pointerDistance = distance(currentPosition, uInteractionPosition.xyz);
1356
+ if (pointerDistance < uMaxRepelDistance) {
1357
+ float mouseRepelModifier = smoothstep(uMaxRepelDistance, 0.0, pointerDistance); // Smoother falloff
1358
+ vec3 repelDirection = normalize(currentPosition - uInteractionPosition.xyz);
1359
+ // Apply force based on proximity and interaction strength (w)
1360
+ finalVelocity += repelDirection * mouseRepelModifier * uInteractionPosition.w * 0.01; // Adjust multiplier
1361
+ }
1362
+ }
1170
1363
 
1364
+ // Optional: Reset position if particle "dies" and respawns (lifespan logic)
1171
1365
  float lifespan = 20.0;
1172
- float age = mod(uTime + lifespan * offset, lifespan);
1173
-
1174
- if (age < 0.1) {
1175
- position.xyz = mixedPosition;
1366
+ float age = mod(uTime * 0.1 + lifespan * offset, lifespan); // Adjust time scale
1367
+ if (age < 0.05) { // Small window for reset
1368
+ finalVelocity = vec3(0.0); // Reset velocity on respawn
1369
+ // Note: Resetting position directly here might cause jumps.
1370
+ // It's often better handled in the position shader or by ensuring
1371
+ // strong attraction force when dist is large.
1176
1372
  }
1177
1373
 
1178
- gl_FragColor = vec4(velocity, 1.0);
1374
+
1375
+ gl_FragColor = vec4(finalVelocity, 1.0);
1179
1376
  }
1180
1377
  `;
1181
1378
  class SimulationRenderer {
1182
1379
  /**
1183
1380
  * Creates a new SimulationRenderer instance.
1184
- * @param size The size of the simulation textures.
1381
+ * @param size The size of the simulation textures (width/height).
1185
1382
  * @param webGLRenderer The WebGL renderer.
1186
- * @param initialPosition The initial position data texture. If not provided, a default sphere will be used.
1383
+ * @param initialPosition The initial position data texture (optional, defaults to sphere).
1187
1384
  */
1188
1385
  constructor(size, webGLRenderer, initialPosition) {
1189
1386
  __publicField(this, "gpuComputationRenderer");
1190
1387
  __publicField(this, "webGLRenderer");
1191
- // calculations
1192
- __publicField(this, "positionDataTexture");
1193
- __publicField(this, "velocityDataTexture");
1194
- // GPUComputationRenderer variables
1195
- __publicField(this, "mixPositionsVar");
1388
+ // GPGPU Variables
1196
1389
  __publicField(this, "velocityVar");
1197
1390
  __publicField(this, "positionVar");
1391
+ // Input Data Textures (References)
1392
+ __publicField(this, "initialPositionDataTexture");
1393
+ // Used only for first init
1394
+ __publicField(this, "initialVelocityDataTexture");
1395
+ // Blank texture for init
1396
+ __publicField(this, "positionAtlasTexture", null);
1397
+ // Holds the current mesh sequence atlas
1398
+ // Uniforms
1198
1399
  __publicField(this, "interactionPosition");
1400
+ // Cache last known output textures
1199
1401
  __publicField(this, "lastKnownPositionDataTexture");
1200
1402
  __publicField(this, "lastKnownVelocityDataTexture");
1201
- __publicField(this, "lastKnownMixProgress");
1202
- __publicField(this, "initialDataTexture");
1203
- this.initialDataTexture = initialPosition ?? createSpherePoints(size);
1204
- this.positionDataTexture = this.initialDataTexture;
1205
1403
  this.webGLRenderer = webGLRenderer;
1206
1404
  this.gpuComputationRenderer = new GPUComputationRenderer(size, size, this.webGLRenderer);
1207
- this.lastKnownMixProgress = 0;
1208
- if (!webGLRenderer.capabilities.isWebGL2) {
1405
+ if (!webGLRenderer.capabilities.isWebGL2 && webGLRenderer.extensions.get("OES_texture_float")) {
1406
+ this.gpuComputationRenderer.setDataType(THREE.FloatType);
1407
+ } else if (!webGLRenderer.capabilities.isWebGL2) {
1209
1408
  this.gpuComputationRenderer.setDataType(THREE.HalfFloatType);
1210
1409
  }
1211
- this.velocityDataTexture = createBlankDataTexture(size);
1410
+ this.initialPositionDataTexture = initialPosition ?? createSpherePoints(size);
1411
+ this.initialVelocityDataTexture = createBlankDataTexture(size);
1212
1412
  this.interactionPosition = new THREE.Vector4(0, 0, 0, 0);
1213
- this.mixPositionsVar = this.gpuComputationRenderer.addVariable("uMixedPosition", mixShader, this.positionDataTexture);
1214
- this.velocityVar = this.gpuComputationRenderer.addVariable("uCurrentVelocity", velocityShader, this.velocityDataTexture);
1215
- this.positionVar = this.gpuComputationRenderer.addVariable("uCurrentPosition", positionShader, this.positionDataTexture);
1216
- this.mixPositionsVar.material.uniforms.uProgress = { value: 0 };
1217
- this.mixPositionsVar.material.uniforms.uPositionA = { value: this.initialDataTexture };
1218
- this.mixPositionsVar.material.uniforms.uPositionB = { value: this.initialDataTexture };
1413
+ this.velocityVar = this.gpuComputationRenderer.addVariable("uCurrentVelocity", velocityShader, this.initialVelocityDataTexture);
1414
+ this.positionVar = this.gpuComputationRenderer.addVariable("uCurrentPosition", positionShader, this.initialPositionDataTexture);
1219
1415
  this.velocityVar.material.uniforms.uTime = { value: 0 };
1220
1416
  this.velocityVar.material.uniforms.uInteractionPosition = { value: this.interactionPosition };
1221
- this.velocityVar.material.uniforms.uCurrentPosition = { value: this.positionDataTexture };
1417
+ this.velocityVar.material.uniforms.uCurrentPosition = { value: null };
1222
1418
  this.velocityVar.material.uniforms.uTractionForce = { value: 0.1 };
1223
1419
  this.velocityVar.material.uniforms.uMaxRepelDistance = { value: 0.3 };
1420
+ this.velocityVar.material.uniforms.uPositionAtlas = { value: null };
1421
+ this.velocityVar.material.uniforms.uOverallProgress = { value: 0 };
1422
+ this.velocityVar.material.uniforms.uNumMeshes = { value: 1 };
1423
+ this.velocityVar.material.uniforms.uSingleTextureSize = { value: size };
1224
1424
  this.positionVar.material.uniforms.uTime = { value: 0 };
1225
- this.positionVar.material.uniforms.uProgress = { value: 0 };
1226
1425
  this.positionVar.material.uniforms.uTractionForce = { value: 0.1 };
1227
1426
  this.positionVar.material.uniforms.uInteractionPosition = { value: this.interactionPosition };
1228
- this.positionVar.material.uniforms.uCurrentPosition = { value: this.positionDataTexture };
1229
- this.gpuComputationRenderer.setVariableDependencies(this.positionVar, [this.velocityVar, this.positionVar, this.mixPositionsVar]);
1230
- this.gpuComputationRenderer.setVariableDependencies(this.velocityVar, [this.velocityVar, this.positionVar, this.mixPositionsVar]);
1231
- const err = this.gpuComputationRenderer.init();
1232
- if (err) {
1233
- throw new Error("failed to initialize SimulationRenderer: " + err);
1427
+ this.positionVar.material.uniforms.uCurrentPosition = { value: null };
1428
+ this.positionVar.material.uniforms.uCurrentVelocity = { value: null };
1429
+ this.positionVar.material.uniforms.uPositionAtlas = { value: null };
1430
+ this.positionVar.material.uniforms.uOverallProgress = { value: 0 };
1431
+ this.positionVar.material.uniforms.uNumMeshes = { value: 1 };
1432
+ this.positionVar.material.uniforms.uSingleTextureSize = { value: size };
1433
+ this.gpuComputationRenderer.setVariableDependencies(this.positionVar, [this.positionVar, this.velocityVar]);
1434
+ this.gpuComputationRenderer.setVariableDependencies(this.velocityVar, [this.velocityVar, this.positionVar]);
1435
+ const initError = this.gpuComputationRenderer.init();
1436
+ if (initError !== null) {
1437
+ throw new Error("Failed to initialize SimulationRenderer: " + initError);
1234
1438
  }
1235
- this.lastKnownVelocityDataTexture = this.getVelocityTexture();
1236
- this.lastKnownPositionDataTexture = this.getPositionTexture();
1439
+ this.positionVar.material.uniforms.uPositionAtlas.value = this.initialPositionDataTexture;
1440
+ this.positionVar.material.uniforms.uNumMeshes.value = 1;
1441
+ this.velocityVar.material.uniforms.uNumMeshes.value = 1;
1442
+ this.positionVar.material.uniforms.uSingleTextureSize.value = size;
1443
+ this.velocityVar.material.uniforms.uSingleTextureSize.value = size;
1444
+ this.positionVar.material.uniforms.uCurrentVelocity.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
1445
+ this.velocityVar.material.uniforms.uCurrentPosition.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
1446
+ this.lastKnownVelocityDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
1447
+ this.lastKnownPositionDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
1237
1448
  }
1238
1449
  /**
1239
- * Sets the source data texture for morphing.
1240
- * @param texture The source data texture.
1450
+ * Sets the mesh sequence position atlas texture and related uniforms.
1451
+ * @param entry Information about the atlas texture.
1241
1452
  */
1242
- setMorphSourceDataTexture(texture) {
1243
- this.mixPositionsVar.material.uniforms.uPositionA.value = texture;
1453
+ setPositionAtlas(entry) {
1454
+ const expectedAtlasWidth = entry.singleTextureSize * entry.numMeshes;
1455
+ if (entry.dataTexture.image.width !== expectedAtlasWidth || entry.dataTexture.image.height !== entry.singleTextureSize) {
1456
+ console.error(
1457
+ `SimulationRenderer: Atlas texture dimension mismatch! Expected ${expectedAtlasWidth}x${entry.singleTextureSize}, Got ${entry.dataTexture.image.width}x${entry.dataTexture.image.height}`
1458
+ );
1459
+ }
1460
+ this.positionAtlasTexture = entry.dataTexture;
1461
+ const numMeshes = entry.numMeshes > 0 ? entry.numMeshes : 1;
1462
+ this.positionVar.material.uniforms.uPositionAtlas.value = this.positionAtlasTexture;
1463
+ this.positionVar.material.uniforms.uNumMeshes.value = numMeshes;
1464
+ this.positionVar.material.uniforms.uSingleTextureSize.value = entry.singleTextureSize;
1465
+ this.velocityVar.material.uniforms.uPositionAtlas.value = this.positionAtlasTexture;
1466
+ this.velocityVar.material.uniforms.uNumMeshes.value = numMeshes;
1467
+ this.velocityVar.material.uniforms.uSingleTextureSize.value = entry.singleTextureSize;
1468
+ this.positionVar.material.uniforms.uCurrentVelocity.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
1469
+ this.velocityVar.material.uniforms.uCurrentPosition.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
1244
1470
  }
1245
1471
  /**
1246
- * Sets the destination data texture for morphing.
1247
- * @param texture The destination data texture.
1472
+ * Sets the overall progress for blending between meshes in the atlas.
1473
+ * @param progress Value between 0.0 and 1.0.
1248
1474
  */
1249
- setMorphDestinationDataTexture(texture) {
1250
- this.mixPositionsVar.material.uniforms.uPositionB.value = texture;
1475
+ setOverallProgress(progress) {
1476
+ const clampedProgress = clamp(progress, 0, 1);
1477
+ this.positionVar.material.uniforms.uOverallProgress.value = clampedProgress;
1478
+ this.velocityVar.material.uniforms.uOverallProgress.value = clampedProgress;
1251
1479
  }
1252
1480
  setMaxRepelDistance(distance) {
1253
1481
  this.velocityVar.material.uniforms.uMaxRepelDistance.value = distance;
1254
1482
  }
1255
- /**
1256
- * Sets the progress of the morphing animation.
1257
- * @param progress The progress value, between 0 and 1.
1258
- */
1259
- setProgress(progress) {
1260
- this.lastKnownMixProgress = clamp(progress, 0, 1);
1261
- this.mixPositionsVar.material.uniforms.uProgress.value = this.lastKnownMixProgress;
1262
- }
1263
1483
  setVelocityTractionForce(force) {
1264
1484
  this.velocityVar.material.uniforms.uTractionForce.value = force;
1265
1485
  }
@@ -1273,36 +1493,31 @@ class SimulationRenderer {
1273
1493
  * Disposes the resources used by the simulation renderer.
1274
1494
  */
1275
1495
  dispose() {
1276
- this.mixPositionsVar.renderTargets.forEach((rtt) => rtt.dispose());
1277
- this.positionVar.renderTargets.forEach((rtt) => rtt.dispose());
1278
- this.velocityVar.renderTargets.forEach((rtt) => rtt.dispose());
1279
- this.positionDataTexture.dispose();
1280
- this.velocityDataTexture.dispose();
1496
+ var _a, _b;
1281
1497
  this.gpuComputationRenderer.dispose();
1498
+ (_a = this.initialPositionDataTexture) == null ? void 0 : _a.dispose();
1499
+ (_b = this.initialVelocityDataTexture) == null ? void 0 : _b.dispose();
1500
+ this.positionAtlasTexture = null;
1282
1501
  }
1283
1502
  /**
1284
1503
  * Computes the next step of the simulation.
1285
- * @param elapsedTime The elapsed time since the simulation started.
1504
+ * @param deltaTime The time elapsed since the last frame, in seconds.
1286
1505
  */
1287
- compute(elapsedTime) {
1288
- this.velocityVar.material.uniforms.uTime.value = elapsedTime;
1289
- this.positionVar.material.uniforms.uTime.value = elapsedTime;
1506
+ compute(deltaTime) {
1507
+ this.velocityVar.material.uniforms.uTime.value += deltaTime;
1508
+ this.positionVar.material.uniforms.uTime.value += deltaTime;
1509
+ this.positionVar.material.uniforms.uCurrentVelocity.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
1510
+ this.velocityVar.material.uniforms.uCurrentPosition.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
1290
1511
  this.gpuComputationRenderer.compute();
1512
+ this.lastKnownVelocityDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
1513
+ this.lastKnownPositionDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
1291
1514
  }
1292
- /**
1293
- * Gets the current velocity texture.
1294
- * @returns The current velocity texture.
1295
- */
1515
+ /** Gets the current velocity texture (output from the last compute step). */
1296
1516
  getVelocityTexture() {
1297
- this.lastKnownVelocityDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
1298
1517
  return this.lastKnownVelocityDataTexture;
1299
1518
  }
1300
- /**
1301
- * Gets the current position texture.
1302
- * @returns The current position texture.
1303
- */
1519
+ /** Gets the current position texture (output from the last compute step). */
1304
1520
  getPositionTexture() {
1305
- this.lastKnownPositionDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
1306
1521
  return this.lastKnownPositionDataTexture;
1307
1522
  }
1308
1523
  }
@@ -1310,18 +1525,22 @@ class SimulationRendererService {
1310
1525
  constructor(eventEmitter, size, webGLRenderer) {
1311
1526
  __publicField(this, "state");
1312
1527
  __publicField(this, "textureSize");
1313
- __publicField(this, "dataTextureTransitionProgress");
1528
+ __publicField(this, "overallProgress");
1529
+ // ADDED: Store overall progress
1314
1530
  __publicField(this, "velocityTractionForce");
1315
1531
  __publicField(this, "positionalTractionForce");
1316
1532
  __publicField(this, "simulationRenderer");
1317
1533
  __publicField(this, "webGLRenderer");
1318
1534
  __publicField(this, "eventEmitter");
1535
+ // Store atlas info
1536
+ __publicField(this, "currentAtlasEntry", null);
1537
+ // ADDED
1319
1538
  __publicField(this, "lastKnownVelocityDataTexture");
1320
1539
  __publicField(this, "lastKnownPositionDataTexture");
1321
1540
  this.eventEmitter = eventEmitter;
1322
1541
  this.webGLRenderer = webGLRenderer;
1323
1542
  this.textureSize = size;
1324
- this.dataTextureTransitionProgress = 0;
1543
+ this.overallProgress = 0;
1325
1544
  this.velocityTractionForce = 0.1;
1326
1545
  this.positionalTractionForce = 0.1;
1327
1546
  this.updateServiceState("initializing");
@@ -1330,6 +1549,27 @@ class SimulationRendererService {
1330
1549
  this.lastKnownPositionDataTexture = this.simulationRenderer.getPositionTexture();
1331
1550
  this.updateServiceState("ready");
1332
1551
  }
1552
+ /**
1553
+ * Sets the position data texture atlas for the simulation.
1554
+ * @param entry An object containing the atlas texture and related parameters.
1555
+ */
1556
+ setPositionAtlas(entry) {
1557
+ const expectedWidth = entry.singleTextureSize * entry.numMeshes;
1558
+ if (entry.dataTexture.image.width !== expectedWidth) {
1559
+ this.eventEmitter.emit("invalidRequest", { message: `Atlas texture width mismatch.` });
1560
+ return;
1561
+ }
1562
+ this.currentAtlasEntry = entry;
1563
+ this.simulationRenderer.setPositionAtlas(entry);
1564
+ }
1565
+ /**
1566
+ * Sets the overall progress for the mesh sequence transition.
1567
+ * @param progress The progress value (0.0 to 1.0).
1568
+ */
1569
+ setOverallProgress(progress) {
1570
+ this.overallProgress = progress;
1571
+ this.simulationRenderer.setOverallProgress(this.overallProgress);
1572
+ }
1333
1573
  setTextureSize(size) {
1334
1574
  this.updateServiceState("initializing");
1335
1575
  this.simulationRenderer.dispose();
@@ -1337,24 +1577,6 @@ class SimulationRendererService {
1337
1577
  this.simulationRenderer = new SimulationRenderer(size, this.webGLRenderer);
1338
1578
  this.updateServiceState("ready");
1339
1579
  }
1340
- setOriginDataTexture(entry) {
1341
- if (this.textureSize !== entry.textureSize) {
1342
- this.eventEmitter.emit("invalidRequest", { message: `Texture size mismatch: ${entry.textureSize} vs ${this.textureSize}` });
1343
- } else {
1344
- this.simulationRenderer.setMorphSourceDataTexture(entry.dataTexture);
1345
- }
1346
- }
1347
- setDestinationDataTexture(entry) {
1348
- if (this.textureSize !== entry.textureSize) {
1349
- this.eventEmitter.emit("invalidRequest", { message: `Texture size mismatch: ${entry.textureSize} vs ${this.textureSize}` });
1350
- } else {
1351
- this.simulationRenderer.setMorphDestinationDataTexture(entry.dataTexture);
1352
- }
1353
- }
1354
- setDataTextureTransitionProgress(progress) {
1355
- this.dataTextureTransitionProgress = progress;
1356
- this.simulationRenderer.setProgress(this.dataTextureTransitionProgress);
1357
- }
1358
1580
  setVelocityTractionForce(force) {
1359
1581
  this.velocityTractionForce = force;
1360
1582
  this.simulationRenderer.setVelocityTractionForce(this.velocityTractionForce);
@@ -1364,21 +1586,21 @@ class SimulationRendererService {
1364
1586
  this.simulationRenderer.setPositionalTractionForce(this.positionalTractionForce);
1365
1587
  }
1366
1588
  compute(elapsedTime) {
1589
+ if (this.state !== "ready") return;
1367
1590
  this.simulationRenderer.compute(elapsedTime);
1591
+ this.lastKnownVelocityDataTexture = this.simulationRenderer.getVelocityTexture();
1592
+ this.lastKnownPositionDataTexture = this.simulationRenderer.getPositionTexture();
1368
1593
  }
1369
1594
  getVelocityTexture() {
1370
- if (this.state === "ready") this.lastKnownVelocityDataTexture = this.simulationRenderer.getVelocityTexture();
1371
1595
  return this.lastKnownVelocityDataTexture;
1372
1596
  }
1373
1597
  getPositionTexture() {
1374
- if (this.state === "ready") this.lastKnownPositionDataTexture = this.simulationRenderer.getPositionTexture();
1375
1598
  return this.lastKnownPositionDataTexture;
1376
1599
  }
1377
1600
  dispose() {
1378
1601
  this.updateServiceState("disposed");
1379
1602
  this.simulationRenderer.dispose();
1380
- this.lastKnownVelocityDataTexture.dispose();
1381
- this.lastKnownPositionDataTexture.dispose();
1603
+ this.currentAtlasEntry = null;
1382
1604
  }
1383
1605
  updateServiceState(serviceState) {
1384
1606
  this.state = serviceState;
@@ -1495,7 +1717,6 @@ class ParticlesEngine {
1495
1717
  */
1496
1718
  constructor(params) {
1497
1719
  __publicField(this, "simulationRendererService");
1498
- __publicField(this, "eventEmitter");
1499
1720
  __publicField(this, "renderer");
1500
1721
  __publicField(this, "scene");
1501
1722
  __publicField(this, "serviceStates");
@@ -1506,6 +1727,9 @@ class ParticlesEngine {
1506
1727
  __publicField(this, "transitionService");
1507
1728
  __publicField(this, "engineState");
1508
1729
  __publicField(this, "intersectionService");
1730
+ __publicField(this, "meshSequenceAtlasTexture", null);
1731
+ // ADDED: To store the generated atlas
1732
+ __publicField(this, "eventEmitter");
1509
1733
  const { scene, renderer, camera, textureSize, useIntersection = true } = params;
1510
1734
  this.eventEmitter = new DefaultEventEmitter();
1511
1735
  this.serviceStates = this.getInitialServiceStates();
@@ -1522,7 +1746,6 @@ class ParticlesEngine {
1522
1746
  this.scene.add(this.instancedMeshManager.getMesh());
1523
1747
  this.intersectionService = new IntersectionService(this.eventEmitter, camera);
1524
1748
  if (!useIntersection) this.intersectionService.setActive(false);
1525
- this.eventEmitter.on("transitionProgressed", this.handleTransitionProgress.bind(this));
1526
1749
  this.eventEmitter.on("interactionPositionUpdated", this.handleInteractionPositionUpdated.bind(this));
1527
1750
  }
1528
1751
  /**
@@ -1530,48 +1753,14 @@ class ParticlesEngine {
1530
1753
  * @param elapsedTime The elapsed time since the last frame.
1531
1754
  */
1532
1755
  render(elapsedTime) {
1756
+ const dt = elapsedTime / 1e3;
1757
+ this.transitionService.compute(dt);
1533
1758
  this.intersectionService.calculate(this.instancedMeshManager.getMesh());
1534
- this.transitionService.compute(elapsedTime);
1535
- this.simulationRendererService.compute(elapsedTime);
1536
- this.instancedMeshManager.update(elapsedTime);
1759
+ this.simulationRendererService.compute(dt);
1760
+ this.instancedMeshManager.update(dt);
1537
1761
  this.instancedMeshManager.updateVelocityTexture(this.simulationRendererService.getVelocityTexture());
1538
1762
  this.instancedMeshManager.updatePositionTexture(this.simulationRendererService.getPositionTexture());
1539
1763
  }
1540
- setOriginDataTexture(meshID, override = false) {
1541
- if (override) this.eventEmitter.emit("transitionCancelled", { type: "data-texture" });
1542
- const mesh = this.assetService.getMesh(meshID);
1543
- if (!mesh) {
1544
- this.eventEmitter.emit("invalidRequest", { message: `Mesh with id "${meshID}" does not exist` });
1545
- return;
1546
- }
1547
- this.dataTextureManager.getDataTexture(mesh).then((dataTexture) => {
1548
- this.engineState.originMeshID = meshID;
1549
- this.simulationRendererService.setOriginDataTexture({ dataTexture, textureSize: this.engineState.textureSize });
1550
- this.intersectionService.setOriginGeometry(mesh);
1551
- });
1552
- }
1553
- setDestinationDataTexture(meshID, override = false) {
1554
- if (override) this.eventEmitter.emit("transitionCancelled", { type: "data-texture" });
1555
- const mesh = this.assetService.getMesh(meshID);
1556
- if (!mesh) {
1557
- this.eventEmitter.emit("invalidRequest", { message: `Mesh with id "${meshID}" does not exist` });
1558
- return;
1559
- }
1560
- this.dataTextureManager.getDataTexture(mesh).then((texture) => {
1561
- this.engineState.destinationMeshID = meshID;
1562
- this.simulationRendererService.setDestinationDataTexture({
1563
- dataTexture: texture,
1564
- textureSize: this.engineState.textureSize
1565
- });
1566
- this.intersectionService.setDestinationGeometry(mesh);
1567
- });
1568
- }
1569
- setDataTextureTransitionProgress(progress, override = false) {
1570
- if (override) this.eventEmitter.emit("transitionCancelled", { type: "data-texture" });
1571
- this.engineState.dataTextureTransitionProgress = progress;
1572
- this.simulationRendererService.setDataTextureTransitionProgress(progress);
1573
- this.intersectionService.setProgress(progress);
1574
- }
1575
1764
  setOriginMatcap(matcapID, override = false) {
1576
1765
  if (override) this.eventEmitter.emit("transitionCancelled", { type: "matcap" });
1577
1766
  this.engineState.originMatcapID = matcapID;
@@ -1607,40 +1796,31 @@ class ParticlesEngine {
1607
1796
  }
1608
1797
  }
1609
1798
  setMatcapProgress(progress, override = false) {
1799
+ const clampedProgress = clamp(progress, 0, 1);
1610
1800
  if (override) this.eventEmitter.emit("transitionCancelled", { type: "matcap" });
1611
- this.engineState.matcapTransitionProgress = progress;
1612
- this.instancedMeshManager.setProgress(progress);
1801
+ this.engineState.matcapTransitionProgress = clampedProgress;
1802
+ this.instancedMeshManager.setProgress(clampedProgress);
1613
1803
  }
1804
+ // --- Update setTextureSize ---
1614
1805
  async setTextureSize(size) {
1806
+ if (this.engineState.textureSize === size) {
1807
+ return;
1808
+ }
1615
1809
  this.engineState.textureSize = size;
1616
1810
  this.dataTextureManager.setTextureSize(size);
1617
1811
  this.simulationRendererService.setTextureSize(size);
1618
1812
  this.instancedMeshManager.resize(size);
1619
- const originMesh = this.assetService.getMesh(this.engineState.originMeshID);
1620
- if (!originMesh) {
1621
- this.eventEmitter.emit("invalidRequest", { message: `Mesh with id "${this.engineState.originMeshID}" does not exist` });
1622
- return;
1813
+ if (this.engineState.meshSequence.length > 0) {
1814
+ await this.setMeshSequence(this.engineState.meshSequence);
1623
1815
  }
1624
- const destinationMesh = this.assetService.getMesh(this.engineState.destinationMeshID);
1625
- if (!destinationMesh) {
1626
- this.eventEmitter.emit("invalidRequest", { message: `Mesh with id "${this.engineState.destinationMeshID}" does not exist` });
1627
- return;
1816
+ if (this.engineState.meshSequence.length > 0) {
1817
+ await this.setMeshSequence(this.engineState.meshSequence);
1628
1818
  }
1629
- this.dataTextureManager.getDataTexture(originMesh).then(
1630
- (texture) => this.simulationRendererService.setOriginDataTexture({
1631
- dataTexture: texture,
1632
- textureSize: size
1633
- })
1634
- );
1635
- this.dataTextureManager.getDataTexture(destinationMesh).then(
1636
- (texture) => this.simulationRendererService.setDestinationDataTexture({
1637
- dataTexture: texture,
1638
- textureSize: size
1639
- })
1640
- );
1641
- this.simulationRendererService.setDataTextureTransitionProgress(this.engineState.dataTextureTransitionProgress);
1642
1819
  this.simulationRendererService.setVelocityTractionForce(this.engineState.velocityTractionForce);
1643
1820
  this.simulationRendererService.setPositionalTractionForce(this.engineState.positionalTractionForce);
1821
+ this.simulationRendererService.setMaxRepelDistance(this.engineState.maxRepelDistance);
1822
+ this.simulationRendererService.setOverallProgress(this.engineState.overallProgress);
1823
+ this.intersectionService.setOverallProgress(this.engineState.overallProgress);
1644
1824
  this.instancedMeshManager.setOriginMatcap(this.assetService.getMatcap(this.engineState.originMatcapID));
1645
1825
  this.instancedMeshManager.setDestinationMatcap(this.assetService.getMatcap(this.engineState.destinationMatcapID));
1646
1826
  this.instancedMeshManager.setProgress(this.engineState.matcapTransitionProgress);
@@ -1663,7 +1843,7 @@ class ParticlesEngine {
1663
1843
  this.engineState.useIntersect = use;
1664
1844
  if (!use) {
1665
1845
  this.engineState.pointerPosition = { x: -99999999, y: -99999999 };
1666
- this.intersectionService.setPointerPosition(this.engineState.pointerPosition);
1846
+ this.simulationRendererService.setInteractionPosition({ x: 0, y: 0, z: 0, w: 0 });
1667
1847
  }
1668
1848
  }
1669
1849
  setPointerPosition(position) {
@@ -1687,50 +1867,170 @@ class ParticlesEngine {
1687
1867
  this.engineState.maxRepelDistance = distance;
1688
1868
  this.simulationRendererService.setMaxRepelDistance(distance);
1689
1869
  }
1690
- scheduleMeshTransition(originMeshID, destinationMeshID, easing = linear, duration = 1e3, override = false) {
1691
- this.transitionService.enqueue(
1692
- "data-texture",
1693
- { easing, duration },
1694
- {
1695
- onTransitionBegin: () => {
1696
- this.setOriginDataTexture(originMeshID, override);
1697
- this.setDestinationDataTexture(destinationMeshID, override);
1698
- this.setDataTextureTransitionProgress(0);
1699
- }
1870
+ /**
1871
+ * Sets the sequence of meshes for particle transitions.
1872
+ * This will generate a texture atlas containing position data for all meshes.
1873
+ * @param meshIDs An array of registered mesh IDs in the desired sequence order.
1874
+ */
1875
+ async setMeshSequence(meshIDs) {
1876
+ if (!meshIDs || meshIDs.length < 1) {
1877
+ this.eventEmitter.emit("invalidRequest", { message: "Mesh sequence must contain at least one mesh ID." });
1878
+ this.engineState.meshSequence = [];
1879
+ this.intersectionService.setMeshSequence([]);
1880
+ return;
1881
+ }
1882
+ this.engineState.meshSequence = meshIDs;
1883
+ this.engineState.overallProgress = 0;
1884
+ const meshes = meshIDs.map((id) => this.assetService.getMesh(id)).filter((mesh) => mesh !== null);
1885
+ if (meshes.length !== meshIDs.length) {
1886
+ const missing = meshIDs.filter((id) => !this.assetService.getMesh(id));
1887
+ console.warn(`Could not find meshes for IDs: ${missing.join(", ")}. Proceeding with ${meshes.length} found meshes.`);
1888
+ this.eventEmitter.emit("invalidRequest", { message: `Could not find meshes for IDs: ${missing.join(", ")}` });
1889
+ if (meshes.length < 1) {
1890
+ this.engineState.meshSequence = [];
1891
+ this.intersectionService.setMeshSequence([]);
1892
+ return;
1700
1893
  }
1701
- );
1894
+ this.engineState.meshSequence = meshes.map((m) => m.name);
1895
+ }
1896
+ try {
1897
+ this.meshSequenceAtlasTexture = await this.dataTextureManager.createSequenceDataTextureAtlas(meshes, this.engineState.textureSize);
1898
+ this.simulationRendererService.setPositionAtlas({
1899
+ dataTexture: this.meshSequenceAtlasTexture,
1900
+ textureSize: this.engineState.textureSize,
1901
+ // Pass the size of the *output* GPGPU texture
1902
+ numMeshes: this.engineState.meshSequence.length,
1903
+ // Use the potentially updated count
1904
+ singleTextureSize: this.engineState.textureSize
1905
+ // Size of one mesh's data within atlas
1906
+ });
1907
+ this.simulationRendererService.setOverallProgress(this.engineState.overallProgress);
1908
+ this.intersectionService.setMeshSequence(meshes);
1909
+ this.intersectionService.setOverallProgress(this.engineState.overallProgress);
1910
+ } catch (error) {
1911
+ console.error("Failed during mesh sequence setup:", error);
1912
+ this.meshSequenceAtlasTexture = null;
1913
+ }
1914
+ }
1915
+ /**
1916
+ * Sets the overall progress through the mesh sequence.
1917
+ * @param progress A value between 0.0 (first mesh) and 1.0 (last mesh).
1918
+ * @param override If true, cancels any ongoing mesh sequence transition before setting the value. Defaults to true.
1919
+ */
1920
+ setOverallProgress(progress, override = true) {
1921
+ if (override) {
1922
+ this.eventEmitter.emit("transitionCancelled", { type: "mesh-sequence" });
1923
+ }
1924
+ const clampedProgress = clamp(progress, 0, 1);
1925
+ this.engineState.overallProgress = clampedProgress;
1926
+ this.simulationRendererService.setOverallProgress(clampedProgress);
1927
+ this.intersectionService.setOverallProgress(clampedProgress);
1702
1928
  }
1703
- scheduleMatcapTransition(originMatcapID, destinationMatcapID, easing = linear, duration = 1e3, override = false) {
1929
+ // --- Transition scheduling methods remain the same ---
1930
+ scheduleMatcapTransition(originMatcapID, destinationMatcapID, easing = linear, duration = 1e3, override = false, options = {}) {
1931
+ if (override) this.eventEmitter.emit("transitionCancelled", { type: "matcap" });
1932
+ const handleProgressUpdate = (transitionProgress) => {
1933
+ var _a;
1934
+ this.setMatcapProgress(transitionProgress, false);
1935
+ (_a = options.onTransitionProgress) == null ? void 0 : _a.call(options, transitionProgress);
1936
+ };
1704
1937
  this.transitionService.enqueue(
1705
1938
  "matcap",
1706
1939
  { easing, duration },
1707
1940
  {
1941
+ ...options,
1942
+ onTransitionProgress: handleProgressUpdate,
1708
1943
  onTransitionBegin: () => {
1709
- this.setOriginMatcap(originMatcapID, override);
1710
- this.setDestinationMatcap(destinationMatcapID, override);
1711
- this.setMatcapProgress(0);
1712
- }
1944
+ var _a;
1945
+ this.setOriginMatcap(originMatcapID, false);
1946
+ this.setDestinationMatcap(destinationMatcapID, false);
1947
+ this.setMatcapProgress(0, false);
1948
+ (_a = options.onTransitionBegin) == null ? void 0 : _a.call(options);
1949
+ },
1950
+ onTransitionFinished: () => {
1951
+ var _a;
1952
+ this.setMatcapProgress(1, false);
1953
+ (_a = options.onTransitionFinished) == null ? void 0 : _a.call(options);
1954
+ },
1955
+ onTransitionCancelled: options.onTransitionCancelled
1713
1956
  }
1714
1957
  );
1715
1958
  }
1716
- scheduleTextureTransition(origin, destination, options) {
1959
+ scheduleTextureTransition(origin, destination, options = {}) {
1717
1960
  const easing = (options == null ? void 0 : options.easing) ?? linear;
1718
1961
  const duration = (options == null ? void 0 : options.duration) ?? 1e3;
1719
- if (options == null ? void 0 : options.override) {
1720
- this.eventEmitter.emit("transitionCancelled", { type: "matcap" });
1721
- }
1962
+ const userCallbacks = {
1963
+ // Extract user callbacks
1964
+ onTransitionBegin: options == null ? void 0 : options.onTransitionBegin,
1965
+ onTransitionProgress: options == null ? void 0 : options.onTransitionProgress,
1966
+ onTransitionFinished: options == null ? void 0 : options.onTransitionFinished,
1967
+ onTransitionCancelled: options == null ? void 0 : options.onTransitionCancelled
1968
+ };
1969
+ if (options == null ? void 0 : options.override) this.eventEmitter.emit("transitionCancelled", { type: "matcap" });
1970
+ const handleProgressUpdate = (transitionProgress) => {
1971
+ var _a;
1972
+ this.setMatcapProgress(transitionProgress, false);
1973
+ (_a = userCallbacks.onTransitionProgress) == null ? void 0 : _a.call(userCallbacks, transitionProgress);
1974
+ };
1722
1975
  this.transitionService.enqueue(
1723
1976
  "matcap",
1977
+ // Still uses matcap type internally
1724
1978
  { easing, duration },
1725
1979
  {
1980
+ ...userCallbacks,
1981
+ // Pass user callbacks
1982
+ onTransitionProgress: handleProgressUpdate,
1726
1983
  onTransitionBegin: () => {
1984
+ var _a;
1727
1985
  this.setOriginTexture(origin);
1728
1986
  this.setDestinationTexture(destination);
1729
1987
  this.setMatcapProgress(0);
1988
+ (_a = userCallbacks.onTransitionBegin) == null ? void 0 : _a.call(userCallbacks);
1989
+ },
1990
+ onTransitionFinished: () => {
1991
+ var _a;
1992
+ this.setMatcapProgress(1);
1993
+ (_a = userCallbacks.onTransitionFinished) == null ? void 0 : _a.call(userCallbacks);
1994
+ },
1995
+ onTransitionCancelled: () => {
1996
+ var _a;
1997
+ (_a = userCallbacks.onTransitionCancelled) == null ? void 0 : _a.call(userCallbacks);
1730
1998
  }
1731
1999
  }
1732
2000
  );
1733
2001
  }
2002
+ /**
2003
+ * Schedules a smooth transition for the overall mesh sequence progress.
2004
+ * @param targetProgress The final progress value (0.0 to 1.0) to transition to.
2005
+ * @param duration Duration of the transition in milliseconds.
2006
+ * @param easing Easing function to use.
2007
+ * @param options Transition options (onBegin, onProgress, onFinished, onCancelled).
2008
+ * @param override If true, cancels any ongoing mesh sequence transitions.
2009
+ */
2010
+ scheduleMeshSequenceTransition(targetProgress, duration = 1e3, easing = linear, options = {}, override = true) {
2011
+ if (override) this.eventEmitter.emit("transitionCancelled", { type: "mesh-sequence" });
2012
+ const startProgress = this.engineState.overallProgress;
2013
+ const progressDiff = targetProgress - startProgress;
2014
+ const handleProgressUpdate = (transitionProgress) => {
2015
+ var _a;
2016
+ const currentOverallProgress = startProgress + progressDiff * transitionProgress;
2017
+ this.setOverallProgress(currentOverallProgress, false);
2018
+ (_a = options.onTransitionProgress) == null ? void 0 : _a.call(options, currentOverallProgress);
2019
+ };
2020
+ const transitionDetail = { duration, easing };
2021
+ const transitionOptions = {
2022
+ ...options,
2023
+ onTransitionProgress: handleProgressUpdate,
2024
+ onTransitionBegin: options.onTransitionBegin,
2025
+ onTransitionFinished: () => {
2026
+ var _a;
2027
+ this.setOverallProgress(targetProgress, false);
2028
+ (_a = options.onTransitionFinished) == null ? void 0 : _a.call(options);
2029
+ },
2030
+ onTransitionCancelled: options.onTransitionCancelled
2031
+ };
2032
+ this.transitionService.enqueue("mesh-sequence", transitionDetail, transitionOptions);
2033
+ }
1734
2034
  handleServiceStateUpdated({ type, state }) {
1735
2035
  this.serviceStates[type] = state;
1736
2036
  }
@@ -1743,23 +2043,43 @@ class ParticlesEngine {
1743
2043
  getMatcapIDs() {
1744
2044
  return this.assetService.getTextureIDs();
1745
2045
  }
2046
+ getMeshes() {
2047
+ return this.assetService.getMeshes();
2048
+ }
2049
+ getTextures() {
2050
+ return this.assetService.getTextures();
2051
+ }
2052
+ getTextureSize() {
2053
+ return this.engineState.textureSize;
2054
+ }
2055
+ getUseIntersect() {
2056
+ return this.engineState.useIntersect;
2057
+ }
2058
+ getEngineStateSnapshot() {
2059
+ return { ...this.engineState };
2060
+ }
1746
2061
  /**
1747
2062
  * Disposes the resources used by the engine.
1748
2063
  */
1749
2064
  dispose() {
1750
- this.scene.remove(this.instancedMeshManager.getMesh());
1751
- this.simulationRendererService.dispose();
1752
- this.instancedMeshManager.dispose();
1753
- this.intersectionService.dispose();
1754
- this.assetService.dispose();
1755
- this.dataTextureManager.dispose();
2065
+ var _a, _b, _c, _d, _e, _f;
2066
+ if (this.scene && this.instancedMeshManager) {
2067
+ this.scene.remove(this.instancedMeshManager.getMesh());
2068
+ }
2069
+ (_a = this.simulationRendererService) == null ? void 0 : _a.dispose();
2070
+ (_b = this.instancedMeshManager) == null ? void 0 : _b.dispose();
2071
+ (_c = this.intersectionService) == null ? void 0 : _c.dispose();
2072
+ (_d = this.assetService) == null ? void 0 : _d.dispose();
2073
+ (_e = this.dataTextureManager) == null ? void 0 : _e.dispose();
2074
+ (_f = this.eventEmitter) == null ? void 0 : _f.dispose();
1756
2075
  }
1757
2076
  initialEngineState(params) {
1758
2077
  return {
1759
2078
  textureSize: params.textureSize,
1760
- originMeshID: "",
1761
- destinationMeshID: "",
1762
- dataTextureTransitionProgress: 0,
2079
+ meshSequence: [],
2080
+ // ADDED
2081
+ overallProgress: 0,
2082
+ // ADDED
1763
2083
  originMatcapID: "",
1764
2084
  destinationMatcapID: "",
1765
2085
  matcapTransitionProgress: 0,
@@ -1780,16 +2100,6 @@ class ParticlesEngine {
1780
2100
  asset: "created"
1781
2101
  };
1782
2102
  }
1783
- handleTransitionProgress({ type, progress }) {
1784
- switch (type) {
1785
- case "data-texture":
1786
- this.setDataTextureTransitionProgress(progress);
1787
- break;
1788
- case "matcap":
1789
- this.setMatcapProgress(progress);
1790
- break;
1791
- }
1792
- }
1793
2103
  handleInteractionPositionUpdated({ position }) {
1794
2104
  this.simulationRendererService.setInteractionPosition(position);
1795
2105
  }