@polarfront-lab/ionian 1.6.0 → 2.0.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/index.d.ts +116 -60
- package/dist/ionian.iife.js +1 -1
- package/dist/ionian.iife.js.map +1 -1
- package/dist/ionian.js +682 -317
- package/dist/ionian.js.map +1 -1
- package/package.json +3 -3
package/dist/ionian.js
CHANGED
|
@@ -206,6 +206,11 @@ const _face = new Triangle();
|
|
|
206
206
|
const _color = new Vector3();
|
|
207
207
|
const _uva = new Vector2(), _uvb = new Vector2(), _uvc = new Vector2();
|
|
208
208
|
class MeshSurfaceSampler {
|
|
209
|
+
/**
|
|
210
|
+
* Constructs a mesh surface sampler.
|
|
211
|
+
*
|
|
212
|
+
* @param {Mesh} mesh - Surface mesh from which to sample.
|
|
213
|
+
*/
|
|
209
214
|
constructor(mesh) {
|
|
210
215
|
this.geometry = mesh.geometry;
|
|
211
216
|
this.randomFunction = Math.random;
|
|
@@ -217,10 +222,27 @@ class MeshSurfaceSampler {
|
|
|
217
222
|
this.weightAttribute = null;
|
|
218
223
|
this.distribution = null;
|
|
219
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Specifies a vertex attribute to be used as a weight when sampling from the surface.
|
|
227
|
+
* Faces with higher weights are more likely to be sampled, and those with weights of
|
|
228
|
+
* zero will not be sampled at all. For vector attributes, only .x is used in sampling.
|
|
229
|
+
*
|
|
230
|
+
* If no weight attribute is selected, sampling is randomly distributed by area.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} name - The attribute name.
|
|
233
|
+
* @return {MeshSurfaceSampler} A reference to this sampler.
|
|
234
|
+
*/
|
|
220
235
|
setWeightAttribute(name) {
|
|
221
236
|
this.weightAttribute = name ? this.geometry.getAttribute(name) : null;
|
|
222
237
|
return this;
|
|
223
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Processes the input geometry and prepares to return samples. Any configuration of the
|
|
241
|
+
* geometry or sampler must occur before this method is called. Time complexity is O(n)
|
|
242
|
+
* for a surface with n faces.
|
|
243
|
+
*
|
|
244
|
+
* @return {MeshSurfaceSampler} A reference to this sampler.
|
|
245
|
+
*/
|
|
224
246
|
build() {
|
|
225
247
|
const indexAttribute = this.indexAttribute;
|
|
226
248
|
const positionAttribute = this.positionAttribute;
|
|
@@ -255,19 +277,37 @@ class MeshSurfaceSampler {
|
|
|
255
277
|
this.distribution = distribution;
|
|
256
278
|
return this;
|
|
257
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Allows to set a custom random number generator. Default is `Math.random()`.
|
|
282
|
+
*
|
|
283
|
+
* @param {Function} randomFunction - A random number generator.
|
|
284
|
+
* @return {MeshSurfaceSampler} A reference to this sampler.
|
|
285
|
+
*/
|
|
258
286
|
setRandomGenerator(randomFunction) {
|
|
259
287
|
this.randomFunction = randomFunction;
|
|
260
288
|
return this;
|
|
261
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Selects a random point on the surface of the input geometry, returning the
|
|
292
|
+
* position and optionally the normal vector, color and UV Coordinate at that point.
|
|
293
|
+
* Time complexity is O(log n) for a surface with n faces.
|
|
294
|
+
*
|
|
295
|
+
* @param {Vector3} targetPosition - The target object holding the sampled position.
|
|
296
|
+
* @param {Vector3} targetNormal - The target object holding the sampled normal.
|
|
297
|
+
* @param {Color} targetColor - The target object holding the sampled color.
|
|
298
|
+
* @param {Vector2} targetUV - The target object holding the sampled uv coordinates.
|
|
299
|
+
* @return {MeshSurfaceSampler} A reference to this sampler.
|
|
300
|
+
*/
|
|
262
301
|
sample(targetPosition, targetNormal, targetColor, targetUV) {
|
|
263
|
-
const faceIndex = this.
|
|
264
|
-
return this.
|
|
302
|
+
const faceIndex = this._sampleFaceIndex();
|
|
303
|
+
return this._sampleFace(faceIndex, targetPosition, targetNormal, targetColor, targetUV);
|
|
265
304
|
}
|
|
266
|
-
|
|
305
|
+
// private
|
|
306
|
+
_sampleFaceIndex() {
|
|
267
307
|
const cumulativeTotal = this.distribution[this.distribution.length - 1];
|
|
268
|
-
return this.
|
|
308
|
+
return this._binarySearch(this.randomFunction() * cumulativeTotal);
|
|
269
309
|
}
|
|
270
|
-
|
|
310
|
+
_binarySearch(x) {
|
|
271
311
|
const dist = this.distribution;
|
|
272
312
|
let start = 0;
|
|
273
313
|
let end = dist.length - 1;
|
|
@@ -285,7 +325,7 @@ class MeshSurfaceSampler {
|
|
|
285
325
|
}
|
|
286
326
|
return index;
|
|
287
327
|
}
|
|
288
|
-
|
|
328
|
+
_sampleFace(faceIndex, targetPosition, targetNormal, targetColor, targetUV) {
|
|
289
329
|
let u = this.randomFunction();
|
|
290
330
|
let v = this.randomFunction();
|
|
291
331
|
if (u + v > 1) {
|
|
@@ -334,6 +374,7 @@ class MeshSurfaceSampler {
|
|
|
334
374
|
}
|
|
335
375
|
}
|
|
336
376
|
class DataTextureService {
|
|
377
|
+
// Cache the current atlas
|
|
337
378
|
/**
|
|
338
379
|
* Creates a new DataTextureManager instance.
|
|
339
380
|
* @param eventEmitter
|
|
@@ -343,6 +384,7 @@ class DataTextureService {
|
|
|
343
384
|
__publicField(this, "textureSize");
|
|
344
385
|
__publicField(this, "dataTextures");
|
|
345
386
|
__publicField(this, "eventEmitter");
|
|
387
|
+
__publicField(this, "currentAtlas", null);
|
|
346
388
|
this.eventEmitter = eventEmitter;
|
|
347
389
|
this.textureSize = textureSize;
|
|
348
390
|
this.dataTextures = /* @__PURE__ */ new Map();
|
|
@@ -353,6 +395,10 @@ class DataTextureService {
|
|
|
353
395
|
this.textureSize = textureSize;
|
|
354
396
|
this.dataTextures.forEach((texture) => texture.dispose());
|
|
355
397
|
this.dataTextures.clear();
|
|
398
|
+
if (this.currentAtlas) {
|
|
399
|
+
this.currentAtlas.dispose();
|
|
400
|
+
this.currentAtlas = null;
|
|
401
|
+
}
|
|
356
402
|
}
|
|
357
403
|
/**
|
|
358
404
|
* Prepares a mesh for sampling.
|
|
@@ -360,23 +406,76 @@ class DataTextureService {
|
|
|
360
406
|
* @param asset The asset to prepare.
|
|
361
407
|
*/
|
|
362
408
|
async getDataTexture(asset) {
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
365
|
-
return
|
|
409
|
+
const cachedTexture = this.dataTextures.get(asset.uuid);
|
|
410
|
+
if (cachedTexture) {
|
|
411
|
+
return cachedTexture;
|
|
366
412
|
}
|
|
367
413
|
const meshData = parseMeshData(asset);
|
|
368
414
|
const array = sampleMesh(meshData, this.textureSize);
|
|
369
415
|
const dataTexture = createDataTexture(array, this.textureSize);
|
|
370
416
|
dataTexture.name = asset.name;
|
|
417
|
+
this.dataTextures.set(asset.uuid, dataTexture);
|
|
371
418
|
return dataTexture;
|
|
372
419
|
}
|
|
373
420
|
async dispose() {
|
|
421
|
+
this.dataTextures.forEach((texture) => texture.dispose());
|
|
374
422
|
this.dataTextures.clear();
|
|
423
|
+
if (this.currentAtlas) {
|
|
424
|
+
this.currentAtlas.dispose();
|
|
425
|
+
this.currentAtlas = null;
|
|
426
|
+
}
|
|
375
427
|
this.updateServiceState("disposed");
|
|
376
428
|
}
|
|
377
429
|
updateServiceState(serviceState) {
|
|
378
430
|
this.eventEmitter.emit("serviceStateUpdated", { type: "data-texture", state: serviceState });
|
|
379
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Creates a Texture Atlas containing position data for a sequence of meshes.
|
|
434
|
+
* @param meshes An array of THREE.Mesh objects in the desired sequence.
|
|
435
|
+
* @param singleTextureSize The desired resolution (width/height) for each mesh's data within the atlas.
|
|
436
|
+
* @returns A Promise resolving to the generated DataTexture atlas.
|
|
437
|
+
*/
|
|
438
|
+
async createSequenceDataTextureAtlas(meshes, singleTextureSize) {
|
|
439
|
+
this.updateServiceState("loading");
|
|
440
|
+
if (this.currentAtlas) {
|
|
441
|
+
this.currentAtlas.dispose();
|
|
442
|
+
this.currentAtlas = null;
|
|
443
|
+
}
|
|
444
|
+
const numMeshes = meshes.length;
|
|
445
|
+
if (numMeshes === 0) {
|
|
446
|
+
throw new Error("Mesh array cannot be empty.");
|
|
447
|
+
}
|
|
448
|
+
const atlasWidth = singleTextureSize * numMeshes;
|
|
449
|
+
const atlasHeight = singleTextureSize;
|
|
450
|
+
const atlasData = new Float32Array(atlasWidth * atlasHeight * 4);
|
|
451
|
+
try {
|
|
452
|
+
for (let i = 0; i < numMeshes; i++) {
|
|
453
|
+
const mesh = meshes[i];
|
|
454
|
+
const meshDataTexture = await this.getDataTexture(mesh);
|
|
455
|
+
const meshTextureData = meshDataTexture.image.data;
|
|
456
|
+
for (let y = 0; y < singleTextureSize; y++) {
|
|
457
|
+
for (let x = 0; x < singleTextureSize; x++) {
|
|
458
|
+
const sourceIndex = (y * singleTextureSize + x) * 4;
|
|
459
|
+
const targetX = x + i * singleTextureSize;
|
|
460
|
+
const targetIndex = (y * atlasWidth + targetX) * 4;
|
|
461
|
+
atlasData[targetIndex] = meshTextureData[sourceIndex];
|
|
462
|
+
atlasData[targetIndex + 1] = meshTextureData[sourceIndex + 1];
|
|
463
|
+
atlasData[targetIndex + 2] = meshTextureData[sourceIndex + 2];
|
|
464
|
+
atlasData[targetIndex + 3] = meshTextureData[sourceIndex + 3];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const atlasTexture = new THREE.DataTexture(atlasData, atlasWidth, atlasHeight, THREE.RGBAFormat, THREE.FloatType);
|
|
469
|
+
atlasTexture.needsUpdate = true;
|
|
470
|
+
atlasTexture.name = `atlas-${meshes.map((m) => m.name).join("-")}`;
|
|
471
|
+
this.currentAtlas = atlasTexture;
|
|
472
|
+
this.updateServiceState("ready");
|
|
473
|
+
return atlasTexture;
|
|
474
|
+
} catch (error) {
|
|
475
|
+
this.updateServiceState("error");
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
380
479
|
}
|
|
381
480
|
function parseMeshData(mesh) {
|
|
382
481
|
var _a;
|
|
@@ -722,32 +821,35 @@ class IntersectionService {
|
|
|
722
821
|
* Creates a new IntersectionService instance.
|
|
723
822
|
* @param eventEmitter The event emitter used for emitting events.
|
|
724
823
|
* @param camera The camera used for raycasting.
|
|
725
|
-
* @param originGeometry The origin geometry.
|
|
726
|
-
* @param destinationGeometry The destination geometry.
|
|
727
824
|
*/
|
|
728
|
-
constructor(eventEmitter, camera
|
|
825
|
+
constructor(eventEmitter, camera) {
|
|
729
826
|
__publicField(this, "active", true);
|
|
730
827
|
__publicField(this, "raycaster", new THREE.Raycaster());
|
|
731
828
|
__publicField(this, "mousePosition", new THREE.Vector2());
|
|
732
829
|
__publicField(this, "camera");
|
|
733
|
-
__publicField(this, "
|
|
734
|
-
|
|
735
|
-
__publicField(this, "
|
|
830
|
+
__publicField(this, "meshSequenceGeometries", []);
|
|
831
|
+
// ADDED: Store cloned geometries
|
|
832
|
+
__publicField(this, "meshSequenceUUIDs", []);
|
|
833
|
+
// ADDED: Track UUIDs to avoid redundant cloning
|
|
834
|
+
__publicField(this, "overallProgress", 0);
|
|
835
|
+
// ADDED: Store overall progress (0-1)
|
|
736
836
|
__publicField(this, "intersectionMesh", new THREE.Mesh());
|
|
837
|
+
// Use a single mesh for intersection target
|
|
737
838
|
__publicField(this, "geometryNeedsUpdate");
|
|
738
839
|
__publicField(this, "eventEmitter");
|
|
739
840
|
__publicField(this, "blendedGeometry");
|
|
841
|
+
// Keep for the final blended result
|
|
740
842
|
__publicField(this, "intersection");
|
|
741
|
-
__publicField(this, "lastKnownOriginMeshID");
|
|
742
|
-
__publicField(this, "lastKnownDestinationMeshID");
|
|
743
843
|
this.camera = camera;
|
|
744
|
-
this.originGeometry = originGeometry;
|
|
745
844
|
this.eventEmitter = eventEmitter;
|
|
746
|
-
this.destinationGeometry = destinationGeometry;
|
|
747
845
|
this.geometryNeedsUpdate = true;
|
|
748
846
|
}
|
|
749
847
|
setActive(active) {
|
|
750
848
|
this.active = active;
|
|
849
|
+
if (!active) {
|
|
850
|
+
this.intersection = void 0;
|
|
851
|
+
this.eventEmitter.emit("interactionPositionUpdated", { position: { x: 0, y: 0, z: 0, w: 0 } });
|
|
852
|
+
}
|
|
751
853
|
}
|
|
752
854
|
getIntersectionMesh() {
|
|
753
855
|
return this.intersectionMesh;
|
|
@@ -760,36 +862,40 @@ class IntersectionService {
|
|
|
760
862
|
this.camera = camera;
|
|
761
863
|
}
|
|
762
864
|
/**
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
|
|
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
|
|
865
|
+
* Sets the sequence of meshes used for intersection calculations.
|
|
866
|
+
* Clones the geometries to avoid modifying originals.
|
|
867
|
+
* @param meshes An array of THREE.Mesh objects in sequence.
|
|
777
868
|
*/
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
this.
|
|
782
|
-
|
|
783
|
-
|
|
869
|
+
setMeshSequence(meshes) {
|
|
870
|
+
this.meshSequenceGeometries.forEach((geom) => geom.dispose());
|
|
871
|
+
this.meshSequenceGeometries = [];
|
|
872
|
+
this.meshSequenceUUIDs = [];
|
|
873
|
+
if (!meshes || meshes.length === 0) {
|
|
874
|
+
this.geometryNeedsUpdate = true;
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
meshes.forEach((mesh) => {
|
|
878
|
+
if (mesh && mesh.geometry) {
|
|
879
|
+
const clonedGeometry = mesh.geometry.clone();
|
|
880
|
+
clonedGeometry.applyMatrix4(mesh.matrixWorld);
|
|
881
|
+
this.meshSequenceGeometries.push(clonedGeometry);
|
|
882
|
+
this.meshSequenceUUIDs.push(mesh.uuid);
|
|
883
|
+
} else {
|
|
884
|
+
console.warn("Invalid mesh provided to IntersectionService sequence.");
|
|
885
|
+
}
|
|
886
|
+
});
|
|
784
887
|
this.geometryNeedsUpdate = true;
|
|
785
888
|
}
|
|
786
889
|
/**
|
|
787
|
-
* Set the progress
|
|
788
|
-
* @param progress
|
|
890
|
+
* Set the overall progress through the mesh sequence.
|
|
891
|
+
* @param progress Value between 0.0 (first mesh) and 1.0 (last mesh).
|
|
789
892
|
*/
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
this.
|
|
893
|
+
setOverallProgress(progress) {
|
|
894
|
+
const newProgress = THREE.MathUtils.clamp(progress, 0, 1);
|
|
895
|
+
if (this.overallProgress !== newProgress) {
|
|
896
|
+
this.overallProgress = newProgress;
|
|
897
|
+
this.geometryNeedsUpdate = true;
|
|
898
|
+
}
|
|
793
899
|
}
|
|
794
900
|
/**
|
|
795
901
|
* Set the mouse position.
|
|
@@ -803,22 +909,46 @@ class IntersectionService {
|
|
|
803
909
|
* @returns The intersection point or undefined if no intersection was found.
|
|
804
910
|
*/
|
|
805
911
|
calculate(instancedMesh) {
|
|
806
|
-
|
|
807
|
-
this.
|
|
808
|
-
|
|
912
|
+
var _a, _b, _c;
|
|
913
|
+
if (!this.active || !this.camera || this.meshSequenceGeometries.length === 0) {
|
|
914
|
+
if (this.intersection) {
|
|
915
|
+
this.intersection = void 0;
|
|
916
|
+
this.eventEmitter.emit("interactionPositionUpdated", { position: { x: 0, y: 0, z: 0, w: 0 } });
|
|
917
|
+
}
|
|
918
|
+
return void 0;
|
|
919
|
+
}
|
|
809
920
|
if (this.geometryNeedsUpdate) {
|
|
810
|
-
this.
|
|
921
|
+
if (this.blendedGeometry && this.blendedGeometry !== this.intersectionMesh.geometry) {
|
|
922
|
+
this.blendedGeometry.dispose();
|
|
923
|
+
}
|
|
811
924
|
this.blendedGeometry = this.getBlendedGeometry();
|
|
925
|
+
this.geometryNeedsUpdate = false;
|
|
926
|
+
if (this.blendedGeometry) {
|
|
927
|
+
if (this.intersectionMesh.geometry !== this.blendedGeometry) {
|
|
928
|
+
if (this.intersectionMesh.geometry) this.intersectionMesh.geometry.dispose();
|
|
929
|
+
this.intersectionMesh.geometry = this.blendedGeometry;
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
if (this.intersectionMesh.geometry) this.intersectionMesh.geometry.dispose();
|
|
933
|
+
this.intersectionMesh.geometry = new THREE.BufferGeometry();
|
|
934
|
+
}
|
|
812
935
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
936
|
+
this.intersectionMesh.matrixWorld.copy(instancedMesh.matrixWorld);
|
|
937
|
+
let newIntersection = void 0;
|
|
938
|
+
if (this.blendedGeometry && this.blendedGeometry.attributes.position) {
|
|
939
|
+
newIntersection = this.getFirstIntersection(this.camera, this.intersectionMesh);
|
|
817
940
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
this.
|
|
941
|
+
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;
|
|
942
|
+
if (hasChanged) {
|
|
943
|
+
this.intersection = newIntersection;
|
|
944
|
+
if (this.intersection) {
|
|
945
|
+
const worldPoint = new THREE.Vector3(this.intersection.x, this.intersection.y, this.intersection.z);
|
|
946
|
+
const localPoint = instancedMesh.worldToLocal(worldPoint.clone());
|
|
947
|
+
this.intersection.set(localPoint.x, localPoint.y, localPoint.z, 1);
|
|
948
|
+
this.eventEmitter.emit("interactionPositionUpdated", { position: this.intersection });
|
|
949
|
+
} else {
|
|
950
|
+
this.eventEmitter.emit("interactionPositionUpdated", { position: { x: 0, y: 0, z: 0, w: 0 } });
|
|
951
|
+
}
|
|
822
952
|
}
|
|
823
953
|
return this.intersection;
|
|
824
954
|
}
|
|
@@ -827,8 +957,13 @@ class IntersectionService {
|
|
|
827
957
|
*/
|
|
828
958
|
dispose() {
|
|
829
959
|
var _a;
|
|
830
|
-
|
|
831
|
-
this.
|
|
960
|
+
this.meshSequenceGeometries.forEach((geom) => geom.dispose());
|
|
961
|
+
this.meshSequenceGeometries = [];
|
|
962
|
+
this.meshSequenceUUIDs = [];
|
|
963
|
+
if (this.blendedGeometry && this.blendedGeometry !== this.intersectionMesh.geometry) {
|
|
964
|
+
this.blendedGeometry.dispose();
|
|
965
|
+
}
|
|
966
|
+
(_a = this.intersectionMesh.geometry) == null ? void 0 : _a.dispose();
|
|
832
967
|
}
|
|
833
968
|
updateIntersectionMesh(instancedMesh) {
|
|
834
969
|
if (this.blendedGeometry) {
|
|
@@ -842,29 +977,50 @@ class IntersectionService {
|
|
|
842
977
|
this.intersectionMesh.matrixAutoUpdate = false;
|
|
843
978
|
this.intersectionMesh.updateMatrixWorld(true);
|
|
844
979
|
}
|
|
845
|
-
getFirstIntersection(camera,
|
|
980
|
+
getFirstIntersection(camera, targetMesh) {
|
|
846
981
|
this.raycaster.setFromCamera(this.mousePosition, camera);
|
|
847
|
-
const
|
|
848
|
-
if (
|
|
849
|
-
const worldPoint =
|
|
850
|
-
|
|
851
|
-
return new THREE.Vector4(localPoint.x, localPoint.y, localPoint.z, 1);
|
|
982
|
+
const intersects = this.raycaster.intersectObject(targetMesh, false);
|
|
983
|
+
if (intersects.length > 0 && intersects[0].point) {
|
|
984
|
+
const worldPoint = intersects[0].point;
|
|
985
|
+
return new THREE.Vector4(worldPoint.x, worldPoint.y, worldPoint.z, 1);
|
|
852
986
|
}
|
|
987
|
+
return void 0;
|
|
853
988
|
}
|
|
854
989
|
getBlendedGeometry() {
|
|
855
|
-
|
|
856
|
-
|
|
990
|
+
const numGeometries = this.meshSequenceGeometries.length;
|
|
991
|
+
if (numGeometries === 0) {
|
|
992
|
+
return void 0;
|
|
857
993
|
}
|
|
858
|
-
if (
|
|
859
|
-
return this.
|
|
994
|
+
if (numGeometries === 1) {
|
|
995
|
+
return this.meshSequenceGeometries[0];
|
|
860
996
|
}
|
|
861
|
-
|
|
862
|
-
|
|
997
|
+
const totalSegments = numGeometries - 1;
|
|
998
|
+
const progressPerSegment = 1 / totalSegments;
|
|
999
|
+
const scaledProgress = this.overallProgress * totalSegments;
|
|
1000
|
+
let indexA = Math.floor(scaledProgress);
|
|
1001
|
+
let indexB = indexA + 1;
|
|
1002
|
+
indexA = THREE.MathUtils.clamp(indexA, 0, totalSegments);
|
|
1003
|
+
indexB = THREE.MathUtils.clamp(indexB, 0, totalSegments);
|
|
1004
|
+
let localProgress = 0;
|
|
1005
|
+
if (progressPerSegment > 0) {
|
|
1006
|
+
localProgress = scaledProgress - indexA;
|
|
863
1007
|
}
|
|
864
|
-
if (this.
|
|
865
|
-
|
|
1008
|
+
if (this.overallProgress >= 1) {
|
|
1009
|
+
indexA = totalSegments;
|
|
1010
|
+
indexB = totalSegments;
|
|
1011
|
+
localProgress = 1;
|
|
866
1012
|
}
|
|
867
|
-
|
|
1013
|
+
localProgress = THREE.MathUtils.clamp(localProgress, 0, 1);
|
|
1014
|
+
const geomA = this.meshSequenceGeometries[indexA];
|
|
1015
|
+
const geomB = this.meshSequenceGeometries[indexB];
|
|
1016
|
+
if (!geomA || !geomB) {
|
|
1017
|
+
console.error("IntersectionService: Invalid geometries found for blending at indices", indexA, indexB);
|
|
1018
|
+
return this.meshSequenceGeometries[0];
|
|
1019
|
+
}
|
|
1020
|
+
if (indexA === indexB) {
|
|
1021
|
+
return geomA;
|
|
1022
|
+
}
|
|
1023
|
+
return this.blendGeometry(geomA, geomB, localProgress);
|
|
868
1024
|
}
|
|
869
1025
|
blendGeometry(from, to, progress) {
|
|
870
1026
|
const blended = new THREE.BufferGeometry();
|
|
@@ -896,15 +1052,34 @@ class FullscreenTriangleGeometry extends BufferGeometry {
|
|
|
896
1052
|
}
|
|
897
1053
|
const _geometry = new FullscreenTriangleGeometry();
|
|
898
1054
|
class FullScreenQuad {
|
|
1055
|
+
/**
|
|
1056
|
+
* Constructs a new full screen quad.
|
|
1057
|
+
*
|
|
1058
|
+
* @param {?Material} material - The material to render te full screen quad with.
|
|
1059
|
+
*/
|
|
899
1060
|
constructor(material) {
|
|
900
1061
|
this._mesh = new Mesh(_geometry, material);
|
|
901
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Frees the GPU-related resources allocated by this instance. Call this
|
|
1065
|
+
* method whenever the instance is no longer used in your app.
|
|
1066
|
+
*/
|
|
902
1067
|
dispose() {
|
|
903
1068
|
this._mesh.geometry.dispose();
|
|
904
1069
|
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Renders the full screen quad.
|
|
1072
|
+
*
|
|
1073
|
+
* @param {WebGLRenderer} renderer - The renderer.
|
|
1074
|
+
*/
|
|
905
1075
|
render(renderer) {
|
|
906
1076
|
renderer.render(this._mesh, _camera);
|
|
907
1077
|
}
|
|
1078
|
+
/**
|
|
1079
|
+
* The quad's material.
|
|
1080
|
+
*
|
|
1081
|
+
* @type {?Material}
|
|
1082
|
+
*/
|
|
908
1083
|
get material() {
|
|
909
1084
|
return this._mesh.material;
|
|
910
1085
|
}
|
|
@@ -914,9 +1089,11 @@ class FullScreenQuad {
|
|
|
914
1089
|
}
|
|
915
1090
|
class GPUComputationRenderer {
|
|
916
1091
|
/**
|
|
917
|
-
*
|
|
918
|
-
|
|
919
|
-
|
|
1092
|
+
* Constructs a new GPU computation renderer.
|
|
1093
|
+
*
|
|
1094
|
+
* @param {number} sizeX - Computation problem size is always 2d: sizeX * sizeY elements.
|
|
1095
|
+
* @param {number} sizeY - Computation problem size is always 2d: sizeX * sizeY elements.
|
|
1096
|
+
* @param {WebGLRenderer} renderer - The renderer.
|
|
920
1097
|
*/
|
|
921
1098
|
constructor(sizeX, sizeY, renderer) {
|
|
922
1099
|
this.variables = [];
|
|
@@ -1087,179 +1264,283 @@ class GPUComputationRenderer {
|
|
|
1087
1264
|
}
|
|
1088
1265
|
}
|
|
1089
1266
|
}
|
|
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
1267
|
const positionShader = `
|
|
1104
|
-
uniform float uProgress;
|
|
1105
1268
|
uniform vec4 uInteractionPosition;
|
|
1106
1269
|
uniform float uTime;
|
|
1107
1270
|
uniform float uTractionForce;
|
|
1271
|
+
uniform sampler2D uPositionAtlas;
|
|
1272
|
+
uniform float uOverallProgress; // (0.0 to 1.0)
|
|
1273
|
+
uniform int uNumMeshes;
|
|
1274
|
+
uniform float uSingleTextureSize;
|
|
1108
1275
|
|
|
1109
1276
|
float rand(vec2 co) {
|
|
1110
1277
|
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
|
|
1111
1278
|
}
|
|
1112
1279
|
|
|
1280
|
+
// Helper function to get position from atlas
|
|
1281
|
+
vec3 getAtlasPosition(vec2 uv, int meshIndex) {
|
|
1282
|
+
float atlasWidth = uSingleTextureSize * float(uNumMeshes);
|
|
1283
|
+
float atlasHeight = uSingleTextureSize; // Assuming height is single texture size
|
|
1284
|
+
|
|
1285
|
+
// Calculate UV within the specific mesh's section of the atlas
|
|
1286
|
+
float segmentWidthRatio = uSingleTextureSize / atlasWidth;
|
|
1287
|
+
vec2 atlasUV = vec2(
|
|
1288
|
+
uv.x * segmentWidthRatio + segmentWidthRatio * float(meshIndex),
|
|
1289
|
+
uv.y // Assuming vertical layout doesn't change y
|
|
1290
|
+
);
|
|
1291
|
+
|
|
1292
|
+
return texture2D(uPositionAtlas, atlasUV).xyz;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1113
1295
|
void main() {
|
|
1296
|
+
// GPGPU UV calculation
|
|
1297
|
+
vec2 uv = gl_FragCoord.xy / resolution.xy; // resolution is the size of the *output* texture (e.g., 256x256)
|
|
1114
1298
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
float offset = rand(uv);
|
|
1299
|
+
vec3 currentPosition = texture2D(uCurrentPosition, uv).xyz;
|
|
1300
|
+
vec3 currentVelocity = texture2D(uCurrentVelocity, uv).xyz;
|
|
1118
1301
|
|
|
1119
|
-
|
|
1120
|
-
vec3
|
|
1121
|
-
|
|
1302
|
+
// --- Calculate Target Position from Atlas ---
|
|
1303
|
+
vec3 targetPosition;
|
|
1304
|
+
if (uNumMeshes <= 1) {
|
|
1305
|
+
targetPosition = getAtlasPosition(uv, 0);
|
|
1306
|
+
} else {
|
|
1307
|
+
float totalSegments = float(uNumMeshes - 1);
|
|
1308
|
+
float progressPerSegment = 1.0 / totalSegments;
|
|
1309
|
+
float scaledProgress = uOverallProgress * totalSegments;
|
|
1122
1310
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1311
|
+
int indexA = int(floor(scaledProgress));
|
|
1312
|
+
// Clamp indexB to avoid going out of bounds
|
|
1313
|
+
int indexB = min(indexA + 1, uNumMeshes - 1);
|
|
1126
1314
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1315
|
+
// Ensure indexA is also within bounds (important if uOverallProgress is exactly 1.0)
|
|
1316
|
+
indexA = min(indexA, uNumMeshes - 1);
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
float localProgress = fract(scaledProgress);
|
|
1320
|
+
|
|
1321
|
+
// Handle edge case where progress is exactly 1.0
|
|
1322
|
+
if (uOverallProgress == 1.0) {
|
|
1323
|
+
indexA = uNumMeshes - 1;
|
|
1324
|
+
indexB = uNumMeshes - 1;
|
|
1325
|
+
localProgress = 1.0; // or 0.0 depending on how you want to handle it
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
vec3 positionA = getAtlasPosition(uv, indexA);
|
|
1330
|
+
vec3 positionB = getAtlasPosition(uv, indexB);
|
|
1331
|
+
|
|
1332
|
+
targetPosition = mix(positionA, positionB, localProgress);
|
|
1333
|
+
}
|
|
1334
|
+
// --- End Target Position Calculation ---
|
|
1335
|
+
|
|
1336
|
+
// Particle attraction to target position
|
|
1337
|
+
vec3 direction = normalize(targetPosition - currentPosition);
|
|
1338
|
+
float dist = length(targetPosition - currentPosition);
|
|
1339
|
+
|
|
1340
|
+
vec3 finalPosition = currentPosition;
|
|
1341
|
+
|
|
1342
|
+
// Apply attraction force (simplified mix)
|
|
1343
|
+
if (dist > 0.01) { // Only apply if significantly far
|
|
1344
|
+
finalPosition = mix(currentPosition, targetPosition, 0.1 * uTractionForce);
|
|
1129
1345
|
}
|
|
1130
1346
|
|
|
1131
|
-
|
|
1132
|
-
gl_FragColor = vec4(
|
|
1347
|
+
finalPosition += currentVelocity;
|
|
1348
|
+
gl_FragColor = vec4(finalPosition, 1.0);
|
|
1133
1349
|
}
|
|
1134
1350
|
`;
|
|
1135
1351
|
const velocityShader = `
|
|
1136
|
-
uniform float uProgress;
|
|
1137
1352
|
uniform vec4 uInteractionPosition;
|
|
1138
1353
|
uniform float uTime;
|
|
1139
1354
|
uniform float uTractionForce;
|
|
1140
1355
|
uniform float uMaxRepelDistance;
|
|
1356
|
+
uniform sampler2D uPositionAtlas;
|
|
1357
|
+
uniform float uOverallProgress;
|
|
1358
|
+
uniform int uNumMeshes;
|
|
1359
|
+
uniform float uSingleTextureSize;
|
|
1141
1360
|
|
|
1142
1361
|
float rand(vec2 co) {
|
|
1143
1362
|
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
|
|
1144
1363
|
}
|
|
1145
1364
|
|
|
1365
|
+
// Helper function (same as in position shader)
|
|
1366
|
+
vec3 getAtlasPosition(vec2 uv, int meshIndex) {
|
|
1367
|
+
float atlasWidth = uSingleTextureSize * float(uNumMeshes);
|
|
1368
|
+
float atlasHeight = uSingleTextureSize;
|
|
1369
|
+
float segmentWidthRatio = uSingleTextureSize / atlasWidth;
|
|
1370
|
+
vec2 atlasUV = vec2(uv.x * segmentWidthRatio + segmentWidthRatio * float(meshIndex), uv.y);
|
|
1371
|
+
return texture2D(uPositionAtlas, atlasUV).xyz;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1146
1374
|
void main() {
|
|
1147
|
-
|
|
1375
|
+
vec2 uv = gl_FragCoord.xy / resolution.xy;
|
|
1148
1376
|
float offset = rand(uv);
|
|
1149
1377
|
|
|
1150
|
-
vec3
|
|
1151
|
-
vec3
|
|
1152
|
-
|
|
1378
|
+
vec3 currentPosition = texture2D(uCurrentPosition, uv).xyz;
|
|
1379
|
+
vec3 currentVelocity = texture2D(uCurrentVelocity, uv).xyz;
|
|
1380
|
+
|
|
1381
|
+
// --- Calculate Target Position from Atlas (same logic as position shader) ---
|
|
1382
|
+
vec3 targetPosition;
|
|
1383
|
+
if (uNumMeshes <= 1) {
|
|
1384
|
+
targetPosition = getAtlasPosition(uv, 0);
|
|
1385
|
+
} else {
|
|
1386
|
+
float totalSegments = float(uNumMeshes - 1);
|
|
1387
|
+
float progressPerSegment = 1.0 / totalSegments;
|
|
1388
|
+
float scaledProgress = uOverallProgress * totalSegments;
|
|
1389
|
+
int indexA = int(floor(scaledProgress));
|
|
1390
|
+
int indexB = min(indexA + 1, uNumMeshes - 1);
|
|
1391
|
+
indexA = min(indexA, uNumMeshes - 1);
|
|
1392
|
+
float localProgress = fract(scaledProgress);
|
|
1393
|
+
if (uOverallProgress == 1.0) {
|
|
1394
|
+
indexA = uNumMeshes - 1;
|
|
1395
|
+
indexB = uNumMeshes - 1;
|
|
1396
|
+
localProgress = 1.0;
|
|
1397
|
+
}
|
|
1398
|
+
vec3 positionA = getAtlasPosition(uv, indexA);
|
|
1399
|
+
vec3 positionB = getAtlasPosition(uv, indexB);
|
|
1400
|
+
targetPosition = mix(positionA, positionB, localProgress);
|
|
1401
|
+
}
|
|
1402
|
+
// --- End Target Position Calculation ---
|
|
1153
1403
|
|
|
1154
|
-
|
|
1404
|
+
vec3 finalVelocity = currentVelocity * 0.9; // Dampening
|
|
1155
1405
|
|
|
1156
|
-
//
|
|
1157
|
-
vec3 direction = normalize(
|
|
1158
|
-
float dist = length
|
|
1406
|
+
// Particle traction force towards target (influences velocity)
|
|
1407
|
+
vec3 direction = normalize(targetPosition - currentPosition);
|
|
1408
|
+
float dist = length(targetPosition - currentPosition);
|
|
1159
1409
|
if (dist > 0.01) {
|
|
1160
|
-
|
|
1410
|
+
// Add force proportional to distance and traction setting
|
|
1411
|
+
finalVelocity += direction * dist * 0.01 * uTractionForce; // Adjust multiplier as needed
|
|
1161
1412
|
}
|
|
1162
1413
|
|
|
1163
|
-
//
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1414
|
+
// Mouse repel force
|
|
1415
|
+
if (uInteractionPosition.w > 0.0) { // Check if interaction is active (w component)
|
|
1416
|
+
float pointerDistance = distance(currentPosition, uInteractionPosition.xyz);
|
|
1417
|
+
if (pointerDistance < uMaxRepelDistance) {
|
|
1418
|
+
float mouseRepelModifier = smoothstep(uMaxRepelDistance, 0.0, pointerDistance); // Smoother falloff
|
|
1419
|
+
vec3 repelDirection = normalize(currentPosition - uInteractionPosition.xyz);
|
|
1420
|
+
// Apply force based on proximity and interaction strength (w)
|
|
1421
|
+
finalVelocity += repelDirection * mouseRepelModifier * uInteractionPosition.w * 0.01; // Adjust multiplier
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1170
1424
|
|
|
1425
|
+
// Optional: Reset position if particle "dies" and respawns (lifespan logic)
|
|
1171
1426
|
float lifespan = 20.0;
|
|
1172
|
-
float age = mod(uTime + lifespan * offset, lifespan);
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
position
|
|
1427
|
+
float age = mod(uTime * 0.1 + lifespan * offset, lifespan); // Adjust time scale
|
|
1428
|
+
if (age < 0.05) { // Small window for reset
|
|
1429
|
+
finalVelocity = vec3(0.0); // Reset velocity on respawn
|
|
1430
|
+
// Note: Resetting position directly here might cause jumps.
|
|
1431
|
+
// It's often better handled in the position shader or by ensuring
|
|
1432
|
+
// strong attraction force when dist is large.
|
|
1176
1433
|
}
|
|
1177
1434
|
|
|
1178
|
-
|
|
1435
|
+
|
|
1436
|
+
gl_FragColor = vec4(finalVelocity, 1.0);
|
|
1179
1437
|
}
|
|
1180
1438
|
`;
|
|
1181
1439
|
class SimulationRenderer {
|
|
1182
1440
|
/**
|
|
1183
1441
|
* Creates a new SimulationRenderer instance.
|
|
1184
|
-
* @param size The size of the simulation textures.
|
|
1442
|
+
* @param size The size of the simulation textures (width/height).
|
|
1185
1443
|
* @param webGLRenderer The WebGL renderer.
|
|
1186
|
-
* @param initialPosition The initial position data texture
|
|
1444
|
+
* @param initialPosition The initial position data texture (optional, defaults to sphere).
|
|
1187
1445
|
*/
|
|
1188
1446
|
constructor(size, webGLRenderer, initialPosition) {
|
|
1189
1447
|
__publicField(this, "gpuComputationRenderer");
|
|
1190
1448
|
__publicField(this, "webGLRenderer");
|
|
1191
|
-
//
|
|
1192
|
-
__publicField(this, "positionDataTexture");
|
|
1193
|
-
__publicField(this, "velocityDataTexture");
|
|
1194
|
-
// GPUComputationRenderer variables
|
|
1195
|
-
__publicField(this, "mixPositionsVar");
|
|
1449
|
+
// GPGPU Variables
|
|
1196
1450
|
__publicField(this, "velocityVar");
|
|
1197
1451
|
__publicField(this, "positionVar");
|
|
1452
|
+
// Input Data Textures (References)
|
|
1453
|
+
__publicField(this, "initialPositionDataTexture");
|
|
1454
|
+
// Used only for first init
|
|
1455
|
+
__publicField(this, "initialVelocityDataTexture");
|
|
1456
|
+
// Blank texture for init
|
|
1457
|
+
__publicField(this, "positionAtlasTexture", null);
|
|
1458
|
+
// Holds the current mesh sequence atlas
|
|
1459
|
+
// Uniforms
|
|
1198
1460
|
__publicField(this, "interactionPosition");
|
|
1461
|
+
// Cache last known output textures
|
|
1199
1462
|
__publicField(this, "lastKnownPositionDataTexture");
|
|
1200
1463
|
__publicField(this, "lastKnownVelocityDataTexture");
|
|
1201
|
-
__publicField(this, "lastKnownMixProgress");
|
|
1202
|
-
__publicField(this, "initialDataTexture");
|
|
1203
|
-
this.initialDataTexture = initialPosition ?? createSpherePoints(size);
|
|
1204
|
-
this.positionDataTexture = this.initialDataTexture;
|
|
1205
1464
|
this.webGLRenderer = webGLRenderer;
|
|
1206
1465
|
this.gpuComputationRenderer = new GPUComputationRenderer(size, size, this.webGLRenderer);
|
|
1207
|
-
|
|
1208
|
-
|
|
1466
|
+
if (!webGLRenderer.capabilities.isWebGL2 && webGLRenderer.extensions.get("OES_texture_float")) {
|
|
1467
|
+
this.gpuComputationRenderer.setDataType(THREE.FloatType);
|
|
1468
|
+
} else if (!webGLRenderer.capabilities.isWebGL2) {
|
|
1209
1469
|
this.gpuComputationRenderer.setDataType(THREE.HalfFloatType);
|
|
1210
1470
|
}
|
|
1211
|
-
this.
|
|
1471
|
+
this.initialPositionDataTexture = initialPosition ?? createSpherePoints(size);
|
|
1472
|
+
this.initialVelocityDataTexture = createBlankDataTexture(size);
|
|
1212
1473
|
this.interactionPosition = new THREE.Vector4(0, 0, 0, 0);
|
|
1213
|
-
this.
|
|
1214
|
-
this.
|
|
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 };
|
|
1474
|
+
this.velocityVar = this.gpuComputationRenderer.addVariable("uCurrentVelocity", velocityShader, this.initialVelocityDataTexture);
|
|
1475
|
+
this.positionVar = this.gpuComputationRenderer.addVariable("uCurrentPosition", positionShader, this.initialPositionDataTexture);
|
|
1219
1476
|
this.velocityVar.material.uniforms.uTime = { value: 0 };
|
|
1220
1477
|
this.velocityVar.material.uniforms.uInteractionPosition = { value: this.interactionPosition };
|
|
1221
|
-
this.velocityVar.material.uniforms.uCurrentPosition = { value:
|
|
1478
|
+
this.velocityVar.material.uniforms.uCurrentPosition = { value: null };
|
|
1222
1479
|
this.velocityVar.material.uniforms.uTractionForce = { value: 0.1 };
|
|
1223
1480
|
this.velocityVar.material.uniforms.uMaxRepelDistance = { value: 0.3 };
|
|
1481
|
+
this.velocityVar.material.uniforms.uPositionAtlas = { value: null };
|
|
1482
|
+
this.velocityVar.material.uniforms.uOverallProgress = { value: 0 };
|
|
1483
|
+
this.velocityVar.material.uniforms.uNumMeshes = { value: 1 };
|
|
1484
|
+
this.velocityVar.material.uniforms.uSingleTextureSize = { value: size };
|
|
1224
1485
|
this.positionVar.material.uniforms.uTime = { value: 0 };
|
|
1225
|
-
this.positionVar.material.uniforms.uProgress = { value: 0 };
|
|
1226
1486
|
this.positionVar.material.uniforms.uTractionForce = { value: 0.1 };
|
|
1227
1487
|
this.positionVar.material.uniforms.uInteractionPosition = { value: this.interactionPosition };
|
|
1228
|
-
this.positionVar.material.uniforms.uCurrentPosition = { value:
|
|
1229
|
-
this.
|
|
1230
|
-
this.
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1488
|
+
this.positionVar.material.uniforms.uCurrentPosition = { value: null };
|
|
1489
|
+
this.positionVar.material.uniforms.uCurrentVelocity = { value: null };
|
|
1490
|
+
this.positionVar.material.uniforms.uPositionAtlas = { value: null };
|
|
1491
|
+
this.positionVar.material.uniforms.uOverallProgress = { value: 0 };
|
|
1492
|
+
this.positionVar.material.uniforms.uNumMeshes = { value: 1 };
|
|
1493
|
+
this.positionVar.material.uniforms.uSingleTextureSize = { value: size };
|
|
1494
|
+
this.gpuComputationRenderer.setVariableDependencies(this.positionVar, [this.positionVar, this.velocityVar]);
|
|
1495
|
+
this.gpuComputationRenderer.setVariableDependencies(this.velocityVar, [this.velocityVar, this.positionVar]);
|
|
1496
|
+
const initError = this.gpuComputationRenderer.init();
|
|
1497
|
+
if (initError !== null) {
|
|
1498
|
+
throw new Error("Failed to initialize SimulationRenderer: " + initError);
|
|
1234
1499
|
}
|
|
1235
|
-
this.
|
|
1236
|
-
this.
|
|
1500
|
+
this.positionVar.material.uniforms.uPositionAtlas.value = this.initialPositionDataTexture;
|
|
1501
|
+
this.positionVar.material.uniforms.uNumMeshes.value = 1;
|
|
1502
|
+
this.velocityVar.material.uniforms.uNumMeshes.value = 1;
|
|
1503
|
+
this.positionVar.material.uniforms.uSingleTextureSize.value = size;
|
|
1504
|
+
this.velocityVar.material.uniforms.uSingleTextureSize.value = size;
|
|
1505
|
+
this.positionVar.material.uniforms.uCurrentVelocity.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
|
|
1506
|
+
this.velocityVar.material.uniforms.uCurrentPosition.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
|
|
1507
|
+
this.lastKnownVelocityDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
|
|
1508
|
+
this.lastKnownPositionDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
|
|
1237
1509
|
}
|
|
1238
1510
|
/**
|
|
1239
|
-
* Sets the
|
|
1240
|
-
* @param
|
|
1511
|
+
* Sets the mesh sequence position atlas texture and related uniforms.
|
|
1512
|
+
* @param entry Information about the atlas texture.
|
|
1241
1513
|
*/
|
|
1242
|
-
|
|
1243
|
-
|
|
1514
|
+
setPositionAtlas(entry) {
|
|
1515
|
+
const expectedAtlasWidth = entry.singleTextureSize * entry.numMeshes;
|
|
1516
|
+
if (entry.dataTexture.image.width !== expectedAtlasWidth || entry.dataTexture.image.height !== entry.singleTextureSize) {
|
|
1517
|
+
console.error(
|
|
1518
|
+
`SimulationRenderer: Atlas texture dimension mismatch! Expected ${expectedAtlasWidth}x${entry.singleTextureSize}, Got ${entry.dataTexture.image.width}x${entry.dataTexture.image.height}`
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
this.positionAtlasTexture = entry.dataTexture;
|
|
1522
|
+
const numMeshes = entry.numMeshes > 0 ? entry.numMeshes : 1;
|
|
1523
|
+
this.positionVar.material.uniforms.uPositionAtlas.value = this.positionAtlasTexture;
|
|
1524
|
+
this.positionVar.material.uniforms.uNumMeshes.value = numMeshes;
|
|
1525
|
+
this.positionVar.material.uniforms.uSingleTextureSize.value = entry.singleTextureSize;
|
|
1526
|
+
this.velocityVar.material.uniforms.uPositionAtlas.value = this.positionAtlasTexture;
|
|
1527
|
+
this.velocityVar.material.uniforms.uNumMeshes.value = numMeshes;
|
|
1528
|
+
this.velocityVar.material.uniforms.uSingleTextureSize.value = entry.singleTextureSize;
|
|
1529
|
+
this.positionVar.material.uniforms.uCurrentVelocity.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
|
|
1530
|
+
this.velocityVar.material.uniforms.uCurrentPosition.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
|
|
1244
1531
|
}
|
|
1245
1532
|
/**
|
|
1246
|
-
* Sets the
|
|
1247
|
-
* @param
|
|
1533
|
+
* Sets the overall progress for blending between meshes in the atlas.
|
|
1534
|
+
* @param progress Value between 0.0 and 1.0.
|
|
1248
1535
|
*/
|
|
1249
|
-
|
|
1250
|
-
|
|
1536
|
+
setOverallProgress(progress) {
|
|
1537
|
+
const clampedProgress = clamp(progress, 0, 1);
|
|
1538
|
+
this.positionVar.material.uniforms.uOverallProgress.value = clampedProgress;
|
|
1539
|
+
this.velocityVar.material.uniforms.uOverallProgress.value = clampedProgress;
|
|
1251
1540
|
}
|
|
1252
1541
|
setMaxRepelDistance(distance) {
|
|
1253
1542
|
this.velocityVar.material.uniforms.uMaxRepelDistance.value = distance;
|
|
1254
1543
|
}
|
|
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
1544
|
setVelocityTractionForce(force) {
|
|
1264
1545
|
this.velocityVar.material.uniforms.uTractionForce.value = force;
|
|
1265
1546
|
}
|
|
@@ -1273,36 +1554,31 @@ class SimulationRenderer {
|
|
|
1273
1554
|
* Disposes the resources used by the simulation renderer.
|
|
1274
1555
|
*/
|
|
1275
1556
|
dispose() {
|
|
1276
|
-
|
|
1277
|
-
this.positionVar.renderTargets.forEach((rtt) => rtt.dispose());
|
|
1278
|
-
this.velocityVar.renderTargets.forEach((rtt) => rtt.dispose());
|
|
1279
|
-
this.positionDataTexture.dispose();
|
|
1280
|
-
this.velocityDataTexture.dispose();
|
|
1557
|
+
var _a, _b;
|
|
1281
1558
|
this.gpuComputationRenderer.dispose();
|
|
1559
|
+
(_a = this.initialPositionDataTexture) == null ? void 0 : _a.dispose();
|
|
1560
|
+
(_b = this.initialVelocityDataTexture) == null ? void 0 : _b.dispose();
|
|
1561
|
+
this.positionAtlasTexture = null;
|
|
1282
1562
|
}
|
|
1283
1563
|
/**
|
|
1284
1564
|
* Computes the next step of the simulation.
|
|
1285
|
-
* @param
|
|
1565
|
+
* @param deltaTime The time elapsed since the last frame, in seconds.
|
|
1286
1566
|
*/
|
|
1287
|
-
compute(
|
|
1288
|
-
this.velocityVar.material.uniforms.uTime.value
|
|
1289
|
-
this.positionVar.material.uniforms.uTime.value
|
|
1567
|
+
compute(deltaTime) {
|
|
1568
|
+
this.velocityVar.material.uniforms.uTime.value += deltaTime;
|
|
1569
|
+
this.positionVar.material.uniforms.uTime.value += deltaTime;
|
|
1570
|
+
this.positionVar.material.uniforms.uCurrentVelocity.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
|
|
1571
|
+
this.velocityVar.material.uniforms.uCurrentPosition.value = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
|
|
1290
1572
|
this.gpuComputationRenderer.compute();
|
|
1573
|
+
this.lastKnownVelocityDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
|
|
1574
|
+
this.lastKnownPositionDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
|
|
1291
1575
|
}
|
|
1292
|
-
/**
|
|
1293
|
-
* Gets the current velocity texture.
|
|
1294
|
-
* @returns The current velocity texture.
|
|
1295
|
-
*/
|
|
1576
|
+
/** Gets the current velocity texture (output from the last compute step). */
|
|
1296
1577
|
getVelocityTexture() {
|
|
1297
|
-
this.lastKnownVelocityDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.velocityVar).texture;
|
|
1298
1578
|
return this.lastKnownVelocityDataTexture;
|
|
1299
1579
|
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Gets the current position texture.
|
|
1302
|
-
* @returns The current position texture.
|
|
1303
|
-
*/
|
|
1580
|
+
/** Gets the current position texture (output from the last compute step). */
|
|
1304
1581
|
getPositionTexture() {
|
|
1305
|
-
this.lastKnownPositionDataTexture = this.gpuComputationRenderer.getCurrentRenderTarget(this.positionVar).texture;
|
|
1306
1582
|
return this.lastKnownPositionDataTexture;
|
|
1307
1583
|
}
|
|
1308
1584
|
}
|
|
@@ -1310,18 +1586,22 @@ class SimulationRendererService {
|
|
|
1310
1586
|
constructor(eventEmitter, size, webGLRenderer) {
|
|
1311
1587
|
__publicField(this, "state");
|
|
1312
1588
|
__publicField(this, "textureSize");
|
|
1313
|
-
__publicField(this, "
|
|
1589
|
+
__publicField(this, "overallProgress");
|
|
1590
|
+
// ADDED: Store overall progress
|
|
1314
1591
|
__publicField(this, "velocityTractionForce");
|
|
1315
1592
|
__publicField(this, "positionalTractionForce");
|
|
1316
1593
|
__publicField(this, "simulationRenderer");
|
|
1317
1594
|
__publicField(this, "webGLRenderer");
|
|
1318
1595
|
__publicField(this, "eventEmitter");
|
|
1596
|
+
// Store atlas info
|
|
1597
|
+
__publicField(this, "currentAtlasEntry", null);
|
|
1598
|
+
// ADDED
|
|
1319
1599
|
__publicField(this, "lastKnownVelocityDataTexture");
|
|
1320
1600
|
__publicField(this, "lastKnownPositionDataTexture");
|
|
1321
1601
|
this.eventEmitter = eventEmitter;
|
|
1322
1602
|
this.webGLRenderer = webGLRenderer;
|
|
1323
1603
|
this.textureSize = size;
|
|
1324
|
-
this.
|
|
1604
|
+
this.overallProgress = 0;
|
|
1325
1605
|
this.velocityTractionForce = 0.1;
|
|
1326
1606
|
this.positionalTractionForce = 0.1;
|
|
1327
1607
|
this.updateServiceState("initializing");
|
|
@@ -1330,6 +1610,27 @@ class SimulationRendererService {
|
|
|
1330
1610
|
this.lastKnownPositionDataTexture = this.simulationRenderer.getPositionTexture();
|
|
1331
1611
|
this.updateServiceState("ready");
|
|
1332
1612
|
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Sets the position data texture atlas for the simulation.
|
|
1615
|
+
* @param entry An object containing the atlas texture and related parameters.
|
|
1616
|
+
*/
|
|
1617
|
+
setPositionAtlas(entry) {
|
|
1618
|
+
const expectedWidth = entry.singleTextureSize * entry.numMeshes;
|
|
1619
|
+
if (entry.dataTexture.image.width !== expectedWidth) {
|
|
1620
|
+
this.eventEmitter.emit("invalidRequest", { message: `Atlas texture width mismatch.` });
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
this.currentAtlasEntry = entry;
|
|
1624
|
+
this.simulationRenderer.setPositionAtlas(entry);
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Sets the overall progress for the mesh sequence transition.
|
|
1628
|
+
* @param progress The progress value (0.0 to 1.0).
|
|
1629
|
+
*/
|
|
1630
|
+
setOverallProgress(progress) {
|
|
1631
|
+
this.overallProgress = progress;
|
|
1632
|
+
this.simulationRenderer.setOverallProgress(this.overallProgress);
|
|
1633
|
+
}
|
|
1333
1634
|
setTextureSize(size) {
|
|
1334
1635
|
this.updateServiceState("initializing");
|
|
1335
1636
|
this.simulationRenderer.dispose();
|
|
@@ -1337,24 +1638,6 @@ class SimulationRendererService {
|
|
|
1337
1638
|
this.simulationRenderer = new SimulationRenderer(size, this.webGLRenderer);
|
|
1338
1639
|
this.updateServiceState("ready");
|
|
1339
1640
|
}
|
|
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
1641
|
setVelocityTractionForce(force) {
|
|
1359
1642
|
this.velocityTractionForce = force;
|
|
1360
1643
|
this.simulationRenderer.setVelocityTractionForce(this.velocityTractionForce);
|
|
@@ -1364,21 +1647,21 @@ class SimulationRendererService {
|
|
|
1364
1647
|
this.simulationRenderer.setPositionalTractionForce(this.positionalTractionForce);
|
|
1365
1648
|
}
|
|
1366
1649
|
compute(elapsedTime) {
|
|
1650
|
+
if (this.state !== "ready") return;
|
|
1367
1651
|
this.simulationRenderer.compute(elapsedTime);
|
|
1652
|
+
this.lastKnownVelocityDataTexture = this.simulationRenderer.getVelocityTexture();
|
|
1653
|
+
this.lastKnownPositionDataTexture = this.simulationRenderer.getPositionTexture();
|
|
1368
1654
|
}
|
|
1369
1655
|
getVelocityTexture() {
|
|
1370
|
-
if (this.state === "ready") this.lastKnownVelocityDataTexture = this.simulationRenderer.getVelocityTexture();
|
|
1371
1656
|
return this.lastKnownVelocityDataTexture;
|
|
1372
1657
|
}
|
|
1373
1658
|
getPositionTexture() {
|
|
1374
|
-
if (this.state === "ready") this.lastKnownPositionDataTexture = this.simulationRenderer.getPositionTexture();
|
|
1375
1659
|
return this.lastKnownPositionDataTexture;
|
|
1376
1660
|
}
|
|
1377
1661
|
dispose() {
|
|
1378
1662
|
this.updateServiceState("disposed");
|
|
1379
1663
|
this.simulationRenderer.dispose();
|
|
1380
|
-
this.
|
|
1381
|
-
this.lastKnownPositionDataTexture.dispose();
|
|
1664
|
+
this.currentAtlasEntry = null;
|
|
1382
1665
|
}
|
|
1383
1666
|
updateServiceState(serviceState) {
|
|
1384
1667
|
this.state = serviceState;
|
|
@@ -1495,7 +1778,6 @@ class ParticlesEngine {
|
|
|
1495
1778
|
*/
|
|
1496
1779
|
constructor(params) {
|
|
1497
1780
|
__publicField(this, "simulationRendererService");
|
|
1498
|
-
__publicField(this, "eventEmitter");
|
|
1499
1781
|
__publicField(this, "renderer");
|
|
1500
1782
|
__publicField(this, "scene");
|
|
1501
1783
|
__publicField(this, "serviceStates");
|
|
@@ -1506,6 +1788,9 @@ class ParticlesEngine {
|
|
|
1506
1788
|
__publicField(this, "transitionService");
|
|
1507
1789
|
__publicField(this, "engineState");
|
|
1508
1790
|
__publicField(this, "intersectionService");
|
|
1791
|
+
__publicField(this, "meshSequenceAtlasTexture", null);
|
|
1792
|
+
// ADDED: To store the generated atlas
|
|
1793
|
+
__publicField(this, "eventEmitter");
|
|
1509
1794
|
const { scene, renderer, camera, textureSize, useIntersection = true } = params;
|
|
1510
1795
|
this.eventEmitter = new DefaultEventEmitter();
|
|
1511
1796
|
this.serviceStates = this.getInitialServiceStates();
|
|
@@ -1522,7 +1807,6 @@ class ParticlesEngine {
|
|
|
1522
1807
|
this.scene.add(this.instancedMeshManager.getMesh());
|
|
1523
1808
|
this.intersectionService = new IntersectionService(this.eventEmitter, camera);
|
|
1524
1809
|
if (!useIntersection) this.intersectionService.setActive(false);
|
|
1525
|
-
this.eventEmitter.on("transitionProgressed", this.handleTransitionProgress.bind(this));
|
|
1526
1810
|
this.eventEmitter.on("interactionPositionUpdated", this.handleInteractionPositionUpdated.bind(this));
|
|
1527
1811
|
}
|
|
1528
1812
|
/**
|
|
@@ -1530,48 +1814,14 @@ class ParticlesEngine {
|
|
|
1530
1814
|
* @param elapsedTime The elapsed time since the last frame.
|
|
1531
1815
|
*/
|
|
1532
1816
|
render(elapsedTime) {
|
|
1817
|
+
const dt = elapsedTime / 1e3;
|
|
1818
|
+
this.transitionService.compute(dt);
|
|
1533
1819
|
this.intersectionService.calculate(this.instancedMeshManager.getMesh());
|
|
1534
|
-
this.
|
|
1535
|
-
this.
|
|
1536
|
-
this.instancedMeshManager.update(elapsedTime);
|
|
1820
|
+
this.simulationRendererService.compute(dt);
|
|
1821
|
+
this.instancedMeshManager.update(dt);
|
|
1537
1822
|
this.instancedMeshManager.updateVelocityTexture(this.simulationRendererService.getVelocityTexture());
|
|
1538
1823
|
this.instancedMeshManager.updatePositionTexture(this.simulationRendererService.getPositionTexture());
|
|
1539
1824
|
}
|
|
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
1825
|
setOriginMatcap(matcapID, override = false) {
|
|
1576
1826
|
if (override) this.eventEmitter.emit("transitionCancelled", { type: "matcap" });
|
|
1577
1827
|
this.engineState.originMatcapID = matcapID;
|
|
@@ -1607,40 +1857,31 @@ class ParticlesEngine {
|
|
|
1607
1857
|
}
|
|
1608
1858
|
}
|
|
1609
1859
|
setMatcapProgress(progress, override = false) {
|
|
1860
|
+
const clampedProgress = clamp(progress, 0, 1);
|
|
1610
1861
|
if (override) this.eventEmitter.emit("transitionCancelled", { type: "matcap" });
|
|
1611
|
-
this.engineState.matcapTransitionProgress =
|
|
1612
|
-
this.instancedMeshManager.setProgress(
|
|
1862
|
+
this.engineState.matcapTransitionProgress = clampedProgress;
|
|
1863
|
+
this.instancedMeshManager.setProgress(clampedProgress);
|
|
1613
1864
|
}
|
|
1865
|
+
// --- Update setTextureSize ---
|
|
1614
1866
|
async setTextureSize(size) {
|
|
1867
|
+
if (this.engineState.textureSize === size) {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1615
1870
|
this.engineState.textureSize = size;
|
|
1616
1871
|
this.dataTextureManager.setTextureSize(size);
|
|
1617
1872
|
this.simulationRendererService.setTextureSize(size);
|
|
1618
1873
|
this.instancedMeshManager.resize(size);
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
this.eventEmitter.emit("invalidRequest", { message: `Mesh with id "${this.engineState.originMeshID}" does not exist` });
|
|
1622
|
-
return;
|
|
1874
|
+
if (this.engineState.meshSequence.length > 0) {
|
|
1875
|
+
await this.setMeshSequence(this.engineState.meshSequence);
|
|
1623
1876
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
this.eventEmitter.emit("invalidRequest", { message: `Mesh with id "${this.engineState.destinationMeshID}" does not exist` });
|
|
1627
|
-
return;
|
|
1877
|
+
if (this.engineState.meshSequence.length > 0) {
|
|
1878
|
+
await this.setMeshSequence(this.engineState.meshSequence);
|
|
1628
1879
|
}
|
|
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
1880
|
this.simulationRendererService.setVelocityTractionForce(this.engineState.velocityTractionForce);
|
|
1643
1881
|
this.simulationRendererService.setPositionalTractionForce(this.engineState.positionalTractionForce);
|
|
1882
|
+
this.simulationRendererService.setMaxRepelDistance(this.engineState.maxRepelDistance);
|
|
1883
|
+
this.simulationRendererService.setOverallProgress(this.engineState.overallProgress);
|
|
1884
|
+
this.intersectionService.setOverallProgress(this.engineState.overallProgress);
|
|
1644
1885
|
this.instancedMeshManager.setOriginMatcap(this.assetService.getMatcap(this.engineState.originMatcapID));
|
|
1645
1886
|
this.instancedMeshManager.setDestinationMatcap(this.assetService.getMatcap(this.engineState.destinationMatcapID));
|
|
1646
1887
|
this.instancedMeshManager.setProgress(this.engineState.matcapTransitionProgress);
|
|
@@ -1663,7 +1904,7 @@ class ParticlesEngine {
|
|
|
1663
1904
|
this.engineState.useIntersect = use;
|
|
1664
1905
|
if (!use) {
|
|
1665
1906
|
this.engineState.pointerPosition = { x: -99999999, y: -99999999 };
|
|
1666
|
-
this.
|
|
1907
|
+
this.simulationRendererService.setInteractionPosition({ x: 0, y: 0, z: 0, w: 0 });
|
|
1667
1908
|
}
|
|
1668
1909
|
}
|
|
1669
1910
|
setPointerPosition(position) {
|
|
@@ -1687,50 +1928,170 @@ class ParticlesEngine {
|
|
|
1687
1928
|
this.engineState.maxRepelDistance = distance;
|
|
1688
1929
|
this.simulationRendererService.setMaxRepelDistance(distance);
|
|
1689
1930
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1931
|
+
/**
|
|
1932
|
+
* Sets the sequence of meshes for particle transitions.
|
|
1933
|
+
* This will generate a texture atlas containing position data for all meshes.
|
|
1934
|
+
* @param meshIDs An array of registered mesh IDs in the desired sequence order.
|
|
1935
|
+
*/
|
|
1936
|
+
async setMeshSequence(meshIDs) {
|
|
1937
|
+
if (!meshIDs || meshIDs.length < 1) {
|
|
1938
|
+
this.eventEmitter.emit("invalidRequest", { message: "Mesh sequence must contain at least one mesh ID." });
|
|
1939
|
+
this.engineState.meshSequence = [];
|
|
1940
|
+
this.intersectionService.setMeshSequence([]);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
this.engineState.meshSequence = meshIDs;
|
|
1944
|
+
this.engineState.overallProgress = 0;
|
|
1945
|
+
const meshes = meshIDs.map((id) => this.assetService.getMesh(id)).filter((mesh) => mesh !== null);
|
|
1946
|
+
if (meshes.length !== meshIDs.length) {
|
|
1947
|
+
const missing = meshIDs.filter((id) => !this.assetService.getMesh(id));
|
|
1948
|
+
console.warn(`Could not find meshes for IDs: ${missing.join(", ")}. Proceeding with ${meshes.length} found meshes.`);
|
|
1949
|
+
this.eventEmitter.emit("invalidRequest", { message: `Could not find meshes for IDs: ${missing.join(", ")}` });
|
|
1950
|
+
if (meshes.length < 1) {
|
|
1951
|
+
this.engineState.meshSequence = [];
|
|
1952
|
+
this.intersectionService.setMeshSequence([]);
|
|
1953
|
+
return;
|
|
1700
1954
|
}
|
|
1701
|
-
|
|
1955
|
+
this.engineState.meshSequence = meshes.map((m) => m.name);
|
|
1956
|
+
}
|
|
1957
|
+
try {
|
|
1958
|
+
this.meshSequenceAtlasTexture = await this.dataTextureManager.createSequenceDataTextureAtlas(meshes, this.engineState.textureSize);
|
|
1959
|
+
this.simulationRendererService.setPositionAtlas({
|
|
1960
|
+
dataTexture: this.meshSequenceAtlasTexture,
|
|
1961
|
+
textureSize: this.engineState.textureSize,
|
|
1962
|
+
// Pass the size of the *output* GPGPU texture
|
|
1963
|
+
numMeshes: this.engineState.meshSequence.length,
|
|
1964
|
+
// Use the potentially updated count
|
|
1965
|
+
singleTextureSize: this.engineState.textureSize
|
|
1966
|
+
// Size of one mesh's data within atlas
|
|
1967
|
+
});
|
|
1968
|
+
this.simulationRendererService.setOverallProgress(this.engineState.overallProgress);
|
|
1969
|
+
this.intersectionService.setMeshSequence(meshes);
|
|
1970
|
+
this.intersectionService.setOverallProgress(this.engineState.overallProgress);
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
console.error("Failed during mesh sequence setup:", error);
|
|
1973
|
+
this.meshSequenceAtlasTexture = null;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Sets the overall progress through the mesh sequence.
|
|
1978
|
+
* @param progress A value between 0.0 (first mesh) and 1.0 (last mesh).
|
|
1979
|
+
* @param override If true, cancels any ongoing mesh sequence transition before setting the value. Defaults to true.
|
|
1980
|
+
*/
|
|
1981
|
+
setOverallProgress(progress, override = true) {
|
|
1982
|
+
if (override) {
|
|
1983
|
+
this.eventEmitter.emit("transitionCancelled", { type: "mesh-sequence" });
|
|
1984
|
+
}
|
|
1985
|
+
const clampedProgress = clamp(progress, 0, 1);
|
|
1986
|
+
this.engineState.overallProgress = clampedProgress;
|
|
1987
|
+
this.simulationRendererService.setOverallProgress(clampedProgress);
|
|
1988
|
+
this.intersectionService.setOverallProgress(clampedProgress);
|
|
1702
1989
|
}
|
|
1703
|
-
|
|
1990
|
+
// --- Transition scheduling methods remain the same ---
|
|
1991
|
+
scheduleMatcapTransition(originMatcapID, destinationMatcapID, easing = linear, duration = 1e3, override = false, options = {}) {
|
|
1992
|
+
if (override) this.eventEmitter.emit("transitionCancelled", { type: "matcap" });
|
|
1993
|
+
const handleProgressUpdate = (transitionProgress) => {
|
|
1994
|
+
var _a;
|
|
1995
|
+
this.setMatcapProgress(transitionProgress, false);
|
|
1996
|
+
(_a = options.onTransitionProgress) == null ? void 0 : _a.call(options, transitionProgress);
|
|
1997
|
+
};
|
|
1704
1998
|
this.transitionService.enqueue(
|
|
1705
1999
|
"matcap",
|
|
1706
2000
|
{ easing, duration },
|
|
1707
2001
|
{
|
|
2002
|
+
...options,
|
|
2003
|
+
onTransitionProgress: handleProgressUpdate,
|
|
1708
2004
|
onTransitionBegin: () => {
|
|
1709
|
-
|
|
1710
|
-
this.
|
|
1711
|
-
this.
|
|
1712
|
-
|
|
2005
|
+
var _a;
|
|
2006
|
+
this.setOriginMatcap(originMatcapID, false);
|
|
2007
|
+
this.setDestinationMatcap(destinationMatcapID, false);
|
|
2008
|
+
this.setMatcapProgress(0, false);
|
|
2009
|
+
(_a = options.onTransitionBegin) == null ? void 0 : _a.call(options);
|
|
2010
|
+
},
|
|
2011
|
+
onTransitionFinished: () => {
|
|
2012
|
+
var _a;
|
|
2013
|
+
this.setMatcapProgress(1, false);
|
|
2014
|
+
(_a = options.onTransitionFinished) == null ? void 0 : _a.call(options);
|
|
2015
|
+
},
|
|
2016
|
+
onTransitionCancelled: options.onTransitionCancelled
|
|
1713
2017
|
}
|
|
1714
2018
|
);
|
|
1715
2019
|
}
|
|
1716
|
-
scheduleTextureTransition(origin, destination, options) {
|
|
2020
|
+
scheduleTextureTransition(origin, destination, options = {}) {
|
|
1717
2021
|
const easing = (options == null ? void 0 : options.easing) ?? linear;
|
|
1718
2022
|
const duration = (options == null ? void 0 : options.duration) ?? 1e3;
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
2023
|
+
const userCallbacks = {
|
|
2024
|
+
// Extract user callbacks
|
|
2025
|
+
onTransitionBegin: options == null ? void 0 : options.onTransitionBegin,
|
|
2026
|
+
onTransitionProgress: options == null ? void 0 : options.onTransitionProgress,
|
|
2027
|
+
onTransitionFinished: options == null ? void 0 : options.onTransitionFinished,
|
|
2028
|
+
onTransitionCancelled: options == null ? void 0 : options.onTransitionCancelled
|
|
2029
|
+
};
|
|
2030
|
+
if (options == null ? void 0 : options.override) this.eventEmitter.emit("transitionCancelled", { type: "matcap" });
|
|
2031
|
+
const handleProgressUpdate = (transitionProgress) => {
|
|
2032
|
+
var _a;
|
|
2033
|
+
this.setMatcapProgress(transitionProgress, false);
|
|
2034
|
+
(_a = userCallbacks.onTransitionProgress) == null ? void 0 : _a.call(userCallbacks, transitionProgress);
|
|
2035
|
+
};
|
|
1722
2036
|
this.transitionService.enqueue(
|
|
1723
2037
|
"matcap",
|
|
2038
|
+
// Still uses matcap type internally
|
|
1724
2039
|
{ easing, duration },
|
|
1725
2040
|
{
|
|
2041
|
+
...userCallbacks,
|
|
2042
|
+
// Pass user callbacks
|
|
2043
|
+
onTransitionProgress: handleProgressUpdate,
|
|
1726
2044
|
onTransitionBegin: () => {
|
|
2045
|
+
var _a;
|
|
1727
2046
|
this.setOriginTexture(origin);
|
|
1728
2047
|
this.setDestinationTexture(destination);
|
|
1729
2048
|
this.setMatcapProgress(0);
|
|
2049
|
+
(_a = userCallbacks.onTransitionBegin) == null ? void 0 : _a.call(userCallbacks);
|
|
2050
|
+
},
|
|
2051
|
+
onTransitionFinished: () => {
|
|
2052
|
+
var _a;
|
|
2053
|
+
this.setMatcapProgress(1);
|
|
2054
|
+
(_a = userCallbacks.onTransitionFinished) == null ? void 0 : _a.call(userCallbacks);
|
|
2055
|
+
},
|
|
2056
|
+
onTransitionCancelled: () => {
|
|
2057
|
+
var _a;
|
|
2058
|
+
(_a = userCallbacks.onTransitionCancelled) == null ? void 0 : _a.call(userCallbacks);
|
|
1730
2059
|
}
|
|
1731
2060
|
}
|
|
1732
2061
|
);
|
|
1733
2062
|
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Schedules a smooth transition for the overall mesh sequence progress.
|
|
2065
|
+
* @param targetProgress The final progress value (0.0 to 1.0) to transition to.
|
|
2066
|
+
* @param duration Duration of the transition in milliseconds.
|
|
2067
|
+
* @param easing Easing function to use.
|
|
2068
|
+
* @param options Transition options (onBegin, onProgress, onFinished, onCancelled).
|
|
2069
|
+
* @param override If true, cancels any ongoing mesh sequence transitions.
|
|
2070
|
+
*/
|
|
2071
|
+
scheduleMeshSequenceTransition(targetProgress, duration = 1e3, easing = linear, options = {}, override = true) {
|
|
2072
|
+
if (override) this.eventEmitter.emit("transitionCancelled", { type: "mesh-sequence" });
|
|
2073
|
+
const startProgress = this.engineState.overallProgress;
|
|
2074
|
+
const progressDiff = targetProgress - startProgress;
|
|
2075
|
+
const handleProgressUpdate = (transitionProgress) => {
|
|
2076
|
+
var _a;
|
|
2077
|
+
const currentOverallProgress = startProgress + progressDiff * transitionProgress;
|
|
2078
|
+
this.setOverallProgress(currentOverallProgress, false);
|
|
2079
|
+
(_a = options.onTransitionProgress) == null ? void 0 : _a.call(options, currentOverallProgress);
|
|
2080
|
+
};
|
|
2081
|
+
const transitionDetail = { duration, easing };
|
|
2082
|
+
const transitionOptions = {
|
|
2083
|
+
...options,
|
|
2084
|
+
onTransitionProgress: handleProgressUpdate,
|
|
2085
|
+
onTransitionBegin: options.onTransitionBegin,
|
|
2086
|
+
onTransitionFinished: () => {
|
|
2087
|
+
var _a;
|
|
2088
|
+
this.setOverallProgress(targetProgress, false);
|
|
2089
|
+
(_a = options.onTransitionFinished) == null ? void 0 : _a.call(options);
|
|
2090
|
+
},
|
|
2091
|
+
onTransitionCancelled: options.onTransitionCancelled
|
|
2092
|
+
};
|
|
2093
|
+
this.transitionService.enqueue("mesh-sequence", transitionDetail, transitionOptions);
|
|
2094
|
+
}
|
|
1734
2095
|
handleServiceStateUpdated({ type, state }) {
|
|
1735
2096
|
this.serviceStates[type] = state;
|
|
1736
2097
|
}
|
|
@@ -1749,23 +2110,37 @@ class ParticlesEngine {
|
|
|
1749
2110
|
getTextures() {
|
|
1750
2111
|
return this.assetService.getTextures();
|
|
1751
2112
|
}
|
|
2113
|
+
getTextureSize() {
|
|
2114
|
+
return this.engineState.textureSize;
|
|
2115
|
+
}
|
|
2116
|
+
getUseIntersect() {
|
|
2117
|
+
return this.engineState.useIntersect;
|
|
2118
|
+
}
|
|
2119
|
+
getEngineStateSnapshot() {
|
|
2120
|
+
return { ...this.engineState };
|
|
2121
|
+
}
|
|
1752
2122
|
/**
|
|
1753
2123
|
* Disposes the resources used by the engine.
|
|
1754
2124
|
*/
|
|
1755
2125
|
dispose() {
|
|
1756
|
-
|
|
1757
|
-
this.
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
this.
|
|
1761
|
-
this.
|
|
2126
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2127
|
+
if (this.scene && this.instancedMeshManager) {
|
|
2128
|
+
this.scene.remove(this.instancedMeshManager.getMesh());
|
|
2129
|
+
}
|
|
2130
|
+
(_a = this.simulationRendererService) == null ? void 0 : _a.dispose();
|
|
2131
|
+
(_b = this.instancedMeshManager) == null ? void 0 : _b.dispose();
|
|
2132
|
+
(_c = this.intersectionService) == null ? void 0 : _c.dispose();
|
|
2133
|
+
(_d = this.assetService) == null ? void 0 : _d.dispose();
|
|
2134
|
+
(_e = this.dataTextureManager) == null ? void 0 : _e.dispose();
|
|
2135
|
+
(_f = this.eventEmitter) == null ? void 0 : _f.dispose();
|
|
1762
2136
|
}
|
|
1763
2137
|
initialEngineState(params) {
|
|
1764
2138
|
return {
|
|
1765
2139
|
textureSize: params.textureSize,
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2140
|
+
meshSequence: [],
|
|
2141
|
+
// ADDED
|
|
2142
|
+
overallProgress: 0,
|
|
2143
|
+
// ADDED
|
|
1769
2144
|
originMatcapID: "",
|
|
1770
2145
|
destinationMatcapID: "",
|
|
1771
2146
|
matcapTransitionProgress: 0,
|
|
@@ -1786,16 +2161,6 @@ class ParticlesEngine {
|
|
|
1786
2161
|
asset: "created"
|
|
1787
2162
|
};
|
|
1788
2163
|
}
|
|
1789
|
-
handleTransitionProgress({ type, progress }) {
|
|
1790
|
-
switch (type) {
|
|
1791
|
-
case "data-texture":
|
|
1792
|
-
this.setDataTextureTransitionProgress(progress);
|
|
1793
|
-
break;
|
|
1794
|
-
case "matcap":
|
|
1795
|
-
this.setMatcapProgress(progress);
|
|
1796
|
-
break;
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
2164
|
handleInteractionPositionUpdated({ position }) {
|
|
1800
2165
|
this.simulationRendererService.setInteractionPosition(position);
|
|
1801
2166
|
}
|