npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@adatechnology/game-destruction-three

v0.15.4

Published

three.js renderer for @adatechnology/game-destruction: dents, holes, char, heat glow, melting, fragments, loose pieces and pooled particles (fire, smoke, sparks, dust, shards, splinters).

Readme

@adatechnology/game-destruction-three

three.js renderer for @adatechnology/game-destruction. The core decides what a hit does and reports it as events; this package draws them on your meshes: vertex dents, bullet-hole and crack decals, char and heat glow, melting and puddles, fragments with light physics, loose pieces after a part comes off, and pooled particles (fire, smoke, sparks, dust, shards, splinters, ...).

  • Nothing is changed in place: geometry and materials are cloned on the first visual change of a mesh, and restore / clear give the originals back.
  • Blasts and the fires of burning parts are drawn by the effects world of @adatechnology/game-vfx (an explode event plays ev.effect ?? 'generic', scaled by the radius when it names none). Pass the world the host shares in vfx; without it the renderer makes its own world and its own game-vfx-three layer and steps them in update. Debris, sparks, drips and leaks still use two pooled THREE.Points (additive and normal blending), with a soft sprite computed in the shader (no texture asset); they move to the shared sprite pool when game-vfx gets one.
  • three is a peer dependency used for types only: you pass your own THREE instance at runtime.
  • The renderer plays nothing: you get keys (impact_metal, shatter_glass, burn_loop, burn_end, ...) through onSound. createDestructionSound(engine) is the sound bank for them on a @adatechnology/game-audio engine: the Cena de Arte's material sounds (clang, wood, glass, crumble, thud, boom...) and the game's explosion thump, a fire loop that follows how many parts burn, and a key table (style: 'art_scene' | 'game', keys to replace) so a game changes what each key plays. An impact, a break or a ricochet also sends detail.keys, the keys to try first, most specific first: impact_<material>_<blow>, impact_<material>, impact_<family>_<blow>, impact_<family> (blow is the hit's DamageType, carried by every destruction event as blow; the family comes from impactFamily: metal, glass, ceramic, soil, masonry, wood, plant, plastic, rubber, wax, soft). IMPACT_SOUND_KEYS gives a bullet, a blow and a blade their own sound on each family; a game replaces or silences any key in keys.

Contracts live in @adatechnology/game-contracts; the types are re-exported here, so existing imports keep working.

Install

pnpm add @adatechnology/game-destruction-three @adatechnology/game-destruction three

Browser without a bundler (the global is GameDestructionThree; it bundles the core, so you only need three.js and this file):

<script src="https://unpkg.com/@adatechnology/game-destruction-three/dist/index.global.js"></script>
<script>
  const { spec, parts } = GameDestructionThree.specFromObject3D(THREE, model)
</script>

Usage

import * as THREE from 'three'
import { createDestruction } from '@adatechnology/game-destruction'
import { createDestructionRenderer, specFromObject3D } from '@adatechnology/game-destruction-three'

const scene = new THREE.Scene()
const world = createDestruction({ seed: 7 })
const renderer = createDestructionRenderer(THREE, {
  scene,
  world,
  quality: 'medium',
  groundAt: (x, z) => terrain.heightAt(x, z), // default: flat at y = 0
  onPiece: (piece) => grabbable.push(piece),
  onSound: (key, point, volume) => audio.play(key, point, volume),
})

// 1. describe a model: one part per mesh, materials guessed from names, joints inferred from touching parts
scene.add(chairModel)
const { spec, parts } = specFromObject3D(THREE, chairModel, { name: 'chair', anchorBelow: 0.01 })

// 2. register it in the world and bind it to the model (the world follows chairModel's transform)
const id = world.add(spec, { position: [2, 0, -3] })
renderer.bind(id, chairModel, { parts })

// 3. hit it, burn it, take it apart
world.shoot([0, 1.5, 0], [0, 0, -1], 2500, 9)
world.hit(id, { type: 'fire', point: [2, 0.4, -3], temperature: 900, radius: 0.3, duration: 0.2 })

// 4. every frame
function frame(dt: number) {
  world.update(dt)
  renderer.update(dt)
}

// make a bound object a thrown prop
const piece = renderer.makePiece(id, { mass: 6 })
renderer.impulse(id, [0, 40, -120], [2, 0.6, -3])

bind finds the meshes of each part from options.parts (what specFromObject3D returns), else from userData.destructionPart or a mesh name that equals a part id, else by the part box that contains the mesh centre. specFromObject3D reads userData.destructionMaterial, destructionPart (a part id, or { id, material?, parent?, kind? }: parent + kind attach the part to its parent with a joint of that kind), destructionIgnore, destructionTags and hollow.

For picking, bind and the loose pieces write back to the meshes: every mesh of a part has userData.destructionPart (the part id) and userData.destructionObject (the id of the world object the part belongs to now; a loose piece made by splitGroup has an id of its own, the same one renderer.piece(id) and world.state(id) use), also on the mesh a vertex-range part gets when it leaves a merged mesh. A raycast hit goes to world.hitPart(hit.object.userData.destructionObject, hit.object.userData.destructionPart, ...). Purely visual meshes (fracture fragments, decals, ground stains) have userData.destructionDebris = true: skip them when picking. Parts drawn by a range of a merged mesh share it, so the merged mesh itself carries no part id.

Parts of a merged mesh

A building or a baked model often has one merged mesh for many parts. Bind each part by the vertex range it occupies ({ mesh, range: [v0, v1] }, end exclusive; a part value can also be an array mixing meshes and ranges):

renderer.bind(houseId, houseRoot, { parts: {
  wall_north: { mesh: merged, range: [0, 1280] },
  wall_south: { mesh: merged, range: [1280, 2610] },
  door: doorMesh,                      // plain meshes still work
} })

For indexed geometry the faces whose three vertices all lie in the range belong to the part; for non-indexed geometry the range is the part's triangles. Dents move only the range's vertices; char and heat go to the range's vertex colours (the mesh gets a color attribute, and a cloned material with vertexColors, when it has none); hole and crack decals stay on the part; melt squashes and widens only the range. When the part detaches or collapses (detach and collapse events), its faces are copied, with their dents and colours and the merged mesh's transform and material, into a new mesh that becomes the loose piece, and the range is collapsed to a point in the merged mesh so it vanishes there. A broken, melted or burnt-out part vanishes the same way. restore and clear put the merged mesh's original geometry and material back and remove the loose meshes. An invalid range (outside the geometry or empty) throws from bind.

What each event draws

| Event | Visual | Sound key | |---|---|---| | dent | vertices within dent.radius pushed in by depth * (1 - (d/r)^2)^2 (skipped on low for meshes over 20k vertices); sparks on metal | impact_* | | hole | dark disc decal on the surface (and on the exit side of a through hole); a crack star on transparent materials; sparks on metal | impact_* | | crack | star of jagged white lines on glass (transparent materials) only; an opaque body draws no lines, its damage shows as chips, holes and dents | impact_* | | ricochet | sparks along the reflected direction | ricochet | | debris | a burst of the requested kind (sparks, dust, shards, splinters, chips, fibers, leaves, drips, ash, smoke, steam) | | | fracture | part hidden; fragments (boxes, thin irregular prisms for shards) with gravity, ground bounce, settling, sleep and shrink-away; debris burst by style | shatter_glass, crumble, splinter, impact_* | | spall | opaque patch (disc decal) in the colour of the part's core material where a coat came off, under the holes; sits on the face of the mesh the point is nearest to (the thin axis of its world box) | | | stage (2, with fragments) | the unit-sized fragments of the breach fly out with the same physics as fracture fragments, the holes of Fragment.marks ride on them as dark discs, a dust burst; the part stays drawn (cutting the opening in its faces is left to the house renderer) | crumble, impact_* | | expose | the bars as instanced cylinders (radius 4 mm), one mesh per part, replaced by each new expose; empty bars removes the cage; renderer.rebarCount() | | | detach | with autoSplit, each loose group becomes its own Group (world transforms kept) and a Piece (onPiece) | detach | | disassemble_progress | | unscrew (throttled) | | ignite / extinguish / burnout | fire and smoke from the surface while part.burning; steam when put out; ash at burnout; char lerps the colour to the material charColor; status ashes hides the part and leaves an ash pile | ignite, burn_loop, extinguish | | heat (damage of type heat/fire, or any hot glowing part) | emissive heatColor(temperature) for materials with visual.glows | | | melt | meshes sag (Y down to 25 %, a bit wider, noisy vertices, bottom stays put); status melted hides the part and leaves a puddle | melt_sizzle | | drip | falling drop that leaves a flat puddle (merged, capped) | | | leak | stream of drops (or a faint puff for gas) from the hole while the part still has liters | | | explode | flash, fire, smoke, dust and sparks, a pooled PointLight pulse, impulses on pieces and fragments in range | explode |

settle (an object with DestructibleSpec.soil settled or tilted): on the first one the renderer moves the children of the bound root under a group named destruction-soil and gives it ObjectState.soil.offset, updated every frame while the soil yields; the root keeps the host's pose, which is what syncTransform keeps sending to the world.

world.explode emits no event of its own (only a part with explodes does), so a host that detonates a grenade or rocket calls renderer.explode(point, energy, radius) next to it to get the same flash, fire, smoke, light, sound and shove.

Pieces report hard landings: above 3 m/s the renderer calls world.hit(id, { type: 'blunt', energy: 0.5 m v^2, radius: 0.08, direction: [0, -1, 0], source: 'fall' }) (throttled to once per 0.15 s per piece; the object's transform is synced to the world first, so the hit lands on the right spot), so a dropped glass pane shatters and a thrown chair can lose a leg. Piece.localCenter is the centre of mass relative to the root origin (it moves when a part breaks off).

Performance

| Budget | medium / high | low | |---|---|---| | Particles (maxParticles) | 2500 | 1250 | | Particle burst counts | 100 % | 60 % | | Fragment meshes (maxFragments) | 160 | 80, and every second fragment of an event is skipped | | Vertex dents / melt noise on meshes over 20k vertices | yes | skipped |

  • Particles are two fixed-size ring buffers (additive 40 %, normal 60 %), one draw call each; dead particles cost a loop iteration, nothing else.
  • Fragments share one box geometry, six shard geometries and one material per source material id and colour; they shrink away instead of fading.
  • Decals share one disc geometry and one material; a mesh keeps at most 48 and the renderer 600.
  • Heat, char and melt are only read for "active" parts (hot, burning, melting) plus a scan of all parts every 0.4 s.
  • The explosion light is created on the first explosion (one light in the scene afterwards, intensity 0 at rest) so plan for the one-time shader recompile.
  • Melt assumes upright meshes (it scales the mesh along its own Y).

API

createDestructionRenderer(THREE, options: RendererOptions): DestructionRenderer
specFromObject3D(THREE, root, options?: SpecFromObjectOptions): { spec, parts }
heatColor(temperatureC: number): number // 0xRRGGBB, 0 below ~450 C, dark red ~500, orange ~800, yellow ~1100, white from 1400

RendererOptions: scene, world, quality, maxFragments, maxParticles, fragmentLife, groundAt, gravity, autoSplit, onPiece, onSound, vfx (shared effects world), camera (of the own world, when vfx is not given), pointScale (point size in pixels per metre at 1 m; about viewportHeight / (2 tan(fov / 2)), default 700).

How long debris stays: fragmentLife is the seconds a resting fragment stays before it shrinks away (default 12, 0 = forever), or a FragmentLifeRule (fragment, objectId) => seconds that decides per fragment from its material, size and shape and the object it came from, e.g. rubble that stays and chips that go: fragmentLife: (f) => (Math.max(...f.size) > 0.1 ? 0 : f.material === 'glass' ? 30 : 12). maxFragments caps how many live at once (the oldest fades first). Loose pieces (a door, a leg, a slab that breaks off whole) never fade: they stay in the map with collision. Dust, smoke and chip sprites have the life of their kind in @adatechnology/game-vfx (patchSpriteKinds(SPRITE_KINDS, { dust: { life: [10, 30] } }) on the effects world).

DestructionRenderer: bind, unbind, makePiece, pieces, piece, impulse, update, emit, explode(point, energy, radius, { effect?, direction?, visual? }) (visual: false keeps only the shove and the sound, for a host that played the effect itself), clear, restore, stats, dispose. clear() removes fragments, decals, puddles and particles from the scene and restores every bound mesh; dispose() also unsubscribes from the world.

Português

Renderizador three.js para o @adatechnology/game-destruction. O núcleo decide o que cada golpe faz e devolve eventos; este pacote desenha: amassados nos vértices, furos de bala e rachaduras, carbonização e brilho de calor, derretimento e poças, fragmentos com física leve, peças soltas e partículas em pool (fogo, fumaça, faíscas, poeira, cacos, lascas). Geometria e materiais só são clonados na primeira mudança visual de uma malha, e restore / clear devolvem os originais. O renderer não toca nada: o jogo recebe chaves (impact_metal, shatter_glass, burn_loop, burn_end, ...) em onSound; createDestructionSound(engine) é o banco de sons delas no game-audio (sons dos materiais da Cena de Arte, o estouro do jogo, o laço de fogo e a tabela chave → som, trocável).

Uso: specFromObject3D(THREE, modelo) gera a especificação e o mapa parte -> malhas; registre no mundo com world.add(spec), ligue com renderer.bind(id, modelo, { parts }) e chame world.update(dt) e renderer.update(dt) a cada quadro. Com quality: 'low' os orçamentos de partículas e fragmentos caem pela metade.