diff --git a/examples/joins.rs b/examples/joins.rs new file mode 100644 index 0000000..f940e13 --- /dev/null +++ b/examples/joins.rs @@ -0,0 +1,68 @@ +use std::f32::consts::TAU; + +use bevy::{ + color::palettes::css::{BLUE, RED}, + prelude::*, +}; +use bevy_polyline::prelude::*; + +const NUM_STEPS: u16 = 8; +const RADIUS: f32 = 1.0; +const WIDTH: f32 = 60.0; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(PolylinePlugin) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut polyline_materials: ResMut>, + mut polylines: ResMut>, +) { + for (joins, offset, color) in [(false, -1.0, RED), (true, 1.0, BLUE)] { + let center = Vec3 { + x: 1.5 * offset, + y: 0.0, + z: 0.0, + }; + + let max = f32::from(NUM_STEPS); + let vertices = (0u16..NUM_STEPS + 1) + .map(|t| { + let angle = f32::from(t) / max * TAU; + Vec3 { + x: angle.cos() * RADIUS, + y: angle.sin() * RADIUS, + z: 0.0, + } + center + }) + .collect(); + + commands.spawn(PolylineBundle { + polyline: PolylineHandle(polylines.add(Polyline { vertices })), + material: PolylineMaterialHandle(polyline_materials.add(PolylineMaterial { + width: WIDTH, + color: color.into(), + perspective: false, + joins, + ..default() + })), + ..default() + }); + } + + // camera + commands.spawn(( + Camera3d::default(), + Camera { + hdr: true, + ..default() + }, + Msaa::Sample4, + Transform::from_xyz(0.0, 0.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} diff --git a/src/material.rs b/src/material.rs index 269e091..035e521 100644 --- a/src/material.rs +++ b/src/material.rs @@ -64,6 +64,9 @@ pub struct PolylineMaterial { /// /// Note that `depth_bias` **does not** interact with this in any way. pub perspective: bool, + + // Whether to render round joins and caps, with a radius of half the line width. + pub joins: bool, } impl Default for PolylineMaterial { @@ -73,6 +76,7 @@ impl Default for PolylineMaterial { color: Color::WHITE.to_linear(), depth_bias: 0.0, perspective: false, + joins: false, } } } @@ -112,6 +116,7 @@ pub struct GpuPolylineMaterial { pub perspective: bool, pub bind_group: BindGroup, pub alpha_mode: AlphaMode, + pub joins: bool, } impl RenderAsset for GpuPolylineMaterial { @@ -156,6 +161,7 @@ impl RenderAsset for GpuPolylineMaterial { perspective: polyline_material.perspective, alpha_mode, bind_group, + joins: polyline_material.joins, }) } } @@ -218,6 +224,15 @@ impl SpecializedRenderPipeline for PolylineMaterialPipeline { .shader_defs .push("POLYLINE_PERSPECTIVE".into()); } + + if key.contains(PolylinePipelineKey::JOINS) { + descriptor.vertex.shader_defs.push("JOINS".into()); + + if let Some(ref mut fragment) = descriptor.fragment { + fragment.shader_defs.push("JOINS".into()); + } + } + descriptor.layout = vec![ self.polyline_pipeline.view_layout.clone(), self.polyline_pipeline.polyline_layout.clone(), @@ -322,6 +337,9 @@ pub fn queue_material_polylines( if material.perspective { polyline_key |= PolylinePipelineKey::PERSPECTIVE } + if material.joins { + polyline_key |= PolylinePipelineKey::JOINS + } let pipeline_id = pipelines.specialize(&pipeline_cache, &material_pipeline, polyline_key); diff --git a/src/polyline.rs b/src/polyline.rs index 9d82c0d..80c96cb 100644 --- a/src/polyline.rs +++ b/src/polyline.rs @@ -8,7 +8,6 @@ use bevy::{ }, }, prelude::*, - reflect::TypePath, render::{ extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, @@ -284,6 +283,7 @@ bitflags::bitflags! { const PERSPECTIVE = (1 << 0); const TRANSPARENT_MAIN_PASS = (1 << 1); const HDR = (1 << 2); + const JOINS = (1 << 3); const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; } } diff --git a/src/shaders/polyline.wgsl b/src/shaders/polyline.wgsl index 47838c2..6a8ae8e 100644 --- a/src/shaders/polyline.wgsl +++ b/src/shaders/polyline.wgsl @@ -28,6 +28,10 @@ struct Vertex { struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) color: vec4, + #ifdef JOINS + @location(1) uv: vec2, + @location(2) max_u: f32, + #endif }; @vertex @@ -53,10 +57,13 @@ fn vertex(vertex: Vertex) -> VertexOutput { let clip = mix(clip0, clip1, position.z); let resolution = vec2(view.viewport.z, view.viewport.w); - let screen0 = resolution * (0.5 * clip0.xy / clip0.w + 0.5); - let screen1 = resolution * (0.5 * clip1.xy / clip1.w + 0.5); + var screen0 = resolution * (0.5 * clip0.xy / clip0.w + 0.5); + var screen1 = resolution * (0.5 * clip1.xy / clip1.w + 0.5); + + let diff = screen1 - screen0; + let len = length(diff); - let x_basis = normalize(screen1 - screen0); + let x_basis = diff / len; let y_basis = vec2(-x_basis.y, x_basis.x); var line_width = material.width; @@ -71,6 +78,16 @@ fn vertex(vertex: Vertex) -> VertexOutput { } #endif + // low-poly join technique similar to + // https://www.researchgate.net/publication/220200701_High-Quality_Cartographic_Roads_on_High-Resolution_DEMs + #ifdef JOINS + screen0 = screen0 - x_basis * line_width / 2.0; + screen1 = screen1 + x_basis * line_width / 2.0; + + let max_u = 1.0 + line_width / len; + let uv = vec2((2.0 * position.z - 1.0) * max_u, position.y * 2.0); + #endif + let pt_offset = line_width * (position.x * x_basis + position.y * y_basis); let pt0 = screen0 + pt_offset; let pt1 = screen1 + pt_offset; @@ -91,7 +108,11 @@ fn vertex(vertex: Vertex) -> VertexOutput { depth = depth * exp2(-material.depth_bias * log2(clip.w / depth - epsilon)); } - return VertexOutput(vec4(clip.w * ((2.0 * pt) / resolution - 1.0), depth, clip.w), color); + #ifdef JOINS + return VertexOutput(vec4(clip.w * ((2.0 * pt) / resolution - 1.0), depth, clip.w), color, uv, max_u - 1.0); + #else + return VertexOutput(vec4(clip.w * ((2.0 * pt) / resolution - 1.0), depth, clip.w), color); + #endif } fn clip_near_plane(a: vec4, b: vec4) -> vec4 { @@ -108,9 +129,25 @@ fn clip_near_plane(a: vec4, b: vec4) -> vec4 { struct FragmentInput { @location(0) color: vec4, + #ifdef JOINS + @location(1) uv: vec2, + @location(2) max_u: f32, + #endif }; @fragment fn fragment(in: FragmentInput) -> @location(0) vec4 { + #ifdef JOINS + if ( abs( in.uv.x ) > 1.0 ) { + let a = in.uv.y; + let b = select(in.uv.x + 1.0, in.uv.x - 1.0, in.uv.x > 0.0) / in.max_u; + let len2 = a * a + b * b; + + if ( len2 > 1.0 ) { + discard; + } + } + #endif + return in.color; }