Skip to content

Draw characters the way the studio does ​

Goal ​

A character exported from the Gameable studio looks in your game as it looks on its page in the studio: its points drawn and added up the studio's way, the dark inside of its mouth, see-through effects in front of it or behind it where they belong, and, if you want them, the studio's lens blur of the room behind it and the studio's forming as it appears. The look of the points has a default, chosen by Isaac on the looks sheet of 30 Sep 2026: the colours added up as stored with the size-aware point; a game, a manifest entry or the package may ask for another by name.

Files you will edit ​

  • src/main.ts (the host: where the engine and the character bridge are made)

Steps ​

  1. Make the engine with edge smoothing off, as the studio draws:

    ts
    const engine = await createEngine({
      canvas,
      manifest,
      renderer: { backend: 'webgpu', antialias: false },
    });

    Pixel ratio: in close views engine.renderer.setPixelRatio(Math.max(window.devicePixelRatio, 2)); otherwise 1.5, stepping to 1 only when frames run long.

  2. The default, nothing to write: every character the bridge spawns from a package is drawn with its colours added up as stored and the size-aware point (chosen by Isaac on the looks sheet of 30 Sep 2026: Ari of Hollow Reign and Logan Paul at a game's distance and close up, every look side by side). To ask for another look, name it at whichever level fits: every character the bridge spawns (the bridge's extras), one character in the game's manifest (rig.look), or the package itself ("look" in its character.json); the host's extras win over the manifest, the manifest over the package. Two fields, each optional:

    • blend: 'stored' (the default) puts the character on the engine's own layer, where its colours add up in the file's own space as the studio adds them (the layer stands ready, renderer.splatBlend: 'characters', and costs nothing until a character is on it; 'studio' puts places on it too, as the games of 28 September ran; 'linear' is no layer at all). 'linear' is the line before 30 September, colours added up in linear light.
    • pointShape: 'size-aware' (the default) keeps the engine's soft point while a point is small on screen and takes the studio's once it is large, blended across a band of sizes (4 to 8 pixels of footprint; bridge.entryOf(entity)?.splat?.setSizeAwareBand(lo, hi) moves it); 'studio' draws each point as the studio's page draws it (a small point keeps its whole opacity, 2.83 sigma with the tail pulled to zero, points under 2 pixels across not drawn); 'full-opacity' is the studio's small-point rule alone; 'engine' is the line's own 2-sigma point with its small-point fade.
    ts
    const bridge = createCharacterBridge({
      engine,
      renderer: engine.renderer,
      scene: engine.scene,
      extras: { pointShape: 'studio', mouthMesh: 'plain', keepSharedFiles: true },
    });

    Measured on Logan Paul's head close-up against the training renderer: the line 28.0 dB; the colours as stored with the line's point 30.7; with the size-aware point 37.5; with the studio's point 37.4 (the studio's own page 35.7). At a game's distance (a face of about 100 pixels) the three looks differ by eye more than by number; the measure page has the command.

  3. The plain mouth (mouthMesh: 'plain' above): its window follows the lips a person sees, its inside the studio's one dark red (mouthLook: 'studio'), tuned with the studio's look on. keepSharedFiles keeps the head and skeleton for a fuller copy swapped in later.

  4. See-through things (rain, trails, sparks) draw in front of a character where they are in front of it. A haze, fog or mist sheet that must never cover a character is flagged:

    ts
    haze.userData.underCharacters = true;
  5. Optional, the studio's lens blur of the room behind the characters. Every frame, focus on the hero's eyes; null turns it off. Things flagged underCharacters blur with the room.

    ts
    engine.splatLayer?.setLensBlur({
      focus: engine.camera.position.distanceTo(heroEyes),
      fStop: 1.4,
    });
  6. Optional, the studio's forming: add pointHooks: true to the extras and give each character's splat a point hook, bridge.entryOf(entity)?.splat?.setPointHook(builder). examples/forming is the whole of it, the studio's particle cloud included.

  7. Start with a character's lighter copy and swap in the full one when it has loaded: await bridge.upgrade?.(entity, '/characters/hero/character.json', { fadeMs: 350 }). The swap keeps everything above, and a line being spoken goes on across it.

Verify ​

  • engine.splatLayer is not null on a WebGPU machine (it is null with splatBlend: 'linear'); a character asked for blend: 'stored' carries userData.splatLayer === true on its points.
  • Put the camera where the studio's stands (its 35 mm lens is 37.8 degrees high) and compare with the character's page in the studio: the same colours, the dark mouth when it opens.
  • Measure it against the training renderer at the same camera: the one command prints the dB.
  • Measured on the desk's card while it was shared with other work: the layer's passes add about 0.4 ms a frame at 1024 x 1024; the lens blur 0.3 to 0.6 ms a frame at 1920 x 1080.

See also ​