diff --git a/assets/shaders/black_hole.wgsl b/assets/shaders/black_hole.wgsl index 51630ea..66c456e 100644 --- a/assets/shaders/black_hole.wgsl +++ b/assets/shaders/black_hole.wgsl @@ -1,5 +1,6 @@ #import bevy_sprite::mesh2d_vertex_output::VertexOutput #import "shaders/ray_gen.wgsl" +#import "shaders/geodesic_schwarzschild.wgsl" struct BlackHoleUniforms { eye: vec4, @@ -31,11 +32,17 @@ struct BlackHoleUniforms { @fragment fn fragment(in: VertexOutput) -> @location(0) vec4 { - // in.uv is [0,1] across the quad. Center and flip y, apply aspect. let aspect = uniforms.resolution.x / uniforms.resolution.y; var uv = (in.uv * 2.0 - 1.0); uv.x *= aspect; let dir = ray_direction(uv); - // Visualize ray direction as a color (sanity check). - return vec4(abs(dir), 1.0); + // Step size scales with how far we need to travel (~ distance/ steps). + let dt = max(length(uniforms.eye.xyz), 20.0) / f32(uniforms.steps); + let res = classify_ray(uniforms.eye.xyz, dir, uniforms.steps, dt); + if (res.status == 1u) { + // Captured = shadow. + return vec4(0.0, 0.0, 0.0, 1.0); + } + // Escaped: dim grey for now (stars come in Task 11). + return vec4(0.02, 0.02, 0.02, 1.0); } diff --git a/assets/shaders/geodesic_schwarzschild.wgsl b/assets/shaders/geodesic_schwarzschild.wgsl new file mode 100644 index 0000000..a4d65ab --- /dev/null +++ b/assets/shaders/geodesic_schwarzschild.wgsl @@ -0,0 +1,55 @@ +const R_ESCAPE: f32 = 1000.0; + +// One RK4 sub-step derivative of (pos, dir) under the Schwarzschild +// bending acceleration. Rs is uniforms.rs. +fn deriv(pos: vec3, dir: vec3) -> (vec3, vec3) { + let r = length(pos); + let rs = uniforms.rs; + // Angular momentum squared: |cross(pos, dir)|^2 + let h = cross(pos, dir); + let h2 = dot(h, h); + // Avoid division by zero. + let r5 = max(r * r * r * r * r, 1e-6); + // d(pos)/dt = dir + let dpos = dir; + // d(dir)/dt = bending acceleration (re-normalized each step in integrate) + let accel = -1.5 * rs * h2 / r5 * pos; + return (dpos, accel); +} + +// Integrate a ray from `pos` along `dir`. Returns: +// .status: 0 = escaped, 1 = captured (shadow) +// .final_pos, .final_dir: end state (used for sky sampling on escape) +struct RayResult { + status: u32, + final_pos: vec3, + final_dir: vec3, +} + +// Accumulator callback pattern: the caller passes a function-style body via +// a per-step check. Because WGSL has no first-class closures, we inline the +// per-step intersection tests in black_hole.wgsl's integrate_and_trace(). +// This function returns ONLY the escape/capture classification, used as a +// fallback when no scene object is hit. +fn classify_ray(start_pos: vec3, start_dir: vec3, steps: u32, dt: f32) -> RayResult { + var pos = start_pos; + var dir = start_dir; + for (var i: u32 = 0u; i < steps; i = i + 1u) { + let r = length(pos); + if (r < uniforms.rs) { + return RayResult(1u, pos, dir); + } + if (r > R_ESCAPE) { + return RayResult(0u, pos, dir); + } + // RK4 + let (k1p, k1d) = deriv(pos, dir); + let (k2p, k2d) = deriv(pos + k1p * dt * 0.5, normalize(dir + k1d * dt * 0.5)); + let (k3p, k3d) = deriv(pos + k2p * dt * 0.5, normalize(dir + k2d * dt * 0.5)); + let (k4p, k4d) = deriv(pos + k3p * dt, normalize(dir + k3d * dt)); + pos = pos + (k1p + 2.0 * k2p + 2.0 * k3p + k4p) * dt / 6.0; + dir = normalize(dir + (k1d + 2.0 * k2d + 2.0 * k3d + k4d) * dt / 6.0); + } + // Ran out of steps without a clear verdict: treat as escaped. + return RayResult(0u, pos, dir); +}