@adatechnology/game-destruction-three 0.11.0 → 0.12.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/README.md +6 -0
- package/dist/index.d.ts +26 -8
- package/dist/index.global.js +687 -182
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +480 -94
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.global.js
CHANGED
|
@@ -6390,6 +6390,24 @@ var GameDestructionThree = (() => {
|
|
|
6390
6390
|
for (const decal2 of this.decals) decal2.on = false;
|
|
6391
6391
|
this.count = 0;
|
|
6392
6392
|
}
|
|
6393
|
+
/**
|
|
6394
|
+
* Takes away the marks inside a box: world `center`, `half` extents along the axes of the world `rotation` (a quaternion, x y z w; none means the
|
|
6395
|
+
* world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.
|
|
6396
|
+
*/
|
|
6397
|
+
clearInside(center, half, rotation = [0, 0, 0, 1]) {
|
|
6398
|
+
const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3];
|
|
6399
|
+
let gone = 0;
|
|
6400
|
+
for (const decal2 of this.decals) {
|
|
6401
|
+
if (!decal2.on) continue;
|
|
6402
|
+
const vx2 = decal2.x - center[0], vy2 = decal2.y - center[1], vz2 = decal2.z - center[2];
|
|
6403
|
+
const tx = 2 * (qy * vz2 - qz * vy2), ty = 2 * (qz * vx2 - qx * vz2), tz = 2 * (qx * vy2 - qy * vx2);
|
|
6404
|
+
const lx = vx2 + qw * tx + (qy * tz - qz * ty), ly = vy2 + qw * ty + (qz * tx - qx * tz), lz = vz2 + qw * tz + (qx * ty - qy * tx);
|
|
6405
|
+
if (Math.abs(lx) > half[0] || Math.abs(ly) > half[1] || Math.abs(lz) > half[2]) continue;
|
|
6406
|
+
decal2.on = false;
|
|
6407
|
+
gone++;
|
|
6408
|
+
}
|
|
6409
|
+
return gone;
|
|
6410
|
+
}
|
|
6393
6411
|
update(dt) {
|
|
6394
6412
|
this.count = 0;
|
|
6395
6413
|
for (const decal2 of this.decals) {
|
|
@@ -7709,6 +7727,11 @@ void main(){
|
|
|
7709
7727
|
var MAX_CONFORM_STEPS = 48;
|
|
7710
7728
|
var MIN_CONFORM_STEPS = 16;
|
|
7711
7729
|
var BLOOD_GROW_RATE = 1.2;
|
|
7730
|
+
var BLOOD_START_GROWTH = 0.05;
|
|
7731
|
+
var DEFAULT_MAX_GROUND_MARKS = 64;
|
|
7732
|
+
var DEFAULT_MAX_ROCKS = 512;
|
|
7733
|
+
var DEFAULT_MAX_BLOOD = 256;
|
|
7734
|
+
var MAX_SPARE_GROUND_GEOMETRIES = 16;
|
|
7712
7735
|
function createMarkLayer(THREE, options) {
|
|
7713
7736
|
const group = new THREE.Group();
|
|
7714
7737
|
const textures = createMarkTextures(THREE, options.createCanvas);
|
|
@@ -7732,110 +7755,168 @@ void main(){
|
|
|
7732
7755
|
const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
|
|
7733
7756
|
const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
|
|
7734
7757
|
owned.push(bloodMaterial, bloodGeometry);
|
|
7735
|
-
const
|
|
7736
|
-
const
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
material = new THREE.MeshStandardMaterial({ color, roughness: 0.9, metalness: 0, flatShading: true });
|
|
7742
|
-
rockMaterials.set(color, material);
|
|
7743
|
-
owned.push(material);
|
|
7758
|
+
const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
|
|
7759
|
+
const instanced = {};
|
|
7760
|
+
function rockMaterial() {
|
|
7761
|
+
if (!instanced.rockMaterial) {
|
|
7762
|
+
instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
|
|
7763
|
+
owned.push(instanced.rockMaterial);
|
|
7744
7764
|
}
|
|
7745
|
-
return
|
|
7765
|
+
return instanced.rockMaterial;
|
|
7766
|
+
}
|
|
7767
|
+
function bloodMesh() {
|
|
7768
|
+
return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
|
|
7769
|
+
}
|
|
7770
|
+
function rockMesh() {
|
|
7771
|
+
return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
|
|
7772
|
+
}
|
|
7773
|
+
function plateMesh() {
|
|
7774
|
+
return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
|
|
7775
|
+
}
|
|
7776
|
+
function attach(mesh2, castShadow = false) {
|
|
7777
|
+
mesh2.castShadow = castShadow;
|
|
7778
|
+
group.add(mesh2);
|
|
7779
|
+
return mesh2;
|
|
7780
|
+
}
|
|
7781
|
+
const groundMarks = [];
|
|
7782
|
+
const spareGeometries = [];
|
|
7783
|
+
const blood = [];
|
|
7784
|
+
const placed = { blood: 0, rock: 0, plate: 0 };
|
|
7785
|
+
const matrix = new THREE.Matrix4();
|
|
7786
|
+
const position = new THREE.Vector3();
|
|
7787
|
+
const quaternion = new THREE.Quaternion();
|
|
7788
|
+
const euler = new THREE.Euler();
|
|
7789
|
+
const scale = new THREE.Vector3();
|
|
7790
|
+
const color = new THREE.Color();
|
|
7791
|
+
const up = new THREE.Vector3(0, 1, 0);
|
|
7792
|
+
function createInstanced(geometry, material, capacity) {
|
|
7793
|
+
const mesh2 = new THREE.InstancedMesh(geometry, material, capacity);
|
|
7794
|
+
mesh2.count = 0;
|
|
7795
|
+
mesh2.frustumCulled = false;
|
|
7796
|
+
mesh2.receiveShadow = true;
|
|
7797
|
+
return mesh2;
|
|
7798
|
+
}
|
|
7799
|
+
function takeSpare(vertexCount) {
|
|
7800
|
+
const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
|
|
7801
|
+
return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
|
|
7802
|
+
}
|
|
7803
|
+
function evictOldestGroundMark() {
|
|
7804
|
+
const oldest = groundMarks.shift();
|
|
7805
|
+
group.remove(oldest.mesh);
|
|
7806
|
+
spareGeometries.push(oldest.mesh.geometry);
|
|
7807
|
+
if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
|
|
7746
7808
|
}
|
|
7747
7809
|
function conform(mark, material) {
|
|
7748
7810
|
const radius = mark.radius;
|
|
7749
7811
|
const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
|
|
7750
|
-
|
|
7751
|
-
const
|
|
7812
|
+
if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
|
|
7813
|
+
const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
|
|
7814
|
+
const grid = geometry.attributes.position;
|
|
7815
|
+
const cell = 2 / steps;
|
|
7752
7816
|
const cos2 = Math.cos(mark.rotation);
|
|
7753
7817
|
const sin2 = Math.sin(mark.rotation);
|
|
7754
|
-
for (let index = 0; index <
|
|
7755
|
-
const localX =
|
|
7756
|
-
const localZ =
|
|
7818
|
+
for (let index = 0; index < grid.count; index++) {
|
|
7819
|
+
const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
|
|
7820
|
+
const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
|
|
7757
7821
|
const worldX = mark.x + localX * cos2 - localZ * sin2;
|
|
7758
7822
|
const worldZ = mark.z + localX * sin2 + localZ * cos2;
|
|
7759
|
-
|
|
7823
|
+
grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
|
|
7760
7824
|
}
|
|
7825
|
+
grid.needsUpdate = true;
|
|
7761
7826
|
geometry.computeVertexNormals();
|
|
7827
|
+
geometry.attributes.normal.needsUpdate = true;
|
|
7828
|
+
geometry.boundingSphere = null;
|
|
7829
|
+
geometry.boundingBox = null;
|
|
7762
7830
|
const mesh2 = new THREE.Mesh(geometry, material);
|
|
7763
7831
|
mesh2.receiveShadow = true;
|
|
7764
|
-
|
|
7832
|
+
group.add(mesh2);
|
|
7833
|
+
groundMarks.push({ mesh: mesh2, steps });
|
|
7765
7834
|
}
|
|
7766
7835
|
function addRock(mark) {
|
|
7767
|
-
const mesh2 =
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
mesh2.
|
|
7772
|
-
mesh2.
|
|
7773
|
-
|
|
7836
|
+
const mesh2 = mark.plate ? plateMesh() : rockMesh();
|
|
7837
|
+
const slot = mark.plate ? "plate" : "rock";
|
|
7838
|
+
const index = placed[slot]++ % mesh2.instanceMatrix.count;
|
|
7839
|
+
quaternion.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
|
|
7840
|
+
mesh2.setMatrixAt(index, matrix.compose(position.set(mark.x, mark.y, mark.z), quaternion, scale.set(mark.scale[0], mark.scale[1], mark.scale[2])));
|
|
7841
|
+
mesh2.setColorAt(index, color.set(mark.color));
|
|
7842
|
+
mesh2.count = Math.min(placed[slot], mesh2.instanceMatrix.count);
|
|
7843
|
+
mesh2.instanceMatrix.needsUpdate = true;
|
|
7844
|
+
mesh2.instanceColor.needsUpdate = true;
|
|
7845
|
+
}
|
|
7846
|
+
function writeBlood(index, drawn, growth) {
|
|
7847
|
+
quaternion.setFromAxisAngle(up, drawn.rotation);
|
|
7848
|
+
const mesh2 = bloodMesh();
|
|
7849
|
+
mesh2.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)));
|
|
7850
|
+
mesh2.instanceMatrix.needsUpdate = true;
|
|
7774
7851
|
}
|
|
7775
7852
|
function addBlood(mark) {
|
|
7776
|
-
const
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
mesh2.scale.multiplyScalar(0.05);
|
|
7782
|
-
blood.push({ mesh: mesh2, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
|
|
7783
|
-
return mesh2;
|
|
7853
|
+
const index = placed.blood++ % bloodMesh().instanceMatrix.count;
|
|
7854
|
+
const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
|
|
7855
|
+
blood[index] = drawn;
|
|
7856
|
+
writeBlood(index, drawn, BLOOD_START_GROWTH);
|
|
7857
|
+
bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
|
|
7784
7858
|
}
|
|
7785
7859
|
function build(mark) {
|
|
7786
|
-
if (mark.kind === "scorch")
|
|
7787
|
-
if (mark.kind === "dirt")
|
|
7788
|
-
if (mark.kind === "rock")
|
|
7789
|
-
|
|
7790
|
-
}
|
|
7791
|
-
function
|
|
7792
|
-
for (const
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7860
|
+
if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
|
|
7861
|
+
else if (mark.kind === "dirt") conform(mark, dirtMaterial);
|
|
7862
|
+
else if (mark.kind === "rock") addRock(mark);
|
|
7863
|
+
else addBlood(mark);
|
|
7864
|
+
}
|
|
7865
|
+
function clearMarks() {
|
|
7866
|
+
for (const drawn of groundMarks) group.remove(drawn.mesh);
|
|
7867
|
+
for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
|
|
7868
|
+
groundMarks.length = 0;
|
|
7869
|
+
for (const geometry of spareGeometries) geometry.dispose();
|
|
7870
|
+
spareGeometries.length = 0;
|
|
7797
7871
|
blood.length = 0;
|
|
7798
|
-
|
|
7872
|
+
placed.blood = placed.rock = placed.plate = 0;
|
|
7873
|
+
for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) if (mesh2) mesh2.count = 0;
|
|
7799
7874
|
}
|
|
7800
7875
|
return {
|
|
7801
7876
|
group,
|
|
7802
7877
|
handle(event) {
|
|
7803
|
-
if (event.type === "clear")
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7878
|
+
if (event.type === "clear") clearMarks();
|
|
7879
|
+
else build(event.mark);
|
|
7880
|
+
},
|
|
7881
|
+
prepare() {
|
|
7882
|
+
bloodMesh();
|
|
7883
|
+
rockMesh();
|
|
7884
|
+
plateMesh();
|
|
7809
7885
|
},
|
|
7810
7886
|
update(dt) {
|
|
7811
|
-
for (
|
|
7812
|
-
|
|
7887
|
+
for (let index = 0; index < blood.length; index++) {
|
|
7888
|
+
const drawn = blood[index];
|
|
7889
|
+
if (!drawn || drawn.age > 1) continue;
|
|
7813
7890
|
drawn.age += dt * BLOOD_GROW_RATE;
|
|
7814
|
-
|
|
7815
|
-
drawn.mesh.scale.set(drawn.scaleX * Math.max(0.05, ease3), 1, drawn.scaleZ * Math.max(0.05, ease3));
|
|
7891
|
+
writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
|
|
7816
7892
|
}
|
|
7817
7893
|
},
|
|
7818
|
-
count: () =>
|
|
7894
|
+
count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh2) => mesh2 && mesh2.count > 0).length,
|
|
7819
7895
|
dispose() {
|
|
7820
|
-
|
|
7896
|
+
clearMarks();
|
|
7897
|
+
for (const mesh2 of [instanced.blood, instanced.rock, instanced.plate]) mesh2?.dispose();
|
|
7821
7898
|
for (const item of owned) item.dispose();
|
|
7822
7899
|
}
|
|
7823
7900
|
};
|
|
7824
7901
|
}
|
|
7825
7902
|
var SPHERE_DEFAULT_WIDTH = 8;
|
|
7826
7903
|
var SPHERE_DEFAULT_HEIGHT = 6;
|
|
7904
|
+
var MAX_PARKED_MESHES_PER_KIND = 256;
|
|
7827
7905
|
function geometryOf(THREE, particle) {
|
|
7828
7906
|
switch (particle.shape) {
|
|
7829
7907
|
case "cone":
|
|
7830
|
-
return new THREE.ConeGeometry(
|
|
7908
|
+
return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
|
|
7831
7909
|
case "cone_open":
|
|
7832
|
-
return new THREE.ConeGeometry(
|
|
7910
|
+
return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
|
|
7833
7911
|
case "sphere":
|
|
7834
|
-
return new THREE.SphereGeometry(
|
|
7912
|
+
return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
|
|
7835
7913
|
case "icosahedron":
|
|
7836
|
-
return new THREE.IcosahedronGeometry(
|
|
7914
|
+
return new THREE.IcosahedronGeometry(1, particle.segments);
|
|
7837
7915
|
}
|
|
7838
7916
|
}
|
|
7917
|
+
function isCone(particle) {
|
|
7918
|
+
return particle.shape === "cone" || particle.shape === "cone_open";
|
|
7919
|
+
}
|
|
7839
7920
|
function materialOf(THREE, particle) {
|
|
7840
7921
|
if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
|
|
7841
7922
|
return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
|
|
@@ -7846,51 +7927,82 @@ void main(){
|
|
|
7846
7927
|
(kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
|
|
7847
7928
|
);
|
|
7848
7929
|
}
|
|
7930
|
+
function geometryKeyOf(particle) {
|
|
7931
|
+
return `${particle.shape}:${particle.segments}:${particle.segments2}`;
|
|
7932
|
+
}
|
|
7849
7933
|
function createMeshLayer(THREE, pool) {
|
|
7850
7934
|
const group = new THREE.Group();
|
|
7851
7935
|
group.name = "workshop-meshes";
|
|
7852
7936
|
const drawn = /* @__PURE__ */ new Map();
|
|
7937
|
+
const geometries = /* @__PURE__ */ new Map();
|
|
7938
|
+
const parked = /* @__PURE__ */ new Map();
|
|
7853
7939
|
const frame = new THREE.Quaternion();
|
|
7854
7940
|
const euler = new THREE.Euler();
|
|
7855
7941
|
const turn = new THREE.Quaternion();
|
|
7856
7942
|
const offset = new THREE.Vector3();
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7943
|
+
let stamp = 0;
|
|
7944
|
+
function geometryFor(particle) {
|
|
7945
|
+
const key = geometryKeyOf(particle);
|
|
7946
|
+
let geometry = geometries.get(key);
|
|
7947
|
+
if (!geometry) {
|
|
7948
|
+
geometry = geometryOf(THREE, particle);
|
|
7949
|
+
geometries.set(key, geometry);
|
|
7950
|
+
}
|
|
7951
|
+
return geometry;
|
|
7952
|
+
}
|
|
7953
|
+
function acquire(particle) {
|
|
7954
|
+
const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
|
|
7955
|
+
const mesh2 = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
|
|
7956
|
+
mesh2.material.color.set(particle.color);
|
|
7957
|
+
mesh2.frustumCulled = false;
|
|
7958
|
+
mesh2.renderOrder = particle.additive ? 5 : 3;
|
|
7959
|
+
group.add(mesh2);
|
|
7960
|
+
return { mesh: mesh2, parkingKey, stamp };
|
|
7961
|
+
}
|
|
7962
|
+
function park(entry) {
|
|
7963
|
+
entry.mesh.removeFromParent();
|
|
7964
|
+
let free = parked.get(entry.parkingKey);
|
|
7965
|
+
if (!free) {
|
|
7966
|
+
free = [];
|
|
7967
|
+
parked.set(entry.parkingKey, free);
|
|
7968
|
+
}
|
|
7969
|
+
if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry.mesh);
|
|
7970
|
+
else entry.mesh.material.dispose();
|
|
7861
7971
|
}
|
|
7862
7972
|
return {
|
|
7863
7973
|
group,
|
|
7864
7974
|
sync() {
|
|
7865
|
-
|
|
7975
|
+
stamp++;
|
|
7866
7976
|
for (const particle of pool.particles) {
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
mesh2.frustumCulled = false;
|
|
7872
|
-
mesh2.renderOrder = particle.additive ? 5 : 3;
|
|
7873
|
-
drawn.set(particle.id, mesh2);
|
|
7874
|
-
group.add(mesh2);
|
|
7977
|
+
let entry = drawn.get(particle.id);
|
|
7978
|
+
if (!entry) {
|
|
7979
|
+
entry = acquire(particle);
|
|
7980
|
+
drawn.set(particle.id, entry);
|
|
7875
7981
|
}
|
|
7982
|
+
entry.stamp = stamp;
|
|
7983
|
+
const mesh2 = entry.mesh;
|
|
7876
7984
|
frame.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
|
|
7877
7985
|
offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame);
|
|
7878
7986
|
mesh2.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
|
|
7879
7987
|
turn.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
|
|
7880
7988
|
mesh2.quaternion.copy(frame).multiply(turn);
|
|
7881
|
-
mesh2.scale.
|
|
7989
|
+
mesh2.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
|
|
7882
7990
|
mesh2.material.opacity = particle.opacity;
|
|
7883
7991
|
}
|
|
7884
|
-
for (const [id,
|
|
7885
|
-
if (
|
|
7886
|
-
|
|
7992
|
+
for (const [id, entry] of drawn) {
|
|
7993
|
+
if (entry.stamp === stamp) continue;
|
|
7994
|
+
park(entry);
|
|
7887
7995
|
drawn.delete(id);
|
|
7888
7996
|
}
|
|
7889
7997
|
return pool.count;
|
|
7890
7998
|
},
|
|
7891
7999
|
dispose() {
|
|
7892
|
-
for (const
|
|
8000
|
+
for (const entry of drawn.values()) park(entry);
|
|
7893
8001
|
drawn.clear();
|
|
8002
|
+
for (const free of parked.values()) for (const mesh2 of free) mesh2.material.dispose();
|
|
8003
|
+
parked.clear();
|
|
8004
|
+
for (const geometry of geometries.values()) geometry.dispose();
|
|
8005
|
+
geometries.clear();
|
|
7894
8006
|
group.removeFromParent();
|
|
7895
8007
|
}
|
|
7896
8008
|
};
|
|
@@ -8458,7 +8570,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8458
8570
|
const shocks = createShockLayer(THREE, pools.shocks);
|
|
8459
8571
|
const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
|
|
8460
8572
|
const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
|
|
8461
|
-
const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry });
|
|
8573
|
+
const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
|
|
8462
8574
|
root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
|
|
8463
8575
|
const stopMarks = world.onMarks((event) => marks.handle(event));
|
|
8464
8576
|
const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
|
|
@@ -8484,6 +8596,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8484
8596
|
const position = options.camera.position;
|
|
8485
8597
|
world.setCamera([position.x, position.y, position.z]);
|
|
8486
8598
|
}
|
|
8599
|
+
function attachWorkshopLayers() {
|
|
8600
|
+
const engine = world.peekWorkshop();
|
|
8601
|
+
if (!engine || engine === workshopEngine) return;
|
|
8602
|
+
workshopLayers?.dispose();
|
|
8603
|
+
if (workshopLayers) root.remove(workshopLayers.group);
|
|
8604
|
+
workshopEngine = engine;
|
|
8605
|
+
workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
|
|
8606
|
+
root.add(workshopLayers.group);
|
|
8607
|
+
applyLayers();
|
|
8608
|
+
}
|
|
8487
8609
|
function syncAll() {
|
|
8488
8610
|
lastDrawn.puffs = puffs.sync();
|
|
8489
8611
|
lastDrawn.sparks = sparks.sync();
|
|
@@ -8492,15 +8614,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8492
8614
|
lastDrawn.shocks = shocks.sync();
|
|
8493
8615
|
lastDrawn.sprites = sprites.sync();
|
|
8494
8616
|
lights.sync();
|
|
8495
|
-
|
|
8496
|
-
if (engine && engine !== workshopEngine) {
|
|
8497
|
-
workshopLayers?.dispose();
|
|
8498
|
-
if (workshopLayers) root.remove(workshopLayers.group);
|
|
8499
|
-
workshopEngine = engine;
|
|
8500
|
-
workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
|
|
8501
|
-
root.add(workshopLayers.group);
|
|
8502
|
-
applyLayers();
|
|
8503
|
-
}
|
|
8617
|
+
attachWorkshopLayers();
|
|
8504
8618
|
const workshopCalls = workshopLayers?.sync() ?? 0;
|
|
8505
8619
|
const glow = pools.lights.glow;
|
|
8506
8620
|
uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
|
|
@@ -8553,6 +8667,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8553
8667
|
},
|
|
8554
8668
|
warmUpProbes() {
|
|
8555
8669
|
releaseAnchors();
|
|
8670
|
+
marks.prepare();
|
|
8671
|
+
world.workshop();
|
|
8672
|
+
attachWorkshopLayers();
|
|
8556
8673
|
const meshes = createMeshLayerProbes(THREE);
|
|
8557
8674
|
for (const mesh2 of meshes) mesh2.frustumCulled = false;
|
|
8558
8675
|
const group = new THREE.Group();
|
|
@@ -8673,6 +8790,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8673
8790
|
var SHEET_SWAY = 3;
|
|
8674
8791
|
var BIG_LENGTH = 0.2;
|
|
8675
8792
|
var BIG_WIDTH = 0.03;
|
|
8793
|
+
var FRAGMENT_SEED = 1374496523;
|
|
8676
8794
|
var isBig = (size) => {
|
|
8677
8795
|
const [, middle, longest] = [...size].sort((a, b) => a - b);
|
|
8678
8796
|
return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
|
|
@@ -8692,7 +8810,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8692
8810
|
__publicField(this, "m");
|
|
8693
8811
|
__publicField(this, "v");
|
|
8694
8812
|
__publicField(this, "w");
|
|
8695
|
-
__publicField(this, "rng", createRng(
|
|
8813
|
+
__publicField(this, "rng", createRng(FRAGMENT_SEED));
|
|
8696
8814
|
/** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
|
|
8697
8815
|
__publicField(this, "tick", 0);
|
|
8698
8816
|
__publicField(this, "markGeometry", null);
|
|
@@ -8793,6 +8911,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8793
8911
|
mesh2.scale.copy(sc);
|
|
8794
8912
|
mesh2.castShadow = true;
|
|
8795
8913
|
mesh2.userData.destructionDebris = true;
|
|
8914
|
+
mesh2.name = "fragment";
|
|
8915
|
+
markKitOrigin(mesh2, { owner: "game-destruction-three", kind: "fragment", id: f.material });
|
|
8796
8916
|
this.o.scene.add(mesh2);
|
|
8797
8917
|
const live = {
|
|
8798
8918
|
mesh: mesh2,
|
|
@@ -8809,7 +8929,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8809
8929
|
dying: false,
|
|
8810
8930
|
air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
|
|
8811
8931
|
body: null,
|
|
8812
|
-
born: this.tick
|
|
8932
|
+
born: this.tick,
|
|
8933
|
+
life: Math.max(0, typeof this.o.life === "function" ? this.o.life(f, home) : this.o.life)
|
|
8813
8934
|
};
|
|
8814
8935
|
if (f.shape !== "shard" && isBig(f.size)) {
|
|
8815
8936
|
live.body = {
|
|
@@ -8818,8 +8939,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8818
8939
|
live.vel.x = x;
|
|
8819
8940
|
live.vel.z = z;
|
|
8820
8941
|
live.vel.y = Math.max(live.vel.y, 0.3);
|
|
8821
|
-
live
|
|
8822
|
-
live.rest = 0;
|
|
8942
|
+
this.rouse(live);
|
|
8823
8943
|
}
|
|
8824
8944
|
};
|
|
8825
8945
|
}
|
|
@@ -8854,16 +8974,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8854
8974
|
d.y += k * 0.4;
|
|
8855
8975
|
f.vel.add(d);
|
|
8856
8976
|
f.ang.add(this.w.set(this.rng() - 0.5, this.rng() - 0.5, this.rng() - 0.5).multiplyScalar(k));
|
|
8857
|
-
f
|
|
8858
|
-
f.rest = 0;
|
|
8977
|
+
this.rouse(f);
|
|
8859
8978
|
}
|
|
8860
8979
|
}
|
|
8980
|
+
/** A fragment that came to rest stops having its matrix recomputed on every frame; whatever moves it again calls this first. */
|
|
8981
|
+
rouse(f) {
|
|
8982
|
+
f.asleep = false;
|
|
8983
|
+
f.rest = 0;
|
|
8984
|
+
f.mesh.matrixAutoUpdate = true;
|
|
8985
|
+
}
|
|
8861
8986
|
/** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
|
|
8862
8987
|
wakeRegion(x, z, radius) {
|
|
8863
8988
|
for (const f of this.list) {
|
|
8864
8989
|
if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
|
|
8865
|
-
f
|
|
8866
|
-
f.rest = 0;
|
|
8990
|
+
this.rouse(f);
|
|
8867
8991
|
}
|
|
8868
8992
|
}
|
|
8869
8993
|
update(dt) {
|
|
@@ -8872,6 +8996,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8872
8996
|
for (let i = this.list.length - 1; i >= 0; i--) {
|
|
8873
8997
|
const f = this.list[i];
|
|
8874
8998
|
if (f.dying) {
|
|
8999
|
+
f.mesh.matrixAutoUpdate = true;
|
|
8875
9000
|
f.fade -= dt / 0.6;
|
|
8876
9001
|
if (f.fade <= 0) {
|
|
8877
9002
|
this.o.scene.remove(f.mesh);
|
|
@@ -8882,7 +9007,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8882
9007
|
}
|
|
8883
9008
|
if (f.asleep) {
|
|
8884
9009
|
f.rest += dt;
|
|
8885
|
-
if (
|
|
9010
|
+
if (f.life > 0 && f.rest > f.life && !f.body) {
|
|
9011
|
+
f.dying = true;
|
|
9012
|
+
f.mesh.matrixAutoUpdate = true;
|
|
9013
|
+
}
|
|
8886
9014
|
continue;
|
|
8887
9015
|
}
|
|
8888
9016
|
this.step(f, dt, g);
|
|
@@ -8931,6 +9059,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8931
9059
|
f.still += dt;
|
|
8932
9060
|
if (f.still > 0.4) {
|
|
8933
9061
|
f.asleep = true;
|
|
9062
|
+
f.mesh.updateMatrix();
|
|
9063
|
+
f.mesh.matrixAutoUpdate = false;
|
|
8934
9064
|
f.vel.set(0, 0, 0);
|
|
8935
9065
|
f.ang.set(0, 0, 0);
|
|
8936
9066
|
}
|
|
@@ -8980,6 +9110,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
8980
9110
|
for (const f of this.list) this.o.scene.remove(f.mesh);
|
|
8981
9111
|
this.list.length = 0;
|
|
8982
9112
|
}
|
|
9113
|
+
/** `clear`, the random stream back to its seed and the tick to 0: what comes next flies as on a new system. */
|
|
9114
|
+
reset() {
|
|
9115
|
+
this.clear();
|
|
9116
|
+
this.rng = createRng(FRAGMENT_SEED);
|
|
9117
|
+
this.tick = 0;
|
|
9118
|
+
}
|
|
8983
9119
|
dispose() {
|
|
8984
9120
|
this.clear();
|
|
8985
9121
|
this.box.dispose();
|
|
@@ -9078,6 +9214,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9078
9214
|
var SUB_DT = 1 / 120;
|
|
9079
9215
|
var MU = 0.7;
|
|
9080
9216
|
var SLEEP_AFTER = 0.5;
|
|
9217
|
+
var CALM_DRIFT = 0.01;
|
|
9218
|
+
var CALM_TURN = 0.05;
|
|
9219
|
+
var CALM_SLEEP_AFTER = 1.5;
|
|
9081
9220
|
var HARD_LANDING = 3;
|
|
9082
9221
|
var SLOP = 2e-3;
|
|
9083
9222
|
var NARROW = 0.3;
|
|
@@ -9086,6 +9225,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9086
9225
|
var TIP_LYING = 0.25;
|
|
9087
9226
|
var TIP_SECONDS = 3;
|
|
9088
9227
|
var STEP = 0.03;
|
|
9228
|
+
var WARM_SLOTS = 8 + 12 * 7;
|
|
9229
|
+
var SOLVER_PASSES = 10;
|
|
9230
|
+
var SIDE_BOUNCE = 0.1;
|
|
9231
|
+
var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
|
|
9089
9232
|
var boxTopAt = (s, x, z) => {
|
|
9090
9233
|
const e = s.e;
|
|
9091
9234
|
const dx = x - s.c[0], dz = z - s.c[2];
|
|
@@ -9156,6 +9299,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9156
9299
|
localCenter: [localCenter.x, localCenter.y, localCenter.z],
|
|
9157
9300
|
centerV: localCenter.clone(),
|
|
9158
9301
|
still: 0,
|
|
9302
|
+
calmAt: null,
|
|
9303
|
+
calm: 0,
|
|
9159
9304
|
lastHit: -1,
|
|
9160
9305
|
onGround: false
|
|
9161
9306
|
};
|
|
@@ -9174,6 +9319,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9174
9319
|
clear() {
|
|
9175
9320
|
this.list.length = 0;
|
|
9176
9321
|
}
|
|
9322
|
+
/** `clear` and the clock back to 0. */
|
|
9323
|
+
reset() {
|
|
9324
|
+
this.clear();
|
|
9325
|
+
this.time = 0;
|
|
9326
|
+
}
|
|
9177
9327
|
/** World position of the centre of mass. */
|
|
9178
9328
|
centerOf(b, out) {
|
|
9179
9329
|
return out.copy(b.centerV).applyQuaternion(b.root.quaternion).add(b.root.position);
|
|
@@ -9198,6 +9348,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9198
9348
|
}
|
|
9199
9349
|
b.sleeping = false;
|
|
9200
9350
|
b.still = 0;
|
|
9351
|
+
b.calmAt = null;
|
|
9201
9352
|
}
|
|
9202
9353
|
/** I^-1 * (x, y, z) in world space for a box with the body's half extents and mass (needs `this.m` to hold the body's rotation). */
|
|
9203
9354
|
mulInvInertia(b, x, y, z) {
|
|
@@ -9215,12 +9366,51 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9215
9366
|
const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
|
|
9216
9367
|
return [e[0] * lx + e[4] * ly + e[8] * lz, e[1] * lx + e[5] * ly + e[9] * lz, e[2] * lx + e[6] * ly + e[10] * lz];
|
|
9217
9368
|
}
|
|
9369
|
+
/** The points of the box that touch the ground, in its own frame: the 8 corners, then (with `edges`) `EDGE_POINTS` along each of its 12 edges. */
|
|
9370
|
+
samples(b, edges) {
|
|
9371
|
+
const [hx, hy, hz] = b.halfExtents;
|
|
9372
|
+
const out = [];
|
|
9373
|
+
for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz : -hz]);
|
|
9374
|
+
if (!edges) return out;
|
|
9375
|
+
const h = [hx, hy, hz];
|
|
9376
|
+
for (let axis2 = 0; axis2 < 3; axis2++) {
|
|
9377
|
+
const a1 = (axis2 + 1) % 3;
|
|
9378
|
+
const a2 = (axis2 + 2) % 3;
|
|
9379
|
+
for (let k = 0; k < 4; k++) {
|
|
9380
|
+
for (const f of EDGE_POINTS) {
|
|
9381
|
+
const p = [0, 0, 0];
|
|
9382
|
+
p[axis2] = f * h[axis2];
|
|
9383
|
+
p[a1] = k & 1 ? h[a1] : -h[a1];
|
|
9384
|
+
p[a2] = k & 2 ? h[a2] : -h[a2];
|
|
9385
|
+
out.push(p);
|
|
9386
|
+
}
|
|
9387
|
+
}
|
|
9388
|
+
}
|
|
9389
|
+
return out;
|
|
9390
|
+
}
|
|
9391
|
+
/**
|
|
9392
|
+
* An impulse along the unit normal `n` at `r` (from the centre of mass, world) that stops the point closing in along `n` (with `bounce`); `e` holds the body's rotation.
|
|
9393
|
+
*/
|
|
9394
|
+
contactImpulse(b, e, r, n, bounce2) {
|
|
9395
|
+
const v = b.velocity;
|
|
9396
|
+
const w = b.angularVelocity;
|
|
9397
|
+
const vn = (v[0] + w[1] * r[2] - w[2] * r[1]) * n[0] + (v[1] + w[2] * r[0] - w[0] * r[2]) * n[1] + (v[2] + w[0] * r[1] - w[1] * r[0]) * n[2];
|
|
9398
|
+
if (vn >= 0) return;
|
|
9399
|
+
const a = this.mulInvInertiaWith(b, e, r[1] * n[2] - r[2] * n[1], r[2] * n[0] - r[0] * n[2], r[0] * n[1] - r[1] * n[0]);
|
|
9400
|
+
const k = (a[1] * r[2] - a[2] * r[1]) * n[0] + (a[2] * r[0] - a[0] * r[2]) * n[1] + (a[0] * r[1] - a[1] * r[0]) * n[2];
|
|
9401
|
+
const j = -(1 + bounce2) * vn / (1 / b.mass + k);
|
|
9402
|
+
for (let i = 0; i < 3; i++) {
|
|
9403
|
+
v[i] += j * n[i] / b.mass;
|
|
9404
|
+
w[i] += a[i] * j;
|
|
9405
|
+
}
|
|
9406
|
+
}
|
|
9218
9407
|
/** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
|
|
9219
9408
|
wakeRegion(x, z, radius) {
|
|
9220
9409
|
for (const b of this.list) {
|
|
9221
9410
|
if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
|
|
9222
9411
|
b.sleeping = false;
|
|
9223
9412
|
b.still = 0;
|
|
9413
|
+
b.calmAt = null;
|
|
9224
9414
|
}
|
|
9225
9415
|
}
|
|
9226
9416
|
update(dt) {
|
|
@@ -9236,6 +9426,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9236
9426
|
b.angularVelocity = [0, 0, 0];
|
|
9237
9427
|
b.sleeping = false;
|
|
9238
9428
|
b.still = 0;
|
|
9429
|
+
b.calmAt = null;
|
|
9239
9430
|
continue;
|
|
9240
9431
|
}
|
|
9241
9432
|
if (b.sleeping) continue;
|
|
@@ -9260,7 +9451,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9260
9451
|
const q = this.q.copy(b.root.quaternion);
|
|
9261
9452
|
const v = b.velocity;
|
|
9262
9453
|
const w = b.angularVelocity;
|
|
9263
|
-
const [hx, hy, hz] = b.halfExtents;
|
|
9264
9454
|
const invM = 1 / b.mass;
|
|
9265
9455
|
const steps = clamp6(Math.ceil(dt / SUB_DT), 1, 8);
|
|
9266
9456
|
const h = dt / steps;
|
|
@@ -9269,7 +9459,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9269
9459
|
let contactedLast = false;
|
|
9270
9460
|
let touched = false;
|
|
9271
9461
|
let unstable = false;
|
|
9462
|
+
const corners = this.samples(b, false);
|
|
9463
|
+
this.m.makeRotationFromQuaternion(q);
|
|
9464
|
+
let gLow = Infinity;
|
|
9465
|
+
let gHigh = -Infinity;
|
|
9466
|
+
for (const [sx, sy, sz] of corners) {
|
|
9467
|
+
const e0 = this.m.elements;
|
|
9468
|
+
const y = pos[1] + e0[1] * sx + e0[5] * sy + e0[9] * sz;
|
|
9469
|
+
const x = pos[0] + e0[0] * sx + e0[4] * sy + e0[8] * sz;
|
|
9470
|
+
const z = pos[2] + e0[2] * sx + e0[6] * sy + e0[10] * sz;
|
|
9471
|
+
const g = Math.max(this.o.groundAt(x, z), this.o.staticTopAt?.(x, z, y, STEP) ?? -Infinity);
|
|
9472
|
+
gLow = Math.min(gLow, g);
|
|
9473
|
+
gHigh = Math.max(gHigh, g);
|
|
9474
|
+
}
|
|
9475
|
+
const local2 = gHigh - gLow > STEP ? this.samples(b, true) : corners;
|
|
9476
|
+
const prev = local2.map(() => [0, 0, 0]);
|
|
9272
9477
|
for (let s = 0; s < steps; s++) {
|
|
9478
|
+
this.m.makeRotationFromQuaternion(q);
|
|
9479
|
+
const e0 = this.m.elements;
|
|
9480
|
+
for (let i = 0; i < local2.length; i++) {
|
|
9481
|
+
const [sx, sy, sz] = local2[i];
|
|
9482
|
+
prev[i][0] = pos[0] + e0[0] * sx + e0[4] * sy + e0[8] * sz;
|
|
9483
|
+
prev[i][1] = pos[1] + e0[1] * sx + e0[5] * sy + e0[9] * sz;
|
|
9484
|
+
prev[i][2] = pos[2] + e0[2] * sx + e0[6] * sy + e0[10] * sz;
|
|
9485
|
+
}
|
|
9273
9486
|
v[1] -= this.o.gravity * h;
|
|
9274
9487
|
pos[0] += v[0] * h;
|
|
9275
9488
|
pos[1] += v[1] * h;
|
|
@@ -9286,20 +9499,60 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9286
9499
|
this.m.makeRotationFromQuaternion(q);
|
|
9287
9500
|
const e = this.m.elements;
|
|
9288
9501
|
const rs = [];
|
|
9502
|
+
const ids = [];
|
|
9289
9503
|
let maxPen = 0;
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9504
|
+
const rise = STEP + Math.max(0, -v[1]) * h * 2;
|
|
9505
|
+
let side = null;
|
|
9506
|
+
let lowest = Infinity;
|
|
9507
|
+
for (let i = 0; i < local2.length; i++) {
|
|
9508
|
+
const [sx, sy, sz] = local2[i];
|
|
9294
9509
|
const r = [e[0] * sx + e[4] * sy + e[8] * sz, e[1] * sx + e[5] * sy + e[9] * sz, e[2] * sx + e[6] * sy + e[10] * sz];
|
|
9510
|
+
const px = pos[0] + r[0];
|
|
9511
|
+
const pz = pos[2] + r[2];
|
|
9295
9512
|
const cy = pos[1] + r[1];
|
|
9296
|
-
const
|
|
9513
|
+
const was = prev[i];
|
|
9514
|
+
const raw = this.o.groundAt(px, pz);
|
|
9515
|
+
if (raw > cy - SLOP && was[1] < raw - STEP) {
|
|
9516
|
+
if (this.o.groundAt(was[0], was[2]) >= raw - STEP) {
|
|
9517
|
+
const floor2 = b.floor;
|
|
9518
|
+
if (floor2 !== void 0 && floor2 - cy > -SLOP && floor2 < raw - STEP) {
|
|
9519
|
+
rs.push(r);
|
|
9520
|
+
ids.push(i);
|
|
9521
|
+
maxPen = Math.max(maxPen, floor2 - cy);
|
|
9522
|
+
}
|
|
9523
|
+
continue;
|
|
9524
|
+
}
|
|
9525
|
+
const dx = px - was[0];
|
|
9526
|
+
const dz = pz - was[2];
|
|
9527
|
+
const len = Math.hypot(dx, dz);
|
|
9528
|
+
if (len > 1e-6) {
|
|
9529
|
+
let lo = 0;
|
|
9530
|
+
let hi = 1;
|
|
9531
|
+
for (let it = 0; it < 6; it++) {
|
|
9532
|
+
const mid = (lo + hi) / 2;
|
|
9533
|
+
if (this.o.groundAt(was[0] + dx * mid, was[2] + dz * mid) >= raw - STEP) hi = mid;
|
|
9534
|
+
else lo = mid;
|
|
9535
|
+
}
|
|
9536
|
+
const back = (1 - lo) * len + 1e-3;
|
|
9537
|
+
if (!side || back > side.back) side = { back, dir: [dx / len, dz / len], r };
|
|
9538
|
+
}
|
|
9539
|
+
continue;
|
|
9540
|
+
}
|
|
9541
|
+
const gy = i < 8 ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
|
|
9297
9542
|
const pen = gy - cy;
|
|
9298
9543
|
if (pen > -SLOP) {
|
|
9299
9544
|
rs.push(r);
|
|
9545
|
+
ids.push(i);
|
|
9300
9546
|
if (pen > maxPen) maxPen = pen;
|
|
9547
|
+
lowest = Math.min(lowest, gy);
|
|
9301
9548
|
}
|
|
9302
9549
|
}
|
|
9550
|
+
if (lowest < Infinity) b.floor = lowest;
|
|
9551
|
+
if (side) {
|
|
9552
|
+
pos[0] -= side.dir[0] * side.back;
|
|
9553
|
+
pos[2] -= side.dir[1] * side.back;
|
|
9554
|
+
this.contactImpulse(b, e, side.r, [-side.dir[0], 0, -side.dir[1]], SIDE_BOUNCE);
|
|
9555
|
+
}
|
|
9303
9556
|
contactedLast = rs.length > 0;
|
|
9304
9557
|
if (contactedLast) touched = true;
|
|
9305
9558
|
if (!contactedLast) continue;
|
|
@@ -9319,45 +9572,88 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9319
9572
|
const lz = pos[2] + sz / rs.length;
|
|
9320
9573
|
landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
|
|
9321
9574
|
}
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9575
|
+
const goal = rs.map((r) => {
|
|
9576
|
+
const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
|
|
9577
|
+
return vy2 < 0 ? -restitution * vy2 : 0;
|
|
9578
|
+
});
|
|
9579
|
+
const vx0 = v[0];
|
|
9580
|
+
const vz0 = v[2];
|
|
9581
|
+
if (!b.warm || b.warm.length !== WARM_SLOTS * 3) b.warm = new Float64Array(WARM_SLOTS * 3);
|
|
9582
|
+
const warm = b.warm;
|
|
9583
|
+
const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 3] : 0);
|
|
9584
|
+
const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 3 + 1], warm[id * 3 + 2]] : [0, 0]);
|
|
9585
|
+
for (let k = 0; k < rs.length; k++) {
|
|
9586
|
+
const r = rs[k];
|
|
9587
|
+
const jn = accN[k];
|
|
9588
|
+
const [fx, fz] = accT[k];
|
|
9589
|
+
if (jn === 0 && fx === 0 && fz === 0) continue;
|
|
9590
|
+
v[0] += fx * invM;
|
|
9591
|
+
v[1] += jn * invM;
|
|
9592
|
+
v[2] += fz * invM;
|
|
9593
|
+
const dw = this.mulInvInertiaWith(b, e, r[1] * fz - r[2] * jn, r[2] * fx - r[0] * fz, r[0] * jn - r[1] * fx);
|
|
9594
|
+
w[0] += dw[0];
|
|
9595
|
+
w[1] += dw[1];
|
|
9596
|
+
w[2] += dw[2];
|
|
9597
|
+
}
|
|
9598
|
+
for (let it = 0; it < SOLVER_PASSES; it++) {
|
|
9599
|
+
for (let k = 0; k < rs.length; k++) {
|
|
9600
|
+
const r = rs[k];
|
|
9601
|
+
const vy2 = v[1] + (w[2] * r[0] - w[0] * r[2]);
|
|
9329
9602
|
const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
|
|
9330
9603
|
const denom = invM + (a[2] * r[0] - a[0] * r[2]);
|
|
9331
|
-
const
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9604
|
+
const total = Math.max(0, accN[k] + (goal[k] - vy2) / denom);
|
|
9605
|
+
const j = total - accN[k];
|
|
9606
|
+
accN[k] = total;
|
|
9607
|
+
if (j !== 0) {
|
|
9608
|
+
v[1] += j * invM;
|
|
9609
|
+
w[0] += a[0] * j;
|
|
9610
|
+
w[1] += a[1] * j;
|
|
9611
|
+
w[2] += a[2] * j;
|
|
9612
|
+
}
|
|
9613
|
+
const vx2 = v[0] + (w[1] * r[2] - w[2] * r[1]);
|
|
9614
|
+
const vz2 = v[2] + (w[0] * r[1] - w[1] * r[0]);
|
|
9340
9615
|
const vt = Math.hypot(vx2, vz2);
|
|
9341
|
-
if (vt
|
|
9616
|
+
if (vt < 1e-6 && accT[k][0] === 0 && accT[k][1] === 0) continue;
|
|
9617
|
+
let fx = accT[k][0];
|
|
9618
|
+
let fz = accT[k][1];
|
|
9619
|
+
if (vt > 1e-6) {
|
|
9342
9620
|
const tx = vx2 / vt;
|
|
9343
9621
|
const tz = vz2 / vt;
|
|
9344
|
-
const
|
|
9345
|
-
const ry = r[2] * tx - r[0] * tz;
|
|
9346
|
-
const rz = -r[1] * tx;
|
|
9347
|
-
const at3 = this.mulInvInertiaWith(b, e, rx, ry, rz);
|
|
9622
|
+
const at3 = this.mulInvInertiaWith(b, e, r[1] * tz, r[2] * tx - r[0] * tz, -r[1] * tx);
|
|
9348
9623
|
const cx = at3[1] * r[2] - at3[2] * r[1];
|
|
9349
9624
|
const cz = at3[0] * r[1] - at3[1] * r[0];
|
|
9350
|
-
const
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9625
|
+
const jt = vt / (invM + cx * tx + cz * tz);
|
|
9626
|
+
fx -= tx * jt;
|
|
9627
|
+
fz -= tz * jt;
|
|
9628
|
+
}
|
|
9629
|
+
const most = MU * accN[k];
|
|
9630
|
+
const f = Math.hypot(fx, fz);
|
|
9631
|
+
if (f > most) {
|
|
9632
|
+
fx *= most / f;
|
|
9633
|
+
fz *= most / f;
|
|
9358
9634
|
}
|
|
9635
|
+
const dx = fx - accT[k][0];
|
|
9636
|
+
const dz = fz - accT[k][1];
|
|
9637
|
+
accT[k] = [fx, fz];
|
|
9638
|
+
if (dx === 0 && dz === 0) continue;
|
|
9639
|
+
v[0] += dx * invM;
|
|
9640
|
+
v[2] += dz * invM;
|
|
9641
|
+
const dw = this.mulInvInertiaWith(b, e, r[1] * dz, r[2] * dx - r[0] * dz, -r[1] * dx);
|
|
9642
|
+
w[0] += dw[0];
|
|
9643
|
+
w[1] += dw[1];
|
|
9644
|
+
w[2] += dw[2];
|
|
9359
9645
|
}
|
|
9360
9646
|
}
|
|
9647
|
+
pos[0] += (v[0] - vx0) * h;
|
|
9648
|
+
pos[2] += (v[2] - vz0) * h;
|
|
9649
|
+
warm.fill(0);
|
|
9650
|
+
for (let k = 0; k < rs.length; k++) {
|
|
9651
|
+
const id = ids[k];
|
|
9652
|
+
if (id >= WARM_SLOTS) continue;
|
|
9653
|
+
warm[id * 3] = accN[k];
|
|
9654
|
+
warm[id * 3 + 1] = accT[k][0];
|
|
9655
|
+
warm[id * 3 + 2] = accT[k][1];
|
|
9656
|
+
}
|
|
9361
9657
|
let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
|
|
9362
9658
|
for (const r of rs) {
|
|
9363
9659
|
x0 = Math.min(x0, r[0]);
|
|
@@ -9372,7 +9668,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9372
9668
|
unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
|
|
9373
9669
|
if (b.tipping) {
|
|
9374
9670
|
b.tipping.seconds -= h;
|
|
9375
|
-
|
|
9671
|
+
const [hx, hy, hz] = b.halfExtents;
|
|
9672
|
+
const upHalf = Math.abs(e[1]) * hx + Math.abs(e[5]) * hy + Math.abs(e[9]) * hz;
|
|
9673
|
+
if (upHalf < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
|
|
9376
9674
|
else {
|
|
9377
9675
|
w[0] += b.tipping.x * TIP_ACCELERATION * h;
|
|
9378
9676
|
w[2] += b.tipping.z * TIP_ACCELERATION * h;
|
|
@@ -9404,16 +9702,26 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9404
9702
|
this.o.onHardLanding(b, landPoint, landing);
|
|
9405
9703
|
}
|
|
9406
9704
|
const still = touched && !unstable && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
|
|
9407
|
-
if (still)
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
}
|
|
9415
|
-
|
|
9416
|
-
|
|
9705
|
+
if (still) b.still += dt;
|
|
9706
|
+
else b.still = 0;
|
|
9707
|
+
if (b.still >= SLEEP_AFTER || this.staysInPlace(b, touched && !b.tipping, pos, q, dt)) {
|
|
9708
|
+
b.sleeping = true;
|
|
9709
|
+
b.calmAt = null;
|
|
9710
|
+
v[0] = v[1] = v[2] = 0;
|
|
9711
|
+
w[0] = w[1] = w[2] = 0;
|
|
9712
|
+
}
|
|
9713
|
+
}
|
|
9714
|
+
/** Whether the piece has kept to the same spot on the ground for `CALM_SLEEP_AFTER` (see `CALM_DRIFT`). */
|
|
9715
|
+
staysInPlace(b, touched, center, q, dt) {
|
|
9716
|
+
const at3 = b.calmAt;
|
|
9717
|
+
const turned = at3 ? 2 * Math.acos(Math.min(1, Math.abs(at3.quaternion[0] * q.x + at3.quaternion[1] * q.y + at3.quaternion[2] * q.z + at3.quaternion[3] * q.w))) : Infinity;
|
|
9718
|
+
if (!touched || !at3 || Math.hypot(center[0] - at3.center[0], center[1] - at3.center[1], center[2] - at3.center[2]) > CALM_DRIFT || turned > CALM_TURN) {
|
|
9719
|
+
b.calmAt = touched ? { center: [center[0], center[1], center[2]], quaternion: [q.x, q.y, q.z, q.w] } : null;
|
|
9720
|
+
b.calm = 0;
|
|
9721
|
+
return false;
|
|
9722
|
+
}
|
|
9723
|
+
b.calm += dt;
|
|
9724
|
+
return b.calm >= CALM_SLEEP_AFTER;
|
|
9417
9725
|
}
|
|
9418
9726
|
/** Half the height of the box of a piece as it is turned now. */
|
|
9419
9727
|
halfHeight(b) {
|
|
@@ -9431,8 +9739,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9431
9739
|
const a = this.list[i];
|
|
9432
9740
|
for (let j = i + 1; j < n; j++) {
|
|
9433
9741
|
const b = this.list[j];
|
|
9434
|
-
|
|
9435
|
-
|
|
9742
|
+
const fixedA = a.held || a.sleeping;
|
|
9743
|
+
const fixedB = b.held || b.sleeping;
|
|
9744
|
+
if (fixedA && fixedB) continue;
|
|
9436
9745
|
const ra = Math.hypot(a.halfExtents[0], a.halfExtents[1], a.halfExtents[2]);
|
|
9437
9746
|
const rb = Math.hypot(b.halfExtents[0], b.halfExtents[1], b.halfExtents[2]);
|
|
9438
9747
|
this.centerOf(a, ca);
|
|
@@ -9450,19 +9759,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
9450
9759
|
d = 1;
|
|
9451
9760
|
}
|
|
9452
9761
|
const push = Math.min(maxMove, (rh - d) * 0.5);
|
|
9453
|
-
const wa =
|
|
9454
|
-
const wb =
|
|
9762
|
+
const wa = fixedA ? 0 : fixedB ? 1 : b.mass / (a.mass + b.mass);
|
|
9763
|
+
const wb = fixedB ? 0 : fixedA ? 1 : a.mass / (a.mass + b.mass);
|
|
9455
9764
|
const ux = dx / (d || 1);
|
|
9456
9765
|
const uz = dz / (d || 1);
|
|
9457
|
-
if (!
|
|
9766
|
+
if (!fixedA) {
|
|
9458
9767
|
a.root.position.x -= ux * push * 2 * wa;
|
|
9459
9768
|
a.root.position.z -= uz * push * 2 * wa;
|
|
9460
|
-
a.sleeping = false;
|
|
9461
9769
|
}
|
|
9462
|
-
if (!
|
|
9770
|
+
if (!fixedB) {
|
|
9463
9771
|
b.root.position.x += ux * push * 2 * wb;
|
|
9464
9772
|
b.root.position.z += uz * push * 2 * wb;
|
|
9465
|
-
b.sleeping = false;
|
|
9466
9773
|
}
|
|
9467
9774
|
}
|
|
9468
9775
|
}
|
|
@@ -10111,6 +10418,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
10111
10418
|
},
|
|
10112
10419
|
hitDebris: "chips",
|
|
10113
10420
|
breakDebris: "dust",
|
|
10421
|
+
visual: { innerColor: 10197652 },
|
|
10114
10422
|
thermal: {
|
|
10115
10423
|
conductivity: 0.15,
|
|
10116
10424
|
heatCapacity: 2.5
|
|
@@ -10141,6 +10449,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
10141
10449
|
],
|
|
10142
10450
|
hitDebris: "chips",
|
|
10143
10451
|
breakDebris: "dust",
|
|
10452
|
+
visual: { innerColor: 12085567 },
|
|
10144
10453
|
thermal: {
|
|
10145
10454
|
conductivity: 0.15,
|
|
10146
10455
|
heatCapacity: 2.5
|
|
@@ -10171,6 +10480,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
10171
10480
|
],
|
|
10172
10481
|
hitDebris: "dust",
|
|
10173
10482
|
breakDebris: "dust",
|
|
10483
|
+
visual: { innerColor: 14999507 },
|
|
10174
10484
|
thermal: {
|
|
10175
10485
|
conductivity: 0.1,
|
|
10176
10486
|
heatCapacity: 1.5
|
|
@@ -10196,6 +10506,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
10196
10506
|
],
|
|
10197
10507
|
hitDebris: "chips",
|
|
10198
10508
|
breakDebris: "dust",
|
|
10509
|
+
visual: { innerColor: 9407364 },
|
|
10199
10510
|
thermal: {
|
|
10200
10511
|
conductivity: 0.2,
|
|
10201
10512
|
heatCapacity: 2.5
|
|
@@ -10867,6 +11178,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
10867
11178
|
}
|
|
10868
11179
|
return out;
|
|
10869
11180
|
};
|
|
11181
|
+
var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
|
|
10870
11182
|
|
|
10871
11183
|
// src/spec.ts
|
|
10872
11184
|
function containsOf(value) {
|
|
@@ -10925,6 +11237,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
10925
11237
|
const fill = options.fillOf?.(o, material);
|
|
10926
11238
|
const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
|
|
10927
11239
|
const contains = containsOf(ud.destructionContains);
|
|
11240
|
+
const powder = typeof ud.destructionPowder?.color === "number" ? { color: ud.destructionPowder.color } : void 0;
|
|
10928
11241
|
const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
|
|
10929
11242
|
const existing = explicit ? byKey.get(explicit) : void 0;
|
|
10930
11243
|
if (existing) {
|
|
@@ -10938,9 +11251,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
10938
11251
|
existing.skin = existing.skin && skin;
|
|
10939
11252
|
if (fill !== void 0) existing.fill = fill;
|
|
10940
11253
|
if (contains) existing.contains = contains;
|
|
11254
|
+
if (powder) existing.powder = powder;
|
|
10941
11255
|
if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
|
|
10942
11256
|
} else {
|
|
10943
|
-
const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
|
|
11257
|
+
const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...powder ? { powder } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
|
|
10944
11258
|
entries.push(e);
|
|
10945
11259
|
if (explicit) byKey.set(explicit, e);
|
|
10946
11260
|
}
|
|
@@ -11015,6 +11329,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11015
11329
|
}
|
|
11016
11330
|
if (e.tags) part.tags = [...e.tags];
|
|
11017
11331
|
if (e.contains) part.contains = { ...e.contains };
|
|
11332
|
+
if (e.powder) part.powder = { ...e.powder };
|
|
11018
11333
|
const ao = options.anchorBelow ?? -1;
|
|
11019
11334
|
if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
|
|
11020
11335
|
parts.push(part);
|
|
@@ -11192,6 +11507,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11192
11507
|
var SKIN_SLACK = 5e-4;
|
|
11193
11508
|
var SKIN_LAYER_SLACK = 4;
|
|
11194
11509
|
var NEAREST_MARGIN = 1.5;
|
|
11510
|
+
var basePositions = (host) => {
|
|
11511
|
+
const p0 = host.mesh.userData._p0;
|
|
11512
|
+
if (p0) return p0;
|
|
11513
|
+
const op = positionAttr(host.origGeometry);
|
|
11514
|
+
return op ? op.array : null;
|
|
11515
|
+
};
|
|
11516
|
+
var collapseForeign = (mesh2, arr) => {
|
|
11517
|
+
for (const [v0, v1] of mesh2.userData.collapsedRanges ?? []) {
|
|
11518
|
+
for (let k = v0 + 1; k < v1; k++) {
|
|
11519
|
+
arr[k * 3] = arr[v0 * 3];
|
|
11520
|
+
arr[k * 3 + 1] = arr[v0 * 3 + 1];
|
|
11521
|
+
arr[k * 3 + 2] = arr[v0 * 3 + 2];
|
|
11522
|
+
}
|
|
11523
|
+
}
|
|
11524
|
+
};
|
|
11525
|
+
var DECAL_SNAP = 0.03;
|
|
11195
11526
|
var Visuals = class {
|
|
11196
11527
|
constructor(THREE, lowQuality, decalCap) {
|
|
11197
11528
|
__publicField(this, "THREE", THREE);
|
|
@@ -11201,6 +11532,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11201
11532
|
__publicField(this, "a");
|
|
11202
11533
|
__publicField(this, "b");
|
|
11203
11534
|
__publicField(this, "m4");
|
|
11535
|
+
__publicField(this, "ray");
|
|
11204
11536
|
__publicField(this, "box");
|
|
11205
11537
|
__publicField(this, "t");
|
|
11206
11538
|
__publicField(this, "lbox");
|
|
@@ -11211,6 +11543,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11211
11543
|
this.a = tmpV(THREE);
|
|
11212
11544
|
this.b = tmpV(THREE);
|
|
11213
11545
|
this.m4 = new THREE.Matrix4();
|
|
11546
|
+
this.ray = new THREE.Raycaster();
|
|
11214
11547
|
this.box = new THREE.Box3();
|
|
11215
11548
|
this.t = tmpV(THREE);
|
|
11216
11549
|
this.lbox = new THREE.Box3();
|
|
@@ -11364,7 +11697,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11364
11697
|
host.ownGeo = true;
|
|
11365
11698
|
const g = host.mesh.geometry.clone();
|
|
11366
11699
|
host.mesh.geometry = g;
|
|
11367
|
-
host.work = new Float32Array(positionAttr(g).array);
|
|
11700
|
+
host.work = new Float32Array(basePositions(host) ?? positionAttr(g).array);
|
|
11368
11701
|
return true;
|
|
11369
11702
|
}
|
|
11370
11703
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
@@ -11534,12 +11867,38 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11534
11867
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
11535
11868
|
// Decals
|
|
11536
11869
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
11870
|
+
/**
|
|
11871
|
+
* The point on the drawn face of `mesh` under a world point, within `DECAL_SNAP` metres along the normal: the world's box of a part and the face
|
|
11872
|
+
* the renderer drew (a coat of plaster, a merged house mesh) can be a few millimetres apart, and a mark behind the drawn face does not show.
|
|
11873
|
+
*/
|
|
11874
|
+
onFace(mesh2, worldPoint, worldNormal) {
|
|
11875
|
+
if (!mesh2.isMesh || !mesh2.geometry?.attributes?.position) return worldPoint;
|
|
11876
|
+
const n = worldNormal.clone().normalize();
|
|
11877
|
+
this.ray.set(worldPoint.clone().addScaledVector(n, DECAL_SNAP), n.clone().negate());
|
|
11878
|
+
this.ray.near = 0;
|
|
11879
|
+
this.ray.far = 2 * DECAL_SNAP;
|
|
11880
|
+
const hits = [];
|
|
11881
|
+
mesh2.raycast(this.ray, hits);
|
|
11882
|
+
for (const c of mesh2.parent?.children ?? []) {
|
|
11883
|
+
if (c === mesh2 || c.name !== "breach-shell") continue;
|
|
11884
|
+
c.raycast(this.ray, hits);
|
|
11885
|
+
const behind = c.userData.behind?.();
|
|
11886
|
+
if (behind) behind.raycast(this.ray, hits);
|
|
11887
|
+
}
|
|
11888
|
+
let best = null;
|
|
11889
|
+
for (const h of hits) {
|
|
11890
|
+
if (h.object.userData.cutAt?.(h.point)) continue;
|
|
11891
|
+
if (h.face && h.face.normal.clone().transformDirection(h.object.matrixWorld).dot(n) <= 0.3) continue;
|
|
11892
|
+
if (!best || Math.abs(h.distance - DECAL_SNAP) < Math.abs(best.distance - DECAL_SNAP)) best = h;
|
|
11893
|
+
}
|
|
11894
|
+
return best ? best.point : worldPoint;
|
|
11895
|
+
}
|
|
11537
11896
|
/** Decal pose in mesh space: position and a rotation taking +Z to the surface normal. */
|
|
11538
11897
|
decalPose(mesh2, worldPoint, worldNormal, lift) {
|
|
11539
11898
|
mesh2.updateWorldMatrix(true, false);
|
|
11540
11899
|
const s = mesh2.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
11541
11900
|
const ln = worldNormal.clone().transformDirection(this.m4.copy(mesh2.matrixWorld).invert()).normalize();
|
|
11542
|
-
const pos = mesh2.worldToLocal(worldPoint.clone()).addScaledVector(ln, lift / s);
|
|
11901
|
+
const pos = mesh2.worldToLocal(this.onFace(mesh2, worldPoint, worldNormal).clone()).addScaledVector(ln, lift / s);
|
|
11543
11902
|
const quat2 = new this.THREE.Quaternion().setFromUnitVectors(new this.THREE.Vector3(0, 0, 1), ln);
|
|
11544
11903
|
return { s, pos, quat: quat2 };
|
|
11545
11904
|
}
|
|
@@ -11559,6 +11918,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11559
11918
|
d.geometry?.dispose();
|
|
11560
11919
|
this.decalCount = Math.max(0, this.decalCount - 1);
|
|
11561
11920
|
}
|
|
11921
|
+
/**
|
|
11922
|
+
* Takes away the decals of a mesh that sit inside a box (world `center`, `half` extents along the axes of the world `rotation`): the surface they
|
|
11923
|
+
* marked is gone (a unit came out of the wall with its bullet holes and spalls).
|
|
11924
|
+
*/
|
|
11925
|
+
clearDecals(mv, center, half, rotation) {
|
|
11926
|
+
const inv = rotation.clone().invert();
|
|
11927
|
+
const p = new this.THREE.Vector3();
|
|
11928
|
+
for (let i = mv.decals.length - 1; i >= 0; i--) {
|
|
11929
|
+
const d = mv.decals[i];
|
|
11930
|
+
d.obj.getWorldPosition(p).sub(center).applyQuaternion(inv);
|
|
11931
|
+
if (Math.abs(p.x) > half.x || Math.abs(p.y) > half.y || Math.abs(p.z) > half.z) continue;
|
|
11932
|
+
this.disposeDecal(d);
|
|
11933
|
+
mv.decals.splice(i, 1);
|
|
11934
|
+
}
|
|
11935
|
+
}
|
|
11562
11936
|
addHole(mv, worldPoint, worldNormal, diameter) {
|
|
11563
11937
|
const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
|
|
11564
11938
|
const work = mv.host ? mv.host.work : mv.work;
|
|
@@ -11763,6 +12137,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11763
12137
|
if (!pos || !work) return;
|
|
11764
12138
|
const arr = pos.array;
|
|
11765
12139
|
arr.set(work);
|
|
12140
|
+
collapseForeign(host.mesh, arr);
|
|
11766
12141
|
for (const s of host.sharers) {
|
|
11767
12142
|
if (s.merged && s.merged.host === host) {
|
|
11768
12143
|
if (s.collapsed) this.collapseRange(arr, s.merged.range);
|
|
@@ -11967,9 +12342,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11967
12342
|
mv.touched = false;
|
|
11968
12343
|
if (!host.ownGeo) return;
|
|
11969
12344
|
const work = host.work;
|
|
11970
|
-
const op =
|
|
12345
|
+
const op = basePositions(host);
|
|
11971
12346
|
const [v0, v1] = merged.range;
|
|
11972
|
-
if (op) work.set(op.
|
|
12347
|
+
if (op) work.set(op.subarray(v0 * 3, v1 * 3), v0 * 3);
|
|
11973
12348
|
const attr = host.mesh.geometry.getAttribute("color");
|
|
11974
12349
|
if (attr) {
|
|
11975
12350
|
const orig = host.origGeometry.getAttribute("color");
|
|
@@ -11983,6 +12358,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
11983
12358
|
if (host.sharers.every((s) => !s.touched)) {
|
|
11984
12359
|
host.mesh.geometry.dispose();
|
|
11985
12360
|
host.mesh.geometry = host.origGeometry;
|
|
12361
|
+
const base = basePositions(host), back = positionAttr(host.origGeometry);
|
|
12362
|
+
if (base && back) {
|
|
12363
|
+
const arr = back.array;
|
|
12364
|
+
arr.set(base);
|
|
12365
|
+
collapseForeign(host.mesh, arr);
|
|
12366
|
+
back.needsUpdate = true;
|
|
12367
|
+
}
|
|
11986
12368
|
host.work = null;
|
|
11987
12369
|
host.ownGeo = false;
|
|
11988
12370
|
if (host.ownMat) {
|
|
@@ -12041,9 +12423,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12041
12423
|
// src/renderer.ts
|
|
12042
12424
|
var rendererCount = 0;
|
|
12043
12425
|
var COLLAPSE_CLOUD_KG = 1e3;
|
|
12426
|
+
var CHIP_DECAL_EDGE = 0.05;
|
|
12427
|
+
var OPAQUE_CRACK = 0.1;
|
|
12428
|
+
var WOOD_RIM = 1.5;
|
|
12044
12429
|
var IMPACT_TAGS = ["metal", "glass", "wood"];
|
|
12045
12430
|
var GLOW_FROM_C = 430;
|
|
12046
12431
|
var QUEUE_CAP = 4e3;
|
|
12432
|
+
var MIN_FRAGMENTS_PER_SLICE = 8;
|
|
12047
12433
|
var STATIC_REFRESH = 0.5;
|
|
12048
12434
|
var STATIC_SPAN = 3;
|
|
12049
12435
|
var FEET_BAND = 0.03;
|
|
@@ -12119,6 +12505,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12119
12505
|
const r = v;
|
|
12120
12506
|
return !!r.mesh && Array.isArray(r.range);
|
|
12121
12507
|
}
|
|
12508
|
+
var RNG_SEED = 12648430;
|
|
12122
12509
|
function createDestructionRenderer(THREE, options) {
|
|
12123
12510
|
return new Renderer(THREE, options);
|
|
12124
12511
|
}
|
|
@@ -12134,6 +12521,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12134
12521
|
__publicField(this, "gravity");
|
|
12135
12522
|
__publicField(this, "autoSplit");
|
|
12136
12523
|
__publicField(this, "maxFragments");
|
|
12524
|
+
__publicField(this, "eventBudgetMs");
|
|
12137
12525
|
__publicField(this, "bounds", /* @__PURE__ */ new Map());
|
|
12138
12526
|
/** World boxes (min xyz, max xyz) of the drawn meshes of the bound objects that are not pieces, for the pieces to land on. */
|
|
12139
12527
|
__publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
|
|
@@ -12144,6 +12532,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12144
12532
|
__publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
|
|
12145
12533
|
__publicField(this, "staticAt", 0);
|
|
12146
12534
|
__publicField(this, "queue", []);
|
|
12535
|
+
/** Presentation work an event left for later frames (fragments still to spawn, loose groups still to split), run in order within the frame budget. */
|
|
12536
|
+
__publicField(this, "slices", []);
|
|
12537
|
+
/** When the work of this frame must stop; `Infinity` outside `drainQueue`, where a call finishes what it started. */
|
|
12538
|
+
__publicField(this, "sliceDeadline", Infinity);
|
|
12147
12539
|
__publicField(this, "unsubscribe");
|
|
12148
12540
|
__publicField(this, "visuals");
|
|
12149
12541
|
__publicField(this, "fragments");
|
|
@@ -12157,7 +12549,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12157
12549
|
__publicField(this, "leaks", []);
|
|
12158
12550
|
__publicField(this, "homeTags", []);
|
|
12159
12551
|
__publicField(this, "throttles", /* @__PURE__ */ new Map());
|
|
12160
|
-
__publicField(this, "rng", createRng(
|
|
12552
|
+
__publicField(this, "rng", createRng(RNG_SEED));
|
|
12161
12553
|
__publicField(this, "vfx");
|
|
12162
12554
|
/** Names this renderer to the world's drips, so a shared world tells each renderer where its own landed. */
|
|
12163
12555
|
__publicField(this, "owner", `destruction-${++rendererCount}`);
|
|
@@ -12181,6 +12573,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12181
12573
|
this.gravity = o.gravity ?? 9.81;
|
|
12182
12574
|
this.autoSplit = o.autoSplit !== false;
|
|
12183
12575
|
this.maxFragments = o.maxFragments ?? 160;
|
|
12576
|
+
this.eventBudgetMs = o.eventBudgetMs ?? Infinity;
|
|
12184
12577
|
this.box = new THREE.Box3();
|
|
12185
12578
|
this.tmpQ = new THREE.Quaternion();
|
|
12186
12579
|
this.v1 = new THREE.Vector3();
|
|
@@ -12266,6 +12659,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12266
12659
|
if (leftovers.length) this.matchByBox(objectId, root, leftovers, add2);
|
|
12267
12660
|
}
|
|
12268
12661
|
const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
|
|
12662
|
+
if (options.onFrame) bound.onFrame = options.onFrame;
|
|
12269
12663
|
const hosts = /* @__PURE__ */ new Map();
|
|
12270
12664
|
for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? [], hosts));
|
|
12271
12665
|
for (const [pid, meshes] of byPart) {
|
|
@@ -12425,6 +12819,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12425
12819
|
dt = clamp6(dt, 0, 0.1);
|
|
12426
12820
|
this.time += dt;
|
|
12427
12821
|
this.drainQueue();
|
|
12822
|
+
for (const b of this.bounds.values()) b.onFrame?.(dt);
|
|
12428
12823
|
for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
|
|
12429
12824
|
this.updateActive(dt);
|
|
12430
12825
|
this.updateLeaks(dt);
|
|
@@ -12436,11 +12831,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12436
12831
|
}
|
|
12437
12832
|
drainQueue() {
|
|
12438
12833
|
const batch = this.queue.splice(0, this.queue.length);
|
|
12439
|
-
|
|
12834
|
+
const deadline = performance.now() + this.eventBudgetMs;
|
|
12835
|
+
this.sliceDeadline = deadline;
|
|
12836
|
+
try {
|
|
12837
|
+
this.runSlices(deadline);
|
|
12838
|
+
for (let i = 0; i < batch.length; i++) {
|
|
12839
|
+
if (i > 0 && performance.now() >= deadline) {
|
|
12840
|
+
this.queue.unshift(...batch.slice(i));
|
|
12841
|
+
return;
|
|
12842
|
+
}
|
|
12843
|
+
try {
|
|
12844
|
+
this.apply(batch[i]);
|
|
12845
|
+
} catch (err) {
|
|
12846
|
+
if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", batch[i].type, err);
|
|
12847
|
+
}
|
|
12848
|
+
}
|
|
12849
|
+
} finally {
|
|
12850
|
+
this.sliceDeadline = Infinity;
|
|
12851
|
+
}
|
|
12852
|
+
}
|
|
12853
|
+
/** Runs the work left by earlier frames until the deadline; at least one piece per frame, so it always finishes. */
|
|
12854
|
+
runSlices(deadline) {
|
|
12855
|
+
for (let ran = 0; this.slices.length > 0 && (ran === 0 || performance.now() < deadline); ran++) {
|
|
12440
12856
|
try {
|
|
12441
|
-
this.
|
|
12857
|
+
this.slices.shift()();
|
|
12442
12858
|
} catch (err) {
|
|
12443
|
-
if (typeof console !== "undefined") console.error("game-destruction-three: failed to
|
|
12859
|
+
if (typeof console !== "undefined") console.error("game-destruction-three: failed to run deferred work", err);
|
|
12444
12860
|
}
|
|
12445
12861
|
}
|
|
12446
12862
|
}
|
|
@@ -12604,7 +13020,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12604
13020
|
break;
|
|
12605
13021
|
}
|
|
12606
13022
|
case "debris":
|
|
12607
|
-
this.vfx.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part));
|
|
13023
|
+
this.vfx.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part, ev.material));
|
|
12608
13024
|
break;
|
|
12609
13025
|
case "fracture":
|
|
12610
13026
|
this.onFracture(ev);
|
|
@@ -12777,10 +13193,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12777
13193
|
}
|
|
12778
13194
|
return def ? fallbackColor(def.id, def) : null;
|
|
12779
13195
|
}
|
|
12780
|
-
debrisOptions(kind, objectId, partId) {
|
|
13196
|
+
debrisOptions(kind, objectId, partId, material) {
|
|
12781
13197
|
const pv = this.partVis(objectId, partId);
|
|
12782
|
-
const def =
|
|
13198
|
+
const def = this.world.materials.get(material ?? pv?.material ?? "");
|
|
12783
13199
|
if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
|
|
13200
|
+
if ((kind === "dust" || kind === "chips") && def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
|
|
12784
13201
|
if (kind === "fibers" || kind === "leaves") {
|
|
12785
13202
|
if (def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
|
|
12786
13203
|
const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
|
|
@@ -12837,12 +13254,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12837
13254
|
const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
|
|
12838
13255
|
this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
|
|
12839
13256
|
} else {
|
|
13257
|
+
const fresh = def?.penetrationClass === "wood" ? def.visual?.innerColor : void 0;
|
|
13258
|
+
if (fresh !== void 0) this.visuals.addSpall(mv, wp, normal, Math.max(6e-3, ev.hole.diameter * WOOD_RIM), fresh);
|
|
12840
13259
|
this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
|
|
12841
13260
|
}
|
|
12842
13261
|
};
|
|
12843
13262
|
draw(ev.worldPoint, n);
|
|
12844
13263
|
if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
|
|
12845
13264
|
if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
|
|
13265
|
+
if (ev.powder) {
|
|
13266
|
+
const puff3 = { color: ev.powder.color, size: 0.6, life: 1.6, alpha: 1.3 };
|
|
13267
|
+
this.vfx.burst("dust", ev.worldPoint, [n.x, n.y, n.z], this.low ? 5 : 10, 0.7, puff3);
|
|
13268
|
+
if (ev.hole.through && ev.exitPoint) this.vfx.burst("dust", ev.exitPoint, [-n.x, -n.y, -n.z], this.low ? 10 : 20, 1.3, puff3);
|
|
13269
|
+
}
|
|
12846
13270
|
this.sound(impactKey(def), ev.worldPoint, 0.6);
|
|
12847
13271
|
}
|
|
12848
13272
|
onCrack(ev) {
|
|
@@ -12853,8 +13277,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12853
13277
|
const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
|
|
12854
13278
|
const mv = this.nearestMesh(pv, wp);
|
|
12855
13279
|
if (mv) {
|
|
12856
|
-
const
|
|
12857
|
-
|
|
13280
|
+
const glass = !!def?.visual?.transparent;
|
|
13281
|
+
const own = this.partColor(pv, def) ?? fallbackColor(pv.material, def);
|
|
13282
|
+
const color = glass ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
|
|
13283
|
+
const length = glass ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
|
|
13284
|
+
this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, length, color, hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]));
|
|
12858
13285
|
}
|
|
12859
13286
|
this.sound(impactKey(def), ev.worldPoint, 0.5);
|
|
12860
13287
|
}
|
|
@@ -12888,8 +13315,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12888
13315
|
const mv = this.nearestMesh(pv, wp);
|
|
12889
13316
|
if (!mv) return;
|
|
12890
13317
|
const def = this.world.materials.get(pv.material);
|
|
12891
|
-
|
|
12892
|
-
|
|
13318
|
+
const normal = this.faceNormal(mv, wp);
|
|
13319
|
+
const core = def?.visual?.innerColor;
|
|
13320
|
+
if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
|
|
13321
|
+
else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
|
|
13322
|
+
if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
|
|
13323
|
+
this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
|
|
12893
13324
|
}
|
|
12894
13325
|
/**
|
|
12895
13326
|
* Stage 2 of a staged part: the unit-sized fragments come out and the part stays drawn (hiding it is what `fracture` does). The opening itself
|
|
@@ -12912,8 +13343,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12912
13343
|
this.sound(impactKey(def), ev.opening.center, 0.8);
|
|
12913
13344
|
}
|
|
12914
13345
|
/**
|
|
12915
|
-
* A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust
|
|
12916
|
-
* the
|
|
13346
|
+
* A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust, and the marks drawn on it (bullet holes, spalls, the impact
|
|
13347
|
+
* marks of the effects) go with it. The gap itself is the drawing's (the house renderer cuts the wall where the unit was); a part drawn as one box
|
|
13348
|
+
* keeps its box.
|
|
12917
13349
|
*/
|
|
12918
13350
|
onChip(ev) {
|
|
12919
13351
|
const pv = this.partVis(ev.object, ev.part);
|
|
@@ -12925,7 +13357,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12925
13357
|
const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
|
|
12926
13358
|
this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone, false), ev.object, maxAlive);
|
|
12927
13359
|
}
|
|
12928
|
-
if (
|
|
13360
|
+
if (ev.torn) {
|
|
13361
|
+
this.sound(impactKey(def), ev.center, 0.5);
|
|
13362
|
+
return;
|
|
13363
|
+
}
|
|
13364
|
+
const T = this.THREE;
|
|
13365
|
+
const center = new T.Vector3(ev.center[0], ev.center[1], ev.center[2]);
|
|
13366
|
+
const half = new T.Vector3(ev.size[0] / 2 + CHIP_DECAL_EDGE, ev.size[1] / 2 + CHIP_DECAL_EDGE, ev.size[2] / 2 + CHIP_DECAL_EDGE);
|
|
13367
|
+
const rotation = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
|
|
13368
|
+
for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation);
|
|
13369
|
+
this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
|
|
13370
|
+
this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
|
|
12929
13371
|
this.sound(impactKey(def), ev.center, 0.7);
|
|
12930
13372
|
}
|
|
12931
13373
|
onFracture(ev) {
|
|
@@ -12943,15 +13385,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12943
13385
|
if (m && m.transparent) opacity = clamp6(m.opacity, 0.2, 0.8);
|
|
12944
13386
|
this.visuals.hide(pv, "broken");
|
|
12945
13387
|
}
|
|
12946
|
-
|
|
12947
|
-
const step = this.low ? 2 : 1;
|
|
12948
|
-
for (let i = 0; i < ev.fragments.length; i += step) {
|
|
12949
|
-
const fr = ev.fragments[i];
|
|
12950
|
-
const fdef = this.world.materials.get(fr.material);
|
|
12951
|
-
const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def || !fdef ? color : fallbackColor(fr.material, fdef);
|
|
12952
|
-
const mat = this.fragments.materialFor(fr.material, tone, transparent || fdef?.visual?.transparent === true, opacity);
|
|
12953
|
-
this.fragments.spawn(fr, mat, ev.object, maxAlive);
|
|
12954
|
-
}
|
|
13388
|
+
this.spawnFragments(ev, 0, { def, color, opacity, transparent });
|
|
12955
13389
|
const up = [0, 1, 0];
|
|
12956
13390
|
const sizeK = clamp6(ev.fragments.length / 10, 0.5, 2);
|
|
12957
13391
|
switch (ev.style) {
|
|
@@ -12977,6 +13411,22 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12977
13411
|
}
|
|
12978
13412
|
this.sound(fractureKey(def, ev.style), ev.point, clamp6(0.4 + ev.fragments.length * 0.03, 0.4, 1));
|
|
12979
13413
|
}
|
|
13414
|
+
/** Spawns the fragments of a fracture from index `from`; what does not fit in this frame's budget is spawned by the next frames. */
|
|
13415
|
+
spawnFragments(ev, from, look) {
|
|
13416
|
+
const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
|
|
13417
|
+
const step = this.low ? 2 : 1;
|
|
13418
|
+
for (let i = from; i < ev.fragments.length; i += step) {
|
|
13419
|
+
if (i - from >= MIN_FRAGMENTS_PER_SLICE * step && performance.now() >= this.sliceDeadline) {
|
|
13420
|
+
this.slices.push(() => this.spawnFragments(ev, i, look));
|
|
13421
|
+
return;
|
|
13422
|
+
}
|
|
13423
|
+
const fr = ev.fragments[i];
|
|
13424
|
+
const fdef = this.world.materials.get(fr.material);
|
|
13425
|
+
const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === look.def || !fdef ? look.color : fallbackColor(fr.material, fdef);
|
|
13426
|
+
const mat = this.fragments.materialFor(fr.material, tone, look.transparent || fdef?.visual?.transparent === true, look.opacity);
|
|
13427
|
+
this.fragments.spawn(fr, mat, ev.object, maxAlive);
|
|
13428
|
+
}
|
|
13429
|
+
}
|
|
12980
13430
|
onDetach(ev) {
|
|
12981
13431
|
this.sound("detach", this.rootPoint(ev.object), 0.5);
|
|
12982
13432
|
this.splitLoose(ev.object);
|
|
@@ -12990,6 +13440,13 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
12990
13440
|
if (!st || st.groups.length < 2) return;
|
|
12991
13441
|
const loose = st.groups.length - 1;
|
|
12992
13442
|
for (let i = 0; i < loose; i++) {
|
|
13443
|
+
if (i > 0 && performance.now() >= this.sliceDeadline) {
|
|
13444
|
+
this.slices.push(() => {
|
|
13445
|
+
this.staticDirty.add(objectId);
|
|
13446
|
+
this.splitLoose(objectId);
|
|
13447
|
+
});
|
|
13448
|
+
return;
|
|
13449
|
+
}
|
|
12993
13450
|
let newId;
|
|
12994
13451
|
try {
|
|
12995
13452
|
newId = this.world.splitGroup(objectId, 1);
|
|
@@ -13006,8 +13463,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
13006
13463
|
old.mass = Math.max(0.05, this.massOf(objectId));
|
|
13007
13464
|
old.sleeping = false;
|
|
13008
13465
|
} else if (kitOriginOf(b.root)?.kind === "furniture") {
|
|
13009
|
-
this.makePiece(objectId);
|
|
13466
|
+
const piece = this.makePiece(objectId);
|
|
13010
13467
|
this.tipIfTopHeavy(b);
|
|
13468
|
+
this.o.onLoosened?.(objectId, piece, "apart");
|
|
13011
13469
|
}
|
|
13012
13470
|
}
|
|
13013
13471
|
/** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
|
|
@@ -13116,7 +13574,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
13116
13574
|
const piece = this.makePiece(b.id, { mass });
|
|
13117
13575
|
const body = this.pieceSys.get(b.id);
|
|
13118
13576
|
if (!body) continue;
|
|
13119
|
-
this.o.onLoosened?.(b.id, piece);
|
|
13577
|
+
this.o.onLoosened?.(b.id, piece, "blast");
|
|
13120
13578
|
const spread = half[0];
|
|
13121
13579
|
const momentum = hit.speed * mass;
|
|
13122
13580
|
this.pieceSys.impulse(body, [hit.direction[0] * momentum, hit.direction[1] * momentum, hit.direction[2] * momentum], [world.x + (this.rng() - 0.5) * spread, world.y + spread * 0.5, world.z + (this.rng() - 0.5) * spread]);
|
|
@@ -13311,6 +13769,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
13311
13769
|
}
|
|
13312
13770
|
clear() {
|
|
13313
13771
|
this.queue.length = 0;
|
|
13772
|
+
this.slices.length = 0;
|
|
13314
13773
|
this.active.clear();
|
|
13315
13774
|
for (const key of [...this.fires.keys()]) this.stopFire(key);
|
|
13316
13775
|
this.leaks.length = 0;
|
|
@@ -13330,6 +13789,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
13330
13789
|
this.vfx.sprites().clear();
|
|
13331
13790
|
this.ownVfx?.world.clear();
|
|
13332
13791
|
}
|
|
13792
|
+
reset() {
|
|
13793
|
+
this.clear();
|
|
13794
|
+
this.rng = createRng(RNG_SEED);
|
|
13795
|
+
this.time = 0;
|
|
13796
|
+
this.staticAt = 0;
|
|
13797
|
+
this.throttles.clear();
|
|
13798
|
+
this.fragments.reset();
|
|
13799
|
+
this.pieceSys.reset();
|
|
13800
|
+
}
|
|
13333
13801
|
wakeRegion(x, z, radius) {
|
|
13334
13802
|
this.fragments.wakeRegion(x, z, radius);
|
|
13335
13803
|
this.pieceSys.wakeRegion(x, z, radius);
|
|
@@ -13634,7 +14102,18 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
13634
14102
|
drag("drag_masonry", [sceneNoise({ type: "bandpass", f: 260, q: 0.9, d: 0.3, vol: 0.62 }), ...sceneMany(3, () => sceneNoise({ type: "bandpass", f: [600, 1200], q: 2, d: 0.04, vol: 0.14, delay: [0, 0.2] }))]),
|
|
13635
14103
|
drag("drag_soft", [sceneNoise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
|
|
13636
14104
|
];
|
|
13637
|
-
var
|
|
14105
|
+
var leaf = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a door, lid or drawer worked by hand"] });
|
|
14106
|
+
var LEAF_SOUNDS = [
|
|
14107
|
+
leaf("latch", [sceneNoise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.35 }), sceneTone({ f: 1900, f2: 1500, d: 0.025, vol: 0.06, delay: 0.035 })]),
|
|
14108
|
+
leaf("creak_wood", [sceneTone({ f: [190, 240], f2: 310, d: 0.5, vol: 0.05 }), sceneNoise({ type: "bandpass", f: 520, q: 6, a: 0.05, d: 0.45, vol: 0.12 })]),
|
|
14109
|
+
leaf("creak_metal", [sceneTone({ f: [620, 760], f2: 900, d: 0.35, vol: 0.035 }), sceneNoise({ type: "bandpass", f: 1400, q: 8, a: 0.04, d: 0.3, vol: 0.08 })]),
|
|
14110
|
+
leaf("shut_wood", [sceneTone({ f: 120, f2: 60, d: 0.14, vol: 0.5 }), sceneNoise({ type: "bandpass", f: 600, q: 1.2, d: 0.1, vol: 0.55, wet: 0.25 }), sceneNoise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.25, delay: 0.02 })]),
|
|
14111
|
+
leaf("shut_metal", [sceneTone({ f: 160, f2: 85, d: 0.18, vol: 0.55 }), sceneNoise({ type: "bandpass", f: 900, q: 1.6, d: 0.12, vol: 0.45, wet: 0.3 }), sceneTone({ f: 1250, f2: 1150, d: 0.12, vol: 0.06 })]),
|
|
14112
|
+
leaf("shut_soft", [sceneTone({ f: 95, f2: 55, d: 0.12, vol: 0.35 }), sceneNoise({ type: "lowpass", f: 500, d: 0.1, vol: 0.3 })]),
|
|
14113
|
+
leaf("rattle", sceneMany(5, () => sceneNoise({ type: "bandpass", f: [1600, 2600], q: 3, d: 0.035, vol: 0.32, delay: [0, 0.45] }))),
|
|
14114
|
+
leaf("slide", [sceneNoise({ type: "bandpass", f: 700, q: 2, a: 0.04, d: 0.35, vol: 0.22 }), sceneNoise({ type: "bandpass", f: 2200, q: 5, d: 0.3, vol: 0.05 })])
|
|
14115
|
+
];
|
|
14116
|
+
var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
|
|
13638
14117
|
|
|
13639
14118
|
// src/audio/sound.ts
|
|
13640
14119
|
var DESTRUCTION_SOUND_KEYS = {
|
|
@@ -13649,6 +14128,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
|
|
|
13649
14128
|
drag_glass: { sound: "drag_glass" },
|
|
13650
14129
|
drag_masonry: { sound: "drag_masonry" },
|
|
13651
14130
|
drag_soft: { sound: "drag_soft" },
|
|
14131
|
+
// a leaf worked by hand (doors, lids, drawers): the latch, the hinge turning, the leaf meeting the frame, a locked one shaken
|
|
14132
|
+
leaf_latch: { sound: "latch" },
|
|
14133
|
+
leaf_creak_wood: { sound: "creak_wood" },
|
|
14134
|
+
leaf_creak_metal: { sound: "creak_metal" },
|
|
14135
|
+
leaf_shut_wood: { sound: "shut_wood" },
|
|
14136
|
+
leaf_shut_metal: { sound: "shut_metal" },
|
|
14137
|
+
leaf_shut_glass: { sound: "shut_metal", volumeScale: 0.6 },
|
|
14138
|
+
leaf_shut_masonry: { sound: "shut_wood" },
|
|
14139
|
+
leaf_shut_soft: { sound: "shut_soft" },
|
|
14140
|
+
leaf_rattle: { sound: "rattle" },
|
|
14141
|
+
leaf_slide: { sound: "slide" },
|
|
14142
|
+
// the keys of the item clips (furniture and vehicle doors, hoods, trunks, drawers)
|
|
14143
|
+
door_latch: { sound: "latch" },
|
|
14144
|
+
hood_latch: { sound: "latch" },
|
|
14145
|
+
trunk_latch: { sound: "latch" },
|
|
14146
|
+
door_open: { sound: "latch" },
|
|
14147
|
+
door_shut: { sound: "shut_metal" },
|
|
14148
|
+
hood_shut: { sound: "shut_metal" },
|
|
14149
|
+
trunk_shut: { sound: "shut_metal" },
|
|
14150
|
+
door_close: { sound: "shut_wood", volumeScale: 0.7 },
|
|
14151
|
+
drawer_open: { sound: "slide" },
|
|
14152
|
+
drawer_close: { sound: "slide" },
|
|
14153
|
+
lid_open: { sound: "latch" },
|
|
14154
|
+
lid_close: { sound: "shut_soft" },
|
|
14155
|
+
flap_open: { sound: "latch" },
|
|
14156
|
+
flap_close: { sound: "shut_soft" },
|
|
13652
14157
|
shatter_glass: { sound: "glass_shatter" },
|
|
13653
14158
|
crumble: { sound: "crumble" },
|
|
13654
14159
|
splinter: { sound: "crack" },
|