Merge suns-never-green (cut suns-never-green; verdicts s1, s2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
2d96c3241f
4 changed files with 231 additions and 3 deletions
|
|
@ -18,6 +18,29 @@ public static class ColorMath
|
|||
return saturate(float3(r,g,b));
|
||||
}
|
||||
|
||||
// A hue band on the circle runs from start up to end, wrapping past 1 when end < start.
|
||||
public static bool HueInBand(float hue, float start, float end) => frac(hue - start) < frac(end - start);
|
||||
|
||||
// Maps u in [0,1) onto the hues outside the band, so a uniform draw stays uniform over the allowed arc.
|
||||
public static float HueOutsideBand(float u, float start, float end) => frac(end + u * AllowedArc(start, end));
|
||||
|
||||
static float AllowedArc(float start, float end) => 1 - frac(end - start);
|
||||
|
||||
// The second hue of a pair whose first is HueOutsideBand(u): distance away along the allowed arc, on the side given by up,
|
||||
// reflected to the other side if that would leave the arc. Both hues lie on one arc, so the straight RGB blend between
|
||||
// the two stops never crosses the band. The distance is capped at half the arc, so the reflection always fits and a
|
||||
// wide band shortens the distance instead of leaving the secondary inside it.
|
||||
public static float SecondaryHueOutsideBand(float u, bool up, float distance, float start, float end)
|
||||
{
|
||||
float arc = AllowedArc(start, end);
|
||||
float d = min(distance, arc / 2);
|
||||
float t = u * arc;
|
||||
float s = up ? t + d : t - d;
|
||||
if (s < 0 || s > arc)
|
||||
s = up ? t - d : t + d;
|
||||
return frac(end + s);
|
||||
}
|
||||
|
||||
public static float3 HsvToRgb(in float3 hsv)
|
||||
{
|
||||
float3 rgb = HueToRgb(hsv.x);
|
||||
|
|
@ -35,7 +58,7 @@ public static class ColorMath
|
|||
public static float3 RgbToHcv(in float3 rgb)
|
||||
{
|
||||
// Based on work by Sam Hocevar and Emil Persson
|
||||
float4 p = (rgb.y < rgb.z) ? float4(rgb.zy, -1.0f, 2.0f/3.0f) : float4(rgb.zy, 0.0f, -1.0f/3.0f);
|
||||
float4 p = (rgb.y < rgb.z) ? float4(rgb.zy, -1.0f, 2.0f/3.0f) : float4(rgb.yz, 0.0f, -1.0f/3.0f);
|
||||
float4 q = (rgb.x < p.x) ? float4(p.xyw, rgb.x) : float4(rgb.x, p.yzx);
|
||||
float c = q.x - min(q.w, q.y);
|
||||
float h = abs((q.w - q.y) / (6 * c + Epsilon) + q.z);
|
||||
|
|
|
|||
|
|
@ -142,6 +142,9 @@ public class ZoneGenerationSettings
|
|||
public float SunSecondaryColorDistance = .25f;
|
||||
public float SunLightSaturation = .5f;
|
||||
public float SunFogTintSaturation = .5f;
|
||||
// The green band no sun colour may take, as fractions of the hue circle (75 to 165 degrees: chartreuse through spring green).
|
||||
public float SunHueExcludedStart = 75f / 360f;
|
||||
public float SunHueExcludedEnd = 165f / 360f;
|
||||
|
||||
public ExponentialLerp GasGiantBandCount;
|
||||
public float GasGiantBandColorSeparation = .25f;
|
||||
|
|
|
|||
|
|
@ -159,8 +159,10 @@ public static class ZoneGenerator
|
|||
{
|
||||
if (gas is SunData sun)
|
||||
{
|
||||
float primary = random.NextFloat();
|
||||
float secondary = frac(primary + 1 + zoneSettings.SunSecondaryColorDistance * (random.NextFloat() > .5 ? 1 : -1));
|
||||
var hueDraw = random.NextFloat();
|
||||
float primary = ColorMath.HueOutsideBand(hueDraw, zoneSettings.SunHueExcludedStart, zoneSettings.SunHueExcludedEnd);
|
||||
float secondary = ColorMath.SecondaryHueOutsideBand(hueDraw, random.NextFloat() > .5, zoneSettings.SunSecondaryColorDistance,
|
||||
zoneSettings.SunHueExcludedStart, zoneSettings.SunHueExcludedEnd);
|
||||
gas.Colors = new []
|
||||
{
|
||||
float4(ColorMath.HsvToRgb(float3(primary, zoneSettings.SunColorSaturation, .5f)), 0),
|
||||
|
|
|
|||
200
tests/Aetheria.Shared.Tests/SunsNeverGreenTests.cs
Normal file
200
tests/Aetheria.Shared.Tests/SunsNeverGreenTests.cs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CultMath;
|
||||
using GameCult.Caching;
|
||||
using Xunit;
|
||||
|
||||
// Operator rulings suns-never-green and suns-never-green-edges: no generated sun colour (the body's two colour stops, its
|
||||
// fog tint and its light colour) takes a hue where green dominates. The band is the Settings default, 75 to 165 degrees;
|
||||
// yellow (60) and teal (170) stay allowed. Hues are recovered from the RGB the production generator wrote.
|
||||
public sealed class SunsNeverGreenTests : IDisposable
|
||||
{
|
||||
private const float Degree = 1f / 360f;
|
||||
// Hue recovered from a float RGB wobbles by a few millionths of a turn; the band is judged with this much margin.
|
||||
private const float Slack = 1e-3f;
|
||||
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-sunsgreen-" + Guid.NewGuid().ToString("N"));
|
||||
private int _runs;
|
||||
private readonly List<CultCache> _caches = new List<CultCache>();
|
||||
|
||||
public SunsNeverGreenTests() => Directory.CreateDirectory(_root);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var cache in _caches) cache.Dispose();
|
||||
Directory.Delete(_root, true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(76f)]
|
||||
[InlineData(80f)]
|
||||
[InlineData(120f)]
|
||||
[InlineData(160f)]
|
||||
[InlineData(164f)]
|
||||
public void HuesInsideTheDefaultBandAreRefused(float degrees)
|
||||
{
|
||||
var settings = new ZoneGenerationSettings();
|
||||
Assert.True(ColorMath.HueInBand(degrees * Degree, settings.SunHueExcludedStart, settings.SunHueExcludedEnd));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0f)]
|
||||
[InlineData(60f)]
|
||||
[InlineData(74f)]
|
||||
[InlineData(166f)]
|
||||
[InlineData(170f)]
|
||||
[InlineData(180f)]
|
||||
[InlineData(300f)]
|
||||
public void YellowTealAndTheRestOfTheCircleAreAllowed(float degrees)
|
||||
{
|
||||
var settings = new ZoneGenerationSettings();
|
||||
Assert.False(ColorMath.HueInBand(degrees * Degree, settings.SunHueExcludedStart, settings.SunHueExcludedEnd));
|
||||
}
|
||||
|
||||
// The hues below are read back from RGB the generator wrote, so the reader must invert the writer.
|
||||
[Fact]
|
||||
public void ReadingHueBackFromRgbInvertsWritingIt()
|
||||
{
|
||||
for (var degrees = 5; degrees < 360; degrees += 5)
|
||||
foreach (var value in new[] { .5f, 1f })
|
||||
{
|
||||
var rgb = ColorMath.HsvToRgb(new float3(degrees * Degree, .75f, value));
|
||||
Assert.Equal(degrees * Degree, ColorMath.RgbToHsv(rgb).x, 4);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheDrawMapsOntoTheWholeAllowedArcAndNothingElse()
|
||||
{
|
||||
var settings = new ZoneGenerationSettings();
|
||||
var start = settings.SunHueExcludedStart;
|
||||
var end = settings.SunHueExcludedEnd;
|
||||
var low = float.MaxValue;
|
||||
var high = float.MinValue;
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
var hue = ColorMath.HueOutsideBand(i / 1000f, start, end);
|
||||
Assert.False(ColorMath.HueInBand(hue, start, end), $"draw {i}/1000 landed in the band");
|
||||
// Unwrapped from the band's far edge, the allowed arc runs 165 up to 75 + 360.
|
||||
var along = ColorMath.HueInBand(hue, 0f, end) ? hue + 1f : hue;
|
||||
low = Math.Min(low, along);
|
||||
high = Math.Max(high, along);
|
||||
}
|
||||
Assert.InRange(low, end, end + 1f * Degree);
|
||||
Assert.InRange(high, 1f + start - 1f * Degree, 1f + start);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratedSunsKeepEveryColourOutOfTheDefaultBand()
|
||||
{
|
||||
var settings = new ZoneGenerationSettings();
|
||||
var suns = GenerateSuns(z => { }, out _);
|
||||
Assert.True(suns.Count >= 10, $"only {suns.Count} suns generated");
|
||||
foreach (var sun in suns)
|
||||
foreach (var hue in Hues(sun))
|
||||
Assert.False(ColorMath.HueInBand(hue, settings.SunHueExcludedStart + Slack, settings.SunHueExcludedEnd - Slack),
|
||||
$"a sun colour fell in the green band (hue {hue / Degree:F1} degrees)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheBandComesFromSettingsNotFromTheGenerator()
|
||||
{
|
||||
// A blue band, 200 to 300 degrees: suns avoid it and now take green freely.
|
||||
var suns = GenerateSuns(z =>
|
||||
{
|
||||
z.SunHueExcludedStart = 200f * Degree;
|
||||
z.SunHueExcludedEnd = 300f * Degree;
|
||||
}, out _);
|
||||
Assert.True(suns.Count >= 10, $"only {suns.Count} suns generated");
|
||||
var hues = suns.SelectMany(Hues).ToList();
|
||||
Assert.All(hues, hue => Assert.False(ColorMath.HueInBand(hue, 200f * Degree + Slack, 300f * Degree - Slack)));
|
||||
Assert.Contains(hues, hue => ColorMath.HueInBand(hue, 75f * Degree, 165f * Degree));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheDrawIsUniformOverTheAllowedArc()
|
||||
{
|
||||
var settings = new ZoneGenerationSettings();
|
||||
const int draws = 1000, bins = 10;
|
||||
var counts = new int[bins];
|
||||
var arc = 1f - (settings.SunHueExcludedEnd - settings.SunHueExcludedStart);
|
||||
for (var i = 0; i < draws; i++)
|
||||
{
|
||||
var hue = ColorMath.HueOutsideBand((i + .5f) / draws, settings.SunHueExcludedStart, settings.SunHueExcludedEnd);
|
||||
var along = hue - settings.SunHueExcludedEnd;
|
||||
if (along < 0) along += 1f;
|
||||
counts[Math.Min(bins - 1, (int)(along / arc * bins))]++;
|
||||
}
|
||||
Assert.All(counts, count => Assert.InRange(count, draws / bins - 1, draws / bins + 1));
|
||||
}
|
||||
|
||||
// The body blends its two stops in RGB (Gradient Blend), so the blend is sampled, not just the stops.
|
||||
[Theory]
|
||||
[InlineData(75f, 165f)]
|
||||
[InlineData(55f, 185f)]
|
||||
[InlineData(20f, 250f)]
|
||||
public void TheRampBetweenTheStopsNeverCrossesTheBand(float startDegrees, float endDegrees)
|
||||
{
|
||||
var suns = GenerateSuns(z =>
|
||||
{
|
||||
z.SunHueExcludedStart = startDegrees * Degree;
|
||||
z.SunHueExcludedEnd = endDegrees * Degree;
|
||||
}, out _);
|
||||
Assert.True(suns.Count >= 10, $"only {suns.Count} suns generated");
|
||||
foreach (var sun in suns)
|
||||
{
|
||||
var a = sun.Colors[0].xyz;
|
||||
var b = sun.Colors[1].xyz;
|
||||
for (var step = 0; step <= 40; step++)
|
||||
{
|
||||
var hue = ColorMath.RgbToHsv(a + (b - a) * (step / 40f)).x;
|
||||
Assert.False(ColorMath.HueInBand(hue, startDegrees * Degree + Slack, endDegrees * Degree - Slack),
|
||||
$"the ramp passed through the band (hue {hue / Degree:F1} degrees, step {step}/40)");
|
||||
}
|
||||
foreach (var colourHue in Hues(sun))
|
||||
Assert.False(ColorMath.HueInBand(colourHue, startDegrees * Degree + Slack, endDegrees * Degree - Slack),
|
||||
$"a sun colour fell in the band (hue {colourHue / Degree:F1} degrees)");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<float> Hues(SunData sun) =>
|
||||
sun.Colors.Select(c => c.xyz)
|
||||
.Concat(new[] { sun.FogTintColor, sun.LightColor })
|
||||
.Select(rgb => ColorMath.RgbToHsv(rgb).x);
|
||||
|
||||
// Production path: Galaxy, ItemManager and ZoneGenerator.GenerateZone over the shipped ZoneSettings, as in OrbitsFromFieldTests.
|
||||
private List<SunData> GenerateSuns(Action<ZoneGenerationSettings> adjust, out int zones)
|
||||
{
|
||||
var suns = new List<SunData>();
|
||||
zones = 0;
|
||||
_runs++;
|
||||
for (uint seed = 1; seed <= 8; seed++)
|
||||
{
|
||||
var dir = Path.Combine(_root, $"galaxy{_runs}-{seed}");
|
||||
Directory.CreateDirectory(dir);
|
||||
var catalog = Path.Combine(dir, "Aetheria.cc");
|
||||
File.Copy(TestCatalog.Repo, catalog);
|
||||
var cache = new CultCache(TestCatalog.Registry());
|
||||
_caches.Add(cache);
|
||||
cache.AddBackingStore(new GameCult.Caching.MessagePack.SingleFileMessagePackBackingStore(catalog), AetheriaStores.CatalogTypes);
|
||||
cache.AddBackingStore(new GameCult.Caching.MessagePack.SingleFileMessagePackBackingStore(Path.Combine(dir, "run.cc")), AetheriaStores.RunTypes);
|
||||
var authored = AuthoredSettings.Load(RunStartTests.FindRepoRoot());
|
||||
var tutorial = authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings");
|
||||
var galaxy = new Galaxy(tutorial, authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
|
||||
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"), cache, new PlayerSettings(),
|
||||
Directory.CreateDirectory(Path.Combine(dir, "Narrative")), _ => { }, null, seed);
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), authored.Read<GameplaySettings>("GameplaySettings"), _ => { });
|
||||
var zoneSettings = authored.Read<ZoneGenerationSettings>("ZoneSettings");
|
||||
adjust(zoneSettings);
|
||||
foreach (var zone in galaxy.Zones)
|
||||
{
|
||||
zones++;
|
||||
var pack = ZoneGenerator.GenerateZone(items, zoneSettings, galaxy, zone, false, false);
|
||||
suns.AddRange(pack.Planets.Select(p => cache.Get(p)).OfType<SunData>());
|
||||
}
|
||||
}
|
||||
return suns;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue