Skip to content

Commit

Permalink
World grid part 1/2: add world grid renderer to re_renderer (#8230)
Browse files Browse the repository at this point in the history
### 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
Wumpf authored Nov 28, 2024
1 parent 1bc62f2 commit a54c2c5
Show file tree
Hide file tree
Showing 19 changed files with 593 additions and 117 deletions.
1 change: 1 addition & 0 deletions crates/viewer/re_renderer/shader/screen_triangle.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ fn main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
out.texcoord.y = 1.0 - out.texcoord.y;
return out;
}

2 changes: 0 additions & 2 deletions crates/viewer/re_renderer/shader/screen_triangle_vertex.wgsl
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#import <./types.wgsl>

struct VertexOutput {
// Mark output position as invariant so it's safe to use it with depth test Equal.
// Without @invariant, different usages in different render pipelines might optimize differently,
Expand Down
11 changes: 11 additions & 0 deletions crates/viewer/re_renderer/shader/utils/interpolation.wgsl
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));
}
9 changes: 9 additions & 0 deletions crates/viewer/re_renderer/shader/utils/plane.wgsl
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;
}
122 changes: 122 additions & 0 deletions crates/viewer/re_renderer/shader/world_grid.wgsl
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);
}
3 changes: 3 additions & 0 deletions crates/viewer/re_renderer/src/draw_phases/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ pub enum DrawPhase {
/// Background, rendering where depth wasn't written.
Background,

/// Transparent objects, performing reads of the depth buffer, but no writes.
Transparent,

/// Everything that can be picked with GPU based picking.
///
/// This should be everything in the `Opaque` phase.
Expand Down
3 changes: 3 additions & 0 deletions crates/viewer/re_renderer/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ pub(crate) use compositor::CompositorDrawData;
mod debug_overlay;
pub use debug_overlay::{DebugOverlayDrawData, DebugOverlayError, DebugOverlayRenderer};

mod world_grid;
pub use world_grid::{WorldGridConfiguration, WorldGridDrawData, WorldGridRenderer};

pub mod gpu_data {
pub use super::lines::gpu_data::{LineStripInfo, LineVertex};
pub use super::point_cloud::gpu_data::PositionRadius;
Expand Down
194 changes: 194 additions & 0 deletions crates/viewer/re_renderer/src/renderer/world_grid.rs
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]
}
}
6 changes: 5 additions & 1 deletion crates/viewer/re_renderer/src/view_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,11 @@ impl ViewBuilder {

pass.set_bind_group(0, &setup.bind_group_0, &[]);

for phase in [DrawPhase::Opaque, DrawPhase::Background] {
for phase in [
DrawPhase::Opaque,
DrawPhase::Background,
DrawPhase::Transparent,
] {
self.draw_phase(&renderers, &pipelines, phase, &mut pass);
}
}
Expand Down
Loading

0 comments on commit a54c2c5

Please sign in to comment.