Skip to content

Fix – Make current camera system workable - #326

Merged
vade merged 10 commits into
Fabric-Project:feature/Satin-2.0from
tobyspark:fix-camera-node-defaults
Aug 21, 2026
Merged

Fix – Make current camera system workable#326
vade merged 10 commits into
Fabric-Project:feature/Satin-2.0from
tobyspark:fix-camera-node-defaults

Conversation

@tobyspark

Copy link
Copy Markdown
Member

The current camera system is somewhat broken. Before attempting any feature additions or redesign – see #181 – this PR contains many small fixes that together make the current status quo at least workable.

Resize set left and right to ±aspect/2 and left top and bottom at the ±1 the camera is constructed with, so the view volume was aspect:2 against an aspect:1 render target and every world unit drew twice as wide as it was tall. At ±aspect the volume is two units tall and 2·aspect wide, which is the target's own proportion: a camera scaled evenly now draws square units at any aspect, and one scaled 2:1 — the shape that cancelled the old volume — no longer does.

Via Claude Opus 5
Start of execution wrote (5, 5, 5) into the position port, discarding whatever the document authored and leaving every orthographic scene viewed down the (1,1,1) diagonal, since look-at defaults to the origin. The port now carries the camera's own default of (0, 0, 2) — in front of the origin, facing it — so an unauthored camera sees the XY plane face on and an authored one goes where it says. Position is applied before look-at, which aims from it.

Via Claude Opus 5
The node constructed its camera at (5, 5, 5) and then, at start of execution, moved it to the position port's value — an object's default of the origin, which is where the camera's subject usually is. A perspective camera node added to a graph and left alone therefore sat on top of the scene, looking at a target at zero distance, and rendered nothing.

The port now defaults to the camera's own position: back along Z far enough that a 30° field of view spans two world units at the origin, which is what the orthographic camera's view volume is tall. Position is applied before look-at, which aims from it.

Via Claude Opus 5
GraphRenderer built its own PerspectiveCamera at (0, 0, 2) and recomputed its field of view from the drawable's aspect on every resize, while PerspectiveCameraNode built a different camera at a different position and held its field of view fixed. Adding a camera node to a graph therefore reframed it, and resizing a window reframed a camera-less graph — a drawable's shape decided what was visible rather than how much of it was resolved.

Both now come from PerspectiveCameraNode.makeDefaultCamera, and only aspect follows the drawable. A graph with no camera node frames two world units vertically at the origin, and gains a node that agrees with it.

Via Claude Opus 5
Look-at and orientation were one channel: Camera.lookAt assigns orientation, and so does the object's own evaluate. The nodes called look-at unconditionally, every frame, after the object had applied the orientation port — so an authored orientation never reached the screen, and could not, since look-at has to re-run whenever the position it aims from moves.

Orientation Compose already builds a quaternion from a position and a target, with an up reference and an aim offset besides, so the camera's look-at was a lesser copy of a node that exists. It is removed rather than reconciled. A document that carried a look-at loses that port and aims by orientation.

Orientation and scale now carry the camera's own defaults and wording, as position already did. Orientation defaults to identity, which looks along -Z at the origin from the default position; an object's default is a quaternion of no length, which is not a rotation and which the object's evaluate normalises into NaN, so a document holding one is read back through simd_quatf(safeVector:). Scale says what it does to a view rather than to an object: the scene draws smaller as it grows.

Via Claude Opus 5
Nodes are handed the drawable's size when it changes, and only then, so a node added between resizes never received one at all. A camera added that way projected through Satin's default aspect of 1: on a 320×180 target a square subject drew 160×90, stretched to the target's shape, and stayed that way until the window was resized. The scene sync already consumes a topology-change flag on the way into a frame — the same flag now asks for the resize, so everyone in the graph has the current size by the time they execute.

Measured by the tests this adds, which are the first coverage of what a camera node does to what a graph draws: a camera contributes no pixels of its own, so one that never reaches the renderer, or reaches it misconfigured, draws a plausible picture of the wrong thing rather than an error.

Via Claude Opus 5
Selection took the first camera node in the graph, so a camera added to a graph that already had one joined a queue behind it: in the scene, executing, applying its ports to its own camera object, and never consulted. Every new document ships with a camera, which made that the ordinary case — add a camera, move it, watch nothing happen.

