Complete portable planetary presentation contracts
This commit is contained in:
parent
42fef7cdd7
commit
102403efac
74 changed files with 1166 additions and 31 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,4 +4,5 @@ obj/
|
|||
**/obj/
|
||||
target/
|
||||
**/target/
|
||||
/native/cultmath-core/target.meta
|
||||
.tools/
|
||||
|
|
|
|||
|
|
@ -5,4 +5,7 @@
|
|||
<Folder Name="/tests/">
|
||||
<Project Path="tests/CultMath.Tests/CultMath.Tests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tools/">
|
||||
<Project Path="Tools~/CultMath.PlanetaryTileFixture/CultMath.PlanetaryTileFixture.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -86,6 +86,20 @@ cargo test --manifest-path native/cultmath-core/Cargo.toml
|
|||
cargo build --release --manifest-path native/cultmath-core/Cargo.toml
|
||||
```
|
||||
|
||||
## Unity compiler
|
||||
|
||||
CultMath uses C# 10 record structs and file-scoped namespaces. Unity projects
|
||||
must select the compiler's latest language mode with an `Assets/csc.rsp` file:
|
||||
|
||||
```text
|
||||
-langversion:latest
|
||||
```
|
||||
|
||||
The package's Unity smoke project verifies this contract against Unity 6. The
|
||||
setting is already present in current GameCult Unity clients; it belongs to the
|
||||
consumer project because Unity only reads compiler response files from its
|
||||
`Assets` root.
|
||||
|
||||
## Shader Tooling
|
||||
|
||||
CultMath keeps portable DXC outside git under `.tools/`:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "CultMath.PlanetaryViewer.Editor",
|
||||
"rootNamespace": "CultMath.Unity.Samples.Editor",
|
||||
"references": [
|
||||
"CultMath.PlanetaryViewer"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CultMath.Unity.Samples.Editor;
|
||||
|
||||
public static class CultMathPlanetaryViewerMenu
|
||||
{
|
||||
[MenuItem("GameObject/CultMath/Planetary Field Viewer", false, 10)]
|
||||
public static void CreateViewer()
|
||||
{
|
||||
var root = new GameObject("CultMath Planetary Field Viewer");
|
||||
Undo.RegisterCreatedObjectUndo(root, "Create CultMath Planetary Viewer");
|
||||
root.AddComponent<CultMathPlanetaryViewer>();
|
||||
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>();
|
||||
camera.nearClipPlane = 0.01f;
|
||||
camera.farClipPlane = 1000;
|
||||
}
|
||||
}
|
||||
16
Samples~/PlanetaryViewer/README.md
Normal file
16
Samples~/PlanetaryViewer/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Planetary Field Viewer
|
||||
|
||||
Import this sample through Unity's Package Manager, then choose
|
||||
`GameObject > CultMath > Planetary Field Viewer`.
|
||||
|
||||
The host project must contain `Assets/csc.rsp` with
|
||||
`-langversion:latest`, as documented in the CultMath package README.
|
||||
|
||||
The generated six-face mesh uses `PlanetaryPatch`. Its shader calls
|
||||
`cultmath_planetary_field_sample` from the packaged CultMath HLSL, while the
|
||||
component exposes the matching CPU `Sample` method. Seed, radius, physical
|
||||
sample footprint, erosion settings, and base-field equations are shared between
|
||||
the two bodies.
|
||||
|
||||
This is a renderer integration fixture, not a gameplay owner. It deliberately
|
||||
contains no Aetheria state, biomes, resources, or simulation decisions.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "CultMath.PlanetaryViewer",
|
||||
"rootNamespace": "CultMath.Unity.Samples",
|
||||
"references": [
|
||||
"CultMath",
|
||||
"CultMath.Unity"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
139
Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.cs
Normal file
139
Samples~/PlanetaryViewer/Runtime/CultMathPlanetaryViewer.cs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CultMath.Unity.Samples;
|
||||
|
||||
[ExecuteAlways]
|
||||
public sealed class CultMathPlanetaryViewer : MonoBehaviour
|
||||
{
|
||||
[Min(0.001f)] public float Radius = 8.4f;
|
||||
[Range(1, 256)] public int CellsPerFace = 64;
|
||||
public int Seed = 42;
|
||||
[Min(0.0001f)] public float SampleFootprint = 0.01f;
|
||||
public Shader? PlanetShader;
|
||||
public Vector3 LightDirection = new(-0.4f, 0.7f, -0.6f);
|
||||
public Color LowColor = new(0.055f, 0.16f, 0.09f, 1);
|
||||
public Color HighColor = new(0.62f, 0.48f, 0.28f, 1);
|
||||
public Color RidgeColor = new(0.88f, 0.83f, 0.7f, 1);
|
||||
|
||||
private readonly List<Mesh> 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<MeshFilter>().sharedMesh = mesh;
|
||||
child.AddComponent<MeshRenderer>().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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
Shader "CultMath/Planetary Field Viewer"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
_LowColor ("Low Color", Color) = (0.055, 0.16, 0.09, 1)
|
||||
_HighColor ("High Color", Color) = (0.62, 0.48, 0.28, 1)
|
||||
_RidgeColor ("Ridge Color", Color) = (0.88, 0.83, 0.7, 1)
|
||||
}
|
||||
SubShader
|
||||
{
|
||||
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
|
||||
Pass
|
||||
{
|
||||
Cull Back
|
||||
ZWrite On
|
||||
HLSLPROGRAM
|
||||
#pragma target 5.0
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag
|
||||
#include "UnityCG.cginc"
|
||||
#include "Packages/org.gamecult.cultmath/shaders/CultMath.hlsl"
|
||||
|
||||
float _Radius;
|
||||
float _SampleFootprint;
|
||||
int _Seed;
|
||||
float _ErosionScale, _ErosionStrength, _GullyWeight, _Detail;
|
||||
float4 _Rounding, _Onset, _AssumedSlope;
|
||||
float _CellScale, _Normalization, _Lacunarity, _Gain;
|
||||
int _Octaves;
|
||||
float3 _LightDirection;
|
||||
float4 _LowColor, _HighColor, _RidgeColor;
|
||||
|
||||
struct Attributes { float4 vertex : POSITION; };
|
||||
struct Varyings
|
||||
{
|
||||
float4 position : SV_POSITION;
|
||||
float3 worldNormal : TEXCOORD0;
|
||||
float height : TEXCOORD1;
|
||||
float ridge : TEXCOORD2;
|
||||
};
|
||||
|
||||
CultMathPlanetarySurfaceSample viewer_sample(float3 direction)
|
||||
{
|
||||
direction = normalize(direction);
|
||||
float value = direction.x * 0.6 + direction.y * direction.z * 0.3;
|
||||
float3 angular_gradient = float3(0.6, direction.z * 0.3, direction.y * 0.3);
|
||||
angular_gradient -= direction * dot(angular_gradient, direction);
|
||||
CultMathPlanetaryBaseFieldSample base_sample;
|
||||
base_sample.radial_displacement = value * _Radius * 0.035;
|
||||
base_sample.radial_gradient = angular_gradient * 0.035;
|
||||
base_sample.field_value = value;
|
||||
base_sample.field_gradient = angular_gradient / _Radius;
|
||||
base_sample.fade_target = clamp(value, -1.0, 1.0);
|
||||
CultMathAdvancedErosionParameters erosion;
|
||||
erosion.scale = _ErosionScale; erosion.strength = _ErosionStrength;
|
||||
erosion.gully_weight = _GullyWeight; erosion.detail = _Detail;
|
||||
erosion.rounding = _Rounding; erosion.onset = _Onset;
|
||||
erosion.assumed_slope = _AssumedSlope.xy; erosion.cell_scale = _CellScale;
|
||||
erosion.normalization = _Normalization; erosion.octaves = _Octaves;
|
||||
erosion.lacunarity = _Lacunarity; erosion.gain = _Gain;
|
||||
CultMathPlanetaryFieldDefinition definition;
|
||||
definition.radius = _Radius; definition.seed = _Seed; definition.erosion = erosion;
|
||||
return cultmath_planetary_field_sample(definition, direction, base_sample, _SampleFootprint);
|
||||
}
|
||||
|
||||
Varyings vert(Attributes input)
|
||||
{
|
||||
float3 direction = normalize(input.vertex.xyz);
|
||||
CultMathPlanetarySurfaceSample sample = viewer_sample(direction);
|
||||
float3 local = direction * (_Radius + sample.radial_displacement);
|
||||
Varyings output;
|
||||
output.position = UnityObjectToClipPos(float4(local, 1));
|
||||
output.worldNormal = UnityObjectToWorldNormal(sample.surface_normal);
|
||||
output.height = sample.radial_displacement / max(_Radius * 0.1, 1.0e-5);
|
||||
output.ridge = sample.ridge;
|
||||
return output;
|
||||
}
|
||||
|
||||
float4 frag(Varyings input) : SV_Target
|
||||
{
|
||||
float height = saturate(input.height * 0.5 + 0.5);
|
||||
float3 albedo = lerp(_LowColor.rgb, _HighColor.rgb, height);
|
||||
albedo = lerp(albedo, _RidgeColor.rgb, saturate((input.ridge - 0.55) * 2.5));
|
||||
float diffuse = saturate(dot(normalize(input.worldNormal), normalize(-_LightDirection))) * 0.82 + 0.08;
|
||||
return float4(albedo * diffuse, 1);
|
||||
}
|
||||
ENDHLSL
|
||||
}
|
||||
}
|
||||
}
|
||||
7
THIRD-PARTY-NOTICES.md.meta
Normal file
7
THIRD-PARTY-NOTICES.md.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7c28c8631c7d4054eb751d7ce952df33
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\CultMath\CultMath.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
22
Tools~/CultMath.PlanetaryTileFixture/Program.cs
Normal file
22
Tools~/CultMath.PlanetaryTileFixture/Program.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using CultMath;
|
||||
|
||||
if (args.Length != 1) throw new ArgumentException("Expected one output path.");
|
||||
var field = PlanetaryFieldDefinition.Create(0x55aa, 10, 7, AdvancedErosionParameters.Default);
|
||||
var layout = new PlanetaryMapTileLayout(new(PlanetaryProjectionKind.EqualEarth), 0, 0, 0, 5, 1);
|
||||
var key = new PlanetaryMapTileKey(field.FieldVersion, 3, 29, layout, PlanetaryQueryScale.AtFootprint(0.25f, 0.125f));
|
||||
var tile = PlanetaryMapTileBaker.Bake(field, key, new FixtureField());
|
||||
var path = Path.GetFullPath(args[0]);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
using var stream = File.Create(path);
|
||||
PlanetaryMapTileEncoding.Write(stream, tile);
|
||||
|
||||
readonly struct FixtureField : IPlanetaryBaseField
|
||||
{
|
||||
public PlanetaryBaseFieldSample Sample(float3 direction)
|
||||
{
|
||||
var value = direction.x * 0.2f + direction.z * 0.4f;
|
||||
var gradient = new float3(0.2f, 0, 0.4f);
|
||||
gradient -= direction * math.dot(gradient, direction);
|
||||
return new(value, gradient, value, gradient * 0.1f, value);
|
||||
}
|
||||
}
|
||||
|
|
@ -80,11 +80,17 @@ coarse meshes from the canonical topology. HLSL calls
|
|||
Renderers still own cameras, clip space, depth, materials, buffers, dispatch,
|
||||
and draw submission.
|
||||
|
||||
`PlanetaryGpuPageBuilder` owns the renderer-neutral page input and metadata
|
||||
payload. Host adapters may convert its `float4` values to native vector types,
|
||||
but do not recalculate directions, spacing, addressing, or transition state.
|
||||
|
||||
The optional `CultMath.Unity` assembly converts a canonical face patch into a
|
||||
Unity `Mesh` and converts a residency snapshot into Unity-native page input and
|
||||
metadata payloads. It owns no field parameters, page selection, or simulation
|
||||
state. Unity render code remains responsible for allocating/binding graphics
|
||||
buffers and dispatching application-specific base-field entry points.
|
||||
Unity `Mesh` and converts the shared GPU payload into Unity-native structs. It
|
||||
owns no field parameters, page selection, or simulation state. The package's
|
||||
Planetary Field Viewer sample renders the shared HLSL field and reports matching
|
||||
CPU queries. Unity render code remains responsible for allocating/binding
|
||||
graphics buffers and dispatching application-specific base-field entry points.
|
||||
The host Unity project must set `-langversion:latest` in `Assets/csc.rsp`.
|
||||
|
||||
## Maps
|
||||
|
||||
|
|
@ -103,6 +109,12 @@ projection and canonical field query. `PlanetaryMapTileKey` includes field,
|
|||
projection, layer, layout, and query-scale identity. Palettes, contours, labels,
|
||||
and strategic overlays do not enter CultMath.
|
||||
|
||||
`PlanetaryMapTileEncoding` writes the resulting evidence as a versioned
|
||||
little-endian `CMPT` binary asset. `web/planetary-tile.ts` decodes that exact
|
||||
format into browser-native values without defining another terrain generator.
|
||||
Typed CultMesh documents should carry the tile key and asset reference; the
|
||||
binary payload is derived CDN/cache data, not committed game state.
|
||||
|
||||
## Numeric contract
|
||||
|
||||
Integer hashes and published field versions are exact. Basic arithmetic follows
|
||||
|
|
@ -111,7 +123,8 @@ CultMath's matched C#/HLSL semantics. Hardware transcendental functions such as
|
|||
Every filtered terrain query carries an unresolved-height bound so consumers can
|
||||
reject insufficient fidelity explicitly.
|
||||
|
||||
Current D3D12 parity evidence covers the advanced erosion kernel and the QSC
|
||||
forward/inverse topology. The remaining production page and patch shaders use
|
||||
the common functions and retain their seam, residual, summary, and radial-hit
|
||||
integration probes in Fensalir.
|
||||
Current D3D12 parity evidence covers the advanced erosion kernel, QSC topology,
|
||||
and forward/inverse transforms for every supported map projection, including
|
||||
center and scale parameters. Production page and patch shaders use the common
|
||||
functions and retain their seam, residual, summary, and radial-hit integration
|
||||
probes in Fensalir.
|
||||
|
|
|
|||
7
docs/planetary.md.meta
Normal file
7
docs/planetary.md.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 23a5751e4cececc4e99e024058d69add
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -10,5 +10,12 @@
|
|||
"Simulation"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"category": "Scripting"
|
||||
"category": "Scripting",
|
||||
"samples": [
|
||||
{
|
||||
"displayName": "Planetary Field Viewer",
|
||||
"description": "Runnable Unity viewer using the canonical CultMath CPU and HLSL planetary field.",
|
||||
"path": "Samples~/PlanetaryViewer"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
8
scripts.meta
Normal file
8
scripts.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0ddff5b782cf14e43ae5cda048e040d8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
scripts/test-web-planetary-tile.ps1
Normal file
9
scripts/test-web-planetary-tile.ps1
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$fixture = Join-Path $root ".tools\planetary-tile\fixture.cmpt"
|
||||
dotnet run --project (Join-Path $root "Tools~\CultMath.PlanetaryTileFixture\CultMath.PlanetaryTileFixture.csproj") -- $fixture
|
||||
if ($LASTEXITCODE -ne 0) { throw "Planetary tile fixture generation failed." }
|
||||
node --no-warnings --experimental-strip-types (Join-Path $root "web\planetary-tile.cross-runtime.test.mjs") $fixture
|
||||
if ($LASTEXITCODE -ne 0) { throw "Planetary tile web decoding failed." }
|
||||
node --no-warnings --experimental-strip-types (Join-Path $root "web\planetary-tile.test.mjs")
|
||||
if ($LASTEXITCODE -ne 0) { throw "Planetary tile decoder unit test failed." }
|
||||
7
scripts/test-web-planetary-tile.ps1.meta
Normal file
7
scripts/test-web-planetary-tile.ps1.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 235e8f203bd1c4d40a8fa211273106ba
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
shaders/AdvancedErosionFilter.hlsl.meta
Normal file
7
shaders/AdvancedErosionFilter.hlsl.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3252ac02e8853ef4b9d1fbc5204b5c6b
|
||||
ShaderIncludeImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -120,6 +120,138 @@ float3 cultmath_planetary_equal_earth_inverse(float2 coordinate)
|
|||
return cultmath_normalize(float3(cos_latitude * cos(longitude), cos_latitude * sin(longitude), sin(latitude)));
|
||||
}
|
||||
|
||||
float cultmath_planetary_wrap_longitude(float longitude)
|
||||
{
|
||||
longitude = fmod(longitude, CULTMATH_TAU);
|
||||
if (longitude > CULTMATH_PI) longitude -= CULTMATH_TAU;
|
||||
if (longitude < -CULTMATH_PI) longitude += CULTMATH_TAU;
|
||||
return longitude;
|
||||
}
|
||||
|
||||
float3 cultmath_planetary_lon_lat_direction(float longitude, float latitude)
|
||||
{
|
||||
float cos_latitude = cos(latitude);
|
||||
return cultmath_normalize(float3(cos_latitude * cos(longitude), cos_latitude * sin(longitude), sin(latitude)));
|
||||
}
|
||||
|
||||
float cultmath_planetary_asinh(float value) { return log(value + sqrt(value * value + 1.0)); }
|
||||
float cultmath_planetary_sinh(float value) { return 0.5 * (exp(value) - exp(-value)); }
|
||||
|
||||
bool cultmath_planetary_azimuthal_forward(float3 direction, float2 center_lon_lat, int kind, out float2 coordinate)
|
||||
{
|
||||
direction = cultmath_normalize(direction);
|
||||
float longitude = cultmath_planetary_wrap_longitude(atan2(direction.y, direction.x) - center_lon_lat.x);
|
||||
float latitude = asin(clamp(direction.z, -1.0, 1.0));
|
||||
float sin0 = sin(center_lon_lat.y), cos0 = cos(center_lon_lat.y);
|
||||
float sin_latitude = sin(latitude), cos_latitude = cos(latitude), cos_longitude = cos(longitude);
|
||||
float cos_c = clamp(sin0 * sin_latitude + cos0 * cos_latitude * cos_longitude, -1.0, 1.0);
|
||||
float2 raw = float2(cos_latitude * sin(longitude), cos0 * sin_latitude - sin0 * cos_latitude * cos_longitude);
|
||||
float factor = 1.0;
|
||||
if (kind == 3)
|
||||
{
|
||||
if (cos_c < 0.0) { coordinate = 0.0; return false; }
|
||||
}
|
||||
else if (kind == 4)
|
||||
{
|
||||
float c = acos(cos_c); factor = (c < 1.0e-6 ? 1.0 : c / sin(c)) / CULTMATH_PI;
|
||||
}
|
||||
else if (kind == 5) factor = sqrt(2.0 / max(1.0 + cos_c, 1.0e-20)) * 0.5;
|
||||
else
|
||||
{
|
||||
if (cos_c <= 0.0) { coordinate = 0.0; return false; }
|
||||
factor = 1.0 / cos_c;
|
||||
}
|
||||
coordinate = raw * factor;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cultmath_planetary_azimuthal_inverse(float2 coordinate, float2 center_lon_lat, int kind, out float3 direction)
|
||||
{
|
||||
float rho = length(coordinate);
|
||||
float c;
|
||||
if (kind == 3) { if (rho > 1.0) { direction = 0.0; return false; } c = asin(clamp(rho, 0.0, 1.0)); }
|
||||
else if (kind == 4) { if (rho > 1.0) { direction = 0.0; return false; } c = rho * CULTMATH_PI; }
|
||||
else if (kind == 5) { if (rho > 1.0) { direction = 0.0; return false; } c = 2.0 * asin(clamp(rho, 0.0, 1.0)); }
|
||||
else c = atan(rho);
|
||||
if (rho < 1.0e-6) { direction = cultmath_planetary_lon_lat_direction(center_lon_lat.x, center_lon_lat.y); return true; }
|
||||
float sin_c = sin(c), cos_c = cos(c), sin0 = sin(center_lon_lat.y), cos0 = cos(center_lon_lat.y);
|
||||
float latitude = asin(clamp(cos_c * sin0 + coordinate.y * sin_c * cos0 / rho, -1.0, 1.0));
|
||||
float delta_longitude = atan2(coordinate.x * sin_c, rho * cos0 * cos_c - coordinate.y * sin0 * sin_c);
|
||||
direction = cultmath_planetary_lon_lat_direction(cultmath_planetary_wrap_longitude(delta_longitude + center_lon_lat.x), latitude);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cultmath_planetary_cube_atlas_forward(float3 direction, out float2 coordinate)
|
||||
{
|
||||
int face;
|
||||
float2 face_coordinate = cultmath_planetary_face_coordinate(direction, face);
|
||||
int column = face % 3;
|
||||
int row = face / 3;
|
||||
coordinate = float2(-1.0 + (column + (face_coordinate.x + 1.0) * 0.5) * (2.0 / 3.0), 1.0 - (row + (face_coordinate.y + 1.0) * 0.5));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cultmath_planetary_cube_atlas_inverse(float2 coordinate, out float3 direction)
|
||||
{
|
||||
if (any(coordinate < -1.0) || any(coordinate > 1.0)) { direction = 0.0; return false; }
|
||||
int column = min((int)((coordinate.x + 1.0) * 1.5), 2);
|
||||
int row = min((int)(1.0 - coordinate.y), 1);
|
||||
float u = ((coordinate.x + 1.0) * 1.5 - column) * 2.0 - 1.0;
|
||||
float v = ((1.0 - coordinate.y) - row) * 2.0 - 1.0;
|
||||
direction = cultmath_planetary_face_direction(row * 3 + column, float2(u, v));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cultmath_planetary_projection_forward(float3 direction, int kind, float3 center_lon_lat_scale, out float2 coordinate)
|
||||
{
|
||||
float scale = max(center_lon_lat_scale.z, 1.0e-20);
|
||||
if (kind == 6)
|
||||
{
|
||||
bool valid = cultmath_planetary_cube_atlas_forward(direction, coordinate);
|
||||
coordinate /= scale; return valid;
|
||||
}
|
||||
if (kind >= 3)
|
||||
{
|
||||
bool valid = cultmath_planetary_azimuthal_forward(direction, center_lon_lat_scale.xy, kind, coordinate);
|
||||
coordinate /= scale; return valid;
|
||||
}
|
||||
direction = cultmath_normalize(direction);
|
||||
float longitude = cultmath_planetary_wrap_longitude(atan2(direction.y, direction.x) - center_lon_lat_scale.x);
|
||||
float latitude = asin(clamp(direction.z, -1.0, 1.0));
|
||||
if (kind == 0) coordinate = float2(longitude / CULTMATH_PI, latitude / CULTMATH_HALF_PI);
|
||||
else if (kind == 1)
|
||||
{
|
||||
const float maximum_latitude = 1.4844222297453324;
|
||||
if (abs(latitude) > maximum_latitude) { coordinate = 0.0; return false; }
|
||||
coordinate = float2(longitude / CULTMATH_PI, cultmath_planetary_asinh(tan(latitude)) / CULTMATH_PI);
|
||||
}
|
||||
else
|
||||
{
|
||||
float3 centered = cultmath_planetary_lon_lat_direction(longitude, latitude);
|
||||
coordinate = cultmath_planetary_equal_earth_forward(centered);
|
||||
}
|
||||
coordinate /= scale;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cultmath_planetary_projection_inverse(float2 coordinate, int kind, float3 center_lon_lat_scale, out float3 direction)
|
||||
{
|
||||
coordinate *= max(center_lon_lat_scale.z, 1.0e-20);
|
||||
if (kind == 6) return cultmath_planetary_cube_atlas_inverse(coordinate, direction);
|
||||
if (kind >= 3) return cultmath_planetary_azimuthal_inverse(coordinate, center_lon_lat_scale.xy, kind, direction);
|
||||
if (any(abs(coordinate) > 1.0)) { direction = 0.0; return false; }
|
||||
float longitude, latitude;
|
||||
if (kind == 0) { longitude = coordinate.x * CULTMATH_PI; latitude = coordinate.y * CULTMATH_HALF_PI; }
|
||||
else if (kind == 1) { longitude = coordinate.x * CULTMATH_PI; latitude = atan(cultmath_planetary_sinh(coordinate.y * CULTMATH_PI)); }
|
||||
else
|
||||
{
|
||||
float3 centered = cultmath_planetary_equal_earth_inverse(coordinate);
|
||||
longitude = atan2(centered.y, centered.x); latitude = asin(clamp(centered.z, -1.0, 1.0));
|
||||
}
|
||||
direction = cultmath_planetary_lon_lat_direction(cultmath_planetary_wrap_longitude(longitude + center_lon_lat_scale.x), latitude);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct CultMathPlanetaryFieldDefinition
|
||||
{
|
||||
float radius;
|
||||
|
|
|
|||
7
shaders/Planetary.hlsl.meta
Normal file
7
shaders/Planetary.hlsl.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bb055ad5328888e479e90ae3fe62c3fe
|
||||
ShaderIncludeImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2
src/CultMath/AdvancedErosionFilter.cs.meta
Normal file
2
src/CultMath/AdvancedErosionFilter.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bec15fdcbe97a7f44a24bdb3ff79e1e7
|
||||
2
src/CultMath/ErosionFrequencyBands.cs.meta
Normal file
2
src/CultMath/ErosionFrequencyBands.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 05e7969589dd09b43bd0fda672d25a0b
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public readonly record struct PlanetaryQueryScale(float FootprintMeters, float MaximumUnresolvedHeight)
|
||||
|
|
|
|||
2
src/CultMath/PlanetaryField.cs.meta
Normal file
2
src/CultMath/PlanetaryField.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b61d6354e3aab054e94721e77e619bfa
|
||||
55
src/CultMath/PlanetaryGpuPages.cs
Normal file
55
src/CultMath/PlanetaryGpuPages.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public readonly record struct PlanetaryGpuPageInput(float4 DirectionRadius, float4 Sampling);
|
||||
|
||||
public readonly record struct PlanetaryGpuPageMetadata(float4 Address, float4 Layout, float4 Bounds, float4 State);
|
||||
|
||||
public readonly record struct PlanetaryGpuPageContent(
|
||||
PlanetaryPageLayout Layout,
|
||||
PlanetaryGpuPageInput[] Inputs,
|
||||
float SampleSpacing,
|
||||
float ParentSampleSpacing);
|
||||
|
||||
public static class PlanetaryGpuPageBuilder
|
||||
{
|
||||
public static PlanetaryGpuPageContent BuildContent(PlanetaryPageLayout layout, float radius)
|
||||
{
|
||||
layout.Validate();
|
||||
if (!float.IsFinite(radius) || radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius));
|
||||
var spacing = PlanetaryPageSampling.NominalAngularTexelSize(layout) * radius;
|
||||
var parentSpacing = layout.Tile.Level == 0
|
||||
? 0
|
||||
: PlanetaryPageSampling.NominalAngularTexelSize(new(layout.Tile.Parent(), layout.InteriorSize, layout.BorderSize)) * radius;
|
||||
var inputs = new PlanetaryGpuPageInput[layout.StorageSize * layout.StorageSize];
|
||||
for (var y = 0; y < layout.StorageSize; y++)
|
||||
for (var x = 0; x < layout.StorageSize; x++)
|
||||
{
|
||||
var direction = PlanetaryPageSampling.Direction(layout, x, y);
|
||||
inputs[y * layout.StorageSize + x] = new(
|
||||
new float4(direction, radius),
|
||||
new float4(spacing, parentSpacing, 0, 0));
|
||||
}
|
||||
return new(layout, inputs, spacing, parentSpacing);
|
||||
}
|
||||
|
||||
public static PlanetaryGpuPageMetadata Metadata(
|
||||
in PlanetaryGpuPageContent content,
|
||||
int outputOffset,
|
||||
float blend,
|
||||
float4 bounds = default)
|
||||
{
|
||||
content.Layout.Validate();
|
||||
if (content.Inputs == null || content.Inputs.Length != content.Layout.StorageSize * content.Layout.StorageSize)
|
||||
throw new ArgumentException("GPU page content does not match its layout.", nameof(content));
|
||||
if (outputOffset < 0) throw new ArgumentOutOfRangeException(nameof(outputOffset));
|
||||
if (!float.IsFinite(blend) || blend is < 0 or > 1) throw new ArgumentOutOfRangeException(nameof(blend));
|
||||
var tile = content.Layout.Tile;
|
||||
return new(
|
||||
new float4((int)tile.Face, tile.Level, tile.X, tile.Y),
|
||||
new float4(outputOffset, content.Layout.StorageSize, content.Layout.InteriorSize, content.Layout.BorderSize),
|
||||
bounds,
|
||||
new float4(1, blend, PlanetaryPageSampling.NominalAngularTexelSize(content.Layout), content.SampleSpacing));
|
||||
}
|
||||
}
|
||||
2
src/CultMath/PlanetaryGpuPages.cs.meta
Normal file
2
src/CultMath/PlanetaryGpuPages.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 42173c60766948a4e8c376687033532e
|
||||
117
src/CultMath/PlanetaryMapTileEncoding.cs
Normal file
117
src/CultMath/PlanetaryMapTileEncoding.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public static class PlanetaryMapTileEncoding
|
||||
{
|
||||
public const uint FormatVersion = 1;
|
||||
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("CMPT");
|
||||
|
||||
public static void Write(Stream stream, in PlanetarySurfaceMapTile tile, bool leaveOpen = false)
|
||||
{
|
||||
if (stream == null || !stream.CanWrite) throw new ArgumentException("A writable stream is required.", nameof(stream));
|
||||
tile.Key.Validate();
|
||||
if (tile.Samples == null || tile.Validity == null || tile.Samples.Length != tile.Validity.Length)
|
||||
throw new ArgumentException("Tile samples and validity must have equal lengths.", nameof(tile));
|
||||
var expectedCount = tile.Key.Layout.StorageSize * tile.Key.Layout.StorageSize;
|
||||
if (tile.Samples.Length != expectedCount) throw new ArgumentException("Tile payload does not match its layout.", nameof(tile));
|
||||
|
||||
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen);
|
||||
writer.Write(Magic);
|
||||
writer.Write(FormatVersion);
|
||||
writer.Write(tile.Key.FieldVersion);
|
||||
writer.Write(tile.Key.ProjectionVersion);
|
||||
writer.Write(tile.Key.LayerId);
|
||||
var projection = tile.Key.Layout.Projection;
|
||||
writer.Write((int)projection.Kind);
|
||||
writer.Write(projection.CenterLongitude);
|
||||
writer.Write(projection.CenterLatitude);
|
||||
writer.Write(projection.Scale);
|
||||
writer.Write(tile.Key.Layout.Level);
|
||||
writer.Write(tile.Key.Layout.X);
|
||||
writer.Write(tile.Key.Layout.Y);
|
||||
writer.Write(tile.Key.Layout.InteriorSize);
|
||||
writer.Write(tile.Key.Layout.BorderSize);
|
||||
writer.Write(tile.Key.QueryScale.FootprintMeters);
|
||||
writer.Write(tile.Key.QueryScale.MaximumUnresolvedHeight);
|
||||
writer.Write(tile.Samples.Length);
|
||||
for (var i = 0; i < tile.Samples.Length; i++)
|
||||
{
|
||||
writer.Write(tile.Validity[i]);
|
||||
if (!tile.Validity[i]) continue;
|
||||
WriteSample(writer, tile.Samples[i], tile.Key.FieldVersion);
|
||||
}
|
||||
}
|
||||
|
||||
public static PlanetarySurfaceMapTile Read(Stream stream, bool leaveOpen = false)
|
||||
{
|
||||
if (stream == null || !stream.CanRead) throw new ArgumentException("A readable stream is required.", nameof(stream));
|
||||
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen);
|
||||
var magic = reader.ReadBytes(Magic.Length);
|
||||
if (magic.Length != Magic.Length || !magic.AsSpan().SequenceEqual(Magic)) throw new InvalidDataException("Not a CultMath planetary tile.");
|
||||
var version = reader.ReadUInt32();
|
||||
if (version != FormatVersion) throw new InvalidDataException($"Unsupported planetary tile version {version}.");
|
||||
var fieldVersion = reader.ReadUInt64();
|
||||
var projectionVersion = reader.ReadUInt32();
|
||||
var layerId = reader.ReadUInt32();
|
||||
var kindValue = reader.ReadInt32();
|
||||
if (!Enum.IsDefined(typeof(PlanetaryProjectionKind), kindValue)) throw new InvalidDataException("Unknown planetary projection kind.");
|
||||
var projection = new PlanetaryProjectionParameters(
|
||||
(PlanetaryProjectionKind)kindValue,
|
||||
reader.ReadDouble(), reader.ReadDouble(), reader.ReadDouble()).Validate();
|
||||
var layout = new PlanetaryMapTileLayout(
|
||||
projection,
|
||||
reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32(),
|
||||
reader.ReadInt32(), reader.ReadInt32()).Validate();
|
||||
var scale = new PlanetaryQueryScale(reader.ReadSingle(), reader.ReadSingle()).Validate();
|
||||
var key = new PlanetaryMapTileKey(fieldVersion, projectionVersion, layerId, layout, scale).Validate();
|
||||
var count = reader.ReadInt32();
|
||||
if (count != layout.StorageSize * layout.StorageSize) throw new InvalidDataException("Planetary tile sample count does not match layout.");
|
||||
var samples = new PlanetarySurfaceSample[count];
|
||||
var validity = new bool[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
validity[i] = reader.ReadBoolean();
|
||||
if (validity[i]) samples[i] = ReadSample(reader, fieldVersion);
|
||||
}
|
||||
if (stream.CanSeek && stream.Position != stream.Length) throw new InvalidDataException("Planetary tile has trailing bytes.");
|
||||
return new(key, samples, validity);
|
||||
}
|
||||
|
||||
private static void WriteSample(BinaryWriter writer, PlanetarySurfaceSample sample, ulong fieldVersion)
|
||||
{
|
||||
if (sample.FieldVersion != fieldVersion) throw new ArgumentException("Sample field version does not match tile identity.");
|
||||
Write(writer, sample.UnitDirection);
|
||||
writer.Write(sample.Radius);
|
||||
writer.Write(sample.RadialDisplacement);
|
||||
Write(writer, sample.TangentGradient);
|
||||
Write(writer, sample.SurfaceNormal);
|
||||
writer.Write(sample.Slope);
|
||||
writer.Write(sample.Ridge);
|
||||
writer.Write(sample.Gully);
|
||||
writer.Write(sample.FinestResolvedWavelength);
|
||||
writer.Write(sample.UnresolvedHeightBound);
|
||||
}
|
||||
|
||||
private static PlanetarySurfaceSample ReadSample(BinaryReader reader, ulong fieldVersion)
|
||||
{
|
||||
var direction = ReadFloat3(reader);
|
||||
var radius = reader.ReadSingle();
|
||||
var displacement = reader.ReadSingle();
|
||||
var gradient = ReadFloat3(reader);
|
||||
var normal = ReadFloat3(reader);
|
||||
return new(
|
||||
fieldVersion, direction, radius, displacement, gradient, normal,
|
||||
reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(),
|
||||
reader.ReadSingle(), reader.ReadSingle());
|
||||
}
|
||||
|
||||
private static void Write(BinaryWriter writer, float3 value)
|
||||
{
|
||||
writer.Write(value.x); writer.Write(value.y); writer.Write(value.z);
|
||||
}
|
||||
|
||||
private static float3 ReadFloat3(BinaryReader reader) => new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
|
||||
}
|
||||
2
src/CultMath/PlanetaryMapTileEncoding.cs.meta
Normal file
2
src/CultMath/PlanetaryMapTileEncoding.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6898f6de57df20646a0b9077bb923592
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public readonly record struct PlanetaryPageLayout(PlanetaryTileAddress Tile, int InteriorSize, int BorderSize)
|
||||
|
|
|
|||
2
src/CultMath/PlanetaryPages.cs.meta
Normal file
2
src/CultMath/PlanetaryPages.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7af9a0354faa56348a2e07188739a0ae
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public readonly record struct PlanetaryPatchVertex(float2 LocalCoordinate, float3 UnitDirection);
|
||||
|
|
|
|||
2
src/CultMath/PlanetaryPatch.cs.meta
Normal file
2
src/CultMath/PlanetaryPatch.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 04651571f7d240a418c1f6aafa217fa6
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public enum PlanetaryProjectionKind
|
||||
|
|
|
|||
2
src/CultMath/PlanetaryProjection.cs.meta
Normal file
2
src/CultMath/PlanetaryProjection.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5021202cf626f7442a57961255a8a217
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public readonly record struct PlanetaryRadialBounds(float MinimumDisplacement, float MaximumDisplacement)
|
||||
|
|
|
|||
2
src/CultMath/PlanetaryQueries.cs.meta
Normal file
2
src/CultMath/PlanetaryQueries.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2ec43c567214b8247bda19b5c120c200
|
||||
2
src/CultMath/PlanetaryRadialRefinement.cs.meta
Normal file
2
src/CultMath/PlanetaryRadialRefinement.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 82e4db521edc253499220458b417fd1e
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public readonly record struct PlanetaryLodParameters(
|
||||
|
|
@ -71,9 +75,9 @@ public readonly record struct PlanetaryResidencySnapshot(
|
|||
/// <summary>Owns presentation residency only. It never owns field content.</summary>
|
||||
public sealed class PlanetaryResidualResidency
|
||||
{
|
||||
private readonly Dictionary<ulong, PlanetaryTileAddress> residents = [];
|
||||
private readonly Dictionary<ulong, float> arrivals = [];
|
||||
private readonly Dictionary<ulong, (float Time, float StartingBlend)> departures = [];
|
||||
private readonly Dictionary<ulong, PlanetaryTileAddress> residents = new();
|
||||
private readonly Dictionary<ulong, float> arrivals = new();
|
||||
private readonly Dictionary<ulong, (float Time, float StartingBlend)> departures = new();
|
||||
private float lastTime = float.NegativeInfinity;
|
||||
|
||||
public void Reset()
|
||||
|
|
|
|||
2
src/CultMath/PlanetaryResidency.cs.meta
Normal file
2
src/CultMath/PlanetaryResidency.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0585ea15edd7f1c4fa272df4911a8d6f
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public enum PlanetaryCubeFace
|
||||
|
|
@ -104,6 +106,14 @@ public static class PlanetaryTopology
|
|||
return math.normalize(cube);
|
||||
}
|
||||
|
||||
public static float3 NormalizedCubeDirection(PlanetaryFaceCoordinate coordinate)
|
||||
{
|
||||
coordinate.Validate(allowOutsideFace: true);
|
||||
var u = (float)coordinate.U;
|
||||
var v = (float)coordinate.V;
|
||||
return math.normalize(CubeVector(coordinate.Face, u, v));
|
||||
}
|
||||
|
||||
public static PlanetaryFaceCoordinate FaceCoordinate(float3 direction)
|
||||
{
|
||||
ValidateDirection(direction);
|
||||
|
|
@ -156,6 +166,17 @@ public static class PlanetaryTopology
|
|||
MathF.Atan(cubeU) / QuarterPi,
|
||||
MathF.Atan(cubeV) / QuarterPi);
|
||||
|
||||
private static float3 CubeVector(PlanetaryCubeFace face, float u, float v) => face switch
|
||||
{
|
||||
PlanetaryCubeFace.PositiveX => new float3(1, v, -u),
|
||||
PlanetaryCubeFace.NegativeX => new float3(-1, v, u),
|
||||
PlanetaryCubeFace.PositiveY => new float3(u, 1, -v),
|
||||
PlanetaryCubeFace.NegativeY => new float3(u, -1, v),
|
||||
PlanetaryCubeFace.PositiveZ => new float3(u, v, 1),
|
||||
PlanetaryCubeFace.NegativeZ => new float3(-u, v, -1),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(face)),
|
||||
};
|
||||
|
||||
internal static void ValidateDirection(float3 direction)
|
||||
{
|
||||
ValidateFinite(direction, nameof(direction));
|
||||
|
|
|
|||
2
src/CultMath/PlanetaryTopology.cs.meta
Normal file
2
src/CultMath/PlanetaryTopology.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 88f6789c4384f7846ac4be37613fe5ed
|
||||
2
src/CultMath/SphericalErosion.cs.meta
Normal file
2
src/CultMath/SphericalErosion.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 59ec2db73065f6c4483ad90f5e17850e
|
||||
2
tests/CultMath.Tests/AdvancedErosionFilterTests.cs.meta
Normal file
2
tests/CultMath.Tests/AdvancedErosionFilterTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a7c85309318ae3f4ba75ac28a4f661cf
|
||||
2
tests/CultMath.Tests/ErosionFrequencyBandsTests.cs.meta
Normal file
2
tests/CultMath.Tests/ErosionFrequencyBandsTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 095f8ed13d2f2f74baefd23310395256
|
||||
|
|
@ -78,6 +78,8 @@ public sealed class HlslMirrorTests
|
|||
Assert.Contains("cultmath_planetary_field_sample", planetary);
|
||||
Assert.Contains("cultmath_planetary_equirectangular_forward", planetary);
|
||||
Assert.Contains("cultmath_planetary_equal_earth_forward", planetary);
|
||||
Assert.Contains("cultmath_planetary_projection_forward", planetary);
|
||||
Assert.Contains("cultmath_planetary_projection_inverse", planetary);
|
||||
Assert.Contains("CultMathPlanetarySurfaceSample", planetary);
|
||||
}
|
||||
|
||||
|
|
|
|||
2
tests/CultMath.Tests/PlanetaryFieldTests.cs.meta
Normal file
2
tests/CultMath.Tests/PlanetaryFieldTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c747cfda80df21d41a57412d8d07c08a
|
||||
25
tests/CultMath.Tests/PlanetaryGpuPageTests.cs
Normal file
25
tests/CultMath.Tests/PlanetaryGpuPageTests.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using Xunit;
|
||||
|
||||
namespace CultMath.Tests;
|
||||
|
||||
public sealed class PlanetaryGpuPageTests
|
||||
{
|
||||
[Fact]
|
||||
public void UploadContentUsesCanonicalDirectionsSpacingAndMetadata()
|
||||
{
|
||||
var layout = new PlanetaryPageLayout(new(PlanetaryCubeFace.NegativeY, 3, 4, 2), 9, 2);
|
||||
var content = PlanetaryGpuPageBuilder.BuildContent(layout, 8.4f);
|
||||
Assert.Equal(layout.StorageSize * layout.StorageSize, content.Inputs.Length);
|
||||
Assert.True(content.ParentSampleSpacing > content.SampleSpacing);
|
||||
var first = content.Inputs[0];
|
||||
Assert.Equal(PlanetaryPageSampling.Direction(layout, 0, 0), first.DirectionRadius.xyz);
|
||||
Assert.Equal(8.4f, first.DirectionRadius.w);
|
||||
Assert.Equal(content.SampleSpacing, first.Sampling.x);
|
||||
Assert.Equal(content.ParentSampleSpacing, first.Sampling.y);
|
||||
|
||||
var metadata = PlanetaryGpuPageBuilder.Metadata(content, 37, 0.25f);
|
||||
Assert.Equal(new float4((int)layout.Tile.Face, layout.Tile.Level, layout.Tile.X, layout.Tile.Y), metadata.Address);
|
||||
Assert.Equal(new float4(37, layout.StorageSize, layout.InteriorSize, layout.BorderSize), metadata.Layout);
|
||||
Assert.Equal(0.25f, metadata.State.y);
|
||||
}
|
||||
}
|
||||
2
tests/CultMath.Tests/PlanetaryGpuPageTests.cs.meta
Normal file
2
tests/CultMath.Tests/PlanetaryGpuPageTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 87266638e8010f546ab6c430a712868b
|
||||
52
tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs
Normal file
52
tests/CultMath.Tests/PlanetaryMapTileEncodingTests.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System.IO;
|
||||
using Xunit;
|
||||
|
||||
namespace CultMath.Tests;
|
||||
|
||||
public sealed class PlanetaryMapTileEncodingTests
|
||||
{
|
||||
[Fact]
|
||||
public void BinaryTileRoundTripsIdentityValidityAndSurfaceEvidence()
|
||||
{
|
||||
var field = PlanetaryFieldDefinition.Create(5, 10, 3, AdvancedErosionParameters.Default);
|
||||
var layout = new PlanetaryMapTileLayout(new(PlanetaryProjectionKind.EqualEarth), 1, 1, 0, 5, 1);
|
||||
var key = new PlanetaryMapTileKey(field.FieldVersion, 2, 17, layout, PlanetaryQueryScale.AtFootprint(0.25f, 0.1f));
|
||||
var tile = PlanetaryMapTileBaker.Bake(field, key, new FlatField());
|
||||
using var bytes = new MemoryStream();
|
||||
PlanetaryMapTileEncoding.Write(bytes, tile, leaveOpen: true);
|
||||
bytes.Position = 0;
|
||||
var decoded = PlanetaryMapTileEncoding.Read(bytes);
|
||||
|
||||
Assert.Equal(tile.Key, decoded.Key);
|
||||
Assert.Equal(tile.Validity, decoded.Validity);
|
||||
for (var i = 0; i < tile.Samples.Length; i++)
|
||||
if (tile.Validity[i]) Assert.Equal(tile.Samples[i], decoded.Samples[i]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecoderRejectsForeignMagicAndVersions()
|
||||
{
|
||||
Assert.Throws<InvalidDataException>(() => 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<InvalidDataException>(() => 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<InvalidDataException>(() => PlanetaryMapTileEncoding.Read(bytes));
|
||||
}
|
||||
|
||||
private readonly struct FlatField : IPlanetaryBaseField
|
||||
{
|
||||
public PlanetaryBaseFieldSample Sample(float3 direction) => new(0, float3.zero, 0, float3.zero, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e916ef83478d48b43ac99777f5e0f731
|
||||
2
tests/CultMath.Tests/PlanetaryPageTests.cs.meta
Normal file
2
tests/CultMath.Tests/PlanetaryPageTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7c3ae5598d3b9164a9bd3a736a22ba9a
|
||||
2
tests/CultMath.Tests/PlanetaryPatchTests.cs.meta
Normal file
2
tests/CultMath.Tests/PlanetaryPatchTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 36840ef44326f84409f850163b00e1f7
|
||||
2
tests/CultMath.Tests/PlanetaryProjectionTests.cs.meta
Normal file
2
tests/CultMath.Tests/PlanetaryProjectionTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f370036bb1663524fb4a4d47dfd50401
|
||||
2
tests/CultMath.Tests/PlanetaryQueryTests.cs.meta
Normal file
2
tests/CultMath.Tests/PlanetaryQueryTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 93cf8771fdd520b41a4e931a6566a80a
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d34c0018ee2b700448b16540eca8ebcf
|
||||
2
tests/CultMath.Tests/PlanetaryResidencyTests.cs.meta
Normal file
2
tests/CultMath.Tests/PlanetaryResidencyTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9ece3b02dbd3d7448baabee7f1fba0ab
|
||||
|
|
@ -52,4 +52,12 @@ public sealed class PlanetaryTopologyTests
|
|||
Assert.NotEqual(edge, border);
|
||||
Assert.Equal(1, math.length(border), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizedCubeBaselineSharesFaceEdges()
|
||||
{
|
||||
var a = PlanetaryTopology.NormalizedCubeDirection(new(PlanetaryCubeFace.PositiveZ, 1, -0.4));
|
||||
var b = PlanetaryTopology.NormalizedCubeDirection(new(PlanetaryCubeFace.PositiveX, -1, -0.4));
|
||||
Assert.InRange(math.distance(a, b), 0, 1.0e-6f);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
tests/CultMath.Tests/PlanetaryTopologyTests.cs.meta
Normal file
2
tests/CultMath.Tests/PlanetaryTopologyTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: be3b0ec07fe65344db889d1e98448113
|
||||
2
tests/CultMath.Tests/SphericalErosionTests.cs.meta
Normal file
2
tests/CultMath.Tests/SphericalErosionTests.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 60eb9e9e9f8ae2948b13403f5080168e
|
||||
8
tools/CultMath.PlanetaryTileFixture.meta
Normal file
8
tools/CultMath.PlanetaryTileFixture.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7197a2202273a9445b974bdd69109b1a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
unity.meta
Normal file
8
unity.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 366efeaef9f68224b80229332c7ecfc6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
unity/Runtime.meta
Normal file
8
unity/Runtime.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8226365c03c5e1a438408b7e42b98740
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
unity/Runtime/CultMath.Unity.asmdef.meta
Normal file
7
unity/Runtime/CultMath.Unity.asmdef.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d1906c397c420ae4eb222a3f09f36843
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -36,12 +36,28 @@ public readonly struct PlanetaryPageMetadataGpu
|
|||
public readonly Vector4 State;
|
||||
}
|
||||
|
||||
public readonly record struct PlanetaryPageUpload(
|
||||
PlanetaryPageInputGpu[] Inputs,
|
||||
PlanetaryPageMetadataGpu[] Metadata,
|
||||
int OutputSampleCount,
|
||||
ulong ContentVersion,
|
||||
ulong PresentationVersion);
|
||||
public readonly struct PlanetaryPageUpload
|
||||
{
|
||||
public PlanetaryPageUpload(
|
||||
PlanetaryPageInputGpu[] inputs,
|
||||
PlanetaryPageMetadataGpu[] metadata,
|
||||
int outputSampleCount,
|
||||
ulong contentVersion,
|
||||
ulong presentationVersion)
|
||||
{
|
||||
Inputs = inputs;
|
||||
Metadata = metadata;
|
||||
OutputSampleCount = outputSampleCount;
|
||||
ContentVersion = contentVersion;
|
||||
PresentationVersion = presentationVersion;
|
||||
}
|
||||
|
||||
public PlanetaryPageInputGpu[] Inputs { get; }
|
||||
public PlanetaryPageMetadataGpu[] Metadata { get; }
|
||||
public int OutputSampleCount { get; }
|
||||
public ulong ContentVersion { get; }
|
||||
public ulong PresentationVersion { get; }
|
||||
}
|
||||
|
||||
public static class PlanetaryPageUploadBuilder
|
||||
{
|
||||
|
|
@ -57,25 +73,23 @@ public static class PlanetaryPageUploadBuilder
|
|||
foreach (var (resident, pageIndex) in snapshot.Tiles.Select((resident, index) => (resident, index)))
|
||||
{
|
||||
var layout = new PlanetaryPageLayout(resident.Tile, interiorSize, borderSize).Validate();
|
||||
var spacing = PlanetaryPageSampling.NominalAngularTexelSize(layout) * radius;
|
||||
var parentSpacing = resident.Tile.Level == 0
|
||||
? 0
|
||||
: PlanetaryPageSampling.NominalAngularTexelSize(new(resident.Tile.Parent(), interiorSize, borderSize)) * radius;
|
||||
var content = PlanetaryGpuPageBuilder.BuildContent(layout, radius);
|
||||
var outputOffset = inputs.Count;
|
||||
for (var y = 0; y < layout.StorageSize; y++)
|
||||
for (var x = 0; x < layout.StorageSize; x++)
|
||||
foreach (var input in content.Inputs)
|
||||
{
|
||||
var direction = PlanetaryPageSampling.Direction(layout, x, y);
|
||||
inputs.Add(new(
|
||||
new Vector4(direction.x, direction.y, direction.z, radius),
|
||||
new Vector4(spacing, parentSpacing, 0, 0)));
|
||||
ToUnity(input.DirectionRadius),
|
||||
ToUnity(input.Sampling)));
|
||||
}
|
||||
var commonMetadata = PlanetaryGpuPageBuilder.Metadata(content, outputOffset, resident.Blend);
|
||||
metadata[pageIndex] = new(
|
||||
new Vector4((int)resident.Tile.Face, resident.Tile.Level, resident.Tile.X, resident.Tile.Y),
|
||||
new Vector4(outputOffset, layout.StorageSize, layout.InteriorSize, layout.BorderSize),
|
||||
Vector4.zero,
|
||||
new Vector4(1, resident.Blend, PlanetaryPageSampling.NominalAngularTexelSize(layout), spacing));
|
||||
ToUnity(commonMetadata.Address),
|
||||
ToUnity(commonMetadata.Layout),
|
||||
ToUnity(commonMetadata.Bounds),
|
||||
ToUnity(commonMetadata.State));
|
||||
}
|
||||
return new(inputs.ToArray(), metadata, inputs.Count, snapshot.ContentVersion, snapshot.PresentationVersion);
|
||||
}
|
||||
|
||||
private static Vector4 ToUnity(float4 value) => new(value.x, value.y, value.z, value.w);
|
||||
}
|
||||
|
|
|
|||
2
unity/Runtime/PlanetaryPageUpload.cs.meta
Normal file
2
unity/Runtime/PlanetaryPageUpload.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7edd3bb2edb2e6448a2d54fc082c27dc
|
||||
2
unity/Runtime/PlanetaryPatchMeshAdapter.cs.meta
Normal file
2
unity/Runtime/PlanetaryPatchMeshAdapter.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9ad7b581e14ef774d8435069bb77ca6a
|
||||
8
web.meta
Normal file
8
web.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: daf45471f8b5f4f44a2cb86e77e010db
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
21
web/planetary-tile.cross-runtime.test.mjs
Normal file
21
web/planetary-tile.cross-runtime.test.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { decodePlanetarySurfaceTile } from "./planetary-tile.ts";
|
||||
|
||||
if (process.argv.length !== 3) throw new Error("Expected a generated CMPT fixture path");
|
||||
const bytes = await readFile(process.argv[2]);
|
||||
const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
const tile = decodePlanetarySurfaceTile(arrayBuffer);
|
||||
assert.equal(tile.header.projectionVersion, 3);
|
||||
assert.equal(tile.header.layerId, 29);
|
||||
assert.equal(tile.header.projectionKind, 2);
|
||||
assert.equal(tile.header.interiorSize, 5);
|
||||
assert.equal(tile.header.borderSize, 1);
|
||||
assert.equal(tile.storageSize, 7);
|
||||
assert.equal(tile.samples.length, 49);
|
||||
assert.ok(tile.samples.some(sample => sample !== null));
|
||||
for (const sample of tile.samples) {
|
||||
if (sample === null) continue;
|
||||
assert.ok(Number.isFinite(sample.radialDisplacement));
|
||||
assert.ok(Number.isFinite(sample.unresolvedHeightBound));
|
||||
}
|
||||
7
web/planetary-tile.cross-runtime.test.mjs.meta
Normal file
7
web/planetary-tile.cross-runtime.test.mjs.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7bc397e5660d2b6448919c41983d35e8
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
39
web/planetary-tile.test.mjs
Normal file
39
web/planetary-tile.test.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { decodePlanetarySurfaceTile } from "./planetary-tile.ts";
|
||||
|
||||
const bytes = new ArrayBuffer(152);
|
||||
const view = new DataView(bytes);
|
||||
let offset = 0;
|
||||
const u8 = value => view.setUint8(offset++, value);
|
||||
const u32 = value => { view.setUint32(offset, value, true); offset += 4; };
|
||||
const i32 = value => { view.setInt32(offset, value, true); offset += 4; };
|
||||
const u64 = value => { view.setBigUint64(offset, value, true); offset += 8; };
|
||||
const f32 = value => { view.setFloat32(offset, value, true); offset += 4; };
|
||||
const f64 = value => { view.setFloat64(offset, value, true); offset += 8; };
|
||||
|
||||
for (const character of "CMPT") u8(character.charCodeAt(0));
|
||||
u32(1); u64(0x1234n); u32(2); u32(17); i32(0);
|
||||
f64(0); f64(0); f64(1);
|
||||
i32(0); i32(0); i32(0); i32(2); i32(0);
|
||||
f32(10); f32(0.5); i32(4);
|
||||
u8(1);
|
||||
for (const value of [1, 0, 0]) f32(value);
|
||||
f32(8.4); f32(0.25);
|
||||
for (const value of [0, 0.1, 0]) f32(value);
|
||||
for (const value of [0.99, -0.1, 0]) f32(value);
|
||||
for (const value of [0.1, 0.8, 0.2, 2.5, 0.05]) f32(value);
|
||||
u8(0); u8(0); u8(0);
|
||||
assert.equal(offset, bytes.byteLength);
|
||||
|
||||
const tile = decodePlanetarySurfaceTile(bytes);
|
||||
assert.equal(tile.header.fieldVersion, 0x1234n);
|
||||
assert.equal(tile.header.layerId, 17);
|
||||
assert.equal(tile.storageSize, 2);
|
||||
assert.equal(tile.samples.length, 4);
|
||||
assert.equal(tile.samples[0]?.radialDisplacement, 0.25);
|
||||
assert.equal(tile.samples[1], null);
|
||||
|
||||
assert.throws(() => decodePlanetarySurfaceTile(bytes.slice(0, bytes.byteLength - 1)), /Truncated/);
|
||||
const trailing = new Uint8Array(bytes.byteLength + 1);
|
||||
trailing.set(new Uint8Array(bytes));
|
||||
assert.throws(() => decodePlanetarySurfaceTile(trailing.buffer), /trailing/);
|
||||
7
web/planetary-tile.test.mjs.meta
Normal file
7
web/planetary-tile.test.mjs.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 04c93ecab90d465499aa3d60722e6fc3
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
89
web/planetary-tile.ts
Normal file
89
web/planetary-tile.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
export const CULTMATH_PLANETARY_TILE_VERSION = 1;
|
||||
|
||||
export interface PlanetaryTileHeader {
|
||||
fieldVersion: bigint;
|
||||
projectionVersion: number;
|
||||
layerId: number;
|
||||
projectionKind: number;
|
||||
centerLongitude: number;
|
||||
centerLatitude: number;
|
||||
projectionScale: number;
|
||||
level: number;
|
||||
x: number;
|
||||
y: number;
|
||||
interiorSize: number;
|
||||
borderSize: number;
|
||||
footprintMeters: number;
|
||||
maximumUnresolvedHeight: number;
|
||||
}
|
||||
|
||||
export interface PlanetaryTileSample {
|
||||
direction: readonly [number, number, number];
|
||||
radius: number;
|
||||
radialDisplacement: number;
|
||||
tangentGradient: readonly [number, number, number];
|
||||
surfaceNormal: readonly [number, number, number];
|
||||
slope: number;
|
||||
ridge: number;
|
||||
gully: number;
|
||||
finestResolvedWavelength: number;
|
||||
unresolvedHeightBound: number;
|
||||
}
|
||||
|
||||
export interface PlanetarySurfaceTile {
|
||||
header: PlanetaryTileHeader;
|
||||
storageSize: number;
|
||||
samples: readonly (PlanetaryTileSample | null)[];
|
||||
}
|
||||
|
||||
export function decodePlanetarySurfaceTile(bytes: ArrayBuffer): PlanetarySurfaceTile {
|
||||
const view = new DataView(bytes);
|
||||
let offset = 0;
|
||||
const u8 = () => view.getUint8(offset++);
|
||||
const u32 = () => { const value = view.getUint32(offset, true); offset += 4; return value; };
|
||||
const i32 = () => { const value = view.getInt32(offset, true); offset += 4; return value; };
|
||||
const f32 = () => { const value = view.getFloat32(offset, true); offset += 4; return value; };
|
||||
const f64 = () => { const value = view.getFloat64(offset, true); offset += 8; return value; };
|
||||
const u64 = () => { const value = view.getBigUint64(offset, true); offset += 8; return value; };
|
||||
const vector3 = (): [number, number, number] => [f32(), f32(), f32()];
|
||||
const requireRemaining = (count: number) => {
|
||||
if (offset + count > view.byteLength) throw new Error("Truncated CultMath planetary tile");
|
||||
};
|
||||
|
||||
requireRemaining(8);
|
||||
if (String.fromCharCode(u8(), u8(), u8(), u8()) !== "CMPT") throw new Error("Not a CultMath planetary tile");
|
||||
const version = u32();
|
||||
if (version !== CULTMATH_PLANETARY_TILE_VERSION) throw new Error(`Unsupported CultMath planetary tile version ${version}`);
|
||||
requireRemaining(76);
|
||||
const header: PlanetaryTileHeader = {
|
||||
fieldVersion: u64(),
|
||||
projectionVersion: u32(),
|
||||
layerId: u32(),
|
||||
projectionKind: i32(),
|
||||
centerLongitude: f64(),
|
||||
centerLatitude: f64(),
|
||||
projectionScale: f64(),
|
||||
level: i32(), x: i32(), y: i32(), interiorSize: i32(), borderSize: i32(),
|
||||
footprintMeters: f32(), maximumUnresolvedHeight: f32(),
|
||||
};
|
||||
const count = i32();
|
||||
const storageSize = header.interiorSize + header.borderSize * 2;
|
||||
if (count !== storageSize * storageSize) throw new Error("CultMath planetary tile sample count does not match layout");
|
||||
const samples: (PlanetaryTileSample | null)[] = new Array(count);
|
||||
for (let index = 0; index < count; index++) {
|
||||
requireRemaining(1);
|
||||
if (u8() === 0) { samples[index] = null; continue; }
|
||||
requireRemaining(64);
|
||||
samples[index] = {
|
||||
direction: vector3(),
|
||||
radius: f32(),
|
||||
radialDisplacement: f32(),
|
||||
tangentGradient: vector3(),
|
||||
surfaceNormal: vector3(),
|
||||
slope: f32(), ridge: f32(), gully: f32(),
|
||||
finestResolvedWavelength: f32(), unresolvedHeightBound: f32(),
|
||||
};
|
||||
}
|
||||
if (offset !== view.byteLength) throw new Error("CultMath planetary tile has trailing bytes");
|
||||
return { header, storageSize, samples };
|
||||
}
|
||||
7
web/planetary-tile.ts.meta
Normal file
7
web/planetary-tile.ts.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f284e64628320844196b7aaea76475a4
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in a new issue