Skip to content

feat: Sprite3d — 3D billboard sprites + shared FrameAnimation engine#1513

Merged
obiot merged 1 commit into
masterfrom
feat/sprite3d-billboards
Jun 23, 2026
Merged

feat: Sprite3d — 3D billboard sprites + shared FrameAnimation engine#1513
obiot merged 1 commit into
masterfrom
feat/sprite3d-billboards

Conversation

@obiot

@obiot obiot commented Jun 23, 2026

Copy link
Copy Markdown
Member

What

Adds Sprite3d, the 3D counterpart of Sprite: a textured quad rendered under a Camera3d for the 2.5D workflow (characters, pickups, foliage, signs, particles). As a thin Mesh subclass it rides the same world-space pipeline (depth testing, frustum culling) and material flags (lit, emissive, alphaCutoff).

Frame animation is now shared between Sprite and Sprite3d via a new FrameAnimation engine, so 2D and 3D animation behave identically.

Highlights

  • Billboardingbillboard: false | "cylindrical" | "spherical" (Camera3d-only). Same up/forward convention as a loaded glTF scene, so they compose without flipping.
  • Shared FrameAnimation engine — one implementation drives both hosts (addAnimation / setCurrentAnimation / play / pause / stop, looping/chaining/speed). Engine owns the animation state; hosts expose it via accessors and apply frames through an applyFrame callback.
    • Dirty-flag ownership: the host owns isDirty (set in _applyFrame); the engine never reads/writes it, and update() returns a pure changed signal. Both hosts' update() are uniform (engine → super.update()).
  • Texture atlas parity — packed TextureAtlas regions including rotated and trimmed frames map onto the quad (90° UV permutation + logical-frame sub-rect placement). Mesh's resolver is now framewidth/frameheight-aware.
  • Alpha cutout — the mesh pass is opaque, so alphaCutoff defaults to 0.5 to cleanly cut out a sprite's transparent background (correct depth, no sorting); 0 for a fully-opaque quad.
  • flipX() / flipY() — mirror the local quad geometry; works across all billboard modes + rotated/trimmed regions, and persists through animation cycles.
  • Camera3d.getBasis / getRight / getUp / getForward — world-space basis accessors.
  • Pooled current.offset released on destroy() (also fixes a pre-existing Sprite leak).

Example

New Billboard Sprites example: the same animated character shown in all three billboard modes side by side (FIXED goes edge-on; UPRIGHT/FACE keep facing the camera) with floating name tags. Uses the shared cityscene atlas (packer-rotated + trimmed capguy/walk frames), so it also validates atlas mapping + transparency end-to-end.

Tests

  • Host-parametrized battery run against both Sprite and Sprite3d (every accessor + proxied method, adversarial cases, dirty-flag ownership, the changed return).
  • FrameAnimation in isolation (proves the engine sets no isDirty).
  • Sprite3d: billboard orientation (deterministic worldToScreen), trim/rotation/combined atlas mapping, alphaCutoff, flipX/flipY (incl. flip × rotated/trimmed atlas, full-cycle persistence, on-screen mirror), resource cleanup.

Full suite: 4399 passing, lint 0 errors, types clean, examples tsc clean.

Docs

CHANGELOG (19.8 _unreleased_) and the Working in 3D wiki page updated (Sprite3d section: billboard modes, animation, atlas parity, transparency/alphaCutoff, flipX/flipY, add-to-world examples).

🤖 Generated with Claude Code

Add `Sprite3d`, the 3D counterpart of `Sprite`: a textured quad rendered
under a `Camera3d` for the 2.5D workflow (characters, pickups, foliage,
signs). Headline feature is billboarding via `billboard: false |
"cylindrical" | "spherical"`. As a thin `Mesh` subclass it rides the same
world-space pipeline (depth testing, frustum culling) and material flags
(`lit`, `emissive`, `alphaCutoff`).

Frame animation is shared with `Sprite` through a new `FrameAnimation`
engine, so 2D and 3D animation behave identically (timing, looping,
chaining, speed). The engine owns the animation state; each host exposes it
via accessors and applies the current frame through an `applyFrame`
callback. The host owns its `isDirty` flag (set in `_applyFrame`); the
engine never touches it and `update()` returns a pure `changed` signal.

Highlights:
- billboard modes (fixed / cylindrical / spherical), Camera3d-only
- `Camera3d.getBasis`/`getRight`/`getUp`/`getForward` basis accessors
- packed `TextureAtlas` parity — rotated AND trimmed regions mapped onto
  the quad (UV permutation + logical-frame sub-rect placement)
- `alphaCutoff` defaults to 0.5 (opaque mesh pass) so transparent sprite
  backgrounds cut out cleanly; `0` for a fully-opaque quad
- `flipX()` / `flipY()` mirror the local quad — works across billboard
  modes and rotated/trimmed regions, persists through animation cycles
- `Mesh` texture resolver is now framewidth/frameheight-aware
- pooled `current.offset` released on destroy (also fixes a pre-existing
  Sprite leak)
- new Billboard Sprites example (same character in all three modes)
- CHANGELOG + Working-in-3D wiki updated

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@obiot obiot merged commit eab8241 into master Jun 23, 2026
6 checks passed
@obiot obiot deleted the feat/sprite3d-billboards branch June 23, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant