From ef1448701543bc9bf29fd9a4add684630bdb5bc4 Mon Sep 17 00:00:00 2001 From: Urho Laukkarinen Date: Sat, 6 Apr 2024 12:24:17 +0300 Subject: [PATCH] Fixed transformations causing weird things at certain angles Gizmo now manipulates individual transform components (rotation, translation, scale) instead of matrices. This allows for better precision. When converting between individual components and 4x4 matrices, floating point errors accumulate and may cause unexpected things --- README.md | 2 +- crates/transform-gizmo-bevy/src/lib.rs | 23 +++++-- crates/transform-gizmo-egui/src/lib.rs | 34 +++++----- crates/transform-gizmo/src/config.rs | 15 ++--- crates/transform-gizmo/src/gizmo.rs | 88 +++++++++++++++----------- crates/transform-gizmo/src/lib.rs | 4 +- crates/transform-gizmo/src/math.rs | 21 ++++++ examples/egui/src/main.rs | 25 ++++++-- 8 files changed, 137 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index d874cc0..79d3ef2 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ with your favorite graphics APIs. ## Other -The gizmo exposes matrices and vectors as [mint](https://github.com/kvark/mint) types, which means it is easy to use with matrix types from various crates +The gizmo exposes mathematical types as [mint](https://github.com/kvark/mint) types, which means it is easy to use with types from various crates such as [nalgebra](https://github.com/dimforge/nalgebra), [glam](https://github.com/bitshifter/glam-rs) and [cgmath](https://github.com/rustgd/cgmath). You may need to enable a `mint` feature, depending on the math library. diff --git a/crates/transform-gizmo-bevy/src/lib.rs b/crates/transform-gizmo-bevy/src/lib.rs index 25c9eff..47bbe43 100644 --- a/crates/transform-gizmo-bevy/src/lib.rs +++ b/crates/transform-gizmo-bevy/src/lib.rs @@ -31,6 +31,7 @@ use bevy::prelude::*; use bevy::utils::{HashMap, Uuid}; use bevy::window::PrimaryWindow; +use bevy_math::{DQuat, DVec3}; use render::{DrawDataHandles, TransformGizmoRenderPlugin}; use transform_gizmo::config::{DEFAULT_SNAP_ANGLE, DEFAULT_SNAP_DISTANCE, DEFAULT_SNAP_SCALE}; @@ -221,7 +222,11 @@ fn update_gizmos( let gizmo_result = gizmo.update( gizmo_interaction, - &[target_transform.compute_matrix().as_dmat4().into()], + &[transform_gizmo::math::Transform { + translation: target_transform.translation.as_dvec3().into(), + rotation: target_transform.rotation.as_dquat().into(), + scale: target_transform.scale.as_dvec3().into(), + }], ); let is_focused = gizmo.is_focused(); @@ -235,8 +240,9 @@ fn update_gizmos( continue; }; - *target_transform = - Transform::from_matrix(bevy::math::DMat4::from(*result_transform).as_mat4()); + target_transform.translation = DVec3::from(result_transform.translation).as_vec3(); + target_transform.rotation = DQuat::from(result_transform.rotation).as_quat(); + target_transform.scale = DVec3::from(result_transform.scale).as_vec3(); } gizmo_target.latest_result = gizmo_result.map(|(result, _)| result); @@ -250,7 +256,11 @@ fn update_gizmos( gizmo_interaction, target_transforms .iter() - .map(|transform| transform.compute_matrix().as_dmat4().into()) + .map(|transform| transform_gizmo::math::Transform { + translation: transform.translation.as_dvec3().into(), + rotation: transform.rotation.as_dquat().into(), + scale: transform.scale.as_dvec3().into(), + }) .collect::>() .as_slice(), ); @@ -267,8 +277,9 @@ fn update_gizmos( continue; }; - *target_transform = - Transform::from_matrix(bevy::math::DMat4::from(*result_transform).as_mat4()); + target_transform.translation = DVec3::from(result_transform.translation).as_vec3(); + target_transform.rotation = DQuat::from(result_transform.rotation).as_quat(); + target_transform.scale = DVec3::from(result_transform.scale).as_vec3(); } gizmo_target.latest_result = gizmo_result.as_ref().map(|(result, _)| *result); diff --git a/crates/transform-gizmo-egui/src/lib.rs b/crates/transform-gizmo-egui/src/lib.rs index 575e651..a80a626 100644 --- a/crates/transform-gizmo-egui/src/lib.rs +++ b/crates/transform-gizmo-egui/src/lib.rs @@ -1,8 +1,7 @@ //! Provides a 3D transformation gizmo for the Egui library. //! -//! transform-gizmo-egui provides a feature-rich and configurable 3D transformation -//! gizmo that can be used to manipulate 4x4 transformation matrices (position, rotation, scale) -//! visually. +//! transform-gizmo-egui provides a feature-rich and configurable gizmo +//! that can be used for 3d transformations (translation, rotation, scale). //! //! # Usage //! @@ -14,7 +13,7 @@ //! let gizmo = Gizmo::default(); //! ``` //! -//! When drawing the gui, update the gizmo configuration. +//! Update the gizmo configuration as needed, for example, when the camera moves. //! //! ```ignore //! gizmo.update_config(GizmoConfig { @@ -26,20 +25,28 @@ //! }); //! ``` //! -//! Finally, interact with the gizmo. The function takes a slice of matrices as an +//! Finally, interact with the gizmo. The function takes a slice of transforms as an //! input. The result is [`Some`] if the gizmo was successfully interacted with this frame. -//! In the result you can find the modified matrices, in the same order as was given to the function +//! In the result you can find the modified transforms, in the same order as was given to the function //! as arguments. //! //! ```ignore -//! if let Some(result) = gizmo.interact(ui, &[model_matrix.into()]) { -//! model_matrix = result.targets.first().copied().unwrap().into(); +//! let mut transform = Transform::from_scale_rotation_translation(scale, rotation, translation); +//! +//! if let Some((result, new_transforms)) = gizmo.interact(ui, &[transform]) { +//! for (new_transform, transform) in +//! new_transforms.iter().zip(std::iter::once(&mut transform)) +//! { +//! // Apply the modified transforms +//! *transform = *new_transform; +//! } //! } //! ``` //! //! use egui::{epaint::Vertex, Mesh, PointerButton, Pos2, Rgba, Ui}; +use transform_gizmo::math::Transform; pub use transform_gizmo::*; pub mod prelude; @@ -47,19 +54,16 @@ pub trait GizmoExt { /// Interact with the gizmo and draw it to Ui. /// /// Returns result of the gizmo interaction. - fn interact( - &mut self, - ui: &Ui, - targets: &[mint::RowMatrix4], - ) -> Option<(GizmoResult, Vec>)>; + fn interact(&mut self, ui: &Ui, targets: &[Transform]) + -> Option<(GizmoResult, Vec)>; } impl GizmoExt for Gizmo { fn interact( &mut self, ui: &Ui, - targets: &[mint::RowMatrix4], - ) -> Option<(GizmoResult, Vec>)> { + targets: &[Transform], + ) -> Option<(GizmoResult, Vec)> { let config = self.config(); let egui_viewport = egui::Rect { diff --git a/crates/transform-gizmo/src/config.rs b/crates/transform-gizmo/src/config.rs index 7fbc00b..b601bde 100644 --- a/crates/transform-gizmo/src/config.rs +++ b/crates/transform-gizmo/src/config.rs @@ -5,7 +5,9 @@ pub use ecolor::Color32; use emath::Rect; use enumset::{enum_set, EnumSet, EnumSetType}; -use crate::math::{screen_to_world, world_to_screen, DMat4, DQuat, DVec3, DVec4, Vec4Swizzles}; +use crate::math::{ + screen_to_world, world_to_screen, DMat4, DQuat, DVec3, DVec4, Transform, Vec4Swizzles, +}; /// The default snapping distance for rotation in radians pub const DEFAULT_SNAP_ANGLE: f32 = std::f32::consts::PI / 32.0; @@ -149,19 +151,16 @@ impl PreparedGizmoConfig { } } - pub(crate) fn update_for_targets(&mut self, targets: &[DMat4]) { + pub(crate) fn update_for_targets(&mut self, targets: &[Transform]) { let mut scale = DVec3::ZERO; let mut translation = DVec3::ZERO; let mut rotation = DQuat::IDENTITY; let mut target_count = 0; for target in targets { - let (s, r, t) = target.to_scale_rotation_translation(); - - scale += s; - translation += t; - - rotation = r; + scale += DVec3::from(target.scale); + translation += DVec3::from(target.translation); + rotation = DQuat::from(target.rotation); target_count += 1; } diff --git a/crates/transform-gizmo/src/gizmo.rs b/crates/transform-gizmo/src/gizmo.rs index 3b52129..c3ac054 100644 --- a/crates/transform-gizmo/src/gizmo.rs +++ b/crates/transform-gizmo/src/gizmo.rs @@ -4,9 +4,9 @@ use enumset::EnumSet; use std::ops::{Add, AddAssign, Sub}; use crate::config::{GizmoConfig, GizmoDirection, GizmoMode, PreparedGizmoConfig}; -use crate::math::screen_to_world; +use crate::math::{screen_to_world, Transform}; use epaint::Mesh; -use glam::{DMat4, DVec3}; +use glam::{DQuat, DVec3}; use crate::subgizmo::rotation::RotationParams; use crate::subgizmo::scale::ScaleParams; @@ -16,7 +16,7 @@ use crate::subgizmo::{ SubGizmoControl, TranslationSubGizmo, }; -/// A transform gizmo for manipulating 4x4 matrices. +/// A 3D transformation gizmo. #[derive(Clone, Debug)] pub struct Gizmo { /// Prepared configuration of the gizmo. @@ -31,7 +31,7 @@ pub struct Gizmo { subgizmos: Vec, active_subgizmo_id: Option, - target_start_transforms: Vec, + target_start_transforms: Vec, } impl Default for Gizmo { @@ -70,14 +70,41 @@ impl Gizmo { /// Updates the gizmo based on given interaction information. /// + /// # Examples + /// + /// ``` + /// # // Dummy values + /// # use transform_gizmo::GizmoInteraction; + /// # let mut gizmo = transform_gizmo::Gizmo::default(); + /// # let cursor_pos = Default::default(); + /// # let drag_started = true; + /// # let dragging = true; + /// # let mut transforms = vec![]; + /// + /// let interaction = GizmoInteraction { + /// cursor_pos, + /// drag_started, + /// dragging + /// }; + /// + /// if let Some((_result, new_transforms)) = gizmo.update(interaction, &transforms) { + /// for (new_transform, transform) in + /// // Update transforms + /// new_transforms.iter().zip(&mut transforms) + /// { + /// *transform = *new_transform; + /// } + /// } + /// ``` + /// /// Returns the result of the interaction with the updated transformation. /// /// [`Some`] is returned when any of the subgizmos is being dragged, [`None`] otherwise. pub fn update( &mut self, interaction: GizmoInteraction, - targets: &[mint::RowMatrix4], - ) -> Option<(GizmoResult, Vec>)> { + targets: &[Transform], + ) -> Option<(GizmoResult, Vec)> { // Mode was changed. Update all subgizmos accordingly. if self.config.modes != self.last_modes { self.last_modes = self.config.modes; @@ -105,10 +132,8 @@ impl Gizmo { return None; } - let targets = targets.iter().copied().map(DMat4::from).collect::>(); - // Update the gizmo based on the given targets. - self.config.update_for_targets(&targets); + self.config.update_for_targets(targets); for subgizmo in &mut self.subgizmos { // Update current configuration to each subgizmo. @@ -128,7 +153,7 @@ impl Gizmo { // If we started dragging from one of the subgizmos, mark it as active. if interaction.drag_started { self.active_subgizmo_id = Some(subgizmo.id()); - self.target_start_transforms = targets.clone(); + self.target_start_transforms = targets.to_vec(); } } } @@ -158,47 +183,36 @@ impl Gizmo { return None; }; - let mut updated_targets = Vec::>::new(); + let mut updated_targets = Vec::::new(); for (target_start_transform, target_transform) in self.target_start_transforms.iter().zip(targets) { - let mut new_target_transform = target_transform; + let mut new_target_transform = *target_transform; match result { GizmoResult::Rotation { delta, total: _ } => { // Rotate around the target group origin - - let group_translation = DMat4::from_translation(self.config.translation); - - new_target_transform = - group_translation.inverse().mul_mat4(&new_target_transform); - - new_target_transform = - DMat4::from_quat(delta.into()).mul_mat4(&new_target_transform); - - new_target_transform = group_translation.mul_mat4(&new_target_transform); + let rotation_delta = DQuat::from(delta); + let origin = self.config.translation; + + new_target_transform.translation = (origin + + rotation_delta * (DVec3::from(target_transform.translation) - origin)) + .into(); + new_target_transform.rotation = + (rotation_delta * DQuat::from(target_transform.rotation)).into(); } GizmoResult::Translation { delta, total: _ } => { - new_target_transform = - DMat4::from_translation(delta.into()).mul_mat4(&new_target_transform); + new_target_transform.translation = + (DVec3::from(delta) + DVec3::from(new_target_transform.translation)).into(); } GizmoResult::Scale { total } => { - let (start_scale, _, _) = - target_start_transform.to_scale_rotation_translation(); - - let (_, target_rotation, target_translation) = - target_transform.to_scale_rotation_translation(); - - new_target_transform = DMat4::from_scale_rotation_translation( - start_scale * DVec3::from(total), - target_rotation, - target_translation, - ); + new_target_transform.scale = + (DVec3::from(target_start_transform.scale) * DVec3::from(total)).into(); } } - updated_targets.push(new_target_transform.into()); + updated_targets.push(new_target_transform); } Some((result, updated_targets)) @@ -472,7 +486,7 @@ pub enum GizmoResult { pub struct GizmoDrawData { /// Vertices in viewport space. pub vertices: Vec<[f32; 2]>, - /// RGBA colors. + /// Linear RGBA colors. pub colors: Vec<[f32; 4]>, /// Indices to the vertex data. pub indices: Vec, diff --git a/crates/transform-gizmo/src/lib.rs b/crates/transform-gizmo/src/lib.rs index c1fbe0b..d08b49d 100644 --- a/crates/transform-gizmo/src/lib.rs +++ b/crates/transform-gizmo/src/lib.rs @@ -1,6 +1,4 @@ -//! Provides a feature-rich and configurable 3D transformation -//! gizmo that can be used to manipulate 4x4 transformation matrices (position, rotation, scale) -//! visually. +//! Provides a feature-rich and configurable gizmo that can be used for 3d transformations (translation, rotation, scale). //! //! Such gizmos are commonly used in applications such as game engines and 3d modeling software. //! diff --git a/crates/transform-gizmo/src/math.rs b/crates/transform-gizmo/src/math.rs index a258828..d9831ae 100644 --- a/crates/transform-gizmo/src/math.rs +++ b/crates/transform-gizmo/src/math.rs @@ -1,6 +1,27 @@ pub use emath::{Pos2, Rect, Vec2}; pub use glam::{DMat3, DMat4, DQuat, DVec2, DVec3, DVec4, Mat4, Quat, Vec3, Vec4Swizzles}; +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub struct Transform { + pub scale: mint::Vector3, + pub rotation: mint::Quaternion, + pub translation: mint::Vector3, +} + +impl Transform { + pub fn from_scale_rotation_translation( + scale: impl Into>, + rotation: impl Into>, + translation: impl Into>, + ) -> Self { + Self { + scale: scale.into(), + rotation: rotation.into(), + translation: translation.into(), + } + } +} + /// Creates a matrix that represents rotation between two 3d vectors /// /// Credit: diff --git a/examples/egui/src/main.rs b/examples/egui/src/main.rs index 12b55df..65867bf 100644 --- a/examples/egui/src/main.rs +++ b/examples/egui/src/main.rs @@ -1,5 +1,5 @@ use eframe::{egui, NativeOptions}; -use transform_gizmo_egui::math::DQuat; +use transform_gizmo_egui::math::{DQuat, Transform}; use transform_gizmo_egui::{ math::{DMat4, DVec3}, *, @@ -11,7 +11,9 @@ struct ExampleApp { gizmo_modes: EnumSet, gizmo_orientation: GizmoOrientation, - model_matrix: DMat4, + scale: DVec3, + rotation: DQuat, + translation: DVec3, } impl ExampleApp { @@ -20,7 +22,9 @@ impl ExampleApp { gizmo: Gizmo::default(), gizmo_modes: enum_set!(GizmoMode::Rotate | GizmoMode::Translate), gizmo_orientation: GizmoOrientation::Local, - model_matrix: DMat4::IDENTITY, + scale: DVec3::ONE, + rotation: DQuat::IDENTITY, + translation: DVec3::ZERO, } } @@ -50,8 +54,19 @@ impl ExampleApp { ..Default::default() }); - if let Some((result, targets)) = self.gizmo.interact(ui, &[self.model_matrix.into()]) { - self.model_matrix = targets.first().copied().unwrap().into(); + let mut transform = + Transform::from_scale_rotation_translation(self.scale, self.rotation, self.translation); + + if let Some((result, new_transforms)) = self.gizmo.interact(ui, &[transform]) { + for (new_transform, transform) in + new_transforms.iter().zip(std::iter::once(&mut transform)) + { + *transform = *new_transform; + } + + self.scale = transform.scale.into(); + self.rotation = transform.rotation.into(); + self.translation = transform.translation.into(); match result { GizmoResult::Rotation { delta: _, total } => {