# Objective - Adds gizmo line joints, suggestion of #9400 ## Solution - Adds `line_joints: GizmoLineJoint` to `GizmoConfig`. Currently the following values are supported: - `GizmoLineJoint::None`: does not draw line joints, same behaviour as previously - `GizmoLineJoint::Bevel`: draws a single triangle between the lines - `GizmoLineJoint::Miter` / 'spiky joints': draws two triangles between the lines extending them until they meet at a (miter) point. - NOTE: for very small angles between the lines, which happens frequently in 3d, the miter point will be very far away from the point at which the lines meet. - `GizmoLineJoint::Round(resolution)`: Draw a circle arc between the lines. The circle is a triangle fan of `resolution` triangles. --- ## Changelog - Added `GizmoLineJoint`, use that in `GizmoConfig` and added necessary pipelines and draw commands. - Added a new `line_joints.wgsl` shader containing three vertex shaders `vertex_bevel`, `vertex_miter` and `vertex_round` as well as a basic `fragment` shader. ## Migration Guide Any manually created `GizmoConfig`s must now set the `.line_joints` field. ## Known issues - The way we currently create basic closed shapes like rectangles, circles, triangles or really any closed 2d shape means that one of the corners will not be drawn with joints, although that would probably be expected. (see the triangle in the 2d image) - This could be somewhat mitigated by introducing line caps or fixed by adding another segment overlapping the first of the strip. (Maybe in a followup PR?) - 3d shapes can look 'off' with line joints (especially bevel) because wherever 3 or more lines meet one of them may stick out beyond the joint drawn between the other 2. - Adding additional lines so that there is a joint between every line at a corner would fix this but would probably be too computationally expensive. - Miter joints are 'unreasonably long' for very small angles between the lines (the angle is the angle between the lines in screen space). This is technically correct but distracting and does not feel right, especially in 3d contexts. I think limiting the length of the miter to the point at which the lines meet might be a good idea. - The joints may be drawn with a different gizmo in-between them and their corresponding lines in 2d. Some sort of z-ordering would probably be good here, but I believe this may be out of scope for this PR. ## Additional information Some pretty images :) <img width="1175" alt="Screenshot 2024-03-02 at 04 53 50" src="https://github.com/bevyengine/bevy/assets/62256001/58df7e63-9376-4430-8871-32adba0cb53b"> - Note that the top vertex does not have a joint drawn. <img width="1440" alt="Screenshot 2024-03-02 at 05 03 55" src="https://github.com/bevyengine/bevy/assets/62256001/137a00cf-cbd4-48c2-a46f-4b47492d4fd9"> Now for a weird video: https://github.com/bevyengine/bevy/assets/62256001/93026f48-f1d6-46fe-9163-5ab548a3fce4 - The black lines shooting out from the cube are miter joints that get very long because the lines between which they are drawn are (almost) collinear in screen space. --------- Co-authored-by: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com>
435 lines
13 KiB
Rust
435 lines
13 KiB
Rust
use crate::{
|
|
config::GizmoMeshConfig, line_gizmo_vertex_buffer_layouts,
|
|
line_joint_gizmo_vertex_buffer_layouts, prelude::GizmoLineJoint, DrawLineGizmo,
|
|
DrawLineJointGizmo, GizmoRenderSystem, LineGizmo, LineGizmoUniformBindgroupLayout,
|
|
SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE,
|
|
};
|
|
use bevy_app::{App, Plugin};
|
|
use bevy_asset::Handle;
|
|
use bevy_core_pipeline::{
|
|
core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT},
|
|
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
|
|
};
|
|
|
|
use bevy_ecs::{
|
|
prelude::Entity,
|
|
query::Has,
|
|
schedule::{IntoSystemConfigs, IntoSystemSetConfigs},
|
|
system::{Query, Res, ResMut, Resource},
|
|
world::{FromWorld, World},
|
|
};
|
|
use bevy_pbr::{MeshPipeline, MeshPipelineKey, SetMeshViewBindGroup};
|
|
use bevy_render::{
|
|
render_asset::{prepare_assets, RenderAssets},
|
|
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
|
|
render_resource::*,
|
|
texture::BevyDefault,
|
|
view::{ExtractedView, Msaa, RenderLayers, ViewTarget},
|
|
Render, RenderApp, RenderSet,
|
|
};
|
|
use bevy_utils::tracing::error;
|
|
|
|
pub struct LineGizmo3dPlugin;
|
|
impl Plugin for LineGizmo3dPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
|
|
render_app
|
|
.add_render_command::<Transparent3d, DrawLineGizmo3d>()
|
|
.add_render_command::<Transparent3d, DrawLineJointGizmo3d>()
|
|
.init_resource::<SpecializedRenderPipelines<LineGizmoPipeline>>()
|
|
.init_resource::<SpecializedRenderPipelines<LineJointGizmoPipeline>>()
|
|
.configure_sets(
|
|
Render,
|
|
GizmoRenderSystem::QueueLineGizmos3d.in_set(RenderSet::Queue),
|
|
)
|
|
.add_systems(
|
|
Render,
|
|
(queue_line_gizmos_3d, queue_line_joint_gizmos_3d)
|
|
.in_set(GizmoRenderSystem::QueueLineGizmos3d)
|
|
.after(prepare_assets::<LineGizmo>),
|
|
);
|
|
}
|
|
|
|
fn finish(&self, app: &mut App) {
|
|
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
|
|
render_app.init_resource::<LineGizmoPipeline>();
|
|
render_app.init_resource::<LineJointGizmoPipeline>();
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Resource)]
|
|
struct LineGizmoPipeline {
|
|
mesh_pipeline: MeshPipeline,
|
|
uniform_layout: BindGroupLayout,
|
|
}
|
|
|
|
impl FromWorld for LineGizmoPipeline {
|
|
fn from_world(render_world: &mut World) -> Self {
|
|
LineGizmoPipeline {
|
|
mesh_pipeline: render_world.resource::<MeshPipeline>().clone(),
|
|
uniform_layout: render_world
|
|
.resource::<LineGizmoUniformBindgroupLayout>()
|
|
.layout
|
|
.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
|
struct LineGizmoPipelineKey {
|
|
view_key: MeshPipelineKey,
|
|
strip: bool,
|
|
perspective: bool,
|
|
}
|
|
|
|
impl SpecializedRenderPipeline for LineGizmoPipeline {
|
|
type Key = LineGizmoPipelineKey;
|
|
|
|
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
|
let mut shader_defs = vec![
|
|
#[cfg(feature = "webgl")]
|
|
"SIXTEEN_BYTE_ALIGNMENT".into(),
|
|
];
|
|
|
|
if key.perspective {
|
|
shader_defs.push("PERSPECTIVE".into());
|
|
}
|
|
|
|
let format = if key.view_key.contains(MeshPipelineKey::HDR) {
|
|
ViewTarget::TEXTURE_FORMAT_HDR
|
|
} else {
|
|
TextureFormat::bevy_default()
|
|
};
|
|
|
|
let view_layout = self
|
|
.mesh_pipeline
|
|
.get_view_layout(key.view_key.into())
|
|
.clone();
|
|
|
|
let layout = vec![view_layout, self.uniform_layout.clone()];
|
|
|
|
RenderPipelineDescriptor {
|
|
vertex: VertexState {
|
|
shader: LINE_SHADER_HANDLE,
|
|
entry_point: "vertex".into(),
|
|
shader_defs: shader_defs.clone(),
|
|
buffers: line_gizmo_vertex_buffer_layouts(key.strip),
|
|
},
|
|
fragment: Some(FragmentState {
|
|
shader: LINE_SHADER_HANDLE,
|
|
shader_defs,
|
|
entry_point: "fragment".into(),
|
|
targets: vec![Some(ColorTargetState {
|
|
format,
|
|
blend: Some(BlendState::ALPHA_BLENDING),
|
|
write_mask: ColorWrites::ALL,
|
|
})],
|
|
}),
|
|
layout,
|
|
primitive: PrimitiveState::default(),
|
|
depth_stencil: Some(DepthStencilState {
|
|
format: CORE_3D_DEPTH_FORMAT,
|
|
depth_write_enabled: true,
|
|
depth_compare: CompareFunction::Greater,
|
|
stencil: StencilState::default(),
|
|
bias: DepthBiasState::default(),
|
|
}),
|
|
multisample: MultisampleState {
|
|
count: key.view_key.msaa_samples(),
|
|
mask: !0,
|
|
alpha_to_coverage_enabled: false,
|
|
},
|
|
label: Some("LineGizmo Pipeline".into()),
|
|
push_constant_ranges: vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Resource)]
|
|
struct LineJointGizmoPipeline {
|
|
mesh_pipeline: MeshPipeline,
|
|
uniform_layout: BindGroupLayout,
|
|
}
|
|
|
|
impl FromWorld for LineJointGizmoPipeline {
|
|
fn from_world(render_world: &mut World) -> Self {
|
|
LineJointGizmoPipeline {
|
|
mesh_pipeline: render_world.resource::<MeshPipeline>().clone(),
|
|
uniform_layout: render_world
|
|
.resource::<LineGizmoUniformBindgroupLayout>()
|
|
.layout
|
|
.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
|
struct LineJointGizmoPipelineKey {
|
|
view_key: MeshPipelineKey,
|
|
perspective: bool,
|
|
joints: GizmoLineJoint,
|
|
}
|
|
|
|
impl SpecializedRenderPipeline for LineJointGizmoPipeline {
|
|
type Key = LineJointGizmoPipelineKey;
|
|
|
|
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
|
let mut shader_defs = vec![
|
|
#[cfg(feature = "webgl")]
|
|
"SIXTEEN_BYTE_ALIGNMENT".into(),
|
|
];
|
|
|
|
if key.perspective {
|
|
shader_defs.push("PERSPECTIVE".into());
|
|
}
|
|
|
|
let format = if key.view_key.contains(MeshPipelineKey::HDR) {
|
|
ViewTarget::TEXTURE_FORMAT_HDR
|
|
} else {
|
|
TextureFormat::bevy_default()
|
|
};
|
|
|
|
let view_layout = self
|
|
.mesh_pipeline
|
|
.get_view_layout(key.view_key.into())
|
|
.clone();
|
|
|
|
let layout = vec![view_layout, self.uniform_layout.clone()];
|
|
|
|
if key.joints == GizmoLineJoint::None {
|
|
error!("There is no entry point for line joints with GizmoLineJoints::None. Please consider aborting the drawing process before reaching this stage.");
|
|
};
|
|
|
|
let entry_point = match key.joints {
|
|
GizmoLineJoint::Miter => "vertex_miter",
|
|
GizmoLineJoint::Round(_) => "vertex_round",
|
|
GizmoLineJoint::None | GizmoLineJoint::Bevel => "vertex_bevel",
|
|
};
|
|
|
|
RenderPipelineDescriptor {
|
|
vertex: VertexState {
|
|
shader: LINE_JOINT_SHADER_HANDLE,
|
|
entry_point: entry_point.into(),
|
|
shader_defs: shader_defs.clone(),
|
|
buffers: line_joint_gizmo_vertex_buffer_layouts(),
|
|
},
|
|
fragment: Some(FragmentState {
|
|
shader: LINE_JOINT_SHADER_HANDLE,
|
|
shader_defs,
|
|
entry_point: "fragment".into(),
|
|
targets: vec![Some(ColorTargetState {
|
|
format,
|
|
blend: Some(BlendState::ALPHA_BLENDING),
|
|
write_mask: ColorWrites::ALL,
|
|
})],
|
|
}),
|
|
layout,
|
|
primitive: PrimitiveState::default(),
|
|
depth_stencil: Some(DepthStencilState {
|
|
format: CORE_3D_DEPTH_FORMAT,
|
|
depth_write_enabled: true,
|
|
depth_compare: CompareFunction::Greater,
|
|
stencil: StencilState::default(),
|
|
bias: DepthBiasState::default(),
|
|
}),
|
|
multisample: MultisampleState {
|
|
count: key.view_key.msaa_samples(),
|
|
mask: !0,
|
|
alpha_to_coverage_enabled: false,
|
|
},
|
|
label: Some("LineJointGizmo Pipeline".into()),
|
|
push_constant_ranges: vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
type DrawLineGizmo3d = (
|
|
SetItemPipeline,
|
|
SetMeshViewBindGroup<0>,
|
|
SetLineGizmoBindGroup<1>,
|
|
DrawLineGizmo,
|
|
);
|
|
type DrawLineJointGizmo3d = (
|
|
SetItemPipeline,
|
|
SetMeshViewBindGroup<0>,
|
|
SetLineGizmoBindGroup<1>,
|
|
DrawLineJointGizmo,
|
|
);
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn queue_line_gizmos_3d(
|
|
draw_functions: Res<DrawFunctions<Transparent3d>>,
|
|
pipeline: Res<LineGizmoPipeline>,
|
|
mut pipelines: ResMut<SpecializedRenderPipelines<LineGizmoPipeline>>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
msaa: Res<Msaa>,
|
|
line_gizmos: Query<(Entity, &Handle<LineGizmo>, &GizmoMeshConfig)>,
|
|
line_gizmo_assets: Res<RenderAssets<LineGizmo>>,
|
|
mut views: Query<(
|
|
&ExtractedView,
|
|
&mut RenderPhase<Transparent3d>,
|
|
Option<&RenderLayers>,
|
|
(
|
|
Has<NormalPrepass>,
|
|
Has<DepthPrepass>,
|
|
Has<MotionVectorPrepass>,
|
|
Has<DeferredPrepass>,
|
|
),
|
|
)>,
|
|
) {
|
|
let draw_function = draw_functions.read().get_id::<DrawLineGizmo3d>().unwrap();
|
|
|
|
for (
|
|
view,
|
|
mut transparent_phase,
|
|
render_layers,
|
|
(normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass),
|
|
) in &mut views
|
|
{
|
|
let render_layers = render_layers.copied().unwrap_or_default();
|
|
|
|
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
|
|
| MeshPipelineKey::from_hdr(view.hdr);
|
|
|
|
if normal_prepass {
|
|
view_key |= MeshPipelineKey::NORMAL_PREPASS;
|
|
}
|
|
|
|
if depth_prepass {
|
|
view_key |= MeshPipelineKey::DEPTH_PREPASS;
|
|
}
|
|
|
|
if motion_vector_prepass {
|
|
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
|
|
}
|
|
|
|
if deferred_prepass {
|
|
view_key |= MeshPipelineKey::DEFERRED_PREPASS;
|
|
}
|
|
|
|
for (entity, handle, config) in &line_gizmos {
|
|
if !config.render_layers.intersects(&render_layers) {
|
|
continue;
|
|
}
|
|
|
|
let Some(line_gizmo) = line_gizmo_assets.get(handle) else {
|
|
continue;
|
|
};
|
|
|
|
let pipeline = pipelines.specialize(
|
|
&pipeline_cache,
|
|
&pipeline,
|
|
LineGizmoPipelineKey {
|
|
view_key,
|
|
strip: line_gizmo.strip,
|
|
perspective: config.line_perspective,
|
|
},
|
|
);
|
|
|
|
transparent_phase.add(Transparent3d {
|
|
entity,
|
|
draw_function,
|
|
pipeline,
|
|
distance: 0.,
|
|
batch_range: 0..1,
|
|
dynamic_offset: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn queue_line_joint_gizmos_3d(
|
|
draw_functions: Res<DrawFunctions<Transparent3d>>,
|
|
pipeline: Res<LineJointGizmoPipeline>,
|
|
mut pipelines: ResMut<SpecializedRenderPipelines<LineJointGizmoPipeline>>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
msaa: Res<Msaa>,
|
|
line_gizmos: Query<(Entity, &Handle<LineGizmo>, &GizmoMeshConfig)>,
|
|
line_gizmo_assets: Res<RenderAssets<LineGizmo>>,
|
|
mut views: Query<(
|
|
&ExtractedView,
|
|
&mut RenderPhase<Transparent3d>,
|
|
Option<&RenderLayers>,
|
|
(
|
|
Has<NormalPrepass>,
|
|
Has<DepthPrepass>,
|
|
Has<MotionVectorPrepass>,
|
|
Has<DeferredPrepass>,
|
|
),
|
|
)>,
|
|
) {
|
|
let draw_function = draw_functions
|
|
.read()
|
|
.get_id::<DrawLineJointGizmo3d>()
|
|
.unwrap();
|
|
|
|
for (
|
|
view,
|
|
mut transparent_phase,
|
|
render_layers,
|
|
(normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass),
|
|
) in &mut views
|
|
{
|
|
let render_layers = render_layers.copied().unwrap_or_default();
|
|
|
|
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
|
|
| MeshPipelineKey::from_hdr(view.hdr);
|
|
|
|
if normal_prepass {
|
|
view_key |= MeshPipelineKey::NORMAL_PREPASS;
|
|
}
|
|
|
|
if depth_prepass {
|
|
view_key |= MeshPipelineKey::DEPTH_PREPASS;
|
|
}
|
|
|
|
if motion_vector_prepass {
|
|
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
|
|
}
|
|
|
|
if deferred_prepass {
|
|
view_key |= MeshPipelineKey::DEFERRED_PREPASS;
|
|
}
|
|
|
|
for (entity, handle, config) in &line_gizmos {
|
|
if !config.render_layers.intersects(&render_layers) {
|
|
continue;
|
|
}
|
|
|
|
let Some(line_gizmo) = line_gizmo_assets.get(handle) else {
|
|
continue;
|
|
};
|
|
|
|
if !line_gizmo.strip || line_gizmo.joints == GizmoLineJoint::None {
|
|
continue;
|
|
}
|
|
|
|
let pipeline = pipelines.specialize(
|
|
&pipeline_cache,
|
|
&pipeline,
|
|
LineJointGizmoPipelineKey {
|
|
view_key,
|
|
perspective: config.line_perspective,
|
|
joints: line_gizmo.joints,
|
|
},
|
|
);
|
|
|
|
transparent_phase.add(Transparent3d {
|
|
entity,
|
|
draw_function,
|
|
pipeline,
|
|
distance: 0.,
|
|
batch_range: 0..1,
|
|
dynamic_offset: None,
|
|
});
|
|
}
|
|
}
|
|
}
|