bevy/crates/bevy_gizmos/src/lib.rs
JMS55 44424391fe
Unload render assets from RAM (#10520)
# Objective
- No point in keeping Meshes/Images in RAM once they're going to be sent
to the GPU, and kept in VRAM. This saves a _significant_ amount of
memory (several GBs) on scenes like bistro.
- References
  - https://github.com/bevyengine/bevy/pull/1782
  - https://github.com/bevyengine/bevy/pull/8624 

## Solution
- Augment RenderAsset with the capability to unload the underlying asset
after extracting to the render world.
- Mesh/Image now have a cpu_persistent_access field. If this field is
RenderAssetPersistencePolicy::Unload, the asset will be unloaded from
Assets<T>.
- A new AssetEvent is sent upon dropping the last strong handle for the
asset, which signals to the RenderAsset to remove the GPU version of the
asset.

---

## Changelog
- Added `AssetEvent::NoLongerUsed` and
`AssetEvent::is_no_longer_used()`. This event is sent when the last
strong handle of an asset is dropped.
- Rewrote the API for `RenderAsset` to allow for unloading the asset
data from the CPU.
- Added `RenderAssetPersistencePolicy`.
- Added `Mesh::cpu_persistent_access` for memory savings when the asset
is not needed except for on the GPU.
- Added `Image::cpu_persistent_access` for memory savings when the asset
is not needed except for on the GPU.
- Added `ImageLoaderSettings::cpu_persistent_access`.
- Added `ExrTextureLoaderSettings`.
- Added `HdrTextureLoaderSettings`.

## Migration Guide
- Asset loaders (GLTF, etc) now load meshes and textures without
`cpu_persistent_access`. These assets will be removed from
`Assets<Mesh>` and `Assets<Image>` once `RenderAssets<Mesh>` and
`RenderAssets<Image>` contain the GPU versions of these assets, in order
to reduce memory usage. If you require access to the asset data from the
CPU in future frames after the GLTF asset has been loaded, modify all
dependent `Mesh` and `Image` assets and set `cpu_persistent_access` to
`RenderAssetPersistencePolicy::Keep`.
- `Mesh` now requires a new `cpu_persistent_access` field. Set it to
`RenderAssetPersistencePolicy::Keep` to mimic the previous behavior.
- `Image` now requires a new `cpu_persistent_access` field. Set it to
`RenderAssetPersistencePolicy::Keep` to mimic the previous behavior.
- `MorphTargetImage::new()` now requires a new `cpu_persistent_access`
parameter. Set it to `RenderAssetPersistencePolicy::Keep` to mimic the
previous behavior.
- `DynamicTextureAtlasBuilder::add_texture()` now requires that the
`TextureAtlas` you pass has an `Image` with `cpu_persistent_access:
RenderAssetPersistencePolicy::Keep`. Ensure you construct the image
properly for the texture atlas.
- The `RenderAsset` trait has significantly changed, and requires
adapting your existing implementations.
  - The trait now requires `Clone`.
- The `ExtractedAsset` associated type has been removed (the type itself
is now extracted).
  - The signature of `prepare_asset()` is slightly different
- A new `persistence_policy()` method is now required (return
RenderAssetPersistencePolicy::Unload to match the previous behavior).
- Match on the new `NoLongerUsed` variant for exhaustive matches of
`AssetEvent`.
2024-01-03 03:31:04 +00:00

555 lines
17 KiB
Rust

#![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`] for more examples.
pub mod arcs;
pub mod arrows;
pub mod circles;
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, ReflectResource},
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, RenderAssetPersistencePolicy, RenderAssetPlugin,
RenderAssets,
},
render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass},
render_resource::{
binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout,
BindGroupLayoutEntries, Buffer, 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.register_type::<GizmoConfig>()
.register_type::<AabbGizmoConfig>()
.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(
"LineGizmoUniform layout",
&BindGroupLayoutEntries::single(
ShaderStages::VERTEX,
uniform_buffer::<LineGizmoUniform>(true),
),
);
render_app.insert_resource(LineGizmoUniformBindgroupLayout { layout });
}
}
/// A [`Resource`] that stores configuration for gizmos.
#[derive(Resource, Clone, Reflect)]
#[reflect(Resource)]
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.
///
/// In 2D this setting has no effect and is effectively always -1.
///
/// 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.
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, Reflect)]
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 PreparedAsset = GpuLineGizmo;
type Param = SRes<RenderDevice>;
fn persistence_policy(&self) -> RenderAssetPersistencePolicy {
RenderAssetPersistencePolicy::Unload
}
fn prepare_asset(
self,
render_device: &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self>> {
let position_buffer_data = cast_slice(&self.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(&self.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: self.positions.len() as u32,
strip: self.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(
"LineGizmoUniform bindgroup",
&line_gizmo_uniform_layout.layout,
&BindGroupEntries::single(binding),
),
});
}
}
struct SetLineGizmoBindGroup<const I: usize>;
impl<const I: usize, P: PhaseItem> RenderCommand<P> for SetLineGizmoBindGroup<I> {
type Param = SRes<LineGizmoUniformBindgroup>;
type ViewData = ();
type ItemData = Read<DynamicUniformIndex<LineGizmoUniform>>;
#[inline]
fn render<'w>(
_item: &P,
_view: ROQueryItem<'w, Self::ViewData>,
uniform_index: ROQueryItem<'w, Self::ItemData>,
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 Param = SRes<RenderAssets<LineGizmo>>;
type ViewData = ();
type ItemData = Read<Handle<LineGizmo>>;
#[inline]
fn render<'w>(
_item: &P,
_view: ROQueryItem<'w, Self::ViewData>,
handle: ROQueryItem<'w, Self::ItemData>,
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]
}
}