Immediate Mode Line/Gizmo Drawing (#6529)

# Objective
Add a convenient immediate mode drawing API for visual debugging.

Fixes #5619
Alternative to #1625
Partial alternative to #5734

Based off https://github.com/Toqozz/bevy_debug_lines with some changes:
 * Simultaneous support for 2D and 3D.
 * Methods for basic shapes; circles, spheres, rectangles, boxes, etc.
 * 2D methods.
 * Removed durations. Seemed niche, and can be handled by users.

<details>
<summary>Performance</summary>

Stress tested using Bevy's recommended optimization settings for the dev
profile with the
following command.
```bash
cargo run --example many_debug_lines \
    --config "profile.dev.package.\"*\".opt-level=3" \
    --config "profile.dev.opt-level=1"
```
I dipped to 65-70 FPS at 300,000 lines
CPU: 3700x
RAM Speed: 3200 Mhz
GPU: 2070 super - probably not very relevant, mostly cpu/memory bound

</details>

<details>
<summary>Fancy bloom screenshot</summary>


![Screenshot_20230207_155033](https://user-images.githubusercontent.com/29694403/217291980-f1e0500e-7a14-4131-8c96-eaaaf52596ae.png)

</details>

## Changelog
 * Added `GizmoPlugin`
 * Added `Gizmos` system parameter for drawing lines and wireshapes.

### TODO
- [ ] Update changelog
- [x] Update performance numbers
- [x] Add credit to PR description

### Future work
- Cache rendering primitives instead of constructing them out of line
segments each frame.
- Support for drawing solid meshes
- Interactions. (See
[bevy_mod_gizmos](https://github.com/LiamGallagher737/bevy_mod_gizmos))
- Fancier line drawing. (See
[bevy_polyline](https://github.com/ForesightMiningSoftwareCorporation/bevy_polyline))
- Support for `RenderLayers`
- Display gizmos for a certain duration. Currently everything displays
for one frame (ie. immediate mode)
- Changing settings per drawn item like drawing on top or drawing to
different `RenderLayers`

Co-Authored By: @lassade <felipe.jorge.pereira@gmail.com>
Co-Authored By: @The5-1 <agaku@hotmail.de> 
Co-Authored By: @Toqozz <toqoz@hotmail.com>
Co-Authored By: @nicopap <nico@nicopap.ch>

---------

Co-authored-by: Robert Swain <robert.swain@gmail.com>
Co-authored-by: IceSentry <c.giguere42@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
ira 2023-03-20 21:57:54 +01:00 committed by GitHub
parent d58ed67fa4
commit 6a85eb3d7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1205 additions and 1 deletions

View file

@ -49,6 +49,7 @@ default = [
"vorbis",
"x11",
"filesystem_watcher",
"bevy_gizmos",
"android_shared_stdcxx",
"tonemapping_luts",
]
@ -98,6 +99,9 @@ bevy_ui = ["bevy_internal/bevy_ui", "bevy_core_pipeline", "bevy_text", "bevy_spr
# winit window and input backend
bevy_winit = ["bevy_internal/bevy_winit"]
# Adds support for rendering gizmos
bevy_gizmos = ["bevy_internal/bevy_gizmos"]
# Tracing support, saving a file in Chrome Tracing format
trace_chrome = ["trace", "bevy_internal/trace_chrome"]
@ -309,6 +313,16 @@ description = "Renders a rectangle, circle, and hexagon"
category = "2D Rendering"
wasm = true
[[example]]
name = "2d_gizmos"
path = "examples/2d/2d_gizmos.rs"
[package.metadata.example.2d_gizmos]
name = "2D Gizmos"
description = "A scene showcasing 2D gizmos"
category = "2D Rendering"
wasm = true
[[example]]
name = "sprite"
path = "examples/2d/sprite.rs"
@ -400,6 +414,16 @@ description = "A scene showcasing the built-in 3D shapes"
category = "3D Rendering"
wasm = true
[[example]]
name = "3d_gizmos"
path = "examples/3d/3d_gizmos.rs"
[package.metadata.example.3d_gizmos]
name = "3D Gizmos"
description = "A scene showcasing 3D gizmos"
category = "3D Rendering"
wasm = true
[[example]]
name = "atmospheric_fog"
path = "examples/3d/atmospheric_fog.rs"
@ -1553,6 +1577,16 @@ description = "Simple benchmark to test per-entity draw overhead. Run with the `
category = "Stress Tests"
wasm = true
[[example]]
name = "many_gizmos"
path = "examples/stress_tests/many_gizmos.rs"
[package.metadata.example.many_gizmos]
name = "Many Gizmos"
description = "Test rendering of many gizmos"
category = "Stress Tests"
wasm = true
[[example]]
name = "many_foxes"
path = "examples/stress_tests/many_foxes.rs"

View file

@ -0,0 +1,23 @@
[package]
name = "bevy_gizmos"
version = "0.11.0-dev"
edition = "2021"
description = "Provides gizmos for Bevy Engine"
homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[dependencies]
# Bevy
bevy_pbr = { path = "../bevy_pbr", version = "0.11.0-dev", optional = true }
bevy_sprite = { path = "../bevy_sprite", version = "0.11.0-dev", optional = true }
bevy_app = { path = "../bevy_app", version = "0.11.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.11.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.11.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.11.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.11.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.11.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.11.0-dev" }

View file

@ -0,0 +1,349 @@
use std::{f32::consts::TAU, iter};
use bevy_ecs::{
system::{Deferred, Resource, SystemBuffer, SystemMeta},
world::World,
};
use bevy_math::{Mat2, Quat, Vec2, Vec3};
use bevy_render::prelude::Color;
type PositionItem = [f32; 3];
type ColorItem = [f32; 4];
const DEFAULT_CIRCLE_SEGMENTS: usize = 32;
#[derive(Resource, Default)]
pub(crate) struct GizmoStorage {
pub list_positions: Vec<PositionItem>,
pub list_colors: Vec<ColorItem>,
pub strip_positions: Vec<PositionItem>,
pub strip_colors: Vec<ColorItem>,
}
pub type Gizmos<'s> = Deferred<'s, GizmoBuffer>;
#[derive(Default)]
pub struct GizmoBuffer {
list_positions: Vec<PositionItem>,
list_colors: Vec<ColorItem>,
strip_positions: Vec<PositionItem>,
strip_colors: Vec<ColorItem>,
}
impl SystemBuffer for GizmoBuffer {
fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) {
let mut storage = world.resource_mut::<GizmoStorage>();
storage.list_positions.append(&mut self.list_positions);
storage.list_colors.append(&mut self.list_colors);
storage.strip_positions.append(&mut self.strip_positions);
storage.strip_colors.append(&mut self.strip_colors);
}
}
impl GizmoBuffer {
#[inline]
pub fn line(&mut self, start: Vec3, end: Vec3, color: Color) {
self.extend_list_positions([start, end]);
self.add_list_color(color, 2);
}
/// Draw a line from `start` to `end`.
#[inline]
pub fn line_gradient(&mut self, start: Vec3, end: Vec3, start_color: Color, end_color: Color) {
self.extend_list_positions([start, end]);
self.extend_list_colors([start_color, end_color]);
}
/// Draw a line from `start` to `start + vector`.
#[inline]
pub fn ray(&mut self, start: Vec3, vector: Vec3, color: Color) {
self.line(start, start + vector, color);
}
/// Draw a line from `start` to `start + vector`.
#[inline]
pub fn ray_gradient(
&mut self,
start: Vec3,
vector: Vec3,
start_color: Color,
end_color: Color,
) {
self.line_gradient(start, start + vector, start_color, end_color);
}
#[inline]
pub fn linestrip(&mut self, positions: impl IntoIterator<Item = Vec3>, color: Color) {
self.extend_strip_positions(positions.into_iter());
self.strip_colors
.resize(self.strip_positions.len() - 1, color.as_linear_rgba_f32());
self.strip_colors.push([f32::NAN; 4]);
}
#[inline]
pub fn linestrip_gradient(&mut self, points: impl IntoIterator<Item = (Vec3, Color)>) {
let points = points.into_iter();
let (min, _) = points.size_hint();
self.strip_positions.reserve(min);
self.strip_colors.reserve(min);
for (position, color) in points {
self.strip_positions.push(position.to_array());
self.strip_colors.push(color.as_linear_rgba_f32());
}
self.strip_positions.push([f32::NAN; 3]);
self.strip_colors.push([f32::NAN; 4]);
}
/// Draw a circle at `position` with the flat side facing `normal`.
#[inline]
pub fn circle(
&mut self,
position: Vec3,
normal: Vec3,
radius: f32,
color: Color,
) -> CircleBuilder {
CircleBuilder {
buffer: self,
position,
normal,
radius,
color,
segments: DEFAULT_CIRCLE_SEGMENTS,
}
}
/// Draw a sphere.
#[inline]
pub fn sphere(
&mut self,
position: Vec3,
rotation: Quat,
radius: f32,
color: Color,
) -> SphereBuilder {
SphereBuilder {
buffer: self,
position,
rotation,
radius,
color,
circle_segments: DEFAULT_CIRCLE_SEGMENTS,
}
}
/// Draw a rectangle.
#[inline]
pub fn rect(&mut self, position: Vec3, rotation: Quat, size: Vec2, color: Color) {
let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2.extend(0.));
self.linestrip([tl, tr, br, bl, tl], color);
}
/// Draw a box.
#[inline]
pub fn cuboid(&mut self, position: Vec3, rotation: Quat, size: Vec3, color: Color) {
let rect = rect_inner(size.truncate());
// Front
let [tlf, trf, brf, blf] = rect.map(|vec2| position + rotation * vec2.extend(size.z / 2.));
// Back
let [tlb, trb, brb, blb] = rect.map(|vec2| position + rotation * vec2.extend(-size.z / 2.));
let positions = [
tlf, trf, trf, brf, brf, blf, blf, tlf, // Front
tlb, trb, trb, brb, brb, blb, blb, tlb, // Back
tlf, tlb, trf, trb, brf, brb, blf, blb, // Front to back
];
self.extend_list_positions(positions);
self.add_list_color(color, 24);
}
/// Draw a line from `start` to `end`.
#[inline]
pub fn line_2d(&mut self, start: Vec2, end: Vec2, color: Color) {
self.line(start.extend(0.), end.extend(0.), color);
}
/// Draw a line from `start` to `end`.
#[inline]
pub fn line_gradient_2d(
&mut self,
start: Vec2,
end: Vec2,
start_color: Color,
end_color: Color,
) {
self.line_gradient(start.extend(0.), end.extend(0.), start_color, end_color);
}
#[inline]
pub fn linestrip_2d(&mut self, positions: impl IntoIterator<Item = Vec2>, color: Color) {
self.linestrip(positions.into_iter().map(|vec2| vec2.extend(0.)), color);
}
#[inline]
pub fn linestrip_gradient_2d(&mut self, positions: impl IntoIterator<Item = (Vec2, Color)>) {
self.linestrip_gradient(
positions
.into_iter()
.map(|(vec2, color)| (vec2.extend(0.), color)),
);
}
/// Draw a line from `start` to `start + vector`.
#[inline]
pub fn ray_2d(&mut self, start: Vec2, vector: Vec2, color: Color) {
self.line_2d(start, start + vector, color);
}
/// Draw a line from `start` to `start + vector`.
#[inline]
pub fn ray_gradient_2d(
&mut self,
start: Vec2,
vector: Vec2,
start_color: Color,
end_color: Color,
) {
self.line_gradient_2d(start, start + vector, start_color, end_color);
}
// Draw a circle.
#[inline]
pub fn circle_2d(&mut self, position: Vec2, radius: f32, color: Color) -> Circle2dBuilder {
Circle2dBuilder {
buffer: self,
position,
radius,
color,
segments: DEFAULT_CIRCLE_SEGMENTS,
}
}
/// Draw a rectangle.
#[inline]
pub fn rect_2d(&mut self, position: Vec2, rotation: f32, size: Vec2, color: Color) {
let rotation = Mat2::from_angle(rotation);
let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2);
self.linestrip_2d([tl, tr, br, bl, tl], color);
}
#[inline]
fn extend_list_positions(&mut self, positions: impl IntoIterator<Item = Vec3>) {
self.list_positions
.extend(positions.into_iter().map(|vec3| vec3.to_array()));
}
#[inline]
fn extend_list_colors(&mut self, colors: impl IntoIterator<Item = Color>) {
self.list_colors
.extend(colors.into_iter().map(|color| color.as_linear_rgba_f32()));
}
#[inline]
fn add_list_color(&mut self, color: Color, count: usize) {
self.list_colors
.extend(iter::repeat(color.as_linear_rgba_f32()).take(count));
}
#[inline]
fn extend_strip_positions(&mut self, positions: impl IntoIterator<Item = Vec3>) {
self.strip_positions.extend(
positions
.into_iter()
.map(|vec3| vec3.to_array())
.chain(iter::once([f32::NAN; 3])),
);
}
}
pub struct CircleBuilder<'a> {
buffer: &'a mut GizmoBuffer,
position: Vec3,
normal: Vec3,
radius: f32,
color: Color,
segments: usize,
}
impl<'a> CircleBuilder<'a> {
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'a> Drop for CircleBuilder<'a> {
fn drop(&mut self) {
let rotation = Quat::from_rotation_arc(Vec3::Z, self.normal);
let positions = circle_inner(self.radius, self.segments)
.map(|vec2| (self.position + rotation * vec2.extend(0.)));
self.buffer.linestrip(positions, self.color);
}
}
pub struct SphereBuilder<'a> {
buffer: &'a mut GizmoBuffer,
position: Vec3,
rotation: Quat,
radius: f32,
color: Color,
circle_segments: usize,
}
impl SphereBuilder<'_> {
pub fn circle_segments(mut self, segments: usize) -> Self {
self.circle_segments = segments;
self
}
}
impl Drop for SphereBuilder<'_> {
fn drop(&mut self) {
for axis in Vec3::AXES {
self.buffer
.circle(self.position, self.rotation * axis, self.radius, self.color)
.segments(self.circle_segments);
}
}
}
pub struct Circle2dBuilder<'a> {
buffer: &'a mut GizmoBuffer,
position: Vec2,
radius: f32,
color: Color,
segments: usize,
}
impl Circle2dBuilder<'_> {
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl Drop for Circle2dBuilder<'_> {
fn drop(&mut self) {
let positions = circle_inner(self.radius, self.segments).map(|vec2| (vec2 + self.position));
self.buffer.linestrip_2d(positions, self.color);
}
}
fn circle_inner(radius: f32, segments: usize) -> impl Iterator<Item = Vec2> {
(0..segments + 1).map(move |i| {
let angle = i as f32 * TAU / segments as f32;
Vec2::from(angle.sin_cos()) * radius
})
}
fn rect_inner(size: Vec2) -> [Vec2; 4] {
let half_size = size / 2.;
let tl = Vec2::new(-half_size.x, half_size.y);
let tr = Vec2::new(half_size.x, half_size.y);
let bl = Vec2::new(-half_size.x, -half_size.y);
let br = Vec2::new(half_size.x, -half_size.y);
[tl, tr, br, bl]
}

View file

@ -0,0 +1,187 @@
use std::mem;
use bevy_app::{Last, Plugin};
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
use bevy_ecs::{
prelude::{Component, DetectChanges},
schedule::IntoSystemConfigs,
system::{Commands, Res, ResMut, Resource},
world::{FromWorld, World},
};
use bevy_math::Mat4;
use bevy_reflect::TypeUuid;
use bevy_render::{
mesh::Mesh,
render_phase::AddRenderCommand,
render_resource::{PrimitiveTopology, Shader, SpecializedMeshPipelines},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
#[cfg(feature = "bevy_pbr")]
use bevy_pbr::MeshUniform;
#[cfg(feature = "bevy_sprite")]
use bevy_sprite::{Mesh2dHandle, Mesh2dUniform};
pub mod gizmos;
#[cfg(feature = "bevy_sprite")]
mod pipeline_2d;
#[cfg(feature = "bevy_pbr")]
mod pipeline_3d;
use crate::gizmos::GizmoStorage;
/// The `bevy_gizmos` prelude.
pub mod prelude {
#[doc(hidden)]
pub use crate::{gizmos::Gizmos, GizmoConfig};
}
const LINE_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7414812689238026784);
pub struct GizmoPlugin;
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>()
.init_resource::<GizmoConfig>()
.init_resource::<GizmoStorage>()
.add_systems(Last, update_gizmo_meshes);
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; };
render_app.add_systems(ExtractSchedule, extract_gizmo_data);
#[cfg(feature = "bevy_sprite")]
{
use bevy_core_pipeline::core_2d::Transparent2d;
use pipeline_2d::*;
render_app
.add_render_command::<Transparent2d, DrawGizmoLines>()
.init_resource::<GizmoLinePipeline>()
.init_resource::<SpecializedMeshPipelines<GizmoLinePipeline>>()
.add_systems(Render, queue_gizmos_2d.in_set(RenderSet::Queue));
}
#[cfg(feature = "bevy_pbr")]
{
use bevy_core_pipeline::core_3d::Opaque3d;
use pipeline_3d::*;
render_app
.add_render_command::<Opaque3d, DrawGizmoLines>()
.init_resource::<GizmoPipeline>()
.init_resource::<SpecializedMeshPipelines<GizmoPipeline>>()
.add_systems(Render, queue_gizmos_3d.in_set(RenderSet::Queue));
}
}
}
#[derive(Resource, Clone, Copy)]
pub struct GizmoConfig {
/// Set to `false` to stop drawing gizmos.
///
/// Defaults to `true`.
pub enabled: bool,
/// Draw gizmos on top of everything else, ignoring depth.
///
/// This setting only affects 3D. In 2D, gizmos are always drawn on top.
///
/// Defaults to `false`.
pub on_top: bool,
}
impl Default for GizmoConfig {
fn default() -> Self {
Self {
enabled: true,
on_top: false,
}
}
}
#[derive(Resource)]
struct MeshHandles {
list: Handle<Mesh>,
strip: Handle<Mesh>,
}
impl FromWorld for MeshHandles {
fn from_world(world: &mut World) -> Self {
let mut meshes = world.resource_mut::<Assets<Mesh>>();
MeshHandles {
list: meshes.add(Mesh::new(PrimitiveTopology::LineList)),
strip: meshes.add(Mesh::new(PrimitiveTopology::LineStrip)),
}
}
}
#[derive(Component)]
struct GizmoMesh;
fn update_gizmo_meshes(
mut meshes: ResMut<Assets<Mesh>>,
handles: Res<MeshHandles>,
mut storage: ResMut<GizmoStorage>,
) {
let list_mesh = meshes.get_mut(&handles.list).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);
let strip_mesh = meshes.get_mut(&handles.strip).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);
}
fn extract_gizmo_data(
mut commands: Commands,
handles: Extract<Res<MeshHandles>>,
config: Extract<Res<GizmoConfig>>,
) {
if config.is_changed() {
commands.insert_resource(**config);
}
if !config.enabled {
return;
}
let transform = Mat4::IDENTITY;
let inverse_transpose_model = transform.inverse().transpose();
commands.spawn_batch([&handles.list, &handles.strip].map(|handle| {
(
GizmoMesh,
#[cfg(feature = "bevy_pbr")]
(
handle.clone(),
MeshUniform {
flags: 0,
transform,
inverse_transpose_model,
},
),
#[cfg(feature = "bevy_sprite")]
(
Mesh2dHandle(handle.clone()),
Mesh2dUniform {
flags: 0,
transform,
inverse_transpose_model,
},
),
)
}));
}

View file

@ -0,0 +1,44 @@
#ifdef GIZMO_LINES_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 VertexOutput {
@builtin(position) pos: vec4<f32>,
@location(0) color: vec4<f32>,
}
struct FragmentOutput {
@builtin(frag_depth) depth: f32,
@location(0) color: vec4<f32>,
}
@vertex
fn vertex(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.pos = view.view_proj * vec4<f32>(in.pos, 1.0);
out.color = in.color;
return out;
}
@fragment
fn fragment(in: VertexOutput) -> FragmentOutput {
var out: FragmentOutput;
#ifdef DEPTH_TEST
out.depth = in.pos.z;
#else
out.depth = 1.0;
#endif
out.color = in.color;
return out;
}

View file

@ -0,0 +1,121 @@
use bevy_asset::Handle;
use bevy_core_pipeline::core_2d::Transparent2d;
use bevy_ecs::{
prelude::Entity,
query::With,
system::{Query, Res, ResMut, Resource},
world::{FromWorld, World},
};
use bevy_render::{
mesh::{Mesh, MeshVertexBufferLayout},
render_asset::RenderAssets,
render_phase::{DrawFunctions, RenderPhase, SetItemPipeline},
render_resource::*,
texture::BevyDefault,
view::Msaa,
};
use bevy_sprite::*;
use bevy_utils::FloatOrd;
use crate::{GizmoMesh, LINE_SHADER_HANDLE};
#[derive(Resource)]
pub(crate) struct GizmoLinePipeline {
mesh_pipeline: Mesh2dPipeline,
shader: Handle<Shader>,
}
impl FromWorld for GizmoLinePipeline {
fn from_world(render_world: &mut World) -> Self {
GizmoLinePipeline {
mesh_pipeline: render_world.resource::<Mesh2dPipeline>().clone(),
shader: LINE_SHADER_HANDLE.typed(),
}
}
}
impl SpecializedMeshPipeline for GizmoLinePipeline {
type Key = Mesh2dPipelineKey;
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),
])?;
Ok(RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone_weak(),
entry_point: "vertex".into(),
shader_defs: vec![],
buffers: vec![vertex_buffer_layout],
},
fragment: Some(FragmentState {
shader: self.shader.clone_weak(),
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::bevy_default(),
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
layout: vec![self.mesh_pipeline.view_layout.clone()],
primitive: PrimitiveState {
topology: key.primitive_topology(),
..Default::default()
},
depth_stencil: None,
multisample: MultisampleState {
count: key.msaa_samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
push_constant_ranges: vec![],
label: Some("gizmo_2d_pipeline".into()),
})
}
}
pub(crate) type DrawGizmoLines = (
SetItemPipeline,
SetMesh2dViewBindGroup<0>,
SetMesh2dBindGroup<1>,
DrawMesh2d,
);
#[allow(clippy::too_many_arguments)]
pub(crate) fn queue_gizmos_2d(
draw_functions: Res<DrawFunctions<Transparent2d>>,
pipeline: Res<GizmoLinePipeline>,
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>>,
mut views: Query<&mut RenderPhase<Transparent2d>>,
) {
let draw_function = draw_functions.read().get_id::<DrawGizmoLines>().unwrap();
let key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples());
for mut phase in &mut views {
for (entity, mesh_handle) in &mesh_handles {
let Some(mesh) = gpu_meshes.get(&mesh_handle.0) else { continue; };
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 {
entity,
draw_function,
pipeline,
sort_key: FloatOrd(f32::MAX),
batch_range: None,
});
}
}
}

View file

@ -0,0 +1,164 @@
use bevy_asset::Handle;
use bevy_core_pipeline::core_3d::Opaque3d;
use bevy_ecs::{
entity::Entity,
query::With,
system::{Query, Res, ResMut, Resource},
world::{FromWorld, World},
};
use bevy_pbr::*;
use bevy_render::{
mesh::Mesh,
render_resource::Shader,
view::{ExtractedView, ViewTarget},
};
use bevy_render::{
mesh::MeshVertexBufferLayout,
render_asset::RenderAssets,
render_phase::{DrawFunctions, RenderPhase, SetItemPipeline},
render_resource::*,
texture::BevyDefault,
view::Msaa,
};
use crate::{GizmoConfig, GizmoMesh, LINE_SHADER_HANDLE};
#[derive(Resource)]
pub(crate) struct GizmoPipeline {
mesh_pipeline: MeshPipeline,
shader: Handle<Shader>,
}
impl FromWorld for GizmoPipeline {
fn from_world(render_world: &mut World) -> Self {
GizmoPipeline {
mesh_pipeline: render_world.resource::<MeshPipeline>().clone(),
shader: LINE_SHADER_HANDLE.typed(),
}
}
}
impl SpecializedMeshPipeline for GizmoPipeline {
type Key = (bool, MeshPipelineKey);
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,
));
shader_defs.push(ShaderDefVal::Int(
"MAX_CASCADES_PER_LIGHT".to_string(),
MAX_CASCADES_PER_LIGHT as i32,
));
if depth_test {
shader_defs.push("DEPTH_TEST".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) {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
};
Ok(RenderPipelineDescriptor {
vertex: VertexState {
shader: self.shader.clone_weak(),
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![vertex_buffer_layout],
},
fragment: Some(FragmentState {
shader: self.shader.clone_weak(),
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
layout: bind_group_layout,
primitive: PrimitiveState {
topology: key.primitive_topology(),
..Default::default()
},
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: CompareFunction::Greater,
stencil: Default::default(),
bias: Default::default(),
}),
multisample: MultisampleState {
count: key.msaa_samples(),
mask: !0,
alpha_to_coverage_enabled: false,
},
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.,
});
}
}
}
}

View file

@ -71,11 +71,14 @@ subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"]
webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"]
# enable systems that allow for automated testing on CI
bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render/ci_limits"]
bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render?/ci_limits"]
# Enable animation support, and glTF animation loading
animation = ["bevy_animation", "bevy_gltf?/bevy_animation"]
bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite"]
bevy_pbr = ["dep:bevy_pbr", "bevy_gizmos?/bevy_pbr"]
# Used to disable code that is unsupported when Bevy is dynamically linked
dynamic_linking = ["bevy_diagnostic/dynamic_linking"]
@ -122,3 +125,4 @@ bevy_text = { path = "../bevy_text", optional = true, version = "0.11.0-dev" }
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.11.0-dev" }
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.11.0-dev" }
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.11.0-dev" }
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.11.0-dev", default-features = false }

View file

@ -135,6 +135,11 @@ impl PluginGroup for DefaultPlugins {
group = group.add(bevy_animation::AnimationPlugin::default());
}
#[cfg(feature = "bevy_gizmos")]
{
group = group.add(bevy_gizmos::GizmoPlugin);
}
group
}
}

View file

@ -172,6 +172,12 @@ pub mod winit {
pub use bevy_winit::*;
}
#[cfg(feature = "bevy_gizmos")]
pub mod gizmos {
//! Immediate mode debug drawing.
pub use bevy_gizmos::*;
}
#[cfg(feature = "bevy_dynamic_plugin")]
pub mod dynamic_plugin {
//! Dynamic linking of plugins

View file

@ -51,6 +51,10 @@ pub use crate::ui::prelude::*;
#[cfg(feature = "bevy_dynamic_plugin")]
pub use crate::dynamic_plugin::*;
#[doc(hidden)]
#[cfg(feature = "bevy_gizmos")]
pub use crate::gizmos::prelude::*;
#[doc(hidden)]
#[cfg(feature = "bevy_gilrs")]
pub use crate::gilrs::*;

View file

@ -18,6 +18,7 @@ The default feature set enables most of the expected features of a game engine,
|bevy_audio|Provides audio functionality|
|bevy_core_pipeline|Provides cameras and other basic render pipeline features|
|bevy_gilrs|Adds gamepad support|
|bevy_gizmos|Adds support for rendering gizmos|
|bevy_gltf|[glTF](https://www.khronos.org/gltf/) support|
|bevy_pbr|Adds PBR rendering|
|bevy_render|Provides rendering functionality|

41
examples/2d/2d_gizmos.rs Normal file
View file

@ -0,0 +1,41 @@
//! This example demonstrates Bevy's immediate mode drawing API intended for visual debugging.
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, system)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
}
fn system(mut gizmos: Gizmos, time: Res<Time>) {
let sin = time.elapsed_seconds().sin() * 50.;
gizmos.line_2d(Vec2::Y * -sin, Vec2::splat(-80.), Color::RED);
gizmos.ray_2d(Vec2::Y * sin, Vec2::splat(80.), Color::GREEN);
// Triangle
gizmos.linestrip_gradient_2d([
(Vec2::Y * 300., Color::BLUE),
(Vec2::new(-255., -155.), Color::RED),
(Vec2::new(255., -155.), Color::GREEN),
(Vec2::Y * 300., Color::BLUE),
]);
gizmos.rect_2d(
Vec2::ZERO,
time.elapsed_seconds() / 3.,
Vec2::splat(300.),
Color::BLACK,
);
// The circles have 32 line-segments by default.
gizmos.circle_2d(Vec2::ZERO, 120., Color::BLACK);
// You may want to increase this for larger circles.
gizmos.circle_2d(Vec2::ZERO, 300., Color::NAVY).segments(64);
}

108
examples/3d/3d_gizmos.rs Normal file
View file

@ -0,0 +1,108 @@
//! This example demonstrates Bevy's immediate mode drawing API intended for visual debugging.
use std::f32::consts::PI;
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (system, rotate_camera, update_config))
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(0., 1.5, 6.).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
// plane
commands.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane::from_size(5.0))),
material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()),
..default()
});
// cube
commands.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
});
// light
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 1500.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// text
commands.spawn(TextBundle::from_section(
"Press 't' to toggle drawing gizmos on top of everything else in the scene",
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 24.,
color: Color::WHITE,
},
));
}
fn system(mut gizmos: Gizmos, time: Res<Time>) {
gizmos.cuboid(
Vec3::Y * -0.5,
Quat::IDENTITY,
Vec3::new(5., 1., 2.),
Color::BLACK,
);
gizmos.rect(
Vec3::new(time.elapsed_seconds().cos() * 2.5, 1., 0.),
Quat::from_rotation_y(PI / 2.),
Vec2::splat(2.),
Color::GREEN,
);
gizmos.sphere(
Vec3::new(1., 0.5, 0.),
Quat::IDENTITY,
0.5,
Color::RED.with_a(0.5),
);
for y in [0., 0.5, 1.] {
gizmos.ray(
Vec3::new(1., y, 0.),
Vec3::new(-3., (time.elapsed_seconds() * 3.).sin(), 0.),
Color::BLUE,
);
}
// Circles have 32 line-segments by default.
gizmos.circle(Vec3::ZERO, Vec3::Y, 3., Color::BLACK);
// You may want to increase this for larger circles or spheres.
gizmos
.circle(Vec3::ZERO, Vec3::Y, 3.1, Color::NAVY)
.segments(64);
gizmos
.sphere(Vec3::ZERO, Quat::IDENTITY, 3.2, Color::BLACK)
.circle_segments(64);
}
fn rotate_camera(mut query: Query<&mut Transform, With<Camera>>, time: Res<Time>) {
let mut transform = query.single_mut();
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;
}
}

View file

@ -88,6 +88,7 @@ Example | Description
Example | Description
--- | ---
[2D Bloom](../examples/2d/bloom_2d.rs) | Illustrates bloom post-processing in 2d
[2D Gizmos](../examples/2d/2d_gizmos.rs) | A scene showcasing 2D gizmos
[2D Rotation](../examples/2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions
[2D Shapes](../examples/2d/2d_shapes.rs) | Renders a rectangle, circle, and hexagon
[Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis
@ -107,6 +108,7 @@ Example | Description
Example | Description
--- | ---
[3D Bloom](../examples/3d/bloom_3d.rs) | Illustrates bloom configuration using HDR and emissive materials
[3D Gizmos](../examples/3d/3d_gizmos.rs) | A scene showcasing 3D gizmos
[3D Scene](../examples/3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting
[3D Shapes](../examples/3d/3d_shapes.rs) | A scene showcasing the built-in 3D shapes
[Atmospheric Fog](../examples/3d/atmospheric_fog.rs) | A scene showcasing the atmospheric fog effect
@ -300,6 +302,7 @@ Example | Description
[Many Buttons](../examples/stress_tests/many_buttons.rs) | Test rendering of many UI elements
[Many Cubes](../examples/stress_tests/many_cubes.rs) | Simple benchmark to test per-entity draw overhead. Run with the `sphere` argument to test frustum culling
[Many Foxes](../examples/stress_tests/many_foxes.rs) | Loads an animated fox model and spawns lots of them. Good for testing skinned mesh performance. Takes an unsigned integer argument for the number of foxes to spawn. Defaults to 1000
[Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos
[Many Glyphs](../examples/stress_tests/many_glyphs.rs) | Simple benchmark to test text rendering.
[Many Lights](../examples/stress_tests/many_lights.rs) | Simple benchmark to test rendering many point lights. Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights
[Many Sprites](../examples/stress_tests/many_sprites.rs) | Displays many sprites in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites.

View file

@ -0,0 +1,109 @@
use std::f32::consts::TAU;
use bevy::{
diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin},
prelude::*,
window::PresentMode,
};
const SYSTEM_COUNT: u32 = 10;
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Many Debug Lines".to_string(),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}))
.add_plugin(FrameTimeDiagnosticsPlugin::default())
.insert_resource(Config {
line_count: 50_000,
fancy: false,
})
.insert_resource(GizmoConfig {
on_top: false,
..default()
})
.add_systems(Startup, setup)
.add_systems(Update, (input, ui_system));
for _ in 0..SYSTEM_COUNT {
app.add_systems(Update, system);
}
app.run();
}
#[derive(Resource, Debug)]
struct Config {
line_count: u32,
fancy: bool,
}
fn input(mut config: ResMut<Config>, input: Res<Input<KeyCode>>) {
if input.just_pressed(KeyCode::Up) {
config.line_count += 10_000;
}
if input.just_pressed(KeyCode::Down) {
config.line_count = config.line_count.saturating_sub(10_000);
}
if input.just_pressed(KeyCode::Space) {
config.fancy = !config.fancy;
}
}
fn system(config: Res<Config>, time: Res<Time>, mut draw: Gizmos) {
if !config.fancy {
for _ in 0..(config.line_count / SYSTEM_COUNT) {
draw.line(Vec3::NEG_Y, Vec3::Y, Color::BLACK);
}
} else {
for i in 0..(config.line_count / SYSTEM_COUNT) {
let angle = i as f32 / (config.line_count / SYSTEM_COUNT) as f32 * TAU;
let vector = Vec2::from(angle.sin_cos()).extend(time.elapsed_seconds().sin());
let start_color = Color::rgb(vector.x, vector.z, 0.5);
let end_color = Color::rgb(-vector.z, -vector.y, 0.5);
draw.line_gradient(vector, -vector, start_color, end_color);
}
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
warn!(include_str!("warning_string.txt"));
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(3., 1., 5.).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
commands.spawn(TextBundle::from_section(
"",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 30.,
..default()
},
));
}
fn ui_system(mut query: Query<&mut Text>, config: Res<Config>, diag: Res<Diagnostics>) {
let mut text = query.single_mut();
let Some(fps) = diag.get(FrameTimeDiagnosticsPlugin::FPS).and_then(|fps| fps.smoothed()) else {
return;
};
text.sections[0].value = format!(
"Line count: {}\n\
FPS: {:.0}\n\n\
Controls:\n\
Up/Down: Raise or lower the line count.\n\
Spacebar: Toggle fancy mode.",
config.line_count, fps,
);
}

View file

@ -34,6 +34,7 @@ crates=(
bevy_gltf
bevy_scene
bevy_sprite
bevy_gizmos
bevy_text
bevy_a11y
bevy_ui