Gravity: one potential per zone owns acceleration and height

GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-15 04:36:28 +02:00
commit 021c4cbcbf
16 changed files with 433 additions and 171 deletions

View file

@ -617,17 +617,12 @@ public class Zone
var result = 0f;
foreach (var body in PlanetInstances.Values)
{
var p = position - body.Orbit.Position; //GetOrbitPosition(body.BodyData.Orbit)
var distSqr = lengthsq(p);
var gravityRadius = body.GravityWellRadius;
if (distSqr < gravityRadius*gravityRadius)
{
var depth = body.GravityWellDepth;
result -= PowerPulse(sqrt(distSqr) / gravityRadius, body.BodyData.GravityDepthExponent) * depth;
}
var offset = position - body.Orbit.Position;
result += GravityField.Potential(body.WellStrength, body.CoreRadius, length(offset))
- GravityField.Potential(body.WellStrength, body.CoreRadius, Pack.Radius);
}
return result;
return result / Settings.HeightExaggeration;
}
public float GetLight(float2 position)
@ -647,33 +642,21 @@ public class Zone
internal float SchematicCellSize => _itemManager.GameplaySettings.SchematicCellSize;
internal float FieldCrossSection(AsteroidBeltData belt) => _itemManager.ItemData.Get(belt.Kind)?.CrossSection ?? 0f;
// The one law of how a gravity well pushes anything: ships, floating items and mines all read it. The shape is
// the one ships have always felt, normalize(f) * strength * (1 / (1 - |f|^2) - 1), gated off below |f|^2 = .001.
// It is an acceleration, per second: the legacy law added that amount once per 60 fps frame with no dt, so
// GravityReferenceRate (60) restates it and `acceleration * dt` gives the legacy push at 60 fps.
public const float GravityReferenceRate = 60f;
public static float2 GravityAcceleration(float2 normalXz, float strength)
{
var magnitude = lengthsq(normalXz);
if (magnitude <= .001f) return float2.zero;
return normalize(normalXz) * strength * (1 / (1 - magnitude) - 1) * GravityReferenceRate;
}
// The acceleration the well exerts at `position`.
// The one source of gravitational acceleration (u/s^2): the sum of every body's well (GravityField). Ships,
// floating items, clouds and mines all read it, and so does Flow. It is the gradient of the potential GetHeight
// draws, so the two cannot disagree.
public float2 GetForce(float2 position)
{
var normal = GetNormal(position);
return GravityAcceleration(new float2(normal.x, normal.z), Settings.GravityStrength);
var force = float2.zero;
foreach (var body in PlanetInstances.Values)
force += GravityField.Acceleration(body.WellStrength, body.CoreRadius, position - body.Orbit.Position);
return force;
}
// The global flow velocity at `position` in zone time, in u/s (FlowField is the law). The slope term reads the
// downhill gradient, which GetNormal's xz is proportional to.
public float3 Flow(float3 position)
{
var normal = GetNormal(position.xz);
return FlowField.At(Settings.Flow, position, FlowScroll, new float2(normal.x, normal.z));
}
// downhill direction, which is the force.
public float3 Flow(float3 position) =>
FlowField.At(Settings.Flow, position, FlowScroll, GetForce(position.xz));
// The flow's noise scroll at zone time (Zone.Flow evaluates at exactly this); lowered to the shaders' _FlowScroll.
public float2 FlowScroll => FlowField.Scroll(Settings.Flow, _time);
@ -688,16 +671,12 @@ public class Zone
return pow((x + 1) * (1 - x), exponent);
}
public float3 GetNormal(float2 pos, float step = .1f, float mul = 1)
// The surface normal of the drawn height (potential / K), derived from the force. Display only (ship tilt): the
// sim never reads it as a force.
public float3 GetNormal(float2 pos)
{
float hL = GetHeight(new float2(pos.x - step, pos.y)) * mul;
float hR = GetHeight(new float2(pos.x + step, pos.y)) * mul;
float hD = GetHeight(new float2(pos.x, pos.y - step)) * mul;
float hU = GetHeight(new float2(pos.x, pos.y + step)) * mul;
// Deduce terrain normal
float3 normal = new float3((hL - hR), (hD - hU), step*2);
return normalize(normal).xzy;
var slope = GetForce(pos) / Settings.HeightExaggeration;
return normalize(new float3(slope.x, 1f, slope.y));
}
}
@ -706,10 +685,17 @@ public class Planet
public Orbit Orbit;
protected readonly PlanetSettings Settings;
public BodyData BodyData;
// Brush inputs only (ZoneRenderer's Gravity Well brushes and planet icon); no sim reader. grid-lowering deletes them.
public float GravityWellDepth;
public float GravityWellRadius;
public float BodyRadius;
// The well (GravityField): strength c and softened core radius R by mass, and the Roche limit, the closest a child
// may orbit. All three are authored PlanetSettings curves of mass; the core must sit inside the Roche limit.
public float WellStrength;
public float CoreRadius;
public float RocheLimit;
public Planet(PlanetSettings settings, BodyData data, Orbit orbit)
{
Settings = settings;
@ -723,6 +709,11 @@ public class Planet
BodyRadius = Settings.BodyRadius.Evaluate(BodyData.Mass) * BodyData.BodyRadiusMultiplier;
GravityWellRadius = Settings.GravityRadius.Evaluate(BodyData.Mass) * BodyData.GravityRadiusMultiplier;
GravityWellDepth = Settings.GravityDepth.Evaluate(BodyData.Mass) * BodyData.GravityDepthMultiplier;
WellStrength = Settings.WellStrength.Evaluate(BodyData.Mass);
CoreRadius = Settings.CoreRadius.Evaluate(BodyData.Mass);
RocheLimit = Settings.RocheLimit.Evaluate(BodyData.Mass);
if (WellStrength > 0 && !(0 < CoreRadius && CoreRadius < RocheLimit))
throw new InvalidOperationException("A body's core radius must be positive and inside its Roche limit (PlanetSettings.CoreRadius, RocheLimit).");
}
}