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
Summary
Add a
Camera3dclass 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
Proposed API
Coordinate Projection Utilities
Camera2d currently has
localToWorld(x, y)andworldToLocal(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)
worldToScreen/screenToWorldreplace (or alias) the existingworldToLocal/localToWorld— same behavior, clearer namingworldToScreenmultiplies through the view-projection matrix, maps NDC [-1,1] to viewport pixels.screenToWorldunprojects using the inverse view-projection + a depth valueKey Design Decisions
Matrix3d.perspective(fov, aspect, near, far)— may need to add this methoddrawMesh()already supports 3D transforms — just needs the perspective projectionpostEffects/beginPostEffect/endPostEffectpipeline (inherits from Renderable)Implementation Notes
Matrix3dclass supports 4x4 matrices and hasortho()— needs aperspective()method addedlookAt()method on Matrix3d would be useful for the view matrixpos.z— this maps naturally to depth in the perspective viewdrawMesh()already enablesDEPTH_TEST— the 3D camera should enable it for the full scenezproperty or render order index as the Z depthlocalToWorld/worldToLocalon Camera2d in favor of the unifiedworldToScreen/screenToWorldnamingRelated
Matrix3dclass with 4x4 matrix supportdrawMesh()with depth testing and 3D transformsCamera2d.localToWorld()/Camera2d.worldToLocal()for 2D projectionpos.zon all Renderables