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.ExtrusionorRing.Component: We could define aGizmoComponentstruct and implementComponentfor it. This component could draw a specified Gizmo after applying the entitiesTransformto the Gizmo. This could provide a uniform solution for drawing hitboxes or colliders.gizmos.primitive_2d(..)and only select 2D primitives likeEllipseorRectcan 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(..)onGizmoBufferto a traitLineBufferand implement it forGizmoBufferand wrappers that apply transformations to the lines drawn. We could then use a genericGizmos: LineBufferin each implementation of gizmos for primitives. This would also enable us to store gizmos for later use.Custom
GizmoBuffersWe 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 onGizmoBufferand 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
LineBufferwhich could be implemented by
GizmoBufferand custom structs likeIsometryGizmos, a thin wrapper around an underlyingGizmoBuffer, that simply applies the providedIsometryto each position drawn.Any additional gizmo-drawing related function like
gizmos.ellipse(..)orgizmos.linestrip_2d(..)present onGizmoBuffercould now be implemented as functions onLineBufferwith a default implementation or via extension traits akin toLineBufferExtthat are implemented for all types implementingLineBuffer. This would also make them available to customGizmoBuffers likeIsometryGizmosas well as users ofGizmosin systems.Implementing
.gizmo()The approach using custom
GizmoBuffers would then allow us to define two traitsGizmoProviderandGizmoDrawakin toMeshableandMeshBuilderfrombevy_mesh.GizmoProviderwould be implemented for any applicable primitive and could produce aGizmoDrawwhich could then get aGizmostype likeIsometryGizmosthat applies the isometry to all line(-strip)s drawn. Please note thatGizmoProvidercan be implemented for both 2D and 3D primitives. To distinguish 2D from 3D primitives, a marker traitGizmoDraw2d: GizmoDrawwould 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::gizmoandGizmoProvider::to_gizmomay seem unneccessary but may be useful for primitives that are notCopy. e.g. drawing aPolygononce should not require cloning the polygon, but always borrowing the vertices of the polygon would prevent storing the builder in aComponentand is as such not sufficient for this usecase.GizmoProvidercould also be split intoGizmoProviderandToGizmoProviderto reduce boilerplate forCopyprimitives.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
GizmoDrawandColorinside 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 + GizmoDrawwe 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 actualGizmoBufferused for drawing.Creating such a component requires that the
builderis 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 throughGizmoComponentas allT: GizmoProviderare alsoGizmoDraw. If we were to make e.g. theEllipseBuilder::half_sizepublic, 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-
Copytype,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 ofGizmosboth simplifying it and being more flexible.With these implementations, we can now do
Beta Was this translation helpful? Give feedback.
All reactions