The GroundProvider interface lets you change ground properties based on position - model fairways, roughs, greens, and other surface variations on flat terrain.
Use for flat terrain with varying materials:
Limitation: Assumes flat terrain. The surface normal is always vertical.
Use for 3D terrain with elevation:
See the Terrain System Guide for implementing 3D terrain.
class GroundProvider {
public:
virtual GroundSurface getGroundAt(float x, float y) const = 0;
};
Parameters:
x: Lateral position in feet (perpendicular to target, positive = right)y: Downrange position in feet (along target line, positive = forward)Returns: GroundSurface struct with physical properties
class SimpleHole : public GroundProvider {
public:
GroundSurface getGroundAt(float x, float y) const override {
float yards = y / physics_constants::YARDS_TO_FEET;
// Green at 250+ yards, elevated 3 feet
// (height, restitution, frictionStatic, frictionDynamic, firmness, spinRetention)
if (yards >= 250.0f) {
return GroundSurface{3.0f, 0.35f, 0.4f, 0.12f, 0.95f, 0.85f};
}
// Fairway with default values
return GroundSurface{};
}
};
// Usage
SimpleHole provider;
FlightSimulator sim(ball, atmos, provider);
GroundSurface{
height, // feet (0.0 = ground level)
restitution, // bounce coefficient (0.0-1.0)
frictionStatic, // impact friction (0.0-1.0+)
frictionDynamic, // rolling resistance (0.0-1.0+)
firmness, // ground hardness (0.0-1.0+)
spinRetention // spin after impact (0.0-1.0)
};
Common presets:
// Fairway
GroundSurface{0.0f, 0.4f, 0.5f, 0.2f, 0.8f, 0.75f};
// Fast green
GroundSurface{0.0f, 0.35f, 0.4f, 0.12f, 0.95f, 0.85f};
// Thick rough
GroundSurface{0.0f, 0.25f, 0.6f, 0.5f, 0.4f, 0.55f};
// Sand bunker
GroundSurface{-0.5f, 0.1f, 0.8f, 0.9f, 0.15f, 0.3f};
height: Ground elevation in feet
0.0f: Ground level3.0f: Elevated green-1.0f: Sunken bunkerrestitution: Coefficient of restitution (COR) - energy retained = COR²
0.1f: Sand bunker (retains 1% of energy)0.4f: Fairway (retains 16% of energy)0.5f: Hard surface (retains 25% of energy)frictionStatic: Impact friction (combined with firmness)
0.3f: Low friction0.5f: Fairway0.8f: SandfrictionDynamic: Rolling resistance - deceleration = friction × gravity
0.12f: Fast green (stimp 12+)0.2f: Medium fairway0.5f: Thick roughfirmness: Ground hardness - softer ground applies more friction on impact
0.15f: Soft sand0.8f: Firm fairway0.95f: Hard greenspinRetention: Fraction of spin remaining after impact
0.3f: Sand bunker0.75f: Fairway0.85f: Soft greenGroundSurface getGroundAt(float x, float y) const override {
float yards = y / physics_constants::YARDS_TO_FEET;
if (yards >= 250.0f) return greenSurface;
return fairwaySurface;
}
GroundSurface getGroundAt(float x, float y) const override {
float lateralYards = std::abs(x) / physics_constants::YARDS_TO_FEET;
float downrangeYards = y / physics_constants::YARDS_TO_FEET;
// Green
if (downrangeYards >= 250.0f && downrangeYards <= 270.0f) {
return GroundSurface{3.0f, 0.35f, 0.4f, 0.12f, 0.95f, 0.85f};
}
// Rough
if (lateralYards > 20.0f) {
return GroundSurface{0.0f, 0.25f, 0.6f, 0.5f, 0.4f, 0.55f};
}
// Fairway
return GroundSurface{};
}
GroundSurface getGroundAt(float x, float y) const override {
float dx = x - centerX;
float dy = y - centerY;
float distance = std::sqrt(dx*dx + dy*dy);
if (distance <= radius) return greenSurface;
return fairwaySurface;
}
class GridTerrain : public GroundProvider {
public:
GridTerrain(const std::vector<std::vector<GroundSurface>>& grid,
float cellSizeYards)
: grid_(grid),
cellSize_(cellSizeYards * physics_constants::YARDS_TO_FEET) {}
GroundSurface getGroundAt(float x, float y) const override {
int col = static_cast<int>(x / cellSize_);
int row = static_cast<int>(y / cellSize_);
if (row < 0 || row >= grid_.size() ||
col < 0 || col >= grid_[0].size()) {
return defaultSurface;
}
return grid_[row][col];
}
private:
std::vector<std::vector<GroundSurface>> grid_;
float cellSize_;
GroundSurface defaultSurface;
};
Ground is checked:
Your getGroundAt() is called 10-30 times per trajectory, not every simulation step (which would be 500+).
This means:
getGroundAt()getGroundAt()If you have an expensive ground provider (database queries, etc.), cache results based on position.
See examples/multi_ground_simulation.cpp:
class GolfHoleProvider : public GroundProvider {
public:
GroundSurface getGroundAt(float x, float y) const override {
const float lateralYards = x / physics_constants::YARDS_TO_FEET;
const float downrangeYards = y / physics_constants::YARDS_TO_FEET;
// Green: 250-270 yards, elevated 3 feet
if (downrangeYards >= 250.0f && downrangeYards <= 270.0f) {
return GroundSurface{3.0f, 0.35f, 0.4f, 0.12f, 0.95f, 0.85f};
}
// Rough: beyond ±20 yards from centerline
if (std::abs(lateralYards) > 20.0f) {
return GroundSurface{0.0f, 0.25f, 0.6f, 0.5f, 0.4f, 0.55f};
}
// Fairway
return GroundSurface{0.0f, 0.4f, 0.5f, 0.2f, 0.8f, 0.75f};
}
};
int main() {
const LaunchData ball{160.0f, 11.0f, 0.0f, 3000.0f, 0.0f};
const AtmosphericData atmos{70.0f, 0.0f, 0.0f, 0.0f, 0.0f, 50.0f, 29.92f};
GolfHoleProvider provider;
FlightSimulator sim(ball, atmos, provider);
sim.run();
LandingResult result = sim.getLandingResult();
printf("Landed at: %.1f yards\n", result.yF);
}
physics_constants::YARDS_TO_FEET for conversion)// Yards to feet
float feet = yards * physics_constants::YARDS_TO_FEET; // 3.0
// Feet to yards
float yards = feet / physics_constants::YARDS_TO_FEET;
If you don’t need position-dependent surfaces, pass a GroundSurface directly:
// Single ground surface (uses default fairway values)
GroundSurface ground;
FlightSimulator sim(ball, atmos, ground);
// Or with custom values
GroundSurface green{0.0f, 0.35f, 0.4f, 0.12f, 0.95f, 0.85f};
FlightSimulator sim2(ball, atmos, green);
Internally creates a UniformGroundProvider that returns the same surface everywhere.