Gizmos
rework
#21507
Replies: 2 comments 1 reply
-
I really like the design, particularly how decoupled/deferred it is. I have done some additional work on my PR which I pushed last night, which I think does actually approach most of the goals outlined here, if not exactly the shape. It doesn't change any of the existing underlying/downstream machinery, only the upstream/high level interfaces.
This is supported in the PR by implementing either (
I've implemented I haven't got I had thought about a simple design for a /// This *could* be collapsed into positions and colors to halve the number of allocations,
/// with a usize field as the delimiter.
///
/// This assumes an IsometryXd::IDENTITY. The actual isometry of the drawn gizmos will be computed from the GlobalTransform.
#[derive(Component, Debug, Clone, Default)]
pub struct BakedGizmo {
/// Vertex positions for line-list topology.
pub list_positions: Vec<Vec3>,
/// Vertex colors for line-list topology.
pub list_colors: Vec<LinearRgba>,
/// Vertex positions for line-strip topology.
pub strip_positions: Vec<Vec3>,
/// Vertex colors for line-strip topology.
pub strip_colors: Vec<LinearRgba>,
}
The current approach relies on Assets (which are basically precompiled Gizmos) which mirror the meshable primitives system almost precisely, other than the fact that the
I haven't touched this, but technically this already works for retained gizmos ( fn setup(
mut commands: Commands,
mut gizmo_assets: ResMut<Assets<GizmoAsset>>,
) {
let mut gizmo = GizmoAsset::new();
gizmo.primitive_2d(&RegularPolygon::new(1.0, 7), Isometry2d::IDENTITY, CRIMSON);
commands.spawn((
Gizmo {
handle: gizmo_assets.add(gizmo),
line_config: GizmoLineConfig {
width: 5.,
..default()
},
..default()
},
Transform::from_xyz(4., 1., 0.),
));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0., 1.5, 6.).looking_at(Vec3::ZERO, Vec3::Y),
FreeCam::default(),
));
}
fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Gizmo>>) {
for mut transform in &mut query {
transform.rotate_local_y(time.delta_secs());
}
}
Strongly related to the previous point I think. It would be nice for Meshable to do the same. |
Beta Was this translation helpful? Give feedback.
-
(Posting this as a second thread of discussion): However, after looking at the There should be a generic "position generation" trait for primitives; this would probably look very much like Additionally, realistically, only segments that are curved actually need to be configured by resolution, and that can be done downstream by the consumer of the boundary segment (gizmo line generator, mesh generator, point cloud? sdfs?), using the parameter t. We could also probably support dynamic resolutions based on the viewport. Something like the following sketch: /// An analytic, resolution-independent definition of a closed 2D shape boundary.
///
/// A `Boundary` defines what is "inside" and "outside" by its winding order.
/// The implementer is responsible for ensuring segments connect continuously.
/// Downstream consumers are responsible for sampling points and generating meshes.
pub trait Boundary {
/// Returns the analytic segments describing this shape’s boundary.
fn boundary(&self, isometry: Isometry3d) -> impl Iterator<Item = BoundarySegment>;
}
/// A continuous analytic segment forming part of a boundary.
///
/// Each segment defines its own parameter space (t between 0 and 1).
/// The implementer is responsible for ensuring segments connect continuously.
pub enum BoundarySegment {
/// A straight line.
Line(LineBoundary),
/// A circular or elliptical arc defined by angle sweep.
Arc(ArcBoundary),
}
/// A line segment starting at `origin`, extending along `direction` for `length`.
/// The normal is right of the tangent
pub struct LineBoundary {
pub origin: Vec3,
pub direction: Vec3,
pub length: f32,
}
/// A circular or elliptical arc centered at `center`, starting at `start_angle` from `Vec3::Y`,
/// sweeping by `sweep_angle` radians.
/// If the sweep is counter-clockwise (positive), the inward normal is "outside" the circle (right of the tangent)
/// If the sweep is clockwise (negative), the inward normal is "inside" the circle (right of the tangent)
pub struct ArcBoundary {
pub center: Vec3,
pub radius: Vec3, // allows ellipse when x != y
pub start_angle: f32,
pub sweep_angle: f32,
}
pub trait AnalyticSegment {
/// Evaluate a point along the segment at parameter t, where start is t = 0, and end is t = 1
fn point_at(&self, t: f32) -> Vec3;
/// Tangent direction at parameter t, where start is t = 0, and end is t = 1
fn tangent_at(&self, t: f32) -> Vec3;
/// Outward-facing normal at parameter t, where start is t = 0, and end is t = 1
fn normal_at(&self, t: f32) -> Vec3;
}
impl AnalyticSegment for LineBoundary { ... }
impl AnalyticSegment for ArcBoundary { ... } The above would be able to represent all existing 2d primitives, but it could be extended to Bezier curves for example. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
The following is based on the ideas from and issues encountered in
Seeing that reworking gizmos has been controversial or contentious, I think that having a discussion about the approach we want to take here before implementing those changes is sensible.
This is an attempt at fixing the issues and enabling the features encountered and proposed there. Please feel free to leave feedback.
Issues / Features
The current implementation of gizmos for primitives is fairly flexible but has reached some limitations encountered in the PRs/issues mentioned above.
In particular, the following features could be added/issues could be fixed in a future rework:
bevy_math
/bevy_gizmos
.Extrusion
orRing
.Component
: We could define aGizmoComponent
struct and implementComponent
for it. This component could draw a specified Gizmo after applying the entitiesTransform
to the Gizmo. This could provide a uniform solution for drawing hitboxes or colliders.gizmos.primitive_2d(..)
and only select 2D primitives likeEllipse
orRect
can be drawn in 3D space using functions likegizmos.ellipse(..)
. We could allow drawing arbitrary 2D primitives in 3D space.Isometry
: Drawing a primitive usually involves calculating vertex positions as if the primitive was not rotated/translated and transforming by an isometry later. This process could be unified across all primitives. The advantages would be in simplifying the gizmos implementation for primitives and ensuring that drawing the primitive behaves the same, no matter the isometry.Solution
Overview
We could abstract the draw functions like
gizmos.line(..)
onGizmoBuffer
to a traitLineBuffer
and implement it forGizmoBuffer
and wrappers that apply transformations to the lines drawn. We could then use a genericGizmos: LineBuffer
in each implementation of gizmos for primitives. This would also enable us to store gizmos for later use.Custom
GizmoBuffer
sWe note that all gizmos could be drawn using two functions:
gizmos.linestrip_gradient(..)
andgizmos.line_gradient(..)
.However, we currently incentivise drawing primitives in only one color. As such, two functions like
gizmos.line(start: Vec3, end: Vec3)
andgizmos.linestrip(positions: impl IntoIterator<Item = Vec3>)
would be sufficient to draw every primitive, if we can add the color information later. This would introduce the additional cost that functions likegizmos.ellipse(..)
would have to be defined with color onGizmoBuffer
and without color on anyT: LineBuffer
. I don't think the added complexity is justified.In addition, drawing any primitive using gizmos can be done (and usually that is how they are implemented) by calculating the positions in any line(-strip) of the gizmo as if the primitive was drawn at the origin and then translating and rotating them by applying the specified
Isometry
.This motivates creating a trait
LineBuffer
which could be implemented by
GizmoBuffer
and custom structs likeIsometryGizmos
, a thin wrapper around an underlyingGizmoBuffer
, that simply applies the providedIsometry
to each position drawn.Any additional gizmo-drawing related function like
gizmos.ellipse(..)
orgizmos.linestrip_2d(..)
present onGizmoBuffer
could now be implemented as functions onLineBuffer
with a default implementation or via extension traits akin toLineBufferExt
that are implemented for all types implementingLineBuffer
. This would also make them available to customGizmoBuffer
s likeIsometryGizmos
as well as users ofGizmos
in systems.Implementing
.gizmo()
The approach using custom
GizmoBuffer
s would then allow us to define two traitsGizmoProvider
andGizmoDraw
akin toMeshable
andMeshBuilder
frombevy_mesh
.GizmoProvider
would be implemented for any applicable primitive and could produce aGizmoDraw
which could then get aGizmos
type likeIsometryGizmos
that applies the isometry to all line(-strip)s drawn. Please note thatGizmoProvider
can be implemented for both 2D and 3D primitives. To distinguish 2D from 3D primitives, a marker traitGizmoDraw2d: GizmoDraw
would be neccessary.By adding an
we can allow drawing primitives without calling
.gizmo()
on them every time or confusing the compiler. This approach would also guarantee thatgizmos.primitive_2d(my_shape.gizmo(), ..)
andgizmos.primitive_2d(&my_shape, ..)
behave identically.Having both
GizmoProvider::gizmo
andGizmoProvider::to_gizmo
may seem unneccessary but may be useful for primitives that are notCopy
. e.g. drawing aPolygon
once should not require cloning the polygon, but always borrowing the vertices of the polygon would prevent storing the builder in aComponent
and is as such not sufficient for this usecase.GizmoProvider
could also be split intoGizmoProvider
andToGizmoProvider
to reduce boilerplate forCopy
primitives.Drawing primitives
Drawing primitives can now be done by calling
gizmos.primitive_Nd(..)
, which could be implemented on an extension trait forLineBuffer
.Notably, 2D primitives can also be drawn and oriented in 3D space using
gizmos.primitive_3d(..)
.Using this API would be very similar to the old one. The only difference is that configurations like
.resolution(..)
would have to be applied on the primitive.Components
We can now store a
GizmoDraw
andColor
inside a component. Storing the builder instead of the compiled line(-strip)s allows for easy modification of the configuration of the gizmos. This may be useful for e.g. reducing the resolution of a sphere when the player moves away. By creating a customstruct CompiledGizmo: LineBuffer + GizmoDraw
we could also store compiled gizmos if computing the points for a gizmo is very expensive. We would then need to simply transform and copy them over to the actualGizmoBuffer
used for drawing.Creating such a component requires that the
builder
is valid for'static
. As mentioned above, this can be achieved by calling.to_gizmo()
on the associated primitive. We could also allow modifying the actual primitive itself throughGizmoComponent
as allT: GizmoProvider
are alsoGizmoDraw
. If we were to make e.g. theEllipseBuilder::half_size
public, the entire primitive and its configuration could be modified, allowing for a lot of flexibility.We can then create a system that draws these gizmos with the entity's transform (or at least translation and rotation) applied. Please note that applying scale aswell could be achieved easily by creating something like a
struct TransformGizmos: LineBuffer
, similar toIsometryGizmos
.Examples
The following is an example implementation of gizmos for
Ellipse
:This is an implementation for a non-
Copy
type,Polyline3d
:As you can see, both of these implementations are a bit simpler than the previous solution using
GizmoPrimitiveNd
. In particular, the implementation does not have to concern itself with the concrete type ofGizmos
both simplifying it and being more flexible.With these implementations, we can now do
Beta Was this translation helpful? Give feedback.
All reactions