bättre ringworld
This commit is contained in:
@@ -13,16 +13,16 @@ public sealed class KnownSolarSystemTests
|
||||
private static KnownSolarSystemCatalog Load() => KnownSolarSystemCatalog.Load(CatalogPath);
|
||||
|
||||
[Fact]
|
||||
public void SolarSystemContainsSunAndEightPlanetsInOrder()
|
||||
public void SolarSystemContainsSunEightPlanetsAndRingHabitat()
|
||||
{
|
||||
var sol = Load().GetById("sol");
|
||||
Assert.Equal(new SectorCoordinate(0, 0, 0), sol.Coordinate);
|
||||
Assert.Equal("sun", Assert.Single(sol.Stars).Id);
|
||||
Assert.Equal(new[] { "mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune" },
|
||||
sol.Planets.Where(p => p.ParentPlanetIndex is null).Select(p => p.Id));
|
||||
sol.Planets.Where(p => p.ParentPlanetIndex is null && !p.IsRingWorld).Select(p => p.Id));
|
||||
Assert.Equal(sol.Planets.Count, sol.Planets.Select(p => p.Id).Distinct().Count());
|
||||
Assert.Equal(sol.Planets.Where(p => p.ParentPlanetIndex is null).OrderBy(p => p.OrbitRadiusMeters),
|
||||
sol.Planets.Where(p => p.ParentPlanetIndex is null));
|
||||
Assert.Equal(sol.Planets.Where(p => p.ParentPlanetIndex is null && !p.IsRingWorld).OrderBy(p => p.OrbitRadiusMeters),
|
||||
sol.Planets.Where(p => p.ParentPlanetIndex is null && !p.IsRingWorld));
|
||||
Assert.NotNull(sol.Planets.Single(p => p.Id == "saturn").Rings);
|
||||
var uranus = sol.Planets.Single(p => p.Id == "uranus");
|
||||
Assert.InRange(System.Math.Abs(uranus.Orientation.ToWorld(new(0, 1, 0)).Y), 0, 0.2);
|
||||
|
||||
114
tests/Universe.Tests/RingWorldSurfaceTests.cs
Normal file
114
tests/Universe.Tests/RingWorldSurfaceTests.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.Tests;
|
||||
|
||||
public sealed class RingWorldSurfaceTests
|
||||
{
|
||||
private static RingWorldSettings Settings => new()
|
||||
{
|
||||
DiameterMeters = 512_000 / System.Math.PI, WidthMeters = 64_000,
|
||||
ProceduralSurface = new() { MetersPerTexel = 1000, MaxTextureSize = 1024,
|
||||
ContinentScaleMeters = 20_000, BiomeScaleMeters = 12_000, MountainScaleMeters = 5000 }
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void ResolutionUsesPhysicalDimensionsAndBoundsMemoryWithoutStretching()
|
||||
{
|
||||
var normal = new RingWorldSettings { ProceduralSurface = new() };
|
||||
Assert.Equal((15708, 500), RingWorldSurface.Resolution(normal));
|
||||
foreach (var ring in new[] { normal, normal with { DiameterMeters = 80_000_000 },
|
||||
normal with { WidthMeters = 8_000_000 }, normal with { ProceduralSurface = new() { MetersPerTexel = .1, MaxTextureSize = 4096 } } })
|
||||
{
|
||||
var (width, height) = RingWorldSurface.Resolution(ring);
|
||||
Assert.InRange((long)width * height, 4, RingWorldSurface.MaximumTexels);
|
||||
Assert.InRange(width, 2, ring.ProceduralSurface!.MaxTextureSize);
|
||||
Assert.InRange(height, 2, ring.ProceduralSurface.MaxTextureSize);
|
||||
var ratio = (ring.DiameterMeters * System.Math.PI / width) / (ring.WidthMeters / height);
|
||||
Assert.InRange(ratio, .97, 1.03);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeedDeterminesColorHeightAndWaterIncludingHighBits()
|
||||
{
|
||||
var a = RingWorldSurface.Generate(Settings, 110, 0, 8000);
|
||||
var same = RingWorldSurface.Generate(Settings, 110, 0, 8000);
|
||||
var other = RingWorldSurface.Generate(Settings, 110UL | (1UL << 63), 0, 8000);
|
||||
Assert.Equal(a.Color.Rgba, same.Color.Rgba);
|
||||
Assert.Equal(a.Height.Samples, same.Height.Samples);
|
||||
Assert.Equal(a.OceanMask.Samples, same.OceanMask.Samples);
|
||||
Assert.False(a.Color.Rgba.SequenceEqual(other.Color.Rgba));
|
||||
Assert.False(a.Height.Samples.SequenceEqual(other.Height.Samples));
|
||||
Assert.False(a.OceanMask.Samples.SequenceEqual(other.OceanMask.Samples));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WiderRingExtendsGeographyInsteadOfStretchingExistingFeatures()
|
||||
{
|
||||
var ring = Settings with { ProceduralSurface = Settings.ProceduralSurface! with { OceanCoverage = 0 } };
|
||||
var a = RingWorldSurface.Generate(ring, 110, 0, 8000);
|
||||
var b = RingWorldSurface.Generate(ring with { WidthMeters = ring.WidthMeters * 2 }, 110, 0, 8000);
|
||||
Assert.Equal(a.Color.Width, b.Color.Width); Assert.Equal(a.Color.Height * 2, b.Color.Height);
|
||||
var offset = a.Color.Height / 2 * a.Color.Width;
|
||||
Assert.Equal(a.Color.Rgba, b.Color.Rgba.Skip(offset * 4).Take(a.Color.Rgba.Length));
|
||||
Assert.Equal(a.Height.Samples, b.Height.Samples.Skip(offset).Take(a.Height.Samples.Length));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WaterMountainsAndBiomesAgreeAndWrapContinuously()
|
||||
{
|
||||
var maps = RingWorldSurface.Generate(Settings, 110, 0, 8000);
|
||||
var width = maps.Color.Width; var height = maps.Color.Height;
|
||||
var water = 0; var green = 0; var dry = 0; var mountains = 0;
|
||||
for (var i = 0; i < maps.Height.Samples.Length; i++)
|
||||
{
|
||||
var r = maps.Color.Rgba[i * 4]; var g = maps.Color.Rgba[i * 4 + 1]; var b = maps.Color.Rgba[i * 4 + 2];
|
||||
var wet = maps.OceanMask.Samples[i]; var h = maps.Height.Samples[i];
|
||||
if (wet > 0) { water++; Assert.Equal(0, h); Assert.True(b > r); }
|
||||
else if (h > .5) mountains++;
|
||||
else if (g > r * 1.3 && g > b * 1.5) green++;
|
||||
else if (r > b * 1.4 && g > b * 1.3) dry++;
|
||||
}
|
||||
Assert.InRange((double)water / (width * height), .53, .57);
|
||||
Assert.True(green > 100 && dry > 100 && mountains > 50, $"green={green}, dry={dry}, mountains={mountains}");
|
||||
double seam = 0, adjacent = 0;
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
var v = (y + .5) / height;
|
||||
Assert.InRange(System.Math.Abs(maps.Height.SampleUv(-1e-10, v) - maps.Height.SampleUv(1e-10, v)), 0, 1e-6);
|
||||
Assert.InRange(System.Math.Abs(maps.OceanMask.SampleUv(-1e-10, v) - maps.OceanMask.SampleUv(1e-10, v)), 0, 1e-6);
|
||||
seam += System.Math.Abs(maps.Height.Samples[y * width] - maps.Height.Samples[(y + 1) * width - 1]);
|
||||
adjacent += System.Math.Abs(maps.Height.Samples[y * width] - maps.Height.Samples[y * width + 1]);
|
||||
}
|
||||
Assert.InRange(seam / adjacent, .3, 3);
|
||||
// CPU collision uses precisely these same generated maps, including the sloping seabed.
|
||||
var planet = new PlanetData(0, "Test", DVector3.Zero, Settings.RadiusMeters, 0, 20_000,
|
||||
System.Numerics.Vector3.One, System.Numerics.Vector3.One, 0, 0, 0, 110)
|
||||
{
|
||||
RingWorld = Settings, Terrain = new() { HeightMap = maps.Height, HeightmapMaxMeters = 8000 },
|
||||
Ocean = new() { DepthMeters = 4000 }, OceanMask = maps.OceanMask
|
||||
};
|
||||
for (var y = 0; y < height; y += 7) for (var x = 0; x < width; x += 13)
|
||||
{
|
||||
var angle = (.5 - (x + .5) / width) * System.Math.Tau;
|
||||
var point = new DVector3(System.Math.Cos(angle) * planet.RadiusMeters,
|
||||
(.5 - (y + .5) / height) * Settings.WidthMeters, System.Math.Sin(angle) * planet.RadiusMeters);
|
||||
var index = y * width + x;
|
||||
var expected = maps.Height.Samples[index] * 8000d * (1 - maps.OceanMask.Samples[index]) - 4000d * maps.OceanMask.Samples[index];
|
||||
Assert.InRange(System.Math.Abs(RingWorldGeometry.Elevation(planet, point) - expected), 0, 1e-7);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidSettingsAreRejectedAndDisablingOceanProducesDryLand()
|
||||
{
|
||||
foreach (var invalid in new[] { new RingWorldSurfaceSettings { MetersPerTexel = 0 },
|
||||
new() { MaxTextureSize = 32768 }, new() { OceanCoverage = double.NaN },
|
||||
new() { MountainScaleMeters = -1 }, new() { BiomeScaleMeters = double.PositiveInfinity } })
|
||||
Assert.Throws<InvalidDataException>(invalid.Validate);
|
||||
Assert.Throws<InvalidDataException>(() => RingWorldSurface.Generate(Settings, 1, 100, 8000, seaLevel: 0));
|
||||
var dry = RingWorldSurface.Generate(Settings, 110, 0, 8000, seaLevel: null);
|
||||
Assert.All(dry.OceanMask.Samples, value => Assert.Equal(0, value));
|
||||
}
|
||||
}
|
||||
333
tests/Universe.Tests/RingWorldTests.cs
Normal file
333
tests/Universe.Tests/RingWorldTests.cs
Normal file
@@ -0,0 +1,333 @@
|
||||
using System.Numerics;
|
||||
using Universe.App.Runtime;
|
||||
using Universe.Core.Configuration;
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
using Universe.Core.Physics;
|
||||
using Universe.Core.World;
|
||||
|
||||
namespace Universe.Tests;
|
||||
|
||||
public sealed class RingWorldTests
|
||||
{
|
||||
private const double Cell = 1e10;
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void IdlePlayerStaysAtSameBodyPositionOnSlopesAtUniverseScale(bool moving)
|
||||
{
|
||||
var cell = UniverseScale.DefaultCellSizeMeters;
|
||||
var planet = Ring() with { TerrainAmplitudeMeters = 120, Terrain = new() { NoiseFrequency = 24 },
|
||||
Orientation = new(35, 73), Rotation = new(86400, 35, 73),
|
||||
Orbit = new(1.7e11, 0, 0, 365 * 86400, new(1, 0, 0), new(0, 0, 1)) };
|
||||
var basis = new StarSystemData(new(0, 0, 0), 1, [new(0, "Sun", DVector3.Zero, 7e8, 1, Vector3.One, 1)], [planet]);
|
||||
var world = new UniverseNeighborhood(CelestialSimulation.Evaluate(basis, 0), []);
|
||||
planet = world.ActiveSystem.Planets[0];
|
||||
var center = UniversePosition.AtCellCenter(world.ActiveSystem.Coordinate, cell);
|
||||
var body = center.Add(planet.PositionMeters, cell);
|
||||
var ground = RingWorldGeometry.Point(planet, .73, 25_123);
|
||||
var up = -RingWorldGeometry.Radial(ground);
|
||||
var player = new PlayerController(body.Add(planet.Orientation.ToWorld(ground + up * 1.7), cell),
|
||||
planet.Orientation.ToWorld(new(0, 1, 0)), planet.Orientation.ToWorld(up), cell, world);
|
||||
for (var i = 0; i < 120; i++) player.Update(1d / 60, 0, 0, false, false, world);
|
||||
Assert.True(player.Grounded);
|
||||
var original = planet.Orientation.ToBody(player.Position.RelativeTo(body, cell));
|
||||
double maximumDrift = 0;
|
||||
for (var i = 1; i <= 3600; i++)
|
||||
{
|
||||
if (moving)
|
||||
{
|
||||
var next = new UniverseNeighborhood(CelestialSimulation.Evaluate(basis, i / 60d), []);
|
||||
player.ApplyCelestialMotion(world, next); world = next;
|
||||
}
|
||||
player.Update(1d / 60, 0, 0, false, false, world);
|
||||
planet = world.ActiveSystem.Planets[0]; body = center.Add(planet.PositionMeters, cell);
|
||||
var local = planet.Orientation.ToBody(player.Position.RelativeTo(body, cell));
|
||||
maximumDrift = System.Math.Max(maximumDrift, (local - original).Length);
|
||||
}
|
||||
Assert.True(maximumDrift < .005, $"Idle drift after 60 seconds (moving={moving}): {maximumDrift:F6} metres.");
|
||||
}
|
||||
private static PlanetData Ring(double radius = 2_500_000) => new(0, "Ring", DVector3.Zero, radius, 0,
|
||||
radius * .088, Vector3.One, Vector3.UnitZ, 0, 0, 0, 110)
|
||||
{
|
||||
Id = "ring-world", SurfaceType = PlanetSurfaceType.Terrestrial,
|
||||
RingWorld = new() { DiameterMeters = radius * 2, WidthMeters = radius * .2, ThicknessMeters = radius * .02, WallThicknessMeters = radius * .002 }
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void CatalogLoadsDimensionsAndProceduralGeographyWithEarthAtmosphere()
|
||||
{
|
||||
var catalog = KnownSolarSystemCatalog.Load(Path.Combine(AppContext.BaseDirectory, "KnownSolarSystem", "solar-systems.json"));
|
||||
var system = catalog.GetById("sol");
|
||||
var earth = system.Planets.Single(p => p.Id == "earth"); var ring = system.Planets.Single(p => p.IsRingWorld);
|
||||
Assert.Equal(5_000_000, ring.RingWorld!.DiameterMeters);
|
||||
Assert.Equal(500_000, ring.RingWorld.WidthMeters); Assert.Equal(50_000, ring.RingWorld.ThicknessMeters);
|
||||
Assert.NotSame(earth.SurfaceTexture, ring.SurfaceTexture); Assert.NotSame(earth.Terrain.HeightMap, ring.Terrain.HeightMap);
|
||||
Assert.Null(ring.SurfaceTexture!.SourcePath); Assert.Null(ring.Terrain.HeightMap!.SourcePath);
|
||||
Assert.Equal((15708, 500), (ring.SurfaceTexture.Width, ring.SurfaceTexture.Height));
|
||||
Assert.Equal(ring.SurfaceTexture.Width, ring.OceanMask!.Width);
|
||||
Assert.Equal(ring.SurfaceTexture.Height, ring.Terrain.HeightMap.Height);
|
||||
Assert.Null(ring.CloudDensityMap); // Clouds use physical 3D noise rather than a stretched Earth map.
|
||||
var landing = FlyCamera.RingInteriorPoint(system, ring);
|
||||
Assert.True(RingWorldGeometry.Elevation(ring, landing) > ring.Ocean!.SeaLevelMeters + 1);
|
||||
Assert.Equal(earth.AtmosphereHeightMeters, ring.AtmosphereHeightMeters);
|
||||
Assert.Equal(earth.AtmosphereColor, ring.AtmosphereColor);
|
||||
Assert.True(ring.Ocean!.Enabled && ring.Clouds!.Enabled && ring.Vegetation!.Enabled);
|
||||
var config = EngineConfiguration.Load(Path.Combine(AppContext.BaseDirectory, "config.json"));
|
||||
Assert.Equal(ring.Id, config.Startup.PlanetHotkeys[8]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DimensionsMustFitAtmosphereTerrainAndHull()
|
||||
{
|
||||
var settings = new RingWorldSettings(); var terrain = new TerrainDefinition();
|
||||
settings.Validate(2_500_000, 220_000, terrain, null);
|
||||
foreach (var invalid in new[] { settings with { WidthMeters = 0 }, settings with { DiameterMeters = double.NaN },
|
||||
settings with { GravityMetersPerSecondSquared = -1 }, settings with { ThicknessMeters = double.PositiveInfinity } })
|
||||
Assert.Throws<InvalidDataException>(() => invalid.Validate(2_500_000, 220_000, terrain, null));
|
||||
Assert.Throws<InvalidDataException>(() => settings.Validate(1, 220_000, terrain, null));
|
||||
Assert.Throws<InvalidDataException>(() => settings.Validate(2_500_000, 2_500_000, terrain, null));
|
||||
Assert.Throws<InvalidDataException>(() => settings.Validate(2_500_000, 220_000, terrain, new() { DepthMeters = 60_000 }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CollisionNormalsFollowFloorAndWallsWhileGravityStaysCylindricalAfterRotation()
|
||||
{
|
||||
var ring = Ring() with { Orientation = new(37, 121) };
|
||||
foreach (var angle in new[] { 0d, 1, 2, 3, 4, 5, 6 })
|
||||
{
|
||||
var point = RingWorldGeometry.Point(ring, angle, 0, 30);
|
||||
var contact = PlanetSurface.Contact(ring, ring.Orientation.ToWorld(point));
|
||||
Assert.InRange(contact.Clearance, 29.99, 30.01);
|
||||
Assert.InRange(DVector3.Dot(contact.Up, ring.Orientation.ToWorld(-RingWorldGeometry.Radial(point))), .99999, 1.000001);
|
||||
Assert.InRange((PlanetSurface.GravityUp(ring, ring.Orientation.ToWorld(point))
|
||||
- ring.Orientation.ToWorld(-RingWorldGeometry.Radial(point))).Length, 0, 1e-12);
|
||||
}
|
||||
var wall = new DVector3(ring.RadiusMeters - 1000, ring.RingWorld!.WidthMeters * .5 - 10, 0);
|
||||
var nearWall = PlanetSurface.Contact(ring, ring.Orientation.ToWorld(wall));
|
||||
Assert.InRange(nearWall.Clearance, 9.999, 10.001);
|
||||
Assert.InRange(DVector3.Dot(nearWall.Up, ring.Orientation.ToWorld(new(0, -1, 0))), .99999, 1.00001);
|
||||
Assert.InRange((PlanetSurface.GravityUp(ring, ring.Orientation.ToWorld(wall))
|
||||
- ring.Orientation.ToWorld(new(-1, 0, 0))).Length, 0, 1e-12);
|
||||
var top = new DVector3(ring.RadiusMeters - ring.AtmosphereHeightMeters - 10, ring.RingWorld.WidthMeters * .5 + 100, 0);
|
||||
Assert.InRange(RingWorldGeometry.Clearance(ring, top), 9.999, 10.001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SweepsHitThinHullAndWallsButPassThroughOpenCenter()
|
||||
{
|
||||
var ring = Ring(); var origin = UniversePosition.AtCellCenter(new(0, 0, 0), Cell);
|
||||
CollisionResult Sweep(DVector3 a, DVector3 b) => PlanetCollisionSolver.Sweep(origin.Add(a, Cell), origin.Add(b, Cell), origin, [ring], Cell, 1.7);
|
||||
Assert.False(Sweep(new(0, -1e8, 0), new(0, 1e8, 0)).Collided);
|
||||
var inner = Sweep(new(0, 0, 0), new(1e8, 0, 0));
|
||||
Assert.True(inner.Collided); Assert.InRange(inner.Position.RelativeTo(origin, Cell).X, ring.RadiusMeters - 1.72, ring.RadiusMeters - 1.69);
|
||||
var outside = Sweep(new(1e8, 0, 0), new(-1e8, 0, 0));
|
||||
Assert.True(outside.Collided);
|
||||
Assert.InRange(outside.Position.RelativeTo(origin, Cell).X, ring.RadiusMeters + 50_001.69, ring.RadiusMeters + 50_001.72);
|
||||
var wall = Sweep(new(ring.RadiusMeters - 1000, 0, 0), new(ring.RadiusMeters - 1000, 1e8, 0));
|
||||
Assert.True(wall.Collided); Assert.InRange(wall.Position.RelativeTo(origin, Cell).Y, 249_998.28, 249_998.31);
|
||||
Assert.True(Sweep(new(ring.RadiusMeters - 1.7, 0, 0), new(-1e8, 0, 0)).Collided);
|
||||
var grazing = Sweep(new(ring.RadiusMeters - 1.71, 0, 0), new(ring.RadiusMeters - 1.71, 0, 1e8));
|
||||
Assert.True(grazing.Collided);
|
||||
Assert.InRange(PlanetSurface.Distance(ring, grazing.Position.RelativeTo(origin, Cell)), 1.69, 1.72);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TerrainAndTextureWrapAndMeshNormalsPointInward()
|
||||
{
|
||||
var ring = Ring() with { TerrainAmplitudeMeters = 120, Terrain = new() { NoiseFrequency = 3 } };
|
||||
var a = RingWorldGeometry.Point(ring, 0, 1234); var b = RingWorldGeometry.Point(ring, System.Math.Tau, 1234);
|
||||
Assert.InRange((a - b).Length, 0, 1e-7);
|
||||
Assert.InRange((RingWorldGeometry.TextureDirection(ring, a) - RingWorldGeometry.TextureDirection(ring, b)).Length, 0, 1e-12);
|
||||
var patch = TerrainPatchBuilder.Build(ring, new(0, 2, 1, 1));
|
||||
for (var i = 0; i < 25 * 25; i++)
|
||||
{
|
||||
var offset = i * 9;
|
||||
var p = patch.Origin + new DVector3(patch.Vertices[offset], patch.Vertices[offset + 1], patch.Vertices[offset + 2]);
|
||||
var n = new DVector3(patch.Vertices[offset + 3], patch.Vertices[offset + 4], patch.Vertices[offset + 5]);
|
||||
Assert.True(DVector3.Dot(n, -RingWorldGeometry.Radial(p)) > .9);
|
||||
Assert.InRange(System.Math.Abs(RingWorldGeometry.Clearance(ring, p)), 0, .1);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoarseTerrainSkirtsStayInsideTheMetalHull()
|
||||
{
|
||||
var ring = Ring();
|
||||
for (var face = 0; face < 6; face++)
|
||||
{
|
||||
var patch = TerrainPatchBuilder.Build(ring, new(face, 0, 0, 0));
|
||||
for (var i = 25 * 25 * 9; i < patch.Vertices.Length; i += 9)
|
||||
{
|
||||
var p = patch.Origin + new DVector3(patch.Vertices[i], patch.Vertices[i + 1], patch.Vertices[i + 2]);
|
||||
Assert.True(new DVector3(p.X, 0, p.Z).Length < ring.RadiusMeters + ring.RingWorld!.ThicknessMeters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayerFallsJumpsCrouchesAndWalksEntireInnerCircumference()
|
||||
{
|
||||
var ring = Ring(100); var system = new StarSystemData(new(0, 0, 0), 1, [], [ring]);
|
||||
var world = new UniverseNeighborhood(system, []); var center = UniversePosition.AtCellCenter(system.Coordinate, Cell);
|
||||
var player = new PlayerController(center.Add(new(94, 0, 0), Cell), new(0, 0, 1), new(-1, 0, 0), Cell, world);
|
||||
for (var i = 0; i < 240; i++) player.Update(1d / 60, 0, 0, false, false, world);
|
||||
Assert.True(player.Grounded);
|
||||
Assert.InRange(PlanetSurface.Distance(ring, player.Position.RelativeTo(center, Cell)), 1.69, 1.72);
|
||||
var turns = 0d; var last = 0d;
|
||||
for (var i = 0; i < 9000; i++)
|
||||
{
|
||||
player.Update(1d / 60, 1, 0, false, false, world);
|
||||
var p = player.Position.RelativeTo(center, Cell); var angle = System.Math.Atan2(p.Z, p.X);
|
||||
turns += System.Math.Atan2(System.Math.Sin(angle - last), System.Math.Cos(angle - last)); last = angle;
|
||||
Assert.InRange(PlanetSurface.Distance(ring, p), 1.69, 1.72);
|
||||
Assert.True(DVector3.Dot(player.Up, -RingWorldGeometry.Radial(p)) > .9999);
|
||||
}
|
||||
Assert.True(turns > System.Math.Tau);
|
||||
player.Update(1d / 60, 0, 0, true, false, world);
|
||||
Assert.False(player.Grounded);
|
||||
for (var i = 0; i < 20; i++) player.Update(1d / 60, 0, 0, false, false, world);
|
||||
Assert.True(PlanetSurface.Distance(ring, player.Position.RelativeTo(center, Cell)) > 2);
|
||||
for (var i = 0; i < 180; i++) player.Update(1d / 60, 0, 0, false, false, world);
|
||||
Assert.True(player.Grounded);
|
||||
player.Update(1d / 60, 0, 0, false, true, world);
|
||||
Assert.InRange(PlanetSurface.Distance(ring, player.Position.RelativeTo(center, Cell)), .99, 1.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SurfaceFlightEntersThroughOpenCenterAndReachesInnerTerrain()
|
||||
{
|
||||
var planet = Ring() with { Orientation = new(35, 20) };
|
||||
var star = new StarData(0, "Sun", new(1e11, 1e11, 0), 7e8, 1, Vector3.One, 1);
|
||||
var system = new StarSystemData(new(0, 0, 0), 1, [star], [planet]);
|
||||
var world = new UniverseNeighborhood(system, []); var body = UniversePosition.AtCellCenter(system.Coordinate, Cell);
|
||||
var overview = planet.Orientation.ToWorld(new DVector3(1, 1.35, .65).Normalized() * planet.RadiusMeters * 3);
|
||||
var camera = new FlyCamera(body.Add(overview, Cell), Cell);
|
||||
camera.RequestSurfaceView([system]);
|
||||
for (var i = 0; i < 400 && camera.IsFocusing; i++) camera.Update(.05, DVector3.Zero, world);
|
||||
Assert.False(camera.IsFocusing);
|
||||
var target = planet.Orientation.ToWorld(FlyCamera.RingInteriorPoint(system, planet));
|
||||
Assert.InRange((camera.Position.RelativeTo(body, Cell) - target).Length, 0, .001);
|
||||
Assert.InRange(PlanetSurface.Distance(planet, camera.Position.RelativeTo(body, Cell)), 999.99, 1000.01);
|
||||
Assert.True(DVector3.Dot(camera.Up, planet.Orientation.ToWorld(-RingWorldGeometry.Radial(planet.Orientation.ToBody(target)))) > .98);
|
||||
camera.RequestFocus([system], planet.Id);
|
||||
for (var i = 0; i < 400 && camera.IsFocusing; i++) camera.Update(.05, DVector3.Zero, world);
|
||||
Assert.False(camera.IsFocusing);
|
||||
Assert.True(camera.Position.RelativeTo(body, Cell).Length > planet.MaximumTerrainRadiusMeters * 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VegetationIsDeterministicFacesInwardAndDoesNotGrowInWater()
|
||||
{
|
||||
var planet = Ring() with { Vegetation = new() { DensityMultiplier = 1 },
|
||||
SurfaceTexture = new(2, 2, Enumerable.Repeat(new byte[] { 45, 80, 35, 255 }, 4).SelectMany(p => p).ToArray()) };
|
||||
var catalog = new VegetationCatalog
|
||||
{
|
||||
Biomes = [new("forest", [45, 80, 35], 24, "forest"), new("dry", [145, 150, 80], 18, "dry"),
|
||||
new("sand", [205, 181, 120], 15, "sand"), new("snow", [240, 242, 245], 12, "snow")],
|
||||
Plants = [new() { Id = "tree", Model = "tree.glb", Type = VegetationType.Tree, MatchColorRgb = [45, 80, 35], MinimumSpacingMeters = 2 }]
|
||||
};
|
||||
var generator = new VegetationTileGenerator(new(planet, catalog));
|
||||
var keys = generator.Around(new(planet.RadiusMeters - 1.7, 0, 0), 0, 300).ToArray();
|
||||
Assert.Contains(keys, k => k.X == 0); Assert.Contains(keys, k => k.X > 60_000);
|
||||
var tiles = keys.Select(k => generator.Generate(k)).ToArray();
|
||||
var instances = tiles.SelectMany(t => t.Instances).ToArray(); Assert.NotEmpty(instances);
|
||||
foreach (var tile in tiles) Assert.Equal(tile.Instances, generator.Generate(tile.Key).Instances);
|
||||
foreach (var instance in instances)
|
||||
{
|
||||
Assert.True(DVector3.Dot(instance.Up, -RingWorldGeometry.Radial(instance.BodyPosition)) > .99999);
|
||||
Assert.InRange(RingWorldGeometry.Clearance(planet, instance.BodyPosition), -.221, -.219);
|
||||
}
|
||||
var wet = new VegetationTileGenerator(new(planet with { OceanMask = new(2, 2, [1, 1, 1, 1]) }, catalog));
|
||||
foreach (var key in keys) Assert.Empty(wet.Generate(key).Instances);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopedGroundContactKeepsEyeHeightAboveTheSameRenderedSurface()
|
||||
{
|
||||
var planet = Ring(10_000) with { Terrain = new()
|
||||
{ HeightMap = new(2, 2, [0, 0, 1, 1]), HeightmapMinMeters = 0, HeightmapMaxMeters = 500 } };
|
||||
var ground = RingWorldGeometry.Point(planet, .37, 350);
|
||||
var normal = RingWorldGeometry.Contact(planet, ground).Up;
|
||||
var contact = RingWorldGeometry.Contact(planet, ground + normal * 1.7);
|
||||
Assert.InRange(contact.Clearance, 1.68, 1.72);
|
||||
Assert.InRange(System.Math.Abs(RingWorldGeometry.Clearance(planet, contact.Point)), 0, .001);
|
||||
var system = new StarSystemData(new(0, 0, 0), 1, [], [planet]); var world = new UniverseNeighborhood(system, []);
|
||||
var center = UniversePosition.AtCellCenter(system.Coordinate, Cell);
|
||||
var player = new PlayerController(center.Add(ground + normal * 1.7, Cell), new(0, 1, 0), normal, Cell, world);
|
||||
for (var i = 0; i < 120; i++) player.Update(1d / 60, .1, 0, false, false, world);
|
||||
Assert.True(player.Grounded);
|
||||
Assert.InRange(PlanetSurface.Distance(planet, player.Position.RelativeTo(center, Cell)), 1.67, 1.73);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 350)]
|
||||
[InlineData(2.1, -350)]
|
||||
[InlineData(6.28, 150)]
|
||||
public void SlopesDoNotTiltThePlayerWhileStandingWalkingCrouchingOrJumping(double angle, double across)
|
||||
{
|
||||
var planet = Ring(10_000) with { Orientation = new(37, 121, 19), Terrain = new()
|
||||
{ HeightMap = new(2, 2, [0, 0, 1, 1]), HeightmapMinMeters = 0, HeightmapMaxMeters = 500 } };
|
||||
var ground = RingWorldGeometry.Point(planet, angle, across);
|
||||
var normal = RingWorldGeometry.Contact(planet, ground).Up;
|
||||
var local = ground + normal * 1.7;
|
||||
var system = new StarSystemData(new(0, 0, 0), 1, [], [planet]);
|
||||
var world = new UniverseNeighborhood(system, []); var center = UniversePosition.AtCellCenter(system.Coordinate, Cell);
|
||||
var player = new PlayerController(center.Add(planet.Orientation.ToWorld(local), Cell),
|
||||
planet.Orientation.ToWorld(new(0, 1, 0)), planet.Orientation.ToWorld(normal), Cell, world);
|
||||
void AssertUpright()
|
||||
{
|
||||
var p = planet.Orientation.ToBody(player.Position.RelativeTo(center, Cell));
|
||||
var up = planet.Orientation.ToBody(player.Up);
|
||||
Assert.InRange((up + RingWorldGeometry.Radial(p)).Length, 0, 1e-9);
|
||||
// A widthwise slope must not introduce camera pitch or roll toward either side wall.
|
||||
Assert.InRange(System.Math.Abs(up.Y), 0, 1e-10);
|
||||
Assert.InRange(System.Math.Abs(DVector3.Dot(player.Forward, player.Up)), 0, 1e-9);
|
||||
}
|
||||
Assert.True(System.Math.Abs(normal.Y) > .2); // This is a real slope, not flat ground.
|
||||
AssertUpright();
|
||||
for (var i = 0; i < 60; i++) { player.Update(1d / 60, 0, 0, false, false, world); AssertUpright(); }
|
||||
Assert.True(player.Grounded);
|
||||
var beforeWalk = player.Position.RelativeTo(center, Cell);
|
||||
for (var i = 0; i < 120; i++) { player.Update(1d / 60, .5, 0, false, false, world); AssertUpright(); }
|
||||
Assert.True(player.Grounded);
|
||||
Assert.True((player.Position.RelativeTo(center, Cell) - beforeWalk).Length > 3);
|
||||
player.Update(1d / 60, 0, 0, false, true, world); AssertUpright();
|
||||
Assert.True(player.Crouching);
|
||||
Assert.InRange(PlanetSurface.Distance(planet, player.Position.RelativeTo(center, Cell)), .98, 1.02);
|
||||
player.Update(1d / 60, 0, 0, false, false, world);
|
||||
var jumpStart = planet.Orientation.ToBody(player.Position.RelativeTo(center, Cell));
|
||||
player.Update(1d / 60, 0, 0, true, false, world); AssertUpright();
|
||||
Assert.False(player.Grounded);
|
||||
for (var i = 0; i < 20; i++) { player.Update(1d / 60, 0, 0, false, false, world); AssertUpright(); }
|
||||
var airborne = planet.Orientation.ToBody(player.Position.RelativeTo(center, Cell));
|
||||
// Cell coordinates are billions of metres; allow sub-millimetre roundoff when
|
||||
// transforming the world-space position back to the tilted body's width axis.
|
||||
Assert.InRange(System.Math.Abs(airborne.Y - jumpStart.Y), 0, 1e-4);
|
||||
Assert.True(new DVector3(airborne.X, 0, airborne.Z).Length < new DVector3(jumpStart.X, 0, jumpStart.Z).Length - .5);
|
||||
for (var i = 0; i < 180; i++) { player.Update(1d / 60, 0, 0, false, false, world); AssertUpright(); }
|
||||
Assert.True(player.Grounded);
|
||||
Assert.InRange(PlanetSurface.Distance(planet, player.Position.RelativeTo(center, Cell)), 1.67, 1.73);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NearbySideWallDoesNotTiltGroundOrPullThePlayerAcrossTheRing()
|
||||
{
|
||||
var planet = Ring() with { Orientation = new(35, 73) };
|
||||
var system = new StarSystemData(new(0, 0, 0), 1, [], [planet]); var world = new UniverseNeighborhood(system, []);
|
||||
var center = UniversePosition.AtCellCenter(system.Coordinate, Cell);
|
||||
var start = new DVector3(planet.RadiusMeters - 1000, planet.RingWorld!.WidthMeters * .5 - 1.7, 0);
|
||||
var player = new PlayerController(center.Add(planet.Orientation.ToWorld(start), Cell),
|
||||
planet.Orientation.ToWorld(new(0, 0, 1)), planet.Orientation.ToWorld(new(0, -1, 0)), Cell, world);
|
||||
Assert.InRange((planet.Orientation.ToBody(player.Up) - new DVector3(-1, 0, 0)).Length, 0, 1e-10);
|
||||
for (var i = 0; i < 120; i++)
|
||||
{
|
||||
player.Update(1d / 60, 0, 0, false, false, world);
|
||||
Assert.False(player.Grounded);
|
||||
Assert.InRange(System.Math.Abs(planet.Orientation.ToBody(player.Up).Y), 0, 1e-10);
|
||||
}
|
||||
var end = planet.Orientation.ToBody(player.Position.RelativeTo(center, Cell));
|
||||
Assert.True(end.X > start.X + 10); // Falls toward the deck, not toward the wall.
|
||||
Assert.InRange(System.Math.Abs(end.Y - start.Y), 0, .02); // Only collision separation is permitted.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user