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:
parent
ff63362f45
commit
021c4cbcbf
16 changed files with 433 additions and 171 deletions
27
Assets/Scripts/ServerShared/GravityField.cs
Normal file
27
Assets/Scripts/ServerShared/GravityField.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using CultMath;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
|
||||
/// <summary>
|
||||
/// The law of one gravity well: a 2D point mass with a softened core. Outside the core radius the potential is
|
||||
/// logarithmic and the pull is c / r; inside it the pull falls linearly to zero at the centre and meets the tail in
|
||||
/// value and slope at the core radius. c is the well's strength (u^2/s^2), R its core radius (u). No threshold
|
||||
/// anywhere: a well pulls at every distance. Zone.GetForce and Zone.GetHeight sum this over a zone's bodies.
|
||||
/// </summary>
|
||||
public static class GravityField
|
||||
{
|
||||
// The potential at distance r from the body, defined up to a constant (callers subtract their reference).
|
||||
public static float Potential(float c, float coreRadius, float r) =>
|
||||
r < coreRadius
|
||||
? c * r * r / (2 * coreRadius * coreRadius)
|
||||
: c * (log(r / coreRadius) + .5f);
|
||||
|
||||
// The acceleration at `offsetFromBody`, toward the body: zero at the body itself.
|
||||
public static float2 Acceleration(float c, float coreRadius, float2 offsetFromBody)
|
||||
{
|
||||
var r = length(offsetFromBody);
|
||||
if (r == 0f) return float2.zero;
|
||||
var pull = r < coreRadius ? c * r / (coreRadius * coreRadius) : c / r;
|
||||
return -offsetFromBody / r * pull;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/GravityField.cs.meta
Normal file
11
Assets/Scripts/ServerShared/GravityField.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5b0c7d3e9a1f4e2c8d6a7f1b3c9e0a42
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -17,11 +17,21 @@ public class PlanetSettings
|
|||
public ExponentialCurve GravityRadius;
|
||||
public ExponentialCurve LightRadius;
|
||||
public ExponentialCurve BodyRadius;
|
||||
|
||||
// The closest a child may orbit its parent, in u, by the parent's mass. Authored like BodyRadius, never per body.
|
||||
public ExponentialCurve RocheLimit = new ExponentialCurve();
|
||||
|
||||
// A body's well, by its mass (GravityField): its strength c in u^2/s^2 (the pull is c / r beyond the core) and its
|
||||
// softened core radius in u, which must stay inside the RocheLimit curve.
|
||||
public ExponentialCurve WellStrength = new ExponentialCurve();
|
||||
public ExponentialCurve CoreRadius = new ExponentialCurve();
|
||||
|
||||
// The one global exaggeration of gravity height: height = potential / K, in u/s^2 per unit of height.
|
||||
public float HeightExaggeration = 1f;
|
||||
public float AsteroidVerticalOffset = -5f;
|
||||
public ExponentialLerp AsteroidSize;
|
||||
public ExponentialLerp AsteroidHitpoints;
|
||||
public ExponentialLerp AsteroidRespawnTime;
|
||||
public float GravityStrength;
|
||||
public float SecureAreaRadiusMultiplier = .45f;
|
||||
|
||||
public ExponentialCurve OrbitPeriod;
|
||||
|
|
|
|||
|
|
@ -130,9 +130,8 @@ public class Ship : Entity
|
|||
|
||||
Position.xz += Velocity * delta;
|
||||
|
||||
Velocity += Zone.GetForce(Position.xz) * delta;
|
||||
var normal = Zone.GetNormal(Position.xz);
|
||||
var force = new float2(normal.x, normal.z);
|
||||
Velocity += Zone.GravityAcceleration(force, Zone.Settings.GravityStrength) * delta;
|
||||
var shipRight = Direction.Rotate(ItemRotation.Clockwise);
|
||||
var forward = cross(float3(shipRight.x, 0, shipRight.y), normal);
|
||||
Rotation = quaternion.LookRotation(forward, normal);
|
||||
|
|
|
|||
|
|
@ -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).");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,9 +92,6 @@ public abstract class BodyData
|
|||
|
||||
[JsonProperty("depthMul")] [Key(7)]
|
||||
public float GravityDepthMultiplier = 1;
|
||||
|
||||
[JsonProperty("depthExp")] [Key(8)]
|
||||
public float GravityDepthExponent = 16;
|
||||
}
|
||||
|
||||
[CultDocument("aetheria.planetdata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue