Camera¶
Namespace Nfty.Model.Render in assembly Nfty.Model.
An immutable turntable-orbit camera: Camera.ViewProjection() is the one function that owns the view and projection mapping together, and every other mapping (picking, zoom-anchoring) derives from it or its inverse rather than restating the matrices beside it.
Remarks¶
Immutable by the same rule Mesh states for itself: every value is fixed at construction, so a Camera may be read from many threads and a caller can never hold a half-updated one. Every apparent mutator (Camera.Orbit(), Camera.Pan(), Camera.Zoom(), Camera.ZoomAnchored(), Camera.Reset()) returns a new instance.
Constructors¶
Camera(Vector3, float, float, float, Vector3, Vector3)¶
public Camera(Vector3 target, float distance, float yaw, float pitch, Vector3 boundsMin, Vector3 boundsMax);
Builds a camera, clamping target into the box, and distance and pitch into range, so every instance that exists is already valid. The box sets the distance limits and clip planes.
Parameters
target(Vector3) - The point orbited and looked at; clamped into the box.distance(float) - Eye-to-target distance; clamped toCamera.MinDistance..Camera.MaxDistance.yaw(float) - Orbit angle around the vertical axis, radians.pitch(float) - Orbit angle above level, radians; clamped.boundsMin(Vector3) - Minimum corner of the subject's box.boundsMax(Vector3) - Maximum corner of the subject's box.
Fields¶
FovDegrees¶
The vertical field of view every projection this camera builds uses.
PitchLimitDegrees¶
Pitch never exceeds this many degrees from level, either direction — an extruded sprite has a definite up, and a camera rolled onto its side shows an orientation that means nothing.
Properties¶
BoundsMax¶
Maximum corner of the subject's bounding box.
BoundsMin¶
Minimum corner of the subject's bounding box. Camera.Target never leaves the box.
Distance¶
World-unit distance from Camera.Target to Camera.Eye, clamped to Camera.MinDistance..Camera.MaxDistance.
Eye¶
World position of the eye, computed fresh on every access — cheap trigonometry, not the lazy-cache pattern that would need a mutable field.
LightDirection¶
The direction TOWARD the light, in world space: a head light riding the camera, up and a little to the left of the eye, so whatever faces the viewer is brightest at any orbit angle. Always a unit vector.
MaxDistance¶
Farthest the eye may orbit from its target: 20 ×.
MinDistance¶
Closest the eye may orbit to its target for the CURRENT orientation: the distance from Camera.Target along the eye direction to where that ray exits the subject's box, plus 0.02 ×. With the target inside the box this keeps the eye outside it from every angle, yet lets a flat sprite viewed head-on be inspected from just in front of its face. Depends on the orientation, so orbiting at the floor pushes the eye out as the exit distance grows.
Pitch¶
Orbit angle above/below level, radians, clamped to ±Camera.PitchLimitDegrees.
Radius¶
Half the box diagonal, in world units (at least 1e-3). The far limit and every clip plane scale with it, so a 16px sprite and a 1000px one behave alike.
Target¶
The point the camera orbits and looks at.
Yaw¶
Orbit angle around the vertical axis, radians. Unclamped — free spin.
Methods¶
FramedOn(Vector3, Vector3, float)¶
A camera framing the given bounds: centered on the box, pulled back to fit its bounding sphere in the NARROWER of the vertical and horizontal fields of view (so a tall viewport, aspect below 1, does not crop sideways) with 15% margin, looking square-on at the front face (yaw 0, pitch 0) with the box center on the viewport center.
Parameters
min(Vector3) - Minimum corner of the subject box.max(Vector3) - Maximum corner of the subject box.aspect(float) - Viewport width over height; a non-positive or non-finite value counts as 1.
Returns. The framing camera.
Orbit(float, float)¶
A new camera orbited by the given deltas; yaw is free, pitch clamps.
Parameters
deltaYawRadians(float) - Change in yaw, in radians.deltaPitchRadians(float) - Change in pitch, in radians; the result is clamped.
Returns. The orbited camera; this one is unchanged.
Pan(Vector2, int)¶
A new camera panned so a screenDeltaPixels drag moves content under the pointer by the same number of pixels, at the current Camera.Distance and Camera.FovDegrees.
Parameters
screenDeltaPixels(Vector2) - The drag, in screen pixels (x right, y down).viewportHeight(int) - Height of the viewport in pixels, which fixes how much world one pixel covers.
Returns. The panned camera; this one is unchanged.
Reset(Vector3, Vector3, float)¶
Reframes on new bounds, discarding the current orientation — what "reset" means when the subject itself has bounds, as opposed to re-centering the existing view.
Parameters
min(Vector3) - Minimum corner of the subject box.max(Vector3) - Maximum corner of the subject box.aspect(float) - Viewport width over height.
Returns. The framing camera, as Camera.FramedOn() builds it.
ViewProjection(int, int)¶
The one function that owns view and projection together. The near and far planes scale with Camera.Radius.
Parameters
width(int) - Viewport width in pixels; fixes the aspect ratio.height(int) - Viewport height in pixels; fixes the aspect ratio.
Returns. View multiplied by projection.
WithSubject(Vector3, Vector3)¶
The same view (target, distance, orientation) over a new subject box: the radius, the distance limits and the target's confinement follow the box, the camera does not jump (the target is only moved if it would fall outside, the distance only if it would break the floor).
Parameters
min(Vector3) - Minimum corner of the new box.max(Vector3) - Maximum corner of the new box.
Returns. The camera over the new box.
Zoom(float)¶
A new camera at Camera.Distance scaled by factor, clamped. factor below 1 moves the eye closer.
Parameters
factor(float) - Multiplier for the eye distance.
Returns. The zoomed camera; this one is unchanged.
ZoomAnchored(float, Vector2, int, int)¶
A new camera zoomed by factor such that the world point under pointerScreen stays under it, to within a pixel. Two passes: zoom around Camera.Target (trivially exact for whatever Camera.Target itself projects to, since Camera.ViewProjection() always centers Camera.Target on screen), then a corrective pan that cancels the screen-space error the zoom introduced for the off-center anchor point. That correction does NOT go through the public Camera.Pan() — Camera.Pan() converts pixels to world units using Camera.Distance (the eye-to-Target depth), which is exact only for a point sitting exactly at Camera.Target's depth; the anchor is generally off that plane. Because a pan perpendicular to the forward axis never changes a fixed point's depth along that axis (the shift has zero component along forward by construction), the anchor's own forward-axis depth is constant through the correction, so converting the screen-space error to world units at THAT depth — instead of at Camera.Distance — makes the correction exact in one pass rather than an approximation that needs iterating. Exact while the corrected target stays inside the subject box; over empty space far beside the model the target clamp wins and the anchor drifts — orbiting about empty air is what the clamp exists to prevent.
Parameters
factor(float) - Multiplier for the eye distance.pointerScreen(Vector2) - The pointer position in screen pixels.width(int) - Viewport width in pixels.height(int) - Viewport height in pixels.
Returns. The zoomed camera; this one is unchanged.