Skip to content

@gameable/gameable-character ​

Classes ​

GameableCharacterError ​

A character could not be loaded; code says why, message says what to do.

Example ​

ts
import { GameableCharacterError, loadGameableCharacter } from 'gameable/three';

try {
  await loadGameableCharacter(renderer, url);
} catch (error) {
  if (error instanceof GameableCharacterError && error.code === 'webgpu-required') {
    console.warn('this browser cannot show the character yet');
  }
}

Extends ​

  • Error

Constructors ​

Constructor ​
ts
new GameableCharacterError(
   code, 
   message, 
   options?
): GameableCharacterError;
Parameters ​
ParameterTypeDescription
codeGameableCharacterErrorCodeWhy.
messagestringWhat happened and what to do.
options?{ cause?: unknown; }The underlying error, when there is one.
options.cause?unknown-
Returns ​

GameableCharacterError

Overrides ​
ts
Error.constructor

Properties ​

code ​
ts
readonly code: GameableCharacterErrorCode;

Why.

Interfaces ​

BonePose ​

A body pose a game hands a character: read by reference every frame, so change the numbers in place.

Properties ​

hips? ​
ts
readonly optional hips?: Readonly<{
  joint: string;
  x: number;
  y: number;
  z: number;
}>;

The hips' place on the joint named, in metres as the character is drawn (at its height), or none to leave them.

names ​
ts
readonly names: readonly string[];

