Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
using System ;
using System.Collections.Generic ;
using GameCult.Caching ;
using CultMath ;
using static CultMath . math ;
2026-10-16 17:58:17 +02:00
using float2 = CultMath . float2 ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
2026-10-16 21:14:58 +02:00
/// <summary>
/// An orbit as the well tree reads it: where it hangs on the tree and the mass of the well-bearing body on it (0 for none).
/// Extent is the outermost distance from the parent of anything riding this orbit's rail (a belt orbit: its outermost
/// asteroid; any other orbit: its Distance). Anchor is a parentless orbit's stationary position, ignored with a parent.
/// </summary>
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
public readonly struct WellNode
{
public readonly CultRecordKey Key , Parent ;
2026-10-16 21:14:58 +02:00
public readonly float Distance , Phase , Mass , Extent ;
public readonly float2 Anchor ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
2026-10-16 21:14:58 +02:00
public WellNode ( CultRecordKey key , CultRecordKey parent , float distance , float phase , float mass , float extent , float2 anchor )
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
{
Key = key ;
Parent = parent ;
Distance = distance ;
Phase = phase ;
Mass = mass ;
2026-10-16 21:14:58 +02:00
Extent = extent ;
Anchor = anchor ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
}
}
/// <summary>
/// The one owner of which gravity terms a zone has: the orbit tree turned into band terms (GravityField is the law of one
/// term). Built once from the pack's orbits and the authored curves, never reading time; the caller passes positions.
///
/// Every node F (a body, or a bodyless node with its ring group standing in for the body) contributes at its own position
/// c(m_F) U(eps(m_F), L_F, B_F) + c(M_F) [ U(eps(M_F), max(B_F, Gamma Lam_F), Lam_F) - U(eps(M_F), L_F, B_F) ]
/// m_F is the body's mass, M_F the family's, c and eps the WellStrength and RocheLimit curves. L_F is the outermost
2026-10-16 21:14:58 +02:00
/// child's extent: inside it the node pulls exactly as its body alone. B_F, the support, bounds every descendant term:
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
/// beyond it the family pulls exactly as one body of mass M_F, out to max(B_F, Gamma Lam_F), and is gone at its reach
2026-10-16 21:14:58 +02:00
/// Lam_F. A zone's sole massive root has infinite reach, so its far field is exactly c(M_zone). A zone of several massive
/// roots (subzones) has one zone node, which is not an orbit: at the roots' centre of mass it owns the only global term,
/// c(M_zone) U_global less c(M_zone) U(eps, A, Beta A), nothing inside A and the whole zone as one body beyond Beta A;
/// each root is bounded to its own subzone (SubzoneReach).
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
/// A body with no massive descendants is one term c(m) U(eps(m), Gamma Lam, Lam). Beyond a node's reach its whole
/// subtree contributes exactly nothing, so Force and Potential skip it: the walk is exact, not an approximation.
/// </summary>
public sealed class WellTree
{
/// <summary>Orbits whose distances differ by no more than this are on one ring (a binary, a ring of siblings).</summary>
public const float RingWindow = . 1f ;
/// <summary>A band term of the field: Strength x GravityField's U(Softening, Inner, Outer) at Node's position, less Reference.</summary>
public readonly struct Term
{
public readonly CultRecordKey Node ;
public readonly float Strength , Softening , Inner , Outer , Reference ;
public Term ( CultRecordKey node , float strength , float softening , float inner , float outer , float reference )
{
Node = node ;
Strength = strength ;
Softening = softening ;
Inner = inner ;
Outer = outer ;
Reference = reference ;
}
}
private readonly Dictionary < CultRecordKey , int > _index = new Dictionary < CultRecordKey , int > ( ) ;
private CultRecordKey [ ] _key ;
private int [ ] [ ] _kids ;
private int [ ] _roots ;
private int [ ] [ ] _ring ;
private int [ ] _termStart , _termCount ;
2026-10-16 21:14:58 +02:00
private float [ ] _distance , _phase , _reach , _support , _extent ;
private float2 [ ] _anchor ;
private int [ ] _massiveRoots ;
private float _packRadius ;
private int _zoneStart , _zoneCount ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
private Term [ ] _terms ;
2026-10-16 21:14:58 +02:00
/// <summary>Where the zone node's terms sit (the family-mass-weighted mean of the massive roots' anchors); meaningful when it has terms.</summary>
public float2 Centre { get ; private set ; }
/// <summary>Every term of the zone, grouped by node, the zone node's last (its Node is unset: it sits at Centre): what the lowering uploads and the shader sums.</summary>
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
public IReadOnlyList < Term > Terms = > _terms ;
private WellTree ( ) { }
2026-10-16 21:14:58 +02:00
/// <summary>A node's reach in its parent's field (+inf for a zone's sole massive root; a subzone's radius for a root of several); 0 for an orbit that carries no mass.</summary>
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
public float Reach ( CultRecordKey node ) = > _reach [ _index [ node ] ] ;
/// <summary>A node's support B: every term of its descendants lies inside it, so beyond it the family is one body.</summary>
public float Support ( CultRecordKey node ) = > _support [ _index [ node ] ] ;
/// <summary>
/// A well's strength and softening at a mass, from the authored curves (the one place they are read and checked).
/// A mass of 0 is no well.
/// </summary>
public static ( float Strength , float Softening ) Curves ( PlanetSettings settings , float mass )
{
var strength = settings . WellStrength . Evaluate ( mass ) ;
var softening = settings . RocheLimit . Evaluate ( mass ) ;
if ( ! ( strength > = 0f ) | | float . IsInfinity ( strength ) )
throw new InvalidOperationException ( "A body's well strength must be finite and not negative (PlanetSettings.WellStrength)." ) ;
if ( strength > 0 & & ! ( softening > 0f & & ! float . IsInfinity ( softening ) ) )
throw new InvalidOperationException ( "A body's Roche limit, which is its softening, must be positive and finite (PlanetSettings.RocheLimit)." ) ;
return ( strength , softening ) ;
}
/// <summary>
/// The tree of a zone's orbits. `packRadius` is where the global terms are zero: each carries its own potential there
/// as its Reference (a bounded term is already 0 beyond its outer edge, so it needs none).
/// </summary>
public static WellTree Build ( IReadOnlyList < WellNode > nodes , PlanetSettings settings , float packRadius )
{
var tree = new WellTree ( ) ;
var n = nodes . Count ;
tree . _key = new CultRecordKey [ n ] ;
tree . _distance = new float [ n ] ;
tree . _phase = new float [ n ] ;
2026-10-16 21:14:58 +02:00
tree . _extent = new float [ n ] ;
tree . _anchor = new float2 [ n ] ;
tree . _packRadius = packRadius ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
for ( var i = 0 ; i < n ; i + + )
{
2026-10-16 21:14:58 +02:00
tree . _extent [ i ] = nodes [ i ] . Extent ;
tree . _anchor [ i ] = nodes [ i ] . Anchor ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
tree . _index [ nodes [ i ] . Key ] = i ;
tree . _key [ i ] = nodes [ i ] . Key ;
tree . _distance [ i ] = nodes [ i ] . Distance ;
tree . _phase [ i ] = nodes [ i ] . Phase ;
}
var parent = new int [ n ] ;
var kids = new List < int > [ n ] ;
var depth = new int [ n ] ;
for ( var i = 0 ; i < n ; i + + ) kids [ i ] = new List < int > ( ) ;
for ( var i = 0 ; i < n ; i + + )
{
parent [ i ] = nodes [ i ] . Parent . IsSet ( ) & & tree . _index . TryGetValue ( nodes [ i ] . Parent , out var p ) ? p : - 1 ;
if ( parent [ i ] > = 0 ) kids [ parent [ i ] ] . Add ( i ) ;
}
for ( var i = 0 ; i < n ; i + + )
for ( var a = parent [ i ] ; a > = 0 ; a = parent [ a ] ) depth [ i ] + + ;
var roots = new List < int > ( ) ;
for ( var i = 0 ; i < n ; i + + )
if ( parent [ i ] < 0 ) roots . Add ( i ) ;
tree . _roots = roots . ToArray ( ) ;
var deepestFirst = new int [ n ] ;
for ( var i = 0 ; i < n ; i + + ) deepestFirst [ i ] = i ;
Array . Sort ( deepestFirst , ( x , y ) = > depth [ y ] ! = depth [ x ] ? depth [ y ] . CompareTo ( depth [ x ] ) : x . CompareTo ( y ) ) ;
var body = new float [ n ] ; // the body's own mass
var family = new float [ n ] ; // the family's mass M_F
for ( var i = 0 ; i < n ; i + + ) family [ i ] = body [ i ] = nodes [ i ] . Mass ;
foreach ( var i in deepestFirst )
if ( parent [ i ] > = 0 ) family [ parent [ i ] ] + = family [ i ] ;
2026-10-16 21:14:58 +02:00
// Massive roots are the zone's subzones; with two or more, their centre of mass is the zone node's position.
var massive = new List < int > ( ) ;
foreach ( var r in roots )
if ( family [ r ] > 0 ) massive . Add ( r ) ;
tree . _massiveRoots = massive . ToArray ( ) ;
var centre = float2 . zero ;
var total = 0f ;
foreach ( var r in massive )
{
centre + = tree . _anchor [ r ] * family [ r ] ;
total + = family [ r ] ;
}
tree . Centre = total > 0f ? centre / total : float2 . zero ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
tree . _kids = new int [ n ] [ ] ;
var ring = tree . _ring = new int [ n ] [ ] ;
for ( var i = 0 ; i < n ; i + + )
{
tree . _kids [ i ] = kids [ i ] . ToArray ( ) ;
ring [ i ] = RingOf ( kids [ i ] , family , tree . _distance ) ;
}
BuildReaches ( tree , parent , body , family , ring , deepestFirst , out var lower , out var group ) ;
for ( var i = 0 ; i < n ; i + + )
if ( body [ i ] > 0 ) ring [ i ] = null ; // a ring stands in for a body only on a bodyless node
tree . EmitTerms ( parent , body , family , ring , lower , group , settings , packRadius ) ;
return tree ;
}
// The massive children of a node at its innermost massive distance, within RingWindow of it; null when there are none.
private static int [ ] RingOf ( List < int > kids , float [ ] family , float [ ] distance )
{
var inner = float . PositiveInfinity ;
foreach ( var k in kids )
if ( family [ k ] > 0 ) inner = Math . Min ( inner , distance [ k ] ) ;
if ( float . IsPositiveInfinity ( inner ) ) return null ;
var members = new List < int > ( ) ;
foreach ( var k in kids )
if ( family [ k ] > 0 & & distance [ k ] - inner < = RingWindow ) members . Add ( k ) ;
return members . ToArray ( ) ;
}
// The massive children of a bodyless node that stand in for its body: their mass, the ring's distance and its support.
private readonly struct RingGroup
{
public readonly float Mass , Inner , Support ;
public RingGroup ( float mass , float inner , float support )
{
Mass = mass ;
Inner = inner ;
Support = support ;
}
}
2026-10-16 21:14:58 +02:00
// Children before parents: a family's support needs its children's reaches. lower = L_F, the outermost child's extent.
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
private static void BuildReaches ( WellTree t , int [ ] parent , float [ ] body , float [ ] family , int [ ] [ ] ring , int [ ] order ,
out float [ ] lower , out RingGroup [ ] group )
{
var n = parent . Length ;
var d = t . _distance ;
lower = new float [ n ] ;
group = new RingGroup [ n ] ;
t . _support = new float [ n ] ;
t . _reach = new float [ n ] ;
foreach ( var i in order )
{
var low = 0f ;
2026-10-16 21:14:58 +02:00
foreach ( var k in t . _kids [ i ] ) low = Math . Max ( low , t . _extent [ k ] ) ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
var support = GravityField . Beta * low ;
2026-10-16 21:14:58 +02:00
foreach ( var k in t . _kids [ i ] ) support = Math . Max ( support , t . _extent [ k ] + t . _reach [ k ] ) ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
if ( family [ i ] > 0 & & body [ i ] < = 0 )
{
// Bodyless: its ring group stands in for a body (it has massive children, or it would carry no mass).
var inner = float . PositiveInfinity ;
var mass = 0f ;
foreach ( var j in ring [ i ] )
{
inner = Math . Min ( inner , d [ j ] ) ;
mass + = family [ j ] ;
}
var ringSupport = GravityField . Beta * inner ;
foreach ( var j in ring [ i ] ) ringSupport = Math . Max ( ringSupport , d [ j ] + t . _reach [ j ] ) ;
low = Math . Max ( low , ringSupport ) ;
support = Math . Max ( support , GravityField . Beta * low ) ;
group [ i ] = new RingGroup ( mass , inner , ringSupport ) ;
}
lower [ i ] = low ;
t . _support [ i ] = support ;
if ( family [ i ] < = 0 ) continue ;
var floor = GravityField . Beta * support ;
var p = parent [ i ] ;
float reach ;
2026-10-16 21:14:58 +02:00
if ( p < 0 ) reach = t . SubzoneReach ( i , floor ) ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
else if ( body [ p ] < = 0 & & ring [ p ] ! = null & & ring [ p ] . Length > = 2 & & Array . IndexOf ( ring [ p ] , i ) > = 0 )
reach = Math . Max ( GravityField . RingReach * d [ i ] , floor ) ;
else reach = Math . Max ( GravityField . Reach * d [ i ] , floor ) ;
t . _reach [ i ] = reach > 0f ? reach : 0f ;
}
}
2026-10-17 04:55:48 +02:00
// A root's reach: the floor (Beta x support) or the subzone reach, whichever is more. The subzone shape is decided in the
// public static SubzoneReach (multi-root-isolated-subzones): the generator calls the same function, so Reach x the nearest
// root's distance is spelled once and the roots' windows are fitted to it (root-window-floor).
2026-10-16 21:14:58 +02:00
private float SubzoneReach ( int root , float floor )
{
2026-10-17 04:55:48 +02:00
var anchors = new List < float2 > ( ) ;
foreach ( var other in _massiveRoots ) anchors . Add ( _anchor [ other ] ) ;
return Math . Max ( SubzoneReach ( anchors , Array . IndexOf ( _massiveRoots , root ) ) , floor ) ;
}
/// <summary>
/// A subzone root's reach among the zone's massive roots' anchors: a sole massive root is the whole zone (+inf, so its
/// global term is the far field); with several, each root is bounded to GravityField.Reach x the distance to the nearest
/// other root (multi-root-isolated-subzones). The superposed-inside option is not a change to this return: it needs every
/// root's support, and roots are processed one at a time.
/// </summary>
public static float SubzoneReach ( IReadOnlyList < float2 > anchors , int root )
{
if ( anchors . Count < 2 ) return float . PositiveInfinity ;
2026-10-16 21:14:58 +02:00
var nearest = float . PositiveInfinity ;
2026-10-17 04:55:48 +02:00
for ( var other = 0 ; other < anchors . Count ; other + + )
if ( other ! = root ) nearest = Math . Min ( nearest , length ( anchors [ other ] - anchors [ root ] ) ) ;
return GravityField . Reach * nearest ;
2026-10-16 21:14:58 +02:00
}
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
private void EmitTerms ( int [ ] parent , float [ ] body , float [ ] family , int [ ] [ ] ring , float [ ] lower , RingGroup [ ] group ,
PlanetSettings settings , float packRadius )
{
var n = parent . Length ;
var terms = new List < Term > ( ) ;
_termStart = new int [ n ] ;
_termCount = new int [ n ] ;
for ( var i = 0 ; i < n ; i + + )
{
_termStart [ i ] = terms . Count ;
if ( family [ i ] < = 0 | | ! ( _reach [ i ] > 0f ) ) continue ;
var global = float . IsPositiveInfinity ( _reach [ i ] ) ;
var fadeFrom = global ? float . PositiveInfinity : Math . Max ( _support [ i ] , GravityField . Gamma * _reach [ i ] ) ;
void Add ( float strength , float softening , float inner , float outer ) = > terms . Add ( new Term ( _key [ i ] , strength , softening , inner ,
outer , float . IsPositiveInfinity ( outer ) ? strength * GravityField . Potential ( softening , inner , outer , packRadius ) : 0f ) ) ;
if ( body [ i ] > 0 & & family [ i ] < = body [ i ] )
{
var ( c , eps ) = Curves ( settings , body [ i ] ) ;
Add ( c , eps , fadeFrom , _reach [ i ] ) ;
_termCount [ i ] = terms . Count - _termStart [ i ] ;
continue ;
}
if ( body [ i ] > 0 )
{
var ( c , eps ) = Curves ( settings , body [ i ] ) ;
Add ( c , eps , lower [ i ] , _support [ i ] ) ;
}
else
{
var ( c , eps ) = Curves ( settings , group [ i ] . Mass ) ;
Add ( c , eps , lower [ i ] , _support [ i ] ) ;
Add ( - c , eps , group [ i ] . Inner , group [ i ] . Support ) ;
}
var ( cf , ef ) = Curves ( settings , family [ i ] ) ;
Add ( cf , ef , fadeFrom , _reach [ i ] ) ;
Add ( - cf , ef , lower [ i ] , _support [ i ] ) ;
_termCount [ i ] = terms . Count - _termStart [ i ] ;
}
2026-10-16 21:14:58 +02:00
_zoneStart = terms . Count ;
if ( _massiveRoots . Length > = 2 )
{
var total = 0f ;
var span = 0f ;
foreach ( var r in _massiveRoots )
{
total + = family [ r ] ;
span = Math . Max ( span , length ( _anchor [ r ] - Centre ) + _reach [ r ] ) ;
}
var ( c , eps ) = Curves ( settings , total ) ;
terms . Add ( new Term ( default , c , eps , float . PositiveInfinity , float . PositiveInfinity ,
c * GravityField . Potential ( eps , float . PositiveInfinity , float . PositiveInfinity , packRadius ) ) ) ;
if ( span > 0f ) terms . Add ( new Term ( default , - c , eps , span , GravityField . Beta * span , 0f ) ) ;
}
_zoneCount = terms . Count - _zoneStart ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
_terms = terms . ToArray ( ) ;
}
// One node's own terms at distance r from it, toward its centre.
2026-10-16 21:14:58 +02:00
private float PullOf ( int node , float r ) = > PullOf ( _termStart [ node ] , _termCount [ node ] , r ) ;
private float PullOf ( int start , int count , float r )
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
{
var pull = 0f ;
2026-10-16 21:14:58 +02:00
for ( var t = start ; t < start + count ; t + + )
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
{
var term = _terms [ t ] ;
pull + = term . Strength * GravityField . Pull ( term . Softening , term . Inner , term . Outer , r ) ;
}
return pull ;
}
/// <summary>
/// The field's force at p (toward the wells, u/s^2): every node's terms, walked from the roots; a subtree is skipped
/// the moment p is outside its reach, which is exact. `positionAt(orbit, time)` is the caller's, so this never owns time.
/// </summary>
public float2 Force ( float2 p , Func < CultRecordKey , double , float2 > positionAt , double time )
{
var force = float2 . zero ;
2026-10-16 21:14:58 +02:00
var toZone = Centre - p ;
var rz = length ( toZone ) ;
if ( _zoneCount > 0 & & rz > 0f ) force + = toZone / rz * PullOf ( _zoneStart , _zoneCount , rz ) ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
foreach ( var root in _roots ) ForceFrom ( root , p , positionAt , time , ref force ) ;
return force ;
}
private void ForceFrom ( int node , float2 p , Func < CultRecordKey , double , float2 > positionAt , double time , ref float2 force )
{
var offset = positionAt ( _key [ node ] , time ) - p ;
var r = length ( offset ) ;
if ( r > 0f )
force + = offset / r * PullOf ( node , r ) ;
foreach ( var kid in _kids [ node ] )
if ( length ( positionAt ( _key [ kid ] , time ) - p ) < _reach [ kid ] ) ForceFrom ( kid , p , positionAt , time , ref force ) ;
}
/// <summary>The field's potential at p: every node's terms less their references, walked as Force walks.</summary>
public float Potential ( float2 p , Func < CultRecordKey , double , float2 > positionAt , double time )
{
var sum = 0f ;
2026-10-16 21:14:58 +02:00
var rz = length ( Centre - p ) ;
for ( var t = _zoneStart ; t < _zoneStart + _zoneCount ; t + + )
sum + = _terms [ t ] . Strength * GravityField . Potential ( _terms [ t ] . Softening , _terms [ t ] . Inner , _terms [ t ] . Outer , rz ) - _terms [ t ] . Reference ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
foreach ( var root in _roots ) sum + = PotentialFrom ( root , p , positionAt , time ) ;
return sum ;
}
private float PotentialFrom ( int node , float2 p , Func < CultRecordKey , double , float2 > positionAt , double time )
{
var r = length ( positionAt ( _key [ node ] , time ) - p ) ;
var sum = 0f ;
for ( var t = _termStart [ node ] ; t < _termStart [ node ] + _termCount [ node ] ; t + + )
{
var term = _terms [ t ] ;
sum + = term . Strength * GravityField . Potential ( term . Softening , term . Inner , term . Outer , r ) - term . Reference ;
}
foreach ( var kid in _kids [ node ] )
if ( length ( positionAt ( _key [ kid ] , time ) - p ) < _reach [ kid ] ) sum + = PotentialFrom ( kid , p , positionAt , time ) ;
return sum ;
}
/// <summary>
/// The pull (toward the node, u/s^2) a rail at `distance` round `parentOrbit` is given: the node's own terms at that
/// distance. A ring member of a bodyless node adds each other member's own terms at their chord (positions at t = 0),
2026-10-17 04:55:48 +02:00
/// projected toward the node. Infinite period for a pull of 0 is the caller's.
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
/// </summary>
public float PullAround ( CultRecordKey parentOrbit , float distance , CultRecordKey self )
{
var node = _index [ parentOrbit ] ;
var pull = PullOf ( node , distance ) ;
var members = _ring [ node ] ;
if ( members = = null | | members . Length < 2 ) return pull ;
if ( _index . TryGetValue ( self , out var me ) & & Array . IndexOf ( members , me ) > = 0 ) return pull + PartnersPull ( members , me ) ;
2026-10-17 04:55:48 +02:00
return pull ;
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
}
// What the other members of a ring pull member m with, along the direction from m to the node.
private float PartnersPull ( int [ ] members , int m )
{
var at = _distance [ m ] * OrbitData . Evaluate ( _phase [ m ] ) ;
var sum = 0f ;
foreach ( var q in members )
{
if ( q = = m ) continue ;
var chord = _distance [ q ] * OrbitData . Evaluate ( _phase [ q ] ) - at ;
var r = length ( chord ) ;
if ( r < = 0f | | _distance [ m ] < = 0f ) continue ;
sum + = PullOf ( q , r ) * dot ( chord / r , - at / _distance [ m ] ) ;
}
return sum ;
}
}