1// Copyright (c) 2023 Tomasz Stachowiak2//3// This contribution is dual licensed under EITHER OF4//5// Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0)6// MIT license (http://opensource.org/licenses/MIT)7//8// at your option.9//10// This is a port of the original [`raymarch.hlsl`] to WGSL. It's deliberately11// kept as close as possible so that patches to the original `raymarch.hlsl`12// have the greatest chances of applying to this version.13//14// [`raymarch.hlsl`]:15// https://gist.github.com/h3r2tic/9c8356bdaefbe80b1a22ae0aaee192db17#define_import_path bevy_pbr::raymarch19#import bevy_pbr::mesh_view_bindings::depth_prepass_texture20#import bevy_pbr::view_transformations::{21 direction_world_to_clip,22 ndc_to_uv,23 perspective_camera_near,24 position_world_to_ndc,25}27#ifdef USE_DEPTH_SAMPLERS28// Allows us to sample from the depth buffer with bilinear filtering.29@group(2) @binding(2) var depth_linear_sampler: sampler;31// Allows us to sample from the depth buffer with nearest-neighbor filtering.32@group(2) @binding(3) var depth_nearest_sampler: sampler;33#endif35// Manual depth fetch helpers used on WebGPU where depth + filtering sampler is invalid.36#ifndef USE_DEPTH_SAMPLERS37fn depth_texel_clamped(texel: vec2<i32>) -> f32 {38 let dims = textureDimensions(depth_prepass_texture);39 let max_coord = vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1);40 let clamped = clamp(texel, vec2<i32>(0), max_coord);41 return textureLoad(depth_prepass_texture, clamped, 0);42}44fn depth_sample_nearest_clamped(uv: vec2<f32>, tex_size: vec2<f32>) -> f32 {45 // Match nearest sampling by snapping to the closest texel center.46 let coord = uv * tex_size - vec2(0.5);47 return depth_texel_clamped(vec2<i32>(floor(coord + vec2(0.5))));48}50fn depth_sample_bilinear_clamped(uv: vec2<f32>, tex_size: vec2<f32>) -> f32 {51 let coord = uv * tex_size - vec2(0.5);52 let base = vec2<i32>(floor(coord));53 let frac = coord - floor(coord);55 let d00 = depth_texel_clamped(base);56 let d10 = depth_texel_clamped(base + vec2(1, 0));57 let d01 = depth_texel_clamped(base + vec2(0, 1));58 let d11 = depth_texel_clamped(base + vec2(1, 1));60 let d0 = mix(d00, d10, frac.x);61 let d1 = mix(d01, d11, frac.x);62 return mix(d0, d1, frac.y);63}64#endif66fn depth_sample_linear(uv: vec2<f32>, tex_size: vec2<f32>) -> f32 {67#ifdef USE_DEPTH_SAMPLERS68 return textureSampleLevel(depth_prepass_texture, depth_linear_sampler, uv, 0u);69#else70 return depth_sample_bilinear_clamped(uv, tex_size);71#endif72}74fn depth_sample_nearest(uv: vec2<f32>, tex_size: vec2<f32>) -> f32 {75#ifdef USE_DEPTH_SAMPLERS76 return textureSampleLevel(depth_prepass_texture, depth_nearest_sampler, uv, 0u);77#else78 return depth_sample_nearest_clamped(uv, tex_size);79#endif80}82// Main code84struct HybridRootFinder {85 linear_steps: u32,86 bisection_steps: u32,87 use_secant: bool,88 linear_march_exponent: f32,90 jitter: f32,91 min_t: f32,92 max_t: f32,93}95fn hybrid_root_finder_new_with_linear_steps(v: u32) -> HybridRootFinder {96 var res: HybridRootFinder;97 res.linear_steps = v;98 res.bisection_steps = 0u;99 res.use_secant = false;100 res.linear_march_exponent = 1.0;101 res.jitter = 1.0;102 res.min_t = 0.0;103 res.max_t = 1.0;104 return res;105}107fn hybrid_root_finder_find_root(108 root_finder: ptr<function, HybridRootFinder>,109 start: vec3<f32>,110 end: vec3<f32>,111 distance_fn: ptr<function, DepthRaymarchDistanceFn>,112 hit_t: ptr<function, f32>,113 miss_t: ptr<function, f32>,114 hit_d: ptr<function, DistanceWithPenetration>,115) -> bool {116 let dir = end - start;118 var min_t = (*root_finder).min_t;119 var max_t = (*root_finder).max_t;121 var min_d = DistanceWithPenetration(0.0, false, 0.0);122 var max_d = DistanceWithPenetration(0.0, false, 0.0);124 let step_size = (max_t - min_t) / f32((*root_finder).linear_steps);126 var intersected = false;128 //129 // Ray march using linear steps131 if ((*root_finder).linear_steps > 0u) {132 let candidate_t = mix(133 min_t,134 max_t,135 pow(136 (*root_finder).jitter / f32((*root_finder).linear_steps),137 (*root_finder).linear_march_exponent138 )139 );141 let candidate = start + dir * candidate_t;142 let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate);143 intersected = candidate_d.distance < 0.0 && candidate_d.valid;145 if (intersected) {146 max_t = candidate_t;147 max_d = candidate_d;148 // The `[min_t .. max_t]` interval contains an intersection. End the linear search.149 } else {150 // No intersection yet. Carry on.151 min_t = candidate_t;152 min_d = candidate_d;154 for (var step = 1u; step < (*root_finder).linear_steps; step += 1u) {155 let candidate_t = mix(156 (*root_finder).min_t,157 (*root_finder).max_t,158 pow(159 (f32(step) + (*root_finder).jitter) / f32((*root_finder).linear_steps),160 (*root_finder).linear_march_exponent161 )162 );164 let candidate = start + dir * candidate_t;165 let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate);166 intersected = candidate_d.distance < 0.0 && candidate_d.valid;168 if (intersected) {169 max_t = candidate_t;170 max_d = candidate_d;171 // The `[min_t .. max_t]` interval contains an intersection.172 // End the linear search.173 break;174 } else {175 // No intersection yet. Carry on.176 min_t = candidate_t;177 min_d = candidate_d;178 }179 }180 }181 }183 *miss_t = min_t;184 *hit_t = min_t;186 //187 // Refine the hit using bisection189 if (intersected) {190 for (var step = 0u; step < (*root_finder).bisection_steps; step += 1u) {191 let mid_t = (min_t + max_t) * 0.5;192 let candidate = start + dir * mid_t;193 let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate);195 if (candidate_d.distance < 0.0 && candidate_d.valid) {196 // Intersection at the mid point. Refine the first half.197 max_t = mid_t;198 max_d = candidate_d;199 } else {200 // No intersection yet at the mid point. Refine the second half.201 min_t = mid_t;202 min_d = candidate_d;203 }204 }206 if ((*root_finder).use_secant) {207 // Finish with one application of the secant method208 let total_d = min_d.distance + -max_d.distance;210 let mid_t = mix(min_t, max_t, min_d.distance / total_d);211 let candidate = start + dir * mid_t;212 let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate);214 // Only accept the result of the secant method if it improves upon215 // the previous result.216 //217 // Technically root_finder should be `abs(candidate_d.distance) <218 // min(min_d.distance, -max_d.distance) * frac`, but root_finder seems219 // sufficient.220 if (abs(candidate_d.distance) < min_d.distance * 0.9 && candidate_d.valid) {221 *hit_t = mid_t;222 *hit_d = candidate_d;223 } else {224 *hit_t = max_t;225 *hit_d = max_d;226 }228 return true;229 } else {230 *hit_t = max_t;231 *hit_d = max_d;232 return true;233 }234 } else {235 // Mark the conservative miss distance.236 *hit_t = min_t;237 return false;238 }239}241struct DistanceWithPenetration {242 /// Distance to the surface of which a root we're trying to find243 distance: f32,245 /// Whether to consider this sample valid for intersection.246 /// Mostly relevant for allowing the ray marcher to travel behind surfaces,247 /// as it will mark surfaces it travels under as invalid.248 valid: bool,250 /// Conservative estimate of depth to which the ray penetrates the marched surface.251 penetration: f32,252}254struct DepthRaymarchDistanceFn {255 depth_tex_size: vec2<f32>,257 march_behind_surfaces: bool,258 depth_thickness: f32,260 use_sloppy_march: bool,261}263fn depth_raymarch_distance_fn_evaluate(264 distance_fn: ptr<function, DepthRaymarchDistanceFn>,265 ray_point_cs: vec3<f32>,266) -> DistanceWithPenetration {267 let interp_uv = ndc_to_uv(ray_point_cs.xy);269 let ray_depth = 1.0 / ray_point_cs.z;271 // We're using both point-sampled and bilinear-filtered values from the depth buffer.272 //273 // That's really stupid but works like magic. For samples taken near the ray origin,274 // the discrete nature of the depth buffer becomes a problem. It's not a land of continuous surfaces,275 // but a bunch of stacked duplo bricks.276 //277 // Technically we should be taking discrete steps in distance_fn duplo land, but then we're at the mercy278 // of arbitrary quantization of our directions -- and sometimes we'll take a step which would279 // claim that the ray is occluded -- even though the underlying smooth surface wouldn't occlude it.280 //281 // If we instead take linear taps from the depth buffer, we reconstruct the linear surface.282 // That fixes acne, but introduces false shadowing near object boundaries, as we now pretend283 // that everything is shrink-wrapped by distance_fn continuous 2.5D surface, and our depth thickness284 // heuristic ends up falling apart.285 //286 // The fix is to consider both the smooth and the discrete surfaces, and only claim occlusion287 // when the ray descends below both.288 //289 // The two approaches end up fixing each other's artifacts:290 // * The false occlusions due to duplo land are rejected because the ray stays above the smooth surface.291 // * The shrink-wrap surface is no longer continuous, so it's possible for rays to miss it.293 let linear_depth = 1.0 / depth_sample_linear(interp_uv, (*distance_fn).depth_tex_size);294 let unfiltered_depth = 1.0 / depth_sample_nearest(interp_uv, (*distance_fn).depth_tex_size);296 var max_depth: f32;297 var min_depth: f32;299 if ((*distance_fn).use_sloppy_march) {300 max_depth = unfiltered_depth;301 min_depth = unfiltered_depth;302 } else {303 max_depth = max(linear_depth, unfiltered_depth);304 min_depth = min(linear_depth, unfiltered_depth);305 }307 let bias = 0.000002;309 var res: DistanceWithPenetration;310 res.distance = max_depth * (1.0 + bias) - ray_depth;312 // distance_fn will be used at the end of the ray march to potentially discard the hit.313 res.penetration = ray_depth - min_depth;315 if ((*distance_fn).march_behind_surfaces) {316 res.valid = res.penetration < (*distance_fn).depth_thickness;317 } else {318 res.valid = true;319 }321 return res;322}324struct DepthRayMarchResult {325 /// True if the raymarch hit something.326 hit: bool,328 /// In case of a hit, the normalized distance to it.329 ///330 /// In case of a miss, the furthest the ray managed to travel, which could either be331 /// exceeding the max range, or getting behind a surface further than the depth thickness.332 ///333 /// Range: `0..=1` as a lerp factor over `ray_start_cs..=ray_end_cs`.334 hit_t: f32,336 /// UV corresponding to `hit_t`.337 hit_uv: vec2<f32>,339 /// The distance that the hit point penetrates into the hit surface.340 /// Will normally be non-zero due to limited precision of the ray march.341 ///342 /// In case of a miss: undefined.343 hit_penetration: f32,345 /// Ditto, within the range `0..DepthRayMarch::depth_thickness_linear_z`346 ///347 /// In case of a miss: undefined.348 hit_penetration_frac: f32,349}351struct DepthRayMarch {352 /// Number of steps to be taken at regular intervals to find an initial intersection.353 /// Must not be zero.354 linear_steps: u32,356 /// Exponent to be applied in the linear part of the march.357 ///358 /// A value of 1.0 will result in equidistant steps, and higher values will compress359 /// the earlier steps, and expand the later ones. This might be desirable in order360 /// to get more detail close to objects in SSR or SSGI.361 ///362 /// For optimal performance, this should be a small compile-time unsigned integer,363 /// such as 1 or 2.364 linear_march_exponent: f32,366 /// Number of steps in a bisection (binary search) to perform once the linear search367 /// has found an intersection. Helps narrow down the hit, increasing the chance of368 /// the secant method finding an accurate hit point.369 ///370 /// Useful when sampling color, e.g. SSR or SSGI, but pointless for contact shadows.371 bisection_steps: u32,373 /// Approximate the root position using the secant method -- by solving for line-line374 /// intersection between the ray approach rate and the surface gradient.375 ///376 /// Useful when sampling color, e.g. SSR or SSGI, but pointless for contact shadows.377 use_secant: bool,379 /// Jitter to apply to the first step of the linear search; 0..=1 range, mapping380 /// to the extent of a single linear step in the first phase of the search.381 /// Use 1.0 if you don't want jitter.382 jitter: f32,384 /// Clip space coordinates (w=1) of the ray.385 ray_start_cs: vec3<f32>,386 ray_end_cs: vec3<f32>,388 /// Should be used for contact shadows, but not for any color bounce, e.g. SSR.389 ///390 /// For SSR etc. this can easily create leaks, but with contact shadows it allows the rays391 /// to pass over invalid occlusions (due to thickness), and find potentially valid ones ahead.392 ///393 /// Note that this will cause the linear search to potentially miss surfaces,394 /// because when the ray overshoots and ends up penetrating a surface further than395 /// `depth_thickness_linear_z`, the ray marcher will just carry on.396 ///397 /// For this reason, this may require a lot of samples, or high depth thickness,398 /// so that `depth_thickness_linear_z >= world space ray length / linear_steps`.399 march_behind_surfaces: bool,401 /// If `true`, the ray marcher only performs nearest lookups of the depth buffer,402 /// resulting in aliasing and false occlusion when marching tiny detail.403 /// It should work fine for longer traces with fewer rays though.404 use_sloppy_march: bool,406 /// When marching the depth buffer, we only have 2.5D information, and don't know how407 /// thick surfaces are. We shall assume that the depth buffer fragments are little squares408 /// with a constant thickness defined by this parameter.409 depth_thickness_linear_z: f32,411 /// Size of the depth buffer we're marching in, in pixels.412 depth_tex_size: vec2<f32>,413}415fn depth_ray_march_new_from_depth(depth_tex_size: vec2<f32>) -> DepthRayMarch {416 var res: DepthRayMarch;417 res.jitter = 1.0;418 res.linear_steps = 4u;419 res.bisection_steps = 0u;420 res.linear_march_exponent = 1.0;421 res.depth_tex_size = depth_tex_size;422 res.depth_thickness_linear_z = 1.0;423 res.march_behind_surfaces = false;424 res.use_sloppy_march = false;425 return res;426}428fn depth_ray_march_to_cs_dir_impl(429 raymarch: ptr<function, DepthRayMarch>,430 dir_cs: vec4<f32>,431 infinite: bool,432) {433 var end_cs = vec4((*raymarch).ray_start_cs, 1.0) + dir_cs;435 // Perform perspective division, but avoid dividing by zero for rays436 // heading directly towards the eye.437 end_cs /= select(-1.0, 1.0, end_cs.w >= 0.0) * max(1e-10, abs(end_cs.w));439 // Clip ray start to the view frustum440 var delta_cs = end_cs.xyz - (*raymarch).ray_start_cs;441 let near_edge = select(vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0), delta_cs < vec3(0.0));442 let dist_to_near_edge = (near_edge - (*raymarch).ray_start_cs) / delta_cs;443 let max_dist_to_near_edge = max(dist_to_near_edge.x, dist_to_near_edge.y);444 (*raymarch).ray_start_cs += delta_cs * max(0.0, max_dist_to_near_edge);446 // Clip ray end to the view frustum448 delta_cs = end_cs.xyz - (*raymarch).ray_start_cs;449 let far_edge = select(vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0), delta_cs >= vec3(0.0));450 let dist_to_far_edge = (far_edge - (*raymarch).ray_start_cs) / delta_cs;451 let min_dist_to_far_edge = min(452 min(dist_to_far_edge.x, dist_to_far_edge.y),453 dist_to_far_edge.z454 );456 if (infinite) {457 delta_cs *= min_dist_to_far_edge;458 } else {459 // If unbounded, would make the ray reach the end of the frustum460 delta_cs *= min(1.0, min_dist_to_far_edge);461 }463 (*raymarch).ray_end_cs = (*raymarch).ray_start_cs + delta_cs;464}466/// March from a clip-space position (w = 1)467fn depth_ray_march_from_cs(raymarch: ptr<function, DepthRayMarch>, v: vec3<f32>) {468 (*raymarch).ray_start_cs = v;469}471/// March to a clip-space position (w = 1)472///473/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum.474fn depth_ray_march_to_cs(raymarch: ptr<function, DepthRayMarch>, end_cs: vec3<f32>) {475 let dir = vec4(end_cs - (*raymarch).ray_start_cs, 0.0) * sign(end_cs.z);476 depth_ray_march_to_cs_dir_impl(raymarch, dir, false);477}479/// March towards a clip-space direction. Infinite (ray is extended to cover the whole view frustum).480///481/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum.482fn depth_ray_march_to_cs_dir(raymarch: ptr<function, DepthRayMarch>, dir: vec4<f32>) {483 depth_ray_march_to_cs_dir_impl(raymarch, dir, true);484}486/// March to a world-space position.487///488/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum.489fn depth_ray_march_to_ws(raymarch: ptr<function, DepthRayMarch>, end: vec3<f32>) {490 depth_ray_march_to_cs(raymarch, position_world_to_ndc(end));491}493/// March towards a world-space direction. Infinite (ray is extended to cover the whole view frustum).494///495/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum.496fn depth_ray_march_to_ws_dir(raymarch: ptr<function, DepthRayMarch>, dir: vec3<f32>) {497 depth_ray_march_to_cs_dir_impl(raymarch, direction_world_to_clip(dir), true);498}500/// Perform the ray march.501fn depth_ray_march_march(raymarch: ptr<function, DepthRayMarch>) -> DepthRayMarchResult {502 var res = DepthRayMarchResult(false, 0.0, vec2(0.0), 0.0, 0.0);504 let ray_start_uv = ndc_to_uv((*raymarch).ray_start_cs.xy);505 let ray_end_uv = ndc_to_uv((*raymarch).ray_end_cs.xy);507 let ray_uv_delta = ray_end_uv - ray_start_uv;508 let ray_len_px = ray_uv_delta * (*raymarch).depth_tex_size;510 let min_px_per_step = 1u;511 let step_count = max(512 2,513 min(i32((*raymarch).linear_steps), i32(floor(length(ray_len_px) / f32(min_px_per_step))))514 );516 let linear_z_to_scaled_linear_z = 1.0 / perspective_camera_near();517 let depth_thickness = (*raymarch).depth_thickness_linear_z * linear_z_to_scaled_linear_z;519 var distance_fn: DepthRaymarchDistanceFn;520 distance_fn.depth_tex_size = (*raymarch).depth_tex_size;521 distance_fn.march_behind_surfaces = (*raymarch).march_behind_surfaces;522 distance_fn.depth_thickness = depth_thickness;523 distance_fn.use_sloppy_march = (*raymarch).use_sloppy_march;525 var hit: DistanceWithPenetration;527 var hit_t = 0.0;528 var miss_t = 0.0;529 var root_finder = hybrid_root_finder_new_with_linear_steps(u32(step_count));530 root_finder.bisection_steps = (*raymarch).bisection_steps;531 root_finder.use_secant = (*raymarch).use_secant;532 root_finder.linear_march_exponent = (*raymarch).linear_march_exponent;533 root_finder.jitter = (*raymarch).jitter;534 let intersected = hybrid_root_finder_find_root(535 &root_finder,536 (*raymarch).ray_start_cs,537 (*raymarch).ray_end_cs,538 &distance_fn,539 &hit_t,540 &miss_t,541 &hit542 );544 res.hit_t = hit_t;546 if (intersected && hit.penetration < depth_thickness && hit.distance < depth_thickness) {547 res.hit = true;548 res.hit_uv = mix(ray_start_uv, ray_end_uv, res.hit_t);549 res.hit_penetration = hit.penetration / linear_z_to_scaled_linear_z;550 res.hit_penetration_frac = hit.penetration / depth_thickness;551 return res;552 }554 res.hit_t = miss_t;555 res.hit_uv = mix(ray_start_uv, ray_end_uv, res.hit_t);557 return res;558}