diff --git a/.gitignore b/.gitignore index 5ae20c8e..3779002b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ obj/ **/obj/ target/ **/target/ +/native/cultmath-core/target.meta .tools/ diff --git a/CultMath.slnx b/CultMath.slnx index 95834573..05d17886 100644 --- a/CultMath.slnx +++ b/CultMath.slnx @@ -5,4 +5,7 @@ + + + diff --git a/README.md b/README.md index 29eb115f..c60a9912 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,20 @@ cargo test --manifest-path native/cultmath-core/Cargo.toml cargo build --release --manifest-path native/cultmath-core/Cargo.toml ``` +## Unity compiler + +CultMath uses C# 10 record structs and file-scoped namespaces. Unity projects +must select the compiler's latest language mode with an `Assets/csc.rsp` file: + +```text +-langversion:latest +``` + +The package's Unity smoke project verifies this contract against Unity 6. The +setting is already present in current GameCult Unity clients; it belongs to the +consumer project because Unity only reads compiler response files from its +`Assets` root. + ## Shader Tooling CultMath keeps portable DXC outside git under `.tools/`: diff --git a/Samples~/PlanetaryViewer/Editor/CultMath.PlanetaryViewer.Editor.asmdef b/Samples~/PlanetaryViewer/Editor/CultMath.PlanetaryViewer.Editor.asmdef new file mode 100644 index 00000000..10512326 --- /dev/null +++ b/Samples~/PlanetaryViewer/Editor/CultMath.PlanetaryViewer.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "CultMath.PlanetaryViewer.Editor", + "rootNamespace": "CultMath.Unity.Samples.Editor", + "references": [ + "CultMath.PlanetaryViewer" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Samples~/PlanetaryViewer/Editor/CultMathPlanetaryViewerMenu.cs b/Samples~/PlanetaryViewer/Editor/CultMathPlanetaryViewerMenu.cs new file mode 100644 index 00000000..ade28c30 --- /dev/null +++ b/Samples~/PlanetaryViewer/Editor/CultMathPlanetaryViewerMenu.cs @@ -0,0 +1,26 @@ +using UnityEditor; +using UnityEngine; + +namespace CultMath.Unity.Samples.Editor; + +public static class CultMathPlanetaryViewerMenu +{ + [MenuItem("GameObject/CultMath/Planetary Field Viewer", false, 10)] + public static void CreateViewer() + { + var root = new GameObject("CultMath Planetary Field Viewer"); + Undo.RegisterCreatedObjectUndo(root, "Create CultMath Planetary Viewer"); + root.AddComponent(); + Selection.activeGameObject = root; + + if (Camera.main != null) return; + var cameraObject = new GameObject("Planetary Viewer Camera"); + Undo.RegisterCreatedObjectUndo(cameraObject, "Create Planetary Viewer Camera"); + cameraObject.tag = "MainCamera"; + cameraObject.transform.position = new Vector3(0, 0, -24); + cameraObject.transform.LookAt(root.transform); + var camera = cameraObject.AddComponent(); + camera.nearClipPlane = 0.01f; + camera.farClipPlane = 1000; + } +} diff --git a/Samples~/PlanetaryViewer/README.md b/Samples~/PlanetaryViewer/README.md new file mode 100644 index 00000000..ec4dcc3a --- /dev/null +++ b/Samples~/PlanetaryViewer/README.md @@ -0,0 +1,16 @@ +# Planetary Field Viewer + +Import this sample through Unity's Package Manager, then choose +`GameObject > CultMath > Planetary Field Viewer`. + +The host project must contain `Assets/csc.rsp` with +`-langversion:latest`, as documented in the CultMath package README. + +The generated six-face mesh uses `PlanetaryPatch`. Its shader calls +`cultmath_planetary_field_sample` from the packaged CultMath HLSL, while the +component exposes the matching CPU `Sample` method. Seed, radius, physical +sample footprint, erosion settings, and base-field equations are shared between +the two bodies. + +This is a renderer integration fixture, not a gameplay owner. It deliberately +contains no Aetheria state, biomes, resources, or simulation decisions. diff --git a/Samples~/PlanetaryViewer/Runtime/CultMath.PlanetaryViewer.asmdef b/Samples~/PlanetaryViewer/Runtime/CultMath.PlanetaryViewer.asmdef new file mode 100644 index 00000000..2d3676f4 --- /dev/null +++ b/Samples~/PlanetaryViewer/Runtime/CultMath.PlanetaryViewer.asmdef @@ -0,0 +1,17 @@ +{ + "name": "CultMath.PlanetaryViewer", + "rootNamespace": "CultMath.Unity.Samples", + "references": [ + "CultMath", + "CultMath.Unity" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.cs b/Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.cs new file mode 100644 index 00000000..06113728 --- /dev/null +++ b/Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace CultMath.Unity.Samples; + +[ExecuteAlways] +public sealed class CultMathPlanetaryViewer : MonoBehaviour +{ + [Min(0.001f)] public float Radius = 8.4f; + [Range(1, 256)] public int CellsPerFace = 64; + public int Seed = 42; + [Min(0.0001f)] public float SampleFootprint = 0.01f; + public Shader? PlanetShader; + public Vector3 LightDirection = new(-0.4f, 0.7f, -0.6f); + public Color LowColor = new(0.055f, 0.16f, 0.09f, 1); + public Color HighColor = new(0.62f, 0.48f, 0.28f, 1); + public Color RidgeColor = new(0.88f, 0.83f, 0.7f, 1); + + private readonly List meshes = new(); + private Material? material; + private int builtCells; + + public PlanetaryFieldDefinition Definition => PlanetaryFieldDefinition.Create( + 1, + Radius, + Seed, + ViewerErosion(Radius)); + + public PlanetarySurfaceSample Sample(float3 direction) + { + var source = new ViewerBaseField(Radius); + var normalized = math.normalize(direction); + return PlanetaryField.Sample(Definition, normalized, source.Sample(normalized), PlanetaryQueryScale.AtFootprint(SampleFootprint)); + } + + private void OnEnable() => Rebuild(); + + private void OnValidate() + { + Radius = Mathf.Max(Radius, 0.001f); + CellsPerFace = Mathf.Clamp(CellsPerFace, 1, 256); + SampleFootprint = Mathf.Max(SampleFootprint, 0.0001f); + if (isActiveAndEnabled) Rebuild(); + } + + private void Update() + { + if (builtCells != CellsPerFace || material == null) Rebuild(); + ApplyMaterial(); + } + + private void Rebuild() + { + ClearGenerated(); + PlanetShader ??= Shader.Find("CultMath/Planetary Field Viewer"); + if (PlanetShader == null) return; + material = new Material(PlanetShader) { name = "CultMath Planetary Viewer Material", hideFlags = HideFlags.HideAndDontSave }; + foreach (var face in (PlanetaryCubeFace[])Enum.GetValues(typeof(PlanetaryCubeFace))) + { + var mesh = PlanetaryPatchMeshAdapter.CreateFaceMesh(face, CellsPerFace); + mesh.hideFlags = HideFlags.HideAndDontSave; + meshes.Add(mesh); + var child = new GameObject(face.ToString()) { hideFlags = HideFlags.DontSave }; + child.transform.SetParent(transform, false); + child.AddComponent().sharedMesh = mesh; + child.AddComponent().sharedMaterial = material; + } + builtCells = CellsPerFace; + ApplyMaterial(); + } + + private void ApplyMaterial() + { + if (material == null) return; + var p = ViewerErosion(Radius); + material.SetFloat("_Radius", Radius); + material.SetFloat("_SampleFootprint", SampleFootprint); + material.SetInt("_Seed", Seed); + material.SetFloat("_ErosionScale", p.Scale); + material.SetFloat("_ErosionStrength", p.Strength); + material.SetFloat("_GullyWeight", p.GullyWeight); + material.SetFloat("_Detail", p.Detail); + material.SetVector("_Rounding", new(p.Rounding.x, p.Rounding.y, p.Rounding.z, p.Rounding.w)); + material.SetVector("_Onset", new(p.Onset.x, p.Onset.y, p.Onset.z, p.Onset.w)); + material.SetVector("_AssumedSlope", new(p.AssumedSlope.x, p.AssumedSlope.y, 0, 0)); + material.SetFloat("_CellScale", p.CellScale); + material.SetFloat("_Normalization", p.Normalization); + material.SetInt("_Octaves", p.Octaves); + material.SetFloat("_Lacunarity", p.Lacunarity); + material.SetFloat("_Gain", p.Gain); + material.SetVector("_LightDirection", LightDirection.normalized); + material.SetColor("_LowColor", LowColor); + material.SetColor("_HighColor", HighColor); + material.SetColor("_RidgeColor", RidgeColor); + } + + private void OnDisable() => ClearGenerated(); + private void OnDestroy() => ClearGenerated(); + + private void ClearGenerated() + { + for (var i = transform.childCount - 1; i >= 0; i--) DestroyGenerated(transform.GetChild(i).gameObject); + foreach (var mesh in meshes) DestroyGenerated(mesh); + meshes.Clear(); + if (material != null) DestroyGenerated(material); + material = null; + builtCells = 0; + } + + private static void DestroyGenerated(UnityEngine.Object value) + { + if (Application.isPlaying) Destroy(value); + else DestroyImmediate(value); + } + + private static AdvancedErosionParameters ViewerErosion(float radius) => new( + radius * 0.075f, 0.12f, 0.58f, 1.45f, + new float4(0.1f, 0.015f, 0.1f, 2), + new float4(1.25f, 1.25f, 2.8f, 1.5f), + new float2(0.7f, 0.85f), 0.7f, 0.5f, 7, 2, 0.5f); + + private readonly struct ViewerBaseField : IPlanetaryBaseField + { + private readonly float radius; + public ViewerBaseField(float radius) => this.radius = radius; + + public PlanetaryBaseFieldSample Sample(float3 direction) + { + var value = direction.x * 0.6f + direction.y * direction.z * 0.3f; + var angularGradient = new float3(0.6f, direction.z * 0.3f, direction.y * 0.3f); + angularGradient -= direction * math.dot(angularGradient, direction); + var fieldGradient = angularGradient / radius; + var radialDisplacement = value * radius * 0.035f; + var radialGradient = angularGradient * 0.035f; + return new(radialDisplacement, radialGradient, value, fieldGradient, math.clamp(value, -1, 1)); + } + } +} diff --git a/Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.shader b/Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.shader new file mode 100644 index 00000000..b5f7274f --- /dev/null +++ b/Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.shader @@ -0,0 +1,90 @@ +Shader "CultMath/Planetary Field Viewer" +{ + Properties + { + _LowColor ("Low Color", Color) = (0.055, 0.16, 0.09, 1) + _HighColor ("High Color", Color) = (0.62, 0.48, 0.28, 1) + _RidgeColor ("Ridge Color", Color) = (0.88, 0.83, 0.7, 1) + } + SubShader + { + Tags { "RenderType"="Opaque" "Queue"="Geometry" } + Pass + { + Cull Back + ZWrite On + HLSLPROGRAM + #pragma target 5.0 + #pragma vertex vert + #pragma fragment frag + #include "UnityCG.cginc" + #include "Packages/org.gamecult.cultmath/shaders/CultMath.hlsl" + + float _Radius; + float _SampleFootprint; + int _Seed; + float _ErosionScale, _ErosionStrength, _GullyWeight, _Detail; + float4 _Rounding, _Onset, _AssumedSlope; + float _CellScale, _Normalization, _Lacunarity, _Gain; + int _Octaves; + float3 _LightDirection; + float4 _LowColor, _HighColor, _RidgeColor; + + struct Attributes { float4 vertex : POSITION; }; + struct Varyings + { + float4 position : SV_POSITION; + float3 worldNormal : TEXCOORD0; + float height : TEXCOORD1; + float ridge : TEXCOORD2; + }; + + CultMathPlanetarySurfaceSample viewer_sample(float3 direction) + { + direction = normalize(direction); + float value = direction.x * 0.6 + direction.y * direction.z * 0.3; + float3 angular_gradient = float3(0.6, direction.z * 0.3, direction.y * 0.3); + angular_gradient -= direction * dot(angular_gradient, direction); + CultMathPlanetaryBaseFieldSample base_sample; + base_sample.radial_displacement = value * _Radius * 0.035; + base_sample.radial_gradient = angular_gradient * 0.035; + base_sample.field_value = value; + base_sample.field_gradient = angular_gradient / _Radius; + base_sample.fade_target = clamp(value, -1.0, 1.0); + CultMathAdvancedErosionParameters erosion; + erosion.scale = _ErosionScale; erosion.strength = _ErosionStrength; + erosion.gully_weight = _GullyWeight; erosion.detail = _Detail; + erosion.rounding = _Rounding; erosion.onset = _Onset; + erosion.assumed_slope = _AssumedSlope.xy; erosion.cell_scale = _CellScale; + erosion.normalization = _Normalization; erosion.octaves = _Octaves; + erosion.lacunarity = _Lacunarity; erosion.gain = _Gain; + CultMathPlanetaryFieldDefinition definition; + definition.radius = _Radius; definition.seed = _Seed; definition.erosion = erosion; + return cultmath_planetary_field_sample(definition, direction, base_sample, _SampleFootprint); + } + + Varyings vert(Attributes input) + { + float3 direction = normalize(input.vertex.xyz); + CultMathPlanetarySurfaceSample sample = viewer_sample(direction); + float3 local = direction * (_Radius + sample.radial_displacement); + Varyings output; + output.position = UnityObjectToClipPos(float4(local, 1)); + output.worldNormal = UnityObjectToWorldNormal(sample.surface_normal); + output.height = sample.radial_displacement / max(_Radius * 0.1, 1.0e-5); + output.ridge = sample.ridge; + return output; + } + + float4 frag(Varyings input) : SV_Target + { + float height = saturate(input.height * 0.5 + 0.5); + float3 albedo = lerp(_LowColor.rgb, _HighColor.rgb, height); + albedo = lerp(albedo, _RidgeColor.rgb, saturate((input.ridge - 0.55) * 2.5)); + float diffuse = saturate(dot(normalize(input.worldNormal), normalize(-_LightDirection))) * 0.82 + 0.08; + return float4(albedo * diffuse, 1); + } + ENDHLSL + } + } +} diff --git a/THIRD-PARTY-NOTICES.md.meta b/THIRD-PARTY-NOTICES.md.meta new file mode 100644 index 00000000..4b43ddaa --- /dev/null +++ b/THIRD-PARTY-NOTICES.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7c28c8631c7d4054eb751d7ce952df33 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tools~/CultMath.PlanetaryTileFixture/CultMath.PlanetaryTileFixture.csproj b/Tools~/CultMath.PlanetaryTileFixture/CultMath.PlanetaryTileFixture.csproj new file mode 100644 index 00000000..9a6f8bdd --- /dev/null +++ b/Tools~/CultMath.PlanetaryTileFixture/CultMath.PlanetaryTileFixture.csproj @@ -0,0 +1,11 @@ + + + Exe + net10.0 + enable + enable + + + + + diff --git a/Tools~/CultMath.PlanetaryTileFixture/Program.cs b/Tools~/CultMath.PlanetaryTileFixture/Program.cs new file mode 100644 index 00000000..1952fa2d --- /dev/null +++ b/Tools~/CultMath.PlanetaryTileFixture/Program.cs @@ -0,0 +1,22 @@ +using CultMath; + +if (args.Length != 1) throw new ArgumentException("Expected one output path."); +var field = PlanetaryFieldDefinition.Create(0x55aa, 10, 7, AdvancedErosionParameters.Default); +var layout = new PlanetaryMapTileLayout(new(PlanetaryProjectionKind.EqualEarth), 0, 0, 0, 5, 1); +var key = new PlanetaryMapTileKey(field.FieldVersion, 3, 29, layout, PlanetaryQueryScale.AtFootprint(0.25f, 0.125f)); +var tile = PlanetaryMapTileBaker.Bake(field, key, new FixtureField()); +var path = Path.GetFullPath(args[0]); +Directory.CreateDirectory(Path.GetDirectoryName(path)!); +using var stream = File.Create(path); +PlanetaryMapTileEncoding.Write(stream, tile); + +readonly struct FixtureField : IPlanetaryBaseField +{ + public PlanetaryBaseFieldSample Sample(float3 direction) + { + var value = direction.x * 0.2f + direction.z * 0.4f; + var gradient = new float3(0.2f, 0, 0.4f); + gradient -= direction * math.dot(gradient, direction); + return new(value, gradient, value, gradient * 0.1f, value); + } +} diff --git a/docs/planetary.md b/docs/planetary.md index 8196c1c7..3503429b 100644 --- a/docs/planetary.md +++ b/docs/planetary.md @@ -80,11 +80,17 @@ coarse meshes from the canonical topology. HLSL calls Renderers still own cameras, clip space, depth, materials, buffers, dispatch, and draw submission. +`PlanetaryGpuPageBuilder` owns the renderer-neutral page input and metadata +payload. Host adapters may convert its `float4` values to native vector types, +but do not recalculate directions, spacing, addressing, or transition state. + The optional `CultMath.Unity` assembly converts a canonical face patch into a -Unity `Mesh` and converts a residency snapshot into Unity-native page input and -metadata payloads. It owns no field parameters, page selection, or simulation -state. Unity render code remains responsible for allocating/binding graphics -buffers and dispatching application-specific base-field entry points. +Unity `Mesh` and converts the shared GPU payload into Unity-native structs. It +owns no field parameters, page selection, or simulation state. The package's +Planetary Field Viewer sample renders the shared HLSL field and reports matching +CPU queries. Unity render code remains responsible for allocating/binding +graphics buffers and dispatching application-specific base-field entry points. +The host Unity project must set `-langversion:latest` in `Assets/csc.rsp`. ## Maps @@ -103,6 +109,12 @@ projection and canonical field query. `PlanetaryMapTileKey` includes field, projection, layer, layout, and query-scale identity. Palettes, contours, labels, and strategic overlays do not enter CultMath. +`PlanetaryMapTileEncoding` writes the resulting evidence as a versioned +little-endian `CMPT` binary asset. `web/planetary-tile.ts` decodes that exact +format into browser-native values without defining another terrain generator. +Typed CultMesh documents should carry the tile key and asset reference; the +binary payload is derived CDN/cache data, not committed game state. + ## Numeric contract Integer hashes and published field versions are exact. Basic arithmetic follows @@ -111,7 +123,8 @@ CultMath's matched C#/HLSL semantics. Hardware transcendental functions such as Every filtered terrain query carries an unresolved-height bound so consumers can reject insufficient fidelity explicitly. -Current D3D12 parity evidence covers the advanced erosion kernel and the QSC -forward/inverse topology. The remaining production page and patch shaders use -the common functions and retain their seam, residual, summary, and radial-hit -integration probes in Fensalir. +Current D3D12 parity evidence covers the advanced erosion kernel, QSC topology, +and forward/inverse transforms for every supported map projection, including +center and scale parameters. Production page and patch shaders use the common +functions and retain their seam, residual, summary, and radial-hit integration +probes in Fensalir. diff --git a/docs/planetary.md.meta b/docs/planetary.md.meta new file mode 100644 index 00000000..af364c25 --- /dev/null +++ b/docs/planetary.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 23a5751e4cececc4e99e024058d69add +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json index 6da2a17a..25643608 100644 --- a/package.json +++ b/package.json @@ -10,5 +10,12 @@ "Simulation" ], "license": "MPL-2.0", - "category": "Scripting" + "category": "Scripting", + "samples": [ + { + "displayName": "Planetary Field Viewer", + "description": "Runnable Unity viewer using the canonical CultMath CPU and HLSL planetary field.", + "path": "Samples~/PlanetaryViewer" + } + ] } diff --git a/scripts.meta b/scripts.meta new file mode 100644 index 00000000..6f03c566 --- /dev/null +++ b/scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0ddff5b782cf14e43ae5cda048e040d8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/scripts/test-web-planetary-tile.ps1 b/scripts/test-web-planetary-tile.ps1 new file mode 100644 index 00000000..5221fb36 --- /dev/null +++ b/scripts/test-web-planetary-tile.ps1 @@ -0,0 +1,9 @@ +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$fixture = Join-Path $root ".tools\planetary-tile\fixture.cmpt" +dotnet run --project (Join-Path $root "Tools~\CultMath.PlanetaryTileFixture\CultMath.PlanetaryTileFixture.csproj") -- $fixture +if ($LASTEXITCODE -ne 0) { throw "Planetary tile fixture generation failed." } +node --no-warnings --experimental-strip-types (Join-Path $root "web\planetary-tile.cross-runtime.test.mjs") $fixture +if ($LASTEXITCODE -ne 0) { throw "Planetary tile web decoding failed." } +node --no-warnings --experimental-strip-types (Join-Path $root "web\planetary-tile.test.mjs") +if ($LASTEXITCODE -ne 0) { throw "Planetary tile decoder unit test failed." } diff --git a/scripts/test-web-planetary-tile.ps1.meta b/scripts/test-web-planetary-tile.ps1.meta new file mode 100644 index 00000000..ffd73234 --- /dev/null +++ b/scripts/test-web-planetary-tile.ps1.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 235e8f203bd1c4d40a8fa211273106ba +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/shaders/AdvancedErosionFilter.hlsl.meta b/shaders/AdvancedErosionFilter.hlsl.meta new file mode 100644 index 00000000..a354e330 --- /dev/null +++ b/shaders/AdvancedErosionFilter.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3252ac02e8853ef4b9d1fbc5204b5c6b +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/shaders/Planetary.hlsl b/shaders/Planetary.hlsl index 729c6332..81e312b7 100644 --- a/shaders/Planetary.hlsl +++ b/shaders/Planetary.hlsl @@ -120,6 +120,138 @@ float3 cultmath_planetary_equal_earth_inverse(float2 coordinate) return cultmath_normalize(float3(cos_latitude * cos(longitude), cos_latitude * sin(longitude), sin(latitude))); } +float cultmath_planetary_wrap_longitude(float longitude) +{ + longitude = fmod(longitude, CULTMATH_TAU); + if (longitude > CULTMATH_PI) longitude -= CULTMATH_TAU; + if (longitude < -CULTMATH_PI) longitude += CULTMATH_TAU; + return longitude; +} + +float3 cultmath_planetary_lon_lat_direction(float longitude, float latitude) +{ + float cos_latitude = cos(latitude); + return cultmath_normalize(float3(cos_latitude * cos(longitude), cos_latitude * sin(longitude), sin(latitude))); +} + +float cultmath_planetary_asinh(float value) { return log(value + sqrt(value * value + 1.0)); } +float cultmath_planetary_sinh(float value) { return 0.5 * (exp(value) - exp(-value)); } + +bool cultmath_planetary_azimuthal_forward(float3 direction, float2 center_lon_lat, int kind, out float2 coordinate) +{ + direction = cultmath_normalize(direction); + float longitude = cultmath_planetary_wrap_longitude(atan2(direction.y, direction.x) - center_lon_lat.x); + float latitude = asin(clamp(direction.z, -1.0, 1.0)); + float sin0 = sin(center_lon_lat.y), cos0 = cos(center_lon_lat.y); + float sin_latitude = sin(latitude), cos_latitude = cos(latitude), cos_longitude = cos(longitude); + float cos_c = clamp(sin0 * sin_latitude + cos0 * cos_latitude * cos_longitude, -1.0, 1.0); + float2 raw = float2(cos_latitude * sin(longitude), cos0 * sin_latitude - sin0 * cos_latitude * cos_longitude); + float factor = 1.0; + if (kind == 3) + { + if (cos_c < 0.0) { coordinate = 0.0; return false; } + } + else if (kind == 4) + { + float c = acos(cos_c); factor = (c < 1.0e-6 ? 1.0 : c / sin(c)) / CULTMATH_PI; + } + else if (kind == 5) factor = sqrt(2.0 / max(1.0 + cos_c, 1.0e-20)) * 0.5; + else + { + if (cos_c <= 0.0) { coordinate = 0.0; return false; } + factor = 1.0 / cos_c; + } + coordinate = raw * factor; + return true; +} + +bool cultmath_planetary_azimuthal_inverse(float2 coordinate, float2 center_lon_lat, int kind, out float3 direction) +{ + float rho = length(coordinate); + float c; + if (kind == 3) { if (rho > 1.0) { direction = 0.0; return false; } c = asin(clamp(rho, 0.0, 1.0)); } + else if (kind == 4) { if (rho > 1.0) { direction = 0.0; return false; } c = rho * CULTMATH_PI; } + else if (kind == 5) { if (rho > 1.0) { direction = 0.0; return false; } c = 2.0 * asin(clamp(rho, 0.0, 1.0)); } + else c = atan(rho); + if (rho < 1.0e-6) { direction = cultmath_planetary_lon_lat_direction(center_lon_lat.x, center_lon_lat.y); return true; } + float sin_c = sin(c), cos_c = cos(c), sin0 = sin(center_lon_lat.y), cos0 = cos(center_lon_lat.y); + float latitude = asin(clamp(cos_c * sin0 + coordinate.y * sin_c * cos0 / rho, -1.0, 1.0)); + float delta_longitude = atan2(coordinate.x * sin_c, rho * cos0 * cos_c - coordinate.y * sin0 * sin_c); + direction = cultmath_planetary_lon_lat_direction(cultmath_planetary_wrap_longitude(delta_longitude + center_lon_lat.x), latitude); + return true; +} + +bool cultmath_planetary_cube_atlas_forward(float3 direction, out float2 coordinate) +{ + int face; + float2 face_coordinate = cultmath_planetary_face_coordinate(direction, face); + int column = face % 3; + int row = face / 3; + coordinate = float2(-1.0 + (column + (face_coordinate.x + 1.0) * 0.5) * (2.0 / 3.0), 1.0 - (row + (face_coordinate.y + 1.0) * 0.5)); + return true; +} + +bool cultmath_planetary_cube_atlas_inverse(float2 coordinate, out float3 direction) +{ + if (any(coordinate < -1.0) || any(coordinate > 1.0)) { direction = 0.0; return false; } + int column = min((int)((coordinate.x + 1.0) * 1.5), 2); + int row = min((int)(1.0 - coordinate.y), 1); + float u = ((coordinate.x + 1.0) * 1.5 - column) * 2.0 - 1.0; + float v = ((1.0 - coordinate.y) - row) * 2.0 - 1.0; + direction = cultmath_planetary_face_direction(row * 3 + column, float2(u, v)); + return true; +} + +bool cultmath_planetary_projection_forward(float3 direction, int kind, float3 center_lon_lat_scale, out float2 coordinate) +{ + float scale = max(center_lon_lat_scale.z, 1.0e-20); + if (kind == 6) + { + bool valid = cultmath_planetary_cube_atlas_forward(direction, coordinate); + coordinate /= scale; return valid; + } + if (kind >= 3) + { + bool valid = cultmath_planetary_azimuthal_forward(direction, center_lon_lat_scale.xy, kind, coordinate); + coordinate /= scale; return valid; + } + direction = cultmath_normalize(direction); + float longitude = cultmath_planetary_wrap_longitude(atan2(direction.y, direction.x) - center_lon_lat_scale.x); + float latitude = asin(clamp(direction.z, -1.0, 1.0)); + if (kind == 0) coordinate = float2(longitude / CULTMATH_PI, latitude / CULTMATH_HALF_PI); + else if (kind == 1) + { + const float maximum_latitude = 1.4844222297453324; + if (abs(latitude) > maximum_latitude) { coordinate = 0.0; return false; } + coordinate = float2(longitude / CULTMATH_PI, cultmath_planetary_asinh(tan(latitude)) / CULTMATH_PI); + } + else + { + float3 centered = cultmath_planetary_lon_lat_direction(longitude, latitude); + coordinate = cultmath_planetary_equal_earth_forward(centered); + } + coordinate /= scale; + return true; +} + +bool cultmath_planetary_projection_inverse(float2 coordinate, int kind, float3 center_lon_lat_scale, out float3 direction) +{ + coordinate *= max(center_lon_lat_scale.z, 1.0e-20); + if (kind == 6) return cultmath_planetary_cube_atlas_inverse(coordinate, direction); + if (kind >= 3) return cultmath_planetary_azimuthal_inverse(coordinate, center_lon_lat_scale.xy, kind, direction); + if (any(abs(coordinate) > 1.0)) { direction = 0.0; return false; } + float longitude, latitude; + if (kind == 0) { longitude = coordinate.x * CULTMATH_PI; latitude = coordinate.y * CULTMATH_HALF_PI; } + else if (kind == 1) { longitude = coordinate.x * CULTMATH_PI; latitude = atan(cultmath_planetary_sinh(coordinate.y * CULTMATH_PI)); } + else + { + float3 centered = cultmath_planetary_equal_earth_inverse(coordinate); + longitude = atan2(centered.y, centered.x); latitude = asin(clamp(centered.z, -1.0, 1.0)); + } + direction = cultmath_planetary_lon_lat_direction(cultmath_planetary_wrap_longitude(longitude + center_lon_lat_scale.x), latitude); + return true; +} + struct CultMathPlanetaryFieldDefinition { float radius; diff --git a/shaders/Planetary.hlsl.meta b/shaders/Planetary.hlsl.meta new file mode 100644 index 00000000..f7771355 --- /dev/null +++ b/shaders/Planetary.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: bb055ad5328888e479e90ae3fe62c3fe +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/CultMath/AdvancedErosionFilter.cs.meta b/src/CultMath/AdvancedErosionFilter.cs.meta new file mode 100644 index 00000000..52f7ca19 --- /dev/null +++ b/src/CultMath/AdvancedErosionFilter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bec15fdcbe97a7f44a24bdb3ff79e1e7 \ No newline at end of file diff --git a/src/CultMath/ErosionFrequencyBands.cs.meta b/src/CultMath/ErosionFrequencyBands.cs.meta new file mode 100644 index 00000000..cae99643 --- /dev/null +++ b/src/CultMath/ErosionFrequencyBands.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 05e7969589dd09b43bd0fda672d25a0b \ No newline at end of file diff --git a/src/CultMath/PlanetaryField.cs b/src/CultMath/PlanetaryField.cs index 13f36c0a..4d5596e5 100644 --- a/src/CultMath/PlanetaryField.cs +++ b/src/CultMath/PlanetaryField.cs @@ -1,3 +1,5 @@ +using System; + namespace CultMath; public readonly record struct PlanetaryQueryScale(float FootprintMeters, float MaximumUnresolvedHeight) diff --git a/src/CultMath/PlanetaryField.cs.meta b/src/CultMath/PlanetaryField.cs.meta new file mode 100644 index 00000000..3e83f637 --- /dev/null +++ b/src/CultMath/PlanetaryField.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b61d6354e3aab054e94721e77e619bfa \ No newline at end of file diff --git a/src/CultMath/PlanetaryGpuPages.cs b/src/CultMath/PlanetaryGpuPages.cs new file mode 100644 index 00000000..024d26b0 --- /dev/null +++ b/src/CultMath/PlanetaryGpuPages.cs @@ -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)); + } +} diff --git a/src/CultMath/PlanetaryGpuPages.cs.meta b/src/CultMath/PlanetaryGpuPages.cs.meta new file mode 100644 index 00000000..7afe847c --- /dev/null +++ b/src/CultMath/PlanetaryGpuPages.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 42173c60766948a4e8c376687033532e \ No newline at end of file diff --git a/src/CultMath/PlanetaryMapTileEncoding.cs b/src/CultMath/PlanetaryMapTileEncoding.cs new file mode 100644 index 00000000..ec556617 --- /dev/null +++ b/src/CultMath/PlanetaryMapTileEncoding.cs @@ -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()); +} diff --git a/src/CultMath/PlanetaryMapTileEncoding.cs.meta b/src/CultMath/PlanetaryMapTileEncoding.cs.meta new file mode 100644 index 00000000..76484f65 --- /dev/null +++ b/src/CultMath/PlanetaryMapTileEncoding.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6898f6de57df20646a0b9077bb923592 \ No newline at end of file diff --git a/src/CultMath/PlanetaryPages.cs b/src/CultMath/PlanetaryPages.cs index c9cff51f..699a5c1d 100644 --- a/src/CultMath/PlanetaryPages.cs +++ b/src/CultMath/PlanetaryPages.cs @@ -1,3 +1,5 @@ +using System; + namespace CultMath; public readonly record struct PlanetaryPageLayout(PlanetaryTileAddress Tile, int InteriorSize, int BorderSize) diff --git a/src/CultMath/PlanetaryPages.cs.meta b/src/CultMath/PlanetaryPages.cs.meta new file mode 100644 index 00000000..91fdc27d --- /dev/null +++ b/src/CultMath/PlanetaryPages.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7af9a0354faa56348a2e07188739a0ae \ No newline at end of file diff --git a/src/CultMath/PlanetaryPatch.cs b/src/CultMath/PlanetaryPatch.cs index 0fd3cbb6..30c852ff 100644 --- a/src/CultMath/PlanetaryPatch.cs +++ b/src/CultMath/PlanetaryPatch.cs @@ -1,3 +1,6 @@ +using System; +using System.Linq; + namespace CultMath; public readonly record struct PlanetaryPatchVertex(float2 LocalCoordinate, float3 UnitDirection); diff --git a/src/CultMath/PlanetaryPatch.cs.meta b/src/CultMath/PlanetaryPatch.cs.meta new file mode 100644 index 00000000..efd8156a --- /dev/null +++ b/src/CultMath/PlanetaryPatch.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 04651571f7d240a418c1f6aafa217fa6 \ No newline at end of file diff --git a/src/CultMath/PlanetaryProjection.cs b/src/CultMath/PlanetaryProjection.cs index f849fe20..a82ebfdb 100644 --- a/src/CultMath/PlanetaryProjection.cs +++ b/src/CultMath/PlanetaryProjection.cs @@ -1,3 +1,5 @@ +using System; + namespace CultMath; public enum PlanetaryProjectionKind diff --git a/src/CultMath/PlanetaryProjection.cs.meta b/src/CultMath/PlanetaryProjection.cs.meta new file mode 100644 index 00000000..7b1e6654 --- /dev/null +++ b/src/CultMath/PlanetaryProjection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5021202cf626f7442a57961255a8a217 \ No newline at end of file diff --git a/src/CultMath/PlanetaryQueries.cs b/src/CultMath/PlanetaryQueries.cs index 1f11ca77..54a9a3e9 100644 --- a/src/CultMath/PlanetaryQueries.cs +++ b/src/CultMath/PlanetaryQueries.cs @@ -1,3 +1,5 @@ +using System; + namespace CultMath; public readonly record struct PlanetaryRadialBounds(float MinimumDisplacement, float MaximumDisplacement) diff --git a/src/CultMath/PlanetaryQueries.cs.meta b/src/CultMath/PlanetaryQueries.cs.meta new file mode 100644 index 00000000..6fd11413 --- /dev/null +++ b/src/CultMath/PlanetaryQueries.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2ec43c567214b8247bda19b5c120c200 \ No newline at end of file diff --git a/src/CultMath/PlanetaryRadialRefinement.cs.meta b/src/CultMath/PlanetaryRadialRefinement.cs.meta new file mode 100644 index 00000000..32af2655 --- /dev/null +++ b/src/CultMath/PlanetaryRadialRefinement.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 82e4db521edc253499220458b417fd1e \ No newline at end of file diff --git a/src/CultMath/PlanetaryResidency.cs b/src/CultMath/PlanetaryResidency.cs index e5dca115..6918ac57 100644 --- a/src/CultMath/PlanetaryResidency.cs +++ b/src/CultMath/PlanetaryResidency.cs @@ -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( /// Owns presentation residency only. It never owns field content. public sealed class PlanetaryResidualResidency { - private readonly Dictionary residents = []; - private readonly Dictionary arrivals = []; - private readonly Dictionary departures = []; + private readonly Dictionary residents = new(); + private readonly Dictionary arrivals = new(); + private readonly Dictionary departures = new(); private float lastTime = float.NegativeInfinity; public void Reset() diff --git a/src/CultMath/PlanetaryResidency.cs.meta b/src/CultMath/PlanetaryResidency.cs.meta new file mode 100644 index 00000000..24347624 --- /dev/null +++ b/src/CultMath/PlanetaryResidency.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0585ea15edd7f1c4fa272df4911a8d6f \ No newline at end of file diff --git a/src/CultMath/PlanetaryTopology.cs b/src/CultMath/PlanetaryTopology.cs index c8535079..57d562cc 100644 --- a/src/CultMath/PlanetaryTopology.cs +++ b/src/CultMath/PlanetaryTopology.cs @@ -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)); diff --git a/src/CultMath/PlanetaryTopology.cs.meta b/src/CultMath/PlanetaryTopology.cs.meta new file mode 100644 index 00000000..411a8bdb --- /dev/null +++ b/src/CultMath/PlanetaryTopology.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 88f6789c4384f7846ac4be37613fe5ed \ No newline at end of file diff --git a/src/CultMath/SphericalErosion.cs.meta b/src/CultMath/SphericalErosion.cs.meta new file mode 100644 index 00000000..82db7730 --- /dev/null +++ b/src/CultMath/SphericalErosion.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 59ec2db73065f6c4483ad90f5e17850e \ No newline at end of file diff --git a/tests/CultMath.Tests/AdvancedErosionFilterTests.cs.meta b/tests/CultMath.Tests/AdvancedErosionFilterTests.cs.meta new file mode 100644 index 00000000..253ee425 --- /dev/null +++ b/tests/CultMath.Tests/AdvancedErosionFilterTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a7c85309318ae3f4ba75ac28a4f661cf \ No newline at end of file diff --git a/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs.meta b/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs.meta new file mode 100644 index 00000000..ef15b7ee --- /dev/null +++ b/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 095f8ed13d2f2f74baefd23310395256 \ No newline at end of file diff --git a/tests/CultMath.Tests/HlslMirrorTests.cs b/tests/CultMath.Tests/HlslMirrorTests.cs index 567b038b..5e1451cd 100644 --- a/tests/CultMath.Tests/HlslMirrorTests.cs +++ b/tests/CultMath.Tests/HlslMirrorTests.cs @@ -78,6 +78,8 @@ public sealed class HlslMirrorTests Assert.Contains("cultmath_planetary_field_sample", planetary); Assert.Contains("cultmath_planetary_equirectangular_forward", planetary); Assert.Contains("cultmath_planetary_equal_earth_forward", planetary); + Assert.Contains("cultmath_planetary_projection_forward", planetary); + Assert.Contains("cultmath_planetary_projection_inverse", planetary); Assert.Contains("CultMathPlanetarySurfaceSample", planetary); } diff --git a/tests/CultMath.Tests/PlanetaryFieldTests.cs.meta b/tests/CultMath.Tests/PlanetaryFieldTests.cs.meta new file mode 100644 index 00000000..1710b5ff --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryFieldTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c747cfda80df21d41a57412d8d07c08a \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryGpuPageTests.cs b/tests/CultMath.Tests/PlanetaryGpuPageTests.cs new file mode 100644 index 00000000..ea17b4b9 --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryGpuPageTests.cs @@ -0,0 +1,25 @@ +using Xunit; + +namespace CultMath.Tests; + +public sealed class PlanetaryGpuPageTests +{ + [Fact] + public void UploadContentUsesCanonicalDirectionsSpacingAndMetadata() + { + var layout = new PlanetaryPageLayout(new(PlanetaryCubeFace.NegativeY, 3, 4, 2), 9, 2); + var content = PlanetaryGpuPageBuilder.BuildContent(layout, 8.4f); + Assert.Equal(layout.StorageSize * layout.StorageSize, content.Inputs.Length); + Assert.True(content.ParentSampleSpacing > content.SampleSpacing); + var first = content.Inputs[0]; + Assert.Equal(PlanetaryPageSampling.Direction(layout, 0, 0), first.DirectionRadius.xyz); + Assert.Equal(8.4f, first.DirectionRadius.w); + Assert.Equal(content.SampleSpacing, first.Sampling.x); + Assert.Equal(content.ParentSampleSpacing, first.Sampling.y); + + var metadata = PlanetaryGpuPageBuilder.Metadata(content, 37, 0.25f); + Assert.Equal(new float4((int)layout.Tile.Face, layout.Tile.Level, layout.Tile.X, layout.Tile.Y), metadata.Address); + Assert.Equal(new float4(37, layout.StorageSize, layout.InteriorSize, layout.BorderSize), metadata.Layout); + Assert.Equal(0.25f, metadata.State.y); + } +} diff --git a/tests/CultMath.Tests/PlanetaryGpuPageTests.cs.meta b/tests/CultMath.Tests/PlanetaryGpuPageTests.cs.meta new file mode 100644 index 00000000..00f4a3a5 --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryGpuPageTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 87266638e8010f546ab6c430a712868b \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs b/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs new file mode 100644 index 00000000..112cb31f --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs @@ -0,0 +1,52 @@ +using System.IO; +using Xunit; + +namespace CultMath.Tests; + +public sealed class PlanetaryMapTileEncodingTests +{ + [Fact] + public void BinaryTileRoundTripsIdentityValidityAndSurfaceEvidence() + { + var field = PlanetaryFieldDefinition.Create(5, 10, 3, AdvancedErosionParameters.Default); + var layout = new PlanetaryMapTileLayout(new(PlanetaryProjectionKind.EqualEarth), 1, 1, 0, 5, 1); + var key = new PlanetaryMapTileKey(field.FieldVersion, 2, 17, layout, PlanetaryQueryScale.AtFootprint(0.25f, 0.1f)); + var tile = PlanetaryMapTileBaker.Bake(field, key, new FlatField()); + using var bytes = new MemoryStream(); + PlanetaryMapTileEncoding.Write(bytes, tile, leaveOpen: true); + bytes.Position = 0; + var decoded = PlanetaryMapTileEncoding.Read(bytes); + + Assert.Equal(tile.Key, decoded.Key); + Assert.Equal(tile.Validity, decoded.Validity); + for (var i = 0; i < tile.Samples.Length; i++) + if (tile.Validity[i]) Assert.Equal(tile.Samples[i], decoded.Samples[i]); + } + + [Fact] + public void DecoderRejectsForeignMagicAndVersions() + { + Assert.Throws(() => PlanetaryMapTileEncoding.Read(new MemoryStream(new byte[] { 1, 2, 3, 4, 1, 0, 0, 0 }))); + var bytes = new byte[] { (byte)'C', (byte)'M', (byte)'P', (byte)'T', 99, 0, 0, 0 }; + Assert.Throws(() => PlanetaryMapTileEncoding.Read(new MemoryStream(bytes))); + } + + [Fact] + public void DecoderRejectsTrailingBytes() + { + var field = PlanetaryFieldDefinition.Create(2, 1, 0, AdvancedErosionParameters.Default); + var layout = new PlanetaryMapTileLayout(new(PlanetaryProjectionKind.Equirectangular), 0, 0, 0, 2, 0); + var key = new PlanetaryMapTileKey(field.FieldVersion, 1, 0, layout, PlanetaryQueryScale.AtFootprint(1)); + var tile = PlanetaryMapTileBaker.Bake(field, key, new FlatField()); + using var bytes = new MemoryStream(); + PlanetaryMapTileEncoding.Write(bytes, tile, leaveOpen: true); + bytes.WriteByte(0xff); + bytes.Position = 0; + Assert.Throws(() => PlanetaryMapTileEncoding.Read(bytes)); + } + + private readonly struct FlatField : IPlanetaryBaseField + { + public PlanetaryBaseFieldSample Sample(float3 direction) => new(0, float3.zero, 0, float3.zero, 0); + } +} diff --git a/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs.meta b/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs.meta new file mode 100644 index 00000000..027754e8 --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e916ef83478d48b43ac99777f5e0f731 \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryPageTests.cs.meta b/tests/CultMath.Tests/PlanetaryPageTests.cs.meta new file mode 100644 index 00000000..70a43864 --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryPageTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7c3ae5598d3b9164a9bd3a736a22ba9a \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryPatchTests.cs.meta b/tests/CultMath.Tests/PlanetaryPatchTests.cs.meta new file mode 100644 index 00000000..85990473 --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryPatchTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 36840ef44326f84409f850163b00e1f7 \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryProjectionTests.cs.meta b/tests/CultMath.Tests/PlanetaryProjectionTests.cs.meta new file mode 100644 index 00000000..7f37f78f --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryProjectionTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f370036bb1663524fb4a4d47dfd50401 \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryQueryTests.cs.meta b/tests/CultMath.Tests/PlanetaryQueryTests.cs.meta new file mode 100644 index 00000000..d72b2cb6 --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryQueryTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 93cf8771fdd520b41a4e931a6566a80a \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs.meta b/tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs.meta new file mode 100644 index 00000000..b9f3bea9 --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d34c0018ee2b700448b16540eca8ebcf \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryResidencyTests.cs.meta b/tests/CultMath.Tests/PlanetaryResidencyTests.cs.meta new file mode 100644 index 00000000..be7de2b4 --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryResidencyTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ece3b02dbd3d7448baabee7f1fba0ab \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryTopologyTests.cs b/tests/CultMath.Tests/PlanetaryTopologyTests.cs index 6e6b0838..759928d8 100644 --- a/tests/CultMath.Tests/PlanetaryTopologyTests.cs +++ b/tests/CultMath.Tests/PlanetaryTopologyTests.cs @@ -52,4 +52,12 @@ public sealed class PlanetaryTopologyTests Assert.NotEqual(edge, border); Assert.Equal(1, math.length(border), 5); } + + [Fact] + public void NormalizedCubeBaselineSharesFaceEdges() + { + var a = PlanetaryTopology.NormalizedCubeDirection(new(PlanetaryCubeFace.PositiveZ, 1, -0.4)); + var b = PlanetaryTopology.NormalizedCubeDirection(new(PlanetaryCubeFace.PositiveX, -1, -0.4)); + Assert.InRange(math.distance(a, b), 0, 1.0e-6f); + } } diff --git a/tests/CultMath.Tests/PlanetaryTopologyTests.cs.meta b/tests/CultMath.Tests/PlanetaryTopologyTests.cs.meta new file mode 100644 index 00000000..b6930142 --- /dev/null +++ b/tests/CultMath.Tests/PlanetaryTopologyTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: be3b0ec07fe65344db889d1e98448113 \ No newline at end of file diff --git a/tests/CultMath.Tests/SphericalErosionTests.cs.meta b/tests/CultMath.Tests/SphericalErosionTests.cs.meta new file mode 100644 index 00000000..00ff1de0 --- /dev/null +++ b/tests/CultMath.Tests/SphericalErosionTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60eb9e9e9f8ae2948b13403f5080168e \ No newline at end of file diff --git a/tools/CultMath.PlanetaryTileFixture.meta b/tools/CultMath.PlanetaryTileFixture.meta new file mode 100644 index 00000000..7f40b6ae --- /dev/null +++ b/tools/CultMath.PlanetaryTileFixture.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7197a2202273a9445b974bdd69109b1a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity.meta b/unity.meta new file mode 100644 index 00000000..de47a618 --- /dev/null +++ b/unity.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 366efeaef9f68224b80229332c7ecfc6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime.meta b/unity/Runtime.meta new file mode 100644 index 00000000..8e0678f8 --- /dev/null +++ b/unity/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8226365c03c5e1a438408b7e42b98740 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/CultMath.Unity.asmdef.meta b/unity/Runtime/CultMath.Unity.asmdef.meta new file mode 100644 index 00000000..2dd3a36a --- /dev/null +++ b/unity/Runtime/CultMath.Unity.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d1906c397c420ae4eb222a3f09f36843 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/PlanetaryPageUpload.cs b/unity/Runtime/PlanetaryPageUpload.cs index f0b168cd..1a61a707 100644 --- a/unity/Runtime/PlanetaryPageUpload.cs +++ b/unity/Runtime/PlanetaryPageUpload.cs @@ -36,12 +36,28 @@ public readonly struct PlanetaryPageMetadataGpu public readonly Vector4 State; } -public readonly record struct PlanetaryPageUpload( - PlanetaryPageInputGpu[] Inputs, - PlanetaryPageMetadataGpu[] Metadata, - int OutputSampleCount, - ulong ContentVersion, - ulong PresentationVersion); +public readonly struct PlanetaryPageUpload +{ + public PlanetaryPageUpload( + PlanetaryPageInputGpu[] inputs, + PlanetaryPageMetadataGpu[] metadata, + int outputSampleCount, + ulong contentVersion, + ulong presentationVersion) + { + Inputs = inputs; + Metadata = metadata; + OutputSampleCount = outputSampleCount; + ContentVersion = contentVersion; + PresentationVersion = presentationVersion; + } + + public PlanetaryPageInputGpu[] Inputs { get; } + public PlanetaryPageMetadataGpu[] Metadata { get; } + public int OutputSampleCount { get; } + public ulong ContentVersion { get; } + public ulong PresentationVersion { get; } +} public static class PlanetaryPageUploadBuilder { @@ -57,25 +73,23 @@ public static class PlanetaryPageUploadBuilder foreach (var (resident, pageIndex) in snapshot.Tiles.Select((resident, index) => (resident, index))) { var layout = new PlanetaryPageLayout(resident.Tile, interiorSize, borderSize).Validate(); - var spacing = PlanetaryPageSampling.NominalAngularTexelSize(layout) * radius; - var parentSpacing = resident.Tile.Level == 0 - ? 0 - : PlanetaryPageSampling.NominalAngularTexelSize(new(resident.Tile.Parent(), interiorSize, borderSize)) * radius; + var content = PlanetaryGpuPageBuilder.BuildContent(layout, radius); var outputOffset = inputs.Count; - for (var y = 0; y < layout.StorageSize; y++) - for (var x = 0; x < layout.StorageSize; x++) + foreach (var input in content.Inputs) { - var direction = PlanetaryPageSampling.Direction(layout, x, y); inputs.Add(new( - new Vector4(direction.x, direction.y, direction.z, radius), - new Vector4(spacing, parentSpacing, 0, 0))); + ToUnity(input.DirectionRadius), + ToUnity(input.Sampling))); } + var commonMetadata = PlanetaryGpuPageBuilder.Metadata(content, outputOffset, resident.Blend); metadata[pageIndex] = new( - new Vector4((int)resident.Tile.Face, resident.Tile.Level, resident.Tile.X, resident.Tile.Y), - new Vector4(outputOffset, layout.StorageSize, layout.InteriorSize, layout.BorderSize), - Vector4.zero, - new Vector4(1, resident.Blend, PlanetaryPageSampling.NominalAngularTexelSize(layout), spacing)); + ToUnity(commonMetadata.Address), + ToUnity(commonMetadata.Layout), + ToUnity(commonMetadata.Bounds), + ToUnity(commonMetadata.State)); } return new(inputs.ToArray(), metadata, inputs.Count, snapshot.ContentVersion, snapshot.PresentationVersion); } + + private static Vector4 ToUnity(float4 value) => new(value.x, value.y, value.z, value.w); } diff --git a/unity/Runtime/PlanetaryPageUpload.cs.meta b/unity/Runtime/PlanetaryPageUpload.cs.meta new file mode 100644 index 00000000..87bcb61a --- /dev/null +++ b/unity/Runtime/PlanetaryPageUpload.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7edd3bb2edb2e6448a2d54fc082c27dc \ No newline at end of file diff --git a/unity/Runtime/PlanetaryPatchMeshAdapter.cs.meta b/unity/Runtime/PlanetaryPatchMeshAdapter.cs.meta new file mode 100644 index 00000000..69af0abc --- /dev/null +++ b/unity/Runtime/PlanetaryPatchMeshAdapter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ad7b581e14ef774d8435069bb77ca6a \ No newline at end of file diff --git a/web.meta b/web.meta new file mode 100644 index 00000000..d65ff3e6 --- /dev/null +++ b/web.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: daf45471f8b5f4f44a2cb86e77e010db +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/web/planetary-tile.cross-runtime.test.mjs b/web/planetary-tile.cross-runtime.test.mjs new file mode 100644 index 00000000..2b457219 --- /dev/null +++ b/web/planetary-tile.cross-runtime.test.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { decodePlanetarySurfaceTile } from "./planetary-tile.ts"; + +if (process.argv.length !== 3) throw new Error("Expected a generated CMPT fixture path"); +const bytes = await readFile(process.argv[2]); +const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); +const tile = decodePlanetarySurfaceTile(arrayBuffer); +assert.equal(tile.header.projectionVersion, 3); +assert.equal(tile.header.layerId, 29); +assert.equal(tile.header.projectionKind, 2); +assert.equal(tile.header.interiorSize, 5); +assert.equal(tile.header.borderSize, 1); +assert.equal(tile.storageSize, 7); +assert.equal(tile.samples.length, 49); +assert.ok(tile.samples.some(sample => sample !== null)); +for (const sample of tile.samples) { + if (sample === null) continue; + assert.ok(Number.isFinite(sample.radialDisplacement)); + assert.ok(Number.isFinite(sample.unresolvedHeightBound)); +} diff --git a/web/planetary-tile.cross-runtime.test.mjs.meta b/web/planetary-tile.cross-runtime.test.mjs.meta new file mode 100644 index 00000000..3c38da54 --- /dev/null +++ b/web/planetary-tile.cross-runtime.test.mjs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7bc397e5660d2b6448919c41983d35e8 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/web/planetary-tile.test.mjs b/web/planetary-tile.test.mjs new file mode 100644 index 00000000..cb054f28 --- /dev/null +++ b/web/planetary-tile.test.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { decodePlanetarySurfaceTile } from "./planetary-tile.ts"; + +const bytes = new ArrayBuffer(152); +const view = new DataView(bytes); +let offset = 0; +const u8 = value => view.setUint8(offset++, value); +const u32 = value => { view.setUint32(offset, value, true); offset += 4; }; +const i32 = value => { view.setInt32(offset, value, true); offset += 4; }; +const u64 = value => { view.setBigUint64(offset, value, true); offset += 8; }; +const f32 = value => { view.setFloat32(offset, value, true); offset += 4; }; +const f64 = value => { view.setFloat64(offset, value, true); offset += 8; }; + +for (const character of "CMPT") u8(character.charCodeAt(0)); +u32(1); u64(0x1234n); u32(2); u32(17); i32(0); +f64(0); f64(0); f64(1); +i32(0); i32(0); i32(0); i32(2); i32(0); +f32(10); f32(0.5); i32(4); +u8(1); +for (const value of [1, 0, 0]) f32(value); +f32(8.4); f32(0.25); +for (const value of [0, 0.1, 0]) f32(value); +for (const value of [0.99, -0.1, 0]) f32(value); +for (const value of [0.1, 0.8, 0.2, 2.5, 0.05]) f32(value); +u8(0); u8(0); u8(0); +assert.equal(offset, bytes.byteLength); + +const tile = decodePlanetarySurfaceTile(bytes); +assert.equal(tile.header.fieldVersion, 0x1234n); +assert.equal(tile.header.layerId, 17); +assert.equal(tile.storageSize, 2); +assert.equal(tile.samples.length, 4); +assert.equal(tile.samples[0]?.radialDisplacement, 0.25); +assert.equal(tile.samples[1], null); + +assert.throws(() => decodePlanetarySurfaceTile(bytes.slice(0, bytes.byteLength - 1)), /Truncated/); +const trailing = new Uint8Array(bytes.byteLength + 1); +trailing.set(new Uint8Array(bytes)); +assert.throws(() => decodePlanetarySurfaceTile(trailing.buffer), /trailing/); diff --git a/web/planetary-tile.test.mjs.meta b/web/planetary-tile.test.mjs.meta new file mode 100644 index 00000000..d622095b --- /dev/null +++ b/web/planetary-tile.test.mjs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 04c93ecab90d465499aa3d60722e6fc3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/web/planetary-tile.ts b/web/planetary-tile.ts new file mode 100644 index 00000000..e68d70fa --- /dev/null +++ b/web/planetary-tile.ts @@ -0,0 +1,89 @@ +export const CULTMATH_PLANETARY_TILE_VERSION = 1; + +export interface PlanetaryTileHeader { + fieldVersion: bigint; + projectionVersion: number; + layerId: number; + projectionKind: number; + centerLongitude: number; + centerLatitude: number; + projectionScale: number; + level: number; + x: number; + y: number; + interiorSize: number; + borderSize: number; + footprintMeters: number; + maximumUnresolvedHeight: number; +} + +export interface PlanetaryTileSample { + direction: readonly [number, number, number]; + radius: number; + radialDisplacement: number; + tangentGradient: readonly [number, number, number]; + surfaceNormal: readonly [number, number, number]; + slope: number; + ridge: number; + gully: number; + finestResolvedWavelength: number; + unresolvedHeightBound: number; +} + +export interface PlanetarySurfaceTile { + header: PlanetaryTileHeader; + storageSize: number; + samples: readonly (PlanetaryTileSample | null)[]; +} + +export function decodePlanetarySurfaceTile(bytes: ArrayBuffer): PlanetarySurfaceTile { + const view = new DataView(bytes); + let offset = 0; + const u8 = () => view.getUint8(offset++); + const u32 = () => { const value = view.getUint32(offset, true); offset += 4; return value; }; + const i32 = () => { const value = view.getInt32(offset, true); offset += 4; return value; }; + const f32 = () => { const value = view.getFloat32(offset, true); offset += 4; return value; }; + const f64 = () => { const value = view.getFloat64(offset, true); offset += 8; return value; }; + const u64 = () => { const value = view.getBigUint64(offset, true); offset += 8; return value; }; + const vector3 = (): [number, number, number] => [f32(), f32(), f32()]; + const requireRemaining = (count: number) => { + if (offset + count > view.byteLength) throw new Error("Truncated CultMath planetary tile"); + }; + + requireRemaining(8); + if (String.fromCharCode(u8(), u8(), u8(), u8()) !== "CMPT") throw new Error("Not a CultMath planetary tile"); + const version = u32(); + if (version !== CULTMATH_PLANETARY_TILE_VERSION) throw new Error(`Unsupported CultMath planetary tile version ${version}`); + requireRemaining(76); + const header: PlanetaryTileHeader = { + fieldVersion: u64(), + projectionVersion: u32(), + layerId: u32(), + projectionKind: i32(), + centerLongitude: f64(), + centerLatitude: f64(), + projectionScale: f64(), + level: i32(), x: i32(), y: i32(), interiorSize: i32(), borderSize: i32(), + footprintMeters: f32(), maximumUnresolvedHeight: f32(), + }; + const count = i32(); + const storageSize = header.interiorSize + header.borderSize * 2; + if (count !== storageSize * storageSize) throw new Error("CultMath planetary tile sample count does not match layout"); + const samples: (PlanetaryTileSample | null)[] = new Array(count); + for (let index = 0; index < count; index++) { + requireRemaining(1); + if (u8() === 0) { samples[index] = null; continue; } + requireRemaining(64); + samples[index] = { + direction: vector3(), + radius: f32(), + radialDisplacement: f32(), + tangentGradient: vector3(), + surfaceNormal: vector3(), + slope: f32(), ridge: f32(), gully: f32(), + finestResolvedWavelength: f32(), unresolvedHeightBound: f32(), + }; + } + if (offset !== view.byteLength) throw new Error("CultMath planetary tile has trailing bytes"); + return { header, storageSize, samples }; +} diff --git a/web/planetary-tile.ts.meta b/web/planetary-tile.ts.meta new file mode 100644 index 00000000..337da3bd --- /dev/null +++ b/web/planetary-tile.ts.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f284e64628320844196b7aaea76475a4 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: