Scene & entities

How the world is represented inside Pip3D: nodes, transforms, components and cameras.

Scene graph basics

Pip3D uses a simple node-based scene graph. Each node holds a transform and may have one or more components attached.

  • Root node: entry point for the whole world or a sub-scene.
  • Children: describe hierarchical relationships (camera attached to a moving rig, light attached to a mesh, etc.).
  • Components: meshes, lights, cameras and other bits of data that describe what a node does.

Transforms and cameras

Every node has a transform (position, rotation, scale). Transforms can be evaluated in local or world space depending on where they sit in the hierarchy.

  • A camera component defines view and projection for a render pass.
  • Most projects use a single camera, but multi-pass setups are possible.
  • Node parenting allows you to move whole groups by changing one transform.

Meshes and lights

Mesh components reference geometry data and basic material properties, while lights define a minimal lighting model tuned for the engine.

  • Meshes: vertices, indices and simple shading flags (solid, wireframe, etc.).
  • Lights: a tiny set of types – typically a main directional light and a few local accents.

In a typical frame your own game logic updates transforms, and then the engine walks the graph, collects visible meshes and sends them through the renderer.

Camera system

The camera subsystem is designed for real-time 3D rendering on ESP32-S3 without PSRAM and follows familiar concepts from larger engines.

Overview

  • World-space camera. Stores position, target (look-at point) and up vector in world coordinates.
  • Configurable projection. Supports perspective, orthographic and fisheye projections.
  • Lazy evaluation. View and projection matrices are cached and recomputed only when needed.
  • Helpers. Includes animation support, free-fly camera, orbit camera and a builder pattern for convenient setup.

Use these APIs when configuring or controlling cameras from your application code.

Camera (core view camera)

The core camera type used by the renderer. This is the main struct you work with when controlling the view.

struct Camera
{
    Vector3 position;
    Vector3 target;
    Vector3 up;
    ProjectionType projectionType;

    float fov;
    float nearPlane;
    float farPlane;

    float orthoWidth;
    float orthoHeight;

    float fisheyeStrength;

    CameraConfig config;
};

Defaults

  • Position (0, 0, -5), target (0, 0, 0), up (0, 1, 0) (normalized).
  • Projection: PERSPECTIVE, FOV 60 degrees.
  • Near/far planes: 0.1f / 100.0f.
  • Orthographic defaults: orthoWidth = orthoHeight = 10.
  • Fisheye: fisheyeStrength = 0 by default.

Projection setup

  • setPerspective(fovDegrees, near, far). Configures perspective projection, clamping FOV to [1, 179] degrees and enforcing a sensible near/far relationship. Sets projectionType = PERSPECTIVE and clears fisheye distortion.
  • setOrtho(width, height, near, far). Configures orthographic projection, clamping width/height and near/far to safe values. Sets projectionType = ORTHOGRAPHIC.
  • setFisheye(fovDegrees, strength, near, far). Enables a fisheye-style projection with radial distortion and clamps parameters to safe ranges.

View/projection matrices

  • getViewMatrix() builds a look-at matrix from position, target and up on demand and caches it.
  • getProjectionMatrix(aspect) recomputes the projection when parameters or aspect ratio change beyond CameraConfig::aspectEps.
  • getViewProjectionMatrix(aspect) returns the cached proj * view matrix, updating it lazily when needed.

When changing projection or camera parameters directly, call markDirty() to rebuild cached matrices on the next frame.

CameraController (view/projection management)

CameraController centralizes camera-dependent matrix updates and projection for the renderer.

class CameraController
{
public:
    static void updateViewProjectionIfNeeded(Camera &camera, const Viewport &viewport,
                                             Matrix4x4 &viewMatrix, Matrix4x4 &projMatrix,
                                             Matrix4x4 &viewProjMatrix, Frustum &frustum,
                                             bool &viewProjMatrixDirty,
                                             bool &cameraChangedThisFrame);

    static Vector3 project(const Vector3 &v, const Matrix4x4 &viewProjMatrix,
                           const Viewport &viewport);
};

updateViewProjectionIfNeeded

  • Checks whether the cached view/projection matrix is dirty due to renderer flags or camera cache flags.
  • When an update is needed, computes aspect ratio from the Viewport, fetches view and projection matrices from Camera, builds viewProjMatrix and extracts the frustum planes.
  • Marks viewProjMatrixDirty as cleared and sets cameraChangedThisFrame so the dirty-region system can react.

This function is called internally from Renderer::beginFrame(); user code normally does not call it directly.

project

  • Transforms a world-space position by viewProjMatrix, performs the homogeneous divide and converts NDC to framebuffer pixel coordinates using the viewport.
  • Preserves z as post-projection depth for visibility tests.
  • Used indirectly via Renderer::project, by MeshRenderer, ShadowRenderer, culling helpers and the dirty-region system.

Projection types and camera config

enum ProjectionType
{
    PERSPECTIVE,
    ORTHOGRAPHIC,
    FISHEYE
};
  • PERSPECTIVE. Standard 3D camera controlled by fov, nearPlane, farPlane and viewport aspect ratio.
  • ORTHOGRAPHIC. Parallel projection without perspective, controlled by orthoWidth, orthoHeight, nearPlane, farPlane.
  • FISHEYE. Wide-angle perspective with additional radial distortion, tuned by fov and fisheyeStrength.
struct CameraConfig
{
    float aspectEps;
};

aspectEps. Threshold for aspect-ratio changes in getProjectionMatrix(aspect); if |aspect - lastAspect| <= aspectEps, the cached projection is reused. Default is 1e-6f.

Camera animation and helpers

CameraAnimation

struct CameraAnimation
{
    Vector3 startPos, startTgt, startUp;
    Vector3 targetPos, targetTgt, targetUp;
    float startFov, targetFov;
    float time, duration;

    enum Type : uint8_t
    {
        LINEAR,
        SMOOTH,
        EASE
    } type;

    bool active : 1;
};

High-level interpolation state used internally by Camera for smooth transitions between positions, targets and FOVs.

  • Stores start/target state, accumulated time, duration and easing type.
  • Use the camera's animate* APIs instead of modifying this struct directly.

FreeCam

class FreeCam : public Camera
{
public:
    float rotSpeed;
    float moveSpeed;
};

A high-level helper camera that provides free-fly controls driven by input axes and buttons.

  • rotSpeed – base rotation speed in degrees per second.
  • moveSpeed – base movement speed in world units per second.
  • Helper methods such as handleJoystick, handleButtons, handleDPad and handleRotateButtons apply movement and rotation based on input.

Use FreeCam for debug views or editor-style cameras.

OrbitCam

class OrbitCam : public Camera
{
public:
    Vector3 center;
    float   radius;
    float   azimuth;
    float   elevation;
    float   zoomSpd;
    float   rotSpd;
};

A camera that orbits around a focus point at a configurable radius and angles.

  • center – world-space orbit center (usually the inspected object).
  • radius, azimuth, elevation – polar coordinates around center.
  • zoomSpd, rotSpd – zoom and rotation speeds.
  • Provides helpers such as setCenter, zoom, handleJoystick and handleButtons to drive the orbit.

Use OrbitCam for object inspection, isometric views and third-person orbiting cameras.

CameraBuilder

class CameraBuilder
{
public:
    CameraBuilder& at(const Vector3& pos);
    CameraBuilder& lookAt(const Vector3& tgt);
    CameraBuilder& withUp(const Vector3& up);
    CameraBuilder& persp(float fov = 60, float near = 0.1f, float far = 100);
    CameraBuilder& ortho(float w = 10, float h = 10, float near = 0.1f, float far = 100);
    CameraBuilder& fisheye(float fov = 120, float str = 1, float near = 0.1f, float far = 100);
    CameraBuilder& withConfig(const CameraConfig& cfg);
    Camera           build();
};

Fluent helper for constructing a configured Camera in a single expression. Typical usage:

Camera camera = CameraBuilder()
    .at({0.0f, 2.5f, 6.0f})
    .lookAt({0.0f, 0.0f, 0.0f})
    .persp(60.0f, 0.1f, 100.0f)
    .build();

Integration notes:

  • The renderer stores and manages one or more Camera instances.
  • The scene graph (CameraNode) applies its transform and projection parameters to a Camera and calls Camera::markDirty() so caches are refreshed.
  • Prefer using public camera APIs (setPerspective, setOrtho, movement/rotation helpers, etc.) instead of editing cache fields directly.

Scene graph API

The scene graph provides a lightweight hierarchy of nodes with parent-child relationships and attached components.

Node

  • Base class with local transform (position, rotation, scale), parent pointer and an array of children.
  • Owns its children and deletes them in the destructor.
  • Transform mutators (setPosition, setRotation, setScale, translate, rotate) mark the node and all descendants as transform-dirty.
  • getWorldTransform() lazily recomputes the world matrix from the parent chain; getWorldPosition() extracts its translation.
  • update(dt) and render(renderer) recurse into children when the node is enabled/visible.

