@adatechnology/game-destruction-three

中文文档
0.15.4发布于 2026/11/04MITTypeScriptES Module构建来源已验证近 30 天下载 5248

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).

原始使用文档
文档目录7 节
  1. Install
  2. Usage
  3. Parts of a merged mesh
  4. What each event draws
  5. Performance
  6. API
  7. Português

@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

EventVisualSound key
dentvertices within dent.radius pushed in by depth * (1 - (d/r)^2)^2 (skipped on low for meshes over 20k vertices); sparks on metalimpact_*
holedark disc decal on the surface (and on the exit side of a through hole); a crack star on transparent materials; sparks on metalimpact_*
crackstar of jagged white lines on glass (transparent materials) only; an opaque body draws no lines, its damage shows as chips, holes and dentsimpact_*
ricochetsparks along the reflected directionricochet
debrisa burst of the requested kind (sparks, dust, shards, splinters, chips, fibers, leaves, drips, ash, smoke, steam)
fracturepart hidden; fragments (boxes, thin irregular prisms for shards) with gravity, ground bounce, settling, sleep and shrink-away; debris burst by styleshatter_glass, crumble, splinter, impact_*
spallopaque 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_*
exposethe bars as instanced cylinders (radius 4 mm), one mesh per part, replaced by each new expose; empty bars removes the cage; renderer.rebarCount()
detachwith autoSplit, each loose group becomes its own Group (world transforms kept) and a Piece (onPiece)detach
disassemble_progressunscrew (throttled)
ignite / extinguish / burnoutfire 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 pileignite, burn_loop, extinguish
heat (damage of type heat/fire, or any hot glowing part)emissive heatColor(temperature) for materials with visual.glows
meltmeshes sag (Y down to 25 %, a bit wider, noisy vertices, bottom stays put); status melted hides the part and leaves a puddlemelt_sizzle
dripfalling drop that leaves a flat puddle (merged, capped)
leakstream of drops (or a faint puff for gas) from the hole while the part still has liters
explodeflash, fire, smoke, dust and sparks, a pooled PointLight pulse, impulses on pieces and fragments in rangeexplode

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

Budgetmedium / highlow
Particles (maxParticles)25001250
Particle burst counts100 %60 %
Fragment meshes (maxFragments)16080, and every second fragment of an event is skipped
Vertex dents / melt noise on meshes over 20k verticesyesskipped
  • 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.

版本与发布记录

@adatechnology/game-destruction-three 共发布 33 个版本,首次发布于 2026/10/23;过去 12 个月发布了 33 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.15.4latest2026/11/041 天前
0.15.32026/11/041 天前
0.15.22026/11/041 天前
0.15.12026/11/042 天前
0.15.02026/11/042 天前
0.14.02026/11/042 天前

关于 @adatechnology/game-destruction-three 的常见问题

@adatechnology/game-destruction-three 是什么?

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).

如何安装 @adatechnology/game-destruction-three?

在项目目录运行 npm install @adatechnology/game-destruction-three,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:three(^0.160.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@adatechnology/game-destruction-three 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@adatechnology/game-destruction-three 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@adatechnology/game-destruction-three 可以商用吗?

@adatechnology/game-destruction-three 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@adatechnology/game-destruction-three 的下载量有多少?

根据 npm 官方下载统计,@adatechnology/game-destruction-three 在 2026/10/05 – 2026/11/03 这 30 天内被下载 5,248 次,日均约 175 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@adatechnology/game-destruction-three 还在维护吗?

最新版本 0.15.4 发布于 2026/11/04(1 天前);至今共发布 33 个版本,其中过去 12 个月发布了 33 个;首次发布于 2026/10/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。