diff --git a/crates/viewer/re_renderer/shader/depth_cloud.wgsl b/crates/viewer/re_renderer/shader/depth_cloud.wgsl index b2784acc7bb45..1504e2579aed3 100644 --- a/crates/viewer/re_renderer/shader/depth_cloud.wgsl +++ b/crates/viewer/re_renderer/shader/depth_cloud.wgsl @@ -182,9 +182,14 @@ fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOut { return out; } +fn coverage(world_position: vec3f, radius: f32, point_center: vec3f) -> f32 { + let sphere_intersection = ray_sphere_distance(camera_ray_to_world_pos(world_position), point_center, radius); + return sphere_quad_coverage(sphere_intersection); +} + @fragment fn fs_main(in: VertexOut) -> @location(0) vec4f { - let coverage = sphere_quad_coverage(in.pos_in_world, in.point_radius, in.point_pos_in_world); + let coverage = coverage(in.pos_in_world, in.point_radius, in.point_pos_in_world); if coverage < 0.001 { discard; } @@ -193,7 +198,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4f { @fragment fn fs_main_picking_layer(in: VertexOut) -> @location(0) vec4u { - let coverage = sphere_quad_coverage(in.pos_in_world, in.point_radius, in.point_pos_in_world); + let coverage = coverage(in.pos_in_world, in.point_radius, in.point_pos_in_world); if coverage <= 0.5 { discard; } @@ -205,7 +210,7 @@ fn fs_main_outline_mask(in: VertexOut) -> @location(0) vec2u { // Output is an integer target, can't use coverage therefore. // But we still want to discard fragments where coverage is low. // Since the outline extends a bit, a very low cut off tends to look better. - let coverage = sphere_quad_coverage(in.pos_in_world, in.point_radius, in.point_pos_in_world); + let coverage = coverage(in.pos_in_world, in.point_radius, in.point_pos_in_world); if coverage < 1.0 { discard; } diff --git a/crates/viewer/re_renderer/shader/point_cloud.wgsl b/crates/viewer/re_renderer/shader/point_cloud.wgsl index 3b7a66b9c9af5..8df83bb69b514 100644 --- a/crates/viewer/re_renderer/shader/point_cloud.wgsl +++ b/crates/viewer/re_renderer/shader/point_cloud.wgsl @@ -4,7 +4,6 @@ #import <./utils/flags.wgsl> #import <./utils/size.wgsl> #import <./utils/sphere_quad.wgsl> -#import <./utils/sphere_depth.wgsl> #import <./utils/depth_offset.wgsl> @group(1) @binding(0) @@ -65,10 +64,6 @@ struct VertexOut { @location(4) @interpolate(flat) picking_instance_id: vec2u, - - // Offset on the projected Z axis corresponding to the frontmost point on the sphere. - @location(5) @interpolate(flat) - sphere_radius_projected_depth: f32, }; struct FragmentOut { @@ -132,7 +127,6 @@ fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOut { out.world_position = quad.pos_in_world; out.point_center = point_data.pos; out.picking_instance_id = point_data.picking_instance_id; - out.sphere_radius_projected_depth = sphere_radius_projected_depth(point_data.pos, world_radius); return out; } @@ -150,28 +144,30 @@ fn circle_quad_coverage(world_position: vec3f, radius: f32, circle_center: vec3f return smoothstep(radius + feathering_radius, radius - feathering_radius, distance); } -fn coverage(world_position: vec3f, radius: f32, point_center: vec3f) -> f32 { +fn coverage_and_depth(projected_z: f32, world_position: vec3f, radius: f32, point_center: vec3f) -> vec2f { if is_camera_orthographic() || has_any_flag(batch.flags, FLAG_DRAW_AS_CIRCLES) { - return circle_quad_coverage(world_position, radius, point_center); + let coverage = circle_quad_coverage(world_position, radius, point_center); + let depth = projected_z; + return vec2f(coverage, depth); } else { - return sphere_quad_coverage(world_position, radius, point_center); + let camera_ray = camera_ray_to_world_pos(world_position); + let sphere_intersection = ray_sphere_distance(camera_ray_to_world_pos(world_position), point_center, radius); + let coverage = sphere_quad_coverage(sphere_intersection); + let sphere_position = camera_ray.origin + camera_ray.direction * sphere_intersection.distance_to_closest_hit_on_ray; + let sphere_position_projected = apply_depth_offset(frame.projection_from_world * vec4f(sphere_position, 1.0), batch.depth_offset); + let depth = sphere_position_projected.z / sphere_position_projected.w; + return vec2f(coverage, depth); } } @fragment fn fs_main(in: VertexOut) -> FragmentOut { - let coverage = coverage(in.world_position, in.radius, in.point_center); - if coverage < 0.001 { + let coverage_depth = coverage_and_depth(in.position.z, in.world_position, in.radius, in.point_center); + if coverage_depth.x < 0.001 { discard; } - let depth_offset = sphere_fragment_projected_depth( - in.radius, - in.sphere_radius_projected_depth, - in.world_position - in.point_center, - ); - // TODO(andreas): Proper shading // TODO(andreas): This doesn't even use the sphere's world position for shading, the world position used here is flat! var shading = 1.0; @@ -179,28 +175,47 @@ fn fs_main(in: VertexOut) -> FragmentOut { shading = max(0.4, sqrt(1.2 - distance(in.point_center, in.world_position) / in.radius)); // quick and dirty coloring } return FragmentOut( - vec4f(in.color.rgb * shading, coverage), - in.position.z + depth_offset, + vec4f(in.color.rgb * shading, coverage_depth.x), + coverage_depth.y, ); } +struct PickingLayerOut { + @location(0) + picking_id: vec4u, + + @builtin(frag_depth) + depth: f32 +} + @fragment -fn fs_main_picking_layer(in: VertexOut) -> @location(0) vec4u { - let coverage = coverage(in.world_position, in.radius, in.point_center); - if coverage <= 0.5 { +fn fs_main_picking_layer(in: VertexOut) -> PickingLayerOut { + let coverage_depth = coverage_and_depth(in.position.z, in.world_position, in.radius, in.point_center); + if coverage_depth.x <= 0.5 { discard; } - return vec4u(batch.picking_layer_object_id, in.picking_instance_id); + return PickingLayerOut( + vec4u(batch.picking_layer_object_id, in.picking_instance_id), + coverage_depth.y, + ); +} + +struct OutlineMaskOut { + @location(0) + mask: vec2u, + + @builtin(frag_depth) + depth: f32 } @fragment -fn fs_main_outline_mask(in: VertexOut) -> @location(0) vec2u { +fn fs_main_outline_mask(in: VertexOut) -> OutlineMaskOut { // Output is an integer target, can't use coverage therefore. // But we still want to discard fragments where coverage is low. // Since the outline extends a bit, a very low cut off tends to look better. - let coverage = coverage(in.world_position, in.radius, in.point_center); - if coverage < 1.0 { + let coverage_depth = coverage_and_depth(in.position.z, in.world_position, in.radius, in.point_center); + if coverage_depth.x < 1.0 { discard; } - return batch.outline_mask; + return OutlineMaskOut(batch.outline_mask, coverage_depth.y); } diff --git a/crates/viewer/re_renderer/shader/utils/camera.wgsl b/crates/viewer/re_renderer/shader/utils/camera.wgsl index 3a30bbfdbae01..c8f1b327bcd43 100644 --- a/crates/viewer/re_renderer/shader/utils/camera.wgsl +++ b/crates/viewer/re_renderer/shader/utils/camera.wgsl @@ -63,16 +63,23 @@ fn camera_ray_direction_from_screenuv(texcoord: vec2f) -> vec3f { return normalize(world_space_dir); } -// Returns distance to sphere surface (x) and distance to closest ray hit (y) +struct SphereIntersection { + distance_to_sphere_surface: f32, + distance_to_closest_hit_on_ray: f32, +} + +// Returns distance to sphere surface and distance to closest ray hit. // Via https://iquilezles.org/articles/spherefunctions/ but with more verbose names. -fn ray_sphere_distance(ray: Ray, sphere_origin: vec3f, sphere_radius: f32) -> vec2f { +fn ray_sphere_distance(ray: Ray, sphere_origin: vec3f, sphere_radius: f32) -> SphereIntersection { let sphere_radius_sq = sphere_radius * sphere_radius; let sphere_to_origin = ray.origin - sphere_origin; let b = dot(sphere_to_origin, ray.direction); let c = dot(sphere_to_origin, sphere_to_origin) - sphere_radius_sq; let h = b * b - c; - let d = sqrt(max(0.0, sphere_radius_sq - h)) - sphere_radius; - return vec2f(d, -b - sqrt(max(h, 0.0))); + var intersection: SphereIntersection; + intersection.distance_to_sphere_surface = sqrt(max(0.0, sphere_radius_sq - h)) - sphere_radius; + intersection.distance_to_closest_hit_on_ray = -b - sqrt(max(h, 0.0)); + return intersection; } // Returns the projected size of a pixel at a given distance from the camera. diff --git a/crates/viewer/re_renderer/shader/utils/sphere_depth.wgsl b/crates/viewer/re_renderer/shader/utils/sphere_depth.wgsl deleted file mode 100644 index 969fb5da7a062..0000000000000 --- a/crates/viewer/re_renderer/shader/utils/sphere_depth.wgsl +++ /dev/null @@ -1,52 +0,0 @@ -#import <../global_bindings.wgsl> - -// Routines for computing per-fragment depth of billboarded spheres or cylinders. -// -// Note: This technique is only an approximation. It ignores that depth is non-linear, so the -// curve of the sphere's surface will be distorted. -// -// It also ignores the fact that spheres not in line with the camera's optical axis are -// distorted by perspective projection in an asymmetric way — their outlines may be ellipses, -// but the peak of the screen-space depth is *not* centered on the screen-space center of the -// ellipse. In addition to that lack of distortion, the `projected_with_offset` point is the -// nearest to the camera in world space, but is not the point with the nearest depth in screen -// space, so the sphere's depth is lower than it should be. -// -// To get it right, we'd need to, essentially, compute the ray-tracing of a sphere. -// But, overall, this produces spheres that have roughly consistent depth behavior independent of -// view direction, which is good enough to, for example, make the look of intersections of -// points and lines consistent as the camera orbits them. Just don't look too closely. - - -// Compute the maximum `frag_depth` offset that a sphere, of radius `object_radius` -// with center located at `object_position`, should have on the middle of its surface. -// This may be used for cylinders too by giving the position of the nearest point on its -// center line. -fn sphere_radius_projected_depth(object_position: vec3f, object_radius: f32) -> f32 { - // If the billboard were a round mesh, where would the point on its surface nearest the camera be? - let front_point = object_position - + normalize(frame.camera_position - object_position) * object_radius; - - // Project that point and the object's center point. - let projected_without_offset = frame.projection_from_world * vec4f(object_position, 1.0); - let projected_with_offset = frame.projection_from_world * vec4f(front_point, 1.0); - - // Take the difference of the projected values. - return projected_with_offset.z / projected_with_offset.w - - projected_without_offset.z / projected_without_offset.w; -} - -// Given the value computed by `sphere_radius_projected_depth()`, and the world-space position -// of the current fragment, compute what should be added to its `frag_depth` to produce the -// (approximate) depth value of the sphere. -fn sphere_fragment_projected_depth( - object_radius: f32, - sphere_radius_projected_depth: f32, - world_frag_offset_from_center: vec3f, -) -> f32 { - let offset_radius_squared = - dot(world_frag_offset_from_center, world_frag_offset_from_center); - let normalized_radius_squared = offset_radius_squared / (object_radius * object_radius); - - return sphere_radius_projected_depth * sqrt(clamp(1.0 - normalized_radius_squared, 0.0, 1.0)); -} diff --git a/crates/viewer/re_renderer/shader/utils/sphere_quad.wgsl b/crates/viewer/re_renderer/shader/utils/sphere_quad.wgsl index ad61de568e9e0..ecbbbeb005db8 100644 --- a/crates/viewer/re_renderer/shader/utils/sphere_quad.wgsl +++ b/crates/viewer/re_renderer/shader/utils/sphere_quad.wgsl @@ -89,16 +89,12 @@ fn sphere_or_circle_quad_span(vertex_idx: u32, point_pos: vec3f, world_radius: f } /// Computes coverage of a 3D sphere placed at `sphere_center` in the fragment shader using the currently set camera. -fn sphere_quad_coverage(world_position: vec3f, radius: f32, sphere_center: vec3f) -> f32 { - let ray = camera_ray_to_world_pos(world_position); - +fn sphere_quad_coverage(sphere_intersection: SphereIntersection) -> f32 { // Sphere intersection with anti-aliasing as described by Iq here // https://www.shadertoy.com/view/MsSSWV // (but rearranged and labeled to it's easier to understand!) - let d = ray_sphere_distance(ray, sphere_center, radius); - let distance_to_sphere_surface = d.x; - let closest_ray_dist = d.y; - let pixel_world_size = approx_pixel_world_size_at(closest_ray_dist); + let distance_to_sphere_surface = sphere_intersection.distance_to_sphere_surface; + let pixel_world_size = approx_pixel_world_size_at(sphere_intersection.distance_to_closest_hit_on_ray); let distance_to_surface_in_pixels = distance_to_sphere_surface / pixel_world_size; diff --git a/crates/viewer/re_renderer/src/workspace_shaders.rs b/crates/viewer/re_renderer/src/workspace_shaders.rs index 82f2899d7110d..e34f8c7158751 100644 --- a/crates/viewer/re_renderer/src/workspace_shaders.rs +++ b/crates/viewer/re_renderer/src/workspace_shaders.rs @@ -187,12 +187,6 @@ pub fn init() { fs.create_file(virtpath, content).unwrap(); } - { - let virtpath = Path::new("shader/utils/sphere_depth.wgsl"); - let content = include_str!("../shader/utils/sphere_depth.wgsl").into(); - fs.create_file(virtpath, content).unwrap(); - } - { let virtpath = Path::new("shader/utils/sphere_quad.wgsl"); let content = include_str!("../shader/utils/sphere_quad.wgsl").into();