Merge remote-tracking branch 'origin/master' into eureka/aetheria-release-sensor-catalog-r2
This commit is contained in:
commit
7633408fee
18 changed files with 1122 additions and 31 deletions
302
Assets/Scripts/Editor/FlowFieldParityVerify.cs
Normal file
302
Assets/Scripts/Editor/FlowFieldParityVerify.cs
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using GameCult.Caching;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Aetheria.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Batchmode check for flow-shader-parity (campaign aetheria-fauna, cut fauna-flow-field): the HLSL globalFlow and
|
||||
/// slopeFlow of Assets/Shaders/Volumetric.cginc give the values of FlowField (ServerShared) for the same position,
|
||||
/// scroll, settings and gradient. It drives the real lowering: a Zone is stepped, VolumeSampling.LowerFlow sets the
|
||||
/// shader globals from the zone's Settings.Flow and FlowScroll, FlowParity.compute reads those globals, and the
|
||||
/// results are compared with FlowField. It also checks that Settings.asset carries the authored flow under
|
||||
/// PlanetSettings and that the shader got the zone's scroll (speed times zone time).
|
||||
///
|
||||
/// Dispatches a compute kernel, so it needs a graphics device: run WITHOUT -nographics (as ShieldPanelCut3Verify).
|
||||
/// Unity -batchmode -projectPath <repo> -executeMethod Aetheria.EditorTools.FlowFieldParityVerify.Run -logFile <path>
|
||||
/// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure.
|
||||
/// </summary>
|
||||
public static class FlowFieldParityVerify
|
||||
{
|
||||
const string Tag = "[FlowFieldParityVerify]";
|
||||
// Measured disagreement between the HLSL and the C# on this tree (float32 on both sides, positions to 10000,
|
||||
// scrolls to 100, through the keyword-gated flow()): max 2.9e-5 of amplitude (the "max error" line this run
|
||||
// prints). The tolerance is 5x that noise, so constant errors of 0.05% (y factor .501, swirl or amplitude
|
||||
// x1.0005: 4e-4 to 8e-4) fail; a 0.01% error is at the noise floor and unresolvable.
|
||||
const float RelativeTolerance = 1.5e-4f;
|
||||
static float _maxError;
|
||||
static bool _ok = true;
|
||||
|
||||
static void OnLog(string message, string stack, LogType type)
|
||||
{
|
||||
if ((type == LogType.Error || type == LogType.Exception || type == LogType.Assert) && !message.StartsWith(Tag))
|
||||
Fail("logged-error", message.Split('\n')[0]);
|
||||
}
|
||||
|
||||
static void Fail(string check, string detail)
|
||||
{
|
||||
Debug.LogError($"{Tag} FAIL {check}: {detail}");
|
||||
_ok = false;
|
||||
}
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null)
|
||||
{
|
||||
Debug.LogError($"{Tag} FAIL: no graphics device in this session; rerun batchmode without -nographics.");
|
||||
EditorApplication.Exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// The flow's error lines (a shader keyword that does not exist, a missing property) fail the check.
|
||||
Application.logMessageReceived += OnLog;
|
||||
var work = Path.Combine(Path.GetTempPath(), "flow-parity-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(work);
|
||||
try
|
||||
{
|
||||
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")
|
||||
?? throw new InvalidOperationException("No Settings.asset.");
|
||||
var compute = AssetDatabase.LoadAssetAtPath<ComputeShader>("Assets/Shaders/Compute/FlowParity.compute")
|
||||
?? throw new InvalidOperationException("No FlowParity.compute.");
|
||||
|
||||
CheckAuthoredFlow(settings.PlanetSettings.Flow);
|
||||
|
||||
// The authored flow (global term only: its slope amplitudes are authored zero), then a flow with every
|
||||
// term live and a scroll speed that reaches scrolls up to 100.
|
||||
var authored = settings.PlanetSettings.Flow;
|
||||
CheckParity("authored", settings, work, compute, authored, new[] { 0.0, 3.1, 40.0, 100.0 });
|
||||
var wide = new FlowSettings
|
||||
{
|
||||
GlobalScale = authored.GlobalScale, GlobalAmplitude = authored.GlobalAmplitude, Period = authored.Period,
|
||||
GlobalScrollSpeed = 1f, SlopeAmplitude = 3f, SwirlAmplitude = 5f
|
||||
};
|
||||
CheckParity("wide", settings, work, compute, wide, new[] { 0.0, 1.0, 17.0, 60.0, 100.0 });
|
||||
CheckNoiseClock(compute);
|
||||
|
||||
Debug.Log($"{Tag} max error {_maxError:G4} of amplitude (tolerance {RelativeTolerance:G4})");
|
||||
Debug.Log(_ok ? $"{Tag} PASS" : $"{Tag} FAIL: see errors above");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"{Tag} FAIL exception: {e}");
|
||||
_ok = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Application.logMessageReceived -= OnLog;
|
||||
try { Directory.Delete(work, true); } catch { }
|
||||
}
|
||||
EditorApplication.Exit(_ok ? 0 : 1);
|
||||
}
|
||||
|
||||
// Settings.asset carries the authored flow under PlanetSettings (it moved from DefaultEnvironment unchanged).
|
||||
static void CheckAuthoredFlow(FlowSettings flow)
|
||||
{
|
||||
if (flow == null) { Fail("authored-flow", "PlanetSettings.Flow is null"); return; }
|
||||
if (flow.GlobalScale != 512f || flow.GlobalAmplitude != 15f || flow.GlobalScrollSpeed != .025f ||
|
||||
flow.Period != 8f || flow.SlopeAmplitude != 0f || flow.SwirlAmplitude != 0f)
|
||||
Fail("authored-flow", $"PlanetSettings.Flow is {flow.GlobalScale}/{flow.GlobalAmplitude}/{flow.GlobalScrollSpeed}/" +
|
||||
$"{flow.Period}/{flow.SlopeAmplitude}/{flow.SwirlAmplitude}, authored 512/15/0.025/8/0/0");
|
||||
}
|
||||
|
||||
internal static Zone BuildZone(GameSettings settings, string work, FlowSettings flow, double time = 0)
|
||||
{
|
||||
var cache = AetheriaStores.Open(Path.Combine(work, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true);
|
||||
cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
cache.FlushAsync().Wait();
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), settings.GameplaySettings, _ => { });
|
||||
return new Zone(items, new PlanetSettings { Flow = flow }, new ZonePack { Time = time }, new GalaxyZone { Name = "Parity", Owner = null }, null);
|
||||
}
|
||||
|
||||
static void CheckParity(string label, GameSettings settings, string work, ComputeShader compute, FlowSettings flow,
|
||||
double[] zoneTimes)
|
||||
{
|
||||
var zone = BuildZone(settings, work, flow);
|
||||
var kernel = compute.FindKernel("Evaluate");
|
||||
var rng = new System.Random(label.Length * 7919);
|
||||
const int positionCount = 256, gradientCount = 64;
|
||||
const int count = positionCount;
|
||||
|
||||
var positions = new Vector3[count];
|
||||
for (var i = 0; i < positionCount; i++)
|
||||
positions[i] = new Vector3((float) (rng.NextDouble() * 20000 - 10000), (float) (rng.NextDouble() * 200 - 100),
|
||||
(float) (rng.NextDouble() * 20000 - 10000));
|
||||
// Gradients of any direction and magnitude, plus one zero gradient.
|
||||
var gradients = new Vector2[count];
|
||||
for (var i = 1; i < gradientCount; i++)
|
||||
{
|
||||
var angle = rng.NextDouble() * Math.PI * 2;
|
||||
var magnitude = (float) Math.Exp(rng.NextDouble() * 8 - 4);
|
||||
gradients[i] = new Vector2((float) Math.Cos(angle) * magnitude, (float) Math.Sin(angle) * magnitude);
|
||||
}
|
||||
gradients[0] = Vector2.zero;
|
||||
|
||||
var positionBuffer = new ComputeBuffer(count, 12);
|
||||
var gradientBuffer = new ComputeBuffer(count, 8);
|
||||
var globalBuffer = new ComputeBuffer(count, 12);
|
||||
var slopeBuffer = new ComputeBuffer(count, 12);
|
||||
var gatedBuffer = new ComputeBuffer(count, 12);
|
||||
ComputeBuffer noiseBuffer = null;
|
||||
// flow() reads the nebula's gravity height under FLOW_SLOPE. A ramp rising along the texture's x gives every
|
||||
// position inside the grid the gradient direction (1, 0), so the slope term is live where a flat height
|
||||
// would zero it (and the FLOW_SLOPE keyword and its pragma would not matter).
|
||||
const int rampSize = 64;
|
||||
var height = new Texture2D(rampSize, rampSize, TextureFormat.RFloat, false)
|
||||
{ filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp };
|
||||
var ramp = new float[rampSize * rampSize];
|
||||
for (var i = 0; i < ramp.Length; i++) ramp[i] = (i % rampSize) / (float) (rampSize - 1);
|
||||
height.SetPixelData(ramp, 0);
|
||||
height.Apply();
|
||||
var rampGradient = new CultMath.float2(1f, 0f);
|
||||
try
|
||||
{
|
||||
positionBuffer.SetData(positions);
|
||||
gradientBuffer.SetData(gradients);
|
||||
compute.SetBuffer(kernel, "positions", positionBuffer);
|
||||
compute.SetBuffer(kernel, "gradients", gradientBuffer);
|
||||
compute.SetBuffer(kernel, "globalOut", globalBuffer);
|
||||
compute.SetBuffer(kernel, "slopeOut", slopeBuffer);
|
||||
compute.SetBuffer(kernel, "gatedOut", gatedBuffer);
|
||||
compute.SetTexture(kernel, "_NebulaSurfaceHeight", height);
|
||||
compute.SetVector("_NebulaSurfaceHeight_TexelSize", new Vector4(1f / rampSize, 1f / rampSize, rampSize, rampSize));
|
||||
// The grid spans 40000: every sampled position (|xz| to 10000) is inside the ramp's uv range.
|
||||
Shader.SetGlobalVector("_GridTransform", new Vector4(0f, 0f, 40000f, 0f));
|
||||
noiseBuffer = new ComputeBuffer(count, 4);
|
||||
compute.SetBuffer(kernel, "noiseOut", noiseBuffer);
|
||||
compute.SetInt("count", count);
|
||||
|
||||
var elapsed = 0.0;
|
||||
foreach (var target in zoneTimes)
|
||||
{
|
||||
// Step the zone to the target by an awkward step; the sum is the zone's time.
|
||||
while (elapsed < target - 1e-9)
|
||||
{
|
||||
var dt = (float) Math.Min(0.37, target - elapsed);
|
||||
zone.Update(dt);
|
||||
elapsed += dt;
|
||||
}
|
||||
VolumeSampling.LowerFlow(zone.Settings.Flow, zone.ExactTime);
|
||||
|
||||
// The shader got the zone's scroll and cycle: the same zone time Zone.Flow evaluates at.
|
||||
var zoneScroll = zone.FlowScroll;
|
||||
var lowered = Shader.GetGlobalVector("_FlowScroll");
|
||||
if (Math.Abs(lowered.x - zoneScroll.x) > 1e-6 || Math.Abs(lowered.y - zoneScroll.y) > 1e-6)
|
||||
Fail($"{label}-lowered-scroll", $"at zone time {elapsed:F3} the shader got _FlowScroll ({lowered.x}, {lowered.y}), the zone's is ({zoneScroll.x}, {zoneScroll.y})");
|
||||
if (Math.Abs(Shader.GetGlobalFloat("_FlowCycle") - zone.CyclePhase(flow.Period)) > 1e-6)
|
||||
Fail($"{label}-lowered-cycle", $"at zone time {elapsed:F3} the shader got _FlowCycle {Shader.GetGlobalFloat("_FlowCycle")}, the zone's phase is {zone.CyclePhase(flow.Period)}");
|
||||
if (Shader.GetGlobalFloat("_FlowScale") != flow.GlobalScale || Shader.GetGlobalFloat("_FlowAmplitude") != flow.GlobalAmplitude ||
|
||||
Shader.GetGlobalFloat("_FlowSlopeAmplitude") != flow.SlopeAmplitude ||
|
||||
Shader.GetGlobalFloat("_FlowSwirlAmplitude") != flow.SwirlAmplitude || Shader.GetGlobalFloat("_FlowPeriod") != flow.Period)
|
||||
Fail($"{label}-lowered-settings", "a flow uniform does not equal the zone's FlowSettings");
|
||||
|
||||
compute.Dispatch(kernel, (count + 63) / 64, 1, 1);
|
||||
var globalOut = new Vector3[count];
|
||||
var slopeOut = new Vector3[count];
|
||||
var gatedOut = new Vector3[count];
|
||||
globalBuffer.GetData(globalOut);
|
||||
slopeBuffer.GetData(slopeOut);
|
||||
gatedBuffer.GetData(gatedOut);
|
||||
|
||||
var at = $"{label} zone time {elapsed:F2} scroll {zoneScroll.x:F4}";
|
||||
for (var i = 0; i < positionCount; i++)
|
||||
{
|
||||
var p = positions[i];
|
||||
var expected = FlowField.Global(flow, new CultMath.float3(p.x, p.y, p.z), zoneScroll);
|
||||
Compare($"{label}-global", at, i, new Vector3(expected.x, expected.y, expected.z), globalOut[i], flow.GlobalAmplitude);
|
||||
}
|
||||
// flow() is the keyword-gated function the nebula and stardust call: the global term plus, under
|
||||
// FLOW_SLOPE, the slope term of the ramp's gradient. A flow that is live must not read zero.
|
||||
var live = 0;
|
||||
var gatedScale = Math.Max(Math.Abs(flow.GlobalAmplitude), Math.Max(Math.Abs(flow.SlopeAmplitude), Math.Abs(flow.SwirlAmplitude)));
|
||||
for (var i = 0; i < positionCount; i++)
|
||||
{
|
||||
var p = positions[i];
|
||||
var expected = FlowField.At(flow, new CultMath.float3(p.x, p.y, p.z), zoneScroll, rampGradient);
|
||||
var e = new Vector3(expected.x, expected.y, expected.z);
|
||||
if (e.magnitude > .1f * gatedScale) live++;
|
||||
Compare($"{label}-flow-keywords", at, i, e, gatedOut[i], gatedScale);
|
||||
}
|
||||
if (gatedScale != 0f && live < positionCount / 4)
|
||||
Fail($"{label}-flow-keywords", $"{at}: only {live} of {positionCount} zone flows are live; the check is vacuous");
|
||||
var slopeScale = Math.Max(Math.Abs(flow.SlopeAmplitude), Math.Abs(flow.SwirlAmplitude));
|
||||
for (var i = 0; i < gradientCount; i++)
|
||||
{
|
||||
var g = gradients[i];
|
||||
var expected = FlowField.Slope(flow, new CultMath.float2(g.x, g.y));
|
||||
Compare($"{label}-slope", at, i, new Vector3(expected.x, expected.y, expected.z), slopeOut[i], slopeScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
positionBuffer.Release(); gradientBuffer.Release(); globalBuffer.Release(); slopeBuffer.Release(); gatedBuffer.Release(); noiseBuffer?.Release(); UnityEngine.Object.DestroyImmediate(height);
|
||||
}
|
||||
}
|
||||
|
||||
// The nebula noise the flow advects evolves on the flow clock VolumeSampling lowers as _NebulaNoiseTime, never on
|
||||
// _Time: with the speed non-zero, the noise changes when _NebulaNoiseTime does and not otherwise (a frame clock
|
||||
// would not follow the global set here).
|
||||
static void CheckNoiseClock(ComputeShader compute)
|
||||
{
|
||||
const int count = 64;
|
||||
var kernel = compute.FindKernel("Evaluate");
|
||||
var rng = new System.Random(4217);
|
||||
var positions = new Vector3[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
positions[i] = new Vector3((float) (rng.NextDouble() * 4000 - 2000), (float) (rng.NextDouble() * 100 - 50), (float) (rng.NextDouble() * 4000 - 2000));
|
||||
var buffers = new[] { new ComputeBuffer(count, 12), new ComputeBuffer(count, 8), new ComputeBuffer(count, 12), new ComputeBuffer(count, 12), new ComputeBuffer(count, 12), new ComputeBuffer(count, 4) };
|
||||
var height = new Texture2D(1, 1, TextureFormat.RFloat, false);
|
||||
try
|
||||
{
|
||||
buffers[0].SetData(positions);
|
||||
buffers[1].SetData(new Vector2[count]);
|
||||
compute.SetBuffer(kernel, "positions", buffers[0]);
|
||||
compute.SetBuffer(kernel, "gradients", buffers[1]);
|
||||
compute.SetBuffer(kernel, "globalOut", buffers[2]);
|
||||
compute.SetBuffer(kernel, "slopeOut", buffers[3]);
|
||||
compute.SetBuffer(kernel, "gatedOut", buffers[4]);
|
||||
compute.SetBuffer(kernel, "noiseOut", buffers[5]);
|
||||
compute.SetTexture(kernel, "_NebulaSurfaceHeight", height);
|
||||
compute.SetInt("count", count);
|
||||
Shader.SetGlobalFloat("_NebulaNoiseSpeed", 1f);
|
||||
float[] Noise(float noiseTime)
|
||||
{
|
||||
Shader.SetGlobalFloat("_NebulaNoiseTime", noiseTime);
|
||||
compute.Dispatch(kernel, 1, 1, 1);
|
||||
var result = new float[count];
|
||||
buffers[5].GetData(result);
|
||||
return result;
|
||||
}
|
||||
var a = Noise(0f);
|
||||
var b = Noise(3f);
|
||||
var again = Noise(3f);
|
||||
int moved = 0, repeated = 0;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (Math.Abs(a[i] - b[i]) > 1e-4f) moved++;
|
||||
if (b[i] == again[i]) repeated++;
|
||||
}
|
||||
if (moved < count / 2) Fail("nebula-noise-clock", $"only {moved} of {count} noise samples follow _NebulaNoiseTime");
|
||||
if (repeated != count) Fail("nebula-noise-clock", $"{count - repeated} of {count} noise samples changed with _NebulaNoiseTime held");
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var buffer in buffers) buffer.Release();
|
||||
UnityEngine.Object.DestroyImmediate(height);
|
||||
}
|
||||
}
|
||||
|
||||
// Error is measured against the term's amplitude; a term with no amplitude must be exactly zero and never NaN.
|
||||
static void Compare(string check, string at, int index, Vector3 expected, Vector3 actual, float amplitude)
|
||||
{
|
||||
var error = (expected - actual).magnitude;
|
||||
if (float.IsNaN(error)) { Fail(check, $"{at} sample {index}: NaN (expected {expected}, shader {actual})"); return; }
|
||||
if (amplitude != 0f) _maxError = Math.Max(_maxError, error / Math.Abs(amplitude));
|
||||
var tolerance = RelativeTolerance * Math.Abs(amplitude);
|
||||
if (error > tolerance)
|
||||
Fail(check, $"{at} sample {index}: expected {expected}, shader {actual}, error {error:G4} > {tolerance:G4}");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/FlowFieldParityVerify.cs.meta
Normal file
2
Assets/Scripts/Editor/FlowFieldParityVerify.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8fae18f5f32f10f43ba892a097f18caa
|
||||
181
Assets/Scripts/Editor/FlowFieldSceneVerify.cs
Normal file
181
Assets/Scripts/Editor/FlowFieldSceneVerify.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace Aetheria.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Batchmode check for the flow as the scene runs it (campaign aetheria-fauna, cut fauna-flow-field; target
|
||||
/// invariants flow-one-owner and flow-shader-parity). FlowFieldParityVerify proves the HLSL against FlowField with
|
||||
/// no scene loaded; this opens ARPG.unity, lets VolumeSampling and Stardust initialise as the game does, drives
|
||||
/// their own Start and Update with a zone attached, and asserts what the presenters did:
|
||||
/// - nothing threw and nothing logged an error (a type that cannot initialise in a scene, as VolumeSampling once
|
||||
/// could when it created its keywords in a static field initializer, throws on its first use);
|
||||
/// - FLOW_GLOBAL and FLOW_SLOPE are enabled, _FlowAmplitude is the zone's, and the nebula noise clock is the zone's;
|
||||
/// - the stardust particle buffer changes with the flow and freezes when the zone pauses;
|
||||
/// - CloudShader pass 0 carries the FLOW_* keywords.
|
||||
/// The scene is opened before any Aetheria type is touched: run this method alone in its own Unity session, never
|
||||
/// after FlowFieldParityVerify (which would initialise VolumeSampling outside a MonoBehaviour constructor).
|
||||
/// Dispatches compute and renders, so it needs a graphics device: run WITHOUT -nographics.
|
||||
/// Unity -batchmode -projectPath <repo> -executeMethod Aetheria.EditorTools.FlowFieldSceneVerify.Run -logFile <path>
|
||||
/// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure.
|
||||
/// </summary>
|
||||
public static class FlowFieldSceneVerify
|
||||
{
|
||||
const string Tag = "[FlowFieldSceneVerify]";
|
||||
static bool _ok = true;
|
||||
|
||||
static void OnLog(string message, string stack, LogType type)
|
||||
{
|
||||
if ((type == LogType.Error || type == LogType.Exception || type == LogType.Assert) && !message.StartsWith(Tag))
|
||||
Fail("logged-error", message.Split('\n')[0]);
|
||||
}
|
||||
|
||||
static void Fail(string check, string detail)
|
||||
{
|
||||
Debug.LogError($"{Tag} FAIL {check}: {detail}");
|
||||
_ok = false;
|
||||
}
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null)
|
||||
{
|
||||
Debug.LogError($"{Tag} FAIL: no graphics device in this session; rerun batchmode without -nographics.");
|
||||
EditorApplication.Exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
Application.logMessageReceived += OnLog;
|
||||
try
|
||||
{
|
||||
// Before anything else touches the project's types: the scene constructs its MonoBehaviours here.
|
||||
EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Single);
|
||||
Check();
|
||||
Debug.Log(_ok ? $"{Tag} PASS" : $"{Tag} FAIL: see errors above");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"{Tag} FAIL exception: {e}");
|
||||
_ok = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Application.logMessageReceived -= OnLog;
|
||||
}
|
||||
EditorApplication.Exit(_ok ? 0 : 1);
|
||||
}
|
||||
|
||||
static void Check()
|
||||
{
|
||||
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")
|
||||
?? throw new InvalidOperationException("No Settings.asset.");
|
||||
var agm = UnityEngine.Object.FindObjectsByType<ActionGameManager>(FindObjectsInactive.Include, FindObjectsSortMode.None).FirstOrDefault();
|
||||
var volume = UnityEngine.Object.FindObjectsByType<VolumeSampling>(FindObjectsInactive.Include, FindObjectsSortMode.None).FirstOrDefault();
|
||||
var dust = UnityEngine.Object.FindObjectsByType<Stardust>(FindObjectsInactive.Include, FindObjectsSortMode.None).FirstOrDefault();
|
||||
if (agm == null || volume == null || dust == null)
|
||||
{
|
||||
Fail("scene-objects", $"ARPG.unity has ActionGameManager={agm != null}, VolumeSampling={volume != null}, Stardust={dust != null}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Every flow term live, so FLOW_SLOPE is on; and the same flow with every amplitude zero.
|
||||
var on = new FlowSettings
|
||||
{
|
||||
GlobalScale = 512f, GlobalAmplitude = 15f, GlobalScrollSpeed = .025f, Period = 8f, SlopeAmplitude = 3f, SwirlAmplitude = 5f
|
||||
};
|
||||
var off = new FlowSettings { GlobalScale = on.GlobalScale, GlobalScrollSpeed = on.GlobalScrollSpeed, Period = on.Period };
|
||||
var work = Path.Combine(Path.GetTempPath(), "flow-scene-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(work);
|
||||
try
|
||||
{
|
||||
var zone = FlowFieldParityVerify.BuildZone(settings, work, on, 86400.0 * 3 + 12.3);
|
||||
typeof(ActionGameManager).GetProperty("Instance").GetSetMethod(true).Invoke(null, new object[] { agm });
|
||||
typeof(ActionGameManager).GetProperty("Zone").GetSetMethod(true).Invoke(agm, new object[] { zone });
|
||||
if (agm.Settings == null) agm.Settings = settings;
|
||||
|
||||
Call(volume, "Start");
|
||||
Call(volume, "Update");
|
||||
if (!Shader.IsKeywordEnabled("FLOW_GLOBAL")) Fail("scene-keywords", "FLOW_GLOBAL is not enabled after VolumeSampling.Update");
|
||||
if (!Shader.IsKeywordEnabled("FLOW_SLOPE")) Fail("scene-keywords", "FLOW_SLOPE is not enabled after VolumeSampling.Update");
|
||||
if (Shader.GetGlobalFloat("_FlowAmplitude") != on.GlobalAmplitude)
|
||||
Fail("scene-amplitude", $"_FlowAmplitude is {Shader.GetGlobalFloat("_FlowAmplitude")}, the zone's flow has {on.GlobalAmplitude}");
|
||||
var scroll = Shader.GetGlobalVector("_FlowScroll");
|
||||
if (scroll.x != zone.FlowScroll.x || scroll.y != zone.FlowScroll.y)
|
||||
Fail("scene-scroll", $"_FlowScroll ({scroll.x}, {scroll.y}) is not the zone's ({zone.FlowScroll.x}, {zone.FlowScroll.y})");
|
||||
var noiseTime = Shader.GetGlobalFloat("_NebulaNoiseTime");
|
||||
if (noiseTime != (float) zone.ExactTime)
|
||||
Fail("scene-noise-clock", $"_NebulaNoiseTime is {noiseTime}, the zone's time is {(float) zone.ExactTime}");
|
||||
|
||||
CheckCloudKeywords();
|
||||
|
||||
// Stardust follows the flow through its own dispatch: the particle buffer read back each frame.
|
||||
Call(dust, "Start");
|
||||
var bufferField = typeof(Stardust).GetField("_particlesBuffer", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
float[] Frame()
|
||||
{
|
||||
Call(dust, "Update");
|
||||
var buffer = (ComputeBuffer) bufferField.GetValue(dust);
|
||||
var data = new float[buffer.count * 7];
|
||||
buffer.GetData(data);
|
||||
return data;
|
||||
}
|
||||
var flowing = Frame();
|
||||
VolumeSampling.LowerFlow(off, VolumeSampling.FlowClock);
|
||||
var still = Frame();
|
||||
if (Differing(flowing, still) == 0) Fail("scene-stardust", "the particle buffer is the same with the zone's flow and with no flow");
|
||||
|
||||
// A paused zone freezes the particles and the nebula noise clock; a running one moves both.
|
||||
Call(volume, "Update");
|
||||
zone.Update(0f);
|
||||
Call(volume, "Update");
|
||||
if (Differing(flowing, Frame()) != 0) Fail("scene-stardust-pause", "the particle buffer moved while the zone was paused");
|
||||
if (Shader.GetGlobalFloat("_NebulaNoiseTime") != noiseTime) Fail("scene-noise-clock", "_NebulaNoiseTime moved while the zone was paused");
|
||||
zone.Update(.05f);
|
||||
Call(volume, "Update");
|
||||
if (Differing(flowing, Frame()) == 0) Fail("scene-stardust-run", "the particle buffer did not move while the zone ran");
|
||||
if (Shader.GetGlobalFloat("_NebulaNoiseTime") == noiseTime) Fail("scene-noise-clock", "_NebulaNoiseTime did not move while the zone ran");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(work, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// CloudShader's pass 0 compiles with the flow keywords (it must #include_with_pragmas the shared include).
|
||||
static void CheckCloudKeywords()
|
||||
{
|
||||
var shader = AssetDatabase.LoadAssetAtPath<Shader>("Assets/Shaders/Raymarching/CloudShader.shader");
|
||||
if (shader == null) { Fail("scene-cloud-keywords", "No CloudShader.shader."); return; }
|
||||
var keywords = ShaderUtil.GetPassKeywords(shader, new PassIdentifier(0, 0), UnityEditor.Rendering.ShaderType.Fragment)
|
||||
.Select(keyword => keyword.name).ToArray();
|
||||
foreach (var name in new[] { "FLOW_GLOBAL", "FLOW_SLOPE" })
|
||||
if (!keywords.Contains(name)) Fail("scene-cloud-keywords", $"CloudShader pass 0 does not carry {name}");
|
||||
}
|
||||
|
||||
static int Differing(float[] a, float[] b)
|
||||
{
|
||||
var count = 0;
|
||||
for (var i = 0; i < a.Length; i++)
|
||||
if (Math.Abs(a[i] - b[i]) > 1e-4f) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
// The presenters' own Start and Update, as the game calls them; an exception is a failure that names the type.
|
||||
static void Call(object target, string method)
|
||||
{
|
||||
var info = target.GetType().GetMethod(method, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
||||
if (info == null) { Fail("scene-presenter-exception", $"{target.GetType().Name}.{method} not found"); return; }
|
||||
try { info.Invoke(target, null); }
|
||||
catch (TargetInvocationException e)
|
||||
{
|
||||
Fail("scene-presenter-exception", $"{target.GetType().Name}.{method} threw {e.InnerException?.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/FlowFieldSceneVerify.cs.meta
Normal file
2
Assets/Scripts/Editor/FlowFieldSceneVerify.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 25fbd79fca1de49498c3bebf9f63243b
|
||||
|
|
@ -9,7 +9,7 @@ using float2 = CultMath.float2;
|
|||
public class ZoneEnvironment
|
||||
{
|
||||
[JsonProperty("nebula"), Key(0)] public NebulaSettings Nebula;
|
||||
[JsonProperty("flow"), Key(1)] public FlowSettings Flow;
|
||||
// Key 1 is retired (it held the flow, now PlanetSettings.Flow); never reuse it.
|
||||
[JsonProperty("noise"), Key(2)] public NoiseSettings Noise;
|
||||
[JsonProperty("lighting"), Key(3)] public AmbientLightingSettings Lighting;
|
||||
[JsonProperty("grid"), Key(4)] public GridSettings Grid;
|
||||
|
|
|
|||
78
Assets/Scripts/ServerShared/FlowField.cs
Normal file
78
Assets/Scripts/ServerShared/FlowField.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using CultMath;
|
||||
using static CultMath.math;
|
||||
using float2 = CultMath.float2;
|
||||
using float3 = CultMath.float3;
|
||||
|
||||
/// <summary>
|
||||
/// The global flow law: the one definition of the drift that stardust and nebula follow and that the sim knows.
|
||||
/// A pure function of the zone's FlowSettings, a position, a scroll and a downhill gradient; nothing here keeps
|
||||
/// a clock. Zone.Flow evaluates it at zone time. Assets/Shaders/Volumetric.cginc (globalFlow, slopeFlow) is its
|
||||
/// HLSL lowering, driven by VolumeSampling.LowerFlow and pinned to it by Aetheria.EditorTools.FlowFieldParityVerify.
|
||||
/// </summary>
|
||||
public static class FlowField
|
||||
{
|
||||
// The second octave of the noise samples at position * Golden (Volumetric.cginc Tri3D).
|
||||
const float Golden = 1.61803398875f;
|
||||
|
||||
// The scroll of the noise field, in field units: speed times zone time, for each of the two octaves, as a phase.
|
||||
// Every term of Tri3D reaches the scrolled axis through frac, so a phase is only meaningful modulo 1: the fold
|
||||
// happens here in double, where zone time is exact, and the float that reaches the shaders is in [0, 1) however
|
||||
// old the zone is. x is the first octave's phase, y the second's (the second octave's axis is scaled by Golden).
|
||||
public static float2 Scroll(FlowSettings settings, double zoneTime)
|
||||
{
|
||||
var scroll = (double) settings.GlobalScrollSpeed * zoneTime;
|
||||
return float2(Phase(scroll), Phase(scroll * Golden));
|
||||
}
|
||||
|
||||
// A phase in [0, 1) strictly: a double just below an integer rounds up to 1f in the cast, and 1 is 0 modulo 1.
|
||||
static float Phase(double value)
|
||||
{
|
||||
var phase = (float) frac(value);
|
||||
return phase < 1f ? phase : 0f;
|
||||
}
|
||||
|
||||
// The phase of a cycle of `period` seconds at zone time, in [0, 1). The nebula's flow-advected noise cycles and
|
||||
// Stardust's particle lifetimes take their phase from here, so drift follows sim time and stops with the sim.
|
||||
public static float Cycle(double zoneTime, float period) => period == 0f ? 0f : Phase(zoneTime / period);
|
||||
|
||||
// The procedural term (Volumetric.cginc globalFlow): a Tri3D field of position / scale, scrolled along y.
|
||||
// Skipped when its amplitude is zero, as the shader's FLOW_GLOBAL keyword skips it.
|
||||
public static float3 Global(FlowSettings settings, float3 position, float2 scroll)
|
||||
{
|
||||
if (settings.GlobalAmplitude == 0f) return float3.zero;
|
||||
var field = position / settings.GlobalScale;
|
||||
var sample = Tri3D(field - float3(0f, scroll.x, 0f), field * Golden - float3(0f, scroll.y, 0f)) * settings.GlobalAmplitude;
|
||||
sample.y *= .5f;
|
||||
return sample;
|
||||
}
|
||||
|
||||
// The slope term (Volumetric.cginc slopeFlow): swirl around and slide along the gravity gradient.
|
||||
// `downhill` is the DOWNHILL gradient, -grad(height) in world xz. The shader's texel gradient is that same
|
||||
// vector (getUV negates the world x and z axes), and Zone.GetNormal's xz is proportional to it.
|
||||
// Skipped when both amplitudes are zero (the FLOW_SLOPE keyword), and zero for a zero gradient.
|
||||
public static float3 Slope(FlowSettings settings, float2 downhill)
|
||||
{
|
||||
if (settings.SlopeAmplitude == 0f && settings.SwirlAmplitude == 0f) return float3.zero;
|
||||
if (dot(downhill, downhill) == 0f) return float3.zero;
|
||||
var direction = normalize(downhill);
|
||||
return float3(direction.y, 0f, -direction.x) * settings.SwirlAmplitude
|
||||
+ float3(direction.x, 0f, direction.y) * settings.SlopeAmplitude;
|
||||
}
|
||||
|
||||
public static float3 At(FlowSettings settings, float3 position, float2 scroll, float2 downhill) =>
|
||||
Global(settings, position, scroll) + Slope(settings, downhill);
|
||||
|
||||
// Volumetric.cginc tri / tri3 / Tri3D, term for term.
|
||||
static float Tri(float x) => abs(frac(x) - .5f);
|
||||
|
||||
static float3 Tri3(float3 p) =>
|
||||
float3(Tri(p.z + Tri(p.y)), Tri(p.z + Tri(p.x)), Tri(p.y + Tri(p.x)));
|
||||
|
||||
// The two octaves' positions are given already scrolled (see Scroll).
|
||||
static float3 Tri3D(float3 p, float3 pGolden)
|
||||
{
|
||||
var t1 = normalize(Tri3(p));
|
||||
var t2 = normalize(Tri3(pGolden));
|
||||
return cross(t1, t2);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ServerShared/FlowField.cs.meta
Normal file
2
Assets/Scripts/ServerShared/FlowField.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8105a45840c4552459aaf44e6c0e19ce
|
||||
|
|
@ -32,6 +32,9 @@ public class PlanetSettings
|
|||
public float SecureAreaRadiusMultiplier = .45f;
|
||||
|
||||
public ExponentialCurve OrbitPeriod;
|
||||
|
||||
// The global flow stardust and nebula follow and the sim knows (FlowField). Zero flow by default.
|
||||
public FlowSettings Flow = new FlowSettings();
|
||||
}
|
||||
|
||||
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ public class Zone
|
|||
}
|
||||
|
||||
// Zone time at full precision, for the index's orbital keys, which must agree with ChunkPose's own.
|
||||
internal double ExactTime => _time;
|
||||
public double ExactTime => _time;
|
||||
public ZonePack Pack { get; }
|
||||
public GalaxyZone GalaxyZone { get; }
|
||||
public Galaxy Galaxy { get; }
|
||||
|
|
@ -695,6 +695,20 @@ public class Zone
|
|||
return GravityAcceleration(new float2(normal.x, normal.z), Settings.GravityStrength);
|
||||
}
|
||||
|
||||
// The global flow velocity at `position` in zone time, in u/s (FlowField is the law). The slope term reads the
|
||||
// downhill gradient, which GetNormal's xz is proportional to.
|
||||
public float3 Flow(float3 position)
|
||||
{
|
||||
var normal = GetNormal(position.xz);
|
||||
return FlowField.At(Settings.Flow, position, FlowScroll, new float2(normal.x, normal.z));
|
||||
}
|
||||
|
||||
// The flow's noise scroll at zone time (Zone.Flow evaluates at exactly this); lowered to the shaders' _FlowScroll.
|
||||
public float2 FlowScroll => FlowField.Scroll(Settings.Flow, _time);
|
||||
|
||||
// The phase of a cycle of `period` seconds at zone time, for presenters that animate on the flow.
|
||||
public float CyclePhase(float period) => FlowField.Cycle(_time, period);
|
||||
|
||||
public static float PowerPulse(float x, float exponent)
|
||||
{
|
||||
x *= 2;
|
||||
|
|
|
|||
|
|
@ -27,16 +27,11 @@ public class VolumeSampling : MonoBehaviour
|
|||
public RenderTexture NebulaTint;
|
||||
|
||||
private MeshRenderer _gridMeshRenderer;
|
||||
private float _flowScroll;
|
||||
private Transform _gridTransform;
|
||||
private GlobalKeyword _keywordFlowGlobal;
|
||||
private GlobalKeyword _keywordFlowSlope;
|
||||
private GlobalKeyword _keywordNoiseSlope;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_keywordFlowGlobal = GlobalKeyword.Create("FLOW_GLOBAL");
|
||||
_keywordFlowSlope = GlobalKeyword.Create("FLOW_SLOPE");
|
||||
_keywordNoiseSlope = GlobalKeyword.Create("NOISE_SLOPE");
|
||||
|
||||
if (GridMesh != null)
|
||||
|
|
@ -52,6 +47,7 @@ public class VolumeSampling : MonoBehaviour
|
|||
Shader.SetGlobalVector("_GridTransform", new Vector4(_gridTransform.position.x,_gridTransform.position.z,GridCamera.orthographicSize*2));
|
||||
|
||||
var environment = ActionGameManager.Instance?.CurrentEnvironment ?? Settings.DefaultEnvironment;
|
||||
var clock = FlowClock;
|
||||
|
||||
if (GridMesh != null)
|
||||
{
|
||||
|
|
@ -91,18 +87,46 @@ public class VolumeSampling : MonoBehaviour
|
|||
Shader.SetGlobalFloat("_NebulaNoiseExponent", environment.Noise.Exponent);
|
||||
Shader.SetGlobalFloat("_NebulaNoiseAmplitude", environment.Noise.Amplitude);
|
||||
Shader.SetGlobalFloat("_NebulaNoiseSpeed", environment.Noise.Speed);
|
||||
// The noise the flow advects evolves on the flow's clock too: it pauses with the sim.
|
||||
Shader.SetGlobalFloat("_NebulaNoiseTime", (float) clock);
|
||||
Shader.SetGlobalFloat("_NebulaNoiseSlopeExponent", environment.Noise.SlopeExponent);
|
||||
|
||||
Shader.SetGlobalFloat("_FlowScale", environment.Flow.GlobalScale);
|
||||
Shader.SetGlobalFloat("_FlowAmplitude", environment.Flow.GlobalAmplitude);
|
||||
Shader.SetGlobalFloat("_FlowSlopeAmplitude", environment.Flow.SlopeAmplitude);
|
||||
Shader.SetGlobalFloat("_FlowSwirlAmplitude", environment.Flow.SwirlAmplitude);
|
||||
_flowScroll += environment.Flow.GlobalScrollSpeed * Time.deltaTime;
|
||||
Shader.SetGlobalFloat("_FlowScroll", _flowScroll);
|
||||
Shader.SetGlobalFloat("_FlowPeriod", environment.Flow.Period);
|
||||
|
||||
Shader.SetKeyword(_keywordFlowGlobal, environment.Flow.GlobalAmplitude != 0);
|
||||
Shader.SetKeyword(_keywordFlowSlope, environment.Flow.SlopeAmplitude != 0 || environment.Flow.SwirlAmplitude != 0);
|
||||
// The flow is the zone's: its settings and its time. Without a zone (menus) the same law runs on the presentation clock.
|
||||
LowerFlow(ActionGameManager.Instance?.Zone?.Settings.Flow ?? Settings.PlanetSettings.Flow, clock);
|
||||
Shader.SetKeyword(_keywordNoiseSlope, environment.Noise.SlopeExponent != 0);
|
||||
}
|
||||
|
||||
// GlobalKeyword's constructor only looks a keyword up; Create registers it. The shaders' pragmas and this owner
|
||||
// agree on the names. Unity refuses Create inside a MonoBehaviour's construction, and a scene constructing the
|
||||
// first VolumeSampling runs this type's static initializer there, so the keywords are created on first use, from
|
||||
// LowerFlow, never in a field initializer.
|
||||
static GlobalKeyword? _keywordFlowGlobal;
|
||||
static GlobalKeyword? _keywordFlowSlope;
|
||||
static GlobalKeyword KeywordFlowGlobal => _keywordFlowGlobal ??= GlobalKeyword.Create("FLOW_GLOBAL");
|
||||
static GlobalKeyword KeywordFlowSlope => _keywordFlowSlope ??= GlobalKeyword.Create("FLOW_SLOPE");
|
||||
|
||||
/// <summary>
|
||||
/// The time the flow runs on: the zone's own (so drift stops with the sim and follows sim speed), or on menus,
|
||||
/// where no zone exists, the presentation clock.
|
||||
/// </summary>
|
||||
public static double FlowClock => ActionGameManager.Instance?.Zone?.ExactTime ?? Time.timeSinceLevelLoad;
|
||||
|
||||
/// <summary>
|
||||
/// The one place that sets the flow uniforms and keywords: every shader flow term derives from FlowSettings and
|
||||
/// the flow clock through FlowField, so no presenter keeps its own flow clock.
|
||||
/// </summary>
|
||||
public static void LowerFlow(FlowSettings flow, double flowClock)
|
||||
{
|
||||
Shader.SetGlobalFloat("_FlowScale", flow.GlobalScale);
|
||||
Shader.SetGlobalFloat("_FlowAmplitude", flow.GlobalAmplitude);
|
||||
Shader.SetGlobalFloat("_FlowSlopeAmplitude", flow.SlopeAmplitude);
|
||||
Shader.SetGlobalFloat("_FlowSwirlAmplitude", flow.SwirlAmplitude);
|
||||
var scroll = FlowField.Scroll(flow, flowClock);
|
||||
Shader.SetGlobalVector("_FlowScroll", new Vector4(scroll.x, scroll.y));
|
||||
Shader.SetGlobalFloat("_FlowPeriod", flow.Period);
|
||||
Shader.SetGlobalFloat("_FlowCycle", FlowField.Cycle(flowClock, flow.Period));
|
||||
|
||||
Shader.SetKeyword(KeywordFlowGlobal, flow.GlobalAmplitude != 0);
|
||||
Shader.SetKeyword(KeywordFlowSlope, flow.SlopeAmplitude != 0 || flow.SwirlAmplitude != 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue