Brodgar client

The camera: reading one, and the registry that names them

The one MapView.Camera a view is looking through: how to read its axes outside a render pass, how a camera is named, built, remembered and swapped, and the two traps in its frustum. The scene it looks at is world-3d.md.

Reading the camera outside a render pass

What Where
The camera, outside a render pass MapView.camera → MapView.Camera’s protected view (haven.render.Camera extends Transform) + proj; camera.view.fin(Matrix4f.id) is render→eye, so invert() (null when singular) gives the camera’s axes as its first three columns (m[col*4+row]) and the eye point as the fourth. Fork: MapView.camview() (// addon:) — Placer.getr has no Pipe, so Homo3D.obj2view is not available there

Gotcha — the camera’s frustum is two traps, and neither is a draw-distance setting. Camera.resized builds Projection.frustum(-field, field, …, 1, 2000): the far plane is fixed at 2000, so a camera pulled back past that clips the scene away entirely, ground included, and no rendering option reaches it — a camera meant to pull further sets its own projection per tick. The second is subtler: makefrustum’s scale term is 2*near/(right-left), so field is a size given at the near plane, not an angle, and the field of view is field/near. Move the near plane (to buy depth precision at distance) with field held at the shipped 0.5f and the view narrows by exactly the factor the distance widened it — the camera moves and the image does not, which reads as a zoom that is stuck.

The camera registry — how one is named, built and remembered

MapView.camera is a plain public field holding one MapView.Camera, and every camera is a non-static inner class of MapView — which is why building one takes the enclosing instance (mv.new SOrthoCam()) and why the registry reflects rather than calls a factory.

What Where
The registry MapView.camtypes, private static, name → Class<? extends Camera>. Each camera class is followed by its own static {camtypes.put(…)} block, so a camera class nobody registered is unreachable: OrthoCam is real and has no name, SOrthoCam is ortho. Fork: it is a LinkedHashMap, so its key order is the order those blocks appear in the file — Java runs static initialisers in source order, which is what makes that order something a caller may rely on
The names the client has follow (FollowCam), worse (SimpleCam), bad (FreeCam), default (DefaultCam, fork), ortho (SOrthoCam), rts (RTSCam) — in that order
The default camera Fork: MapView.DefaultCam, an orbit on FreeCam’s gestures, standing alone rather than extending it so that RTSCam keeps FreeCam as it is. Its options are the MapView.dcam* statics (prefs-and-options.md), read every tick: a proportional wheel, a vertical field of view (the horizontal derived from the aspect, where Camera.resized holds the horizontal), a projection rebuilt every tick for that reason — and, past the frustum’s first trap below, one whose planes follow the eye’s distance d: the far plane is max(2000, 2.5 d + 1500), and past d + MapView.recallreach() while the view distance is on (terrain-raster.md), the near plane max(1, d / 200) with the field scaled by it, so up close it is upstream’s 1..2000 and first person keeps its one unit; Projection.makefrustum’s 2·far·near is d²/40 in float, so past sqrt(Float.MAX_VALUE) the view goes non-finite and an eased NaN never eases back, which is why the wheel refuses a target past it and a stored distance past it heals to the default —, collision, first person, a tilt below the horizon independent of the collision (off, the eye goes under the terrain), and the wheel’s ease. The ground’s limit is a hard ceiling on the eye’s distance, since the ground only ever moves continuously; the objects’, which cross the line in one frame, is eased in at the wheel’s own pace (dcamzsmooth), and let out slower still, only after the way has stayed open a moment. Collision pulls the eye in along its own line to CLEAR short of whatever is first: the ground, sampled through MCache.getcz and bisected (ground the cache does not hold is not in the way, and is asked of MCache.groundheld first: getcz on an absent grid queues a map request before it throws), and the objects, whose obst rings bar build are stood up as prisms from Gob.getc to the top of the resource’s meshes (FastMesh.pbounds). First person is Camera.firstperson(); MapView.fpsync withholds the player’s Drawable through Gob.camvisible, a flag beside the addon layer’s addoninvis on the same surgery, so neither undoes the other
Reading the names from outside Fork: MapView.camnames(), public static, a fresh List of the registry’s keys in that order. The registry itself is private, so this is the only way anything else learns which cameras exist, and both the selector below and setcam’s own refusal read it rather than keeping a second list
Building one MapView.makecam(Class, String...) reflects for a (MapView, String[]) constructor, then for (MapView); with neither it throws naming the class. The String[] is the console’s trailing words, and a camera that wants none simply declares the shorter constructor
The two preferences defcam is the name, Utils.getpref/setpref; camargs is the argument array, serialized whole through Utils.getprefb/setprefb + Utils.serialize/deserialize. They are written together, by the one writer below
Restoring at construction MapView.restorecam(), called in the camera field initialiser — so it runs before the widget is attached and can reach nothing but the prefs
Installing one — the one writer Fork: MapView.setcam(String name, String... args), public — it resolves the name, makecams it into camera and writes both prefs, and throws an unchecked IllegalArgumentException on a name the registry does not have, naming what it got and every name it does. Everything that chooses a camera goes through it, so a camera chosen one way reads back the same through the others
Which one is INSTALLED Fork: MapView.camname() reverse-looks the live camera’s class up in the registry, null for a camera nobody registered. It is not the defcam pref: setcam installs on the view it is called on, and with several sessions (multi-session.md) the other views are still on whatever they were built with, so the pref answers what a session will come up on and this answers what is on screen now
One camera, several sessions Fork: a camera is an inner class of the view it draws, so sessions cannot share the object — they share its state. MapView.adoptcam(Camera, Coord2d) takes the camera the session losing the screen was being played with: it rebuilds the type when the two views differ (bare, with none of :cam’s arguments) and then Camera.restate copies the settings over. restate walks getDeclaredFields down the subclass chain and stops at Camera — view and proj are this view’s own render state, derived from a size the other view need not share, and resized() rebuilds them. A field holding a place (Coord, Coord2d, Coord3f) is nulled rather than copied: coordinates are per-session and every such field in the shipped cameras is a cache the next tick refills. The exception is RTSCam.center, a pan the player set, which that class’s own restate puts back translated by the session offset it is given
Choosing one by hand the cam command in MapView’s cmdmap (Console.Directory) — :cam <name> [args…], whose whole body is setcam. findcmds exposes the map, so the command lives on the map view rather than on the client, and ConsoleHost.done catches what setcam throws and prints it
Choosing one from Options Fork: OptWnd.CameraPanel.CamSelector, an SDropBox<String, Widget> whose items are camnames() — the registry’s keys carry no display label of their own, so a camera has one spelling in the dropdown, at the console and in the refusals
⚠️ A camera that cannot answer blacks the WHOLE view out MapView.tick runs camera.tick(dt) in a try and keeps whatever it throws in camload; draw re-throws that before anything else, and its own catch(Loading) fills the widget with black and centres the message — no scene, no gobs, no ground. So the point a camera decides to look at must not propagate Loading: right for one bolted to the player, who cannot be where they have never been, and ruinous for one that can be aimed anywhere. Loading.boostprio (5 on the way in, 6 in draw) is the only thing that keeps the wait short
The camera’s own input Camera.keydown, click, drag, release, wheel — all no-ops on the base class. MapView.keydown runs the fork’s dispatch before camera.keydown, so a camera never sees a key the client has already claimed
The middle button is the camera’s alone MapView.mousedown tests ev.b == 2 first and sends it straight to camera.click, with no modifier branch and no fallthrough — the plob placement, the map grab, the RTS layer and Click all sit in the else chain below it. mouseup mirrors it against camdrag, and mousemove gives the drag to camera.drag whenever camdrag is held. So a camera may claim any modifier it likes on a middle drag without colliding with anything else on the widget

Gotcha — an unknown defcam is silent, not an error. restorecam returns a bare camera both when camtypes.get misses and when makecam throws anything at all — upstream’s SOrthoCam, the fork’s DefaultCam, which is therefore also what a new player, with no defcam at all, comes up on. A preference naming a camera the client does not have comes up on default with nothing said, and the pref keeps its dead value until something writes it — so a camera that loses its name reads as “my setting was ignored”, never as a failure anyone can see.

See also