Skip to content

Commit

Permalink
Fixed transformations causing weird things at certain angles
Browse files Browse the repository at this point in the history
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
  • Loading branch information
urholaukkarinen committed Apr 6, 2024
1 parent b53e630 commit ef14487
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 75 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
23 changes: 17 additions & 6 deletions crates/transform-gizmo-bevy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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::<Vec<_>>()
.as_slice(),
);
Expand All @@ -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);
Expand Down
34 changes: 19 additions & 15 deletions crates/transform-gizmo-egui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
//!
Expand All @@ -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 {
Expand All @@ -26,40 +25,45 @@
//! });
//! ```
//!
//! 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;

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<f64>],
) -> Option<(GizmoResult, Vec<mint::RowMatrix4<f64>>)>;
fn interact(&mut self, ui: &Ui, targets: &[Transform])
-> Option<(GizmoResult, Vec<Transform>)>;
}

impl GizmoExt for Gizmo {
fn interact(
&mut self,
ui: &Ui,
targets: &[mint::RowMatrix4<f64>],
) -> Option<(GizmoResult, Vec<mint::RowMatrix4<f64>>)> {
targets: &[Transform],
) -> Option<(GizmoResult, Vec<Transform>)> {
let config = self.config();

let egui_viewport = egui::Rect {
Expand Down
15 changes: 7 additions & 8 deletions crates/transform-gizmo/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
88 changes: 51 additions & 37 deletions crates/transform-gizmo/src/gizmo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -31,7 +31,7 @@ pub struct Gizmo {
subgizmos: Vec<SubGizmo>,
active_subgizmo_id: Option<u64>,

target_start_transforms: Vec<DMat4>,
target_start_transforms: Vec<Transform>,
}

impl Default for Gizmo {
Expand Down Expand Up @@ -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<f64>],
) -> Option<(GizmoResult, Vec<mint::RowMatrix4<f64>>)> {
targets: &[Transform],
) -> Option<(GizmoResult, Vec<Transform>)> {
// Mode was changed. Update all subgizmos accordingly.
if self.config.modes != self.last_modes {
self.last_modes = self.config.modes;
Expand Down Expand Up @@ -105,10 +132,8 @@ impl Gizmo {
return None;
}

let targets = targets.iter().copied().map(DMat4::from).collect::<Vec<_>>();

// 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.
Expand All @@ -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();
}
}
}
Expand Down Expand Up @@ -158,47 +183,36 @@ impl Gizmo {
return None;
};

let mut updated_targets = Vec::<mint::RowMatrix4<f64>>::new();
let mut updated_targets = Vec::<Transform>::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))
Expand Down Expand Up @@ -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<u32>,
Expand Down
4 changes: 1 addition & 3 deletions crates/transform-gizmo/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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.
//!
Expand Down
21 changes: 21 additions & 0 deletions crates/transform-gizmo/src/math.rs
Original file line number Diff line number Diff line change
@@ -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<f64>,
pub rotation: mint::Quaternion<f64>,
pub translation: mint::Vector3<f64>,
}

impl Transform {
pub fn from_scale_rotation_translation(
scale: impl Into<mint::Vector3<f64>>,
rotation: impl Into<mint::Quaternion<f64>>,
translation: impl Into<mint::Vector3<f64>>,
) -> Self {
Self {
scale: scale.into(),
rotation: rotation.into(),
translation: translation.into(),
}
}
}

/// Creates a matrix that represents rotation between two 3d vectors
///
/// Credit: <https://www.iquilezles.org/www/articles/noacos/noacos.htm>
Expand Down
Loading

0 comments on commit ef14487

Please sign in to comment.