Instanced line rendering for gizmos based on bevy_polyline (#8427)

# Objective

Adopt code from
[bevy_polyline](https://github.com/ForesightMiningSoftwareCorporation/bevy_polyline)
for gizmo line-rendering.
This adds configurable width and perspective rendering for the lines.

Many thanks to @mtsr for the initial work on bevy_polyline. Thanks to
@aevyrie for maintaining it, @nicopap for adding the depth_bias feature
and the other
[contributors](https://github.com/ForesightMiningSoftwareCorporation/bevy_polyline/graphs/contributors)
for squashing bugs and keeping bevy_polyline up-to-date.

#### Before

![Before](https://user-images.githubusercontent.com/29694403/232831591-a8e6ed0c-3a09-4413-80fa-74cb8e0d33dd.png)
#### After - with line perspective

![After](https://user-images.githubusercontent.com/29694403/232831692-ba7cbeb7-e63a-4f8e-9b1b-1b80c668f149.png)

Line perspective is not on by default because with perspective there is
no default line width that works for every scene.

<details><summary>After - without line perspective</summary>
<p>

![After - no
perspective](https://user-images.githubusercontent.com/29694403/232836344-0dbfb4c8-09b7-4cf5-95f9-a4c26f38dca3.png)

</p>
</details>

Somewhat unexpectedly, the performance is improved with this PR.
At 200,000 lines in many_gizmos I get ~110 FPS on main and ~200 FPS with
this PR.
I'm guessing this is a CPU side difference as I would expect the
rendering technique to be more expensive on the GPU to some extent, but
I am not entirely sure.

---------

Co-authored-by: Jonas Matser <github@jonasmatser.nl>
Co-authored-by: Aevyrie <aevyrie@gmail.com>
Co-authored-by: Nicola Papale <nico@nicopap.ch>
Co-authored-by: Nicola Papale <nicopap@users.noreply.github.com>
This commit is contained in:
ira 2023-06-13 08:49:47 +02:00 committed by GitHub
parent 3fba34c9e6
commit 001b3eb97c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 652 additions and 336 deletions

View file

@ -8,6 +8,9 @@ repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[features]
webgl = []
[dependencies]
# Bevy
bevy_pbr = { path = "../bevy_pbr", version = "0.11.0-dev", optional = true }

View file

@ -19,36 +19,40 @@
use std::mem;
use bevy_app::{Last, Plugin, Update};
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
use bevy_asset::{load_internal_asset, AddAsset, Assets, Handle, HandleUntyped};
use bevy_core::cast_slice;
use bevy_ecs::{
change_detection::DetectChanges,
component::Component,
entity::Entity,
query::Without,
query::{ROQueryItem, Without},
reflect::ReflectComponent,
schedule::IntoSystemConfigs,
system::{Commands, Query, Res, ResMut, Resource},
world::{FromWorld, World},
system::{
lifetimeless::{Read, SRes},
Commands, Query, Res, ResMut, Resource, SystemParamItem,
},
};
use bevy_math::Mat4;
use bevy_reflect::{
std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect, TypeUuid,
std_traits::ReflectDefault, FromReflect, Reflect, ReflectFromReflect, TypePath, TypeUuid,
};
use bevy_render::{
color::Color,
mesh::Mesh,
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
primitives::Aabb,
render_phase::AddRenderCommand,
render_resource::{PrimitiveTopology, Shader, SpecializedMeshPipelines},
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass},
render_resource::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
BindGroupLayoutEntry, BindingType, Buffer, BufferBindingType, BufferInitDescriptor,
BufferUsages, Shader, ShaderStages, ShaderType, VertexAttribute, VertexBufferLayout,
VertexFormat, VertexStepMode,
},
renderer::RenderDevice,
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::components::{GlobalTransform, Transform};
#[cfg(feature = "bevy_pbr")]
use bevy_pbr::MeshUniform;
#[cfg(feature = "bevy_sprite")]
use bevy_sprite::{Mesh2dHandle, Mesh2dUniform};
pub mod gizmos;
#[cfg(feature = "bevy_sprite")]
@ -74,7 +78,10 @@ impl Plugin for GizmoPlugin {
fn build(&self, app: &mut bevy_app::App) {
load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl);
app.init_resource::<MeshHandles>()
app.add_plugin(UniformComponentPlugin::<LineGizmoUniform>::default())
.add_asset::<LineGizmo>()
.add_plugin(RenderAssetPlugin::<LineGizmo>::default())
.init_resource::<LineGizmoHandles>()
.init_resource::<GizmoConfig>()
.init_resource::<GizmoStorage>()
.add_systems(Last, update_gizmo_meshes)
@ -88,47 +95,35 @@ impl Plugin for GizmoPlugin {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; };
render_app.add_systems(ExtractSchedule, extract_gizmo_data);
render_app
.add_systems(ExtractSchedule, extract_gizmo_data)
.add_systems(Render, queue_line_gizmo_bind_group.in_set(RenderSet::Queue));
#[cfg(feature = "bevy_sprite")]
{
use bevy_core_pipeline::core_2d::Transparent2d;
use pipeline_2d::*;
render_app
.add_render_command::<Transparent2d, DrawGizmoLines>()
.init_resource::<SpecializedMeshPipelines<GizmoLinePipeline>>()
.add_systems(Render, queue_gizmos_2d.in_set(RenderSet::Queue));
}
app.add_plugin(pipeline_2d::LineGizmo2dPlugin);
#[cfg(feature = "bevy_pbr")]
{
use bevy_core_pipeline::core_3d::Opaque3d;
use pipeline_3d::*;
render_app
.add_render_command::<Opaque3d, DrawGizmoLines>()
.init_resource::<SpecializedMeshPipelines<GizmoPipeline>>()
.add_systems(Render, queue_gizmos_3d.in_set(RenderSet::Queue));
}
app.add_plugin(pipeline_3d::LineGizmo3dPlugin);
}
fn finish(&self, app: &mut bevy_app::App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; };
#[cfg(feature = "bevy_sprite")]
{
use pipeline_2d::*;
let render_device = render_app.world.resource::<RenderDevice>();
let layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX,
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: Some(LineGizmoUniform::min_size()),
},
count: None,
}],
label: Some("LineGizmoUniform layout"),
});
render_app.init_resource::<GizmoLinePipeline>();
}
#[cfg(feature = "bevy_pbr")]
{
use pipeline_3d::*;
render_app.init_resource::<GizmoPipeline>();
}
render_app.insert_resource(LineGizmoUniformBindgroupLayout { layout });
}
}
@ -139,12 +134,29 @@ pub struct GizmoConfig {
///
/// Defaults to `true`.
pub enabled: bool,
/// Draw gizmos on top of everything else, ignoring depth.
/// Line width specified in pixels.
///
/// This setting only affects 3D. In 2D, gizmos are always drawn on top.
/// If `line_perspective` is `true` then this is the size in pixels at the camera's near plane.
///
/// Defaults to `2.0`.
pub line_width: f32,
/// Apply perspective to gizmo lines.
///
/// This setting only affects 3D, non-orhographic cameras.
///
/// Defaults to `false`.
pub on_top: bool,
pub line_perspective: bool,
/// How closer to the camera than real geometry the line should be.
///
/// Value between -1 and 1 (inclusive).
/// * 0 means that there is no change to the line position when rendering
/// * 1 means it is furthest away from camera as possible
/// * -1 means that it will always render in front of other things.
///
/// This is typically useful if you are drawing wireframes on top of polygons
/// and your wireframe is z-fighting (flickering on/off) with your main model.
/// You would set this value to a negative number close to 0.0.
pub depth_bias: f32,
/// Configuration for the [`AabbGizmo`].
pub aabb: AabbGizmoConfig,
}
@ -153,7 +165,9 @@ impl Default for GizmoConfig {
fn default() -> Self {
Self {
enabled: true,
on_top: false,
line_width: 2.,
line_perspective: false,
depth_bias: 0.,
aabb: Default::default(),
}
}
@ -227,77 +241,59 @@ fn aabb_transform(aabb: Aabb, transform: GlobalTransform) -> GlobalTransform {
)
}
#[derive(Resource)]
struct MeshHandles {
list: Option<Handle<Mesh>>,
strip: Option<Handle<Mesh>>,
#[derive(Resource, Default)]
struct LineGizmoHandles {
list: Option<Handle<LineGizmo>>,
strip: Option<Handle<LineGizmo>>,
}
impl FromWorld for MeshHandles {
fn from_world(_world: &mut World) -> Self {
MeshHandles {
list: None,
strip: None,
}
}
}
#[derive(Component)]
struct GizmoMesh;
fn update_gizmo_meshes(
mut meshes: ResMut<Assets<Mesh>>,
mut handles: ResMut<MeshHandles>,
mut line_gizmos: ResMut<Assets<LineGizmo>>,
mut handles: ResMut<LineGizmoHandles>,
mut storage: ResMut<GizmoStorage>,
) {
if storage.list_positions.is_empty() {
handles.list = None;
} else if let Some(handle) = handles.list.as_ref() {
let list_mesh = meshes.get_mut(handle).unwrap();
let list = line_gizmos.get_mut(handle).unwrap();
let positions = mem::take(&mut storage.list_positions);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
let colors = mem::take(&mut storage.list_colors);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
list.positions = mem::take(&mut storage.list_positions);
list.colors = mem::take(&mut storage.list_colors);
} else {
let mut list_mesh = Mesh::new(PrimitiveTopology::LineList);
let mut list = LineGizmo {
strip: false,
..Default::default()
};
let positions = mem::take(&mut storage.list_positions);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
list.positions = mem::take(&mut storage.list_positions);
list.colors = mem::take(&mut storage.list_colors);
let colors = mem::take(&mut storage.list_colors);
list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
handles.list = Some(meshes.add(list_mesh));
handles.list = Some(line_gizmos.add(list));
}
if storage.strip_positions.is_empty() {
handles.strip = None;
} else if let Some(handle) = handles.strip.as_ref() {
let strip_mesh = meshes.get_mut(handle).unwrap();
let strip = line_gizmos.get_mut(handle).unwrap();
let positions = mem::take(&mut storage.strip_positions);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
let colors = mem::take(&mut storage.strip_colors);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
strip.positions = mem::take(&mut storage.strip_positions);
strip.colors = mem::take(&mut storage.strip_colors);
} else {
let mut strip_mesh = Mesh::new(PrimitiveTopology::LineStrip);
let mut strip = LineGizmo {
strip: true,
..Default::default()
};
let positions = mem::take(&mut storage.strip_positions);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
strip.positions = mem::take(&mut storage.strip_positions);
strip.colors = mem::take(&mut storage.strip_colors);
let colors = mem::take(&mut storage.strip_colors);
strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
handles.strip = Some(meshes.add(strip_mesh));
handles.strip = Some(line_gizmos.add(strip));
}
}
fn extract_gizmo_data(
mut commands: Commands,
handles: Extract<Res<MeshHandles>>,
handles: Extract<Res<LineGizmoHandles>>,
config: Extract<Res<GizmoConfig>>,
) {
if config.is_changed() {
@ -308,35 +304,206 @@ fn extract_gizmo_data(
return;
}
let transform = Mat4::IDENTITY;
let inverse_transpose_model = transform.inverse().transpose();
commands.spawn_batch(
[handles.list.clone(), handles.strip.clone()]
.into_iter()
.flatten()
.map(move |handle| {
(
GizmoMesh,
#[cfg(feature = "bevy_pbr")]
(
handle.clone_weak(),
MeshUniform {
flags: 0,
transform,
previous_transform: transform,
inverse_transpose_model,
},
),
#[cfg(feature = "bevy_sprite")]
(
Mesh2dHandle(handle),
Mesh2dUniform {
flags: 0,
transform,
inverse_transpose_model,
},
),
)
}),
);
for handle in [&handles.list, &handles.strip].into_iter().flatten() {
commands.spawn((
LineGizmoUniform {
line_width: config.line_width,
depth_bias: config.depth_bias,
#[cfg(feature = "webgl")]
_padding: Default::default(),
},
handle.clone_weak(),
));
}
}
#[derive(Component, ShaderType, Clone, Copy)]
struct LineGizmoUniform {
line_width: f32,
depth_bias: f32,
/// WebGL2 structs must be 16 byte aligned.
#[cfg(feature = "webgl")]
_padding: bevy_math::Vec2,
}
#[derive(Debug, Default, Clone, TypeUuid, TypePath)]
#[uuid = "02b99cbf-bb26-4713-829a-aee8e08dedc0"]
struct LineGizmo {
positions: Vec<[f32; 3]>,
colors: Vec<[f32; 4]>,
/// Whether this gizmo's topology is a line-strip or line-list
strip: bool,
}
#[derive(Debug, Clone)]
struct GpuLineGizmo {
position_buffer: Buffer,
color_buffer: Buffer,
vertex_count: u32,
strip: bool,
}
impl RenderAsset for LineGizmo {
type ExtractedAsset = LineGizmo;
type PreparedAsset = GpuLineGizmo;
type Param = SRes<RenderDevice>;
fn extract_asset(&self) -> Self::ExtractedAsset {
self.clone()
}
fn prepare_asset(
line_gizmo: Self::ExtractedAsset,
render_device: &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self::ExtractedAsset>> {
let position_buffer_data = cast_slice(&line_gizmo.positions);
let position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
usage: BufferUsages::VERTEX,
label: Some("LineGizmo Position Buffer"),
contents: position_buffer_data,
});
let color_buffer_data = cast_slice(&line_gizmo.colors);
let color_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
usage: BufferUsages::VERTEX,
label: Some("LineGizmo Color Buffer"),
contents: color_buffer_data,
});
Ok(GpuLineGizmo {
position_buffer,
color_buffer,
vertex_count: line_gizmo.positions.len() as u32,
strip: line_gizmo.strip,
})
}
}
#[derive(Resource)]
struct LineGizmoUniformBindgroupLayout {
layout: BindGroupLayout,
}
#[derive(Resource)]
struct LineGizmoUniformBindgroup {
bindgroup: BindGroup,
}
fn queue_line_gizmo_bind_group(
mut commands: Commands,
line_gizmo_uniform_layout: Res<LineGizmoUniformBindgroupLayout>,
render_device: Res<RenderDevice>,
line_gizmo_uniforms: Res<ComponentUniforms<LineGizmoUniform>>,
) {
if let Some(binding) = line_gizmo_uniforms.uniforms().binding() {
commands.insert_resource(LineGizmoUniformBindgroup {
bindgroup: render_device.create_bind_group(&BindGroupDescriptor {
entries: &[BindGroupEntry {
binding: 0,
resource: binding,
}],
label: Some("LineGizmoUniform bindgroup"),
layout: &line_gizmo_uniform_layout.layout,
}),
});
}
}
struct SetLineGizmoBindGroup<const I: usize>;
impl<const I: usize, P: PhaseItem> RenderCommand<P> for SetLineGizmoBindGroup<I> {
type ViewWorldQuery = ();
type ItemWorldQuery = Read<DynamicUniformIndex<LineGizmoUniform>>;
type Param = SRes<LineGizmoUniformBindgroup>;
#[inline]
fn render<'w>(
_item: &P,
_view: ROQueryItem<'w, Self::ViewWorldQuery>,
uniform_index: ROQueryItem<'w, Self::ItemWorldQuery>,
bind_group: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
pass.set_bind_group(
I,
&bind_group.into_inner().bindgroup,
&[uniform_index.index()],
);
RenderCommandResult::Success
}
}
struct DrawLineGizmo;
impl<P: PhaseItem> RenderCommand<P> for DrawLineGizmo {
type ViewWorldQuery = ();
type ItemWorldQuery = Read<Handle<LineGizmo>>;
type Param = SRes<RenderAssets<LineGizmo>>;
#[inline]
fn render<'w>(
_item: &P,
_view: ROQueryItem<'w, Self::ViewWorldQuery>,
handle: ROQueryItem<'w, Self::ItemWorldQuery>,
line_gizmos: SystemParamItem<'w, '_, Self::Param>,
pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult {
let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else {
return RenderCommandResult::Failure;
};
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..));
pass.set_vertex_buffer(1, line_gizmo.color_buffer.slice(..));
let instances = if line_gizmo.strip {
u32::max(line_gizmo.vertex_count, 1) - 1
} else {
line_gizmo.vertex_count / 2
};
pass.draw(0..6, 0..instances);
RenderCommandResult::Success
}
}
fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
let stride_multiplier = if strip { 1 } else { 2 };
use VertexFormat::*;
vec![
// Positions
VertexBufferLayout {
array_stride: Float32x3.size() * stride_multiplier,
step_mode: VertexStepMode::Instance,
attributes: vec![
VertexAttribute {
format: Float32x3,
offset: 0,
shader_location: 0,
},
VertexAttribute {
format: Float32x3,
offset: Float32x3.size(),
shader_location: 1,
},
],
},
// Colors
VertexBufferLayout {
array_stride: Float32x4.size() * stride_multiplier,
step_mode: VertexStepMode::Instance,
attributes: vec![
VertexAttribute {
format: Float32x4,
offset: 0,
shader_location: 2,
},
VertexAttribute {
format: Float32x4,
offset: Float32x4.size(),
shader_location: 3,
},
],
},
]
}

View file

@ -1,48 +1,105 @@
#ifdef GIZMO_LINES_3D
#ifdef GIZMO_3D
#import bevy_pbr::mesh_view_bindings
#else
#import bevy_sprite::mesh2d_view_bindings
#endif
struct VertexInput {
@location(0) pos: vec3<f32>,
@location(1) color: vec4<f32>,
struct LineGizmoUniform {
line_width: f32,
depth_bias: f32,
#ifdef SIXTEEN_BYTE_ALIGNMENT
// WebGL2 structs must be 16 byte aligned.
_padding: vec2<f32>,
#endif
}
@group(1) @binding(0)
var<uniform> line_gizmo: LineGizmoUniform;
struct VertexInput {
@location(0) position_a: vec3<f32>,
@location(1) position_b: vec3<f32>,
@location(2) color_a: vec4<f32>,
@location(3) color_b: vec4<f32>,
@builtin(vertex_index) index: u32,
};
struct VertexOutput {
@builtin(position) pos: vec4<f32>,
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
}
struct FragmentOutput {
#ifdef GIZMO_LINES_3D
@builtin(frag_depth) depth: f32,
#endif
@location(0) color: vec4<f32>,
}
};
@vertex
fn vertex(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
fn vertex(vertex: VertexInput) -> VertexOutput {
var positions = array<vec3<f32>, 6>(
vec3(0., -0.5, 0.),
vec3(0., -0.5, 1.),
vec3(0., 0.5, 1.),
vec3(0., -0.5, 0.),
vec3(0., 0.5, 1.),
vec3(0., 0.5, 0.)
);
let position = positions[vertex.index];
out.pos = view.view_proj * vec4<f32>(in.pos, 1.0);
out.color = in.color;
// algorithm based on https://wwwtyro.net/2019/11/18/instanced-lines.html
let clip_a = view.view_proj * vec4(vertex.position_a, 1.);
let clip_b = view.view_proj * vec4(vertex.position_b, 1.);
let clip = mix(clip_a, clip_b, position.z);
return out;
let resolution = view.viewport.zw;
let screen_a = resolution * (0.5 * clip_a.xy / clip_a.w + 0.5);
let screen_b = resolution * (0.5 * clip_b.xy / clip_b.w + 0.5);
let x_basis = normalize(screen_a - screen_b);
let y_basis = vec2(-x_basis.y, x_basis.x);
var color = mix(vertex.color_a, vertex.color_b, position.z);
var line_width = line_gizmo.line_width;
var alpha = 1.;
#ifdef PERSPECTIVE
line_width /= clip.w;
#endif
// Line thinness fade from https://acegikmo.com/shapes/docs/#anti-aliasing
if line_width < 1. {
color.a *= line_width;
line_width = 1.;
}
let offset = line_width * (position.x * x_basis + position.y * y_basis);
let screen = mix(screen_a, screen_b, position.z) + offset;
var depth: f32;
if line_gizmo.depth_bias >= 0. {
depth = clip.z * (1. - line_gizmo.depth_bias);
} else {
let epsilon = 4.88e-04;
// depth * (clip.w / depth)^-depth_bias. So that when -depth_bias is 1.0, this is equal to clip.w
// and when equal to 0.0, it is exactly equal to depth.
// the epsilon is here to prevent the depth from exceeding clip.w when -depth_bias = 1.0
// clip.w represents the near plane in homogenous clip space in bevy, having a depth
// of this value means nothing can be in front of this
// The reason this uses an exponential function is that it makes it much easier for the
// user to chose a value that is convinient for them
depth = clip.z * exp2(-line_gizmo.depth_bias * log2(clip.w / clip.z - epsilon));
}
var clip_position = vec4(clip.w * ((2. * screen) / resolution - 1.), depth, clip.w);
return VertexOutput(clip_position, color);
}
struct FragmentInput {
@location(0) color: vec4<f32>,
};
struct FragmentOutput {
@location(0) color: vec4<f32>,
};
@fragment
fn fragment(in: VertexOutput) -> FragmentOutput {
var out: FragmentOutput;
#ifdef GIZMO_LINES_3D
#ifdef DEPTH_TEST
out.depth = in.pos.z;
#else
out.depth = 1.0;
#endif
#endif
out.color = in.color;
return out;
fn fragment(in: FragmentInput) -> FragmentOutput {
return FragmentOutput(in.color);
}

View file

@ -1,68 +1,101 @@
use crate::{
line_gizmo_vertex_buffer_layouts, DrawLineGizmo, LineGizmo, LineGizmoUniformBindgroupLayout,
SetLineGizmoBindGroup, LINE_SHADER_HANDLE,
};
use bevy_app::{App, Plugin};
use bevy_asset::Handle;
use bevy_core_pipeline::core_2d::Transparent2d;
use bevy_ecs::{
prelude::Entity,
query::With,
schedule::IntoSystemConfigs,
system::{Query, Res, ResMut, Resource},
world::{FromWorld, World},
};
use bevy_render::{
mesh::{Mesh, MeshVertexBufferLayout},
render_asset::RenderAssets,
render_phase::{DrawFunctions, RenderPhase, SetItemPipeline},
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
render_resource::*,
texture::BevyDefault,
view::{ExtractedView, Msaa, ViewTarget},
Render, RenderApp, RenderSet,
};
use bevy_sprite::*;
use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup};
use bevy_utils::FloatOrd;
use crate::{GizmoMesh, LINE_SHADER_HANDLE};
pub struct LineGizmo2dPlugin;
#[derive(Resource)]
pub(crate) struct GizmoLinePipeline {
mesh_pipeline: Mesh2dPipeline,
shader: Handle<Shader>,
impl Plugin for LineGizmo2dPlugin {
fn build(&self, app: &mut App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return };
render_app
.add_render_command::<Transparent2d, DrawLineGizmo2d>()
.init_resource::<SpecializedRenderPipelines<LineGizmoPipeline>>()
.add_systems(Render, queue_line_gizmos_2d.in_set(RenderSet::Queue));
}
fn finish(&self, app: &mut App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return };
render_app.init_resource::<LineGizmoPipeline>();
}
}
impl FromWorld for GizmoLinePipeline {
#[derive(Clone, Resource)]
struct LineGizmoPipeline {
mesh_pipeline: Mesh2dPipeline,
uniform_layout: BindGroupLayout,
}
impl FromWorld for LineGizmoPipeline {
fn from_world(render_world: &mut World) -> Self {
GizmoLinePipeline {
LineGizmoPipeline {
mesh_pipeline: render_world.resource::<Mesh2dPipeline>().clone(),
shader: LINE_SHADER_HANDLE.typed(),
uniform_layout: render_world
.resource::<LineGizmoUniformBindgroupLayout>()
.layout
.clone(),
}
}
}
impl SpecializedMeshPipeline for GizmoLinePipeline {
type Key = Mesh2dPipelineKey;
#[derive(PartialEq, Eq, Hash, Clone)]
struct LineGizmoPipelineKey {
mesh_key: Mesh2dPipelineKey,
strip: bool,
}
fn specialize(
&self,
key: Self::Key,
layout: &MeshVertexBufferLayout,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let vertex_buffer_layout = layout.get_layout(&[
Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
Mesh::ATTRIBUTE_COLOR.at_shader_location(1),
])?;
impl SpecializedRenderPipeline for LineGizmoPipeline {
type Key = LineGizmoPipelineKey;
let format = if key.contains(Mesh2dPipelineKey::HDR) {
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let format = if key.mesh_key.contains(Mesh2dPipelineKey::HDR) {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
};
Ok(RenderPipelineDescriptor {
let shader_defs = vec![
#[cfg(feature = "webgl")]
"SIXTEEN_BYTE_ALIGNMENT".into(),
];
let layout = vec![
self.mesh_pipeline.view_layout.clone(),
self.uniform_layout.clone(),
];
RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone_weak(),
shader: LINE_SHADER_HANDLE.typed(),
entry_point: "vertex".into(),
shader_defs: vec![],
buffers: vec![vertex_buffer_layout],
shader_defs: shader_defs.clone(),
buffers: line_gizmo_vertex_buffer_layouts(key.strip),
},
fragment: Some(FragmentState {
shader: self.shader.clone_weak(),
shader_defs: vec![],
shader: LINE_SHADER_HANDLE.typed(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format,
@ -70,57 +103,61 @@ impl SpecializedMeshPipeline for GizmoLinePipeline {
write_mask: ColorWrites::ALL,
})],
}),
layout: vec![self.mesh_pipeline.view_layout.clone()],
primitive: PrimitiveState {
topology: key.primitive_topology(),
..Default::default()
},
layout,
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState {
count: key.msaa_samples(),
count: key.mesh_key.msaa_samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some("LineGizmo Pipeline 2D".into()),
push_constant_ranges: vec![],
label: Some("gizmo_2d_pipeline".into()),
})
}
}
}
pub(crate) type DrawGizmoLines = (
type DrawLineGizmo2d = (
SetItemPipeline,
SetMesh2dViewBindGroup<0>,
SetMesh2dBindGroup<1>,
DrawMesh2d,
SetLineGizmoBindGroup<1>,
DrawLineGizmo,
);
#[allow(clippy::too_many_arguments)]
pub(crate) fn queue_gizmos_2d(
fn queue_line_gizmos_2d(
draw_functions: Res<DrawFunctions<Transparent2d>>,
pipeline: Res<GizmoLinePipeline>,
pipeline: Res<LineGizmoPipeline>,
mut pipelines: ResMut<SpecializedRenderPipelines<LineGizmoPipeline>>,
pipeline_cache: Res<PipelineCache>,
mut specialized_pipelines: ResMut<SpecializedMeshPipelines<GizmoLinePipeline>>,
gpu_meshes: Res<RenderAssets<Mesh>>,
msaa: Res<Msaa>,
mesh_handles: Query<(Entity, &Mesh2dHandle), With<GizmoMesh>>,
line_gizmos: Query<(Entity, &Handle<LineGizmo>)>,
line_gizmo_assets: Res<RenderAssets<LineGizmo>>,
mut views: Query<(&ExtractedView, &mut RenderPhase<Transparent2d>)>,
) {
let draw_function = draw_functions.read().get_id::<DrawGizmoLines>().unwrap();
let key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples());
for (view, mut phase) in &mut views {
let key = key | Mesh2dPipelineKey::from_hdr(view.hdr);
for (entity, mesh_handle) in &mesh_handles {
let Some(mesh) = gpu_meshes.get(&mesh_handle.0) else { continue; };
let draw_function = draw_functions.read().get_id::<DrawLineGizmo2d>().unwrap();
let key = key | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology);
let pipeline = specialized_pipelines
.specialize(&pipeline_cache, &pipeline, key, &mesh.layout)
.unwrap();
phase.add(Transparent2d {
for (view, mut transparent_phase) in &mut views {
let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples())
| Mesh2dPipelineKey::from_hdr(view.hdr);
for (entity, handle) in &line_gizmos {
let Some(line_gizmo) = line_gizmo_assets.get(handle) else { continue };
let pipeline = pipelines.specialize(
&pipeline_cache,
&pipeline,
LineGizmoPipelineKey {
mesh_key,
strip: line_gizmo.strip,
},
);
transparent_phase.add(Transparent2d {
entity,
draw_function,
pipeline,
sort_key: FloatOrd(f32::MAX),
sort_key: FloatOrd(0.),
batch_range: None,
});
}

View file

@ -1,53 +1,83 @@
use crate::{
line_gizmo_vertex_buffer_layouts, DrawLineGizmo, GizmoConfig, LineGizmo,
LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_SHADER_HANDLE,
};
use bevy_app::{App, Plugin};
use bevy_asset::Handle;
use bevy_core_pipeline::core_3d::Opaque3d;
use bevy_core_pipeline::core_3d::Transparent3d;
use bevy_ecs::{
entity::Entity,
query::With,
prelude::Entity,
schedule::IntoSystemConfigs,
system::{Query, Res, ResMut, Resource},
world::{FromWorld, World},
};
use bevy_pbr::*;
use bevy_render::{
mesh::Mesh,
render_resource::Shader,
view::{ExtractedView, ViewTarget},
use bevy_pbr::{
MeshPipeline, MeshPipelineKey, SetMeshViewBindGroup, MAX_CASCADES_PER_LIGHT,
MAX_DIRECTIONAL_LIGHTS,
};
use bevy_render::{
mesh::MeshVertexBufferLayout,
render_asset::RenderAssets,
render_phase::{DrawFunctions, RenderPhase, SetItemPipeline},
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
render_resource::*,
texture::BevyDefault,
view::Msaa,
view::{ExtractedView, Msaa, ViewTarget},
Render, RenderApp, RenderSet,
};
use crate::{GizmoConfig, GizmoMesh, LINE_SHADER_HANDLE};
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 };
#[derive(Resource)]
pub(crate) struct GizmoPipeline {
mesh_pipeline: MeshPipeline,
shader: Handle<Shader>,
render_app
.add_render_command::<Transparent3d, DrawLineGizmo3d>()
.init_resource::<SpecializedRenderPipelines<LineGizmoPipeline>>()
.add_systems(Render, queue_line_gizmos_3d.in_set(RenderSet::Queue));
}
fn finish(&self, app: &mut App) {
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return };
render_app.init_resource::<LineGizmoPipeline>();
}
}
impl FromWorld for GizmoPipeline {
#[derive(Clone, Resource)]
struct LineGizmoPipeline {
mesh_pipeline: MeshPipeline,
uniform_layout: BindGroupLayout,
}
impl FromWorld for LineGizmoPipeline {
fn from_world(render_world: &mut World) -> Self {
GizmoPipeline {
LineGizmoPipeline {
mesh_pipeline: render_world.resource::<MeshPipeline>().clone(),
shader: LINE_SHADER_HANDLE.typed(),
uniform_layout: render_world
.resource::<LineGizmoUniformBindgroupLayout>()
.layout
.clone(),
}
}
}
impl SpecializedMeshPipeline for GizmoPipeline {
type Key = (bool, MeshPipelineKey);
#[derive(PartialEq, Eq, Hash, Clone)]
struct LineGizmoPipelineKey {
mesh_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![
"GIZMO_3D".into(),
#[cfg(feature = "webgl")]
"SIXTEEN_BYTE_ALIGNMENT".into(),
];
fn specialize(
&self,
(depth_test, key): Self::Key,
layout: &MeshVertexBufferLayout,
) -> Result<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
let mut shader_defs = Vec::new();
shader_defs.push("GIZMO_LINES_3D".into());
shader_defs.push(ShaderDefVal::Int(
"MAX_DIRECTIONAL_LIGHTS".to_string(),
MAX_DIRECTIONAL_LIGHTS as i32,
@ -56,109 +86,106 @@ impl SpecializedMeshPipeline for GizmoPipeline {
"MAX_CASCADES_PER_LIGHT".to_string(),
MAX_CASCADES_PER_LIGHT as i32,
));
if depth_test {
shader_defs.push("DEPTH_TEST".into());
if key.perspective {
shader_defs.push("PERSPECTIVE".into());
}
let vertex_buffer_layout = layout.get_layout(&[
Mesh::ATTRIBUTE_POSITION.at_shader_location(0),
Mesh::ATTRIBUTE_COLOR.at_shader_location(1),
])?;
let bind_group_layout = match key.msaa_samples() {
1 => vec![self.mesh_pipeline.view_layout.clone()],
_ => {
shader_defs.push("MULTISAMPLED".into());
vec![self.mesh_pipeline.view_layout_multisampled.clone()]
}
};
let format = if key.contains(MeshPipelineKey::HDR) {
let format = if key.mesh_key.contains(MeshPipelineKey::HDR) {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
};
Ok(RenderPipelineDescriptor {
let view_layout = if key.mesh_key.msaa_samples() == 1 {
self.mesh_pipeline.view_layout.clone()
} else {
self.mesh_pipeline.view_layout_multisampled.clone()
};
let layout = vec![view_layout, self.uniform_layout.clone()];
RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone_weak(),
shader: LINE_SHADER_HANDLE.typed(),
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_buffer_layout],
buffers: line_gizmo_vertex_buffer_layouts(key.strip),
},
fragment: Some(FragmentState {
shader: self.shader.clone_weak(),
shader: LINE_SHADER_HANDLE.typed(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format,
blend: None,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
layout: bind_group_layout,
primitive: PrimitiveState {
topology: key.primitive_topology(),
..Default::default()
},
layout,
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: CompareFunction::Greater,
stencil: Default::default(),
bias: Default::default(),
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
multisample: MultisampleState {
count: key.msaa_samples(),
count: key.mesh_key.msaa_samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
label: Some("LineGizmo Pipeline".into()),
push_constant_ranges: vec![],
label: Some("gizmo_3d_pipeline".into()),
})
}
}
pub(crate) type DrawGizmoLines = (
SetItemPipeline,
SetMeshViewBindGroup<0>,
SetMeshBindGroup<1>,
DrawMesh,
);
#[allow(clippy::too_many_arguments)]
pub(crate) fn queue_gizmos_3d(
draw_functions: Res<DrawFunctions<Opaque3d>>,
pipeline: Res<GizmoPipeline>,
mut pipelines: ResMut<SpecializedMeshPipelines<GizmoPipeline>>,
pipeline_cache: Res<PipelineCache>,
render_meshes: Res<RenderAssets<Mesh>>,
msaa: Res<Msaa>,
mesh_handles: Query<(Entity, &Handle<Mesh>), With<GizmoMesh>>,
config: Res<GizmoConfig>,
mut views: Query<(&ExtractedView, &mut RenderPhase<Opaque3d>)>,
) {
let draw_function = draw_functions.read().get_id::<DrawGizmoLines>().unwrap();
let key = MeshPipelineKey::from_msaa_samples(msaa.samples());
for (view, mut phase) in &mut views {
let key = key | MeshPipelineKey::from_hdr(view.hdr);
for (entity, mesh_handle) in &mesh_handles {
if let Some(mesh) = render_meshes.get(mesh_handle) {
let key = key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology);
let pipeline = pipelines
.specialize(
&pipeline_cache,
&pipeline,
(!config.on_top, key),
&mesh.layout,
)
.unwrap();
phase.add(Opaque3d {
entity,
pipeline,
draw_function,
distance: 0.,
});
}
}
}
}
type DrawLineGizmo3d = (
SetItemPipeline,
SetMeshViewBindGroup<0>,
SetLineGizmoBindGroup<1>,
DrawLineGizmo,
);
#[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>,
config: Res<GizmoConfig>,
line_gizmos: Query<(Entity, &Handle<LineGizmo>)>,
line_gizmo_assets: Res<RenderAssets<LineGizmo>>,
mut views: Query<(&ExtractedView, &mut RenderPhase<Transparent3d>)>,
) {
let draw_function = draw_functions.read().get_id::<DrawLineGizmo3d>().unwrap();
for (view, mut transparent_phase) in &mut views {
let mesh_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
| MeshPipelineKey::from_hdr(view.hdr);
for (entity, handle) in &line_gizmos {
let Some(line_gizmo) = line_gizmo_assets.get(handle) else { continue };
let pipeline = pipelines.specialize(
&pipeline_cache,
&pipeline,
LineGizmoPipelineKey {
mesh_key,
strip: line_gizmo.strip,
perspective: config.line_perspective,
},
);
transparent_phase.add(Transparent3d {
entity,
draw_function,
pipeline,
distance: 0.,
});
}
}
}

View file

@ -75,7 +75,7 @@ x11 = ["bevy_winit/x11"]
subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"]
# Optimise for WebGL2
webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"]
webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl", "bevy_gizmos?/webgl"]
# enable systems that allow for automated testing on CI
bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_time/bevy_ci_testing", "bevy_render?/bevy_ci_testing", "bevy_render?/ci_limits"]

View file

@ -8,12 +8,21 @@ fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, system)
.add_systems(Update, (system, update_config))
.run();
}
fn setup(mut commands: Commands) {
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
// text
commands.spawn(TextBundle::from_section(
"Hold 'Left' or 'Right' to change the line width",
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 24.,
color: Color::WHITE,
},
));
}
fn system(mut gizmos: Gizmos, time: Res<Time>) {
@ -45,3 +54,12 @@ fn system(mut gizmos: Gizmos, time: Res<Time>) {
// 1 and 32, using the arc length as scalar.
gizmos.arc_2d(Vec2::ZERO, sin / 10., PI / 2., 350., Color::ORANGE_RED);
}
fn update_config(mut config: ResMut<GizmoConfig>, keyboard: Res<Input<KeyCode>>, time: Res<Time>) {
if keyboard.pressed(KeyCode::Right) {
config.line_width += 5. * time.delta_seconds();
}
if keyboard.pressed(KeyCode::Left) {
config.line_width -= 5. * time.delta_seconds();
}
}

View file

@ -1,4 +1,5 @@
//! This example demonstrates Bevy's immediate mode drawing API intended for visual debugging.
use std::f32::consts::PI;
use bevy::prelude::*;
@ -47,7 +48,9 @@ fn setup(
// example instructions
commands.spawn(
TextBundle::from_section(
"Press 't' to toggle drawing gizmos on top of everything else in the scene",
"Press 'D' to toggle drawing gizmos on top of everything else in the scene\n\
Press 'P' to toggle perspective for line gizmos\n\
Hold 'Left' or 'Right' to change the line width",
TextStyle {
font_size: 20.,
..default()
@ -64,7 +67,7 @@ fn setup(
fn system(mut gizmos: Gizmos, time: Res<Time>) {
gizmos.cuboid(
Transform::from_translation(Vec3::Y * -0.5).with_scale(Vec3::new(5., 1., 2.)),
Transform::from_translation(Vec3::Y * 0.5).with_scale(Vec3::splat(1.)),
Color::BLACK,
);
gizmos.rect(
@ -74,12 +77,7 @@ fn system(mut gizmos: Gizmos, time: Res<Time>) {
Color::GREEN,
);
gizmos.sphere(
Vec3::new(1., 0.5, 0.),
Quat::IDENTITY,
0.5,
Color::RED.with_a(0.5),
);
gizmos.sphere(Vec3::new(1., 0.5, 0.), Quat::IDENTITY, 0.5, Color::RED);
for y in [0., 0.5, 1.] {
gizmos.ray(
@ -106,8 +104,21 @@ fn rotate_camera(mut query: Query<&mut Transform, With<Camera>>, time: Res<Time>
transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(time.delta_seconds() / 2.));
}
fn update_config(mut gizmo_config: ResMut<GizmoConfig>, keyboard: Res<Input<KeyCode>>) {
if keyboard.just_pressed(KeyCode::T) {
gizmo_config.on_top = !gizmo_config.on_top;
fn update_config(mut config: ResMut<GizmoConfig>, keyboard: Res<Input<KeyCode>>, time: Res<Time>) {
if keyboard.just_pressed(KeyCode::D) {
config.depth_bias = if config.depth_bias == 0. { -1. } else { 0. };
}
if keyboard.just_pressed(KeyCode::P) {
// Toggle line_perspective
config.line_perspective ^= true;
// Increase the line width when line_perspective is on
config.line_width *= if config.line_perspective { 5. } else { 1. / 5. };
}
if keyboard.pressed(KeyCode::Right) {
config.line_width += 5. * time.delta_seconds();
}
if keyboard.pressed(KeyCode::Left) {
config.line_width -= 5. * time.delta_seconds();
}
}

View file

@ -23,10 +23,6 @@ fn main() {
line_count: 50_000,
fancy: false,
})
.insert_resource(GizmoConfig {
on_top: false,
..default()
})
.add_systems(Startup, setup)
.add_systems(Update, (input, ui_system));