From f11b89fb4834996aca40695a49bc504c8ecc3744 Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Fri, 13 Oct 2023 17:15:09 -0400 Subject: [PATCH] Fixes, cleanup * Fix z-ordering issues with the gizmo elements * Restore the translucent far-side rotator ring arcs * Restore the translation arrow fading * Remove dead and/or commented-out code --- studio/src/interaction/gizmos/model.rs | 34 ++- studio/src/interaction/gizmos/view.rs | 403 ++++++++----------------- 2 files changed, 151 insertions(+), 286 deletions(-) diff --git a/studio/src/interaction/gizmos/model.rs b/studio/src/interaction/gizmos/model.rs index 2d23359..1e173e6 100644 --- a/studio/src/interaction/gizmos/model.rs +++ b/studio/src/interaction/gizmos/model.rs @@ -186,14 +186,7 @@ impl GizmoElement { let start = xform.translation + a * base_size * 0.925; let end = xform.translation + b * base_size * 0.925; - - let vector = end - start; - let length = vector.length(); - let direction = vector / length; - - line.origin = start; - line.direction = direction; - line.length = length; + *line = Line::from((start, end)); } #[inline] @@ -313,3 +306,28 @@ bitflags! { const SCREEN_Z = 0b1100; } } + +impl From<(Vec3, Vec3)> for Line { + #[inline] + fn from(value: (Vec3, Vec3)) -> Self { + let (start, end) = value; + let vector = end - start; + let length = vector.length(); + let direction = vector / length; + + Self { + origin: start, + direction, + length, + } + } +} + +impl From for (Vec3, Vec3) { + #[inline] + fn from(value: Line) -> Self { + let start = value.origin; + let end = start + value.direction * value.length; + (start, end) + } +} diff --git a/studio/src/interaction/gizmos/view.rs b/studio/src/interaction/gizmos/view.rs index 69a7ed6..73bf03b 100644 --- a/studio/src/interaction/gizmos/view.rs +++ b/studio/src/interaction/gizmos/view.rs @@ -2,20 +2,13 @@ use std::f32::consts; use bevy::{prelude::*, window::PrimaryWindow}; -// use crate::{design_system, math::Lerp}; +use crate::math::Lerp; -#[rustfmt::skip] use super::{ model::{Arc, Axes, Control, Gizmo, GizmoColor, Ring, Shape}, - // InteractionMode, Selected, TransformSpace, TransformType, + InteractionMode, TransformSpace, TransformType, }; -// #[derive(Clone, Copy)] -// enum GizmoVariant { -// Full, -// Partial, -// } - pub(super) fn configure( q_window: Query<&Window, With>, mut config: ResMut, @@ -23,75 +16,112 @@ pub(super) fn configure( let scale_factor = q_window.single().scale_factor() as f32; config.line_width = 4. * scale_factor; config.line_perspective = false; - config.depth_bias = -1.; + config.depth_bias = -0.1; } pub(super) fn draw( - q_gizmo: Query<&Gizmo>, + q_gizmo: Query<(&Gizmo, &GlobalTransform)>, q_camera_xform: Query<&Transform, With>, mut gizmos: Gizmos, + mode: Res, ) { let cam_xform = q_camera_xform.single(); - // TODO (maybe): - // 1. Map GizmoElements to a new Vec<(Shape, Color)> - // 2. Bisect the rotator rings and push the back sides into the vec - // 3. Sort by camera depth - // 4. Draw - // - // (Starting to feel like a really gross hack -- wish I had a better - // grasp of Bevy's render pipeline so we didn't have to lean so hard on - // the Gizmos API) + let Ok((gizmo, world_xform)) = q_gizmo.get_single() else { + return; + }; - for gizmo in q_gizmo.iter() { - for el in gizmo.0.iter() { - match el.shape { - Shape::Ring(ring) => { - let GizmoColor::Single(color) = el.color else { - unreachable!(); - }; + let mut cached_xform: Option = None; - if let Control::Rotation(Axes::X | Axes::Y | Axes::Z) = el.control { - let (front, _back) = bisect_rotator_ring(&ring, cam_xform); - gizmos.arc(front.center, front.axis, front.start, front.length, color); - // TODO - // gizmos.arc( - // back.center, - // back.axis, - // back.start, - // back.length, - // color.with_a(0.2), - // ); - } else { - gizmos.circle(ring.origin, ring.normal, ring.radius, color); + for el in gizmo.0.iter() { + match el.shape { + Shape::Ring(ring) => { + let GizmoColor::Single(color) = el.color else { + warn!("Unexpected gradient color specified for gizmo ring!"); + continue; + }; + + if let Control::Rotation(Axes::X | Axes::Y | Axes::Z) = el.control { + let (front, back) = bisect_rotator_ring(&ring, cam_xform); + gizmos.arc(front.center, front.axis, front.start, front.length, color); + + if !mode.transform.intersects(TransformType::TRANSLATE) { + gizmos.arc( + back.center, + back.axis, + back.start, + back.length, + color.with_a(0.2), + ); } + } else { + gizmos.circle(ring.origin, ring.normal, ring.radius, color); } - Shape::Line(line) => { - // TODO: Camera-direction fade - let start = line.origin; - let end = line.origin + line.direction * line.length; + } + shape @ (Shape::Line(_) | Shape::Cone(_)) => { + let cam_pos = cam_xform.translation; + let xform = match cached_xform { + Some(xform) => xform, + None => { + let xform = world_xform.compute_transform(); + cached_xform = Some(xform); + xform + } + }; - match el.color { - GizmoColor::Single(color) => { - gizmos.line(start, end, color); - } - GizmoColor::Gradient(color_a, color_b) => { - gizmos.line_gradient(start, end, color_a, color_b); + let basis = match mode.space { + TransformSpace::Local => xform, + TransformSpace::World => Transform::from_translation(xform.translation), + }; + + let Control::Translation(axes) = el.control else { + unreachable!(); + }; + + let alpha = fade_aligned_control(axes, &basis, cam_pos); + + match shape { + Shape::Line(line) => { + let (start, end) = line.into(); + match el.color { + GizmoColor::Single(color) => { + gizmos.line(start, end, color.with_a(alpha)); + } + GizmoColor::Gradient(start_color, end_color) => { + gizmos.line_gradient( + start, + end, + start_color.with_a(alpha), + end_color.with_a(alpha), + ); + } } } - } - Shape::Cone(cone) => { - // TODO: Camera-direction fade - let GizmoColor::Single(color) = el.color else { - unreachable!() - }; - gizmos.cone(cone.base, cone.direction, cone.radius, cone.length, color); + Shape::Cone(cone) => { + let GizmoColor::Single(color) = el.color else { + warn!("Unexpected gradient color specified for gizmo cone!"); + continue; + }; + + gizmos.cone( + cone.base, + cone.direction, + cone.radius, + cone.length, + color.with_a(alpha), + ); + } + _ => unreachable!(), } } } } } +/// Slice a non-screen-space rotator ring in half by the camera's XY plane, +/// returning the semicircle closer to the camera as the first tuple element, +/// and the one further away as the second. +#[inline] fn bisect_rotator_ring(ring: &Ring, cam_xform: &Transform) -> (Arc, Arc) { let plane_intersection = Vec3::cross(ring.normal, cam_xform.forward()).normalize(); let start_a = ring.origin + plane_intersection * ring.radius; @@ -110,230 +140,47 @@ fn bisect_rotator_ring(ring: &Ring, cam_xform: &Transform) -> (Arc, Arc) { (arc_a, arc_b) } -// pub(super) fn draw_old( -// q_selection: Query<&GlobalTransform, With>, -// q_camera_xform: Query<&Transform, With>, -// q_window: Query<&Window, With>, -// mode: Res, -// mut gizmos: Gizmos, -// mut config: ResMut, -// ) { -// if !q_selection.is_empty() { -// // TODO: Use an app state or something so we only have to set this up once -// let scale_factor = q_window.single().scale_factor() as f32; -// config.line_width = 4. * scale_factor; -// config.line_perspective = false; -// config.depth_bias = -1.; +/// Compute an alpha value to fade the translation arrows out when they're +/// pointed directly at or away from the camera. +/// +/// This prevents overlapping/obscuring the center screen-space translation +/// ring, or just generally getting in the way when the control is not very +/// useful to grab onto. +#[inline] +fn fade_aligned_control(axes: Axes, xform: &Transform, cam_pos: Vec3) -> f32 { + let to_camera = (cam_pos - xform.translation).normalize_or_zero(); + let base_factor = match axes { + Axes::X | Axes::Y | Axes::Z => { + let direction = match axes { + Axes::X => xform.local_x(), + Axes::Y => xform.local_y(), + Axes::Z => xform.local_z(), + _ => unreachable!(), + }; -// let camera_xform = q_camera_xform.single(); + Vec3::dot(to_camera, direction).abs() + } + Axes::XY | Axes::XZ | Axes::YZ => { + let (a, b) = match axes { + Axes::XY => (xform.local_x(), xform.local_y()), + Axes::YZ => (xform.local_y(), xform.local_z()), + Axes::XZ => (xform.local_x(), xform.local_z()), + _ => unreachable!(), + }; -// // TODO: We only want to render one gizmo -- this is just for testing -// for world_xform in q_selection.iter() { -// let xform = world_xform.compute_transform(); -// // Note: Scaling the gizmo size like this isn't perfect due to -// // distortion from the perspective projection. It bugged me at -// // first, but even Blender exhibits this type of gizmo distortion if -// // you dial the camera's focal length way down, so I'm gonna call it -// // good enough. -// let dist = Vec3::distance(camera_xform.translation, xform.translation); -// let size = dist / 20.; + f32::max(Vec3::dot(to_camera, a).abs(), Vec3::dot(to_camera, b).abs()) + } + _ => panic!( + "Unexpected `axes` value provided to `fade_aligned_control`: {:?}", + axes + ), + }; -// let variant = if mode -// .transform -// .contains(TransformType::TRANSLATE | TransformType::ROTATE) -// { -// GizmoVariant::Partial -// } else { -// GizmoVariant::Full -// }; - -// if mode.transform.contains(TransformType::TRANSLATE) { -// draw_translate_gizmo(&mut gizmos, variant, &xform, camera_xform, mode.space, size); -// } -// if mode.transform.contains(TransformType::ROTATE) { -// draw_rotate_gizmo(&mut gizmos, variant, &xform, camera_xform, mode.space, size); -// } -// } -// } -// } - -// // FIXME: We might need more control over the Z-ordering / depth bias than the -// // Bevy gizmos give us. As best as I can tell, the elements are drawn in -// // reverse invocation order (the methods called last are rendered first, -// // like the commands are being popped from a LIFO stack), but it's not -// // 100% consistent and there seems to be lots of flickering / Z-fighting. -// // Might not be a big deal, so I'm inclined to roll with it for now and -// // see how users respond. - -// #[inline] -// fn draw_translate_gizmo( -// gizmos: &mut Gizmos, -// variant: GizmoVariant, -// xform: &Transform, -// camera_xform: &Transform, -// space: TransformSpace, -// size: f32, -// ) { -// let origin = xform.translation; -// let basis = match space { -// TransformSpace::Local => xform, -// TransformSpace::World => &Transform::IDENTITY, -// }; - -// for (direction, mut color) in [ -// (basis.local_x(), design_system::Color::RED), -// (basis.local_y(), design_system::Color::GREEN), -// (basis.local_z(), design_system::Color::BLUE), -// ] { -// // Fade the arrow out when it's pointed directly at the camera. -// // (Prevents overlapping/obscuring the center screen-space ring, or just -// // generally getting in the way when it's not very useful to grab onto.) -// let alpha = (1. - Vec3::dot(camera_xform.forward(), direction).abs()) -// .remap((0., 0.1), (0., 1.)) -// .clamp(0., 1.); - -// color = color.with_a(alpha); - -// // In "Partial" mode, we only draw the center ring and the arrow tips to -// // leave room for the rotator rings. -// if matches!(variant, GizmoVariant::Full) { -// // Draw a line from just outside the center ring to the tip of the arrow -// let start = origin + direction * size * 0.25; -// let end = origin + direction * size * 1.6; -// gizmos.line(start, end, color); - -// // Draw a line from the midpoint of this arrow to the midpoint of -// // the next one. This "crossbar" can be used to translate the object -// // on the plane defined by the corresponding axes. -// let midpoint_self = origin + ((end - origin) / 2.); -// let (ortho, mut color_ortho) = get_ortho(basis, direction); - -// // Fade the crossbar when its plane is edge-on with the camera, just -// // like we did for the arrow pointing toward/away from the camera. -// let ortho_alpha = (1. - Vec3::dot(camera_xform.forward(), ortho).abs()) -// .remap((0., 0.1), (0., 1.)) -// .clamp(0., 1.); - -// color_ortho = color_ortho.with_a(f32::min(ortho_alpha, alpha)); - -// let ortho_end = origin + ortho * size * 1.6; -// let midpoint_ortho = origin + ((ortho_end - origin) / 2.); -// gizmos.line_gradient(midpoint_self, midpoint_ortho, color, color_ortho); -// } - -// // Draw the arrow tip -// let factor = match variant { -// GizmoVariant::Full => 1.2, -// GizmoVariant::Partial => 1.4, -// }; -// let cone_base = origin + direction * size * factor; - -// let radius = size * 0.1; -// let length = size * 0.4; -// gizmos.cone(cone_base, direction, radius, length, color); -// } - -// // Draw the center ring. This can be dragged around to translate the object -// // in screen-space (i.e. on the camera's XY plane). -// gizmos.circle(origin, camera_xform.forward(), size * 0.2, Color::WHITE); -// } - -// // Dumb helper function for grabbing the normal vector/color for the translation -// // gizmo's next arrow. We use this to determine where and how to render the -// // planar translation "crossbars". -// #[inline] -// fn get_ortho(xform: &Transform, compare: Vec3) -> (Vec3, Color) { -// let x = xform.local_x(); -// let y = xform.local_y(); -// let z = xform.local_z(); - -// let diff_x = (compare - x).length_squared(); -// let diff_y = (compare - y).length_squared(); -// let diff_z = (compare - z).length_squared(); - -// if diff_x < diff_y && diff_x < diff_z { -// (y, design_system::Color::GREEN) -// } else if diff_y < diff_x && diff_y < diff_z { -// (z, design_system::Color::BLUE) -// } else { -// (x, design_system::Color::RED) -// } -// } - -// #[inline] -// fn draw_rotate_gizmo( -// gizmos: &mut Gizmos, -// variant: GizmoVariant, -// xform: &Transform, -// camera_xform: &Transform, -// space: TransformSpace, -// radius: f32, -// ) { -// let origin = xform.translation; -// let basis = match space { -// TransformSpace::Local => xform, -// TransformSpace::World => &Transform::IDENTITY, -// }; -// let camera_fwd = camera_xform.forward(); - -// // Draw a ring around the whole gizmo for rotating the object in screen -// // space (i.e. around the camera's forward vector). -// gizmos.circle(origin, camera_fwd, radius * 1.2, Color::WHITE); - -// // Draw semicircles for the main basis vector axes, bisected by the camera's -// // XY plane. The `RotatorRings` argument determines which side of the plane -// // to draw on. -// draw_rotator_axis_rings( -// gizmos, -// RotatorRings::Near, -// origin, -// camera_fwd, -// basis, -// radius, -// ); - -// // We skip drawing the far-side of the rings in "Partial" mode to cut down -// // on visual noise and leave less room for mis-clicks. -// if matches!(variant, GizmoVariant::Full) { -// draw_rotator_axis_rings(gizmos, RotatorRings::Far, origin, camera_fwd, basis, radius); -// } -// } - -// enum RotatorRings { -// Near, -// Far, -// } - -// #[inline] -// fn draw_rotator_axis_rings( -// gizmos: &mut Gizmos, -// which: RotatorRings, -// origin: Vec3, -// camera_fwd: Vec3, -// basis: &Transform, -// radius: f32, -// ) { -// for (normal, color) in [ -// (basis.local_x(), design_system::Color::RED), -// (basis.local_y(), design_system::Color::GREEN), -// (basis.local_z(), design_system::Color::BLUE), -// ] { -// // The line of intersection between two planes will be mutually -// // orthogonal to the normal vectors of those planes, so we can find the -// // points of intersection between the ring and the camera plane by -// // taking the cross product of the camera direction and the rotation -// // axis, and scaling it by the ring's radius. The order of the operands -// // determines which of the intersection points we get. -// let (color, perp) = match which { -// RotatorRings::Near => (color, Vec3::cross(normal, camera_fwd)), -// RotatorRings::Far => (color.with_a(0.2), Vec3::cross(camera_fwd, normal)), -// }; - -// let start = origin + perp.normalize() * radius; - -// gizmos.arc(origin, normal, start, consts::PI, color); -// } -// } + #[rustfmt::skip] // This is harder to read without the line breaks + return (1. - base_factor) + .remap((0., 0.1), (0., 1.)) + .clamp(0., 1.); +} trait GizmoExtras { /// Draw an arc around `center` with angular `length` (in radians),