screen space reflections vatten och dämpad motion blur på skärnor

This commit is contained in:
2026-11-01 21:46:25 +01:00
parent a7207086b2
commit cb5070dc1c
19 changed files with 480 additions and 55 deletions

View File

@@ -85,7 +85,7 @@ internal static class GraphicsReport
camera.SetPlayerPose(center.Add(new(0,0,-12*scale),cell),camera.Forward,camera.Up);
var moving=Frame(stars);
var stopped=Frame(stars);
int Coverage(float[] pixels)=>Enumerable.Range(0,width*height).Count(i=>pixels[i*4]>.035);
int Coverage(float[] pixels)=>Enumerable.Range(0,width*height).Count(i=>pixels[i*4]>.001);
var movingCoverage=Coverage(moving);var stillCoverage=Coverage(stopped);
if(moving.Any(p=>!float.IsFinite(p))||movingCoverage<stillCoverage*1.5)
throw new InvalidOperationException($"Star trails did not stretch the moving stars: moving {movingCoverage}, stopped {stillCoverage} pixels.");
@@ -95,10 +95,25 @@ internal static class GraphicsReport
if(trails.History.Valid||cut.Zip(stopped,(a,b)=>System.Math.Abs(a-b)).Max()>.0001)
throw new InvalidOperationException("Star trails retained an afterimage after stopping or a camera cut.");
using var single=new GpuMesh(gl,7,[0,0,-10,1,1,1,2]);
double Energy(float[] pixels) => pixels.Where((_,i)=>i%4<3).Sum(v=>(double)v);
var energyRatios = new List<double>();
foreach (var travel in new[] { .05, 1d, 12d, 80d, 180d, 300d })
{
camera.SetPlayerPose(center,new(0,0,-1),new(0,1,0),resetMotionHistory:true);
var point = Frame(single);
var dx=travel*20*System.Math.Tan(camera.FieldOfViewRadians*.5)/width;
camera.SetPlayerPose(center.Add(new(dx*scale,0,0),cell),camera.Forward,camera.Up);
var streak = Frame(single);
var ratio=Energy(streak)/Energy(point); energyRatios.Add(ratio);
// movingBrightness is an optional artistic multiplier; the default 1 conserves HDR energy.
var expected=1+(settings.MovingBrightness-1)*(1-System.Math.Exp(-System.Math.Min(travel*settings.ShutterFraction,settings.MaximumTrailPixels)/14));
if(System.Math.Abs(ratio/expected-1)>.04)
throw new InvalidOperationException($"Star energy changed with motion: {travel} px, ratio {ratio:F4}, expected {expected:F4}.");
}
camera.SetPlayerPose(center,camera.Forward,camera.Up,resetMotionHistory:true);Frame(single);
camera.SetPlayerPose(center.Add(new(-6*scale,0,0),cell),camera.Forward,camera.Up);
var bounded=Frame(single);
var columns=Enumerable.Range(0,width).Where(x=>Enumerable.Range(0,height).Any(y=>bounded[(y*width+x)*4]>.035)).ToArray();
var columns=Enumerable.Range(0,width).Where(x=>Enumerable.Range(0,height).Any(y=>bounded[(y*width+x)*4]>.001)).ToArray();
var span=columns.Length==0?0:columns[^1]-columns[0]+1;
if(span<settings.MaximumTrailPixels*.5||span>settings.MaximumTrailPixels+14)
throw new InvalidOperationException($"Star trail pixel limit failed: span {span}, limit {settings.MaximumTrailPixels}.");
@@ -106,7 +121,7 @@ internal static class GraphicsReport
var directory=Path.Combine(AppContext.BaseDirectory,"graphics-report");Directory.CreateDirectory(directory);
WritePng(Path.Combine(directory,"star-motion-blur.png"),width,height,moving);
WritePng(Path.Combine(directory,"star-motion-stopped.png"),width,height,stopped);
Console.WriteLine($"Star trails passed: {movingCoverage} moving vs {stillCoverage} stationary pixels; capped trail {span} px; camera cut/stop clean; OpenGL {gl.GetStringS(GLEnum.Renderer)}.");
Console.WriteLine($"Star trails passed: {movingCoverage} moving vs {stillCoverage} stationary pixels; energy ratios {string.Join(", ",energyRatios.Select(r=>r.ToString("F3")))}; capped trail {span} px; camera cut/stop clean; OpenGL {gl.GetStringS(GLEnum.Renderer)}.");
gl.BindFramebuffer(GLEnum.Framebuffer,0);gl.Disable(GLEnum.Blend);gl.Enable(GLEnum.DepthTest);gl.DepthMask(true);
}
@@ -339,6 +354,7 @@ internal static class GraphicsReport
public static void RunOcean(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
{
ValidateOceanReflections(gl);
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);
@@ -362,12 +378,96 @@ internal static class GraphicsReport
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.");
renderer.SimulationTimeSeconds = 12;
UniverseNeighborhood WithoutSsr(UniverseNeighborhood world) => world with { ActiveSystem = world.ActiveSystem with
{ Planets = world.ActiveSystem.Planets.Select(p => p.Ocean is { } ocean ? p with
{ Ocean = ocean with { ScreenSpaceReflections = ocean.ScreenSpaceReflections with { Enabled = false } } } : p).ToArray() } };
float[] Snapshot(UniverseNeighborhood world, string file)
{
camera.SetPlayerPose(camera.Position,camera.Forward,camera.Up,resetMotionHistory:true);
renderer.Render(camera,world,0); gl.Finish();
if(gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException("Ocean SSR GL error.");
var pixels=Read(gl,width,height);WritePng(Path.Combine(directory,file),width,height,pixels,false);return pixels;
}
Snapshot(WithoutSsr(neighborhood),"earth-ocean-reflections-off.png");
var withReflections=Snapshot(neighborhood,"earth-ocean-reflections-on.png");
var withoutReflections=Snapshot(WithoutSsr(neighborhood),"earth-ocean-reflections-off.png");
var reflectionDifference=withReflections.Zip(withoutReflections,(a,b)=>System.Math.Abs(a-b)).Average();
if(reflectionDifference<.0001)throw new InvalidOperationException($"Water reflections did not change the production image: {reflectionDifference}.");
renderer.ProfileFrame=true;Snapshot(neighborhood,"earth-ocean-reflections-on.png");renderer.ProfileFrame=false;
var reflectionMs=renderer.FrameTimings.GetValueOrDefault("water reflections");
var earth=neighborhood.ActiveSystem.Planets.Single(p=>p.Id=="earth");
var up=(camera.Position.RelativeTo(origin,cellSize)-earth.PositionMeters).Normalized();
var night=neighborhood with { ActiveSystem=neighborhood.ActiveSystem with
{ Stars=neighborhood.ActiveSystem.Stars.Select(s=>s with {PositionMeters=earth.PositionMeters-up*UniverseScale.AstronomicalUnitMeters}).ToArray() } };
Snapshot(WithoutSsr(night),"earth-ocean-night-reflections-off.png");
var nightOn=Snapshot(night,"earth-ocean-night-reflections-on.png");
var nightOff=Snapshot(WithoutSsr(night),"earth-ocean-night-reflections-off.png");
var nightDifference=nightOn.Zip(nightOff,(a,b)=>System.Math.Abs(a-b)).Average();
if(nightDifference<.00001)throw new InvalidOperationException("The night sky did not reflect in the water.");
renderer.SimulationTimeSeconds = null;
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}");
Console.WriteLine($"Ocean scene passed: finite frames, wave animation delta {difference:G5}, reflection delta {reflectionDifference:G5} day / {nightDifference:G5} night; SSR {reflectionMs:F2} ms at {width}x{height}; underwater and orbit views. Full rendering {timer.Elapsed.TotalMilliseconds / 10:F1} ms/frame. Images: {directory}");
}
private static void ValidateOceanReflections(GL gl)
{
const int width=640,height=360; const float far=100000;
using var source=new RenderTarget(gl,width,height,true,auxiliaryColors:2);
using var reflection=new RenderTarget(gl,width/2,height/2);
using var composite=new RenderTarget(gl,width,height);
using var fullscreen=new GpuMesh(gl,3,[-1,-1,0,3,-1,0,-1,3,0]);
using var fixture=ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"),"""
#version 330 core
in vec2 vUv;
layout(location=0)out vec4 color;
layout(location=1)out vec4 normal;
layout(location=2)out vec4 fallback;
void main(){
vec2 p=(vUv*2.0-1.0)*.67;
vec3 ray=normalize(vec3(p.x*1.7777778,p.y,-1));
float wall=-12.0/ray.z,water=ray.y<0.0?-2.0/ray.y:1e8;
vec3 hit=ray*wall;
bool object=abs(hit.x)<3.0&&hit.y>-2.0&&hit.y<4.0;
float t=object?wall:1e8;
color=vec4(.08,.2,.4,1);normal=vec4(0);fallback=vec4(0);
if(object)color=vec4(2,.04,.02,1);
if(water<t){t=water;color=vec4(.03,.07,.12,1);normal=vec4(0,.7,0,1);fallback=vec4(.03,.07,.12,1);}
gl_FragDepth=t<1e7?log2(1.0-t*ray.z)/log2(100001.0):1.0;
}
""");
source.Bind(allColors:true);gl.Disable(GLEnum.Blend);gl.Enable(GLEnum.DepthTest);gl.DepthFunc(GLEnum.Always);gl.DepthMask(true);
fixture.Use();fullscreen.Draw();source.GenerateColorMipmaps();
source.Bind();gl.Finish();var original=Read(gl,width,height);
using var trace=ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"),ShaderSources.Load("ocean-reflection.frag"));
using var resolve=ShaderProgram.Create(gl,ShaderSources.Load("fullscreen.vert"),ShaderSources.Load("ocean-reflection-composite.frag"));
void Bind(int unit,uint texture){gl.ActiveTexture((GLEnum)((int)GLEnum.Texture0+unit));gl.BindTexture(GLEnum.Texture2D,texture);}
reflection.Bind();gl.Disable(GLEnum.DepthTest);gl.DepthMask(false);trace.Use();
Bind(0,source.Color);Bind(1,source.Depth);Bind(2,source.AuxiliaryColors[0]);
trace.Set("uScene",0);trace.Set("uDepth",1);trace.Set("uWaterNormals",2);trace.Set("uFar",far);
trace.Set("uForward",new Vector3(0,0,-1));trace.Set("uRight",Vector3.UnitX);trace.Set("uUp",Vector3.UnitY);
trace.Set("uAspect",(float)width/height);trace.Set("uTanHalfFov",.67f);
trace.Set("uMaxSteps",48);trace.Set("uTraceSettings",new Vector3(100,1.5f,.12f));
trace.Set("uReflectionSettings",new Vector3(.07f,2,.08f));trace.Set("uMaxSurfaceDistance",1000f);
trace.Set("uReflectionStrength",1f);trace.Set("uReflectSky",0);fullscreen.Draw();gl.Finish();
var raw=Read(gl,width/2,height/2);var offset=(height/2/5*(width/2)+width/4)*4;
if(raw[offset+3]<.4 || raw[offset]<raw[offset+1]*4)
throw new InvalidOperationException($"SSR did not reflect the visible red wall: {string.Join(",",raw.Skip(offset).Take(4))}.");
composite.Bind();resolve.Use();Bind(0,source.Color);Bind(1,source.Depth);Bind(2,source.AuxiliaryColors[0]);
Bind(3,source.AuxiliaryColors[1]);Bind(5,reflection.Color);
resolve.Set("uScene",0);resolve.Set("uDepth",1);resolve.Set("uWaterNormals",2);resolve.Set("uWaterFallback",3);
resolve.Set("uReflection",5);resolve.Set("uClouds",0);resolve.Set("uHasClouds",0);resolve.Set("uFar",far);fullscreen.Draw();gl.Finish();
var result=Read(gl,width,height);
var landError=Enumerable.Range(width*height/2,width*height/2).SelectMany(i=>Enumerable.Range(0,3).Select(c=>System.Math.Abs(original[i*4+c]-result[i*4+c]))).Max();
if(landError>.001 || gl.GetError()!=GLEnum.NoError)throw new InvalidOperationException($"SSR modified non-water geometry: {landError}.");
var dir=Path.Combine(AppContext.BaseDirectory,"graphics-report");Directory.CreateDirectory(dir);
WritePng(Path.Combine(dir,"ocean-ssr-wall-test.png"),width,height,result);
Console.WriteLine($"SSR GPU test passed: visible wall reflected, confidence {raw[offset+3]:F3}, non-water error {landError:G3}.");
gl.BindFramebuffer(GLEnum.Framebuffer,0);gl.Enable(GLEnum.DepthTest);gl.DepthFunc(GLEnum.Lequal);gl.DepthMask(true);
}
public static void RunScene(GL gl, UniverseRenderer renderer, UniverseNeighborhood neighborhood, double cellSize, int width, int height)
{

View File

@@ -35,6 +35,7 @@ internal sealed class PerformanceOverlay : IDisposable
AddMetric("GAS GIANTS", "GAS GIANTS");
AddMetric("RINGS / ROCKS", "RINGS / ROCKS");
AddMetric("OCEAN", "OCEAN");
AddMetric("WATER REFLECTIONS", "WATER REFLECTIONS");
AddMetric("ATMOSPHERE", "ATMOSPHERE");
AddMetric("CLOUDS / COMPOSITE", "CLOUDS / COMPOSITE");
AddMetric("SUN GLOW", "SUN GLOW");

View File

@@ -8,10 +8,11 @@ internal sealed unsafe class RenderTarget : IDisposable
public uint Framebuffer { get; }
public uint Color { get; }
public uint Depth { get; }
public uint[] AuxiliaryColors { get; }
public int Width { get; }
public int Height { get; }
public RenderTarget(GL gl, int width, int height, bool depth = false, bool fullPrecision = false)
public RenderTarget(GL gl, int width, int height, bool depth = false, bool fullPrecision = false, int auxiliaryColors = 0)
{
_gl = gl;
Width = width;
@@ -23,6 +24,15 @@ internal sealed unsafe class RenderTarget : IDisposable
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);
AuxiliaryColors = new uint[auxiliaryColors];
for (var i = 0; i < auxiliaryColors; i++)
{
var texture = gl.GenTexture(); AuxiliaryColors[i] = texture;
gl.BindTexture(GLEnum.Texture2D, texture);
gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.Rgba16f, (uint)width, (uint)height, 0, PixelFormat.Rgba, PixelType.Float, null);
SetParameters(gl, GLEnum.Nearest);
gl.FramebufferTexture2D(GLEnum.Framebuffer, (GLEnum)((uint)GLEnum.ColorAttachment1 + i), GLEnum.Texture2D, texture, 0);
}
if (depth)
{
Depth = gl.GenTexture();
@@ -36,10 +46,24 @@ internal sealed unsafe class RenderTarget : IDisposable
gl.BindFramebuffer(GLEnum.Framebuffer, 0);
}
public void Bind()
public void Bind(bool allColors = false)
{
_gl.BindFramebuffer(GLEnum.Framebuffer, Framebuffer);
_gl.Viewport(0, 0, (uint)Width, (uint)Height);
if (allColors && AuxiliaryColors.Length > 0)
{
var attachments = stackalloc GLEnum[AuxiliaryColors.Length + 1];
for (var i = 0; i <= AuxiliaryColors.Length; i++) attachments[i] = (GLEnum)((uint)GLEnum.ColorAttachment0 + i);
_gl.DrawBuffers((uint)(AuxiliaryColors.Length + 1), attachments);
}
else _gl.DrawBuffer(GLEnum.ColorAttachment0);
}
public void GenerateColorMipmaps()
{
_gl.BindTexture(GLEnum.Texture2D, Color);
_gl.GenerateMipmap(GLEnum.Texture2D);
_gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)GLEnum.LinearMipmapLinear);
}
private static void SetParameters(GL gl, GLEnum filter)
@@ -53,6 +77,7 @@ internal sealed unsafe class RenderTarget : IDisposable
public void Dispose()
{
_gl.DeleteTexture(Color);
foreach (var texture in AuxiliaryColors) _gl.DeleteTexture(texture);
if (Depth != 0) _gl.DeleteTexture(Depth);
_gl.DeleteFramebuffer(Framebuffer);
}

View File

@@ -17,7 +17,7 @@ internal static class ShaderSources
}
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" })
foreach (var include in new[] { "noise", "planet-height", "camera-ray", "ring-optics", "rock-common", "asteroid-shape", "cloud-common", "screen-space-reflection" })
{
var directive = $"#include \"{include}.glsl\"";
if (source.Contains(directive, StringComparison.Ordinal))

View File

@@ -0,0 +1,40 @@
#version 330 core
in vec2 vUv;
uniform sampler2D uScene;
uniform sampler2D uDepth;
uniform sampler2D uWaterNormals;
uniform sampler2D uWaterFallback;
uniform sampler2D uReflection;
uniform sampler2D uClouds;
uniform int uHasClouds;
uniform float uFar;
out vec4 fragColor;
float depth(vec2 uv) { return exp2(textureLod(uDepth,uv,0).r*log2(uFar+1.0))-1.0; }
void main() {
vec3 scene=textureLod(uScene,vUv,0).rgb;
vec4 surface=texture(uWaterNormals,vUv);
float reflectance=length(surface.rgb);
fragColor=vec4(scene,1);
if(surface.a<.5 || reflectance<.0001)return;
// Depth/normal-aware bilinear upsampling prevents a half-resolution shoreline from reflecting onto land.
ivec2 size=textureSize(uReflection,0);
vec2 p=vUv*vec2(size)-.5,base=floor(p),f=fract(p);
float z=depth(vUv),weight=0.0;
vec4 sum=vec4(0);
for(int y=0;y<2;y++)for(int x=0;x<2;x++) {
ivec2 coord=clamp(ivec2(base)+ivec2(x,y),ivec2(0),size-1);
vec2 uv=(vec2(coord)+.5)/vec2(size);
vec4 other=textureLod(uWaterNormals,uv,0);
if(other.a<.5 || length(other.rgb)<.0001)continue;
float w=(x==0?1.0-f.x:f.x)*(y==0?1.0-f.y:f.y);
w*=exp(-abs(depth(uv)-z)/max(.3,z*.025));
w*=smoothstep(.8,.99,dot(normalize(surface.rgb),normalize(other.rgb)));
sum+=texelFetch(uReflection,coord,0)*w;weight+=w;
}
if(weight<=.00001 || sum.a<=.00001)return;
float confidence=clamp(sum.a/weight,0.0,1.0);
if(uHasClouds!=0)confidence*=texture(uClouds,vUv).a;
vec3 reflected=sum.rgb/sum.a;
vec3 fallback=texture(uWaterFallback,vUv).rgb;
fragColor=vec4(max(vec3(0),scene+(reflected*reflectance-fallback)*confidence),1);
}

View File

@@ -0,0 +1,17 @@
#version 330 core
#include "camera-ray.glsl"
#include "screen-space-reflection.glsl"
in vec2 vUv;
out vec4 fragColor;
void main() {
fragColor=vec4(0);
vec4 surface=textureLod(uWaterNormals,vUv,0.0);
if(surface.a<.5 || length(surface.rgb)<.0001)return;
vec3 ray=cameraRay(vUv);
float distance=linearDepth(vUv)/max(.001,dot(ray,uForward));
if(distance<=.03 || distance>uMaxSurfaceDistance)return;
vec4 hit=traceReflection(ray*distance,normalize(surface.rgb));
float fade=1.0-smoothstep(uMaxSurfaceDistance*.6,uMaxSurfaceDistance,distance);
fade*=1.0-smoothstep(.6,1.0,uReflectionSettings.z);
fragColor=hit*(fade*uReflectionStrength);
}

View File

@@ -27,7 +27,9 @@ uniform vec3 uLightDirections[2];
uniform vec3 uLightColors[2];
uniform float uLightFlux[2];
uniform float uTime;
out vec4 fragColor;
layout(location=0) out vec4 fragColor;
layout(location=1) out vec4 reflectionNormal;
layout(location=2) out vec4 reflectionFallback;
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; }
@@ -98,25 +100,9 @@ vec3 skyReflection(vec3 direction,vec3 up) {
}
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() {
// Premultiplied-alpha MRT blending preserves earlier ocean planets outside this one's silhouette.
reflectionNormal=vec4(0); reflectionFallback=vec4(0);
vec3 scene=texture(uScene,vUv).rgb;
float originalDepth=texture(uDepth,vUv).r;
gl_FragDepth=originalDepth;
@@ -167,7 +153,6 @@ void main() {
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));
@@ -204,5 +189,8 @@ void main() {
color=color*through+uShallowColor*(1.0-through)*.5;
}
fragColor=vec4(max(color,vec3(0)),1);
float reflectance=underwater?0.0:fresnel*uWaterOptics.y*(1.0-foam);
reflectionNormal=vec4(toWorld(n)*reflectance,1);
reflectionFallback=vec4(reflected*reflectance,1);
gl_FragDepth=clamp(log2(1.0+t*max(.001,dot(worldRay,uForward)))/log2(uFar+1.0),0.0,1.0);
}

View File

@@ -0,0 +1,84 @@
// Perspective-correct screen-space segment traversal, following the DDA principle in
// McGuire & Mara, JCGT 3(4), 2014. Camera-space Z is positive and depth is logarithmic.
uniform sampler2D uScene;
uniform sampler2D uDepth;
uniform sampler2D uWaterNormals;
uniform float uFar;
uniform int uMaxSteps;
uniform vec3 uTraceSettings; // maximum ray distance, thickness, normal bias (metres)
uniform vec3 uReflectionSettings; // edge fade, pixel stride, roughness
uniform float uMaxSurfaceDistance;
uniform float uReflectionStrength;
uniform int uReflectSky;
float linearDepth(vec2 uv) { return exp2(textureLod(uDepth,uv,0.0).r*log2(uFar+1.0))-1.0; }
vec3 viewPoint(vec3 world) { return vec3(dot(world,uRight),dot(world,uUp),dot(world,uForward)); }
vec2 projectPoint(vec3 p) { return .5+p.xy/(2.0*p.z*uTanHalfFov*vec2(uAspect,1)); }
float edgeConfidence(vec2 uv) {
float edge=min(min(uv.x,uv.y),min(1.0-uv.x,1.0-uv.y));
return smoothstep(0.0,uReflectionSettings.x,edge);
}
vec4 traceReflection(vec3 world,vec3 normal) {
vec3 reflection=reflect(normalize(world),normal);
vec3 origin=viewPoint(world+normal*uTraceSettings.z),direction=viewPoint(reflection);
if(origin.z<=.02)return vec4(0);
float distanceLimit=uTraceSettings.x;
if(direction.z<0.0)distanceLimit=min(distanceLimit,(.05-origin.z)/direction.z);
vec4 result=vec4(0);
float roughness=uReflectionSettings.z;
float mip=roughness*5.0;
// Infinite/distant sky lookup includes the actual stars, atmospheric colour and cloud texture.
// Opaque foreground pixels are never treated as sky or used to fill missing geometry.
if(uReflectSky!=0 && direction.z>.001) {
vec2 uv=projectPoint(direction);
if(all(greaterThan(uv,vec2(0)))&&all(lessThan(uv,vec2(1)))) {
float z=linearDepth(uv);
if(z>uTraceSettings.x*4.0 && textureLod(uWaterNormals,uv,0.0).a<.5) {
float confidence=edgeConfidence(uv);
result=vec4(textureLod(uScene,uv,mip).rgb*confidence,confidence);
}
}
}
if(distanceLimit<=uTraceSettings.z)return result;
vec3 end=origin+direction*distanceLimit;
vec2 startUv=projectPoint(origin),endUv=projectPoint(end);
vec2 size=vec2(textureSize(uDepth,0));
float pixelLength=length((endUv-startUv)*size);
float stepFraction=max(uReflectionSettings.y/max(pixelLength,1.0),1.0/float(uMaxSteps));
vec3 q0=origin/origin.z,q1=end/end.z;
float k0=1.0/origin.z,k1=1.0/end.z;
float previous=0.0,previousZ=origin.z;
for(int i=0;i<128;i++) {
if(i>=uMaxSteps)break;
float fraction=min(1.0,float(i+1)*stepFraction);
vec2 uv=mix(startUv,endUv,fraction);
if(any(lessThanEqual(uv,vec2(0)))||any(greaterThanEqual(uv,vec2(1))))break;
float rayZ=1.0/mix(k0,k1,fraction);
float sceneZ=linearDepth(uv);
float thickness=uTraceSettings.y+sceneZ*.00005;
bool water=textureLod(uWaterNormals,uv,0.0).a>.5;
if(!water && sceneZ<uFar*.99 && max(previousZ,rayZ)>=sceneZ && min(previousZ,rayZ)<=sceneZ+thickness) {
float lo=previous,hi=fraction;
// Refine the front crossing instead of accepting an entire coarse depth interval.
for(int j=0;j<7;j++) {
float mid=(lo+hi)*.5;
vec2 midUv=mix(startUv,endUv,mid);
float delta=1.0/mix(k0,k1,mid)-linearDepth(midUv);
if(delta>0.0)hi=mid;else lo=mid;
}
float hit=(lo+hi)*.5;
vec2 hitUv=mix(startUv,endUv,hit);
vec3 point=mix(q0,q1,hit)/mix(k0,k1,hit);
float error=point.z-linearDepth(hitUv);
float travel=length(point-origin);
if(abs(error)<thickness && travel>uTraceSettings.z*2.0 && textureLod(uWaterNormals,hitUv,0.0).a<.5) {
float confidence=edgeConfidence(hitUv)*(1.0-smoothstep(distanceLimit*.7,distanceLimit,travel));
confidence*=1.0-smoothstep(thickness*.3,thickness,abs(error));
return vec4(textureLod(uScene,hitUv,mip).rgb*confidence,confidence);
}
}
previous=fraction; previousZ=rayZ;
if(fraction>=1.0)break;
}
return result;
}

View File

@@ -4,14 +4,21 @@ flat in vec3 gColor;
flat in float gLength;
flat in float gSigma;
out vec4 fragColor;
// Gaussian convolved with a normalized shutter interval. Its integral is the
// same for a stationary point and a long streak (no extra light from area).
float erfApprox(float x) {
float t=1.0/(1.0+.3275911*abs(x));
float p=(((((1.061405429*t-1.453152027)*t)+1.421413741)*t-.284496736)*t+.254829592)*t;
return sign(x)*(1.0-p*exp(-x*x));
}
void main() {
float closest=clamp(gPixel.x,0.0,gLength);
vec2 delta=vec2(gPixel.x-closest,gPixel.y);
float sigma2=gSigma*gSigma+1.0/12.0;
float profile=exp(-dot(delta,delta)/(2.0*sigma2));
float along=gLength>.5?mix(.32,1.0,smoothstep(0.0,1.0,closest/gLength)):1.0;
float head=exp(-((gPixel.x-gLength)*(gPixel.x-gLength)+gPixel.y*gPixel.y)/(2.0*sigma2));
float brightness=profile*along+head*.15;
if(brightness<.003)discard;
float sigma=sqrt(sigma2);
float along;
if(gLength<.2)along=exp(-pow(gPixel.x-gLength*.5,2.0)/(2.0*sigma2));
else along=1.253314137*sigma/gLength*(erfApprox(gPixel.x/(1.414213562*sigma))
-erfApprox((gPixel.x-gLength)/(1.414213562*sigma)));
float brightness=exp(-gPixel.y*gPixel.y/(2.0*sigma2))*max(0.0,along);
if(brightness<.00001)discard;
fragColor=vec4(gColor*brightness,0);
}

View File

@@ -33,26 +33,28 @@ void main() {
if(tail.w<nearPlane)tail=mix(head,tail,(head.w-nearPlane)/(head.w-tail.w));
vec2 h=head.xy/head.w*uViewport.xy*.5;
vec2 t=mix(h,tail.xy/tail.w*uViewport.xy*.5,uShutter);
float fullLength=length(h-t);
if(fullLength>uMaximumTrailPixels)t=h-(h-t)*(uMaximumTrailPixels/fullLength);
vec2 originalTail=t;
gLength=length(h-t);
vec2 delta=h-t;
float lo=0.0,hi=1.0;
vec2 bounds=uViewport.xy*.5+vec2(8.0);
if(!clipAxis(t.x,delta.x,bounds.x,lo,hi)||!clipAxis(t.y,delta.y,bounds.y,lo,hi))return;
h=t+delta*hi;t+=delta*lo;
float distancePixels=length(h-t);
if(distancePixels>uMaximumTrailPixels)t=h-(h-t)*(uMaximumTrailPixels/distancePixels);
gLength=length(h-t);
vec2 direction=gLength>.001?(h-t)/gLength:vec2(1,0);
vec2 direction=length(h-t)>.001?normalize(h-t):vec2(1,0);
vec2 perpendicular=vec2(-direction.y,direction.x);
// A minimum Gaussian footprint averages subpixel motion instead of toggling
// a one-pixel point on and off as the camera passes it.
gSigma=max(uMinimumWidthPixels/2.0,star[0].size*.3);
float radius=gSigma*3.0;
float radius=sqrt(gSigma*gSigma+1.0/12.0)*3.5;
float moving=1.0-exp(-gLength/14.0);
float boost=mix(1.0,uMovingBrightness,moving);
gColor=star[0].color*.7*boost;
vertex(t-direction*radius-perpendicular*radius,vec2(-radius,-radius));
vertex(t-direction*radius+perpendicular*radius,vec2(-radius,radius));
vertex(h+direction*radius-perpendicular*radius,vec2(gLength+radius,-radius));
vertex(h+direction*radius+perpendicular*radius,vec2(gLength+radius,radius));
gColor=star[0].color*.805*boost;
float start=length(t-originalTail),end=length(h-originalTail);
vertex(t-direction*radius-perpendicular*radius,vec2(start-radius,-radius));
vertex(t-direction*radius+perpendicular*radius,vec2(start-radius,radius));
vertex(h+direction*radius-perpendicular*radius,vec2(end+radius,-radius));
vertex(h+direction*radius+perpendicular*radius,vec2(end+radius,radius));
EndPrimitive();
}

View File

@@ -45,6 +45,8 @@ internal sealed class UniverseRenderer : IDisposable
private ShaderProgram _sunGlow;
private ShaderProgram _blackHole;
private ShaderProgram _ocean;
private ShaderProgram _oceanReflection;
private ShaderProgram _oceanReflectionComposite;
private ShaderProgram _clouds;
private ShaderProgram _cloudComposite;
private readonly ShaderProgram _overlay;
@@ -52,6 +54,10 @@ internal sealed class UniverseRenderer : IDisposable
private readonly CloudNoiseTexture _cloudNoise;
private readonly SurfaceMaterials _materials;
private RenderTarget? _cloudRaw;
private RenderTarget? _waterReflectionRaw;
private RenderTarget _waterReflectionTarget = null!;
private bool _oceanRendered;
private string? _cloudLayerPlanetId;
internal RenderTarget? CloudLayer => _cloudRaw;
private readonly GpuMesh _navigationStars;
private RenderTarget _scene = null!;
@@ -118,6 +124,8 @@ internal sealed class UniverseRenderer : IDisposable
_sunGlow = Program("fullscreen.vert", "sun-glow.frag");
_blackHole = Program("fullscreen.vert", "black-hole.frag");
_ocean = Program("fullscreen.vert", "ocean.frag");
_oceanReflection = Program("fullscreen.vert", "ocean-reflection.frag");
_oceanReflectionComposite = Program("fullscreen.vert", "ocean-reflection-composite.frag");
_clouds = Program("fullscreen.vert", "cloud.frag");
_cloudComposite = Program("fullscreen.vert", "cloud-composite.frag");
_overlay = ShaderProgram.Create(gl, """
@@ -178,7 +186,8 @@ internal sealed class UniverseRenderer : IDisposable
foreach (var pair in new[] { ("terrain.vert","terrain.frag"),("star.vert","star.frag"),("fullscreen.vert","sky.frag"),("point.vert","point.frag"),
("fullscreen.vert","atmosphere.frag"),("fullscreen.vert","bright.frag"),("fullscreen.vert","blur.frag"),("fullscreen.vert","final.frag"),("fullscreen.vert","ring.frag"),
("fullscreen.vert","gas.frag"),("rock.vert","rock.frag"),("rock-point.vert","rock-point.frag"),("fullscreen.vert","sun-glow.frag"),("fullscreen.vert","ocean.frag"),
("fullscreen.vert","cloud.frag"),("fullscreen.vert","cloud-composite.frag"),("fullscreen.vert","black-hole.frag") })
("fullscreen.vert","cloud.frag"),("fullscreen.vert","cloud-composite.frag"),("fullscreen.vert","black-hole.frag"),
("fullscreen.vert","ocean-reflection.frag"),("fullscreen.vert","ocean-reflection-composite.frag") })
replacements.Add(Program(pair.Item1, pair.Item2));
_starTrails.ReloadShaders();
}
@@ -198,6 +207,8 @@ internal sealed class UniverseRenderer : IDisposable
_ocean.Dispose(); _ocean = replacements[13];
_clouds.Dispose(); _clouds = replacements[14]; _cloudComposite.Dispose(); _cloudComposite = replacements[15];
_blackHole.Dispose(); _blackHole = replacements[16];
_oceanReflection.Dispose(); _oceanReflection = replacements[17];
_oceanReflectionComposite.Dispose(); _oceanReflectionComposite = replacements[18];
}
public void SetActiveSystem(SectorCoordinate coordinate)
@@ -236,9 +247,11 @@ internal sealed class UniverseRenderer : IDisposable
_width = System.Math.Max(width, 1);
_height = System.Math.Max(height, 1);
_cloudRaw?.Dispose(); _cloudRaw = null;
_waterReflectionRaw?.Dispose(); _waterReflectionRaw = null; _waterReflectionTarget?.Dispose();
_scene?.Dispose(); _gasInput?.Dispose(); _glareTarget?.Dispose(); _scatterA?.Dispose(); _scatterB?.Dispose(); _bloomA?.Dispose(); _bloomB?.Dispose();
_blackHoleTarget?.Dispose();
_scene = new(_gl, _width, _height, true);
_scene = new(_gl, _width, _height, true, auxiliaryColors: 2);
_waterReflectionTarget = new(_gl, _width, _height);
_gasInput = new(_gl, _width, _height, true);
_glareTarget = new(_gl, _width, _height);
_blackHoleTarget = new(_gl, _width, _height);
@@ -276,10 +289,12 @@ internal sealed class UniverseRenderer : IDisposable
var starPositions = system.Stars.Select(s => center.Add(s.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters)).ToArray();
BindNoise();
_gpuProfiler.BeginFrame(DebugMode);
_scene.Bind();
_oceanRendered = false; _cloudLayerPlanetId = null;
_scene.Bind(allColors: true);
_gl.DepthMask(true);
_gl.ClearColor(0, 0, 0, 1);
_gl.ClearColor(0, 0, 0, 0);
_gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
_scene.Bind();
_gl.Disable(GLEnum.Blend);
_gl.Disable(GLEnum.DepthTest);
_gl.DepthMask(false);
@@ -330,6 +345,10 @@ internal sealed class UniverseRenderer : IDisposable
composed = DrawClouds(camera, system, far, composed);
_gpuProfiler.EndPass();
Mark("clouds");
_gpuProfiler.BeginPass("WATER REFLECTIONS");
composed = DrawOceanReflections(camera, system, far, composed);
_gpuProfiler.EndPass();
Mark("water reflections");
_gpuProfiler.BeginPass("BLACK HOLE / LENSING");
composed = DrawBlackHoles(camera, system, far, composed);
_gpuProfiler.EndPass();
@@ -674,7 +693,9 @@ internal sealed class UniverseRenderer : IDisposable
_gl.BindFramebuffer(GLEnum.DrawFramebuffer, _gasInput.Framebuffer);
_gl.BlitFramebuffer(0, 0, _width, _height, 0, 0, _width, _height,
ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit, BlitFramebufferFilter.Nearest);
_scene.Bind(); _gl.Enable(GLEnum.DepthTest); _gl.DepthFunc(GLEnum.Always); _gl.DepthMask(true);
_scene.Bind(allColors: true); _gl.Enable(GLEnum.DepthTest); _gl.DepthFunc(GLEnum.Always); _gl.DepthMask(true);
_gl.Enable(GLEnum.Blend); _gl.BlendFunc(GLEnum.One, GLEnum.OneMinusSrcAlpha);
_oceanRendered = true;
_ocean.Use(); SetCameraRay(_ocean, camera); BindPlanetTerrain(_ocean, planet, textures);
BindCloudSettings(_ocean, camera, system, planet, relative);
BindTexture(0, _gasInput.Color); BindTexture(1, _gasInput.Depth);
@@ -717,9 +738,49 @@ internal sealed class UniverseRenderer : IDisposable
System.Math.Pow((s.PositionMeters - planet.PositionMeters).Length / UniverseScale.AstronomicalUnitMeters, 2), .02, 7)).ToArray());
_fullscreen.Draw();
}
_gl.Disable(GLEnum.Blend); _scene.Bind();
_gl.DepthFunc(GLEnum.Lequal);
}
private RenderTarget DrawOceanReflections(FlyCamera camera, StarSystemData system, float far, RenderTarget source)
{
if (!_oceanRendered) return source;
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
// Only a nearby ocean needs detailed SSR; orbital views retain the cheaper analytic reflection.
var planet = system.Planets.Where(p => p.Ocean is { Enabled: true, ScreenSpaceReflections: { Enabled: true, Strength: > 0 } })
.Where(p => System.Math.Abs(center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters).Length
- p.RadiusMeters - p.Ocean!.SeaLevelMeters) < p.Ocean.ScreenSpaceReflections.MaxSurfaceDistanceMeters)
.MinBy(p => center.Add(p.PositionMeters, camera.CellSizeMeters).RelativeTo(camera.Position, camera.CellSizeMeters).Length - p.RadiusMeters);
if (planet is null) return source;
var r = planet.Ocean!.ScreenSpaceReflections;
var width = System.Math.Max(1, (int)System.Math.Round(_width * r.ResolutionScale));
var height = System.Math.Max(1, (int)System.Math.Round(_height * r.ResolutionScale));
if (_waterReflectionRaw is null || _waterReflectionRaw.Width != width || _waterReflectionRaw.Height != height)
{ _waterReflectionRaw?.Dispose(); _waterReflectionRaw = new(_gl, width, height); }
source.GenerateColorMipmaps();
_gl.Disable(GLEnum.Blend); _gl.Disable(GLEnum.DepthTest); _gl.DepthMask(false);
_waterReflectionRaw.Bind(); _oceanReflection.Use(); SetCameraRay(_oceanReflection, camera);
BindTexture(0, source.Color); BindTexture(1, _scene.Depth); BindTexture(2, _scene.AuxiliaryColors[0]);
_oceanReflection.Set("uScene", 0); _oceanReflection.Set("uDepth", 1); _oceanReflection.Set("uWaterNormals", 2);
_oceanReflection.Set("uFar", far); _oceanReflection.Set("uMaxSteps", r.MaxSteps);
_oceanReflection.Set("uTraceSettings", new Vector3((float)r.MaxDistanceMeters, (float)r.ThicknessMeters, (float)r.NormalBiasMeters));
_oceanReflection.Set("uReflectionSettings", new Vector3((float)r.EdgeFade, (float)r.StridePixels, (float)planet.Ocean.Roughness));
_oceanReflection.Set("uMaxSurfaceDistance", (float)r.MaxSurfaceDistanceMeters);
_oceanReflection.Set("uReflectionStrength", (float)r.Strength); _oceanReflection.Set("uReflectSky", r.ReflectSky ? 1 : 0);
_fullscreen.Draw();
_waterReflectionTarget.Bind(); _oceanReflectionComposite.Use();
BindTexture(0, source.Color); BindTexture(1, _scene.Depth); BindTexture(2, _scene.AuxiliaryColors[0]);
BindTexture(3, _scene.AuxiliaryColors[1]); BindTexture(5, _waterReflectionRaw.Color);
if (_cloudRaw is not null) BindTexture(6, _cloudRaw.Color);
_oceanReflectionComposite.Set("uScene", 0); _oceanReflectionComposite.Set("uDepth", 1);
_oceanReflectionComposite.Set("uWaterNormals", 2); _oceanReflectionComposite.Set("uWaterFallback", 3);
_oceanReflectionComposite.Set("uReflection", 5); _oceanReflectionComposite.Set("uClouds", 6);
_oceanReflectionComposite.Set("uHasClouds", _cloudLayerPlanetId == planet.Id ? 1 : 0);
_oceanReflectionComposite.Set("uFar", far); _fullscreen.Draw();
_gl.Enable(GLEnum.DepthTest); _gl.DepthMask(true);
return _waterReflectionTarget;
}
private void DrawRings(FlyCamera camera, StarSystemData system, float far)
{
var center = UniversePosition.AtCellCenter(system.Coordinate, camera.CellSizeMeters);
@@ -968,6 +1029,7 @@ internal sealed class UniverseRenderer : IDisposable
_cloudComposite.Set("uScene", 0); _cloudComposite.Set("uDepth", 1); _cloudComposite.Set("uClouds", 2);
_cloudComposite.Set("uCloudTexel", new Vector3(1f / width, 1f / height, 0));
_fullscreen.Draw(); source = target;
_cloudLayerPlanetId = planet.Id;
}
return source;
}
@@ -1119,6 +1181,7 @@ internal sealed class UniverseRenderer : IDisposable
_sunGlow.Dispose(); _glareTarget.Dispose();
_blackHole.Dispose(); _blackHoleTarget.Dispose();
_ocean.Dispose();
_oceanReflection.Dispose(); _oceanReflectionComposite.Dispose(); _waterReflectionRaw?.Dispose(); _waterReflectionTarget.Dispose();
_clouds.Dispose(); _cloudComposite.Dispose(); _cloudNoise.Dispose(); _emptyCloudDensity.Dispose(); _materials.Dispose(); _cloudRaw?.Dispose();
_scene.Dispose(); _scatterA.Dispose(); _scatterB.Dispose(); _bloomA.Dispose(); _bloomB.Dispose(); _noise.Dispose();
}