Outdoor lighting was ~32% too bright (washed-out, weak shading). Live cdb on retail (SmartBox::SetWorldAmbientLight + SkyDesc::GetLighting + LScape::sunlight, binary matches refs/acclient.pdb) pinned it: at the SAME game time + DayGroup, acdream's ambient COLOR matched retail exactly (the purple is correct, authored per-time-of-day in the sky dat) but the LEVEL was 0.607 vs retail's 0.459. level = AmbBright + 0.2·|sunVec|, both AmbBright=0.40, so acdream's |sunVec|≈1.06 vs retail's ≈0.30. Retail's LScape::sunlight read live = (0.2238, ~0, 0.00352), magnitude 0.224 = DirBright, y≈0. RetailSunVector had `y = cos(P)` (≈1) — the raw PRE-transform value SkyDesc:: GetLighting writes to arg5 (0x00500ac9), before LScape::set_sky_position's world transform. acdream ported the un-transformed vector, so the y=cos(P)≈1 term inflated |sunVec| to ~1.06. That magnitude feeds BOTH the ambient boost (SkyKeyframe.AmbientColor) AND the sun colour (SkyKeyframe.SunColor = DirColor×|sunVec|), over-brightening the whole scene (terrain, objects, sky) ~30% and also pointing the sun the wrong way. Fix: RetailSunVector = DirBright × (cos(P)·sin(H), cos(P)·cos(H), sin(P)) — the world-space spherical form LScape::sunlight actually holds; |sunVec| == DirBright for all H/P. After: acdream ambient (0.353,0.176,0.449) vs retail (0.360,0.180, 0.459) — within ~2%, user-confirmed "better outside". Sun direction also corrected (was pointing ~North from the bad y term). Tests updated to the cdb-verified values (the prior tests pinned the inflated magnitude). 18/18 sky tests green. reference-retail-ambient-values memory updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
467 lines
20 KiB
C#
467 lines
20 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
|
||
namespace AcDream.Core.World;
|
||
|
||
/// <summary>
|
||
/// Fog modes mirroring retail's <c>D3DFOGMODE</c>. Retail only ever uses
|
||
/// <see cref="Off"/> and <see cref="Linear"/>; the Exp variants are
|
||
/// supported by the dat schema but never appear in shipped data. See r12
|
||
/// §5 and <c>SkyTimeOfDay.WorldFog</c> (dat <c>uint</c>).
|
||
/// </summary>
|
||
public enum FogMode
|
||
{
|
||
Off = 0,
|
||
Linear = 1,
|
||
Exp = 2,
|
||
Exp2 = 3,
|
||
}
|
||
|
||
/// <summary>
|
||
/// One sky keyframe — the full lighting + fog state for a specific
|
||
/// day-fraction. Multiple keyframes across <c>[0, 1)</c> interpolate
|
||
/// linearly (with angular-shortest-arc wrap on sun direction) to produce
|
||
/// the current sky state.
|
||
///
|
||
/// <para>
|
||
/// Retail's <c>SkyTimeOfDay</c> dat struct carries this exact data plus
|
||
/// references to sky objects (sun mesh, moon mesh, cloud layer) which
|
||
/// belong to the renderer. This record exposes the shader-relevant
|
||
/// subset — sun direction, sun color, ambient color, linear fog. See
|
||
/// <c>references/DatReaderWriter/DatReaderWriter/Generated/Types/SkyTimeOfDay.generated.cs</c>
|
||
/// and r12 §4 + §5.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Colors are stored RAW (NOT pre-multiplied by brightness) in
|
||
/// <see cref="DirColor"/> / <see cref="AmbColor"/> with the brightness
|
||
/// scalars in <see cref="DirBright"/> / <see cref="AmbBright"/>. Retail's
|
||
/// <c>SkyDesc::GetLighting</c> at <c>0x00500ac9</c> (decomp lines
|
||
/// 261317-261331) lerps each channel separately and lerps brightness
|
||
/// separately, then multiplies post-lerp. Lerping the pre-multiplied
|
||
/// product gives mathematically different results when both color and
|
||
/// brightness change between adjacent keyframes — the cause of subtle
|
||
/// brightness discrepancies vs retail observed in dual-client
|
||
/// comparisons (Issue #3 visual sub-bug, 2026-04-27).
|
||
/// </para>
|
||
/// <para>
|
||
/// The computed properties <see cref="SunColor"/> and
|
||
/// <see cref="AmbientColor"/> return the post-multiplied product, so
|
||
/// downstream shader uniform plumbing (sky.vert / mesh.vert /
|
||
/// SceneLightingUbo) is unchanged.
|
||
/// </para>
|
||
/// </summary>
|
||
public readonly record struct SkyKeyframe(
|
||
float Begin, // [0, 1] day-fraction this keyframe kicks in
|
||
float SunHeadingDeg, // compass heading (0=N, 90=E, 180=S, 270=W)
|
||
float SunPitchDeg, // elevation above horizon (-90=below, +90=zenith)
|
||
Vector3 DirColor, // RGB linear, RAW (NOT × DirBright)
|
||
float DirBright, // sun brightness multiplier
|
||
Vector3 AmbColor, // RGB linear, RAW (NOT × AmbBright)
|
||
float AmbBright, // ambient brightness multiplier
|
||
Vector3 FogColor,
|
||
float FogDensity, // retained for tests; derive from FogStart/End
|
||
float FogStart = 80f, // meters (retail default ~120 clear, ~40 storm)
|
||
float FogEnd = 350f, // meters (retail default ~350 clear, ~150 storm)
|
||
FogMode FogMode = FogMode.Linear)
|
||
{
|
||
/// <summary>
|
||
/// Final directional sun color the shader feeds into N·L lighting.
|
||
/// Retail-faithful magnitude formula:
|
||
/// <code>SunColor = DirColor × |sunVec|</code>
|
||
/// where <c>sunVec</c> is retail's heading+pitch+brightness vector
|
||
/// (see <see cref="SkyStateProvider.RetailSunVector"/>).
|
||
///
|
||
/// <para>
|
||
/// <c>|sunVec|</c> is retail's <c>D3DLIGHT9.Diffuse = DirColor × sqrt(x²+y²+z²)</c>
|
||
/// scaling (<c>PrimD3DRender::UpdateLightsInternal</c> 0x0059b57c, decomp
|
||
/// 424118-424119) of the WORLD-space sun vector (<c>LScape::sunlight</c>).
|
||
/// Because <see cref="SkyStateProvider.RetailSunVector"/> is now the
|
||
/// DirBright-scaled spherical vector (magnitude == DirBright, cdb-verified —
|
||
/// see that method), <c>|sunVec| == DirBright</c>, so this is effectively
|
||
/// <c>SunColor = DirColor × DirBright</c>. (A prior bug used the un-transformed
|
||
/// y=cos(P) vector ⇒ |sunVec|≈1.06 ⇒ the sun was ~4–5× too bright at dawn/dusk;
|
||
/// [[reference-retail-ambient-values]].)
|
||
/// </para>
|
||
/// </summary>
|
||
public Vector3 SunColor => DirColor * SkyStateProvider.RetailSunVector(this).Length();
|
||
|
||
/// <summary>
|
||
/// Final ambient color the shader feeds into the per-vertex tint.
|
||
/// Retail-faithful magnitude formula:
|
||
/// <code>AmbientColor = AmbColor × (AmbBright + 0.2 × |sunVec|)</code>
|
||
/// matching <c>SmartBox::SetWorldAmbientLight</c> as called at
|
||
/// <c>0x0050560b</c> (decomp line 267117):
|
||
/// <code>SetWorldAmbientLight(sqrt(|sunVec|²) × 0.2 + ambient_level, ambient_color)</code>
|
||
/// Retail boosts the ambient brightness by 20% of the sun-vector
|
||
/// magnitude — i.e. ambient feels warmer when the sun is up, cooler
|
||
/// at night. acdream previously used <c>AmbBright</c> alone, which
|
||
/// is roughly 44% too dim mid-day ⇒ contributed to the blue-white
|
||
/// bias because the warm fill was missing.
|
||
/// </summary>
|
||
public Vector3 AmbientColor =>
|
||
AmbColor * (AmbBright + 0.2f * SkyStateProvider.RetailSunVector(this).Length());
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sky keyframe interpolator — given a day fraction in [0, 1), returns
|
||
/// the blended lighting state between the surrounding keyframes.
|
||
///
|
||
/// <para>
|
||
/// Math (r12 §4):
|
||
/// <list type="number">
|
||
/// <item><description>
|
||
/// Pick the two keyframes bracketing <c>t</c>: <c>k1</c> = last
|
||
/// keyframe with <c>Begin <= t</c>, <c>k2</c> = next keyframe
|
||
/// (wraps: if <c>k1</c> is last, <c>k2</c> is first).
|
||
/// </description></item>
|
||
/// <item><description>
|
||
/// Local blend <c>u = (t - k1.Begin) / (k2.Begin - k1.Begin)</c>
|
||
/// with wrap handling.
|
||
/// </description></item>
|
||
/// <item><description>
|
||
/// Lerp every vector component; use shortest-arc lerp for the sun
|
||
/// heading so k1=350° → k2=10° doesn't sweep backwards across the sky.
|
||
/// </description></item>
|
||
/// </list>
|
||
/// </para>
|
||
/// </summary>
|
||
public sealed class SkyStateProvider
|
||
{
|
||
private readonly List<SkyKeyframe> _keyframes;
|
||
|
||
public SkyStateProvider(IReadOnlyList<SkyKeyframe> keyframes)
|
||
{
|
||
if (keyframes is null || keyframes.Count == 0)
|
||
throw new ArgumentException("At least one keyframe required", nameof(keyframes));
|
||
// Sort by Begin so the walk is deterministic regardless of input order.
|
||
var sorted = new List<SkyKeyframe>(keyframes);
|
||
sorted.Sort((a, b) => a.Begin.CompareTo(b.Begin));
|
||
_keyframes = sorted;
|
||
}
|
||
|
||
public int KeyframeCount => _keyframes.Count;
|
||
public IReadOnlyList<SkyKeyframe> Keyframes => _keyframes;
|
||
|
||
/// <summary>
|
||
/// Default keyframe set based on retail observations — sunrise at 6am,
|
||
/// noon at 12pm, sunset at 6pm. Used when the dat-loaded set isn't
|
||
/// available yet or the player is in a region whose Region dat
|
||
/// doesn't override it.
|
||
///
|
||
/// <para>
|
||
/// Fog values approximate retail clear-weather defaults: ~80m..~350m
|
||
/// linear fog with color matching the horizon band so mountains at
|
||
/// distance fade into the sky instead of popping at the clip plane.
|
||
/// See r12 §5.1.
|
||
/// </para>
|
||
/// </summary>
|
||
public static SkyStateProvider Default()
|
||
{
|
||
// Day fractions: 0.0=midnight, 0.25=dawn, 0.5=noon, 0.75=dusk.
|
||
return new SkyStateProvider(new[]
|
||
{
|
||
// Default factory: brightness scalars are 1.0 here — the colors
|
||
// ARE the final intended values. Live Dereth keyframes loaded
|
||
// from the dat have separate non-1.0 DirBright/AmbBright values
|
||
// and the renderer multiplies them post-lerp.
|
||
new SkyKeyframe(
|
||
Begin: 0.0f,
|
||
SunHeadingDeg: 0f, // below horizon (north)
|
||
SunPitchDeg: -30f,
|
||
DirColor: new Vector3(0.02f, 0.02f, 0.08f), // deep blue
|
||
DirBright: 1.0f,
|
||
AmbColor: new Vector3(0.05f, 0.05f, 0.12f),
|
||
AmbBright: 1.0f,
|
||
FogColor: new Vector3(0.02f, 0.02f, 0.05f),
|
||
FogDensity: 0.004f,
|
||
FogStart: 30f,
|
||
FogEnd: 180f,
|
||
FogMode: FogMode.Linear),
|
||
new SkyKeyframe(
|
||
Begin: 0.25f,
|
||
SunHeadingDeg: 90f, // east at dawn
|
||
SunPitchDeg: 0f,
|
||
DirColor: new Vector3(1.0f, 0.7f, 0.4f), // sunrise warm
|
||
DirBright: 1.0f,
|
||
AmbColor: new Vector3(0.4f, 0.35f, 0.3f),
|
||
AmbBright: 1.0f,
|
||
FogColor: new Vector3(0.8f, 0.55f, 0.4f),
|
||
FogDensity: 0.002f,
|
||
FogStart: 60f,
|
||
FogEnd: 260f,
|
||
FogMode: FogMode.Linear),
|
||
new SkyKeyframe(
|
||
Begin: 0.5f,
|
||
SunHeadingDeg: 180f, // south at noon
|
||
SunPitchDeg: 70f,
|
||
DirColor: new Vector3(1.0f, 0.98f, 0.95f), // bright white-ish
|
||
DirBright: 1.0f,
|
||
AmbColor: new Vector3(0.5f, 0.5f, 0.55f),
|
||
AmbBright: 1.0f,
|
||
FogColor: new Vector3(0.7f, 0.75f, 0.85f),
|
||
FogDensity: 0.0008f,
|
||
FogStart: 120f,
|
||
FogEnd: 500f,
|
||
FogMode: FogMode.Linear),
|
||
new SkyKeyframe(
|
||
Begin: 0.75f,
|
||
SunHeadingDeg: 270f, // west at dusk
|
||
SunPitchDeg: 0f,
|
||
DirColor: new Vector3(0.95f, 0.4f, 0.25f), // sunset red
|
||
DirBright: 1.0f,
|
||
AmbColor: new Vector3(0.35f, 0.25f, 0.25f),
|
||
AmbBright: 1.0f,
|
||
FogColor: new Vector3(0.85f, 0.45f, 0.35f),
|
||
FogDensity: 0.002f,
|
||
FogStart: 60f,
|
||
FogEnd: 260f,
|
||
FogMode: FogMode.Linear),
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Current interpolated sky state at day fraction <paramref name="t"/>.
|
||
/// Wraps correctly across the day boundary (1.0 → 0.0).
|
||
/// </summary>
|
||
public SkyKeyframe Interpolate(float t)
|
||
{
|
||
t = (float)(t - Math.Floor(t)); // wrap to [0, 1)
|
||
|
||
// Find k1: last keyframe with Begin <= t.
|
||
int k1Index = _keyframes.Count - 1;
|
||
for (int i = 0; i < _keyframes.Count; i++)
|
||
{
|
||
if (_keyframes[i].Begin <= t)
|
||
k1Index = i;
|
||
else
|
||
break;
|
||
}
|
||
int k2Index = (k1Index + 1) % _keyframes.Count;
|
||
|
||
var k1 = _keyframes[k1Index];
|
||
var k2 = _keyframes[k2Index];
|
||
|
||
// Compute blend weight, handling wrap (k1 is last, k2 is first).
|
||
float k1Begin = k1.Begin;
|
||
float k2Begin = k2.Begin;
|
||
if (k2Begin <= k1Begin) k2Begin += 1.0f; // unroll wrap
|
||
float tWrapped = t;
|
||
if (tWrapped < k1Begin) tWrapped += 1.0f;
|
||
|
||
float span = Math.Max(1e-6f, k2Begin - k1Begin);
|
||
float u = (tWrapped - k1Begin) / span;
|
||
u = Math.Clamp(u, 0f, 1f);
|
||
|
||
// Angular lerp for sun heading: pick shortest arc.
|
||
float heading = ShortestAngleLerp(k1.SunHeadingDeg, k2.SunHeadingDeg, u);
|
||
|
||
// Retail-faithful interpolation: lerp DirColor / DirBright /
|
||
// AmbColor / AmbBright as SEPARATE CHANNELS, not as the
|
||
// pre-multiplied product. Mirrors SkyDesc::GetLighting at
|
||
// 0x00500ac9 (decomp lines 261317-261331). The post-multiplied
|
||
// SunColor / AmbientColor are computed properties on the result.
|
||
// Fog mode doesn't interpolate — pick k1's mode (retail uses
|
||
// Linear everywhere).
|
||
return new SkyKeyframe(
|
||
Begin: t,
|
||
SunHeadingDeg: heading,
|
||
SunPitchDeg: Lerp(k1.SunPitchDeg, k2.SunPitchDeg, u),
|
||
DirColor: Vector3.Lerp(k1.DirColor, k2.DirColor, u),
|
||
DirBright: Lerp(k1.DirBright, k2.DirBright, u),
|
||
AmbColor: Vector3.Lerp(k1.AmbColor, k2.AmbColor, u),
|
||
AmbBright: Lerp(k1.AmbBright, k2.AmbBright, u),
|
||
FogColor: Vector3.Lerp(k1.FogColor, k2.FogColor, u),
|
||
FogDensity: Lerp(k1.FogDensity, k2.FogDensity, u),
|
||
FogStart: Lerp(k1.FogStart, k2.FogStart, u),
|
||
FogEnd: Lerp(k1.FogEnd, k2.FogEnd, u),
|
||
FogMode: k1.FogMode);
|
||
}
|
||
|
||
private static float Lerp(float a, float b, float u) => a + (b - a) * u;
|
||
|
||
/// <summary>
|
||
/// Shortest-arc heading lerp: r12 §4. If <c>a=350</c> and <c>b=10</c>
|
||
/// the lerp walks 20° forward through 0° rather than 340° backward.
|
||
/// </summary>
|
||
public static float ShortestAngleLerp(float aDeg, float bDeg, float u)
|
||
{
|
||
float delta = bDeg - aDeg;
|
||
while (delta > 180f) delta -= 360f;
|
||
while (delta < -180f) delta += 360f;
|
||
return aDeg + delta * u;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retail's world-space sun vector (NOT normalized): the standard
|
||
/// spherical-to-cartesian direction (East=x, North=y, Up=z) scaled by
|
||
/// <c>DirBright</c>:
|
||
/// <code>
|
||
/// sunVec.x = DirBright × cos(P) × sin(H)
|
||
/// sunVec.y = DirBright × cos(P) × cos(H)
|
||
/// sunVec.z = DirBright × sin(P)
|
||
/// </code>
|
||
/// so <c>|sunVec| == DirBright</c> exactly (cos²P·(sin²H+cos²H)+sin²P = 1).
|
||
///
|
||
/// <para>
|
||
/// GROUNDED IN A LIVE cdb CAPTURE (2026-06-18, [[reference-retail-ambient-values]]):
|
||
/// retail's <c>LScape::sunlight</c> read at a dawn keyframe (H=90°, P=0.9°,
|
||
/// DirBright≈0.224) = <c>(0.2238, ~0, 0.00352)</c> — y≈0, magnitude 0.224 =
|
||
/// DirBright. That fed <c>level = 0.2·|sunlight| + ambient_level = 0.2·0.224 +
|
||
/// 0.40 = 0.445</c>, matching the captured <c>SetWorldAmbientLight</c> level.
|
||
/// </para>
|
||
/// <para>
|
||
/// PRIOR BUG: an earlier version returned <c>y = cos(P)</c> (≈1) — the raw
|
||
/// PRE-transform value the decomp's <c>SkyDesc::GetLighting</c> writes to its
|
||
/// <c>arg5</c> (0x00500ac9, before <c>LScape::set_sky_position</c>'s world
|
||
/// transform). Porting that un-transformed vector inflated <c>|sunVec|</c> to
|
||
/// ~1.06 instead of ~0.22, over-brightening BOTH the ambient boost
|
||
/// (<see cref="SkyKeyframe.AmbientColor"/>) AND the sun colour
|
||
/// (<see cref="SkyKeyframe.SunColor"/>) by ~30% vs retail. The world-space
|
||
/// form above is what <c>LScape::sunlight</c> actually holds at runtime.
|
||
/// </para>
|
||
/// The shader uses the NORMALIZED vector for N·L; the magnitude (= DirBright)
|
||
/// feeds the sun-colour intensity and the ambient brightness boost.
|
||
/// </summary>
|
||
public static Vector3 RetailSunVector(SkyKeyframe kf)
|
||
{
|
||
float h = kf.SunHeadingDeg * (MathF.PI / 180f);
|
||
float p = kf.SunPitchDeg * (MathF.PI / 180f);
|
||
float cosP = MathF.Cos(p);
|
||
float sinP = MathF.Sin(p);
|
||
float B = kf.DirBright;
|
||
return new Vector3(
|
||
B * cosP * MathF.Sin(h), // x = DirBright × cos(P) × sin(H)
|
||
B * cosP * MathF.Cos(h), // y = DirBright × cos(P) × cos(H)
|
||
B * sinP); // z = DirBright × sin(P)
|
||
}
|
||
|
||
/// <summary>
|
||
/// World-space sun direction unit vector pointing FROM the surface
|
||
/// TOWARDS the sun, derived from <see cref="RetailSunVector"/> and
|
||
/// normalized. The shader sunDir uniform should use this directly
|
||
/// (or -this if the lighting math wants the L-vector pointing AT the
|
||
/// surface). The previous implementation used standard spherical
|
||
/// coordinates (sin(H)cos(P), cos(H)cos(P), sin(P)) which didn't match
|
||
/// retail's deliberate Y-decoupled-from-heading convention. Switching
|
||
/// to the retail vector subtly tilts the lighting on objects but
|
||
/// matches retail's screenshots when both clients view the same scene.
|
||
/// </summary>
|
||
public static Vector3 SunDirectionFromKeyframe(SkyKeyframe kf)
|
||
{
|
||
var v = RetailSunVector(kf);
|
||
float len = v.Length();
|
||
return len > 1e-6f ? v / len : Vector3.UnitZ;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Service that turns server-delivered tick counts into live sky state.
|
||
/// Owns the "current time" clock (seeded from server sync, advanced by
|
||
/// real-time elapsed between syncs).
|
||
///
|
||
/// <para>
|
||
/// Supports a debug "time override" (slash-command <c>/time 0.5</c>) that
|
||
/// forces a specific day fraction regardless of server sync — used for
|
||
/// screenshots and visual debugging. The override is transient and gets
|
||
/// cleared on the next TimeSync packet.
|
||
/// </para>
|
||
/// </summary>
|
||
public sealed class WorldTimeService
|
||
{
|
||
private SkyStateProvider _sky;
|
||
private double _lastSyncedTicks;
|
||
private DateTime _lastSyncedWallClockUtc = DateTime.UtcNow;
|
||
|
||
private float? _debugDayFractionOverride;
|
||
|
||
/// <summary>
|
||
/// Rate at which in-game time advances relative to real time. Retail
|
||
/// default is 1.0 (one wall-clock second = one in-game tick). Server
|
||
/// config can override via <c>SkyDesc.TickSize</c>; see r12 §1.2.
|
||
/// </summary>
|
||
public double TickSize { get; set; } = 1.0;
|
||
|
||
public WorldTimeService(SkyStateProvider sky)
|
||
{
|
||
_sky = sky ?? throw new ArgumentNullException(nameof(sky));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Hot-swap the keyframe source — typically called once at world-load
|
||
/// time after the Region dat has been parsed by <see cref="SkyDescLoader"/>.
|
||
/// </summary>
|
||
public void SetProvider(SkyStateProvider sky)
|
||
{
|
||
_sky = sky ?? throw new ArgumentNullException(nameof(sky));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Set the authoritative tick count from a server TimeSync packet.
|
||
/// Clears any debug override.
|
||
/// </summary>
|
||
public void SyncFromServer(double serverTicks)
|
||
{
|
||
_lastSyncedTicks = serverTicks;
|
||
_lastSyncedWallClockUtc = System.DateTime.UtcNow;
|
||
_debugDayFractionOverride = null;
|
||
|
||
if (System.Environment.GetEnvironmentVariable("ACDREAM_DUMP_SKY") == "1")
|
||
{
|
||
var df = DerethDateTime.DayFraction(serverTicks);
|
||
var cal = DerethDateTime.ToCalendar(serverTicks);
|
||
System.Console.WriteLine(
|
||
$"[sky-dump] SyncFromServer: ticks={serverTicks:F1} dayFraction={df:F4} " +
|
||
$"calendar=PY{cal.Year} {cal.Month} {cal.Day} {cal.Hour}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Debug-only: force a specific day fraction in [0, 1). Overrides
|
||
/// server-synced time until cleared by <see cref="SyncFromServer"/>
|
||
/// or <see cref="ClearDebugTime"/>.
|
||
/// </summary>
|
||
public void SetDebugTime(float dayFraction)
|
||
{
|
||
_debugDayFractionOverride = dayFraction;
|
||
}
|
||
|
||
public void ClearDebugTime() => _debugDayFractionOverride = null;
|
||
|
||
/// <summary>
|
||
/// Current ticks at <see cref="DateTime.UtcNow"/>, advanced from the
|
||
/// last sync by real-time elapsed seconds times <see cref="TickSize"/>.
|
||
/// </summary>
|
||
public double NowTicks
|
||
{
|
||
get
|
||
{
|
||
double elapsed = (DateTime.UtcNow - _lastSyncedWallClockUtc).TotalSeconds;
|
||
return _lastSyncedTicks + elapsed * TickSize;
|
||
}
|
||
}
|
||
|
||
/// <summary>Current day fraction in [0, 1).</summary>
|
||
public double DayFraction
|
||
{
|
||
get
|
||
{
|
||
if (_debugDayFractionOverride.HasValue)
|
||
return _debugDayFractionOverride.Value;
|
||
return DerethDateTime.DayFraction(NowTicks);
|
||
}
|
||
}
|
||
|
||
/// <summary>Current sky lighting state.</summary>
|
||
public SkyKeyframe CurrentSky => _sky.Interpolate((float)DayFraction);
|
||
|
||
/// <summary>Convenience: current sun direction from derived sky state.</summary>
|
||
public Vector3 CurrentSunDirection =>
|
||
SkyStateProvider.SunDirectionFromKeyframe(CurrentSky);
|
||
|
||
public DerethDateTime.Calendar CurrentCalendar =>
|
||
DerethDateTime.ToCalendar(NowTicks);
|
||
|
||
public bool IsDaytime => DerethDateTime.IsDaytime(NowTicks);
|
||
}
|