Drawing Primitives with Gizmos (#11072)

The PR is in a reviewable state now in the sense that the basic
implementations are there. There are still some ToDos that I'm aware of:

- [x] docs for all the new structs and traits
- [x] implement `Default` and derive other useful traits for the new
structs
- [x] Take a look at the notes again (Do this after a first round of
reviews)
- [x] Take care of the repetition in the circle drawing functions

---

# Objective

- TLDR: This PR enables us to quickly draw all the newly added
primitives from `bevy_math` in immediate mode with gizmos
- Addresses #10571

## Solution

- This implements the first design idea I had that covered everything
that was mentioned in the Issue
https://github.com/bevyengine/bevy/issues/10571#issuecomment-1863646197

--- 

## Caveats

- I added the `Primitive(2/3)d` impls for `Direction(2/3)d` to make them
work with the current solution. We could impose less strict requirements
for the gizmoable objects and remove the impls afterwards if the
community doesn't like the current approach.

---

## Changelog

- implement capabilities to draw ellipses on the gizmo in general (this
was required to have some code which is able to draw the ellipse
primitive)
- refactored circle drawing code to use the more general ellipse drawing
code to keep code duplication low
- implement `Primitive2d` for `Direction2d` and impl `Primitive3d` for
`Direction3d`
- implement trait to draw primitives with specialized details with
gizmos
  - `GizmoPrimitive2d` for all the 2D primitives
  - `GizmoPrimitive3d` for all the 3D primitives
- (question while writing this: Does it actually matter if we split this
in 2D and 3D? I guess it could be useful in the future if we do
something based on the main rendering mode even though atm it's kinda
useless)

---

---------

Co-authored-by: nothendev <borodinov.ilya@gmail.com>
This commit is contained in:
Robert Walter 2024-02-02 21:13:03 +00:00 committed by GitHub
parent e2916fbad1
commit 041731b7e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 2104 additions and 43 deletions

View file

@ -134,7 +134,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
///
/// # Builder methods
/// The number of segments of the arc (i.e. the level of detail) can be adjusted with the
/// `.segements(...)` method.
/// `.segments(...)` method.
///
/// # Example
/// ```
@ -190,7 +190,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
///
/// # Builder methods
/// The number of segments of the arc (i.e. the level of detail) can be adjusted with the
/// `.segements(...)` method.
/// `.segments(...)` method.
///
/// # Examples
/// ```
@ -236,7 +236,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
///
/// # Builder methods
/// The number of segments of the arc (i.e. the level of detail) can be adjusted with the
/// `.segements(...)` method.
/// `.segments(...)` method.
///
/// # Examples
/// ```

View file

@ -67,7 +67,7 @@ impl<T: GizmoConfigGroup> Drop for ArrowBuilder<'_, '_, '_, T> {
}
impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
/// Draw an arrow in 3D, from `start` to `end`. Has four tips for convienent viewing from any direction.
/// Draw an arrow in 3D, from `start` to `end`. Has four tips for convenient viewing from any direction.
///
/// This should be called for each frame the arrow needs to be rendered.
///

View file

@ -4,20 +4,98 @@
//! and assorted support items.
use crate::prelude::{GizmoConfigGroup, Gizmos};
use bevy_math::Mat2;
use bevy_math::{primitives::Direction3d, Quat, Vec2, Vec3};
use bevy_render::color::Color;
use std::f32::consts::TAU;
pub(crate) const DEFAULT_CIRCLE_SEGMENTS: usize = 32;
fn circle_inner(radius: f32, segments: usize) -> impl Iterator<Item = Vec2> {
fn ellipse_inner(half_size: Vec2, 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
let (x, y) = angle.sin_cos();
Vec2::new(x, y) * half_size
})
}
impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
/// Draw an ellipse in 3D at `position` with the flat side facing `normal`.
///
/// This should be called for each frame the ellipse needs to be rendered.
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.ellipse(Vec3::ZERO, Quat::IDENTITY, Vec2::new(1., 2.), Color::GREEN);
///
/// // Ellipses have 32 line-segments by default.
/// // You may want to increase this for larger ellipses.
/// gizmos
/// .ellipse(Vec3::ZERO, Quat::IDENTITY, Vec2::new(5., 1.), Color::RED)
/// .segments(64);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
#[inline]
pub fn ellipse(
&mut self,
position: Vec3,
rotation: Quat,
half_size: Vec2,
color: Color,
) -> EllipseBuilder<'_, 'w, 's, T> {
EllipseBuilder {
gizmos: self,
position,
rotation,
half_size,
color,
segments: DEFAULT_CIRCLE_SEGMENTS,
}
}
/// Draw an ellipse in 2D.
///
/// This should be called for each frame the ellipse needs to be rendered.
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.ellipse_2d(Vec2::ZERO, 180.0_f32.to_radians(), Vec2::new(2., 1.), Color::GREEN);
///
/// // Ellipses have 32 line-segments by default.
/// // You may want to increase this for larger ellipses.
/// gizmos
/// .ellipse_2d(Vec2::ZERO, 180.0_f32.to_radians(), Vec2::new(5., 1.), Color::RED)
/// .segments(64);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
#[inline]
pub fn ellipse_2d(
&mut self,
position: Vec2,
angle: f32,
half_size: Vec2,
color: Color,
) -> Ellipse2dBuilder<'_, 'w, 's, T> {
Ellipse2dBuilder {
gizmos: self,
position,
rotation: Mat2::from_angle(angle),
half_size,
color,
segments: DEFAULT_CIRCLE_SEGMENTS,
}
}
/// Draw a circle in 3D at `position` with the flat side facing `normal`.
///
/// This should be called for each frame the circle needs to be rendered.
@ -45,12 +123,12 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
normal: Direction3d,
radius: f32,
color: Color,
) -> CircleBuilder<'_, 'w, 's, T> {
CircleBuilder {
) -> EllipseBuilder<'_, 'w, 's, T> {
EllipseBuilder {
gizmos: self,
position,
normal,
radius,
rotation: Quat::from_rotation_arc(Vec3::Z, *normal),
half_size: Vec2::splat(radius),
color,
segments: DEFAULT_CIRCLE_SEGMENTS,
}
@ -82,70 +160,76 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
position: Vec2,
radius: f32,
color: Color,
) -> Circle2dBuilder<'_, 'w, 's, T> {
Circle2dBuilder {
) -> Ellipse2dBuilder<'_, 'w, 's, T> {
Ellipse2dBuilder {
gizmos: self,
position,
radius,
rotation: Mat2::IDENTITY,
half_size: Vec2::splat(radius),
color,
segments: DEFAULT_CIRCLE_SEGMENTS,
}
}
}
/// A builder returned by [`Gizmos::circle`].
pub struct CircleBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
/// A builder returned by [`Gizmos::ellipse`].
pub struct EllipseBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
position: Vec3,
normal: Direction3d,
radius: f32,
rotation: Quat,
half_size: Vec2,
color: Color,
segments: usize,
}
impl<T: GizmoConfigGroup> CircleBuilder<'_, '_, '_, T> {
/// Set the number of line-segments for this circle.
impl<T: GizmoConfigGroup> EllipseBuilder<'_, '_, '_, T> {
/// Set the number of line-segments for this ellipse.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<T: GizmoConfigGroup> Drop for CircleBuilder<'_, '_, '_, T> {
impl<T: GizmoConfigGroup> Drop for EllipseBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
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.));
let positions = ellipse_inner(self.half_size, self.segments)
.map(|vec2| self.rotation * vec2.extend(0.))
.map(|vec3| vec3 + self.position);
self.gizmos.linestrip(positions, self.color);
}
}
/// A builder returned by [`Gizmos::circle_2d`].
pub struct Circle2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
/// A builder returned by [`Gizmos::ellipse_2d`].
pub struct Ellipse2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
position: Vec2,
radius: f32,
rotation: Mat2,
half_size: Vec2,
color: Color,
segments: usize,
}
impl<T: GizmoConfigGroup> Circle2dBuilder<'_, '_, '_, T> {
/// Set the number of line-segments for this circle.
impl<T: GizmoConfigGroup> Ellipse2dBuilder<'_, '_, '_, T> {
/// Set the number of line-segments for this ellipse.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<T: GizmoConfigGroup> Drop for Circle2dBuilder<'_, '_, '_, T> {
impl<T: GizmoConfigGroup> Drop for Ellipse2dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let positions = circle_inner(self.radius, self.segments).map(|vec2| vec2 + self.position);
};
let positions = ellipse_inner(self.half_size, self.segments)
.map(|vec2| self.rotation * vec2)
.map(|vec2| vec2 + self.position);
self.gizmos.linestrip_2d(positions, self.color);
}
}

