-
Notifications
You must be signed in to change notification settings - Fork 384
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
World grid part 1/2: add world grid renderer to
re_renderer
(#8230)
### Related This is the first half of * #872 The second half is here: * #8234 ### What Implements a "world grid" to be used later in the spatial views of the viewer. Haven't tested yet how suitable this is for 2D views, might need some adjustments. ## Video Youtube unfortunately compresses this a lot and Github doesn't let me upload any significant amount of video (click me) [![youtube video of world grid v1](http://img.youtube.com/vi/Ho-iGdmJi4Q/0.jpg)](http://www.youtube.com/watch?v=Ho-iGdmJi4Q "World Grid v1") ## Screenshots Analytical anti-aliasing (shown is what is supposed to be a 1ui unit == 2pixel wide line): <img width="1215" alt="image" src="https://github.com/user-attachments/assets/702b82ac-2629-4aa5-9304-0cd3c4d87fc5"> Distance has only the cardinal lines (== every tenth line). Those fade eventually as well: <img width="747" alt="image" src="https://github.com/user-attachments/assets/ebe6b2c9-37e5-4406-8d28-5260cf2940d4"> Fading is based on "how many lines coincide here" which makes fading view dependent rather than distance dependent: <img width="439" alt="image" src="https://github.com/user-attachments/assets/9bea7a42-9edc-4a7d-be19-9498a1f29fdf"> (this makes this hopefully robust for many usecases) Grid intersects non-transparent geometry (transparent geometry will be ignored in the future. previous surveying showed that this is common and not a biggy) <img width="426" alt="image" src="https://github.com/user-attachments/assets/19de2cc9-015d-4fdd-a275-096768119a9e"> Grid fades at acute viewing angles (because empirically and unsurprisingly it looks weird if we don't!) <img width="1437" alt="image" src="https://github.com/user-attachments/assets/2a05b8e5-9915-4dda-9a54-4db829e22ac3"> Tested image quality to be satisfying on high dpi (ui unit == 2 pixels) and low dpi (ui unit == pixel). ## How does it work * Draw a large shader generated plane (mostly because setting up vertex buffers is just more hassle 😄 ) * depth test enabled * depth write disabled * premultiplied alpha blend * make sure we draw this after the background (reminder: we first draw solid, then background) * Fragment shader looks at 2d coordinate on the plane and decides how "liney" it is * using screen space derivatives (ddx/ddy) we figure out how far to go on the plane to achieve a certain line thickness * fades: * fade if the line density gets too high * fade if view angle is too acute relative * ... lot's of details documented in the shader code! I considered just drawing a screen space triangle, but drawing a plane that moves with the camera has lots of advantages: * don't have to manipulate depth * faster since early z just works * less thinking required! * don't cover things above the horizon - even if only the grid is visible, less pixels will be shaded ## Known shortcomings * various hardcoded values around how fading works. Tricky to formalize this better, but likely good enough * Doesn't look equally good at all pixel widths, but decent for the range we care * every tenth line is a "cardinal line", that's nice but it stops there - "infinite" amount of cardinal lines would be nicer * Experimented with that but so far didn't get anything that was compelling. Having many order-of-magnitude lines looks way too busy imho, it needs a mechanism that limits that to two. * Blender's 2D view does this really well, but in 3D they only do two cardinals as well. ## Try it yourself Controls: - Space: Pause - G: Toggle camera mode native: ``` cargo run -p re_renderer_examples --bin world_grid ``` web: ``` cargo run-wasm -p re_renderer_examples --bin world_grid ``` ## Backend testing matrix * [x] Metal * [x] Vulkan * [x] WebGL * [x] WebGPU
- Loading branch information
Showing
19 changed files
with
593 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
//! Additional interpolation functions. | ||
|
||
// Like smoothstep, but linear. 1D. | ||
fn linearstep(edge0: f32, edge1: f32, x: f32) -> f32 { | ||
return saturate((x - edge0) / (edge1 - edge0)); | ||
} | ||
|
||
// Like smoothstep, but linear. 2D. | ||
fn linearstep2(edge0: vec2f, edge1: vec2f, x: vec2f) -> vec2f { | ||
return saturate((x - edge0) / (edge1 - edge0)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
struct Plane { | ||
normal: vec3f, | ||
distance: f32, | ||
} | ||
|
||
/// How far away is a given point from the plane? | ||
fn distance_to_plane(plane: Plane, point: vec3f) -> f32 { | ||
return dot(plane.normal, point) + plane.distance; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
#import <./global_bindings.wgsl> | ||
#import <./utils/interpolation.wgsl> | ||
#import <./utils/plane.wgsl> | ||
struct WorldGridUniformBuffer { | ||
color: vec4f, | ||
|
||
/// Plane equation, normal + distance. | ||
plane: Plane, | ||
|
||
/// How far apart the closest sets of lines are. | ||
spacing: f32, | ||
|
||
/// How thick the lines are in UI units. | ||
thickness_ui: f32, | ||
} | ||
|
||
@group(1) @binding(0) | ||
var<uniform> config: WorldGridUniformBuffer; | ||
|
||
struct VertexOutput { | ||
@builtin(position)W | ||
position: vec4f, | ||
|
||
@location(0) | ||
scaled_world_plane_position: vec2f, | ||
}; | ||
|
||
// We have to make up some world space geometry which then necessarily gets a limited size. | ||
// Putting a too high number here makes things break down because of floating point inaccuracies. | ||
// But arguably at that point we're potentially doomed either way since precision will break down in other parts of the rendering as well. | ||
// | ||
// This is the main drawback of the plane approach over the screen space filling one. | ||
const PLANE_GEOMETRY_SIZE: f32 = 10000.0; | ||
|
||
// Spans a large quad where centered around the camera. | ||
// | ||
// This gives us the "canvas" to drawn the grid on. | ||
// Compared to a fullscreen pass, we get the z value (and thus early z testing) for free, | ||
// as well as never covering the screen above the horizon. | ||
@vertex | ||
fn main_vs(@builtin(vertex_index) v_idx: u32) -> VertexOutput { | ||
var out: VertexOutput; | ||
|
||
var plane_position = (vec2f(f32(v_idx / 2u), f32(v_idx % 2u)) * 2.0 - 1.0) * PLANE_GEOMETRY_SIZE; | ||
|
||
// Make up x and y axis for the plane. | ||
let plane_y_axis = normalize(cross(config.plane.normal, select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), config.plane.normal.x != 0.0))); | ||
let plane_x_axis = cross(plane_y_axis, config.plane.normal); | ||
|
||
// Move plane geometry with the camera. | ||
let camera_on_plane = vec2f(dot(plane_x_axis, frame.camera_position), dot(plane_y_axis, frame.camera_position)); | ||
let shifted_plane_position = plane_position + camera_on_plane; | ||
|
||
// Compute world position from shifted plane position. | ||
let world_position = config.plane.normal * -config.plane.distance + plane_x_axis * shifted_plane_position.x + plane_y_axis * shifted_plane_position.y; | ||
|
||
out.position = frame.projection_from_world * vec4f(world_position, 1.0); | ||
out.scaled_world_plane_position = shifted_plane_position / config.spacing; | ||
|
||
return out; | ||
} | ||
|
||
|
||
// Distance to a grid line in x and y ranging from 0 to 1. | ||
fn calc_distance_to_grid_line(scaled_world_plane_position: vec2f) -> vec2f { | ||
return 1.0 - abs(fract(scaled_world_plane_position) * 2.0 - 1.0); | ||
} | ||
|
||
@fragment | ||
fn main_fs(in: VertexOutput) -> @location(0) vec4f { | ||
// Most basics are very well explained by Ben Golus here: https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8 | ||
// We're not actually implementing the "pristine grid shader" which is a grid with world space thickness, | ||
// but rather the pixel space grid, which is a lot simpler, but happens to be also described very well in this article. | ||
|
||
// Distance to a grid line in x and y ranging from 0 to 1. | ||
let distance_to_grid_line = calc_distance_to_grid_line(in.scaled_world_plane_position); | ||
|
||
// Figure out the how wide the lines are in this "draw space". | ||
let plane_unit_pixel_derivative = fwidthFine(in.scaled_world_plane_position); | ||
let line_anti_alias = plane_unit_pixel_derivative; | ||
let width_in_pixels = config.thickness_ui * frame.pixels_from_point; | ||
let width_in_grid_units = width_in_pixels * plane_unit_pixel_derivative; | ||
var intensity_regular = linearstep2(width_in_grid_units + line_anti_alias, width_in_grid_units - line_anti_alias, distance_to_grid_line); | ||
|
||
// Fade lines that get too close to each other. | ||
// Once the number of pixels per line (== from one line to the next) is below a threshold fade them out. | ||
// | ||
// Note that `1/plane_unit_pixel_derivative` would give us more literal pixels per line, | ||
// but we actually want to know how dense the lines get here so we use `1/width_in_grid_units` instead, | ||
// such that a value of 1.0 means roughly "100% lines" and 10.0 means "Every 10 pixels there is a lines". | ||
// Empirically (== making the fade a hard cut and taking screenshot), this works out pretty accurately! | ||
// | ||
// Tried smoothstep here, but didn't feel right even with lots of range tweaking. | ||
let screen_space_line_spacing = 1.0 / max(width_in_grid_units.x, width_in_grid_units.y); | ||
let grid_closeness_fade = linearstep(1.0, 10.0, screen_space_line_spacing); | ||
intensity_regular *= grid_closeness_fade; | ||
|
||
// Every tenth line is a more intense, we call those "cardinal" lines. | ||
// Experimented previously with more levels of cardinal lines, but it gets too busy: | ||
// It seems that if we want to go down this path, we should ensure that there's only two levels of lines on screen at a time. | ||
const CARDINAL_LINE_FACTOR: f32 = 10.0; | ||
let distance_to_grid_line_cardinal = calc_distance_to_grid_line(in.scaled_world_plane_position * (1.0 / CARDINAL_LINE_FACTOR)); | ||
var cardinal_line_intensity = linearstep2(width_in_grid_units + line_anti_alias, width_in_grid_units - line_anti_alias, | ||
distance_to_grid_line_cardinal * CARDINAL_LINE_FACTOR); | ||
let cardinal_grid_closeness_fade = linearstep(2.0, 10.0, screen_space_line_spacing * CARDINAL_LINE_FACTOR); // Fade cardinal lines a little bit earlier (because it looks nicer) | ||
cardinal_line_intensity *= cardinal_grid_closeness_fade; | ||
|
||
// Combine all lines. | ||
// | ||
// Lerp for cardinal & regular. | ||
// This way we don't break anti-aliasing (as addition would!), mute the regular lines, and make cardinals weaker when there's no regular to support them. | ||
let cardinal_and_regular = mix(intensity_regular, cardinal_line_intensity, 0.4); | ||
// X and Y are combined like akin to premultiplied alpha operations. | ||
let intensity_combined = saturate(cardinal_and_regular.x * (1.0 - cardinal_and_regular.y) + cardinal_and_regular.y); | ||
|
||
|
||
return config.color * intensity_combined; | ||
|
||
// Useful debugging visualizations: | ||
//return vec4f(intensity_combined); | ||
//return vec4f(grid_closeness_fade, cardinal_grid_closeness_fade, 0.0, 1.0); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
use crate::{ | ||
allocator::create_and_fill_uniform_buffer, | ||
draw_phases::DrawPhase, | ||
include_shader_module, | ||
wgpu_resources::{ | ||
BindGroupDesc, BindGroupLayoutDesc, GpuBindGroup, GpuBindGroupLayoutHandle, | ||
GpuRenderPipelineHandle, GpuRenderPipelinePoolAccessor, PipelineLayoutDesc, | ||
RenderPipelineDesc, | ||
}, | ||
ViewBuilder, | ||
}; | ||
|
||
use super::{DrawData, DrawError, RenderContext, Renderer}; | ||
use crate::Rgba; | ||
|
||
use smallvec::smallvec; | ||
|
||
/// Configuration for the world grid renderer. | ||
pub struct WorldGridConfiguration { | ||
/// The color of the grid lines. | ||
pub color: Rgba, | ||
|
||
/// The plane in which the grid lines are drawn. | ||
pub plane: re_math::Plane3, | ||
|
||
/// How far apart the closest sets of lines are. | ||
pub spacing: f32, | ||
|
||
/// How thick the lines are in UI units. | ||
pub thickness_ui: f32, | ||
} | ||
|
||
mod gpu_data { | ||
use crate::wgpu_buffer_types; | ||
|
||
/// Keep in sync with `world_grid.wgsl` | ||
#[repr(C)] | ||
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] | ||
pub struct WorldGridUniformBuffer { | ||
pub color: wgpu_buffer_types::Vec4, | ||
|
||
/// Plane equation, normal + distance. | ||
pub plane: wgpu_buffer_types::Vec4, | ||
|
||
/// How far apart the closest sets of lines are. | ||
pub spacing: f32, | ||
|
||
/// Radius of the lines in UI units. | ||
pub thickness_ui: f32, | ||
|
||
pub _padding: [f32; 2], | ||
pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 3], | ||
} | ||
} | ||
|
||
pub struct WorldGridRenderer { | ||
render_pipeline: GpuRenderPipelineHandle, | ||
bind_group_layout: GpuBindGroupLayoutHandle, | ||
} | ||
|
||
/// Draw data for a world grid renderer. | ||
#[derive(Clone)] | ||
pub struct WorldGridDrawData { | ||
bind_group: GpuBindGroup, | ||
} | ||
|
||
impl DrawData for WorldGridDrawData { | ||
type Renderer = WorldGridRenderer; | ||
} | ||
|
||
impl WorldGridDrawData { | ||
pub fn new(ctx: &RenderContext, config: &WorldGridConfiguration) -> Self { | ||
let world_grid_renderer = ctx.renderer::<WorldGridRenderer>(); | ||
|
||
let uniform_buffer_binding = create_and_fill_uniform_buffer( | ||
ctx, | ||
"WorldGridDrawData".into(), | ||
gpu_data::WorldGridUniformBuffer { | ||
color: config.color.into(), | ||
plane: config.plane.as_vec4().into(), | ||
spacing: config.spacing, | ||
thickness_ui: config.thickness_ui, | ||
_padding: Default::default(), | ||
end_padding: Default::default(), | ||
}, | ||
); | ||
|
||
Self { | ||
bind_group: ctx.gpu_resources.bind_groups.alloc( | ||
&ctx.device, | ||
&ctx.gpu_resources, | ||
&BindGroupDesc { | ||
label: "WorldGrid".into(), | ||
entries: smallvec![uniform_buffer_binding], | ||
layout: world_grid_renderer.bind_group_layout, | ||
}, | ||
), | ||
} | ||
} | ||
} | ||
|
||
impl Renderer for WorldGridRenderer { | ||
type RendererDrawData = WorldGridDrawData; | ||
|
||
fn create_renderer(ctx: &RenderContext) -> Self { | ||
re_tracing::profile_function!(); | ||
|
||
let bind_group_layout = ctx.gpu_resources.bind_group_layouts.get_or_create( | ||
&ctx.device, | ||
&BindGroupLayoutDesc { | ||
label: "WorldGrid::bind_group_layout".into(), | ||
entries: vec![wgpu::BindGroupLayoutEntry { | ||
binding: 0, | ||
visibility: wgpu::ShaderStages::FRAGMENT | wgpu::ShaderStages::VERTEX, | ||
ty: wgpu::BindingType::Buffer { | ||
ty: wgpu::BufferBindingType::Uniform, | ||
has_dynamic_offset: false, | ||
min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::< | ||
gpu_data::WorldGridUniformBuffer, | ||
>() | ||
as _), | ||
}, | ||
count: None, | ||
}], | ||
}, | ||
); | ||
|
||
let shader_module = ctx | ||
.gpu_resources | ||
.shader_modules | ||
.get_or_create(ctx, &include_shader_module!("../../shader/world_grid.wgsl")); | ||
let render_pipeline = ctx.gpu_resources.render_pipelines.get_or_create( | ||
ctx, | ||
&RenderPipelineDesc { | ||
label: "WorldGridDrawData::render_pipeline_regular".into(), | ||
pipeline_layout: ctx.gpu_resources.pipeline_layouts.get_or_create( | ||
ctx, | ||
&PipelineLayoutDesc { | ||
label: "WorldGrid".into(), | ||
entries: vec![ctx.global_bindings.layout, bind_group_layout], | ||
}, | ||
), | ||
vertex_entrypoint: "main_vs".into(), | ||
vertex_handle: shader_module, | ||
fragment_entrypoint: "main_fs".into(), | ||
fragment_handle: shader_module, | ||
vertex_buffers: smallvec![], | ||
render_targets: smallvec![Some(wgpu::ColorTargetState { | ||
format: ViewBuilder::MAIN_TARGET_COLOR_FORMAT, | ||
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), | ||
write_mask: wgpu::ColorWrites::ALL, | ||
})], | ||
primitive: wgpu::PrimitiveState { | ||
// drawn as a (close to) infinite quad | ||
topology: wgpu::PrimitiveTopology::TriangleStrip, | ||
cull_mode: None, | ||
..Default::default() | ||
}, | ||
depth_stencil: Some(wgpu::DepthStencilState { | ||
format: ViewBuilder::MAIN_TARGET_DEPTH_FORMAT, | ||
depth_compare: wgpu::CompareFunction::GreaterEqual, | ||
depth_write_enabled: false, | ||
stencil: wgpu::StencilState::default(), | ||
bias: wgpu::DepthBiasState::default(), | ||
}), | ||
multisample: ViewBuilder::MAIN_TARGET_DEFAULT_MSAA_STATE, | ||
}, | ||
); | ||
Self { | ||
render_pipeline, | ||
bind_group_layout, | ||
} | ||
} | ||
|
||
fn draw( | ||
&self, | ||
render_pipelines: &GpuRenderPipelinePoolAccessor<'_>, | ||
_phase: DrawPhase, | ||
pass: &mut wgpu::RenderPass<'_>, | ||
draw_data: &WorldGridDrawData, | ||
) -> Result<(), DrawError> { | ||
let pipeline = render_pipelines.get(self.render_pipeline)?; | ||
|
||
pass.set_pipeline(pipeline); | ||
pass.set_bind_group(1, &draw_data.bind_group, &[]); | ||
pass.draw(0..4, 0..1); | ||
|
||
Ok(()) | ||
} | ||
|
||
fn participated_phases() -> &'static [DrawPhase] { | ||
&[DrawPhase::Transparent] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.