From bcae8e9a8b6496a2d7db92e0e42889a2415bd41e Mon Sep 17 00:00:00 2001 From: Robert Walter <26892280+RobWalt@users.noreply.github.com> Date: Sun, 28 Jan 2024 02:13:17 +0000 Subject: [PATCH] Implement Arc3D for Gizmos (#11540) # Objective - Implement an arc3d API for gizmos - Solves #11536 ## Solution ### `arc_3d` - The current `arc3d` method on gizmos only takes an angle - It draws an "standard arc" by default, this is an arc starting at `Vec3::X`, in the XZ plane, in counter clockwise direction with a normal that is facing up - The "standard arc" can be customized with the usual gizmo builder pattern. This way you'll be able to draw arbitrary arcs ### `short/long_arc_3d_between` - Given `center`, `from`, `to` draws an arc between `from` and `to` --- ## Changelog > This section is optional. If this was a trivial fix, or has no externally-visible impact, you can delete this section. - Added: `Gizmos::arc3d(&mut self, angle)` method - Added: `Gizmos::long_arc_3d_between(&mut self, center, from, to)` method - Added: `Gizmos::short_arc_3d_between(&mut self, center, from, to)` method --- This PR factors out an orthogonal part of another PR as mentioned in [this comment](https://github.com/bevyengine/bevy/pull/11072#issuecomment-1883859573) --- crates/bevy_gizmos/src/arcs.rs | 301 +++++++++++++++++++++++++++++++-- examples/3d/3d_gizmos.rs | 10 ++ 2 files changed, 300 insertions(+), 11 deletions(-) diff --git a/crates/bevy_gizmos/src/arcs.rs b/crates/bevy_gizmos/src/arcs.rs index 27f698e00c..4c93f5ec02 100644 --- a/crates/bevy_gizmos/src/arcs.rs +++ b/crates/bevy_gizmos/src/arcs.rs @@ -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. /// @@ -73,7 +75,7 @@ pub struct Arc2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { impl 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 } } @@ -83,20 +85,18 @@ impl 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 = arc_2d_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 arc_2d_inner( direction_angle: f32, arc_angle: f32, radius: f32, @@ -109,3 +109,282 @@ 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. For default values + /// this is drawing a standard arc. A standard arc is defined as + /// + /// - an arc with a center at `Vec3::ZERO` + /// - starting at `Vec3::X` + /// - embedded in the XZ plane + /// - rotates counterclockwise + /// + /// 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) + /// - `radius`: distance between the arc and it's center point + /// - `position`: position of the arcs center point + /// - `rotation`: defines orientation of the arc, by default we assume the arc is contained in a + /// plane parallel to the XZ plane and the default starting point is (`position + Vec3::X`) + /// - `color`: color of the arc + /// + /// # Builder methods + /// The number of segments of the arc (i.e. the level of detail) can be adjusted with the + /// `.segements(...)` method. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// # use std::f32::consts::PI; + /// fn system(mut gizmos: Gizmos) { + /// // 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(), + /// 0.25, + /// Vec3::ONE, + /// rotation, + /// Color::ORANGE + /// ) + /// .segments(100); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + #[inline] + pub fn arc_3d( + &mut self, + angle: f32, + radius: f32, + position: Vec3, + rotation: Quat, + color: Color, + ) -> Arc3dBuilder<'_, 'w, 's, T> { + Arc3dBuilder { + gizmos: self, + start_vertex: Vec3::X, + center: position, + rotation, + angle, + radius, + color, + segments: None, + } + } + + /// Draws the shortest arc between two points (`from` and `to`) relative to a specified `center` point. + /// + /// # 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. + /// - `color`: color of the arc + /// + /// # Builder methods + /// The number of segments of the arc (i.e. the level of detail) can be adjusted with the + /// `.segements(...)` method. + /// + /// # Examples + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.short_arc_3d_between( + /// Vec3::ONE, + /// Vec3::ONE + Vec3::NEG_ONE, + /// Vec3::ZERO, + /// Color::ORANGE + /// ) + /// .segments(100); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + /// + /// # Notes + /// - This method assumes that the points `from` and `to` are distinct from `center`. If one of + /// the points is coincident with `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, + color: Color, + ) -> Arc3dBuilder<'_, 'w, 's, T> { + self.arc_from_to(center, from, to, color, |x| x) + } + + /// Draws the longest arc between two points (`from` and `to`) relative to a specified `center` point. + /// + /// # 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. + /// - `color`: color of the arc + /// + /// # Builder methods + /// The number of segments of the arc (i.e. the level of detail) can be adjusted with the + /// `.segements(...)` method. + /// + /// # Examples + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.long_arc_3d_between( + /// Vec3::ONE, + /// Vec3::ONE + Vec3::NEG_ONE, + /// Vec3::ZERO, + /// Color::ORANGE + /// ) + /// .segments(100); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + /// + /// # Notes + /// - This method assumes that the points `from` and `to` are distinct from `center`. If one of + /// the points is coincident with `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, + color: Color, + ) -> Arc3dBuilder<'_, 'w, 's, T> { + self.arc_from_to(center, from, to, color, |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, + color: Color, + angle_fn: impl Fn(f32) -> f32, + ) -> Arc3dBuilder<'_, 'w, 's, T> { + // `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, + 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, +} + +impl Arc3dBuilder<'_, '_, '_, T> { + /// Set the number of line-segments for this arc. + pub fn segments(mut self, segments: usize) -> Self { + self.segments.replace(segments); + self + } +} + +impl 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 = arc_3d_inner( + self.start_vertex, + self.center, + self.rotation, + self.angle, + self.radius, + segments, + ); + self.gizmos.linestrip(positions, self.color); + } +} + +fn arc_3d_inner( + start_vertex: Vec3, + center: Vec3, + rotation: Quat, + angle: f32, + radius: f32, + segments: usize, +) -> impl Iterator { + // 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 +} diff --git a/examples/3d/3d_gizmos.rs b/examples/3d/3d_gizmos.rs index f2bf53a524..f25443f129 100644 --- a/examples/3d/3d_gizmos.rs +++ b/examples/3d/3d_gizmos.rs @@ -96,6 +96,16 @@ fn system(mut gizmos: Gizmos, mut my_gizmos: Gizmos, time: Res