@adatechnology/game-characters-three
v0.9.4
Published
three.js presentation for @adatechnology/game-characters: the instanced crowd body (GPU animation, LOD, baking, variants, flying limbs) and the rich bodies (the Bestiário v17 builder, survivors with accessories and hands, the animator, wounds, eyes).
Maintainers
Readme
@adatechnology/game-characters-three
three.js presentation for @adatechnology/game-characters. three.js is passed in by the host (import * as THREE from 'three'), so the same code runs from a bundler or from a plain script (dist/index.global.js exposes the GameCharactersThree global).
Instanced crowd body (literal port of the game's, G 65336-65541):
buildInstancedBody(THREE, { segments, body? }): the merged geometry (torso, head, hair, jaw, eyes, arms, legs) tagged with bone, pivot and material; 16, 10 and 5 segments are the three levels of detail.bodydefaults toTEAM_GAME; anyBodySpecmoves the joints.BODY_CHUNK/createBodyChunk: the GPU pose chunk, byte for byte from the game (only the hip joint is a parameter).createBodyMaterialadds the tints and glowing eyes.createBodyBatch:begin,pushper body,end; one draw call for a whole variant.bakeBodyandbakeVariants: bake a rich body into an instanceable geometry; the three variants rule of the game (7000 + c·101 + v·13).createFlyingLimbs: 40 torn-off pieces that fly, bounce and stay, with injected blood effects.
import * as THREE from 'three'
import { buildInstancedBody, createBodyBatch } from '@adatechnology/game-characters-three'
const time = { value: 0 }
const batch = createBodyBatch(THREE, buildInstancedBody(THREE, { segments: 10 }), { max: 700, time })
scene.add(batch.mesh)
batch.begin()
batch.push({ a: [x, z, yaw, phase], b: [1.8, 1, stride, 0], c: [missingMask, infection, 1, skinTone], color: { r: 0.3, g: 0.4, b: 0.5 }, eye: { r: 1, g: 0.2, b: 0.1 } })
batch.end()
time.value += dtSound and blood of the bodies (on @adatechnology/game-audio and the running film of @adatechnology/game-fluids-three):
createCharacterSound(host.audio, { style? }):voice(typeId, 'idle' | 'alert' | 'special', position)for the 13 undead types (UNDEAD_VOICES: the game's groans and scream, the art scene'svoicegroan, scream and leap, with the class rule of the page for the types it does not have),body(event, position)(wound = flesh, cut = sever, burst or split = gore),player('grab' | 'bite' | 'hurt'). The 16 recipes (CHARACTER_SOUND_BANK) leave the same trace as the team code.createFluidsBridge({ sink, film })andbloodLookOfAge(age): abodyevent throws drops of blood into the film, in the fresh and dry colours of the infection age (ages.json),old_bloodfor the thin ones.
Player avatar. createPlayerAvatar(THREE, options?) is the team game's player body: the rich human of the Bestiário/Elenco built from presets/player-avatar.json
({ type: 'Lento', human: true, seed: 4242, skin, hs: 'short', hair }, shirt 0x3d5a73), overridable with { spec, shirtColor, clips, blend, fallSeconds, cascade, clipCascade } (clipCascade, default 5: how many times faster the joints follow while a clip that does not loop plays, so a strike reaches its pose by its impact).
const player = createPlayerAvatar(THREE)
scene.add(player.root)
player.update(dt, { speed, crouch, down, armed, pitch, hip }) // the gait is tied to the ground speed (cadence and swing from `walkCadence`/`walkGait`)
player.hand('R').add(weaponMesh) // a socket at the weapon hand
player.play('grabbed_bitten_human', { onEvent: (event) => event.type === 'impact' && applyBlow() }) // or player.play(myClip); player.on('event', fn); player.stop()Crouch, down and the aimed arms are derived literally from the game's GPU pose code (BODY_CHUNK, lines quoted in src/player/avatar.ts): crouch is thigh -0.95, knee 1.9, trunk +0.25, hip -0.35;
down turns the body about the hip (±1.5 rad, face up or down by the phase) with the limbs of pose 2; armed follows (pitch + 1) / 3. play overlays a PoseClip of @adatechnology/game-characters
(channels: 'upper' leaves the legs to the gait; while crouched only a kick takes the legs, so a punch or the guard stays low) with a 0.12 s blend, and fires its events (impact, phase) once through onEvent and on('event'). hand('R') is the weapon hand (the puppet's right, at -x). A pose clip's armL/legL channels move that side (arms[1]/legs[0]), so punch_right and front_kick of ATTACK_CLIPS drive armL and legL, the same arm and leg the first-person view shows.
In the test room. room.setAvatar(toRoomAvatar(player, { strikeClips: ATTACK_CLIPS })) (or defaultCharacterClips() of game-kit-defaults, which also has the first-person clips) shows the body at the player's feet (toRoomAvatar only flips the pitch: the room counts a look up as positive, the team game a look down) and V switches between the first and the third person of
@adatechnology/game-test-room. createAvatarTestModule({ avatar, camera, state }) is the module of the area avatar_area (AVATAR_AREA_ID): bench buttons for the view (1st/3rd person), crouch, stand, fall, get up, armed on/off,
one button per registered clip (Tocar: grabbed_bitten_human, grab_bite_zombie, and with clips: [...POSE_CLIPS, ...ATTACK_CLIPS] the punch, shove, kick and the recoils; clipLocales gives the phase texts) and stop; metrics for the camera, the clip playing, the last clip event and the joints. Every action is a JSON line in the room log ({"type":"avatar","action":"play_clip","clip":...}), clip events included, and an impact event also gets its own {"action":"impact","clip":...,"t":...} line.
The module takes the body, the view (camera: { mode, set }) and the state (state: { set }) by option and imports only @adatechnology/game-contracts.
toRoomAvatar also gives the room strike(clipId), which plays the attack clip on the body (the only source of a strike, first and third person) and answers the seconds to the impact read from that clip (the same in both views). strike(clip, { leg: 'left' }) kicks with the left leg and strike(clip, { hand: 'left' }) punches with the left hand: the mirror of the clip on the body (mirrorPoseClip, id <clip>_left). After a punch (an impact of kind punch) the body holds the guard (guardClip, default 'fight_guard', false for none) for
guardHold seconds (default 1.2) while it stands still, so a combo goes guard, punch, guard; the punch blends in over punchBlend (default 0.05 s). Walking, running, a weapon or a held item drop the guard.
First-person arms. createFirstPersonArms(THREE, { colors?, rig?, stances?, clips?, blend?, bob? }) builds sleeves (upper arm, forearm, elbow, cuff) and hands of skin (palm, four fingers of three segments, a thumb that close on a grip) drawn in the camera's space, plus the right leg that only shows during a kick. arms.update(dt, { camera, targets, stance, speed, running, crouch, contact? }): the relaxed and fists stances are rest poses (FIRST_PERSON_STANCES, from presets/first-person-arms.json), hold follows the HandTargets of game-contracts (world space) with a two-bone IK from fixed shoulders, the elbow bending down and out; a target out of reach is clamped (the shoulder moves reachShift toward it first). arms.play('punch_right' | 'shove_two_hands' | 'front_kick') overlays a FirstPersonClip (FIRST_PERSON_CLIPS of game-characters-people, same durations and impact seconds as the attack clips); arms.on('event', fn) gives its impact; arms.handPose(side) reads where a hand is drawn. contact(origin, direction, maxDistance) (optional; distance to the first surface or null) makes the hands and the kick foot stop a few millimetres before a surface along the camera forward (the elbow bends); arms.play('front_kick', { leg: 'left' }) kicks with the left leg and arms.play('punch_right', { hand: 'left' }) punches with the left hand (the hands of the clip in a mirror). Hand frame: x wrist to knuckles, y back of the hand, z little-finger side; a left hand is the right one mirrored.
Geometry
The builders (createPersonBuilder, createBestBuilder, bakeBody, buildInstancedBody, createFlyingLimbs, createEye) draw with
shapeGeometries(host) of game-shapes-three: boxes, cylinders, cones, spheres, circles, rings, lathes and tori come
from game-shapes with the same bytes; dodecahedron, shape and capsule geometries are three's. tests/exact-golden.test.ts holds every bit as
the old path made them.
