From f7e05555fb4ff6d8e51bdb5734db4dab1841d593 Mon Sep 17 00:00:00 2001 From: hachem Date: Wed, 19 Aug 2026 02:02:20 +0200 Subject: [feat]: migrate to slang from glsl --- .gitignore | 5 + Assets/Shaders/Blur.glsl | 60 ---- Assets/Shaders/Blur.slang | 60 ++++ Assets/Shaders/EquirectToCubemap.glsl | 39 --- Assets/Shaders/EquirectToCubemap.slang | 33 +++ Assets/Shaders/Geodesic.glsl | 512 --------------------------------- Assets/Shaders/Geodesic.slang | 490 +++++++++++++++++++++++++++++++ Assets/Shaders/Grid.glsl | 41 --- Assets/Shaders/Grid.slang | 34 +++ Assets/Shaders/Skybox.glsl | 31 -- Assets/Shaders/Skybox.slang | 23 ++ Assets/Shaders/Sphere.glsl | 70 ----- Assets/Shaders/Sphere.slang | 67 +++++ Assets/Shaders/TexturedQuad.glsl | 28 -- Assets/Shaders/TexturedQuad.slang | 28 ++ Tools/compile-shaders.sh | 110 +++++++ Tools/fetch-slang.sh | 29 ++ docs/configuration.md | 335 --------------------- docs/mathematical-theory.md | 169 ----------- docs/numerical-methods.md | 50 ---- docs/ray-tracing-implementation.md | 258 ----------------- premake5.lua | 7 + src/Platform/OpenGL/OpenGLShader.cpp | 47 ++- src/Platform/OpenGL/OpenGLShader.h | 4 + src/Platform/OpenGL/OpenGLTexture.cpp | 6 +- 25 files changed, 928 insertions(+), 1608 deletions(-) delete mode 100644 Assets/Shaders/Blur.glsl create mode 100644 Assets/Shaders/Blur.slang delete mode 100644 Assets/Shaders/EquirectToCubemap.glsl create mode 100644 Assets/Shaders/EquirectToCubemap.slang delete mode 100644 Assets/Shaders/Geodesic.glsl create mode 100644 Assets/Shaders/Geodesic.slang delete mode 100644 Assets/Shaders/Grid.glsl create mode 100644 Assets/Shaders/Grid.slang delete mode 100644 Assets/Shaders/Skybox.glsl create mode 100644 Assets/Shaders/Skybox.slang delete mode 100644 Assets/Shaders/Sphere.glsl create mode 100644 Assets/Shaders/Sphere.slang delete mode 100644 Assets/Shaders/TexturedQuad.glsl create mode 100644 Assets/Shaders/TexturedQuad.slang create mode 100755 Tools/compile-shaders.sh create mode 100755 Tools/fetch-slang.sh delete mode 100644 docs/configuration.md delete mode 100644 docs/mathematical-theory.md delete mode 100644 docs/numerical-methods.md delete mode 100644 docs/ray-tracing-implementation.md diff --git a/.gitignore b/.gitignore index 9392e7f..c27381f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ logs/ config/ +compile_flags.txt + *.log imgui.ini Scene.json @@ -21,3 +23,6 @@ Makefile *.sln *.vcxproj *.vcxproj.* + +Tools/slang/ +Assets/Shaders/generated/ diff --git a/Assets/Shaders/Blur.glsl b/Assets/Shaders/Blur.glsl deleted file mode 100644 index 984f0ba..0000000 --- a/Assets/Shaders/Blur.glsl +++ /dev/null @@ -1,60 +0,0 @@ -#type vertex - -#version 330 core - -layout (location = 0) in vec2 a_Pos; -layout (location = 1) in vec2 a_TexCoord; - -out vec2 v_TexCoord; - -void main() -{ - gl_Position = vec4(a_Pos, 0.0, 1.0); - v_TexCoord = a_TexCoord; -} - -#type fragment - -#version 330 core - -in vec2 v_TexCoord; -out vec4 o_FragColor; - -uniform sampler2D u_ScreenTexture; -uniform vec2 u_Resolution; -uniform float u_BlurStrength; -uniform float u_GlowIntensity; - -void main() -{ - vec2 texelSize = 1.0 / u_Resolution; - vec4 centerColor = texture(u_ScreenTexture, v_TexCoord); - - float brightness = (centerColor.r + centerColor.g + centerColor.b) / 3.0; - float glowMask = smoothstep(0.05, 0.3, brightness); - - vec4 blurredColor = vec4(0.0); - float totalWeight = 0.0; - - for (int x = -8; x <= 8; x++) - { - for (int y = -8; y <= 8; y++) - { - vec2 offset = vec2(x, y) * texelSize * u_BlurStrength; - vec4 sampleColor = texture(u_ScreenTexture, v_TexCoord + offset); - - float weight = exp(-(x*x + y*y) / (2.0 * 8.0 * 8.0)); - blurredColor += sampleColor * weight; - totalWeight += weight; - } - } - - blurredColor /= totalWeight; - - vec3 emissionColor = vec3(1.0, 0.8, 0.6); - vec3 glowColor = emissionColor * glowMask * u_GlowIntensity * 2.0; - vec3 auraColor = blurredColor.rgb * glowMask * u_GlowIntensity * 0.8; - vec3 finalColor = centerColor.rgb + glowColor + auraColor; - - o_FragColor = vec4(finalColor, centerColor.a); -} diff --git a/Assets/Shaders/Blur.slang b/Assets/Shaders/Blur.slang new file mode 100644 index 0000000..17e4eae --- /dev/null +++ b/Assets/Shaders/Blur.slang @@ -0,0 +1,60 @@ +struct VSInput +{ + float2 position : POSITION; + float2 texCoord : TEXCOORD0; +}; + +struct VSOutput +{ + float4 position : SV_Position; + float2 texCoord : TEXCOORD0; +}; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.position = float4(input.position, 0.0, 1.0); + output.texCoord = input.texCoord; + return output; +} + +Sampler2D u_ScreenTexture; +uniform float2 u_Resolution; +uniform float u_BlurStrength; +uniform float u_GlowIntensity; + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + float2 texelSize = 1.0 / u_Resolution; + float4 centerColor = u_ScreenTexture.Sample(input.texCoord); + + float brightness = (centerColor.r + centerColor.g + centerColor.b) / 3.0; + float glowMask = smoothstep(0.05, 0.3, brightness); + + float4 blurredColor = float4(0.0, 0.0, 0.0, 0.0); + float totalWeight = 0.0; + + for (int x = -8; x <= 8; x++) + { + for (int y = -8; y <= 8; y++) + { + float2 offset = float2(x, y) * texelSize * u_BlurStrength; + float4 sampleColor = u_ScreenTexture.Sample(input.texCoord + offset); + + float weight = exp(-float(x * x + y * y) / (2.0 * 8.0 * 8.0)); + blurredColor += sampleColor * weight; + totalWeight += weight; + } + } + + blurredColor /= totalWeight; + + float3 emissionColor = float3(1.0, 0.8, 0.6); + float3 glowColor = emissionColor * glowMask * u_GlowIntensity * 2.0; + float3 auraColor = blurredColor.rgb * glowMask * u_GlowIntensity * 0.8; + float3 finalColor = centerColor.rgb + glowColor + auraColor; + + return float4(finalColor, centerColor.a); +} diff --git a/Assets/Shaders/EquirectToCubemap.glsl b/Assets/Shaders/EquirectToCubemap.glsl deleted file mode 100644 index 690706f..0000000 --- a/Assets/Shaders/EquirectToCubemap.glsl +++ /dev/null @@ -1,39 +0,0 @@ -#type vertex - -#version 330 core - -layout (location = 0) in vec3 a_Pos; -out vec3 v_WorldPos; - -uniform mat4 u_Projection; -uniform mat4 u_View; - -void main() -{ - v_WorldPos = a_Pos; - gl_Position = u_Projection * u_View * vec4(v_WorldPos, 1.0); -} - -#type fragment - -#version 330 core - -out vec4 o_FragColor; -in vec3 v_WorldPos; - -uniform sampler2D u_EquirectangularMap; -const vec2 invAtan = vec2(0.1591, 0.3183); - -vec2 SampleSphericalMap(vec3 v) -{ - vec2 uv = vec2(atan(v.z, v.x), asin(v.y)); - uv *= invAtan; - uv += 0.5; - return uv; -} -void main() -{ - vec2 uv = SampleSphericalMap(normalize(v_WorldPos)); - vec3 color = texture(u_EquirectangularMap, uv).rgb; - o_FragColor = vec4(color, 1.0); -} diff --git a/Assets/Shaders/EquirectToCubemap.slang b/Assets/Shaders/EquirectToCubemap.slang new file mode 100644 index 0000000..9b2ea4b --- /dev/null +++ b/Assets/Shaders/EquirectToCubemap.slang @@ -0,0 +1,33 @@ +uniform float4x4 u_Projection; +uniform float4x4 u_View; + +struct VSInput { float3 position : POSITION; }; +struct VSOutput { float4 position : SV_Position; float3 worldPos : TEXCOORD0; }; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.worldPos = input.position; + output.position = mul(u_Projection, mul(u_View, float4(input.position, 1.0))); + return output; +} + +Sampler2D u_EquirectangularMap; +static const float2 invAtan = float2(0.1591, 0.3183); + +float2 SampleSphericalMap(float3 v) +{ + float2 uv = float2(atan2(v.z, v.x), asin(v.y)); + uv *= invAtan; + uv += 0.5; + return uv; +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + float2 uv = SampleSphericalMap(normalize(input.worldPos)); + float3 color = u_EquirectangularMap.Sample(uv).rgb; + return float4(color, 1.0); +} diff --git a/Assets/Shaders/Geodesic.glsl b/Assets/Shaders/Geodesic.glsl deleted file mode 100644 index cb435d0..0000000 --- a/Assets/Shaders/Geodesic.glsl +++ /dev/null @@ -1,512 +0,0 @@ -// Geodesic ray tracer. -// -// Originally an OpenGL 4.3 compute shader that wrote its result with -// imageStore(). macOS OpenGL is frozen at 4.1 and has neither compute shaders -// nor image load/store, so this is expressed as a fullscreen vertex+fragment -// pass (GLSL 4.10) that renders into an FBO colour attachment instead. Each -// fragment does the work one compute invocation used to: gl_FragCoord replaces -// gl_GlobalInvocationID, and the shading maths below is unchanged. - -#type vertex -#version 410 core - -layout(location = 0) in vec2 a_Position; - -void main() -{ - gl_Position = vec4(a_Position, 0.0, 1.0); -} - -#type fragment -#version 410 core - -out vec4 fragColor; - -// Compute-resolution in pixels; supplied by the host each frame (replaces the -// compute shader's imageSize(outImage)). -uniform vec2 u_Resolution; - -// GLSL 4.10 does not allow explicit binding qualifiers on uniform blocks or -// samplers; the host associates these with binding points via -// glUniformBlockBinding / glUniform1i. -uniform samplerCube u_HDRIEnvironment; - -layout(std140) uniform Camera -{ - vec3 camPos; float _pad0; - vec3 camRight; float _pad1; - vec3 camUp; float _pad2; - vec3 camForward; float _pad3; - float tanHalfFov; - float aspect; - bool moving; - int _pad4; -} cam; - -layout(std140) uniform Disk -{ - float disk_r1; - float disk_r2; - float disk_num; - float thickness; - float disk_density; -}; - -layout(std140) uniform Objects -{ - int numObjects; - vec4 objPosRadius[16]; - vec4 objColor[16]; - float mass[16]; -}; - -layout(std140) uniform Simulation -{ - int maxStepsMoving; - int maxStepsStatic; - float earlyExitDistance; - float time; -}; - -const float SagA_rs = 1.269e10; -const float D_LAMBDA = 1e7; -const float ESCAPE_R = 1e30; - -const int DEFAULT_MAX_STEPS_MOVING = 12000; -const int DEFAULT_MAX_STEPS_STATIC = 8000; -const float DEFAULT_EARLY_EXIT_DISTANCE = 2e12; - -const float MIN_STEP_SIZE = 1e6; -const float MAX_STEP_SIZE = 5e7; -const float STEP_ADAPTATION_FACTOR = 1.5; - -vec4 objectColor = vec4(0.0); -vec3 hitCenter = vec3(0.0); -float hitRadius = 0.0; - -vec3 SampleHDRI(vec3 direction) -{ - return texture(u_HDRIEnvironment, direction).rgb; -} - -float hash(float p) -{ - p = fract(p * 0.1031); - p *= p + 33.33; - p *= p + p; - return fract(p); -} - -float hash(vec2 p) -{ - vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973)); - p3 += dot(p3, p3.yzx + 33.33); - return fract((p3.x + p3.y) * p3.z); -} - -float hash(vec3 p) -{ - p = fract(p * vec3(0.1031, 0.1030, 0.0973)); - p += dot(p, p.yxz + 33.33); - return fract((p.x + p.y) * p.z); -} - -float noise(vec3 x) -{ - vec3 i = floor(x); - vec3 frac = fract(x); - - vec3 u = frac * frac * (3.0 - 2.0 * frac); - - float a = hash(i); - float b = hash(i + vec3(1.0, 0.0, 0.0)); - float c = hash(i + vec3(0.0, 1.0, 0.0)); - float d = hash(i + vec3(1.0, 1.0, 0.0)); - float e = hash(i + vec3(0.0, 0.0, 1.0)); - float f = hash(i + vec3(1.0, 0.0, 1.0)); - float g = hash(i + vec3(0.0, 1.0, 1.0)); - float h = hash(i + vec3(1.0, 1.0, 1.0)); - - return mix(mix(mix(a, b, u.x), mix(c, d, u.x), u.y), - mix(mix(e, f, u.x), mix(g, h, u.x), u.y), u.z); -} - -float fbm(vec3 x, int octaves) -{ - float v = 0.0; - float a = 0.5; - float f = 1.0; - vec3 shift = vec3(100, 200, 300); - - for (int i = 0; i < octaves; ++i) - { - v += a * noise(x * f); - x = x * 2.0 + shift; - a *= 0.5; - f *= 2.0; - } - return v; -} - -float GetCloudDensity(vec3 pos) -{ - float r_cyl = length(vec2(pos.x, pos.z)); - float r_norm = (r_cyl - disk_r1) / (disk_r2 - disk_r1); - - if (r_norm < 0.0 || r_norm > 1.0) - return 0.0; - - float h_norm = abs(pos.y) / thickness; - float vertical_falloff = exp(-h_norm * h_norm * 3.0); - float radial_density = 1.0 - r_norm * 0.5; - - vec3 noise_pos = pos * 1e-10; - float keplerian_speed = 1.0 / sqrt(r_norm + 0.1); - - float rotation_angle = time * keplerian_speed * 0.5; - vec3 rotated_pos = vec3( - pos.x * cos(rotation_angle) - pos.z * sin(rotation_angle), - pos.y, - pos.x * sin(rotation_angle) + pos.z * cos(rotation_angle) - ) * 1e-10; - - float large_turbulence = fbm(rotated_pos * 1.2, 5); - - float medium_wisps = fbm(rotated_pos * 2.5, 4); - float small_detail = fbm(rotated_pos * 6.0, 3); - float fine_detail = fbm(rotated_pos * 10.0, 2); - - float noise_mask = large_turbulence * 0.4 + - medium_wisps * 0.3 + - small_detail * 0.2 + - fine_detail * 0.1; - - noise_mask = smoothstep(0.25, 0.75, noise_mask); - - float angle = atan(pos.z, pos.x); - float rotated_angle = angle + time * 0.5; - - float spiral_arms = sin(rotated_angle * 3.0 + r_norm * 15.0) * 0.15 + 0.85; - - float orbital_angle = angle + time * keplerian_speed * 0.8; - float orbital_pattern = sin(orbital_angle * 2.0 + r_norm * 8.0) * 0.2 + 0.8; - - float density = vertical_falloff * radial_density * noise_mask * spiral_arms * orbital_pattern; - return density * disk_density; -} - -struct Ray -{ - float x, y, z; - float r, theta, phi; - float dr, dtheta, dphi; - float E, L; -}; - -Ray InitRay(vec3 pos, vec3 dir) -{ - Ray ray; - ray.x = pos.x; - ray.y = pos.y; - ray.z = pos.z; - ray.r = length(pos); - ray.theta = acos(pos.z / ray.r); - ray.phi = atan(pos.y, pos.x); - - float dx = dir.x; - float dy = dir.y; - float dz = dir.z; - - ray.dr = sin(ray.theta)*cos(ray.phi)*dx + - sin(ray.theta)*sin(ray.phi)*dy + - cos(ray.theta)*dz; - - ray.dtheta = (cos(ray.theta)*cos(ray.phi)*dx + - cos(ray.theta)*sin(ray.phi)*dy - - sin(ray.theta)*dz) / ray.r; - - ray.dphi = (-sin(ray.phi)*dx + cos(ray.phi)*dy) / - (ray.r * sin(ray.theta)); - - ray.L = ray.r * ray.r * sin(ray.theta) * ray.dphi; - float f = 1.0 - SagA_rs / ray.r; - float dt_dL = sqrt((ray.dr*ray.dr)/f + - ray.r*ray.r*(ray.dtheta*ray.dtheta + - sin(ray.theta)*sin(ray.theta)* - ray.dphi*ray.dphi)); - ray.E = f * dt_dL; - - return ray; -} - -bool Intercept(Ray ray, float rs) -{ - return ray.r <= rs; -} - -bool InterceptObject(Ray ray) -{ - vec3 P = vec3(ray.x, ray.y, ray.z); - - for (int i = 0; i < numObjects; ++i) - { - vec3 center = objPosRadius[i].xyz; - float radius = objPosRadius[i].w; - - float distSq = dot(P - center, P - center); - if (distSq > radius * radius * 4.0) - continue; - - if (distSq <= radius * radius) - { - objectColor = objColor[i]; - hitCenter = center; - hitRadius = radius; - return true; - } - } - - return false; -} - -void GeodesicRHS(Ray ray, out vec3 d1, out vec3 d2) -{ - float r = ray.r; - float theta = ray.theta; - float dr = ray.dr; - float dtheta = ray.dtheta; - float dphi = ray.dphi; - float f = 1.0 - SagA_rs / r; - float dt_dL = ray.E / f; - - d1 = vec3(dr, dtheta, dphi); - d2.x = - (SagA_rs / (2.0 * r*r)) * f * dt_dL * dt_dL - + (SagA_rs / (2.0 * r*r * f)) * dr * dr - + r * (dtheta*dtheta + sin(theta)*sin(theta)*dphi*dphi); - d2.y = -2.0*dr*dtheta/r + sin(theta)*cos(theta)*dphi*dphi; - d2.z = -2.0*dr*dphi/r - 2.0*cos(theta)/(sin(theta)) * dtheta * dphi; -} - -void RK4Step(inout Ray ray, float dL) -{ - vec3 k1a, k1b; - GeodesicRHS(ray, k1a, k1b); - - ray.r += dL * k1a.x; - ray.theta += dL * k1a.y; - ray.phi += dL * k1a.z; - ray.dr += dL * k1b.x; - ray.dtheta += dL * k1b.y; - ray.dphi += dL * k1b.z; - - ray.x = ray.r * sin(ray.theta) * cos(ray.phi); - ray.y = ray.r * sin(ray.theta) * sin(ray.phi); - ray.z = ray.r * cos(ray.theta); -} - -bool IsInDiskVolume(vec3 pos) -{ - float r_cyl = length(vec2(pos.x, pos.z)); - return (r_cyl >= disk_r1 && r_cyl <= disk_r2 && abs(pos.y) <= thickness); -} - -vec4 SampleDiskColor(vec3 pos) -{ - float r_cyl = length(vec2(pos.x, pos.z)); - float r_norm = (r_cyl - disk_r1) / (disk_r2 - disk_r1); - - vec3 innerColor = vec3(1.0, 0.9, 0.5); - vec3 midColor = vec3(1.0, 0.6, 0.2); - vec3 outerColor = vec3(0.9, 0.3, 0.1); - - vec3 baseColor; - if (r_norm < 0.5) - baseColor = mix(innerColor, midColor, r_norm * 2.0); - else - baseColor = mix(midColor, outerColor, (r_norm - 0.5) * 2.0); - - float r_norm_rot = (r_cyl - disk_r1) / (disk_r2 - disk_r1); - float keplerian_speed = 1.0 / sqrt(r_norm_rot + 0.1); - - vec3 noise_pos = pos * 1e-10; - - float color_rotation_angle = time * keplerian_speed * 0.3; - vec3 rotated_color_pos = vec3( - pos.x * cos(color_rotation_angle) - pos.z * sin(color_rotation_angle), - pos.y, - pos.x * sin(color_rotation_angle) + pos.z * cos(color_rotation_angle) - ) * 1e-10; - - float large_color = fbm(rotated_color_pos * 1.8, 4); - float medium_color = fbm(rotated_color_pos * 4.0, 3); - float small_color = fbm(rotated_color_pos * 8.0, 2); - float colorVariation = (large_color * 0.5 + medium_color * 0.3 + small_color * 0.2) * 0.6; - baseColor = baseColor * (1.0 + colorVariation); - - float density = GetCloudDensity(pos); - vec3 brightness_noise_pos = pos * 1e-10; - - float brightness_rotation_angle = time * keplerian_speed * 0.7; - vec3 rotated_brightness_pos = vec3( - pos.x * cos(brightness_rotation_angle) - pos.z * sin(brightness_rotation_angle), - pos.y, - pos.x * sin(brightness_rotation_angle) + pos.z * cos(brightness_rotation_angle) - ) * 1e-10; - - float brightness_large = fbm(rotated_brightness_pos * 3.0, 3); - float brightness_medium = fbm(rotated_brightness_pos * 5.0, 2); - float brightness_small = fbm(rotated_brightness_pos * 7.0, 2); - float brightness_noise = (brightness_large * 0.6 + brightness_medium * 0.3 + brightness_small * 0.1); - - // Enhanced brightness with glow effect - float baseBrightness = 1.0 + density * 1.5; // Increased density contribution - float glowBrightness = brightness_noise * 0.8; // Enhanced noise contribution - float brightness = baseBrightness + glowBrightness; - - return vec4(baseColor * brightness, density); -} - -float CalculateAdaptiveStepSize(Ray ray, float baseStepSize) -{ - float r_factor = clamp(ray.r / (SagA_rs * 10.0), 0.1, 1.0); - float curvature = length(vec3(ray.dr, ray.dtheta * ray.r, ray.dphi * ray.r * sin(ray.theta))); - float curvature_factor = clamp(1e12 / (curvature + 1e6), 0.1, 2.0); - - return clamp(baseStepSize * r_factor * curvature_factor, MIN_STEP_SIZE, MAX_STEP_SIZE); -} - -void main() -{ - ivec2 pix = ivec2(gl_FragCoord.xy); - int WIDTH = int(u_Resolution.x); - int HEIGHT = int(u_Resolution.y); - - float u = (2.0 * (pix.x + 0.5) / WIDTH - 1.0) * - cam.aspect * cam.tanHalfFov; - float v = (1.0 - 2.0 * (pix.y + 0.5) / HEIGHT) * - cam.tanHalfFov; - vec3 dir = normalize(u * cam.camRight - - v * cam.camUp + - cam.camForward); - Ray ray = InitRay(cam.camPos, dir); - - vec4 color = vec4(0.0); - vec3 prevPos = vec3(ray.x, ray.y, ray.z); - float lambda = 0.0; - - bool hitBlackHole = false; - bool hitObject = false; - - vec4 accumulatedColor = vec4(0.0); - float transmittance = 1.0; - - int maxSteps = cam.moving ? maxStepsMoving : maxStepsStatic; - - if (maxSteps <= 0) - maxSteps = cam.moving ? DEFAULT_MAX_STEPS_MOVING : DEFAULT_MAX_STEPS_STATIC; - - float cameraDistance = length(cam.camPos); - if (cameraDistance > 2e12) - maxSteps = maxSteps / 2; - else if (cameraDistance > 1e12) - maxSteps = int(maxSteps * 0.75); - - float initialEscapeVelocity = sqrt(2.0 * SagA_rs / ray.r); - if (ray.dr > initialEscapeVelocity * 0.95 && - ray.r > SagA_rs * 200.0) - maxSteps = maxSteps / 2; - - float currentStepSize = D_LAMBDA; - int objectCheckInterval = 5; - - for (int i = 0; i < maxSteps; ++i) - { - float exitDistance = earlyExitDistance > 0.0 ? earlyExitDistance : DEFAULT_EARLY_EXIT_DISTANCE; - if (ray.r > exitDistance) - break; - if (ray.r > ESCAPE_R) - break; - - if (Intercept(ray, SagA_rs)) - { - hitBlackHole = true; - break; - } - - currentStepSize = CalculateAdaptiveStepSize(ray, D_LAMBDA); - - RK4Step(ray, currentStepSize); - lambda += currentStepSize; - - vec3 newPos = vec3(ray.x, ray.y, ray.z); - - if (IsInDiskVolume(newPos)) - { - vec4 diskSample = SampleDiskColor(newPos); - float density = diskSample.a; - vec3 diskColor = diskSample.rgb; - - float stepLength = currentStepSize * 1e-8; - - float absorption = density * stepLength * 0.8; - float scattering = density * stepLength * 1.5; - float extinction = absorption + scattering; - - float stepTransmittance = exp(-extinction); - - vec3 emission = diskColor * density * stepLength * 4.0 * sqrt(disk_density); - - vec3 glowColor = mix(diskColor, vec3(1.0, 0.8, 0.6), 0.3); - float glowIntensity = density * stepLength * 2.0; - vec3 atmosphericGlow = glowColor * glowIntensity * 0.8; - - vec3 totalEmission = emission + atmosphericGlow; - accumulatedColor.rgb += totalEmission * transmittance; - - transmittance *= stepTransmittance; - - if (transmittance < 0.01) - { - accumulatedColor.a = 1.0 - transmittance; - break; - } - } - - if (i % objectCheckInterval == 0 && InterceptObject(ray)) - { - hitObject = true; - break; - } - - prevPos = newPos; - - if (ray.dr > 0.0 && ray.r > SagA_rs * 100.0 && lambda > 2e8) - break; - } - - accumulatedColor.a = 1.0 - transmittance; - - if (hitBlackHole) - color = vec4(0.0, 0.0, 0.0, 1.0); - else if (hitObject) - { - vec3 P = vec3(ray.x, ray.y, ray.z); - vec3 N = normalize(P - hitCenter); - vec3 V = normalize(cam.camPos - P); - - float ambient = 0.1; - float diff = max(dot(N, V), 0.0); - float intensity = ambient + (1.0 - ambient) * diff; - vec3 shaded = objectColor.rgb * intensity; - - color = vec4(shaded, objectColor.a); - color = mix(accumulatedColor, color, color.a); - } else - { - vec3 rayDirection = normalize(vec3(ray.x, ray.y, ray.z) - cam.camPos); - vec3 hdriColor = SampleHDRI(rayDirection); - color = vec4(mix(accumulatedColor.rgb, hdriColor, 1.0 - accumulatedColor.a), 1.0); - } - - fragColor = color; -} diff --git a/Assets/Shaders/Geodesic.slang b/Assets/Shaders/Geodesic.slang new file mode 100644 index 0000000..9490f21 --- /dev/null +++ b/Assets/Shaders/Geodesic.slang @@ -0,0 +1,490 @@ +struct VSInput { float2 position : POSITION; float2 texCoord : TEXCOORD0; }; +struct VSOutput { float4 position : SV_Position; float2 texCoord : TEXCOORD0; }; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.position = float4(input.position, 0.0, 1.0); + output.texCoord = input.texCoord; + return output; +} + +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 _pad4; +}; +ConstantBuffer cam; + +struct Disk +{ + float disk_r1; + float disk_r2; + float disk_num; + float thickness; + float disk_density; +}; +ConstantBuffer disk; + +struct Objects +{ + int numObjects; + float4 objPosRadius[16]; + float4 objColor[16]; + float mass[16]; +}; +ConstantBuffer obj; + +struct Simulation +{ + int maxStepsMoving; + int maxStepsStatic; + float earlyExitDistance; + float time; +}; +ConstantBuffer sim; + +SamplerCube u_HDRIEnvironment; + +static const float SagA_rs = 1.269e10; +static const float D_LAMBDA = 1e7; +static const float ESCAPE_R = 1e30; + +static const int DEFAULT_MAX_STEPS_MOVING = 12000; +static const int DEFAULT_MAX_STEPS_STATIC = 8000; +static const float DEFAULT_EARLY_EXIT_DISTANCE = 2e12; + +static const float MIN_STEP_SIZE = 1e6; +static const float MAX_STEP_SIZE = 5e7; + +struct Hit +{ + float4 objectColor; + float3 hitCenter; + float hitRadius; +}; + +float3 SampleHDRI(float3 direction) +{ + return u_HDRIEnvironment.Sample(direction).rgb; +} + +float hash(float p) +{ + p = frac(p * 0.1031); + p *= p + 33.33; + p *= p + p; + return frac(p); +} + +float hash(float2 p) +{ + float3 p3 = frac(float3(p.xyx) * float3(0.1031, 0.1030, 0.0973)); + p3 += dot(p3, p3.yzx + 33.33); + return frac((p3.x + p3.y) * p3.z); +} + +float hash(float3 p) +{ + p = frac(p * float3(0.1031, 0.1030, 0.0973)); + p += dot(p, p.yxz + 33.33); + return frac((p.x + p.y) * p.z); +} + +float noise(float3 x) +{ + float3 i = floor(x); + float3 fr = frac(x); + + float3 u = fr * fr * (3.0 - 2.0 * fr); + + float a = hash(i); + float b = hash(i + float3(1.0, 0.0, 0.0)); + float c = hash(i + float3(0.0, 1.0, 0.0)); + float d = hash(i + float3(1.0, 1.0, 0.0)); + float e = hash(i + float3(0.0, 0.0, 1.0)); + float f = hash(i + float3(1.0, 0.0, 1.0)); + float g = hash(i + float3(0.0, 1.0, 1.0)); + float h = hash(i + float3(1.0, 1.0, 1.0)); + + return lerp(lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y), + lerp(lerp(e, f, u.x), lerp(g, h, u.x), u.y), u.z); +} + +float fbm(float3 x, int octaves) +{ + float v = 0.0; + float a = 0.5; + float f = 1.0; + float3 shift = float3(100, 200, 300); + + for (int i = 0; i < octaves; ++i) + { + v += a * noise(x * f); + x = x * 2.0 + shift; + a *= 0.5; + f *= 2.0; + } + return v; +} + +float GetCloudDensity(float3 pos) +{ + float r_cyl = length(float2(pos.x, pos.z)); + float r_norm = (r_cyl - disk.disk_r1) / (disk.disk_r2 - disk.disk_r1); + + if (r_norm < 0.0 || r_norm > 1.0) + return 0.0; + + float h_norm = abs(pos.y) / disk.thickness; + float vertical_falloff = exp(-h_norm * h_norm * 3.0); + float radial_density = 1.0 - r_norm * 0.5; + + float keplerian_speed = 1.0 / sqrt(r_norm + 0.1); + + float rotation_angle = sim.time * keplerian_speed * 0.5; + float3 rotated_pos = float3( + pos.x * cos(rotation_angle) - pos.z * sin(rotation_angle), + pos.y, + pos.x * sin(rotation_angle) + pos.z * cos(rotation_angle) + ) * 1e-10; + + float large_turbulence = fbm(rotated_pos * 1.2, 5); + float medium_wisps = fbm(rotated_pos * 2.5, 4); + float small_detail = fbm(rotated_pos * 6.0, 3); + float fine_detail = fbm(rotated_pos * 10.0, 2); + + float noise_mask = large_turbulence * 0.4 + + medium_wisps * 0.3 + + small_detail * 0.2 + + fine_detail * 0.1; + + noise_mask = smoothstep(0.25, 0.75, noise_mask); + + float angle = atan2(pos.z, pos.x); + float rotated_angle = angle + sim.time * 0.5; + + float spiral_arms = sin(rotated_angle * 3.0 + r_norm * 15.0) * 0.15 + 0.85; + + float orbital_angle = angle + sim.time * keplerian_speed * 0.8; + float orbital_pattern = sin(orbital_angle * 2.0 + r_norm * 8.0) * 0.2 + 0.8; + + float density = vertical_falloff * radial_density * noise_mask * spiral_arms * orbital_pattern; + return density * disk.disk_density; +} + +struct Ray +{ + float x, y, z; + float r, theta, phi; + float dr, dtheta, dphi; + float E, L; +}; + +Ray InitRay(float3 pos, float3 dir) +{ + Ray ray; + ray.x = pos.x; + ray.y = pos.y; + ray.z = pos.z; + ray.r = length(pos); + ray.theta = acos(pos.z / ray.r); + ray.phi = atan2(pos.y, pos.x); + + float dx = dir.x; + float dy = dir.y; + float dz = dir.z; + + ray.dr = sin(ray.theta)*cos(ray.phi)*dx + + sin(ray.theta)*sin(ray.phi)*dy + + cos(ray.theta)*dz; + + ray.dtheta = (cos(ray.theta)*cos(ray.phi)*dx + + cos(ray.theta)*sin(ray.phi)*dy - + sin(ray.theta)*dz) / ray.r; + + ray.dphi = (-sin(ray.phi)*dx + cos(ray.phi)*dy) / + (ray.r * sin(ray.theta)); + + ray.L = ray.r * ray.r * sin(ray.theta) * ray.dphi; + float f = 1.0 - SagA_rs / ray.r; + float dt_dL = sqrt((ray.dr*ray.dr)/f + + ray.r*ray.r*(ray.dtheta*ray.dtheta + + sin(ray.theta)*sin(ray.theta)* + ray.dphi*ray.dphi)); + ray.E = f * dt_dL; + + return ray; +} + +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) + { + float3 center = obj.objPosRadius[i].xyz; + float radius = obj.objPosRadius[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.hitCenter = center; + hit.hitRadius = radius; + return true; + } + } + + return false; +} + +void GeodesicRHS(Ray ray, out float3 d1, out float3 d2) +{ + float r = ray.r; + float theta = ray.theta; + float dr = ray.dr; + float dtheta = ray.dtheta; + float dphi = ray.dphi; + float f = 1.0 - SagA_rs / r; + float dt_dL = ray.E / f; + + d1 = float3(dr, dtheta, dphi); + d2.x = - (SagA_rs / (2.0 * r*r)) * f * dt_dL * dt_dL + + (SagA_rs / (2.0 * r*r * f)) * dr * dr + + r * (dtheta*dtheta + sin(theta)*sin(theta)*dphi*dphi); + d2.y = -2.0*dr*dtheta/r + sin(theta)*cos(theta)*dphi*dphi; + d2.z = -2.0*dr*dphi/r - 2.0*cos(theta)/(sin(theta)) * dtheta * dphi; +} + +void RK4Step(inout Ray ray, float dL) +{ + float3 k1a, k1b; + GeodesicRHS(ray, k1a, k1b); + + ray.r += dL * k1a.x; + ray.theta += dL * k1a.y; + ray.phi += dL * k1a.z; + ray.dr += dL * k1b.x; + ray.dtheta += dL * k1b.y; + ray.dphi += dL * k1b.z; + + ray.x = ray.r * sin(ray.theta) * cos(ray.phi); + ray.y = ray.r * sin(ray.theta) * sin(ray.phi); + ray.z = ray.r * cos(ray.theta); +} + +bool IsInDiskVolume(float3 pos) +{ + float r_cyl = length(float2(pos.x, pos.z)); + return (r_cyl >= disk.disk_r1 && r_cyl <= disk.disk_r2 && abs(pos.y) <= disk.thickness); +} + +float4 SampleDiskColor(float3 pos) +{ + float r_cyl = length(float2(pos.x, pos.z)); + float r_norm = (r_cyl - disk.disk_r1) / (disk.disk_r2 - disk.disk_r1); + + float3 innerColor = float3(1.0, 0.9, 0.5); + float3 midColor = float3(1.0, 0.6, 0.2); + float3 outerColor = float3(0.9, 0.3, 0.1); + + float3 baseColor; + if (r_norm < 0.5) + baseColor = lerp(innerColor, midColor, r_norm * 2.0); + else + baseColor = lerp(midColor, outerColor, (r_norm - 0.5) * 2.0); + + float r_norm_rot = (r_cyl - disk.disk_r1) / (disk.disk_r2 - disk.disk_r1); + float keplerian_speed = 1.0 / sqrt(r_norm_rot + 0.1); + + float color_rotation_angle = sim.time * keplerian_speed * 0.3; + float3 rotated_color_pos = float3( + pos.x * cos(color_rotation_angle) - pos.z * sin(color_rotation_angle), + pos.y, + pos.x * sin(color_rotation_angle) + pos.z * cos(color_rotation_angle) + ) * 1e-10; + + float large_color = fbm(rotated_color_pos * 1.8, 4); + float medium_color = fbm(rotated_color_pos * 4.0, 3); + float small_color = fbm(rotated_color_pos * 8.0, 2); + float colorVariation = (large_color * 0.5 + medium_color * 0.3 + small_color * 0.2) * 0.6; + baseColor = baseColor * (1.0 + colorVariation); + + float density = GetCloudDensity(pos); + + float brightness_rotation_angle = sim.time * keplerian_speed * 0.7; + float3 rotated_brightness_pos = float3( + pos.x * cos(brightness_rotation_angle) - pos.z * sin(brightness_rotation_angle), + pos.y, + pos.x * sin(brightness_rotation_angle) + pos.z * cos(brightness_rotation_angle) + ) * 1e-10; + + float brightness_large = fbm(rotated_brightness_pos * 3.0, 3); + float brightness_medium = fbm(rotated_brightness_pos * 5.0, 2); + float brightness_small = fbm(rotated_brightness_pos * 7.0, 2); + float brightness_noise = (brightness_large * 0.6 + brightness_medium * 0.3 + brightness_small * 0.1); + + float baseBrightness = 1.0 + density * 1.5; + float glowBrightness = brightness_noise * 0.8; + float brightness = baseBrightness + glowBrightness; + + return float4(baseColor * brightness, density); +} + +float CalculateAdaptiveStepSize(Ray ray, float baseStepSize) +{ + float r_factor = clamp(ray.r / (SagA_rs * 10.0), 0.1, 1.0); + float curvature = length(float3(ray.dr, ray.dtheta * ray.r, ray.dphi * ray.r * sin(ray.theta))); + float curvature_factor = clamp(1e12 / (curvature + 1e6), 0.1, 2.0); + + return clamp(baseStepSize * r_factor * curvature_factor, MIN_STEP_SIZE, MAX_STEP_SIZE); +} + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + float u = (2.0 * input.texCoord.x - 1.0) * cam.aspect * cam.tanHalfFov; + float v = (1.0 - 2.0 * input.texCoord.y) * cam.tanHalfFov; + float3 dir = normalize(u * cam.camRight - v * cam.camUp + cam.camForward); + Ray ray = InitRay(cam.camPos, dir); + + float4 color = float4(0.0, 0.0, 0.0, 0.0); + + bool hitBlackHole = false; + bool hitObject = false; + + Hit hit; + hit.objectColor = float4(0.0, 0.0, 0.0, 0.0); + hit.hitCenter = float3(0.0, 0.0, 0.0); + hit.hitRadius = 0.0; + + float4 accumulatedColor = float4(0.0, 0.0, 0.0, 0.0); + float transmittance = 1.0; + + int maxSteps = cam.moving ? sim.maxStepsMoving : sim.maxStepsStatic; + + if (maxSteps <= 0) + maxSteps = cam.moving ? DEFAULT_MAX_STEPS_MOVING : DEFAULT_MAX_STEPS_STATIC; + + float cameraDistance = length(cam.camPos); + if (cameraDistance > 2e12) + maxSteps = maxSteps / 2; + else if (cameraDistance > 1e12) + maxSteps = int(maxSteps * 0.75); + + float initialEscapeVelocity = sqrt(2.0 * SagA_rs / ray.r); + if (ray.dr > initialEscapeVelocity * 0.95 && + ray.r > SagA_rs * 200.0) + maxSteps = maxSteps / 2; + + float lambda = 0.0; + int objectCheckInterval = 5; + + for (int i = 0; i < maxSteps; ++i) + { + float exitDistance = sim.earlyExitDistance > 0.0 ? sim.earlyExitDistance : DEFAULT_EARLY_EXIT_DISTANCE; + if (ray.r > exitDistance) + break; + if (ray.r > ESCAPE_R) + break; + + if (Intercept(ray, SagA_rs)) + { + hitBlackHole = true; + break; + } + + float currentStepSize = CalculateAdaptiveStepSize(ray, D_LAMBDA); + + RK4Step(ray, currentStepSize); + lambda += currentStepSize; + + float3 newPos = float3(ray.x, ray.y, ray.z); + + if (IsInDiskVolume(newPos)) + { + float4 diskSample = SampleDiskColor(newPos); + float density = diskSample.a; + float3 diskColor = diskSample.rgb; + + float stepLength = currentStepSize * 1e-8; + + float absorption = density * stepLength * 0.8; + float scattering = density * stepLength * 1.5; + float extinction = absorption + scattering; + + float stepTransmittance = exp(-extinction); + + float3 emission = diskColor * density * stepLength * 4.0 * sqrt(disk.disk_density); + + float3 glowColor = lerp(diskColor, float3(1.0, 0.8, 0.6), 0.3); + float glowIntensity = density * stepLength * 2.0; + float3 atmosphericGlow = glowColor * glowIntensity * 0.8; + + float3 totalEmission = emission + atmosphericGlow; + accumulatedColor.rgb += totalEmission * transmittance; + + transmittance *= stepTransmittance; + + if (transmittance < 0.01) + { + accumulatedColor.a = 1.0 - transmittance; + break; + } + } + + if (i % objectCheckInterval == 0 && InterceptObject(ray, hit)) + { + hitObject = true; + break; + } + + if (ray.dr > 0.0 && ray.r > SagA_rs * 100.0 && lambda > 2e8) + break; + } + + accumulatedColor.a = 1.0 - transmittance; + + if (hitBlackHole) + { + color = float4(0.0, 0.0, 0.0, 1.0); + } + else if (hitObject) + { + float3 P = float3(ray.x, ray.y, ray.z); + float3 N = normalize(P - hit.hitCenter); + float3 V = normalize(cam.camPos - P); + + float ambient = 0.1; + float diff = max(dot(N, V), 0.0); + float intensity = ambient + (1.0 - ambient) * diff; + float3 shaded = hit.objectColor.rgb * intensity; + + color = float4(shaded, hit.objectColor.a); + color = lerp(accumulatedColor, color, color.a); + } + else + { + float3 rayDirection = normalize(float3(ray.x, ray.y, ray.z) - cam.camPos); + float3 hdriColor = SampleHDRI(rayDirection); + color = float4(lerp(accumulatedColor.rgb, hdriColor, 1.0 - accumulatedColor.a), 1.0); + } + + return color; +} diff --git a/Assets/Shaders/Grid.glsl b/Assets/Shaders/Grid.glsl deleted file mode 100644 index 32c091a..0000000 --- a/Assets/Shaders/Grid.glsl +++ /dev/null @@ -1,41 +0,0 @@ -#type vertex - -#version 330 core - -layout(location = 0) in vec3 a_Position; - -uniform mat4 u_ViewProjection; -uniform mat4 u_Transform; -uniform float u_GridSize; - -void main() -{ - // Scale the grid based on the grid size uniform - vec3 scaledPosition = a_Position * (u_GridSize / 50.0); - gl_Position = u_ViewProjection * u_Transform * vec4(scaledPosition, 1.0); -} - -#type fragment - -#version 330 core - -uniform vec3 u_GridColor; -uniform float u_GridAlpha; -uniform float u_GridSize; -uniform vec3 u_CameraPos; - -out vec4 o_FragColor; - -void main() -{ - float distance = length(u_CameraPos); - float fadeFactor = 1.0 - clamp(distance / 100.0, 0.0, 0.8); - - vec3 color = u_GridColor; - float alpha = u_GridAlpha * fadeFactor; - - float gridFade = 1.0 - clamp(distance / 50.0, 0.0, 0.9); - alpha *= gridFade; - - o_FragColor = vec4(color, alpha); -} diff --git a/Assets/Shaders/Grid.slang b/Assets/Shaders/Grid.slang new file mode 100644 index 0000000..ea7d263 --- /dev/null +++ b/Assets/Shaders/Grid.slang @@ -0,0 +1,34 @@ +uniform float4x4 u_ViewProjection; +uniform float4x4 u_Transform; +uniform float u_GridSize; + +struct VSInput { float3 position : POSITION; }; +struct VSOutput { float4 position : SV_Position; }; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + float3 scaledPosition = input.position * (u_GridSize / 50.0); + output.position = mul(u_ViewProjection, mul(u_Transform, float4(scaledPosition, 1.0))); + return output; +} + +uniform float3 u_GridColor; +uniform float u_GridAlpha; +uniform float3 u_CameraPos; + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + float dist = length(u_CameraPos); + float fadeFactor = 1.0 - clamp(dist / 100.0, 0.0, 0.8); + + float3 color = u_GridColor; + float alpha = u_GridAlpha * fadeFactor; + + float gridFade = 1.0 - clamp(dist / 50.0, 0.0, 0.9); + alpha *= gridFade; + + return float4(color, alpha); +} diff --git a/Assets/Shaders/Skybox.glsl b/Assets/Shaders/Skybox.glsl deleted file mode 100644 index 654bed1..0000000 --- a/Assets/Shaders/Skybox.glsl +++ /dev/null @@ -1,31 +0,0 @@ -#type vertex - -#version 330 core - -layout (location = 0) in vec3 aPos; - -out vec3 TexCoords; - -uniform mat4 u_Projection; -uniform mat4 u_View; - -void main() -{ - TexCoords = aPos; - vec4 pos = u_Projection * u_View * vec4(aPos, 1.0); - gl_Position = pos.xyww; -} - -#type fragment - -#version 330 core - -out vec4 FragColor; -in vec3 TexCoords; - -uniform samplerCube u_Skybox; - -void main() -{ - FragColor = texture(u_Skybox, TexCoords); -} diff --git a/Assets/Shaders/Skybox.slang b/Assets/Shaders/Skybox.slang new file mode 100644 index 0000000..6ba81bc --- /dev/null +++ b/Assets/Shaders/Skybox.slang @@ -0,0 +1,23 @@ +uniform float4x4 u_Projection; +uniform float4x4 u_View; + +struct VSInput { float3 position : POSITION; }; +struct VSOutput { float4 position : SV_Position; float3 texCoords : TEXCOORD0; }; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.texCoords = input.position; + float4 pos = mul(u_Projection, mul(u_View, float4(input.position, 1.0))); + output.position = pos.xyww; // force depth = 1 so the skybox stays behind everything + return output; +} + +SamplerCube u_Skybox; + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + return u_Skybox.Sample(input.texCoords); +} diff --git a/Assets/Shaders/Sphere.glsl b/Assets/Shaders/Sphere.glsl deleted file mode 100644 index 9f620f7..0000000 --- a/Assets/Shaders/Sphere.glsl +++ /dev/null @@ -1,70 +0,0 @@ -#type vertex - -#version 330 core - -layout(location = 0) in vec3 a_Position; -layout(location = 1) in vec3 a_Normal; - -uniform mat4 u_ViewProjection; -uniform mat4 u_Transform; - -out vec3 v_Normal; -out vec3 v_WorldPos; - -void main() -{ - v_WorldPos = vec3(u_Transform * vec4(a_Position, 1.0)); - v_Normal = mat3(transpose(inverse(u_Transform))) * a_Normal; - gl_Position = u_ViewProjection * vec4(v_WorldPos, 1.0); -} - -#type fragment - -#version 330 core - -in vec3 v_Normal; -in vec3 v_WorldPos; - -uniform vec3 u_Color; -uniform float u_Specular; -uniform float u_Emission; -uniform vec3 u_LightPos; -uniform vec3 u_CameraPos; -uniform int u_IsSelected; -uniform vec3 u_OutlineColor; -uniform float u_OutlineWidth; -uniform samplerCube u_HDRIEnvironment; - -out vec4 o_FragColor; - -void main() -{ - vec3 normal = normalize(v_Normal); - vec3 lightDir = normalize(u_LightPos - v_WorldPos); - vec3 viewDir = normalize(u_CameraPos - v_WorldPos); - vec3 reflectDir = reflect(-lightDir, normal); - - // HDRI lighting - vec3 hdriLight = texture(u_HDRIEnvironment, normal).rgb; - float hdriIntensity = 0.3; // Adjust this for HDRI lighting strength - - float diff = max(dot(normal, lightDir), 0.0); - float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); - - vec3 diffuse = u_Color * (diff + hdriIntensity * hdriLight); - vec3 specular = vec3(u_Specular) * spec; - vec3 emission = u_Color * u_Emission; - - vec3 result = diffuse + specular + emission; - - if (u_IsSelected > 0) - { - float ndotv = max(dot(normal, viewDir), 0.0); - float rim = 1.0 - ndotv; - float width = clamp(u_OutlineWidth, 0.0, 1.0); - float outlineMask = step(1.0 - u_OutlineWidth, rim);; - result = mix(result, u_OutlineColor, outlineMask); - } - - o_FragColor = vec4(result, 1.0); -} diff --git a/Assets/Shaders/Sphere.slang b/Assets/Shaders/Sphere.slang new file mode 100644 index 0000000..0871438 --- /dev/null +++ b/Assets/Shaders/Sphere.slang @@ -0,0 +1,67 @@ +uniform float4x4 u_ViewProjection; +uniform float4x4 u_Transform; + +struct VSInput +{ + float3 position : POSITION; + float3 normal : NORMAL; +}; + +struct VSOutput +{ + float4 position : SV_Position; + float3 normal : TEXCOORD0; + float3 worldPos : TEXCOORD1; +}; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.worldPos = mul(u_Transform, float4(input.position, 1.0)).xyz; + float3x3 normalMatrix = (float3x3)u_Transform; + output.normal = mul(normalMatrix, input.normal); + output.position = mul(u_ViewProjection, float4(output.worldPos, 1.0)); + return output; +} + +uniform float3 u_Color; +uniform float u_Specular; +uniform float u_Emission; +uniform float3 u_LightPos; +uniform float3 u_CameraPos; +uniform int u_IsSelected; +uniform float3 u_OutlineColor; +uniform float u_OutlineWidth; +SamplerCube u_HDRIEnvironment; + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + float3 normal = normalize(input.normal); + float3 lightDir = normalize(u_LightPos - input.worldPos); + float3 viewDir = normalize(u_CameraPos - input.worldPos); + float3 reflectDir = reflect(-lightDir, normal); + + float3 hdriLight = u_HDRIEnvironment.Sample(normal).rgb; + float hdriIntensity = 0.3; + + float diff = max(dot(normal, lightDir), 0.0); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); + + float3 diffuse = u_Color * (diff + hdriIntensity * hdriLight); + float3 specular = float3(u_Specular, u_Specular, u_Specular) * spec; + float3 emission = u_Color * u_Emission; + + float3 result = diffuse + specular + emission; + + if (u_IsSelected > 0) + { + float ndotv = max(dot(normal, viewDir), 0.0); + float rim = 1.0 - ndotv; + float outlineMask = step(1.0 - u_OutlineWidth, rim); + result = lerp(result, u_OutlineColor, outlineMask); + } + + return float4(result, 1.0); +} diff --git a/Assets/Shaders/TexturedQuad.glsl b/Assets/Shaders/TexturedQuad.glsl deleted file mode 100644 index 95f433d..0000000 --- a/Assets/Shaders/TexturedQuad.glsl +++ /dev/null @@ -1,28 +0,0 @@ -#type vertex - -#version 330 core - -layout (location = 0) in vec2 a_Pos; -layout (location = 1) in vec2 a_TexCoord; - -out vec2 v_TexCoord; - -void main() -{ - gl_Position = vec4(a_Pos, 0.0, 1.0); - v_TexCoord = a_TexCoord; -} - -#type fragment - -#version 330 core - -in vec2 v_TexCoord; -out vec4 o_FragColor; - -uniform sampler2D u_ScreenTexture; - -void main() -{ - o_FragColor = texture(u_ScreenTexture, v_TexCoord); -} diff --git a/Assets/Shaders/TexturedQuad.slang b/Assets/Shaders/TexturedQuad.slang new file mode 100644 index 0000000..52d46ed --- /dev/null +++ b/Assets/Shaders/TexturedQuad.slang @@ -0,0 +1,28 @@ +struct VSInput +{ + float2 position : POSITION; + float2 texCoord : TEXCOORD0; +}; + +struct VSOutput +{ + float4 position : SV_Position; + float2 texCoord : TEXCOORD0; +}; + +[shader("vertex")] +VSOutput vertexMain(VSInput input) +{ + VSOutput output; + output.position = float4(input.position, 0.0, 1.0); + output.texCoord = input.texCoord; + return output; +} + +Sampler2D u_ScreenTexture; + +[shader("fragment")] +float4 fragmentMain(VSOutput input) : SV_Target +{ + return u_ScreenTexture.Sample(input.texCoord); +} diff --git a/Tools/compile-shaders.sh b/Tools/compile-shaders.sh new file mode 100755 index 0000000..ea1aa15 --- /dev/null +++ b/Tools/compile-shaders.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# +# Compiles every Assets/Shaders/*.slang to all backend targets, into +# Assets/Shaders/generated/ (gitignored): +# +# OpenGL : .glsl - combined "#type" GLSL 4.10 (Slang -> SPIR-V -> +# SPIRV-Cross), macOS-4.1-core valid, loaded by +# OpenGLShader. +# Vulkan : ..spv - SPIR-V per entry point. +# Metal : .metal - MSL, whole module. +# +# Entry points are auto-discovered from Slang [shader("stage")] attributes. +# Override tool paths with SLANGC / SPIRV_CROSS. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SLANGC="${SLANGC:-$ROOT/Tools/slang/bin/slangc}" +SPIRV_CROSS="${SPIRV_CROSS:-$(command -v spirv-cross || echo spirv-cross)}" +SRC="$ROOT/Assets/Shaders" +OUT="$SRC/generated" + +[ -x "$SLANGC" ] || { echo "compile-shaders: slangc not found at $SLANGC (run Tools/fetch-slang.sh)"; exit 1; } +command -v "$SPIRV_CROSS" >/dev/null 2>&1 || { echo "compile-shaders: spirv-cross not found (brew install spirv-cross)"; exit 1; } + +mkdir -p "$OUT" +shopt -s nullglob +shaders=("$SRC"/*.slang) +[ ${#shaders[@]} -gt 0 ] || { echo "compile-shaders: no .slang files in $SRC yet"; exit 0; } + +# Prints "stage entryName" for each [shader("stage")] entry point in a file. +entry_points() { + awk ' + /\[shader\(/ { if (match($0, /"[a-zA-Z]+"/)) { stage = substr($0, RSTART+1, RLENGTH-2); want = 1 } next } + want && $0 ~ /^[ \t]*\[/ { next } # skip further attributes + want && /\(/ { + sig = $0; sub(/\(.*/, "", sig) + n = split(sig, t, /[ \t*&]+/); name = t[n] + if (name != "") print stage, name + want = 0 + } + ' "$1" +} + +fail=0 +for f in "${shaders[@]}"; do + name="$(basename "$f" .slang)" + echo "== $name.slang ==" + + # Metal: whole module. + if "$SLANGC" "$f" -target metal -o "$OUT/$name.metal" 2>"$OUT/.err"; then + echo " metal -> $name.metal" + else echo " metal FAILED:"; sed 's/^/ /' "$OUT/.err"; fail=1; fi + + # OpenGL + Vulkan: per entry point. Shaders that declare a ConstantBuffer keep + # real UBO blocks (the Engine binds them by name); the rest have Slang's + # $Globals flattened to loose uniforms so per-draw glUniform* calls still work. + if grep -q 'ConstantBuffer<' "$f"; then ubo=1; else ubo=0; fi + combined="$OUT/$name.glsl"; : > "$combined" + while read -r stage entry; do + [ -z "${stage:-}" ] && continue + spv="$OUT/$name.$entry.spv" + + if ! "$SLANGC" "$f" -target spirv -entry "$entry" -stage "$stage" -o "$spv" 2>"$OUT/.err"; then + echo " spirv FAILED ($entry):"; sed 's/^/ /' "$OUT/.err"; fail=1; continue + fi + echo " spirv -> $name.$entry.spv ($stage)" + + # SPIR-V -> classic GLSL 4.10 (combined samplers, no binding layouts). + xcopts="--version 410 --no-es --no-420pack-extension" + [ "$ubo" = 0 ] && xcopts="$xcopts --glsl-emit-ubo-as-plain-uniforms" + + if "$SPIRV_CROSS" $xcopts "$spv" --output "$OUT/.stage.glsl" 2>"$OUT/.err"; then + if [ "$ubo" = 1 ]; then + # Real UBOs: strip SPIRV-Cross's "_std140" block-name suffix so the block + # names match glGetUniformBlockIndex("Camera") etc. in the Engine. + perl -i -pe 's/_std140//g' "$OUT/.stage.glsl" + else + # Flatten "struct S {..}; uniform S inst;" into loose "uniform TYPE NAME;" + # and strip the "inst." prefix, so uniforms keep their original names and + # the existing C++ SetMat4("u_X") / SetFloat("u_Y") calls work unchanged. + perl -0777 -i -pe ' + while (/struct\s+(\w+)\s*\{(.*?)\}\s*;\s*uniform\s+\1\s+(\w+)\s*;/s) { + my ($s,$body,$inst)=($1,$2,$3); my @u; + while ($body =~ /([A-Za-z_]\w*)\s+([A-Za-z_]\w*(?:\[\d+\])?)\s*;/g) { push @u,"uniform $1 $2;"; } + my $loose=join("\n",@u); + s/struct\s+\Q$s\E\s*\{.*?\}\s*;\s*uniform\s+\Q$s\E\s+\Q$inst\E\s*;/$loose/s; + s/\b\Q$inst\E\.//g; + } + ' "$OUT/.stage.glsl" + fi + # SPIRV-Cross names inter-stage varyings per stage and relies on explicit + # locations, but macOS's GL linker matches varyings by NAME. Rename each + # varying to a location-canonical name (_v) so stages agree: rename + # `out` in vertex stages and `in` in fragment stages (never attributes). + case "$stage" in vertex) vdir=out ;; fragment) vdir=in ;; *) vdir="" ;; esac + if [ -n "$vdir" ]; then + DIR="$vdir" perl -0777 -i -pe ' + my $d=$ENV{DIR}; my %m; + while (/layout\(location = (\d+)\)\s+\Q$d\E\s+\w+\s+(\w+)\s*;/g) { $m{$2}="_v$1"; } + for my $n (keys %m) { s/\b\Q$n\E\b/$m{$n}/g; } + ' "$OUT/.stage.glsl" + fi + { echo "#type $stage"; cat "$OUT/.stage.glsl"; echo; } >> "$combined" + echo " glsl -> $name.glsl (#type $stage)" + else echo " glsl FAILED ($entry):"; sed 's/^/ /' "$OUT/.err"; fail=1; fi + done < <(entry_points "$f") +done + +rm -f "$OUT/.err" "$OUT/.stage.glsl" +exit $fail diff --git a/Tools/fetch-slang.sh b/Tools/fetch-slang.sh new file mode 100755 index 0000000..5ee6de5 --- /dev/null +++ b/Tools/fetch-slang.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Fetches the Slang shader-compiler toolchain into Tools/slang (gitignored). +# Slang is the single shader-authoring language for Donut; slangc cross-compiles +# each Assets/Shaders/*.slang to GLSL (OpenGL), SPIR-V (Vulkan), and MSL (Metal). +# +# Re-run to (re)install. Pin the version here so builds are reproducible. +set -euo pipefail + +VERSION="2026.14.1" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DEST="$ROOT/Tools/slang" + +case "$(uname -s)-$(uname -m)" in + Darwin-arm64|Darwin-aarch64) ASSET="macos-aarch64" ;; + Darwin-x86_64) ASSET="macos-x86_64" ;; + Linux-x86_64) ASSET="linux-x86_64" ;; + Linux-aarch64|Linux-arm64) ASSET="linux-aarch64" ;; + *) echo "fetch-slang: unsupported platform $(uname -s)-$(uname -m)"; exit 1 ;; +esac + +URL="https://github.com/shader-slang/slang/releases/download/v${VERSION}/slang-${VERSION}-${ASSET}.tar.gz" + +echo "fetch-slang: downloading Slang ${VERSION} (${ASSET})..." +rm -rf "$DEST" +mkdir -p "$DEST" +curl -fsSL "$URL" | tar -xz -C "$DEST" + +"$DEST/bin/slangc" -v >/dev/null 2>&1 && echo "fetch-slang: installed to $DEST (slangc $("$DEST/bin/slangc" -v))" diff --git a/docs/configuration.md b/docs/configuration.md deleted file mode 100644 index 293a4a6..0000000 --- a/docs/configuration.md +++ /dev/null @@ -1,335 +0,0 @@ -# Configuration -## Configuration Files - -### Settings File Location - -The application uses TOML format for configuration files: - -- **Primary location**: `config/settings.toml` -- **User settings**: Loaded at startup, saved on exit - -### TOML Format - -The configuration uses TOML format: - -```toml -[simulation] -max_steps_static = 15000 -max_steps_moving = 30000 -compute_height = 256 -target_fps = 60 -early_exit_distance = 5e+12 -gravity_enabled = true - -[graphics] -render_api = "OpenGL" -vsync_enabled = true -enable_anti_aliasing = true -show_fps = true -show_performance_metrics = true -show_debug_info = false -selected_theme = "Dark" -``` - -## Simulation Parameters - -### Ray Tracing Settings - -#### Max Steps (Static) -- **Description**: Maximum number of integration steps when camera is stationary -- **Range**: 1,000 - 30,000 -- **Default**: 15,000 -- **Impact**: Higher values = better accuracy, lower performance - -```toml -max_steps_static = 15000 -``` - -#### Max Steps (Moving) -- **Description**: Maximum number of integration steps when camera is moving -- **Range**: 1,000 - 60,000 -- **Default**: 30,000 -- **Impact**: Higher values = smoother motion, lower performance - -```toml -max_steps_moving = 30000 -``` - -#### Early Exit Distance -- **Description**: Distance at which ray marching stops to improve performance -- **Range**: 1×10¹¹ - 1×10¹³ meters -- **Default**: 5×10¹² meters -- **Impact**: Lower values = faster rendering, may miss distant objects - -```toml -early_exit_distance = 5e+12 -``` - -### Performance Settings - -#### Target FPS -- **Description**: Target frame rate for the application -- **Range**: 30 - 120 FPS -- **Default**: 60 FPS -- **Impact**: Higher values = smoother animation, higher CPU usage - -```toml -target_fps = 60 -``` - -#### Compute Height -- **Description**: Resolution of the compute shader (height component) -- **Range**: 64 - 2,048 pixels -- **Default**: 256 pixels -- **Impact**: Higher values = better quality, lower performance - -```toml -compute_height = 256 -``` - -### Physics Settings - -#### Gravity Enabled -- **Description**: Enable/disable gravitational interactions between objects -- **Type**: Boolean -- **Default**: true -- **Impact**: Affects object motion and orbital dynamics - -```toml -gravity_enabled = true -``` - -## Graphics Settings - -### Rendering API - -#### Render API -- **Description**: Graphics API to use for rendering -- **Options**: "OpenGL", "Vulkan" -- **Default**: "OpenGL" -- **Impact**: Affects performance and feature availability - -```toml -render_api = "OpenGL" -``` - -### Display Settings - -#### V-Sync Enabled -- **Description**: Enable vertical synchronization -- **Type**: Boolean -- **Default**: true -- **Impact**: Prevents screen tearing, may limit frame rate - -```toml -vsync_enabled = true -``` - -#### Enable Anti-Aliasing -- **Description**: Enable anti-aliasing for smoother edges -- **Type**: Boolean -- **Default**: true -- **Impact**: Better visual quality, slight performance cost - -```toml -enable_anti_aliasing = true -``` - -### UI Settings - -#### Show FPS -- **Description**: Display frame rate counter -- **Type**: Boolean -- **Default**: true -- **Impact**: Performance monitoring, minimal overhead - -```toml -show_fps = true -``` - -#### Show Performance Metrics -- **Description**: Display detailed performance information -- **Type**: Boolean -- **Default**: true -- **Impact**: Debug information, minimal overhead - -```toml -show_performance_metrics = true -``` - -#### Show Debug Info -- **Description**: Display debug information -- **Type**: Boolean -- **Default**: false -- **Impact**: Development information, may impact performance - -```toml -show_debug_info = false -``` - -#### Selected Theme -- **Description**: UI theme selection -- **Options**: "Dark", "Light" -- **Default**: "Dark" -- **Impact**: Visual appearance only - -```toml -selected_theme = "Dark" -``` - -## Performance Tuning - -### Preset-Performance - -The configuration system allows users to balance quality and performance: - -#### High Quality Settings -```toml -[simulation] -max_steps_static = 30000 -max_steps_moving = 60000 -compute_height = 1024 -early_exit_distance = 1e+13 -``` - -#### Balanced Settings -```toml -[simulation] -max_steps_static = 15000 -max_steps_moving = 30000 -compute_height = 512 -early_exit_distance = 5e+12 -``` - -#### Performance Settings -```toml -[simulation] -max_steps_static = 5000 -max_steps_moving = 10000 -compute_height = 256 -early_exit_distance = 2e+12 -``` - -### Adaptive Performance - -The application automatically adjusts performance based on conditions: - -```glsl -// Reduce steps for distant cameras -float cameraDistance = length(cam.camPos); -if (cameraDistance > 2e12) - maxSteps = maxSteps / 2; -else if (cameraDistance > 1e12) - maxSteps = int(maxSteps * 0.75); - -// Reduce steps for escaping rays -float initialEscapeVelocity = sqrt(2.0 * SagA_rs / ray.r); -if (ray.dr > initialEscapeVelocity * 0.95 && ray.r > SagA_rs * 200.0) - maxSteps = maxSteps / 2; -``` - -## Configuration Interface - -### GUI Configuration - -The application provides a graphical interface for configuration: - -```cpp -void ConfigState::OnImGuiRender() -{ - ImGui::Begin("Configuration"); - - // Simulation settings - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Simulation"); - ImGui::Separator(); - - ImGui::SliderInt("Max Steps (Static)", &m_MaxStepsStatic, 1000, 30000, "%d"); - ImGui::SliderInt("Max Steps (Moving)", &m_MaxStepsMoving, 1000, 60000, "%d"); - ImGui::SliderFloat("Early Exit Distance", &m_EarlyExitDistance, 1e11f, 1e13f, "%.2e"); - ImGui::SliderInt("Compute Height", &m_ComputeHeight, 64, 2048, "%d px"); - ImGui::SliderInt("Target FPS", &m_TargetFPS, 30, 120, "%d"); - - // Physics settings - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Physics"); - ImGui::Separator(); - - ImGui::Checkbox("Enable Gravity", &m_GravityEnabled); - - // Graphics settings - ImGui::TextColored(ImVec4(0.9f, 0.9f, 1.0f, 1.0f), "Graphics"); - ImGui::Separator(); - - ImGui::Checkbox("V-Sync", &m_VSyncEnabled); - ImGui::Checkbox("Anti-Aliasing", &m_AntiAliasingEnabled); - ImGui::Checkbox("Show FPS", &m_ShowFPS); - ImGui::Checkbox("Performance Metrics", &m_ShowPerformanceMetrics); - - ImGui::End(); -} -``` - - -## Default Configurations - -### Preset Configurations - -The application includes several preset configurations: - -#### Ultra Quality -```toml -[simulation] -max_steps_static = 50000 -max_steps_moving = 100000 -compute_height = 2048 -early_exit_distance = 1e+13 -target_fps = 30 -``` - -#### High Quality -```toml -[simulation] -max_steps_static = 30000 -max_steps_moving = 60000 -compute_height = 1024 -early_exit_distance = 8e+12 -target_fps = 60 -``` - -#### Balanced -```toml -[simulation] -max_steps_static = 15000 -max_steps_moving = 30000 -compute_height = 512 -early_exit_distance = 5e+12 -target_fps = 60 -``` - -#### Performance -```toml -[simulation] -max_steps_static = 5000 -max_steps_moving = 10000 -compute_height = 256 -early_exit_distance = 2e+12 -target_fps = 120 -``` - -## Troubleshooting - -### Common Issues - -#### Performance Problems -- **Symptom**: Low frame rate, stuttering -- **Solution**: Reduce `max_steps_moving`, `max_steps_static`, or `compute_height` -- **Alternative**: Increase `early_exit_distance` - -#### Quality Issues -- **Symptom**: Poor image quality, artifacts -- **Solution**: Increase `compute_height` and step counts -- **Alternative**: Reduce `early_exit_distance` - -#### Configuration Errors -- **Symptom**: Application crashes on startup -- **Solution**: Delete configuration file to reset to defaults -- **Alternative**: Check TOML syntax in settings file \ No newline at end of file diff --git a/docs/mathematical-theory.md b/docs/mathematical-theory.md deleted file mode 100644 index 4c73425..0000000 --- a/docs/mathematical-theory.md +++ /dev/null @@ -1,169 +0,0 @@ -# Mathematical Theory of Black Hole Geodesics - -### Einstein’s Field Equations - -General Relativity describes gravity not as a force, but as the curvature of spacetime caused by mass and energy. This curvature is captured by **Einstein’s field equations**: - -$$ -G_{\mu\nu} = \frac{8\pi G}{c^4} T_{\mu\nu} -$$ - -Here: - -* $G_{\mu\nu}$ is the **Einstein tensor**, describing spacetime curvature. -* $T_{\mu\nu}$ is the **stress-energy tensor**, representing matter and energy. -* $G$ is Newton’s gravitational constant, and $c$ is the speed of light. - -### Spacetime Metric - -Distances in spacetime are described using the **metric tensor** $g_{\mu\nu}$: - -$$ -ds^2 = g_{\mu\nu} dx^\mu dx^\nu -$$ - -where $ds^2$ is the spacetime interval between two events. - -## Schwarzschild Metric - -For a **spherically symmetric, non-rotating mass** (like a static black hole), the Schwarzschild solution gives the spacetime geometry: - -$$ -ds^2 = -\left(1 - \frac{2GM}{c^2 r}\right) dt^2 + \left(1 - \frac{2GM}{c^2 r}\right)^{-1} dr^2 + r^2 (d\theta^2 + \sin^2\theta \, d\phi^2) -$$ - -* $M$ = mass of the black hole -* $r, \theta, \phi$ = spherical coordinates -* $t$ = time coordinate - -The **Schwarzschild radius** $r_s$ marks the event horizon: - -$$ -r_s = \frac{2GM}{c^2} -$$ - -Inside $r_s$, not even light can escape. - -We often write the metric using the **lapse function** $f(r)$: - -$$ -ds^2 = -f(r) dt^2 + f(r)^{-1} dr^2 + r^2 (d\theta^2 + \sin^2\theta \, d\phi^2), \quad f(r) = 1 - \frac{r_s}{r} -$$ - -## Geodesics: Paths of Free-Falling Particles and Light - -A **geodesic** is the path that a particle follows when moving under gravity alone. For light rays, $ds^2 = 0$ (null geodesics). - -### Lagrangian Formulation - -We can derive the geodesic equations from a Lagrangian: - -$$ -L = \frac{1}{2} g_{\mu\nu} \dot{x}^\mu \dot{x}^\nu, \quad \dot{x}^\mu = \frac{dx^\mu}{d\lambda} -$$ - -where $\lambda$ is an affine parameter along the geodesic. - -### Conserved Quantities - -Because the Schwarzschild metric is **time-independent** and **spherically symmetric**, we have two key conserved quantities: - -1. **Energy** (from time translation symmetry): - -$$ -E = - g_{tt} \frac{dt}{d\lambda} = f(r) \frac{dt}{d\lambda} -$$ - -2. **Angular Momentum** (from rotational symmetry): - -$$ -L = g_{\phi\phi} \frac{d\phi}{d\lambda} = r^2 \sin^2 \theta \frac{d\phi}{d\lambda} -$$ - -### Derivation of the Geodesic Equations - -Geodesics satisfy the **Euler-Lagrange equations**: - -$$ -\frac{d}{d\lambda} \left(\frac{\partial L}{\partial \dot{x}^\mu}\right) - \frac{\partial L}{\partial x^\mu} = 0 -$$ - -#### 1. Radial Motion - -For the Schwarzschild metric: - -$$ -L = \frac{1}{2} \left[-f(r) \dot{t}^2 + f(r)^{-1} \dot{r}^2 + r^2 (\dot{\theta}^2 + \sin^2\theta \, \dot{\phi}^2)\right] -$$ - -The radial Euler-Lagrange equation becomes: - -$$ -\ddot{r} = -\frac{GM}{r^2} (\dot{t})^2 + \frac{GM}{r^2 f(r)} (\dot{r})^2 + r f(r) \left(\dot{\theta}^2 + \sin^2\theta \, \dot{\phi}^2\right) -$$ - -#### 2. Angular Motion - -$$ -\ddot{\theta} = -\frac{2}{r} \dot{r} \dot{\theta} + \sin\theta \cos\theta \, \dot{\phi}^2 -$$ - -$$ -\ddot{\phi} = -\frac{2}{r} \dot{r} \dot{\phi} - 2 \cot\theta \, \dot{\theta} \dot{\phi} -$$ - -Here, $\dot{}$ denotes derivative with respect to $\lambda$. - -These equations fully describe how light or particles move around a Schwarzschild black hole. - -### Numerical Implementation - -In a shader or simulation, we integrate these equations using: - -```glsl -void GeodesicRHS(Ray ray, out vec3 d1, out vec3 d2) -{ - float r = ray.r; - float theta = ray.theta; - float dr = ray.dr; - float dtheta = ray.dtheta; - float dphi = ray.dphi; - float f = 1.0 - SagA_rs / r; - float dt_dL = ray.E / f; - - d1 = vec3(dr, dtheta, dphi); - d2.x = - (SagA_rs / (2.0 * r*r)) * f * dt_dL * dt_dL - + (SagA_rs / (2.0 * r*r * f)) * dr * dr - + r * (dtheta*dtheta + sin(theta)*sin(theta)*dphi*dphi); - d2.y = -2.0*dr*dtheta/r + sin(theta)*cos(theta)*dphi*dphi; - d2.z = -2.0*dr*dphi/r - 2.0*cos(theta)/(sin(theta)) * dtheta * dphi; -} -``` - -### Conserved Quantities in Code - -```glsl -ray.E = f * dt_dL; // Energy -ray.L = ray.r * ray.r * sin(ray.theta) * ray.dphi; // Angular momentum -``` - -### Effective Potential - -The **radial motion** can be described using an effective potential: - -$$ -V_\text{eff}(r) = \left(1 - \frac{r_s}{r}\right) \frac{L^2}{r^2} -$$ - -This potential defines the possible orbits of light or particles. - -### Relativistic Effects Around Black Holes - -* **Gravitational Lensing:** Light bends around the black hole, producing Einstein rings, multiple images, or distorted images. -* **Event Horizon:** Located at $r = r_s$, where nothing escapes. -* **Photon Sphere:** At $r = 1.5 r_s$, light can orbit in unstable circular paths. - -### Numerical Considerations - -* **Event Horizon:** Integration becomes singular at $r = r_s$. Use adaptive step sizes or terminate integration near the horizon. -* **Coordinate Poles:** Spherical coordinates have singularities at $\theta = 0, \pi$. Avoid direct integration through these points or use transformations. diff --git a/docs/numerical-methods.md b/docs/numerical-methods.md deleted file mode 100644 index 4e102e6..0000000 --- a/docs/numerical-methods.md +++ /dev/null @@ -1,50 +0,0 @@ -## Runge-Kutta 4 (RK4) Integration — Explained - -When we talk about geodesics in curved spacetime, we’re dealing with a system of differential equations that describe how a particle—or in our case, a ray of light—moves. These equations are usually too complicated to solve exactly, so we turn to numerical methods. One of the most popular choices is the **fourth-order Runge-Kutta method (RK4)**. - -Think of RK4 like taking careful steps along a winding mountain trail. At each step, instead of just looking straight ahead, RK4 takes a few “sneak peeks” along the way to estimate the path more accurately. - -### The Idea in Simple Terms - -Suppose you know where you are at a particular moment and you know the slope of your path (the derivative). A naive method like **Euler’s method** would take a single step using that slope and call it a day. But if the slope changes a lot along your step, Euler can easily go off-track. - -RK4 improves on this by taking **four evaluations** of the slope at carefully chosen points: - -1. **Start of the step** — check the slope right where you are (`k1`). -2. **Halfway in, using the first slope** — imagine taking a mid-step to see if the slope changes (`k2`). -3. **Halfway in, using the second slope** — another mid-step with a slightly better estimate (`k3`). -4. **End of the step** — take a peek at the slope at the far end of your step (`k4`). - -Then RK4 combines all these slopes in a weighted average: - -$$ -y_{n+1} = y_n + \frac{h}{6} (k_1 + 2 k_2 + 2 k_3 + k_4) -$$ - -This weighted combination gives a very accurate estimate of where you should be at the next step. - -### Why RK4 Works Well for Geodesics - -In the context of geodesics: - -* Each ray has six “pieces of information”: position `(r, θ, φ)` and velocity `(dr/dλ, dθ/dλ, dφ/dλ)`. -* The RK4 method allows us to update all six components **simultaneously**, while keeping the accumulated error small. -* Because spacetime curvature can change dramatically near a black hole, RK4 is especially helpful: it’s stable enough to handle strong curvature without requiring tiny steps everywhere. - -### A Visual Analogy - -Imagine you’re rowing a boat down a twisting river: - -* **Euler**: You look at the current direction, row a fixed distance, and hope for the best. You’ll likely drift off course if the river bends sharply. -* **RK4**: You peek ahead four times along your intended path and adjust your stroke accordingly. You stay much closer to the true river path, even around tight bends. - -### Accuracy - -* RK4 is called **fourth-order** because the error per step scales with $h^5$, and the total accumulated error scales roughly with $h^4$ (where $h$ is your step size). -* This means you can take reasonably large steps without losing accuracy, which is critical when simulating millions of rays efficiently. - -### Connecting to Code - -In your code, each `k` evaluation corresponds to calculating how the ray’s position and velocity would change at different “guesses” along the step. The final weighted combination moves the ray forward accurately in spacetime. - -By using RK4, we’re essentially giving each ray a **very careful and informed nudge**, instead of blindly pushing it along, which is why the results are both stable and accurate—even near the extreme curvature of a black hole. \ No newline at end of file diff --git a/docs/ray-tracing-implementation.md b/docs/ray-tracing-implementation.md deleted file mode 100644 index 89778b7..0000000 --- a/docs/ray-tracing-implementation.md +++ /dev/null @@ -1,258 +0,0 @@ -# Ray Tracing in Curved Spacetime: A Detailed Overview - -This ray tracer simulates how light behaves near a black hole, including the effects of curved spacetime and interactions with objects like stars and an accretion disk. The implementation follows a step-by-step approach, which includes initializing rays, tracing their paths through spacetime, detecting intersections with objects, and finally rendering the scene. - -## 1. Ray Initialization - -The first step in ray tracing is to create rays originating from the camera that will eventually traverse through spacetime. Each ray represents a possible path of light. - -### Camera Setup - -The camera is positioned in three-dimensional space and is defined by its orientation and field of view. It has the following parameters: - -```glsl -layout(std140, binding = 1) uniform Camera -{ - vec3 camPos; float _pad0; - vec3 camRight; float _pad1; - vec3 camUp; float _pad2; - vec3 camForward; float _pad3; - float tanHalfFov; // Field of view - float aspect; // Aspect ratio - bool moving; // Whether the camera is moving - int _pad4; -} cam; -``` - -- `camPos`: The 3D position of the camera. -- `camRight`, `camUp`, `camForward`: Orthonormal vectors defining the camera's orientation. -- `tanHalfFov` and `aspect`: Determine the camera’s field of view and the shape of the image plane. - -### Ray Generation per Pixel - -For each pixel on the image, we calculate the corresponding direction in world space and generate a ray pointing in that direction: - -```glsl -float u = (2.0 * (pix.x + 0.5) / WIDTH - 1.0) * cam.aspect * cam.tanHalfFov; -float v = (1.0 - 2.0 * (pix.y + 0.5) / HEIGHT) * cam.tanHalfFov; -vec3 dir = normalize(u * cam.camRight - v * cam.camUp + cam.camForward); -Ray ray = InitRay(cam.camPos, dir); -``` - -Here, `u` and `v` are normalized coordinates on the image plane. The `InitRay` function takes the camera position and the computed direction to create a ray in both Cartesian and spherical coordinates. - -### Ray Structure - -Each ray contains not only the standard 3D Cartesian position but also spherical coordinates and velocity components, which are necessary for simulating curved spacetime: - -```glsl -struct Ray -{ - float x, y, z; // Cartesian coordinates - float r, theta, phi; // Spherical coordinates - float dr, dtheta, dphi; // Radial and angular velocities - float E, L; // Conserved quantities in Schwarzschild geometry -}; -``` - -### Converting Cartesian to Spherical Coordinates - -The `InitRay` function converts the position and direction of the ray into spherical coordinates and computes initial velocities: - -```glsl -Ray InitRay(vec3 pos, vec3 dir) -{ - Ray ray; - ray.x = pos.x; - ray.y = pos.y; - ray.z = pos.z; - - // Spherical coordinates - ray.r = length(pos); - ray.theta = acos(pos.z / ray.r); - ray.phi = atan(pos.y, pos.x); - - // Convert direction vector to spherical velocities - float dx = dir.x; - float dy = dir.y; - float dz = dir.z; - - ray.dr = sin(ray.theta)*cos(ray.phi)*dx + sin(ray.theta)*sin(ray.phi)*dy + cos(ray.theta)*dz; - ray.dtheta = (cos(ray.theta)*cos(ray.phi)*dx + cos(ray.theta)*sin(ray.phi)*dy - sin(ray.theta)*dz) / ray.r; - ray.dphi = (-sin(ray.phi)*dx + cos(ray.phi)*dy) / (ray.r * sin(ray.theta)); - - // Calculate conserved quantities (energy E and angular momentum L) - ray.L = ray.r * ray.r * sin(ray.theta) * ray.dphi; - float f = 1.0 - SagA_rs / ray.r; - float dt_dL = sqrt((ray.dr*ray.dr)/f + ray.r*ray.r*(ray.dtheta*ray.dtheta + sin(ray.theta)*sin(ray.theta)*ray.dphi*ray.dphi)); - ray.E = f * dt_dL; - - return ray; -} -``` - -## 2. Geodesic Integration - -Once the ray is initialized, we trace its path through curved spacetime using the Schwarzschild metric. This requires solving the geodesic equations for the ray. - -### Integration Loop - -The ray is advanced step by step using a numerical integrator (Runge-Kutta 4th order). The loop continues until the ray either escapes the scene, falls into the black hole, or intersects an object. - -```glsl -for (int i = 0; i < maxSteps; ++i) -{ - if (ray.r > exitDistance) break; - if (ray.r > ESCAPE_R) break; - if (Intercept(ray, SagA_rs)) { hitBlackHole = true; break; } - - currentStepSize = CalculateAdaptiveStepSize(ray, D_LAMBDA); - RK4Step(ray, currentStepSize); - lambda += currentStepSize; - - vec3 newPos = vec3(ray.x, ray.y, ray.z); - if (CrossesEquatorialPlane(prevPos, newPos)) { hitDisk = true; break; } - if (i % objectCheckInterval == 0 && InterceptObject(ray)) { hitObject = true; break; } - - prevPos = newPos; -} -``` - -### Runge-Kutta 4 (RK4) - -The RK4 integrator provides high accuracy by computing intermediate slopes and averaging them to advance the ray: - -```glsl -void RK4Step(inout Ray ray, float dL) -{ - vec3 k1a, k1b; - GeodesicRHS(ray, k1a, k1b); - - ray.r += dL * k1a.x; - ray.theta += dL * k1a.y; - ray.phi += dL * k1a.z; - ray.dr += dL * k1b.x; - ray.dtheta += dL * k1b.y; - ray.dphi += dL * k1b.z; - - // Update Cartesian coordinates - ray.x = ray.r * sin(ray.theta) * cos(ray.phi); - ray.y = ray.r * sin(ray.theta) * sin(ray.phi); - ray.z = ray.r * cos(ray.theta); -} -``` - -### Geodesic Derivatives - -The function `GeodesicRHS` computes the derivatives needed for integration: - -```glsl -void GeodesicRHS(Ray ray, out vec3 d1, out vec3 d2) -{ - float r = ray.r; - float theta = ray.theta; - float dr = ray.dr; - float dtheta = ray.dtheta; - float dphi = ray.dphi; - float f = 1.0 - SagA_rs / r; - float dt_dL = ray.E / f; - - d1 = vec3(dr, dtheta, dphi); - - d2.x = - (SagA_rs / (2.0 * r*r)) * f * dt_dL * dt_dL - + (SagA_rs / (2.0 * r*r * f)) * dr * dr - + r * (dtheta*dtheta + sin(theta)*sin(theta)*dphi*dphi); - d2.y = -2.0*dr*dtheta/r + sin(theta)*cos(theta)*dphi*dphi; - d2.z = -2.0*dr*dphi/r - 2.0*cos(theta)/(sin(theta)) * dtheta * dphi; -} -``` - -## 3. Intersection Testing - -Rays can intersect three types of entities: the black hole, spherical objects, and the accretion disk. - -### Black Hole Intersection - -A ray hitting the event horizon is considered absorbed: - -```glsl -bool Intercept(Ray ray, float rs) -{ - return ray.r <= rs; -} -``` - -### Object Intersection - -The scene can contain multiple spherical objects such as stars or planets: - -```glsl -bool InterceptObject(Ray ray) -{ - vec3 P = vec3(ray.x, ray.y, ray.z); - - for (int i = 0; i < numObjects; ++i) - { - vec3 center = objPosRadius[i].xyz; - float radius = objPosRadius[i].w; - - float distSq = dot(P - center, P - center); - if (distSq > radius * radius * 4.0) continue; - if (distSq <= radius * radius) - { - objectColor = objColor[i]; - hitCenter = center; - hitRadius = radius; - return true; - } - } - return false; -} -``` - -### Accretion Disk Intersection - -The accretion disk lies in the equatorial plane and is checked by detecting if the ray crosses this plane: - -```glsl -bool CrossesEquatorialPlane(vec3 oldPos, vec3 newPos) -{ - bool crossed = (oldPos.y * newPos.y <0.0); - if (crossed) { diskIntersection = newPos; } - return crossed; -} -``` - -## 4. Shading and Color Computation - -Once an intersection is found, we compute the color of the pixel based on the object hit and relativistic effects such as gravitational redshift and Doppler shift. - -```glsl -vec3 ComputeColor(Ray ray) -{ - if (hitBlackHole) return vec3(0.0); // Black hole is black - if (hitDisk) return SampleDiskTexture(diskIntersection); - if (hitObject) return ApplyLighting(ray, objectColor, hitCenter, hitRadius); - - return SampleBackground(ray); // Background stars, etc. -} -```` - -This ensures that each pixel reflects both the geometrical position and relativistic effects along the ray. - -## 5. Rendering Loop - -Finally, the main rendering loop iterates over every pixel on the screen, traces a ray, and stores the computed color: - -```glsl -for (int y = 0; y < HEIGHT; ++y) -{ - for (int x = 0; x < WIDTH; ++x) - { - Ray ray = InitRayForPixel(x, y); - TraceRay(ray); - vec3 color = ComputeColor(ray); - framebuffer[y*WIDTH + x] = vec4(color, 1.0); - } -} -``` \ No newline at end of file diff --git a/premake5.lua b/premake5.lua index 8729d3f..88eb729 100644 --- a/premake5.lua +++ b/premake5.lua @@ -369,6 +369,13 @@ project "Donut" "src/**.mm" } + -- Compile the Slang shaders to Assets/Shaders/generated/ before building. + -- Requires Tools/slang (Tools/fetch-slang.sh) and spirv-cross on PATH. + prebuildcommands + { + 'bash "%{wks.location}/Tools/compile-shaders.sh"' + } + links { "Cocoa.framework", diff --git a/src/Platform/OpenGL/OpenGLShader.cpp b/src/Platform/OpenGL/OpenGLShader.cpp index 2992cdc..5d6dd66 100644 --- a/src/Platform/OpenGL/OpenGLShader.cpp +++ b/src/Platform/OpenGL/OpenGLShader.cpp @@ -19,9 +19,30 @@ namespace Donut return 0; } + // Shaders are authored in Slang and compiled to Assets/Shaders/generated/ + // .glsl by Tools/compile-shaders.sh. Given a legacy ".../.glsl" + // path, prefer that generated file when present; otherwise fall back to the + // hand-written GLSL (e.g. shaders not yet ported to Slang). + static std::string ResolveShaderPath(const std::string& filepath) + { + size_t slash = filepath.find_last_of("/\\"); + std::string dir = (slash == std::string::npos) ? std::string() : filepath.substr(0, slash + 1); + std::string file = (slash == std::string::npos) ? filepath : filepath.substr(slash + 1); + size_t dot = file.rfind('.'); + std::string base = (dot == std::string::npos) ? file : file.substr(0, dot); + + std::string generated = dir + "generated/" + base + ".glsl"; + std::ifstream test(generated); + if (test.good()) + return generated; + return filepath; + } + OpenGLShader::OpenGLShader(const std::string& filepath) { - std::string source = ReadFile(filepath); + std::string resolved = ResolveShaderPath(filepath); + m_IsSlang = (resolved != filepath); + std::string source = ReadFile(resolved); auto shaderSources = PreProcess(source); Compile(shaderSources); @@ -33,7 +54,7 @@ namespace Donut } OpenGLShader::OpenGLShader(const std::string& name, const std::string& vertexSrc, const std::string& fragmentSrc) - : m_Name(name) + : m_Name(name) { std::unordered_map sources; sources[GL_VERTEX_SHADER] = vertexSrc; @@ -42,7 +63,7 @@ namespace Donut } OpenGLShader::OpenGLShader(const std::string& name, const std::string& computeSrc) - : m_Name(name) + : m_Name(name) { std::unordered_map sources; sources[GL_COMPUTE_SHADER] = computeSrc; @@ -57,9 +78,9 @@ namespace Donut std::string OpenGLShader::ReadFile(const std::string& filepath) { std::string result; - std::ifstream in(filepath, std::ios::in | + std::ifstream in(filepath, std::ios::in | std::ios::binary); - + if (in) { in.seekg(0, std::ios::end); @@ -90,7 +111,7 @@ namespace Donut size_t nextLinePos = source.find_first_not_of("\r\n", eol); pos = source.find(typeToken, nextLinePos); - shaderSources[ShaderTypeFromString(type)] = (pos == std::string::npos) ? source.substr(nextLinePos) : + shaderSources[ShaderTypeFromString(type)] = (pos == std::string::npos) ? source.substr(nextLinePos) : source.substr(nextLinePos, pos - nextLinePos); } @@ -225,34 +246,34 @@ namespace Donut glUniform1f(location, value); } - void OpenGLShader::UploadUniformFloat2(const std::string& name, const glm::vec2& value) + void OpenGLShader::UploadUniformFloat2(const std::string& name, const glm::vec2& value) { int location = glGetUniformLocation(m_RendererID, name.c_str()); glUniform2f(location, value.x, value.y); } - void OpenGLShader::UploadUniformFloat3(const std::string& name, const glm::vec3& value) + void OpenGLShader::UploadUniformFloat3(const std::string& name, const glm::vec3& value) { int location = glGetUniformLocation(m_RendererID, name.c_str()); glUniform3f(location, value.x, value.y, value.z); } - void OpenGLShader::UploadUniformFloat4(const std::string& name, const glm::vec4& value) + void OpenGLShader::UploadUniformFloat4(const std::string& name, const glm::vec4& value) { int location = glGetUniformLocation(m_RendererID, name.c_str()); glUniform4f(location, value.x, value.y, value.z, value.w); } - void OpenGLShader::UploadUniformMat3(const std::string& name, const glm::mat3& matrix) + void OpenGLShader::UploadUniformMat3(const std::string& name, const glm::mat3& matrix) { int location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniformMatrix3fv(location, 1, GL_FALSE, glm::value_ptr(matrix)); + glUniformMatrix3fv(location, 1, m_IsSlang ? GL_TRUE : GL_FALSE, glm::value_ptr(matrix)); } - void OpenGLShader::UploadUniformMat4(const std::string& name, const glm::mat4& matrix) + void OpenGLShader::UploadUniformMat4(const std::string& name, const glm::mat4& matrix) { int location = glGetUniformLocation(m_RendererID, name.c_str()); - glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(matrix)); + glUniformMatrix4fv(location, 1, m_IsSlang ? GL_TRUE : GL_FALSE, glm::value_ptr(matrix)); } void OpenGLShader::Dispatch(uint32_t x, uint32_t y, uint32_t z) diff --git a/src/Platform/OpenGL/OpenGLShader.h b/src/Platform/OpenGL/OpenGLShader.h index 72d1d6e..4b63597 100644 --- a/src/Platform/OpenGL/OpenGLShader.h +++ b/src/Platform/OpenGL/OpenGLShader.h @@ -50,5 +50,9 @@ namespace Donut private: uint32_t m_RendererID = 0; std::string m_Name; + // True when loaded from a Slang-compiled GLSL. Slang expects row-major + // matrix data, so matrix uniforms are transposed on upload (glm is + // column-major) to keep all matrix math correct. + bool m_IsSlang = false; }; }; diff --git a/src/Platform/OpenGL/OpenGLTexture.cpp b/src/Platform/OpenGL/OpenGLTexture.cpp index 1895575..16892c6 100644 --- a/src/Platform/OpenGL/OpenGLTexture.cpp +++ b/src/Platform/OpenGL/OpenGLTexture.cpp @@ -225,7 +225,9 @@ namespace Donut glUseProgram(shaderProgram); glUniform1i(glGetUniformLocation(shaderProgram, "u_EquirectangularMap"), 0); - glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "u_Projection"), 1, GL_FALSE, &captureProjection[0][0]); + // EquirectToCubemap is authored in Slang (row-major); transpose glm's + // column-major matrices on upload (GL_TRUE) to match. + glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "u_Projection"), 1, GL_TRUE, &captureProjection[0][0]); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, hdrTexture); @@ -233,7 +235,7 @@ namespace Donut glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); for (unsigned int i = 0; i < 6; ++i) { - glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "u_View"), 1, GL_FALSE, &captureViews[i][0][0]); + glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "u_View"), 1, GL_TRUE, &captureViews[i][0][0]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, m_RendererID, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glBindVertexArray(cubeVAO); -- cgit v1.3