From 6a162606e4bed08de8b3250d708db9b841c92d1b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 22 Jul 2026 03:47:38 +0100 Subject: [PATCH] Transfer planetary geometry to GameCult.Geometry --- CultMath.slnx | 3 - README.md | 11 - .../CultMath.PlanetaryViewer.Editor.asmdef | 18 - .../Editor/CultMathPlanetaryViewerMenu.cs | 26 -- Samples~/PlanetaryViewer/README.md | 16 - .../Runtime/CultMath.PlanetaryViewer.asmdef | 17 - .../Runtime/CultMathPlanetaryViewer.cs | 139 ------- .../Runtime/CultMathPlanetaryViewer.shader | 90 ---- THIRD-PARTY-NOTICES.md | 23 - .../CultMath.PlanetaryTileFixture.csproj | 11 - .../CultMath.PlanetaryTileFixture/Program.cs | 22 - docs/design.md | 6 - docs/planetary.md | 130 ------ docs/planetary.md.meta | 7 - package.json | 9 +- scripts/test-web-planetary-tile.ps1 | 9 - scripts/test-web-planetary-tile.ps1.meta | 7 - shaders/AdvancedErosionFilter.hlsl | 147 ------- shaders/AdvancedErosionFilter.hlsl.meta | 7 - shaders/CultMath.hlsl | 94 ----- shaders/Planetary.hlsl | 392 ------------------ shaders/Planetary.hlsl.meta | 7 - src/CultMath/AdvancedErosionFilter.cs | 139 ------- src/CultMath/AdvancedErosionFilter.cs.meta | 2 - src/CultMath/CultMath.csproj | 2 - src/CultMath/ErosionFrequencyBands.cs | 57 --- src/CultMath/ErosionFrequencyBands.cs.meta | 2 - src/CultMath/PlanetaryField.cs | 215 ---------- src/CultMath/PlanetaryField.cs.meta | 2 - src/CultMath/PlanetaryGpuPages.cs | 55 --- src/CultMath/PlanetaryGpuPages.cs.meta | 2 - src/CultMath/PlanetaryMapTileEncoding.cs | 117 ------ src/CultMath/PlanetaryMapTileEncoding.cs.meta | 2 - src/CultMath/PlanetaryPages.cs | 187 --------- src/CultMath/PlanetaryPages.cs.meta | 2 - src/CultMath/PlanetaryPatch.cs | 45 -- src/CultMath/PlanetaryPatch.cs.meta | 2 - src/CultMath/PlanetaryProjection.cs | 322 -------------- src/CultMath/PlanetaryProjection.cs.meta | 2 - src/CultMath/PlanetaryQueries.cs | 193 --------- src/CultMath/PlanetaryQueries.cs.meta | 2 - src/CultMath/PlanetaryRadialRefinement.cs | 28 -- .../PlanetaryRadialRefinement.cs.meta | 2 - src/CultMath/PlanetaryResidency.cs | 148 ------- src/CultMath/PlanetaryResidency.cs.meta | 2 - src/CultMath/PlanetaryTopology.cs | 191 --------- src/CultMath/PlanetaryTopology.cs.meta | 2 - src/CultMath/SphericalErosion.cs | 108 ----- src/CultMath/SphericalErosion.cs.meta | 2 - src/CultMath/math.cs | 3 - src/CultMath/rect.cs | 21 - src/CultMath/rect.cs.meta | 2 - .../AdvancedErosionFilterTests.cs | 44 -- .../AdvancedErosionFilterTests.cs.meta | 2 - .../ErosionFrequencyBandsTests.cs | 34 -- .../ErosionFrequencyBandsTests.cs.meta | 2 - tests/CultMath.Tests/HlslMirrorTests.cs | 37 +- tests/CultMath.Tests/MathTests.cs | 13 - tests/CultMath.Tests/PlanetaryFieldTests.cs | 80 ---- .../PlanetaryFieldTests.cs.meta | 2 - tests/CultMath.Tests/PlanetaryGpuPageTests.cs | 25 -- .../PlanetaryGpuPageTests.cs.meta | 2 - .../PlanetaryMapTileEncodingTests.cs | 52 --- .../PlanetaryMapTileEncodingTests.cs.meta | 2 - tests/CultMath.Tests/PlanetaryPageTests.cs | 57 --- .../CultMath.Tests/PlanetaryPageTests.cs.meta | 2 - tests/CultMath.Tests/PlanetaryPatchTests.cs | 40 -- .../PlanetaryPatchTests.cs.meta | 2 - .../PlanetaryProjectionTests.cs | 73 ---- .../PlanetaryProjectionTests.cs.meta | 2 - tests/CultMath.Tests/PlanetaryQueryTests.cs | 46 -- .../PlanetaryQueryTests.cs.meta | 2 - .../PlanetaryRadialRefinementTests.cs | 29 -- .../PlanetaryRadialRefinementTests.cs.meta | 2 - .../CultMath.Tests/PlanetaryResidencyTests.cs | 57 --- .../PlanetaryResidencyTests.cs.meta | 2 - .../CultMath.Tests/PlanetaryTopologyTests.cs | 63 --- .../PlanetaryTopologyTests.cs.meta | 2 - tests/CultMath.Tests/SphericalErosionTests.cs | 34 -- .../SphericalErosionTests.cs.meta | 2 - tools/CultMath.PlanetaryTileFixture.meta | 8 - unity/Runtime/PlanetaryPageUpload.cs | 95 ----- unity/Runtime/PlanetaryPageUpload.cs.meta | 2 - unity/Runtime/PlanetaryPatchMeshAdapter.cs | 36 -- .../Runtime/PlanetaryPatchMeshAdapter.cs.meta | 2 - web/planetary-tile.cross-runtime.test.mjs | 21 - ...planetary-tile.cross-runtime.test.mjs.meta | 7 - web/planetary-tile.test.mjs | 39 -- web/planetary-tile.test.mjs.meta | 7 - web/planetary-tile.ts | 89 ---- web/planetary-tile.ts.meta | 7 - 91 files changed, 6 insertions(+), 4067 deletions(-) delete mode 100644 Samples~/PlanetaryViewer/Editor/CultMath.PlanetaryViewer.Editor.asmdef delete mode 100644 Samples~/PlanetaryViewer/Editor/CultMathPlanetaryViewerMenu.cs delete mode 100644 Samples~/PlanetaryViewer/README.md delete mode 100644 Samples~/PlanetaryViewer/Runtime/CultMath.PlanetaryViewer.asmdef delete mode 100644 Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.cs delete mode 100644 Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.shader delete mode 100644 Tools~/CultMath.PlanetaryTileFixture/CultMath.PlanetaryTileFixture.csproj delete mode 100644 Tools~/CultMath.PlanetaryTileFixture/Program.cs delete mode 100644 docs/planetary.md delete mode 100644 docs/planetary.md.meta delete mode 100644 scripts/test-web-planetary-tile.ps1 delete mode 100644 scripts/test-web-planetary-tile.ps1.meta delete mode 100644 shaders/AdvancedErosionFilter.hlsl delete mode 100644 shaders/AdvancedErosionFilter.hlsl.meta delete mode 100644 shaders/Planetary.hlsl delete mode 100644 shaders/Planetary.hlsl.meta delete mode 100644 src/CultMath/AdvancedErosionFilter.cs delete mode 100644 src/CultMath/AdvancedErosionFilter.cs.meta delete mode 100644 src/CultMath/ErosionFrequencyBands.cs delete mode 100644 src/CultMath/ErosionFrequencyBands.cs.meta delete mode 100644 src/CultMath/PlanetaryField.cs delete mode 100644 src/CultMath/PlanetaryField.cs.meta delete mode 100644 src/CultMath/PlanetaryGpuPages.cs delete mode 100644 src/CultMath/PlanetaryGpuPages.cs.meta delete mode 100644 src/CultMath/PlanetaryMapTileEncoding.cs delete mode 100644 src/CultMath/PlanetaryMapTileEncoding.cs.meta delete mode 100644 src/CultMath/PlanetaryPages.cs delete mode 100644 src/CultMath/PlanetaryPages.cs.meta delete mode 100644 src/CultMath/PlanetaryPatch.cs delete mode 100644 src/CultMath/PlanetaryPatch.cs.meta delete mode 100644 src/CultMath/PlanetaryProjection.cs delete mode 100644 src/CultMath/PlanetaryProjection.cs.meta delete mode 100644 src/CultMath/PlanetaryQueries.cs delete mode 100644 src/CultMath/PlanetaryQueries.cs.meta delete mode 100644 src/CultMath/PlanetaryRadialRefinement.cs delete mode 100644 src/CultMath/PlanetaryRadialRefinement.cs.meta delete mode 100644 src/CultMath/PlanetaryResidency.cs delete mode 100644 src/CultMath/PlanetaryResidency.cs.meta delete mode 100644 src/CultMath/PlanetaryTopology.cs delete mode 100644 src/CultMath/PlanetaryTopology.cs.meta delete mode 100644 src/CultMath/SphericalErosion.cs delete mode 100644 src/CultMath/SphericalErosion.cs.meta delete mode 100644 src/CultMath/rect.cs delete mode 100644 src/CultMath/rect.cs.meta delete mode 100644 tests/CultMath.Tests/AdvancedErosionFilterTests.cs delete mode 100644 tests/CultMath.Tests/AdvancedErosionFilterTests.cs.meta delete mode 100644 tests/CultMath.Tests/ErosionFrequencyBandsTests.cs delete mode 100644 tests/CultMath.Tests/ErosionFrequencyBandsTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryFieldTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryFieldTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryGpuPageTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryGpuPageTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryPageTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryPageTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryPatchTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryPatchTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryProjectionTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryProjectionTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryQueryTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryQueryTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryResidencyTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryResidencyTests.cs.meta delete mode 100644 tests/CultMath.Tests/PlanetaryTopologyTests.cs delete mode 100644 tests/CultMath.Tests/PlanetaryTopologyTests.cs.meta delete mode 100644 tests/CultMath.Tests/SphericalErosionTests.cs delete mode 100644 tests/CultMath.Tests/SphericalErosionTests.cs.meta delete mode 100644 tools/CultMath.PlanetaryTileFixture.meta delete mode 100644 unity/Runtime/PlanetaryPageUpload.cs delete mode 100644 unity/Runtime/PlanetaryPageUpload.cs.meta delete mode 100644 unity/Runtime/PlanetaryPatchMeshAdapter.cs delete mode 100644 unity/Runtime/PlanetaryPatchMeshAdapter.cs.meta delete mode 100644 web/planetary-tile.cross-runtime.test.mjs delete mode 100644 web/planetary-tile.cross-runtime.test.mjs.meta delete mode 100644 web/planetary-tile.test.mjs delete mode 100644 web/planetary-tile.test.mjs.meta delete mode 100644 web/planetary-tile.ts delete mode 100644 web/planetary-tile.ts.meta diff --git a/CultMath.slnx b/CultMath.slnx index 05d1788..9583457 100644 --- a/CultMath.slnx +++ b/CultMath.slnx @@ -5,7 +5,4 @@ - - - diff --git a/README.md b/README.md index c60a991..b471c7f 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,6 @@ shader semantics. - `Voronoi.SampleTones`, a C# batch surface that calls the Rust `cultmath-core` native kernel when `cultmath_core` is available and falls back to the managed parity path otherwise. -- `PlanetaryField`, a scale-aware CPU/HLSL spherical field contract with - deterministic identity, cube-sphere topology, residual pages, LOD residency, - ray/path/region queries, coarse patches, and projected map-tile baking. See - `docs/planetary.md`. -- `CultMath.Unity`, an engine-facing assembly containing only Unity mesh and - page-upload adapters for the canonical planetary contracts. ## Native Core @@ -72,11 +66,6 @@ local `AetheriaMath` surface. The value-noise family follows public shader literature from Inigo Quilez's noise and mini-spline articles, expressed in CultMath names with tests instead of copied project-local helper drift. -The advanced erosion filter is an MPL-2.0 adaptation of Rune Skovbo Johansen's -published filter. Its faithful planar core is isolated in -`AdvancedErosionFilter.cs` and `AdvancedErosionFilter.hlsl`; spherical mapping -is a separate CultMath concern. See `THIRD-PARTY-NOTICES.md`. - ## Build ```powershell diff --git a/Samples~/PlanetaryViewer/Editor/CultMath.PlanetaryViewer.Editor.asmdef b/Samples~/PlanetaryViewer/Editor/CultMath.PlanetaryViewer.Editor.asmdef deleted file mode 100644 index 1051232..0000000 --- a/Samples~/PlanetaryViewer/Editor/CultMath.PlanetaryViewer.Editor.asmdef +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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 deleted file mode 100644 index ade28c3..0000000 --- a/Samples~/PlanetaryViewer/Editor/CultMathPlanetaryViewerMenu.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index ec4dcc3..0000000 --- a/Samples~/PlanetaryViewer/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# 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 deleted file mode 100644 index 2d3676f..0000000 --- a/Samples~/PlanetaryViewer/Runtime/CultMath.PlanetaryViewer.asmdef +++ /dev/null @@ -1,17 +0,0 @@ -{ - "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 deleted file mode 100644 index 0611372..0000000 --- a/Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.cs +++ /dev/null @@ -1,139 +0,0 @@ -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 deleted file mode 100644 index b5f7274..0000000 --- a/Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.shader +++ /dev/null @@ -1,90 +0,0 @@ -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 b/THIRD-PARTY-NOTICES.md index 54e280f..c283df2 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -9,26 +9,3 @@ Gustavson's `webgl-noise` project and Unity Mathematics. - Copyright 2011 Ashima Arts. - Upstream: - License: MIT. - -## Advanced Terrain Erosion Filter - -`src/CultMath/AdvancedErosionFilter.cs` and -`shaders/AdvancedErosionFilter.hlsl` adapt the mathematical procedure from -Rune Skovbo Johansen's **Advanced Terrain Erosion Filter**: - -- Copyright 2025 Rune Skovbo Johansen. -- Upstream: -- Explanation: -- License: Mozilla Public License 2.0, - - -The CultMath files retain the MPL notice and separate the faithful planar -filter from CultMath's spherical adaptation. Modifications include C#/HLSL -type lowering, explicit bounded octave counts, finite zero-rounding behavior, -and CultMath naming. CultMath replaces the reference floating hash with a -shared integer avalanche hash so C# and GPU evaluation select identical cell -pivots across devices; this changes the stochastic pattern, not the filter -mechanics. - -The technique builds on earlier work credited by the upstream author to Fewes -and Clay John. The hash form follows the public shader math of Inigo Quilez. diff --git a/Tools~/CultMath.PlanetaryTileFixture/CultMath.PlanetaryTileFixture.csproj b/Tools~/CultMath.PlanetaryTileFixture/CultMath.PlanetaryTileFixture.csproj deleted file mode 100644 index 9a6f8bd..0000000 --- a/Tools~/CultMath.PlanetaryTileFixture/CultMath.PlanetaryTileFixture.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - Exe - net10.0 - enable - enable - - - - - diff --git a/Tools~/CultMath.PlanetaryTileFixture/Program.cs b/Tools~/CultMath.PlanetaryTileFixture/Program.cs deleted file mode 100644 index 1952fa2..0000000 --- a/Tools~/CultMath.PlanetaryTileFixture/Program.cs +++ /dev/null @@ -1,22 +0,0 @@ -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/design.md b/docs/design.md index f4ca5b4..8250ba7 100644 --- a/docs/design.md +++ b/docs/design.md @@ -19,12 +19,6 @@ Rust hot kernels, C# host code, and HLSL shader code. - Keep the surface boring and exact. Noise primitives are allowed when they are source-grounded, shader-shaped, and shared by more than one project. Matrices, quaternions, and packing helpers still need an owner before they enter. -- Planetary geometry is a scale-aware field query, not a renderer asset. CPU - simulation is the reference consumer; pages, patches, and maps are derived - lowerings carrying field identity and unresolved error. -- Application base fields and gameplay meaning enter through narrow inputs. - CultMath does not own biomes, resources, traversability, render materials, or - committed game consequences. ## Non-Goals diff --git a/docs/planetary.md b/docs/planetary.md deleted file mode 100644 index 3503429..0000000 --- a/docs/planetary.md +++ /dev/null @@ -1,130 +0,0 @@ -# Planetary Field Runtime - -CultMath owns the deterministic, renderer-independent planetary field. The same -definition can be queried by a CPU simulation, baked into residual pages, -sampled through a map projection, or evaluated by HLSL. Render resources and -gameplay interpretation remain consumer responsibilities. - -## Authority - -- `PlanetaryFieldDefinition` identifies radius, seed, erosion parameters, and a - version that includes the consumer-owned base-field version. -- `IPlanetaryBaseField` supplies deterministic application-authored base - evidence without making CultMath depend on an application. -- `PlanetaryQueryScale` makes the physical footprint and accepted unresolved - error explicit. There is deliberately no scale-free authoritative - `HeightAt` call. -- `PlanetarySurfaceSample` carries displacement, tangent gradient, normal, - slope, ridge/gully evidence, resolved wavelength, and unresolved bound. -- pages, residency, patches, and map tiles are disposable lowerings of that - contract. - -The C# surface targets .NET and Unity-compatible `netstandard2.1`. The matching -HLSL lives in `shaders/Planetary.hlsl` and is included by `CultMath.hlsl`. - -## Point and batch queries - -```csharp -var erosion = AdvancedErosionParameters.Default with -{ - Scale = radius * 0.075f, - Strength = 0.12f, - Octaves = 7, -}; -var field = PlanetaryFieldDefinition.Create( - baseFieldVersion: 17, - radius, - seed: 42, - erosion); -var scale = PlanetaryQueryScale.AtFootprint( - footprintMeters: 10, - maximumUnresolvedHeight: 0.5f); - -PlanetaryBaseFieldSample basis = source.Sample(direction); -PlanetarySurfaceSample sample = PlanetaryField.Sample( - field, direction, basis, scale); -``` - -`PlanetaryField.SampleBatch` writes into a caller-owned span. `SamplePosition` -accepts `double3`, reduces the astronomical position to a stable unit direction, -and then evaluates the same float field contract. - -Higher-level CPU queries include bounded ray intersection, segment clearance, -great-circle profiles, and region summaries. Their answers remain field queries; -only a consuming simulation can turn them into committed game consequences. - -## Cube-sphere topology and pages - -`PlanetaryTopology` owns the tangent/QSC cube-sphere mapping, inverse face -coordinates, tile selection, local page coordinates, and radial-graph surface -normal. `PlanetaryPageSampling` owns bordered directions, nominal spacing, -bilinear sampling, and summaries. - -`PlanetaryPageBaker` evaluates a root page at its physical sample spacing. A -child stores `child filtered field - parent filtered field`. A -`PlanetaryPageSetSampling` query adds every containing ancestor contribution -and applies transition weights only to residuals. - -`PlanetaryLodSelector` selects six roots plus one camera-facing ancestor chain -from the unresolved-height error. `PlanetaryResidualResidency` owns arrival and -departure presentation weights, content and presentation versions, and nothing -about field truth. - -## Patches - -`PlanetaryPatch.CreateFace` and `CreateCubeSphere` produce renderer-neutral -coarse meshes from the canonical topology. HLSL calls -`cultmath_planetary_face_direction` for procedural vertices and -`cultmath_planetary_radial_refinement_step` for bounded pixel refinement. - -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 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 - -`PlanetaryProjection` supplies forward and inverse transforms for: - -- equirectangular; -- Web Mercator with an explicit polar cutoff; -- Equal Earth; -- orthographic; -- azimuthal equidistant and equal-area; -- cube atlas; -- local tangent/gnomonic views. - -`PlanetaryMapTileBaker` maps each valid tile sample through the inverse -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 -CultMath's matched C#/HLSL semantics. Hardware transcendental functions such as -`atan` and `tan` use measured tolerances rather than fictional bit identity. -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, 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 deleted file mode 100644 index af364c2..0000000 --- a/docs/planetary.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 23a5751e4cececc4e99e024058d69add -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/package.json b/package.json index 2564360..6da2a17 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,5 @@ "Simulation" ], "license": "MPL-2.0", - "category": "Scripting", - "samples": [ - { - "displayName": "Planetary Field Viewer", - "description": "Runnable Unity viewer using the canonical CultMath CPU and HLSL planetary field.", - "path": "Samples~/PlanetaryViewer" - } - ] + "category": "Scripting" } diff --git a/scripts/test-web-planetary-tile.ps1 b/scripts/test-web-planetary-tile.ps1 deleted file mode 100644 index 5221fb3..0000000 --- a/scripts/test-web-planetary-tile.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -$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 deleted file mode 100644 index ffd7323..0000000 --- a/scripts/test-web-planetary-tile.ps1.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 235e8f203bd1c4d40a8fa211273106ba -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/shaders/AdvancedErosionFilter.hlsl b/shaders/AdvancedErosionFilter.hlsl deleted file mode 100644 index a0a350e..0000000 --- a/shaders/AdvancedErosionFilter.hlsl +++ /dev/null @@ -1,147 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at https://mozilla.org/MPL/2.0/. -// Mathematical procedure adapted from Rune Skovbo Johansen's Advanced Terrain -// Erosion Filter (2025): https://www.shadertoy.com/view/wXcfWn - -#ifndef CULTMATH_ADVANCED_EROSION_FILTER_HLSL -#define CULTMATH_ADVANCED_EROSION_FILTER_HLSL - -struct CultMathAdvancedErosionParameters -{ - float scale; float strength; float gully_weight; float detail; - float4 rounding; float4 onset; float2 assumed_slope; - float cell_scale; float normalization; int octaves; float lacunarity; float gain; -}; - -struct CultMathAdvancedErosionResult -{ - float3 delta; float magnitude; float ridge_map; float fade_target; -}; - -struct CultMathErosionBandSelection -{ - int active_octaves; float final_octave_weight; float finest_included_wavelength; float unresolved_height_bound; -}; - -CultMathErosionBandSelection cultmath_select_erosion_bands(float base_wavelength, float sample_spacing, int maximum_octaves, float lacunarity, float base_amplitude, float gain, float transition_ratio) -{ - base_wavelength=max(base_wavelength,1.0e-6); sample_spacing=max(sample_spacing,0.0); lacunarity=max(lacunarity,1.01); transition_ratio=max(transition_ratio,1.01); maximum_octaves=min(max(maximum_octaves,0),16); - float nyquist=sample_spacing*2.0, wavelength=base_wavelength, final_weight=0.0, finest=0.0; int active=0; - [loop] for(int octave=0; octave=active) omitted+=amplitude; else if(index==active-1) omitted+=amplitude*(1.0-final_weight); amplitude*=abs(gain); - } - CultMathErosionBandSelection result; result.active_octaves=active; result.final_octave_weight=active>0?final_weight:0.0; result.finest_included_wavelength=finest; result.unresolved_height_bound=omitted; return result; -} - -float cultmath_erosion_hash_unit(int2 cell, uint salt) -{ - uint value = (uint)cell.x * 0x8da6b343u ^ (uint)cell.y * 0xd8163841u ^ salt; - value ^= value >> 16; value *= 0x7feb352du; value ^= value >> 15; value *= 0x846ca68bu; value ^= value >> 16; - return (value & 0x00ffffffu) * (1.0 / 16777216.0); -} -float2 cultmath_erosion_hash2(float2 input) -{ - int2 cell = (int2)input; - return float2(cultmath_erosion_hash_unit(cell, 0x68bc21ebu), cultmath_erosion_hash_unit(cell, 0x02e5be93u)) * 2.0 - 1.0; -} - -float4 cultmath_phacelle_noise(float2 position, float2 normal_direction, float frequency, float offset_cycles, float normalization) -{ - float2 side_direction = float2(-normal_direction.y, normal_direction.x) * frequency * 6.28318530718; - float offset = offset_cycles * 6.28318530718; - float2 cell = floor(position); - float2 local = frac(position); - float2 phase = 0.0; - float weight_sum = 0.0; - [unroll] for (int x = -1; x <= 2; x++) - [unroll] for (int y = -1; y <= 2; y++) - { - float2 grid_offset = float2(x, y); - float2 grid_point = cell + grid_offset; - float2 random_offset = cultmath_erosion_hash2(grid_point) * 0.5; - float2 delta = local - grid_offset - random_offset; - float weight = max(0.0, exp(-dot(delta, delta) * 2.0) - 0.01111); - weight_sum += weight; - float wave = dot(delta, side_direction) + offset; - phase += float2(cos(wave), sin(wave)) * weight; - } - float2 interpolated = phase / max(weight_sum, 1.0e-20); - float magnitude = max(1.0 - saturate(normalization), length(interpolated)); - return float4(interpolated / max(magnitude, 1.0e-20), side_direction); -} - -float cultmath_erosion_pow_inverse(float value, float power) { return 1.0 - pow(1.0 - saturate(value), power); } -float cultmath_erosion_ease_out(float value) { float inverse = 1.0 - saturate(value); return 1.0 - inverse * inverse; } -float cultmath_erosion_smooth_start(float value, float smoothing) -{ - return smoothing <= 1.0e-10 || value >= smoothing ? value - 0.5 * smoothing : 0.5 * value * value / smoothing; -} -float2 cultmath_erosion_safe_normalize(float2 value) -{ - float value_length = length(value); - return abs(value_length) > 1.0e-10 ? value / value_length : value; -} - -CultMathAdvancedErosionResult cultmath_advanced_erosion_filter_banded(float2 position, float3 base_height_and_slope, float fade_target, CultMathAdvancedErosionParameters p, CultMathErosionBandSelection band) -{ - float strength = p.strength * p.scale; - fade_target = clamp(fade_target, -1.0, 1.0); - float3 input_value = base_height_and_slope; - float3 height_and_slope = input_value; - float frequency = 1.0 / max(p.scale * p.cell_scale, 1.0e-10); - float2 slope = height_and_slope.yz; - float slope_length = max(length(slope), 1.0e-10); - float magnitude = 0.0; - float rounding_multiplier = 1.0; - float input_rounding = lerp(p.rounding.y, p.rounding.x, saturate(fade_target + 0.5)) * p.rounding.z; - float combined_mask = cultmath_erosion_ease_out(cultmath_erosion_smooth_start(slope_length * p.onset.x, input_rounding * p.onset.x)); - float ridge_mask = cultmath_erosion_ease_out(slope_length * p.onset.z); - float ridge_fade = fade_target; - float2 gully_slope = lerp(slope, slope / slope_length * p.assumed_slope.x, p.assumed_slope.y); - - int octave_count=min(min(max(p.octaves,0),8),max(band.active_octaves,0)); - [loop] for (int octave = 0; octave < octave_count; octave++) - { - float4 phacelle = cultmath_phacelle_noise(position * frequency, cultmath_erosion_safe_normalize(gully_slope), p.cell_scale, 0.25, p.normalization); - float2 derivative_direction = phacelle.zw * -frequency; - float sloping = abs(phacelle.y); - gully_slope += sign(phacelle.y) * derivative_direction * strength * p.gully_weight; - float3 gullies = float3(phacelle.x, phacelle.y * derivative_direction.x, phacelle.y * derivative_direction.y); - float3 faded = lerp(float3(fade_target, 0.0, 0.0), gullies * p.gully_weight, combined_mask); - float octave_weight=octave==octave_count-1?saturate(band.final_octave_weight):1.0; - height_and_slope += faded * strength * octave_weight; - magnitude += strength * octave_weight; - fade_target = faded.x; - float octave_rounding = lerp(p.rounding.y, p.rounding.x, saturate(phacelle.x + 0.5)) * rounding_multiplier; - float new_mask = cultmath_erosion_ease_out(cultmath_erosion_smooth_start(sloping * p.onset.y, octave_rounding * p.onset.y)); - combined_mask = cultmath_erosion_pow_inverse(combined_mask, p.detail) * new_mask; - ridge_fade = lerp(ridge_fade, lerp(ridge_fade, gullies.x, ridge_mask), octave_weight); - ridge_mask *= lerp(1.0, cultmath_erosion_ease_out(sloping * p.onset.w), octave_weight); - strength *= p.gain; - frequency *= p.lacunarity; - rounding_multiplier *= p.rounding.w; - } - - CultMathAdvancedErosionResult result; - result.delta = height_and_slope - input_value; - result.magnitude = magnitude; - result.ridge_map = ridge_fade * (1.0 - ridge_mask); - result.fade_target = fade_target; - return result; -} - -CultMathAdvancedErosionResult cultmath_advanced_erosion_filter(float2 position, float3 base_height_and_slope, float fade_target, CultMathAdvancedErosionParameters p) -{ - CultMathErosionBandSelection band; band.active_octaves=p.octaves; band.final_octave_weight=1.0; band.finest_included_wavelength=0.0; band.unresolved_height_bound=0.0; - return cultmath_advanced_erosion_filter_banded(position, base_height_and_slope, fade_target, p, band); -} - -#endif diff --git a/shaders/AdvancedErosionFilter.hlsl.meta b/shaders/AdvancedErosionFilter.hlsl.meta deleted file mode 100644 index a354e33..0000000 --- a/shaders/AdvancedErosionFilter.hlsl.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 3252ac02e8853ef4b9d1fbc5204b5c6b -ShaderIncludeImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/shaders/CultMath.hlsl b/shaders/CultMath.hlsl index 84d244b..4c00fb6 100644 --- a/shaders/CultMath.hlsl +++ b/shaders/CultMath.hlsl @@ -299,98 +299,4 @@ float cultmath_value_noise_texture_bicubic(Texture2D noise_texture, Sampl return cultmath_catmullrom(row0, row1, row2, row3, local.y); } -#include "AdvancedErosionFilter.hlsl" -#include "Planetary.hlsl" - -struct CultMathSphericalErosionParameters -{ - float frequency; float amplitude; float lacunarity; float gain; - float slope_strength; float detail; float seed; int octaves; -}; - -struct CultMathSphericalErosionSample -{ - float height_offset; float ridge; float gully; float3 gradient; -}; - -float cultmath_spherical_cell_wave(float3 position, float3 flow, float3 cell, float seed) -{ - float3 pivot = cell + float3(cultmath_hash(cell + seed + 11.7), cultmath_hash(cell.yzx + seed + 37.1), cultmath_hash(cell.zyx + seed + 73.9)); - return dot(position - pivot, flow) * 6.28318530718 + cultmath_hash(cell + seed + 101.3) * 6.28318530718; -} - -float cultmath_spherical_directional_wave(float3 position, float3 flow, float seed) -{ - float3 cell = floor(position); - float3 local = cultmath_frac(position); - float3 weights = local * local * (3.0 - 2.0 * local); - float z0 = cultmath_lerp( - cultmath_lerp(cultmath_spherical_cell_wave(position, flow, cell + float3(0,0,0), seed), cultmath_spherical_cell_wave(position, flow, cell + float3(1,0,0), seed), weights.x), - cultmath_lerp(cultmath_spherical_cell_wave(position, flow, cell + float3(0,1,0), seed), cultmath_spherical_cell_wave(position, flow, cell + float3(1,1,0), seed), weights.x), weights.y); - float z1 = cultmath_lerp( - cultmath_lerp(cultmath_spherical_cell_wave(position, flow, cell + float3(0,0,1), seed), cultmath_spherical_cell_wave(position, flow, cell + float3(1,0,1), seed), weights.x), - cultmath_lerp(cultmath_spherical_cell_wave(position, flow, cell + float3(0,1,1), seed), cultmath_spherical_cell_wave(position, flow, cell + float3(1,1,1), seed), weights.x), weights.y); - return cultmath_lerp(z0, z1, weights.z); -} - -float3 cultmath_spherical_stable_tangent(float3 position) -{ - float3 axis = abs(position.z) < 0.8 ? float3(0,0,1) : float3(0,1,0); - return cultmath_normalize(cross(axis, position)); -} - -CultMathSphericalErosionSample cultmath_spherical_erosion(float3 unit_position, float base_height, float3 base_gradient, CultMathSphericalErosionParameters p) -{ - float3 position = cultmath_normalize(unit_position); - float3 gradient = base_gradient - position * dot(base_gradient, position); - float slope = length(gradient); - float3 flow = slope > 1.0e-6 ? -gradient / slope : cultmath_spherical_stable_tangent(position); - float frequency = max(p.frequency, 0.001); - float amplitude = p.amplitude; - float combi_mask = 1.0 - pow(1.0 - cultmath_saturate(slope * p.slope_strength), 2.0); - float fade_target = cultmath_saturate(base_height * 0.5 + 0.5) * 2.0 - 1.0; - CultMathSphericalErosionSample result = (CultMathSphericalErosionSample)0; - [loop] for (int octave = 0; octave < min(max(p.octaves, 0), 8); octave++) - { - float phase = cultmath_spherical_directional_wave(position * frequency, flow, p.seed + octave * 19.19); - float wave = cos(phase); - float wave_slope = sin(phase); - float faded = cultmath_lerp(fade_target, wave, combi_mask); - result.height_offset += faded * amplitude; - result.ridge = max(result.ridge, cultmath_saturate(faded)); - result.gully = max(result.gully, cultmath_saturate(-faded)); - float3 across = cultmath_normalize(cross(position, flow)); - gradient += across * (sign(wave_slope) * amplitude * frequency * combi_mask); - gradient -= position * dot(gradient, position); - slope = length(gradient); - flow = slope > 1.0e-6 ? -gradient / slope : cultmath_spherical_stable_tangent(position); - float new_mask = 1.0 - pow(1.0 - cultmath_saturate(abs(wave_slope) * 1.35), 2.0); - combi_mask = (1.0 - pow(1.0 - cultmath_saturate(combi_mask), max(p.detail, 0.001))) * new_mask; - fade_target = faded; - frequency *= max(p.lacunarity, 1.01); - amplitude *= cultmath_saturate(p.gain); - } - result.gradient = gradient; - return result; -} - -float3 cultmath_planetary_radial_refinement_step( - float3 position, - float3 ray_direction, - float3 center, - float target_radius, - float minimum_derivative, - float maximum_correction) -{ - float3 local=position-center; - float radius=length(local); - float3 radial=local/max(radius,0.0001); - float error=radius-target_radius; - float derivative=dot(ray_direction,radial); - float safe_minimum=max(minimum_derivative,0.0001); - float safe_derivative=abs(derivative)>safe_minimum?derivative:(derivative<0?-safe_minimum:safe_minimum); - float correction=clamp(-error/safe_derivative,-abs(maximum_correction),abs(maximum_correction)); - return position+ray_direction*correction; -} - #endif diff --git a/shaders/Planetary.hlsl b/shaders/Planetary.hlsl deleted file mode 100644 index 81e312b..0000000 --- a/shaders/Planetary.hlsl +++ /dev/null @@ -1,392 +0,0 @@ -#ifndef CULTMATH_PLANETARY_HLSL -#define CULTMATH_PLANETARY_HLSL - -static const float CULTMATH_PLANETARY_QUARTER_PI = 0.78539816339744830962; - -float3 cultmath_planetary_face_direction(int face, float2 coordinate) -{ - float2 tangent = tan(coordinate * CULTMATH_PLANETARY_QUARTER_PI); - float3 cube = face == 0 ? float3(1.0, tangent.y, -tangent.x) - : face == 1 ? float3(-1.0, tangent.y, tangent.x) - : face == 2 ? float3(tangent.x, 1.0, -tangent.y) - : face == 3 ? float3(tangent.x, -1.0, tangent.y) - : face == 4 ? float3(tangent.x, tangent.y, 1.0) - : float3(-tangent.x, tangent.y, -1.0); - return cultmath_normalize(cube); -} - -float2 cultmath_planetary_face_coordinate(float3 direction, out int face) -{ - float3 d = cultmath_normalize(direction); - float3 a = abs(d); - float2 cube_uv; - if (a.x >= a.y && a.x >= a.z) - { - if (d.x >= 0.0) { face = 0; cube_uv = float2(-d.z, d.y) / a.x; } - else { face = 1; cube_uv = float2(d.z, d.y) / a.x; } - } - else if (a.y >= a.z) - { - if (d.y >= 0.0) { face = 2; cube_uv = float2(d.x, -d.z) / a.y; } - else { face = 3; cube_uv = float2(d.x, d.z) / a.y; } - } - else - { - if (d.z >= 0.0) { face = 4; cube_uv = d.xy / a.z; } - else { face = 5; cube_uv = float2(-d.x, d.y) / a.z; } - } - return atan(cube_uv) / CULTMATH_PLANETARY_QUARTER_PI; -} - -bool cultmath_planetary_page_local(float3 direction, float4 tile_address, float active, out float2 local) -{ - int face; - float2 uv = cultmath_planetary_face_coordinate(direction, face); - local = 0.0; - if (active < 0.5 || abs((float)face - tile_address.x) > 0.25) return false; - float axis_tiles = exp2(tile_address.y); - float2 scaled = (uv * 0.5 + 0.5) * axis_tiles; - float2 canonical = floor(min(scaled, axis_tiles - 1.0e-5)); - if (any(abs(canonical - tile_address.zw) > 0.25)) return false; - local = scaled - tile_address.zw; - return all(local >= 0.0) && all(local <= 1.0); -} - -float3 cultmath_planetary_surface_normal(float3 direction, float3 world_distance_tangent_gradient) -{ - direction = cultmath_normalize(direction); - float3 tangent_gradient = world_distance_tangent_gradient - direction * dot(world_distance_tangent_gradient, direction); - return cultmath_normalize(direction - tangent_gradient); -} - -float2 cultmath_planetary_equirectangular_forward(float3 direction) -{ - direction = cultmath_normalize(direction); - return float2(atan2(direction.y, direction.x) / CULTMATH_PI, asin(clamp(direction.z, -1.0, 1.0)) / CULTMATH_HALF_PI); -} - -float3 cultmath_planetary_equirectangular_inverse(float2 coordinate) -{ - float longitude = coordinate.x * CULTMATH_PI; - float latitude = coordinate.y * CULTMATH_HALF_PI; - float cos_latitude = cos(latitude); - return cultmath_normalize(float3(cos_latitude * cos(longitude), cos_latitude * sin(longitude), sin(latitude))); -} - -static const float CULTMATH_EQUAL_EARTH_A1 = 1.340264; -static const float CULTMATH_EQUAL_EARTH_A2 = -0.081106; -static const float CULTMATH_EQUAL_EARTH_A3 = 0.000893; -static const float CULTMATH_EQUAL_EARTH_A4 = 0.003796; -static const float CULTMATH_EQUAL_EARTH_M = 0.8660254037844386; -static const float CULTMATH_EQUAL_EARTH_X_MAX = 2.706629; -static const float CULTMATH_EQUAL_EARTH_Y_MAX = 1.3173628; - -float cultmath_equal_earth_y(float theta) -{ - float theta2 = theta * theta; - float theta6 = theta2 * theta2 * theta2; - return theta * (CULTMATH_EQUAL_EARTH_A1 + CULTMATH_EQUAL_EARTH_A2 * theta2 + theta6 * (CULTMATH_EQUAL_EARTH_A3 + CULTMATH_EQUAL_EARTH_A4 * theta2)); -} - -float2 cultmath_planetary_equal_earth_forward(float3 direction) -{ - direction = cultmath_normalize(direction); - float longitude = atan2(direction.y, direction.x); - float latitude = asin(clamp(direction.z, -1.0, 1.0)); - float theta = asin(CULTMATH_EQUAL_EARTH_M * sin(latitude)); - float theta2 = theta * theta; - float theta6 = theta2 * theta2 * theta2; - float denominator = CULTMATH_EQUAL_EARTH_M * (CULTMATH_EQUAL_EARTH_A1 + 3.0 * CULTMATH_EQUAL_EARTH_A2 * theta2 + theta6 * (7.0 * CULTMATH_EQUAL_EARTH_A3 + 9.0 * CULTMATH_EQUAL_EARTH_A4 * theta2)); - return float2(longitude * cos(theta) / denominator / CULTMATH_EQUAL_EARTH_X_MAX, cultmath_equal_earth_y(theta) / CULTMATH_EQUAL_EARTH_Y_MAX); -} - -float3 cultmath_planetary_equal_earth_inverse(float2 coordinate) -{ - float target_y = coordinate.y * CULTMATH_EQUAL_EARTH_Y_MAX; - float theta = target_y / CULTMATH_EQUAL_EARTH_A1; - [unroll] for (int iteration = 0; iteration < 8; iteration++) - { - float theta2 = theta * theta; - float theta6 = theta2 * theta2 * theta2; - float derivative = CULTMATH_EQUAL_EARTH_A1 + 3.0 * CULTMATH_EQUAL_EARTH_A2 * theta2 + theta6 * (7.0 * CULTMATH_EQUAL_EARTH_A3 + 9.0 * CULTMATH_EQUAL_EARTH_A4 * theta2); - theta -= (cultmath_equal_earth_y(theta) - target_y) / derivative; - } - float latitude = asin(clamp(sin(theta) / CULTMATH_EQUAL_EARTH_M, -1.0, 1.0)); - float theta2 = theta * theta; - float theta6 = theta2 * theta2 * theta2; - float denominator = CULTMATH_EQUAL_EARTH_M * (CULTMATH_EQUAL_EARTH_A1 + 3.0 * CULTMATH_EQUAL_EARTH_A2 * theta2 + theta6 * (7.0 * CULTMATH_EQUAL_EARTH_A3 + 9.0 * CULTMATH_EQUAL_EARTH_A4 * theta2)); - float longitude = coordinate.x * CULTMATH_EQUAL_EARTH_X_MAX * denominator / cos(theta); - float cos_latitude = cos(latitude); - 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; - int seed; - CultMathAdvancedErosionParameters erosion; -}; - -struct CultMathPlanetaryBaseFieldSample -{ - float radial_displacement; - float3 radial_gradient; - float field_value; - float3 field_gradient; - float fade_target; -}; - -struct CultMathPlanetarySurfaceSample -{ - float radial_displacement; - float3 tangent_gradient; - float3 surface_normal; - float slope; - float ridge; - float gully; - float finest_resolved_wavelength; - float unresolved_height_bound; -}; - -struct CultMathPlanetaryPageSample -{ - float4 height_gradient; - float2 masks; -}; - -CultMathPlanetaryPageSample cultmath_planetary_page_lerp( - CultMathPlanetaryPageSample a, - CultMathPlanetaryPageSample b, - CultMathPlanetaryPageSample c, - CultMathPlanetaryPageSample d, - float2 fraction) -{ - CultMathPlanetaryPageSample result; - result.height_gradient = lerp(lerp(a.height_gradient, b.height_gradient, fraction.x), lerp(c.height_gradient, d.height_gradient, fraction.x), fraction.y); - result.masks = lerp(lerp(a.masks, b.masks, fraction.x), lerp(c.masks, d.masks, fraction.x), fraction.y); - return result; -} - -CultMathPlanetaryPageSample cultmath_planetary_residual_sample( - CultMathPlanetarySurfaceSample child, - CultMathPlanetarySurfaceSample parent, - bool has_parent) -{ - CultMathPlanetaryPageSample result; - result.height_gradient = float4(child.radial_displacement, child.tangent_gradient); - if (has_parent) result.height_gradient -= float4(parent.radial_displacement, parent.tangent_gradient); - result.masks = float2(child.ridge, child.gully); - return result; -} - -float4 cultmath_planetary_summary_empty() -{ - return float4(3.402823466e+38, -3.402823466e+38, 0.0, 0.0); -} - -float4 cultmath_planetary_summary_accumulate(float4 summary, float4 height_gradient, float unresolved_height) -{ - summary.x = min(summary.x, height_gradient.x); - summary.y = max(summary.y, height_gradient.x); - summary.z = max(summary.z, length(height_gradient.yzw)); - summary.w = max(summary.w, max(unresolved_height, 0.0)); - return summary; -} - -float cultmath_planetary_pow4(float value) { float square = value * value; return square * square; } - -CultMathPlanetarySurfaceSample cultmath_planetary_field_sample( - CultMathPlanetaryFieldDefinition definition, - float3 unit_direction, - CultMathPlanetaryBaseFieldSample base_sample, - float footprint_meters) -{ - float3 direction = cultmath_normalize(unit_direction); - float3 radial_gradient = base_sample.radial_gradient - direction * dot(base_sample.radial_gradient, direction); - float3 field_gradient = base_sample.field_gradient - direction * dot(base_sample.field_gradient, direction); - CultMathAdvancedErosionParameters p = definition.erosion; - CultMathErosionBandSelection band = cultmath_select_erosion_bands( - p.scale * p.cell_scale, footprint_meters, p.octaves, p.lacunarity, - p.strength * p.scale, p.gain, 2.0); - float3 powers = float3( - cultmath_planetary_pow4(abs(direction.x)), - cultmath_planetary_pow4(abs(direction.y)), - cultmath_planetary_pow4(abs(direction.z))); - float power_sum = max(powers.x + powers.y + powers.z, 1.0e-6); - float3 weights = powers / power_sum; - float3 world = direction * definition.radius; - float seed = (float)definition.seed; - CultMathAdvancedErosionResult xy = cultmath_advanced_erosion_filter_banded( - world.xy + float2(713.0 + seed * 0.754877666, -291.0 + seed * 0.569840296), - float3(base_sample.field_value, field_gradient.x, field_gradient.y), base_sample.fade_target, p, band); - CultMathAdvancedErosionResult yz = cultmath_advanced_erosion_filter_banded( - world.yz + float2(-431.0 + seed * 0.438289027, 887.0 + seed * 0.328438163), - float3(base_sample.field_value, field_gradient.y, field_gradient.z), base_sample.fade_target, p, band); - CultMathAdvancedErosionResult zx = cultmath_advanced_erosion_filter_banded( - world.zx + float2(197.0 + seed * 0.219783071, 557.0 + seed * 0.145898034), - float3(base_sample.field_value, field_gradient.z, field_gradient.x), base_sample.fade_target, p, band); - float erosion_height = xy.delta.x * weights.z + yz.delta.x * weights.x + zx.delta.x * weights.y; - float3 erosion_gradient = float3( - xy.delta.y * weights.z + zx.delta.z * weights.y, - xy.delta.z * weights.z + yz.delta.y * weights.x, - yz.delta.z * weights.x + zx.delta.y * weights.y); - float3 power_gradient_x = float3(4.0 * direction.x * direction.x * direction.x / definition.radius, 0.0, 0.0); - float3 power_gradient_y = float3(0.0, 4.0 * direction.y * direction.y * direction.y / definition.radius, 0.0); - float3 power_gradient_z = float3(0.0, 0.0, 4.0 * direction.z * direction.z * direction.z / definition.radius); - power_gradient_x -= direction * dot(power_gradient_x, direction); - power_gradient_y -= direction * dot(power_gradient_y, direction); - power_gradient_z -= direction * dot(power_gradient_z, direction); - float3 power_sum_gradient = power_gradient_x + power_gradient_y + power_gradient_z; - float inverse_power_sum_squared = 1.0 / (power_sum * power_sum); - float3 weight_gradient_x = (power_gradient_x * power_sum - power_sum_gradient * powers.x) * inverse_power_sum_squared; - float3 weight_gradient_y = (power_gradient_y * power_sum - power_sum_gradient * powers.y) * inverse_power_sum_squared; - float3 weight_gradient_z = (power_gradient_z * power_sum - power_sum_gradient * powers.z) * inverse_power_sum_squared; - erosion_gradient += yz.delta.x * weight_gradient_x + zx.delta.x * weight_gradient_y + xy.delta.x * weight_gradient_z; - float3 gradient = radial_gradient + erosion_gradient; - gradient -= direction * dot(gradient, direction); - float ridge_evidence = xy.ridge_map * weights.z + yz.ridge_map * weights.x + zx.ridge_map * weights.y; - CultMathPlanetarySurfaceSample result; - result.radial_displacement = base_sample.radial_displacement + erosion_height; - result.tangent_gradient = gradient; - result.surface_normal = cultmath_planetary_surface_normal(direction, gradient); - result.slope = length(gradient); - result.ridge = saturate(ridge_evidence * 0.5 + 0.5); - result.gully = saturate(0.5 - ridge_evidence * 0.5); - result.finest_resolved_wavelength = band.finest_included_wavelength; - result.unresolved_height_bound = band.unresolved_height_bound; - return result; -} - -#endif diff --git a/shaders/Planetary.hlsl.meta b/shaders/Planetary.hlsl.meta deleted file mode 100644 index f777135..0000000 --- a/shaders/Planetary.hlsl.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: bb055ad5328888e479e90ae3fe62c3fe -ShaderIncludeImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/src/CultMath/AdvancedErosionFilter.cs b/src/CultMath/AdvancedErosionFilter.cs deleted file mode 100644 index 65db367..0000000 --- a/src/CultMath/AdvancedErosionFilter.cs +++ /dev/null @@ -1,139 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at https://mozilla.org/MPL/2.0/. -// -// Mathematical procedure adapted from Rune Skovbo Johansen's Advanced Terrain -// Erosion Filter (2025): https://www.shadertoy.com/view/wXcfWn - -namespace CultMath; - -public readonly record struct AdvancedErosionParameters( - float Scale, - float Strength, - float GullyWeight, - float Detail, - float4 Rounding, - float4 Onset, - float2 AssumedSlope, - float CellScale, - float Normalization, - int Octaves, - float Lacunarity, - float Gain) -{ - public static AdvancedErosionParameters Default => new( - 0.15f, 0.22f, 0.5f, 1.5f, - new float4(0.1f, 0.0f, 0.1f, 2.0f), - new float4(1.25f, 1.25f, 2.8f, 1.5f), - new float2(0.7f, 1.0f), - 0.7f, 0.5f, 5, 2.0f, 0.5f); -} - -public readonly record struct AdvancedErosionResult(float3 Delta, float Magnitude, float RidgeMap, float FadeTarget); - -public static class AdvancedErosionFilter -{ - public static AdvancedErosionResult Sample(float2 position, float3 baseHeightAndSlope, float fadeTarget, AdvancedErosionParameters p) - => Sample(position, baseHeightAndSlope, fadeTarget, p, new ErosionBandSelection(p.Octaves, 1.0f, 0.0f, 0.0f)); - - public static AdvancedErosionResult Sample(float2 position, float3 baseHeightAndSlope, float fadeTarget, AdvancedErosionParameters p, ErosionBandSelection band) - { - var strength = p.Strength * p.Scale; - fadeTarget = math.clamp(fadeTarget, -1.0f, 1.0f); - var input = baseHeightAndSlope; - var heightAndSlope = input; - var frequency = 1.0f / math.max(p.Scale * p.CellScale, 1.0e-10f); - var slope = new float2(heightAndSlope.y, heightAndSlope.z); - var slopeLength = math.max(math.length(slope), 1.0e-10f); - var magnitude = 0.0f; - var roundingMultiplier = 1.0f; - - var inputRounding = math.lerp(p.Rounding.y, p.Rounding.x, math.saturate(fadeTarget + 0.5f)) * p.Rounding.z; - var combinedMask = EaseOut(SmoothStart(slopeLength * p.Onset.x, inputRounding * p.Onset.x)); - var ridgeMask = EaseOut(slopeLength * p.Onset.z); - var ridgeFade = fadeTarget; - var gullySlope = math.lerp(slope, slope / slopeLength * p.AssumedSlope.x, p.AssumedSlope.y); - - var octaves = math.min(math.min(math.max(p.Octaves, 0), 8), math.max(band.ActiveOctaves, 0)); - for (var octave = 0; octave < octaves; octave++) - { - var phacelle = Phacelle(position * frequency, SafeNormalize(gullySlope), p.CellScale, 0.25f, p.Normalization); - var derivativeDirection = new float2(phacelle.z, phacelle.w) * -frequency; - var sloping = math.abs(phacelle.y); - gullySlope += math.sign(phacelle.y) * derivativeDirection * strength * p.GullyWeight; - - var gullies = new float3(phacelle.x, phacelle.y * derivativeDirection.x, phacelle.y * derivativeDirection.y); - var faded = math.lerp(new float3(fadeTarget, 0.0f, 0.0f), gullies * p.GullyWeight, combinedMask); - var octaveWeight = octave == octaves - 1 ? math.saturate(band.FinalOctaveWeight) : 1.0f; - heightAndSlope += faded * strength * octaveWeight; - magnitude += strength * octaveWeight; - fadeTarget = faded.x; - - var octaveRounding = math.lerp(p.Rounding.y, p.Rounding.x, math.saturate(phacelle.x + 0.5f)) * roundingMultiplier; - var newMask = EaseOut(SmoothStart(sloping * p.Onset.y, octaveRounding * p.Onset.y)); - combinedMask = PowInverse(combinedMask, p.Detail) * newMask; - - ridgeFade = math.lerp(ridgeFade, math.lerp(ridgeFade, gullies.x, ridgeMask), octaveWeight); - ridgeMask *= math.lerp(1.0f, EaseOut(sloping * p.Onset.w), octaveWeight); - strength *= p.Gain; - frequency *= p.Lacunarity; - roundingMultiplier *= p.Rounding.w; - } - - return new AdvancedErosionResult(heightAndSlope - input, magnitude, ridgeFade * (1.0f - ridgeMask), fadeTarget); - } - - public static float4 Phacelle(float2 position, float2 normalDirection, float frequency, float offsetCycles, float normalization) - { - var sideDirection = new float2(-normalDirection.y, normalDirection.x) * frequency * math.TAU; - var offset = offsetCycles * math.TAU; - var cell = math.floor(position); - var local = math.frac(position); - var phase = float2.zero; - var weightSum = 0.0f; - - for (var x = -1; x <= 2; x++) - for (var y = -1; y <= 2; y++) - { - var gridOffset = new float2(x, y); - var gridPoint = cell + gridOffset; - var randomOffset = Hash2(gridPoint) * 0.5f; - var delta = local - gridOffset - randomOffset; - var weight = math.max(0.0f, math.exp(-math.dot(delta, delta) * 2.0f) - 0.01111f); - weightSum += weight; - var wave = math.dot(delta, sideDirection) + offset; - phase += new float2(math.cos(wave), math.sin(wave)) * weight; - } - - var interpolated = phase / math.max(weightSum, 1.0e-20f); - var magnitude = math.max(1.0f - math.saturate(normalization), math.length(interpolated)); - var normalized = interpolated / math.max(magnitude, 1.0e-20f); - return new float4(normalized, sideDirection.x, sideDirection.y); - } - - private static float2 Hash2(float2 input) - { - var x = (int)input.x; - var y = (int)input.y; - return new float2(HashUnit(x, y, 0x68bc21ebu), HashUnit(x, y, 0x02e5be93u)) * 2.0f - float2.one; - } - - private static float HashUnit(int x, int y, uint salt) - { - var value = unchecked((uint)x * 0x8da6b343u ^ (uint)y * 0xd8163841u ^ salt); - value ^= value >> 16; value *= 0x7feb352du; value ^= value >> 15; value *= 0x846ca68bu; value ^= value >> 16; - return (value & 0x00ffffffu) * (1.0f / 16777216.0f); - } - - private static float2 SafeNormalize(float2 value) - { - var length = math.length(value); - return math.abs(length) > 1.0e-10f ? value / length : value; - } - - private static float PowInverse(float value, float power) => 1.0f - math.pow(1.0f - math.saturate(value), power); - private static float EaseOut(float value) { var inverse = 1.0f - math.saturate(value); return 1.0f - inverse * inverse; } - private static float SmoothStart(float value, float smoothing) => smoothing <= 1.0e-10f || value >= smoothing - ? value - 0.5f * smoothing - : 0.5f * value * value / smoothing; -} diff --git a/src/CultMath/AdvancedErosionFilter.cs.meta b/src/CultMath/AdvancedErosionFilter.cs.meta deleted file mode 100644 index 52f7ca1..0000000 --- a/src/CultMath/AdvancedErosionFilter.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: bec15fdcbe97a7f44a24bdb3ff79e1e7 \ No newline at end of file diff --git a/src/CultMath/CultMath.csproj b/src/CultMath/CultMath.csproj index 61b57c9..01901b6 100644 --- a/src/CultMath/CultMath.csproj +++ b/src/CultMath/CultMath.csproj @@ -19,8 +19,6 @@ - - diff --git a/src/CultMath/ErosionFrequencyBands.cs b/src/CultMath/ErosionFrequencyBands.cs deleted file mode 100644 index 3c63eb6..0000000 --- a/src/CultMath/ErosionFrequencyBands.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace CultMath; - -public readonly record struct ErosionBandSelection( - int ActiveOctaves, - float FinalOctaveWeight, - float FinestIncludedWavelength, - float UnresolvedHeightBound) -{ - public bool HasSamples => ActiveOctaves > 0; -} - -public static class ErosionFrequencyBands -{ - public static ErosionBandSelection Select( - float baseWavelength, - float sampleSpacing, - int maximumOctaves, - float lacunarity, - float baseAmplitude, - float gain, - float transitionRatio = 2.0f) - { - baseWavelength = math.max(baseWavelength, 1.0e-6f); - sampleSpacing = math.max(sampleSpacing, 0.0f); - lacunarity = math.max(lacunarity, 1.01f); - transitionRatio = math.max(transitionRatio, 1.01f); - maximumOctaves = math.min(math.max(maximumOctaves, 0), 16); - var nyquistWavelength = sampleSpacing * 2.0f; - var wavelength = baseWavelength; - var active = 0; - var finalWeight = 0.0f; - var finest = 0.0f; - - for (var octave = 0; octave < maximumOctaves; octave++) - { - var weight = nyquistWavelength <= 0.0f - ? 1.0f - : math.smoothstep(nyquistWavelength, nyquistWavelength * transitionRatio, wavelength); - if (weight <= 0.0f) break; - active++; - finalWeight = weight; - finest = wavelength; - wavelength /= lacunarity; - } - - var omittedAmplitude = 0.0f; - var amplitude = math.abs(baseAmplitude); - for (var octave = 0; octave < maximumOctaves; octave++) - { - if (octave >= active) omittedAmplitude += amplitude; - else if (octave == active - 1) omittedAmplitude += amplitude * (1.0f - finalWeight); - amplitude *= math.abs(gain); - } - - return new ErosionBandSelection(active, active > 0 ? finalWeight : 0.0f, finest, omittedAmplitude); - } -} diff --git a/src/CultMath/ErosionFrequencyBands.cs.meta b/src/CultMath/ErosionFrequencyBands.cs.meta deleted file mode 100644 index cae9964..0000000 --- a/src/CultMath/ErosionFrequencyBands.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 05e7969589dd09b43bd0fda672d25a0b \ No newline at end of file diff --git a/src/CultMath/PlanetaryField.cs b/src/CultMath/PlanetaryField.cs deleted file mode 100644 index 4d5596e..0000000 --- a/src/CultMath/PlanetaryField.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System; - -namespace CultMath; - -public readonly record struct PlanetaryQueryScale(float FootprintMeters, float MaximumUnresolvedHeight) -{ - public static PlanetaryQueryScale AtFootprint(float footprintMeters, float maximumUnresolvedHeight = float.PositiveInfinity) - => new PlanetaryQueryScale(footprintMeters, maximumUnresolvedHeight).Validate(); - - public PlanetaryQueryScale Validate() - { - if (!float.IsFinite(FootprintMeters) || FootprintMeters < 0) throw new ArgumentOutOfRangeException(nameof(FootprintMeters)); - if (float.IsNaN(MaximumUnresolvedHeight) || MaximumUnresolvedHeight < 0) throw new ArgumentOutOfRangeException(nameof(MaximumUnresolvedHeight)); - return this; - } -} - -public readonly record struct PlanetaryFieldDefinition( - ulong FieldVersion, - float Radius, - int Seed, - AdvancedErosionParameters Erosion) -{ - public static PlanetaryFieldDefinition Create( - ulong baseFieldVersion, - float radius, - int seed, - AdvancedErosionParameters erosion) - => new PlanetaryFieldDefinition(PlanetaryFieldIdentity.Compute(baseFieldVersion, radius, seed, erosion), radius, seed, erosion).Validate(); - - public PlanetaryFieldDefinition Validate() - { - if (FieldVersion == 0) throw new ArgumentOutOfRangeException(nameof(FieldVersion)); - if (!float.IsFinite(Radius) || Radius <= 0) throw new ArgumentOutOfRangeException(nameof(Radius)); - if (!float.IsFinite(Erosion.Scale) || Erosion.Scale <= 0) throw new ArgumentOutOfRangeException(nameof(Erosion)); - return this; - } -} - -public static class PlanetaryFieldIdentity -{ - public static ulong Compute(ulong baseFieldVersion, float radius, int seed, in AdvancedErosionParameters erosion) - { - if (baseFieldVersion == 0) throw new ArgumentOutOfRangeException(nameof(baseFieldVersion)); - const ulong offset = 14695981039346656037UL; - const ulong prime = 1099511628211UL; - var hash = offset; - Add(baseFieldVersion); - Add((uint)BitConverter.SingleToInt32Bits(radius)); Add((uint)seed); - Add((uint)BitConverter.SingleToInt32Bits(erosion.Scale)); Add((uint)BitConverter.SingleToInt32Bits(erosion.Strength)); - Add((uint)BitConverter.SingleToInt32Bits(erosion.GullyWeight)); Add((uint)BitConverter.SingleToInt32Bits(erosion.Detail)); - AddFloat4(erosion.Rounding); AddFloat4(erosion.Onset); - Add((uint)BitConverter.SingleToInt32Bits(erosion.AssumedSlope.x)); Add((uint)BitConverter.SingleToInt32Bits(erosion.AssumedSlope.y)); - Add((uint)BitConverter.SingleToInt32Bits(erosion.CellScale)); Add((uint)BitConverter.SingleToInt32Bits(erosion.Normalization)); - Add((uint)erosion.Octaves); Add((uint)BitConverter.SingleToInt32Bits(erosion.Lacunarity)); Add((uint)BitConverter.SingleToInt32Bits(erosion.Gain)); - return hash == 0 ? 1 : hash; - - void Add(ulong value) => hash = unchecked((hash ^ value) * prime); - void AddFloat4(float4 value) - { - Add((uint)BitConverter.SingleToInt32Bits(value.x)); Add((uint)BitConverter.SingleToInt32Bits(value.y)); - Add((uint)BitConverter.SingleToInt32Bits(value.z)); Add((uint)BitConverter.SingleToInt32Bits(value.w)); - } - } -} - -/// -/// Application-supplied base evidence at one unit spherical direction. -/// FieldValue and FieldGradient drive erosion. RadialDisplacement and -/// RadialGradient describe the uneroded surface in metres. -/// -public readonly record struct PlanetaryBaseFieldSample( - float RadialDisplacement, - float3 RadialGradient, - float FieldValue, - float3 FieldGradient, - float FadeTarget); - -public readonly record struct PlanetarySurfaceSample( - ulong FieldVersion, - float3 UnitDirection, - float Radius, - float RadialDisplacement, - float3 TangentGradient, - float3 SurfaceNormal, - float Slope, - float Ridge, - float Gully, - float FinestResolvedWavelength, - float UnresolvedHeightBound) -{ - public float SurfaceRadius => Radius + RadialDisplacement; - public float3 PlanetLocalPosition => UnitDirection * SurfaceRadius; - public bool Meets(PlanetaryQueryScale scale) => UnresolvedHeightBound <= scale.MaximumUnresolvedHeight; -} - -public static class PlanetaryField -{ - public static PlanetarySurfaceSample Sample( - in PlanetaryFieldDefinition definition, - float3 unitDirection, - in PlanetaryBaseFieldSample baseSample, - in PlanetaryQueryScale scale) - { - definition.Validate(); - scale.Validate(); - PlanetaryTopology.ValidateDirection(unitDirection); - PlanetaryTopology.ValidateFinite(baseSample.RadialGradient, nameof(baseSample)); - PlanetaryTopology.ValidateFinite(baseSample.FieldGradient, nameof(baseSample)); - if (!float.IsFinite(baseSample.RadialDisplacement) || !float.IsFinite(baseSample.FieldValue) || !float.IsFinite(baseSample.FadeTarget)) - throw new ArgumentOutOfRangeException(nameof(baseSample)); - - var direction = math.normalize(unitDirection); - var radialGradient = Tangent(direction, baseSample.RadialGradient); - var fieldGradient = Tangent(direction, baseSample.FieldGradient); - var p = definition.Erosion; - var band = ErosionFrequencyBands.Select( - p.Scale * p.CellScale, - scale.FootprintMeters, - p.Octaves, - p.Lacunarity, - p.Strength * p.Scale, - p.Gain); - - var powers = new float3( - Pow4(MathF.Abs(direction.x)), - Pow4(MathF.Abs(direction.y)), - Pow4(MathF.Abs(direction.z))); - var powerSum = math.max(powers.x + powers.y + powers.z, 1.0e-6f); - var weights = powers / powerSum; - var world = direction * definition.Radius; - var seed = definition.Seed; - var xy = AdvancedErosionFilter.Sample( - world.xy + new float2(713 + seed * 0.754877666f, -291 + seed * 0.569840296f), - new float3(baseSample.FieldValue, fieldGradient.x, fieldGradient.y), baseSample.FadeTarget, p, band); - var yz = AdvancedErosionFilter.Sample( - world.yz + new float2(-431 + seed * 0.438289027f, 887 + seed * 0.328438163f), - new float3(baseSample.FieldValue, fieldGradient.y, fieldGradient.z), baseSample.FadeTarget, p, band); - var zx = AdvancedErosionFilter.Sample( - new float2(world.z, world.x) + new float2(197 + seed * 0.219783071f, 557 + seed * 0.145898034f), - new float3(baseSample.FieldValue, fieldGradient.z, fieldGradient.x), baseSample.FadeTarget, p, band); - - var erosionHeight = xy.Delta.x * weights.z + yz.Delta.x * weights.x + zx.Delta.x * weights.y; - var erosionGradient = new float3( - xy.Delta.y * weights.z + zx.Delta.z * weights.y, - xy.Delta.z * weights.z + yz.Delta.y * weights.x, - yz.Delta.z * weights.x + zx.Delta.y * weights.y); - var powerGradientX = Tangent(direction, new float3(4 * direction.x * direction.x * direction.x / definition.Radius, 0, 0)); - var powerGradientY = Tangent(direction, new float3(0, 4 * direction.y * direction.y * direction.y / definition.Radius, 0)); - var powerGradientZ = Tangent(direction, new float3(0, 0, 4 * direction.z * direction.z * direction.z / definition.Radius)); - var sumGradient = powerGradientX + powerGradientY + powerGradientZ; - var weightGradientX = (powerGradientX * powerSum - sumGradient * powers.x) / (powerSum * powerSum); - var weightGradientY = (powerGradientY * powerSum - sumGradient * powers.y) / (powerSum * powerSum); - var weightGradientZ = (powerGradientZ * powerSum - sumGradient * powers.z) / (powerSum * powerSum); - erosionGradient += yz.Delta.x * weightGradientX + zx.Delta.x * weightGradientY + xy.Delta.x * weightGradientZ; - var gradient = Tangent(direction, radialGradient + erosionGradient); - var ridgeEvidence = xy.RidgeMap * weights.z + yz.RidgeMap * weights.x + zx.RidgeMap * weights.y; - var ridge = math.saturate(ridgeEvidence * 0.5f + 0.5f); - var gully = math.saturate(0.5f - ridgeEvidence * 0.5f); - var displacement = baseSample.RadialDisplacement + erosionHeight; - - return new( - definition.FieldVersion, - direction, - definition.Radius, - displacement, - gradient, - PlanetaryTopology.SurfaceNormal(direction, gradient), - math.length(gradient), - ridge, - gully, - band.FinestIncludedWavelength, - band.UnresolvedHeightBound); - } - - public static void SampleBatch( - in PlanetaryFieldDefinition definition, - ReadOnlySpan directions, - in PlanetaryQueryScale scale, - TSource source, - Span destination) - where TSource : IPlanetaryBaseField - { - if (destination.Length < directions.Length) throw new ArgumentException("Destination is shorter than directions.", nameof(destination)); - for (var i = 0; i < directions.Length; i++) - { - var direction = math.normalize(directions[i]); - var baseSample = source.Sample(direction); - destination[i] = Sample(definition, direction, baseSample, scale); - } - } - - public static PlanetarySurfaceSample SamplePosition( - in PlanetaryFieldDefinition definition, - double3 planetLocalPosition, - in PlanetaryQueryScale scale, - TSource source) - where TSource : IPlanetaryBaseField - { - if (!double.IsFinite(planetLocalPosition.x) || !double.IsFinite(planetLocalPosition.y) || !double.IsFinite(planetLocalPosition.z)) - throw new ArgumentOutOfRangeException(nameof(planetLocalPosition)); - var length = Math.Sqrt(planetLocalPosition.x * planetLocalPosition.x + planetLocalPosition.y * planetLocalPosition.y + planetLocalPosition.z * planetLocalPosition.z); - if (length < 1.0e-12) throw new ArgumentOutOfRangeException(nameof(planetLocalPosition)); - var direction = new float3((float)(planetLocalPosition.x / length), (float)(planetLocalPosition.y / length), (float)(planetLocalPosition.z / length)); - return Sample(definition, direction, source.Sample(direction), scale); - } - - private static float3 Tangent(float3 direction, float3 gradient) => gradient - direction * math.dot(gradient, direction); - private static float Pow4(float value) { var square = value * value; return square * square; } -} - -public interface IPlanetaryBaseField -{ - PlanetaryBaseFieldSample Sample(float3 unitDirection); -} diff --git a/src/CultMath/PlanetaryField.cs.meta b/src/CultMath/PlanetaryField.cs.meta deleted file mode 100644 index 3e83f63..0000000 --- a/src/CultMath/PlanetaryField.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b61d6354e3aab054e94721e77e619bfa \ No newline at end of file diff --git a/src/CultMath/PlanetaryGpuPages.cs b/src/CultMath/PlanetaryGpuPages.cs deleted file mode 100644 index 024d26b..0000000 --- a/src/CultMath/PlanetaryGpuPages.cs +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 7afe847..0000000 --- a/src/CultMath/PlanetaryGpuPages.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 42173c60766948a4e8c376687033532e \ No newline at end of file diff --git a/src/CultMath/PlanetaryMapTileEncoding.cs b/src/CultMath/PlanetaryMapTileEncoding.cs deleted file mode 100644 index ec55661..0000000 --- a/src/CultMath/PlanetaryMapTileEncoding.cs +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index 76484f6..0000000 --- a/src/CultMath/PlanetaryMapTileEncoding.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 6898f6de57df20646a0b9077bb923592 \ No newline at end of file diff --git a/src/CultMath/PlanetaryPages.cs b/src/CultMath/PlanetaryPages.cs deleted file mode 100644 index 699a5c1..0000000 --- a/src/CultMath/PlanetaryPages.cs +++ /dev/null @@ -1,187 +0,0 @@ -using System; - -namespace CultMath; - -public readonly record struct PlanetaryPageLayout(PlanetaryTileAddress Tile, int InteriorSize, int BorderSize) -{ - public int StorageSize => checked(InteriorSize + BorderSize * 2); - - public PlanetaryPageLayout Validate() - { - if (InteriorSize < 2) throw new ArgumentOutOfRangeException(nameof(InteriorSize)); - if (BorderSize < 0 || BorderSize >= InteriorSize) throw new ArgumentOutOfRangeException(nameof(BorderSize)); - return this; - } -} - -public readonly record struct PlanetaryPageSummary( - float MinimumHeight, - float MaximumHeight, - float MaximumSlope, - float UnresolvedHeightBound, - float AngularTexelSize); - -public readonly record struct PlanetaryPageSample(float Height, float3 TangentGradient, float Ridge, float Gully); - -public static class PlanetaryPageSampling -{ - public static float3 Direction(PlanetaryPageLayout page, int storageX, int storageY) - { - page.Validate(); - if ((uint)storageX >= (uint)page.StorageSize || (uint)storageY >= (uint)page.StorageSize) - throw new ArgumentOutOfRangeException(nameof(storageX)); - var localU = (storageX - page.BorderSize) / (double)(page.InteriorSize - 1); - var localV = (storageY - page.BorderSize) / (double)(page.InteriorSize - 1); - return DirectionAtLocal(page, localU, localV); - } - - public static float3 DirectionAtLocal(PlanetaryPageLayout page, double localU, double localV) - { - page.Validate(); - if (!double.IsFinite(localU) || !double.IsFinite(localV)) throw new ArgumentOutOfRangeException(nameof(localU)); - var count = page.Tile.AxisTileCount; - return PlanetaryTopology.Direction(new( - page.Tile.Face, - -1.0 + 2.0 * (page.Tile.X + localU) / count, - -1.0 + 2.0 * (page.Tile.Y + localV) / count)); - } - - public static float AngularTexelSize(PlanetaryPageLayout page) - { - var center = page.BorderSize + (page.InteriorSize - 1) / 2; - var a = Direction(page, center, center); - var b = Direction(page, Math.Min(center + 1, page.StorageSize - 1), center); - return MathF.Acos(math.clamp(math.dot(a, b), -1.0f, 1.0f)); - } - - public static float NominalAngularTexelSize(PlanetaryPageLayout page) - { - page.Validate(); - return MathF.PI * 0.5f / (page.Tile.AxisTileCount * (page.InteriorSize - 1)); - } - - public static PlanetaryPageSample Bilinear( - ReadOnlySpan samples, - PlanetaryPageLayout page, - float2 local) - { - page.Validate(); - if (samples.Length != page.StorageSize * page.StorageSize) throw new ArgumentException("Page sample count does not match layout.", nameof(samples)); - var texel = local * (page.InteriorSize - 1) + page.BorderSize; - var x0 = Math.Clamp((int)MathF.Floor(texel.x), 0, page.StorageSize - 1); - var y0 = Math.Clamp((int)MathF.Floor(texel.y), 0, page.StorageSize - 1); - var x1 = Math.Min(x0 + 1, page.StorageSize - 1); - var y1 = Math.Min(y0 + 1, page.StorageSize - 1); - var tx = math.frac(texel.x); - var ty = math.frac(texel.y); - var a = Lerp(samples[y0 * page.StorageSize + x0], samples[y0 * page.StorageSize + x1], tx); - var b = Lerp(samples[y1 * page.StorageSize + x0], samples[y1 * page.StorageSize + x1], tx); - return Lerp(a, b, ty); - } - - public static PlanetaryPageSummary Summarize(ReadOnlySpan samples, PlanetaryPageLayout page, float unresolvedHeightBound) - { - page.Validate(); - if (samples.Length != page.StorageSize * page.StorageSize) throw new ArgumentException("Page sample count does not match layout.", nameof(samples)); - var min = float.PositiveInfinity; - var max = float.NegativeInfinity; - var maxSlope = 0.0f; - foreach (var sample in samples) - { - min = MathF.Min(min, sample.Height); - max = MathF.Max(max, sample.Height); - maxSlope = MathF.Max(maxSlope, math.length(sample.TangentGradient)); - } - return new(min, max, maxSlope, MathF.Max(unresolvedHeightBound, 0), AngularTexelSize(page)); - } - - private static PlanetaryPageSample Lerp(PlanetaryPageSample a, PlanetaryPageSample b, float t) => new( - math.lerp(a.Height, b.Height, t), - math.lerp(a.TangentGradient, b.TangentGradient, t), - math.lerp(a.Ridge, b.Ridge, t), - math.lerp(a.Gully, b.Gully, t)); -} - -public readonly record struct PlanetaryBakedPage( - PlanetaryPageLayout Layout, - PlanetaryPageSample[] Samples, - PlanetaryPageSummary Summary); - -public static class PlanetaryPageBaker -{ - public static PlanetaryBakedPage Bake( - in PlanetaryFieldDefinition field, - PlanetaryPageLayout layout, - TSource source) - where TSource : IPlanetaryBaseField - { - layout.Validate(); field.Validate(); - var spacing = PlanetaryPageSampling.NominalAngularTexelSize(layout) * field.Radius; - var parentSpacing = layout.Tile.Level == 0 - ? 0 - : PlanetaryPageSampling.NominalAngularTexelSize(new(layout.Tile.Parent(), layout.InteriorSize, layout.BorderSize)) * field.Radius; - var samples = new PlanetaryPageSample[layout.StorageSize * layout.StorageSize]; - var unresolved = 0.0f; - for (var y = 0; y < layout.StorageSize; y++) - for (var x = 0; x < layout.StorageSize; x++) - { - var direction = PlanetaryPageSampling.Direction(layout, x, y); - var baseSample = source.Sample(direction); - var child = PlanetaryField.Sample(field, direction, baseSample, PlanetaryQueryScale.AtFootprint(spacing)); - var height = child.RadialDisplacement; - var gradient = child.TangentGradient; - if (parentSpacing > 0) - { - var parent = PlanetaryField.Sample(field, direction, baseSample, PlanetaryQueryScale.AtFootprint(parentSpacing)); - height -= parent.RadialDisplacement; - gradient -= parent.TangentGradient; - } - samples[y * layout.StorageSize + x] = new(height, gradient, child.Ridge, child.Gully); - unresolved = MathF.Max(unresolved, child.UnresolvedHeightBound); - } - return new(layout, samples, PlanetaryPageSampling.Summarize(samples, layout, unresolved)); - } -} - -public readonly record struct PlanetaryResidentPage(PlanetaryBakedPage Page, float Blend) -{ - public PlanetaryResidentPage Validate() - { - if (!float.IsFinite(Blend) || Blend is < 0 or > 1) throw new ArgumentOutOfRangeException(nameof(Blend)); - Page.Layout.Validate(); - if (Page.Samples.Length != Page.Layout.StorageSize * Page.Layout.StorageSize) throw new ArgumentException("Page payload does not match layout."); - return this; - } -} - -public static class PlanetaryPageSetSampling -{ - public static bool TrySample(ReadOnlySpan pages, float3 direction, out PlanetaryPageSample sample) - { - PlanetaryTopology.ValidateDirection(direction); - var height = 0.0f; - var gradient = float3.zero; - var ridge = 0.0f; - var gully = 0.0f; - var found = false; - var deepestLevel = -1; - foreach (var resident in pages) - { - resident.Validate(); - if (!PlanetaryTopology.TryLocalCoordinate(direction, resident.Page.Layout.Tile, out var local)) continue; - var contribution = PlanetaryPageSampling.Bilinear(resident.Page.Samples, resident.Page.Layout, local); - var blend = math.saturate(resident.Blend); - height += contribution.Height * blend; - gradient += contribution.TangentGradient * blend; - if (resident.Page.Layout.Tile.Level >= deepestLevel) - { - ridge = found ? math.lerp(ridge, contribution.Ridge, blend) : contribution.Ridge; - gully = found ? math.lerp(gully, contribution.Gully, blend) : contribution.Gully; - deepestLevel = resident.Page.Layout.Tile.Level; - } - found = true; - } - sample = new(height, gradient, ridge, gully); - return found; - } -} diff --git a/src/CultMath/PlanetaryPages.cs.meta b/src/CultMath/PlanetaryPages.cs.meta deleted file mode 100644 index 91fdc27..0000000 --- a/src/CultMath/PlanetaryPages.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 7af9a0354faa56348a2e07188739a0ae \ No newline at end of file diff --git a/src/CultMath/PlanetaryPatch.cs b/src/CultMath/PlanetaryPatch.cs deleted file mode 100644 index 30c852f..0000000 --- a/src/CultMath/PlanetaryPatch.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Linq; - -namespace CultMath; - -public readonly record struct PlanetaryPatchVertex(float2 LocalCoordinate, float3 UnitDirection); - -public readonly record struct PlanetaryPatchMesh( - PlanetaryCubeFace Face, - int CellsPerAxis, - PlanetaryPatchVertex[] Vertices, - int[] Indices); - -public static class PlanetaryPatch -{ - public static PlanetaryPatchMesh CreateFace(PlanetaryCubeFace face, int cellsPerAxis) - { - if (!Enum.IsDefined(typeof(PlanetaryCubeFace), face)) throw new ArgumentOutOfRangeException(nameof(face)); - if (cellsPerAxis < 1 || cellsPerAxis > 4096) throw new ArgumentOutOfRangeException(nameof(cellsPerAxis)); - var axisVertices = cellsPerAxis + 1; - var vertices = new PlanetaryPatchVertex[checked(axisVertices * axisVertices)]; - for (var y = 0; y < axisVertices; y++) - for (var x = 0; x < axisVertices; x++) - { - var local = new float2(x / (float)cellsPerAxis, y / (float)cellsPerAxis); - vertices[y * axisVertices + x] = new(local, PlanetaryTopology.Direction(new(face, local.x * 2 - 1, local.y * 2 - 1))); - } - var indices = new int[checked(cellsPerAxis * cellsPerAxis * 6)]; - var write = 0; - for (var y = 0; y < cellsPerAxis; y++) - for (var x = 0; x < cellsPerAxis; x++) - { - var lowerLeft = y * axisVertices + x; - var lowerRight = lowerLeft + 1; - var upperLeft = lowerLeft + axisVertices; - var upperRight = upperLeft + 1; - indices[write++] = lowerLeft; indices[write++] = lowerRight; indices[write++] = upperRight; - indices[write++] = lowerLeft; indices[write++] = upperRight; indices[write++] = upperLeft; - } - return new(face, cellsPerAxis, vertices, indices); - } - - public static PlanetaryPatchMesh[] CreateCubeSphere(int cellsPerAxis) - => ((PlanetaryCubeFace[])Enum.GetValues(typeof(PlanetaryCubeFace))).Select(face => CreateFace(face, cellsPerAxis)).ToArray(); -} diff --git a/src/CultMath/PlanetaryPatch.cs.meta b/src/CultMath/PlanetaryPatch.cs.meta deleted file mode 100644 index efd8156..0000000 --- a/src/CultMath/PlanetaryPatch.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 04651571f7d240a418c1f6aafa217fa6 \ No newline at end of file diff --git a/src/CultMath/PlanetaryProjection.cs b/src/CultMath/PlanetaryProjection.cs deleted file mode 100644 index a82ebfd..0000000 --- a/src/CultMath/PlanetaryProjection.cs +++ /dev/null @@ -1,322 +0,0 @@ -using System; - -namespace CultMath; - -public enum PlanetaryProjectionKind -{ - Equirectangular, - WebMercator, - EqualEarth, - Orthographic, - AzimuthalEquidistant, - AzimuthalEqualArea, - CubeAtlas, - LocalTangent, -} - -public readonly record struct PlanetaryProjectionParameters( - PlanetaryProjectionKind Kind, - double CenterLongitude = 0, - double CenterLatitude = 0, - double Scale = 1) -{ - public PlanetaryProjectionParameters Validate() - { - if (!double.IsFinite(CenterLongitude) || !double.IsFinite(CenterLatitude) || !double.IsFinite(Scale) || Scale <= 0) - throw new ArgumentOutOfRangeException(nameof(Scale)); - if (CenterLatitude is < -Math.PI / 2 or > Math.PI / 2) throw new ArgumentOutOfRangeException(nameof(CenterLatitude)); - return this; - } -} - -public static class PlanetaryProjection -{ - private const double EqualEarthA1 = 1.340264; - private const double EqualEarthA2 = -0.081106; - private const double EqualEarthA3 = 0.000893; - private const double EqualEarthA4 = 0.003796; - private static readonly double EqualEarthM = Math.Sqrt(3) / 2; - private static readonly double EqualEarthXMax = Math.PI / (EqualEarthM * EqualEarthA1); - private static readonly double EqualEarthYMax = EqualEarthY(Math.Asin(EqualEarthM)); - public static readonly double WebMercatorMaximumLatitude = Math.Atan(Math.Sinh(Math.PI)); - - public static bool TryForward(float3 direction, PlanetaryProjectionParameters parameters, out double2 coordinate) - { - parameters.Validate(); - PlanetaryTopology.ValidateDirection(direction); - var d = math.normalize(direction); - if (parameters.Kind == PlanetaryProjectionKind.CubeAtlas) - { - var face = PlanetaryTopology.FaceCoordinate(d); - var index = (int)face.Face; - var column = index % 3; - var row = index / 3; - coordinate = new( - (-1 + (column + (face.U + 1) * 0.5) * (2.0 / 3.0)) / parameters.Scale, - (1 - (row + (face.V + 1) * 0.5)) / parameters.Scale); - return true; - } - - var longitude = Math.Atan2(d.y, d.x); - var latitude = Math.Asin(Math.Clamp(d.z, -1, 1)); - var deltaLongitude = WrapLongitude(longitude - parameters.CenterLongitude); - var centered = CenteredForward(deltaLongitude, latitude, parameters, out coordinate); - coordinate /= parameters.Scale; - return centered; - } - - public static bool TryInverse(double2 coordinate, PlanetaryProjectionParameters parameters, out float3 direction) - { - parameters.Validate(); - if (!double.IsFinite(coordinate.x) || !double.IsFinite(coordinate.y)) - { - direction = float3.zero; - return false; - } - coordinate *= parameters.Scale; - if (parameters.Kind == PlanetaryProjectionKind.CubeAtlas) - return TryInverseCubeAtlas(coordinate, out direction); - - if (!CenteredInverse(coordinate, parameters, out var deltaLongitude, out var latitude)) - { - direction = float3.zero; - return false; - } - var longitude = WrapLongitude(deltaLongitude + parameters.CenterLongitude); - var cosLatitude = Math.Cos(latitude); - direction = math.normalize(new float3( - (float)(cosLatitude * Math.Cos(longitude)), - (float)(cosLatitude * Math.Sin(longitude)), - (float)Math.Sin(latitude))); - return true; - } - - private static bool CenteredForward(double longitude, double latitude, PlanetaryProjectionParameters p, out double2 coordinate) - { - switch (p.Kind) - { - case PlanetaryProjectionKind.Equirectangular: - coordinate = new(longitude / Math.PI, latitude / (Math.PI * 0.5)); - return true; - case PlanetaryProjectionKind.WebMercator: - if (Math.Abs(latitude) > WebMercatorMaximumLatitude) { coordinate = double2.zero; return false; } - coordinate = new(longitude / Math.PI, Math.Asinh(Math.Tan(latitude)) / Math.PI); - return true; - case PlanetaryProjectionKind.EqualEarth: - { - var theta = Math.Asin(EqualEarthM * Math.Sin(latitude)); - var theta2 = theta * theta; - var theta6 = theta2 * theta2 * theta2; - var denominator = EqualEarthM * (EqualEarthA1 + 3 * EqualEarthA2 * theta2 + theta6 * (7 * EqualEarthA3 + 9 * EqualEarthA4 * theta2)); - coordinate = new(longitude * Math.Cos(theta) / denominator / EqualEarthXMax, EqualEarthY(theta) / EqualEarthYMax); - return true; - } - case PlanetaryProjectionKind.Orthographic: - case PlanetaryProjectionKind.AzimuthalEquidistant: - case PlanetaryProjectionKind.AzimuthalEqualArea: - case PlanetaryProjectionKind.LocalTangent: - return ForwardAzimuthal(longitude, latitude, p, out coordinate); - default: - throw new ArgumentOutOfRangeException(nameof(p)); - } - } - - private static bool CenteredInverse(double2 coordinate, PlanetaryProjectionParameters p, out double longitude, out double latitude) - { - longitude = latitude = 0; - switch (p.Kind) - { - case PlanetaryProjectionKind.Equirectangular: - if (Math.Abs(coordinate.y) > 1) return false; - longitude = coordinate.x * Math.PI; latitude = coordinate.y * Math.PI * 0.5; return Math.Abs(coordinate.x) <= 1; - case PlanetaryProjectionKind.WebMercator: - if (Math.Abs(coordinate.x) > 1 || Math.Abs(coordinate.y) > 1) return false; - longitude = coordinate.x * Math.PI; latitude = Math.Atan(Math.Sinh(coordinate.y * Math.PI)); return true; - case PlanetaryProjectionKind.EqualEarth: - { - if (Math.Abs(coordinate.x) > 1 || Math.Abs(coordinate.y) > 1) return false; - var targetY = coordinate.y * EqualEarthYMax; - var theta = targetY / EqualEarthA1; - for (var i = 0; i < 8; i++) - { - var theta2 = theta * theta; - var theta6 = theta2 * theta2 * theta2; - var f = EqualEarthY(theta) - targetY; - var derivative = EqualEarthA1 + 3 * EqualEarthA2 * theta2 + theta6 * (7 * EqualEarthA3 + 9 * EqualEarthA4 * theta2); - theta -= f / derivative; - } - var sinLatitude = Math.Sin(theta) / EqualEarthM; - if (Math.Abs(sinLatitude) > 1.0000000001) return false; - latitude = Math.Asin(Math.Clamp(sinLatitude, -1, 1)); - var thetaSquared = theta * theta; - var thetaSixth = thetaSquared * thetaSquared * thetaSquared; - var denominator = EqualEarthM * (EqualEarthA1 + 3 * EqualEarthA2 * thetaSquared + thetaSixth * (7 * EqualEarthA3 + 9 * EqualEarthA4 * thetaSquared)); - longitude = coordinate.x * EqualEarthXMax * denominator / Math.Cos(theta); - return Math.Abs(longitude) <= Math.PI + 1.0e-10; - } - case PlanetaryProjectionKind.Orthographic: - case PlanetaryProjectionKind.AzimuthalEquidistant: - case PlanetaryProjectionKind.AzimuthalEqualArea: - case PlanetaryProjectionKind.LocalTangent: - return InverseAzimuthal(coordinate, p, out longitude, out latitude); - default: - throw new ArgumentOutOfRangeException(nameof(p)); - } - } - - private static bool ForwardAzimuthal(double longitude, double latitude, PlanetaryProjectionParameters p, out double2 coordinate) - { - var sin0 = Math.Sin(p.CenterLatitude); var cos0 = Math.Cos(p.CenterLatitude); - var sin = Math.Sin(latitude); var cos = Math.Cos(latitude); var cosLon = Math.Cos(longitude); - var cosC = Math.Clamp(sin0 * sin + cos0 * cos * cosLon, -1, 1); - var rawX = cos * Math.Sin(longitude); - var rawY = cos0 * sin - sin0 * cos * cosLon; - double factor; - switch (p.Kind) - { - case PlanetaryProjectionKind.Orthographic: - if (cosC < 0) { coordinate = double2.zero; return false; } - factor = 1; break; - case PlanetaryProjectionKind.AzimuthalEquidistant: - { - var c = Math.Acos(cosC); factor = c < 1.0e-12 ? 1 : c / Math.Sin(c); factor /= Math.PI; break; - } - case PlanetaryProjectionKind.AzimuthalEqualArea: - factor = Math.Sqrt(2 / Math.Max(1 + cosC, 1.0e-20)) * 0.5; break; - case PlanetaryProjectionKind.LocalTangent: - if (cosC <= 0) { coordinate = double2.zero; return false; } - factor = 1 / cosC; break; - default: throw new ArgumentOutOfRangeException(nameof(p)); - } - coordinate = new(rawX * factor, rawY * factor); - return true; - } - - private static bool InverseAzimuthal(double2 coordinate, PlanetaryProjectionParameters p, out double longitude, out double latitude) - { - longitude = latitude = 0; - var rho = Math.Sqrt(coordinate.x * coordinate.x + coordinate.y * coordinate.y); - double c; - switch (p.Kind) - { - case PlanetaryProjectionKind.Orthographic: if (rho > 1) return false; c = Math.Asin(Math.Clamp(rho, 0, 1)); break; - case PlanetaryProjectionKind.AzimuthalEquidistant: if (rho > 1) return false; c = rho * Math.PI; break; - case PlanetaryProjectionKind.AzimuthalEqualArea: if (rho > 1) return false; c = 2 * Math.Asin(Math.Clamp(rho, 0, 1)); break; - case PlanetaryProjectionKind.LocalTangent: c = Math.Atan(rho); break; - default: throw new ArgumentOutOfRangeException(nameof(p)); - } - if (rho < 1.0e-12) { latitude = p.CenterLatitude; return true; } - var sinC = Math.Sin(c); var cosC = Math.Cos(c); - var sin0 = Math.Sin(p.CenterLatitude); var cos0 = Math.Cos(p.CenterLatitude); - latitude = Math.Asin(Math.Clamp(cosC * sin0 + coordinate.y * sinC * cos0 / rho, -1, 1)); - longitude = Math.Atan2(coordinate.x * sinC, rho * cos0 * cosC - coordinate.y * sin0 * sinC); - return true; - } - - private static bool TryInverseCubeAtlas(double2 coordinate, out float3 direction) - { - direction = float3.zero; - if (coordinate.x is < -1 or > 1 || coordinate.y is < -1 or > 1) return false; - var column = Math.Min((int)((coordinate.x + 1) * 1.5), 2); - var row = Math.Min((int)((1 - coordinate.y)), 1); - var u = ((coordinate.x + 1) * 1.5 - column) * 2 - 1; - var v = ((1 - coordinate.y) - row) * 2 - 1; - direction = PlanetaryTopology.Direction(new((PlanetaryCubeFace)(row * 3 + column), u, v)); - return true; - } - - private static double EqualEarthY(double theta) - { - var theta2 = theta * theta; - var theta6 = theta2 * theta2 * theta2; - return theta * (EqualEarthA1 + EqualEarthA2 * theta2 + theta6 * (EqualEarthA3 + EqualEarthA4 * theta2)); - } - - private static double WrapLongitude(double longitude) - { - longitude %= Math.PI * 2; - if (longitude > Math.PI) longitude -= Math.PI * 2; - if (longitude < -Math.PI) longitude += Math.PI * 2; - return longitude; - } -} - -public readonly record struct PlanetaryMapTileLayout( - PlanetaryProjectionParameters Projection, - int Level, - int X, - int Y, - int InteriorSize, - int BorderSize) -{ - public int StorageSize => checked(InteriorSize + BorderSize * 2); - - public PlanetaryMapTileLayout Validate() - { - Projection.Validate(); - if (Level is < 0 or > 30) throw new ArgumentOutOfRangeException(nameof(Level)); - var count = 1 << Level; - if ((uint)X >= (uint)count || (uint)Y >= (uint)count) throw new ArgumentOutOfRangeException(nameof(X)); - if (InteriorSize < 2 || BorderSize < 0 || BorderSize >= InteriorSize) throw new ArgumentOutOfRangeException(nameof(InteriorSize)); - return this; - } -} - -public static class PlanetaryMapTileSampling -{ - public static bool TryDirection(PlanetaryMapTileLayout tile, int storageX, int storageY, out float3 direction) - { - tile.Validate(); - if ((uint)storageX >= (uint)tile.StorageSize || (uint)storageY >= (uint)tile.StorageSize) throw new ArgumentOutOfRangeException(nameof(storageX)); - var count = 1 << tile.Level; - var localU = (storageX - tile.BorderSize) / (double)(tile.InteriorSize - 1); - var localV = (storageY - tile.BorderSize) / (double)(tile.InteriorSize - 1); - var map = new double2(-1 + 2 * (tile.X + localU) / count, 1 - 2 * (tile.Y + localV) / count); - return PlanetaryProjection.TryInverse(map, tile.Projection, out direction); - } -} - -public readonly record struct PlanetaryMapTileKey( - ulong FieldVersion, - uint ProjectionVersion, - uint LayerId, - PlanetaryMapTileLayout Layout, - PlanetaryQueryScale QueryScale) -{ - public PlanetaryMapTileKey Validate() - { - if (FieldVersion == 0 || ProjectionVersion == 0) throw new ArgumentOutOfRangeException(nameof(FieldVersion)); - Layout.Validate(); QueryScale.Validate(); - return this; - } -} - -public readonly record struct PlanetarySurfaceMapTile( - PlanetaryMapTileKey Key, - PlanetarySurfaceSample[] Samples, - bool[] Validity); - -public static class PlanetaryMapTileBaker -{ - public static PlanetarySurfaceMapTile Bake( - in PlanetaryFieldDefinition field, - PlanetaryMapTileKey key, - TSource source) - where TSource : IPlanetaryBaseField - { - field.Validate(); key.Validate(); - if (field.FieldVersion != key.FieldVersion) throw new ArgumentException("Tile field version does not match definition.", nameof(key)); - var count = key.Layout.StorageSize * key.Layout.StorageSize; - var samples = new PlanetarySurfaceSample[count]; - var validity = new bool[count]; - for (var y = 0; y < key.Layout.StorageSize; y++) - for (var x = 0; x < key.Layout.StorageSize; x++) - { - var index = y * key.Layout.StorageSize + x; - if (!PlanetaryMapTileSampling.TryDirection(key.Layout, x, y, out var direction)) continue; - samples[index] = PlanetaryField.Sample(field, direction, source.Sample(direction), key.QueryScale); - validity[index] = true; - } - return new(key, samples, validity); - } -} diff --git a/src/CultMath/PlanetaryProjection.cs.meta b/src/CultMath/PlanetaryProjection.cs.meta deleted file mode 100644 index 7b1e665..0000000 --- a/src/CultMath/PlanetaryProjection.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 5021202cf626f7442a57961255a8a217 \ No newline at end of file diff --git a/src/CultMath/PlanetaryQueries.cs b/src/CultMath/PlanetaryQueries.cs deleted file mode 100644 index 54a9a3e..0000000 --- a/src/CultMath/PlanetaryQueries.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; - -namespace CultMath; - -public readonly record struct PlanetaryRadialBounds(float MinimumDisplacement, float MaximumDisplacement) -{ - public PlanetaryRadialBounds Validate() - { - if (!float.IsFinite(MinimumDisplacement) || !float.IsFinite(MaximumDisplacement) || MinimumDisplacement > MaximumDisplacement) - throw new ArgumentOutOfRangeException(nameof(MinimumDisplacement)); - return this; - } -} - -public readonly record struct PlanetaryRayHit( - double Distance, - double3 PlanetLocalPosition, - PlanetarySurfaceSample Surface); - -public readonly record struct PlanetaryPathSample(double Distance, PlanetarySurfaceSample Surface); - -public readonly record struct PlanetaryRegionSummary( - float MinimumDisplacement, - float MaximumDisplacement, - float MaximumSlope, - float MaximumUnresolvedHeight); - -public static class PlanetaryQueries -{ - public static bool TryIntersectRay( - in PlanetaryFieldDefinition field, - double3 rayOrigin, - double3 rayDirection, - double maximumDistance, - in PlanetaryQueryScale scale, - in PlanetaryRadialBounds bounds, - TSource source, - out PlanetaryRayHit hit) - where TSource : IPlanetaryBaseField - { - field.Validate(); scale.Validate(); bounds.Validate(); - ValidateFinite(rayOrigin, nameof(rayOrigin)); ValidateFinite(rayDirection, nameof(rayDirection)); - if (!double.IsFinite(maximumDistance) || maximumDistance <= 0) throw new ArgumentOutOfRangeException(nameof(maximumDistance)); - var directionLength = Length(rayDirection); - if (directionLength < 1.0e-15) throw new ArgumentOutOfRangeException(nameof(rayDirection)); - var direction = rayDirection / directionLength; - var outerRadius = field.Radius + bounds.MaximumDisplacement + scale.MaximumUnresolvedHeight; - if (!double.IsFinite(outerRadius)) outerRadius = field.Radius + bounds.MaximumDisplacement; - if (!TrySphereInterval(rayOrigin, direction, outerRadius, out var enter, out var leave)) { hit = default; return false; } - var start = Math.Max(enter, 0); - var end = Math.Min(leave, maximumDistance); - if (start > end) { hit = default; return false; } - - var previousDistance = start; - var previous = SignedSurfaceDistance(field, rayOrigin + direction * previousDistance, scale, source, out var previousSample); - if (previous <= 0) - { - hit = new(previousDistance, rayOrigin + direction * previousDistance, previousSample); - return true; - } - var minimumStep = Math.Max(scale.FootprintMeters * 0.5, field.Radius * 1.0e-7); - for (var iteration = 0; iteration < 512 && previousDistance < end; iteration++) - { - var nextDistance = Math.Min(previousDistance + Math.Max(previous * 0.5, minimumStep), end); - var next = SignedSurfaceDistance(field, rayOrigin + direction * nextDistance, scale, source, out var nextSample); - if (next <= 0) - { - var low = previousDistance; - var high = nextDistance; - var surface = nextSample; - for (var refine = 0; refine < 32; refine++) - { - var middle = (low + high) * 0.5; - var value = SignedSurfaceDistance(field, rayOrigin + direction * middle, scale, source, out var middleSample); - if (value > 0) low = middle; - else { high = middle; surface = middleSample; } - } - var position = rayOrigin + direction * high; - hit = new(high, position, surface); - return true; - } - if (nextDistance >= end) break; - previousDistance = nextDistance; - previous = next; - previousSample = nextSample; - } - hit = default; - return false; - } - - public static double SegmentClearance( - in PlanetaryFieldDefinition field, - double3 start, - double3 end, - double sampleSpacing, - in PlanetaryQueryScale scale, - TSource source) - where TSource : IPlanetaryBaseField - { - ValidateFinite(start, nameof(start)); ValidateFinite(end, nameof(end)); - if (!double.IsFinite(sampleSpacing) || sampleSpacing <= 0) throw new ArgumentOutOfRangeException(nameof(sampleSpacing)); - var delta = end - start; - var distance = Length(delta); - var steps = Math.Max((int)Math.Ceiling(distance / sampleSpacing), 1); - var minimum = double.PositiveInfinity; - for (var i = 0; i <= steps; i++) - { - var position = start + delta * (i / (double)steps); - minimum = Math.Min(minimum, SignedSurfaceDistance(field, position, scale, source, out _)); - } - return minimum; - } - - public static PlanetaryPathSample[] SampleGreatCircle( - in PlanetaryFieldDefinition field, - float3 startDirection, - float3 endDirection, - int segmentCount, - in PlanetaryQueryScale scale, - TSource source) - where TSource : IPlanetaryBaseField - { - if (segmentCount < 1) throw new ArgumentOutOfRangeException(nameof(segmentCount)); - PlanetaryTopology.ValidateDirection(startDirection); PlanetaryTopology.ValidateDirection(endDirection); - var a = math.normalize(startDirection); var b = math.normalize(endDirection); - var angle = MathF.Acos(math.clamp(math.dot(a, b), -1, 1)); - var sinAngle = MathF.Sin(angle); - var result = new PlanetaryPathSample[segmentCount + 1]; - for (var i = 0; i <= segmentCount; i++) - { - var t = i / (float)segmentCount; - var direction = sinAngle < 1.0e-6f - ? math.normalize(math.lerp(a, b, t)) - : math.normalize(a * (MathF.Sin((1 - t) * angle) / sinAngle) + b * (MathF.Sin(t * angle) / sinAngle)); - var sample = PlanetaryField.Sample(field, direction, source.Sample(direction), scale); - result[i] = new(angle * field.Radius * t, sample); - } - return result; - } - - public static PlanetaryRegionSummary Summarize( - in PlanetaryFieldDefinition field, - ReadOnlySpan directions, - in PlanetaryQueryScale scale, - TSource source) - where TSource : IPlanetaryBaseField - { - if (directions.IsEmpty) throw new ArgumentException("At least one direction is required.", nameof(directions)); - var min = float.PositiveInfinity; var max = float.NegativeInfinity; var slope = 0.0f; var unresolved = 0.0f; - foreach (var direction in directions) - { - var normalized = math.normalize(direction); - var sample = PlanetaryField.Sample(field, normalized, source.Sample(normalized), scale); - min = MathF.Min(min, sample.RadialDisplacement); - max = MathF.Max(max, sample.RadialDisplacement); - slope = MathF.Max(slope, sample.Slope); - unresolved = MathF.Max(unresolved, sample.UnresolvedHeightBound); - } - return new(min, max, slope, unresolved); - } - - private static double SignedSurfaceDistance( - in PlanetaryFieldDefinition field, - double3 position, - in PlanetaryQueryScale scale, - TSource source, - out PlanetarySurfaceSample sample) - where TSource : IPlanetaryBaseField - { - var radius = Length(position); - if (radius < 1.0e-15) { sample = default; return double.NegativeInfinity; } - var direction = new float3((float)(position.x / radius), (float)(position.y / radius), (float)(position.z / radius)); - sample = PlanetaryField.Sample(field, direction, source.Sample(direction), scale); - return radius - sample.SurfaceRadius; - } - - private static bool TrySphereInterval(double3 origin, double3 direction, double radius, out double enter, out double leave) - { - var b = Dot(origin, direction); - var c = Dot(origin, origin) - radius * radius; - var discriminant = b * b - c; - if (discriminant < 0) { enter = leave = 0; return false; } - var root = Math.Sqrt(discriminant); - enter = -b - root; leave = -b + root; return leave >= 0; - } - - private static double Dot(double3 a, double3 b) => a.x * b.x + a.y * b.y + a.z * b.z; - private static double Length(double3 value) => Math.Sqrt(Dot(value, value)); - private static void ValidateFinite(double3 value, string name) - { - if (!double.IsFinite(value.x) || !double.IsFinite(value.y) || !double.IsFinite(value.z)) throw new ArgumentOutOfRangeException(name); - } -} diff --git a/src/CultMath/PlanetaryQueries.cs.meta b/src/CultMath/PlanetaryQueries.cs.meta deleted file mode 100644 index 6fd1141..0000000 --- a/src/CultMath/PlanetaryQueries.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 2ec43c567214b8247bda19b5c120c200 \ No newline at end of file diff --git a/src/CultMath/PlanetaryRadialRefinement.cs b/src/CultMath/PlanetaryRadialRefinement.cs deleted file mode 100644 index 7188a51..0000000 --- a/src/CultMath/PlanetaryRadialRefinement.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace CultMath; - -public static class PlanetaryRadialRefinement -{ - public static float3 Step( - float3 position, - float3 rayDirection, - float3 center, - float targetRadius, - float minimumDerivative = 0.2f, - float maximumCorrection = 0.08f) - { - var local = position - center; - var radius = math.length(local); - var radial = local / math.max(radius, 0.0001f); - var error = radius - targetRadius; - var derivative = math.dot(rayDirection, radial); - var safeMinimum = math.max(minimumDerivative, 0.0001f); - var safeDerivative = math.abs(derivative) > safeMinimum - ? derivative - : derivative < 0.0f ? -safeMinimum : safeMinimum; - var correction = math.clamp( - -error / safeDerivative, - -math.abs(maximumCorrection), - math.abs(maximumCorrection)); - return position + rayDirection * correction; - } -} diff --git a/src/CultMath/PlanetaryRadialRefinement.cs.meta b/src/CultMath/PlanetaryRadialRefinement.cs.meta deleted file mode 100644 index 32af265..0000000 --- a/src/CultMath/PlanetaryRadialRefinement.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 82e4db521edc253499220458b417fd1e \ No newline at end of file diff --git a/src/CultMath/PlanetaryResidency.cs b/src/CultMath/PlanetaryResidency.cs deleted file mode 100644 index 6918ac5..0000000 --- a/src/CultMath/PlanetaryResidency.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace CultMath; - -public readonly record struct PlanetaryLodParameters( - int MaximumLevel, - int PageInteriorSize, - int PageBorderSize, - float ViewportHeightPixels, - float MinimumFootprintMeters) -{ - public PlanetaryLodParameters Validate() - { - if (MaximumLevel is < 0 or > PlanetaryTileAddress.MaxLevel) throw new ArgumentOutOfRangeException(nameof(MaximumLevel)); - _ = new PlanetaryPageLayout(new(PlanetaryCubeFace.PositiveX, 0, 0, 0), PageInteriorSize, PageBorderSize).Validate(); - if (!float.IsFinite(ViewportHeightPixels) || ViewportHeightPixels <= 0) throw new ArgumentOutOfRangeException(nameof(ViewportHeightPixels)); - if (!float.IsFinite(MinimumFootprintMeters) || MinimumFootprintMeters < 0) throw new ArgumentOutOfRangeException(nameof(MinimumFootprintMeters)); - return this; - } -} - -public static class PlanetaryLodSelector -{ - public static int SelectLevel( - in PlanetaryFieldDefinition field, - float cameraDistanceFromCenter, - in PlanetaryLodParameters parameters) - { - field.Validate(); parameters.Validate(); - if (!float.IsFinite(cameraDistanceFromCenter) || cameraDistanceFromCenter < 0) throw new ArgumentOutOfRangeException(nameof(cameraDistanceFromCenter)); - var altitude = MathF.Max(cameraDistanceFromCenter - field.Radius, 0.001f); - var footprint = MathF.Max(altitude / parameters.ViewportHeightPixels * 2, parameters.MinimumFootprintMeters); - var erosion = field.Erosion; - for (var level = 0; level <= parameters.MaximumLevel; level++) - { - var page = new PlanetaryPageLayout(new(PlanetaryCubeFace.PositiveX, level, 0, 0), parameters.PageInteriorSize, parameters.PageBorderSize); - var spacing = PlanetaryPageSampling.NominalAngularTexelSize(page) * field.Radius; - var unresolved = ErosionFrequencyBands.Select( - erosion.Scale * erosion.CellScale, spacing, erosion.Octaves, - erosion.Lacunarity, erosion.Strength * erosion.Scale, erosion.Gain).UnresolvedHeightBound; - if (unresolved <= footprint) return level; - } - return parameters.MaximumLevel; - } - - public static PlanetaryTileAddress[] SelectAncestorChain( - in PlanetaryFieldDefinition field, - float3 cameraDirection, - float cameraDistanceFromCenter, - in PlanetaryLodParameters parameters) - { - PlanetaryTopology.ValidateDirection(cameraDirection); - var level = SelectLevel(field, cameraDistanceFromCenter, parameters); - var result = new PlanetaryTileAddress[6 + level]; - for (var face = 0; face < 6; face++) result[face] = new((PlanetaryCubeFace)face, 0, 0, 0); - var leaf = PlanetaryTopology.TileAt(cameraDirection, level); - for (var currentLevel = level; currentLevel >= 1; currentLevel--) - { - result[6 + currentLevel - 1] = leaf; - leaf = leaf.Parent(); - } - return result; - } -} - -public readonly record struct PlanetaryResidentTile(PlanetaryTileAddress Tile, float Blend, bool Departing); - -public readonly record struct PlanetaryResidencySnapshot( - ulong ContentVersion, - ulong PresentationVersion, - PlanetaryResidentTile[] Tiles); - -/// Owns presentation residency only. It never owns field content. -public sealed class PlanetaryResidualResidency -{ - private readonly Dictionary residents = new(); - private readonly Dictionary arrivals = new(); - private readonly Dictionary departures = new(); - private float lastTime = float.NegativeInfinity; - - public void Reset() - { - residents.Clear(); arrivals.Clear(); departures.Clear(); lastTime = float.NegativeInfinity; - } - - public PlanetaryResidencySnapshot Update(ReadOnlySpan desired, float timeSeconds, float transitionSeconds) - { - if (!float.IsFinite(timeSeconds)) throw new ArgumentOutOfRangeException(nameof(timeSeconds)); - if (!float.IsFinite(transitionSeconds) || transitionSeconds < 0) throw new ArgumentOutOfRangeException(nameof(transitionSeconds)); - if (timeSeconds < lastTime) Reset(); - lastTime = timeSeconds; - - var desiredKeys = new HashSet(); - foreach (var tile in desired) - { - var key = tile.StableKey; - desiredKeys.Add(key); - residents[key] = tile; - if (!arrivals.ContainsKey(key)) arrivals[key] = timeSeconds; - departures.Remove(key); - } - - foreach (var pair in residents.ToArray()) - { - if (pair.Value.Level == 0 || desiredKeys.Contains(pair.Key)) continue; - if (!departures.ContainsKey(pair.Key)) - { - var startingBlend = transitionSeconds == 0 - ? 1 - : Math.Clamp((timeSeconds - arrivals[pair.Key]) / transitionSeconds, 0, 1); - departures[pair.Key] = (timeSeconds, startingBlend); - } - if (transitionSeconds > 0 && timeSeconds - departures[pair.Key].Time < transitionSeconds) continue; - residents.Remove(pair.Key); arrivals.Remove(pair.Key); departures.Remove(pair.Key); - } - - var ordered = residents - .OrderBy(pair => pair.Value.Level) - .ThenBy(pair => pair.Value.Face) - .ThenBy(pair => pair.Value.X) - .ThenBy(pair => pair.Value.Y) - .ToArray(); - var tiles = new PlanetaryResidentTile[ordered.Length]; - for (var i = 0; i < ordered.Length; i++) - { - var pair = ordered[i]; - var departing = departures.TryGetValue(pair.Key, out var departure); - var blend = pair.Value.Level == 0 || transitionSeconds == 0 - ? 1 - : departing - ? departure.StartingBlend * Math.Clamp(1 - (timeSeconds - departure.Time) / transitionSeconds, 0, 1) - : Math.Clamp((timeSeconds - arrivals[pair.Key]) / transitionSeconds, 0, 1); - tiles[i] = new(pair.Value, blend, departing); - } - return new(Hash(ordered.Select(pair => pair.Key)), Hash(tiles.Select(tile => tile.Tile.StableKey ^ (uint)BitConverter.SingleToInt32Bits(tile.Blend))), tiles); - } - - private static ulong Hash(IEnumerable values) - { - const ulong offset = 14695981039346656037UL; - const ulong prime = 1099511628211UL; - var hash = offset; - foreach (var value in values) hash = unchecked((hash ^ value) * prime); - return hash == 0 ? 1 : hash; - } -} diff --git a/src/CultMath/PlanetaryResidency.cs.meta b/src/CultMath/PlanetaryResidency.cs.meta deleted file mode 100644 index 2434762..0000000 --- a/src/CultMath/PlanetaryResidency.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 0585ea15edd7f1c4fa272df4911a8d6f \ No newline at end of file diff --git a/src/CultMath/PlanetaryTopology.cs b/src/CultMath/PlanetaryTopology.cs deleted file mode 100644 index 57d562c..0000000 --- a/src/CultMath/PlanetaryTopology.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System; - -namespace CultMath; - -public enum PlanetaryCubeFace -{ - PositiveX, - NegativeX, - PositiveY, - NegativeY, - PositiveZ, - NegativeZ, -} - -public readonly record struct PlanetaryFaceCoordinate(PlanetaryCubeFace Face, double U, double V) -{ - public PlanetaryFaceCoordinate Validate(bool allowOutsideFace = false) - { - if (!double.IsFinite(U) || !double.IsFinite(V)) throw new ArgumentOutOfRangeException(nameof(U)); - if (!allowOutsideFace && (U is < -1.0 or > 1.0 || V is < -1.0 or > 1.0)) - throw new ArgumentOutOfRangeException(nameof(U), "Face coordinates must be in [-1, 1]."); - if (!Enum.IsDefined(typeof(PlanetaryCubeFace), Face)) throw new ArgumentOutOfRangeException(nameof(Face)); - return this; - } -} - -public readonly record struct PlanetaryTileAddress -{ - public const int MaxLevel = 30; - - public PlanetaryTileAddress(PlanetaryCubeFace face, int level, int x, int y) - { - if (!Enum.IsDefined(typeof(PlanetaryCubeFace), face)) throw new ArgumentOutOfRangeException(nameof(face)); - if (level is < 0 or > MaxLevel) throw new ArgumentOutOfRangeException(nameof(level)); - var count = 1 << level; - if ((uint)x >= (uint)count) throw new ArgumentOutOfRangeException(nameof(x)); - if ((uint)y >= (uint)count) throw new ArgumentOutOfRangeException(nameof(y)); - Face = face; - Level = level; - X = x; - Y = y; - } - - public PlanetaryCubeFace Face { get; } - public int Level { get; } - public int X { get; } - public int Y { get; } - public int AxisTileCount => 1 << Level; - - public PlanetaryTileAddress Parent() => Level == 0 - ? throw new InvalidOperationException("Root cube tile has no parent.") - : new(Face, Level - 1, X >> 1, Y >> 1); - - public PlanetaryTileAddress Child(int childX, int childY) - { - if ((uint)childX > 1) throw new ArgumentOutOfRangeException(nameof(childX)); - if ((uint)childY > 1) throw new ArgumentOutOfRangeException(nameof(childY)); - if (Level == MaxLevel) throw new InvalidOperationException($"Level {MaxLevel} cannot be subdivided."); - return new(Face, Level + 1, (X << 1) | childX, (Y << 1) | childY); - } - - public PlanetaryFaceCoordinate PositionAt(double localU, double localV) - { - if (!double.IsFinite(localU) || localU is < 0.0 or > 1.0) throw new ArgumentOutOfRangeException(nameof(localU)); - if (!double.IsFinite(localV) || localV is < 0.0 or > 1.0) throw new ArgumentOutOfRangeException(nameof(localV)); - var count = AxisTileCount; - return new(Face, -1.0 + 2.0 * (X + localU) / count, -1.0 + 2.0 * (Y + localV) / count); - } - - public ulong StableKey - { - get - { - const ulong offset = 14695981039346656037UL; - const ulong prime = 1099511628211UL; - var hash = offset; - hash = unchecked((hash ^ (uint)Face) * prime); - hash = unchecked((hash ^ (uint)Level) * prime); - hash = unchecked((hash ^ (uint)X) * prime); - hash = unchecked((hash ^ (uint)Y) * prime); - return hash == 0 ? 1 : hash; - } - } - public override string ToString() => $"{Face}/L{Level:D2}/{X}/{Y}"; -} - -public static class PlanetaryTopology -{ - private const float QuarterPi = MathF.PI * 0.25f; - - public static float3 Direction(PlanetaryFaceCoordinate coordinate) - { - coordinate.Validate(allowOutsideFace: true); - var u = MathF.Tan((float)coordinate.U * QuarterPi); - var v = MathF.Tan((float)coordinate.V * QuarterPi); - var cube = coordinate.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(coordinate)), - }; - 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); - var d = math.normalize(direction); - var a = math.abs(d); - if (a.x >= a.y && a.x >= a.z) - return d.x >= 0 - ? FromCubeUv(PlanetaryCubeFace.PositiveX, -d.z / a.x, d.y / a.x) - : FromCubeUv(PlanetaryCubeFace.NegativeX, d.z / a.x, d.y / a.x); - if (a.y >= a.z) - return d.y >= 0 - ? FromCubeUv(PlanetaryCubeFace.PositiveY, d.x / a.y, -d.z / a.y) - : FromCubeUv(PlanetaryCubeFace.NegativeY, d.x / a.y, d.z / a.y); - return d.z >= 0 - ? FromCubeUv(PlanetaryCubeFace.PositiveZ, d.x / a.z, d.y / a.z) - : FromCubeUv(PlanetaryCubeFace.NegativeZ, -d.x / a.z, d.y / a.z); - } - - public static PlanetaryTileAddress TileAt(float3 direction, int level) - { - if (level is < 0 or > PlanetaryTileAddress.MaxLevel) throw new ArgumentOutOfRangeException(nameof(level)); - var face = FaceCoordinate(direction); - var count = 1 << level; - var x = Math.Clamp((int)((face.U * 0.5 + 0.5) * count), 0, count - 1); - var y = Math.Clamp((int)((face.V * 0.5 + 0.5) * count), 0, count - 1); - return new(face.Face, level, x, y); - } - - public static bool TryLocalCoordinate(float3 direction, PlanetaryTileAddress tile, out float2 local) - { - var face = FaceCoordinate(direction); - if (face.Face != tile.Face) { local = float2.zero; return false; } - var scaledU = (face.U * 0.5 + 0.5) * tile.AxisTileCount; - var scaledV = (face.V * 0.5 + 0.5) * tile.AxisTileCount; - local = new((float)(scaledU - tile.X), (float)(scaledV - tile.Y)); - return local.x is >= 0 and <= 1 && local.y is >= 0 and <= 1; - } - - public static float3 SurfaceNormal(float3 unitDirection, float3 worldDistanceTangentGradient) - { - ValidateDirection(unitDirection); - ValidateFinite(worldDistanceTangentGradient, nameof(worldDistanceTangentGradient)); - var direction = math.normalize(unitDirection); - var tangent = worldDistanceTangentGradient - direction * math.dot(worldDistanceTangentGradient, direction); - return math.normalize(direction - tangent); - } - - private static PlanetaryFaceCoordinate FromCubeUv(PlanetaryCubeFace face, float cubeU, float cubeV) => new( - face, - 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)); - if (math.length(direction) < 1.0e-12f) throw new ArgumentOutOfRangeException(nameof(direction)); - } - - internal static void ValidateFinite(float3 value, string name) - { - if (!float.IsFinite(value.x) || !float.IsFinite(value.y) || !float.IsFinite(value.z)) - throw new ArgumentOutOfRangeException(name); - } -} diff --git a/src/CultMath/PlanetaryTopology.cs.meta b/src/CultMath/PlanetaryTopology.cs.meta deleted file mode 100644 index 411a8bd..0000000 --- a/src/CultMath/PlanetaryTopology.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 88f6789c4384f7846ac4be37613fe5ed \ No newline at end of file diff --git a/src/CultMath/SphericalErosion.cs b/src/CultMath/SphericalErosion.cs deleted file mode 100644 index 93ddb0e..0000000 --- a/src/CultMath/SphericalErosion.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace CultMath; - -public readonly record struct SphericalErosionParameters( - float Frequency, - float Amplitude, - float Lacunarity, - float Gain, - float SlopeStrength, - float Detail, - float Seed, - int Octaves) -{ - public static SphericalErosionParameters Default => new(18.0f, 0.035f, 2.03f, 0.52f, 6.0f, 1.35f, 0.0f, 5); -} - -public readonly record struct SphericalErosionSample(float HeightOffset, float Ridge, float Gully, float3 Gradient); - -/// -/// Stateless, point-evaluable erosion-like synthesis on a unit sphere. -/// The matching shader body is cultmath_spherical_erosion in CultMath.hlsl. -/// Position and gradient are planet-space; the gradient is projected tangent -/// internally so cube faces and tile request order never enter the result. -/// -public static class SphericalErosion -{ - public static SphericalErosionSample Sample( - float3 unitPosition, - float baseHeight, - float3 baseGradient, - SphericalErosionParameters parameters) - { - var position = math.normalize(unitPosition); - var gradient = baseGradient - position * math.dot(baseGradient, position); - var slope = math.length(gradient); - var flow = slope > 1.0e-6f ? -gradient / slope : StableTangent(position); - var frequency = math.max(parameters.Frequency, 0.001f); - var amplitude = parameters.Amplitude; - var combiMask = EaseOut(math.saturate(slope * parameters.SlopeStrength)); - var fadeTarget = math.saturate(baseHeight * 0.5f + 0.5f) * 2.0f - 1.0f; - var heightOffset = 0.0f; - var ridge = 0.0f; - var gully = 0.0f; - - var octaveCount = math.min(math.max(parameters.Octaves, 0), 8); - for (var octave = 0; octave < octaveCount; octave++) - { - var phase = DirectionalCellWave(position * frequency, flow, parameters.Seed + octave * 19.19f); - var wave = math.cos(phase); - var waveSlope = math.sin(phase); - var faded = math.lerp(fadeTarget, wave, combiMask); - heightOffset += faded * amplitude; - ridge = math.max(ridge, math.saturate(faded)); - gully = math.max(gully, math.saturate(-faded)); - - var across = math.normalize(math.cross(position, flow)); - gradient += across * (math.sign(waveSlope) * amplitude * frequency * combiMask); - gradient -= position * math.dot(gradient, position); - slope = math.length(gradient); - flow = slope > 1.0e-6f ? -gradient / slope : StableTangent(position); - - var newMask = EaseOut(math.saturate(math.abs(waveSlope) * 1.35f)); - combiMask = PowInverse(combiMask, parameters.Detail) * newMask; - fadeTarget = faded; - frequency *= math.max(parameters.Lacunarity, 1.01f); - amplitude *= math.saturate(parameters.Gain); - } - - return new SphericalErosionSample(heightOffset, ridge, gully, gradient); - } - - private static float DirectionalCellWave(float3 position, float3 flow, float seed) - { - var cell = math.floor(position); - var local = math.frac(position); - var weights = local * local * (3.0f - 2.0f * local); - var x00 = CellWave(position, flow, cell + new float3(0, 0, 0), seed); - var x10 = CellWave(position, flow, cell + new float3(1, 0, 0), seed); - var x01 = CellWave(position, flow, cell + new float3(0, 1, 0), seed); - var x11 = CellWave(position, flow, cell + new float3(1, 1, 0), seed); - var y00 = math.lerp(x00, x10, weights.x); - var y10 = math.lerp(x01, x11, weights.x); - var z0 = math.lerp(y00, y10, weights.y); - var x00z = CellWave(position, flow, cell + new float3(0, 0, 1), seed); - var x10z = CellWave(position, flow, cell + new float3(1, 0, 1), seed); - var x01z = CellWave(position, flow, cell + new float3(0, 1, 1), seed); - var x11z = CellWave(position, flow, cell + new float3(1, 1, 1), seed); - var z1 = math.lerp(math.lerp(x00z, x10z, weights.x), math.lerp(x01z, x11z, weights.x), weights.y); - return math.lerp(z0, z1, weights.z); - } - - private static float CellWave(float3 position, float3 flow, float3 cell, float seed) - { - var pivot = cell + new float3( - math.hash(cell + seed + 11.7f), - math.hash(cell.yzx + seed + 37.1f), - math.hash(cell.zyx + seed + 73.9f)); - return math.dot(position - pivot, flow) * math.TAU + math.hash(cell + seed + 101.3f) * math.TAU; - } - - private static float3 StableTangent(float3 position) - { - var axis = math.abs(position.z) < 0.8f ? new float3(0, 0, 1) : new float3(0, 1, 0); - return math.normalize(math.cross(axis, position)); - } - - private static float EaseOut(float value) => 1.0f - (1.0f - value) * (1.0f - value); - private static float PowInverse(float value, float power) => 1.0f - math.pow(1.0f - math.saturate(value), math.max(power, 0.001f)); -} diff --git a/src/CultMath/SphericalErosion.cs.meta b/src/CultMath/SphericalErosion.cs.meta deleted file mode 100644 index 82db773..0000000 --- a/src/CultMath/SphericalErosion.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 59ec2db73065f6c4483ad90f5e17850e \ No newline at end of file diff --git a/src/CultMath/math.cs b/src/CultMath/math.cs index e15fbf0..d0a56e7 100644 --- a/src/CultMath/math.cs +++ b/src/CultMath/math.cs @@ -22,9 +22,6 @@ public static class math public static float4 float4(float3 xyz, float w) => new(xyz, w); public static float4 float4(float x, float3 yzw) => new(x, yzw); public static float4 float4(float value) => new(value, value, value, value); - public static rect rect(float2 min, float2 max) => new(min, max); - public static rect rect(float minX, float minY, float maxX, float maxY) => new(minX, minY, maxX, maxY); - public static float radians(float degrees) => degrees * (PI / 180.0f); public static float degrees(float radians) => radians * (180.0f / PI); public static float sin(float value) => MathF.Sin(value); diff --git a/src/CultMath/rect.cs b/src/CultMath/rect.cs deleted file mode 100644 index 67b210b..0000000 --- a/src/CultMath/rect.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace CultMath; - -public readonly record struct rect(float2 min, float2 max) -{ - public rect(float minX, float minY, float maxX, float maxY) - : this(math.float2(math.min(minX, maxX), math.min(minY, maxY)), - math.float2(math.max(minX, maxX), math.max(minY, maxY))) - { - } - - public float2 size => max - min; - public float2 center => (min + max) * 0.5f; - - public bool Contains(float2 point) - { - return point.x >= min.x && - point.x <= max.x && - point.y >= min.y && - point.y <= max.y; - } -} diff --git a/src/CultMath/rect.cs.meta b/src/CultMath/rect.cs.meta deleted file mode 100644 index 9f7598e..0000000 --- a/src/CultMath/rect.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b00dc5f108967db47ac2f54a47033e33 \ No newline at end of file diff --git a/tests/CultMath.Tests/AdvancedErosionFilterTests.cs b/tests/CultMath.Tests/AdvancedErosionFilterTests.cs deleted file mode 100644 index cccebd6..0000000 --- a/tests/CultMath.Tests/AdvancedErosionFilterTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class AdvancedErosionFilterTests -{ - [Fact] - public void DefaultReferenceFixtureIsStable() - { - var result = AdvancedErosionFilter.Sample( - new float2(0.42f, 0.31f), - new float3(0.5f, 0.1f, -0.2f), - 0.0f, - AdvancedErosionParameters.Default); - - Assert.Equal(0.0639375f, result.Magnitude, precision: 7); - Assert.True(float.IsFinite(result.Delta.x)); - Assert.True(float.IsFinite(result.Delta.y)); - Assert.True(float.IsFinite(result.Delta.z)); - Assert.InRange(result.RidgeMap, -1.0f, 1.0f); - } - - [Fact] - public void FlatInputAndZeroRoundingRemainFinite() - { - var parameters = AdvancedErosionParameters.Default with { Rounding = float4.zero }; - var result = AdvancedErosionFilter.Sample(new float2(0.25f, 0.75f), float3.zero, 0.0f, parameters); - - Assert.True(float.IsFinite(result.Delta.x)); - Assert.True(float.IsFinite(result.RidgeMap)); - Assert.True(float.IsFinite(result.FadeTarget)); - } - - [Fact] - public void PhacellePartialNormalizationIsFiniteAtEndpoints() - { - foreach (var normalization in new[] { 0.0f, 0.5f, 1.0f }) - { - var sample = AdvancedErosionFilter.Phacelle(new float2(1.7f, -3.2f), new float2(0.6f, -0.8f), 0.7f, 0.25f, normalization); - Assert.True(float.IsFinite(sample.x)); - Assert.True(float.IsFinite(sample.y)); - } - } -} diff --git a/tests/CultMath.Tests/AdvancedErosionFilterTests.cs.meta b/tests/CultMath.Tests/AdvancedErosionFilterTests.cs.meta deleted file mode 100644 index 253ee42..0000000 --- a/tests/CultMath.Tests/AdvancedErosionFilterTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a7c85309318ae3f4ba75ac28a4f661cf \ No newline at end of file diff --git a/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs b/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs deleted file mode 100644 index f57213a..0000000 --- a/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class ErosionFrequencyBandsTests -{ - [Fact] - public void RadiusIndependentWorldWavelengthSelectsSameBands() - { - var small = ErosionFrequencyBands.Select(1_000.0f, 100.0f, 8, 2.0f, 20.0f, 0.5f); - var large = ErosionFrequencyBands.Select(1_000.0f, 100.0f, 8, 2.0f, 20.0f, 0.5f); - Assert.Equal(small, large); - Assert.Equal(3, small.ActiveOctaves); - } - - [Fact] - public void TransitionPartiallyWeightsLastResolvableOctave() - { - var selection = ErosionFrequencyBands.Select(1_000.0f, 50.0f, 8, 2.0f, 1.0f, 0.5f); - Assert.Equal(4, selection.ActiveOctaves); - Assert.InRange(selection.FinalOctaveWeight, 0.0f, 1.0f); - Assert.True(selection.UnresolvedHeightBound > 0.0f); - } - - [Fact] - public void BandedFilterPreservesLowOctaveResultExactly() - { - var p = AdvancedErosionParameters.Default; - var band = new ErosionBandSelection(2, 1.0f, 0.0f, 0.0f); - var banded = AdvancedErosionFilter.Sample(new float2(0.42f, 0.31f), new float3(0.5f, 0.1f, -0.2f), 0.0f, p, band); - var truncated = AdvancedErosionFilter.Sample(new float2(0.42f, 0.31f), new float3(0.5f, 0.1f, -0.2f), 0.0f, p with { Octaves = 2 }); - Assert.Equal(truncated, banded); - } -} diff --git a/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs.meta b/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs.meta deleted file mode 100644 index ef15b7e..0000000 --- a/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -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 47f5897..b9396bd 100644 --- a/tests/CultMath.Tests/HlslMirrorTests.cs +++ b/tests/CultMath.Tests/HlslMirrorTests.cs @@ -5,22 +5,13 @@ namespace CultMath.Tests; public sealed class HlslMirrorTests { [Fact] - public void HlslMirrorPublishesSphericalErosionKernel() + public void HlslMirrorDoesNotPublishGeometryKernels() { var source = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "shaders", "CultMath.hlsl")); - Assert.Contains("cultmath_spherical_erosion", source); - Assert.Contains("CultMathSphericalErosionParameters", source); - } - - [Fact] - public void HlslMirrorPublishesFaithfulAdvancedErosionKernel() - { - var mirror = Path.Combine(GetRepositoryRoot(), "shaders", "CultMath.hlsl"); - var source = File.ReadAllText(mirror); - Assert.Contains("AdvancedErosionFilter.hlsl", source); - var advanced = File.ReadAllText(Path.Combine(Path.GetDirectoryName(mirror)!, "AdvancedErosionFilter.hlsl")); - Assert.Contains("cultmath_phacelle_noise", advanced); - Assert.Contains("cultmath_advanced_erosion_filter", advanced); + Assert.DoesNotContain("AdvancedErosionFilter.hlsl", source); + Assert.DoesNotContain("Planetary.hlsl", source); + Assert.DoesNotContain("spherical_erosion", source); + Assert.DoesNotContain("planetary_radial_refinement", source); } [Fact] @@ -66,24 +57,6 @@ public sealed class HlslMirrorTests Assert.Contains("max(length(value), CULTMATH_NORMALIZE_EPSILON)", include); } - [Fact] - public void HlslMirrorPublishesPlanetaryTopologyAndField() - { - var root = GetRepositoryRoot(); - var include = File.ReadAllText(Path.Combine(root, "shaders", "CultMath.hlsl")); - var planetary = File.ReadAllText(Path.Combine(root, "shaders", "Planetary.hlsl")); - Assert.Contains("Planetary.hlsl", include); - Assert.Contains("cultmath_planetary_face_direction", planetary); - Assert.Contains("cultmath_planetary_face_coordinate", planetary); - Assert.Contains("cultmath_planetary_page_local", planetary); - 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); - } - private static string GetRepositoryRoot() { var directory = new DirectoryInfo(AppContext.BaseDirectory); diff --git a/tests/CultMath.Tests/MathTests.cs b/tests/CultMath.Tests/MathTests.cs index 6979bdd..ad26821 100644 --- a/tests/CultMath.Tests/MathTests.cs +++ b/tests/CultMath.Tests/MathTests.cs @@ -32,19 +32,6 @@ public sealed class MathTests Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), new float4(new float3(1.0f, 2.0f, 3.0f), 4.0f)); } - [Fact] - public void RectNormalizesBoundsAndContainsPoints() - { - var viewport = math.rect(10.0f, 20.0f, -5.0f, -2.0f); - - Assert.Equal(new float2(-5.0f, -2.0f), viewport.min); - Assert.Equal(new float2(10.0f, 20.0f), viewport.max); - Assert.Equal(new float2(15.0f, 22.0f), viewport.size); - Assert.Equal(new float2(2.5f, 9.0f), viewport.center); - Assert.True(viewport.Contains(new float2(0.0f, 0.0f))); - Assert.False(viewport.Contains(new float2(11.0f, 0.0f))); - } - [Fact] public void VectorsExposeCommonReadOnlySwizzles() { diff --git a/tests/CultMath.Tests/PlanetaryFieldTests.cs b/tests/CultMath.Tests/PlanetaryFieldTests.cs deleted file mode 100644 index 4dc7532..0000000 --- a/tests/CultMath.Tests/PlanetaryFieldTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class PlanetaryFieldTests -{ - private static readonly PlanetaryFieldDefinition Definition = new( - 0x7f59_6d11_02ab_9c31UL, - 8.4f, - 42, - AdvancedErosionParameters.Default with { Scale = 0.63f, Strength = 0.12f, Octaves = 7 }); - - [Fact] - public void SampleIsDeterministicScaleAwareAndTangent() - { - var direction = math.normalize(new float3(0.31f, -0.72f, 0.61f)); - var source = new FixtureField(); - var coarse = PlanetaryField.Sample(Definition, direction, source.Sample(direction), PlanetaryQueryScale.AtFootprint(0.1f)); - var fine = PlanetaryField.Sample(Definition, direction, source.Sample(direction), PlanetaryQueryScale.AtFootprint(0.01f)); - var repeated = PlanetaryField.Sample(Definition, direction, source.Sample(direction), PlanetaryQueryScale.AtFootprint(0.01f)); - - Assert.Equal(fine, repeated); - Assert.Equal(Definition.FieldVersion, fine.FieldVersion); - Assert.True(fine.UnresolvedHeightBound <= coarse.UnresolvedHeightBound); - Assert.True(fine.FinestResolvedWavelength <= coarse.FinestResolvedWavelength); - Assert.InRange(MathF.Abs(math.dot(fine.UnitDirection, fine.TangentGradient)), 0, 2.0e-5f); - Assert.Equal(1, math.length(fine.SurfaceNormal), 5); - } - - [Fact] - public void BatchAndDoublePositionUseThePointContract() - { - var directions = new[] - { - math.normalize(new float3(1, 2, 3)), - math.normalize(new float3(-2, 0.5f, 1)), - math.normalize(new float3(0.1f, -0.2f, -1)), - }; - var output = new PlanetarySurfaceSample[directions.Length]; - var source = new FixtureField(); - var scale = PlanetaryQueryScale.AtFootprint(0.05f); - PlanetaryField.SampleBatch(Definition, directions, scale, source, output); - - for (var i = 0; i < directions.Length; i++) - { - var normalized = math.normalize(directions[i]); - var expected = PlanetaryField.Sample(Definition, normalized, source.Sample(normalized), scale); - Assert.InRange(MathF.Abs(expected.RadialDisplacement - output[i].RadialDisplacement), 0, 1.0e-6f); - Assert.InRange(math.distance(expected.TangentGradient, output[i].TangentGradient), 0, 1.0e-6f); - } - - var d = directions[0]; - var position = new double3(d.x * 8.0e11, d.y * 8.0e11, d.z * 8.0e11); - var positioned = PlanetaryField.SamplePosition(Definition, position, scale, source); - Assert.InRange(math.distance(positioned.UnitDirection, d), 0, 2.0e-6f); - } - - [Fact] - public void FieldIdentityChangesWithEveryLoadBearingInput() - { - var erosion = AdvancedErosionParameters.Default; - var baseline = PlanetaryFieldDefinition.Create(3, 8.4f, 7, erosion); - Assert.Equal(baseline.FieldVersion, PlanetaryFieldDefinition.Create(3, 8.4f, 7, erosion).FieldVersion); - Assert.NotEqual(baseline.FieldVersion, PlanetaryFieldDefinition.Create(4, 8.4f, 7, erosion).FieldVersion); - Assert.NotEqual(baseline.FieldVersion, PlanetaryFieldDefinition.Create(3, 8.5f, 7, erosion).FieldVersion); - Assert.NotEqual(baseline.FieldVersion, PlanetaryFieldDefinition.Create(3, 8.4f, 8, erosion).FieldVersion); - Assert.NotEqual(baseline.FieldVersion, PlanetaryFieldDefinition.Create(3, 8.4f, 7, erosion with { Gain = 0.49f }).FieldVersion); - } - - private readonly struct FixtureField : IPlanetaryBaseField - { - public PlanetaryBaseFieldSample Sample(float3 direction) - { - var value = direction.x * 0.6f + direction.y * direction.z * 0.3f; - var fieldGradient = new float3(0.6f, direction.z * 0.3f, direction.y * 0.3f); - fieldGradient -= direction * math.dot(fieldGradient, direction); - return new(value * 0.1f, fieldGradient * 0.1f, value, fieldGradient, math.clamp(value, -1, 1)); - } - } -} diff --git a/tests/CultMath.Tests/PlanetaryFieldTests.cs.meta b/tests/CultMath.Tests/PlanetaryFieldTests.cs.meta deleted file mode 100644 index 1710b5f..0000000 --- a/tests/CultMath.Tests/PlanetaryFieldTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c747cfda80df21d41a57412d8d07c08a \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryGpuPageTests.cs b/tests/CultMath.Tests/PlanetaryGpuPageTests.cs deleted file mode 100644 index ea17b4b..0000000 --- a/tests/CultMath.Tests/PlanetaryGpuPageTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 00f4a3a..0000000 --- a/tests/CultMath.Tests/PlanetaryGpuPageTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 87266638e8010f546ab6c430a712868b \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs b/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs deleted file mode 100644 index 112cb31..0000000 --- a/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 027754e..0000000 --- a/tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e916ef83478d48b43ac99777f5e0f731 \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryPageTests.cs b/tests/CultMath.Tests/PlanetaryPageTests.cs deleted file mode 100644 index c4e4045..0000000 --- a/tests/CultMath.Tests/PlanetaryPageTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class PlanetaryPageTests -{ - [Fact] - public void BilinearSamplingAndSummaryUseOnePageContract() - { - var layout = new PlanetaryPageLayout(new(PlanetaryCubeFace.PositiveZ, 0, 0, 0), 3, 1); - var samples = new PlanetaryPageSample[layout.StorageSize * layout.StorageSize]; - for (var y = 0; y < layout.StorageSize; y++) - for (var x = 0; x < layout.StorageSize; x++) - samples[y * layout.StorageSize + x] = new(x + y * 10, new float3(x, y, 0), x / 10.0f, y / 10.0f); - - var sample = PlanetaryPageSampling.Bilinear(samples, layout, new float2(0.25f, 0.75f)); - Assert.Equal(26.5f, sample.Height, 4); - Assert.Equal(new float3(1.5f, 2.5f, 0), sample.TangentGradient); - var summary = PlanetaryPageSampling.Summarize(samples, layout, 0.125f); - Assert.Equal(0, summary.MinimumHeight); - Assert.Equal(44, summary.MaximumHeight); - Assert.Equal(0.125f, summary.UnresolvedHeightBound); - Assert.True(summary.MaximumSlope > 0); - } - - [Fact] - public void ChildPageStoresOnlyResidualAndComposesBackToDirectField() - { - var field = new PlanetaryFieldDefinition(7, 8.4f, 3, AdvancedErosionParameters.Default with - { - Scale = 0.63f, - Strength = 0.12f, - Octaves = 7, - }); - var source = new FixtureField(); - var root = PlanetaryPageBaker.Bake(field, new(new(PlanetaryCubeFace.PositiveZ, 0, 0, 0), 17, 2), source); - var child = PlanetaryPageBaker.Bake(field, new(new(PlanetaryCubeFace.PositiveZ, 1, 1, 0), 17, 2), source); - var direction = PlanetaryPageSampling.DirectionAtLocal(child.Layout, 0.5, 0.5); - var pages = new[] { new PlanetaryResidentPage(root, 1), new PlanetaryResidentPage(child, 1) }; - Assert.True(PlanetaryPageSetSampling.TrySample(pages, direction, out var composed)); - var spacing = PlanetaryPageSampling.NominalAngularTexelSize(child.Layout) * field.Radius; - var direct = PlanetaryField.Sample(field, direction, source.Sample(direction), PlanetaryQueryScale.AtFootprint(spacing)); - Assert.InRange(MathF.Abs(composed.Height - direct.RadialDisplacement), 0, 2.0e-5f); - Assert.InRange(math.distance(composed.TangentGradient, direct.TangentGradient), 0, 3.0e-5f); - } - - private readonly struct FixtureField : IPlanetaryBaseField - { - public PlanetaryBaseFieldSample Sample(float3 direction) - { - var value = direction.x * 0.4f - direction.y * 0.2f + direction.z * 0.5f; - var gradient = new float3(0.4f, -0.2f, 0.5f); - gradient -= direction * math.dot(gradient, direction); - return new(value * 0.08f, gradient * 0.08f, value, gradient, value); - } - } -} diff --git a/tests/CultMath.Tests/PlanetaryPageTests.cs.meta b/tests/CultMath.Tests/PlanetaryPageTests.cs.meta deleted file mode 100644 index 70a4386..0000000 --- a/tests/CultMath.Tests/PlanetaryPageTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 7c3ae5598d3b9164a9bd3a736a22ba9a \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryPatchTests.cs b/tests/CultMath.Tests/PlanetaryPatchTests.cs deleted file mode 100644 index 6564c77..0000000 --- a/tests/CultMath.Tests/PlanetaryPatchTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class PlanetaryPatchTests -{ - [Fact] - public void CubeSpherePatchMeshUsesCanonicalTopology() - { - var meshes = PlanetaryPatch.CreateCubeSphere(8); - Assert.Equal(6, meshes.Length); - foreach (var mesh in meshes) - { - Assert.Equal(81, mesh.Vertices.Length); - Assert.Equal(8 * 8 * 6, mesh.Indices.Length); - Assert.All(mesh.Vertices, vertex => Assert.Equal(1, math.length(vertex.UnitDirection), 5)); - foreach (var index in mesh.Indices) Assert.InRange(index, 0, mesh.Vertices.Length - 1); - } - } - - [Fact] - public void NeighborFaceCornersAreSingleValued() - { - var directions = PlanetaryPatch.CreateCubeSphere(1) - .SelectMany(mesh => mesh.Vertices.Select(vertex => vertex.UnitDirection)) - .ToArray(); - var groups = directions.GroupBy(direction => new - { - X = Math.Sign(direction.x), - Y = Math.Sign(direction.y), - Z = Math.Sign(direction.z), - }); - Assert.Equal(8, groups.Count()); - Assert.All(groups, group => - { - var first = group.First(); - Assert.All(group, direction => Assert.InRange(math.distance(first, direction), 0, 1.0e-6f)); - }); - } -} diff --git a/tests/CultMath.Tests/PlanetaryPatchTests.cs.meta b/tests/CultMath.Tests/PlanetaryPatchTests.cs.meta deleted file mode 100644 index 8599047..0000000 --- a/tests/CultMath.Tests/PlanetaryPatchTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 36840ef44326f84409f850163b00e1f7 \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryProjectionTests.cs b/tests/CultMath.Tests/PlanetaryProjectionTests.cs deleted file mode 100644 index ad811d6..0000000 --- a/tests/CultMath.Tests/PlanetaryProjectionTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class PlanetaryProjectionTests -{ - [Theory] - [InlineData(PlanetaryProjectionKind.Equirectangular)] - [InlineData(PlanetaryProjectionKind.WebMercator)] - [InlineData(PlanetaryProjectionKind.EqualEarth)] - [InlineData(PlanetaryProjectionKind.Orthographic)] - [InlineData(PlanetaryProjectionKind.AzimuthalEquidistant)] - [InlineData(PlanetaryProjectionKind.AzimuthalEqualArea)] - [InlineData(PlanetaryProjectionKind.LocalTangent)] - public void ProjectionRoundTripsVisibleDirections(PlanetaryProjectionKind kind) - { - var parameters = new PlanetaryProjectionParameters(kind, 0.31, -0.22); - foreach (var longitude in new[] { -0.4, -0.1, 0.2, 0.65 }) - foreach (var latitude in new[] { -0.6, -0.15, 0.3, 0.7 }) - { - var direction = Direction(longitude + parameters.CenterLongitude, latitude); - if (!PlanetaryProjection.TryForward(direction, parameters, out var map)) continue; - Assert.True(PlanetaryProjection.TryInverse(map, parameters, out var roundTrip)); - Assert.InRange(math.distance(direction, roundTrip), 0, 3.0e-5f); - } - } - - [Fact] - public void CubeAtlasRoundTripsEveryFaceInterior() - { - var projection = new PlanetaryProjectionParameters(PlanetaryProjectionKind.CubeAtlas); - foreach (var face in Enum.GetValues()) - { - var direction = PlanetaryTopology.Direction(new(face, 0.23, -0.41)); - Assert.True(PlanetaryProjection.TryForward(direction, projection, out var map)); - Assert.True(PlanetaryProjection.TryInverse(map, projection, out var roundTrip)); - Assert.InRange(math.distance(direction, roundTrip), 0, 2.0e-6f); - } - } - - [Fact] - public void MercatorRejectsPolarSingularity() - { - var northPole = new float3(0, 0, 1); - Assert.False(PlanetaryProjection.TryForward(northPole, new(PlanetaryProjectionKind.WebMercator), out _)); - } - - [Fact] - public void MapTileBakerCarriesFieldIdentityAndProjectionValidity() - { - var field = new PlanetaryFieldDefinition(99, 10, 0, AdvancedErosionParameters.Default); - var layout = new PlanetaryMapTileLayout( - new(PlanetaryProjectionKind.Orthographic), 0, 0, 0, 9, 1); - var key = new PlanetaryMapTileKey(field.FieldVersion, 1, 7, layout, PlanetaryQueryScale.AtFootprint(0.1f)); - var tile = PlanetaryMapTileBaker.Bake(field, key, new FlatField()); - Assert.Equal(key, tile.Key); - Assert.Equal(layout.StorageSize * layout.StorageSize, tile.Samples.Length); - Assert.Contains(tile.Validity, valid => valid); - Assert.Contains(tile.Validity, valid => !valid); - Assert.All(tile.Samples.Where((_, index) => tile.Validity[index]), sample => Assert.Equal(field.FieldVersion, sample.FieldVersion)); - } - - private static float3 Direction(double longitude, double latitude) - { - var cos = Math.Cos(latitude); - return new((float)(cos * Math.Cos(longitude)), (float)(cos * Math.Sin(longitude)), (float)Math.Sin(latitude)); - } - - private readonly struct FlatField : IPlanetaryBaseField - { - public PlanetaryBaseFieldSample Sample(float3 unitDirection) => new(0, float3.zero, 0, float3.zero, 0); - } -} diff --git a/tests/CultMath.Tests/PlanetaryProjectionTests.cs.meta b/tests/CultMath.Tests/PlanetaryProjectionTests.cs.meta deleted file mode 100644 index 7f37f78..0000000 --- a/tests/CultMath.Tests/PlanetaryProjectionTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f370036bb1663524fb4a4d47dfd50401 \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryQueryTests.cs b/tests/CultMath.Tests/PlanetaryQueryTests.cs deleted file mode 100644 index 48d865f..0000000 --- a/tests/CultMath.Tests/PlanetaryQueryTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class PlanetaryQueryTests -{ - private static readonly PlanetaryFieldDefinition Field = new(12, 10, 0, AdvancedErosionParameters.Default with { Strength = 0, Octaves = 0 }); - private static readonly PlanetaryQueryScale Scale = PlanetaryQueryScale.AtFootprint(0.1f, 0); - - [Fact] - public void RayIntersectionAndClearanceUseTheCpuSurfaceContract() - { - var source = new BulgedSphere(); - Assert.True(PlanetaryQueries.TryIntersectRay( - Field, new double3(0, 0, 20), new double3(0, 0, -1), 30, - Scale, new PlanetaryRadialBounds(-1, 1), source, out var hit)); - Assert.InRange(hit.Distance, 8.99999, 9.00001); - Assert.InRange(hit.PlanetLocalPosition.z, 10.99999, 11.00001); - var clearance = PlanetaryQueries.SegmentClearance( - Field, new double3(-2, 0, 12), new double3(2, 0, 12), 0.1, Scale, source); - Assert.True(clearance > 0.8); - } - - [Fact] - public void GreatCircleAndRegionQueriesReturnVersionedSamples() - { - var source = new BulgedSphere(); - var path = PlanetaryQueries.SampleGreatCircle(Field, new float3(1, 0, 0), new float3(0, 1, 0), 8, Scale, source); - Assert.Equal(9, path.Length); - Assert.Equal(MathF.PI * 5, path[^1].Distance, 4); - Assert.All(path, point => Assert.Equal(Field.FieldVersion, point.Surface.FieldVersion)); - var summary = PlanetaryQueries.Summarize(Field, path.Select(point => point.Surface.UnitDirection).ToArray(), Scale, source); - Assert.True(summary.MaximumDisplacement >= summary.MinimumDisplacement); - Assert.True(summary.MaximumSlope >= 0); - } - - private readonly struct BulgedSphere : IPlanetaryBaseField - { - public PlanetaryBaseFieldSample Sample(float3 direction) - { - var displacement = direction.z; - var gradient = new float3(0, 0, 1) - direction * direction.z; - return new(displacement, gradient, 0, float3.zero, 0); - } - } -} diff --git a/tests/CultMath.Tests/PlanetaryQueryTests.cs.meta b/tests/CultMath.Tests/PlanetaryQueryTests.cs.meta deleted file mode 100644 index d72b2cb..0000000 --- a/tests/CultMath.Tests/PlanetaryQueryTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 93cf8771fdd520b41a4e931a6566a80a \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs b/tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs deleted file mode 100644 index d0263b6..0000000 --- a/tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class PlanetaryRadialRefinementTests -{ - [Fact] - public void FourBoundedStepsConvergeFromTheRasterCaptureShell() - { - var center=new float3(0.3f,-0.2f,0.7f); - var radial=math.normalize(new float3(0.2f,-0.4f,0.9f)); - var targetRadius=3.85f; - var target=center+radial*targetRadius; - var origin=target+math.normalize(radial+new float3(0.12f,-0.05f,0.02f))*6.0f; - var position=center+radial*(targetRadius+0.30f); - var ray=math.normalize(position-origin); - for(var step=0;step<4;step++) - position=PlanetaryRadialRefinement.Step(position,ray,center,targetRadius); - Assert.InRange(math.abs(math.length(position-center)-targetRadius),0.0f,1.0e-4f); - } - - [Fact] - public void StepClampsWeakDerivativeCorrections() - { - var position=new float3(0,0,4.1f); - var result=PlanetaryRadialRefinement.Step(position,new float3(1,0,0),float3.zero,3.8f); - Assert.InRange(math.length(result-position),0.07999f,0.08001f); - } -} diff --git a/tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs.meta b/tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs.meta deleted file mode 100644 index b9f3bea..0000000 --- a/tests/CultMath.Tests/PlanetaryRadialRefinementTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d34c0018ee2b700448b16540eca8ebcf \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryResidencyTests.cs b/tests/CultMath.Tests/PlanetaryResidencyTests.cs deleted file mode 100644 index d2b89b7..0000000 --- a/tests/CultMath.Tests/PlanetaryResidencyTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class PlanetaryResidencyTests -{ - private static readonly PlanetaryFieldDefinition Field = new( - 1, 8.4f, 0, AdvancedErosionParameters.Default with { Scale = 0.63f, Strength = 0.12f, Octaves = 7 }); - private static readonly PlanetaryLodParameters Lod = new(5, 17, 2, 720, 8.4f / 65536); - - [Fact] - public void SelectorReturnsSixRootsAndOneAncestorChain() - { - var direction = math.normalize(new float3(-0.2f, 0.91f, 0.37f)); - var selected = PlanetaryLodSelector.SelectAncestorChain(Field, direction, 8.45f, Lod); - Assert.Equal(6, selected.Count(tile => tile.Level == 0)); - var descendants = selected.Where(tile => tile.Level > 0).OrderBy(tile => tile.Level).ToArray(); - for (var i = 1; i < descendants.Length; i++) Assert.Equal(descendants[i - 1], descendants[i].Parent()); - if (descendants.Length > 0) Assert.Equal(PlanetaryTopology.TileAt(direction, descendants[^1].Level), descendants[^1]); - } - - [Fact] - public void ResidencyFadesResidualsWithoutChangingContentDuringPresentation() - { - var root = new PlanetaryTileAddress(PlanetaryCubeFace.PositiveX, 0, 0, 0); - var child = root.Child(1, 0); - var residency = new PlanetaryResidualResidency(); - var arrival = residency.Update(new[] { root, child }, 0, 1); - var midpoint = residency.Update(new[] { root, child }, 0.5f, 1); - var settled = residency.Update(new[] { root, child }, 1, 1); - Assert.Equal(arrival.ContentVersion, midpoint.ContentVersion); - Assert.Equal(midpoint.ContentVersion, settled.ContentVersion); - Assert.Equal(0, arrival.Tiles.Single(tile => tile.Tile == child).Blend); - Assert.Equal(0.5f, midpoint.Tiles.Single(tile => tile.Tile == child).Blend); - Assert.Equal(1, settled.Tiles.Single(tile => tile.Tile == child).Blend); - - var leaving = residency.Update(new[] { root }, 1.25f, 1); - Assert.True(leaving.Tiles.Single(tile => tile.Tile == child).Departing); - Assert.Equal(1, leaving.Tiles.Single(tile => tile.Tile == child).Blend); - var gone = residency.Update(new[] { root }, 2.25f, 1); - Assert.DoesNotContain(gone.Tiles, tile => tile.Tile == child); - } - - [Fact] - public void ReversingAnArrivalDepartsFromItsCurrentWeightWithoutJumping() - { - var root = new PlanetaryTileAddress(PlanetaryCubeFace.PositiveX, 0, 0, 0); - var child = root.Child(0, 1); - var residency = new PlanetaryResidualResidency(); - _ = residency.Update(new[] { root, child }, 0, 1); - var arriving = residency.Update(new[] { root, child }, 0.25f, 1); - var departing = residency.Update(new[] { root }, 0.25f, 1); - Assert.Equal(0.25f, arriving.Tiles.Single(tile => tile.Tile == child).Blend); - Assert.Equal(0.25f, departing.Tiles.Single(tile => tile.Tile == child).Blend); - Assert.True(departing.Tiles.Single(tile => tile.Tile == child).Departing); - } -} diff --git a/tests/CultMath.Tests/PlanetaryResidencyTests.cs.meta b/tests/CultMath.Tests/PlanetaryResidencyTests.cs.meta deleted file mode 100644 index be7de2b..0000000 --- a/tests/CultMath.Tests/PlanetaryResidencyTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 9ece3b02dbd3d7448baabee7f1fba0ab \ No newline at end of file diff --git a/tests/CultMath.Tests/PlanetaryTopologyTests.cs b/tests/CultMath.Tests/PlanetaryTopologyTests.cs deleted file mode 100644 index 759928d..0000000 --- a/tests/CultMath.Tests/PlanetaryTopologyTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class PlanetaryTopologyTests -{ - [Fact] - public void FaceDirectionRoundTripsAcrossEveryFace() - { - foreach (var face in Enum.GetValues()) - foreach (var u in new[] { -0.9, -0.25, 0.0, 0.4, 0.9 }) - foreach (var v in new[] { -0.85, -0.1, 0.3, 0.88 }) - { - var direction = PlanetaryTopology.Direction(new(face, u, v)); - var roundTrip = PlanetaryTopology.FaceCoordinate(direction); - Assert.Equal(face, roundTrip.Face); - Assert.Equal(u, roundTrip.U, 5); - Assert.Equal(v, roundTrip.V, 5); - } - } - - [Fact] - public void SiblingPagesShareBitStableBoundaryDirections() - { - var left = new PlanetaryPageLayout(new(PlanetaryCubeFace.PositiveZ, 1, 0, 0), 17, 2); - var right = new PlanetaryPageLayout(new(PlanetaryCubeFace.PositiveZ, 1, 1, 0), 17, 2); - for (var y = 0; y < left.InteriorSize; y++) - { - var a = PlanetaryPageSampling.Direction(left, left.BorderSize + left.InteriorSize - 1, left.BorderSize + y); - var b = PlanetaryPageSampling.Direction(right, right.BorderSize, right.BorderSize + y); - Assert.Equal(a, b); - } - } - - [Fact] - public void DirectionSelectsContainingTileAndLocalCoordinate() - { - var expected = new PlanetaryTileAddress(PlanetaryCubeFace.NegativeY, 5, 19, 7); - var direction = PlanetaryPageSampling.DirectionAtLocal(new(expected, 17, 2), 0.37, 0.61); - Assert.Equal(expected, PlanetaryTopology.TileAt(direction, expected.Level)); - Assert.True(PlanetaryTopology.TryLocalCoordinate(direction, expected, out var local)); - Assert.Equal(0.37f, local.x, 4); - Assert.Equal(0.61f, local.y, 4); - } - - [Fact] - public void BorderSamplesDoNotRequireNeighborResidency() - { - var page = new PlanetaryPageLayout(new(PlanetaryCubeFace.PositiveX, 0, 0, 0), 17, 3); - var edge = PlanetaryPageSampling.Direction(page, page.BorderSize + page.InteriorSize - 1, page.BorderSize + 8); - var border = PlanetaryPageSampling.Direction(page, page.StorageSize - 1, page.BorderSize + 8); - 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 deleted file mode 100644 index b693014..0000000 --- a/tests/CultMath.Tests/PlanetaryTopologyTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: be3b0ec07fe65344db889d1e98448113 \ No newline at end of file diff --git a/tests/CultMath.Tests/SphericalErosionTests.cs b/tests/CultMath.Tests/SphericalErosionTests.cs deleted file mode 100644 index f634ba6..0000000 --- a/tests/CultMath.Tests/SphericalErosionTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Xunit; - -namespace CultMath.Tests; - -public sealed class SphericalErosionTests -{ - [Fact] - public void SampleIsDeterministicAndScaleInvariant() - { - var parameters = SphericalErosionParameters.Default with { Seed = 42.0f }; - var a = SphericalErosion.Sample(new float3(0.31f, -0.74f, 0.59f), 0.2f, new float3(0.4f, -0.1f, 0.2f), parameters); - var repeated = SphericalErosion.Sample(new float3(0.31f, -0.74f, 0.59f), 0.2f, new float3(0.4f, -0.1f, 0.2f), parameters); - var b = SphericalErosion.Sample(new float3(3.1f, -7.4f, 5.9f), 0.2f, new float3(0.4f, -0.1f, 0.2f), parameters); - - Assert.Equal(a, repeated); - Assert.InRange(math.abs(a.HeightOffset-b.HeightOffset),0.0f,1.0e-6f); - Assert.InRange(math.abs(a.Ridge-b.Ridge),0.0f,1.0e-6f); - Assert.InRange(math.abs(a.Gully-b.Gully),0.0f,5.0e-5f); - Assert.InRange(math.length(a.Gradient-b.Gradient),0.0f,5.0e-6f); - Assert.InRange(a.HeightOffset, -0.08f, 0.08f); - Assert.InRange(a.Ridge, 0.0f, 1.0f); - Assert.InRange(a.Gully, 0.0f, 1.0f); - } - - [Fact] - public void NearbySamplesRemainContinuousAcrossAChartBoundary() - { - var parameters = SphericalErosionParameters.Default; - var left = SphericalErosion.Sample(math.normalize(new float3(1.0f, 0.99999f, 0.2f)), 0.1f, new float3(0.2f, -0.1f, 0.05f), parameters); - var right = SphericalErosion.Sample(math.normalize(new float3(0.99999f, 1.0f, 0.2f)), 0.1f, new float3(0.2f, -0.1f, 0.05f), parameters); - - Assert.InRange(math.abs(left.HeightOffset - right.HeightOffset), 0.0f, 0.0002f); - } -} diff --git a/tests/CultMath.Tests/SphericalErosionTests.cs.meta b/tests/CultMath.Tests/SphericalErosionTests.cs.meta deleted file mode 100644 index 00ff1de..0000000 --- a/tests/CultMath.Tests/SphericalErosionTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 60eb9e9e9f8ae2948b13403f5080168e \ No newline at end of file diff --git a/tools/CultMath.PlanetaryTileFixture.meta b/tools/CultMath.PlanetaryTileFixture.meta deleted file mode 100644 index 7f40b6a..0000000 --- a/tools/CultMath.PlanetaryTileFixture.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 7197a2202273a9445b974bdd69109b1a -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/unity/Runtime/PlanetaryPageUpload.cs b/unity/Runtime/PlanetaryPageUpload.cs deleted file mode 100644 index 1a61a70..0000000 --- a/unity/Runtime/PlanetaryPageUpload.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using UnityEngine; - -namespace CultMath.Unity; - -[StructLayout(LayoutKind.Sequential)] -public readonly struct PlanetaryPageInputGpu -{ - public PlanetaryPageInputGpu(Vector4 directionRadius, Vector4 sampling) - { - DirectionRadius = directionRadius; - Sampling = sampling; - } - - public readonly Vector4 DirectionRadius; - public readonly Vector4 Sampling; -} - -[StructLayout(LayoutKind.Sequential)] -public readonly struct PlanetaryPageMetadataGpu -{ - public PlanetaryPageMetadataGpu(Vector4 address, Vector4 layout, Vector4 bounds, Vector4 state) - { - Address = address; - Layout = layout; - Bounds = bounds; - State = state; - } - - public readonly Vector4 Address; - public readonly Vector4 Layout; - public readonly Vector4 Bounds; - public readonly Vector4 State; -} - -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 -{ - public static PlanetaryPageUpload Build( - in PlanetaryResidencySnapshot snapshot, - int interiorSize, - int borderSize, - float radius) - { - if (!float.IsFinite(radius) || radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius)); - var inputs = new List(); - var metadata = new PlanetaryPageMetadataGpu[snapshot.Tiles.Length]; - foreach (var (resident, pageIndex) in snapshot.Tiles.Select((resident, index) => (resident, index))) - { - var layout = new PlanetaryPageLayout(resident.Tile, interiorSize, borderSize).Validate(); - var content = PlanetaryGpuPageBuilder.BuildContent(layout, radius); - var outputOffset = inputs.Count; - foreach (var input in content.Inputs) - { - inputs.Add(new( - ToUnity(input.DirectionRadius), - ToUnity(input.Sampling))); - } - var commonMetadata = PlanetaryGpuPageBuilder.Metadata(content, outputOffset, resident.Blend); - metadata[pageIndex] = new( - 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 deleted file mode 100644 index 87bcb61..0000000 --- a/unity/Runtime/PlanetaryPageUpload.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 7edd3bb2edb2e6448a2d54fc082c27dc \ No newline at end of file diff --git a/unity/Runtime/PlanetaryPatchMeshAdapter.cs b/unity/Runtime/PlanetaryPatchMeshAdapter.cs deleted file mode 100644 index 69a72a2..0000000 --- a/unity/Runtime/PlanetaryPatchMeshAdapter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using UnityEngine; -using UnityEngine.Rendering; - -namespace CultMath.Unity; - -public static class PlanetaryPatchMeshAdapter -{ - public static Mesh CreateFaceMesh(PlanetaryCubeFace face, int cellsPerAxis, float radius = 1) - { - if (!float.IsFinite(radius) || radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius)); - var source = PlanetaryPatch.CreateFace(face, cellsPerAxis); - var vertices = new Vector3[source.Vertices.Length]; - var normals = new Vector3[source.Vertices.Length]; - var uv = new Vector2[source.Vertices.Length]; - for (var i = 0; i < source.Vertices.Length; i++) - { - var direction = source.Vertices[i].UnitDirection; - var normal = new Vector3(direction.x, direction.y, direction.z); - vertices[i] = normal * radius; - normals[i] = normal; - uv[i] = new Vector2(source.Vertices[i].LocalCoordinate.x, source.Vertices[i].LocalCoordinate.y); - } - var mesh = new Mesh - { - name = $"CultMath Planetary {face} {cellsPerAxis}x{cellsPerAxis}", - indexFormat = vertices.Length > ushort.MaxValue ? IndexFormat.UInt32 : IndexFormat.UInt16, - vertices = vertices, - normals = normals, - uv = uv, - triangles = source.Indices, - }; - mesh.RecalculateBounds(); - return mesh; - } -} diff --git a/unity/Runtime/PlanetaryPatchMeshAdapter.cs.meta b/unity/Runtime/PlanetaryPatchMeshAdapter.cs.meta deleted file mode 100644 index 69af0ab..0000000 --- a/unity/Runtime/PlanetaryPatchMeshAdapter.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 9ad7b581e14ef774d8435069bb77ca6a \ No newline at end of file diff --git a/web/planetary-tile.cross-runtime.test.mjs b/web/planetary-tile.cross-runtime.test.mjs deleted file mode 100644 index 2b45721..0000000 --- a/web/planetary-tile.cross-runtime.test.mjs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 3c38da5..0000000 --- a/web/planetary-tile.cross-runtime.test.mjs.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 7bc397e5660d2b6448919c41983d35e8 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/web/planetary-tile.test.mjs b/web/planetary-tile.test.mjs deleted file mode 100644 index cb054f2..0000000 --- a/web/planetary-tile.test.mjs +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index d622095..0000000 --- a/web/planetary-tile.test.mjs.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 04c93ecab90d465499aa3d60722e6fc3 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/web/planetary-tile.ts b/web/planetary-tile.ts deleted file mode 100644 index e68d70f..0000000 --- a/web/planetary-tile.ts +++ /dev/null @@ -1,89 +0,0 @@ -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 deleted file mode 100644 index 337da3b..0000000 --- a/web/planetary-tile.ts.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: f284e64628320844196b7aaea76475a4 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: