mirror of
https://github.com/bevyengine/bevy
synced 2024-12-22 19:13:08 +00:00
7063c86ed4
# Objective To celebrate the turning of the seasons, I took a small walk through the codebase guided by the "[code spell checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker)" VS Code extension and fixed a few typos.
557 lines
18 KiB
Rust
557 lines
18 KiB
Rust
#![allow(clippy::type_complexity)]
|
|
#![warn(missing_docs)]
|
|
|
|
//! This crate adds an immediate mode drawing api to Bevy for visual debugging.
|
|
//!
|
|
//! # Example
|
|
//! ```
|
|
//! # use bevy_gizmos::prelude::*;
|
|
//! # use bevy_render::prelude::*;
|
|
//! # use bevy_math::prelude::*;
|
|
//! fn system(mut gizmos: Gizmos) {
|
|
//! gizmos.line(Vec3::ZERO, Vec3::X, Color::GREEN);
|
|
//! }
|
|
//! # bevy_ecs::system::assert_is_system(system);
|
|
//! ```
|
|
//!
|
|
//! See the documentation on [`Gizmos`](crate::gizmos::Gizmos) for more examples.
|
|
|
|
pub mod gizmos;
|
|
|
|
#[cfg(feature = "bevy_sprite")]
|
|
mod pipeline_2d;
|
|
#[cfg(feature = "bevy_pbr")]
|
|
mod pipeline_3d;
|
|
|
|
/// The `bevy_gizmos` prelude.
|
|
pub mod prelude {
|
|
#[doc(hidden)]
|
|
pub use crate::{gizmos::Gizmos, AabbGizmo, AabbGizmoConfig, GizmoConfig};
|
|
}
|
|
|
|
use bevy_app::{Last, Plugin, PostUpdate};
|
|
use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle};
|
|
use bevy_core::cast_slice;
|
|
use bevy_ecs::{
|
|
change_detection::DetectChanges,
|
|
component::Component,
|
|
entity::Entity,
|
|
query::{ROQueryItem, Without},
|
|
reflect::ReflectComponent,
|
|
schedule::IntoSystemConfigs,
|
|
system::{
|
|
lifetimeless::{Read, SRes},
|
|
Commands, Query, Res, ResMut, Resource, SystemParamItem,
|
|
},
|
|
};
|
|
use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath};
|
|
use bevy_render::{
|
|
color::Color,
|
|
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
|
|
primitives::Aabb,
|
|
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,
|
|
view::RenderLayers,
|
|
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
|
|
};
|
|
use bevy_transform::{
|
|
components::{GlobalTransform, Transform},
|
|
TransformSystem,
|
|
};
|
|
use gizmos::{GizmoStorage, Gizmos};
|
|
use std::mem;
|
|
|
|
const LINE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(7414812689238026784);
|
|
|
|
/// A [`Plugin`] that provides an immediate mode drawing api for visual debugging.
|
|
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.add_plugins(UniformComponentPlugin::<LineGizmoUniform>::default())
|
|
.init_asset::<LineGizmo>()
|
|
.add_plugins(RenderAssetPlugin::<LineGizmo>::default())
|
|
.init_resource::<LineGizmoHandles>()
|
|
.init_resource::<GizmoConfig>()
|
|
.init_resource::<GizmoStorage>()
|
|
.add_systems(Last, update_gizmo_meshes)
|
|
.add_systems(
|
|
PostUpdate,
|
|
(
|
|
draw_aabbs,
|
|
draw_all_aabbs.run_if(|config: Res<GizmoConfig>| config.aabb.draw_all),
|
|
)
|
|
.after(TransformSystem::TransformPropagate),
|
|
);
|
|
|
|
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
|
|
render_app
|
|
.add_systems(ExtractSchedule, extract_gizmo_data)
|
|
.add_systems(
|
|
Render,
|
|
prepare_line_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups),
|
|
);
|
|
|
|
#[cfg(feature = "bevy_sprite")]
|
|
app.add_plugins(pipeline_2d::LineGizmo2dPlugin);
|
|
#[cfg(feature = "bevy_pbr")]
|
|
app.add_plugins(pipeline_3d::LineGizmo3dPlugin);
|
|
}
|
|
|
|
fn finish(&self, app: &mut bevy_app::App) {
|
|
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
|
|
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.insert_resource(LineGizmoUniformBindgroupLayout { layout });
|
|
}
|
|
}
|
|
|
|
/// A [`Resource`] that stores configuration for gizmos.
|
|
#[derive(Resource, Clone)]
|
|
pub struct GizmoConfig {
|
|
/// Set to `false` to stop drawing gizmos.
|
|
///
|
|
/// Defaults to `true`.
|
|
pub enabled: bool,
|
|
/// Line width specified in pixels.
|
|
///
|
|
/// 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-orthographic cameras.
|
|
///
|
|
/// Defaults to `false`.
|
|
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,
|
|
/// Describes which rendering layers gizmos will be rendered to.
|
|
///
|
|
/// Gizmos will only be rendered to cameras with intersecting layers.
|
|
pub render_layers: RenderLayers,
|
|
}
|
|
|
|
impl Default for GizmoConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
line_width: 2.,
|
|
line_perspective: false,
|
|
depth_bias: 0.,
|
|
aabb: Default::default(),
|
|
render_layers: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration for drawing the [`Aabb`] component on entities.
|
|
#[derive(Clone, Default)]
|
|
pub struct AabbGizmoConfig {
|
|
/// Draws all bounding boxes in the scene when set to `true`.
|
|
///
|
|
/// To draw a specific entity's bounding box, you can add the [`AabbGizmo`] component.
|
|
///
|
|
/// Defaults to `false`.
|
|
pub draw_all: bool,
|
|
/// The default color for bounding box gizmos.
|
|
///
|
|
/// A random color is chosen per box if `None`.
|
|
///
|
|
/// Defaults to `None`.
|
|
pub default_color: Option<Color>,
|
|
}
|
|
|
|
/// Add this [`Component`] to an entity to draw its [`Aabb`] component.
|
|
#[derive(Component, Reflect, Default, Debug)]
|
|
#[reflect(Component, Default)]
|
|
pub struct AabbGizmo {
|
|
/// The color of the box.
|
|
///
|
|
/// The default color from the [`GizmoConfig`] resource is used if `None`,
|
|
pub color: Option<Color>,
|
|
}
|
|
|
|
fn draw_aabbs(
|
|
query: Query<(Entity, &Aabb, &GlobalTransform, &AabbGizmo)>,
|
|
config: Res<GizmoConfig>,
|
|
mut gizmos: Gizmos,
|
|
) {
|
|
for (entity, &aabb, &transform, gizmo) in &query {
|
|
let color = gizmo
|
|
.color
|
|
.or(config.aabb.default_color)
|
|
.unwrap_or_else(|| color_from_entity(entity));
|
|
gizmos.cuboid(aabb_transform(aabb, transform), color);
|
|
}
|
|
}
|
|
|
|
fn draw_all_aabbs(
|
|
query: Query<(Entity, &Aabb, &GlobalTransform), Without<AabbGizmo>>,
|
|
config: Res<GizmoConfig>,
|
|
mut gizmos: Gizmos,
|
|
) {
|
|
for (entity, &aabb, &transform) in &query {
|
|
let color = config
|
|
.aabb
|
|
.default_color
|
|
.unwrap_or_else(|| color_from_entity(entity));
|
|
gizmos.cuboid(aabb_transform(aabb, transform), color);
|
|
}
|
|
}
|
|
|
|
fn color_from_entity(entity: Entity) -> Color {
|
|
let index = entity.index();
|
|
|
|
// from https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/
|
|
//
|
|
// See https://en.wikipedia.org/wiki/Low-discrepancy_sequence
|
|
// Map a sequence of integers (eg: 154, 155, 156, 157, 158) into the [0.0..1.0] range,
|
|
// so that the closer the numbers are, the larger the difference of their image.
|
|
const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up
|
|
const RATIO_360: f32 = 360.0 / u32::MAX as f32;
|
|
let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360;
|
|
|
|
Color::hsl(hue, 1., 0.5)
|
|
}
|
|
|
|
fn aabb_transform(aabb: Aabb, transform: GlobalTransform) -> GlobalTransform {
|
|
transform
|
|
* GlobalTransform::from(
|
|
Transform::from_translation(aabb.center.into())
|
|
.with_scale((aabb.half_extents * 2.).into()),
|
|
)
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
struct LineGizmoHandles {
|
|
list: Option<Handle<LineGizmo>>,
|
|
strip: Option<Handle<LineGizmo>>,
|
|
}
|
|
|
|
fn update_gizmo_meshes(
|
|
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 = line_gizmos.get_mut(handle).unwrap();
|
|
|
|
list.positions = mem::take(&mut storage.list_positions);
|
|
list.colors = mem::take(&mut storage.list_colors);
|
|
} else {
|
|
let mut list = LineGizmo {
|
|
strip: false,
|
|
..Default::default()
|
|
};
|
|
|
|
list.positions = mem::take(&mut storage.list_positions);
|
|
list.colors = mem::take(&mut storage.list_colors);
|
|
|
|
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 = line_gizmos.get_mut(handle).unwrap();
|
|
|
|
strip.positions = mem::take(&mut storage.strip_positions);
|
|
strip.colors = mem::take(&mut storage.strip_colors);
|
|
} else {
|
|
let mut strip = LineGizmo {
|
|
strip: true,
|
|
..Default::default()
|
|
};
|
|
|
|
strip.positions = mem::take(&mut storage.strip_positions);
|
|
strip.colors = mem::take(&mut storage.strip_colors);
|
|
|
|
handles.strip = Some(line_gizmos.add(strip));
|
|
}
|
|
}
|
|
|
|
fn extract_gizmo_data(
|
|
mut commands: Commands,
|
|
handles: Extract<Res<LineGizmoHandles>>,
|
|
config: Extract<Res<GizmoConfig>>,
|
|
) {
|
|
if config.is_changed() {
|
|
commands.insert_resource(config.clone());
|
|
}
|
|
|
|
if !config.enabled {
|
|
return;
|
|
}
|
|
|
|
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(Asset, Debug, Default, Clone, TypePath)]
|
|
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 prepare_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;
|
|
};
|
|
|
|
if line_gizmo.vertex_count < 2 {
|
|
return RenderCommandResult::Success;
|
|
}
|
|
|
|
let instances = if line_gizmo.strip {
|
|
let item_size = VertexFormat::Float32x3.size();
|
|
let buffer_size = line_gizmo.position_buffer.size() - item_size;
|
|
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..buffer_size));
|
|
pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(item_size..));
|
|
|
|
let item_size = VertexFormat::Float32x4.size();
|
|
let buffer_size = line_gizmo.color_buffer.size() - item_size;
|
|
pass.set_vertex_buffer(2, line_gizmo.color_buffer.slice(..buffer_size));
|
|
pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(item_size..));
|
|
|
|
u32::max(line_gizmo.vertex_count, 1) - 1
|
|
} else {
|
|
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..));
|
|
pass.set_vertex_buffer(1, line_gizmo.color_buffer.slice(..));
|
|
|
|
line_gizmo.vertex_count / 2
|
|
};
|
|
|
|
pass.draw(0..6, 0..instances);
|
|
|
|
RenderCommandResult::Success
|
|
}
|
|
}
|
|
|
|
fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
|
|
use VertexFormat::*;
|
|
let mut position_layout = VertexBufferLayout {
|
|
array_stride: Float32x3.size(),
|
|
step_mode: VertexStepMode::Instance,
|
|
attributes: vec![VertexAttribute {
|
|
format: Float32x3,
|
|
offset: 0,
|
|
shader_location: 0,
|
|
}],
|
|
};
|
|
|
|
let mut color_layout = VertexBufferLayout {
|
|
array_stride: Float32x4.size(),
|
|
step_mode: VertexStepMode::Instance,
|
|
attributes: vec![VertexAttribute {
|
|
format: Float32x4,
|
|
offset: 0,
|
|
shader_location: 2,
|
|
}],
|
|
};
|
|
|
|
if strip {
|
|
vec![
|
|
position_layout.clone(),
|
|
{
|
|
position_layout.attributes[0].shader_location = 1;
|
|
position_layout
|
|
},
|
|
color_layout.clone(),
|
|
{
|
|
color_layout.attributes[0].shader_location = 3;
|
|
color_layout
|
|
},
|
|
]
|
|
} else {
|
|
position_layout.array_stride *= 2;
|
|
position_layout.attributes.push(VertexAttribute {
|
|
format: Float32x3,
|
|
offset: Float32x3.size(),
|
|
shader_location: 1,
|
|
});
|
|
|
|
color_layout.array_stride *= 2;
|
|
color_layout.attributes.push(VertexAttribute {
|
|
format: Float32x4,
|
|
offset: Float32x4.size(),
|
|
shader_location: 3,
|
|
});
|
|
|
|
vec![position_layout, color_layout]
|
|
}
|
|
}
|