Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 110 additions & 21 deletions rts/Game/Camera/SpringController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ CONFIG(bool, CamSpringEdgeRotate).defaultValue(false).description("Rotate camer
CONFIG(float, CamSpringFastScaleMouseMove).defaultValue(3.0f / 10.0f).description("Scaling for CameraMoveFastMult in spring camera mode while moving mouse.");
CONFIG(float, CamSpringFastScaleMousewheelMove).defaultValue(2.0f / 10.0f).description("Scaling for CameraMoveFastMult in spring camera mode while scrolling with mouse.");
CONFIG(int, CamSpringTrackMapHeightMode).defaultValue(HeightTracking::Terrain).description("Camera height is influenced by terrain height. 0=Static 1=Terrain 2=Smoothmesh");
CONFIG(float, CamSpringSmoothMeshBlendMinDist).defaultValue(150.0f).description("Zoom distance below which smoothmesh height tracking mode (2) follows the raw terrain.");
CONFIG(float, CamSpringSmoothMeshBlendMaxDist).defaultValue(600.0f).description("Zoom distance above which smoothmesh height tracking mode (2) fully follows the smooth mesh.");


CSpringController::CSpringController()
Expand All @@ -51,7 +53,7 @@ CSpringController::CSpringController()
{
RECOIL_DETAILED_TRACY_ZONE;
enabled = configHandler->GetBool("CamSpringEnabled");
configHandler->NotifyOnChange(this, {"CamSpringScrollSpeed", "CamSpringFOV", "CamSpringMinZoomDistance", "CamSpringZoomInToMousePos", "CamSpringZoomOutFromMousePos", "CamSpringFastScaleMousewheelMove", "CamSpringFastScaleMouseMove", "CamSpringEdgeRotate", "CamSpringLockCardinalDirections", "CamSpringTrackMapHeightMode"});
configHandler->NotifyOnChange(this, {"CamSpringScrollSpeed", "CamSpringFOV", "CamSpringMinZoomDistance", "CamSpringZoomInToMousePos", "CamSpringZoomOutFromMousePos", "CamSpringFastScaleMousewheelMove", "CamSpringFastScaleMouseMove", "CamSpringEdgeRotate", "CamSpringLockCardinalDirections", "CamSpringTrackMapHeightMode", "CamSpringSmoothMeshBlendMinDist", "CamSpringSmoothMeshBlendMaxDist"});
ConfigUpdate();
}

Expand All @@ -75,11 +77,12 @@ void CSpringController::ConfigUpdate()
doRotate = configHandler->GetBool("CamSpringEdgeRotate");
lockCardinalDirections = configHandler->GetBool("CamSpringLockCardinalDirections");
trackMapHeight = configHandler->GetInt("CamSpringTrackMapHeightMode");
meshBlendMinDist = configHandler->GetFloat("CamSpringSmoothMeshBlendMinDist");
meshBlendMaxDist = std::max(configHandler->GetFloat("CamSpringSmoothMeshBlendMaxDist"), meshBlendMinDist + 1.0f);

if (trackMapHeight == HeightTracking::Smooth && !modInfo.enableSmoothMesh) {
LOG_L(L_ERROR, "Smooth mesh disabled");
trackMapHeight = HeightTracking::Terrain;
}
// the smooth-mesh-disabled fallback is resolved in UseSmoothMesh(): modInfo
// is not loaded yet when the ctor runs ConfigUpdate, and we don't rewrite the
// user's config value
}

void CSpringController::ConfigNotify(const std::string & key, const std::string & value)
Expand All @@ -88,7 +91,7 @@ void CSpringController::ConfigNotify(const std::string & key, const std::string
ConfigUpdate();
}

