Complete portable planetary presentation contracts

This commit is contained in:
Meta Vens 2026-07-13 02:52:13 +01:00
commit 102403efac
74 changed files with 1166 additions and 31 deletions

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bec15fdcbe97a7f44a24bdb3ff79e1e7

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 05e7969589dd09b43bd0fda672d25a0b

View file

@ -1,3 +1,5 @@
using System;
namespace CultMath;
public readonly record struct PlanetaryQueryScale(float FootprintMeters, float MaximumUnresolvedHeight)

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b61d6354e3aab054e94721e77e619bfa

View file

@ -0,0 +1,55 @@
using System;
namespace CultMath;
public readonly record struct PlanetaryGpuPageInput(float4 DirectionRadius, float4 Sampling);
public readonly record struct PlanetaryGpuPageMetadata(float4 Address, float4 Layout, float4 Bounds, float4 State);
public readonly record struct PlanetaryGpuPageContent(
PlanetaryPageLayout Layout,
PlanetaryGpuPageInput[] Inputs,
float SampleSpacing,
float ParentSampleSpacing);
public static class PlanetaryGpuPageBuilder
{
public static PlanetaryGpuPageContent BuildContent(PlanetaryPageLayout layout, float radius)
{
layout.Validate();
if (!float.IsFinite(radius) || radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius));
var spacing = PlanetaryPageSampling.NominalAngularTexelSize(layout) * radius;
var parentSpacing = layout.Tile.Level == 0
? 0
: PlanetaryPageSampling.NominalAngularTexelSize(new(layout.Tile.Parent(), layout.InteriorSize, layout.BorderSize)) * radius;
var inputs = new PlanetaryGpuPageInput[layout.StorageSize * layout.StorageSize];
for (var y = 0; y < layout.StorageSize; y++)
for (var x = 0; x < layout.StorageSize; x++)
{
var direction = PlanetaryPageSampling.Direction(layout, x, y);
inputs[y * layout.StorageSize + x] = new(
new float4(direction, radius),
new float4(spacing, parentSpacing, 0, 0));
}
return new(layout, inputs, spacing, parentSpacing);
}
public static PlanetaryGpuPageMetadata Metadata(
in PlanetaryGpuPageContent content,
int outputOffset,
float blend,
float4 bounds = default)
{
content.Layout.Validate();
if (content.Inputs == null || content.Inputs.Length != content.Layout.StorageSize * content.Layout.StorageSize)
throw new ArgumentException("GPU page content does not match its layout.", nameof(content));
if (outputOffset < 0) throw new ArgumentOutOfRangeException(nameof(outputOffset));
if (!float.IsFinite(blend) || blend is < 0 or > 1) throw new ArgumentOutOfRangeException(nameof(blend));
var tile = content.Layout.Tile;
return new(
new float4((int)tile.Face, tile.Level, tile.X, tile.Y),
new float4(outputOffset, content.Layout.StorageSize, content.Layout.InteriorSize, content.Layout.BorderSize),
bounds,
new float4(1, blend, PlanetaryPageSampling.NominalAngularTexelSize(content.Layout), content.SampleSpacing));
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 42173c60766948a4e8c376687033532e

View file

@ -0,0 +1,117 @@
using System;
using System.IO;
using System.Text;
namespace CultMath;
public static class PlanetaryMapTileEncoding
{
public const uint FormatVersion = 1;
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("CMPT");
public static void Write(Stream stream, in PlanetarySurfaceMapTile tile, bool leaveOpen = false)
{
if (stream == null || !stream.CanWrite) throw new ArgumentException("A writable stream is required.", nameof(stream));
tile.Key.Validate();
if (tile.Samples == null || tile.Validity == null || tile.Samples.Length != tile.Validity.Length)
throw new ArgumentException("Tile samples and validity must have equal lengths.", nameof(tile));
var expectedCount = tile.Key.Layout.StorageSize * tile.Key.Layout.StorageSize;
if (tile.Samples.Length != expectedCount) throw new ArgumentException("Tile payload does not match its layout.", nameof(tile));
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen);
writer.Write(Magic);
writer.Write(FormatVersion);
writer.Write(tile.Key.FieldVersion);
writer.Write(tile.Key.ProjectionVersion);
writer.Write(tile.Key.LayerId);
var projection = tile.Key.Layout.Projection;
writer.Write((int)projection.Kind);
writer.Write(projection.CenterLongitude);
writer.Write(projection.CenterLatitude);
writer.Write(projection.Scale);
writer.Write(tile.Key.Layout.Level);
writer.Write(tile.Key.Layout.X);
writer.Write(tile.Key.Layout.Y);
writer.Write(tile.Key.Layout.InteriorSize);
writer.Write(tile.Key.Layout.BorderSize);
writer.Write(tile.Key.QueryScale.FootprintMeters);
writer.Write(tile.Key.QueryScale.MaximumUnresolvedHeight);
writer.Write(tile.Samples.Length);
for (var i = 0; i < tile.Samples.Length; i++)
{
writer.Write(tile.Validity[i]);
if (!tile.Validity[i]) continue;
WriteSample(writer, tile.Samples[i], tile.Key.FieldVersion);
}
}
public static PlanetarySurfaceMapTile Read(Stream stream, bool leaveOpen = false)
{
if (stream == null || !stream.CanRead) throw new ArgumentException("A readable stream is required.", nameof(stream));
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen);
var magic = reader.ReadBytes(Magic.Length);
if (magic.Length != Magic.Length || !magic.AsSpan().SequenceEqual(Magic)) throw new InvalidDataException("Not a CultMath planetary tile.");
var version = reader.ReadUInt32();
if (version != FormatVersion) throw new InvalidDataException($"Unsupported planetary tile version {version}.");
var fieldVersion = reader.ReadUInt64();
var projectionVersion = reader.ReadUInt32();
var layerId = reader.ReadUInt32();
var kindValue = reader.ReadInt32();
if (!Enum.IsDefined(typeof(PlanetaryProjectionKind), kindValue)) throw new InvalidDataException("Unknown planetary projection kind.");
var projection = new PlanetaryProjectionParameters(
(PlanetaryProjectionKind)kindValue,
reader.ReadDouble(), reader.ReadDouble(), reader.ReadDouble()).Validate();
var layout = new PlanetaryMapTileLayout(
projection,
reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32(),
reader.ReadInt32(), reader.ReadInt32()).Validate();
var scale = new PlanetaryQueryScale(reader.ReadSingle(), reader.ReadSingle()).Validate();
var key = new PlanetaryMapTileKey(fieldVersion, projectionVersion, layerId, layout, scale).Validate();
var count = reader.ReadInt32();
if (count != layout.StorageSize * layout.StorageSize) throw new InvalidDataException("Planetary tile sample count does not match layout.");
var samples = new PlanetarySurfaceSample[count];
var validity = new bool[count];
for (var i = 0; i < count; i++)
{
validity[i] = reader.ReadBoolean();
if (validity[i]) samples[i] = ReadSample(reader, fieldVersion);
}
if (stream.CanSeek && stream.Position != stream.Length) throw new InvalidDataException("Planetary tile has trailing bytes.");
return new(key, samples, validity);
}
private static void WriteSample(BinaryWriter writer, PlanetarySurfaceSample sample, ulong fieldVersion)
{
if (sample.FieldVersion != fieldVersion) throw new ArgumentException("Sample field version does not match tile identity.");
Write(writer, sample.UnitDirection);
writer.Write(sample.Radius);
writer.Write(sample.RadialDisplacement);
Write(writer, sample.TangentGradient);
Write(writer, sample.SurfaceNormal);
writer.Write(sample.Slope);
writer.Write(sample.Ridge);
writer.Write(sample.Gully);
writer.Write(sample.FinestResolvedWavelength);
writer.Write(sample.UnresolvedHeightBound);
}
private static PlanetarySurfaceSample ReadSample(BinaryReader reader, ulong fieldVersion)
{
var direction = ReadFloat3(reader);
var radius = reader.ReadSingle();
var displacement = reader.ReadSingle();
var gradient = ReadFloat3(reader);
var normal = ReadFloat3(reader);
return new(
fieldVersion, direction, radius, displacement, gradient, normal,
reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(),
reader.ReadSingle(), reader.ReadSingle());
}
private static void Write(BinaryWriter writer, float3 value)
{
writer.Write(value.x); writer.Write(value.y); writer.Write(value.z);
}
private static float3 ReadFloat3(BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6898f6de57df20646a0b9077bb923592

View file

@ -1,3 +1,5 @@
using System;
namespace CultMath;
public readonly record struct PlanetaryPageLayout(PlanetaryTileAddress Tile, int InteriorSize, int BorderSize)

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7af9a0354faa56348a2e07188739a0ae

View file

@ -1,3 +1,6 @@
using System;
using System.Linq;
namespace CultMath;
public readonly record struct PlanetaryPatchVertex(float2 LocalCoordinate, float3 UnitDirection);

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 04651571f7d240a418c1f6aafa217fa6

View file

@ -1,3 +1,5 @@
using System;
namespace CultMath;
public enum PlanetaryProjectionKind

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5021202cf626f7442a57961255a8a217

View file

@ -1,3 +1,5 @@
using System;
namespace CultMath;
public readonly record struct PlanetaryRadialBounds(float MinimumDisplacement, float MaximumDisplacement)

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2ec43c567214b8247bda19b5c120c200

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 82e4db521edc253499220458b417fd1e

View file

@ -1,3 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace CultMath;
public readonly record struct PlanetaryLodParameters(
@ -71,9 +75,9 @@ public readonly record struct PlanetaryResidencySnapshot(
/// <summary>Owns presentation residency only. It never owns field content.</summary>
public sealed class PlanetaryResidualResidency
{
private readonly Dictionary<ulong, PlanetaryTileAddress> residents = [];
private readonly Dictionary<ulong, float> arrivals = [];
private readonly Dictionary<ulong, (float Time, float StartingBlend)> departures = [];
private readonly Dictionary<ulong, PlanetaryTileAddress> residents = new();
private readonly Dictionary<ulong, float> arrivals = new();
private readonly Dictionary<ulong, (float Time, float StartingBlend)> departures = new();
private float lastTime = float.NegativeInfinity;
public void Reset()

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0585ea15edd7f1c4fa272df4911a8d6f

View file

@ -1,3 +1,5 @@
using System;
namespace CultMath;
public enum PlanetaryCubeFace
@ -104,6 +106,14 @@ public static class PlanetaryTopology
return math.normalize(cube);
}
public static float3 NormalizedCubeDirection(PlanetaryFaceCoordinate coordinate)
{
coordinate.Validate(allowOutsideFace: true);
var u = (float)coordinate.U;
var v = (float)coordinate.V;
return math.normalize(CubeVector(coordinate.Face, u, v));
}
public static PlanetaryFaceCoordinate FaceCoordinate(float3 direction)
{
ValidateDirection(direction);
@ -156,6 +166,17 @@ public static class PlanetaryTopology
MathF.Atan(cubeU) / QuarterPi,
MathF.Atan(cubeV) / QuarterPi);
private static float3 CubeVector(PlanetaryCubeFace face, float u, float v) => face switch
{
PlanetaryCubeFace.PositiveX => new float3(1, v, -u),
PlanetaryCubeFace.NegativeX => new float3(-1, v, u),
PlanetaryCubeFace.PositiveY => new float3(u, 1, -v),
PlanetaryCubeFace.NegativeY => new float3(u, -1, v),
PlanetaryCubeFace.PositiveZ => new float3(u, v, 1),
PlanetaryCubeFace.NegativeZ => new float3(-u, v, -1),
_ => throw new ArgumentOutOfRangeException(nameof(face)),
};
internal static void ValidateDirection(float3 direction)
{
ValidateFinite(direction, nameof(direction));

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 88f6789c4384f7846ac4be37613fe5ed

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 59ec2db73065f6c4483ad90f5e17850e