SpringController: rework smooth mesh camera height tracking (mode 2)#3012
SpringController: rework smooth mesh camera height tracking (mode 2)#3012PtaQQ wants to merge 3 commits into
Conversation
| LOG_L(L_ERROR, "Smooth mesh disabled"); | ||
| trackMapHeight = HeightTracking::Terrain; | ||
| } | ||
| // fallback is at point of use, modInfo is not loaded yet when the ctor runs this |
There was a problem hiding this comment.
As far as I can tell now it's checked continuously which is bad, do it once as soon as modinfo loads. This sounds like something that could be a standalone fix commit too.
There was a problem hiding this comment.
Fixed in a separate commit. Guard moved out of ConfigUpdate (ran in ctor before modInfo loaded, and rewrote the user's config every change) to point-of-use, warns once, leaves config untouched
| if (meshBlend <= 0.0f) | ||
| return CGround::GetHeightReal(x, z, false); | ||
|
|
||
| if (meshBlend >= 1.0f) | ||
| return smoothGround.GetHeightSmooth(x, z); |
There was a problem hiding this comment.
mix should take care of these
There was a problem hiding this comment.
Right, GetFocusSurfaceHeight collapsed to one no-mesh guard + a single mix()
| static constexpr float CatmullRom(float p0, float p1, float p2, float p3, float t) | ||
| { | ||
| return p1 + 0.5f * t * ((p2 - p0) + t * ((2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) + t * (3.0f * (p1 - p2) + p3 - p0))); | ||
| } | ||
|
|
||
| // C1-continuous, unlike Interpolate whose gradient jumps at cell borders | ||
| static float InterpolateBicubic(float x, float y, const int maxx, const int maxy, const float res, const float* heightmap) | ||
| { | ||
| RECOIL_DETAILED_TRACY_ZONE; | ||
| x = std::clamp(x / res, 0.0f, (float)maxx); | ||
| y = std::clamp(y / res, 0.0f, (float)maxy); |
There was a problem hiding this comment.
these sound like they belong as generic math functions (that you would then pass heights as args into)
There was a problem hiding this comment.
CatmullRom + a generic InterpolateBicubic(p[4][4], dx, dy) moved to SpringMath.h; SmoothHeightMesh just gathers the 4×4 patch and calls it
| return groundDist; // ground above the surface | ||
|
|
||
| // march to bracket the first crossing (grazing rays can cross several times), then bisect | ||
| constexpr int NUM_STEPS = 16; |
There was a problem hiding this comment.
The march brackets the first crossing of the view ray with the blended surface.
Grazing rays near cliffs can dip below the surface and back out several times, so too coarse a march would step over a near crossing and bracket a farther one, locking the camera onto the wrong hill.
It runs once per zoom action, not per frame, so 16 is cheap. Added a comment to that effect; happy to drop to 8 if you'd rather.
| const float* row = &heightmap[std::clamp(sy + i - 1, 0, maxy - 1) * maxx]; | ||
| rows[i] = CatmullRom(row[x0], row[sx], row[x2], row[x3], dx); |
There was a problem hiding this comment.
why does this edit the mesh?
There was a problem hiding this comment.
It doesn't, InterpolateBicubic only reads from mesh. The confusion was the local rows[] scratch array; renamed to patch and commented it as a read-only gather. The camera samples the mesh on the main thread while the sim updates it, but this path never writes.
Pure rename in preparation for the smooth mesh height tracking rework. The camera focus height and the zoom-distance-to-focus computation are about to stop being plain raw-ground lookups, so route them through GetFocusSurfaceHeight() and DistanceToFocusSurface() wrappers. Behavior is unchanged: both currently forward to the raw ground. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The guard lived in ConfigUpdate(), which runs from the constructor before modInfo is loaded, so modInfo.enableSmoothMesh was always still at its default and the check never fired in practice. It also rewrote the user's CamSpringTrackMapHeightMode config value and logged an error on every config change. Resolve the fallback at point of use instead, where modInfo is loaded: fall back to terrain tracking when the game disabled the smooth mesh, warn once, and leave the user's config untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CamSpringTrackMapHeightMode=2 now bases the camera focus height on the air
smooth mesh directly instead of nudging the zoom distance by per-frame
smooth mesh deltas. The old implementation only applied while scrolling
(zoom, minimap and Lua camera moves bypassed it), under-compensated by the
view tilt factor, and accumulated drift since the offset lived in the zoom
distance. The new implementation is stateless.
- focus height = mix(ground, smooth mesh, smoothstep(min, max, dist)), fading
to the raw terrain at close zoom so minimum zoom depth and panning speed
stay identical to mode 1 near the ground
- the mesh is sampled with a Catmull-Rom bicubic patch (new
SmoothHeightMesh::GetHeightSmooth), as the gradient creases of bilinear
sampling show up as small camera jolts; CatmullRom and the generic bicubic
patch helper live in SpringMath
- ZoomIn/ZoomOut derive the camera distance against the same blended surface
(march + bisection along the view ray), otherwise each zoom step lifts the
camera by the mesh-to-ground gap and the view drifts forward
- blend window configurable via CamSpringSmoothMeshBlend{Min,Max}Dist
- mode 0 (Disabled) keeps the old raycast re-base as FreezeCamHeight, minus
its now-dead smooth delta branch; mode 1 (Terrain) is unchanged
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
03d0500 to
3acf092
Compare
Reworks spring camera height tracking mode 2 (
CamSpringTrackMapHeightMode=2) to base the camera focus height on the air smooth mesh directly, so the camera no longer bobs over every terrain bump while scrolling at fixed zoom. Related: #854Why rework rather than fix in place
The old mode 2 nudged the zoom distance by per-frame smooth mesh deltas. It only applied while scrolling (zoom, minimap clicks and Lua camera moves bypassed it), under-compensated by the view tilt factor, and accumulated drift because the offset lived in the zoom distance. The new implementation is stateless: the focus height itself comes from the mesh, which covers all camera paths uniformly and cannot drift.
How it works
mix(ground, smoothmesh, smoothstep(blendMin, blendMax, zoomDist))— fades to the raw terrain at close zoom so minimum zoom depth and panning speed near the ground are identical to mode 1 (near the ground the camera clamp forces terrain following anyway)SmoothHeightMesh::GetHeightSmooth, camera-only — aircraft still use bilinearGetHeight), since bilinear gradient creases at cell borders show up as small camera joltsZoomIn/ZoomOutderive the camera distance against the same blended surface (coarse march to the first crossing + bisection along the view ray); deriving against the raw ground would lift the camera by the mesh-to-ground gap on every wheel tick and drift the view forwardCamSpringSmoothMeshBlendMinDist(150) andCamSpringSmoothMeshBlendMaxDist(600) control the blend windowenableSmoothMesh=false), mode 2 falls back to terrain tracking at point of use; the previousConfigUpdateguard ran before modInfo was loaded so it never fired, and the mesh is zero-filled when disabled, which would pin the focus at sea level. The user's config value is no longer rewrittenFreezeCamHeight, formerlySmoothCamHeightminus the now-dead delta branch); mode 1 (Terrain, default) is unchangedBehavior change note: anyone using mode 2 today gets the new behavior. Saved camera states round-trip fine (height is re-derived every frame), but
GetCameraState().pyreports the smoothed focus height in mode 2.Manual test procedure (BAR, Windows, maps with rough terrain and tall cliffs)
/set CamSpringTrackMapHeightMode, runtime switch works/airmeshoverlay matches the camera's vertical path at high zoomSpring.SetCameraState: consistent altitude (these bypassed the old mode 2)AI disclosure:
Investigated and implemented with Claude Code under my direction; all design decisions and in-game testing are my own.