MeshNode

  • Derives from Node and attaches a Mesh* plus a castShadows flag.
  • Can optionally own the mesh (controlled by the ownsMesh parameter in constructors and setMesh).
  • In render(renderer) copies the node transform into the mesh and calls either ObjectHelper::renderWithShadow or Renderer::drawMesh, then traverses children.

CameraNode

  • Derives from Node and stores fov, nearPlane, farPlane and projectionType.
  • applyToCamera(Camera&) copies these parameters plus the node world position into a target Camera and calls Camera::markDirty().
  • Typical usage: create a CameraNode, move it like any other node and set it as the active camera in SceneGraph.

LightNode

  • Derives from Node and stores LightType, Color, intensity, direction and range.
  • applyToLight(Light&) fills a Light struct: for directional lights it uses the normalized direction; for point lights it uses the node world position and range for attenuation.

SceneGraph and SceneBuilder

  • SceneGraph owns a single root Node and a pointer to a Renderer.
  • Manages creation of MeshNode, CameraNode and LightNode attached to the root, the active camera and a small list of active lights.
  • update(dt) calls root->update(dt).
  • render() applies the active camera, submits up to four enabled & visible lights to the renderer, then calls renderer.beginFrame(), root->render(renderer) and renderer.endFrame().

SceneBuilder provides a small fluent wrapper around SceneGraph:

SceneBuilder builder(&renderer);
SceneGraph* scene = builder
    .withCamera(0.0f, 2.5f, 6.0f, 60.0f)
    .withSun(-0.3f, -1.0f, -0.2f)
    .addMesh(&someMesh, "Object")
    .build();

This pattern lets you quickly set up a camera, sun light and a few meshes with sensible defaults.

Frustum and culling

View frustum utilities reject objects that lie completely outside the camera field of view.

The renderer and instance manager use this system internally:

  • Renderer maintains a single Frustum built from the active Camera.
  • Renderer::drawMesh / drawMeshInstance perform a fast sphere test before drawing.
  • InstanceManager::cull filters instances using the same sphere test.

FrustumPlane

struct FrustumPlane
{
    Vector3 n; // plane normal (pointing inward into the frustum)
    float d;   // plane distance in the equation n·p + d = 0

    FrustumPlane();
    FrustumPlane(const Vector3 &normal, float dist);
    FrustumPlane(const Vector3 &p0, const Vector3 &p1, const Vector3 &p2);

    float distanceToPoint(const Vector3 &p) const;
    bool containsSphere(const Vector3 &center, float radius) const;
    bool containsPoint(const Vector3 &p) const;
};
  • Represents planes in standard form n·p + d = 0 in world space; n is normalized when constructed from three points.
  • distanceToPoint returns signed distance; positive values mean the point lies inside the frustum half-space.
  • containsPoint and containsSphere are helpers for visibility tests.

CullingResult

enum CullingResult
{
    CULLED  = 0,
    PARTIAL = 1,
    VISIBLE = 2
};

CULLED means the tested volume lies completely outside the frustum, VISIBLE means fully inside and PARTIAL means it intersects the frustum.

CameraFrustum and Frustum

class CameraFrustum
{
public:
    enum
    {
        NEAR = 0,
        FAR = 1,
        LEFT = 2,
        RIGHT = 3,
        TOP = 4,
        BOTTOM = 5
    };

    void extractFromViewProjection(const Matrix4x4 &vp);
    void extract(const Matrix4x4 &vp);

    bool sphere(const Vector3 &center, float radius) const;
    bool box(const Vector3 &min, const Vector3 &max) const;
    bool point(const Vector3 &p) const;

    CullingResult cull(const Vector3 &center, float radius) const;
    float factor(const Vector3 &center, float radius) const;

    const FrustumPlane &getPlane(int i) const;
};
using Frustum = CameraFrustum;
  • extractFromViewProjection builds six frustum planes from a combined proj * view matrix and is called from the renderer whenever the camera changes.
  • sphere, box and point provide fast inclusion tests for bounding volumes.
  • cull returns a CullingResult for more detailed visibility decisions.
  • factor returns a continuous visibility factor in [0, 1] for smooth fade-in/out or quality scaling near frustum edges.

For most projects you let the engine handle frustum culling via the renderer and instance manager. Advanced code can query Renderer::getFrustum() to perform custom visibility tests.