libshotscope

Ground Providers

Overview

The GroundProvider interface lets you change ground properties based on position - model fairways, roughs, greens, elevation changes, etc.

class GroundProvider {
public:
    virtual GroundSurface getGroundAt(float x, float y) const = 0;
};

Parameters:

Returns: GroundSurface struct with physical properties

Basic Example

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
        if (yards >= 250.0f) {
            return GroundSurface{3.0f, 0.35f, 0.4f, 0.12f, 0.95f, 0.85f};
        }

        // Fairway
        return GroundSurface{};  // Default values
    }
};

// Usage
SimpleHole provider;
FlightSimulator sim(physVars, ball, atmos, provider);

Ground Surface Parameters

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};

Parameter Details

height: Ground elevation in feet

restitution: Coefficient of restitution (COR) - energy retained = COR²

frictionStatic: Impact friction (combined with firmness)

frictionDynamic: Rolling resistance - deceleration = friction × gravity

firmness: Ground hardness - softer ground applies more friction on impact

spinRetention: Fraction of spin remaining after impact

Implementation Patterns

Distance-based

GroundSurface getGroundAt(float x, float y) const override {
    float yards = y / physics_constants::YARDS_TO_FEET;
    if (yards >= 250.0f) return greenSurface;
    return fairwaySurface;
}

With lateral rough

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{};
}

Circular features

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;
}

Grid-based

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;
};

When Ground is Queried

Ground is checked:

  1. Before each bounce (when ball contacts ground while moving downward)
  2. Every 0.1 seconds during roll phase
  3. At phase transitions (aerial → bounce, bounce → roll)

Your getGroundAt() is called 10-30 times per trajectory, not every simulation step (which would be 500+).

This means:

Performance

If you have an expensive ground provider (database queries, etc.), cache results based on position.

Complete Example

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 golfBall ball{0.0, 0.0, 0.0, 160.0, 11.0, 0.0, 3000.0, 0.0};
    const atmosphericData atmos{70.0, 0.0, 0.0, 0.0, 0.0, 50.0, 29.92};

    GolfHoleProvider provider;
    GolfBallPhysicsVariables physVars(ball, atmos);
    FlightSimulator sim(physVars, ball, atmos, provider);

    const float v0_fps = ball.exitSpeed * physics_constants::MPH_TO_FT_PER_S;
    BallState initialState = BallState::fromLaunchParameters(
        v0_fps, ball.launchAngle, ball.direction,
        Vector3D{0.0f, 0.0f, 0.0f},
        physics_constants::GRAVITY_FT_PER_S2,
        physVars.getROmega()
    );

    sim.initialize(initialState);

    while (!sim.isComplete()) {
        sim.step(0.01f);
    }

    const BallState& final = sim.getState();
    printf("Landed at: %.1f yards\n",
           final.position[1] / physics_constants::YARDS_TO_FEET);
}

Coordinate System

// Yards to feet
float feet = yards * physics_constants::YARDS_TO_FEET;  // 3.0

// Feet to yards
float yards = feet / physics_constants::YARDS_TO_FEET;

Backward Compatibility

Old code still works:

// Single ground surface - still works
GroundSurface ground;
FlightSimulator sim(physVars, ball, atmos, ground);

Internally creates a UniformGroundProvider that returns the same surface everywhere.