diff --git a/GameData/RealSolarSystem/RSSKopernicus/Earth/Earth.cfg b/GameData/RealSolarSystem/RSSKopernicus/Earth/Earth.cfg
index 65a07432..1d905ab7 100644
--- a/GameData/RealSolarSystem/RSSKopernicus/Earth/Earth.cfg
+++ b/GameData/RealSolarSystem/RSSKopernicus/Earth/Earth.cfg
@@ -447,7 +447,8 @@
{
minOffset = -15
maxOffset = 15
- slopeScale = 8
+ coastSpacings = 2
+ gradientStencil = 1
order = 105
}
VertexHeightNoiseVertHeight
diff --git a/Source/GUI.cs b/Source/GUI.cs
index 4e28760a..54991bcb 100644
--- a/Source/GUI.cs
+++ b/Source/GUI.cs
@@ -37,6 +37,8 @@ public class RealSolarSystemEditor : MonoBehaviour
private string _sMinHeightOffset;
private string _sMaxHeightOffset;
private string _sSlopeScale;
+ private string _sCoastSpacings;
+ private string _sGradientStencil;
private string _sRssDefineOrder;
private string _sHeightStart;
@@ -228,6 +230,8 @@ private void ShowGUI(int windowID)
_sMinHeightOffset = _pModRssDefine.minHeightOffset.ToString();
_sMaxHeightOffset = _pModRssDefine.maxHeightOffset.ToString();
_sSlopeScale = _pModRssDefine.slopeScale.ToString();
+ _sCoastSpacings = _pModRssDefine.coastSpacings.ToString();
+ _sGradientStencil = _pModRssDefine.gradientStencil.ToString();
_sRssDefineOrder = _pModRssDefine.order.ToString();
}
@@ -387,6 +391,24 @@ private void ShowGUI(int windowID)
_pModRssDefine.slopeScale = val;
}
+ GUILayout.BeginHorizontal();
+ GUILayout.Label("coastSpacings: ");
+ GUILayout.EndHorizontal();
+ _sCoastSpacings = GUILayout.TextField(_sCoastSpacings);
+ if (double.TryParse(_sCoastSpacings, out double coastSpacings))
+ {
+ _pModRssDefine.coastSpacings = coastSpacings;
+ }
+
+ GUILayout.BeginHorizontal();
+ GUILayout.Label("gradientStencil (texels): ");
+ GUILayout.EndHorizontal();
+ _sGradientStencil = GUILayout.TextField(_sGradientStencil);
+ if (double.TryParse(_sGradientStencil, out double gradientStencil))
+ {
+ _pModRssDefine.gradientStencil = gradientStencil;
+ }
+
GUILayout.BeginHorizontal();
GUILayout.Label("Mod order: ");
GUILayout.EndHorizontal();
diff --git a/Source/KopernicusMods/PQSMod_VertexDefineCoastSmooth.cs b/Source/KopernicusMods/PQSMod_VertexDefineCoastSmooth.cs
index d9e185b2..292002c0 100644
--- a/Source/KopernicusMods/PQSMod_VertexDefineCoastSmooth.cs
+++ b/Source/KopernicusMods/PQSMod_VertexDefineCoastSmooth.cs
@@ -4,6 +4,7 @@
*/
using System;
+using UnityEngine;
namespace RealSolarSystem
{
@@ -14,16 +15,81 @@ public class PQSMod_VertexDefineCoastSmooth : PQSMod
{
public double minHeightOffset;
public double maxHeightOffset;
+
+ // Legacy fixed slopeScale behaviour. Has no effect in adaptive mode.
+ // This is unavoidably either too soft on flat coasts or steep enough to quantise every vertex onto the two
+ // plateaus - and once that happens the waterline snaps to the vertex grid's edge midpoints
+ // and turns into a staircase.
public double slopeScale;
+ // Target half-width of the land/water transition, in vertex spacings of the quad being
+ // built. When positive the mod runs in adaptive mode: the ramp is sized from the local
+ // height map gradient so that the coast always crosses sea level over roughly this
+ // distance, whatever the terrain does. Zero (the default) keeps the fixed slopeScale behaviour.
+ //
+ // Spacings rather than metres because a quad's vertex spacing doubles for every subdivision
+ // level below the maximum, and the maximum itself moves with the terrain detail preset. A
+ // width fixed in metres is only correct in a shell around the camera.
+ public double coastSpacings;
+
+ // Half-width of the central difference used to measure that gradient, in height map texels.
+ public double gradientStencil;
+
private double minHeight;
private double maxHeight;
+ private double invDepth;
+ private double invRise;
+ private bool bandCrossesSeaLevel;
+
+ private bool isAdaptive;
+ private MapSO heightMap;
+ private double mapDeformity;
+ private double du;
+ private double dv;
+ private double invTwoDu;
+ private double invTwoDv;
+ private double metresPerU;
+ private double metresPerV;
+ private double[] levelSpacing;
+ private double maxLevelSpacing;
+
+ // Resolved adaptive setup. Exposed so the Burst path can mirror this mod exactly instead of
+ // repeating the height map search and re-deriving the constants - the two implementations
+ // have to agree vertex for vertex or terrain changes depending on whether BurstPQS is
+ // installed. Only meaningful while IsAdaptive is true.
+ public bool IsAdaptive => isAdaptive;
+ public MapSO AdaptiveHeightMap => heightMap;
+ public double AdaptiveMapDeformity => mapDeformity;
+ public double AdaptiveStencilU => du;
+ public double AdaptiveStencilV => dv;
+ public double AdaptiveMetresPerU => metresPerU;
+ public double AdaptiveMetresPerV => metresPerV;
+
+ ///
+ /// Half-width of the ramp in metres of ground for a quad at the given subdivision level.
+ /// The Burst path resolves this once per quad rather than per vertex, since it is handed
+ /// the quad up front.
+ ///
+ public double AdaptiveRampWidth(int subdivision)
+ {
+ if (levelSpacing == null)
+ {
+ return 0.0;
+ }
+
+ double spacing = subdivision >= 0 && subdivision < levelSpacing.Length
+ ? levelSpacing[subdivision]
+ : maxLevelSpacing;
+ return coastSpacings * spacing;
+ }
private void Reset()
{
minHeightOffset = -1.0;
maxHeightOffset = 1.0;
slopeScale = 1.0;
+ coastSpacings = 0.0;
+ gradientStencil = 1.0;
}
public override void OnSetup()
@@ -31,18 +97,100 @@ public override void OnSetup()
requirements = PQS.ModiferRequirements.MeshCustomNormals;
minHeight = sphere.radius + minHeightOffset;
maxHeight = sphere.radius + maxHeightOffset;
+ isAdaptive = false;
+
+ // The two halves of the band are scaled independently, so the offsets need not be
+ // symmetric. Dropping the seabed further than the land rises steepens the waterline
+ // without turning coastal lowland into a mesa - the deep side is hidden under the ocean.
+ bandCrossesSeaLevel = minHeightOffset < 0.0 && maxHeightOffset > 0.0;
+ if (!bandCrossesSeaLevel)
+ {
+ Debug.LogWarning($"[RealSolarSystem] VertexDefineCoastSmooth on {sphere.name} is inactive: band [{minHeightOffset}, {maxHeightOffset}] does not cross sea level");
+ return;
+ }
+
+ invDepth = -1.0 / minHeightOffset;
+ invRise = 1.0 / maxHeightOffset;
+
+ if (coastSpacings > 0.0)
+ {
+ isAdaptive = BindHeightMap();
+ if (isAdaptive)
+ {
+ requirements |= PQS.ModiferRequirements.VertexMapCoords;
+ BuildSpacingTable();
+ }
+ }
+
+ // The rendered waterline sits where the mesh crosses sea level, and with different
+ // amplitudes either side the crossing is pulled towards the shallower one - a constant
+ // bias of tens of metres that no ramp width fixes. Keep the band symmetric.
+ if (isAdaptive && Math.Abs(maxHeightOffset + minHeightOffset) > 1E-6)
+ {
+ Debug.LogWarning($"[RealSolarSystem] VertexDefineCoastSmooth on {sphere.name}:"
+ + $" asymmetric band [{minHeightOffset}, {maxHeightOffset}] displaces the waterline"
+ + " off the height map contour; prefer equal offsets");
+ }
}
public override void OnVertexBuildHeight(PQS.VertexBuildData data)
{
- if (data.vertHeight > minHeight && data.vertHeight < maxHeight)
+ if (!bandCrossesSeaLevel)
+ {
+ return;
+ }
+
+ if (data.vertHeight <= minHeight || data.vertHeight >= maxHeight)
{
- // 7th order polynomial smoothstep.
- double x = (data.vertHeight - minHeight) / (maxHeight - minHeight);
- x = Math.Min(Math.Max(0.0, (x - 0.5) * slopeScale + 0.5), 1.0); // No Math.clamp?
- double y = -20.0 * Math.Pow(x, 7.0) + 70 * Math.Pow(x, 6.0) - 84.0 * Math.Pow(x, 5.0) + 35.0 * Math.Pow(x, 4.0);
- data.vertHeight = y * (maxHeight - minHeight) + minHeight;
+ return;
+ }
+
+ double height = data.vertHeight - sphere.radius;
+
+ // Signed position within the band: sea level at 0, the two band edges at -1 and 1.
+ double t;
+ if (isAdaptive)
+ {
+ // Grade the coast over a fixed number of vertex spacings of whatever quad is being
+ // built, so distant low-detail quads get a proportionally wider ramp instead of
+ // collapsing onto the plateaus. buildQuad is null on the GetSurfaceHeight path,
+ // which has no mesh to grade, so answer those at the finest level.
+ double spacing = maxLevelSpacing;
+ if (data.buildQuad != null)
+ {
+ int level = data.buildQuad.subdivision;
+ if (level >= 0 && level < levelSpacing.Length)
+ {
+ spacing = levelSpacing[level];
+ }
+ }
+
+ // Raw height the terrain gains over that distance of ground. Capping it at the band
+ // keeps the ramp finishing exactly on the plateau, with no step at the edge.
+ // Grouped so this is the same product the Burst path folds into its per-quad ramp
+ // width; multiplication is not associative in floating point and the two
+ // implementations have to agree to the last bit.
+ double window = GetLocalGradient(data) * (coastSpacings * spacing);
+ window = Math.Min(window, height < 0.0 ? -minHeightOffset : maxHeightOffset);
+ // Not Math.Sign, which throws on NaN - and a NaN height slips through the band test
+ // above, since every comparison against NaN is false.
+ t = window > 0.0 ? height / window : (height < 0.0 ? -1.0 : (height > 0.0 ? 1.0 : 0.0));
}
+ else
+ {
+ // slopeScale below 1 leaves a step at the band edges, same as it always has.
+ t = (height < 0.0 ? height * invDepth : height * invRise) * slopeScale;
+ }
+ t = Math.Min(Math.Max(-1.0, t), 1.0);
+
+ // Odd extension of the 7th order smoothstep onto [-1, 1], i.e. 2 * S((t + 1) / 2) - 1.
+ // Sea level is an exact fixed point of this, so the waterline stays on the height
+ // map's own contour instead of drifting.
+ double x = (t + 1.0) * 0.5;
+ double x2 = x * x;
+ double s = 2.0 * (x2 * x2 * (35.0 - 84.0 * x + 70.0 * x2 - 20.0 * x2 * x)) - 1.0;
+
+ data.vertHeight = sphere.radius + (s < 0.0 ? -s * minHeightOffset : s * maxHeightOffset);
}
public override double GetVertexMaxHeight()
@@ -54,5 +202,94 @@ public override double GetVertexMinHeight()
{
return minHeightOffset;
}
+
+ ///
+ /// Magnitude of the height map's slope at this vertex, in metres of rise per metre travelled.
+ ///
+ private double GetLocalGradient(PQS.VertexBuildData data)
+ {
+ // GetPixelFloat wraps both axes, which is right for longitude but would jump across the
+ // pole in v, so keep the stencil inside the map vertically.
+ double v = Math.Min(Math.Max(data.v, dv), 1.0 - dv);
+
+ double dHdu = mapDeformity * invTwoDu *
+ (heightMap.GetPixelFloat(data.u + du, v) - heightMap.GetPixelFloat(data.u - du, v));
+ double dHdv = mapDeformity * invTwoDv *
+ (heightMap.GetPixelFloat(data.u, v + dv) - heightMap.GetPixelFloat(data.u, v - dv));
+
+ // u spans the full circumference, v spans pole to pole; meridians converge with latitude.
+ // directionFromCenter is a unit radial, so the length of its horizontal part is exactly cos(latitude)
+ Vector3d dir = data.directionFromCenter;
+ double cosLat = Math.Sqrt(dir.x * dir.x + dir.z * dir.z);
+ if (cosLat < 1E-3)
+ {
+ cosLat = 1E-3;
+ }
+
+ double gu = dHdu / (metresPerU * cosLat);
+ double gv = dHdv / metresPerV;
+ return Math.Sqrt(gu * gu + gv * gv);
+ }
+
+ ///
+ /// Vertex spacing for every subdivision level. A quad at level L spans (pi * R / 2) / 2^L
+ /// and carries cacheSideVertCount vertices per side, so spacing doubles for each level
+ /// below the maximum - which is exactly how far a coast has to be graded to stay smooth.
+ ///
+ private void BuildSpacingTable()
+ {
+ int intervals = PQS.cacheSideVertCount > 1 ? PQS.cacheSideVertCount - 1 : 1;
+ double rootEdge = Math.PI * sphere.radius * 0.5;
+
+ levelSpacing = new double[sphere.maxLevel + 1];
+ for (int level = 0; level <= sphere.maxLevel; level++)
+ {
+ levelSpacing[level] = rootEdge / (1 << level) / intervals;
+ }
+ maxLevelSpacing = levelSpacing[sphere.maxLevel];
+ }
+
+ ///
+ /// Locates the VertexHeightMap this body's coastline comes from, so the ramp can be sized
+ /// from the map's own gradient. Sampling the map rather than differencing the mesh keeps the
+ /// measured gradient independent of subdivision level, so the waterline itself stays put as
+ /// quads split - only the steepness either side of it changes.
+ ///
+ private bool BindHeightMap()
+ {
+ PQSMod_VertexHeightMap best = null;
+ foreach (PQSMod_VertexHeightMap mod in sphere.GetComponentsInChildren(true))
+ {
+ // Mirror the filtering PQS itself applies when it assembles the mod list.
+ if (mod.heightMap == null || !mod.modEnabled || !mod.gameObject.activeSelf)
+ {
+ continue;
+ }
+ if (best == null || mod.order < best.order)
+ {
+ best = mod;
+ }
+ }
+
+ if (best == null)
+ {
+ Debug.LogWarning($"[RealSolarSystem] VertexDefineCoastSmooth on {sphere.name}: coastWidth is set but no VertexHeightMap was found, falling back to fixed slopeScale");
+ return false;
+ }
+
+ heightMap = best.heightMap;
+ // The stock mod adds heightMapOffset + heightMapDeformity * pixel; the offset is constant
+ // and drops out of a difference, so only the deformity scales the gradient.
+ mapDeformity = best.heightMapDeformity;
+
+ double stencil = gradientStencil > 0.0 ? gradientStencil : 1.0;
+ du = stencil / heightMap.Width;
+ dv = stencil / heightMap.Height;
+ invTwoDu = 1.0 / (2.0 * du);
+ invTwoDv = 1.0 / (2.0 * dv);
+ metresPerU = 2.0 * Math.PI * sphere.radius;
+ metresPerV = Math.PI * sphere.radius;
+ return true;
+ }
}
}
diff --git a/Source/KopernicusMods/VertexDefineCoastSmooth.cs b/Source/KopernicusMods/VertexDefineCoastSmooth.cs
index 7230495b..a3a9f0c6 100644
--- a/Source/KopernicusMods/VertexDefineCoastSmooth.cs
+++ b/Source/KopernicusMods/VertexDefineCoastSmooth.cs
@@ -34,5 +34,22 @@ public NumericParser slopeScale
get { return Mod.slopeScale; }
set { Mod.slopeScale = value; }
}
+
+ // Target half-width of the transition, in vertex spacings. Positive enables adaptive mode,
+ // which sizes the ramp from the height map gradient and ignores slopeScale.
+ [ParserTarget("coastSpacings")]
+ public NumericParser coastSpacings
+ {
+ get { return Mod.coastSpacings; }
+ set { Mod.coastSpacings = value; }
+ }
+
+ // Central difference half-width used to measure that gradient, in height map texels.
+ [ParserTarget("gradientStencil")]
+ public NumericParser gradientStencil
+ {
+ get { return Mod.gradientStencil; }
+ set { Mod.gradientStencil = value; }
+ }
}
}
\ No newline at end of file
diff --git a/Source/RealSolarSystem.BurstPQS/BatchPQSMod_VertexDefineCoastSmooth.cs b/Source/RealSolarSystem.BurstPQS/BatchPQSMod_VertexDefineCoastSmooth.cs
index c35468b1..8ba32efc 100644
--- a/Source/RealSolarSystem.BurstPQS/BatchPQSMod_VertexDefineCoastSmooth.cs
+++ b/Source/RealSolarSystem.BurstPQS/BatchPQSMod_VertexDefineCoastSmooth.cs
@@ -1,52 +1,187 @@
-using System;
+using System;
using BurstPQS;
-using BurstPQS.Util;
+using BurstPQS.Map;
using RealSolarSystem;
using Unity.Burst;
+using UnityEngine;
[BurstCompile]
[BatchPQSMod(typeof(PQSMod_VertexDefineCoastSmooth))]
public class BatchPQSMod_VertexDefineCoastSmooth : BatchPQSMod
{
+ private bool _mapSupported;
+
public BatchPQSMod_VertexDefineCoastSmooth(PQSMod_VertexDefineCoastSmooth mod) : base(mod)
{
}
+ public override void OnSetup()
+ {
+ base.OnSetup();
+
+ // The stock mod has already resolved which height map the coastline comes from; all this
+ // has to decide is whether BurstPQS can read it. If it cannot, fall back to the fixed
+ // slopeScale ramp and say so, because silently dropping adaptive mode here would make the
+ // terrain depend on whether BurstPQS happens to be installed.
+ _mapSupported = Mod.IsAdaptive
+ && Mod.AdaptiveHeightMap != null
+ && BurstMapSO.IsSupported(Mod.AdaptiveHeightMap);
+
+ if (Mod.IsAdaptive && !_mapSupported)
+ {
+ Debug.LogWarning("[RealSolarSystem] VertexDefineCoastSmooth: height map"
+ + $" {Mod.AdaptiveHeightMap?.GetType().Name ?? "(none)"} is not supported by BurstPQS,"
+ + " falling back to fixed slopeScale");
+ }
+ }
+
public override void OnQuadPreBuild(PQ quad, BatchPQSJobSet jobSet)
{
base.OnQuadPreBuild(quad, jobSet);
- jobSet.Add(new BuildJob
+
+ double minHeightOffset = Mod.minHeightOffset;
+ double maxHeightOffset = Mod.maxHeightOffset;
+ if (minHeightOffset >= 0.0 || maxHeightOffset <= 0.0)
{
- minHeightOffset = Mod.minHeightOffset,
- maxHeightOffset = Mod.maxHeightOffset,
+ // Band does not straddle sea level; the stock mod logs this and goes inert.
+ return;
+ }
+
+ BuildJob job = new BuildJob
+ {
+ minHeightOffset = minHeightOffset,
+ maxHeightOffset = maxHeightOffset,
+ invDepth = -1.0 / minHeightOffset,
+ invRise = 1.0 / maxHeightOffset,
slopeScale = Mod.slopeScale,
- });
+ };
+
+ if (_mapSupported)
+ {
+ double du = Mod.AdaptiveStencilU;
+ double dv = Mod.AdaptiveStencilV;
+
+ job.isAdaptive = true;
+ job.heightMap = BurstMapSO.Create(Mod.AdaptiveHeightMap);
+ job.mapDeformity = Mod.AdaptiveMapDeformity;
+ job.du = du;
+ job.dv = dv;
+ job.invTwoDu = 1.0 / (2.0 * du);
+ job.invTwoDv = 1.0 / (2.0 * dv);
+ job.metresPerU = Mod.AdaptiveMetresPerU;
+ job.metresPerV = Mod.AdaptiveMetresPerV;
+
+ // The subdivision level is known here, so the ramp width is resolved once per quad
+ // instead of once per vertex the way the stock path has to do it.
+ job.rampWidth = Mod.AdaptiveRampWidth(quad.subdivision);
+ }
+
+ jobSet.Add(job);
}
[BurstCompile]
- struct BuildJob : IBatchPQSHeightJob
+ struct BuildJob : IBatchPQSHeightJob, IDisposable
{
public double minHeightOffset;
public double maxHeightOffset;
+ public double invDepth;
+ public double invRise;
public double slopeScale;
+ public bool isAdaptive;
+ public BurstMapSO heightMap;
+ public double mapDeformity;
+ public double du;
+ public double dv;
+ public double invTwoDu;
+ public double invTwoDv;
+ public double metresPerU;
+ public double metresPerV;
+ public double rampWidth;
+
public void BuildHeights(in BuildHeightsData data)
{
- var minHeight = data.sphere.radius + minHeightOffset;
- var maxHeight = data.sphere.radius + maxHeightOffset;
+ double radius = data.sphere.radius;
+ double minHeight = radius + minHeightOffset;
+ double maxHeight = radius + maxHeightOffset;
+
+ // Each of these properties returns the span by value, so hoist them out of the loop.
+ var vertHeight = data.vertHeight;
+ var uCoord = data.u;
+ var vCoord = data.v;
+ var direction = data.directionFromCenter;
for (int i = 0; i < data.VertexCount; ++i)
{
- if (data.vertHeight[i] > minHeight && data.vertHeight[i] < maxHeight)
+ double height = vertHeight[i];
+ if (height <= minHeight || height >= maxHeight)
{
- // 7th order polynomial smoothstep.
- double x = (data.vertHeight[i] - minHeight) / (maxHeight - minHeight);
- x = MathUtil.Clamp01((x - 0.5) * slopeScale + 0.5);
- double y = -20.0 * Math.Pow(x, 7.0) + 70 * Math.Pow(x, 6.0) - 84.0 * Math.Pow(x, 5.0) +
- 35.0 * Math.Pow(x, 4.0);
- data.vertHeight[i] = y * (maxHeight - minHeight) + minHeight;
+ continue;
}
+
+ height -= radius;
+
+ // Signed position within the band: sea level at 0, the two band edges at -1 and 1.
+ double t;
+ if (isAdaptive)
+ {
+ // Raw height the terrain gains over rampWidth metres of ground. Capping it at
+ // the band keeps the ramp finishing exactly on the plateau, with no step.
+ double window = Gradient(uCoord[i], vCoord[i], direction[i]) * rampWidth;
+ window = Math.Min(window, height < 0.0 ? -minHeightOffset : maxHeightOffset);
+ t = window > 0.0 ? height / window : (height < 0.0 ? -1.0 : (height > 0.0 ? 1.0 : 0.0));
+ }
+ else
+ {
+ t = (height < 0.0 ? height * invDepth : height * invRise) * slopeScale;
+ }
+ t = Math.Min(Math.Max(-1.0, t), 1.0);
+
+ // Odd extension of the 7th order smoothstep onto [-1, 1], i.e. 2 * S((t + 1) / 2) - 1.
+ // Sea level is an exact fixed point of this, so the waterline stays on the height
+ // map's own contour instead of drifting.
+ double x = (t + 1.0) * 0.5;
+ double x2 = x * x;
+ double s = 2.0 * (x2 * x2 * (35.0 - 84.0 * x + 70.0 * x2 - 20.0 * x2 * x)) - 1.0;
+
+ vertHeight[i] = radius + (s < 0.0 ? -s * minHeightOffset : s * maxHeightOffset);
+ }
+ }
+
+ ///
+ /// Magnitude of the height map's slope at this vertex, in metres of rise per metre
+ /// travelled. Must stay in step with PQSMod_VertexDefineCoastSmooth.LocalGradient.
+ ///
+ private double Gradient(double u, double v, Vector3d dir)
+ {
+ // GetPixelFloat wraps both axes, which is right for longitude but would jump across the
+ // pole in v, so keep the stencil inside the map vertically.
+ v = Math.Min(Math.Max(v, dv), 1.0 - dv);
+
+ double dHdu = mapDeformity * invTwoDu *
+ (heightMap.GetPixelFloat(u + du, v) - heightMap.GetPixelFloat(u - du, v));
+ double dHdv = mapDeformity * invTwoDv *
+ (heightMap.GetPixelFloat(u, v + dv) - heightMap.GetPixelFloat(u, v - dv));
+
+ // directionFromCenter is a unit radial, so the length of its horizontal part is exactly
+ // cos(latitude), which is cheaper than Math.Cos and avoids needing the latitude span.
+ double cosLat = Math.Sqrt(dir.x * dir.x + dir.z * dir.z);
+ if (cosLat < 1E-3)
+ {
+ cosLat = 1E-3;
+ }
+
+ double gu = dHdu / (metresPerU * cosLat);
+ double gv = dHdv / metresPerV;
+ return Math.Sqrt(gu * gu + gv * gv);
+ }
+
+ public void Dispose()
+ {
+ if (isAdaptive)
+ {
+ heightMap.Dispose();
}
}
}
-}
\ No newline at end of file
+}