@adatechnology/game-destruction-three 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/index.d.ts +19 -3
- package/dist/index.global.js +425 -37
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +425 -37
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1255,6 +1255,8 @@ var Visuals = class {
|
|
|
1255
1255
|
__publicField(this, "b");
|
|
1256
1256
|
__publicField(this, "m4");
|
|
1257
1257
|
__publicField(this, "box");
|
|
1258
|
+
__publicField(this, "t");
|
|
1259
|
+
__publicField(this, "lbox");
|
|
1258
1260
|
/** Total live decals across meshes (kept in sync by add/remove). */
|
|
1259
1261
|
__publicField(this, "decalCount", 0);
|
|
1260
1262
|
__publicField(this, "tints", /* @__PURE__ */ new WeakMap());
|
|
@@ -1263,9 +1265,22 @@ var Visuals = class {
|
|
|
1263
1265
|
this.b = tmpV(THREE);
|
|
1264
1266
|
this.m4 = new THREE.Matrix4();
|
|
1265
1267
|
this.box = new THREE.Box3();
|
|
1268
|
+
this.t = tmpV(THREE);
|
|
1269
|
+
this.lbox = new THREE.Box3();
|
|
1266
1270
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1271
|
+
/**
|
|
1272
|
+
* @param range vertex range `[v0, v1)` of a merged mesh this part is drawn by (see `MeshRange`); `hosts` caches the shared state per mesh for one `bind`.
|
|
1273
|
+
*/
|
|
1274
|
+
makeMeshVis(mesh, home, range, hosts) {
|
|
1275
|
+
let host = null;
|
|
1276
|
+
if (range) {
|
|
1277
|
+
host = hosts?.get(mesh) ?? null;
|
|
1278
|
+
if (!host) {
|
|
1279
|
+
host = { mesh, origGeometry: mesh.geometry, origMaterial: mesh.material, ownGeo: false, work: null, ownMat: false, sharers: [] };
|
|
1280
|
+
hosts?.set(mesh, host);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
const mv = {
|
|
1269
1284
|
mesh,
|
|
1270
1285
|
home,
|
|
1271
1286
|
origGeometry: mesh.geometry,
|
|
@@ -1282,31 +1297,72 @@ var Visuals = class {
|
|
|
1282
1297
|
decals: [],
|
|
1283
1298
|
meltApplied: 0,
|
|
1284
1299
|
bottomY: 0,
|
|
1285
|
-
moved: false
|
|
1300
|
+
moved: false,
|
|
1301
|
+
host,
|
|
1302
|
+
range: range ? [range[0], range[1]] : null,
|
|
1303
|
+
merged: host && range ? { host, range: [range[0], range[1]] } : null,
|
|
1304
|
+
loose: false,
|
|
1305
|
+
collapsed: false,
|
|
1306
|
+
hidden: false,
|
|
1307
|
+
touched: false,
|
|
1308
|
+
charK: 0,
|
|
1309
|
+
charHex: 0,
|
|
1310
|
+
glowHex: 0,
|
|
1311
|
+
glowK: 0
|
|
1286
1312
|
};
|
|
1313
|
+
host?.sharers.push(mv);
|
|
1314
|
+
return mv;
|
|
1287
1315
|
}
|
|
1288
1316
|
vertexCount(mv) {
|
|
1317
|
+
if (mv.range) return mv.range[1] - mv.range[0];
|
|
1289
1318
|
return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
|
|
1290
1319
|
}
|
|
1320
|
+
/** Is the part drawn? (A ranged part shares its mesh, so `mesh.visible` says nothing about it.) */
|
|
1321
|
+
shown(mv) {
|
|
1322
|
+
return mv.host ? !mv.hidden : mv.mesh.visible;
|
|
1323
|
+
}
|
|
1324
|
+
/** Box of the part's vertices in the space of its mesh (the whole geometry for a plain mesh). */
|
|
1325
|
+
localBox(mv, out) {
|
|
1326
|
+
out.makeEmpty();
|
|
1327
|
+
if (!mv.host || !mv.range) {
|
|
1328
|
+
const geo = mv.mesh.geometry;
|
|
1329
|
+
if (!geo.boundingBox) geo.computeBoundingBox();
|
|
1330
|
+
return geo.boundingBox ? out.copy(geo.boundingBox) : out;
|
|
1331
|
+
}
|
|
1332
|
+
const arr = mv.host.work ?? positionAttr(mv.mesh.geometry)?.array;
|
|
1333
|
+
if (!arr) return out;
|
|
1334
|
+
for (let i = mv.range[0] * 3; i < mv.range[1] * 3; i += 3) out.expandByPoint(this.t.set(arr[i], arr[i + 1], arr[i + 2]));
|
|
1335
|
+
return out;
|
|
1336
|
+
}
|
|
1337
|
+
/** Box of the part in world space. */
|
|
1338
|
+
worldBox(mv, out) {
|
|
1339
|
+
if (!mv.host) return out.setFromObject(mv.mesh);
|
|
1340
|
+
this.localBox(mv, out);
|
|
1341
|
+
mv.mesh.updateWorldMatrix(true, false);
|
|
1342
|
+
return out.isEmpty() ? out : out.applyMatrix4(mv.mesh.matrixWorld);
|
|
1343
|
+
}
|
|
1291
1344
|
/**
|
|
1292
1345
|
* Average vertex colour of a mesh whose material uses `vertexColors` (computed once per mesh, from its original geometry), else null. Fragments
|
|
1293
1346
|
* and debris of such a mesh multiply it by `material.color`; without it they come out white.
|
|
1294
1347
|
*/
|
|
1295
1348
|
vertexTint(mv) {
|
|
1296
|
-
if (this.tints.has(mv
|
|
1349
|
+
if (this.tints.has(mv)) return this.tints.get(mv) ?? null;
|
|
1297
1350
|
let tint = null;
|
|
1298
1351
|
const used = materialsOf(mv.origMaterial).some((m) => m.vertexColors === true);
|
|
1299
1352
|
const attr = used ? mv.origGeometry.getAttribute("color") : void 0;
|
|
1300
1353
|
if (attr && attr.count > 0 && attr.itemSize >= 3) {
|
|
1354
|
+
const from = mv.merged ? mv.merged.range[0] : 0;
|
|
1355
|
+
const to = mv.merged ? Math.min(attr.count, mv.merged.range[1]) : attr.count;
|
|
1301
1356
|
let r = 0, g = 0, b = 0;
|
|
1302
|
-
for (let i =
|
|
1357
|
+
for (let i = from; i < to; i++) {
|
|
1303
1358
|
r += attr.getX(i);
|
|
1304
1359
|
g += attr.getY(i);
|
|
1305
1360
|
b += attr.getZ(i);
|
|
1306
1361
|
}
|
|
1307
|
-
|
|
1362
|
+
const n = Math.max(1, to - from);
|
|
1363
|
+
tint = new this.THREE.Color(r / n, g / n, b / n);
|
|
1308
1364
|
}
|
|
1309
|
-
this.tints.set(mv
|
|
1365
|
+
this.tints.set(mv, tint);
|
|
1310
1366
|
return tint;
|
|
1311
1367
|
}
|
|
1312
1368
|
/** Colour of the original material (before any char) times the vertex tint, `0xRRGGBB`, or -1. */
|
|
@@ -1330,6 +1386,7 @@ var Visuals = class {
|
|
|
1330
1386
|
}
|
|
1331
1387
|
/** Clone the geometry (vertex edits); false when its positions cannot be edited. */
|
|
1332
1388
|
ownGeometry(mv) {
|
|
1389
|
+
if (mv.host) return this.ownHostGeometry(mv.host);
|
|
1333
1390
|
if (mv.ownGeo) return true;
|
|
1334
1391
|
const pos = positionAttr(mv.mesh.geometry);
|
|
1335
1392
|
if (!pos) return false;
|
|
@@ -1339,6 +1396,16 @@ var Visuals = class {
|
|
|
1339
1396
|
mv.work = new Float32Array(positionAttr(g).array);
|
|
1340
1397
|
return true;
|
|
1341
1398
|
}
|
|
1399
|
+
ownHostGeometry(host) {
|
|
1400
|
+
if (host.ownGeo) return true;
|
|
1401
|
+
const pos = positionAttr(host.mesh.geometry);
|
|
1402
|
+
if (!pos) return false;
|
|
1403
|
+
host.ownGeo = true;
|
|
1404
|
+
const g = host.mesh.geometry.clone();
|
|
1405
|
+
host.mesh.geometry = g;
|
|
1406
|
+
host.work = new Float32Array(positionAttr(g).array);
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1342
1409
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
1343
1410
|
// Dents
|
|
1344
1411
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
@@ -1356,10 +1423,15 @@ var Visuals = class {
|
|
|
1356
1423
|
const r = dent.radius / s;
|
|
1357
1424
|
const depth = dent.depth / s;
|
|
1358
1425
|
const geo = mesh.geometry;
|
|
1359
|
-
|
|
1360
|
-
if (
|
|
1426
|
+
let bb;
|
|
1427
|
+
if (mv.host) bb = this.localBox(mv, this.lbox);
|
|
1428
|
+
else {
|
|
1429
|
+
if (!geo.boundingBox) geo.computeBoundingBox();
|
|
1430
|
+
bb = geo.boundingBox;
|
|
1431
|
+
}
|
|
1432
|
+
if (bb && !bb.isEmpty() && bb.distanceToPoint(lp) > r) return false;
|
|
1361
1433
|
if (!this.ownGeometry(mv)) return false;
|
|
1362
|
-
const work = mv.work;
|
|
1434
|
+
const work = mv.host ? mv.host.work : mv.work;
|
|
1363
1435
|
const pos = positionAttr(mesh.geometry);
|
|
1364
1436
|
const r2 = r * r;
|
|
1365
1437
|
let touched = false;
|
|
@@ -1369,7 +1441,9 @@ var Visuals = class {
|
|
|
1369
1441
|
const nx = ln.x;
|
|
1370
1442
|
const ny = ln.y;
|
|
1371
1443
|
const nz = ln.z;
|
|
1372
|
-
|
|
1444
|
+
const from = mv.range ? mv.range[0] * 3 : 0;
|
|
1445
|
+
const to = mv.range ? mv.range[1] * 3 : work.length;
|
|
1446
|
+
for (let i = from; i < to; i += 3) {
|
|
1373
1447
|
const dx = work[i] - px;
|
|
1374
1448
|
const dy = work[i + 1] - py;
|
|
1375
1449
|
const dz = work[i + 2] - pz;
|
|
@@ -1383,11 +1457,16 @@ var Visuals = class {
|
|
|
1383
1457
|
touched = true;
|
|
1384
1458
|
}
|
|
1385
1459
|
if (!touched) return false;
|
|
1460
|
+
mv.touched = true;
|
|
1386
1461
|
this.writePositions(mv, pos);
|
|
1387
1462
|
return true;
|
|
1388
1463
|
}
|
|
1389
1464
|
/** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
|
|
1390
1465
|
writePositions(mv, pos) {
|
|
1466
|
+
if (mv.host) {
|
|
1467
|
+
this.rebuildHost(mv.host);
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1391
1470
|
const work = mv.work;
|
|
1392
1471
|
const arr = pos.array;
|
|
1393
1472
|
const melt = mv.meltApplied;
|
|
@@ -1501,6 +1580,12 @@ var Visuals = class {
|
|
|
1501
1580
|
pv.charApplied = char;
|
|
1502
1581
|
const target = new this.THREE.Color(charColor);
|
|
1503
1582
|
for (const mv of pv.meshes) {
|
|
1583
|
+
if (mv.host) {
|
|
1584
|
+
mv.charK = clamp(char, 0, 1);
|
|
1585
|
+
mv.charHex = charColor;
|
|
1586
|
+
this.paint(mv);
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1504
1589
|
this.ownMaterial(mv);
|
|
1505
1590
|
const mats = materialsOf(mv.mesh.material);
|
|
1506
1591
|
mats.forEach((m, i) => {
|
|
@@ -1518,6 +1603,12 @@ var Visuals = class {
|
|
|
1518
1603
|
pv.glowApplied = key;
|
|
1519
1604
|
const intensity = hex === 0 ? 0 : clamp(0.35 + (temperatureC - 450) / 900, 0, 1.4);
|
|
1520
1605
|
for (const mv of pv.meshes) {
|
|
1606
|
+
if (mv.host) {
|
|
1607
|
+
mv.glowHex = hex;
|
|
1608
|
+
mv.glowK = intensity;
|
|
1609
|
+
this.paint(mv);
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1521
1612
|
this.ownMaterial(mv);
|
|
1522
1613
|
for (const m of materialsOf(mv.mesh.material)) {
|
|
1523
1614
|
const e = m.emissive;
|
|
@@ -1538,6 +1629,12 @@ var Visuals = class {
|
|
|
1538
1629
|
pv.meltApplied = m;
|
|
1539
1630
|
for (const mv of pv.meshes) {
|
|
1540
1631
|
const mesh = mv.mesh;
|
|
1632
|
+
if (mv.host) {
|
|
1633
|
+
mv.meltApplied = m;
|
|
1634
|
+
mv.touched = true;
|
|
1635
|
+
if (!(this.lowQuality && this.vertexCount(mv) > 2e4) && this.ownGeometry(mv)) this.rebuildHost(mv.host);
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1541
1638
|
if (mv.meltApplied === 0 && m > 0) {
|
|
1542
1639
|
mesh.updateWorldMatrix(true, false);
|
|
1543
1640
|
mv.bottomY = this.box.setFromObject(mesh).min.y;
|
|
@@ -1568,10 +1665,270 @@ var Visuals = class {
|
|
|
1568
1665
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
1569
1666
|
hide(pv, reason) {
|
|
1570
1667
|
pv.gone = reason;
|
|
1571
|
-
for (const mv of pv.meshes)
|
|
1668
|
+
for (const mv of pv.meshes) {
|
|
1669
|
+
if (!mv.host || mv.loose) {
|
|
1670
|
+
mv.mesh.visible = false;
|
|
1671
|
+
mv.hidden = true;
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
mv.hidden = true;
|
|
1675
|
+
for (const d of mv.decals) d.obj.visible = false;
|
|
1676
|
+
this.collapse(mv);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
1680
|
+
// Parts drawn by a vertex range of a merged mesh
|
|
1681
|
+
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
1682
|
+
/** Collapse the part's vertices in the merged mesh to one point (its triangles become degenerate and vanish). */
|
|
1683
|
+
collapse(mv) {
|
|
1684
|
+
if (!mv.merged || !this.ownHostGeometry(mv.merged.host)) return;
|
|
1685
|
+
mv.collapsed = true;
|
|
1686
|
+
mv.touched = true;
|
|
1687
|
+
this.rebuildHost(mv.merged.host);
|
|
1688
|
+
}
|
|
1689
|
+
/** Write `work` into the geometry again with the melt of ranged parts and the collapsed ranges applied. */
|
|
1690
|
+
rebuildHost(host) {
|
|
1691
|
+
const geo = host.mesh.geometry;
|
|
1692
|
+
const pos = positionAttr(geo);
|
|
1693
|
+
const work = host.work;
|
|
1694
|
+
if (!pos || !work) return;
|
|
1695
|
+
const arr = pos.array;
|
|
1696
|
+
arr.set(work);
|
|
1697
|
+
for (const s of host.sharers) {
|
|
1698
|
+
if (s.merged && s.merged.host === host) {
|
|
1699
|
+
if (s.collapsed) this.collapseRange(arr, s.merged.range);
|
|
1700
|
+
else if (s.meltApplied > 0 && !s.loose) this.meltRange(arr, work, s.merged.range, s.meltApplied);
|
|
1701
|
+
} else if (s.host === host && s.range && s.meltApplied > 0) this.meltRange(arr, work, s.range, s.meltApplied);
|
|
1702
|
+
}
|
|
1703
|
+
pos.needsUpdate = true;
|
|
1704
|
+
geo.computeVertexNormals();
|
|
1705
|
+
geo.boundingBox = null;
|
|
1706
|
+
geo.boundingSphere = null;
|
|
1707
|
+
}
|
|
1708
|
+
collapseRange(arr, range) {
|
|
1709
|
+
const x = arr[range[0] * 3];
|
|
1710
|
+
const y = arr[range[0] * 3 + 1];
|
|
1711
|
+
const z = arr[range[0] * 3 + 2];
|
|
1712
|
+
for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
|
|
1713
|
+
arr[i] = x;
|
|
1714
|
+
arr[i + 1] = y;
|
|
1715
|
+
arr[i + 2] = z;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
/** Melt of one range: shorter in Y (down to 25 %) with the bottom fixed, a bit wider around its centre, with the same noise as a whole mesh. */
|
|
1719
|
+
meltRange(arr, work, range, melt) {
|
|
1720
|
+
let minY = Infinity;
|
|
1721
|
+
let maxY = -Infinity;
|
|
1722
|
+
let cx = 0;
|
|
1723
|
+
let cz = 0;
|
|
1724
|
+
const n = Math.max(1, range[1] - range[0]);
|
|
1725
|
+
for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
|
|
1726
|
+
minY = Math.min(minY, work[i + 1]);
|
|
1727
|
+
maxY = Math.max(maxY, work[i + 1]);
|
|
1728
|
+
cx += work[i];
|
|
1729
|
+
cz += work[i + 2];
|
|
1730
|
+
}
|
|
1731
|
+
cx /= n;
|
|
1732
|
+
cz /= n;
|
|
1733
|
+
const h = Math.max(1e-4, maxY - minY);
|
|
1734
|
+
const sy = lerp(1, 0.25, melt);
|
|
1735
|
+
const sxz = 1 + 0.18 * melt;
|
|
1736
|
+
for (let i = range[0] * 3, v = 0; i < range[1] * 3; i += 3, v++) {
|
|
1737
|
+
const noise = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
|
|
1738
|
+
arr[i] = cx + (work[i] - cx) * sxz;
|
|
1739
|
+
arr[i + 2] = cz + (work[i + 2] - cz) * sxz;
|
|
1740
|
+
arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise * melt * h * 0.04);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
/** The colour attribute of the host's current geometry (created white when there is none) with vertex colours switched on in its material. */
|
|
1744
|
+
ensureColors(host) {
|
|
1745
|
+
if (!this.ownHostGeometry(host)) return null;
|
|
1746
|
+
const geo = host.mesh.geometry;
|
|
1747
|
+
let attr = geo.getAttribute("color");
|
|
1748
|
+
if (!attr) {
|
|
1749
|
+
const n = geo.getAttribute(POS_ATTR).count;
|
|
1750
|
+
attr = new this.THREE.BufferAttribute(new Float32Array(n * 3).fill(1), 3);
|
|
1751
|
+
geo.setAttribute("color", attr);
|
|
1752
|
+
}
|
|
1753
|
+
if (!host.ownMat) {
|
|
1754
|
+
host.ownMat = true;
|
|
1755
|
+
const src = materialsOf(host.mesh.material);
|
|
1756
|
+
const cloned = src.map((m) => m.clone());
|
|
1757
|
+
host.mesh.material = Array.isArray(host.mesh.material) ? cloned : cloned[0];
|
|
1758
|
+
}
|
|
1759
|
+
for (const m of materialsOf(host.mesh.material)) {
|
|
1760
|
+
if (m.vertexColors !== true) {
|
|
1761
|
+
;
|
|
1762
|
+
m.vertexColors = true;
|
|
1763
|
+
m.needsUpdate = true;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
return attr;
|
|
1767
|
+
}
|
|
1768
|
+
/** Colour of the part's vertices from the original colours, its char and its heat. Only the part's range is written. */
|
|
1769
|
+
paint(mv) {
|
|
1770
|
+
if (!mv.host || !mv.range || !mv.merged) return;
|
|
1771
|
+
const idle = mv.charK <= 0 && mv.glowK <= 0;
|
|
1772
|
+
if (idle && !mv.touched) return;
|
|
1773
|
+
const attr = this.ensureColors(mv.host);
|
|
1774
|
+
if (!attr) return;
|
|
1775
|
+
const size = attr.itemSize;
|
|
1776
|
+
const arr = attr.array;
|
|
1777
|
+
const orig = mv.merged.host.origGeometry.getAttribute("color");
|
|
1778
|
+
const oSize = orig?.itemSize ?? 3;
|
|
1779
|
+
const oArr = orig?.array;
|
|
1780
|
+
const shift = mv.merged.range[0] - mv.range[0];
|
|
1781
|
+
const ch = new this.THREE.Color(mv.charHex);
|
|
1782
|
+
const heat = new this.THREE.Color(mv.glowHex);
|
|
1783
|
+
const k = clamp(mv.charK, 0, 1);
|
|
1784
|
+
const gk = clamp(mv.glowK, 0, 1);
|
|
1785
|
+
const boost = 1 + Math.max(0, mv.glowK - 1);
|
|
1786
|
+
for (let i = mv.range[0]; i < mv.range[1]; i++) {
|
|
1787
|
+
const o = (i + shift) * oSize;
|
|
1788
|
+
let r = oArr ? oArr[o] : 1;
|
|
1789
|
+
let g = oArr ? oArr[o + 1] : 1;
|
|
1790
|
+
let b = oArr ? oArr[o + 2] : 1;
|
|
1791
|
+
if (k > 0) {
|
|
1792
|
+
r = lerp(r, ch.r, k);
|
|
1793
|
+
g = lerp(g, ch.g, k);
|
|
1794
|
+
b = lerp(b, ch.b, k);
|
|
1795
|
+
}
|
|
1796
|
+
if (gk > 0) {
|
|
1797
|
+
r = lerp(r, heat.r * boost, gk);
|
|
1798
|
+
g = lerp(g, heat.g * boost, gk);
|
|
1799
|
+
b = lerp(b, heat.b * boost, gk);
|
|
1800
|
+
}
|
|
1801
|
+
arr[i * size] = r;
|
|
1802
|
+
arr[i * size + 1] = g;
|
|
1803
|
+
arr[i * size + 2] = b;
|
|
1804
|
+
}
|
|
1805
|
+
mv.touched = true;
|
|
1806
|
+
attr.needsUpdate = true;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Move a ranged part to a mesh of its own: the faces whose vertices all lie in its range are copied (with the current dents and colours) into a
|
|
1810
|
+
* new mesh that takes the merged mesh's transform and material, then attached to `group`; the range is collapsed in the merged mesh. Decals
|
|
1811
|
+
* go with it.
|
|
1812
|
+
*/
|
|
1813
|
+
extract(mv, group) {
|
|
1814
|
+
if (!mv.host || !mv.range || !mv.merged || mv.loose) return;
|
|
1815
|
+
const host = mv.host;
|
|
1816
|
+
const [v0, v1] = mv.range;
|
|
1817
|
+
const src = host.mesh.geometry;
|
|
1818
|
+
const geo = new this.THREE.BufferGeometry();
|
|
1819
|
+
const count = v1 - v0;
|
|
1820
|
+
for (const name of Object.keys(src.attributes)) {
|
|
1821
|
+
const a = src.getAttribute(name);
|
|
1822
|
+
if (a.isInterleavedBufferAttribute) continue;
|
|
1823
|
+
const size = a.itemSize;
|
|
1824
|
+
const from = (name === POS_ATTR ? host.work : null) ?? a.array;
|
|
1825
|
+
const slice = from.slice(v0 * size, v1 * size);
|
|
1826
|
+
const Ctor = a.array.constructor;
|
|
1827
|
+
geo.setAttribute(name, new this.THREE.BufferAttribute(new Ctor(slice), size, a.normalized));
|
|
1828
|
+
}
|
|
1829
|
+
const index = src.getIndex();
|
|
1830
|
+
if (index) {
|
|
1831
|
+
const out = [];
|
|
1832
|
+
for (let i = 0; i + 2 < index.count; i += 3) {
|
|
1833
|
+
const a = index.getX(i);
|
|
1834
|
+
const b = index.getX(i + 1);
|
|
1835
|
+
const c = index.getX(i + 2);
|
|
1836
|
+
if (a >= v0 && a < v1 && b >= v0 && b < v1 && c >= v0 && c < v1) out.push(a - v0, b - v0, c - v0);
|
|
1837
|
+
}
|
|
1838
|
+
geo.setIndex(count > 65535 ? new this.THREE.Uint32BufferAttribute(out, 1) : new this.THREE.Uint16BufferAttribute(out, 1));
|
|
1839
|
+
}
|
|
1840
|
+
const mats = materialsOf(host.mesh.material).map((m) => m.clone());
|
|
1841
|
+
const loose = new this.THREE.Mesh(geo, mats[0]);
|
|
1842
|
+
loose.name = `${host.mesh.name || "mesh"}:${v0}-${v1}`;
|
|
1843
|
+
loose.castShadow = host.mesh.castShadow;
|
|
1844
|
+
loose.receiveShadow = host.mesh.receiveShadow;
|
|
1845
|
+
loose.renderOrder = host.mesh.renderOrder;
|
|
1846
|
+
loose.frustumCulled = host.mesh.frustumCulled;
|
|
1847
|
+
loose.layers.mask = host.mesh.layers.mask;
|
|
1848
|
+
loose.position.copy(host.mesh.position);
|
|
1849
|
+
loose.quaternion.copy(host.mesh.quaternion);
|
|
1850
|
+
loose.scale.copy(host.mesh.scale);
|
|
1851
|
+
const parent = host.mesh.parent;
|
|
1852
|
+
if (parent) parent.add(loose);
|
|
1853
|
+
loose.updateMatrixWorld(true);
|
|
1854
|
+
this.collapse(mv);
|
|
1855
|
+
for (const d of mv.decals) {
|
|
1856
|
+
d.obj.visible = true;
|
|
1857
|
+
loose.add(d.obj);
|
|
1858
|
+
}
|
|
1859
|
+
const lp = positionAttr(geo);
|
|
1860
|
+
const lh = {
|
|
1861
|
+
mesh: loose,
|
|
1862
|
+
origGeometry: geo,
|
|
1863
|
+
origMaterial: loose.material,
|
|
1864
|
+
ownGeo: true,
|
|
1865
|
+
work: new Float32Array(lp.array),
|
|
1866
|
+
ownMat: true,
|
|
1867
|
+
sharers: [mv]
|
|
1868
|
+
};
|
|
1869
|
+
mv.host = lh;
|
|
1870
|
+
mv.range = [0, count];
|
|
1871
|
+
mv.mesh = loose;
|
|
1872
|
+
mv.loose = true;
|
|
1873
|
+
mv.moved = true;
|
|
1874
|
+
group.attach(loose);
|
|
1875
|
+
for (const m of materialsOf(loose.material)) m.needsUpdate = true;
|
|
1876
|
+
}
|
|
1877
|
+
restoreRanged(mv) {
|
|
1878
|
+
const merged = mv.merged;
|
|
1879
|
+
const host = merged.host;
|
|
1880
|
+
for (const d of mv.decals) this.disposeDecal(d);
|
|
1881
|
+
mv.decals.length = 0;
|
|
1882
|
+
if (mv.loose) {
|
|
1883
|
+
const loose = mv.mesh;
|
|
1884
|
+
loose.parent?.remove(loose);
|
|
1885
|
+
loose.geometry.dispose();
|
|
1886
|
+
for (const m of materialsOf(loose.material)) m.dispose();
|
|
1887
|
+
mv.host = host;
|
|
1888
|
+
mv.range = [merged.range[0], merged.range[1]];
|
|
1889
|
+
mv.mesh = host.mesh;
|
|
1890
|
+
mv.loose = false;
|
|
1891
|
+
mv.moved = false;
|
|
1892
|
+
}
|
|
1893
|
+
mv.collapsed = false;
|
|
1894
|
+
mv.hidden = false;
|
|
1895
|
+
mv.meltApplied = 0;
|
|
1896
|
+
mv.charK = 0;
|
|
1897
|
+
mv.glowK = 0;
|
|
1898
|
+
mv.touched = false;
|
|
1899
|
+
if (!host.ownGeo) return;
|
|
1900
|
+
const work = host.work;
|
|
1901
|
+
const op = positionAttr(host.origGeometry);
|
|
1902
|
+
const [v0, v1] = merged.range;
|
|
1903
|
+
if (op) work.set(op.array.subarray(v0 * 3, v1 * 3), v0 * 3);
|
|
1904
|
+
const attr = host.mesh.geometry.getAttribute("color");
|
|
1905
|
+
if (attr) {
|
|
1906
|
+
const orig = host.origGeometry.getAttribute("color");
|
|
1907
|
+
const size = attr.itemSize;
|
|
1908
|
+
const a = attr.array;
|
|
1909
|
+
for (let i = v0; i < v1; i++) {
|
|
1910
|
+
for (let c = 0; c < 3; c++) a[i * size + c] = orig ? orig.array[i * orig.itemSize + c] : 1;
|
|
1911
|
+
}
|
|
1912
|
+
attr.needsUpdate = true;
|
|
1913
|
+
}
|
|
1914
|
+
if (host.sharers.every((s) => !s.touched)) {
|
|
1915
|
+
host.mesh.geometry.dispose();
|
|
1916
|
+
host.mesh.geometry = host.origGeometry;
|
|
1917
|
+
host.work = null;
|
|
1918
|
+
host.ownGeo = false;
|
|
1919
|
+
if (host.ownMat) {
|
|
1920
|
+
for (const m of materialsOf(host.mesh.material)) m.dispose();
|
|
1921
|
+
host.mesh.material = host.origMaterial;
|
|
1922
|
+
host.ownMat = false;
|
|
1923
|
+
}
|
|
1924
|
+
} else this.rebuildHost(host);
|
|
1572
1925
|
}
|
|
1573
1926
|
/** Bring one mesh back to how it was when it was bound. */
|
|
1574
1927
|
restoreMesh(mv) {
|
|
1928
|
+
if (mv.merged) {
|
|
1929
|
+
this.restoreRanged(mv);
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1575
1932
|
const mesh = mv.mesh;
|
|
1576
1933
|
for (const d of mv.decals) this.disposeDecal(d);
|
|
1577
1934
|
mv.decals.length = 0;
|
|
@@ -1671,6 +2028,10 @@ function fallbackColor(id, def) {
|
|
|
1671
2028
|
function materialsOf2(m) {
|
|
1672
2029
|
return Array.isArray(m) ? m : [m];
|
|
1673
2030
|
}
|
|
2031
|
+
function isMeshRange(v) {
|
|
2032
|
+
const r = v;
|
|
2033
|
+
return !!r.mesh && Array.isArray(r.range);
|
|
2034
|
+
}
|
|
1674
2035
|
function createDestructionRenderer(THREE, options) {
|
|
1675
2036
|
return new Renderer(THREE, options);
|
|
1676
2037
|
}
|
|
@@ -1764,12 +2125,23 @@ var Renderer = class {
|
|
|
1764
2125
|
const partIds = new Set((state?.parts ?? []).map((p) => p.id));
|
|
1765
2126
|
const byPart = /* @__PURE__ */ new Map();
|
|
1766
2127
|
const taken = /* @__PURE__ */ new Set();
|
|
2128
|
+
const put = (pid, e) => {
|
|
2129
|
+
const l = byPart.get(pid);
|
|
2130
|
+
if (l) l.push(e);
|
|
2131
|
+
else byPart.set(pid, [e]);
|
|
2132
|
+
};
|
|
1767
2133
|
const add = (pid, m) => {
|
|
1768
2134
|
if (taken.has(m)) return;
|
|
1769
2135
|
taken.add(m);
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2136
|
+
put(pid, { mesh: m });
|
|
2137
|
+
};
|
|
2138
|
+
const addRange = (pid, r) => {
|
|
2139
|
+
const n = r.mesh.geometry.getAttribute("position")?.count ?? 0;
|
|
2140
|
+
const [v0, v1] = r.range;
|
|
2141
|
+
if (!Number.isInteger(v0) || !Number.isInteger(v1) || v0 < 0 || v1 <= v0 || v1 > n) {
|
|
2142
|
+
throw new Error(`game-destruction-three: invalid vertex range [${v0}, ${v1}) for part "${pid}" (the mesh has ${n} vertices)`);
|
|
2143
|
+
}
|
|
2144
|
+
put(pid, { mesh: r.mesh, range: [v0, v1] });
|
|
1773
2145
|
};
|
|
1774
2146
|
const meshesIn = (o) => {
|
|
1775
2147
|
const out = [];
|
|
@@ -1780,7 +2152,10 @@ var Renderer = class {
|
|
|
1780
2152
|
};
|
|
1781
2153
|
if (options.parts) {
|
|
1782
2154
|
for (const [pid, v] of Object.entries(options.parts)) {
|
|
1783
|
-
for (const o of Array.isArray(v) ? v : [v])
|
|
2155
|
+
for (const o of Array.isArray(v) ? v : [v]) {
|
|
2156
|
+
if (isMeshRange(o)) addRange(pid, o);
|
|
2157
|
+
else for (const m of meshesIn(o)) add(pid, m);
|
|
2158
|
+
}
|
|
1784
2159
|
}
|
|
1785
2160
|
} else {
|
|
1786
2161
|
const leftovers = [];
|
|
@@ -1793,14 +2168,15 @@ var Renderer = class {
|
|
|
1793
2168
|
if (leftovers.length) this.matchByBox(objectId, root, leftovers, add);
|
|
1794
2169
|
}
|
|
1795
2170
|
const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
|
|
1796
|
-
|
|
2171
|
+
const hosts = /* @__PURE__ */ new Map();
|
|
2172
|
+
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));
|
|
1797
2173
|
for (const [pid, meshes] of byPart) {
|
|
1798
|
-
if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes));
|
|
2174
|
+
if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes, hosts));
|
|
1799
2175
|
}
|
|
1800
2176
|
this.bounds.set(objectId, bound);
|
|
1801
2177
|
}
|
|
1802
|
-
makePartVis(home, id, material, meshes) {
|
|
1803
|
-
return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m, home)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
|
|
2178
|
+
makePartVis(home, id, material, meshes, hosts) {
|
|
2179
|
+
return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m.mesh, home, m.range, hosts)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
|
|
1804
2180
|
}
|
|
1805
2181
|
/** Match unlabelled meshes to the part whose shape contains the centre of the mesh's bounding box. */
|
|
1806
2182
|
matchByBox(objectId, root, meshes, add) {
|
|
@@ -1860,11 +2236,13 @@ var Renderer = class {
|
|
|
1860
2236
|
const inv = root.matrixWorld.clone().invert();
|
|
1861
2237
|
const total = new this.THREE.Box3();
|
|
1862
2238
|
const m4 = new this.THREE.Matrix4();
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
2239
|
+
const lb = new this.THREE.Box3();
|
|
2240
|
+
for (const mv of meshes) {
|
|
2241
|
+
const m = mv.mesh;
|
|
2242
|
+
m.updateWorldMatrix(true, false);
|
|
2243
|
+
this.visuals.localBox(mv, lb);
|
|
2244
|
+
if (lb.isEmpty()) continue;
|
|
2245
|
+
total.union(lb.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
|
|
1868
2246
|
}
|
|
1869
2247
|
if (total.isEmpty()) return { half: [0.25, 0.25, 0.25], center: new this.THREE.Vector3() };
|
|
1870
2248
|
const size = total.getSize(new this.THREE.Vector3());
|
|
@@ -1872,7 +2250,7 @@ var Renderer = class {
|
|
|
1872
2250
|
}
|
|
1873
2251
|
meshesOf(b) {
|
|
1874
2252
|
const out = [];
|
|
1875
|
-
for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv
|
|
2253
|
+
for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv);
|
|
1876
2254
|
return out;
|
|
1877
2255
|
}
|
|
1878
2256
|
massOf(objectId) {
|
|
@@ -2003,6 +2381,9 @@ var Renderer = class {
|
|
|
2003
2381
|
case "detach":
|
|
2004
2382
|
this.onDetach(ev);
|
|
2005
2383
|
break;
|
|
2384
|
+
case "collapse":
|
|
2385
|
+
this.splitLoose(ev.object);
|
|
2386
|
+
break;
|
|
2006
2387
|
case "disassemble_progress": {
|
|
2007
2388
|
const k = `unscrew:${ev.joint}`;
|
|
2008
2389
|
if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
|
|
@@ -2121,8 +2502,8 @@ var Renderer = class {
|
|
|
2121
2502
|
let best = null;
|
|
2122
2503
|
let bd = Infinity;
|
|
2123
2504
|
for (const mv of pv.meshes) {
|
|
2124
|
-
if (!
|
|
2125
|
-
const d = this.
|
|
2505
|
+
if (!this.visuals.shown(mv)) continue;
|
|
2506
|
+
const d = this.visuals.worldBox(mv, this.box).distanceToPoint(p);
|
|
2126
2507
|
if (d < bd) {
|
|
2127
2508
|
bd = d;
|
|
2128
2509
|
best = mv;
|
|
@@ -2142,7 +2523,7 @@ var Renderer = class {
|
|
|
2142
2523
|
const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
|
|
2143
2524
|
const wn = this.worldDir(b, ev.dent.normal);
|
|
2144
2525
|
for (const mv of pv.meshes) {
|
|
2145
|
-
if (!
|
|
2526
|
+
if (!this.visuals.shown(mv)) continue;
|
|
2146
2527
|
this.visuals.applyDent(mv, wp, wn, ev.dent);
|
|
2147
2528
|
}
|
|
2148
2529
|
if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [wn.x, wn.y, wn.z], 4 + Math.min(8, ev.dent.depth * 150), 3.2);
|
|
@@ -2233,16 +2614,20 @@ var Renderer = class {
|
|
|
2233
2614
|
}
|
|
2234
2615
|
onDetach(ev) {
|
|
2235
2616
|
this.sound("detach", this.rootPoint(ev.object), 0.5);
|
|
2617
|
+
this.splitLoose(ev.object);
|
|
2618
|
+
}
|
|
2619
|
+
/** Turn every loose group of the object (a detach, a collapse) into a piece of its own. */
|
|
2620
|
+
splitLoose(objectId) {
|
|
2236
2621
|
if (!this.autoSplit) return;
|
|
2237
|
-
const b = this.bounds.get(
|
|
2622
|
+
const b = this.bounds.get(objectId);
|
|
2238
2623
|
if (!b) return;
|
|
2239
|
-
const st = this.world.state(
|
|
2624
|
+
const st = this.world.state(objectId);
|
|
2240
2625
|
if (!st || st.groups.length < 2) return;
|
|
2241
2626
|
const loose = st.groups.length - 1;
|
|
2242
2627
|
for (let i = 0; i < loose; i++) {
|
|
2243
2628
|
let newId;
|
|
2244
2629
|
try {
|
|
2245
|
-
newId = this.world.splitGroup(
|
|
2630
|
+
newId = this.world.splitGroup(objectId, 1);
|
|
2246
2631
|
} catch {
|
|
2247
2632
|
break;
|
|
2248
2633
|
}
|
|
@@ -2253,7 +2638,7 @@ var Renderer = class {
|
|
|
2253
2638
|
const { half, center } = this.measure(b.root, this.meshesOf(b));
|
|
2254
2639
|
old.halfExtents = half;
|
|
2255
2640
|
this.pieceSys.setCenter(old, center);
|
|
2256
|
-
old.mass = Math.max(0.05, this.massOf(
|
|
2641
|
+
old.mass = Math.max(0.05, this.massOf(objectId));
|
|
2257
2642
|
old.sleeping = false;
|
|
2258
2643
|
}
|
|
2259
2644
|
}
|
|
@@ -2275,8 +2660,11 @@ var Renderer = class {
|
|
|
2275
2660
|
from.parts.delete(p.id);
|
|
2276
2661
|
nb.parts.set(p.id, pv);
|
|
2277
2662
|
for (const mv of pv.meshes) {
|
|
2278
|
-
|
|
2279
|
-
|
|
2663
|
+
if (mv.host && !mv.loose) this.visuals.extract(mv, group);
|
|
2664
|
+
else {
|
|
2665
|
+
group.attach(mv.mesh);
|
|
2666
|
+
mv.moved = true;
|
|
2667
|
+
}
|
|
2280
2668
|
}
|
|
2281
2669
|
}
|
|
2282
2670
|
this.bounds.set(newId, nb);
|
|
@@ -2392,7 +2780,7 @@ var Renderer = class {
|
|
|
2392
2780
|
}
|
|
2393
2781
|
onGone(objectId, pv, reason, def) {
|
|
2394
2782
|
const bb = new this.THREE.Box3();
|
|
2395
|
-
for (const mv of pv.meshes) bb.union(this.
|
|
2783
|
+
for (const mv of pv.meshes) bb.union(this.visuals.worldBox(mv, this.box));
|
|
2396
2784
|
this.visuals.hide(pv, reason);
|
|
2397
2785
|
if (bb.isEmpty()) return;
|
|
2398
2786
|
const c = bb.getCenter(new this.THREE.Vector3());
|
|
@@ -2409,7 +2797,7 @@ var Renderer = class {
|
|
|
2409
2797
|
const bb = this.box;
|
|
2410
2798
|
bb.makeEmpty();
|
|
2411
2799
|
const tmp = new this.THREE.Box3();
|
|
2412
|
-
for (const mv of pv.meshes) if (
|
|
2800
|
+
for (const mv of pv.meshes) if (this.visuals.shown(mv)) bb.union(this.visuals.worldBox(mv, tmp));
|
|
2413
2801
|
let cx = f.point[0];
|
|
2414
2802
|
let cy = f.point[1];
|
|
2415
2803
|
let cz = f.point[2];
|