aboutsummaryrefslogtreecommitdiff
path: root/assets/shaders/geodesic.slang
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-24 17:07:54 +0200
committerhachem <im@hachem.wtf>2026-08-24 17:07:54 +0200
commit06398a7a176e123506de6e8851866a8bec0b3427 (patch)
tree7167ef4818f2c224840122961882efb5ec26c80b /assets/shaders/geodesic.slang
parent1c85c2ecaa826afd8bf80f93741cf1b2deba5798 (diff)
[chore]: cleanup shaders
Diffstat (limited to 'assets/shaders/geodesic.slang')
-rw-r--r--assets/shaders/geodesic.slang148
1 files changed, 77 insertions, 71 deletions
diff --git a/assets/shaders/geodesic.slang b/assets/shaders/geodesic.slang
index eee62da..eb536c9 100644
--- a/assets/shaders/geodesic.slang
+++ b/assets/shaders/geodesic.slang
@@ -1,3 +1,9 @@
+// Ray-traced Schwarzschild black hole: back-traces one null geodesic per pixel
+// through curved spacetime, shading the accretion disk, the lensed HDRI
+// background, and placed objects. Runs as a full-screen fragment pass. Uniforms
+// live in ConstantBuffers (UBOs) driven through the RHI; the HDRI is a cubemap
+// sampler. See docs/physics.md for the maths.
+
struct VSInput { float2 position : POSITION; float2 texCoord : TEXCOORD0; };
struct VSOutput { float4 position : SV_Position; float2 texCoord : TEXCOORD0; };
@@ -12,54 +18,44 @@ VSOutput vertexMain(VSInput input)
struct Camera
{
- float3 camPos; float _pad0;
- float3 camRight; float _pad1;
- float3 camUp; float _pad2;
- float3 camForward; float _pad3;
- float tanHalfFov;
- float aspect;
- bool moving;
- int outputChannel; // 0 = colour; 1 = redshift g; 2 = emission T; 3 = impact parameter
- int rawOutput; // 0 = display (false-colour / tone-mapped); 1 = raw float value
+ float3 u_cam_pos; float _pad0;
+ float3 u_cam_right; float _pad1;
+ float3 u_cam_up; float _pad2;
+ float3 u_cam_forward; float _pad3;
+ float u_tan_half_fov;
+ float u_aspect;
+ bool u_moving;
+ int u_output_channel; // 0 = colour; 1 = redshift g; 2 = emission T; 3 = impact parameter
+ int u_raw_output; // 0 = display (false-colour / tone-mapped); 1 = raw float value
};
-
-// A jet-ish false-colour ramp (blue -> cyan -> green -> yellow -> red) for the
-// observable export channels. t is expected in [0, 1].
-float3 Falsecolor(float t)
-{
- t = clamp(t, 0.0, 1.0);
- return clamp(float3(1.5 - abs(4.0 * t - 3.0),
- 1.5 - abs(4.0 * t - 2.0),
- 1.5 - abs(4.0 * t - 1.0)), 0.0, 1.0);
-}
ConstantBuffer<Camera> cam;
struct Disk
{
- float disk_r1; // inner edge (clamped to the ISCO, 3 r_s, below)
- float disk_r2; // outer edge
- float disk_num; // turbulence strength (0 = smooth physical disk)
- float thickness; // slab half-height (anti-aliases the edge-on disk)
- float disk_density; // disk brightness / exposure
- float temperature; // Kelvin at the flux peak (disk colour)
+ float u_inner_radius; // inner edge (clamped to the ISCO, 3 r_s, below)
+ float u_outer_radius; // outer edge
+ float u_turbulence; // turbulence strength (0 = smooth physical disk)
+ float u_thickness; // slab half-height (anti-aliases the edge-on disk)
+ float u_brightness; // disk brightness / exposure
+ float u_temperature; // Kelvin at the flux peak (disk colour)
};
ConstantBuffer<Disk> disk;
struct Objects
{
- int numObjects;
- float4 objPosRadius[16];
- float4 objColor[16];
- float mass[16];
+ int u_num_objects;
+ float4 u_obj_pos_radius[16];
+ float4 u_obj_color[16];
+ float u_mass[16];
};
ConstantBuffer<Objects> obj;
struct Simulation
{
- int maxStepsMoving;
- int maxStepsStatic;
- float earlyExitDistance;
- float time;
+ int u_max_steps_moving;
+ int u_max_steps_static;
+ float u_early_exit_distance;
+ float u_time;
};
ConstantBuffer<Simulation> sim;
@@ -163,8 +159,8 @@ float3 DiskEmission(float3 P, float3 rayDir, out float outG, out float outTemit)
{
outG = 0.0; outTemit = 0.0;
float rc = length(float2(P.x, P.z)); // cylindrical radius (disk axis = +Y)
- float rin = max(disk.disk_r1, R_ISCO);
- float rout = disk.disk_r2;
+ float rin = max(disk.u_inner_radius, R_ISCO);
+ float rout = disk.u_outer_radius;
if (rc < rin || rc > rout)
return float3(0.0);
@@ -173,7 +169,7 @@ float3 DiskEmission(float3 P, float3 rayDir, out float outG, out float outTemit)
float xr = rc / rin;
float flux = max((1.0 - sqrt(1.0 / xr)) / (xr * xr * xr), 0.0);
float Tn = pow(flux / FLUX_PEAK, 0.25); // normalised temperature, peak ~ 1
- float Temit = disk.temperature * Tn;
+ float Temit = disk.u_temperature * Tn;
// Keplerian orbit (prograde about +Y). Locally-measured orbital speed for a
// Schwarzschild circular geodesic: v = sqrt( M / (r - 2M) ) = 0.5 c at ISCO.
@@ -198,18 +194,18 @@ float3 DiskEmission(float3 P, float3 rayDir, out float outG, out float outTemit)
float edge = smoothstep(rin, rin * 1.12, rc) * (1.0 - smoothstep(rout * 0.88, rout, rc));
bright *= edge;
- // Optional turbulence overlay (disk.disk_num = strength; 0 = smooth).
- if (disk.disk_num > 0.0)
+ // Optional turbulence overlay (disk.u_turbulence = strength; 0 = smooth).
+ if (disk.u_turbulence > 0.0)
{
- float ang = sim.time * 0.3 / sqrt(xr);
+ float ang = sim.u_time * 0.3 / sqrt(xr);
float3 rp = float3(P.x * cos(ang) - P.z * sin(ang),
0.0,
P.x * sin(ang) + P.z * cos(ang)) * 1e-10;
- float turb = 1.0 + disk.disk_num * (fbm(rp * 3.0, 3) - 0.5);
+ float turb = 1.0 + disk.u_turbulence * (fbm(rp * 3.0, 3) - 0.5);
bright *= max(turb, 0.0);
}
- return colour * bright * max(disk.disk_density, 0.0);
+ return colour * bright * max(disk.u_brightness, 0.0);
}
struct Ray
@@ -255,15 +251,15 @@ bool Intercept(Ray ray, float rs) { return ray.r <= rs; }
bool InterceptObject(Ray ray, inout Hit hit)
{
float3 P = float3(ray.x, ray.y, ray.z);
- for (int i = 0; i < obj.numObjects; ++i)
+ for (int i = 0; i < obj.u_num_objects; ++i)
{
- float3 center = obj.objPosRadius[i].xyz;
- float radius = obj.objPosRadius[i].w;
+ float3 center = obj.u_obj_pos_radius[i].xyz;
+ float radius = obj.u_obj_pos_radius[i].w;
float distSq = dot(P - center, P - center);
if (distSq > radius * radius * 4.0) continue;
if (distSq <= radius * radius)
{
- hit.objectColor = obj.objColor[i];
+ hit.objectColor = obj.u_obj_color[i];
hit.hitCenter = center;
hit.hitRadius = radius;
return true;
@@ -317,8 +313,8 @@ float CalculateAdaptiveStepSize(Ray ray, float baseStepSize)
// Slow down when near the disk plane (within its radial extent) so the thin
// slab is never stepped over -- otherwise grazing rays leak through it.
float rc = length(float2(ray.x, ray.z));
- if (rc < disk.disk_r2 * 3.0 && abs(ray.y) < disk.thickness * 8.0)
- step = min(step, disk.thickness);
+ if (rc < disk.u_outer_radius * 3.0 && abs(ray.y) < disk.u_thickness * 8.0)
+ step = min(step, disk.u_thickness);
return clamp(step, MIN_STEP_SIZE, MAX_STEP_SIZE);
}
@@ -328,15 +324,25 @@ float3 ACESFilm(float3 x)
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
}
+// A jet-ish false-colour ramp (blue -> cyan -> green -> yellow -> red) for the
+// observable export channels. t is expected in [0, 1].
+float3 Falsecolor(float t)
+{
+ t = clamp(t, 0.0, 1.0);
+ return clamp(float3(1.5 - abs(4.0 * t - 3.0),
+ 1.5 - abs(4.0 * t - 2.0),
+ 1.5 - abs(4.0 * t - 1.0)), 0.0, 1.0);
+}
+
// Trace one primary ray for the given image UV and return its linear,
// pre-tone-map radiance. Called once per sub-sample by fragmentMain.
float3 TracePixel(float2 texCoord, out float outG, out float outTemit, out bool outHitDisk)
{
outG = 0.0; outTemit = 0.0; outHitDisk = false;
- float u = (2.0 * texCoord.x - 1.0) * cam.aspect * cam.tanHalfFov;
- float v = (1.0 - 2.0 * texCoord.y) * cam.tanHalfFov;
- float3 dir = normalize(u * cam.camRight - v * cam.camUp + cam.camForward);
- Ray ray = InitRay(cam.camPos, dir);
+ float u = (2.0 * texCoord.x - 1.0) * cam.u_aspect * cam.u_tan_half_fov;
+ float v = (1.0 - 2.0 * texCoord.y) * cam.u_tan_half_fov;
+ float3 dir = normalize(u * cam.u_cam_right - v * cam.u_cam_up + cam.u_cam_forward);
+ Ray ray = InitRay(cam.u_cam_pos, dir);
bool hitBlackHole = false;
bool hitObject = false;
@@ -348,11 +354,11 @@ float3 TracePixel(float2 texCoord, out float outG, out float outTemit, out bool
bool hitDisk = false;
float3 diskColor = float3(0.0); // emission of the first (opaque) disk surface hit
- int maxSteps = cam.moving ? sim.maxStepsMoving : sim.maxStepsStatic;
+ int maxSteps = cam.u_moving ? sim.u_max_steps_moving : sim.u_max_steps_static;
if (maxSteps <= 0)
- maxSteps = cam.moving ? DEFAULT_MAX_STEPS_MOVING : DEFAULT_MAX_STEPS_STATIC;
+ maxSteps = cam.u_moving ? DEFAULT_MAX_STEPS_MOVING : DEFAULT_MAX_STEPS_STATIC;
- float exitDistance = sim.earlyExitDistance > 0.0 ? sim.earlyExitDistance : DEFAULT_EARLY_EXIT_DISTANCE;
+ float exitDistance = sim.u_early_exit_distance > 0.0 ? sim.u_early_exit_distance : DEFAULT_EARLY_EXIT_DISTANCE;
int objectCheckInterval = 5;
for (int i = 0; i < maxSteps; ++i)
@@ -370,7 +376,7 @@ float3 TracePixel(float2 texCoord, out float outG, out float outTemit, out bool
// while grazing along it. Real (nonzero) thickness stops the zero-height
// edge-on "razor" from aliasing into a beam streaking across the frame.
{
- float H = disk.thickness;
+ float H = disk.u_thickness;
bool crossed = prevPos.y * newPos.y < 0.0;
bool inSlab = abs(newPos.y) <= H;
if (crossed || inSlab)
@@ -379,7 +385,7 @@ float3 TracePixel(float2 texCoord, out float outG, out float outTemit, out bool
? lerp(prevPos, newPos, prevPos.y / (prevPos.y - newPos.y))
: newPos;
float rc = length(float2(hitP.x, hitP.z));
- if (rc >= max(disk.disk_r1, R_ISCO) && rc <= disk.disk_r2)
+ if (rc >= max(disk.u_inner_radius, R_ISCO) && rc <= disk.u_outer_radius)
{
diskColor = DiskEmission(hitP, newPos - prevPos, outG, outTemit);
hitDisk = true;
@@ -400,7 +406,7 @@ float3 TracePixel(float2 texCoord, out float outG, out float outTemit, out bool
// Computed UNCONDITIONALLY (before the branch) so ddx/ddy are valid; strongly
// lensed background rays diverge fast, so they read a blurred cubemap mip and
// the starfield stops aliasing into a fan along the equatorial plane.
- float3 rayDir = normalize(float3(ray.x, ray.y, ray.z) - cam.camPos);
+ float3 rayDir = normalize(float3(ray.x, ray.y, ray.z) - cam.u_cam_pos);
float footprint = max(length(ddx(rayDir)), length(ddy(rayDir)));
float envLod = clamp(log2(max(footprint / 0.0015, 1.0)), 0.0, 10.0);
@@ -417,7 +423,7 @@ float3 TracePixel(float2 texCoord, out float outG, out float outTemit, out bool
{
float3 P = float3(ray.x, ray.y, ray.z);
float3 N = normalize(P - hit.hitCenter);
- float3 V = normalize(cam.camPos - P);
+ float3 V = normalize(cam.u_cam_pos - P);
float intensity = 0.1 + 0.9 * max(dot(N, V), 0.0);
shade = hit.objectColor.rgb * intensity;
}
@@ -438,41 +444,41 @@ float4 fragmentMain(VSOutput input) : SV_Target
// ordered grid). Radiance is averaged before tone-mapping; ddx/ddy give the
// resolution-correct per-pixel UV footprint.
// Observable export channels: the chosen scalar quantity, either as a raw
- // float (rawOutput: value in RGB, validity mask in A) or false-coloured.
+ // float (u_raw_output: value in RGB, validity mask in A) or false-coloured.
// Channel 0 is the normal colour image.
- if (cam.outputChannel != 0)
+ if (cam.u_output_channel != 0)
{
float g, Temit; bool hitDisk;
TracePixel(input.texCoord, g, Temit, hitDisk);
float value; float valid = 1.0;
- if (cam.outputChannel == 3) // impact parameter: a per-ray geometric quantity (r_s)
+ if (cam.u_output_channel == 3) // impact parameter: a per-ray geometric quantity (r_s)
{
- float uu = (2.0 * input.texCoord.x - 1.0) * cam.aspect * cam.tanHalfFov;
- float vv = (1.0 - 2.0 * input.texCoord.y) * cam.tanHalfFov;
- float3 dir = normalize(uu * cam.camRight - vv * cam.camUp + cam.camForward);
- value = length(cross(cam.camPos, dir)) / SagA_rs;
+ float uu = (2.0 * input.texCoord.x - 1.0) * cam.u_aspect * cam.u_tan_half_fov;
+ float vv = (1.0 - 2.0 * input.texCoord.y) * cam.u_tan_half_fov;
+ float3 dir = normalize(uu * cam.u_cam_right - vv * cam.u_cam_up + cam.u_cam_forward);
+ value = length(cross(cam.u_cam_pos, dir)) / SagA_rs;
}
else // g / T are disk-only
{
valid = hitDisk ? 1.0 : 0.0;
- value = (cam.outputChannel == 1) ? g : Temit;
+ value = (cam.u_output_channel == 1) ? g : Temit;
}
- if (cam.rawOutput != 0)
+ if (cam.u_raw_output != 0)
return float4(value, value, value, valid); // raw: physical value + validity
if (valid < 0.5) return float4(0.0, 0.0, 0.0, 1.0);
- float norm = (cam.outputChannel == 1) ? value / 1.5
- : (cam.outputChannel == 2) ? value / 15000.0
+ float norm = (cam.u_output_channel == 1) ? value / 1.5
+ : (cam.u_output_channel == 2) ? value / 15000.0
: value / 30.0;
return float4(Falsecolor(norm), 1.0);
}
float _g, _t; bool _hd;
- if (cam.rawOutput != 0) // raw colour: linear pre-tone-map radiance (HDR)
+ if (cam.u_raw_output != 0) // raw colour: linear pre-tone-map radiance (HDR)
return float4(TracePixel(input.texCoord, _g, _t, _hd), 1.0);
- if (cam.moving)
+ if (cam.u_moving)
return float4(ACESFilm(TracePixel(input.texCoord, _g, _t, _hd)), 1.0);
float2 dUV = float2(ddx(input.texCoord.x), ddy(input.texCoord.y));