Improve rotation in the 3D transform gizmo

* Get rid of deadzones.
* Make it easier to select rotation handles at very oblique angles.
* Handle rotation for axes that are perpendicular to the camera.
This commit is contained in:
jfons 2022-01-17 15:48:21 +01:00
parent 39562294ff
commit 8345aabaf4
2 changed files with 61 additions and 28 deletions

View file

@ -1041,24 +1041,40 @@ bool Node3DEditorViewport::_transform_gizmo_select(const Vector2 &p_screenpos, b
if (spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE) {
int col_axis = -1;
float col_d = 1e20;
for (int i = 0; i < 3; i++) {
Plane plane(gt.basis.get_axis(i).normalized(), gt.origin);
Vector3 r;
if (!plane.intersects_ray(ray_pos, ray, &r)) {
continue;
Vector3 hit_position;
Vector3 hit_normal;
real_t ray_length = gt.origin.distance_to(ray_pos) + (GIZMO_CIRCLE_SIZE * gizmo_scale) * 4.0f;
if (Geometry3D::segment_intersects_sphere(ray_pos, ray_pos + ray * ray_length, gt.origin, gizmo_scale * (GIZMO_CIRCLE_SIZE), &hit_position, &hit_normal)) {
if (hit_normal.dot(_get_camera_normal()) < 0.05) {
hit_position = gt.xform_inv(hit_position).abs();
int min_axis = hit_position.min_axis_index();
if (hit_position[min_axis] < gizmo_scale * GIZMO_RING_HALF_WIDTH) {
col_axis = min_axis;
}
}
}
const real_t dist = r.distance_to(gt.origin);
const Vector3 r_dir = (r - gt.origin).normalized();
if (col_axis == -1) {
float col_d = 1e20;
if (_get_camera_normal().dot(r_dir) <= 0.005) {
if (dist > gizmo_scale * (GIZMO_CIRCLE_SIZE - GIZMO_RING_HALF_WIDTH) && dist < gizmo_scale * (GIZMO_CIRCLE_SIZE + GIZMO_RING_HALF_WIDTH)) {
const real_t d = ray_pos.distance_to(r);
if (d < col_d) {
col_d = d;
col_axis = i;
for (int i = 0; i < 3; i++) {
Plane plane(gt.basis.get_axis(i).normalized(), gt.origin);
Vector3 r;
if (!plane.intersects_ray(ray_pos, ray, &r)) {
continue;
}
const real_t dist = r.distance_to(gt.origin);
const Vector3 r_dir = (r - gt.origin).normalized();
if (_get_camera_normal().dot(r_dir) <= 0.005) {
if (dist > gizmo_scale * (GIZMO_CIRCLE_SIZE - GIZMO_RING_HALF_WIDTH) && dist < gizmo_scale * (GIZMO_CIRCLE_SIZE + GIZMO_RING_HALF_WIDTH)) {
const real_t d = ray_pos.distance_to(r);
if (d < col_d) {
col_d = d;
col_axis = i;
}
}
}
}
@ -2730,7 +2746,7 @@ void Node3DEditorViewport::_draw() {
font->draw_string(ci, msgpos, message, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(1, 1, 1, 1));
}
if (_edit.mode == TRANSFORM_ROTATE) {
if (_edit.mode == TRANSFORM_ROTATE && _edit.show_rotation_line) {
Point2 center = _point_to_screen(_edit.center);
Color handle_color;
@ -4278,24 +4294,23 @@ void Node3DEditorViewport::update_transform(Point2 p_mousepos, bool p_shift) {
} break;
case TRANSFORM_ROTATE: {
Plane plane;
Vector3 axis;
Plane plane = Plane(_get_camera_normal(), _edit.center);
Vector3 local_axis;
Vector3 global_axis;
switch (_edit.plane) {
case TRANSFORM_VIEW:
plane = Plane(_get_camera_normal(), _edit.center);
// local_axis unused
global_axis = _get_camera_normal();
break;
case TRANSFORM_X_AXIS:
plane = Plane(spatial_editor->get_gizmo_transform().basis.get_axis(0).normalized(), _edit.center);
axis = Vector3(1, 0, 0);
local_axis = Vector3(1, 0, 0);
break;
case TRANSFORM_Y_AXIS:
plane = Plane(spatial_editor->get_gizmo_transform().basis.get_axis(1).normalized(), _edit.center);
axis = Vector3(0, 1, 0);
local_axis = Vector3(0, 1, 0);
break;
case TRANSFORM_Z_AXIS:
plane = Plane(spatial_editor->get_gizmo_transform().basis.get_axis(2).normalized(), _edit.center);
axis = Vector3(0, 0, 1);
local_axis = Vector3(0, 0, 1);
break;
case TRANSFORM_YZ:
case TRANSFORM_XZ:
@ -4303,6 +4318,10 @@ void Node3DEditorViewport::update_transform(Point2 p_mousepos, bool p_shift) {
break;
}
if (_edit.plane != TRANSFORM_VIEW) {
global_axis = spatial_editor->get_gizmo_transform().basis.xform(local_axis).normalized();
}
Vector3 intersection;
if (!plane.intersects_ray(ray_pos, ray, &intersection)) {
break;
@ -4313,10 +4332,22 @@ void Node3DEditorViewport::update_transform(Point2 p_mousepos, bool p_shift) {
break;
}
Vector3 y_axis = (click - _edit.center).normalized();
Vector3 x_axis = plane.normal.cross(y_axis).normalized();
static const float orthogonal_threshold = Math::cos(Math::deg2rad(87.0f));
bool axis_is_orthogonal = ABS(plane.normal.dot(global_axis)) < orthogonal_threshold;
double angle = Math::atan2(x_axis.dot(intersection - _edit.center), y_axis.dot(intersection - _edit.center));
double angle = 0.0f;
if (axis_is_orthogonal) {
_edit.show_rotation_line = false;
Vector3 projection_axis = plane.normal.cross(global_axis);
Vector3 delta = intersection - click;
float projection = delta.dot(projection_axis);
angle = (projection * (Math_PI / 2.0f)) / (gizmo_scale * GIZMO_CIRCLE_SIZE);
} else {
_edit.show_rotation_line = true;
Vector3 click_axis = (click - _edit.center).normalized();
Vector3 current_axis = (intersection - _edit.center).normalized();
angle = click_axis.signed_angle_to(current_axis, global_axis);
}
if (_edit.snap || spatial_editor->is_snap_enabled()) {
snap = spatial_editor->get_rotate_snap();
@ -4344,7 +4375,7 @@ void Node3DEditorViewport::update_transform(Point2 p_mousepos, bool p_shift) {
continue;
}
Vector3 compute_axis = local_coords ? axis : plane.normal;
Vector3 compute_axis = local_coords ? local_axis : global_axis;
if (se->gizmo.is_valid()) {
for (KeyValue<int, Transform3D> &GE : se->subgizmos) {
Transform3D xform = GE.value;
@ -4377,6 +4408,7 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, Edito
_edit.mode = TRANSFORM_NONE;
_edit.plane = TRANSFORM_VIEW;
_edit.snap = true;
_edit.show_rotation_line = true;
_edit.instant = false;
_edit.gizmo_handle = -1;
_edit.gizmo_handle_secondary = false;

View file

@ -311,6 +311,7 @@ private:
Point2 mouse_pos;
Point2 original_mouse_pos;
bool snap = false;
bool show_rotation_line = false;
Ref<EditorNode3DGizmo> gizmo;
int gizmo_handle = 0;
bool gizmo_handle_secondary = false;