TerrainInterface controls ground geometry and surface properties throughout the simulation. Implement it to model elevation changes, slopes, and position-dependent surfaces like fairways, roughs, and greens. For uniform flat ground, pass a GroundSurface directly — the simulator wraps it in FlatTerrain automatically.
For flat terrain (backward compatible with existing code):
#include <terrain_interface.hpp>
// (height, restitution, frictionStatic, frictionDynamic, firmness, spinRetention)
GroundSurface ground{0.0F, 0.4F, 0.5F, 0.2F, 0.8F, 0.75F};
auto terrain = std::make_shared<FlatTerrain>(ground);
FlightSimulator sim(ball, atmos, terrain);
When a terrain is provided, the flight simulator queries terrain properties at each position during the simulation.
The TerrainInterface defines three required methods:
Returns the terrain elevation at a given horizontal position:
float getHeight(float x, float y) const override
{
// Return terrain height (z-coordinate) in feet at position (x, y)
return heightmap.lookup(x, y);
}
Returns the surface normal vector at a given position. The normal should be unit length and point upward (away from solid terrain, into the air):
Vector3D getNormal(float x, float y) const override
{
// Compute gradient from heightmap
Vector3D gradient = heightmap.computeGradient(x, y);
// Normal is perpendicular to tangent plane
Vector3D normal = {-gradient[0], -gradient[1], 1.0F};
// Must return unit vector
return math_utils::normalize(normal);
}
For flat surfaces, the normal is always {0.0F, 0.0F, 1.0F}. For sloped surfaces, the normal tilts accordingly while maintaining unit length.
Important: The normal vector should be unit length (magnitude = 1.0) for correct physics. Non-unit normals produce incorrect results but will not throw errors.
Returns material properties at a given position:
const GroundSurface& getSurfaceProperties(float x, float y) const override
{
// Return properties based on terrain type at this location
if (isInBunker(x, y))
return bunkerSurface;
else if (isOnGreen(x, y))
return greenSurface;
else
return fairwaySurface;
}
Example of a sloped terrain implementation:
class SlopedTerrain : public TerrainInterface
{
public:
SlopedTerrain(float slopeAngleDegrees, const GroundSurface& surface)
: surface_(surface)
{
float angleRad = slopeAngleDegrees * physics_constants::DEG_TO_RAD;
slopeRise_ = std::tan(angleRad);
// Precompute unit normal for this uniform slope
normal_[0] = 0.0F;
normal_[1] = std::sin(angleRad);
normal_[2] = std::cos(angleRad);
}
float getHeight(float x, float y) const override
{
(void)x; // Slope only varies in y direction
return -y * slopeRise_; // Descends as y increases
}
Vector3D getNormal(float x, float y) const override
{
(void)x;
(void)y;
return normal_; // Constant for uniform slope
}
const GroundSurface& getSurfaceProperties(float x, float y) const override
{
(void)x;
(void)y;
return surface_;
}
private:
GroundSurface surface_;
float slopeRise_;
Vector3D normal_;
};
Usage:
GroundSurface fairway{0.0F, 0.4F, 0.5F, 0.15F, 0.8F, 0.75F};
auto terrain = std::make_shared<SlopedTerrain>(5.0F, fairway); // 5-degree slope
FlightSimulator sim(ball, atmos, terrain);
The GroundSurface struct defines physical characteristics:
ground.restitution = 0.4F; // Range: 0.0 to 1.0
Coefficient of restitution controls bounce height. Higher values produce higher bounces:
ground.frictionStatic = 0.5F; // Affects bounce
ground.frictionDynamic = 0.2F; // Affects roll
Friction reduces horizontal velocity:
Higher friction values slow the ball more aggressively.
ground.firmness = 0.8F; // Range: 0.0 to 1.0
Surface firmness modulates friction effectiveness:
Friction factor = 1.0 - frictionStatic * (1.0 - firmness)
ground.spinRetention = 0.75F; // Range: 0.0 to 1.0
Fraction of spin retained after bounce:
When the ball impacts the ground, velocity is decomposed into components normal and tangent to the surface:
The physics correctly handle sloped surfaces, with bounce direction determined by the surface normal at the impact point.
During the roll phase, two forces act on the ball:
On flat surfaces, rolling friction alone causes deceleration. On slopes, the ball will accelerate if the gravity component exceeds friction.
Spin decay during roll: Linear decay model where ground friction applies constant torque opposing spin. This differs from aerial phase which uses exponential decay due to aerodynamic damping.
The simulation automatically transitions between phases:
The library does not validate physics parameters:
All flight phases require a valid terrain pointer:
if (!terrain)
{
throw std::invalid_argument("Terrain interface must not be null");
}
Terrain validation occurs in constructor initialization, failing fast if invalid.
Terrain queries occur multiple times per simulation timestep:
For complex terrain implementations (heightmaps, procedural generation):
The current implementation queries getHeight(), getNormal(), and getSurfaceProperties() separately, allowing simple implementations while permitting optimization in complex cases.
class GolfCourseTerrain : public TerrainInterface
{
public:
GolfCourseTerrain(const Heightmap& heights) : heightmap_(heights) {}
float getHeight(float x, float y) const override
{
return heightmap_.lookup(x, y);
}
Vector3D getNormal(float x, float y) const override
{
return heightmap_.computeNormal(x, y);
}
const GroundSurface& getSurfaceProperties(float x, float y) const override
{
// Different properties for different regions
if (isInBunker(x, y))
{
// (height, restitution, frictionStatic, frictionDynamic, firmness, spinRetention)
static GroundSurface sand{0.0F, 0.25F, 0.8F, 0.6F, 0.3F, 0.4F};
return sand;
}
else if (isOnGreen(x, y))
{
static GroundSurface green{0.0F, 0.5F, 0.4F, 0.15F, 0.9F, 0.85F};
return green;
}
else // Fairway
{
static GroundSurface fairway{0.0F, 0.4F, 0.5F, 0.2F, 0.8F, 0.75F};
return fairway;
}
}
private:
Heightmap heightmap_;
bool isInBunker(float x, float y) const
{
// Implementation specific to course layout
return bunkerRegions_.contains(x, y);
}
bool isOnGreen(float x, float y) const
{
// Implementation specific to course layout
return greenRegions_.contains(x, y);
}
BunkerRegions bunkerRegions_;
GreenRegions greenRegions_;
};
Code that passes a GroundSurface directly still works — FlatTerrain is created internally:
GroundSurface ground;
FlightSimulator sim(ball, atmos, ground); // equivalent to FlatTerrain(ground)
To switch to custom terrain, implement TerrainInterface and pass it instead:
auto terrain = std::make_shared<YourTerrainImpl>();
FlightSimulator sim(ball, atmos, terrain);
include/ground_surface.hppinclude/terrain_interface.hppinclude/ground_physics.hppinclude/physics_constants.hpptest/test_terrain_interface.cpptest/test_ground_physics.cpp