View file

@ -32,6 +32,7 @@ pub mod arrows;
pub mod circles;
pub mod config;
pub mod gizmos;
pub mod primitives;
#[cfg(feature = "bevy_sprite")]
mod pipeline_2d;
@ -45,6 +46,7 @@ pub mod prelude {
aabb::{AabbGizmoConfigGroup, ShowAabbGizmo},
config::{DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore},
gizmos::Gizmos,
primitives::{dim2::GizmoPrimitive2d, dim3::GizmoPrimitive3d},
AppGizmoBuilder,
};
}

View file

@ -0,0 +1,545 @@
//! A module for rendering each of the 2D [`bevy_math::primitives`] with [`Gizmos`].
use std::f32::consts::PI;
use super::helpers::*;
use bevy_math::primitives::{
BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Direction2d, Ellipse, Line2d, Plane2d,
Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Segment2d, Triangle2d,
};
use bevy_math::{Mat2, Vec2};
use bevy_render::color::Color;
use crate::prelude::{GizmoConfigGroup, Gizmos};
// some magic number since using directions as offsets will result in lines of length 1 pixel
const MIN_LINE_LEN: f32 = 50.0;
const HALF_MIN_LINE_LEN: f32 = 25.0;
// length used to simulate infinite lines
const INFINITE_LEN: f32 = 100_000.0;
/// A trait for rendering 2D geometric primitives (`P`) with [`Gizmos`].
pub trait GizmoPrimitive2d<P: Primitive2d> {
/// The output of `primitive_2d`. This is a builder to set non-default values.
type Output<'a>
where
Self: 'a;
/// Renders a 2D primitive with its associated details.
fn primitive_2d(
&mut self,
primitive: P,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_>;
}
// direction 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<Direction2d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self : 'a;
fn primitive_2d(
&mut self,
primitive: Direction2d,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let direction = Mat2::from_angle(angle) * *primitive;
let start = position;
let end = position + MIN_LINE_LEN * direction;
self.arrow_2d(start, end, color);
}
}
// circle 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<Circle> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Circle,
position: Vec2,
_angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.circle_2d(position, primitive.radius, color);
}
}
// ellipse 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<Ellipse> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Ellipse,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.ellipse_2d(position, angle, primitive.half_size, color);
}
}
// capsule 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<Capsule2d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Capsule2d,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let rotation = Mat2::from_angle(angle);
// transform points from the reference unit square to capsule "rectangle"
let [top_left, top_right, bottom_left, bottom_right, top_center, bottom_center] = [
[-1.0, 1.0],
[1.0, 1.0],
[-1.0, -1.0],
[1.0, -1.0],
// just reuse the pipeline for these points as well
[0.0, 1.0],
[0.0, -1.0],
]
.map(|[sign_x, sign_y]| Vec2::X * sign_x + Vec2::Y * sign_y)
.map(|reference_point| {
let scaling = Vec2::X * primitive.radius + Vec2::Y * primitive.half_length;
reference_point * scaling
})
.map(rotate_then_translate_2d(angle, position));
// draw left and right side of capsule "rectangle"
self.line_2d(bottom_left, top_left, color);
self.line_2d(bottom_right, top_right, color);
// if the capsule is rotated we have to start the arc at a different offset angle,
// calculate that here
let angle_offset = (rotation * Vec2::Y).angle_between(Vec2::Y);
let start_angle_top = angle_offset;
let start_angle_bottom = PI + angle_offset;
// draw arcs
self.arc_2d(top_center, start_angle_top, PI, primitive.radius, color);
self.arc_2d(
bottom_center,
start_angle_bottom,
PI,
primitive.radius,
color,
);
}
}
// line 2d
//
/// Builder for configuring the drawing options of [`Line2d`].
pub struct Line2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
direction: Direction2d, // Direction of the line
position: Vec2, // position of the center of the line
rotation: Mat2, // rotation of the line
color: Color, // color of the line
draw_arrow: bool, // decides whether to indicate the direction of the line with an arrow
}
impl<T: GizmoConfigGroup> Line2dBuilder<'_, '_, '_, T> {
/// Set the drawing mode of the line (arrow vs. plain line)
pub fn draw_arrow(mut self, is_enabled: bool) -> Self {
self.draw_arrow = is_enabled;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<Line2d> for Gizmos<'w, 's, T> {
type Output<'a> = Line2dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Line2d,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
Line2dBuilder {
gizmos: self,
direction: primitive.direction,
position,
rotation: Mat2::from_angle(angle),
color,
draw_arrow: false,
}
}
}
impl<T: GizmoConfigGroup> Drop for Line2dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let direction = self.rotation * *self.direction;
let [start, end] = [1.0, -1.0]
.map(|sign| sign * INFINITE_LEN)
// offset the line from the origin infinitely into the given direction
.map(|length| direction * length)
// translate the line to the given position
.map(|offset| self.position + offset);
self.gizmos.line_2d(start, end, self.color);
// optionally draw an arrow head at the center of the line
if self.draw_arrow {
self.gizmos.arrow_2d(
self.position - direction * MIN_LINE_LEN,
self.position,
self.color,
);
}
}
}
// plane 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<Plane2d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Plane2d,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let rotation = Mat2::from_angle(angle);
// draw normal of the plane (orthogonal to the plane itself)
let normal = primitive.normal;
let normal_segment = Segment2d {
direction: normal,
half_length: HALF_MIN_LINE_LEN,
};
self.primitive_2d(
normal_segment,
// offset the normal so it starts on the plane line
position + HALF_MIN_LINE_LEN * rotation * *normal,
angle,
color,
)
.draw_arrow(true);
// draw the plane line
let direction = Direction2d::new_unchecked(-normal.perp());
self.primitive_2d(Line2d { direction }, position, angle, color)
.draw_arrow(false);
// draw an arrow such that the normal is always left side of the plane with respect to the
// planes direction. This is to follow the "counter-clockwise" convention
self.arrow_2d(
position,
position + MIN_LINE_LEN * (rotation * *direction),
color,
);
}
}
// segment 2d
/// Builder for configuring the drawing options of [`Segment2d`].
pub struct Segment2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
direction: Direction2d, // Direction of the line segment
half_length: f32, // Half-length of the line segment
position: Vec2, // position of the center of the line segment
rotation: Mat2, // rotation of the line segment
color: Color, // color of the line segment
draw_arrow: bool, // decides whether to draw just a line or an arrow
}
impl<T: GizmoConfigGroup> Segment2dBuilder<'_, '_, '_, T> {
/// Set the drawing mode of the line (arrow vs. plain line)
pub fn draw_arrow(mut self, is_enabled: bool) -> Self {
self.draw_arrow = is_enabled;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<Segment2d> for Gizmos<'w, 's, T> {
type Output<'a> = Segment2dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Segment2d,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
Segment2dBuilder {
gizmos: self,
direction: primitive.direction,
half_length: primitive.half_length,
position,
rotation: Mat2::from_angle(angle),
color,
draw_arrow: Default::default(),
}
}
}
impl<T: GizmoConfigGroup> Drop for Segment2dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let direction = self.rotation * *self.direction;
let start = self.position - direction * self.half_length;
let end = self.position + direction * self.half_length;
if self.draw_arrow {
self.gizmos.arrow_2d(start, end, self.color);
} else {
self.gizmos.line_2d(start, end, self.color);
}
}
}
// polyline 2d
impl<'w, 's, const N: usize, T: GizmoConfigGroup> GizmoPrimitive2d<Polyline2d<N>>
for Gizmos<'w, 's, T>
{
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Polyline2d<N>,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.linestrip_2d(
primitive
.vertices
.iter()
.copied()
.map(rotate_then_translate_2d(angle, position)),
color,
);
}
}
// boxed polyline 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<BoxedPolyline2d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: BoxedPolyline2d,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.linestrip_2d(
primitive
.vertices
.iter()
.copied()
.map(rotate_then_translate_2d(angle, position)),
color,
);
}
}
// triangle 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<Triangle2d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Triangle2d,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let [a, b, c] = primitive.vertices;
let positions = [a, b, c, a].map(rotate_then_translate_2d(angle, position));
self.linestrip_2d(positions, color);
}
}
// rectangle 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<Rectangle> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Rectangle,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let [a, b, c, d] =
[(1.0, 1.0), (1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0)].map(|(sign_x, sign_y)| {
Vec2::new(
primitive.half_size.x * sign_x,
primitive.half_size.y * sign_y,
)
});
let positions = [a, b, c, d, a].map(rotate_then_translate_2d(angle, position));
self.linestrip_2d(positions, color);
}
}
// polygon 2d
impl<'w, 's, const N: usize, T: GizmoConfigGroup> GizmoPrimitive2d<Polygon<N>>
for Gizmos<'w, 's, T>
{
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: Polygon<N>,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
// Check if the polygon needs a closing point
let closing_point = {
let last = primitive.vertices.last();
(primitive.vertices.first() != last)
.then_some(last)
.flatten()
.cloned()
};
self.linestrip_2d(
primitive
.vertices
.iter()
.copied()
.chain(closing_point)
.map(rotate_then_translate_2d(angle, position)),
color,
);
}
}
// boxed polygon 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<BoxedPolygon> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: BoxedPolygon,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let closing_point = {
let last = primitive.vertices.last();
(primitive.vertices.first() != last)
.then_some(last)
.flatten()
.cloned()
};
self.linestrip_2d(
primitive
.vertices
.iter()
.copied()
.chain(closing_point)
.map(rotate_then_translate_2d(angle, position)),
color,
);
}
}
// regular polygon 2d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive2d<RegularPolygon> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_2d(
&mut self,
primitive: RegularPolygon,
position: Vec2,
angle: f32,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let points = (0..=primitive.sides)
.map(|p| single_circle_coordinate(primitive.circumcircle.radius, primitive.sides, p))
.map(rotate_then_translate_2d(angle, position));
self.linestrip_2d(points, color);
}
}

View file

@ -0,0 +1,919 @@
//! A module for rendering each of the 3D [`bevy_math::primitives`] with [`Gizmos`].
use super::helpers::*;
use std::f32::consts::TAU;
use bevy_math::primitives::{
BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d,
Plane3d, Polyline3d, Primitive3d, Segment3d, Sphere, Torus,
};
use bevy_math::{Quat, Vec3};
use bevy_render::color::Color;
use crate::prelude::{GizmoConfigGroup, Gizmos};
const DEFAULT_NUMBER_SEGMENTS: usize = 5;
// length used to simulate infinite lines
const INFINITE_LEN: f32 = 10_000.0;
/// A trait for rendering 3D geometric primitives (`P`) with [`Gizmos`].
pub trait GizmoPrimitive3d<P: Primitive3d> {
/// The output of `primitive_3d`. This is a builder to set non-default values.
type Output<'a>
where
Self: 'a;
/// Renders a 3D primitive with its associated details.
fn primitive_3d(
&mut self,
primitive: P,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_>;
}
// direction 3d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Direction3d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Direction3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
self.arrow(position, position + (rotation * *primitive), color);
}
}
// sphere
/// Builder for configuring the drawing options of [`Sphere`].
pub struct SphereBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the sphere
radius: f32,
// Rotation of the sphere around the origin in 3D space
rotation: Quat,
// Center position of the sphere in 3D space
position: Vec3,
// Color of the sphere
color: Color,
// Number of segments used to approximate the sphere geometry
segments: usize,
}
impl<T: GizmoConfigGroup> SphereBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the sphere geometry.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Sphere> for Gizmos<'w, 's, T> {
type Output<'a> = SphereBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Sphere,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
SphereBuilder {
gizmos: self,
radius: primitive.radius,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for SphereBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let SphereBuilder {
radius,
position: center,
rotation,
color,
segments,
..
} = self;
// draws the upper and lower semi spheres
[-1.0, 1.0].into_iter().for_each(|sign| {
let top = *center + (*rotation * Vec3::Y) * sign * *radius;
draw_semi_sphere(
self.gizmos,
*radius,
*segments,
*rotation,
*center,
top,
*color,
);
});
// draws one great circle of the sphere
draw_circle_3d(self.gizmos, *radius, *segments, *rotation, *center, *color);
}
}
// plane 3d
/// Builder for configuring the drawing options of [`Sphere`].
pub struct Plane3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// direction of the normal orthogonal to the plane
normal: Direction3d,
// Rotation of the sphere around the origin in 3D space
rotation: Quat,
// Center position of the sphere in 3D space
position: Vec3,
// Color of the sphere
color: Color,
// Number of axis to hint the plane
axis_count: usize,
// Number of segments used to hint the plane
segment_count: usize,
// Length of segments used to hint the plane
segment_length: f32,
}
impl<T: GizmoConfigGroup> Plane3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to hint the plane.
pub fn segment_count(mut self, count: usize) -> Self {
self.segment_count = count;
self
}
/// Set the length of segments used to hint the plane.
pub fn segment_length(mut self, length: f32) -> Self {
self.segment_length = length;
self
}
/// Set the number of axis used to hint the plane.
pub fn axis_count(mut self, count: usize) -> Self {
self.axis_count = count;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Plane3d> for Gizmos<'w, 's, T> {
type Output<'a> = Plane3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Plane3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Plane3dBuilder {
gizmos: self,
normal: primitive.normal,
rotation,
position,
color,
axis_count: 4,
segment_count: 3,
segment_length: 0.25,
}
}
}
impl<T: GizmoConfigGroup> Drop for Plane3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
// draws the normal
let normal = self.rotation * *self.normal;
self.gizmos
.primitive_3d(self.normal, self.position, self.rotation, self.color);
let normals_normal = normal.any_orthonormal_vector();
// draws the axes
// get rotation for each direction
(0..self.axis_count)
.map(|i| i as f32 * (1.0 / self.axis_count as f32) * TAU)
.map(|angle| Quat::from_axis_angle(normal, angle))
.for_each(|quat| {
let axis_direction = quat * normals_normal;
let direction = Direction3d::new_unchecked(axis_direction);
// for each axis draw dotted line
(0..)
.filter(|i| i % 2 != 0)
.map(|percent| (percent as f32 + 0.5) * self.segment_length * axis_direction)
.map(|position| position + self.position)
.take(self.segment_count)
.for_each(|position| {
self.gizmos.primitive_3d(
Segment3d {
direction,
half_length: self.segment_length * 0.5,
},
position,
Quat::IDENTITY,
self.color,
);
});
});
}
}
// line 3d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Line3d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Line3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let direction = rotation * *primitive.direction;
self.arrow(position, position + direction, color);
let [start, end] = [1.0, -1.0]
.map(|sign| sign * INFINITE_LEN)
.map(|length| direction * length)
.map(|offset| position + offset);
self.line(start, end, color);
}
}
// segment 3d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Segment3d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Segment3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let direction = rotation * *primitive.direction;
let start = position - direction * primitive.half_length;
let end = position + direction * primitive.half_length;
self.line(start, end, color);
}
}
// polyline 3d
impl<'w, 's, const N: usize, T: GizmoConfigGroup> GizmoPrimitive3d<Polyline3d<N>>
for Gizmos<'w, 's, T>
{
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Polyline3d<N>,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.linestrip(
primitive
.vertices
.map(rotate_then_translate_3d(rotation, position)),
color,
);
}
}
// boxed polyline 3d
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<BoxedPolyline3d> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: BoxedPolyline3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
self.linestrip(
primitive
.vertices
.iter()
.copied()
.map(rotate_then_translate_3d(rotation, position)),
color,
);
}
}
// cuboid
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Cuboid> for Gizmos<'w, 's, T> {
type Output<'a> = () where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Cuboid,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
if !self.enabled {
return;
}
let [half_extend_x, half_extend_y, half_extend_z] = primitive.half_size.to_array();
// transform the points from the reference unit cube to the cuboid coords
let vertices @ [a, b, c, d, e, f, g, h] = [
[1.0, 1.0, 1.0],
[-1.0, 1.0, 1.0],
[-1.0, -1.0, 1.0],
[1.0, -1.0, 1.0],
[1.0, 1.0, -1.0],
[-1.0, 1.0, -1.0],
[-1.0, -1.0, -1.0],
[1.0, -1.0, -1.0],
]
.map(|[sx, sy, sz]| Vec3::new(sx * half_extend_x, sy * half_extend_y, sz * half_extend_z))
.map(rotate_then_translate_3d(rotation, position));
// lines for the upper rectangle of the cuboid
let upper = [a, b, c, d]
.into_iter()
.zip([a, b, c, d].into_iter().cycle().skip(1));
// lines for the lower rectangle of the cuboid
let lower = [e, f, g, h]
.into_iter()
.zip([e, f, g, h].into_iter().cycle().skip(1));
// lines connecting upper and lower rectangles of the cuboid
let connections = vertices.into_iter().zip(vertices.into_iter().skip(4));
upper
.chain(lower)
.chain(connections)
.for_each(|(start, end)| {
self.line(start, end, color);
});
}
}
// cylinder 3d
/// Builder for configuring the drawing options of [`Cylinder`].
pub struct Cylinder3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the cylinder
radius: f32,
// Half height of the cylinder
half_height: f32,
// Center position of the cylinder
position: Vec3,
// Rotation of the cylinder
//
// default orientation is: the cylinder is aligned with `Vec3::Y` axis
rotation: Quat,
// Color of the cylinder
color: Color,
// Number of segments used to approximate the cylinder geometry
segments: usize,
}
impl<T: GizmoConfigGroup> Cylinder3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the cylinder geometry.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Cylinder> for Gizmos<'w, 's, T> {
type Output<'a> = Cylinder3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Cylinder,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Cylinder3dBuilder {
gizmos: self,
radius: primitive.radius,
half_height: primitive.half_height,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for Cylinder3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let Cylinder3dBuilder {
gizmos,
radius,
half_height,
position,
rotation,
color,
segments,
} = self;
let normal = *rotation * Vec3::Y;
// draw upper and lower circle of the cylinder
[-1.0, 1.0].into_iter().for_each(|sign| {
draw_circle_3d(
gizmos,
*radius,
*segments,
*rotation,
*position + sign * *half_height * normal,
*color,
);
});
// draw lines connecting the two cylinder circles
draw_cylinder_vertical_lines(
gizmos,
*radius,
*segments,
*half_height,
*rotation,
*position,
*color,
);
}
}
// capsule 3d
/// Builder for configuring the drawing options of [`Capsule3d`].
pub struct Capsule3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the capsule
radius: f32,
// Half length of the capsule
half_length: f32,
// Center position of the capsule
position: Vec3,
// Rotation of the capsule
//
// default orientation is: the capsule is aligned with `Vec3::Y` axis
rotation: Quat,
// Color of the capsule
color: Color,
// Number of segments used to approximate the capsule geometry
segments: usize,
}
impl<T: GizmoConfigGroup> Capsule3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the capsule geometry.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Capsule3d> for Gizmos<'w, 's, T> {
type Output<'a> = Capsule3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Capsule3d,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Capsule3dBuilder {
gizmos: self,
radius: primitive.radius,
half_length: primitive.half_length,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for Capsule3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let Capsule3dBuilder {
gizmos,
radius,
half_length,
position,
rotation,
color,
segments,
} = self;
let normal = *rotation * Vec3::Y;
// draw two semi spheres for the capsule
[1.0, -1.0].into_iter().for_each(|sign| {
let center = *position + sign * *half_length * normal;
let top = center + sign * *radius * normal;
draw_semi_sphere(gizmos, *radius, *segments, *rotation, center, top, *color);
draw_circle_3d(gizmos, *radius, *segments, *rotation, center, *color);
});
// connect the two semi spheres with lines
draw_cylinder_vertical_lines(
gizmos,
*radius,
*segments,
*half_length,
*rotation,
*position,
*color,
);
}
}
// cone 3d
/// Builder for configuring the drawing options of [`Cone`].
pub struct Cone3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the cone
radius: f32,
// Height of the cone
height: f32,
// Center of the cone, half-way between the tip and the base
position: Vec3,
// Rotation of the cone
//
// default orientation is: cone base normal is aligned with the `Vec3::Y` axis
rotation: Quat,
// Color of the cone
color: Color,
// Number of segments used to approximate the cone geometry
segments: usize,
}
impl<T: GizmoConfigGroup> Cone3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the cone geometry.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Cone> for Gizmos<'w, 's, T> {
type Output<'a> = Cone3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Cone,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Cone3dBuilder {
gizmos: self,
radius: primitive.radius,
height: primitive.height,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for Cone3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let Cone3dBuilder {
gizmos,
radius,
height,
position,
rotation,
color,
segments,
} = self;
let half_height = *height * 0.5;
// draw the base circle of the cone
draw_circle_3d(
gizmos,
*radius,
*segments,
*rotation,
*position - *rotation * Vec3::Y * half_height,
*color,
);
// connect the base circle with the tip of the cone
let end = Vec3::Y * half_height;
circle_coordinates(*radius, *segments)
.map(|p| Vec3::new(p.x, -half_height, p.y))
.map(move |p| [p, end])
.map(|ps| ps.map(rotate_then_translate_3d(*rotation, *position)))
.for_each(|[start, end]| {
gizmos.line(start, end, *color);
});
}
}
// conical frustum 3d
/// Builder for configuring the drawing options of [`ConicalFrustum`].
pub struct ConicalFrustum3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the top circle
radius_top: f32,
// Radius of the bottom circle
radius_bottom: f32,
// Height of the conical frustum
height: f32,
// Center of conical frustum, half-way between the top and the bottom
position: Vec3,
// Rotation of the conical frustrum
//
// default orientation is: conical frustrum base shape normals are aligned with `Vec3::Y` axis
rotation: Quat,
// Color of the conical frustum
color: Color,
// Number of segments used to approximate the curved surfaces
segments: usize,
}
impl<T: GizmoConfigGroup> ConicalFrustum3dBuilder<'_, '_, '_, T> {
/// Set the number of segments used to approximate the curved surfaces.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<ConicalFrustum> for Gizmos<'w, 's, T> {
type Output<'a> = ConicalFrustum3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: ConicalFrustum,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
ConicalFrustum3dBuilder {
gizmos: self,
radius_top: primitive.radius_top,
radius_bottom: primitive.radius_bottom,
height: primitive.height,
position,
rotation,
color,
segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for ConicalFrustum3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let ConicalFrustum3dBuilder {
gizmos,
radius_top,
radius_bottom,
height,
position,
rotation,
color,
segments,
} = self;
let half_height = *height * 0.5;
let normal = *rotation * Vec3::Y;
// draw the two circles of the conical frustrum
[(*radius_top, half_height), (*radius_bottom, -half_height)]
.into_iter()
.for_each(|(radius, height)| {
draw_circle_3d(
gizmos,
radius,
*segments,
*rotation,
*position + height * normal,
*color,
);
});
// connect the two circles of the conical frustrum
circle_coordinates(*radius_top, *segments)
.map(move |p| Vec3::new(p.x, half_height, p.y))
.zip(
circle_coordinates(*radius_bottom, *segments)
.map(|p| Vec3::new(p.x, -half_height, p.y)),
)
.map(|(start, end)| [start, end])
.map(|ps| ps.map(rotate_then_translate_3d(*rotation, *position)))
.for_each(|[start, end]| {
gizmos.line(start, end, *color);
});
}
}
// torus 3d
/// Builder for configuring the drawing options of [`Torus`].
pub struct Torus3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// Radius of the minor circle (tube)
minor_radius: f32,
// Radius of the major circle (ring)
major_radius: f32,
// Center of the torus
position: Vec3,
// Rotation of the conical frustrum
//
// default orientation is: major circle normal is aligned with `Vec3::Y` axis
rotation: Quat,
// Color of the torus
color: Color,
// Number of segments in the minor (tube) direction
minor_segments: usize,
// Number of segments in the major (ring) direction
major_segments: usize,
}
impl<T: GizmoConfigGroup> Torus3dBuilder<'_, '_, '_, T> {
/// Set the number of segments in the minor (tube) direction.
pub fn minor_segments(mut self, minor_segments: usize) -> Self {
self.minor_segments = minor_segments;
self
}
/// Set the number of segments in the major (ring) direction.
pub fn major_segments(mut self, major_segments: usize) -> Self {
self.major_segments = major_segments;
self
}
}
impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d<Torus> for Gizmos<'w, 's, T> {
type Output<'a> = Torus3dBuilder<'a, 'w, 's, T> where Self: 'a;
fn primitive_3d(
&mut self,
primitive: Torus,
position: Vec3,
rotation: Quat,
color: Color,
) -> Self::Output<'_> {
Torus3dBuilder {
gizmos: self,
minor_radius: primitive.minor_radius,
major_radius: primitive.major_radius,
position,
rotation,
color,
minor_segments: DEFAULT_NUMBER_SEGMENTS,
major_segments: DEFAULT_NUMBER_SEGMENTS,
}
}
}
impl<T: GizmoConfigGroup> Drop for Torus3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}
let Torus3dBuilder {
gizmos,
minor_radius,
major_radius,
position,
rotation,
color,
minor_segments,
major_segments,
} = self;
let normal = *rotation * Vec3::Y;
// draw 4 circles with major_radius
[
(*major_radius - *minor_radius, 0.0),
(*major_radius + *minor_radius, 0.0),
(*major_radius, *minor_radius),
(*major_radius, -*minor_radius),
]
.into_iter()
.for_each(|(radius, height)| {
draw_circle_3d(
gizmos,
radius,
*major_segments,
*rotation,
*position + height * normal,
*color,
);
});
// along the major circle draw orthogonal minor circles
let affine = rotate_then_translate_3d(*rotation, *position);
circle_coordinates(*major_radius, *major_segments)
.map(|p| Vec3::new(p.x, 0.0, p.y))
.flat_map(|major_circle_point| {
let minor_center = affine(major_circle_point);
// direction facing from the center of the torus towards the minor circles center
let dir_to_translation = (minor_center - *position).normalize();
// the minor circle is draw with 4 arcs this is done to make the minor circle
// connect properly with each of the major circles
let circle_points = [dir_to_translation, normal, -dir_to_translation, -normal]
.map(|offset| minor_center + offset.normalize() * *minor_radius);
circle_points
.into_iter()
.zip(circle_points.into_iter().cycle().skip(1))
.map(move |(from, to)| (minor_center, from, to))
.collect::<Vec<_>>()
})
.for_each(|(center, from, to)| {
gizmos
.short_arc_3d_between(center, from, to, *color)
.segments(*minor_segments);
});
}
}

View file

@ -0,0 +1,115 @@
use std::f32::consts::TAU;
use bevy_math::{Mat2, Quat, Vec2, Vec3};
use bevy_render::color::Color;
use crate::prelude::{GizmoConfigGroup, Gizmos};
/// Performs an isometric transformation on 2D vectors.
///
/// This function takes angle and a position vector, and returns a closure that applies
/// the isometric transformation to any given 2D vector. The transformation involves rotating
/// the vector by the specified angle and then translating it by the given position.
pub(crate) fn rotate_then_translate_2d(angle: f32, position: Vec2) -> impl Fn(Vec2) -> Vec2 {
move |v| Mat2::from_angle(angle) * v + position
}
/// Performs an isometric transformation on 3D vectors.
///
/// This function takes a quaternion representing rotation and a 3D vector representing
/// translation, and returns a closure that applies the isometric transformation to any
/// given 3D vector. The transformation involves rotating the vector by the specified
/// quaternion and then translating it by the given translation vector.
pub(crate) fn rotate_then_translate_3d(rotation: Quat, translation: Vec3) -> impl Fn(Vec3) -> Vec3 {
move |v| rotation * v + translation
}
/// Calculates the `nth` coordinate of a circle segment.
///
/// Given a circle's radiu and the number of segments, this function computes the position
/// of the `nth` point along the circumference of the circle. The rotation starts at `(0.0, radius)`
/// and proceeds counter-clockwise.
pub(crate) fn single_circle_coordinate(radius: f32, segments: usize, nth_point: usize) -> Vec2 {
let angle = nth_point as f32 * TAU / segments as f32;
let (x, y) = angle.sin_cos();
Vec2::new(x, y) * radius
}
/// Generates an iterator over the coordinates of a circle segment.
///
/// This function creates an iterator that yields the positions of points approximating a
/// circle with the given radius, divided into linear segments. The iterator produces `segments`
/// number of points.
pub(crate) fn circle_coordinates(radius: f32, segments: usize) -> impl Iterator<Item = Vec2> {
(0..)
.map(move |p| single_circle_coordinate(radius, segments, p))
.take(segments)
}
/// Draws a semi-sphere.
///
/// This function draws a semi-sphere at the specified `center` point with the given `rotation`,
/// `radius`, and `color`. The `segments` parameter determines the level of detail, and the `top`
/// argument specifies the shape of the semi-sphere's tip.
pub(crate) fn draw_semi_sphere<T: GizmoConfigGroup>(
gizmos: &mut Gizmos<'_, '_, T>,
radius: f32,
segments: usize,
rotation: Quat,
center: Vec3,
top: Vec3,
color: Color,
) {
circle_coordinates(radius, segments)
.map(|p| Vec3::new(p.x, 0.0, p.y))
.map(rotate_then_translate_3d(rotation, center))
.for_each(|from| {
gizmos
.short_arc_3d_between(center, from, top, color)
.segments(segments / 2);
});
}
/// Draws a circle in 3D space.
///
/// # Note
///
/// This function is necessary to use instead of `gizmos.circle` for certain primitives to ensure that points align correctly. For example, the major circles of a torus are drawn with this method, and using `gizmos.circle` would result in the minor circles not being positioned precisely on the major circles' segment points.
pub(crate) fn draw_circle_3d<T: GizmoConfigGroup>(
gizmos: &mut Gizmos<'_, '_, T>,
radius: f32,
segments: usize,
rotation: Quat,
translation: Vec3,
color: Color,
) {
let positions = (0..=segments)
.map(|frac| frac as f32 / segments as f32)
.map(|percentage| percentage * TAU)
.map(|angle| Vec2::from(angle.sin_cos()) * radius)
.map(|p| Vec3::new(p.x, 0.0, p.y))
.map(rotate_then_translate_3d(rotation, translation));
gizmos.linestrip(positions, color);
}
/// Draws the connecting lines of a cylinder between the top circle and the bottom circle.
pub(crate) fn draw_cylinder_vertical_lines<T: GizmoConfigGroup>(
gizmos: &mut Gizmos<'_, '_, T>,
radius: f32,
segments: usize,
half_height: f32,
rotation: Quat,
center: Vec3,
color: Color,
) {
circle_coordinates(radius, segments)
.map(move |point_2d| {
[1.0, -1.0]
.map(|sign| sign * half_height)
.map(|height| Vec3::new(point_2d.x, height, point_2d.y))
})
.map(|ps| ps.map(rotate_then_translate_3d(rotation, center)))
.for_each(|[start, end]| {
gizmos.line(start, end, color);
});
}

View file

@ -0,0 +1,5 @@
//! A module for rendering each of the 2D and 3D [`bevy_math::primitives`] with [`crate::prelude::Gizmos`].
pub mod dim2;
pub mod dim3;
pub(crate) mod helpers;

View file

@ -7,6 +7,7 @@ use crate::Vec2;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct Direction2d(Vec2);
impl Primitive2d for Direction2d {}
impl Direction2d {
/// A unit vector pointing along the positive X axis.

View file

@ -7,6 +7,7 @@ use crate::{Quat, Vec3};
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct Direction3d(Vec3);
impl Primitive3d for Direction3d {}
impl Direction3d {
/// A unit vector pointing along the positive X axis.

View file

@ -1,15 +1,17 @@
//! This example demonstrates Bevy's immediate mode drawing API intended for visual debugging.
use std::f32::consts::PI;
use std::f32::consts::{PI, TAU};
use bevy::prelude::*;
fn main() {
App::new()
.init_state::<PrimitiveState>()
.add_plugins(DefaultPlugins)
.init_gizmo_group::<MyRoundGizmos>()
.add_systems(Startup, setup)
.add_systems(Update, (system, update_config))
.add_systems(Update, (draw_example_collection, update_config))
.add_systems(Update, (draw_primitives, update_primitives))
.run();
}
@ -17,13 +19,61 @@ fn main() {
#[derive(Default, Reflect, GizmoConfigGroup)]
struct MyRoundGizmos {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default)]
enum PrimitiveState {
#[default]
Nothing,
Circle,
Ellipse,
Capsule,
Line,
Plane,
Segment,
Triangle,
Rectangle,
RegularPolygon,
}
impl PrimitiveState {
const ALL: [Self; 10] = [
Self::Nothing,
Self::Circle,
Self::Ellipse,
Self::Capsule,
Self::Line,
Self::Plane,
Self::Segment,
Self::Triangle,
Self::Rectangle,
Self::RegularPolygon,
];
fn next(self) -> Self {
Self::ALL
.into_iter()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
fn last(self) -> Self {
Self::ALL
.into_iter()
.rev()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
// text
commands.spawn(TextBundle::from_section(
"Hold 'Left' or 'Right' to change the line width of straight gizmos\n\
Hold 'Up' or 'Down' to change the line width of round gizmos\n\
Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos",
Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos\n\
Press 'K' or 'J' to cycle through primitives rendered with gizmos",
TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 24.,
@ -32,7 +82,11 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
));
}
fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Time>) {
fn draw_example_collection(
mut gizmos: Gizmos,
mut my_gizmos: Gizmos<MyRoundGizmos>,
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);
@ -54,6 +108,12 @@ fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Ti
// The circles have 32 line-segments by default.
my_gizmos.circle_2d(Vec2::ZERO, 120., Color::BLACK);
my_gizmos.ellipse_2d(
Vec2::ZERO,
time.elapsed_seconds() % TAU,
Vec2::new(100., 200.),
Color::YELLOW_GREEN,
);
// You may want to increase this for larger circles.
my_gizmos
.circle_2d(Vec2::ZERO, 300., Color::NAVY)
@ -70,6 +130,92 @@ fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Ti
);
}
fn draw_primitives(
mut gizmos: Gizmos,
time: Res<Time>,
primitive_state: Res<State<PrimitiveState>>,
) {
let angle = time.elapsed_seconds();
let rotation = Mat2::from_angle(angle);
let position = rotation * Vec2::X;
let color = Color::WHITE;
const SIZE: f32 = 50.0;
match primitive_state.get() {
PrimitiveState::Nothing => {}
PrimitiveState::Circle => {
gizmos.primitive_2d(Circle { radius: SIZE }, position, angle, color);
}
PrimitiveState::Ellipse => gizmos.primitive_2d(
Ellipse {
half_size: Vec2::new(SIZE, SIZE * 0.5),
},
position,
angle,
color,
),
PrimitiveState::Capsule => gizmos.primitive_2d(
Capsule2d {
radius: SIZE * 0.5,
half_length: SIZE,
},
position,
angle,
color,
),
PrimitiveState::Line => drop(gizmos.primitive_2d(
Line2d {
direction: Direction2d::X,
},
position,
angle,
color,
)),
PrimitiveState::Plane => gizmos.primitive_2d(
Plane2d {
normal: Direction2d::Y,
},
position,
angle,
color,
),
PrimitiveState::Segment => drop(gizmos.primitive_2d(
Segment2d {
direction: Direction2d::X,
half_length: SIZE * 0.5,
},
position,
angle,
color,
)),
PrimitiveState::Triangle => gizmos.primitive_2d(
Triangle2d {
vertices: [Vec2::ZERO, Vec2::Y, Vec2::X].map(|p| p * SIZE * 0.5),
},
position,
angle,
color,
),
PrimitiveState::Rectangle => gizmos.primitive_2d(
Rectangle {
half_size: Vec2::splat(SIZE * 0.5),
},
position,
angle,
color,
),
PrimitiveState::RegularPolygon => gizmos.primitive_2d(
RegularPolygon {
circumcircle: Circle { radius: SIZE * 0.5 },
sides: 5,
},
position,
angle,
color,
),
}
}
fn update_config(
mut config_store: ResMut<GizmoConfigStore>,
keyboard: Res<ButtonInput<KeyCode>>,
@ -101,3 +247,16 @@ fn update_config(
my_config.enabled ^= true;
}
}
fn update_primitives(
keyboard: Res<ButtonInput<KeyCode>>,
mut next_primitive_state: ResMut<NextState<PrimitiveState>>,
primitive_state: Res<State<PrimitiveState>>,
) {
if keyboard.just_pressed(KeyCode::KeyJ) {
next_primitive_state.set(primitive_state.get().last());
}
if keyboard.just_pressed(KeyCode::KeyK) {
next_primitive_state.set(primitive_state.get().next());
}
}

View file

@ -2,15 +2,21 @@
use std::f32::consts::PI;
use bevy::math::primitives::Direction3d;
use bevy::math::primitives::{
Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Segment3d, Sphere, Torus,
};
use bevy::prelude::*;
fn main() {
App::new()
.insert_state(PrimitiveState::Nothing)
.init_resource::<PrimitiveSegments>()
.add_plugins(DefaultPlugins)
.init_gizmo_group::<MyRoundGizmos>()
.add_systems(Startup, setup)
.add_systems(Update, (system, rotate_camera, update_config))
.add_systems(Update, rotate_camera)
.add_systems(Update, (draw_example_collection, update_config))
.add_systems(Update, (draw_primitives, update_primitives))
.run();
}
@ -18,6 +24,62 @@ fn main() {
#[derive(Default, Reflect, GizmoConfigGroup)]
struct MyRoundGizmos {}
#[derive(Debug, Clone, Resource)]
pub struct PrimitiveSegments(usize);
impl Default for PrimitiveSegments {
fn default() -> Self {
Self(10)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States)]
enum PrimitiveState {
Nothing,
Sphere,
Plane,
Line,
LineSegment,
Cuboid,
Cylinder,
Capsule,
Cone,
ConicalFrustum,
Torus,
}
impl PrimitiveState {
const ALL: [Self; 11] = [
Self::Sphere,
Self::Plane,
Self::Line,
Self::LineSegment,
Self::Cuboid,
Self::Cylinder,
Self::Capsule,
Self::Cone,
Self::ConicalFrustum,
Self::Torus,
Self::Nothing,
];
fn next(self) -> Self {
Self::ALL
.into_iter()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
fn last(self) -> Self {
Self::ALL
.into_iter()
.rev()
.cycle()
.skip_while(|&x| x != self)
.nth(1)
.unwrap()
}
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
@ -59,7 +121,9 @@ fn setup(
Hold 'Left' or 'Right' to change the line width of straight gizmos\n\
Hold 'Up' or 'Down' to change the line width of round gizmos\n\
Press '1' or '2' to toggle the visibility of straight gizmos or round gizmos\n\
Press 'A' to show all AABB boxes",
Press 'A' to show all AABB boxes\n\
Press 'K' or 'J' to cycle through primitives rendered with gizmos\n\
Press 'H' or 'L' to decrease/increase the amount of segments in the primitives",
TextStyle {
font_size: 20.,
..default()
@ -74,7 +138,17 @@ fn setup(
);
}
fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Time>) {
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 draw_example_collection(
mut gizmos: Gizmos,
mut my_gizmos: Gizmos<MyRoundGizmos>,
time: Res<Time>,
) {
gizmos.cuboid(
Transform::from_translation(Vec3::Y * 0.5).with_scale(Vec3::splat(1.25)),
Color::BLACK,
@ -119,10 +193,143 @@ fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Ti
gizmos.arrow(Vec3::ZERO, Vec3::ONE * 1.5, Color::YELLOW);
}
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 draw_primitives(
mut gizmos: Gizmos,
time: Res<Time>,
primitive_state: Res<State<PrimitiveState>>,
segments: Res<PrimitiveSegments>,
) {
let normal = Vec3::new(
time.elapsed_seconds().sin(),
time.elapsed_seconds().cos(),
time.elapsed_seconds().sin().cos(),
)
.try_normalize()
.unwrap_or(Vec3::X);
let angle = time.elapsed_seconds().to_radians() * 10.0;
let center = Quat::from_axis_angle(Vec3::Z, angle) * Vec3::X;
let rotation = Quat::from_rotation_arc(Vec3::Y, normal);
let segments = segments.0;
match primitive_state.get() {
PrimitiveState::Nothing => {}
PrimitiveState::Sphere => {
gizmos
.primitive_3d(Sphere { radius: 1.0 }, center, rotation, Color::default())
.segments(segments);
}
PrimitiveState::Plane => {
gizmos
.primitive_3d(
Plane3d {
normal: Direction3d::Y,
},
center,
rotation,
Color::default(),
)
.axis_count((segments / 5).max(4))
.segment_count(segments)
.segment_length(1.0 / segments as f32);
}
PrimitiveState::Line => {
gizmos.primitive_3d(
Line3d {
direction: Direction3d::X,
},
center,
rotation,
Color::default(),
);
}
PrimitiveState::LineSegment => {
gizmos.primitive_3d(
Segment3d {
direction: Direction3d::X,
half_length: 1.0,
},
center,
rotation,
Color::default(),
);
}
PrimitiveState::Cuboid => {
gizmos.primitive_3d(
Cuboid {
half_size: Vec3::new(1.0, 0.5, 2.0),
},
center,
rotation,
Color::default(),
);
}
PrimitiveState::Cylinder => {
gizmos
.primitive_3d(
Cylinder {
radius: 1.0,
half_height: 1.0,
},
center,
rotation,
Color::default(),
)
.segments(segments);
}
PrimitiveState::Capsule => {
gizmos
.primitive_3d(
Capsule3d {
radius: 1.0,
half_length: 1.0,
},
center,
rotation,
Color::default(),
)
.segments(segments);
}
PrimitiveState::Cone => {
gizmos
.primitive_3d(
Cone {
radius: 1.0,
height: 1.0,
},
center,
rotation,
Color::default(),
)
.segments(segments);
}
PrimitiveState::ConicalFrustum => {
gizmos
.primitive_3d(
ConicalFrustum {
radius_top: 0.5,
radius_bottom: 1.0,
height: 1.0,
},
center,
rotation,
Color::default(),
)
.segments(segments);
}
PrimitiveState::Torus => {
gizmos
.primitive_3d(
Torus {
minor_radius: 0.3,
major_radius: 1.0,
},
center,
rotation,
Color::default(),
)
.major_segments(segments)
.minor_segments((segments / 4).max(1));
}
}
}
fn update_config(
@ -176,3 +383,26 @@ fn update_config(
config_store.config_mut::<AabbGizmoConfigGroup>().1.draw_all ^= true;
}
}
fn update_primitives(
keyboard: Res<ButtonInput<KeyCode>>,
primitive_state: Res<State<PrimitiveState>>,
mut next_primitive_state: ResMut<NextState<PrimitiveState>>,
mut segments: ResMut<PrimitiveSegments>,
mut segments_f: Local<f32>,
) {
if keyboard.just_pressed(KeyCode::KeyK) {
next_primitive_state.set(primitive_state.get().next());
}
if keyboard.just_pressed(KeyCode::KeyJ) {
next_primitive_state.set(primitive_state.get().last());
}
if keyboard.pressed(KeyCode::KeyL) {
*segments_f = (*segments_f + 0.05).max(2.0);
segments.0 = segments_f.floor() as usize;
}
if keyboard.pressed(KeyCode::KeyH) {
*segments_f = (*segments_f - 0.05).max(2.0);
segments.0 = segments_f.floor() as usize;
}
}