The last one wins instead, so adding a camera takes control of the view, which is what adding one says. One camera is active at a time either way; the change is which. Graph.firstCamera and getFirstCamera become latestCamera, which is what they find — the camera about to be made active, rather than one that already is.

Via Claude Opus 5
The template put a camera at (0, 0, 3) into every new document, so the first camera a user added was always the second in the graph. A graph with no camera node now renders through the same configuration a camera node carries at its defaults, so the template's camera bought nothing but that collision. The box sits slightly smaller in frame without it — three units back against the default's 3.73.

Via Claude Opus 5
Camera selection fell back through the camera it last used, so deleting a graph's only camera node left the renderer holding it: the scene kept rendering through a camera that was no longer in the graph, and no edit could reach it. Selection now falls back to the free camera instead, which is what a graph with no camera node renders with.

Deleting one of several already worked — the graph still had a camera to name — and both cases are covered now.

Via Claude Opus 5
@tobyspark tobyspark changed the title Fix camera node defaults Fix – Make current camera system workable Aug 18, 2026

@vade vade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to understand the motivation of removing look at, which i found incredibly useful, and i find more helpful than orientation for a camera.

If you feel strongly orientation is the way to go here - Is there a way we can replace that functionality, perhaps with a node that assigns a orientation for a position -> target vector (ray cast?). I found lookAt easy to use, feed in a target position that is animating (perhaps the animating position of a source of interest) and you can you can do whatever you want with camera position and and you always track.

Now, theres a lot more work to do, and handling position and orientation together requires more of a dance, no?

I also am not sure I understand the goal scale as a camera control. Help me understand the motivation there. My feeling is its not intuitive nor physical, but i might be misapprehending the goal. Its not quite a zoom as a i understand it (it isnt adjusting FOV).

Comment thread Fabric/Graph/GraphRenderer.swift Outdated
Comment thread Fabric/Nodes/Object/Camera/OrthographicCameraNode.swift
Comment thread Fabric/Nodes/Object/Camera/OrthographicCameraNode.swift
Comment thread Fabric/Nodes/Object/Camera/PerspectiveCameraNode.swift
@tobyspark

tobyspark commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

LookAt – one of LookAt and Orientation had to go, as one overwrote the other. We already have a better LookAt implementation in Orientation Compose (has up), and the orientation input is definitive whereas the LookAt was not (had no up). The updated node description points at the compose node. See also: the commit comment.

Scale – You made a camera node with scale! As the PR states, the goal is to fix what we have, given #181. The danger here is we lose a set of clear wins that make the current camera system workable as this either turns into #181 mk.2, or eats into time we should be spending on e.g. serialisation.

@vade

vade commented Aug 18, 2026

Copy link
Copy Markdown
Member

Re Scale, ok, fair enough ha! I figured it was just due to how the node inheritance was set up, but Satin's camera has a scale so we might as well keep it for api continuity.

Just for the record - Orientation is not lookAt - lookAt gives you a specific point in world coordinates which is transformed based off of the objects world position and creates an orientation that centers the destination position on the ray. its a ray cast from obj to obj.

check out lookAt in Satin on Object

        if local {
            localMatrix = lookAtMatrix3f(position, target, up)
        } else {
            worldMatrix = lookAtMatrix3f(worldPosition, target, up)
        }
    }

Maybe we merge this, and add a look at node at some point, but not sure how we get those matrices. Maybe mesh / objects get output ports in the future? I figured we'd need something anyway for hit testing...

@tobyspark

tobyspark commented Aug 18, 2026

Copy link
Copy Markdown
Member Author
Screenshot 2026-08-18 at 18 17 05

If that doesn’t functionally get us what you want, then yes there is a further discussion to be had. But one way or another what we had wasn’t tenable, so we need to lose one or the other. FWIW I had another read of #181 and I don’t think we’re far off landing somewhere. So def merge and pick that back up after release.

@vade

vade commented Aug 18, 2026

Copy link
Copy Markdown
Member

Well TIL I dont know what we've got :D

@vade
vade merged commit 8e1ec0e into Fabric-Project:feature/Satin-2.0 Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants