SharpMeow commited on
Commit
dc2ef88
·
1 Parent(s): a9ce211

Destroy debris: landed shards and dust fade out soon after landing instead of resting 25 to 30 s on the undrawn floor, where they read as hanging in mid-air; reset clears every instanced slot; watchdog retires anything drawn and unmoving for 2.5 s; damageStats reports stillMax, stillAirMax, retired

Browse files
Files changed (2) hide show
  1. app.js +1 -0
  2. damage.js +46 -9
app.js CHANGED
@@ -1276,6 +1276,7 @@ requestAnimationFrame(() => loadOverlay());
1276
  // camera hooks for the keyboard shortcuts in polish.js
1277
  window.unrollView = {
1278
  damageStats: () => damage.stats(),
 
1279
  reset() {
1280
  introDone(); autoplayAt = 0;
1281
  if (mode === 'overview') flyTo(...overview(), 1.4);
 
1276
  // camera hooks for the keyboard shortcuts in polish.js
1277
  window.unrollView = {
1278
  damageStats: () => damage.stats(),
1279
+ damageSim: (n, dt) => damage.simulate(n, dt), // test hook: step the debris without waiting for frames
1280
  reset() {
1281
  introDone(); autoplayAt = 0;
1282
  if (mode === 'overview') flyTo(...overview(), 1.4);
damage.js CHANGED
@@ -211,9 +211,16 @@ export function createDamage({ THREE, scene, meshes, MOBILE, ground, uDmgT }) {
211
  const fPos = new Float32Array(FMAX * 3), fVel = new Float32Array(FMAX * 3), fRot = new Float32Array(FMAX * 3), fSpin = new Float32Array(FMAX * 3);
212
  const fSize = new Float32Array(FMAX), fAge = new Float32Array(FMAX), fRest = new Uint8Array(FMAX), fLive = new Uint8Array(FMAX);
213
  let fHead = 0;
 
 
 
 
 
 
 
214
  function spawnF(x, y, z, vx, vy, vz, size) {
215
  const i = fHead++ % FMAX;
216
- fLive[i] = 1; fRest[i] = 0; fAge[i] = 0; fSize[i] = size;
217
  fPos[i * 3] = x; fPos[i * 3 + 1] = y; fPos[i * 3 + 2] = z;
218
  fVel[i * 3] = vx; fVel[i * 3 + 1] = vy; fVel[i * 3 + 2] = vz;
219
  for (let c = 0; c < 3; c++) { fRot[i * 3 + c] = Math.random() * 6.3; fSpin[i * 3 + c] = (Math.random() - 0.5) * 14; }
@@ -366,20 +373,27 @@ export function createDamage({ THREE, scene, meshes, MOBILE, ground, uDmgT }) {
366
  if (t === 2 || t === 4) { pCol[i * 4 + 3] = 0; continue; }
367
  // dust and ash land: snap to the floor, stop, sleep
368
  pPos[o + 1] = G + 0.05; pVel[o] = pVel[o + 1] = pVel[o + 2] = 0; pSleep[i] = 1;
369
- pLife[i] = pAge[i] + 6 + Math.random() * 4;
370
  continue;
371
  }
372
  if (t === 2) { pSize[i] += dt * 2.6; pCol[i * 4 + 3] = 0.42 * fade * Math.min(1, pAge[i] * 2); }
373
  else if (t === 4) pCol[i * 4 + 3] = fade;
374
  else if (t !== 3) pCol[i * 4 + 3] = Math.min(0.8, fade * 1.6) * (t === 0 ? 0.75 : 0.9);
375
  }
 
 
 
 
 
 
 
376
  pGeo.attributes.position.needsUpdate = pGeo.attributes.aCol.needsUpdate = pGeo.attributes.aSize.needsUpdate = true;
377
  // fragments flutter and tumble with lift; big flakes drop
378
  for (let i = 0; i < fMesh.count; i++) {
379
  const o = i * 3;
380
- if (!fLive[i]) { tmpM.makeScale(0, 0, 0); fMesh.setMatrixAt(i, tmpM); continue; }
381
  fAge[i] += dt;
382
- if (!Number.isFinite(fPos[o] + fPos[o + 1] + fPos[o + 2])) { fLive[i] = 0; continue; }
383
  if (!fRest[i]) {
384
  const big = fSize[i] > 1.0;
385
  const g = big ? 380 : 160, k = big ? 0.6 : 3.0;
@@ -391,8 +405,10 @@ export function createDamage({ THREE, scene, meshes, MOBILE, ground, uDmgT }) {
391
  for (let c = 0; c < 3; c++) { fPos[o + c] += fVel[o + c] * dt; fRot[o + c] += fSpin[o + c] * dt; }
392
  if (fPos[o + 1] < ground() + 0.1) { fPos[o + 1] = ground() + 0.1; fRest[i] = 1; fRot[o] = Math.PI / 2 + (Math.random() - 0.5) * 0.3; }
393
  }
394
- const fadeS = fAge[i] > 25 ? Math.max(0, 1 - (fAge[i] - 25) / 5) : 1;
395
- if (fadeS <= 0) fLive[i] = 0;
 
 
396
  tmpE.set(fRot[o], fRot[o + 1], fRot[o + 2]);
397
  tmpQ.setFromEuler(tmpE);
398
  tmpS.setScalar(fSize[i] * fadeS);
@@ -400,6 +416,7 @@ export function createDamage({ THREE, scene, meshes, MOBILE, ground, uDmgT }) {
400
  tmpM.compose(tmpP, tmpQ, tmpS);
401
  fMesh.setMatrixAt(i, tmpM);
402
  }
 
403
  fMesh.instanceMatrix.needsUpdate = true;
404
  // puddles spread and slowly soak away
405
  const G2 = ground() + 0.05;
@@ -414,18 +431,38 @@ export function createDamage({ THREE, scene, meshes, MOBILE, ground, uDmgT }) {
414
  qMesh.instanceMatrix.needsUpdate = true;
415
  }
416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  function reset() {
418
  for (const k of masks.values()) { own(k.m).uDmgOn.value = 0; own(k.m).uDmg.value = blankTex; k.tex.dispose(); }
419
  masks.clear();
420
  emberCount = 0;
421
- pCol.fill(0); fLive.fill(0); fMesh.count = 0; fHead = 0; puddles.length = 0; qMesh.count = 0;
 
 
422
  }
423
  const blankTex = new THREE.DataTexture(new Uint8Array(4), 1, 1); blankTex.needsUpdate = true;
424
  const belowFloor = () => { const G = ground(); let n = 0, sleeping = 0; for (let i = 0; i < PMAX; i++) if (pCol[i * 4 + 3] > 0) { if (pPos[i * 3 + 1] < G - 0.01) n++; if (pSleep[i]) sleeping++; } return { n, sleeping }; };
425
- const stats = () => ({ floor: belowFloor(), masks: masks.size, embers: emberCount, particles: [...pCol].filter((_, i) => i % 4 === 3).filter((a) => a > 0).length, shards: fLive.reduce((a, b) => a + b, 0), puddles: puddles.length });
 
426
  // map a material-space hit (mm) from the flat sandbox onto the same mask
427
  function crushMM(m, xMM, yMM, rMM) { const w = m.userData.w; crush(m, xMM / w.widthMM, yMM / w.heightMM, rMM, 1.0); }
428
  function tearMM(m, ax, ay, bx, by) { const w = m.userData.w; const k = mask(m); line(k, ax / w.widthMM, ay / w.heightMM, bx / w.widthMM, by / w.heightMM, 0.28, 0, 0.75, 0.3); line(k, ax / w.widthMM, ay / w.heightMM, bx / w.widthMM, by / w.heightMM, 0.8, 1, 0.3, 0.4); }
429
  const objects = [points, fMesh, qMesh];
430
- return { objects, shared, blankTex, bruise, crush, tear, wetAt, ignite, debris, drops, update, reset, stats, crushMM, tearMM, wetness: (m, u, v) => (masks.has(m) ? at(masks.get(m), u, v, 3) : 0), dust: (x, y, z) => debris({ x, y, z }, 1.5, 0, 'tear') || spawnP(0, x, y, z, (Math.random() - 0.5) * 30, 8 + Math.random() * 12, 8 + Math.random() * 15, 0.3 + Math.random() * 0.4, 6, 0.13, 0.1, 0.08, 0.75) };
431
  }
 
211
  const fPos = new Float32Array(FMAX * 3), fVel = new Float32Array(FMAX * 3), fRot = new Float32Array(FMAX * 3), fSpin = new Float32Array(FMAX * 3);
212
  const fSize = new Float32Array(FMAX), fAge = new Float32Array(FMAX), fRest = new Uint8Array(FMAX), fLive = new Uint8Array(FMAX);
213
  let fHead = 0;
214
+ // Settle rule: nothing may hang still where it is drawn. In the rolled view the floor is not drawn, so a piece
215
+ // resting on it reads as hanging in mid-air; landed pieces therefore fade out soon after they land, and a
216
+ // watchdog retires anything that has been drawn without moving for STILL_MAX seconds.
217
+ const REST_HOLD = 1.0, REST_FADE = 1.2, STILL_MAX = 2.5;
218
+ const fRestT = new Float32Array(FMAX), fStill = new Float32Array(FMAX), fLast = new Float32Array(FMAX * 3), pStill = new Float32Array(PMAX);
219
+ let auditMax = 0, auditAir = 0, retired = 0;
220
+ const hide = (i) => { tmpM.makeScale(0, 0, 0); fMesh.setMatrixAt(i, tmpM); };
221
  function spawnF(x, y, z, vx, vy, vz, size) {
222
  const i = fHead++ % FMAX;
223
+ fLive[i] = 1; fRest[i] = 0; fAge[i] = 0; fSize[i] = size; fRestT[i] = 0; fStill[i] = 0;
224
  fPos[i * 3] = x; fPos[i * 3 + 1] = y; fPos[i * 3 + 2] = z;
225
  fVel[i * 3] = vx; fVel[i * 3 + 1] = vy; fVel[i * 3 + 2] = vz;
226
  for (let c = 0; c < 3; c++) { fRot[i * 3 + c] = Math.random() * 6.3; fSpin[i * 3 + c] = (Math.random() - 0.5) * 14; }
 
373
  if (t === 2 || t === 4) { pCol[i * 4 + 3] = 0; continue; }
374
  // dust and ash land: snap to the floor, stop, sleep
375
  pPos[o + 1] = G + 0.05; pVel[o] = pVel[o + 1] = pVel[o + 2] = 0; pSleep[i] = 1;
376
+ pLife[i] = pAge[i] + REST_HOLD + REST_FADE + Math.random() * 0.5;
377
  continue;
378
  }
379
  if (t === 2) { pSize[i] += dt * 2.6; pCol[i * 4 + 3] = 0.42 * fade * Math.min(1, pAge[i] * 2); }
380
  else if (t === 4) pCol[i * 4 + 3] = fade;
381
  else if (t !== 3) pCol[i * 4 + 3] = Math.min(0.8, fade * 1.6) * (t === 0 ? 0.75 : 0.9);
382
  }
383
+ // watchdog for particles: visible, not asleep on the floor, and not moving
384
+ for (let i = 0; i < PMAX; i++) {
385
+ if (pCol[i * 4 + 3] <= 0 || pSleep[i]) { pStill[i] = 0; continue; }
386
+ const sp = Math.abs(pVel[i * 3]) + Math.abs(pVel[i * 3 + 1]) + Math.abs(pVel[i * 3 + 2]);
387
+ pStill[i] = sp < 0.05 ? pStill[i] + dt : 0;
388
+ if (pStill[i] > STILL_MAX) { pCol[i * 4 + 3] = 0; pStill[i] = 0; retired++; }
389
+ }
390
  pGeo.attributes.position.needsUpdate = pGeo.attributes.aCol.needsUpdate = pGeo.attributes.aSize.needsUpdate = true;
391
  // fragments flutter and tumble with lift; big flakes drop
392
  for (let i = 0; i < fMesh.count; i++) {
393
  const o = i * 3;
394
+ if (!fLive[i]) { hide(i); continue; }
395
  fAge[i] += dt;
396
+ if (!Number.isFinite(fPos[o] + fPos[o + 1] + fPos[o + 2] + fVel[o] + fVel[o + 1] + fVel[o + 2] + fRot[o] + fRot[o + 1] + fRot[o + 2])) { fLive[i] = 0; hide(i); continue; }
397
  if (!fRest[i]) {
398
  const big = fSize[i] > 1.0;
399
  const g = big ? 380 : 160, k = big ? 0.6 : 3.0;
 
405
  for (let c = 0; c < 3; c++) { fPos[o + c] += fVel[o + c] * dt; fRot[o + c] += fSpin[o + c] * dt; }
406
  if (fPos[o + 1] < ground() + 0.1) { fPos[o + 1] = ground() + 0.1; fRest[i] = 1; fRot[o] = Math.PI / 2 + (Math.random() - 0.5) * 0.3; }
407
  }
408
+ // landed: lie a moment, then fade; airborne pieces still fade by age as a backstop
409
+ if (fRest[i]) fRestT[i] += dt;
410
+ const fadeS = Math.min(fAge[i] > 25 ? 1 - (fAge[i] - 25) / 5 : 1, fRest[i] ? 1 - (fRestT[i] - REST_HOLD) / REST_FADE : 1, 1);
411
+ if (fadeS <= 0) { fLive[i] = 0; hide(i); continue; }
412
  tmpE.set(fRot[o], fRot[o + 1], fRot[o + 2]);
413
  tmpQ.setFromEuler(tmpE);
414
  tmpS.setScalar(fSize[i] * fadeS);
 
416
  tmpM.compose(tmpP, tmpQ, tmpS);
417
  fMesh.setMatrixAt(i, tmpM);
418
  }
419
+ audit(dt);
420
  fMesh.instanceMatrix.needsUpdate = true;
421
  // puddles spread and slowly soak away
422
  const G2 = ground() + 0.05;
 
431
  qMesh.instanceMatrix.needsUpdate = true;
432
  }
433
 
434
+ // Audit what is actually drawn, from the instance matrices themselves, so a stale or orphaned slot counts too:
435
+ // a slot with a visible scale whose position has not changed. Past STILL_MAX it is hidden and freed.
436
+ function audit(dt) {
437
+ const a = fMesh.instanceMatrix.array, G = ground();
438
+ let mx = 0, air = 0;
439
+ for (let i = 0; i < FMAX; i++) {
440
+ const j = i * 16, o = i * 3;
441
+ const drawn = i < fMesh.count && a[j] * a[j] + a[j + 1] * a[j + 1] + a[j + 2] * a[j + 2] > 1e-8;
442
+ const moved = Math.abs(a[j + 12] - fLast[o]) + Math.abs(a[j + 13] - fLast[o + 1]) + Math.abs(a[j + 14] - fLast[o + 2]) > 1e-4;
443
+ fLast[o] = a[j + 12]; fLast[o + 1] = a[j + 13]; fLast[o + 2] = a[j + 14];
444
+ fStill[i] = drawn && !moved && dt > 0 ? fStill[i] + dt : drawn && !moved ? fStill[i] : 0;
445
+ if (fStill[i] > STILL_MAX) { fLive[i] = 0; hide(i); fStill[i] = 0; retired++; continue; }
446
+ if (fStill[i] > mx) mx = fStill[i];
447
+ if (fStill[i] > 0 && a[j + 13] > G + 0.5) air = Math.max(air, fStill[i]);
448
+ }
449
+ auditMax = Math.max(auditMax, mx); auditAir = Math.max(auditAir, air);
450
+ }
451
  function reset() {
452
  for (const k of masks.values()) { own(k.m).uDmgOn.value = 0; own(k.m).uDmg.value = blankTex; k.tex.dispose(); }
453
  masks.clear();
454
  emberCount = 0;
455
+ pCol.fill(0); fLive.fill(0); pStill.fill(0); fStill.fill(0);
456
+ for (let i = 0; i < FMAX; i++) hide(i); // no stale matrices left in the pool
457
+ fMesh.instanceMatrix.needsUpdate = true; fMesh.count = 0; fHead = 0; puddles.length = 0; qMesh.count = 0;
458
  }
459
  const blankTex = new THREE.DataTexture(new Uint8Array(4), 1, 1); blankTex.needsUpdate = true;
460
  const belowFloor = () => { const G = ground(); let n = 0, sleeping = 0; for (let i = 0; i < PMAX; i++) if (pCol[i * 4 + 3] > 0) { if (pPos[i * 3 + 1] < G - 0.01) n++; if (pSleep[i]) sleeping++; } return { n, sleeping }; };
461
+ const stats = () => ({ floor: belowFloor(), masks: masks.size, embers: emberCount, particles: [...pCol].filter((_, i) => i % 4 === 3).filter((a) => a > 0).length, shards: fLive.reduce((a, b) => a + b, 0), puddles: puddles.length,
462
+ stillMax: +auditMax.toFixed(2), stillAirMax: +auditAir.toFixed(2), retired });
463
  // map a material-space hit (mm) from the flat sandbox onto the same mask
464
  function crushMM(m, xMM, yMM, rMM) { const w = m.userData.w; crush(m, xMM / w.widthMM, yMM / w.heightMM, rMM, 1.0); }
465
  function tearMM(m, ax, ay, bx, by) { const w = m.userData.w; const k = mask(m); line(k, ax / w.widthMM, ay / w.heightMM, bx / w.widthMM, by / w.heightMM, 0.28, 0, 0.75, 0.3); line(k, ax / w.widthMM, ay / w.heightMM, bx / w.widthMM, by / w.heightMM, 0.8, 1, 0.3, 0.4); }
466
  const objects = [points, fMesh, qMesh];
467
+ return { objects, shared, blankTex, bruise, crush, tear, wetAt, ignite, debris, drops, update, reset, stats, simulate: (n, dt) => { for (let i = 0; i < n; i++) update(dt, shared.uDmgT.value + dt, dt); return stats(); }, crushMM, tearMM, wetness: (m, u, v) => (masks.has(m) ? at(masks.get(m), u, v, 3) : 0), dust: (x, y, z) => debris({ x, y, z }, 1.5, 0, 'tear') || spawnP(0, x, y, z, (Math.random() - 0.5) * 30, 8 + Math.random() * 12, 8 + Math.random() * 15, 0.3 + Math.random() * 0.4, 6, 0.13, 0.1, 0.08, 0.75) };
468
  }