Lysa Nodes  0.0
Lysa Nodes — Scene Graph for the Lysa Engine
Camera

lysa::nodes::Camera

Camera extends both Node and lysa::Camera. Its updateGlobalTransform() override keeps lysa::Camera::transform equal to globalTransform and immediately calls SceneTree::setCamera(*this), so the renderer always sees the current world-space camera transform without any manual synchronisation step.

Creating a perspective camera

auto camera = std::make_shared<Camera>(
75.0f, // FOV in degrees
0.01f, // near clipping distance
100.0f, // far clipping distance
"Camera" // node name
);

The projection matrix is computed when the camera is attached to a scene tree that is itself attached to a render target. Camera::_attachToScene reads the render target's current aspect ratio and subscribes to RESIZED so the projection is automatically rebuilt whenever the window is resized.

For this reason the camera must be added to the scene tree after SceneTree::attach(window) has been called (see Scene tree).

Node hierarchy for yaw / pitch

The camera is placed inside a two-node hierarchy that separates yaw from pitch to avoid gimbal lock:

cameraAttachment = std::make_shared<Node>("Camera attachment"); // world pos + yaw
cameraPivot = std::make_shared<Node>("Camera pivot"); // pitch
camera = std::make_shared<Camera>(75.0f, 0.01f, 100.0f, "Camera");
cameraPivot->addChild(camera);
cameraAttachment->addChild(cameraPivot);
addChild(cameraAttachment);
  • attachment holds the world-space position and accumulates yaw rotations (rotation around the world Y axis).
  • pivot is a child of attachment and accumulates pitch rotations (rotation around the camera's local X axis). Pitch is clamped to [MAX_CAMERA_ANGLE_DOWN, MAX_CAMERA_ANGLE_UP] to prevent the camera from flipping upside-down.
  • camera is a leaf child of pivot. It carries no independent rotation; its updateGlobalTransform override automatically propagates the combined yaw+pitch to lysa::Camera::transform and to SceneTree::setCamera.

When cameraAttachment->setTransform(…) is called, the transform change propagates through the entire subtree. By the time the call returns, the renderer's camera is already updated. No explicit camera push is needed.

rotateCamera() implementation

void RotatingAssetScene::rotateCamera(const float x, const float y) {
cameraAttachment->setTransform(lysa::mul(
lysa::float4x4::rotation_y(y),
cameraAttachment->getTransform()));
cameraPivot->setTransform(lysa::mul(
lysa::float4x4::rotation_x(x),
cameraPivot->getTransform()));
const auto pivotRot = lysa::quaternion{lysa::float3x3{cameraPivot->getTransform()}};
const auto pivotRotX = static_cast<float>(lysa::euler_angles(pivotRot).x);
const auto clamped = std::clamp(pivotRotX, MAX_CAMERA_ANGLE_DOWN, MAX_CAMERA_ANGLE_UP);
cameraPivot->setTransform(lysa::mul(
lysa::float4x4::rotation_x(clamped - pivotRotX),
cameraPivot->getTransform()));
}

A Y-axis rotation is pre-multiplied onto the attachment's current transform (pre-multiply because mul(A, B) applies A first; putting the rotation first turns the camera in world space). The same technique applies pitch to the pivot, followed by a clamp step that corrects over-rotation.

Each call to setTransform triggers updateGlobalTransform cascading down to camera, which updates the renderer. The three calls in rotateCamera each trigger a cascade; this is harmless because the renderer reads the camera only at render time.

Camera movement

Movement is accumulated during the fixed-timestep onPhysicsProcess ticks and interpolated to the variable-rate onProcess frame:

// onPhysicsProcess: accumulate displacement and look direction
currentCameraState.displacement += direction * currentMovementSpeed;
// onProcess: apply interpolated translation to attachment
const auto displacement =
previousState.displacement * (1.0f - fAlpha) +
currentCameraState.displacement * fAlpha;
cameraAttachment->setTransform(lysa::mul(
lysa::float4x4::translation(displacement),
cameraAttachment->getTransform()));

Translating by pre-multiplying a pure-translation matrix onto the current transform moves the camera in world space (the translation rows of attachment->getTransform() are in world space).

Initial placement

cameraAttachment->setPosition(0.0f, 0.0f, 4.0f);

setPosition writes directly to localTransform[3] and calls updateGlobalTransform. Since the attachment has no parent at this point, localTransform = globalTransform, placing the camera 4 units in front of the world origin.

Next : Loading and displaying an asset