using System;
using CultMath;
using static CultMath.math;
///
/// The law of one band term U(r; eps, a, b), the only primitive of a zone's gravity (WellTree decides which terms
/// exist). A Plummer-softened log source whose pull toward the centre is exact up to a and fades to nothing over [a, b]:
/// pull r / (r^2 + eps^2) * B(s), s = r^2, B = 1 for s <= a^2, 1 - S(t) on the band with t = (s - a^2) / (b^2 - a^2),
/// 0 for s >= b^2; S is the order-Order smoothstep. The potential is 0 beyond b. With a = b = +inf it is the global term
/// 1/2 ln(r^2 + eps^2). The potential is a function of r^2 (smooth at the centre), and C^Order on the circles r = a and
/// r = b (C4 at Order 4: the player feels higher-order seams), so the drawn height is exactly the sim's potential.
/// On the band the closed form is 1/2 [R ln((t + tau) / (1 + tau)) - (I(1) - I(t))], tau = (a^2 + eps^2) / (b^2 - a^2),
/// (1 - S(t)) = (t + tau) Q(t) + R, I the antiderivative of Q; for tau > 1 it cancels catastrophically in float32
/// (7e-3 at tau 3, 12 at tau 10), so the band is summed as a series in (1 - t) / (1 + tau), whose ratio is below 1/2.
/// Pull and Potential are per unit strength; the caller scales by the term's strength (WellStrength at a mass).
/// Gravity.cginc is generated from this file's constants and coefficients (GravityShader).
///
public static class GravityField
{
/// The smoothstep's order: the potential is C^Order on every band edge.
public const int Order = 4;
/// A family's charge is fully on by its support, Beta x its outermost child.
public const float Beta = 1.5f;
/// A family is felt whole out to max(support, Gamma x its reach) and fades from there to its reach.
public const float Gamma = .5f;
/// A body's reach in its parent's field, in its distance (the nominal reach before the fade's room).
public const float Reach = .5f;
/// A ring member of a bodyless node reaches RingReach x its distance: whole out to the ring's diameter.
public const float RingReach = 4f;
/// The band is summed as a series for tau above this. float32 error per unit strength against the double integral is at most 1.8e-6 anywhere (worst 1.7e-6 just above the seam, where the series has not reached float32 resolution); the closed form is float32-clean only to about 0.45.
public const float SeriesFrom = .4f;
/// The series branch's length: its ratio is below 1/1.4, so the truncated tail is below 1.4e-6 at the seam and falls geometrically above it.
public const int SeriesTerms = 40;
/// S(t) as polynomial coefficients in t, lowest first: t^Order sum_j C(Order-1+j, j) (1-t)^j.
public static readonly float[] S = BuildS();
/// 1 - S(t) as polynomial coefficients in t, lowest first: the band's profile B.
public static readonly float[] OneMinusS = BuildOneMinusS();
private static float[] BuildS()
{
var c = new double[2 * Order];
for (var j = 0; j < Order; j++)
{
var cj = Binomial(Order - 1 + j, j);
for (var i = 0; i <= j; i++)
c[Order + i] += cj * Binomial(j, i) * (i % 2 == 0 ? 1 : -1);
}
var s = new float[2 * Order];
for (var i = 0; i < s.Length; i++) s[i] = (float) c[i];
return s;
}
private static float[] BuildOneMinusS()
{
var f = new float[2 * Order];
for (var i = 0; i < f.Length; i++) f[i] = -S[i];
f[0] += 1f;
return f;
}
private static double Binomial(int n, int k)
{
var r = 1.0;
for (var i = 1; i <= k; i++) r = r * (n - k + i) / i;
return r;
}
// B(s) for the band term (a, b); b = +inf is no fade at all.
private static float Profile(float inner, float outer, float s)
{
if (float.IsPositiveInfinity(outer)) return 1f;
var a2 = inner * inner;
var b2 = outer * outer;
if (s <= a2) return 1f;
if (s >= b2) return 0f;
var t = (s - a2) / (b2 - a2);
var acc = 0f;
for (var i = OneMinusS.Length - 1; i >= 0; i--) acc = acc * t + OneMinusS[i];
return acc;
}
/// The pull toward the centre at distance r, per unit strength.
public static float Pull(float softening, float inner, float outer, float r)
{
var s = r * r;
return r * Profile(inner, outer, s) / (s + softening * softening);
}
/// The potential at distance r, per unit strength: 0 at and beyond outer, the global log when outer is +inf.
public static float Potential(float softening, float inner, float outer, float r)
{
var s = r * r;
var e2 = softening * softening;
if (float.IsPositiveInfinity(outer)) return .5f * log(s + e2);
var a2 = inner * inner;
var b2 = outer * outer;
if (s >= b2) return 0f;
var tau = (a2 + e2) / (b2 - a2);
if (s <= a2) return .5f * log((s + e2) / (a2 + e2)) + Band(tau, 0f);
return Band(tau, (s - a2) / (b2 - a2));
}
// The band's potential at t in [0, 1], 0 at t = 1.
private static float Band(float tau, float t) => tau <= SeriesFrom ? BandClosed(tau, t) : BandSeries(tau, t);
private static float BandClosed(float tau, float t)
{
// (1 - S(t)) = (t + tau) Q(t) + R by synthetic division; I is Q's antiderivative.
var n = OneMinusS.Length - 1;
Span q = stackalloc float[n];
q[n - 1] = OneMinusS[n];
for (var i = n - 1; i > 0; i--) q[i - 1] = OneMinusS[i] - tau * q[i];
var remainder = OneMinusS[0] - tau * q[0];
return .5f * (remainder * log((t + tau) / (1f + tau)) - (Antiderivative(q, 1f) - Antiderivative(q, t)));
}
private static float Antiderivative(ReadOnlySpan q, float t)
{
var acc = 0f;
for (var i = q.Length - 1; i >= 0; i--) acc = (acc + q[i] / (i + 1)) * t;
return acc;
}
private static float BandSeries(float tau, float t)
{
// With y = 1 - t, 1 - S(1 - y) = S(y) and 1 / (t + tau) = sum_k y^k / (1 + tau)^(k+1):
// band = -1/2 sum_k (1 + tau)^-(k+1) sum_j S_j y^(j+k+1) / (j+k+1).
var y = 1f - t;
Span yj = stackalloc float[S.Length];
yj[0] = 1f;
for (var j = 1; j < yj.Length; j++) yj[j] = yj[j - 1] * y;
var w = 1f / (1f + tau);
var wk = w;
var yk = y;
var acc = 0f;
for (var k = 0; k < SeriesTerms; k++)
{
var inner = 0f;
for (var j = Order; j < S.Length; j++) inner += S[j] * yj[j] / (j + k + 1);
acc += wk * yk * inner;
wk *= w;
yk *= y;
}
return -.5f * acc;
}
}