@adatechnology/game-vfx-three 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -418,6 +418,11 @@ function createMarkTextures(THREE, createCanvas) {
418
418
  var MAX_CONFORM_STEPS = 48;
419
419
  var MIN_CONFORM_STEPS = 16;
420
420
  var BLOOD_GROW_RATE = 1.2;
421
+ var BLOOD_START_GROWTH = 0.05;
422
+ var DEFAULT_MAX_GROUND_MARKS = 64;
423
+ var DEFAULT_MAX_ROCKS = 512;
424
+ var DEFAULT_MAX_BLOOD = 256;
425
+ var MAX_SPARE_GROUND_GEOMETRIES = 16;
421
426
  function createMarkLayer(THREE, options) {
422
427
  const group = new THREE.Group();
423
428
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -441,92 +446,146 @@ function createMarkLayer(THREE, options) {
441
446
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
442
447
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
443
448
  owned.push(bloodMaterial, bloodGeometry);
444
- const rockMaterials = /* @__PURE__ */ new Map();
445
- const blood = [];
446
- let meshCount = 0;
447
- function rockMaterial(color) {
448
- let material = rockMaterials.get(color);
449
- if (!material) {
450
- material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0, flatShading: true });
451
- rockMaterials.set(color, material);
452
- owned.push(material);
449
+ const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
450
+ const instanced = {};
451
+ function rockMaterial() {
452
+ if (!instanced.rockMaterial) {
453
+ instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
454
+ owned.push(instanced.rockMaterial);
453
455
  }
454
- return material;
456
+ return instanced.rockMaterial;
457
+ }
458
+ function bloodMesh() {
459
+ return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
460
+ }
461
+ function rockMesh() {
462
+ return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
463
+ }
464
+ function plateMesh() {
465
+ return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
466
+ }
467
+ function attach(mesh, castShadow = false) {
468
+ mesh.castShadow = castShadow;
469
+ group.add(mesh);
470
+ return mesh;
471
+ }
472
+ const groundMarks = [];
473
+ const spareGeometries = [];
474
+ const blood = [];
475
+ const placed = { blood: 0, rock: 0, plate: 0 };
476
+ const matrix = new THREE.Matrix4();
477
+ const position = new THREE.Vector3();
478
+ const quaternion = new THREE.Quaternion();
479
+ const euler = new THREE.Euler();
480
+ const scale = new THREE.Vector3();
481
+ const color = new THREE.Color();
482
+ const up = new THREE.Vector3(0, 1, 0);
483
+ function createInstanced(geometry, material, capacity) {
484
+ const mesh = new THREE.InstancedMesh(geometry, material, capacity);
485
+ mesh.count = 0;
486
+ mesh.frustumCulled = false;
487
+ mesh.receiveShadow = true;
488
+ return mesh;
489
+ }
490
+ function takeSpare(vertexCount) {
491
+ const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
492
+ return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
493
+ }
494
+ function evictOldestGroundMark() {
495
+ const oldest = groundMarks.shift();
496
+ group.remove(oldest.mesh);
497
+ spareGeometries.push(oldest.mesh.geometry);
498
+ if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
455
499
  }
456
500
  function conform(mark, material) {
457
501
  const radius = mark.radius;
458
502
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
459
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
460
- const position = geometry.attributes.position;
503
+ if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
504
+ const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
505
+ const grid = geometry.attributes.position;
506
+ const cell = 2 / steps;
461
507
  const cos = Math.cos(mark.rotation);
462
508
  const sin = Math.sin(mark.rotation);
463
- for (let index = 0; index < position.count; index++) {
464
- const localX = position.getX(index) * radius;
465
- const localZ = position.getZ(index) * radius;
509
+ for (let index = 0; index < grid.count; index++) {
510
+ const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
511
+ const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
466
512
  const worldX = mark.x + localX * cos - localZ * sin;
467
513
  const worldZ = mark.z + localX * sin + localZ * cos;
468
- position.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
514
+ grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
469
515
  }
516
+ grid.needsUpdate = true;
470
517
  geometry.computeVertexNormals();
518
+ geometry.attributes.normal.needsUpdate = true;
519
+ geometry.boundingSphere = null;
520
+ geometry.boundingBox = null;
471
521
  const mesh = new THREE.Mesh(geometry, material);
472
522
  mesh.receiveShadow = true;
473
- return mesh;
523
+ group.add(mesh);
524
+ groundMarks.push({ mesh, steps });
474
525
  }
475
526
  function addRock(mark) {
476
- const mesh = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
477
- mesh.position.set(mark.x, mark.y, mark.z);
478
- mesh.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
479
- mesh.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
480
- mesh.castShadow = true;
481
- mesh.receiveShadow = true;
482
- return mesh;
527
+ const mesh = mark.plate ? plateMesh() : rockMesh();
528
+ const slot = mark.plate ? "plate" : "rock";
529
+ const index = placed[slot]++ % mesh.instanceMatrix.count;
530
+ quaternion.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
531
+ mesh.setMatrixAt(index, matrix.compose(position.set(mark.x, mark.y, mark.z), quaternion, scale.set(mark.scale[0], mark.scale[1], mark.scale[2])));
532
+ mesh.setColorAt(index, color.set(mark.color));
533
+ mesh.count = Math.min(placed[slot], mesh.instanceMatrix.count);
534
+ mesh.instanceMatrix.needsUpdate = true;
535
+ mesh.instanceColor.needsUpdate = true;
536
+ }
537
+ function writeBlood(index, drawn, growth) {
538
+ quaternion.setFromAxisAngle(up, drawn.rotation);
539
+ const mesh = bloodMesh();
540
+ mesh.setMatrixAt(index, matrix.compose(position.set(drawn.position[0], drawn.position[1], drawn.position[2]), quaternion, scale.set(drawn.scaleX * growth, 1, drawn.scaleZ * growth)));
541
+ mesh.instanceMatrix.needsUpdate = true;
483
542
  }
484
543
  function addBlood(mark) {
485
- const mesh = new THREE.Mesh(bloodGeometry, bloodMaterial);
486
- mesh.position.set(mark.x, mark.y, mark.z);
487
- mesh.scale.set(mark.scaleX, 1, mark.scaleZ);
488
- mesh.rotation.y = mark.rotation;
489
- mesh.receiveShadow = true;
490
- mesh.scale.multiplyScalar(0.05);
491
- blood.push({ mesh, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
492
- return mesh;
544
+ const index = placed.blood++ % bloodMesh().instanceMatrix.count;
545
+ const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
546
+ blood[index] = drawn;
547
+ writeBlood(index, drawn, BLOOD_START_GROWTH);
548
+ bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
493
549
  }
494
550
  function build(mark) {
495
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
496
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
497
- if (mark.kind === "rock") return addRock(mark);
498
- return addBlood(mark);
551
+ if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
552
+ else if (mark.kind === "dirt") conform(mark, dirtMaterial);
553
+ else if (mark.kind === "rock") addRock(mark);
554
+ else addBlood(mark);
499
555
  }
500
- function clearMeshes() {
501
- for (const child of [...group.children]) {
502
- group.remove(child);
503
- const geometry = child.geometry;
504
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
505
- }
556
+ function clearMarks() {
557
+ for (const drawn of groundMarks) group.remove(drawn.mesh);
558
+ for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
559
+ groundMarks.length = 0;
560
+ for (const geometry of spareGeometries) geometry.dispose();
561
+ spareGeometries.length = 0;
506
562
  blood.length = 0;
507
- meshCount = 0;
563
+ placed.blood = placed.rock = placed.plate = 0;
564
+ for (const mesh of [instanced.blood, instanced.rock, instanced.plate]) if (mesh) mesh.count = 0;
508
565
  }
509
566
  return {
510
567
  group,
511
568
  handle(event) {
512
- if (event.type === "clear") {
513
- clearMeshes();
514
- return;
515
- }
516
- group.add(build(event.mark));
517
- meshCount++;
569
+ if (event.type === "clear") clearMarks();
570
+ else build(event.mark);
571
+ },
572
+ prepare() {
573
+ bloodMesh();
574
+ rockMesh();
575
+ plateMesh();
518
576
  },
519
577
  update(dt) {
520
- for (const drawn of blood) {
521
- if (drawn.age > 1) continue;
578
+ for (let index = 0; index < blood.length; index++) {
579
+ const drawn = blood[index];
580
+ if (!drawn || drawn.age > 1) continue;
522
581
  drawn.age += dt * BLOOD_GROW_RATE;
523
- const ease = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
524
- drawn.mesh.scale.set(drawn.scaleX * Math.max(0.05, ease), 1, drawn.scaleZ * Math.max(0.05, ease));
582
+ writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
525
583
  }
526
584
  },
527
- count: () => meshCount,
585
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh) => mesh && mesh.count > 0).length,
528
586
  dispose() {
529
- clearMeshes();
587
+ clearMarks();
588
+ for (const mesh of [instanced.blood, instanced.rock, instanced.plate]) mesh?.dispose();
530
589
  for (const item of owned) item.dispose();
531
590
  }
532
591
  };
@@ -535,18 +594,22 @@ function createMarkLayer(THREE, options) {
535
594
  // src/mesh-layer.ts
536
595
  var SPHERE_DEFAULT_WIDTH = 8;
537
596
  var SPHERE_DEFAULT_HEIGHT = 6;
597
+ var MAX_PARKED_MESHES_PER_KIND = 256;
538
598
  function geometryOf(THREE, particle) {
539
599
  switch (particle.shape) {
540
600
  case "cone":
541
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
601
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
542
602
  case "cone_open":
543
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
603
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
544
604
  case "sphere":
545
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
605
+ return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
546
606
  case "icosahedron":
547
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
607
+ return new THREE.IcosahedronGeometry(1, particle.segments);
548
608
  }
549
609
  }
610
+ function isCone(particle) {
611
+ return particle.shape === "cone" || particle.shape === "cone_open";
612
+ }
550
613
  function materialOf(THREE, particle) {
551
614
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
552
615
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -557,51 +620,82 @@ function createMeshLayerProbes(THREE) {
557
620
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
558
621
  );
559
622
  }
623
+ function geometryKeyOf(particle) {
624
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
625
+ }
560
626
  function createMeshLayer(THREE, pool) {
561
627
  const group = new THREE.Group();
562
628
  group.name = "workshop-meshes";
563
629
  const drawn = /* @__PURE__ */ new Map();
630
+ const geometries = /* @__PURE__ */ new Map();
631
+ const parked = /* @__PURE__ */ new Map();
564
632
  const frame = new THREE.Quaternion();
565
633
  const euler = new THREE.Euler();
566
634
  const turn = new THREE.Quaternion();
567
635
  const offset = new THREE.Vector3();
568
- function release(mesh) {
569
- mesh.removeFromParent();
570
- mesh.geometry.dispose();
571
- mesh.material.dispose();
636
+ let stamp = 0;
637
+ function geometryFor(particle) {
638
+ const key = geometryKeyOf(particle);
639
+ let geometry = geometries.get(key);
640
+ if (!geometry) {
641
+ geometry = geometryOf(THREE, particle);
642
+ geometries.set(key, geometry);
643
+ }
644
+ return geometry;
645
+ }
646
+ function acquire(particle) {
647
+ const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
648
+ const mesh = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
649
+ mesh.material.color.set(particle.color);
650
+ mesh.frustumCulled = false;
651
+ mesh.renderOrder = particle.additive ? 5 : 3;
652
+ group.add(mesh);
653
+ return { mesh, parkingKey, stamp };
654
+ }
655
+ function park(entry) {
656
+ entry.mesh.removeFromParent();
657
+ let free = parked.get(entry.parkingKey);
658
+ if (!free) {
659
+ free = [];
660
+ parked.set(entry.parkingKey, free);
661
+ }
662
+ if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry.mesh);
663
+ else entry.mesh.material.dispose();
572
664
  }
573
665
  return {
574
666
  group,
575
667
  sync() {
576
- const live = /* @__PURE__ */ new Set();
668
+ stamp++;
577
669
  for (const particle of pool.particles) {
578
- live.add(particle.id);
579
- let mesh = drawn.get(particle.id);
580
- if (!mesh) {
581
- mesh = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
582
- mesh.frustumCulled = false;
583
- mesh.renderOrder = particle.additive ? 5 : 3;
584
- drawn.set(particle.id, mesh);
585
- group.add(mesh);
670
+ let entry = drawn.get(particle.id);
671
+ if (!entry) {
672
+ entry = acquire(particle);
673
+ drawn.set(particle.id, entry);
586
674
  }
675
+ entry.stamp = stamp;
676
+ const mesh = entry.mesh;
587
677
  frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
588
678
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
589
679
  mesh.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
590
680
  turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
591
681
  mesh.quaternion.copy(frame).multiply(turn);
592
- mesh.scale.setScalar(particle.scale);
682
+ mesh.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
593
683
  mesh.material.opacity = particle.opacity;
594
684
  }
595
- for (const [id, mesh] of drawn) {
596
- if (live.has(id)) continue;
597
- release(mesh);
685
+ for (const [id, entry] of drawn) {
686
+ if (entry.stamp === stamp) continue;
687
+ park(entry);
598
688
  drawn.delete(id);
599
689
  }
600
690
  return pool.count;
601
691
  },
602
692
  dispose() {
603
- for (const mesh of drawn.values()) release(mesh);
693
+ for (const entry of drawn.values()) park(entry);
604
694
  drawn.clear();
695
+ for (const free of parked.values()) for (const mesh of free) mesh.material.dispose();
696
+ parked.clear();
697
+ for (const geometry of geometries.values()) geometry.dispose();
698
+ geometries.clear();
605
699
  group.removeFromParent();
606
700
  }
607
701
  };
@@ -1186,7 +1280,7 @@ function createVfxRenderer(THREE, options) {
1186
1280
  const shocks = createShockLayer(THREE, pools.shocks);
1187
1281
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
1188
1282
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
1189
- const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry });
1283
+ const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
1190
1284
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
1191
1285
  const stopMarks = world.onMarks((event) => marks.handle(event));
1192
1286
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -1212,6 +1306,16 @@ function createVfxRenderer(THREE, options) {
1212
1306
  const position = options.camera.position;
1213
1307
  world.setCamera([position.x, position.y, position.z]);
1214
1308
  }
1309
+ function attachWorkshopLayers() {
1310
+ const engine = world.peekWorkshop();
1311
+ if (!engine || engine === workshopEngine) return;
1312
+ workshopLayers?.dispose();
1313
+ if (workshopLayers) root.remove(workshopLayers.group);
1314
+ workshopEngine = engine;
1315
+ workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
1316
+ root.add(workshopLayers.group);
1317
+ applyLayers();
1318
+ }
1215
1319
  function syncAll() {
1216
1320
  lastDrawn.puffs = puffs.sync();
1217
1321
  lastDrawn.sparks = sparks.sync();
@@ -1220,15 +1324,7 @@ function createVfxRenderer(THREE, options) {
1220
1324
  lastDrawn.shocks = shocks.sync();
1221
1325
  lastDrawn.sprites = sprites.sync();
1222
1326
  lights.sync();
1223
- const engine = world.peekWorkshop();
1224
- if (engine && engine !== workshopEngine) {
1225
- workshopLayers?.dispose();
1226
- if (workshopLayers) root.remove(workshopLayers.group);
1227
- workshopEngine = engine;
1228
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
1229
- root.add(workshopLayers.group);
1230
- applyLayers();
1231
- }
1327
+ attachWorkshopLayers();
1232
1328
  const workshopCalls = workshopLayers?.sync() ?? 0;
1233
1329
  const glow = pools.lights.glow;
1234
1330
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -1281,6 +1377,9 @@ function createVfxRenderer(THREE, options) {
1281
1377
  },
1282
1378
  warmUpProbes() {
1283
1379
  releaseAnchors();
1380
+ marks.prepare();
1381
+ world.workshop();
1382
+ attachWorkshopLayers();
1284
1383
  const meshes = createMeshLayerProbes(THREE);
1285
1384
  for (const mesh of meshes) mesh.frustumCulled = false;
1286
1385
  const group = new THREE.Group();