init
This commit is contained in:
191
src/Universe.App/Graphics/AdaptivePlanetMesh.cs
Normal file
191
src/Universe.App/Graphics/AdaptivePlanetMesh.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
/// <summary>Builds tiles on worker threads. A parent remains visible until all four children are ready.</summary>
|
||||
internal sealed class AdaptivePlanetMesh : IDisposable
|
||||
{
|
||||
private static readonly SemaphoreSlim Workers = new(System.Math.Clamp(Environment.ProcessorCount - 2, 1, 4));
|
||||
private readonly GL _gl;
|
||||
private readonly PlanetData _planet;
|
||||
private readonly Dictionary<TerrainPatchKey, Tile> _tiles = [];
|
||||
private readonly CancellationTokenSource _cancel = new();
|
||||
private int _frame;
|
||||
private int _uploads;
|
||||
private int _pending;
|
||||
private DVector3 _forward;
|
||||
private DVector3 _right;
|
||||
private DVector3 _up;
|
||||
private double _aspect;
|
||||
private double _tanHalfFov;
|
||||
private DVector3 _cameraDirection;
|
||||
private double _horizonAngle;
|
||||
private double _localSurfaceRadius;
|
||||
private double _innerRadius;
|
||||
private double _nearSurfaceDistance;
|
||||
public int DrawnTiles { get; private set; }
|
||||
public int DeepestLevel { get; private set; }
|
||||
public int PendingTiles => _pending;
|
||||
public int NearestLevel { get; private set; }
|
||||
public double NearestDistance { get; private set; }
|
||||
|
||||
public AdaptivePlanetMesh(GL gl, PlanetData planet)
|
||||
{
|
||||
_gl = gl;
|
||||
_planet = planet;
|
||||
for (var face = 0; face < 6; face++)
|
||||
{
|
||||
var key = new TerrainPatchKey(face, 0, 0, 0);
|
||||
var data = TerrainPatchBuilder.Build(planet, key);
|
||||
_tiles[key] = new() { Data = data, Mesh = new(gl, 9, data.Vertices, data.Indices) };
|
||||
}
|
||||
}
|
||||
|
||||
public void Draw(DVector3 cameraLocal, DVector3 forward, DVector3 right, DVector3 up, int screenHeight, double aspect, double tanHalfFov, Action<DVector3, GpuMesh> draw)
|
||||
{
|
||||
_forward = forward; _right = right; _up = up; _aspect = aspect; _tanHalfFov = tanHalfFov;
|
||||
_cameraDirection = cameraLocal.Normalized();
|
||||
_localSurfaceRadius = TerrainHeight.Sample(_planet, _cameraDirection);
|
||||
var altitude = System.Math.Abs(cameraLocal.Length - _localSurfaceRadius);
|
||||
_nearSurfaceDistance = altitude < 1000 ? altitude + 20 : 0;
|
||||
_innerRadius = TerrainVisibility.InnerRadius(_planet);
|
||||
_horizonAngle = TerrainVisibility.HorizonAngle(cameraLocal, _innerRadius, _planet.MaximumTerrainRadiusMeters);
|
||||
_frame++;
|
||||
_uploads = 0;
|
||||
foreach (var tile in _tiles.Values)
|
||||
{
|
||||
if (tile.Task is not { IsCompleted: true }) continue;
|
||||
tile.Data = tile.Task.GetAwaiter().GetResult();
|
||||
tile.Task = null;
|
||||
_pending--;
|
||||
}
|
||||
DrawnTiles = 0;
|
||||
DeepestLevel = 0;
|
||||
NearestLevel = 0; NearestDistance = double.PositiveInfinity;
|
||||
Span<TerrainPatchKey> roots = stackalloc TerrainPatchKey[6];
|
||||
for (var face = 0; face < roots.Length; face++) roots[face] = new(face, 0, 0, 0);
|
||||
SortNearest(roots, cameraLocal);
|
||||
foreach (var root in roots) Visit(root, cameraLocal, screenHeight, draw);
|
||||
if (_frame % 120 != 0) return;
|
||||
foreach (var pair in _tiles.Where(pair => pair.Key.Level > 0 && pair.Value.Task is null && _frame - pair.Value.LastFrame > 360).ToArray())
|
||||
{
|
||||
pair.Value.Mesh?.Dispose();
|
||||
_tiles.Remove(pair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private void Visit(TerrainPatchKey key, DVector3 camera, int screenHeight, Action<DVector3, GpuMesh> draw)
|
||||
{
|
||||
if (!Needed(key, camera)) return;
|
||||
var radius = _planet.RadiusMeters;
|
||||
var tile = _tiles[key];
|
||||
tile.LastFrame = _frame;
|
||||
var distance = Distance(key, camera);
|
||||
var spacingPixels = radius * key.Width / TerrainPatchBuilder.Segments / System.Math.Max(distance, 1) * screenHeight / (2 * _tanHalfFov);
|
||||
if (spacingPixels > TerrainVisibility.PixelTarget(distance) && key.Level < 19)
|
||||
{
|
||||
Span<TerrainPatchKey> buffer = stackalloc TerrainPatchKey[4];
|
||||
var childCount = 0;
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var child = key.Child(i);
|
||||
if (Needed(child, camera)) buffer[childCount++] = child;
|
||||
}
|
||||
var children = buffer[..childCount];
|
||||
SortNearest(children, camera);
|
||||
var ready = true;
|
||||
foreach (var child in children) ready &= EnsureReady(child);
|
||||
if (ready)
|
||||
{
|
||||
foreach (var child in children) Visit(child, camera, screenHeight, draw);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (GeometryVisible(tile.Data!, camera))
|
||||
{
|
||||
DrawnTiles++;
|
||||
DeepestLevel = System.Math.Max(DeepestLevel, key.Level);
|
||||
if (distance < NearestDistance) { NearestDistance = distance; NearestLevel = key.Level; }
|
||||
draw(tile.Data!.Origin, tile.Mesh!);
|
||||
}
|
||||
}
|
||||
|
||||
private double Distance(TerrainPatchKey key, DVector3 camera) => TerrainVisibility.DistanceToPatch(key, camera, _localSurfaceRadius);
|
||||
private bool Needed(TerrainPatchKey key, DVector3 camera)
|
||||
=> Visible(key, camera) || (_nearSurfaceDistance > 0 && Distance(key, camera) <= _nearSurfaceDistance);
|
||||
|
||||
private void SortNearest(Span<TerrainPatchKey> keys, DVector3 camera)
|
||||
{
|
||||
for (var i = 1; i < keys.Length; i++)
|
||||
{
|
||||
var key = keys[i]; var distance = Distance(key, camera); var j = i;
|
||||
while (j > 0 && Distance(keys[j - 1], camera) > distance) { keys[j] = keys[j - 1]; j--; }
|
||||
keys[j] = key;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Visible(TerrainPatchKey key, DVector3 camera)
|
||||
{
|
||||
var radius = _planet.RadiusMeters;
|
||||
var outer = _planet.MaximumTerrainRadiusMeters;
|
||||
// Include the recessed skirt, but keep its uncertainty radial instead of expanding
|
||||
// the tile by the planet's full mountain height in every horizontal direction.
|
||||
var spacing = radius * key.Width / TerrainPatchBuilder.Segments;
|
||||
var skirt = spacing * 2 + System.Math.Min(_planet.TerrainAmplitudeMeters * .012, spacing * 2);
|
||||
var lower = System.Math.Max(0, _innerRadius - skirt);
|
||||
if (TerrainVisibility.OutsideFrustum(key, camera, _forward, _right, _up, _tanHalfFov, _aspect, lower, outer)) return false;
|
||||
return !TerrainVisibility.BehindHorizon(key, _cameraDirection, _horizonAngle);
|
||||
}
|
||||
|
||||
private bool GeometryVisible(TerrainPatchData data, DVector3 camera)
|
||||
{
|
||||
var toPatch = data.BoundsCenter - camera;
|
||||
var z = DVector3.Dot(toPatch, _forward);
|
||||
var bound = data.BoundsRadius;
|
||||
var horizontalTan = _tanHalfFov * _aspect;
|
||||
return z >= -bound && System.Math.Abs(DVector3.Dot(toPatch, _up)) <= z * _tanHalfFov + bound * System.Math.Sqrt(1 + _tanHalfFov * _tanHalfFov)
|
||||
&& System.Math.Abs(DVector3.Dot(toPatch, _right)) <= z * horizontalTan + bound * System.Math.Sqrt(1 + horizontalTan * horizontalTan);
|
||||
}
|
||||
|
||||
private bool EnsureReady(TerrainPatchKey key)
|
||||
{
|
||||
if (!_tiles.TryGetValue(key, out var tile))
|
||||
{
|
||||
if (_pending >= 16) return false;
|
||||
tile = new() { LastFrame = _frame };
|
||||
_tiles.Add(key, tile);
|
||||
_pending++;
|
||||
var token = _cancel.Token;
|
||||
tile.Task = Task.Run(async () =>
|
||||
{
|
||||
await Workers.WaitAsync(token);
|
||||
try { token.ThrowIfCancellationRequested(); return TerrainPatchBuilder.Build(_planet, key); }
|
||||
finally { Workers.Release(); }
|
||||
});
|
||||
}
|
||||
tile.LastFrame = _frame;
|
||||
if (tile.Mesh is not null) return true;
|
||||
if (tile.Data is null || _uploads >= 6) return false;
|
||||
tile.Mesh = new(_gl, 9, tile.Data.Vertices, tile.Data.Indices);
|
||||
_uploads++;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancel.Cancel();
|
||||
foreach (var tile in _tiles.Values) tile.Mesh?.Dispose();
|
||||
_tiles.Clear();
|
||||
_cancel.Dispose();
|
||||
}
|
||||
|
||||
private sealed class Tile
|
||||
{
|
||||
public TerrainPatchData? Data;
|
||||
public Task<TerrainPatchData>? Task;
|
||||
public GpuMesh? Mesh;
|
||||
public int LastFrame;
|
||||
}
|
||||
}
|
||||
156
src/Universe.App/Graphics/BenchmarkOverlay.cs
Normal file
156
src/Universe.App/Graphics/BenchmarkOverlay.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using System.Globalization;
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.App.Runtime;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
/// <summary>Compact live/results panel with a decimated frame-rate history graph.</summary>
|
||||
internal sealed class BenchmarkOverlay : IDisposable
|
||||
{
|
||||
private readonly GpuMesh _panel;
|
||||
private readonly GpuMesh _grid;
|
||||
private readonly GpuMesh _curve;
|
||||
private long _lastRevision = -1;
|
||||
private int _lastTextTick = -1;
|
||||
private string _lastPhase = string.Empty;
|
||||
|
||||
public BenchmarkOverlay(GL gl)
|
||||
{
|
||||
_panel = new(gl, 6, [], dynamic: true);
|
||||
_grid = new(gl, 6, [], dynamic: true);
|
||||
_curve = new(gl, 6, [], dynamic: true);
|
||||
}
|
||||
|
||||
public void Draw(int width, int height, BenchmarkRunner benchmark)
|
||||
{
|
||||
var scale = System.Math.Clamp(System.Math.Min(width / 1440d, height / 900d), .58, 1.2);
|
||||
var x = width - 402 * scale;
|
||||
var y = 14 * scale;
|
||||
const double logicalWidth = 388;
|
||||
const double logicalHeight = 232;
|
||||
var panelWidth = logicalWidth * scale;
|
||||
var panelHeight = logicalHeight * scale;
|
||||
var graphX = x + 17 * scale;
|
||||
var graphY = y + 151 * scale;
|
||||
var graphWidth = (logicalWidth - 34) * scale;
|
||||
var graphHeight = 62 * scale;
|
||||
var tick = (int)(benchmark.ElapsedSeconds * 5);
|
||||
|
||||
if (_lastRevision != benchmark.GraphRevision)
|
||||
{
|
||||
UpdateGraph(width, height, benchmark.GraphSamples, graphX, graphY, graphWidth, graphHeight, scale);
|
||||
_lastRevision = benchmark.GraphRevision;
|
||||
}
|
||||
if (_lastTextTick != tick || _lastPhase != benchmark.Phase)
|
||||
{
|
||||
UpdatePanel(width, height, benchmark, x, y, panelWidth, panelHeight, scale);
|
||||
_lastTextTick = tick;
|
||||
_lastPhase = benchmark.Phase;
|
||||
}
|
||||
|
||||
_panel.Draw(GLEnum.Triangles);
|
||||
_grid.Draw(GLEnum.Lines);
|
||||
_curve.Draw(GLEnum.LineStrip);
|
||||
}
|
||||
|
||||
private void UpdatePanel(int width, int height, BenchmarkRunner benchmark, double x, double y,
|
||||
double panelWidth, double panelHeight, double scale)
|
||||
{
|
||||
var vertices = new List<float>(20_000);
|
||||
Rect(x, y, panelWidth, panelHeight, .018f, .028f, .05f);
|
||||
Rect(x, y, 3 * scale, panelHeight, .13f, .55f, .8f);
|
||||
Text(benchmark.IsRunning ? "BENCHMARK LIVE" : benchmark.Phase == "STOPPED" ? "BENCHMARK STOPPED" : "BENCHMARK COMPLETE",
|
||||
x + 16 * scale, y + 11 * scale, 1.75 * scale, .76f, .91f, 1f);
|
||||
Text($"PHASE {benchmark.Phase}", x + 16 * scale, y + 29 * scale, 1.32 * scale, .52f, .7f, .82f);
|
||||
Text($"TARGET {benchmark.TargetLabel}", x + 16 * scale, y + 47 * scale,
|
||||
1.25 * scale, .65f, .75f, .84f);
|
||||
Text($"RADIUS {Number(benchmark.TargetEarthRadii)} R", x + 16 * scale, y + 64 * scale,
|
||||
1.25 * scale, .65f, .75f, .84f);
|
||||
Text($"TIME {Time(benchmark.ElapsedSeconds)} / {Time(benchmark.DurationSeconds)}", x + 16 * scale,
|
||||
y + 81 * scale, 1.18 * scale, .65f, .75f, .84f);
|
||||
Text($"CURRENT {Number(benchmark.CurrentFps)} FPS", x + 16 * scale, y + 98 * scale,
|
||||
1.30 * scale, .84f, .92f, 1f);
|
||||
Text($"AVERAGE {Number(benchmark.AverageFps)} FPS", x + 16 * scale, y + 115 * scale,
|
||||
1.20 * scale, .7f, .83f, .91f);
|
||||
Text($"MIN {Number(benchmark.MinimumFps)} 1 PCT LOW {Number(benchmark.OnePercentLowFps)}",
|
||||
x + 16 * scale, y + 132 * scale, 1.02 * scale, .7f, .83f, .91f);
|
||||
|
||||
_panel.ReplaceVertices(vertices.ToArray());
|
||||
|
||||
void Rect(double px, double py, double w, double h, float r, float g, float b)
|
||||
{
|
||||
Vertex(px, py); Vertex(px + w, py); Vertex(px + w, py + h);
|
||||
Vertex(px, py); Vertex(px + w, py + h); Vertex(px, py + h);
|
||||
void Vertex(double vx, double vy)
|
||||
{
|
||||
vertices.Add(X(vx, width)); vertices.Add(Y(vy, height)); vertices.Add(0);
|
||||
vertices.Add(r); vertices.Add(g); vertices.Add(b);
|
||||
}
|
||||
}
|
||||
|
||||
void Text(string value, double px, double py, double glyphScale, float r, float g, float b)
|
||||
{
|
||||
foreach (var character in value)
|
||||
{
|
||||
if (PauseMenu.Font.TryGetValue(character, out var glyph))
|
||||
for (var row = 0; row < glyph.Length; row++)
|
||||
for (var col = 0; col < glyph[row].Length; col++)
|
||||
if (glyph[row][col] == '1') Rect(px + col * glyphScale, py + row * glyphScale,
|
||||
glyphScale * .9, glyphScale * .9, r, g, b);
|
||||
px += 6 * glyphScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGraph(int width, int height, IReadOnlyList<double> samples, double x, double y,
|
||||
double graphWidth, double graphHeight, double scale)
|
||||
{
|
||||
var grid = new List<float>(1800);
|
||||
var curve = new List<float>(samples.Count * 6);
|
||||
var maximum = samples.Count == 0 ? 60 : System.Math.Max(60, System.Math.Ceiling(samples.Max() / 30) * 30);
|
||||
maximum = System.Math.Min(maximum, 240);
|
||||
for (var i = 0; i <= 4; i++)
|
||||
{
|
||||
var py = y + graphHeight * i / 4;
|
||||
AddLine(grid, width, height, x, py, x + graphWidth, py, .12f, .19f, .25f);
|
||||
}
|
||||
for (var i = 0; i <= 8; i++)
|
||||
{
|
||||
var px = x + graphWidth * i / 8;
|
||||
AddLine(grid, width, height, px, y, px, y + graphHeight, .075f, .12f, .17f);
|
||||
}
|
||||
for (var i = 0; i < samples.Count; i++)
|
||||
{
|
||||
var px = x + graphWidth * (samples.Count <= 1 ? .5 : (double)i / (samples.Count - 1));
|
||||
var py = y + graphHeight * (1 - System.Math.Clamp(samples[i] / maximum, 0, 1));
|
||||
curve.Add(X(px, width)); curve.Add(Y(py, height)); curve.Add(0);
|
||||
curve.Add(.25f); curve.Add(.85f); curve.Add(1f);
|
||||
}
|
||||
_grid.ReplaceVertices(grid.ToArray());
|
||||
_curve.ReplaceVertices(curve.ToArray());
|
||||
}
|
||||
|
||||
private static void AddLine(List<float> vertices, int width, int height, double x1, double y1, double x2, double y2,
|
||||
float r, float g, float b)
|
||||
{
|
||||
Add(x1, y1); Add(x2, y2);
|
||||
void Add(double x, double y)
|
||||
{
|
||||
vertices.Add(X(x, width)); vertices.Add(Y(y, height)); vertices.Add(0);
|
||||
vertices.Add(r); vertices.Add(g); vertices.Add(b);
|
||||
}
|
||||
}
|
||||
|
||||
private static float X(double x, int width) => (float)(2 * x / width - 1);
|
||||
private static float Y(double y, int height) => (float)(1 - 2 * y / height);
|
||||
private static string Number(double value) => value.ToString("F1", CultureInfo.GetCultureInfo("sv-SE"));
|
||||
private static string Time(double seconds) =>
|
||||
$"{(int)(seconds / 60):00}:{(int)(seconds % 60):00}";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_panel.Dispose();
|
||||
_grid.Dispose();
|
||||
_curve.Dispose();
|
||||
}
|
||||
}
|
||||
28
src/Universe.App/Graphics/CloudDensityTexture.cs
Normal file
28
src/Universe.App/Graphics/CloudDensityTexture.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed unsafe class CloudDensityTexture : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
public uint Handle { get; }
|
||||
|
||||
public CloudDensityTexture(GL gl, CloudDensityMap? map)
|
||||
{
|
||||
_gl = gl;
|
||||
Handle = gl.GenTexture();
|
||||
gl.BindTexture(GLEnum.Texture2D, Handle);
|
||||
var samples = map?.Samples ?? [255];
|
||||
fixed (byte* pixels = samples)
|
||||
gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.R8, (uint)(map?.Width ?? 1), (uint)(map?.Height ?? 1), 0,
|
||||
PixelFormat.Red, PixelType.UnsignedByte, pixels);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)(map is null ? GLEnum.Linear : GLEnum.LinearMipmapLinear));
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMagFilter, (int)GLEnum.Linear);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapS, (int)GLEnum.Repeat);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapT, (int)GLEnum.ClampToEdge);
|
||||
if (map is not null) gl.GenerateMipmap(GLEnum.Texture2D);
|
||||
}
|
||||
|
||||
public void Dispose() => _gl.DeleteTexture(Handle);
|
||||
}
|
||||
33
src/Universe.App/Graphics/CloudNoiseTexture.cs
Normal file
33
src/Universe.App/Graphics/CloudNoiseTexture.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed unsafe class CloudNoiseTexture : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
public uint Shape { get; }
|
||||
public uint Detail { get; }
|
||||
public CloudNoiseTexture(GL gl)
|
||||
{
|
||||
_gl = gl;
|
||||
Shape = Upload(128, false);
|
||||
Detail = Upload(64, true);
|
||||
}
|
||||
private uint Upload(int size, bool detail)
|
||||
{
|
||||
var handle = _gl.GenTexture();
|
||||
_gl.BindTexture(GLEnum.Texture3D, handle);
|
||||
var data = CloudNoise.Build(size, detail);
|
||||
fixed (byte* p = data)
|
||||
_gl.TexImage3D(GLEnum.Texture3D, 0, InternalFormat.Rgba8, (uint)size, (uint)size, (uint)size, 0, PixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
_gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureMinFilter, (int)GLEnum.LinearMipmapLinear);
|
||||
_gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureMagFilter, (int)GLEnum.Linear);
|
||||
_gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureWrapS, (int)GLEnum.Repeat);
|
||||
_gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureWrapT, (int)GLEnum.Repeat);
|
||||
_gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureWrapR, (int)GLEnum.Repeat);
|
||||
_gl.GenerateMipmap(GLEnum.Texture3D);
|
||||
return handle;
|
||||
}
|
||||
public void Dispose() { _gl.DeleteTexture(Shape); _gl.DeleteTexture(Detail); }
|
||||
}
|
||||
111
src/Universe.App/Graphics/GpuMesh.cs
Normal file
111
src/Universe.App/Graphics/GpuMesh.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed unsafe class GpuMesh : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly uint _vao;
|
||||
private readonly uint _vertexBuffer;
|
||||
private readonly uint _indexBuffer;
|
||||
private readonly int _strideFloats;
|
||||
private readonly bool _hasIndices;
|
||||
private int _vertexCount;
|
||||
private int _indexCount;
|
||||
private bool _disposed;
|
||||
|
||||
public GpuMesh(GL gl, int strideFloats, float[] vertices, uint[]? indices = null, bool dynamic = false)
|
||||
{
|
||||
_gl = gl;
|
||||
_strideFloats = strideFloats;
|
||||
_hasIndices = indices is { Length: > 0 };
|
||||
_vao = gl.GenVertexArray();
|
||||
_vertexBuffer = gl.GenBuffer();
|
||||
_indexBuffer = _hasIndices ? gl.GenBuffer() : 0;
|
||||
gl.BindVertexArray(_vao);
|
||||
gl.BindBuffer(GLEnum.ArrayBuffer, _vertexBuffer);
|
||||
UploadVertices(vertices, dynamic);
|
||||
|
||||
if (_hasIndices)
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ElementArrayBuffer, _indexBuffer);
|
||||
UploadIndices(indices!, dynamic);
|
||||
}
|
||||
|
||||
ConfigureAttributes();
|
||||
gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
public void ReplaceVertices(float[] vertices, bool dynamic = true)
|
||||
{
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, _vertexBuffer);
|
||||
UploadVertices(vertices, dynamic);
|
||||
ConfigureAttributes();
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
public void Draw(GLEnum primitive = GLEnum.Triangles)
|
||||
{
|
||||
_gl.BindVertexArray(_vao);
|
||||
if (_hasIndices)
|
||||
_gl.DrawElements(primitive, (uint)_indexCount, DrawElementsType.UnsignedInt, null);
|
||||
else
|
||||
_gl.DrawArrays(primitive, 0, (uint)_vertexCount);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
private void UploadVertices(float[] vertices, bool dynamic)
|
||||
{
|
||||
_vertexCount = vertices.Length / _strideFloats;
|
||||
fixed (float* pointer = vertices)
|
||||
{
|
||||
_gl.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(nuint)(vertices.Length * sizeof(float)),
|
||||
pointer,
|
||||
dynamic ? GLEnum.DynamicDraw : GLEnum.StaticDraw);
|
||||
}
|
||||
}
|
||||
|
||||
private void UploadIndices(uint[] indices, bool dynamic)
|
||||
{
|
||||
_indexCount = indices.Length;
|
||||
fixed (uint* pointer = indices)
|
||||
{
|
||||
_gl.BufferData(
|
||||
GLEnum.ElementArrayBuffer,
|
||||
(nuint)(indices.Length * sizeof(uint)),
|
||||
pointer,
|
||||
dynamic ? GLEnum.DynamicDraw : GLEnum.StaticDraw);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureAttributes()
|
||||
{
|
||||
var stride = _strideFloats * sizeof(float);
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, (uint)stride, (void*)0);
|
||||
if (_strideFloats >= 6)
|
||||
{
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, (uint)stride, (void*)(3 * sizeof(float)));
|
||||
}
|
||||
if (_strideFloats >= 7)
|
||||
{
|
||||
_gl.EnableVertexAttribArray(2);
|
||||
_gl.VertexAttribPointer(2, _strideFloats >= 9 ? 3 : 1, VertexAttribPointerType.Float, false, (uint)stride, (void*)(6 * sizeof(float)));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_gl.DeleteVertexArray(_vao);
|
||||
_gl.DeleteBuffer(_vertexBuffer);
|
||||
if (_indexBuffer != 0)
|
||||
_gl.DeleteBuffer(_indexBuffer);
|
||||
}
|
||||
}
|
||||
139
src/Universe.App/Graphics/GpuPassProfiler.cs
Normal file
139
src/Universe.App/Graphics/GpuPassProfiler.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
/// <summary>Non-blocking GPU timestamp measurements. Results are collected several frames later.</summary>
|
||||
internal sealed class GpuPassProfiler : IDisposable
|
||||
{
|
||||
private const int FramesInFlight = 4;
|
||||
|
||||
private sealed class QuerySeries
|
||||
{
|
||||
public readonly uint[] Starts = new uint[FramesInFlight];
|
||||
public readonly uint[] Ends = new uint[FramesInFlight];
|
||||
public readonly bool[] Pending = new bool[FramesInFlight];
|
||||
public readonly long[] IssuedFrame = new long[FramesInFlight];
|
||||
public readonly SortedDictionary<long, double> Recent = [];
|
||||
public bool HasResult;
|
||||
public double LatestMilliseconds;
|
||||
public long LatestFrame = -1;
|
||||
}
|
||||
|
||||
private readonly GL _gl;
|
||||
private readonly Dictionary<string, QuerySeries> _series = new(StringComparer.Ordinal);
|
||||
private readonly Stack<QuerySeries> _open = new();
|
||||
private int _slot;
|
||||
private long _frame;
|
||||
private bool _enabled;
|
||||
private bool _slotAvailable;
|
||||
private bool _disposed;
|
||||
|
||||
public GpuPassProfiler(GL gl) => _gl = gl;
|
||||
|
||||
public void BeginFrame(bool enabled)
|
||||
{
|
||||
Poll();
|
||||
_frame++;
|
||||
_enabled = enabled;
|
||||
_slot = (int)(_frame % FramesInFlight);
|
||||
_slotAvailable = enabled && !_series.Values.Any(s => s.Pending[_slot]);
|
||||
_open.Clear();
|
||||
if (_slotAvailable) BeginPass("GPU FRAME");
|
||||
}
|
||||
|
||||
public void EndFrame()
|
||||
{
|
||||
if (!_slotAvailable) return;
|
||||
if (_open.Count != 1)
|
||||
throw new InvalidOperationException($"Unbalanced GPU profiling scopes: {_open.Count - 1} render passes remain open.");
|
||||
EndPass();
|
||||
}
|
||||
|
||||
public void BeginPass(string name)
|
||||
{
|
||||
if (!_enabled || !_slotAvailable) return;
|
||||
var queries = Get(name);
|
||||
_gl.QueryCounter(queries.Starts[_slot], QueryCounterTarget.Timestamp);
|
||||
_open.Push(queries);
|
||||
}
|
||||
|
||||
public void EndPass()
|
||||
{
|
||||
if (!_enabled || !_slotAvailable) return;
|
||||
if (_open.Count == 0) throw new InvalidOperationException("GPU profiling pass ended without a matching start.");
|
||||
var queries = _open.Pop();
|
||||
_gl.QueryCounter(queries.Ends[_slot], QueryCounterTarget.Timestamp);
|
||||
queries.Pending[_slot] = true;
|
||||
queries.IssuedFrame[_slot] = _frame;
|
||||
}
|
||||
|
||||
public IReadOnlyList<KeyValuePair<string, double>> Results() => _series
|
||||
.Where(p => p.Value.HasResult)
|
||||
.Select(p => new KeyValuePair<string, double>(p.Key, p.Value.LatestMilliseconds))
|
||||
.ToArray();
|
||||
|
||||
public bool TryGetAverageDifference(string baselineName, string reducedName, out double milliseconds)
|
||||
{
|
||||
milliseconds = 0;
|
||||
if (!_series.TryGetValue(baselineName, out var baseline) || !_series.TryGetValue(reducedName, out var reduced)) return false;
|
||||
var matching = baseline.Recent.Keys.Intersect(reduced.Recent.Keys).OrderDescending().Take(4).ToArray();
|
||||
if (matching.Length == 0) return false;
|
||||
milliseconds = matching.Average(frame => baseline.Recent[frame] - reduced.Recent[frame]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private QuerySeries Get(string name)
|
||||
{
|
||||
if (_series.TryGetValue(name, out var found)) return found;
|
||||
var created = new QuerySeries();
|
||||
for (var i = 0; i < FramesInFlight; i++)
|
||||
{
|
||||
created.Starts[i] = _gl.GenQuery();
|
||||
created.Ends[i] = _gl.GenQuery();
|
||||
}
|
||||
_series.Add(name, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private void Poll()
|
||||
{
|
||||
foreach (var queries in _series.Values)
|
||||
for (var i = 0; i < FramesInFlight; i++)
|
||||
{
|
||||
if (!queries.Pending[i]) continue;
|
||||
_gl.GetQueryObject(queries.Ends[i], GLEnum.QueryResultAvailable, out int available);
|
||||
if (available == 0) continue;
|
||||
_gl.GetQueryObject(queries.Starts[i], GLEnum.QueryResult, out ulong start);
|
||||
_gl.GetQueryObject(queries.Ends[i], GLEnum.QueryResult, out ulong end);
|
||||
if (end >= start && queries.IssuedFrame[i] >= queries.LatestFrame)
|
||||
{
|
||||
queries.LatestMilliseconds = (end - start) / 1_000_000d;
|
||||
queries.LatestFrame = queries.IssuedFrame[i];
|
||||
queries.HasResult = true;
|
||||
queries.Recent[queries.IssuedFrame[i]] = queries.LatestMilliseconds;
|
||||
while (queries.Recent.Count > 256) queries.Recent.Remove(queries.Recent.Keys.First());
|
||||
}
|
||||
queries.Pending[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Releases per-system planet query objects when the streamed system changes.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var queries in _series.Values)
|
||||
for (var i = 0; i < FramesInFlight; i++)
|
||||
{
|
||||
_gl.DeleteQuery(queries.Starts[i]);
|
||||
_gl.DeleteQuery(queries.Ends[i]);
|
||||
}
|
||||
_series.Clear();
|
||||
_open.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
121
src/Universe.App/Graphics/GraphicsDiagnostics.cs
Normal file
121
src/Universe.App/Graphics/GraphicsDiagnostics.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
/// <summary>Checks the actual GPU height field against the collision field; never reads the desktop.</summary>
|
||||
internal static class GraphicsDiagnostics
|
||||
{
|
||||
public static unsafe void ValidatePlanetTerrain(GL gl, GpuMesh fullscreen, PlanetData planet, Action<ShaderProgram> bind)
|
||||
{
|
||||
const int size = 64;
|
||||
using var target = new RenderTarget(gl, size, size, fullPrecision: true);
|
||||
using var program = ShaderProgram.Create(gl, ShaderSources.Load("fullscreen.vert"),
|
||||
"#version 330 core\n" + ShaderSources.Load("noise") + "\n" + ShaderSources.Load("planet-height") + """
|
||||
in vec2 vUv;
|
||||
out vec4 fragColor;
|
||||
void main() { vec3 d=normalize(vec3(vUv*2.0-1.0,1.0)); vec3 b=bodyDirection(d); vec2 uv=planetUv(b);
|
||||
fragColor=vec4(surfaceElevation(d),uv,heightMapValue(b)); }
|
||||
""");
|
||||
target.Bind(); gl.Disable(GLEnum.DepthTest);
|
||||
program.Use(); bind(program); fullscreen.Draw();
|
||||
var values = new float[size * size * 4];
|
||||
fixed (float* data = values) gl.ReadPixels(0, 0, size, size, PixelFormat.Rgba, PixelType.Float, data);
|
||||
var maximumError = 0d;
|
||||
var maximumUvError = 0d;
|
||||
var maximumMapError = 0d;
|
||||
var worst = string.Empty;
|
||||
for (var y = 0; y < size; y++)
|
||||
for (var x = 0; x < size; x++)
|
||||
{
|
||||
var d = new DVector3((x + 0.5) / size * 2 - 1, (y + 0.5) / size * 2 - 1, 1).Normalized();
|
||||
var cpu = TerrainHeight.Sample(planet, d) - planet.RadiusMeters;
|
||||
var gpu = values[(y * size + x) * 4] * planet.RadiusMeters;
|
||||
var body = planet.Orientation.ToBody(d);
|
||||
var uv = HeightMap.Coordinates(body);
|
||||
var map = planet.Terrain.HeightMap!.Sample(body);
|
||||
maximumUvError = System.Math.Max(maximumUvError, System.Math.Max(System.Math.Abs(uv.U - values[(y * size + x) * 4 + 1]), System.Math.Abs(uv.V - values[(y * size + x) * 4 + 2])));
|
||||
maximumMapError = System.Math.Max(maximumMapError, System.Math.Abs(map - values[(y * size + x) * 4 + 3]));
|
||||
if (System.Math.Abs(cpu - gpu) > maximumError)
|
||||
{
|
||||
maximumError = System.Math.Abs(cpu - gpu);
|
||||
worst = $"pixel {x},{y}: CPU {cpu:G6} m GPU {gpu:G6} m; UV {uv.U:G6},{uv.V:G6} vs {values[(y * size + x) * 4 + 1]:G6},{values[(y * size + x) * 4 + 2]:G6}; map {map:G6} vs {values[(y * size + x) * 4 + 3]:G6}";
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"Graphics validation: '{planet.Id}' heightmap + noise CPU/GPU error {maximumError:G6} m");
|
||||
Console.WriteLine($"Map diagnostic UV error {maximumUvError:G6}, map error {maximumMapError:G6}; {worst}");
|
||||
gl.BindFramebuffer(GLEnum.Framebuffer, 0);
|
||||
if (maximumError > System.Math.Max(0.5, planet.TerrainAmplitudeMeters * 0.002))
|
||||
throw new InvalidOperationException($"GPU terrain for '{planet.Id}' does not match its collision heightmap.");
|
||||
if (planet.Terrain.RegionalHeight is { } regional) ValidateRegionalTerrain(gl, fullscreen, planet, regional, bind);
|
||||
}
|
||||
|
||||
private static unsafe void ValidateRegionalTerrain(GL gl, GpuMesh fullscreen, PlanetData planet, RegionalHeightMap region, Action<ShaderProgram> bind)
|
||||
{
|
||||
const int size = 64;
|
||||
using var target = new RenderTarget(gl, size, size, fullPrecision: true);
|
||||
using var program = ShaderProgram.Create(gl, ShaderSources.Load("fullscreen.vert"),
|
||||
"#version 330 core\n" + ShaderSources.Load("noise") + "\n" + ShaderSources.Load("planet-height") + """
|
||||
in vec2 vUv; out vec4 fragColor;
|
||||
void main() {
|
||||
float lon=radians(mix(uRegionBounds.x,uRegionBounds.z,vUv.x));
|
||||
float lat=radians(mix(uRegionBounds.y,uRegionOther.x,vUv.y));
|
||||
vec3 body=vec3(cos(lat)*cos(lon),sin(lat),-cos(lat)*sin(lon));
|
||||
fragColor=vec4(surfaceElevation(transpose(mat3(uWorldToBody))*body),0,0,1);
|
||||
}
|
||||
""");
|
||||
target.Bind(); gl.Disable(GLEnum.DepthTest); program.Use(); bind(program); fullscreen.Draw();
|
||||
var pixels = new float[size * size * 4];
|
||||
fixed (float* data = pixels) gl.ReadPixels(0, 0, size, size, PixelFormat.Rgba, PixelType.Float, data);
|
||||
var maximumError = 0d;
|
||||
for (var y = 0; y < size; y++) for (var x = 0; x < size; x++)
|
||||
{
|
||||
var lon = (region.WestDegrees + (region.EastDegrees - region.WestDegrees) * (x + .5) / size) * System.Math.PI / 180;
|
||||
var lat = (region.SouthDegrees + (region.NorthDegrees - region.SouthDegrees) * (y + .5) / size) * System.Math.PI / 180;
|
||||
var body = new DVector3(System.Math.Cos(lat) * System.Math.Cos(lon), System.Math.Sin(lat), -System.Math.Cos(lat) * System.Math.Sin(lon));
|
||||
var direction = planet.Orientation.ToWorld(body);
|
||||
var cpu = TerrainHeight.Sample(planet, direction) - planet.RadiusMeters;
|
||||
maximumError = System.Math.Max(maximumError, System.Math.Abs(cpu - pixels[(y * size + x) * 4] * planet.RadiusMeters));
|
||||
}
|
||||
Console.WriteLine($"Graphics validation: '{planet.Id}' regional heights CPU/GPU maximum error {maximumError:G6} m");
|
||||
gl.BindFramebuffer(GLEnum.Framebuffer, 0);
|
||||
if (maximumError > 30) throw new InvalidOperationException("Regional GPU terrain differs from collision height by over 30 m.");
|
||||
}
|
||||
|
||||
public static unsafe void ValidateTerrain(GL gl, GpuMesh fullscreen)
|
||||
{
|
||||
const int size = 64;
|
||||
using var target = new RenderTarget(gl, size, size, fullPrecision: true);
|
||||
using var program = ShaderProgram.Create(gl, ShaderSources.Load("fullscreen.vert"),
|
||||
"#version 330 core\n" + ShaderSources.Load("noise") + """
|
||||
in vec2 vUv;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor=vec4(terrainHeight(normalize(vec3(vUv*2.0-1.0,1.0)),vec3(3.125,17.25,6.5),9),preciseNoise3(vec3(vUv*9.0,4.7)),noise3(vec3(vUv*9.0,4.7)),1); }
|
||||
""");
|
||||
target.Bind();
|
||||
gl.Disable(GLEnum.DepthTest);
|
||||
program.Use(); program.Set("uNoise", 4);
|
||||
fullscreen.Draw();
|
||||
var values = new float[size * size * 4];
|
||||
fixed (float* data = values)
|
||||
gl.ReadPixels(0, 0, size, size, PixelFormat.Rgba, PixelType.Float, data);
|
||||
var error = 0d;
|
||||
var noiseError = 0d;
|
||||
var filteredError = 0d;
|
||||
for (var y = 0; y < size; y++)
|
||||
for (var x = 0; x < size; x++)
|
||||
{
|
||||
var direction = new DVector3((x + 0.5) / size * 2 - 1, (y + 0.5) / size * 2 - 1, 1).Normalized();
|
||||
var cpu = TerrainHeight.Elevation(direction, new(3.125, 17.25, 6.5));
|
||||
error = System.Math.Max(error, System.Math.Abs(cpu - values[(y * size + x) * 4]));
|
||||
var noise = ProceduralNoise.Sample(new DVector3((x + 0.5) / size * 9, (y + 0.5) / size * 9, 4.7));
|
||||
noiseError = System.Math.Max(noiseError, System.Math.Abs(noise - values[(y * size + x) * 4 + 1]));
|
||||
filteredError = System.Math.Max(filteredError, System.Math.Abs(noise - values[(y * size + x) * 4 + 2]));
|
||||
}
|
||||
Console.WriteLine($"Graphics validation: CPU/GPU terrain maximum normalized error {error:G6}");
|
||||
Console.WriteLine($"Graphics validation: precise noise error {noiseError:G6}; hardware filtered noise error {filteredError:G6}; GPU {gl.GetStringS(GLEnum.Renderer)}");
|
||||
gl.BindFramebuffer(GLEnum.Framebuffer, 0);
|
||||
if (error > 0.0003) throw new InvalidOperationException("GPU terrain does not match the collision surface.");
|
||||
}
|
||||
}
|
||||
352
src/Universe.App/Graphics/GraphicsReport.cs
Normal file
352
src/Universe.App/Graphics/GraphicsReport.cs
Normal file
@@ -0,0 +1,352 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.IO.Compression;
|
||||
using System.Numerics;
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.World;
|
||||
using Universe.Core.Configuration;
|
||||
using Universe.Core.Math;
|
||||
using Universe.App.Runtime;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
/// <summary>Offscreen tests of the production shaders; no desktop capture or mouse control.</summary>
|
||||
internal static class GraphicsReport
|
||||
{
|
||||
public static void RunTerrainPerformance(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
||||
{
|
||||
var system = neighborhood.ActiveSystem;
|
||||
var planet = system.Planets.Single(p => p.Id == "mars");
|
||||
var center = UniversePosition.AtCellCenter(system.Coordinate, cellSize);
|
||||
var camera = new FlyCamera(center, cellSize);
|
||||
camera.SetInitialView(system, new StartupConfiguration { PlanetId = "mars" });
|
||||
camera.RequestSurfaceView([system]);
|
||||
for (var i = 0; i < 400; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||
var body = center.Add(planet.PositionMeters, cellSize);
|
||||
var radial = camera.Position.RelativeTo(body, cellSize).Normalized();
|
||||
camera.SetPlayerPose(body.Add(radial * (TerrainHeight.Sample(planet, radial) + 250), cellSize), camera.Forward, radial);
|
||||
for (var i = 0; i < 600; i++)
|
||||
{
|
||||
renderer.Render(camera, neighborhood, 1d / 60);
|
||||
if ((i + 1) % 100 == 0) Console.WriteLine($"Terrain warmup {i + 1}: tiles {renderer.TerrainTiles}, nearest level {renderer.TerrainNearestLevel}, deepest {renderer.TerrainDeepestLevel}, pending {renderer.TerrainPendingTiles}.");
|
||||
}
|
||||
gl.Finish();
|
||||
renderer.ProfileFrame = true;
|
||||
var totals = new Dictionary<string, double>();
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
renderer.Render(camera, neighborhood, 1d / 60);
|
||||
foreach (var (key, value) in renderer.FrameTimings) totals[key] = totals.GetValueOrDefault(key) + value / 5;
|
||||
}
|
||||
renderer.ProfileFrame = false;
|
||||
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
||||
WritePng(Path.Combine(directory, "terrain-performance.png"), width, height, Read(gl, width, height), false);
|
||||
Console.WriteLine($"Terrain performance ({width}x{height}, synchronized): {string.Join(", ", totals.Select(p => $"{p.Key} {p.Value:F2} ms"))}; tiles {renderer.TerrainTiles}, nearest level {renderer.TerrainNearestLevel} at {renderer.TerrainNearestDistance:F1} m, deepest {renderer.TerrainDeepestLevel}, pending {renderer.TerrainPendingTiles}.");
|
||||
}
|
||||
public static void RunMenu(GL gl, UniverseRenderer renderer, FlyCamera camera, UniverseNeighborhood neighborhood, int width, int height)
|
||||
{
|
||||
renderer.Render(camera, neighborhood, 0); renderer.DrawMenu(0, false); gl.Finish();
|
||||
if (gl.GetError() is var error && error != GLEnum.NoError) throw new InvalidOperationException($"Menu GL error: {error}");
|
||||
var pixels = Read(gl, width, height);
|
||||
var dir = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(dir);
|
||||
WritePng(Path.Combine(dir, "pause-menu.png"), width, height, pixels, false);
|
||||
var scale = System.Math.Min(width / 1440d, height / 900d);
|
||||
if (renderer.MenuHit(width * .5, height * .5 - 152 * scale, width, height) != 0
|
||||
|| renderer.MenuHit(width * .5, height * .5 - 95 * scale, width, height) != 1
|
||||
|| renderer.MenuHit(width * .5, height * .5 - 38 * scale, width, height) != 2
|
||||
|| renderer.MenuHit(width * .5, height * .5 + 19 * scale, width, height) != 3
|
||||
|| renderer.MenuHit(width * .5, height * .5 + 76 * scale, width, height) != 4
|
||||
|| renderer.MenuHit(width * .5, height * .5 + 133 * scale, width, height) != 5
|
||||
|| renderer.MenuHit(0, 0, width, height) != -1)
|
||||
throw new InvalidOperationException("Menu hit testing failed.");
|
||||
Console.WriteLine($"Pause menu rendered and hit testing passed. Image: {dir}");
|
||||
}
|
||||
public static void RunTerrain(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
||||
{
|
||||
var system = neighborhood.ActiveSystem;
|
||||
var earth = system.Planets.Single(p => p.Id == "earth");
|
||||
var origin = UniversePosition.AtCellCenter(system.Coordinate, cellSize);
|
||||
var camera = new FlyCamera(origin, cellSize);
|
||||
camera.SetInitialView(system, new StartupConfiguration { PlanetId = "earth", LongitudeDegrees = -7.5, LatitudeDegrees = 46.5,
|
||||
DistanceInRadii = 1.003, SpeedMetersPerSecond = 1000 });
|
||||
var radial = (camera.Position.RelativeTo(origin, cellSize) - earth.PositionMeters).Normalized();
|
||||
var distance = (camera.Position.RelativeTo(origin, cellSize) - earth.PositionMeters).Length;
|
||||
var altitude = distance - TerrainHeight.Sample(earth, radial);
|
||||
var steps = (int)System.Math.Max(0, System.Math.Floor((altitude - 1000) / 100));
|
||||
for (var i = 0; i < steps; i++) camera.Update(.1, camera.Forward, neighborhood);
|
||||
var litSystem = system with { Stars = system.Stars.Select((s, i) => i == earth.HostStarIndex
|
||||
? s with { PositionMeters = earth.PositionMeters + radial * UniverseScale.AstronomicalUnitMeters } : s).ToArray(),
|
||||
Planets = system.Planets.Select(p => p.Id == earth.Id ? p with { Clouds = null, Ocean = null, OceanMask = null } : p).ToArray() };
|
||||
var lit = neighborhood with { ActiveSystem = litSystem };
|
||||
for (var i = 0; i < 60; i++) renderer.Render(camera, lit, .016);
|
||||
renderer.Render(camera, lit, .016); gl.Finish();
|
||||
if (gl.GetError() is var error && error != GLEnum.NoError) throw new InvalidOperationException($"Terrain report GL error: {error}");
|
||||
var textured = Read(gl, width, height);
|
||||
var noMaterials = litSystem with { Planets = litSystem.Planets.Select(p => p.Id == earth.Id ? p with { Materials = p.Materials with { Enabled = false } } : p).ToArray() };
|
||||
renderer.Render(camera, lit with { ActiveSystem = noMaterials }, .016); gl.Finish();
|
||||
var plain = Read(gl, width, height);
|
||||
var difference = textured.Zip(plain, (a, b) => System.Math.Abs(a - b)).Average();
|
||||
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
||||
WritePng(Path.Combine(directory, "earth-alps-materials.png"), width, height, textured, false);
|
||||
var remaining = (camera.Position.RelativeTo(origin, cellSize) - earth.PositionMeters).Length - TerrainHeight.Sample(earth, radial);
|
||||
if (difference < .0001) throw new InvalidOperationException($"Surface materials do not visibly change the near-ground scene (difference {difference:G5}, altitude {remaining:G5} m).");
|
||||
Console.WriteLine($"Terrain report passed: regional terrain and visible material contribution {difference:G5}. Images: {directory}");
|
||||
}
|
||||
|
||||
public static void RunClouds(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
||||
{
|
||||
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
||||
var system = neighborhood.ActiveSystem;
|
||||
var earth = system.Planets.Single(p => p.Id == "earth");
|
||||
var origin = UniversePosition.AtCellCenter(system.Coordinate, cellSize);
|
||||
var camera = new FlyCamera(origin, cellSize);
|
||||
camera.SetInitialView(system, new StartupConfiguration()); camera.RequestOceanView([system]);
|
||||
for (var i = 0; i < 400; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||
var current = neighborhood;
|
||||
float[] Capture(string name)
|
||||
{
|
||||
renderer.Render(camera, current, .1); gl.Finish();
|
||||
if (gl.GetError() is var error && error != GLEnum.NoError) throw new InvalidOperationException($"Cloud report GL error: {error}");
|
||||
var pixels = Read(gl, width, height);
|
||||
if (pixels.Any(v => !float.IsFinite(v))) throw new InvalidOperationException("Cloud scene produced non-finite pixels.");
|
||||
WritePng(Path.Combine(directory, name), width, height, pixels, false);
|
||||
return pixels;
|
||||
}
|
||||
float[] Layer()
|
||||
{
|
||||
var target = renderer.CloudLayer ?? throw new InvalidOperationException("Cloud volume was not rendered.");
|
||||
target.Bind(); var pixels = Read(gl, target.Width, target.Height);
|
||||
if (pixels.Any(v => !float.IsFinite(v))) throw new InvalidOperationException("Cloud volume produced non-finite radiance.");
|
||||
var trans = pixels.Where((_, i) => i % 4 == 3).ToArray();
|
||||
if (trans.Any(t => t < 0 || t > 1)) throw new InvalidOperationException("Invalid cloud transmittance.");
|
||||
return pixels;
|
||||
}
|
||||
var day = Capture("earth-clouds-day.png"); var dayLayer = Layer();
|
||||
if (dayLayer.Where((_, i) => i % 4 == 3).Min() > .98) throw new InvalidOperationException("Cloud layer has no visible density.");
|
||||
var clear = system with { Planets = system.Planets.Select(p => p.Clouds is null ? p : p with { Clouds = p.Clouds with { Enabled = false } }).ToArray() };
|
||||
current = neighborhood with { ActiveSystem = clear };
|
||||
var clearPixels = Capture("earth-clouds-disabled.png");
|
||||
if (day.Zip(clearPixels, (a, b) => System.Math.Abs(a - b)).Average() < .001) throw new InvalidOperationException("Disabling clouds did not change the scene.");
|
||||
var radial = (camera.Position.RelativeTo(origin, cellSize) - earth.PositionMeters).Normalized();
|
||||
var sunlight = (system.Stars[0].PositionMeters - earth.PositionMeters).Normalized();
|
||||
var tangent = (sunlight - radial * DVector3.Dot(sunlight, radial)).Normalized();
|
||||
var sunset = system with { Stars = system.Stars.Select((s, i) => i == earth.HostStarIndex ? s with
|
||||
{ PositionMeters = earth.PositionMeters + (tangent + radial * .012).Normalized() * UniverseScale.AstronomicalUnitMeters } : s).ToArray() };
|
||||
current = neighborhood with { ActiveSystem = sunset };
|
||||
Capture("earth-clouds-sunset.png"); var sunsetLayer = Layer();
|
||||
static double Warmth(float[] values) => values.Where((_, i) => i % 4 == 0).Sum(v => (double)v)
|
||||
/ System.Math.Max(.00001, values.Where((_, i) => i % 4 == 2).Sum(v => (double)v));
|
||||
var dayWarmth = Warmth(dayLayer); var sunsetWarmth = Warmth(sunsetLayer);
|
||||
if (sunsetWarmth < dayWarmth * 1.1) throw new InvalidOperationException($"Cloud sunset is not warmer: day {dayWarmth}, sunset {sunsetWarmth}.");
|
||||
current = neighborhood;
|
||||
Capture("earth-clouds-wind-before.png"); var beforeWind = Layer();
|
||||
var timer = System.Diagnostics.Stopwatch.StartNew();
|
||||
for (var i = 0; i < 24; i++) renderer.Render(camera, current, .1);
|
||||
gl.Finish(); timer.Stop(); var afterWind = Layer();
|
||||
var motion = beforeWind.Where((_, i) => i % 4 == 3).Zip(afterWind.Where((_, i) => i % 4 == 3), (a, b) => System.Math.Abs(a - b)).Average();
|
||||
if (motion < .000001) throw new InvalidOperationException("Cloud wind did not move the volume.");
|
||||
camera.RequestCloudView([system]);
|
||||
for (var i = 0; i < 400; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||
Capture("earth-clouds-above.png");
|
||||
for (var i = 0; i < 350; i++) camera.Update(.1, -camera.Up, neighborhood);
|
||||
Capture("earth-clouds-inside.png");
|
||||
camera.RequestFocus([system], "earth");
|
||||
for (var i = 0; i < 400; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||
for (var i = 0; i < 20; i++) renderer.Render(camera, current, .05);
|
||||
Capture("earth-clouds-orbit.png");
|
||||
var orbit = Layer(); var orbitTarget = renderer.CloudLayer!;
|
||||
WritePng(Path.Combine(directory, "earth-clouds-orbit-layer.png"), orbitTarget.Width, orbitTarget.Height, orbit);
|
||||
var alphaImage = orbit.Select((_, i) => i % 4 == 3 ? 1f : orbit[i / 4 * 4 + 3]).ToArray();
|
||||
WritePng(Path.Combine(directory, "earth-clouds-orbit-transmission.png"), orbitTarget.Width, orbitTarget.Height, alphaImage, false);
|
||||
double spatialDelta = 0; var samples = 0;
|
||||
for (var y = orbitTarget.Height / 2 - 20; y < orbitTarget.Height / 2 + 20; y++)
|
||||
for (var x = orbitTarget.Width / 2 - 30; x < orbitTarget.Width / 2 + 30; x++)
|
||||
{
|
||||
var i = (y * orbitTarget.Width + x) * 4 + 3;
|
||||
spatialDelta += System.Math.Abs(orbit[i] - orbit[i + 4]); samples++;
|
||||
}
|
||||
spatialDelta /= samples;
|
||||
if (spatialDelta > .12) throw new InvalidOperationException($"Cloud orbit view has excessive subpixel aliasing: {spatialDelta}.");
|
||||
var thinCuts = 0;
|
||||
for (var y = orbitTarget.Height / 2 - 95; y < orbitTarget.Height / 2 + 95; y++)
|
||||
for (var x = orbitTarget.Width / 2 - 95; x < orbitTarget.Width / 2 + 95; x++)
|
||||
{
|
||||
var dx = x - orbitTarget.Width / 2; var dy = y - orbitTarget.Height / 2;
|
||||
if (dx * dx + dy * dy > 10000) continue;
|
||||
var i = (y * orbitTarget.Width + x) * 4 + 3;
|
||||
if (orbit[i] > .95 && ((orbit[i - 4] < .6 && orbit[i + 4] < .6)
|
||||
|| (orbit[i - orbitTarget.Width * 4] < .6 && orbit[i + orbitTarget.Width * 4] < .6))) thinCuts++;
|
||||
}
|
||||
if (thinCuts > 12) throw new InvalidOperationException($"Cloud depth reconstruction has thin pixel cuts: {thinCuts}.");
|
||||
Console.WriteLine($"Cloud GPU report passed: sunset R/B {dayWarmth:F3} -> {sunsetWarmth:F3}, wind delta {motion:G5}, orbit variation {spatialDelta:G4}, finite ground/inside/orbit views. Full rendering {timer.Elapsed.TotalMilliseconds / 24:F1} ms/frame. Images: {directory}");
|
||||
}
|
||||
|
||||
public static void RunOcean(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
||||
{
|
||||
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
||||
var origin = UniversePosition.AtCellCenter(neighborhood.ActiveSystem.Coordinate, cellSize);
|
||||
var camera = new FlyCamera(origin, cellSize);
|
||||
camera.SetInitialView(neighborhood.ActiveSystem, new StartupConfiguration());
|
||||
camera.RequestOceanView([neighborhood.ActiveSystem]);
|
||||
for (var i = 0; i < 400; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||
for (var i = 0; i < 12; i++) renderer.Render(camera, neighborhood, 1d / 60);
|
||||
float[] Capture(string name)
|
||||
{
|
||||
renderer.Render(camera, neighborhood, .1); gl.Finish();
|
||||
if (gl.GetError() is var error && error != GLEnum.NoError) throw new InvalidOperationException($"Ocean report GL error: {error}");
|
||||
var pixels = Read(gl, width, height);
|
||||
if (pixels.Any(v => !float.IsFinite(v))) throw new InvalidOperationException("Ocean produced non-finite pixels.");
|
||||
WritePng(Path.Combine(directory, name), width, height, pixels, false);
|
||||
return pixels;
|
||||
}
|
||||
var before = Capture("earth-ocean.png");
|
||||
var timer = System.Diagnostics.Stopwatch.StartNew();
|
||||
for (var i = 0; i < 10; i++) renderer.Render(camera, neighborhood, .1);
|
||||
gl.Finish(); timer.Stop();
|
||||
var after = Capture("earth-ocean-animated.png");
|
||||
var difference = before.Zip(after, (a, b) => System.Math.Abs(a - b)).Average();
|
||||
if (difference < .00001) throw new InvalidOperationException("Ocean animation did not change the rendered image.");
|
||||
for (var i = 0; i < 18; i++) camera.Update(.1, -camera.Up, neighborhood);
|
||||
Capture("earth-underwater.png");
|
||||
camera.RequestFocus([neighborhood.ActiveSystem], "earth");
|
||||
for (var i = 0; i < 400; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||
Capture("earth-ocean-orbit.png");
|
||||
Console.WriteLine($"Ocean scene passed: finite frames, wave animation delta {difference:G5}, underwater and orbit views. Full rendering {timer.Elapsed.TotalMilliseconds / 10:F1} ms/frame. Images: {directory}");
|
||||
}
|
||||
public static void RunScene(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
|
||||
{
|
||||
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
||||
var system = neighborhood.ActiveSystem;
|
||||
var origin = UniversePosition.AtCellCenter(system.Coordinate, cellSize);
|
||||
var camera = new FlyCamera(origin, cellSize);
|
||||
camera.SetInitialView(system, new StartupConfiguration()); camera.RequestRingView([system]);
|
||||
for (var i = 0; i < 900; i++) camera.Update(1d / 60, DVector3.Zero, neighborhood);
|
||||
void Capture(string name)
|
||||
{
|
||||
renderer.Render(camera, neighborhood, 1d / 60); gl.Finish();
|
||||
if (gl.GetError() is var error && error != GLEnum.NoError) throw new InvalidOperationException($"Scene report GL error: {error}");
|
||||
WritePng(Path.Combine(directory, name), width, height, Read(gl, width, height), false);
|
||||
}
|
||||
Capture("saturn-ring-scene.png");
|
||||
var planet = system.Planets.Single(p => p.Id == "saturn");
|
||||
var bodyCamera = planet.Orientation.ToBody(camera.Position.RelativeTo(origin, cellSize) - planet.PositionMeters);
|
||||
var frame = new RingRockStreamer(new RingProfile(planet.Rings!)).Update(bodyCamera);
|
||||
var rock = frame.Rocks.Where(r => r.Radius > .3).MaxBy(r => r.Radius / ((r.Position - bodyCamera).Length + 1));
|
||||
var world = planet.PositionMeters + planet.Orientation.ToWorld(rock.Position);
|
||||
var sun = (system.Stars[planet.HostStarIndex].PositionMeters - world).Normalized();
|
||||
var side = DVector3.Cross(sun, new(0, 1, 0)).Normalized();
|
||||
var radial = (sun * .62 + side * .78).Normalized();
|
||||
var body = new PlanetData(0, "Diagnostic asteroid", world, rock.Radius, 0, 0, Vector3.One, Vector3.Zero, 0, 0, 0, 1) { Id = "diagnostic" };
|
||||
camera.SetInitialView(system with { Planets = [body] }, new StartupConfiguration
|
||||
{
|
||||
PlanetId = "diagnostic",
|
||||
DistanceInRadii = 3.8,
|
||||
FieldOfViewDegrees = 50,
|
||||
LongitudeDegrees = System.Math.Atan2(radial.Z, radial.X) * 180 / System.Math.PI,
|
||||
LatitudeDegrees = System.Math.Asin(radial.Y) * 180 / System.Math.PI
|
||||
});
|
||||
Capture("asteroid-in-rings.png");
|
||||
camera = new FlyCamera(origin.Add(new DVector3(30 * UniverseScale.AstronomicalUnitMeters, 0, 0), cellSize), cellSize);
|
||||
camera.Rotate(-System.Math.PI / (2 * .0022), 0); Capture("sun-from-30au.png");
|
||||
Console.WriteLine($"Integrated scene report passed: rings, asteroid close-up and sunlight at 30 AU. Images: {directory}");
|
||||
}
|
||||
public static void Run(GL gl, GpuMesh fullscreen, ShaderProgram rocks, ShaderProgram glow, uint emptyRing)
|
||||
{
|
||||
var directory = Path.Combine(AppContext.BaseDirectory, "graphics-report"); Directory.CreateDirectory(directory);
|
||||
const int size = 768;
|
||||
using (var fieldTarget = new RenderTarget(gl, 64, 64, fullPrecision: true))
|
||||
using (var field = ShaderProgram.Create(gl, ShaderSources.Load("fullscreen.vert"), "#version 330 core\n" + ShaderSources.Load("noise") + "uniform vec3 uRockMaterial;\n" + ShaderSources.Load("asteroid-shape") + "\nin vec2 vUv;out vec4 fragColor;void main(){vec3 d=normalize(vec3(vUv*2.0-1.0,1));fragColor=vec4(asteroidPosition(d,.271),asteroidHeight(d,.271));}"))
|
||||
{
|
||||
fieldTarget.Bind(); gl.Disable(GLEnum.DepthTest); field.Use(); field.Set("uNoise", 4); field.Set("uRockMaterial", new Vector3(.20f, .86f, .04f)); UniverseRenderer.BindAsteroidSettings(field, new()); fullscreen.Draw();
|
||||
var values = Read(gl, 64, 64); var heights = values.Where((_, i) => i % 4 == 3).ToArray();
|
||||
Console.WriteLine($"Asteroid field: height min {heights.Min()}, max {heights.Max()}; GL {gl.GetError()}");
|
||||
Console.WriteLine($"Asteroid neighbor delta: {Enumerable.Range(0, 64 * 63).Max(i => MathF.Abs(heights[i + 64] - heights[i]))}");
|
||||
}
|
||||
using var asteroid = new RenderTarget(gl, size, size, true);
|
||||
using var mesh = new InstancedAsteroidMesh(gl, 6);
|
||||
var rotation = Quaternion.CreateFromYawPitchRoll(.3f, -.1f, 1.25f);
|
||||
mesh.Upload([0, 0, 0, 1, .62f, .61f, .58f, .271f, rotation.X, rotation.Y, rotation.Z, rotation.W]);
|
||||
asteroid.Bind(); gl.ClearColor(0, 0, 0, 1); gl.ClearDepth(1); gl.DepthMask(true);
|
||||
gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); gl.Enable(GLEnum.DepthTest); gl.Disable(GLEnum.Blend);
|
||||
rocks.Use();
|
||||
rocks.SetMatrix("uViewProjection", Matrix4x4.CreatePerspectiveFieldOfView(50 * MathF.PI / 180, 1, .02f, 100000));
|
||||
rocks.SetMatrix("uBodyToWorld", Matrix4x4.Identity); rocks.SetMatrix("uWorldToBody", Matrix4x4.Identity);
|
||||
rocks.Set("uAnchorWorld", new Vector3(0, 0, -3.3f)); rocks.Set("uPixelScale", size / (2 * MathF.Tan(25 * MathF.PI / 180)));
|
||||
rocks.Set("uFar", 100000f); rocks.Set("uRadius", 1f); rocks.Set("uCameraBody", new Vector3(0, 0, 20)); rocks.Set("uPolarScale", 1f);
|
||||
rocks.Set("uNoise", 4); rocks.Set("uHasRings", 0); rocks.Set("uRingProfile", 7); rocks.Set("uRingShadowSoftness", .001f);
|
||||
gl.ActiveTexture(GLEnum.Texture7); gl.BindTexture(GLEnum.Texture2D, emptyRing);
|
||||
rocks.Set("uRockLod", new Vector3(600, 180, 3.5f)); rocks.Set("uRockFade", new Vector3(2600, 3500, 0));
|
||||
rocks.Set("uRockMaterial", new Vector3(.20f, .86f, .04f)); rocks.Set("uRockLighting", new Vector3(.85f, .008f, 0));
|
||||
rocks.Set("uLightCount", 1); rocks.Set("uLightDirections[0]", Vector3.Normalize(new(-.75f, .55f, .45f)));
|
||||
rocks.Set("uLightColors[0]", Vector3.One); rocks.Set("uLightFlux[0]", 1f);
|
||||
UniverseRenderer.BindAsteroidSettings(rocks, new()); mesh.Draw();
|
||||
var asteroidPixels = Read(gl, size, size);
|
||||
if (asteroidPixels.Any(x => !float.IsFinite(x)) || asteroidPixels.Where((_, i) => i % 4 != 3).Max() < .1)
|
||||
throw new InvalidOperationException("Asteroid shader produced invalid or empty output.");
|
||||
WritePng(Path.Combine(directory, "asteroid-detail.png"), size, size, asteroidPixels);
|
||||
gl.Finish(); var timer = System.Diagnostics.Stopwatch.StartNew();
|
||||
gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); mesh.Draw(); gl.Finish(); timer.Stop();
|
||||
Console.WriteLine($"Asteroid close-up GPU render: {timer.Elapsed.TotalMilliseconds:F2} ms at {size}x{size}.");
|
||||
rocks.Set("uDebugNormals", 1); gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); mesh.Draw(); WritePng(Path.Combine(directory, "asteroid-normals.png"), size, size, Read(gl, size, size));
|
||||
rocks.Set("uDebugNormals", 2); gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); mesh.Draw(); WritePng(Path.Combine(directory, "asteroid-solid.png"), size, size, Read(gl, size, size)); rocks.Set("uDebugNormals", 0);
|
||||
|
||||
using var source = new RenderTarget(gl, size, size, true); using var target = new RenderTarget(gl, size, size);
|
||||
source.Bind(); gl.DepthMask(true); gl.ClearColor(0, 0, 0, 1); gl.ClearDepth(1);
|
||||
gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||
target.Bind(); gl.Disable(GLEnum.DepthTest); gl.DepthMask(false); glow.Use();
|
||||
gl.ActiveTexture(GLEnum.Texture0); gl.BindTexture(GLEnum.Texture2D, source.Color);
|
||||
gl.ActiveTexture(GLEnum.Texture1); gl.BindTexture(GLEnum.Texture2D, source.Depth);
|
||||
glow.Set("uScene", 0); glow.Set("uDepth", 1); glow.Set("uAspect", 1f); glow.Set("uStarCount", 2);
|
||||
var settings = new StarGlowSettings();
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
glow.Set($"uStarPosition[{i}]", new Vector3(i == 0 ? .25f : .75f, i == 0 ? .65f : .35f, .6f));
|
||||
var intensity = (float)settings.Attenuation(UniverseScale.AstronomicalUnitMeters * (i == 0 ? 1 : 30), 1);
|
||||
glow.Set($"uStarShape[{i}]", new Vector3(.0015f, intensity, 1.2f / size));
|
||||
glow.Set($"uStarHalo[{i}]", new Vector3(.035f, .3f, 1.2f)); glow.Set($"uStarRays[{i}]", new Vector3(.09f, 10, 70));
|
||||
glow.Set($"uStarGhosts[{i}]", new Vector3(.14f, .42f, .2f)); glow.Set($"uStarColors[{i}]", i == 0 ? new Vector3(1, .94f, .8f) : new Vector3(.72f, .82f, 1));
|
||||
glow.Set($"uStarRotation[{i}]", i * .35f);
|
||||
}
|
||||
fullscreen.Draw(); var lit = Read(gl, size, size);
|
||||
float Peak(int cx, int cy)
|
||||
{
|
||||
var peak = 0f; for (var y = cy - 2; y <= cy + 2; y++) for (var x = cx - 2; x <= cx + 2; x++) peak = MathF.Max(peak, lit[(y * size + x) * 4]); return peak;
|
||||
}
|
||||
var near = Peak(size / 4, (int)(size * .65)); var far = Peak(size * 3 / 4, (int)(size * .35));
|
||||
if (near < far * 2 || far < .03f) throw new InvalidOperationException($"Glow attenuation failed: near {near}, far {far}.");
|
||||
WritePng(Path.Combine(directory, "sun-glow.png"), size, size, lit);
|
||||
source.Bind(); gl.DepthMask(true); gl.ClearDepth(0); gl.Clear(ClearBufferMask.DepthBufferBit); gl.ClearDepth(1);
|
||||
target.Bind(); gl.DepthMask(false); fullscreen.Draw(); var occluded = Read(gl, size, size);
|
||||
if (occluded.Where((_, i) => i % 4 != 3).Max() > .00001f) throw new InvalidOperationException("Solar glare leaked through an occluder.");
|
||||
Console.WriteLine($"Graphics report: asteroid LOD6 {IcosphereMesh.Build(6).Indices.Length / 3} triangles; glow 1 AU={near:G5}, 30 AU={far:G5}; occlusion passed. Images: {directory}");
|
||||
gl.BindFramebuffer(GLEnum.Framebuffer, 0); gl.DepthMask(true); gl.Enable(GLEnum.DepthTest);
|
||||
}
|
||||
private static unsafe float[] Read(GL gl, int width, int height)
|
||||
{ var result = new float[width * height * 4]; fixed (float* p = result) gl.ReadPixels(0, 0, (uint)width, (uint)height, PixelFormat.Rgba, PixelType.Float, p); return result; }
|
||||
private static void WritePng(string path, int width, int height, float[] hdr, bool toneMap = true)
|
||||
{
|
||||
using var output = File.Create(path); output.Write(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 });
|
||||
var header = new byte[13]; BinaryPrimitives.WriteInt32BigEndian(header, width); BinaryPrimitives.WriteInt32BigEndian(header.AsSpan(4), height); header[8] = 8; header[9] = 2;
|
||||
Chunk(output, "IHDR", header);
|
||||
using var compressed = new MemoryStream();
|
||||
using (var zlib = new ZLibStream(compressed, CompressionLevel.Optimal, true))
|
||||
for (var y = height - 1; y >= 0; y--)
|
||||
{
|
||||
zlib.WriteByte(0);
|
||||
for (var x = 0; x < width; x++) for (var c = 0; c < 3; c++)
|
||||
{
|
||||
var value = MathF.Max(0, hdr[(y * width + x) * 4 + c]);
|
||||
if (toneMap) { value *= .88f; value = System.Math.Clamp((value * (2.51f * value + .03f)) / (value * (2.43f * value + .59f) + .14f), 0, 1); value = MathF.Pow(value, 1 / 2.2f); }
|
||||
zlib.WriteByte((byte)(System.Math.Clamp(value, 0, 1) * 255 + .5f));
|
||||
}
|
||||
}
|
||||
Chunk(output, "IDAT", compressed.ToArray()); Chunk(output, "IEND", []);
|
||||
}
|
||||
private static void Chunk(Stream output, string type, byte[] data)
|
||||
{
|
||||
var name = System.Text.Encoding.ASCII.GetBytes(type); var length = new byte[4]; BinaryPrimitives.WriteInt32BigEndian(length, data.Length);
|
||||
output.Write(length); output.Write(name); output.Write(data); uint crc = 0xffffffff;
|
||||
foreach (var b in name.Concat(data)) { crc ^= b; for (var k = 0; k < 8; k++) crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xedb88320 : crc >> 1; }
|
||||
BinaryPrimitives.WriteUInt32BigEndian(length, ~crc); output.Write(length);
|
||||
}
|
||||
}
|
||||
40
src/Universe.App/Graphics/InstancedAsteroidMesh.cs
Normal file
40
src/Universe.App/Graphics/InstancedAsteroidMesh.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed unsafe class InstancedAsteroidMesh : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly uint _vao, _vertices, _instances, _indices;
|
||||
private readonly uint _indexCount;
|
||||
private int _count;
|
||||
public InstancedAsteroidMesh(GL gl, int subdivision)
|
||||
{
|
||||
_gl = gl; var mesh = IcosphereMesh.Build(subdivision); _indexCount = (uint)mesh.Indices.Length;
|
||||
_vao = gl.GenVertexArray(); _vertices = gl.GenBuffer(); _instances = gl.GenBuffer(); _indices = gl.GenBuffer();
|
||||
gl.BindVertexArray(_vao); gl.BindBuffer(GLEnum.ArrayBuffer, _vertices);
|
||||
fixed (float* p = mesh.Vertices) gl.BufferData(GLEnum.ArrayBuffer, (nuint)(mesh.Vertices.Length * 4), p, GLEnum.StaticDraw);
|
||||
Attribute(gl, 0, 3, 24, 0); Attribute(gl, 1, 3, 24, 12);
|
||||
gl.BindBuffer(GLEnum.ElementArrayBuffer, _indices);
|
||||
fixed (uint* p = mesh.Indices) gl.BufferData(GLEnum.ElementArrayBuffer, (nuint)(mesh.Indices.Length * 4), p, GLEnum.StaticDraw);
|
||||
ConfigureInstances(gl, _instances, 1); gl.BindVertexArray(0);
|
||||
}
|
||||
private static void Attribute(GL gl, uint location, int count, uint stride, int offset)
|
||||
{ gl.EnableVertexAttribArray(location); gl.VertexAttribPointer(location, count, VertexAttribPointerType.Float, false, stride, (void*)offset); }
|
||||
internal static void ConfigureInstances(GL gl, uint buffer, uint divisor)
|
||||
{
|
||||
gl.BindBuffer(GLEnum.ArrayBuffer, buffer);
|
||||
Attribute(gl, 2, 3, 48, 0); Attribute(gl, 3, 1, 48, 12); Attribute(gl, 4, 3, 48, 16); Attribute(gl, 5, 1, 48, 28); Attribute(gl, 6, 4, 48, 32);
|
||||
for (uint i = 2; i <= 6; i++) gl.VertexAttribDivisor(i, divisor);
|
||||
}
|
||||
public void Upload(float[] data)
|
||||
{
|
||||
_count = data.Length / 12; _gl.BindBuffer(GLEnum.ArrayBuffer, _instances);
|
||||
fixed (float* p = data) _gl.BufferData(GLEnum.ArrayBuffer, (nuint)(data.Length * 4), p, GLEnum.DynamicDraw);
|
||||
}
|
||||
public void Draw()
|
||||
{ if (_count == 0) return; _gl.BindVertexArray(_vao); _gl.DrawElementsInstanced(GLEnum.Triangles, _indexCount, DrawElementsType.UnsignedInt, null, (uint)_count); _gl.BindVertexArray(0); }
|
||||
public void Dispose()
|
||||
{ _gl.DeleteVertexArray(_vao); _gl.DeleteBuffer(_vertices); _gl.DeleteBuffer(_instances); _gl.DeleteBuffer(_indices); }
|
||||
}
|
||||
24
src/Universe.App/Graphics/NoiseTexture.cs
Normal file
24
src/Universe.App/Graphics/NoiseTexture.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed unsafe class NoiseTexture : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
public uint Handle { get; }
|
||||
public NoiseTexture(GL gl)
|
||||
{
|
||||
_gl = gl;
|
||||
Handle = gl.GenTexture();
|
||||
gl.BindTexture(GLEnum.Texture3D, Handle);
|
||||
fixed (float* data = ProceduralNoise.PackedGradients)
|
||||
gl.TexImage3D(GLEnum.Texture3D, 0, InternalFormat.Rgba32f, ProceduralNoise.Size, ProceduralNoise.Size, ProceduralNoise.Size, 0, PixelFormat.Rgba, PixelType.Float, data);
|
||||
gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureMinFilter, (int)GLEnum.Linear);
|
||||
gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureMagFilter, (int)GLEnum.Linear);
|
||||
gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureWrapS, (int)GLEnum.Repeat);
|
||||
gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureWrapT, (int)GLEnum.Repeat);
|
||||
gl.TexParameter(GLEnum.Texture3D, GLEnum.TextureWrapR, (int)GLEnum.Repeat);
|
||||
}
|
||||
public void Dispose() => _gl.DeleteTexture(Handle);
|
||||
}
|
||||
89
src/Universe.App/Graphics/PauseMenu.cs
Normal file
89
src/Universe.App/Graphics/PauseMenu.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
/// <summary>Small resolution-independent menu using a built-in bitmap font.</summary>
|
||||
internal sealed class PauseMenu : IDisposable
|
||||
{
|
||||
private readonly GpuMesh _mesh;
|
||||
public int Selected { get; set; }
|
||||
public PauseMenu(GL gl) => _mesh = new(gl, 6, [], dynamic: true);
|
||||
private static readonly string[] Labels = ["PLAY", "FREE CAMERA", "DEBUG OFF", "BENCHMARK EARTH", "BENCHMARK UNKNOWN", "QUIT"];
|
||||
internal static readonly Dictionary<char, string[]> Font = new()
|
||||
{
|
||||
['A']=["01110","10001","10001","11111","10001","10001","10001"], ['C']=["01111","10000","10000","10000","10000","10000","01111"],
|
||||
['D']=["11110","10001","10001","10001","10001","10001","11110"], ['E']=["11111","10000","10000","11110","10000","10000","11111"],
|
||||
['F']=["11111","10000","10000","11110","10000","10000","10000"], ['I']=["11111","00100","00100","00100","00100","00100","11111"],
|
||||
['B']=["11110","10001","10001","11110","10001","10001","11110"], ['G']=["01111","10000","10000","10111","10001","10001","01111"],
|
||||
['H']=["10001","10001","10001","11111","10001","10001","10001"], ['N']=["10001","11001","10101","10011","10001","10001","10001"],
|
||||
['V']=["10001","10001","10001","10001","10001","01010","00100"], ['W']=["10001","10001","10001","10101","10101","10101","01010"],
|
||||
['K']=["10001","10010","10100","11000","10100","10010","10001"],
|
||||
['L']=["10000","10000","10000","10000","10000","10000","11111"], ['M']=["10001","11011","10101","10101","10001","10001","10001"],
|
||||
['O']=["01110","10001","10001","10001","10001","10001","01110"], ['P']=["11110","10001","10001","11110","10000","10000","10000"],
|
||||
['Q']=["01110","10001","10001","10001","10101","10010","01101"], ['R']=["11110","10001","10001","11110","10100","10010","10001"],
|
||||
['S']=["01111","10000","10000","01110","00001","00001","11110"], ['T']=["11111","00100","00100","00100","00100","00100","00100"],
|
||||
['U']=["10001","10001","10001","10001","10001","10001","01110"], ['Y']=["10001","10001","01010","00100","00100","00100","00100"],
|
||||
['0']=["01110","10001","10011","10101","11001","10001","01110"], ['1']=["00100","01100","00100","00100","00100","00100","01110"],
|
||||
['2']=["01110","10001","00001","00010","00100","01000","11111"], ['3']=["11110","00001","00001","01110","00001","00001","11110"],
|
||||
['4']=["00010","00110","01010","10010","11111","00010","00010"], ['5']=["11111","10000","10000","11110","00001","00001","11110"],
|
||||
['6']=["01110","10000","10000","11110","10001","10001","01110"], ['7']=["11111","00001","00010","00100","01000","01000","01000"],
|
||||
['8']=["01110","10001","10001","01110","10001","10001","01110"], ['9']=["01110","10001","10001","01111","00001","00001","01110"],
|
||||
[':']=["00000","00110","00110","00000","00110","00110","00000"], ['.']=["00000","00000","00000","00000","00000","00110","00110"],
|
||||
[',']=["00000","00000","00000","00000","00110","00110","00100"], ['/']=["00001","00010","00010","00100","01000","01000","10000"],
|
||||
['+']=["00000","00100","00100","11111","00100","00100","00000"], ['-']=["00000","00000","00000","11111","00000","00000","00000"]
|
||||
};
|
||||
|
||||
private static (double Scale, double X, double Y) Layout(int width, int height)
|
||||
{
|
||||
var s = System.Math.Min(width / 1440d, height / 900d);
|
||||
return (s, width / 2d, height / 2d);
|
||||
}
|
||||
|
||||
public int Hit(double x, double y, int width, int height)
|
||||
{
|
||||
var (s, cx, cy) = Layout(width, height);
|
||||
if (s <= 0 || x < cx - 190 * s || x > cx + 190 * s) return -1;
|
||||
for (var i = 0; i < Labels.Length; i++)
|
||||
if (y >= cy + (-174 + i * 57) * s && y <= cy + (-130 + i * 57) * s) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Draw(int width, int height, bool debugEnabled)
|
||||
{
|
||||
var (s, cx, cy) = Layout(width, height);
|
||||
var data = new List<float>();
|
||||
void Rect(double x, double y, double w, double h, float r, float g, float b)
|
||||
{
|
||||
float X(double n) => (float)(2 * n / width - 1);
|
||||
float Y(double n) => (float)(1 - 2 * n / height);
|
||||
void V(double px, double py) { data.Add(X(px)); data.Add(Y(py)); data.Add(0); data.Add(r); data.Add(g); data.Add(b); }
|
||||
V(x,y); V(x+w,y); V(x+w,y+h); V(x,y); V(x+w,y+h); V(x,y+h);
|
||||
}
|
||||
void Text(string text, double centerX, double y, double unit, float r, float g, float b)
|
||||
{
|
||||
var x = centerX - text.Length * 6 * unit / 2;
|
||||
foreach (var c in text)
|
||||
{
|
||||
if (Font.TryGetValue(c, out var glyph))
|
||||
for (var row = 0; row < 7; row++) for (var col = 0; col < 5; col++)
|
||||
if (glyph[row][col] == '1') Rect(x + col * unit, y + row * unit, unit * .9, unit * .9, r, g, b);
|
||||
x += 6 * unit;
|
||||
}
|
||||
}
|
||||
Rect(cx - 230*s, cy - 260*s, 460*s, 520*s, .025f, .04f, .075f);
|
||||
Rect(cx - 230*s, cy - 260*s, 460*s, 4*s, .25f, .65f, .9f);
|
||||
Text("PAUSED", cx, cy - 226*s, 5*s, .9f, .95f, 1);
|
||||
for (var i = 0; i < Labels.Length; i++)
|
||||
{
|
||||
var selected = i == Selected;
|
||||
var y = cy + (-174 + i * 57) * s;
|
||||
Rect(cx - 190*s, y, 380*s, 44*s, selected ? .12f : .055f, selected ? .25f : .10f, selected ? .36f : .16f);
|
||||
if (selected) Rect(cx - 190*s, y, 4*s, 44*s, .35f, .8f, 1);
|
||||
var label = i == 2 ? debugEnabled ? "DEBUG ON" : "DEBUG OFF" : Labels[i];
|
||||
Text(label, cx, y + 12*s, (i >= 3 ? 3.35 : 4) * s, .92f, .95f, 1);
|
||||
}
|
||||
Text("P OR ESC TO RESUME", cx, cy + 220*s, 2*s, .6f, .7f, .8f);
|
||||
_mesh.ReplaceVertices(data.ToArray()); _mesh.Draw(GLEnum.Triangles);
|
||||
}
|
||||
public void Dispose() => _mesh.Dispose();
|
||||
}
|
||||
113
src/Universe.App/Graphics/PerformanceOverlay.cs
Normal file
113
src/Universe.App/Graphics/PerformanceOverlay.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System.Globalization;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed class PerformanceOverlay : IDisposable
|
||||
{
|
||||
private readonly GpuMesh _mesh;
|
||||
|
||||
public PerformanceOverlay(GL gl) => _mesh = new(gl, 6, [], dynamic: true);
|
||||
|
||||
public void Draw(int width, int height, IReadOnlyList<KeyValuePair<string, double>> samples,
|
||||
double cpuMilliseconds, double fps, int terrainTiles, int terrainLod, int pendingTiles, string? probePlanetId)
|
||||
{
|
||||
var values = samples.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal);
|
||||
var rows = new List<(string Label, string Value, bool Emphasis)>();
|
||||
AddMetric("GPU FRAME", "GPU FRAME", true);
|
||||
AddMetric("SKY / STARFIELD", "SKY / STARFIELD");
|
||||
AddMetric("PLANETS TOTAL", "PLANETS TOTAL");
|
||||
foreach (var key in values.Keys.Where(k => k.StartsWith("PLANET ", StringComparison.Ordinal) && k != "PLANETS TOTAL" && k != "GPU FRAME")
|
||||
.OrderBy(k => k, StringComparer.Ordinal))
|
||||
AddMetric(key, key);
|
||||
if (probePlanetId is { } probeId)
|
||||
{
|
||||
var prefix = probeId.ToUpperInvariant();
|
||||
AddSaving($"{prefix} SAVE MATERIALS", "SAVE MATERIALS");
|
||||
AddSaving($"{prefix} SAVE CLOUD SHADOWS", "SAVE CLOUD SHADOWS");
|
||||
AddSaving($"{prefix} SAVE MOUNTAIN SHADOWS", "SAVE MOUNTAIN SHADOWS");
|
||||
AddSaving($"{prefix} SAVE BODY SHADOWS", "SAVE BODY SHADOWS");
|
||||
AddSaving($"{prefix} SAVE SURFACE NOISE", "SAVE SURFACE NOISE");
|
||||
}
|
||||
AddMetric("STARS", "STARS");
|
||||
AddMetric("GAS GIANTS", "GAS GIANTS");
|
||||
AddMetric("RINGS / ROCKS", "RINGS / ROCKS");
|
||||
AddMetric("OCEAN", "OCEAN");
|
||||
AddMetric("ATMOSPHERE", "ATMOSPHERE");
|
||||
AddMetric("CLOUDS / COMPOSITE", "CLOUDS / COMPOSITE");
|
||||
AddMetric("SUN GLOW", "SUN GLOW");
|
||||
AddMetric("BLOOM / POST", "BLOOM / POST");
|
||||
rows.Add(("SHADOWS IN PLANET", "SEE PLANET MS", false));
|
||||
rows.Add(("CPU SUBMIT", $"{Number(cpuMilliseconds)} MS", false));
|
||||
rows.Add(("FPS", $"{Number(fps, "F1")}", false));
|
||||
rows.Add(("TERRAIN TILES", terrainTiles.ToString(CultureInfo.InvariantCulture), false));
|
||||
rows.Add(("LOD / PENDING", $"{terrainLod} / {pendingTiles}", false));
|
||||
|
||||
var scale = System.Math.Clamp(System.Math.Min(width / 1440d, height / 900d), .58, 1.25);
|
||||
var x = 14 * scale;
|
||||
var top = 14 * scale;
|
||||
var panelWidth = 438 * scale;
|
||||
var rowHeight = 17 * scale;
|
||||
var unit = 1.65 * scale;
|
||||
var panelHeight = 42 * scale + rowHeight * rows.Count + 20 * scale;
|
||||
var data = new List<float>(12000);
|
||||
|
||||
void Rect(double px, double py, double w, double h, float r, float g, float b)
|
||||
{
|
||||
float X(double n) => (float)(2 * n / width - 1);
|
||||
float Y(double n) => (float)(1 - 2 * n / height);
|
||||
void Vertex(double vx, double vy) { data.Add(X(vx)); data.Add(Y(vy)); data.Add(0); data.Add(r); data.Add(g); data.Add(b); }
|
||||
Vertex(px, py); Vertex(px + w, py); Vertex(px + w, py + h);
|
||||
Vertex(px, py); Vertex(px + w, py + h); Vertex(px, py + h);
|
||||
}
|
||||
|
||||
void Text(string value, double px, double py, double glyphScale, float r, float g, float b)
|
||||
{
|
||||
foreach (var c in value)
|
||||
{
|
||||
if (PauseMenu.Font.TryGetValue(c, out var glyph))
|
||||
for (var row = 0; row < glyph.Length; row++)
|
||||
for (var col = 0; col < glyph[row].Length; col++)
|
||||
if (glyph[row][col] == '1') Rect(px + col * glyphScale, py + row * glyphScale,
|
||||
glyphScale * .9, glyphScale * .9, r, g, b);
|
||||
px += 6 * glyphScale;
|
||||
}
|
||||
}
|
||||
|
||||
Rect(x, top, panelWidth, panelHeight, .018f, .026f, .045f);
|
||||
Rect(x, top, 3 * scale, panelHeight, .13f, .48f, .72f);
|
||||
Text("DEBUG / GPU MS", x + 16 * scale, top + 10 * scale, unit, .72f, .88f, 1f);
|
||||
Text("ASYNC; PROBE 1 / 8 FRAMES", x + 16 * scale, top + 26 * scale, unit * .8, .40f, .60f, .72f);
|
||||
var y = top + 49 * scale;
|
||||
foreach (var (label, value, emphasis) in rows)
|
||||
{
|
||||
var color = emphasis ? (R: .55f, G: .88f, B: 1f) : (R: .78f, G: .84f, B: .9f);
|
||||
Text(label, x + 16 * scale, y, unit, color.R, color.G, color.B);
|
||||
var valueX = x + panelWidth - (value.Length * 6 * unit) - 14 * scale;
|
||||
Text(value, valueX, y, unit, emphasis ? .9f : .58f, emphasis ? .95f : .7f, emphasis ? 1f : .78f);
|
||||
y += rowHeight;
|
||||
}
|
||||
Text("SAVINGS ARE DIFFERENTIAL", x + 16 * scale, top + panelHeight - 15 * scale,
|
||||
unit * .72, .36f, .52f, .65f);
|
||||
|
||||
_mesh.ReplaceVertices(data.ToArray());
|
||||
_mesh.Draw(GLEnum.Triangles);
|
||||
|
||||
void AddMetric(string label, string key, bool emphasis = false)
|
||||
{
|
||||
rows.Add((label, values.TryGetValue(key, out var time) ? $"{Number(time)} MS" : "-- MS", emphasis));
|
||||
}
|
||||
void AddSaving(string label, string key)
|
||||
{
|
||||
var value = values.TryGetValue(key, out var delta)
|
||||
? $"{(delta >= 0 ? "+" : "-")}{Number(System.Math.Abs(delta))} MS"
|
||||
: "-- MS";
|
||||
rows.Add((label, value, false));
|
||||
}
|
||||
}
|
||||
|
||||
private static string Number(double value, string format = "F2") =>
|
||||
value.ToString(format, CultureInfo.GetCultureInfo("sv-SE"));
|
||||
|
||||
public void Dispose() => _mesh.Dispose();
|
||||
}
|
||||
58
src/Universe.App/Graphics/PlanetTextures.cs
Normal file
58
src/Universe.App/Graphics/PlanetTextures.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed unsafe class PlanetTextures : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
public uint Color { get; }
|
||||
public uint Height { get; }
|
||||
public uint OceanMask { get; }
|
||||
public uint RegionalHeight { get; }
|
||||
|
||||
public PlanetTextures(GL gl, PlanetData planet)
|
||||
{
|
||||
_gl = gl;
|
||||
Color = gl.GenTexture();
|
||||
gl.BindTexture(GLEnum.Texture2D, Color);
|
||||
var image = planet.SurfaceTexture;
|
||||
var pixels = image?.Rgba ?? [255, 255, 255, 255];
|
||||
fixed (byte* data = pixels)
|
||||
gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.Srgb8Alpha8, (uint)(image?.Width ?? 1), (uint)(image?.Height ?? 1), 0, PixelFormat.Rgba, PixelType.UnsignedByte, data);
|
||||
SetParameters(gl, true);
|
||||
gl.GenerateMipmap(GLEnum.Texture2D);
|
||||
Height = gl.GenTexture();
|
||||
gl.BindTexture(GLEnum.Texture2D, Height);
|
||||
var map = planet.Terrain.HeightMap;
|
||||
var heights = map?.Samples ?? [0f];
|
||||
fixed (float* data = heights)
|
||||
gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.R32f, (uint)(map?.Width ?? 1), (uint)(map?.Height ?? 1), 0, PixelFormat.Red, PixelType.Float, data);
|
||||
SetParameters(gl, false);
|
||||
RegionalHeight = gl.GenTexture();
|
||||
gl.BindTexture(GLEnum.Texture2D, RegionalHeight);
|
||||
var regional = planet.Terrain.RegionalHeight;
|
||||
var regionalSamples = regional?.Meters ?? [0f];
|
||||
fixed (float* data = regionalSamples)
|
||||
gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.R32f, (uint)(regional?.Width ?? 1), (uint)(regional?.Height ?? 1), 0, PixelFormat.Red, PixelType.Float, data);
|
||||
SetParameters(gl, false);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapS, (int)GLEnum.ClampToEdge);
|
||||
OceanMask = gl.GenTexture();
|
||||
gl.BindTexture(GLEnum.Texture2D, OceanMask);
|
||||
var mask = planet.OceanMask;
|
||||
var samples = mask?.Samples ?? [0f];
|
||||
fixed (float* data = samples)
|
||||
gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.R32f, (uint)(mask?.Width ?? 1), (uint)(mask?.Height ?? 1), 0, PixelFormat.Red, PixelType.Float, data);
|
||||
SetParameters(gl, false);
|
||||
}
|
||||
|
||||
private static void SetParameters(GL gl, bool mipmaps)
|
||||
{
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)(mipmaps ? GLEnum.LinearMipmapLinear : GLEnum.Linear));
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMagFilter, (int)GLEnum.Linear);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapS, (int)GLEnum.Repeat);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapT, (int)GLEnum.ClampToEdge);
|
||||
}
|
||||
|
||||
public void Dispose() { _gl.DeleteTexture(Color); _gl.DeleteTexture(Height); _gl.DeleteTexture(OceanMask); _gl.DeleteTexture(RegionalHeight); }
|
||||
}
|
||||
67
src/Universe.App/Graphics/PrimitiveMeshes.cs
Normal file
67
src/Universe.App/Graphics/PrimitiveMeshes.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal static class PrimitiveMeshes
|
||||
{
|
||||
public static (float[] Vertices, uint[] Indices) Ring(float innerRadius, float outerRadius, int segments = 384)
|
||||
{
|
||||
var vertices = new float[(segments + 1) * 2 * 6];
|
||||
var indices = new uint[segments * 6];
|
||||
for (var i = 0; i <= segments; i++)
|
||||
{
|
||||
var angle = i * System.Math.Tau / segments;
|
||||
for (var edge = 0; edge < 2; edge++)
|
||||
{
|
||||
var r = edge == 0 ? innerRadius : outerRadius;
|
||||
var v = (i * 2 + edge) * 6;
|
||||
vertices[v] = (float)System.Math.Cos(angle) * r;
|
||||
vertices[v + 2] = (float)System.Math.Sin(angle) * r;
|
||||
vertices[v + 4] = 1;
|
||||
}
|
||||
if (i == segments) continue;
|
||||
var n = (uint)(i * 2); var offset = i * 6;
|
||||
indices[offset] = n; indices[offset + 1] = n + 1; indices[offset + 2] = n + 2;
|
||||
indices[offset + 3] = n + 1; indices[offset + 4] = n + 3; indices[offset + 5] = n + 2;
|
||||
}
|
||||
return (vertices, indices);
|
||||
}
|
||||
public static (float[] Vertices, uint[] Indices) UnitSphere(int latitudeSegments = 24, int longitudeSegments = 32)
|
||||
{
|
||||
var vertices = new float[(latitudeSegments + 1) * (longitudeSegments + 1) * 6];
|
||||
var indices = new uint[latitudeSegments * longitudeSegments * 6];
|
||||
var write = 0;
|
||||
for (var latitude = 0; latitude <= latitudeSegments; latitude++)
|
||||
{
|
||||
var phi = System.Math.PI * latitude / latitudeSegments;
|
||||
var y = System.Math.Cos(phi);
|
||||
var ring = System.Math.Sin(phi);
|
||||
for (var longitude = 0; longitude <= longitudeSegments; longitude++)
|
||||
{
|
||||
var theta = System.Math.Tau * longitude / longitudeSegments;
|
||||
var x = ring * System.Math.Cos(theta);
|
||||
var z = ring * System.Math.Sin(theta);
|
||||
vertices[write++] = (float)x;
|
||||
vertices[write++] = (float)y;
|
||||
vertices[write++] = (float)z;
|
||||
vertices[write++] = (float)x;
|
||||
vertices[write++] = (float)y;
|
||||
vertices[write++] = (float)z;
|
||||
}
|
||||
}
|
||||
|
||||
write = 0;
|
||||
var row = longitudeSegments + 1;
|
||||
for (var latitude = 0; latitude < latitudeSegments; latitude++)
|
||||
for (var longitude = 0; longitude < longitudeSegments; longitude++)
|
||||
{
|
||||
var topLeft = (uint)(latitude * row + longitude);
|
||||
var bottomLeft = topLeft + (uint)row;
|
||||
indices[write++] = topLeft;
|
||||
indices[write++] = bottomLeft;
|
||||
indices[write++] = topLeft + 1;
|
||||
indices[write++] = topLeft + 1;
|
||||
indices[write++] = bottomLeft;
|
||||
indices[write++] = bottomLeft + 1;
|
||||
}
|
||||
return (vertices, indices);
|
||||
}
|
||||
}
|
||||
59
src/Universe.App/Graphics/RenderTarget.cs
Normal file
59
src/Universe.App/Graphics/RenderTarget.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed unsafe class RenderTarget : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
public uint Framebuffer { get; }
|
||||
public uint Color { get; }
|
||||
public uint Depth { get; }
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
|
||||
public RenderTarget(GL gl, int width, int height, bool depth = false, bool fullPrecision = false)
|
||||
{
|
||||
_gl = gl;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Framebuffer = gl.GenFramebuffer();
|
||||
gl.BindFramebuffer(GLEnum.Framebuffer, Framebuffer);
|
||||
Color = gl.GenTexture();
|
||||
gl.BindTexture(GLEnum.Texture2D, Color);
|
||||
gl.TexImage2D(GLEnum.Texture2D, 0, fullPrecision ? InternalFormat.Rgba32f : InternalFormat.Rgba16f, (uint)width, (uint)height, 0, PixelFormat.Rgba, PixelType.Float, null);
|
||||
SetParameters(gl, GLEnum.Linear);
|
||||
gl.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.ColorAttachment0, GLEnum.Texture2D, Color, 0);
|
||||
if (depth)
|
||||
{
|
||||
Depth = gl.GenTexture();
|
||||
gl.BindTexture(GLEnum.Texture2D, Depth);
|
||||
gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.DepthComponent32f, (uint)width, (uint)height, 0, PixelFormat.DepthComponent, PixelType.Float, null);
|
||||
SetParameters(gl, GLEnum.Nearest);
|
||||
gl.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.DepthAttachment, GLEnum.Texture2D, Depth, 0);
|
||||
}
|
||||
var status = gl.CheckFramebufferStatus(GLEnum.Framebuffer);
|
||||
if (status != GLEnum.FramebufferComplete) throw new InvalidOperationException($"Incomplete HDR framebuffer: {status}");
|
||||
gl.BindFramebuffer(GLEnum.Framebuffer, 0);
|
||||
}
|
||||
|
||||
public void Bind()
|
||||
{
|
||||
_gl.BindFramebuffer(GLEnum.Framebuffer, Framebuffer);
|
||||
_gl.Viewport(0, 0, (uint)Width, (uint)Height);
|
||||
}
|
||||
|
||||
private static void SetParameters(GL gl, GLEnum filter)
|
||||
{
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)filter);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMagFilter, (int)filter);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapS, (int)GLEnum.ClampToEdge);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapT, (int)GLEnum.ClampToEdge);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteTexture(Color);
|
||||
if (Depth != 0) _gl.DeleteTexture(Depth);
|
||||
_gl.DeleteFramebuffer(Framebuffer);
|
||||
}
|
||||
}
|
||||
95
src/Universe.App/Graphics/RingParticles.cs
Normal file
95
src/Universe.App/Graphics/RingParticles.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
using Universe.Core.Math;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
/// <summary>Only local debris cells are stored. Geodesic mesh LODs depend on projected size.</summary>
|
||||
internal sealed unsafe class RingParticles : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly RingRockStreamer _stream;
|
||||
private readonly RingRockSettings _settings;
|
||||
private readonly uint _pointVao, _pointInstances;
|
||||
private readonly Dictionary<int, InstancedAsteroidMesh> _meshes = [];
|
||||
private readonly Dictionary<int, ulong> _signatures = [];
|
||||
private RingRock[] _candidates = [];
|
||||
private int[] _previousLevels = [];
|
||||
private int _revision = -1, _pointCount;
|
||||
private DVector3? _lastCamera;
|
||||
private double _lastPixelScale;
|
||||
public DVector3 Anchor { get; private set; }
|
||||
public int Count => _pointCount;
|
||||
public int HighDetailCount { get; private set; }
|
||||
public RingParticles(GL gl, RingProfile profile)
|
||||
{
|
||||
_gl = gl; _stream = new(profile); _settings = profile.Rings.Rocks;
|
||||
_pointVao = gl.GenVertexArray(); _pointInstances = gl.GenBuffer();
|
||||
gl.BindVertexArray(_pointVao); InstancedAsteroidMesh.ConfigureInstances(gl, _pointInstances, 0); gl.BindVertexArray(0);
|
||||
}
|
||||
public void Update(DVector3 camera, double pixelScale)
|
||||
{
|
||||
var frame = _stream.Update(camera);
|
||||
var changed = frame.Revision != _revision;
|
||||
if (!changed && _lastCamera == camera && _lastPixelScale == pixelScale) return;
|
||||
_lastCamera = camera; _lastPixelScale = pixelScale;
|
||||
if (changed)
|
||||
{
|
||||
_revision = frame.Revision; Anchor = frame.Anchor; _pointCount = frame.Rocks.Count;
|
||||
var points = new List<float>(_pointCount * 12); var candidates = new List<RingRock>();
|
||||
var meshRange = _settings.MeshDistanceMeters + _settings.LodTransitionMeters + _settings.CellSizeMeters * 2;
|
||||
foreach (var rock in frame.Rocks)
|
||||
{
|
||||
Pack(points, rock);
|
||||
if ((rock.Position - camera).Length < meshRange) candidates.Add(rock);
|
||||
}
|
||||
_candidates = candidates.ToArray(); _previousLevels = new int[_candidates.Length];
|
||||
_gl.BindBuffer(GLEnum.ArrayBuffer, _pointInstances);
|
||||
var data = points.ToArray(); fixed (float* p = data) _gl.BufferData(GLEnum.ArrayBuffer, (nuint)(data.Length * 4), p, GLEnum.DynamicDraw);
|
||||
}
|
||||
var detail = _settings.Asteroid;
|
||||
var entries = new List<(int Index, int Level, double Pixels)>(_candidates.Length);
|
||||
for (var i = 0; i < _candidates.Length; i++)
|
||||
{
|
||||
var rock = _candidates[i]; var distance = (rock.Position - camera).Length;
|
||||
var pixels = 2 * rock.Radius * pixelScale / System.Math.Max(.05, distance);
|
||||
var level = AsteroidLod.Select(pixels, distance, detail);
|
||||
var previous = _previousLevels[i];
|
||||
if (previous != 0 && System.Math.Abs(level - previous) == 1)
|
||||
{
|
||||
var boundary = detail.LodStartPixels * System.Math.Pow(detail.LodPixelMultiplier, System.Math.Min(level, previous) - detail.BaseSubdivision);
|
||||
if (pixels > boundary * .88 && pixels < boundary * 1.12 && distance <= detail.HighDetailDistanceMeters) level = previous;
|
||||
}
|
||||
entries.Add((i, level, pixels));
|
||||
}
|
||||
entries.Sort((a, b) => { var result = b.Pixels.CompareTo(a.Pixels); return result != 0 ? result : a.Index.CompareTo(b.Index); });
|
||||
HighDetailCount = 0;
|
||||
var buckets = new Dictionary<int, List<float>>(); var signatures = new Dictionary<int, ulong>();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var level = entry.Level;
|
||||
if (level > 3 && HighDetailCount++ >= detail.MaxHighDetailInstances) level = 3;
|
||||
_previousLevels[entry.Index] = level;
|
||||
if (!buckets.TryGetValue(level, out var list)) { buckets[level] = list = []; signatures[level] = 14695981039346656037UL; }
|
||||
Pack(list, _candidates[entry.Index]);
|
||||
signatures[level] = unchecked((signatures[level] ^ (ulong)entry.Index) * 1099511628211UL);
|
||||
}
|
||||
HighDetailCount = System.Math.Min(HighDetailCount, detail.MaxHighDetailInstances);
|
||||
foreach (var level in _meshes.Keys.Union(buckets.Keys).ToArray())
|
||||
{
|
||||
var signature = signatures.GetValueOrDefault(level);
|
||||
if (!changed && _signatures.TryGetValue(level, out var prior) && prior == signature) continue;
|
||||
if (!_meshes.TryGetValue(level, out var mesh)) _meshes.Add(level, mesh = new(_gl, level));
|
||||
mesh.Upload(buckets.TryGetValue(level, out var list) ? list.ToArray() : []); _signatures[level] = signature;
|
||||
}
|
||||
}
|
||||
private void Pack(List<float> target, RingRock rock)
|
||||
{
|
||||
var p = rock.Position - Anchor;
|
||||
target.AddRange([(float)p.X,(float)p.Y,(float)p.Z,(float)rock.Radius,rock.Color.X,rock.Color.Y,rock.Color.Z,rock.ShapeSeed,
|
||||
rock.Rotation.X,rock.Rotation.Y,rock.Rotation.Z,rock.Rotation.W]);
|
||||
}
|
||||
public void DrawMeshes() { foreach (var mesh in _meshes.Values) mesh.Draw(); }
|
||||
public void DrawPoints() { if (_pointCount == 0) return; _gl.BindVertexArray(_pointVao); _gl.DrawArrays(GLEnum.Points, 0, (uint)_pointCount); _gl.BindVertexArray(0); }
|
||||
public void Dispose() { foreach (var mesh in _meshes.Values) mesh.Dispose(); _gl.DeleteVertexArray(_pointVao); _gl.DeleteBuffer(_pointInstances); }
|
||||
}
|
||||
43
src/Universe.App/Graphics/RingProfileTexture.cs
Normal file
43
src/Universe.App/Graphics/RingProfileTexture.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed unsafe class RingProfileTexture : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
public uint Handle { get; }
|
||||
public RingProfile? Profile { get; }
|
||||
public RingProfileTexture(GL gl, PlanetRings? rings)
|
||||
{
|
||||
_gl = gl;
|
||||
Profile = rings is null ? null : new(rings);
|
||||
var data = Profile?.Rgba ?? new float[4];
|
||||
var maxWidth = gl.GetInteger(GLEnum.MaxTextureSize);
|
||||
if (data.Length / 4 > maxWidth)
|
||||
{
|
||||
var source = data;
|
||||
var sourceWidth = source.Length / 4;
|
||||
data = new float[maxWidth * 4];
|
||||
for (var x = 0; x < maxWidth; x++)
|
||||
{
|
||||
var first = (int)((long)x * sourceWidth / maxWidth);
|
||||
var last = (int)((long)(x + 1) * sourceWidth / maxWidth);
|
||||
for (var i = first; i < last; i++)
|
||||
for (var channel = 0; channel < 4; channel++)
|
||||
data[x * 4 + channel] += source[i * 4 + channel] / (last - first);
|
||||
}
|
||||
Console.WriteLine($"Ring profile reduced from {sourceWidth} to GPU limit {maxWidth} texels.");
|
||||
}
|
||||
Handle = gl.GenTexture();
|
||||
gl.ActiveTexture(GLEnum.Texture7);
|
||||
gl.BindTexture(GLEnum.Texture2D, Handle);
|
||||
fixed (float* p = data) gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.Rgba16f, (uint)(data.Length / 4), 1, 0, PixelFormat.Rgba, PixelType.Float, p);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)GLEnum.LinearMipmapLinear);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMagFilter, (int)GLEnum.Linear);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapS, (int)GLEnum.ClampToEdge);
|
||||
gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapT, (int)GLEnum.ClampToEdge);
|
||||
gl.GenerateMipmap(GLEnum.Texture2D);
|
||||
}
|
||||
public void Dispose() => _gl.DeleteTexture(Handle);
|
||||
}
|
||||
96
src/Universe.App/Graphics/ShaderProgram.cs
Normal file
96
src/Universe.App/Graphics/ShaderProgram.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Numerics;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal sealed unsafe class ShaderProgram : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly Dictionary<string, int> _uniformLocations = new(StringComparer.Ordinal);
|
||||
private bool _disposed;
|
||||
|
||||
private ShaderProgram(GL gl, uint handle)
|
||||
{
|
||||
_gl = gl;
|
||||
Handle = handle;
|
||||
}
|
||||
|
||||
public uint Handle { get; }
|
||||
|
||||
public static ShaderProgram Create(GL gl, string vertexSource, string fragmentSource)
|
||||
{
|
||||
var vertex = Compile(gl, ShaderType.VertexShader, vertexSource);
|
||||
var fragment = Compile(gl, ShaderType.FragmentShader, fragmentSource);
|
||||
var program = gl.CreateProgram();
|
||||
gl.AttachShader(program, vertex);
|
||||
gl.AttachShader(program, fragment);
|
||||
gl.LinkProgram(program);
|
||||
gl.GetProgram(program, GLEnum.LinkStatus, out var linked);
|
||||
gl.DeleteShader(vertex);
|
||||
gl.DeleteShader(fragment);
|
||||
if (linked == 0)
|
||||
{
|
||||
var log = gl.GetProgramInfoLog(program);
|
||||
gl.DeleteProgram(program);
|
||||
throw new InvalidOperationException($"OpenGL shader link failed: {log}");
|
||||
}
|
||||
return new(gl, program);
|
||||
}
|
||||
|
||||
public void Use() => _gl.UseProgram(Handle);
|
||||
|
||||
public void Set(string name, int value) => _gl.Uniform1(Location(name), value);
|
||||
public void Set(string name, float value) => _gl.Uniform1(Location(name), value);
|
||||
public void Set(string name, Vector3 value) => _gl.Uniform3(Location(name), value.X, value.Y, value.Z);
|
||||
|
||||
public void SetMatrix(string name, Matrix4x4 value)
|
||||
{
|
||||
var location = Location(name);
|
||||
// System.Numerics stores row-vector matrices in row-major order. OpenGL consumes
|
||||
// column-major data, so the same memory represents the required transposed matrix.
|
||||
_gl.UniformMatrix4(location, 1, false, (float*)&value);
|
||||
}
|
||||
|
||||
public void SetVectorArray(string name, IReadOnlyList<Vector3> values)
|
||||
{
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
Set($"{name}[{i}]", values[i]);
|
||||
}
|
||||
|
||||
public void SetFloatArray(string name, IReadOnlyList<float> values)
|
||||
{
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
Set($"{name}[{i}]", values[i]);
|
||||
}
|
||||
|
||||
private int Location(string name)
|
||||
{
|
||||
if (!_uniformLocations.TryGetValue(name, out var location))
|
||||
{
|
||||
location = _gl.GetUniformLocation(Handle, name);
|
||||
_uniformLocations.Add(name, location);
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
private static uint Compile(GL gl, ShaderType type, string source)
|
||||
{
|
||||
var shader = gl.CreateShader(type);
|
||||
gl.ShaderSource(shader, source);
|
||||
gl.CompileShader(shader);
|
||||
gl.GetShader(shader, GLEnum.CompileStatus, out var compiled);
|
||||
if (compiled != 0)
|
||||
return shader;
|
||||
var log = gl.GetShaderInfoLog(shader);
|
||||
gl.DeleteShader(shader);
|
||||
throw new InvalidOperationException($"OpenGL {type} compilation failed: {log}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
_gl.DeleteProgram(Handle);
|
||||
}
|
||||
}
|
||||
28
src/Universe.App/Graphics/ShaderSources.cs
Normal file
28
src/Universe.App/Graphics/ShaderSources.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
internal static class ShaderSources
|
||||
{
|
||||
public static string Load(string name)
|
||||
{
|
||||
#if DEBUG
|
||||
var sourcePath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../Graphics/Shaders", name + ".glsl"));
|
||||
if (File.Exists(sourcePath))
|
||||
return Expand(name, File.ReadAllText(sourcePath));
|
||||
#endif
|
||||
using var stream = typeof(ShaderSources).Assembly.GetManifestResourceStream($"Universe.App.Graphics.Shaders.{name}.glsl")
|
||||
?? throw new FileNotFoundException($"Embedded shader {name} is missing.");
|
||||
using var reader = new StreamReader(stream);
|
||||
var source = reader.ReadToEnd();
|
||||
return Expand(name, source);
|
||||
}
|
||||
private static string Expand(string name, string source)
|
||||
{
|
||||
foreach (var include in new[] { "noise", "planet-height", "camera-ray", "ring-optics", "rock-common", "asteroid-shape", "cloud-common" })
|
||||
{
|
||||
var directive = $"#include \"{include}.glsl\"";
|
||||
if (source.Contains(directive, StringComparison.Ordinal))
|
||||
source = source.Replace(directive, name == include ? string.Empty : Load(include), StringComparison.Ordinal);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
}
|
||||
62
src/Universe.App/Graphics/Shaders/asteroid-shape.glsl
Normal file
62
src/Universe.App/Graphics/Shaders/asteroid-shape.glsl
Normal file
@@ -0,0 +1,62 @@
|
||||
uniform vec3 uAsteroidShape;
|
||||
uniform vec3 uAsteroidNoise;
|
||||
uniform vec3 uAsteroidCraters;
|
||||
uniform vec3 uAsteroidSurface;
|
||||
vec3 asteroidHash(vec3 p) {
|
||||
// Integer hashing keeps crater positions identical in vertex and fragment stages.
|
||||
uvec3 q=uvec3(ivec3(floor(p*97.0)));
|
||||
q=q*1664525u+1013904223u;
|
||||
q.x+=q.y*q.z;q.y+=q.z*q.x;q.z+=q.x*q.y;
|
||||
q^=q>>16u;
|
||||
q.x+=q.y*q.z;q.y+=q.z*q.x;q.z+=q.x*q.y;
|
||||
return vec3(q)*(1.0/4294967295.0);
|
||||
}
|
||||
vec3 asteroidAxes(float seed) {
|
||||
vec3 h=asteroidHash(vec3(seed*173.0,19.3,47.1));
|
||||
vec3 axes=vec3(1.0)+(h-0.5)*uAsteroidShape.x*2.0;
|
||||
return axes/pow(axes.x*axes.y*axes.z,1.0/3.0);
|
||||
}
|
||||
float asteroidCraters(vec3 direction,float frequency,float seed) {
|
||||
vec3 offset=vec3(seed*31.7,seed*17.3,seed*23.1);
|
||||
vec3 q=direction*frequency+offset;
|
||||
vec3 cell=floor(q);float height=0.0;
|
||||
for(int z=-1;z<=1;z++)for(int y=-1;y<=1;y++)for(int x=-1;x<=1;x++) {
|
||||
vec3 lattice=cell+vec3(x,y,z),h=asteroidHash(lattice+seed*41.0);
|
||||
if(h.z>uAsteroidCraters.z)continue;
|
||||
vec3 center=lattice+0.5+(h-0.5)*0.30-offset;
|
||||
if(abs(length(center)-frequency)>.58)continue;
|
||||
float radius=mix(.24,.63,h.x);
|
||||
float r=length(direction-normalize(center))*frequency/radius;
|
||||
if(r>1.22)continue;
|
||||
float bowl=-0.48*(1.0-r*r)*(1.0-smoothstep(.72,1.0,r));
|
||||
float rim=uAsteroidSurface.x*exp(-pow((r-.96)/.095,2.0));
|
||||
height+=(bowl+rim)*radius/frequency;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
float asteroidHeight(vec3 direction,float seed) {
|
||||
vec3 offset=vec3(seed*31.0,seed*17.0,seed*47.0);
|
||||
vec3 p=direction*uAsteroidShape.y+offset;
|
||||
vec3 warp=vec3(noise3(p),noise3(p+vec3(9.3,4.7,1.1)),noise3(p+vec3(2.9,17.1,7.3)));
|
||||
p+=warp*.45;
|
||||
float sum=0.0,amplitude=.7,weight=0.0;
|
||||
for(int i=0;i<7;i++) {
|
||||
if(i>=int(uAsteroidNoise.x))break;
|
||||
sum+=gradientNoise3(p)*amplitude;
|
||||
weight+=amplitude;
|
||||
p=octaveRotation*p*2.07+vec3(7.3,13.1,5.9);amplitude*=uAsteroidNoise.y;
|
||||
}
|
||||
float ridges=pow(1.0-abs(noise3(direction*11.0+offset)),3.0)-.4;
|
||||
float craters=asteroidCraters(direction,uAsteroidCraters.x,seed);
|
||||
craters+=asteroidCraters(direction,uAsteroidCraters.x*3.1,seed+5.7)*.8;
|
||||
return sum/max(.1,weight)*uRockMaterial.x+ridges*uAsteroidShape.z+craters*uAsteroidCraters.y;
|
||||
}
|
||||
vec3 asteroidPosition(vec3 direction,float seed) {
|
||||
return direction*asteroidAxes(seed)*clamp(1.0+asteroidHeight(direction,seed),.55,1.6);
|
||||
}
|
||||
float asteroidMicroHeight(vec3 d,float seed,float footprint) {
|
||||
vec3 p=d*uAsteroidSurface.z+seed*61.0;
|
||||
float fine=(noise3(p)+noise3(p*2.7)*.32)*(1.0-smoothstep(.35,1.2,footprint*uAsteroidSurface.z));
|
||||
float smallCraters=asteroidCraters(d,uAsteroidCraters.x*9.0,seed+13.1)*uAsteroidCraters.y;
|
||||
return fine*uAsteroidSurface.y+smallCraters;
|
||||
}
|
||||
84
src/Universe.App/Graphics/Shaders/atmosphere.frag.glsl
Normal file
84
src/Universe.App/Graphics/Shaders/atmosphere.frag.glsl
Normal file
@@ -0,0 +1,84 @@
|
||||
#version 330 core
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uScene;
|
||||
uniform sampler2D uDepth;
|
||||
uniform vec3 uForward;
|
||||
uniform vec3 uRight;
|
||||
uniform vec3 uUp;
|
||||
uniform float uAspect;
|
||||
uniform float uTanHalfFov;
|
||||
uniform float uFar;
|
||||
uniform vec3 uCameraPlanet;
|
||||
uniform float uRadius;
|
||||
uniform float uAtmosphereRadius;
|
||||
uniform float uGroundRadius;
|
||||
uniform vec3 uAtmosphereColor;
|
||||
uniform float uDensityScale;
|
||||
uniform int uLightCount;
|
||||
uniform vec3 uLightDirections[2];
|
||||
uniform vec3 uLightColors[2];
|
||||
uniform float uLightFlux[2];
|
||||
out vec4 fragColor;
|
||||
const float PI=3.14159265359;
|
||||
vec2 sphere(vec3 origin, vec3 direction, float radius) {
|
||||
float b=dot(origin,direction);
|
||||
float c=dot(origin,origin)-radius*radius;
|
||||
float h=b*b-c;
|
||||
if(h<0.0) return vec2(1e8,-1e8);
|
||||
h=sqrt(h);
|
||||
return vec2(-b-h,-b+h);
|
||||
}
|
||||
vec2 density(vec3 p) {
|
||||
float altitude=max(length(p)-1.0,-0.0003);
|
||||
float thickness=uAtmosphereRadius-1.0;
|
||||
return exp(-altitude/vec2(thickness*0.055,thickness*0.0175));
|
||||
}
|
||||
vec2 sunDepth(vec3 p, vec3 direction) {
|
||||
vec2 ground=sphere(p,direction,uGroundRadius);
|
||||
if(ground.y>ground.x && ground.x>0.0) return vec2(1e5);
|
||||
float distance=sphere(p,direction,uAtmosphereRadius).y;
|
||||
float stepLength=max(0.0,distance)/7.0;
|
||||
vec2 optical=vec2(0.0);
|
||||
for(int i=0;i<7;i++) optical+=density(p+direction*(float(i)+0.5)*stepLength)*stepLength;
|
||||
return optical;
|
||||
}
|
||||
void main() {
|
||||
vec3 scene=texture(uScene,vUv).rgb;
|
||||
vec2 xy=(vUv*2.0-1.0)*uTanHalfFov;
|
||||
vec3 ray=normalize(uForward+uRight*xy.x*uAspect+uUp*xy.y);
|
||||
vec2 hit=sphere(uCameraPlanet,ray,uAtmosphereRadius);
|
||||
float start=max(hit.x,0.0);
|
||||
float depth=texture(uDepth,vUv).r;
|
||||
float worldDepth=(exp2(depth*log2(uFar+1.0))-1.0)/max(dot(ray,uForward),0.01);
|
||||
float end=min(hit.y,worldDepth/uRadius);
|
||||
if(end<=start) { fragColor=vec4(scene,1.0); return; }
|
||||
vec3 betaR=uAtmosphereColor*110.0*uDensityScale;
|
||||
vec3 betaM=vec3(4.0)*uDensityScale;
|
||||
vec3 betaAbsorption=vec3(2.1,4.2,0.35)*uDensityScale;
|
||||
vec2 optical=vec2(0.0);
|
||||
vec3 scattering=vec3(0.0);
|
||||
float ds=(end-start)/28.0;
|
||||
float jitter=fract(sin(dot(gl_FragCoord.xy,vec2(12.9898,78.233)))*43758.5453);
|
||||
for(int i=0;i<28;i++) {
|
||||
vec3 p=uCameraPlanet+ray*(start+(float(i)+0.35+jitter*0.3)*ds);
|
||||
vec2 localDensity=density(p);
|
||||
vec2 stepDepth=localDensity*ds;
|
||||
optical+=stepDepth*0.5;
|
||||
for(int l=0;l<2;l++) {
|
||||
if(l>=uLightCount) break;
|
||||
vec3 ld=uLightDirections[l];
|
||||
vec2 lightOptical=sunDepth(p,ld);
|
||||
vec2 total=optical+lightOptical;
|
||||
vec3 transmittance=exp(-min(betaR*total.x+betaM*total.y*1.1+betaAbsorption*total.x,vec3(70.0)));
|
||||
float mu=dot(ray,ld);
|
||||
float rayleighPhase=3.0/(16.0*PI)*(1.0+mu*mu);
|
||||
float g=0.78;
|
||||
float miePhase=3.0/(8.0*PI)*((1.0-g*g)*(1.0+mu*mu))/((2.0+g*g)*pow(max(0.02,1.0+g*g-2.0*g*mu),1.5));
|
||||
scattering+=transmittance*(betaR*stepDepth.x*rayleighPhase+betaM*stepDepth.y*miePhase)
|
||||
*mix(vec3(1.0),uLightColors[l],0.4)*uLightFlux[l]*6.5;
|
||||
}
|
||||
optical+=stepDepth*0.5;
|
||||
}
|
||||
vec3 extinction=exp(-min(betaR*optical.x+betaM*optical.y*1.1+betaAbsorption*optical.x,vec3(70.0)));
|
||||
fragColor=vec4(scene*extinction+scattering,1.0);
|
||||
}
|
||||
13
src/Universe.App/Graphics/Shaders/blur.frag.glsl
Normal file
13
src/Universe.App/Graphics/Shaders/blur.frag.glsl
Normal file
@@ -0,0 +1,13 @@
|
||||
#version 330 core
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uScene;
|
||||
uniform vec3 uDirection;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec3 color=texture(uScene,vUv).rgb*0.227027;
|
||||
color+=texture(uScene,vUv+uDirection.xy*1.384615).rgb*0.316216;
|
||||
color+=texture(uScene,vUv-uDirection.xy*1.384615).rgb*0.316216;
|
||||
color+=texture(uScene,vUv+uDirection.xy*3.230769).rgb*0.070270;
|
||||
color+=texture(uScene,vUv-uDirection.xy*3.230769).rgb*0.070270;
|
||||
fragColor=vec4(color,1.0);
|
||||
}
|
||||
17
src/Universe.App/Graphics/Shaders/bright.frag.glsl
Normal file
17
src/Universe.App/Graphics/Shaders/bright.frag.glsl
Normal file
@@ -0,0 +1,17 @@
|
||||
#version 330 core
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uScene;
|
||||
uniform vec3 uTexel;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec3 color=vec3(0.0);
|
||||
for(int x=-1;x<=1;x++) for(int y=-1;y<=1;y++) {
|
||||
vec3 c=texture(uScene,vUv+vec2(x,y)*uTexel.xy).rgb;
|
||||
float brightness=max(c.r,max(c.g,c.b));
|
||||
float soft=clamp(brightness-0.65,0.0,0.7);
|
||||
soft=soft*soft/(2.8+0.0001);
|
||||
float contribution=max(brightness-1.0,soft)/max(brightness,0.0001);
|
||||
color+=c*contribution;
|
||||
}
|
||||
fragColor=vec4(color/9.0,1.0);
|
||||
}
|
||||
9
src/Universe.App/Graphics/Shaders/camera-ray.glsl
Normal file
9
src/Universe.App/Graphics/Shaders/camera-ray.glsl
Normal file
@@ -0,0 +1,9 @@
|
||||
uniform vec3 uForward;
|
||||
uniform vec3 uRight;
|
||||
uniform vec3 uUp;
|
||||
uniform float uAspect;
|
||||
uniform float uTanHalfFov;
|
||||
vec3 cameraRay(vec2 uv) {
|
||||
vec2 xy=(uv*2.0-1.0)*uTanHalfFov;
|
||||
return normalize(uForward+uRight*xy.x*uAspect+uUp*xy.y);
|
||||
}
|
||||
114
src/Universe.App/Graphics/Shaders/cloud-common.glsl
Normal file
114
src/Universe.App/Graphics/Shaders/cloud-common.glsl
Normal file
@@ -0,0 +1,114 @@
|
||||
uniform sampler3D uCloudShape;
|
||||
uniform sampler3D uCloudDetail;
|
||||
uniform sampler2D uCloudDensityMap;
|
||||
uniform int uCloudEnabled;
|
||||
uniform int uCloudShadowEnabled;
|
||||
uniform int uCloudHasDensityMap;
|
||||
uniform float uCloudDensityTextureStrength;
|
||||
uniform float uCloudPlanetRadius;
|
||||
uniform vec3 uCloudCamera;
|
||||
uniform vec3 uCloudAxisX;
|
||||
uniform vec3 uCloudAxisY;
|
||||
uniform vec3 uCloudAxisZ;
|
||||
uniform mat4 uCloudWorldToBody;
|
||||
uniform vec3 uCloudOrigins[3];
|
||||
uniform vec3 uCloudLayer; // base altitude, thickness, shape scale
|
||||
uniform vec3 uCloudShapeSettings; // coverage, erosion, type
|
||||
uniform vec3 uCloudOptics; // density, extinction/m, shadow strength
|
||||
uniform float uCloudDetailScale;
|
||||
uniform float uCloudWeatherScale;
|
||||
uniform float uCloudPixelAngle;
|
||||
uniform vec3 uCloudAir; // atmosphere height, density, sunset strength
|
||||
uniform vec3 uCloudAirColor;
|
||||
uniform vec3 uCloudPrimarySun;
|
||||
uniform vec3 uCloudPrimaryColor;
|
||||
|
||||
vec3 cloudToLocal(vec3 v) { return vec3(dot(v,uCloudAxisX),dot(v,uCloudAxisY),dot(v,uCloudAxisZ)); }
|
||||
vec3 cloudToWorld(vec3 v) { return uCloudAxisX*v.x+uCloudAxisY*v.y+uCloudAxisZ*v.z; }
|
||||
float cloudAltitude(vec3 p) {
|
||||
return (dot(p,p)+2.0*uCloudPlanetRadius*p.y)/(length(p+vec3(0,uCloudPlanetRadius,0))+uCloudPlanetRadius);
|
||||
}
|
||||
vec2 cloudSphere(vec3 ro,vec3 rd,float altitude) {
|
||||
float b=dot(ro+vec3(0,uCloudPlanetRadius,0),rd);
|
||||
float c=dot(ro,ro)+2.0*uCloudPlanetRadius*(ro.y-altitude)-altitude*altitude;
|
||||
float h=b*b-c;
|
||||
if(h<0.0)return vec2(1e25,-1e25);
|
||||
float q=-b-(b<0.0?-1.0:1.0)*sqrt(h);
|
||||
float d=c/(abs(q)<1e-9?1e-9:q);
|
||||
return vec2(min(q,d),max(q,d));
|
||||
}
|
||||
float cloudDensity(vec3 p,float lod,bool fine) {
|
||||
float height=(cloudAltitude(p)-uCloudLayer.x)/uCloudLayer.y;
|
||||
if(height<=0.0 || height>=1.0)return 0.0;
|
||||
vec3 body=mat3(uCloudWorldToBody)*cloudToWorld(p);
|
||||
float weather=textureLod(uCloudShape,body/uCloudWeatherScale+uCloudOrigins[2],0.0).a;
|
||||
float weatherCover=smoothstep(.30,.62,weather);
|
||||
float coverage=clamp(uCloudShapeSettings.x+(weatherCover-.5)*.55,0.0,1.0);
|
||||
if(coverage<.005)return 0.0;
|
||||
vec3 structure=textureLod(uCloudShape,body/uCloudLayer.z+uCloudOrigins[0],lod).rgb;
|
||||
float shape=structure.r+(dot(structure.gb,vec2(.7,.3))-.5)*.28;
|
||||
float profile=smoothstep(0.0,.09,height)*(1.0-smoothstep(mix(.28,.62,uCloudShapeSettings.z),mix(.52,1.0,uCloudShapeSettings.z),height));
|
||||
float density=clamp((shape-(1.0-coverage))/max(.08,coverage)*3.1,0.0,1.0);
|
||||
if(fine && density>.001) {
|
||||
vec3 erosion=textureLod(uCloudDetail,body/uCloudDetailScale+uCloudOrigins[1],max(0.0,lod-1.0)).rgb;
|
||||
float detail=dot(erosion,vec3(.6,.3,.1));
|
||||
density=clamp(density-(1.0-detail)*uCloudShapeSettings.y*(.45+.55*(1.0-density))*(1.0-smoothstep(1.0,3.0,lod)),0.0,1.0);
|
||||
}
|
||||
// From orbit a pixel covers many individual puffs. Integrate their weather-scale coverage.
|
||||
float distant=smoothstep(.28,.65,coverage)*.5;
|
||||
density=mix(density,distant,smoothstep(2.5,5.5,lod));
|
||||
if(uCloudHasDensityMap!=0) {
|
||||
vec3 radialBody=normalize(mat3(uCloudWorldToBody)*cloudToWorld(p+vec3(0.0,uCloudPlanetRadius,0.0)));
|
||||
vec2 mapUv=vec2(.5-atan(radialBody.z,radialBody.x)/6.28318530718,
|
||||
atan(length(radialBody.xz),radialBody.y)/3.14159265359);
|
||||
float mapWidth=float(textureSize(uCloudDensityMap,0).x);
|
||||
float footprint=max(1.0,length(p)*uCloudPixelAngle*mapWidth/(6.28318530718*uCloudPlanetRadius));
|
||||
float mapLod=clamp(log2(footprint),0.0,log2(max(1.0,mapWidth)));
|
||||
float mask=textureLod(uCloudDensityMap,mapUv,mapLod).r;
|
||||
float coverageMask=smoothstep(.055,.48,mask);
|
||||
density*=mix(1.0,coverageMask,clamp(uCloudDensityTextureStrength,0.0,1.0));
|
||||
}
|
||||
return density*profile*uCloudOptics.x;
|
||||
}
|
||||
vec3 cloudSunTint(vec3 p,vec3 light) {
|
||||
vec3 up=normalize(p+vec3(0,uCloudPlanetRadius,0));
|
||||
float mu=dot(up,light);
|
||||
float scaleHeight=max(100.0,uCloudAir.x*.055);
|
||||
float mass=exp(-max(0.0,cloudAltitude(p))/scaleHeight)/max(.025,mu+sqrt(mu*mu+2.0*scaleHeight/uCloudPlanetRadius));
|
||||
vec3 beta=vec3(.022,.055,.14)*(vec3(.7)+uCloudAirColor*.3);
|
||||
return exp(-beta*mass*uCloudAir.y*uCloudAir.z);
|
||||
}
|
||||
float cloudShadow(vec3 worldPoint,vec3 worldLight) {
|
||||
if(uCloudEnabled==0 || uCloudShadowEnabled==0 || uCloudOptics.z<=0.0)return 1.0;
|
||||
vec3 ro=uCloudCamera+cloudToLocal(worldPoint),rd=cloudToLocal(worldLight);
|
||||
if(cloudAltitude(ro)>uCloudLayer.x+uCloudLayer.y)return 1.0;
|
||||
vec2 outer=cloudSphere(ro,rd,uCloudLayer.x+uCloudLayer.y),inner=cloudSphere(ro,rd,uCloudLayer.x);
|
||||
float start=max(0.0,outer.x),end=min(outer.y,start+150000.0);
|
||||
if(inner.x<0.0 && inner.y>0.0)start=max(start,inner.y);
|
||||
else if(inner.x>start)end=min(end,inner.x);
|
||||
if(end<=start)return 1.0;
|
||||
float ds=(end-start)/6.0,optical=0.0;
|
||||
float lod=clamp(log2(max(1.0,length(worldPoint)*uCloudPixelAngle*128.0/uCloudLayer.z)),1.2,7.0);
|
||||
for(int i=0;i<6;i++)optical+=cloudDensity(ro+rd*(start+(float(i)+.5)*ds),lod,false)*ds;
|
||||
return mix(1.0,exp(-min(20.0,optical*uCloudOptics.y)),uCloudOptics.z);
|
||||
}
|
||||
vec3 cloudReflection(vec3 worldPoint,vec3 worldDirection,vec3 sky) {
|
||||
if(uCloudEnabled==0)return sky;
|
||||
vec3 ro=uCloudCamera+cloudToLocal(worldPoint),rd=cloudToLocal(worldDirection);
|
||||
vec2 outer=cloudSphere(ro,rd,uCloudLayer.x+uCloudLayer.y),inner=cloudSphere(ro,rd,uCloudLayer.x);
|
||||
float start=max(0.0,outer.x),end=min(outer.y,start+100000.0);
|
||||
if(inner.x<0.0 && inner.y>0.0)start=max(start,inner.y);
|
||||
else if(inner.x>start)end=min(end,inner.x);
|
||||
if(end<=start)return sky;
|
||||
float ds=(end-start)/10.0,transmission=1.0;
|
||||
vec3 light=vec3(0.0);
|
||||
for(int i=0;i<10;i++) {
|
||||
vec3 p=ro+rd*(start+(float(i)+.5)*ds);
|
||||
float lod=clamp(log2(max(1.0,(length(worldPoint)+start+(float(i)+.5)*ds)*uCloudPixelAngle*128.0/uCloudLayer.z)),1.0,7.0);
|
||||
float a=1.0-exp(-cloudDensity(p,lod,false)*ds*uCloudOptics.y);
|
||||
float day=smoothstep(-.05,.12,dot(normalize(p+vec3(0,uCloudPlanetRadius,0)),uCloudPrimarySun));
|
||||
vec3 color=(vec3(.15,.2,.3)*.2+uCloudPrimaryColor*cloudSunTint(p,uCloudPrimarySun)*.8)*day+vec3(.001);
|
||||
light+=color*a*transmission;transmission*=1.0-a;
|
||||
}
|
||||
return sky*transmission+light;
|
||||
}
|
||||
31
src/Universe.App/Graphics/Shaders/cloud-composite.frag.glsl
Normal file
31
src/Universe.App/Graphics/Shaders/cloud-composite.frag.glsl
Normal file
@@ -0,0 +1,31 @@
|
||||
#version 330 core
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uScene;
|
||||
uniform sampler2D uClouds;
|
||||
uniform sampler2D uDepth;
|
||||
uniform vec3 uCloudTexel;
|
||||
out vec4 fragColor;
|
||||
float depthAt(vec2 uv) {
|
||||
ivec2 size=textureSize(uDepth,0);
|
||||
vec2 pixel=uv*vec2(size)-.5,f=fract(pixel);ivec2 base=ivec2(floor(pixel));
|
||||
float value=0.0;
|
||||
for(int y=0;y<2;y++)for(int x=0;x<2;x++)
|
||||
value+=texelFetch(uDepth,clamp(base+ivec2(x,y),ivec2(0),size-1),0).r
|
||||
*(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y);
|
||||
return value;
|
||||
}
|
||||
void main() {
|
||||
vec2 pixel=vUv/uCloudTexel.xy-.5,base=floor(pixel),blend=fract(pixel);
|
||||
float centerDepth=texture(uDepth,vUv).r;
|
||||
vec4 cloud=vec4(0);float total=0.0;
|
||||
for(int y=0;y<2;y++)for(int x=0;x<2;x++) {
|
||||
vec2 uv=(base+vec2(x,y)+.5)*uCloudTexel.xy;
|
||||
float bilinear=(x==0?1.0-blend.x:blend.x)*(y==0?1.0-blend.y:blend.y);
|
||||
float depth=depthAt(uv);
|
||||
float weight=bilinear*exp(-abs(depth-centerDepth)*2200.0)+.00001;
|
||||
cloud+=texture(uClouds,uv)*weight;total+=weight;
|
||||
}
|
||||
cloud/=total;
|
||||
vec4 scene=texture(uScene,vUv);
|
||||
fragColor=vec4(scene.rgb*cloud.a+cloud.rgb,scene.a*cloud.a);
|
||||
}
|
||||
124
src/Universe.App/Graphics/Shaders/cloud.frag.glsl
Normal file
124
src/Universe.App/Graphics/Shaders/cloud.frag.glsl
Normal file
@@ -0,0 +1,124 @@
|
||||
#version 330 core
|
||||
#include "camera-ray.glsl"
|
||||
#include "cloud-common.glsl"
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uDepth;
|
||||
uniform float uFar;
|
||||
uniform int uCloudViewSteps;
|
||||
uniform int uCloudLightSteps;
|
||||
uniform float uCloudMaxDistance;
|
||||
uniform vec3 uCloudLighting; // ambient, sunlight, single scattering albedo
|
||||
uniform vec3 uCloudPhase; // anisotropy, silver lining, unused
|
||||
uniform int uLightCount;
|
||||
uniform vec3 uLightDirections[2];
|
||||
uniform vec3 uLightColors[2];
|
||||
uniform float uLightFlux[2];
|
||||
out vec4 fragColor;
|
||||
float opaqueClipDistance(vec2 uv) {
|
||||
// Half-resolution rays land between opaque pixel centres. Interpolate inverse eye depth,
|
||||
// which is linear across a projected triangle, instead of borrowing a neighbour's distance.
|
||||
ivec2 size=textureSize(uDepth,0);
|
||||
vec2 pixel=uv*vec2(size)-.5,f=fract(pixel);
|
||||
ivec2 base=ivec2(floor(pixel));
|
||||
float inverseDepth=0.0;
|
||||
for(int y=0;y<2;y++)for(int x=0;x<2;x++) {
|
||||
float weight=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y);
|
||||
float encoded=texelFetch(uDepth,clamp(base+ivec2(x,y),ivec2(0),size-1),0).r;
|
||||
float distance=exp2(encoded*log2(uFar+1.0))-1.0;
|
||||
inverseDepth+=weight/max(.001,distance);
|
||||
}
|
||||
return 1.0/max(1e-20,inverseDepth);
|
||||
}
|
||||
float phaseHG(float mu,float g) { return (1.0-g*g)/pow(max(.01,1.0+g*g-2.0*g*mu),1.5); }
|
||||
float lightTransmission(vec3 p,vec3 light,float lod) {
|
||||
vec2 ground=cloudSphere(p,light,0.0);
|
||||
if(ground.y>ground.x && ground.x>1.0)return 0.0;
|
||||
float distance=min(max(0.0,cloudSphere(p,light,uCloudLayer.x+uCloudLayer.y).y),uCloudLayer.y*12.0);
|
||||
float optical=0.0;
|
||||
for(int i=0;i<16;i++) {
|
||||
if(i>=uCloudLightSteps)break;
|
||||
float a=float(i)/float(uCloudLightSteps),b=(float(i)+1.0)/float(uCloudLightSteps);
|
||||
float sampleT=distance*pow((a+b)*.5,2.0);
|
||||
optical+=cloudDensity(p+light*sampleT,max(.5,lod),false)*distance*(b*b-a*a);
|
||||
}
|
||||
return exp(-min(25.0,optical*uCloudOptics.y));
|
||||
}
|
||||
vec3 sampleLighting(vec3 p,vec3 ray,float density,float lod) {
|
||||
vec3 up=normalize(p+vec3(0,uCloudPlanetRadius,0));
|
||||
float height=clamp((cloudAltitude(p)-uCloudLayer.x)/uCloudLayer.y,0.0,1.0);
|
||||
vec3 light=vec3(.001,.0015,.0025);
|
||||
for(int l=0;l<2;l++) {
|
||||
if(l>=uLightCount)break;
|
||||
vec3 ld=uLightDirections[l];
|
||||
float sunHeight=dot(up,ld),day=smoothstep(-.075,.10,sunHeight);
|
||||
vec3 tint=uLightColors[l]*cloudSunTint(p,ld);
|
||||
float transmission=lightTransmission(p,ld,lod);
|
||||
float mu=dot(ray,ld);
|
||||
float phase=.8*phaseHG(mu,uCloudPhase.x)+.2*phaseHG(mu,-.22);
|
||||
float single=transmission*(.14*phase+.15);
|
||||
float multiple=pow(transmission,.32)*.38;
|
||||
float edge=pow(max(0.0,mu),16.0)*sqrt(transmission)*uCloudPhase.y*.5;
|
||||
light+=tint*uLightFlux[l]*day*(single+multiple+edge)*uCloudLighting.y;
|
||||
vec3 sky=mix(vec3(.18,.23,.33),vec3(.60,.70,.84),height);
|
||||
// Multiple scattering inherits the horizon's reddened illumination, including cloud undersides.
|
||||
vec3 ambientTint=tint/max(.001,max(tint.r,max(tint.g,tint.b)));
|
||||
sky*=mix(vec3(1.0),ambientTint,.85);
|
||||
sky*=1.0+(1.0-clamp(ambientTint.b/max(.001,ambientTint.r),0.0,1.0))*1.5;
|
||||
light+=sky*uCloudLighting.x*day*sqrt(uLightFlux[l])*(.65+.35*exp(-density*1.8));
|
||||
}
|
||||
return light*uCloudLighting.z;
|
||||
}
|
||||
void marchRange(float start,float end,vec3 ray,inout vec3 radiance,inout float transmission,inout float weightedDistance) {
|
||||
if(end<=start || transmission<.01)return;
|
||||
end=min(end,start+uCloudMaxDistance);
|
||||
float count=clamp(ceil((end-start)/max(40.0,uCloudLayer.y/40.0)),16.0,float(uCloudViewSteps));
|
||||
float ds=(end-start)/count;
|
||||
float lod=clamp(log2(max(1.0,ds*128.0/uCloudLayer.z)),0.0,3.0);
|
||||
// Static interleaving avoids frame-to-frame noise; the bilateral pass removes low-resolution edges.
|
||||
float jitter=fract(52.9829189*fract(dot(floor(gl_FragCoord.xy),vec2(.06711056,.00583715))));
|
||||
for(int i=0;i<192;i++) {
|
||||
if(float(i)>=count || transmission<.01)break;
|
||||
float t=start+(float(i)+.15+jitter*.7)*ds;
|
||||
vec3 p=uCloudCamera+ray*t;
|
||||
float pixelLod=clamp(log2(max(1.0,t*uCloudPixelAngle*128.0/uCloudLayer.z)),0.0,7.0);
|
||||
float density=cloudDensity(p,max(lod,pixelLod),true);
|
||||
if(density<.001)continue;
|
||||
float alpha=1.0-exp(-density*ds*uCloudOptics.y);
|
||||
float weight=alpha*transmission;
|
||||
radiance+=sampleLighting(p,ray,density,max(lod,pixelLod))*weight;
|
||||
weightedDistance+=t*weight;
|
||||
transmission*=1.0-alpha;
|
||||
}
|
||||
}
|
||||
void main() {
|
||||
fragColor=vec4(0,0,0,1);
|
||||
vec3 worldRay=cameraRay(vUv),ray=cloudToLocal(worldRay);
|
||||
vec2 outer=cloudSphere(uCloudCamera,ray,uCloudLayer.x+uCloudLayer.y);
|
||||
float opaque=opaqueClipDistance(vUv)/max(.001,dot(worldRay,uForward));
|
||||
float start=max(0.0,outer.x),end=min(opaque,outer.y);
|
||||
if(end<=start)return;
|
||||
vec2 inner=cloudSphere(uCloudCamera,ray,uCloudLayer.x);
|
||||
vec3 radiance=vec3(0);float transmission=1.0,weightedDistance=0.0;
|
||||
if(inner.y>start && inner.x<end) {
|
||||
marchRange(start,min(end,inner.x),ray,radiance,transmission,weightedDistance);
|
||||
marchRange(max(start,inner.y),end,ray,radiance,transmission,weightedDistance);
|
||||
} else marchRange(start,end,ray,radiance,transmission,weightedDistance);
|
||||
float alpha=1.0-transmission;
|
||||
if(alpha>.0001) {
|
||||
float distance=weightedDistance/alpha;
|
||||
vec2 air=cloudSphere(uCloudCamera,ray,uCloudAir.x);
|
||||
float airStart=max(0.0,air.x),ds=max(0.0,distance-airStart)/6.0,column=0.0;
|
||||
for(int i=0;i<6;i++) {
|
||||
float altitude=cloudAltitude(uCloudCamera+ray*(airStart+(float(i)+.5)*ds));
|
||||
column+=exp(-max(0.0,altitude)/max(100.0,uCloudAir.x*.055))*ds;
|
||||
}
|
||||
vec3 extinction=exp(-(vec3(.000004)+uCloudAirColor*.000012)*column*uCloudAir.y);
|
||||
vec3 p=uCloudCamera+ray*distance;
|
||||
vec3 tint=cloudSunTint(p,uCloudPrimarySun);
|
||||
float warm=1.0-clamp(tint.b/max(.001,tint.r),0.0,1.0);
|
||||
float day=smoothstep(-.08,.10,dot(normalize(p+vec3(0,uCloudPlanetRadius,0)),uCloudPrimarySun));
|
||||
vec3 haze=mix(vec3(.45,.63,.87),vec3(.85,.28,.10),warm)*day*.75;
|
||||
radiance=radiance*extinction+haze*(1.0-extinction)*alpha;
|
||||
}
|
||||
fragColor=vec4(radiance,transmission);
|
||||
}
|
||||
27
src/Universe.App/Graphics/Shaders/final.frag.glsl
Normal file
27
src/Universe.App/Graphics/Shaders/final.frag.glsl
Normal file
@@ -0,0 +1,27 @@
|
||||
#version 330 core
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uScene;
|
||||
uniform sampler2D uBloom;
|
||||
uniform vec3 uTexel;
|
||||
uniform float uExposure;
|
||||
out vec4 fragColor;
|
||||
vec3 tone(vec2 uv) {
|
||||
vec3 c=(texture(uScene,uv).rgb+texture(uBloom,uv).rgb*0.16)*uExposure;
|
||||
return clamp((c*(2.51*c+0.03))/(c*(2.43*c+0.59)+0.14),0.0,1.0);
|
||||
}
|
||||
void main() {
|
||||
vec3 c=tone(vUv);
|
||||
vec3 n=tone(vUv+vec2(0,uTexel.y)), s=tone(vUv-vec2(0,uTexel.y));
|
||||
vec3 e=tone(vUv+vec2(uTexel.x,0)), w=tone(vUv-vec2(uTexel.x,0));
|
||||
vec3 luma=vec3(0.299,0.587,0.114);
|
||||
float l=dot(c,luma), lo=min(l,min(min(dot(n,luma),dot(s,luma)),min(dot(e,luma),dot(w,luma))));
|
||||
float hi=max(l,max(max(dot(n,luma),dot(s,luma)),max(dot(e,luma),dot(w,luma))));
|
||||
if(hi-lo>max(0.045,hi*0.18)) {
|
||||
vec2 dir=vec2(dot(s-n,luma),dot(e-w,luma));
|
||||
dir=normalize(dir+vec2(0.000001))*uTexel.xy;
|
||||
c=mix(c,(tone(vUv+dir*0.45)+tone(vUv-dir*0.45))*0.5,0.62);
|
||||
}
|
||||
c=pow(c,vec3(1.0/2.2));
|
||||
float dither=(fract(sin(dot(gl_FragCoord.xy,vec2(12.9898,78.233)))*43758.5453)-0.5)/255.0;
|
||||
fragColor=vec4(c+dither,1.0);
|
||||
}
|
||||
7
src/Universe.App/Graphics/Shaders/fullscreen.vert.glsl
Normal file
7
src/Universe.App/Graphics/Shaders/fullscreen.vert.glsl
Normal file
@@ -0,0 +1,7 @@
|
||||
#version 330 core
|
||||
layout(location=0) in vec3 aPosition;
|
||||
out vec2 vUv;
|
||||
void main() {
|
||||
vUv=aPosition.xy*0.5+0.5;
|
||||
gl_Position=vec4(aPosition.xy,0.0,1.0);
|
||||
}
|
||||
143
src/Universe.App/Graphics/Shaders/gas.frag.glsl
Normal file
143
src/Universe.App/Graphics/Shaders/gas.frag.glsl
Normal file
@@ -0,0 +1,143 @@
|
||||
#version 330 core
|
||||
#include "noise.glsl"
|
||||
#include "camera-ray.glsl"
|
||||
#include "ring-optics.glsl"
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uScene;
|
||||
uniform sampler2D uDepth;
|
||||
uniform float uFar;
|
||||
uniform float uRadius;
|
||||
uniform vec3 uCameraBody;
|
||||
uniform mat4 uWorldToBody;
|
||||
uniform vec3 uSurfaceColor;
|
||||
uniform vec3 uBandColor;
|
||||
uniform vec3 uPolarColor;
|
||||
uniform vec3 uHazeColor;
|
||||
uniform vec3 uSeedOffset;
|
||||
uniform vec3 uGasShape;
|
||||
uniform vec3 uGasOptics;
|
||||
uniform vec3 uGasLighting;
|
||||
uniform vec3 uGasBands;
|
||||
uniform vec3 uGasDetail;
|
||||
uniform vec3 uGasQuality;
|
||||
uniform float uLightExtinction;
|
||||
uniform float uStopTransmittance;
|
||||
uniform float uWindSpeed;
|
||||
uniform float uTime;
|
||||
uniform float uBandFrequency;
|
||||
uniform float uStormStrength;
|
||||
uniform int uLightCount;
|
||||
uniform vec3 uLightDirections[2];
|
||||
uniform vec3 uLightColors[2];
|
||||
uniform float uLightFlux[2];
|
||||
uniform int uPlanetIndex;
|
||||
uniform int uOccluderCount;
|
||||
uniform vec3 uOccluderCenters[16];
|
||||
uniform float uOccluderRadii[16];
|
||||
out vec4 fragColor;
|
||||
|
||||
vec3 axes() { return vec3(1.0,uGasShape.y,1.0); }
|
||||
vec2 intersectGas(vec3 o,vec3 d) {
|
||||
vec3 a=o/axes(), b=d/axes();
|
||||
float aa=dot(b,b), middle=-dot(a,b)/aa;
|
||||
vec3 closest=a+b*middle;
|
||||
float h=uGasShape.x*uGasShape.x-dot(closest,closest);
|
||||
if(h<0.0)return vec2(1e8,-1e8);
|
||||
h=sqrt(h/aa);return vec2(middle-h,middle+h);
|
||||
}
|
||||
float radialDensity(vec3 p) {
|
||||
float r=length(p/axes());
|
||||
float edge=1.0-smoothstep(uGasShape.x-max(0.0001,uGasShape.z*0.25),uGasShape.x,r);
|
||||
return exp(clamp((1.0-r)/uGasShape.z,-12.0,4.0))*edge;
|
||||
}
|
||||
float sunTransmittance(vec3 p,vec3 ld) {
|
||||
float lengthToExit=max(0.0,intersectGas(p,ld).y);
|
||||
float optical=0.0;
|
||||
int count=int(uGasQuality.y);
|
||||
for(int i=0;i<16;i++) {
|
||||
if(i>=count)break;
|
||||
float a=float(i)/float(count), b=float(i+1)/float(count);
|
||||
float ds=lengthToExit*(b*b-a*a);
|
||||
float t=lengthToExit*(a*a+b*b)*0.5;
|
||||
optical+=radialDensity(p+ld*t)*ds*uGasOptics.x;
|
||||
if(optical*uLightExtinction>15.0)break;
|
||||
}
|
||||
return exp(-optical*uLightExtinction);
|
||||
}
|
||||
vec3 cloudColor(vec3 p) {
|
||||
vec3 d=normalize(p/axes());
|
||||
float latitude=atan(d.y,length(d.xz));
|
||||
float longitude=atan(d.z,d.x)-uTime*uWindSpeed;
|
||||
float flow=noise3(vec3(cos(longitude)*8.0,latitude*5.0,sin(longitude)*8.0)+uSeedOffset);
|
||||
float bandPosition=latitude*uBandFrequency+flow*uGasDetail.x;
|
||||
float broad=noise3(vec3(bandPosition,2.7,uSeedOffset.x));
|
||||
float fine=noise3(vec3(latitude*uGasBands.y+flow*0.3,7.1,uSeedOffset.y));
|
||||
float stripe=clamp(0.5+broad*uGasBands.x+fine*uGasBands.z,0.0,1.0);
|
||||
vec3 color=mix(uSurfaceColor,uBandColor,stripe);
|
||||
color*=1.0+noise3(d*uGasDetail.z+uSeedOffset)*uGasDetail.y;
|
||||
float polar=smoothstep(0.62,1.35,abs(latitude));
|
||||
color=mix(color,uPolarColor,polar*0.65);
|
||||
float dx=atan(sin(longitude+0.35),cos(longitude+0.35));
|
||||
vec2 storm=vec2(dx*cos(latitude),latitude+0.36)/vec2(0.20,0.075);
|
||||
float spot=exp(-dot(storm,storm))*uStormStrength;
|
||||
color=mix(color,uBandColor*0.60,spot);
|
||||
float highAltitude=smoothstep(0.995,uGasShape.x,length(p/axes()));
|
||||
return mix(color,uHazeColor,highAltitude*0.18);
|
||||
}
|
||||
float eclipse(vec3 p,vec3 ld) {
|
||||
float visibility=1.0;
|
||||
for(int j=0;j<16;j++) {
|
||||
if(j>=uOccluderCount)break;
|
||||
if(j==uPlanetIndex)continue;
|
||||
vec3 o=uOccluderCenters[j]-p;
|
||||
float t=dot(o,ld);
|
||||
if(t>0.0) {
|
||||
float miss=length(o-ld*t);
|
||||
float soft=max(0.00001,uOccluderRadii[j]*0.02);
|
||||
visibility*=smoothstep(uOccluderRadii[j]-soft,uOccluderRadii[j]+soft,miss);
|
||||
}
|
||||
}
|
||||
return visibility;
|
||||
}
|
||||
void main() {
|
||||
vec3 ray=cameraRay(vUv);
|
||||
vec3 rd=normalize(mat3(uWorldToBody)*ray);
|
||||
vec2 hit=intersectGas(uCameraBody,rd);
|
||||
float sceneDepth=texture(uDepth,vUv).r;
|
||||
float sceneDistance=(exp2(sceneDepth*log2(uFar+1.0))-1.0)/max(dot(ray,uForward),0.001);
|
||||
float start=max(0.0,hit.x), end=min(hit.y,sceneDistance/uRadius);
|
||||
if(end<=start)discard;
|
||||
int count=int(uGasQuality.x);
|
||||
float transmittance=1.0, firstDepth=-1.0;
|
||||
vec3 scatter=vec3(0.0);
|
||||
float jitter=fract(sin(dot(gl_FragCoord.xy,vec2(12.9898,78.233)))*43758.5453);
|
||||
for(int i=0;i<192;i++) {
|
||||
if(i>=count)break;
|
||||
float a=pow(float(i)/float(count),uGasQuality.z), b=pow(float(i+1)/float(count),uGasQuality.z);
|
||||
float ds=(end-start)*(b-a);
|
||||
float t=start+(end-start)*mix(a,b,0.35+jitter*0.3);
|
||||
vec3 p=uCameraBody+rd*t;
|
||||
float rho=radialDensity(p)*(1.0+uGasDetail.y*noise3(normalize(p)*uGasDetail.z+uSeedOffset));
|
||||
float alpha=1.0-exp(-rho*ds*uGasOptics.x);
|
||||
vec3 illumination=vec3(uGasLighting.y);
|
||||
for(int l=0;l<2;l++) {
|
||||
if(l>=uLightCount)break;
|
||||
vec3 ld=uLightDirections[l];
|
||||
float visibility=sunTransmittance(p,ld)*eclipse(p,ld)*ringTransmission(p,ld);
|
||||
float g=uGasOptics.z, mu=dot(rd,ld);
|
||||
float phase=(1.0-g*g)/pow(max(0.05,1.0+g*g-2.0*g*mu),1.5);
|
||||
illumination+=uLightColors[l]*uLightFlux[l]*uGasLighting.x*visibility*mix(1.0,phase,0.35);
|
||||
}
|
||||
scatter+=transmittance*alpha*cloudColor(p)*illumination*uGasOptics.y;
|
||||
transmittance*=1.0-alpha;
|
||||
if(firstDepth<0.0 && transmittance<0.65)firstDepth=t;
|
||||
if(transmittance<uStopTransmittance)break;
|
||||
}
|
||||
if(transmittance>0.9995)discard;
|
||||
fragColor=vec4(texture(uScene,vUv).rgb*transmittance+scatter,1.0);
|
||||
gl_FragDepth=sceneDepth;
|
||||
if(firstDepth>=0.0) {
|
||||
float clipDistance=firstDepth*uRadius*max(dot(ray,uForward),0.001);
|
||||
gl_FragDepth=min(sceneDepth,log2(1.0+clipDistance)/log2(1.0+uFar));
|
||||
}
|
||||
}
|
||||
55
src/Universe.App/Graphics/Shaders/noise.glsl
Normal file
55
src/Universe.App/Graphics/Shaders/noise.glsl
Normal file
@@ -0,0 +1,55 @@
|
||||
uniform sampler3D uNoise;
|
||||
const mat3 octaveRotation = mat3(0.0,-0.8,-0.6, 0.8,0.36,-0.48, 0.6,-0.48,0.64);
|
||||
float noise3(vec3 p) {
|
||||
vec3 i = floor(p);
|
||||
vec3 f = fract(p);
|
||||
f = f*f*f*(f*(f*6.0-15.0)+10.0);
|
||||
return texture(uNoise, (i+f+0.5)/64.0).a;
|
||||
}
|
||||
float preciseNoise3(vec3 p) {
|
||||
ivec3 i=ivec3(floor(p));
|
||||
vec3 f=fract(p);
|
||||
f=f*f*f*(f*(f*6.0-15.0)+10.0);
|
||||
float a=mix(texelFetch(uNoise,i&63,0).a,texelFetch(uNoise,(i+ivec3(1,0,0))&63,0).a,f.x);
|
||||
float b=mix(texelFetch(uNoise,(i+ivec3(0,1,0))&63,0).a,texelFetch(uNoise,(i+ivec3(1,1,0))&63,0).a,f.x);
|
||||
float c=mix(texelFetch(uNoise,(i+ivec3(0,0,1))&63,0).a,texelFetch(uNoise,(i+ivec3(1,0,1))&63,0).a,f.x);
|
||||
float d=mix(texelFetch(uNoise,(i+ivec3(0,1,1))&63,0).a,texelFetch(uNoise,(i+ivec3(1,1,1))&63,0).a,f.x);
|
||||
return mix(mix(a,b,f.y),mix(c,d,f.y),f.z);
|
||||
}
|
||||
float gradientNoise3(vec3 p) {
|
||||
ivec3 i=ivec3(floor(p));
|
||||
vec3 f=fract(p), t=f*f*f*(f*(f*6.0-15.0)+10.0);
|
||||
float a=mix(dot(texelFetch(uNoise,i&63,0).rgb,f),dot(texelFetch(uNoise,(i+ivec3(1,0,0))&63,0).rgb,f-vec3(1,0,0)),t.x);
|
||||
float b=mix(dot(texelFetch(uNoise,(i+ivec3(0,1,0))&63,0).rgb,f-vec3(0,1,0)),dot(texelFetch(uNoise,(i+ivec3(1,1,0))&63,0).rgb,f-vec3(1,1,0)),t.x);
|
||||
float c=mix(dot(texelFetch(uNoise,(i+ivec3(0,0,1))&63,0).rgb,f-vec3(0,0,1)),dot(texelFetch(uNoise,(i+ivec3(1,0,1))&63,0).rgb,f-vec3(1,0,1)),t.x);
|
||||
float d=mix(dot(texelFetch(uNoise,(i+ivec3(0,1,1))&63,0).rgb,f-vec3(0,1,1)),dot(texelFetch(uNoise,(i+ivec3(1,1,1))&63,0).rgb,f-vec3(1,1,1)),t.x);
|
||||
return clamp(mix(mix(a,b,t.y),mix(c,d,t.y),t.z)*1.7,-1.0,1.0);
|
||||
}
|
||||
float fbm(vec3 p) {
|
||||
float result=0.0, amplitude=0.5;
|
||||
for(int i=0;i<5;i++) {
|
||||
result += noise3(p)*amplitude;
|
||||
p = octaveRotation*p*2.03 + vec3(17.1,9.2,13.5);
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
float terrainHeight(vec3 direction, vec3 offset, int octaves) {
|
||||
vec3 p = direction*3.2 + offset;
|
||||
vec3 warp = vec3(preciseNoise3(p*0.82+vec3(5.2,1.3,9.7)),
|
||||
preciseNoise3(p*0.82+vec3(19.1,7.7,2.4)),
|
||||
preciseNoise3(p*0.82+vec3(3.4,21.8,11.6)));
|
||||
float continent=preciseNoise3(p*0.47);
|
||||
p=direction*52.0+offset+warp*2.6;
|
||||
float weight=1.0, amplitude=0.58, sum=0.0;
|
||||
for(int i=0;i<7;i++) {
|
||||
if(i>=octaves) break;
|
||||
float ridge = 1.0-abs(gradientNoise3(p));
|
||||
ridge = ridge*ridge*ridge*weight;
|
||||
sum += ridge*amplitude;
|
||||
weight=0.55+0.45*ridge;
|
||||
p=octaveRotation*p*2.03 + vec3(17.1,9.2,13.5);
|
||||
amplitude*=0.50;
|
||||
}
|
||||
return (sum-0.42)*0.95+continent*0.16;
|
||||
}
|
||||
208
src/Universe.App/Graphics/Shaders/ocean.frag.glsl
Normal file
208
src/Universe.App/Graphics/Shaders/ocean.frag.glsl
Normal file
@@ -0,0 +1,208 @@
|
||||
#version 330 core
|
||||
#include "noise.glsl"
|
||||
#include "planet-height.glsl"
|
||||
#include "camera-ray.glsl"
|
||||
#include "cloud-common.glsl"
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uScene;
|
||||
uniform sampler2D uDepth;
|
||||
uniform float uFar;
|
||||
uniform float uSeaRadius;
|
||||
uniform float uCameraHeight;
|
||||
uniform vec3 uAxisX;
|
||||
uniform vec3 uAxisY;
|
||||
uniform vec3 uAxisZ;
|
||||
uniform vec3 uWaveVectors[12];
|
||||
uniform float uWavePhases[12];
|
||||
uniform float uWaveAmplitudes[12];
|
||||
uniform vec3 uDetailOrigins[2];
|
||||
uniform vec3 uWaveSettings; // maximum displacement, choppiness, pixel angular size
|
||||
uniform vec3 uWaterOptics; // roughness, reflection, refraction
|
||||
uniform vec3 uDeepColor;
|
||||
uniform vec3 uShallowColor;
|
||||
uniform vec3 uAbsorption;
|
||||
uniform float uFoamStrength;
|
||||
uniform int uLightCount;
|
||||
uniform vec3 uLightDirections[2];
|
||||
uniform vec3 uLightColors[2];
|
||||
uniform float uLightFlux[2];
|
||||
uniform float uTime;
|
||||
out vec4 fragColor;
|
||||
const float PI=3.14159265359;
|
||||
vec3 toLocal(vec3 v) { return vec3(dot(v,uAxisX),dot(v,uAxisY),dot(v,uAxisZ)); }
|
||||
vec3 toWorld(vec3 v) { return uAxisX*v.x+uAxisY*v.y+uAxisZ*v.z; }
|
||||
float sceneDistance(vec2 uv,vec3 ray) {
|
||||
return (exp2(texture(uDepth,uv).r*log2(uFar+1.0))-1.0)/max(.001,dot(ray,uForward));
|
||||
}
|
||||
// Stable roots avoid subtracting two planet-sized floats when the camera is metres above water.
|
||||
vec2 oceanSphere(vec3 ro,vec3 rd,float expansion) {
|
||||
float b=dot(ro+vec3(0,uSeaRadius,0),rd);
|
||||
float c=dot(ro,ro)+2.0*uSeaRadius*(ro.y-expansion)-expansion*expansion;
|
||||
float h=b*b-c;
|
||||
if(h<0.0)return vec2(1e25,-1e25);
|
||||
float q=-b-(b<0.0?-1.0:1.0)*sqrt(h);
|
||||
float a=q, d=c/(abs(q)<1e-12?1e-12:q);
|
||||
return vec2(min(a,d),max(a,d));
|
||||
}
|
||||
float radialHeight(vec3 p) {
|
||||
return (dot(p,p)+2.0*uSeaRadius*p.y)/(length(p+vec3(0,uSeaRadius,0))+uSeaRadius);
|
||||
}
|
||||
float waveHeight(vec3 p,float footprint) {
|
||||
float height=0.0;
|
||||
for(int i=0;i<8;i++) {
|
||||
float weight=1.0-smoothstep(.35,1.0,length(uWaveVectors[i])*footprint);
|
||||
float base=dot(p,uWaveVectors[i])+uWavePhases[i];
|
||||
float phase=base;
|
||||
for(int j=0;j<2;j++)phase=base-uWaveSettings.y*.48*cos(phase);
|
||||
height+=sin(phase)*uWaveAmplitudes[i]*weight;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
vec3 waveNormal(vec3 p,float footprint,out float compression) {
|
||||
vec3 gradient=vec3(0); compression=0.0;
|
||||
float total=0.0;
|
||||
for(int i=0;i<12;i++) {
|
||||
float weight=1.0-smoothstep(.35,1.0,length(uWaveVectors[i])*footprint);
|
||||
float base=dot(p,uWaveVectors[i])+uWavePhases[i], phase=base;
|
||||
float q=uWaveSettings.y*.48;
|
||||
for(int j=0;j<3;j++)phase=base-q*cos(phase);
|
||||
float a=uWaveAmplitudes[i]*weight;
|
||||
gradient+=uWaveVectors[i]*(a*cos(phase)/max(.15,1.0-q*sin(phase)));
|
||||
compression+=max(0.0,sin(phase))*a;
|
||||
total+=a;
|
||||
}
|
||||
compression/=max(.001,total);
|
||||
vec3 up=normalize(p+vec3(0,uSeaRadius,0));
|
||||
gradient-=up*dot(gradient,up);
|
||||
// Capillary ripples use a planet-anchored noise field and disappear below the pixel footprint.
|
||||
vec3 bodyPoint=mat3(uWorldToBody)*toWorld(p);
|
||||
vec3 bodyX=mat3(uWorldToBody)*uAxisX,bodyZ=mat3(uWorldToBody)*uAxisZ;
|
||||
for(int detail=0;detail<2;detail++) {
|
||||
float frequency=detail==0?.35:1.4;
|
||||
float weight=1.0-smoothstep(.25,.8,footprint*frequency);
|
||||
vec3 q=bodyPoint*frequency+uDetailOrigins[detail];
|
||||
float gx=(noise3(q+bodyX*.04)-noise3(q-bodyX*.04))/.08;
|
||||
float gz=(noise3(q+bodyZ*.04)-noise3(q-bodyZ*.04))/.08;
|
||||
gradient+=vec3(gx,0,gz)*weight*(detail==0?.13:.075);
|
||||
}
|
||||
return normalize(up-gradient);
|
||||
}
|
||||
vec3 skyReflection(vec3 direction,vec3 up) {
|
||||
float daylight=0.0;
|
||||
for(int l=0;l<2;l++){if(l>=uLightCount)break;daylight+=sqrt(uLightFlux[l])*smoothstep(-.08,.16,dot(up,uLightDirections[l]));}
|
||||
float elevation=max(0.0,dot(direction,up));
|
||||
vec3 sky=mix(vec3(.56,.73,.87),vec3(.12,.32,.64),sqrt(elevation));
|
||||
for(int l=0;l<2;l++) {
|
||||
if(l>=uLightCount)break;
|
||||
sky+=uLightColors[l]*pow(max(0.0,dot(direction,uLightDirections[l])),24.0)*.22*uLightFlux[l];
|
||||
}
|
||||
return sky*(.006+daylight*1.35);
|
||||
}
|
||||
vec3 screenReflection(vec3 position,vec3 direction,vec3 fallback) {
|
||||
vec3 world=toWorld(position-vec3(0,uCameraHeight,0));
|
||||
vec3 reflected=toWorld(direction);
|
||||
float stepSize=3.0;
|
||||
for(int i=0;i<9;i++) {
|
||||
vec3 q=world+reflected*stepSize;
|
||||
float z=dot(q,uForward);
|
||||
if(z>.1) {
|
||||
vec2 uv=.5+vec2(dot(q,uRight)/(uAspect*uTanHalfFov),dot(q,uUp)/uTanHalfFov)/(2.0*z);
|
||||
if(all(greaterThan(uv,vec2(.015))) && all(lessThan(uv,vec2(.985)))) {
|
||||
float depth=exp2(texture(uDepth,uv).r*log2(uFar+1.0))-1.0;
|
||||
if(z>depth && z-depth<max(1.0,stepSize*.09))return texture(uScene,uv).rgb;
|
||||
}
|
||||
}
|
||||
stepSize*=1.9;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
void main() {
|
||||
vec3 scene=texture(uScene,vUv).rgb;
|
||||
float originalDepth=texture(uDepth,vUv).r;
|
||||
gl_FragDepth=originalDepth;
|
||||
vec3 worldRay=cameraRay(vUv), rd=toLocal(worldRay), ro=vec3(0,uCameraHeight,0);
|
||||
float opaque=sceneDistance(vUv,worldRay);
|
||||
float displacement=uWaveSettings.x;
|
||||
bool underwater=uCameraHeight<displacement && uCameraHeight<waveHeight(ro,.02);
|
||||
vec2 envelope=oceanSphere(ro,rd,displacement);
|
||||
vec2 base=oceanSphere(ro,rd,0.0);
|
||||
float t=base.x>0.0?base.x:base.y;
|
||||
bool hit=envelope.y>0.0 && t>0.0;
|
||||
if(hit && abs(uCameraHeight)<2500.0 && t<2000.0) {
|
||||
vec2 inner=oceanSphere(ro,rd,-displacement);
|
||||
float low=max(.015,envelope.x),high=inner.x>0.0?inner.x:envelope.y;
|
||||
if(underwater) {low=max(.015,inner.y);high=envelope.y;}
|
||||
float reference=radialHeight(ro+rd*low)-waveHeight(ro+rd*low,max(.03,low*uWaveSettings.z));
|
||||
float endValue=radialHeight(ro+rd*high)-waveHeight(ro+rd*high,max(.03,high*uWaveSettings.z));
|
||||
if(reference*endValue<0.0 && high>low) {
|
||||
for(int i=0;i<18;i++) {
|
||||
float mid=(low+high)*.5;
|
||||
vec3 p=ro+rd*mid;
|
||||
float height=radialHeight(p)-waveHeight(p,max(.03,mid*uWaveSettings.z));
|
||||
if(height*reference>0.0)low=mid;else high=mid;
|
||||
}
|
||||
float solved=(low+high)*.5;
|
||||
t=mix(solved,t,smoothstep(800.0,2000.0,t));
|
||||
}
|
||||
}
|
||||
vec3 p=ro+rd*max(0.0,t);
|
||||
vec3 radial=normalize(uAxisY+toWorld(p)/uSeaRadius);
|
||||
float waterMask=uHasOceanMask!=0?oceanMaskValue(bodyDirection(radial)):1.0;
|
||||
hit=hit && waterMask>.01 && t<opaque+.15;
|
||||
if(!hit) {
|
||||
float maskHere=uHasOceanMask!=0?oceanMaskValue(bodyDirection(uAxisY)):1.0;
|
||||
if(underwater && maskHere>.5) {
|
||||
vec3 transmission=exp(-uAbsorption*min(opaque,400.0));
|
||||
scene=scene*transmission+uShallowColor*(1.0-transmission)*.5;
|
||||
}
|
||||
fragColor=vec4(scene,1);return;
|
||||
}
|
||||
float footprint=max(.02,t*uWaveSettings.z/max(.12,abs(dot(rd,normalize(p+vec3(0,uSeaRadius,0))))));
|
||||
float compression;
|
||||
vec3 n=waveNormal(p,footprint,compression), up=normalize(p+vec3(0,uSeaRadius,0));
|
||||
vec3 view=-rd;
|
||||
if(dot(n,view)<0.0)n=-n;
|
||||
float ndv=max(.02,dot(n,view));
|
||||
float fresnel=.0204+.9796*pow(1.0-ndv,5.0);
|
||||
vec3 reflectionDirection=reflect(rd,n);
|
||||
vec3 reflected=skyReflection(reflectionDirection,up);
|
||||
reflected=cloudReflection(toWorld(p-ro),toWorld(reflectionDirection),reflected);
|
||||
if(t<1500.0 && !underwater)reflected=screenReflection(p,reflectionDirection,reflected);
|
||||
vec2 refractUv=clamp(vUv+vec2(dot(toWorld(n),uRight),dot(toWorld(n),uUp))*uWaterOptics.z/(1.0+t*.012),vec2(.001),vec2(.999));
|
||||
float floorDistance=sceneDistance(refractUv,cameraRay(refractUv));
|
||||
float thickness=max(.0,min(400.0,floorDistance-t));
|
||||
vec3 transmission=exp(-uAbsorption*thickness);
|
||||
vec3 scatter=mix(uShallowColor,uDeepColor,1.0-exp(-thickness*.075));
|
||||
vec3 lighting=vec3(.04), highlights=vec3(0);
|
||||
for(int l=0;l<2;l++) {
|
||||
if(l>=uLightCount)break;
|
||||
vec3 light=uLightDirections[l];
|
||||
float sunHeight=dot(up,light), visible=smoothstep(-.02,.03,sunHeight);
|
||||
visible*=cloudShadow(toWorld(p-ro),toWorld(light));
|
||||
float ndl=max(.001,dot(n,light));
|
||||
lighting+=uLightColors[l]*uLightFlux[l]*visible*(.25+.75*max(0.0,sunHeight));
|
||||
vec3 halfVector=normalize(view+light);
|
||||
float ndh=max(0.0,dot(n,halfVector));
|
||||
float alpha=max(.045,uWaterOptics.x*uWaterOptics.x+min(.14,footprint*.001));
|
||||
float a2=alpha*alpha, den=ndh*ndh*(a2-1.0)+1.0;
|
||||
float distribution=a2/(PI*den*den);
|
||||
float k=(alpha+1.0)*(alpha+1.0)/8.0;
|
||||
float geometry=ndv/(ndv*(1.0-k)+k)*ndl/(ndl*(1.0-k)+k);
|
||||
float f=.0204+.9796*pow(1.0-max(0.0,dot(view,halfVector)),5.0);
|
||||
highlights+=uLightColors[l]*uLightFlux[l]*visible*min(25.0,distribution*geometry*f/(4.0*ndv))*1.8;
|
||||
scatter+=uShallowColor*pow(max(0.0,dot(view,-light)),4.0)*compression*.28*visible;
|
||||
}
|
||||
vec3 refracted=texture(uScene,refractUv).rgb*transmission+scatter*(1.0-transmission)*lighting;
|
||||
vec3 color=mix(refracted,reflected*uWaterOptics.y,fresnel)+highlights;
|
||||
float shore=(1.0-smoothstep(.2,3.5,thickness))*(.5+.5*sin(uTime*1.3+t*.03));
|
||||
vec3 bodyDetail=mat3(uWorldToBody)*toWorld(p)*.35+uDetailOrigins[0];
|
||||
float breakup=noise3(bodyDetail+vec3(uTime*.09,0,uTime*.06))*.5+.5;
|
||||
float foam=clamp((smoothstep(.54,.78,compression)+shore*.7)*uFoamStrength*smoothstep(.22,.72,breakup),0.0,.9);
|
||||
color=mix(color,vec3(.72,.86,.88)*lighting,foam);
|
||||
if(underwater) {
|
||||
vec3 through=exp(-uAbsorption*min(t,400.0));
|
||||
color=color*through+uShallowColor*(1.0-through)*.5;
|
||||
}
|
||||
fragColor=vec4(max(color,vec3(0)),1);
|
||||
gl_FragDepth=clamp(log2(1.0+t*max(.001,dot(worldRay,uForward)))/log2(uFar+1.0),0.0,1.0);
|
||||
}
|
||||
53
src/Universe.App/Graphics/Shaders/planet-height.glsl
Normal file
53
src/Universe.App/Graphics/Shaders/planet-height.glsl
Normal file
@@ -0,0 +1,53 @@
|
||||
uniform sampler2D uHeightMap;
|
||||
uniform int uHasHeightMap;
|
||||
uniform float uRadius;
|
||||
uniform float uAmplitude;
|
||||
uniform float uNoiseFrequency;
|
||||
uniform vec3 uNoiseOffset;
|
||||
uniform vec3 uHeightRange;
|
||||
uniform vec3 uLandMask;
|
||||
uniform mat4 uWorldToBody;
|
||||
uniform sampler2D uOceanMask;
|
||||
uniform int uHasOceanMask;
|
||||
uniform vec3 uOceanTerrain; // sea level/radius, depth/radius, enabled
|
||||
uniform sampler2D uRegionalHeight;
|
||||
uniform int uHasRegionalHeight;
|
||||
uniform vec3 uRegionBounds; // west, south, east in degrees
|
||||
uniform vec3 uRegionOther; // north, fade in degrees
|
||||
|
||||
vec3 bodyDirection(vec3 direction) { return normalize(mat3(uWorldToBody)*normalize(direction)); }
|
||||
vec2 planetUv(vec3 d) { return vec2(0.5-atan(d.z,d.x)/6.28318530718,atan(length(d.xz),d.y)/3.14159265359); }
|
||||
float mapTexel(sampler2D raster,ivec2 p, ivec2 size) {
|
||||
// Bilinear sampling can cross only one column outside either longitude edge.
|
||||
int x=p.x<0 ? p.x+size.x : (p.x>=size.x ? p.x-size.x : p.x);
|
||||
return texelFetch(raster,ivec2(x,clamp(p.y,0,size.y-1)),0).r;
|
||||
}
|
||||
float rasterValue(sampler2D raster,vec2 uv) {
|
||||
ivec2 size=textureSize(raster,0);
|
||||
vec2 p=vec2(fract(uv.x)*float(size.x)-0.5,clamp(uv.y*float(size.y)-0.5,0.0,float(size.y-1)));
|
||||
ivec2 i=ivec2(floor(p)); vec2 f=fract(p);
|
||||
float a=mix(mapTexel(raster,i,size),mapTexel(raster,i+ivec2(1,0),size),f.x);
|
||||
float b=mix(mapTexel(raster,i+ivec2(0,1),size),mapTexel(raster,i+ivec2(1,1),size),f.x);
|
||||
return mix(a,b,f.y);
|
||||
}
|
||||
float heightMapValue(vec3 localDirection) { return rasterValue(uHeightMap,planetUv(localDirection)); }
|
||||
float oceanMaskValue(vec3 localDirection) { return rasterValue(uOceanMask,planetUv(localDirection)); }
|
||||
float surfaceElevation(vec3 worldDirection) {
|
||||
vec3 d=bodyDirection(worldDirection);
|
||||
float mapValue=uHasHeightMap!=0 ? heightMapValue(d) : 0.0;
|
||||
float mapped=uHasHeightMap!=0 ? mix(uHeightRange.x,uHeightRange.y,pow(mapValue,uHeightRange.z)) : 0.0;
|
||||
if(uHasRegionalHeight!=0) {
|
||||
// Avoid subtracting nearby global UVs: the regional tile is only one degree wide.
|
||||
float lon=-degrees(atan(d.z,d.x)),lat=degrees(atan(d.y,length(d.xz)));
|
||||
float edge=min(min(lon-uRegionBounds.x,uRegionBounds.z-lon),min(lat-uRegionBounds.y,uRegionOther.x-lat));
|
||||
if(edge>0.0) {
|
||||
vec2 pos=vec2((lon-uRegionBounds.x)/(uRegionBounds.z-uRegionBounds.x),
|
||||
(uRegionOther.x-lat)/(uRegionOther.x-uRegionBounds.y));
|
||||
mapped=mix(mapped,texture(uRegionalHeight,clamp(pos,0.0,1.0)).r/uRadius,smoothstep(0.0,uRegionOther.y,edge));
|
||||
}
|
||||
}
|
||||
float mask=(uHasHeightMap!=0 && uLandMask.x>0.5) ? smoothstep(uLandMask.y,uLandMask.y+uLandMask.z,mapValue) : 1.0;
|
||||
float detail=uAmplitude>0.0 ? terrainHeight(d*uNoiseFrequency,uNoiseOffset,9)*uAmplitude*mask : 0.0;
|
||||
float water=uHasOceanMask!=0 ? oceanMaskValue(d) : 0.0;
|
||||
return mix(mapped+detail,uOceanTerrain.x-uOceanTerrain.y,water*uOceanTerrain.z);
|
||||
}
|
||||
10
src/Universe.App/Graphics/Shaders/point.frag.glsl
Normal file
10
src/Universe.App/Graphics/Shaders/point.frag.glsl
Normal file
@@ -0,0 +1,10 @@
|
||||
#version 330 core
|
||||
in vec3 vColor;
|
||||
uniform float uBrightness;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
float r=length(gl_PointCoord*2.0-1.0);
|
||||
if(r>1.0) discard;
|
||||
float core=exp(-r*r*6.0);
|
||||
fragColor=vec4(vColor*core*uBrightness,1.0);
|
||||
}
|
||||
15
src/Universe.App/Graphics/Shaders/point.vert.glsl
Normal file
15
src/Universe.App/Graphics/Shaders/point.vert.glsl
Normal file
@@ -0,0 +1,15 @@
|
||||
#version 330 core
|
||||
layout(location=0) in vec3 aPosition;
|
||||
layout(location=1) in vec3 aColor;
|
||||
layout(location=2) in float aSize;
|
||||
uniform mat4 uViewProjection;
|
||||
uniform float uFar;
|
||||
uniform float uScale;
|
||||
out vec3 vColor;
|
||||
void main() {
|
||||
vec4 clip=uViewProjection*vec4(aPosition*uScale,1.0);
|
||||
gl_Position=clip;
|
||||
gl_Position.z=(2.0*log2(max(0.0001,clip.w+1.0))/log2(uFar+1.0)-1.0)*clip.w;
|
||||
gl_PointSize=aSize;
|
||||
vColor=aColor;
|
||||
}
|
||||
17
src/Universe.App/Graphics/Shaders/ring-optics.glsl
Normal file
17
src/Universe.App/Graphics/Shaders/ring-optics.glsl
Normal file
@@ -0,0 +1,17 @@
|
||||
uniform sampler2D uRingProfile;
|
||||
uniform int uHasRings;
|
||||
uniform vec3 uRingBounds;
|
||||
uniform float uRingShadowSoftness;
|
||||
vec4 ringMaterial(float radius) {
|
||||
if(radius<uRingBounds.x || radius>uRingBounds.y)return vec4(0.0);
|
||||
float u=(radius-uRingBounds.x)/(uRingBounds.y-uRingBounds.x);
|
||||
return textureLod(uRingProfile,vec2(u,0.5),0.0);
|
||||
}
|
||||
float ringTransmission(vec3 bodyPosition,vec3 bodyLightDirection) {
|
||||
if(uHasRings==0 || abs(bodyLightDirection.y)<0.000001)return 1.0;
|
||||
float t=-bodyPosition.y/bodyLightDirection.y;
|
||||
if(t<=0.0)return 1.0;
|
||||
float r=length((bodyPosition+bodyLightDirection*t).xz);
|
||||
float tau=(ringMaterial(r-uRingShadowSoftness).a+2.0*ringMaterial(r).a+ringMaterial(r+uRingShadowSoftness).a)*0.25;
|
||||
return exp(-tau/max(abs(bodyLightDirection.y),0.035));
|
||||
}
|
||||
54
src/Universe.App/Graphics/Shaders/ring.frag.glsl
Normal file
54
src/Universe.App/Graphics/Shaders/ring.frag.glsl
Normal file
@@ -0,0 +1,54 @@
|
||||
#version 330 core
|
||||
#include "camera-ray.glsl"
|
||||
#include "ring-optics.glsl"
|
||||
in vec2 vUv;
|
||||
uniform float uRadius;
|
||||
uniform float uFar;
|
||||
uniform vec3 uCameraBody;
|
||||
uniform mat4 uWorldToBody;
|
||||
uniform float uPolarScale;
|
||||
uniform vec3 uRingLighting;
|
||||
uniform vec3 uRingPhase;
|
||||
uniform vec3 uRockRange;
|
||||
uniform int uLightCount;
|
||||
uniform vec3 uLightDirections[2];
|
||||
uniform vec3 uLightColors[2];
|
||||
uniform float uLightFlux[2];
|
||||
out vec4 fragColor;
|
||||
float planetShadow(vec3 p,vec3 l) {
|
||||
vec3 o=p/vec3(1,uPolarScale,1),d=l/vec3(1,uPolarScale,1);
|
||||
float t=max(0.0,-dot(o,d)/dot(d,d));
|
||||
float miss=length(o+d*t);
|
||||
return smoothstep(1.0-uRingShadowSoftness,1.0+uRingShadowSoftness,miss);
|
||||
}
|
||||
void main() {
|
||||
vec3 ray=cameraRay(vUv),rd=normalize(mat3(uWorldToBody)*ray);
|
||||
if(abs(rd.y)<1e-9)discard;
|
||||
float t=-uCameraBody.y/rd.y;
|
||||
if(t<=0.0)discard;
|
||||
vec3 p=uCameraBody+rd*t;
|
||||
float r=length(p.xz);
|
||||
if(r<uRingBounds.x || r>uRingBounds.y)discard;
|
||||
// Derivatives choose the radial profile's mip level, avoiding distant moire.
|
||||
float x=(r-uRingBounds.x)/(uRingBounds.y-uRingBounds.x);
|
||||
vec4 material=texture(uRingProfile,vec2(x,0.5));
|
||||
float optical=material.a;
|
||||
if(optical<0.0001)discard;
|
||||
float meters=t*uRadius;
|
||||
float resolved=1.0-smoothstep(uRockRange.x,uRockRange.y,meters);
|
||||
float dust=mix(1.0,uRockRange.z,resolved);
|
||||
float alpha=(1.0-exp(-optical/max(abs(rd.y),0.025)))*dust;
|
||||
vec3 light=vec3(uRingLighting.y);
|
||||
for(int i=0;i<2;i++) {
|
||||
if(i>=uLightCount)break;
|
||||
vec3 l=uLightDirections[i];
|
||||
float g=uRingPhase.x,mu=dot(rd,l);
|
||||
float phase=(1.0-g*g)/pow(max(0.06,1.0+g*g-2.0*g*mu),1.5);
|
||||
float sameSide=step(0.0,-rd.y*l.y);
|
||||
float reflection=(0.20+0.80*sqrt(abs(l.y)))*sameSide;
|
||||
float transmission=uRingPhase.y*phase*(1.0-sameSide)*exp(-optical*0.6);
|
||||
light+=uLightColors[i]*uLightFlux[i]*uRingLighting.x*planetShadow(p,l)*(reflection+transmission);
|
||||
}
|
||||
fragColor=vec4(material.rgb*light,alpha);
|
||||
gl_FragDepth=log2(1.0+meters*max(dot(ray,uForward),0.001))/log2(1.0+uFar);
|
||||
}
|
||||
16
src/Universe.App/Graphics/Shaders/ring.vert.glsl
Normal file
16
src/Universe.App/Graphics/Shaders/ring.vert.glsl
Normal file
@@ -0,0 +1,16 @@
|
||||
#version 330 core
|
||||
layout(location=0) in vec3 aPosition;
|
||||
uniform mat4 uModel;
|
||||
uniform mat4 uViewProjection;
|
||||
uniform float uFar;
|
||||
out vec3 vRingPosition;
|
||||
out vec3 vWorldPosition;
|
||||
out float vFragDepth;
|
||||
void main() {
|
||||
vRingPosition=aPosition;
|
||||
vWorldPosition=(uModel*vec4(aPosition,1.0)).xyz;
|
||||
vec4 clip=uViewProjection*vec4(vWorldPosition,1.0);
|
||||
vFragDepth=clip.w+1.0;
|
||||
gl_Position=clip;
|
||||
gl_Position.z=(2.0*log2(max(0.0001,vFragDepth))/log2(uFar+1.0)-1.0)*clip.w;
|
||||
}
|
||||
37
src/Universe.App/Graphics/Shaders/rock-common.glsl
Normal file
37
src/Universe.App/Graphics/Shaders/rock-common.glsl
Normal file
@@ -0,0 +1,37 @@
|
||||
uniform float uFar;
|
||||
uniform float uRadius;
|
||||
uniform float uPolarScale;
|
||||
uniform vec3 uLightDirections[2];
|
||||
uniform vec3 uLightColors[2];
|
||||
uniform float uLightFlux[2];
|
||||
uniform int uLightCount;
|
||||
uniform vec3 uRockLighting;
|
||||
uniform vec3 uRockMaterial;
|
||||
uniform mat4 uWorldToBody;
|
||||
uniform vec3 uCameraBody;
|
||||
uniform vec3 uRockLod;
|
||||
uniform vec3 uRockFade;
|
||||
float rockMeshWeight(float distanceMeters,float pixels) {
|
||||
return (1.0-smoothstep(uRockLod.x-uRockLod.y,uRockLod.x+uRockLod.y,distanceMeters))*smoothstep(uRockLod.z*0.65,uRockLod.z*1.35,pixels);
|
||||
}
|
||||
float rockPlanetShadow(vec3 p,vec3 l) {
|
||||
vec3 o=p/vec3(1,uPolarScale,1),d=l/vec3(1,uPolarScale,1);
|
||||
float t=max(0.0,-dot(o,d)/dot(d,d));
|
||||
return smoothstep(1.0-uRingShadowSoftness,1.0+uRingShadowSoftness,length(o+d*t));
|
||||
}
|
||||
vec3 rockIllumination(vec3 color,vec3 normal,vec3 worldPosition,float seed) {
|
||||
vec3 p=uCameraBody+mat3(uWorldToBody)*worldPosition/uRadius;
|
||||
vec3 n=normalize(mat3(uWorldToBody)*normal);
|
||||
vec3 view=normalize(mat3(uWorldToBody)*(-worldPosition));
|
||||
vec3 light=color*uRockLighting.y;
|
||||
for(int i=0;i<2;i++) {
|
||||
if(i>=uLightCount)break;
|
||||
vec3 l=uLightDirections[i];
|
||||
float selfShadow=ringTransmission(p,l);
|
||||
float visibility=rockPlanetShadow(p,l)*mix(0.3,1.0,selfShadow);
|
||||
float diffuse=max(dot(n,l),0.0);
|
||||
float ice=pow(max(dot(n,normalize(l+view)),0.0),mix(70.0,8.0,uRockMaterial.y))*uRockMaterial.z;
|
||||
light+=(color*diffuse+vec3(ice))*uLightColors[i]*uLightFlux[i]*uRockLighting.x*visibility;
|
||||
}
|
||||
return light;
|
||||
}
|
||||
13
src/Universe.App/Graphics/Shaders/rock-point.frag.glsl
Normal file
13
src/Universe.App/Graphics/Shaders/rock-point.frag.glsl
Normal file
@@ -0,0 +1,13 @@
|
||||
#version 330 core
|
||||
in vec3 vColor;
|
||||
in float vAlpha;
|
||||
in float vClipDepth;
|
||||
uniform float uFar;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
float r=length(gl_PointCoord*2.0-1.0);
|
||||
if(r>1.0 || vAlpha<0.0005)discard;
|
||||
float alpha=(1.0-smoothstep(0.4,1.0,r))*vAlpha;
|
||||
fragColor=vec4(vColor,alpha);
|
||||
gl_FragDepth=log2(vClipDepth)/log2(1.0+uFar);
|
||||
}
|
||||
28
src/Universe.App/Graphics/Shaders/rock-point.vert.glsl
Normal file
28
src/Universe.App/Graphics/Shaders/rock-point.vert.glsl
Normal file
@@ -0,0 +1,28 @@
|
||||
#version 330 core
|
||||
layout(location=2)in vec3 aOffset;
|
||||
layout(location=3)in float aRadius;
|
||||
layout(location=4)in vec3 aColor;
|
||||
layout(location=5)in float aSeed;
|
||||
uniform mat4 uViewProjection;
|
||||
uniform mat4 uBodyToWorld;
|
||||
uniform vec3 uAnchorWorld;
|
||||
uniform float uPixelScale;
|
||||
#include "ring-optics.glsl"
|
||||
#include "rock-common.glsl"
|
||||
out vec3 vColor;
|
||||
out float vAlpha;
|
||||
out float vClipDepth;
|
||||
void main() {
|
||||
vec3 position=uAnchorWorld+mat3(uBodyToWorld)*aOffset;
|
||||
float distanceMeters=length(position);
|
||||
float pixels=2.0*aRadius*uPixelScale/max(0.1,distanceMeters);
|
||||
float weight=1.0-rockMeshWeight(distanceMeters,pixels);
|
||||
weight*=1.0-smoothstep(uRockFade.x,uRockFade.y,distanceMeters);
|
||||
float size=clamp(pixels,1.0,24.0);
|
||||
vAlpha=weight*min(1.0,pixels*pixels/(size*size));
|
||||
vColor=rockIllumination(aColor,normalize(-position),position,aSeed);
|
||||
gl_Position=uViewProjection*vec4(position,1);
|
||||
vClipDepth=1.0+max(0.0,gl_Position.w);
|
||||
gl_Position.z=(2.0*log2(vClipDepth)/log2(1.0+uFar)-1.0)*gl_Position.w;
|
||||
gl_PointSize=size;
|
||||
}
|
||||
35
src/Universe.App/Graphics/Shaders/rock.frag.glsl
Normal file
35
src/Universe.App/Graphics/Shaders/rock.frag.glsl
Normal file
@@ -0,0 +1,35 @@
|
||||
#version 330 core
|
||||
#include "noise.glsl"
|
||||
#include "ring-optics.glsl"
|
||||
#include "rock-common.glsl"
|
||||
#include "asteroid-shape.glsl"
|
||||
in vec3 vPosition;
|
||||
in vec3 vNormal;
|
||||
in vec3 vRockLocal;
|
||||
in vec3 vColor;
|
||||
flat in float vSeed;
|
||||
in float vWeight;
|
||||
in float vClipDepth;
|
||||
flat in vec4 vRotation;
|
||||
out vec4 fragColor;
|
||||
uniform int uDebugNormals;
|
||||
void main() {
|
||||
float dither=fract(sin(dot(gl_FragCoord.xy,vec2(12.9898,78.233)))*43758.5453);
|
||||
vec3 d=normalize(vRockLocal);
|
||||
float footprint=max(length(dFdx(d)),length(dFdy(d)));
|
||||
vec3 axes=asteroidAxes(vSeed);
|
||||
vec3 p=asteroidPosition(d,vSeed)+d*axes*asteroidMicroHeight(d,vSeed,footprint);
|
||||
vec3 localNormal=normalize(cross(dFdx(p),dFdy(p)));
|
||||
if(dot(localNormal,d)<0.0)localNormal=-localNormal;
|
||||
localNormal=normalize(mix(normalize(d/axes),localNormal,uAsteroidNoise.z));
|
||||
vec3 rotated=localNormal+2.0*cross(vRotation.xyz,cross(vRotation.xyz,localNormal)+vRotation.w*localNormal);
|
||||
vec3 normal=normalize(transpose(mat3(uWorldToBody))*rotated);
|
||||
float geology=noise3(d*19.0+vSeed*119.0);
|
||||
float crater=asteroidCraters(d,uAsteroidCraters.x,vSeed)*uAsteroidCraters.x;
|
||||
float cavity=mix(.55,1.0,smoothstep(-.25,0.0,crater));
|
||||
if(dither>vWeight)discard;
|
||||
fragColor=vec4(rockIllumination(vColor*(.88+geology*.17)*cavity,normal,vPosition,vSeed),1.0);
|
||||
if(uDebugNormals==1)fragColor=vec4(normal*.5+.5,1);
|
||||
if(uDebugNormals==2)fragColor=vec4(1,1,1,1);
|
||||
gl_FragDepth=log2(vClipDepth)/log2(1.0+uFar);
|
||||
}
|
||||
40
src/Universe.App/Graphics/Shaders/rock.vert.glsl
Normal file
40
src/Universe.App/Graphics/Shaders/rock.vert.glsl
Normal file
@@ -0,0 +1,40 @@
|
||||
#version 330 core
|
||||
#include "noise.glsl"
|
||||
layout(location=0)in vec3 aPosition;
|
||||
layout(location=1)in vec3 aNormal;
|
||||
layout(location=2)in vec3 aOffset;
|
||||
layout(location=3)in float aRadius;
|
||||
layout(location=4)in vec3 aColor;
|
||||
layout(location=5)in float aSeed;
|
||||
layout(location=6)in vec4 aRotation;
|
||||
uniform mat4 uViewProjection;
|
||||
uniform mat4 uBodyToWorld;
|
||||
uniform vec3 uAnchorWorld;
|
||||
uniform float uPixelScale;
|
||||
#include "ring-optics.glsl"
|
||||
#include "rock-common.glsl"
|
||||
#include "asteroid-shape.glsl"
|
||||
out vec3 vPosition;
|
||||
out vec3 vNormal;
|
||||
out vec3 vRockLocal;
|
||||
out vec3 vColor;
|
||||
flat out float vSeed;
|
||||
out float vWeight;
|
||||
out float vClipDepth;
|
||||
flat out vec4 vRotation;
|
||||
vec3 rotate(vec3 v) { return v+2.0*cross(aRotation.xyz,cross(aRotation.xyz,v)+aRotation.w*v); }
|
||||
void main() {
|
||||
vec3 center=uAnchorWorld+mat3(uBodyToWorld)*aOffset;
|
||||
float distanceMeters=length(center);
|
||||
float pixels=2.0*aRadius*uPixelScale/max(0.1,distanceMeters);
|
||||
vWeight=rockMeshWeight(distanceMeters,pixels)*(1.0-smoothstep(uRockFade.x,uRockFade.y,distanceMeters));
|
||||
vec3 local=rotate(asteroidPosition(normalize(aPosition),aSeed))*aRadius;
|
||||
vPosition=center+mat3(uBodyToWorld)*local;
|
||||
vNormal=normalize(mat3(uBodyToWorld)*rotate(aNormal));
|
||||
vRockLocal=aPosition;
|
||||
vRotation=aRotation;
|
||||
vColor=aColor;vSeed=aSeed;
|
||||
gl_Position=uViewProjection*vec4(vPosition,1);
|
||||
vClipDepth=1.0+max(0.0,gl_Position.w);
|
||||
gl_Position.z=(2.0*log2(vClipDepth)/log2(1.0+uFar)-1.0)*gl_Position.w;
|
||||
}
|
||||
32
src/Universe.App/Graphics/Shaders/sky.frag.glsl
Normal file
32
src/Universe.App/Graphics/Shaders/sky.frag.glsl
Normal file
@@ -0,0 +1,32 @@
|
||||
#version 330 core
|
||||
#include "noise.glsl"
|
||||
in vec2 vUv;
|
||||
uniform vec3 uForward;
|
||||
uniform vec3 uRight;
|
||||
uniform vec3 uUp;
|
||||
uniform float uAspect;
|
||||
uniform float uTanHalfFov;
|
||||
uniform int uLightCount;
|
||||
uniform vec3 uStarDirections[2];
|
||||
uniform vec3 uStarColors[2];
|
||||
uniform float uStarAngles[2];
|
||||
uniform float uTime;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec2 xy=(vUv*2.0-1.0)*uTanHalfFov;
|
||||
vec3 rd=normalize(uForward+uRight*xy.x*uAspect+uUp*xy.y);
|
||||
float band=pow(max(0.0,1.0-abs(dot(rd,normalize(vec3(0.32,0.72,0.48))))),17.0);
|
||||
float dust=smoothstep(-0.26,0.36,fbm(rd*8.0+vec3(4.1,9.2,3.4)));
|
||||
vec3 color=vec3(0.00013,0.00020,0.00042)+vec3(0.0013,0.0016,0.0026)*band*dust;
|
||||
for(int i=0;i<2;i++) {
|
||||
if(i>=uLightCount) break;
|
||||
float angle=acos(clamp(dot(rd,uStarDirections[i]),-1.0,1.0));
|
||||
float r=angle/max(uStarAngles[i],0.00001);
|
||||
float corona=exp(-max(0.0,r-1.0)*6.0)*0.12;
|
||||
corona+=exp(-max(0.0,r-1.0)*1.5)*0.004;
|
||||
corona+=0.0002*exp(-angle*28.0);
|
||||
float strands=0.85+0.15*noise3(rd*18.0+uTime*0.015);
|
||||
color+=uStarColors[i]*corona*strands;
|
||||
}
|
||||
fragColor=vec4(color,1.0);
|
||||
}
|
||||
33
src/Universe.App/Graphics/Shaders/star.frag.glsl
Normal file
33
src/Universe.App/Graphics/Shaders/star.frag.glsl
Normal file
@@ -0,0 +1,33 @@
|
||||
#version 330 core
|
||||
#include "noise.glsl"
|
||||
in vec3 vDirection;
|
||||
in vec3 vWorldPosition;
|
||||
in float vFragDepth;
|
||||
uniform vec3 uColor;
|
||||
uniform vec3 uNoiseOffset;
|
||||
uniform float uTime;
|
||||
uniform float uFar;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec3 d=normalize(vDirection);
|
||||
vec3 p=d*2.4+uNoiseOffset+vec3(0.0,uTime*0.018,uTime*0.006);
|
||||
vec3 q=vec3(fbm(p),fbm(p+vec3(8.3,1.7,4.6)),fbm(p+vec3(1.2,9.4,13.7)));
|
||||
vec3 w=vec3(fbm(p*1.8+q*4.2+vec3(5.2,1.3,0.7)),fbm(p*1.8+q*4.2+vec3(9.1,6.7,3.4)),fbm(p*1.8+q*4.2+vec3(2.3,8.7,9.2)));
|
||||
float field=fbm(p*0.8+w*2.0+q*3.0);
|
||||
float activeRegions=fbm(d*3.0+uNoiseOffset*0.2);
|
||||
float filaments=pow(1.0-abs(noise3(p*4.0+w*6.0)),8.0);
|
||||
float cells=noise3(d*200.0+w*10.0)*0.5+0.5;
|
||||
float hot=smoothstep(-0.14,0.23,field)*(0.25+0.75*smoothstep(-0.30,0.32,activeRegions));
|
||||
float spots=1.0-smoothstep(0.37,0.57,fbm(d*7.0+uNoiseOffset*0.2))*0.82;
|
||||
vec3 cool=vec3(0.55,0.13,0.085);
|
||||
vec3 bright=vec3(2.5,1.50,0.45);
|
||||
vec3 plasma=mix(cool,bright,pow(hot,0.85));
|
||||
plasma+=vec3(0.8,0.48,0.10)*filaments*0.25;
|
||||
plasma*=mix(0.96,1.04,cells)*spots;
|
||||
float blueStar=smoothstep(0.88,1.16,uColor.b/max(uColor.r,0.01));
|
||||
plasma=mix(plasma,vec3(0.65,1.05,2.15)*(0.65+hot*2.4+filaments*0.5),blueStar);
|
||||
float mu=max(dot(d,normalize(-vWorldPosition)),0.0);
|
||||
float limb=0.09+0.91*pow(mu,0.80);
|
||||
fragColor=vec4(plasma*limb,1.0);
|
||||
gl_FragDepth=clamp(log2(max(0.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0);
|
||||
}
|
||||
17
src/Universe.App/Graphics/Shaders/star.vert.glsl
Normal file
17
src/Universe.App/Graphics/Shaders/star.vert.glsl
Normal file
@@ -0,0 +1,17 @@
|
||||
#version 330 core
|
||||
layout(location=0) in vec3 aPosition;
|
||||
uniform mat4 uViewProjection;
|
||||
uniform vec3 uCenter;
|
||||
uniform float uRadius;
|
||||
uniform float uFar;
|
||||
out vec3 vDirection;
|
||||
out vec3 vWorldPosition;
|
||||
out float vFragDepth;
|
||||
void main() {
|
||||
vDirection=aPosition;
|
||||
vWorldPosition=uCenter+aPosition*uRadius;
|
||||
vec4 clip=uViewProjection*vec4(vWorldPosition,1.0);
|
||||
vFragDepth=clip.w+1.0;
|
||||
gl_Position=clip;
|
||||
gl_Position.z=(2.0*log2(max(0.0001,vFragDepth))/log2(uFar+1.0)-1.0)*clip.w;
|
||||
}
|
||||
69
src/Universe.App/Graphics/Shaders/sun-glow.frag.glsl
Normal file
69
src/Universe.App/Graphics/Shaders/sun-glow.frag.glsl
Normal file
@@ -0,0 +1,69 @@
|
||||
#version 330 core
|
||||
in vec2 vUv;
|
||||
uniform sampler2D uScene;
|
||||
uniform sampler2D uDepth;
|
||||
uniform int uStarCount;
|
||||
uniform float uAspect;
|
||||
uniform vec3 uStarPosition[2]; // UV position, logarithmic front depth
|
||||
uniform vec3 uStarShape[2]; // apparent radius, intensity, minimum core radius
|
||||
uniform vec3 uStarHalo[2]; // halo radius, halo strength, ray strength
|
||||
uniform vec3 uStarRays[2]; // ray length, ray count, ray sharpness
|
||||
uniform vec3 uStarGhosts[2]; // strength, spacing, chromatic aberration
|
||||
uniform vec3 uStarColors[2];
|
||||
uniform float uStarRotation[2];
|
||||
out vec4 fragColor;
|
||||
float starVisibility(int i) {
|
||||
vec2 center=uStarPosition[i].xy;
|
||||
if(any(lessThan(center,vec2(0)))||any(greaterThan(center,vec2(1))))return 0.0;
|
||||
float visible=0.0,total=0.0;
|
||||
for(int y=-1;y<=1;y++)for(int x=-1;x<=1;x++) {
|
||||
vec2 uv=center+vec2(x/uAspect,y)*uStarShape[i].x*.55;
|
||||
if(any(lessThan(uv,vec2(0)))||any(greaterThan(uv,vec2(1))))continue;
|
||||
float weight=(x==0 && y==0)?4.0:1.0;
|
||||
visible+=step(uStarPosition[i].z-.000002,texture(uDepth,uv).r)*texture(uScene,uv).a*weight;total+=weight;
|
||||
}
|
||||
return visible/max(1.0,total);
|
||||
}
|
||||
float aperture(vec2 p,float radius) {
|
||||
float angle=atan(p.y,p.x);
|
||||
float hex=cos(floor(.5+angle/1.04719755)*1.04719755-angle)*length(p);
|
||||
return (1.0-smoothstep(radius*.86,radius,hex))*.12+exp(-pow((hex-radius*.88)/(radius*.09),2.0))*.24;
|
||||
}
|
||||
vec3 sunGlare(int i) {
|
||||
float intensity=uStarShape[i].y*starVisibility(i);
|
||||
if(intensity<.00001)return vec3(0.0);
|
||||
vec2 metric=vec2(uAspect,1.0),center=uStarPosition[i].xy;
|
||||
vec2 p=(vUv-center)*metric;
|
||||
float r=length(p),angle=atan(p.y,p.x)+uStarRotation[i];
|
||||
float core=max(uStarShape[i].z,uStarShape[i].x*.075);
|
||||
float halo=uStarHalo[i].x;
|
||||
float central=14.0*exp(-r*r/max(1e-12,core*core));
|
||||
float glow=exp(-r/max(.0001,halo))*uStarHalo[i].y/(1.0+r/max(core*16.0,.0001));
|
||||
float rays=pow(.5+.5*cos(angle*uStarRays[i].y),uStarRays[i].z);
|
||||
float smallRays=pow(.5+.5*cos(angle*(uStarRays[i].y+6.0)+1.1),uStarRays[i].z*1.5)*.25;
|
||||
float taper=exp(-r/max(.0001,uStarRays[i].x))/(1.0+r/max(core*5.0,.0001));
|
||||
vec3 color=uStarColors[i]*(central+glow+(rays+smallRays)*taper*uStarHalo[i].z);
|
||||
vec2 axis=vec2(.5)-center;
|
||||
float axisLength=length(axis*metric);
|
||||
float ghostWeight=smoothstep(.025,.15,axisLength)*uStarGhosts[i].x;
|
||||
for(int ghost=0;ghost<6;ghost++) {
|
||||
float g=float(ghost);
|
||||
vec2 ghostCenter=center+axis*(.7+g*uStarGhosts[i].y);
|
||||
float radius=.013+.006*g;
|
||||
vec2 q=(vUv-ghostCenter)*metric;
|
||||
float chroma=uStarGhosts[i].z;
|
||||
vec3 tint=.45+.45*cos(vec3(.1,2.2,4.3)+g*1.37);
|
||||
vec3 spectral=vec3(aperture(q,radius*(1.0-chroma*.15)),aperture(q,radius),aperture(q,radius*(1.0+chroma*.15)));
|
||||
color+=spectral*tint*ghostWeight/(1.0+g*.3);
|
||||
}
|
||||
vec2 ringCenter=vec2(.5)+axis*.25;
|
||||
float ringDistance=length((vUv-ringCenter)*metric);
|
||||
float rainbow=exp(-pow((ringDistance-.27)/.012,2.0));
|
||||
color+=rainbow*(.5+.5*cos(vec3(0,2,4)+ringDistance*160.0))*ghostWeight*.035;
|
||||
return color*intensity;
|
||||
}
|
||||
void main() {
|
||||
vec3 color=texture(uScene,vUv).rgb;
|
||||
for(int i=0;i<2;i++){if(i>=uStarCount)break;color+=sunGlare(i);}
|
||||
fragColor=vec4(color,1.0);
|
||||
}
|
||||
190
src/Universe.App/Graphics/Shaders/terrain.frag.glsl
Normal file
190
src/Universe.App/Graphics/Shaders/terrain.frag.glsl
Normal file
@@ -0,0 +1,190 @@
|
||||
#version 330 core
|
||||
#include "noise.glsl"
|
||||
#include "planet-height.glsl"
|
||||
#include "cloud-common.glsl"
|
||||
in vec3 vWorldPosition;
|
||||
in vec3 vPlanetLocal;
|
||||
in vec3 vNormal;
|
||||
in float vFragDepth;
|
||||
in vec3 vMaterialPosition;
|
||||
uniform int uMaterialsEnabled;
|
||||
uniform sampler2D uFlatColor;
|
||||
uniform sampler2D uFlatNormal;
|
||||
uniform sampler2D uSlopeColor;
|
||||
uniform sampler2D uSlopeNormal;
|
||||
uniform vec3 uMaterialSettings;
|
||||
uniform float uFar;
|
||||
uniform sampler2D uSurfaceTexture;
|
||||
uniform int uHasSurfaceTexture;
|
||||
uniform int uSurfaceType;
|
||||
uniform vec3 uCloudParameters;
|
||||
#include "ring-optics.glsl"
|
||||
uniform vec3 uSurfaceColor;
|
||||
uniform vec3 uAtmosphereColor;
|
||||
uniform int uPlanetIndex;
|
||||
uniform int uLightCount;
|
||||
uniform vec3 uLightPositions[2];
|
||||
uniform vec3 uLightColors[2];
|
||||
uniform float uLightLuminosities[2];
|
||||
uniform int uOccluderCount;
|
||||
uniform vec3 uOccluderCenters[16];
|
||||
uniform float uOccluderRadii[16];
|
||||
uniform int uTerrainShadows;
|
||||
uniform int uDebugSkipSurfaceNoise;
|
||||
uniform int uDebugDisableBodyShadows;
|
||||
out vec4 fragColor;
|
||||
|
||||
vec4 triColor(sampler2D image,vec3 p,vec3 weights) {
|
||||
return texture(image,p.yz)*weights.x+texture(image,p.xz)*weights.y+texture(image,p.xy)*weights.z;
|
||||
}
|
||||
vec3 triNormal(sampler2D image,vec3 p,vec3 n,vec3 weights) {
|
||||
vec3 x=texture(image,p.yz).xyz*2.0-1.0;
|
||||
vec3 y=texture(image,p.xz).xyz*2.0-1.0;
|
||||
vec3 z=texture(image,p.xy).xyz*2.0-1.0;
|
||||
return normalize(vec3(sign(n.x)*x.z,x.x,x.y)*weights.x+
|
||||
vec3(y.x,sign(n.y)*y.z,y.y)*weights.y+
|
||||
vec3(z.x,z.y,sign(n.z)*z.z)*weights.z);
|
||||
}
|
||||
|
||||
float mountainShadow(vec3 p, vec3 lightDir, float sunHeight) {
|
||||
if(uTerrainShadows==0 || sunHeight>0.8) return 1.0;
|
||||
float visibility=1.0;
|
||||
float t=0.000055;
|
||||
for(int i=0;i<6;i++) {
|
||||
vec3 q=p+lightDir*t;
|
||||
float ground=1.0+surfaceElevation(normalize(q));
|
||||
float clearance=length(q)-ground+0.000055;
|
||||
visibility=min(visibility,clamp(18.0*clearance/t,0.0,1.0));
|
||||
if(visibility<0.015) break;
|
||||
t*=2.75;
|
||||
}
|
||||
return visibility;
|
||||
}
|
||||
|
||||
vec3 cloudMaterial(vec3 d) {
|
||||
float latitude=asin(clamp(d.y,-1.0,1.0));
|
||||
float turbulence=fbm(d*6.0+uNoiseOffset+vec3(uCloudParameters.z*0.006,0,0));
|
||||
float wisps=fbm(d*27.0+uNoiseOffset+turbulence*3.0);
|
||||
if(uSurfaceType==2) return uSurfaceColor*(0.8+0.4*smoothstep(-0.5,0.5,turbulence+wisps*0.4));
|
||||
float bands=sin(latitude*uCloudParameters.x+turbulence*2.5+wisps*0.8)*0.5+0.5;
|
||||
float contrast=uSurfaceType==4 ? 0.20 : 0.72;
|
||||
vec3 cloud=mix(uSurfaceColor*(1.0-contrast*0.5),uSurfaceColor*(1.0+contrast*0.5),bands);
|
||||
vec3 pale=uSurfaceType==4 ? vec3(0.5,0.73,0.78) : vec3(0.79,0.69,0.50);
|
||||
cloud=mix(cloud,pale,smoothstep(0.66,0.98,bands)*0.45);
|
||||
float longitude=atan(d.z,d.x);
|
||||
float dx=atan(sin(longitude+0.35),cos(longitude+0.35));
|
||||
vec2 storm=vec2(dx*cos(latitude),latitude+0.36)/vec2(0.20,0.075);
|
||||
float spot=exp(-dot(storm,storm))*uCloudParameters.y;
|
||||
vec3 stormColor=uSurfaceType==4 ? uSurfaceColor*0.25 : vec3(0.57,0.20,0.10);
|
||||
return mix(cloud,stormColor,spot)*(0.95+wisps*0.12);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void main() {
|
||||
vec3 radial=normalize(vPlanetLocal);
|
||||
vec3 localDirection=bodyDirection(radial);
|
||||
vec3 textureColor=uHasSurfaceTexture!=0 ? texture(uSurfaceTexture,planetUv(localDirection)).rgb : vec3(0.0);
|
||||
float ocean=(uHasSurfaceTexture!=0 && uSurfaceType==1 && textureColor.b>textureColor.g*1.2 && textureColor.b>textureColor.r*1.4) ? 1.0:0.0;
|
||||
// Vertices already carry terrain normals from the collision height field. Reusing them
|
||||
// avoids three full procedural height evaluations for every shaded pixel.
|
||||
float elevation=length(vPlanetLocal)-1.0;
|
||||
float height=uAmplitude>0.0 ? elevation/uAmplitude : 0.0;
|
||||
vec3 normal=normalize(vNormal);
|
||||
// Small rock relief is filtered by the pixel footprint to avoid distant sparkling.
|
||||
vec3 rockPosition=radial*18000.0+uNoiseOffset;
|
||||
float grainFootprint=length(fwidth(rockPosition));
|
||||
float grain=0.0,erosion=0.0;
|
||||
if(uDebugSkipSurfaceNoise==0) {
|
||||
grain=noise3(rockPosition)*(1.0-smoothstep(0.55,1.0,grainFootprint))
|
||||
+0.25*noise3(rockPosition*3.1)*(1.0-smoothstep(0.55,1.0,grainFootprint*3.1));
|
||||
erosion=fbm(radial*2400.0+uNoiseOffset)*0.8;
|
||||
}
|
||||
float detail=grain*0.10+erosion;
|
||||
vec3 sigmaX=dFdx(vWorldPosition), sigmaY=dFdy(vWorldPosition);
|
||||
vec3 r1=cross(sigmaY,normal), r2=cross(normal,sigmaX);
|
||||
float det=dot(sigmaX,r1);
|
||||
float microWeight=1.0-smoothstep(0.4,3.0,grainFootprint);
|
||||
vec3 gradient=(dFdx(detail)*r1+dFdy(detail)*r2)/max(abs(det),0.0001)*sign(det);
|
||||
float microAmplitude=uHasSurfaceTexture!=0 ? min(uRadius*0.000006,uAmplitude*uRadius*0.04) : uRadius*0.000006;
|
||||
normal=normalize(normal-gradient*microAmplitude*microWeight*(1.0-ocean));
|
||||
if(uSurfaceType>=2 || ocean>0.5)normal=radial;
|
||||
float slope=clamp(1.0-dot(normal,radial),0.0,1.0);
|
||||
float geology=0.0,deposits=0.5,strata=0.5;
|
||||
if(uDebugSkipSurfaceNoise==0) {
|
||||
geology=fbm(radial*31.0+uNoiseOffset);
|
||||
deposits=noise3(radial*170.0+uNoiseOffset);
|
||||
strata=sin(height*150.0+noise3(radial*76.0)*2.2)*0.5+0.5;
|
||||
}
|
||||
vec3 mineral=mix(uSurfaceColor*0.55,uSurfaceColor*1.62,smoothstep(-0.45,0.5,geology));
|
||||
mineral=mix(mineral,vec3(0.28,0.23,0.20),smoothstep(0.08,0.48,slope)*0.67);
|
||||
mineral*=mix(0.91,1.07,strata*(0.35+slope*0.65));
|
||||
mineral*=0.96+0.08*deposits+grain*0.045;
|
||||
float snowLine=0.60-abs(radial.y)*0.10;
|
||||
float snow=uDebugSkipSurfaceNoise!=0 ? 0.0 : smoothstep(snowLine,snowLine+0.09,height+geology*0.11)*(1.0-smoothstep(0.16,0.48,slope));
|
||||
mineral=mix(mineral,vec3(0.75,0.81,0.84),snow*0.85);
|
||||
float ao=mix(0.58,1.0,smoothstep(-0.16,0.38,height));
|
||||
if(uHasSurfaceTexture!=0) { mineral=textureColor; ao=1.0; snow=0.0; }
|
||||
if(uSurfaceType>=2) { mineral=cloudMaterial(localDirection); ao=1.0; snow=0.0; }
|
||||
float materialRoughness=.8;
|
||||
if(uMaterialsEnabled!=0 && ocean<.5) {
|
||||
float strength=uMaterialSettings.z*(1.0-smoothstep(uMaterialSettings.y*.35,uMaterialSettings.y,length(vWorldPosition)));
|
||||
if(strength>.001) {
|
||||
vec3 bodyNormal=normalize(mat3(uWorldToBody)*normal);
|
||||
vec3 weights=pow(abs(bodyNormal),vec3(4.0)); weights/=max(.0001,weights.x+weights.y+weights.z);
|
||||
vec3 position=vMaterialPosition/uMaterialSettings.x;
|
||||
float rock=smoothstep(.04,.23,slope);
|
||||
vec4 material=mix(triColor(uFlatColor,position,weights),triColor(uSlopeColor,position,weights),rock);
|
||||
mineral=mix(mineral,material.rgb,strength);
|
||||
materialRoughness=mix(.8,material.a,strength);
|
||||
vec3 detail=mix(triNormal(uFlatNormal,position,bodyNormal,weights),triNormal(uSlopeNormal,position,bodyNormal,weights),rock);
|
||||
normal=normalize(mix(normal,transpose(mat3(uWorldToBody))*detail,strength*.4));
|
||||
}
|
||||
}
|
||||
vec3 light=mineral*(vec3(0.007)+uAtmosphereColor*0.018)*ao;
|
||||
vec3 viewDirection=normalize(-vWorldPosition);
|
||||
for(int l=0;l<2;l++) {
|
||||
if(l>=uLightCount) break;
|
||||
vec3 toLight=uLightPositions[l]-vWorldPosition;
|
||||
float distanceToLight=length(toLight);
|
||||
vec3 ld=toLight/max(distanceToLight,1.0);
|
||||
float sunHeight=dot(radial,ld);
|
||||
float visibility=smoothstep(-0.025,0.018,sunHeight);
|
||||
if(visibility>0.001) {
|
||||
visibility*=cloudShadow(vWorldPosition,ld);
|
||||
if(uDebugDisableBodyShadows==0) {
|
||||
visibility*=ringTransmission(mat3(uWorldToBody)*vPlanetLocal,mat3(uWorldToBody)*ld);
|
||||
for(int j=0;j<16;j++) {
|
||||
if(j>=uOccluderCount) break;
|
||||
if(j==uPlanetIndex) continue;
|
||||
vec3 o=uOccluderCenters[j]-vWorldPosition;
|
||||
float along=dot(o,ld);
|
||||
if(along>0.0 && along<distanceToLight) {
|
||||
float miss=length(o-ld*along);
|
||||
float soft=uOccluderRadii[j]*0.045;
|
||||
visibility*=smoothstep(uOccluderRadii[j]-soft,uOccluderRadii[j]+soft,miss);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(visibility>0.01 && uSurfaceType<2) visibility*=mountainShadow(radial*(1.0+elevation)+normal*0.000060,ld,sunHeight);
|
||||
float au=distanceToLight/149597870700.0;
|
||||
float flux=clamp(uLightLuminosities[l]/max(au*au,0.06),0.13,7.0);
|
||||
float diffuse=max(dot(normal,ld),0.0);
|
||||
vec3 halfDir=normalize(viewDirection+ld);
|
||||
float specular=pow(max(dot(normal,halfDir),0.0),mix(90.0,12.0,materialRoughness))*0.045;
|
||||
if(ocean>0.5) {
|
||||
float fresnel=0.02+0.98*pow(1.0-max(dot(normal,viewDirection),0.0),5.0);
|
||||
specular=pow(max(dot(normal,halfDir),0.0),180.0)*(0.15+fresnel)*3.0;
|
||||
}
|
||||
vec3 sunColor=uLightColors[l];
|
||||
// Low sunlight loses blue to the atmosphere before reaching the ground.
|
||||
vec3 extinction=exp(-vec3(0.035,0.07,0.16)/(max(sunHeight,0.04)+0.08));
|
||||
light+=(mineral*diffuse*(1.0-ocean*0.25)+specular)*sunColor*extinction*flux*1.65*visibility;
|
||||
float skyHemisphere=0.22+0.78*max(dot(normal,radial),0.0);
|
||||
vec3 skyIrradiance=mix(vec3(0.22,0.35,0.70),uAtmosphereColor,0.5);
|
||||
light+=mineral*skyIrradiance*0.40*sqrt(max(sunHeight,0.0))*sqrt(flux)*ao*skyHemisphere;
|
||||
}
|
||||
fragColor=vec4(max(light,vec3(0.0)),1.0);
|
||||
gl_FragDepth=clamp(log2(max(0.0001,vFragDepth))/log2(uFar+1.0),0.0,1.0);
|
||||
}
|
||||
26
src/Universe.App/Graphics/Shaders/terrain.vert.glsl
Normal file
26
src/Universe.App/Graphics/Shaders/terrain.vert.glsl
Normal file
@@ -0,0 +1,26 @@
|
||||
#version 330 core
|
||||
layout(location=0) in vec3 aPosition;
|
||||
layout(location=1) in vec3 aNormal;
|
||||
layout(location=2) in vec3 aDirection;
|
||||
uniform vec3 uChunkPosition;
|
||||
uniform vec3 uChunkOrigin;
|
||||
uniform float uRadius;
|
||||
uniform mat4 uViewProjection;
|
||||
uniform float uFar;
|
||||
uniform mat4 uWorldToBody;
|
||||
uniform vec3 uMaterialAnchor;
|
||||
out vec3 vWorldPosition;
|
||||
out vec3 vPlanetLocal;
|
||||
out vec3 vNormal;
|
||||
out float vFragDepth;
|
||||
out vec3 vMaterialPosition;
|
||||
void main() {
|
||||
vWorldPosition=uChunkPosition+aPosition;
|
||||
vPlanetLocal=uChunkOrigin+aPosition/uRadius;
|
||||
vNormal=aNormal;
|
||||
vMaterialPosition=mat3(uWorldToBody)*aPosition+uMaterialAnchor;
|
||||
vec4 clip=uViewProjection*vec4(vWorldPosition,1.0);
|
||||
vFragDepth=clip.w+1.0;
|
||||
gl_Position=clip;
|
||||
gl_Position.z=(2.0*log2(max(0.0001,vFragDepth))/log2(uFar+1.0)-1.0)*clip.w;
|
||||
}
|
||||
80
src/Universe.App/Graphics/SurfaceMaterials.cs
Normal file
80
src/Universe.App/Graphics/SurfaceMaterials.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.Text.Json;
|
||||
using Silk.NET.OpenGL;
|
||||
using Universe.Core.Generation;
|
||||
|
||||
namespace Universe.App.Graphics;
|
||||
|
||||
/// <summary>Content catalog and lazily uploaded reusable material textures.</summary>
|
||||
internal sealed unsafe class SurfaceMaterials : IDisposable
|
||||
{
|
||||
private sealed record Entry(string Id, string Color, string Normal, string Roughness);
|
||||
private sealed record Catalog(Entry[] Materials, string[] TerrestrialFlat, string[] RockyFlat, string[] Slopes);
|
||||
private readonly GL _gl;
|
||||
private readonly string _directory;
|
||||
private readonly Catalog _catalog;
|
||||
private readonly Dictionary<string, Entry> _entries;
|
||||
private readonly Dictionary<string, (uint Color, uint Normal)> _textures = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public SurfaceMaterials(GL gl, string directory)
|
||||
{
|
||||
_gl = gl; _directory = Path.GetFullPath(directory);
|
||||
var path = Path.Combine(_directory, "materials.json");
|
||||
_catalog = JsonSerializer.Deserialize<Catalog>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||
?? throw new InvalidDataException("Empty material catalog.");
|
||||
_entries = _catalog.Materials.ToDictionary(e => e.Id, StringComparer.OrdinalIgnoreCase);
|
||||
if (_catalog.TerrestrialFlat.Length == 0 || _catalog.RockyFlat.Length == 0 || _catalog.Slopes.Length == 0
|
||||
|| _catalog.TerrestrialFlat.Concat(_catalog.RockyFlat).Concat(_catalog.Slopes).Any(id => !_entries.ContainsKey(id)))
|
||||
throw new InvalidDataException("Material catalog contains empty pools or unknown material IDs.");
|
||||
}
|
||||
|
||||
public (uint FlatColor, uint FlatNormal, uint SlopeColor, uint SlopeNormal) ForPlanet(PlanetData planet)
|
||||
{
|
||||
var pool = planet.SurfaceType == PlanetSurfaceType.Terrestrial ? _catalog.TerrestrialFlat : _catalog.RockyFlat;
|
||||
var flat = planet.Materials.FlatMaterial ?? pool[(int)(DeterministicRandom.Mix(planet.Seed ^ 0xF1A7UL) % (ulong)pool.Length)];
|
||||
var slope = planet.Materials.SlopeMaterial ?? _catalog.Slopes[(int)(DeterministicRandom.Mix(planet.Seed ^ 0x5109EUL) % (ulong)_catalog.Slopes.Length)];
|
||||
var a = Get(flat); var b = Get(slope);
|
||||
return (a.Color, a.Normal, b.Color, b.Normal);
|
||||
}
|
||||
|
||||
private string Resolve(string relative)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(relative) || Path.IsPathRooted(relative)) throw new InvalidDataException("Material paths must be relative to Content.");
|
||||
var full = Path.GetFullPath(Path.Combine(_directory, relative));
|
||||
if (!full.StartsWith(_directory.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("Material paths must stay inside Content.");
|
||||
return full;
|
||||
}
|
||||
|
||||
private (uint Color, uint Normal) Get(string id)
|
||||
{
|
||||
if (_textures.TryGetValue(id, out var existing)) return existing;
|
||||
if (!_entries.TryGetValue(id, out var entry)) throw new InvalidDataException($"Unknown surface material '{id}'.");
|
||||
var color = RasterImage.Load(Resolve(entry.Color));
|
||||
var roughness = RasterImage.Load(Resolve(entry.Roughness));
|
||||
var normal = RasterImage.Load(Resolve(entry.Normal));
|
||||
if (color.Width != roughness.Width || color.Height != roughness.Height || color.Width != normal.Width || color.Height != normal.Height)
|
||||
throw new InvalidDataException($"Material '{id}' images must have equal dimensions.");
|
||||
for (var i = 0; i < color.Width * color.Height; i++) color.Rgba[i * 4 + 3] = roughness.Rgba[i * 4];
|
||||
var result = (Upload(color, true), Upload(normal, false));
|
||||
_textures.Add(id, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private uint Upload(RasterImage image, bool srgb)
|
||||
{
|
||||
var handle = _gl.GenTexture(); _gl.BindTexture(GLEnum.Texture2D, handle);
|
||||
fixed (byte* pixels = image.Rgba)
|
||||
_gl.TexImage2D(GLEnum.Texture2D, 0, srgb ? InternalFormat.Srgb8Alpha8 : InternalFormat.Rgba8,
|
||||
(uint)image.Width, (uint)image.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, pixels);
|
||||
_gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapS, (int)GLEnum.Repeat);
|
||||
_gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapT, (int)GLEnum.Repeat);
|
||||
_gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)GLEnum.LinearMipmapLinear);
|
||||
_gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMagFilter, (int)GLEnum.Linear);
|
||||
_gl.GenerateMipmap(GLEnum.Texture2D); return handle;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var value in _textures.Values) { _gl.DeleteTexture(value.Color); _gl.DeleteTexture(value.Normal); }
|
||||
}
|
||||
}
|
||||
1051
src/Universe.App/Graphics/UniverseRenderer.cs
Normal file
1051
src/Universe.App/Graphics/UniverseRenderer.cs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user