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) andupvector 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, FOV60degrees. -
Near/far planes:
0.1f/100.0f. -
Orthographic defaults:
orthoWidth = orthoHeight = 10. -
Fisheye:
fisheyeStrength = 0by default.
Projection setup
-
setPerspective(fovDegrees, near, far).
Configures perspective projection, clamping FOV to
[1, 179]degrees and enforcing a sensible near/far relationship. SetsprojectionType = PERSPECTIVEand 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 fromposition,targetandupon demand and caches it. -
getProjectionMatrix(aspect)recomputes the projection when parameters or aspect ratio change beyondCameraConfig::aspectEps. -
getViewProjectionMatrix(aspect)returns the cachedproj * viewmatrix, 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 fromCamera, buildsviewProjMatrixand extracts the frustum planes. -
Marks
viewProjMatrixDirtyas cleared and setscameraChangedThisFrameso 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
zas post-projection depth for visibility tests. -
Used indirectly via
Renderer::project, byMeshRenderer,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,farPlaneand viewport aspect ratio. -
ORTHOGRAPHIC.
Parallel projection without perspective, controlled by
orthoWidth,orthoHeight,nearPlane,farPlane. -
FISHEYE.
Wide-angle perspective with additional radial distortion, tuned by
fovandfisheyeStrength.
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,handleDPadandhandleRotateButtonsapply 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 aroundcenter. -
zoomSpd,rotSpd– zoom and rotation speeds. -
Provides helpers such as
setCenter,zoom,handleJoystickandhandleButtonsto 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
Camerainstances. -
The scene graph (
CameraNode) applies its transform and projection parameters to aCameraand callsCamera::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)andrender(renderer)recurse into children when the node is enabled/visible.
MeshNode
-
Derives from
Nodeand attaches aMesh*plus acastShadowsflag. -
Can optionally own the mesh (controlled by the
ownsMeshparameter in constructors andsetMesh). -
In
render(renderer)copies the node transform into the mesh and calls eitherObjectHelper::renderWithShadoworRenderer::drawMesh, then traverses children.
CameraNode
-
Derives from
Nodeand storesfov,nearPlane,farPlaneandprojectionType. -
applyToCamera(Camera&)copies these parameters plus the node world position into a targetCameraand callsCamera::markDirty(). -
Typical usage: create a
CameraNode, move it like any other node and set it as the active camera inSceneGraph.
LightNode
-
Derives from
Nodeand storesLightType,Color,intensity,directionandrange. -
applyToLight(Light&)fills aLightstruct: for directional lights it uses the normalizeddirection; for point lights it uses the node world position andrangefor attenuation.
SceneGraph and SceneBuilder
-
SceneGraphowns a single rootNodeand a pointer to aRenderer. -
Manages creation of
MeshNode,CameraNodeandLightNodeattached to the root, the active camera and a small list of active lights. -
update(dt)callsroot->update(dt). -
render()applies the active camera, submits up to four enabled & visible lights to the renderer, then callsrenderer.beginFrame(),root->render(renderer)andrenderer.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:
-
Renderermaintains a singleFrustumbuilt from the activeCamera. -
Renderer::drawMesh/drawMeshInstanceperform a fast sphere test before drawing. -
InstanceManager::cullfilters 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 ¢er, float radius) const;
bool containsPoint(const Vector3 &p) const;
};
-
Represents planes in standard form
n·p + d = 0in world space;nis normalized when constructed from three points. -
distanceToPointreturns signed distance; positive values mean the point lies inside the frustum half-space. -
containsPointandcontainsSphereare 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 ¢er, float radius) const;
bool box(const Vector3 &min, const Vector3 &max) const;
bool point(const Vector3 &p) const;
CullingResult cull(const Vector3 ¢er, float radius) const;
float factor(const Vector3 ¢er, float radius) const;
const FrustumPlane &getPlane(int i) const;
};
using Frustum = CameraFrustum;
-
extractFromViewProjectionbuilds six frustum planes from a combinedproj * viewmatrix and is called from the renderer whenever the camera changes. -
sphere,boxandpointprovide fast inclusion tests for bounding volumes. -
cullreturns aCullingResultfor more detailed visibility decisions. -
factorreturns 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.