Skip to content

Port: keep follow-camera turns moving at walls and shorten zoom recovery - #134

Merged
Adubbz merged 2 commits into
TheMoonPeople:masterfrom
carrdylan52:pr/camera-walls
Oct 23, 2026
Merged

Adubbz merged 2 commits into
TheMoonPeople:masterfrom
carrdylan52:pr/camera-walls

Conversation

@carrdylan52

Copy link
Copy Markdown
Contributor

Before

Turning at walls. Right-stick look and mouse look both go through direct yaw (MouseCameraYaw, port/src/camera_mouse.cpp). It proved the whole turn clear at the current distance, using one capsule around both the eased and the pending eye, and dropped any part it could not prove. Retail rotates the pending eye and corrects its distance at walls. The port's direct-input clearance check can instead reject the turn. Retail's wall correction leaves the pending eye a few units from a wall it slides along, so even the starting pose failed the check and every later turn was refused. In the first Underground Channel corridor, holding the right stick or moving the mouse toward the wall turns the view a few degrees and then nothing more.

Wheel zoom at walls (input.mouse_zoom, off by default):

  • Every game frame that a wall pushed the eye further in counted as another pull-in, so backing into one wall reached the longest hold at once. After walking away, the eye stayed at retail's follow distance for up to six seconds before it began to return.
  • Once clear, the eye waited 20 game frames and then returned 4% of the gap per frame, so coming most of the way back took about two and a half seconds.
  • Any pull-in, however small, took up the hold again, so foliage brushing a still view kept a far camera held.
  • An eased eye placed in front of a wall always cut presentation interpolation, so a zoomed-out turn along a wall jumped.

After

  • Right-stick and mouse yaw use retail's rotate-then-correct collision response, so the eye can shorten at a wall while the view keeps turning. The port keeps its immediate turn of the drawn eye. Mouse yaw now suppresses automatic avoidance during the turn, as stick yaw already did.
  • A zoomed camera that backed into one wall comes back out to the chosen distance as soon as the view behind it is clear: nine tenths of the way within about a second and a third. A wall met again soon after still holds the shorter distance (1.5 s, doubling up to 6 s), so walking through rocks does not pump the camera.
  • Zoomed turns along a clear wall-slide path can blend. Camera geometry crossing a blended view cuts interpolation; an incomplete geometry collection after a visibility placement also cuts conservatively.

Gameplay changes requiring maintainer approval before merge: wall response for the port's direct stick/mouse controls, and opt-in wheel-zoom hold/return timing. These do not restore exact retail camera feel; the port retains its immediate, distance-independent direct turns.

How

  1. Keep direct follow-camera turns moving at walls.
    • MouseCameraYaw rotates both eyes, then retail's CheckGround/CheckCollision corrects the pending eye against the bounded orbit collection that CCameraControl::MoveCamera already gathers.
    • It validates and repairs the actual post-step drawn view, including when retail collision uses an explicit check_ref. A view already obstructed before input does not exempt newly crossed geometry from visibility checks.
    • KeepInSight first tries moving only the drawn eye along its easing path and then toward its look-at point, retaining retail's pending pose where possible. If neither works, it finds and validates a nondegenerate fallback through the actual step. It checks the eased look-at sightline and, when required, the explicit collision-target sightline against each polygon that blocked neither starting sightline. The fallback places both eyes at least ten units from the stepped look-at point: on the stepped view, or on the nearest in heading of views turned from it in eighths of a half turn. Each view's search starts at 10.01 units to allow for float rounding at ordinary game coordinates; the stored, stepped view must still pass the ten-unit length and sightline checks. This fixed margin does not guarantee a usable anchor at arbitrary far-from-origin coordinates. This is a bounded search of sampled headings. If none has a usable starting point, the turn is dropped for that update and retail's camera stays as it was. Recovery can change the heading abruptly in tight wall-end geometry.
    • It cuts presentation blending when eye travel crosses any camera wall or either protected sightline sweeps across newly blocking camera geometry, including when a different wall already hid the starting view. This reuses CameraViewSweeps from camera_zoom.cpp.
    • The conservative pitch check stays.
    • The capsule proof of the arc (ClearArc) is removed.
    • cameracontrol.cpp sets CAMERA_ROT_CANCEL_AUTO_MOVE for a fresh mouse yaw, as it already did for stick yaw.
  2. Return the zoomed camera to its distance sooner after walls.
    • In CameraZoom (port/src/camera_zoom.*), Strike(drop) counts significant pull-ins once until outward recovery starts (pulled), and ignores drops under minor_pull_in (10 units).
    • A first pull-in does not hold.
    • clear_ticks goes from 20 to 15, release_rate from 0.04 to 0.1 and release_min from 1 to 2.
    • After placing an obstructed eased eye in front of a wall, KeepVisible checks the blended views against both camera and character collision. It cuts if the sweep finds an obstruction or cannot complete its geometry collection.
    • The docs give the timings in game frames (30 a second). They had been written as if per 60 Hz tick, which halved every time.

No files under ps2/ are touched. tools/input/dungeon-camera-wall-turn.input is the only new file.

Testing

  • Reproduced on master c27f3d0 (Windows Release, data v20, --jump dungeon:0:1 --fast-load --headless --offscreen, tools/input/dungeon-mouse-camera.input up to frame 2260, then a turn toward the corridor wall):
    • Right stick held right from frame 2400: on master, the captures at 2500, 2600 and 2700 show the same camera orientation, stuck a few degrees in. On the branch the view keeps turning past the wall.
    • Mouse, 40 counts every 4 updates from 2400: master stops by 2500; the branch keeps turning.
  • New replay: tools/input/dungeon-camera-wall-turn.input (right stick from 2400, then 2000 mouse counts from 2700; captures at 2600 and 2900).
    • On master the stick turn stops at the wall well short of a half turn, and the mouse adds only a little by 2900.
    • On the branch the stick turns the view roughly 180° by 2600, past the wall. The mouse then brings it round to roughly 20° past the start by 2900, with the player visible in the inspected captures. The angles are visual estimates.
  • Unit tests (head ca757e8): -R Camera passes 95/95 on the branch, and 91/91 on the first commit alone (1817c82).
    • Round-1 regressions: CameraMouse.TurnPastACornerWithALaggingTargetStaysInViewAfterTheStep (a wall ending short of a lagging drawn target), CameraMouse.TurnCarryingTheViewAcrossAThinPostCutsInterpolation and CameraMouse.StickAndMouseTurnsAcrossAThinPostCutInterpolationWithoutZoom (default zoom off, through MoveCamera for both the stick and the mouse).
    • Round-2 regressions: CameraMouse.APostAlreadyHidingTheViewExcusesNoOtherWall, CameraMouse.TurnKeepsTheViewInSightOfAnExplicitCollisionTarget and CameraMouse.StickAndMouseTurnsKeepAnExplicitCollisionTargetInSight.
      • The first checks a newly blocked endpoint and a newly crossed thin post while a different post already hides the starting view.
      • The second and third check two walls with an explicit check_ref. The view must be clear from both the look-at point and the collision target after the actual step.
    • Round-3 regressions:
      • CameraMouse.TurnNearAWallEndKeepsAUsableView (direct, with and without an explicit collision target) and CameraMouse.StickAndMouseTurnsNearAWallEndKeepAUsableView (both wrappers) cover the zero-direction wall-end case. They require the yaw to be accepted in the direct case. After the actual step they require a view longer than five units, a camera matrix whose 3x3 determinant has magnitude 1, and clear look-at and collision-target sightlines.
      • CameraMouse.TurnCarryingThePlayersSightlineAcrossAPostCutsInterpolation covers the off-centre explicit-target post, direct and through both wrappers, with zoom off. The endpoints stay clear, the turn is accepted and interpolation cuts.
      • All three fail on the previous head (433e653c6) in every variant.
      • A sustained wall-end regression also holds each input across repeated real steps, covering float rounding at the ten-unit fallback boundary so a clear heading does not leave the camera stuck. These are CameraMouse.TurnHeldPastALedWallEndKeepsTurning (direct yaw accepted on each of 300 updates) and CameraMouse.StickAndMouseTurnsHeldPastALedWallEndKeepTurning (both wrappers for 300 updates; never 30 updates without motion, over a full turn in all). Every update's view must be usable, with both sightlines clear. Both fail on 2dfc825f5, where the camera never moves.
    • Against master's camera_mouse.cpp, camera_mouse.hpp and cameracontrol.cpp, the round-1 regressions and five rewritten wall tests failed (8 of 21 CameraMouse tests). That run predates rounds 2 and 3.
  • Full ctest (Windows Debug tests build, -j2, head ca757e8): 1,867 passed, 54 skipped and none failed, plus one disabled test.
    • On the previous head (2dfc825f5) one run had 1,864 passed, 54 skipped and one failed, plus one disabled. The failure was GeoramaParts.PaintedFencesMatchRetailRecursion, which is intermittent even when run alone and also fails on the item-limit branch build, which has none of this camera code.
    • A second full run on that previous head had the same totals with MglibAspect.FloorMapPassagesAreSolidAt3840x2160 timing out instead; PaintedFences passed in that run, and the GPU case passed in the earlier run.
  • Replays: the wall-turn, stick and mouse replays on the ca757e8 Release build produce screenshots byte-identical to the previous head's.
  • clang-format clean; clang-tidy reports only existing warnings outside changed lines.

Right-stick and mouse look both turn the follow camera through direct
yaw. It proved the whole turn clear at the current distance, with one
capsule around both the eased and the pending eye, and dropped whatever
it could not prove. Retail rotates the pending eye and corrects its
distance at walls; the port's clearance check instead rejected the
turn. Retail's correction leaves the pending eye a few units from a
wall it slides along, so even the starting pose failed the check and
every later turn was refused. In the first Underground Channel corridor
a held stick or mouse turn toward the wall stops after a few degrees.

Direct yaw now uses retail's rotate-then-correct response: the pending
eye rotates about its look-at point, and retail's ground and wall
correction brings it in front of any wall on its view, so the turn goes
on and the next update moves the eye back out. The port keeps turning
the drawn eye by the same angle at once.

The view the tick's step then shows is checked against every wall
except those that already hid the drawn view, from the eased look-at
point and, when retail's correction uses an explicit collision target,
from that target too. Each placement is checked through the step
itself. Where moving the drawn eye suffices, retail's pending pose is
kept: the drawn eye moves on along its easing path toward a pending eye
its look-at point sees, then in toward its look-at point. Failing both,
both eyes are placed on a view from the stepped look-at point at least
ten units long, so the camera keeps a usable direction: the stepped
view, or the nearest in heading of views turned from it in eighths of a
half turn either way, as far out as a search finds in sight. Each
search starts just beyond ten units, so the view still measures ten once
stored as floats and stepped. If none of the sampled headings has a
usable starting point, the turn is dropped for that update. Presentation
blending is cut when the eye passes a camera wall, or when any of those
other walls crosses either protected sightline between the drawn pose
and the next, such as a thin post the view passes across, using the
view sweep that wheel zoom already checks, now shared as
CameraViewSweeps. Retail's obstacle avoidance now waits
during mouse turns, as it already did for stick turns; otherwise it
swung the eye back against a turn along a wall.

Direct pitch keeps its conservative check. The tests that expected a
refused or clipped turn now expect the eye in front of the wall, and new
ones cover a turn that starts beside a wall, a pending eye behind one, a
full turn along a wall without a cut, obstacle avoidance waiting, a
corner hidden from a lagging look-at point, a thin post crossed by
direct, stick and mouse turns with zoom off, a post already hiding the
view that excuses no other wall, an explicit collision target kept in
sight by direct, stick and mouse turns, a turn near a wall end that
must keep a usable view, a post crossing only the collision target's
sightline, and a turn held past a wall end with a led look-at point,
which must keep turning.
tools/input/dungeon-camera-wall-turn.input replays a stick and then a
mouse turn into the first corridor's wall.
With wheel zoom on, a zoomed-out camera that a wall pulled in was slow
to come back out, and turning it along a wall jumped:

- Every game frame the wall pushed the eye further in struck another
  pull-in, so backing into one wall reached the longest hold at once.
  Walking away left the eye at retail's follow distance for up to six
  seconds, closer than retail's own camera, before it began to return.
- Once the view behind it cleared, it waited 20 game frames and then
  returned 4% of the remaining gap per frame, about two and a half
  seconds to come most of the way back to the far limit.
- Every pull-in, however small, reset the quiet period and took up the
  hold again, so foliage brushing a still view kept a far camera held.
- An eased eye placed in front of a wall always cut presentation
  interpolation, so a zoomed-out turn along a wall jumped repeatedly.

Pulling in further before the eye begins moving out now counts as the
same pull-in, and a first pull-in does not hold: once past the wall the eye
returns as soon as the view farther stays clear, as retail's does. A
wall met again while recent pull-ins are remembered still holds, from
a second and a half doubling up to six seconds, so walking through
rocks keeps the shorter distance. The return waits 15 game frames and
takes 10% of the gap per frame, at least two units, so the eye is nine
tenths of the way back within about a second and a third; a shorter
wait let the orbiting pillar ring pump the distance. A pull-in of less
than ten units still shortens the eye but neither counts nor holds.
The placed eye is cut to only where a display frame blending to it
would show a wall, the characters' walls included; otherwise frames
blend to it as to any pull-in.

The camera updates once per game frame, 30 a second, so docs/PC.md now
gives the zoom timings in game frames; it described them as 60 Hz ticks,
which halved every time. Tests cover backing into a wall and away,
repeated pull-ins holding longer, small pull-ins, and the return time.
@Adubbz
Adubbz merged commit e1d41d7 into TheMoonPeople:master Oct 23, 2026
3 of 7 checks passed
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