The joints, by the skeleton's own names (a package's bone names), '' for one the game has and the skeleton does not (a game's own root). Keep the same array: it is bound to the bones once, and a new array binds again.

rotations ​
ts
readonly rotations: readonly Readonly<{
  w: number;
  x: number;
  y: number;
  z: number;
}>[];

Each joint's rotation in its parent's frame, one per name (x, y, z, w; three's own quaternions do).


CreateSharedCharacterSplatOptions ​

The options of createSharedCharacterSplat.

Example ​

ts
// two fighters of about 150,000 points each, on a stage 8 m across
const options = { capacity: 400_000, bounds: { center: [0, 1, 0], radius: 8 } };

Properties ​

bounds? ​
ts
readonly optional bounds?: object;

Where its characters stay, in the frame of the object it is added to: a sphere (centre, radius in metres). The sort spreads its 4096 depth steps across it, so a sphere much larger than the stage costs precision. Default: 12 m round the origin.

center ​
ts
readonly center: [number, number, number];
radius ​
ts
readonly radius: number;
capacity ​
ts
readonly capacity: number;

Points it can hold, fixed for its life: the characters it will hold added up (a character's points are its package's splatCount and its pose corrections'; a character it has no room for draws on its own).


EyeShear ​

The studio's two eye volumes and reach fade; null at the setter switches them off.

Example ​

ts
character.setEyeShear({
  left: { centre: [0.03, 1.6, 0], radius: 0.014, shear: [0.2, 0, 0] },
  right: { centre: [-0.03, 1.6, 0], radius: 0.014, shear: [0.2, 0, 0] },
  fade: [1, 1.35],
});

Properties ​

fade? ​
ts
readonly optional fade?: readonly [number, number];

Reach fade in eye radii, increasing. Studio default [1, 1.35].

left ​
ts
readonly left: EyeShearVolume;
ts
readonly right: EyeShearVolume;

EyeShearVolume ​

One calibrated eye volume in the package rest frame, as pcGazeFrame supplies it.

Example ​

ts
const eye: EyeShearVolume = { centre: [0.03, 1.6, 0], radius: 0.014, shear: [0.2, 0, 0] };

Properties ​

centre ​
ts
readonly centre: readonly [number, number, number];

Rest centre in metres, including the package coordinate conversion.

radius ​
ts
readonly radius: number;

Effective volume radius (studio rho * eye.radius), metres, positive.

shear ​
ts
readonly shear: readonly [number, number, number];

Studio shearVector output; no easing, clamping or camera math in the engine.


GameableCharacter ​

A loaded character.

Example ​

ts
scene.add(character.object3D);
character.play('idle');
character.setExpression('arkit52', { jawOpen: 0.3 });
// every frame: character.update(dt, camera)

Properties ​

castShadow ​
ts
castShadow: boolean;

Whether the character casts a shadow into your scene's shadow maps, from its body mesh (a character whose package has none casts nothing). The mesh is never drawn to the screen.

clip ​
ts
readonly clip: string;

The clip playing now (the one fading in, during a fade).

clips ​
ts
readonly clips: readonly string[];

The clip names this character can play, e.g. idle, wave.

expressionSpace ​
ts
readonly expressionSpace: object;

The head's own expression space; native frames must have exactly dim coefficients.

dim ​
ts
readonly dim: number;
kind ​
ts
readonly kind: "arkit52" | "gnm" | "gnm68";
hair ​
ts
readonly hair: AosrigHairState | null;

Sara's strand hair, when the package carries hair/ (its guide strands as bone slots after the rig's joints, simulated each frame): the slots, her sim's ms a frame, reset() after a teleport; null without it.

measures ​
ts
readonly measures: CharacterMeasures | null;

The character's measures as drawn, metres above its feet: the eyes, the head joint, the head's top, the crown of its points (hair and all), the hips, the stature (floor to crown) and its width. A framing or a camera is built from these, never from a number typed in. Null for a package whose rig has no head.

object3D ​
ts
readonly object3D: Object3D;

Add this to your scene. Move, turn and parent it like any Object3D; its feet are at its origin.

scale ​
ts
readonly scale: number;

How much larger (or smaller) it is drawn than it was exported, for the height option: 1 without it. The package's own measures are measures divided by it.

Methods ​

dispose() ​
ts
dispose(): void;

Remove the character from its parent and free its GPU memory. Idempotent.

Returns ​

void

Nothing.

lookAt() ​
ts
lookAt(target, options?): void;

Look at something: the eyes, the head and a little of the upper body turn toward it, over whatever clip plays, and follow it while it moves (an Object3D such as your camera is read every frame; so is a Vector3 you keep changing). null hands the head back to the clip.

The shares are the Gameable studio's defaults ("Looks at you"): the face turns head of the way (0.55, a tenth of the look taken by the upper body), and the eyes take eyes of what is left (1: all of it). The head turns at most 45 degrees from the body, the eyes 35 more.

Parameters ​
ParameterTypeDescription
targetObject3D<Object3DEventMap> | Vector3 | nullWhat to look at, in world space, or null.
options?LookAtOptionsEach part's share, 0..1.
Returns ​

void

Nothing.

Example ​
ts
character.lookAt(camera); // the visitor
character.lookAt(new Vector3(2, 1.5, 0), { head: 0.3 }); // a shelf, mostly with the eyes
character.lookAt(null); // back to the clip
play() ​
ts
play(name, options?): void;

Play a body clip by name.

Playing the clip that is already playing does nothing: a one-shot runs on to its end (and returns to the looping clip as it would have), a looping clip keeps looping.

Parameters ​
ParameterTypeDescription
namestringOne of clips.
options?PlayOptionsFade time and looping.
Returns ​

void

Nothing.

Throws ​

When the character has no clip by that name.

setBonePose() ​
ts
setBonePose(pose): void;

Your own body pose on the bones: a pose your game solves itself (its physics, its fight maths), laid on the skeleton after the clips and the head aim on every update, so your numbers are the last word on the body while the face, the blinks and the eyes stay the character's own. null hands the body back to the clips, the bones as they were when you took them. Read by reference every frame: keep the same names array (bound to the bones once) and change the numbers in place; the hips are in metres as drawn.

Parameters ​
ParameterTypeDescription
poseBonePose | nullThe pose, or null.
Returns ​

void

Nothing.

Example ​
ts
const pose = { names: skeleton.names, rotations: myRotations, hips: { joint: 'hips', x: 0, y: 0.97, z: 0 } };
character.setBonePose(pose); // then each frame: write myRotations and pose.hips, call update
character.setBonePose(null); // the clips again
setExposure() ​
ts
setExposure(exposure): void;

Multiply the character's brightness, on top of the tint.

Parameters ​
ParameterTypeDescription
exposurenumber1 is as captured; 1.2 is 20 % brighter.
Returns ​

void

Nothing.

Example ​
ts
character.setExposure(0.9);
setExpression() ​
Call Signature ​
ts
setExpression(space, weights): void;

Set the face, in Apple ARKit's 52 blendshapes: an array of 52 weights in ARKit order, or named weights ({ jawOpen: 0.4, eyeBlinkLeft: 1 }; names not given are 0; any capitalisation). The weights hold until the next call. Blinking carries on on top of ARKit input.

Use 'native' for the head's own GNM coefficients (see expressionSpace). Native frames are copied, keep signed values without clamping, and replace non-finite values with 0.

Parameters ​
ParameterTypeDescription
space"arkit52"'arkit52' or 'native'.
weightsArrayLike<number> | Readonly<Record<string, number>>ARKit weights in [0, 1] (clamped; NaN is 0), or exactly expressionSpace.dim native coefficients.
Returns ​

void

Nothing.

Throws ​

For a native frame of the wrong length, or native input on an ARKit head.

Example ​
ts
if (character.expressionSpace.kind !== 'arkit52') {
  const frame = new Float32Array(character.expressionSpace.dim);
  frame[0] = -0.5; // signed coefficients from the head's lip-sync model
  character.setExpression('native', frame);
}
character.setExpression('arkit52', { jawOpen: 0.3 });
Call Signature ​
ts
setExpression(space, weights): void;

Set the face directly in the head's own space; see expressionSpace.

Parameters ​
ParameterTypeDescription
space"native"'native'.
weightsArrayLike<number>Exactly expressionSpace.dim coefficients, copied without clamping; non-finite values become 0.
Returns ​

void

Nothing.

Throws ​

For a different length, or a character whose space is arkit52.

Example ​
ts
if (character.expressionSpace.kind !== 'arkit52') {
  character.setExpression('native', new Float32Array(character.expressionSpace.dim));
}
setEyeShear() ​
ts
setEyeShear(frame): void;

Apply the studio's calibrated virtual-eye shear on rest centres and covariances. Caller computes shearVector/viewSlope and converts centres/vectors into the package frame. An explicit frame replaces procedural eye requests; null restores them. Off by default.

Parameters ​
ParameterTypeDescription
frameEyeShear | nullTwo eye volumes with effective radii, shear vectors and optional reach fade, or null.
Returns ​

void

Nothing.

Throws ​

For invalid values or the vertex-only WebGL skin path.

Example ​
ts
character.setEyeShear({
  left: { centre: [0.03, 1.6, 0], radius: 0.014, shear: [0.2, 0, 0] },
  right: { centre: [-0.03, 1.6, 0], radius: 0.014, shear: [0.2, 0, 0] },
  fade: [1, 1.35],
});
character.setEyeShear(null);
setJointAffines() ​
ts
setJointAffines(names, affines): void;

Replace surface proportions with the studio's per-joint affine deltas, before skinning. Centres and covariances take the same normalized weighted affine. Off by default. Skeleton transforms remain supplied by the caller through setBonePose.

Parameters ​
ParameterTypeDescription
namesreadonly string[]Package joint names in the same order as the affine blocks; omitted joints are identity.
affinesFloat32Array<ArrayBufferLike> | nullThree row-major [A-I
Returns ​

void

Nothing.

Throws ​

On invalid names/data or the vertex-only WebGL skin path.

Example ​
ts
character.setJointAffines(['Hips'], new Float32Array([
  0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]));
character.setJointAffines([], null);
setTint() ​
ts
setTint(color): void;

Multiply the character's colour (linear light): a tint to fit it into your scene's light.

Parameters ​
ParameterTypeDescription
colorColorRepresentationAny three colour: '#ffe6cc', 0xffe6cc, a Color. White is none.
Returns ​

void

Nothing.

Example ​
ts
character.setTint('#ffe6cc'); // a warm room
update() ​
ts
update(dt, camera): void;

Advance the animation and pose the character. Call once per frame, before renderer.render(scene, camera).

Parameters ​
ParameterTypeDescription
dtnumberSeconds since the last frame.
cameraCameraThe camera you render with (the character's colours depend on the view).
Returns ​

void

Nothing.


LoadGameableCharacterOptions ​

Options for loadGameableCharacter.

Example ​

ts
const character = await loadGameableCharacter(renderer, url, {
  clip: 'wave',
  fetch: (input, init) => fetch(input, { ...init, credentials: 'include' }),
});

Properties ​

allowSoftwareRenderer? ​
ts
readonly optional allowSoftwareRenderer?: boolean;

Draw even when the renderer runs in software (no GPU; see isSoftwareRenderer). Default false: the load throws software-renderer there, before anything is downloaded, since a character draws at well under a frame a second on the CPU and freezes the page. Left unset, the page decides: a headless capture browser that wants the characters in its pictures sets globalThis.__gameableAllowSoftwareRenderer = true before the game loads (see pageAllowsSoftwareRenderer).

allowWebGL? ​
ts
readonly optional allowWebGL?: boolean;

Draw on the WebGL2 fallback, when the browser has no WebGPU. Default true: the same passes run there as transform feedback, slower, and the gaussians' draw order is sorted on the CPU and lags the camera by a frame or a few. False refuses the fallback: the load throws webgpu-required, for an app that would rather show something else.

attachPass? ​
ts
readonly optional attachPass?: boolean;

Attach the sRGB pass (see attachSrgbPass) to the character's scene on its first update, with the scene's onBeforeRender / onAfterRender hooks. Default true. False for an app that assigns those hooks itself or renders through something that skips them: it attaches the pass with attachManualPass and calls begin(camera) just before its render and end() just after.

castShadow? ​
ts
readonly optional castShadow?: boolean;

Cast a shadow into your scene's shadow maps (the renderer's shadowMap.enabled and a light with castShadow), from the character's body mesh, skinned to its skeleton. Default false: it costs a skinned draw per shadow-casting light each frame (six for a point light). The mesh is made the first time it turns on. See castShadow.

clip? ​
ts
readonly optional clip?: string;

The clip to start with. Defaults to idle when the character has one, else its first.

exposure? ​
ts
readonly optional exposure?: number;

A multiplier on the character's brightness, like renderer.toneMappingExposure. Default 1.

fetch? ​
ts
readonly optional fetch?: (input, init?) => Promise<Response>;

Fetch every file of the package with this instead of the global fetch (for credentials, a proxy, or an asset manager). Same signature as fetch.

Parameters ​
ParameterType
inputURL | RequestInfo
init?RequestInit
Returns ​

Promise<Response>

hair? ​
ts
readonly optional hair?: boolean;

Sara's strand hair, when the package carries it (hair/): its rows ride her simulated strands and character.hair reports it. Default true. False loads the character with the hair as captured (skinned to the rig).

height? ​
ts
readonly optional height?: number;

Draw the character at its real height, metres, floor to the crown of its own points (hair and all). The studio makes every character about 1.75 m tall; a 1.88 m person's character is then drawn 1.88 m tall: its points, its joints (object3D is scaled, from the floor between its feet), its shadow and its GameableCharacter.measures all at that size. Default: as exported (GameableCharacter.scale 1).

Example ​
ts
const logan = await loadGameableCharacter(renderer, url, { height: 1.88 });
lighting? ​
ts
readonly optional lighting?: boolean;

Give every point a surface direction at load, read from the meshes the points ride on and turned with the joints every frame, so a scene's lights fall evenly on skin and cloth once the character is lit (a shared cloud's SharedCharacterSplat.setLighting). Default false: nothing is read and the load is as before. Lit without it, each point's direction is guessed from its own shape, which breaks a face and a chest into blotches close up. It costs loading time, read in slices between frames: about 11 s more for four fighters of 2.5 million points between them on a desk (5.4 s to 16 s). WebGPU only.

Example ​
ts
const fighter = await loadGameableCharacter(renderer, url, { shared: cloud, lighting: true });
mouth? ​
ts
readonly optional mouth?: boolean;

Draw the inside of the mouth (teeth, gums, tongue) when the lips part, for a package that carries the teeth's files. Default false, as on the studio's own stage, where it is off unless switched on.

mouthMesh? ​
ts
readonly optional mouthMesh?: "plain" | "textured";

Draw the package's frozen mouth mesh with plain colours or its texture. Enables the mouth draw and loads mouth.json / mouth.glb (and mouth_texture.png for textured). Omit to keep the default mouth behaviour.

Example ​
ts
const character = await loadGameableCharacter(renderer, 'characters/miranda/character.json', {
  mouthMesh: 'textured',
});
scene.add(character.object3D);
mouthMotion? ​
ts
readonly optional mouthMotion?: "frame" | "studio";

Mouth-neighbourhood expression motion: frame (default) keeps the triangle frame; studio uses four weighted vertex displacements without expression rotation/stretch. Body skinning, plugs and visibility rules are retained. As in the studio, band size caps apply while the mouth interior is drawn; the dark sheet keeps the skin's full coverage. Selected at load, independently of whether the interior is drawn.

Example ​
ts
const character = await loadGameableCharacter(renderer, 'characters/miranda/character.json', {
  mouthMotion: 'studio',
});
scene.add(character.object3D);
onProgress? ​
ts
readonly optional onProgress?: (loadedBytes, totalBytes) => void;

Told as the character's files arrive: the bytes so far and the bytes expected in all (the sizes the package lists, its shared clip pack included). Both count the files as they are, not as they travel compressed. The last call has loadedBytes === totalBytes; the character is then built on the GPU (a moment more) before loadGameableCharacter resolves.

Parameters ​
ParameterType
loadedBytesnumber
totalBytesnumber
Returns ​

void

Example ​
ts
onProgress: (loaded, total) => (bar.value = total > 0 ? loaded / total : 0),
shared? ​
ts
readonly optional shared?: SharedCharacterSplat;

Draw the character into a cloud it shares with other characters (createSharedCharacterSplat), so the points of all of them are sorted together: where two overlap, each point is drawn in its true order (a fighter's arm round the other's legs in front of them, his back behind them), not one whole character over the other. Its tint and exposure are then the cloud's, and the inside of its mouth is not drawn. Ignored on the WebGL2 fallback, and for a character the cloud has no room for, or whose colours are not sRGB: it draws in a cloud of its own, as without the option.

signal? ​
ts
readonly optional signal?: AbortSignal;

Aborts the download.

tint? ​
ts
readonly optional tint?: ColorRepresentation;

A colour to multiply the character's by (in linear light), to fit it into your scene's light: '#ffe6cc' warms it. White (the default) keeps the captured colours. See setTint.


LookAtOptions ​

Each part's share of a look (see GameableCharacter.lookAt).

Example ​

ts
const mostlyEyes: LookAtOptions = { head: 0.3, eyes: 1 };
character.lookAt(camera, mostlyEyes);

Properties ​

eyes? ​
ts
readonly optional eyes?: number;

How much of what the head leaves the eyes take. Default 1.

ts
readonly optional head?: number;

How far the face turns toward the target, of the whole look. Default 0.55.


ManualPass ​

The characters' pass driven by the app (see attachManualPass).

Example ​

ts
const pass: ManualPass = attachManualPass(renderer, scene);
pass.begin(camera);
renderer.render(scene, camera);
pass.end();

Methods ​

begin() ​
ts
begin(camera): void;

Call just before renderer.render(scene, camera): draws the characters for this camera.

Parameters ​
ParameterTypeDescription
cameraCameraThe camera the app renders with.
Returns ​

void

Nothing.

dispose() ​
ts
dispose(): void;

Take the pass off the scene.

Returns ​

void

Nothing.

end() ​
ts
end(): void;

Call just after the render.

Returns ​

void

Nothing.


PlayOptions ​

Options for GameableCharacter.play.

Example ​

ts
character.play('wave', { fade: 0.4 });

Properties ​

fade? ​
ts
readonly optional fade?: number;

Cross-fade time from the current clip, seconds. Default 0.25.

loop? ​
ts
readonly optional loop?: boolean;

Loop the clip. Defaults to what the clip says (the studio marks one-shot gestures such as a wave as not looping). A clip that does not loop fades back to the previous looping clip when it ends.


SharedCharacterSplat ​

A cloud of points that several characters draw into, so their points are sorted together.

Splats sort only within one object (docs/concepts/splats.md, Draw order). Two characters each in a cloud of their own are drawn one whole after the other, and wherever they overlap on screen the second covers the first, whichever is nearer: a fighter's arm round the other's legs is drawn behind them, then, a moment later, his whole body in front. Characters loaded with this as their shared option each write their points into a slice of the one cloud, which sorts and draws them all at once, every point in its true order.

Example ​

ts
const cloud = await createSharedCharacterSplat(renderer, { capacity: 800_000 });
scene.add(cloud.object3D);
const fighter = await loadGameableCharacter(renderer, url, { shared: cloud });
cloud.setExposure(1.2); // every character in it

Properties ​

available ​
ts
readonly available: number;

Points not given to a character yet.

capacity ​
ts
readonly capacity: number;

Points it holds.

object3D ​
ts
readonly object3D: Object3D;

Add it to the scene once, where the characters are. Each character stands where its own object3D is.

sorted ​
ts
readonly sorted: boolean;

False on the WebGL2 fallback: there each character draws in a cloud of its own.

Methods ​

dispose() ​
ts
dispose(): void;

Free the cloud (dispose its characters first).

Returns ​

void

setExposure() ​
ts
setExposure(value): void;

A multiplier on the brightness of every character in the cloud (see GameableCharacter.setExposure).

Parameters ​
ParameterType
valuenumber
Returns ​

void

setLighting() ​
ts
setLighting(lighting): void;

Light every character in the cloud with the scene's lights (directional, spot, point, hemisphere and ambient, as three lights a mesh), mixed with the light each was captured in by emissionWeight; null draws them as captured again, the default. Load the characters with lighting: true for their surface directions. Call it when the list of lights changes, not every frame: the lights themselves stay live. Does nothing on the WebGL2 fallback.

Parameters ​
ParameterType
lightingSplatEnvironmentLighting | null
Returns ​

void

Example ​
ts
import { AmbientLight, SpotLight } from 'three/webgpu';
const spot = new SpotLight(0xff9a5c, 46, 0, Math.PI / 5, 0.6, 2);
cloud.setLighting({
  radianceSH: Array.from({ length: 9 }, () => [0, 0, 0] as const),
  spotLights: [spot], ambientLights: [new AmbientLight(0x2a2230, 1)],
  emissionWeight: 0.25, diffuseWeight: 1, wrap: 0.3,
});
setTint() ​
ts
setTint(color): void;

A colour to multiply every character in the cloud by (see GameableCharacter.setTint).

Parameters ​
ParameterType
colorColorRepresentation
Returns ​

void

Type Aliases ​

GameableCharacterErrorCode ​

ts
type GameableCharacterErrorCode = 
  | "webgpu-required"
  | "renderer-not-ready"
  | "load-failed"
  | "software-renderer";

Why a character could not be loaded.

Example ​

ts
import type { GameableCharacterErrorCode } from 'gameable/three';
const code: GameableCharacterErrorCode = 'webgpu-required';

Variables ​

PACKAGE ​

ts
const PACKAGE: "gameable/three";

Package identity marker.

Example ​

ts
import { PACKAGE } from 'gameable/three';
console.log(PACKAGE); // 'gameable/three'

Functions ​

attachManualPass() ​

ts
function attachManualPass(renderer, scene): ManualPass;

The pass the characters are drawn in, driven by the app instead of by the scene's onBeforeRender / onAfterRender hooks: for an app that assigns those itself, or renders through something that skips them. Load the characters with { attachPass: false }, attach this once per scene, and call begin(camera) just before each render and end() just after.

Parameters ​

ParameterTypeDescription
rendererWebGPURendererThe app's renderer.
sceneSceneThe scene the characters are in.

Returns ​

ManualPass

The pass.

Example ​

ts
const character = await loadGameableCharacter(renderer, url, { attachPass: false });
scene.add(character.object3D);
const pass = attachManualPass(renderer, scene);
renderer.setAnimationLoop(() => {
  character.update(timer.getDelta(), camera);
  pass.begin(camera);
  renderer.render(scene, camera);
  pass.end();
});

createSharedCharacterSplat() ​

ts
function createSharedCharacterSplat(renderer, options): Promise<SharedCharacterSplat>;

Make a cloud for several characters to draw into, sorted together (see SharedCharacterSplat).

Parameters ​

ParameterTypeDescription
rendererWebGPURendererYour WebGPURenderer, after await renderer.init().
optionsCreateSharedCharacterSplatOptionsIts capacity and where its characters stay.

Returns ​

Promise<SharedCharacterSplat>

The cloud. Add object3D to your scene, then load each character with { shared: cloud }.

Example ​

ts
const cloud = await createSharedCharacterSplat(renderer, { capacity: 800_000 });
scene.add(cloud.object3D);
const a = await loadGameableCharacter(renderer, urlA, { shared: cloud });
const b = await loadGameableCharacter(renderer, urlB, { shared: cloud });
scene.add(a.object3D, b.object3D);

isSoftwareRenderer() ​

ts
function isSoftwareRenderer(renderer): boolean;

Whether the renderer runs in software: no GPU, the browser emulating one on the CPU (SwiftShader, llvmpipe, Windows' basic render driver). A character draws there at well under a frame a second, so loadGameableCharacter refuses it unless allowSoftwareRenderer is set; an app can ask first and show its placeholder straight away.

Parameters ​

ParameterTypeDescription
rendererWebGPURendererThe app's renderer, after await renderer.init().

Returns ​

boolean

True when it runs in software; false on a GPU or when the browser does not say.

Example ​

ts
await renderer.init();
if (isSoftwareRenderer(renderer)) showPlaceholder();
else scene.add((await loadGameableCharacter(renderer, url)).object3D);

loadGameableCharacter() ​

ts
function loadGameableCharacter(
   renderer, 
   url, 
   options?
): Promise<GameableCharacter>;

Load a character published from the Gameable studio into your own three.js app.

url is the character's character.json; every file it names is fetched relative to it and checked against its sha256 before use, so a URL on another origin is safe to load (the host must send CORS headers; the studio does). Versions 1 and 2 of the package format load, plain or packed.

The renderer must be a WebGPURenderer (from three/webgpu) on real WebGPU, initialised. The character uses the renderer's own device and needs no device limits above WebGPU's defaults, so there is nothing to call before renderer.init().

Parameters ​

ParameterTypeDescription
rendererWebGPURendererYour WebGPURenderer, after await renderer.init().
urlstringThe character's character.json.
optionsLoadGameableCharacterOptionsThe first clip, the mouth, tint, exposure and shadow, a custom fetch, progress, an abort signal, and allowWebGL.

Returns ​

Promise<GameableCharacter>

The character. Add object3D to your scene and call update every frame.

Throws ​

webgpu-required on the WebGL2 fallback with allowWebGL: false, renderer-not-ready before renderer.init(), load-failed when the package cannot be read or has no clip named options.clip, software-renderer when the renderer runs in software.

Example ​

ts
import { WebGPURenderer, Scene, PerspectiveCamera, Timer } from 'three/webgpu';
import { loadGameableCharacter } from 'gameable/three';

const renderer = new WebGPURenderer({ antialias: true });
await renderer.init();
const scene = new Scene();
const camera = new PerspectiveCamera(40, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 1.5, 3.5);

const character = await loadGameableCharacter(renderer, 'https://studio.example/.../character.json');
scene.add(character.object3D);
character.play('wave');

const timer = new Timer();
renderer.setAnimationLoop((time) => {
  timer.update(time);
  character.update(timer.getDelta(), camera);
  renderer.render(scene, camera);
});

pageAllowsSoftwareRenderer() ​

ts
function pageAllowsSoftwareRenderer(): boolean;

Whether the page lets characters draw on a software renderer when the app does not say: globalThis.__gameableAllowSoftwareRenderer === true. A headless capture browser with no GPU (a sandbox taking a picture of a game nobody has open) sets it before the game loads, so the characters are in its pictures without the game asking for allowSoftwareRenderer itself.

Returns ​

boolean

True when the page has opted in.

Example ​

ts
// in the capture browser, before the game's own scripts run
(globalThis as Record<string, unknown>).__gameableAllowSoftwareRenderer = true;
pageAllowsSoftwareRenderer(); // true