void CSpringController::SmoothCamHeight(const float3& prevPos) {
void CSpringController::FreezeCamHeight() {
RECOIL_DETAILED_TRACY_ZONE;
if (!pos.IsInBounds()) {
return;
Expand All @@ -103,12 +106,8 @@ void CSpringController::SmoothCamHeight(const float3& prevPos) {
// when there's a hill blocking the view
const float3 newGroundPos = camPos + dir * distToGround;
if (distToGround > 0.0f && newGroundPos.IsInBounds()) {
const float camHeightDiff = (trackMapHeight == HeightTracking::Smooth) ?
smoothGround.GetHeight(pos.x, pos.z) - smoothGround.GetHeight(prevPos.x, prevPos.z) :
0.0f;

pos = newGroundPos;
curDist = distToGround + (dir * camHeightDiff).Length() * Sign(camHeightDiff);
curDist = distToGround;
}
}

Expand All @@ -126,8 +125,6 @@ void CSpringController::KeyMove(float3 move)
return;
}

const float3 prevPos = pos;

move *= 200.0f;
const float3 flatForward = (dir * XZVector).ANormalize();
pos += (camera->GetRight() * move.x + flatForward * move.y) * pixelSize * 2.0f * scrollSpeed;
Expand All @@ -138,13 +135,14 @@ void CSpringController::KeyMove(float3 move)
// - 'pos' point of focus on the ground
// - 'curDist' camera distance
break;
case HeightTracking::Smooth:
// focus height handled in Update()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not here?

break;
case HeightTracking::Disabled:
// freezing camera height requires raycasting from current
// camera position and recalculating
// point of focus and distance
[[fallthrough]];
case HeightTracking::Smooth:
SmoothCamHeight(prevPos);
FreezeCamHeight();
break;
}

Expand Down Expand Up @@ -253,8 +251,8 @@ float CSpringController::ZoomIn(const float3& curCamPos, const float3& newDir, c
const float zoomAmount = std::min(1.0f - scaledMode, (curDistPre - minDist) / curDistPre);
const float3 wantedPos = curCamPos + cursorVec * zoomAmount;

// figure out how far we will end up from the ground at new wanted point
curDist = DistanceToGround(wantedPos, dir, pos.y);
// figure out how far we will end up from the focus surface at new wanted point
curDist = DistanceToFocusSurface(wantedPos);
Comment thread
sprunk marked this conversation as resolved.
pos = wantedPos + dir * curDist;

return 0.25f;
Expand Down Expand Up @@ -293,7 +291,7 @@ float CSpringController::ZoomOut(const float3& curCamPos, const float3& newDir,

auto extrapolate_position = [&] (float scale) {
const float3 wantedCamPos = curCamPos + cursorVec * (1.0f - scaledMode) * scale;
const float newDist = DistanceToGround(wantedCamPos, dir, pos.y);
const float newDist = DistanceToFocusSurface(wantedCamPos);
return std::pair{wantedCamPos, newDist};
};

Expand All @@ -318,19 +316,110 @@ float CSpringController::ZoomOut(const float3& curCamPos, const float3& newDir,



bool CSpringController::UseSmoothMesh() const
{
if (trackMapHeight != HeightTracking::Smooth)
return false;

// modInfo is loaded by the time the camera is actually used, so warn here
// once instead of in ConfigUpdate (which runs in the ctor before it loads)
if (!modInfo.enableSmoothMesh) {
if (!warnedSmoothMeshDisabled) {
LOG_L(L_WARNING, "[CSpringController] smoothmesh height tracking (mode 2) requested but the game disabled the smooth mesh, falling back to terrain tracking");
warnedSmoothMeshDisabled = true;
}

return false;
}

// the mesh is empty outside games
return smoothGround.HasMesh();
}

float CSpringController::GetFocusSurfaceHeight(float x, float z, float dist) const
{
RECOIL_DETAILED_TRACY_ZONE;
const float groundHeight = CGround::GetHeightReal(x, z, false);

// guard the empty-mesh case here, GetHeightSmooth asserts on it
if (!UseSmoothMesh())
return groundHeight;

// fade to the raw ground at close zoom, the mesh hovers high near cliffs
// and would otherwise limit zoom-in depth and panning speed there
const float meshBlend = smoothstep(meshBlendMinDist, meshBlendMaxDist, dist);
return mix(groundHeight, smoothGround.GetHeightSmooth(x, z), meshBlend);
}

float CSpringController::DistanceToFocusSurface(const float3& from) const
{
RECOIL_DETAILED_TRACY_ZONE;
const float groundDist = DistanceToGround(from, dir, pos.y);

if (!UseSmoothMesh() || groundDist <= 0.0f)
return groundDist;

// intersect the view ray with the focus surface; using the ground distance
// would lift the camera by the mesh-ground gap on every zoom step
const auto heightAboveSurface = [&](float t) {
const float3 p = from + dir * t;
return p.y - GetFocusSurfaceHeight(p.x, p.z, t);
};

if (heightAboveSurface(0.0f) <= 0.0f)
return groundDist; // camera below the surface

if (heightAboveSurface(groundDist) >= 0.0f)
return groundDist; // ground above the surface
Comment thread
sprunk marked this conversation as resolved.

// March to bracket the first crossing, then bisect. The march has to be fine
// enough not to step over a near crossing on grazing rays (which can dip below
// the surface and back out several times), otherwise we would bracket a farther
// crossing and lock the camera onto the wrong hill. This runs once per zoom
// action, not per frame, so a generous step count is cheap.
constexpr int NUM_MARCH_STEPS = 16;
constexpr int NUM_BISECTION_STEPS = 16;
const float step = groundDist / NUM_MARCH_STEPS;

float above = 0.0f;
float below = groundDist;

for (int i = 1; i < NUM_MARCH_STEPS; ++i) {
const float t = step * i;

if (heightAboveSurface(t) > 0.0f) {
above = t;
} else {
below = t;
break;
}
}

for (int i = 0; i < NUM_BISECTION_STEPS; ++i) {
const float mid = (above + below) * 0.5f;

if (heightAboveSurface(mid) > 0.0f)
above = mid;
else
below = mid;
}

return (above + below) * 0.5f;
}

void CSpringController::Update()
{
RECOIL_DETAILED_TRACY_ZONE;

pos.x = std::clamp(pos.x, 0.01f, mapDims.mapx * SQUARE_SIZE - 0.01f);
pos.z = std::clamp(pos.z, 0.01f, mapDims.mapy * SQUARE_SIZE - 0.01f);
pos.y = CGround::GetHeightReal(pos.x, pos.z, false); // always focus on the ground
curDist = std::clamp(curDist, minDist, maxDist);
pos.y = GetFocusSurfaceHeight(pos.x, pos.z, curDist); // always focus on the ground
rot.x = std::clamp(rot.x, math::PI * 0.51f, math::PI * 0.99f);

// camera->SetRot(float3(rot.x, GetAzimuth(), rot.z));
dir = CCamera::GetFwdFromRot(this->GetRot());

curDist = std::clamp(curDist, minDist, maxDist);
pixelSize = (camera->GetTanHalfFov() * 2.0f) / globalRendering->viewSizeY * curDist * 2.0f;
}

Expand Down
10 changes: 9 additions & 1 deletion rts/Game/Camera/SpringController.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ class CSpringController : public CCameraController
inline float ZoomIn(const float3& curCamPos, const float3& dir, const float& curDistPre, const float& scaledMode);
inline float ZoomOut(const float3& curCamPos, const float3& dir, const float& curDistPre, const float& scaledMode);

void SmoothCamHeight(const float3& prevPos);
void FreezeCamHeight();
bool UseSmoothMesh() const;
float GetFocusSurfaceHeight(float x, float z, float dist) const;
float DistanceToFocusSurface(const float3& from) const;

private:
float3 rot;
Expand All @@ -55,12 +58,17 @@ class CSpringController : public CCameraController
float fastScaleMove;
float fastScaleMousewheel;

float meshBlendMinDist;
float meshBlendMaxDist;

bool zoomBack;
bool cursorZoomIn;
bool cursorZoomOut;
bool doRotate;
bool lockCardinalDirections;
int trackMapHeight;

mutable bool warnedSmoothMeshDisabled = false; // one-shot log guard, see UseSmoothMesh()
};

#endif // _SPRING_CONTROLLER_H
30 changes: 30 additions & 0 deletions rts/Sim/Misc/SmoothHeightMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,29 @@ static float Interpolate(float x, float y, const int maxx, const int maxy, const
return mix(hi1, hi2, dy);
}


// C1-continuous, unlike Interpolate whose gradient jumps at cell borders
static float SampleBicubic(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);
const int sx = std::min((int)x, maxx - 1);
const int sy = std::min((int)y, maxy - 1);
const float dx = (x - sx);
const float dy = (y - sy);

// gather the read-only 4x4 neighbourhood around the cell, clamped at the edges
float patch[4][4];
for (int j = 0; j < 4; ++j) {
const float* row = &heightmap[std::clamp(sy + j - 1, 0, maxy - 1) * maxx];
for (int i = 0; i < 4; ++i)
patch[j][i] = row[std::clamp(sx + i - 1, 0, maxx - 1)];
}

return InterpolateBicubic(patch, dx, dy);
}

void SmoothHeightMesh::Init(int2 max, int res, int smoothRad)
{
RECOIL_DETAILED_TRACY_ZONE;
Expand Down Expand Up @@ -135,6 +158,13 @@ float SmoothHeightMesh::GetHeightAboveWater(float x, float y)
return std::max(0.0f, Interpolate(x, y, maxx, maxy, fresolution, &mesh[0]));
}

float SmoothHeightMesh::GetHeightSmooth(float x, float y)
{
RECOIL_DETAILED_TRACY_ZONE;
assert(!mesh.empty());
return SampleBicubic(x, y, maxx, maxy, fresolution, &mesh[0]);
}

float SmoothHeightMesh::SetHeight(int index, float h)
{
RECOIL_DETAILED_TRACY_ZONE;
Expand Down
3 changes: 3 additions & 0 deletions rts/Sim/Misc/SmoothHeightMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ class SmoothHeightMesh

float GetHeight(float x, float y);
float GetHeightAboveWater(float x, float y);
float GetHeightSmooth(float x, float y);
float SetHeight(int index, float h);
float AddHeight(int index, float h);
float SetMaxHeight(int index, float h);

bool HasMesh() const { return !mesh.empty(); }

int GetMaxX() const { return maxx; }
int GetMaxY() const { return maxy; }
float GetFMaxX() const { return fmaxx; }
Expand Down
20 changes: 20 additions & 0 deletions rts/System/SpringMath.h

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this generic math could be its own commit at the front

Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,26 @@ template <class T, class T2> constexpr T mixRotation(T v1, T v2, T2 a) {

template<class T> constexpr T Blend(const T v1, const T v2, const float a) { return mix(v1, v2, a); }

// Catmull-Rom cubic interpolation through p1..p2, with p0/p3 the outer neighbours, t in [0,1].
// C1-continuous (matching gradients across segment borders), unlike linear mix.
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)));
}

// Bicubic Catmull-Rom over a 4x4 patch of samples p[row][col]; dx/dy are the
// fractional offsets in [0,1] from the inner sample p[1][1] toward p[2][2].
inline float InterpolateBicubic(const float p[4][4], float dx, float dy)
{
const float cols[4] = {
CatmullRom(p[0][0], p[0][1], p[0][2], p[0][3], dx),
CatmullRom(p[1][0], p[1][1], p[1][2], p[1][3], dx),
CatmullRom(p[2][0], p[2][1], p[2][2], p[2][3], dx),
CatmullRom(p[3][0], p[3][1], p[3][2], p[3][3], dx),
};
return CatmullRom(cols[0], cols[1], cols[2], cols[3], dy);
}

int Round(const float f) _const _warn_unused_result;

template<class T> constexpr T Square(const T x) { return x*x; }
Expand Down
Loading