Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ AGENTS.md

__pycache__/
debug_runs/

# Calibration debug artifacts
tools/shot_calibration/debug_fs.html
tools/shot_calibration/debug_fs.png
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The worlds first open source golf physics library. Realistic golf ball physics engine for Godot 4. Provides force, torque, bounce, and surface interaction calculations usable from both C# and GDScript.

- Latest release **v1.0.5** data compared to FlightScope and GSPro. See data results here: [Shot Data]
- Latest release **v1.0.5** data compared to FS and GSP reference sources. See data results here: [Shot Data]
- Fast iteration tooling using godot console to calculate physics tuning against source of truth.
- (https://github.com/digitalhand/openfairway/tree/main/assets/data/calibration/history)

Expand Down Expand Up @@ -288,13 +288,13 @@ Run the headless benchmark suite and produce a carry/total/rollout table for the
godot --headless --script run_benchmarks.gd
```

For carry work, also run the Godot-backed Garmin carry-window suite:
For carry work, also run the Godot-backed LM carry-window suite:

```bash
dotnet test --filter "Category=GarminCarryWindow"
dotnet test --filter "Category=LmCarryWindow"
```

Use the benchmark script for broad flight/rollout trend checking and the Garmin suite for carry-window regression. See [`tests/PhysicsTests/README.md`](/home/jesher/Code/Github/digitalhand/openfairway/tests/PhysicsTests/README.md) for the full workflow and expected diagnostics.
Use the benchmark script for broad flight/rollout trend checking and the LM suite for carry-window regression. See [`tests/PhysicsTests/README.md`](/home/jesher/Code/Github/digitalhand/openfairway/tests/PhysicsTests/README.md) for the full workflow and expected diagnostics.

## Calibration Workflow

Expand Down
124 changes: 93 additions & 31 deletions addons/openfairway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Realistic golf ball physics engine for Godot 4.5+ C# projects. Usable from both
- [BallPhysics](#ballphysics)
- [PhysicsParams](#physicsparams)
- [BounceResult](#bounceresult)
- [BounceCalculator](#bouncecalculator)
- [Aerodynamics](#aerodynamics)
- [Surface](#surface)
- [PhysicsEnums](#physicsenums)
Expand Down Expand Up @@ -75,10 +76,10 @@ params.air_density = aero.get_air_density(0.0, 75.0, PhysicsEnums.Units.IMPERIAL
params.air_viscosity = aero.get_dynamic_viscosity(75.0, PhysicsEnums.Units.IMPERIAL)
params.drag_scale = 1.0
params.lift_scale = 1.0
params.surface_type = PhysicsEnums.SurfaceType.Fairway
params.surface_type = PhysicsEnums.SurfaceType.FAIRWAY
params.floor_normal = Vector3.UP

var fairway = surface.get_params(PhysicsEnums.SurfaceType.Fairway)
var fairway = surface.get_params(PhysicsEnums.SurfaceType.FAIRWAY)
params.kinetic_friction = fairway["u_k"]
params.rolling_friction = fairway["u_kr"]
params.grass_viscosity = fairway["nu_g"]
Expand Down Expand Up @@ -124,7 +125,7 @@ For calibration commands and regime/window configuration details, see:

## Regime Tuning Workflow

Use this workflow when the goal is to improve addon physics against FlightScope reference data without launching gameplay scenes.
Use this workflow when the goal is to improve addon physics against FS reference data without launching gameplay scenes.

### What actually improves shots

Expand Down Expand Up @@ -180,7 +181,7 @@ Resolution order is most-specific to least-specific:
3. `I-S1a`
4. `I`

Default regime overrides are baked into `BallPhysicsProfile.BuildDefaultRegimeOverrides()` (22 keys as of iteration 072). The `calibration_profile.json` is optional and only needed for experimental overrides during tuning.
Default regime overrides are baked into `BallPhysicsProfile.BuildDefaultRegimeOverrides()` (32 keys as of iteration 099). The `calibration_profile.json` is optional and only needed for experimental overrides during tuning.

Prefer specific regime keys (e.g. `D-S4-V1-P0`) over broad catch-alls (e.g. `D-S4-V1`) when sub-bins have opposite carry directions. Removing the `D-S4-V1` catch-all and replacing it with `D-S4-V1-P0`, `D-S4-V1-P1`, `D-S4-V1-P2` was necessary because P0 shots were short while P1/P2 were long.

Expand Down Expand Up @@ -213,7 +214,7 @@ If a regime has mixed signs, do not keep pushing it. Split the regime more narro

1. Update source defaults in `BallPhysicsProfile.cs` (or optionally `assets/data/calibration/calibration_profile.json` for experimental overrides)
2. Re-export physics headlessly
3. Re-run analysis against the same FlightScope corpus
3. Re-run analysis against the same FS reference corpus
4. Compare the new critical-carry report against the prior baseline

Example:
Expand Down Expand Up @@ -330,7 +331,7 @@ params.kinetic_friction = 0.50
params.rolling_friction = 0.050
params.grass_viscosity = 0.0017
params.critical_angle = 0.29
params.surface_type = PhysicsEnums.SurfaceType.Fairway
params.surface_type = PhysicsEnums.SurfaceType.FAIRWAY
params.floor_normal = Vector3.UP
params.rollout_impact_spin = 0.0
params.spinback_response_scale = 0.78
Expand Down Expand Up @@ -360,6 +361,25 @@ var new_omega: Vector3 = result.new_omega
var new_state: PhysicsEnums.BallState = result.new_state
```

### BounceCalculator

Standalone bounce physics, extracted from `BallPhysics`. Useful when you need bounce resolution without the full force/torque API.

```gdscript
var bc = BounceCalculator.new()

var result: BounceResult = bc.calculate_bounce(vel, omega, normal, state, params)
var cor: float = bc.get_coefficient_of_restitution(speed_normal)
```

Profile-aware overloads accept a `BounceProfile` for tunable COR curves and retention parameters:

```gdscript
var bp = BounceProfile.new()
var result: BounceResult = bc.calculate_bounce(vel, omega, normal, state, params, bp)
var cor: float = bc.get_coefficient_of_restitution(speed_normal, bp)
```

### Aerodynamics

Air density, viscosity, and drag/lift helpers.
Expand All @@ -380,7 +400,7 @@ Compatibility helper for GDScript consumers. Internally it forwards to `SurfaceP

```gdscript
var surface = Surface.new()
var p: Dictionary = surface.get_params(PhysicsEnums.SurfaceType.Green)
var p: Dictionary = surface.get_params(PhysicsEnums.SurfaceType.GREEN)
```

Returned dictionary keys:
Expand All @@ -400,29 +420,29 @@ Available surface types:

| GDScript enum | Description |
|---|---|
| `PhysicsEnums.SurfaceType.Fairway` | Standard fairway baseline |
| `PhysicsEnums.SurfaceType.FairwaySoft` | Softer fairway with more check and less rollout |
| `PhysicsEnums.SurfaceType.Rough` | Higher friction and drag |
| `PhysicsEnums.SurfaceType.Firm` | Lower friction and more forward release |
| `PhysicsEnums.SurfaceType.Green` | Strongest spinback/check response |
| `PhysicsEnums.SurfaceType.FAIRWAY` | Standard fairway baseline |
| `PhysicsEnums.SurfaceType.FAIRWAY_SOFT` | Softer fairway with more check and less rollout |
| `PhysicsEnums.SurfaceType.ROUGH` | Higher friction and drag |
| `PhysicsEnums.SurfaceType.FIRM` | Lower friction and more forward release |
| `PhysicsEnums.SurfaceType.GREEN` | Strongest spinback/check response |

### PhysicsEnums

The addon ships `physics_enums.gd` so enums are always available in GDScript.

```gdscript
PhysicsEnums.BallState.Rest
PhysicsEnums.BallState.Flight
PhysicsEnums.BallState.Rollout

PhysicsEnums.Units.Metric
PhysicsEnums.Units.Imperial

PhysicsEnums.SurfaceType.Fairway
PhysicsEnums.SurfaceType.FairwaySoft
PhysicsEnums.SurfaceType.Rough
PhysicsEnums.SurfaceType.Firm
PhysicsEnums.SurfaceType.Green
PhysicsEnums.BallState.REST
PhysicsEnums.BallState.FLIGHT
PhysicsEnums.BallState.ROLLOUT

PhysicsEnums.Units.METRIC
PhysicsEnums.Units.IMPERIAL

PhysicsEnums.SurfaceType.FAIRWAY
PhysicsEnums.SurfaceType.FAIRWAY_SOFT
PhysicsEnums.SurfaceType.ROUGH
PhysicsEnums.SurfaceType.FIRM
PhysicsEnums.SurfaceType.GREEN
```

### ShotSetup
Expand Down Expand Up @@ -463,6 +483,14 @@ var result_green: Dictionary = adapter.simulate_shot_from_json(
)
```

Carry-only variants run the flight loop only (no bounce or rollout), useful for rapid calibration:

```gdscript
var carry_result: Dictionary = adapter.simulate_carry_only_from_json(shot_dict)
var carry_profile: Dictionary = adapter.simulate_carry_only_with_profile(shot_dict, profile)
var carry_flight: Dictionary = adapter.simulate_carry_only(shot_dict, flight_profile)
```

Returned keys include:

- `carry_yd`
Expand Down Expand Up @@ -579,13 +607,13 @@ Source: [`assets/diagrams/landing-surface-sequence.puml`](assets/diagrams/landin

`SurfacePhysicsCatalog` is the single source of truth for the built-in surfaces:

| Surface | `u_k` | `u_kr` | `theta_c` rad | `spin_scale` | `theta_boost` rad |
|---|---:|---:|---:|---:|---:|
| Fairway | 0.50 | 0.050 | 0.29 | 0.78 | 0.00 |
| FairwaySoft | 0.56 | 0.070 | 0.32 | 0.92 | 0.00 |
| Rough | 0.62 | 0.095 | 0.35 | 0.70 | 0.00 |
| Firm | 0.30 | 0.030 | 0.25 | 0.60 | 0.00 |
| Green | 0.58 | 0.028 | 0.36 | 1.12 | 0.12 |
| Surface | `u_k` | `u_kr` | `nu_g` | `theta_c` rad | `spin_scale` | `theta_boost` rad |
|---|---:|---:|---:|---:|---:|---:|
| Fairway | 0.50 | 0.050 | 0.0017 | 0.29 | 0.78 | 0.00 |
| FairwaySoft | 0.56 | 0.070 | 0.0024 | 0.32 | 0.92 | 0.00 |
| Rough | 0.62 | 0.095 | 0.0032 | 0.35 | 0.70 | 0.00 |
| Firm | 0.30 | 0.030 | 0.0010 | 0.25 | 0.60 | 0.00 |
| Green | 0.58 | 0.028 | 0.0009 | 0.36 | 1.12 | 0.12 |

Green also enables a spinback ramp:

Expand All @@ -600,6 +628,40 @@ Tune these files when behavior changes:
- `addons/openfairway/physics/BallPhysics.cs` for shared formulas and physical constants
- `addons/openfairway/physics/BallPhysicsProfile.cs` for ball-specific modifiers

## GDScript Interop: Drift Risks and Migration Notes

### Enum naming convention

C# enums use PascalCase (`SurfaceType.Fairway`); GDScript mirrors in `physics_enums.gd` use UPPER_SNAKE_CASE (`SurfaceType.FAIRWAY`). The integer values match, but names differ:

| C# (`PhysicsEnums.cs`) | GDScript (`physics_enums.gd`) |
|---|---|
| `BallState.Rest` | `BallState.REST` |
| `BallState.Flight` | `BallState.FLIGHT` |
| `BallState.Rollout` | `BallState.ROLLOUT` |
| `Units.Metric` | `Units.METRIC` |
| `Units.Imperial` | `Units.IMPERIAL` |
| `SurfaceType.Fairway` | `SurfaceType.FAIRWAY` |
| `SurfaceType.FairwaySoft` | `SurfaceType.FAIRWAY_SOFT` |
| `SurfaceType.Rough` | `SurfaceType.ROUGH` |
| `SurfaceType.Firm` | `SurfaceType.FIRM` |
| `SurfaceType.Green` | `SurfaceType.GREEN` |

When adding new enum values to `PhysicsEnums.cs`, update `physics_enums.gd` to match. Integer values must stay aligned across both files.

### `.tres` file compatibility

`PhysicsParams` extends `Resource` and can be saved as `.tres`. Property renames or removals in C# will silently break saved `.tres` files — Godot loads them without error but the renamed properties revert to defaults. After renaming or removing a `PhysicsParams` property, re-export any `.tres` files that reference it.

### New API additions (iter 099)

- `BounceCalculator` — standalone bounce resolution with profile-aware overloads (see [BounceCalculator](#bouncecalculator))
- `SimulateCarryOnlyFromJson`, `SimulateCarryOnlyWithProfile`, `SimulateCarryOnly` — flight-only simulation variants on `PhysicsAdapter` (see [PhysicsAdapter](#physicsadapter))

### Surface type additions

`Green` was added after the initial release. GDScript consumers should include a default/fallback branch when switching on `SurfaceType`. `SurfacePhysicsCatalog.Get()` falls back to Fairway for any unrecognized surface type.

## Diagrams

- [`assets/images/physics-runtime-components.png`](assets/images/physics-runtime-components.png)
Expand Down
5 changes: 3 additions & 2 deletions addons/openfairway/physics/BallPhysicsProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public RegimeScaleOverride ResolveScaleOverride(
}

/// <summary>
/// Calibrated regime-specific scale overrides derived from FlightScope
/// Calibrated regime-specific scale overrides derived from FS reference
/// reference data. These correct systematic carry biases per launch regime
/// (e.g. chip shots under-carry at low Re, driver shots over-carry at high Re).
/// </summary>
Expand Down Expand Up @@ -154,7 +154,7 @@ private static Dictionary<string, RegimeScaleOverride> BuildDefaultRegimeOverrid
["W-S1a-V3-P3"] = new() { DragScaleMultiplier = 0.83f, LiftScaleMultiplier = 1.10f },

// Fast iron with high spin: over-carry
["I-S3-V2-P3"] = new() { DragScaleMultiplier = 1.08f, LiftScaleMultiplier = 0.96f },
["I-S3-V2-P3"] = new() { DragScaleMultiplier = 1.11f, LiftScaleMultiplier = 0.94f },
["I-S3-V1-P2"] = new() { DragScaleMultiplier = 1.04f, LiftScaleMultiplier = 0.98f },
["I-S2-V2-P3"] = new() { DragScaleMultiplier = 1.05f },
["I-S2-V2-P4"] = new() { DragScaleMultiplier = 1.06f, LiftScaleMultiplier = 0.95f },
Expand All @@ -172,6 +172,7 @@ private static Dictionary<string, RegimeScaleOverride> BuildDefaultRegimeOverrid
// Driver regime: slight over-carry
["D-S3-V1"] = new() { DragScaleMultiplier = 1.04f, LiftScaleMultiplier = 0.99f },
["D-S4-V0-P1"] = new() { DragScaleMultiplier = 1.03f, LiftScaleMultiplier = 0.98f },
["D-S4-V0-P2"] = new() { DragScaleMultiplier = 1.09f, LiftScaleMultiplier = 0.94f },
["D-S4-V1-P0"] = new() { DragScaleMultiplier = 0.98f, LiftScaleMultiplier = 1.02f },
["D-S4-V1-P1"] = new() { DragScaleMultiplier = 1.04f },
["D-S4-V1-P2"] = new() { DragScaleMultiplier = 1.04f },
Expand Down
19 changes: 0 additions & 19 deletions assets/data/SOT/flightscope_SoT.csv.import

This file was deleted.

File renamed without changes.
19 changes: 19 additions & 0 deletions assets/data/SOT/fs_SoT.csv.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[remap]

importer="csv_translation"
type="Translation"
uid="uid://c8hmov08l2m0j"

[deps]

files=["res://assets/data/SOT/fs_SoT.filename.translation", "res://assets/data/SOT/fs_SoT.speed.translation", "res://assets/data/SOT/fs_SoT.vla.translation", "res://assets/data/SOT/fs_SoT.hla.translation", "res://assets/data/SOT/fs_SoT.total.translation", "res://assets/data/SOT/fs_SoT.spin.translation", "res://assets/data/SOT/fs_SoT.backspin.translation", "res://assets/data/SOT/fs_SoT.sidespin.translation", "res://assets/data/SOT/fs_SoT.carry.translation", "res://assets/data/SOT/fs_SoT.total.translation", "res://assets/data/SOT/fs_SoT.rollout.translation", "res://assets/data/SOT/fs_SoT.apex.translation"]

source_file="res://assets/data/SOT/fs_SoT.csv"
dest_files=["res://assets/data/SOT/fs_SoT.filename.translation", "res://assets/data/SOT/fs_SoT.speed.translation", "res://assets/data/SOT/fs_SoT.vla.translation", "res://assets/data/SOT/fs_SoT.hla.translation", "res://assets/data/SOT/fs_SoT.total.translation", "res://assets/data/SOT/fs_SoT.spin.translation", "res://assets/data/SOT/fs_SoT.backspin.translation", "res://assets/data/SOT/fs_SoT.sidespin.translation", "res://assets/data/SOT/fs_SoT.carry.translation", "res://assets/data/SOT/fs_SoT.total.translation", "res://assets/data/SOT/fs_SoT.rollout.translation", "res://assets/data/SOT/fs_SoT.apex.translation"]

[params]

compress=1
delimiter=0
unescape_keys=false
unescape_translations=true
1 change: 1 addition & 0 deletions assets/data/SOT/gsp_SoT.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
shot_name,filename,carry_yd,total_yd,apex_ft
19 changes: 19 additions & 0 deletions assets/data/SOT/gsp_SoT.csv.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[remap]

importer="csv_translation"
type="Translation"
uid="uid://2klftri57qul"

[deps]

files=["res://assets/data/SOT/gsp_SoT.en.translation", "res://assets/data/SOT/gsp_SoT.en.translation", "res://assets/data/SOT/gsp_SoT.en.translation", "res://assets/data/SOT/gsp_SoT.en.translation"]

source_file="res://assets/data/SOT/gsp_SoT.csv"
dest_files=["res://assets/data/SOT/gsp_SoT.en.translation", "res://assets/data/SOT/gsp_SoT.en.translation", "res://assets/data/SOT/gsp_SoT.en.translation", "res://assets/data/SOT/gsp_SoT.en.translation"]

[params]

compress=1
delimiter=0
unescape_keys=false
unescape_translations=true
1 change: 1 addition & 0 deletions assets/data/SOT/gsp_reference.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
19 changes: 0 additions & 19 deletions assets/data/calibration/flightscope.csv.import

This file was deleted.

Loading
Loading