|
| 1 | +//! Ported to Rust from <https://www.shadertoy.com/view/XtBXDz> |
| 2 | +//! |
| 3 | +//! Original comment: |
| 4 | +//! ```glsl |
| 5 | +//! // ---------------------------------------------------------------------------- |
| 6 | +//! // Rayleigh and Mie scattering atmosphere system |
| 7 | +//! // |
| 8 | +//! // implementation of the techniques described here: |
| 9 | +//! // http://www.scratchapixel.com/old/lessons/3d-advanced-lessons/simulating-the-colors-of-the-sky/atmospheric-scattering/ |
| 10 | +//! // ---------------------------------------------------------------------------- |
| 11 | +//! ``` |
| 12 | +
|
| 13 | +use shared::*; |
| 14 | +use spirv_std::glam::{const_vec3, vec2, vec3, Mat3, Vec2, Vec3, Vec3Swizzles, Vec4}; |
| 15 | + |
| 16 | +// Note: This cfg is incorrect on its surface, it really should be "are we compiling with std", but |
| 17 | +// we tie #[no_std] above to the same condition, so it's fine. |
| 18 | +#[cfg(target_arch = "spirv")] |
| 19 | +use spirv_std::num_traits::Float; |
| 20 | + |
| 21 | +pub struct Inputs { |
| 22 | + pub resolution: Vec3, |
| 23 | + pub time: f32, |
| 24 | + pub mouse: Vec4, |
| 25 | +} |
| 26 | + |
| 27 | +pub struct State { |
| 28 | + inputs: Inputs, |
| 29 | + sun_dir: Vec3, |
| 30 | +} |
| 31 | + |
| 32 | +impl State { |
| 33 | + pub fn new(inputs: Inputs) -> Self { |
| 34 | + State { |
| 35 | + inputs, |
| 36 | + sun_dir: vec3(0.0, 1.0, 0.0), |
| 37 | + } |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +const PI: f32 = 3.14159265359; |
| 42 | + |
| 43 | +#[derive(Copy, Clone)] |
| 44 | +struct Ray { |
| 45 | + origin: Vec3, |
| 46 | + direction: Vec3, |
| 47 | +} |
| 48 | +const _BIAS: f32 = 1e-4; // small offset to avoid self-intersections |
| 49 | + |
| 50 | +struct Sphere { |
| 51 | + origin: Vec3, |
| 52 | + radius: f32, |
| 53 | + _material: i32, |
| 54 | +} |
| 55 | + |
| 56 | +struct _Plane { |
| 57 | + direction: Vec3, |
| 58 | + distance: f32, |
| 59 | + material: i32, |
| 60 | +} |
| 61 | + |
| 62 | +fn rotate_around_x(angle_degrees: f32) -> Mat3 { |
| 63 | + let angle: f32 = angle_degrees.deg_to_radians(); |
| 64 | + let _sin: f32 = angle.sin(); |
| 65 | + let _cos: f32 = angle.cos(); |
| 66 | + Mat3::from_cols_array(&[1.0, 0.0, 0.0, 0.0, _cos, -_sin, 0.0, _sin, _cos]) |
| 67 | +} |
| 68 | + |
| 69 | +fn get_primary_ray(cam_local_point: Vec3, cam_origin: &mut Vec3, cam_look_at: &mut Vec3) -> Ray { |
| 70 | + let fwd: Vec3 = (*cam_look_at - *cam_origin).normalize(); |
| 71 | + let mut up: Vec3 = vec3(0.0, 1.0, 0.0); |
| 72 | + let right: Vec3 = up.cross(fwd); |
| 73 | + up = fwd.cross(right); |
| 74 | + |
| 75 | + Ray { |
| 76 | + origin: *cam_origin, |
| 77 | + direction: (fwd + up * cam_local_point.y + right * cam_local_point.x).normalize(), |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +fn isect_sphere(ray: Ray, sphere: Sphere, t0: &mut f32, t1: &mut f32) -> bool { |
| 82 | + let rc: Vec3 = sphere.origin - ray.origin; |
| 83 | + let radius2: f32 = sphere.radius * sphere.radius; |
| 84 | + let tca: f32 = rc.dot(ray.direction); |
| 85 | + let d2: f32 = rc.dot(rc) - tca * tca; |
| 86 | + if d2 > radius2 { |
| 87 | + return false; |
| 88 | + } |
| 89 | + let thc: f32 = (radius2 - d2).sqrt(); |
| 90 | + *t0 = tca - thc; |
| 91 | + *t1 = tca + thc; |
| 92 | + true |
| 93 | +} |
| 94 | + |
| 95 | +// scattering coefficients at sea level (m) |
| 96 | +const BETA_R: Vec3 = const_vec3!([5.5e-6, 13.0e-6, 22.4e-6]); // Rayleigh |
| 97 | +const BETA_M: Vec3 = const_vec3!([21e-6, 21e-6, 21e-6]); // Mie |
| 98 | + |
| 99 | +// scale height (m) |
| 100 | +// thickness of the atmosphere if its density were uniform |
| 101 | +const H_R: f32 = 7994.0; // Rayleigh |
| 102 | +const H_M: f32 = 1200.0; // Mie |
| 103 | + |
| 104 | +fn rayleigh_phase_func(mu: f32) -> f32 { |
| 105 | + 3.0 * (1.0 + mu*mu) |
| 106 | + / //------------------------ |
| 107 | + (16.0 * PI) |
| 108 | +} |
| 109 | + |
| 110 | +// Henyey-Greenstein phase function factor [-1, 1] |
| 111 | +// represents the average cosine of the scattered directions |
| 112 | +// 0 is isotropic scattering |
| 113 | +// > 1 is forward scattering, < 1 is backwards |
| 114 | +const G: f32 = 0.76; |
| 115 | +fn henyey_greenstein_phase_func(mu: f32) -> f32 { |
| 116 | + (1. - G*G) |
| 117 | + / //--------------------------------------------- |
| 118 | + ((4. * PI) * (1. + G*G - 2.*G*mu).powf(1.5)) |
| 119 | +} |
| 120 | + |
| 121 | +// Schlick Phase Function factor |
| 122 | +// Pharr and Humphreys [2004] equivalence to g above |
| 123 | +const K: f32 = 1.55 * G - 0.55 * (G * G * G); |
| 124 | +fn schlick_phase_func(mu: f32) -> f32 { |
| 125 | + (1. - K*K) |
| 126 | + / //------------------------------------------- |
| 127 | + (4. * PI * (1. + K*mu) * (1. + K*mu)) |
| 128 | +} |
| 129 | + |
| 130 | +const EARTH_RADIUS: f32 = 6360e3; // (m) |
| 131 | +const ATMOSPHERE_RADIUS: f32 = 6420e3; // (m) |
| 132 | + |
| 133 | +const SUN_POWER: f32 = 20.0; |
| 134 | + |
| 135 | +const ATMOSPHERE: Sphere = Sphere { |
| 136 | + origin: Vec3::zero(), |
| 137 | + radius: ATMOSPHERE_RADIUS, |
| 138 | + _material: 0, |
| 139 | +}; |
| 140 | + |
| 141 | +const NUM_SAMPLES: i32 = 16; |
| 142 | +const NUM_SAMPLES_LIGHT: i32 = 8; |
| 143 | + |
| 144 | +fn get_sun_light(ray: Ray, optical_depth_r: &mut f32, optical_depth_m: &mut f32) -> bool { |
| 145 | + let mut t0: f32 = 0.0; |
| 146 | + let mut t1: f32 = 0.0; |
| 147 | + isect_sphere(ray, ATMOSPHERE, &mut t0, &mut t1); |
| 148 | + |
| 149 | + let mut march_pos: f32 = 0.0; |
| 150 | + let march_step: f32 = t1 / NUM_SAMPLES_LIGHT as f32; |
| 151 | + |
| 152 | + let mut i = 0; |
| 153 | + while i < NUM_SAMPLES_LIGHT { |
| 154 | + let s: Vec3 = ray.origin + ray.direction * (march_pos + 0.5 * march_step); |
| 155 | + let height: f32 = s.length() - EARTH_RADIUS; |
| 156 | + if height < 0.0 { |
| 157 | + return false; |
| 158 | + } |
| 159 | + |
| 160 | + *optical_depth_r += (-height / H_R).exp() * march_step; |
| 161 | + *optical_depth_m += (-height / H_M).exp() * march_step; |
| 162 | + |
| 163 | + march_pos += march_step; |
| 164 | + i += 1; |
| 165 | + } |
| 166 | + true |
| 167 | +} |
| 168 | + |
| 169 | +impl State { |
| 170 | + fn get_incident_light(&self, ray: Ray) -> Vec3 { |
| 171 | + // "pierce" the atmosphere with the viewing ray |
| 172 | + let mut t0: f32 = 0.0; |
| 173 | + let mut t1: f32 = 0.0; |
| 174 | + if !isect_sphere(ray, ATMOSPHERE, &mut t0, &mut t1) { |
| 175 | + return Vec3::zero(); |
| 176 | + } |
| 177 | + |
| 178 | + let march_step: f32 = t1 / NUM_SAMPLES as f32; |
| 179 | + |
| 180 | + // cosine of angle between view and light directions |
| 181 | + let mu: f32 = ray.direction.dot(self.sun_dir); |
| 182 | + |
| 183 | + // Rayleigh and Mie phase functions |
| 184 | + // A black box indicating how light is interacting with the material |
| 185 | + // Similar to BRDF except |
| 186 | + // * it usually considers a single angle |
| 187 | + // (the phase angle between 2 directions) |
| 188 | + // * integrates to 1 over the entire sphere of directions |
| 189 | + let phase_r: f32 = rayleigh_phase_func(mu); |
| 190 | + let phase_m: f32 = if true { |
| 191 | + henyey_greenstein_phase_func(mu) |
| 192 | + } else { |
| 193 | + schlick_phase_func(mu) |
| 194 | + }; |
| 195 | + |
| 196 | + // optical depth (or "average density") |
| 197 | + // represents the accumulated extinction coefficients |
| 198 | + // along the path, multiplied by the length of that path |
| 199 | + let mut optical_depth_r: f32 = 0.0; |
| 200 | + let mut optical_depth_m: f32 = 0.0; |
| 201 | + |
| 202 | + let mut sum_r: Vec3 = Vec3::zero(); |
| 203 | + let mut sum_m: Vec3 = Vec3::zero(); |
| 204 | + let mut march_pos: f32 = 0.0; |
| 205 | + |
| 206 | + let mut i = 0; |
| 207 | + while i < NUM_SAMPLES { |
| 208 | + let s: Vec3 = ray.origin + ray.direction * (march_pos + 0.5 * march_step); |
| 209 | + let height: f32 = s.length() - EARTH_RADIUS; |
| 210 | + |
| 211 | + // integrate the height scale |
| 212 | + let hr: f32 = (-height / H_R).exp() * march_step; |
| 213 | + let hm: f32 = (-height / H_M).exp() * march_step; |
| 214 | + optical_depth_r += hr; |
| 215 | + optical_depth_m += hm; |
| 216 | + |
| 217 | + // gather the sunlight |
| 218 | + let light_ray: Ray = Ray { |
| 219 | + origin: s, |
| 220 | + direction: self.sun_dir, |
| 221 | + }; |
| 222 | + let mut optical_depth_light_r: f32 = 0.0; |
| 223 | + let mut optical_depth_light_m: f32 = 0.0; |
| 224 | + let overground: bool = get_sun_light( |
| 225 | + light_ray, |
| 226 | + &mut optical_depth_light_r, |
| 227 | + &mut optical_depth_light_m, |
| 228 | + ); |
| 229 | + |
| 230 | + if overground { |
| 231 | + let tau: Vec3 = BETA_R * (optical_depth_r + optical_depth_light_r) |
| 232 | + + BETA_M * 1.1 * (optical_depth_m + optical_depth_light_m); |
| 233 | + let attenuation: Vec3 = exp(-tau); |
| 234 | + |
| 235 | + sum_r += hr * attenuation; |
| 236 | + sum_m += hm * attenuation; |
| 237 | + } |
| 238 | + |
| 239 | + march_pos += march_step; |
| 240 | + i += 1; |
| 241 | + } |
| 242 | + |
| 243 | + SUN_POWER * (sum_r * phase_r * BETA_R + sum_m * phase_m * BETA_M) |
| 244 | + } |
| 245 | + |
| 246 | + pub fn main_image(&mut self, frag_color: &mut Vec4, frag_coord: Vec2) { |
| 247 | + let aspect_ratio: Vec2 = vec2(self.inputs.resolution.x / self.inputs.resolution.y, 1.0); |
| 248 | + let fov: f32 = 45.0.deg_to_radians().tan(); |
| 249 | + let point_ndc: Vec2 = frag_coord / self.inputs.resolution.xy(); |
| 250 | + let point_cam: Vec3 = ((2.0 * point_ndc - Vec2::one()) * aspect_ratio * fov).extend(-1.0); |
| 251 | + |
| 252 | + let col: Vec3; |
| 253 | + |
| 254 | + // sun |
| 255 | + let rot: Mat3 = rotate_around_x(-(self.inputs.time / 2.0).sin().abs() * 90.0); |
| 256 | + self.sun_dir = rot.transpose() * self.sun_dir; |
| 257 | + |
| 258 | + if self.inputs.mouse.z < 0.1 { |
| 259 | + // sky dome angles |
| 260 | + let p: Vec3 = point_cam; |
| 261 | + let z2: f32 = p.x * p.x + p.y * p.y; |
| 262 | + let phi: f32 = p.y.atan2(p.x); |
| 263 | + let theta: f32 = (1.0 - z2).acos(); |
| 264 | + let dir: Vec3 = vec3( |
| 265 | + theta.sin() * phi.cos(), |
| 266 | + theta.cos(), |
| 267 | + theta.sin() * phi.sin(), |
| 268 | + ); |
| 269 | + |
| 270 | + let ray: Ray = Ray { |
| 271 | + origin: vec3(0.0, EARTH_RADIUS + 1.0, 0.0), |
| 272 | + direction: dir, |
| 273 | + }; |
| 274 | + |
| 275 | + col = self.get_incident_light(ray); |
| 276 | + } else { |
| 277 | + let mut eye: Vec3 = vec3(0.0, EARTH_RADIUS + 1.0, 0.0); |
| 278 | + let mut look_at: Vec3 = vec3(0.0, EARTH_RADIUS + 1.5, -1.0); |
| 279 | + |
| 280 | + let ray: Ray = get_primary_ray(point_cam, &mut eye, &mut look_at); |
| 281 | + |
| 282 | + if ray.direction.dot(vec3(0.0, 1.0, 0.0)) > 0.0 { |
| 283 | + col = self.get_incident_light(ray); |
| 284 | + } else { |
| 285 | + col = Vec3::splat(0.333); |
| 286 | + } |
| 287 | + } |
| 288 | + |
| 289 | + *frag_color = col.extend(1.0); |
| 290 | + } |
| 291 | +} |
0 commit comments