Well law fix batch: zone node for several massive roots, belt extent, series from tau 0.4, Unity parity on the multi-root fixture
One zone node at the massive roots' centre of mass owns the only global term (SubzoneReach is the one place the subzone shape is decided: isolated-subzones). WellNode gains Extent and Anchor; a belt orbit's extent is its outermost asteroid. GravityField.SeriesFrom = 0.4 is emitted into the shader template and Gravity.cginc. Tests: the spec's WellLawTests; the old seam test is replaced by TheBandPotentialIsExactInSinglePrecision. GravityFieldParityVerify runs the multi-root fixture (second sun, moon inside the planet's Roche limit, far window): PASS, largest drawn tau 1.228, max error 0.01028 u. The dotnet suite has not been run at this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4fb5cb9466
commit
250b61be2c
9 changed files with 412 additions and 60 deletions
|
|
@ -5,19 +5,26 @@ using CultMath;
|
|||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
|
||||
/// <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).</summary>
|
||||
/// <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>
|
||||
public readonly struct WellNode
|
||||
{
|
||||
public readonly CultRecordKey Key, Parent;
|
||||
public readonly float Distance, Phase, Mass;
|
||||
public readonly float Distance, Phase, Mass, Extent;
|
||||
public readonly float2 Anchor;
|
||||
|
||||
public WellNode(CultRecordKey key, CultRecordKey parent, float distance, float phase, float mass)
|
||||
public WellNode(CultRecordKey key, CultRecordKey parent, float distance, float phase, float mass, float extent, float2 anchor)
|
||||
{
|
||||
Key = key;
|
||||
Parent = parent;
|
||||
Distance = distance;
|
||||
Phase = phase;
|
||||
Mass = mass;
|
||||
Extent = extent;
|
||||
Anchor = anchor;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -28,9 +35,12 @@ public readonly struct WellNode
|
|||
/// 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
|
||||
/// child's distance: inside it the node pulls exactly as its body alone. B_F, the support, bounds every descendant term:
|
||||
/// child's extent: inside it the node pulls exactly as its body alone. B_F, the support, bounds every descendant term:
|
||||
/// 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
|
||||
/// Lam_F. A root's reach is infinite, so the zone's far field is exactly c(M_zone) however the generator split it.
|
||||
/// 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).
|
||||
/// 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>
|
||||
|
|
@ -62,15 +72,22 @@ public sealed class WellTree
|
|||
private int[] _roots;
|
||||
private int[][] _ring;
|
||||
private int[] _termStart, _termCount;
|
||||
private float[] _distance, _phase, _reach, _support;
|
||||
private float[] _distance, _phase, _reach, _support, _extent;
|
||||
private float2[] _anchor;
|
||||
private int[] _massiveRoots;
|
||||
private float _packRadius;
|
||||
private int _zoneStart, _zoneCount;
|
||||
private Term[] _terms;
|
||||
|
||||
/// <summary>Every term of the zone, grouped by node: what the lowering uploads and the shader sums.</summary>
|
||||
/// <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>
|
||||
public IReadOnlyList<Term> Terms => _terms;
|
||||
|
||||
private WellTree() { }
|
||||
|
||||
/// <summary>A node's reach in its parent's field (+inf for a root); 0 for an orbit that carries no mass.</summary>
|
||||
/// <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>
|
||||
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>
|
||||
|
|
@ -102,8 +119,13 @@ public sealed class WellTree
|
|||
tree._key = new CultRecordKey[n];
|
||||
tree._distance = new float[n];
|
||||
tree._phase = new float[n];
|
||||
tree._extent = new float[n];
|
||||
tree._anchor = new float2[n];
|
||||
tree._packRadius = packRadius;
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
tree._extent[i] = nodes[i].Extent;
|
||||
tree._anchor[i] = nodes[i].Anchor;
|
||||
tree._index[nodes[i].Key] = i;
|
||||
tree._key[i] = nodes[i].Key;
|
||||
tree._distance[i] = nodes[i].Distance;
|
||||
|
|
@ -135,6 +157,20 @@ public sealed class WellTree
|
|||
foreach (var i in deepestFirst)
|
||||
if (parent[i] >= 0) family[parent[i]] += family[i];
|
||||
|
||||
// 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;
|
||||
|
||||
tree._kids = new int[n][];
|
||||
var ring = tree._ring = new int[n][];
|
||||
for (var i = 0; i < n; i++)
|
||||
|
|
@ -175,7 +211,7 @@ public sealed class WellTree
|
|||
}
|
||||
}
|
||||
|
||||
// Children before parents: a family's support needs its children's reaches. lower = L_F, the outermost child's distance.
|
||||
// Children before parents: a family's support needs its children's reaches. lower = L_F, the outermost child's extent.
|
||||
private static void BuildReaches(WellTree t, int[] parent, float[] body, float[] family, int[][] ring, int[] order,
|
||||
out float[] lower, out RingGroup[] group)
|
||||
{
|
||||
|
|
@ -188,9 +224,9 @@ public sealed class WellTree
|
|||
foreach (var i in order)
|
||||
{
|
||||
var low = 0f;
|
||||
foreach (var k in t._kids[i]) low = Math.Max(low, d[k]);
|
||||
foreach (var k in t._kids[i]) low = Math.Max(low, t._extent[k]);
|
||||
var support = GravityField.Beta * low;
|
||||
foreach (var k in t._kids[i]) support = Math.Max(support, d[k] + t._reach[k]);
|
||||
foreach (var k in t._kids[i]) support = Math.Max(support, t._extent[k] + t._reach[k]);
|
||||
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).
|
||||
|
|
@ -213,7 +249,7 @@ public sealed class WellTree
|
|||
var floor = GravityField.Beta * support;
|
||||
var p = parent[i];
|
||||
float reach;
|
||||
if (p < 0) reach = float.PositiveInfinity;
|
||||
if (p < 0) reach = t.SubzoneReach(i, floor);
|
||||
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);
|
||||
|
|
@ -221,6 +257,19 @@ public sealed class WellTree
|
|||
}
|
||||
}
|
||||
|
||||
// A root's reach. The one place the subzone shape is decided (question multi-root-zone-shape): a sole massive root is the
|
||||
// whole zone (+inf, so its global term is the far field); with several, isolated-subzones bounds each root to Reach x the
|
||||
// distance to the nearest other massive root, or its floor (Beta x support) if more. The superposed-inside option is
|
||||
// this one return: Beta x max(pack radius, max over massive roots of |anchor - Centre| + support).
|
||||
private float SubzoneReach(int root, float floor)
|
||||
{
|
||||
if (_massiveRoots.Length < 2) return float.PositiveInfinity;
|
||||
var nearest = float.PositiveInfinity;
|
||||
foreach (var other in _massiveRoots)
|
||||
if (other != root) nearest = Math.Min(nearest, length(_anchor[other] - _anchor[root]));
|
||||
return Math.Max(GravityField.Reach * nearest, floor);
|
||||
}
|
||||
|
||||
private void EmitTerms(int[] parent, float[] body, float[] family, int[][] ring, float[] lower, RingGroup[] group,
|
||||
PlanetSettings settings, float packRadius)
|
||||
{
|
||||
|
|
@ -259,14 +308,32 @@ public sealed class WellTree
|
|||
Add(-cf, ef, lower[i], _support[i]);
|
||||
_termCount[i] = terms.Count - _termStart[i];
|
||||
}
|
||||
_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;
|
||||
_terms = terms.ToArray();
|
||||
}
|
||||
|
||||
// One node's own terms at distance r from it, toward its centre.
|
||||
private float PullOf(int node, float r)
|
||||
private float PullOf(int node, float r) => PullOf(_termStart[node], _termCount[node], r);
|
||||
|
||||
private float PullOf(int start, int count, float r)
|
||||
{
|
||||
var pull = 0f;
|
||||
for (var t = _termStart[node]; t < _termStart[node] + _termCount[node]; t++)
|
||||
for (var t = start; t < start + count; t++)
|
||||
{
|
||||
var term = _terms[t];
|
||||
pull += term.Strength * GravityField.Pull(term.Softening, term.Inner, term.Outer, r);
|
||||
|
|
@ -281,6 +348,9 @@ public sealed class WellTree
|
|||
public float2 Force(float2 p, Func<CultRecordKey, double, float2> positionAt, double time)
|
||||
{
|
||||
var force = float2.zero;
|
||||
var toZone = Centre - p;
|
||||
var rz = length(toZone);
|
||||
if (_zoneCount > 0 && rz > 0f) force += toZone / rz * PullOf(_zoneStart, _zoneCount, rz);
|
||||
foreach (var root in _roots) ForceFrom(root, p, positionAt, time, ref force);
|
||||
return force;
|
||||
}
|
||||
|
|
@ -299,6 +369,9 @@ public sealed class WellTree
|
|||
public float Potential(float2 p, Func<CultRecordKey, double, float2> positionAt, double time)
|
||||
{
|
||||
var sum = 0f;
|
||||
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;
|
||||
foreach (var root in _roots) sum += PotentialFrom(root, p, positionAt, time);
|
||||
return sum;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue