Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight, ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping against a co-moving ship at display rates above the step rate. Not yet built or run (Unity presenter files are verified by compile only on Starfire). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
09ec04a8d5
commit
b57207045d
14 changed files with 429 additions and 62 deletions
|
|
@ -3,16 +3,29 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using static CultMath.math;
|
||||
|
||||
// Pure presentation statics over sim state, never called by ServerShared, with no state of their own: the sim owns every
|
||||
// pose and no presenter stores one, a previous state or a velocity of its own. Ships are drawn where the frame falls Lead
|
||||
// sim seconds after the last step, by their last step's velocity and turn, and ride the field at that drawn xz (a ship's height is the field's there, not
|
||||
// the last step's). A ballistic round's flight is presentation
|
||||
// pose and no presenter stores one, a previous state or a velocity of its own.
|
||||
// The rule: a presenter draws every moving thing at the drawn time T = Zone.ExactTime + Clock.Lead. A pose the sim defines
|
||||
// in closed form of time (an orbit, a belt chunk, a well, the field's height, the flow scroll) is that function evaluated
|
||||
// at T; a pose the sim integrates (a ship, a kinematic body) is its last step plus its step velocity times Lead; a child
|
||||
// is drawn where its parent is. DrawAhead.Time is the one definition of T. Ships ride the field at that drawn xz and T (a
|
||||
// ship's height is the field's there, not the last step's). A ballistic round's flight is presentation
|
||||
// (ruling projectile-flight-is-presentation), drawn from the sim's facts: its frozen line, the published outcome, the
|
||||
// commit tick, the impact cell on the target's drawn pose, the burst point.
|
||||
public static class DrawAhead
|
||||
{
|
||||
// The drawn time: the one definition of T.
|
||||
public static double Time(Zone zone, float lead) => zone.ExactTime + lead;
|
||||
|
||||
// An orbit's node at the drawn time: the rail law at T, not the step's memo.
|
||||
public static float2 Orbit(Zone zone, CultRecordKey orbit, float lead) => zone.OrbitPositionAt(orbit, Time(zone, lead));
|
||||
|
||||
// The field's height at a point at the drawn time, its wells where their rails put them at T.
|
||||
public static float Height(Zone zone, float2 planar, float lead) => zone.GetHeight(planar, Time(zone, lead));
|
||||
|
||||
// A ship in a wormhole animation has its pose scripted by the step, not integrated from Velocity and TurnRate,
|
||||
// so it is drawn where the step put it: extrapolating a scripted pose draws a streak that resets every step.
|
||||
// A child is drawn where its parent is drawn, as the step copies the parent's pose onto it.
|
||||
|
|
@ -20,8 +33,11 @@ public static class DrawAhead
|
|||
{
|
||||
if (entity is Ship { WormholeAnimationInProgress: true }) return entity.Position;
|
||||
if (entity.Parent != null) return Position(entity.Parent, lead);
|
||||
var planar = entity.Position.xz + entity.Velocity * lead;
|
||||
return float3(planar.x, entity.RideHeight(planar), planar.y);
|
||||
// A body on a rail (a station) is on its orbit at T: the rail is closed form, not integrated.
|
||||
var planar = entity is OrbitalEntity { OrbitData: var orbit } && orbit.IsSet()
|
||||
? Orbit(entity.Zone, orbit, lead)
|
||||
: entity.Position.xz + entity.Velocity * lead;
|
||||
return float3(planar.x, entity.RideHeight(planar, Time(entity.Zone, lead)), planar.y);
|
||||
}
|
||||
|
||||
// A free body (a vapour cloud) drawn where the frame falls lead sim seconds after the last step, by the velocity
|
||||
|
|
|
|||
|
|
@ -1975,8 +1975,10 @@ public abstract class Entity
|
|||
}
|
||||
|
||||
// Where an unparented entity sits on the field at a planar point: the zone's height there plus the hull's grid offset.
|
||||
// The step puts the entity here at its own xz, and the presenter at its drawn xz (DrawAhead).
|
||||
public float RideHeight(float2 planar) => Zone.GetHeight(planar) + (ItemManager.GetData(Hull) as HullData).GridOffset;
|
||||
// The step rides at its own time and xz, the presenter at the drawn time and its drawn xz (DrawAhead).
|
||||
public float RideHeight(float2 planar, double time) => Zone.GetHeight(planar, time) + (ItemManager.GetData(Hull) as HullData).GridOffset;
|
||||
|
||||
public float RideHeight(float2 planar) => RideHeight(planar, Zone.ExactTime);
|
||||
|
||||
private void UpdateTemperature(float delta)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -507,31 +507,57 @@ public class Zone
|
|||
if (!_updatedOrbits.Contains(orbitID))
|
||||
{
|
||||
var orbit = Orbits[orbitID];
|
||||
float2 pos = float2.zero;
|
||||
// An infinite period (a parentless orbit, or one with no field to circle in) leaves the orbit at its phase:
|
||||
// frac(t / inf) is 0.
|
||||
if (orbit.Period > .01f)
|
||||
{
|
||||
var phase = (float) frac(_time / orbit.Period);
|
||||
pos = OrbitData.Evaluate(frac(phase + orbit.Data.Phase)) * orbit.Data.Distance;
|
||||
|
||||
if (float.IsNaN(pos.x))
|
||||
{
|
||||
//_context.Log("Orbit position is NaN, something went very wrong!");
|
||||
pos = float2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
var parentPosition = !Orbits[orbitID].Data.Parent.IsSet()
|
||||
? Orbits[orbitID].Data.FixedPosition :
|
||||
var parentPosition = !orbit.Data.Parent.IsSet()
|
||||
? orbit.Data.FixedPosition :
|
||||
GetOrbitPosition(orbit.Data.Parent.Key);
|
||||
Orbits[orbitID].Position = parentPosition + pos;
|
||||
orbit.Position = parentPosition + Offset(orbit, _time);
|
||||
_updatedOrbits.Add(orbitID);
|
||||
}
|
||||
|
||||
return Orbits[orbitID].Position;
|
||||
}
|
||||
|
||||
// The rail law, once: where an orbit sits from its parent's node at a time. An infinite period (a parentless orbit,
|
||||
// or one with no field to circle in) leaves the orbit at its phase: frac(t / inf) is 0.
|
||||
private static float2 Offset(Orbit orbit, double time)
|
||||
{
|
||||
var pos = float2.zero;
|
||||
if (orbit.Period > .01f)
|
||||
{
|
||||
var phase = (float) frac(time / orbit.Period);
|
||||
pos = OrbitData.Evaluate(frac(phase + orbit.Data.Phase)) * orbit.Data.Distance;
|
||||
|
||||
if (float.IsNaN(pos.x))
|
||||
pos = float2.zero;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
// The same rail law at any time, with no memo read or written: a parent is its own rail at that time.
|
||||
private float2 RailAt(CultRecordKey orbitID, double time)
|
||||
{
|
||||
if (!orbitID.IsSet() || !Orbits.TryGetValue(orbitID, out var orbit))
|
||||
return float2.zero;
|
||||
var parentPosition = !orbit.Data.Parent.IsSet() ? orbit.Data.FixedPosition : RailAt(orbit.Data.Parent.Key, time);
|
||||
return parentPosition + Offset(orbit, time);
|
||||
}
|
||||
|
||||
// Where an orbit is at a time: pure, writes nothing. At the step's own time it is the step's memo (GetOrbitPosition);
|
||||
// at any other time (a presenter's drawn time) it is the rail law evaluated there.
|
||||
public float2 OrbitPositionAt(CultRecordKey orbitID, double time)
|
||||
{
|
||||
if (!orbitID.IsSet())
|
||||
return float2.zero;
|
||||
if (!Orbits.ContainsKey(orbitID))
|
||||
{
|
||||
Log?.Invoke("Requested orbit is not part of this zone!");
|
||||
return float2.zero;
|
||||
}
|
||||
|
||||
return time == _time ? GetOrbitPosition(orbitID) : RailAt(orbitID, time);
|
||||
}
|
||||
|
||||
// orbit -> the well-bearing body on it, and orbit -> the orbits whose parent it is. Derived from the pack in the
|
||||
// constructor, read only by PeriodAround and Strength.
|
||||
private readonly Dictionary<CultRecordKey, Planet> _wellOn = new Dictionary<CultRecordKey, Planet>();
|
||||
|
|
@ -656,12 +682,15 @@ public class Zone
|
|||
|
||||
// Cut 1 (docs/mining-cut.md): a chunk's pose is a pure function of zone time, computed when asked. No stored
|
||||
// pose survives a tick; the sim and the renderer both read it fresh through here.
|
||||
public float4 ChunkPose(CultRecordKey belt, int index)
|
||||
public float4 ChunkPose(CultRecordKey belt, int index) => ChunkPose(belt, index, _time);
|
||||
|
||||
// The same pose at any time: the sim reads it at _time, a presenter at the drawn time (DrawAhead.Time).
|
||||
public float4 ChunkPose(CultRecordKey belt, int index, double time)
|
||||
{
|
||||
var beltData = Planets[belt] as AsteroidBeltData;
|
||||
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
||||
var parentPosition = OrbitPositionAt(Orbits[beltData.Orbit.Key].Data.Parent.Key, time);
|
||||
var size = ChunkRadius(new ChunkId(belt, index));
|
||||
return AsteroidBelts[belt].Pose(index, _time, parentPosition, size);
|
||||
return AsteroidBelts[belt].Pose(index, time, parentPosition, size);
|
||||
}
|
||||
|
||||
// Mining Cut 3 (docs/mining-cut-refresh.md): how bright a chunk is -- Reflector's rule (cross-section × the light
|
||||
|
|
@ -676,13 +705,13 @@ public class Zone
|
|||
return kind.CrossSection * PI * cells * cells * GetLight(ChunkPose(chunk.Field, chunk.Index).xy);
|
||||
}
|
||||
|
||||
public void EvaluateBelt(CultRecordKey belt, Span<float4> into)
|
||||
public void EvaluateBelt(CultRecordKey belt, Span<float4> into, double time)
|
||||
{
|
||||
var beltData = Planets[belt] as AsteroidBeltData;
|
||||
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
||||
var parentPosition = OrbitPositionAt(Orbits[beltData.Orbit.Key].Data.Parent.Key, time);
|
||||
var beltInstance = AsteroidBelts[belt];
|
||||
for (var i = 0; i < beltData.Asteroids.Length; i++)
|
||||
into[i] = beltInstance.Pose(i, _time, parentPosition, ChunkRadius(new ChunkId(belt, i)));
|
||||
into[i] = beltInstance.Pose(i, time, parentPosition, ChunkRadius(new ChunkId(belt, i)));
|
||||
}
|
||||
|
||||
public SecurityLevel GetSecurityLevel(float2 pos)
|
||||
|
|
@ -706,12 +735,16 @@ public class Zone
|
|||
// reference. GetHeight sums with it and the renderer's lowering (GravityLowering) hands it to the shader unchanged.
|
||||
public float WellReference(Planet body) => GravityField.Potential(body.WellStrength, body.RocheLimit, Pack.Radius);
|
||||
|
||||
public float GetHeight(float2 position)
|
||||
public float GetHeight(float2 position) => GetHeight(position, _time);
|
||||
|
||||
// The field at a time: each well sits where its body's rail puts it then. The sim reads it at _time, a presenter at
|
||||
// the drawn time, so a ship rides the same slope the drawn planet's well makes.
|
||||
public float GetHeight(float2 position, double time)
|
||||
{
|
||||
var result = 0f;
|
||||
foreach (var body in PlanetInstances.Values)
|
||||
{
|
||||
var offset = position - body.Orbit.Position;
|
||||
var offset = position - OrbitPositionAt(body.BodyData.Orbit.Key, time);
|
||||
result += GravityField.Potential(body.WellStrength, body.RocheLimit, length(offset)) - WellReference(body);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue