Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Arc3D for Gizmos #11540

Merged
merged 8 commits into from
Jan 28, 2024
327 changes: 315 additions & 12 deletions crates/bevy_gizmos/src/arcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

use crate::circles::DEFAULT_CIRCLE_SEGMENTS;
use crate::prelude::{GizmoConfigGroup, Gizmos};
use bevy_math::Vec2;
use bevy_math::{Quat, Vec2, Vec3};
use bevy_render::color::Color;
use std::f32::consts::TAU;

// === 2D ===

impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
/// Draw an arc, which is a part of the circumference of a circle, in 2D.
///
Expand Down Expand Up @@ -73,7 +75,7 @@ pub struct Arc2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
impl<T: GizmoConfigGroup> Arc2dBuilder<'_, '_, '_, T> {
/// Set the number of line-segments for this arc.
pub fn segments(mut self, segments: usize) -> Self {
self.segments = Some(segments);
self.segments.replace(segments);
self
}
}
Expand All @@ -83,20 +85,18 @@ impl<T: GizmoConfigGroup> Drop for Arc2dBuilder<'_, '_, '_, T> {
if !self.gizmos.enabled {
return;
}
let segments = match self.segments {
Some(segments) => segments,
// Do a linear interpolation between 1 and `DEFAULT_CIRCLE_SEGMENTS`
// using the arc angle as scalar.
None => ((self.arc_angle.abs() / TAU) * DEFAULT_CIRCLE_SEGMENTS as f32).ceil() as usize,
};

let positions = arc_inner(self.direction_angle, self.arc_angle, self.radius, segments)
.map(|vec2| vec2 + self.position);

let segments = self
.segments
.unwrap_or_else(|| segments_from_angle(self.arc_angle));

let positions = arc2d_inner(self.direction_angle, self.arc_angle, self.radius, segments)
.map(|vec2| (vec2 + self.position));
self.gizmos.linestrip_2d(positions, self.color);
}
}

fn arc_inner(
fn arc2d_inner(
RobWalt marked this conversation as resolved.
Show resolved Hide resolved
direction_angle: f32,
arc_angle: f32,
radius: f32,
Expand All @@ -109,3 +109,306 @@ fn arc_inner(
Vec2::from(angle.sin_cos()) * radius
})
}

// === 3D ===

impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
/// Draw an arc, which is a part of the circumference of a circle, in 3D. This defaults to
/// drawing a standard arc. This standard arc starts at `Vec3::X`, is embedded in the XZ plane,
/// rotates counterclockwise and has the following default properties:
///
/// - radius: 1.0
/// - center: `Vec3::ZERO`
/// - rotation: `Quat::IDENTITY` (in XZ plane, normal points upwards)
/// - color: white
/// - segments: depending on angle
///
/// All of these properties can be modified with builder methods which are available on the
/// returned struct.
///
/// This should be called for each frame the arc needs to be rendered.
///
/// # Arguments
/// - `angle` sets how much of a circle circumference is passed, e.g. PI is half a circle. This
/// value should be in the range (-2 * PI..=2 * PI)
///
/// # Example
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use std::f32::consts::PI;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.arc_3d(PI);
///
/// // This example shows how to modify the default settings
///
/// // rotation rotates normal to point in the direction of `Vec3::NEG_ONE`
/// let rotation = Quat::from_rotation_arc(Vec3::Y, Vec3::NEG_ONE.normalize());
///
/// gizmos
/// .arc_3d(270.0_f32.to_radians())
/// .radius(0.25)
/// .center(Vec3::ONE)
/// .rotation(rotation)
/// .segments(100)
/// .color(Color::ORANGE);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
#[inline]
pub fn arc_3d(&mut self, angle: f32) -> Arc3dBuilder<'_, 'w, 's, T> {
Arc3dBuilder {
gizmos: self,
start_vertex: Vec3::X,
center: Vec3::ZERO,
rotation: Quat::IDENTITY,
angle,
radius: 1.0,
color: Color::default(),
segments: None,
}
}

/// Draws the shortest arc between two points (`from` and `to`) relative to a specified `center` point.
///
/// The arc can be mofified via the arc builder pattern. The following properties can be
/// adjusted:
///
/// - radius
/// - center
/// - rotation
/// - color
/// - segments
///
/// # Arguments
///
/// - `center` - The center point around which the arc is drawn.
/// - `from` - The starting point of the arc.
/// - `to` - The ending point of the arc.
///
/// # Examples
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use std::f32::consts::PI;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.short_arc_3d_between(Vec3::ZERO, Vec3::X, Vec3::Y);
///
/// // This example shows how to modify the default settings
///
/// gizmos.short_arc_3d_between(Vec3::ONE, Vec3::ONE + Vec3::NEG_ONE, Vec3::ZERO)
/// .radius(0.25)
/// .center(Vec3::ZERO)
/// .segments(100)
/// .color(Color::ORANGE);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
///
/// # Notes
/// - This method assumes that the points `from` and `to` are distinct from the `center`. If
/// the points are coincident with the `center`, nothing is rendered.
/// - The arc is drawn as a portion of a circle with a radius equal to the distance from the
/// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then
/// the results will behave as if this were the case
#[inline]
pub fn short_arc_3d_between(
&mut self,
center: Vec3,
from: Vec3,
to: Vec3,
) -> Arc3dBuilder<'_, 'w, 's, T> {
self.arc_from_to(center, from, to, |x| x)
}

/// Draws the longest arc between two points (`from` and `to`) relative to a specified `center` point.
///
/// The arc can be modified via the arc builder pattern. The following properties can be adjusted:
///
/// - radius
/// - center
/// - rotation
/// - color
/// - segments
///
/// # Arguments
/// - `center` - The center point around which the arc is drawn.
/// - `from` - The starting point of the arc.
/// - `to` - The ending point of the arc.
///
/// # Examples
/// ```
/// # use bevy_gizmos::prelude::*;
/// # use bevy_render::prelude::*;
/// # use bevy_math::prelude::*;
/// # use std::f32::consts::PI;
/// fn system(mut gizmos: Gizmos) {
/// gizmos.long_arc_3d_between(Vec3::ZERO, Vec3::X, Vec3::Y);
///
/// // This example shows how to modify the default settings
/// gizmos.long_arc_3d_between(Vec3::ONE, Vec3::ONE + Vec3::NEG_ONE, Vec3::ZERO)
/// .radius(0.25)
/// .center(Vec3::ZERO)
/// .segments(100)
/// .color(Color::ORANGE);
/// }
/// # bevy_ecs::system::assert_is_system(system);
/// ```
///
/// # Notes
/// - This method assumes that the points `from` and `to` are distinct from the `center`. If
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit nervous about using "behavior is undefined" here (and above): it's not UB in the compiler sense and we may confuse readers. Maybe "graphical glitches may occur"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for spotting this!

I just tested these cases (after all we're also part scientists, right? :D). It seems like nothing happens at all. We should probably communicate this to the user.

Do you think an early return might make sense? I'm not really sure. On the one hand we could save some compute in the case that we wouldn't draw anything anyways. On the other hand this is an edge case and adding two checks for every non-edge case might be needless overhead.

I think I'm just going for changing the comment for now, but please let me know what you think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think let's just change the comment and leave the behavior for now. I'm really not concerned about the performance in pathological cases and if it doesn't crash I'm quite happy with it as is.

/// the points are coincident with the `center`, nothing is rendered.
/// - The arc is drawn as a portion of a circle with a radius equal to the distance from the
/// `center` to `from`. If the distance from `center` to `to` is not equal to the radius, then
/// the results will behave as if this were the case.
#[inline]
pub fn long_arc_3d_between(
&mut self,
center: Vec3,
from: Vec3,
to: Vec3,
) -> Arc3dBuilder<'_, 'w, 's, T> {
self.arc_from_to(center, from, to, |angle| {
if angle > 0.0 {
TAU - angle
} else if angle < 0.0 {
-TAU - angle
} else {
0.0
}
})
}

#[inline]
fn arc_from_to(
&mut self,
center: Vec3,
from: Vec3,
to: Vec3,
angle_fn: impl Fn(f32) -> f32,
) -> Arc3dBuilder<'_, 'w, 's, T> {
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
// `from` and `to` can be the same here since in either case nothing gets rendered and the
// orientation ambiguity of `up` doesn't matter
let from_axis = (from - center).normalize_or_zero();
let to_axis = (to - center).normalize_or_zero();
let (up, angle) = Quat::from_rotation_arc(from_axis, to_axis).to_axis_angle();

let angle = angle_fn(angle);
let radius = center.distance(from);
let rotation = Quat::from_rotation_arc(Vec3::Y, up);

let start_vertex = rotation.inverse() * from_axis;

Arc3dBuilder {
gizmos: self,
start_vertex,
center,
rotation,
angle,
radius,
color: Color::default(),
segments: None,
}
}
}

/// A builder returned by [`Gizmos::arc_2d`].
pub struct Arc3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> {
gizmos: &'a mut Gizmos<'w, 's, T>,
// this is the vertex the arc starts on in the XZ plane. For the normal arc_3d method this is
// always starting at Vec3::X. For the short/long arc methods we actually need a way to start
// at the from position and this is where this internal field comes into play. Some implicit
// assumptions:
//
// 1. This is always in the XZ plane
// 2. This is always normalized
//
// DO NOT expose this field to users as it is easy to mess this up
start_vertex: Vec3,
center: Vec3,
rotation: Quat,
angle: f32,
radius: f32,
color: Color,
segments: Option<usize>,
}

impl<T: GizmoConfigGroup> Arc3dBuilder<'_, '_, '_, T> {
/// Set the number of line-segments for this arc.
pub fn segments(mut self, segments: usize) -> Self {
self.segments.replace(segments);
self
}

/// Set the center of the standard arc
pub fn center(mut self, center: Vec3) -> Self {
self.center = center;
self
}

/// Set the radius of the arc
pub fn radius(mut self, radius: f32) -> Self {
self.radius = radius;
self
}

/// Rotate the standard arc from the XZ plane with this rotation
pub fn rotation(mut self, rotation: Quat) -> Self {
self.rotation = rotation;
self
}

/// Set the color of the arc
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
}

impl<T: GizmoConfigGroup> Drop for Arc3dBuilder<'_, '_, '_, T> {
fn drop(&mut self) {
if !self.gizmos.enabled {
return;
}

let segments = self
.segments
.unwrap_or_else(|| segments_from_angle(self.angle));

let positions = arc3d_inner(
self.start_vertex,
self.center,
self.rotation,
self.angle,
self.radius,
segments,
);
self.gizmos.linestrip(positions, self.color);
}
}

fn arc3d_inner(
RobWalt marked this conversation as resolved.
Show resolved Hide resolved
start_vertex: Vec3,
center: Vec3,
rotation: Quat,
angle: f32,
radius: f32,
segments: usize,
) -> impl Iterator<Item = Vec3> {
// drawing arcs bigger than TAU degrees or smaller than -TAU degrees makes no sense since
// we won't see the overlap and we would just decrease the level of details since the segments
// would be larger
let angle = angle.clamp(-TAU, TAU);
(0..=segments)
.map(move |frac| frac as f32 / segments as f32)
.map(move |percentage| angle * percentage)
.map(move |frac_angle| Quat::from_axis_angle(Vec3::Y, frac_angle) * start_vertex)
.map(move |p| rotation * (p * radius) + center)
}

// helper function for getting a default value for the segments parameter
fn segments_from_angle(angle: f32) -> usize {
((angle.abs() / TAU) * DEFAULT_CIRCLE_SEGMENTS as f32).ceil() as usize
}
8 changes: 8 additions & 0 deletions examples/3d/3d_gizmos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos<MyRoundGizmos>, time: Res<Ti
);
}

my_gizmos
.arc_3d(180.0_f32.to_radians())
.radius(0.2)
.center(Vec3::ONE)
.rotation(Quat::from_rotation_arc(Vec3::Y, Vec3::ONE.normalize()))
.segments(10)
.color(Color::ORANGE);

// Circles have 32 line-segments by default.
my_gizmos.circle(Vec3::ZERO, Direction3d::Y, 3., Color::BLACK);
// You may want to increase this for larger circles or spheres.
Expand Down
Loading