Tactical Camera View
How W3DView turns abstract view state into a real RTS camera, world picking, and scripted fly-through movement.
W3DView is the concrete tactical camera for the game. The base View class in Core/GameEngine/Source/GameClient/View.cpp owns generic state like position, zoom, pitch, angle, and scroll helpers, but it does not know anything about a 3D camera, terrain picking, or scene rendering. Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp is where those abstract values become an actual world-space camera.
The heart of it is buildCameraTransform. That function starts from the logical look-at position, camera offset, zoom, pitch, and yaw, then converts them into source and target positions for a W3D camera transform. Along the way it applies camera shake, clamps the camera inside precomputed area constraints, handles the optional "real zoom" mode by shrinking FOV instead of only moving the camera, and adjusts pitch differently when the camera is close to the ground.
It also exposes one of the more unusual hooks in the renderer: camera slaving. If a script has attached the camera to an object and bone name, buildCameraTransform finds the named unit, walks its draw modules until one exposes an ObjectDrawInterface, asks for the render-object bone transform, and then replaces the camera transform with that bone transform. That same branch updates the audio listener position, which means camera slaving affects sound as well as visuals.
setCameraTransform is the second half of the job. It decides the far clip plane, rebuilds camera constraints when necessary, clamps the view so the player cannot see beyond the map edges, applies the finished transform to the 3D camera, then notifies the terrain and radar that the view changed. The map-edge constraint calculation is not trivial. calcCameraAreaConstraints casts rays from the center and lower part of the screen into the world and computes how much buffer is needed so the final rendered view never sees outside the map rectangle.
This camera layer also owns most world-to-screen conversion. getPickRay converts an absolute screen pixel into a world-space ray using the W3D camera's unproject logic. worldToScreenTriReturn projects a 3D point back into absolute display coordinates. screenToTerrain casts against the terrain render object, then optionally replaces the hit with a bridge hit if the bridge is above the terrain intersection. It also caches recent requests so repeated cursor queries do not keep re-casting the same pixel.
Unit picking uses the same geometry path with extra UI rules on top. pickDrawable first asks the window manager whether an opaque UI window is under the cursor and blocks selection if so:
Drawable *W3DView::pickDrawable(const ICoord2D *screen, Bool forceAttack, PickType pickType) { GameWindow *window = TheWindowManager->getWindowUnderCursor(screen->x, screen->y); while (window) { if (!BitIsSet(window->winGetStatus(), WIN_STATUS_SEE_THRU)) return nullptr; // opaque UI blocks selection window = window->winGetParent(); } return pickDrawableIgnoreUI(screen, forceAttack, pickType); }
Only after the opacity check passes does it fall through to pickDrawableIgnoreUI, which casts the ray into the 3D scene and resolves the hit render object back into a Drawable through the render object's user-data pointer. The WIN_STATUS_SEE_THRU flag is what lets informational overlays (like minimap labels) coexist with world picking.
Scripted camera motion is layered on top of the same view state. moveCameraTo creates a tiny waypoint path consisting of the current position and destination. moveCameraAlongWaypointPath builds a full path from linked waypoints. setupWaypointPath computes segment lengths, smoothed camera angles, interpolated ground levels, and per-segment time multipliers. moveAlongWaypointPath then advances along that path with easing, shuttered updates, angle smoothing across wraparound, and quadratic midpoint interpolation so the path does not look piecewise linear.
One small but important detail appears in updateCameraMovements: waypoint-path movement advances using TheFramePacer->getLogicTimeStepMilliseconds(FramePacer::IgnoreFrozenTime). That means scripted camera paths keep moving when game time is frozen for presentation, but still stop when the game is fully halted. The camera system is intentionally distinguishing cinematic time from simulation halt.
The file also documents why view updates were moved earlier in the frame. updateView exists so reflections can render using final camera and object positions before the main draw. Without that separation, reflection textures were always one frame behind the primary view.
Quirks
- Camera constraints are derived from ray casts, not just map extents. The view clamps based on what the current projection can actually see.
calcCameraAreaConstraintscasts rays from the center and the lower part of the screen; a FOV change orUseRealZoomtoggle changes which constraints apply, not the shape of the map. - Picking is UI-aware before it is scene-aware. An opaque game window blocks selection even if a unit is directly underneath the cursor. A new UI that appears to "steal" unit selection is almost always missing
WIN_STATUS_SEE_THRUon the intended-transparent areas. - Bridge picking overrides terrain picking when the bridge hit is higher.
screenToTerrainfirst returns the terrain intersection, then checks if a bridge is also hit at a higher Z — if so, the bridge wins. Commanding a unit onto a bridge that sits above terrain works because of this override; without it, click-to-move would drop units onto the terrain underneath the bridge. - Scripted camera movement ignores frozen time but not halted-game state.
TheFramePacer->getLogicTimeStepMilliseconds(FramePacer::IgnoreFrozenTime)is the call that produces this behavior. Cutscenes freeze simulation time so units don't move during camera pans, but the camera itself must keep moving — which is exactly the flag combination this provides. - Camera slaving to an object bone walks draw modules.
buildCameraTransformfinds the named unit, iterates draw modules until one exposes anObjectDrawInterface, asks for the render-object bone transform, then replaces the camera transform with it. Binding the audio listener position on the same branch means in-car/in-tank sounds work without a second lookup. - View update moved earlier in the frame for reflections.
updateViewruns before the main draw so reflection textures see final camera and object positions instead of a one-frame-old snapshot. Moving it back to post-draw produces subtly wrong reflections during scripted camera moves. - Single-player campaign clamps max camera height.
MaxCameraHeightis forced to the original ZH value (310) for campaign missions to preserve cinematic framing; skirmish keeps the bumped 700. See campaign_camera_fidelity.md. screenToTerraincaches repeated queries. The cache keys on absolute screen coordinates so repeated cursor-hover queries don't re-cast. A cache miss on every frame (e.g., during a drag-select) falls through to the real raycast cost.