Skip to content

Add Camera3d with perspective projection for 2D layers and 3D meshes #1412

@obiot

Description

@obiot

Summary

Add a Camera3d class that renders the existing 2D world (tile layers, sprites) and 3D meshes using a perspective projection. This enables a "Paper Mario" style effect where 2D layers are rendered as flat planes at different Z depths, giving the scene real 3D parallax and depth.

Use Cases

  • Paper Mario / diorama effect: 2D tile layers and sprites rendered as flat cards in 3D space, with perspective foreshortening revealing the layered construction
  • Mixed 2D/3D scenes: 2D gameplay with 3D mesh objects (decorations, characters, items)
  • Cinematic camera moves: pan, zoom, orbit, and tilt on a 2D game world for dramatic effect

Proposed API

// create a 3D perspective camera
const cam = new Camera3d(0, 0, 800, 600, {
    fov: 60,          // field of view in degrees
    near: 0.1,
    far: 2000,
});

// position the camera in 3D space
cam.position.set(400, 300, 500);   // x, y, z
cam.lookAt(400, 300, 0);           // look at the world origin

// basic transforms
cam.pan(dx, dy);     // move camera in screen plane
cam.zoom(amount);    // dolly forward/backward along view axis

// layers get Z depth from their z-index or a dedicated depth property
// sprites and meshes use their existing pos.z

Coordinate Projection Utilities

Camera2d currently has localToWorld(x, y) and worldToLocal(x, y) for 2D coordinate conversion. Camera3d needs 3D equivalents, and the naming should be unified across both camera types:

Unified API (both Camera2d and Camera3d)

// world → screen: project a world position to screen pixel coordinates
const screenPos = cam.worldToScreen(x, y, z);  // returns Vector2d (pixel coords)

// screen → world: unproject screen pixels to world position
const worldPos = cam.screenToWorld(screenX, screenY, depth);  // returns Vector3d
  • Camera2d: worldToScreen / screenToWorld replace (or alias) the existing worldToLocal / localToWorld — same behavior, clearer naming
  • Camera3d: worldToScreen multiplies through the view-projection matrix, maps NDC [-1,1] to viewport pixels. screenToWorld unprojects using the inverse view-projection + a depth value
  • Use case: position a 2D sprite at a 3D location (e.g. player standing on a 3D platform floor), UI labels anchored to 3D objects, picking

Key Design Decisions

  • Extends Camera2d or new class? — likely a new class extending Renderable directly, since Camera2d's ortho projection is fundamental to its design
  • Perspective matrix: Matrix3d.perspective(fov, aspect, near, far) — may need to add this method
  • View matrix: lookAt-style or position + rotation
  • 2D layers as 3D planes: each TMX layer / container is rendered at a Z depth, using the existing draw pipeline but with the perspective projection active
  • Mesh integration: existing drawMesh() already supports 3D transforms — just needs the perspective projection
  • Post-effects: should work with the existing postEffects / beginPostEffect / endPostEffect pipeline (inherits from Renderable)

Implementation Notes

  • The existing Matrix3d class supports 4x4 matrices and has ortho() — needs a perspective() method added
  • A lookAt() method on Matrix3d would be useful for the view matrix
  • Renderables already have pos.z — this maps naturally to depth in the perspective view
  • drawMesh() already enables DEPTH_TEST — the 3D camera should enable it for the full scene
  • TMX layers could use their z property or render order index as the Z depth
  • Consider deprecating localToWorld / worldToLocal on Camera2d in favor of the unified worldToScreen / screenToWorld naming

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions