Add View3DController for editor 3D view manipulation

This commit is contained in:
Michael Alexsander 2026-02-05 11:40:33 -03:00
parent bfbe95e510
commit 27c86165f7
No known key found for this signature in database
GPG key ID: A9C91EE110F4EABA
9 changed files with 1609 additions and 1378 deletions

View file

@ -43,6 +43,7 @@
#include "scene/2d/camera_2d.h"
#include "scene/debugger/scene_debugger_object.h"
#include "scene/gui/popup_menu.h"
#include "scene/gui/view_panner.h"
#include "scene/main/canvas_layer.h"
#include "scene/main/scene_tree.h"
#include "scene/resources/mesh.h"
@ -91,6 +92,8 @@ void RuntimeNodeSelect::_setup(const Dictionary &p_settings) {
max_selection = p_settings.get("debugger/max_node_selection", 1);
// Panner Setup
panner.instantiate();
panner->set_callbacks(callable_mp(this, &RuntimeNodeSelect::_pan_callback), callable_mp(this, &RuntimeNodeSelect::_zoom_callback));
@ -119,20 +122,79 @@ void RuntimeNodeSelect::_setup(const Dictionary &p_settings) {
RS::get_singleton()->canvas_item_set_parent(sbox_2d_ci, draw_canvas);
#ifndef _3D_DISABLED
cursor = Cursor();
camera_fov = p_settings.get("editors/3d/default_fov", 70);
camera_znear = p_settings.get("editors/3d/default_z_near", 0.05);
camera_zfar = p_settings.get("editors/3d/default_z_far", 4'000);
invert_x_axis = p_settings.get("editors/3d/navigation/invert_x_axis", false);
invert_y_axis = p_settings.get("editors/3d/navigation/invert_y_axis", false);
warped_mouse_panning_3d = p_settings.get("editors/3d/navigation/warped_mouse_panning", true);
// View3DController Setup
freelook_base_speed = p_settings.get("editors/3d/freelook/freelook_base_speed", 5);
freelook_sensitivity = Math::deg_to_rad((real_t)p_settings.get("editors/3d/freelook/freelook_sensitivity", 0.25));
orbit_sensitivity = Math::deg_to_rad((real_t)p_settings.get("editors/3d/navigation_feel/orbit_sensitivity", 0.004));
translation_sensitivity = p_settings.get("editors/3d/navigation_feel/translation_sensitivity", 1);
view_3d_controller.instantiate();
view_3d_controller->set_freelook_scheme((View3DController::FreelookScheme)p_settings.get("editors/3d/freelook/freelook_navigation_scheme", View3DController::FREELOOK_DEFAULT).operator int());
view_3d_controller->set_freelook_base_speed(p_settings.get("editors/3d/freelook/freelook_base_speed", 5));
view_3d_controller->set_freelook_sensitivity(p_settings.get("editors/3d/freelook/freelook_sensitivity", 0.25));
view_3d_controller->set_freelook_inertia(p_settings.get("editors/3d/freelook/freelook_inertia", 0));
view_3d_controller->set_freelook_speed_zoom_link(p_settings.get("editors/3d/freelook/freelook_speed_zoom_link", false));
view_3d_controller->set_freelook_invert_y_axis(p_settings.get("editors/3d/freelook/freelook_invert_y_axis", false));
view_3d_controller->set_translation_sensitivity(p_settings.get("editors/3d/navigation_feel/translation_sensitivity", 1));
view_3d_controller->set_translation_inertia(p_settings.get("editors/3d/navigation_feel/translation_inertia", 0));
view_3d_controller->set_pan_mouse_button(p_settings.get("editors/3d/navigation/pan_mouse_button", View3DController::NAV_MOUSE_BUTTON_MIDDLE));
view_3d_controller->set_orbit_mouse_button(p_settings.get("editors/3d/navigation/orbit_mouse_button", View3DController::NAV_MOUSE_BUTTON_MIDDLE));
view_3d_controller->set_orbit_sensitivity(p_settings.get("editors/3d/navigation_feel/orbit_sensitivity", 0.004));
view_3d_controller->set_orbit_inertia(p_settings.get("editors/3d/navigation_feel/orbit_inertia", 0));
view_3d_controller->set_zoom_style(p_settings.get("editors/3d/navigation/zoom_style", View3DController::ZOOM_VERTICAL));
view_3d_controller->set_zoom_inertia(p_settings.get("editors/3d/navigation_feel/zoom_inertia", 0));
view_3d_controller->set_zoom_mouse_button(p_settings.get("editors/3d/navigation/zoom_mouse_button", View3DController::NAV_MOUSE_BUTTON_MIDDLE));
view_3d_controller->set_angle_snap_threshold(p_settings.get("editors/3d/navigation_feel/angle_snap_threshold", 10));
view_3d_controller->set_emulate_3_button_mouse(p_settings.get("editors/3d/navigation/emulate_3_button_mouse", false));
view_3d_controller->set_emulate_numpad(p_settings.get("editors/3d/navigation/emulate_numpad", true));
view_3d_controller->set_z_near(camera_znear);
view_3d_controller->set_z_far(camera_zfar);
view_3d_controller->set_invert_x_axis(p_settings.get("editors/3d/navigation/invert_x_axis", false));
view_3d_controller->set_invert_x_axis(p_settings.get("editors/3d/navigation/invert_y_axis", false));
view_3d_controller->set_warped_mouse_panning(p_settings.get("editors/3d/navigation/warped_mouse_panning", true));
view_3d_controller->connect("fov_scaled", callable_mp(this, &RuntimeNodeSelect::_fov_scaled));
view_3d_controller->connect("cursor_interpolated", callable_mp(this, &RuntimeNodeSelect::_cursor_interpolated));
#define SET_SHORTCUT(p_name, p_setting) \
{ \
Ref<Shortcut> shortcut = DebuggerMarshalls::deserialize_key_shortcut(p_settings.get(p_setting, Array()).operator Array()); \
if (shortcut.is_valid()) { \
view_3d_controller->set_shortcut(p_name, shortcut); \
} \
}
SET_SHORTCUT(View3DController::SHORTCUT_FOV_DECREASE, "spatial_editor/decrease_fov");
SET_SHORTCUT(View3DController::SHORTCUT_FOV_INCREASE, "spatial_editor/increase_fov");
SET_SHORTCUT(View3DController::SHORTCUT_FOV_RESET, "spatial_editor/reset_fov");
SET_SHORTCUT(View3DController::SHORTCUT_PAN_MOD_1, "spatial_editor/viewport_pan_modifier_1");
SET_SHORTCUT(View3DController::SHORTCUT_PAN_MOD_2, "spatial_editor/viewport_pan_modifier_2");
SET_SHORTCUT(View3DController::SHORTCUT_ORBIT_MOD_1, "spatial_editor/viewport_orbit_modifier_1");
SET_SHORTCUT(View3DController::SHORTCUT_ORBIT_MOD_2, "spatial_editor/viewport_orbit_modifier_2");
SET_SHORTCUT(View3DController::SHORTCUT_ORBIT_SNAP_MOD_1, "spatial_editor/viewport_orbit_snap_modifier_1");
SET_SHORTCUT(View3DController::SHORTCUT_ORBIT_SNAP_MOD_2, "spatial_editor/viewport_orbit_snap_modifier_2");
SET_SHORTCUT(View3DController::SHORTCUT_ZOOM_MOD_1, "spatial_editor/viewport_zoom_modifier_1");
SET_SHORTCUT(View3DController::SHORTCUT_ZOOM_MOD_2, "spatial_editor/viewport_zoom_modifier_2");
SET_SHORTCUT(View3DController::SHORTCUT_FREELOOK_FORWARD, "spatial_editor/freelook_forward");
SET_SHORTCUT(View3DController::SHORTCUT_FREELOOK_BACKWARDS, "spatial_editor/freelook_backwards");
SET_SHORTCUT(View3DController::SHORTCUT_FREELOOK_LEFT, "spatial_editor/freelook_left");
SET_SHORTCUT(View3DController::SHORTCUT_FREELOOK_RIGHT, "spatial_editor/freelook_right");
SET_SHORTCUT(View3DController::SHORTCUT_FREELOOK_UP, "spatial_editor/freelook_up");
SET_SHORTCUT(View3DController::SHORTCUT_FREELOOK_DOWN, "spatial_editor/freelook_down");
SET_SHORTCUT(View3DController::SHORTCUT_FREELOOK_SPEED_MOD, "spatial_editor/freelook_speed_modifier");
SET_SHORTCUT(View3DController::SHORTCUT_FREELOOK_SLOW_MOD, "spatial_editor/freelook_slow_modifier");
#undef SET_SHORTCUT
/// 3D Selection Box Generation
// Copied from the Node3DEditor implementation.
@ -212,8 +274,8 @@ void RuntimeNodeSelect::_set_camera_override_enabled(bool p_enabled) {
Window *root = SceneTree::get_singleton()->get_root();
ERR_FAIL_COND(!root->is_camera_3d_override_enabled());
Camera3D *override_camera = root->get_override_camera_3d();
override_camera->set_transform(_get_cursor_transform());
override_camera->set_perspective(camera_fov * cursor.fov_scale, camera_znear, camera_zfar);
override_camera->set_transform(view_3d_controller->to_camera_transform());
override_camera->set_perspective(camera_fov * view_3d_controller->cursor.fov_scale, camera_znear, camera_zfar);
#endif // _3D_DISABLED
}
}
@ -311,64 +373,24 @@ void RuntimeNodeSelect::_update_input_state() {
void RuntimeNodeSelect::_process_frame() {
#ifndef _3D_DISABLED
if (camera_freelook) {
Transform3D transform = _get_cursor_transform();
Vector3 forward = transform.basis.xform(Vector3(0, 0, -1));
const Vector3 right = transform.basis.xform(Vector3(1, 0, 0));
Vector3 up = transform.basis.xform(Vector3(0, 1, 0));
Vector3 direction;
// Calculate the process time manually, as the time scale can be frozen.
const double process_time = (1.0 / Engine::get_singleton()->get_frames_per_second()) * Engine::get_singleton()->get_unfrozen_time_scale();
if (view_3d_controller->is_freelook_enabled()) {
Input *input = Input::get_singleton();
bool was_input_disabled = input->is_input_disabled();
if (was_input_disabled) {
input->set_disable_input(false);
}
if (input->is_physical_key_pressed(Key::A)) {
direction -= right;
}
if (input->is_physical_key_pressed(Key::D)) {
direction += right;
}
if (input->is_physical_key_pressed(Key::W)) {
direction += forward;
}
if (input->is_physical_key_pressed(Key::S)) {
direction -= forward;
}
if (input->is_physical_key_pressed(Key::E)) {
direction += up;
}
if (input->is_physical_key_pressed(Key::Q)) {
direction -= up;
}
real_t speed = freelook_base_speed;
if (input->is_physical_key_pressed(Key::SHIFT)) {
speed *= 3.0;
}
if (input->is_physical_key_pressed(Key::ALT)) {
speed *= 0.333333;
}
view_3d_controller->update_freelook(process_time);
if (was_input_disabled) {
input->set_disable_input(true);
}
if (direction != Vector3()) {
Window *root = SceneTree::get_singleton()->get_root();
ERR_FAIL_COND(!root->is_camera_3d_override_enabled());
// Calculate the process time manually, as the time scale is frozen.
const double process_time = (1.0 / Engine::get_singleton()->get_frames_per_second()) * Engine::get_singleton()->get_unfrozen_time_scale();
const Vector3 motion = direction * speed * process_time;
cursor.pos += motion;
cursor.eye_pos += motion;
root->get_override_camera_3d()->set_transform(_get_cursor_transform());
}
}
view_3d_controller->update_camera(process_time);
#endif // _3D_DISABLED
if (selection_update_queued || !SceneTree::get_singleton()->is_suspended()) {
@ -1390,231 +1412,58 @@ Vector3 RuntimeNodeSelect::_get_screen_to_space(const Vector3 &p_vector3) {
return camera_transform.xform(Vector3(((p_vector3.x / size.width) * 2.0 - 1.0) * screen_he.x, ((1.0 - (p_vector3.y / size.height)) * 2.0 - 1.0) * screen_he.y, -(znear + p_vector3.z)));
}
void RuntimeNodeSelect::_fov_scaled() {
SceneTree::get_singleton()->get_root()->get_override_camera_3d()->set_perspective(camera_fov * view_3d_controller->cursor.fov_scale, camera_znear, camera_zfar);
}
void RuntimeNodeSelect::_cursor_interpolated() {
Window *root = SceneTree::get_singleton()->get_root();
ERR_FAIL_COND(!root->is_camera_3d_override_enabled());
root->get_override_camera_3d()->set_transform(view_3d_controller->interp_to_camera_transform());
}
bool RuntimeNodeSelect::_handle_3d_input(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseButton> b = p_event;
if (b.is_valid()) {
const real_t zoom_factor = 1.08 * b->get_factor();
switch (b->get_button_index()) {
case MouseButton::WHEEL_UP: {
if (!camera_freelook) {
_cursor_scale_distance(1.0 / zoom_factor);
} else {
_scale_freelook_speed(zoom_factor);
}
return true;
} break;
case MouseButton::WHEEL_DOWN: {
if (!camera_freelook) {
_cursor_scale_distance(zoom_factor);
} else {
_scale_freelook_speed(1.0 / zoom_factor);
}
return true;
} break;
case MouseButton::RIGHT: {
_set_camera_freelook_enabled(b->is_pressed());
return true;
} break;
default: {
}
}
if (b.is_valid() && b->get_button_index() == MouseButton::RIGHT) {
view_3d_controller->set_freelook_enabled(b->is_pressed());
return true;
}
Ref<InputEventMouseMotion> m = p_event;
if (m.is_valid()) {
if (camera_freelook) {
_cursor_look(m);
} else if (m->get_button_mask().has_flag(MouseButtonMask::MIDDLE)) {
if (m->is_shift_pressed()) {
_cursor_pan(m);
} else {
_cursor_orbit(m);
}
}
Window *root = SceneTree::get_singleton()->get_root();
ERR_FAIL_COND_V(!root->is_camera_3d_override_enabled(), true);
Input *input = Input::get_singleton();
bool was_input_disabled = input->is_input_disabled();
if (was_input_disabled) {
input->set_disable_input(false);
}
// Reduce all sides of the area by 1, so warping works when windows are maximized/fullscreen.
bool view_3d_input_received = view_3d_controller->gui_input(p_event, Rect2(Vector2(1, 1), root->get_size() - Vector2(2, 2)));
if (was_input_disabled) {
input->set_disable_input(true);
}
if (view_3d_input_received) {
root->get_override_camera_3d()->set_transform(view_3d_controller->interp_to_camera_transform());
return true;
}
Ref<InputEventKey> k = p_event;
if (k.is_valid()) {
if (k->get_physical_keycode() == Key::ESCAPE) {
_set_camera_freelook_enabled(false);
return true;
} else if (k->is_ctrl_pressed()) {
switch (k->get_physical_keycode()) {
case Key::EQUAL: {
ERR_FAIL_COND_V(!SceneTree::get_singleton()->get_root()->is_camera_3d_override_enabled(), false);
cursor.fov_scale = CLAMP(cursor.fov_scale - 0.05, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE);
SceneTree::get_singleton()->get_root()->get_override_camera_3d()->set_perspective(camera_fov * cursor.fov_scale, camera_znear, camera_zfar);
return true;
} break;
case Key::MINUS: {
ERR_FAIL_COND_V(!SceneTree::get_singleton()->get_root()->is_camera_3d_override_enabled(), false);
cursor.fov_scale = CLAMP(cursor.fov_scale + 0.05, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE);
SceneTree::get_singleton()->get_root()->get_override_camera_3d()->set_perspective(camera_fov * cursor.fov_scale, camera_znear, camera_zfar);
return true;
} break;
case Key::KEY_0: {
ERR_FAIL_COND_V(!SceneTree::get_singleton()->get_root()->is_camera_3d_override_enabled(), false);
cursor.fov_scale = 1;
SceneTree::get_singleton()->get_root()->get_override_camera_3d()->set_perspective(camera_fov, camera_znear, camera_zfar);
return true;
} break;
default: {
}
}
}
if (k.is_valid() && k->get_physical_keycode() == Key::ESCAPE) {
view_3d_controller->set_freelook_enabled(false);
return true;
}
// TODO: Handle magnify and pan input gestures.
return false;
}
void RuntimeNodeSelect::_set_camera_freelook_enabled(bool p_enabled) {
camera_freelook = p_enabled;
if (p_enabled) {
// Make sure eye_pos is synced, because freelook referential is eye pos rather than orbit pos
Vector3 forward = _get_cursor_transform().basis.xform(Vector3(0, 0, -1));
cursor.eye_pos = cursor.pos - cursor.distance * forward;
previous_mouse_position = SceneTree::get_singleton()->get_root()->get_mouse_position();
// Hide mouse like in an FPS (warping doesn't work).
Input::get_singleton()->set_mouse_mode_override(Input::MouseMode::MOUSE_MODE_CAPTURED);
} else {
// Restore mouse.
Input::get_singleton()->set_mouse_mode_override(Input::MouseMode::MOUSE_MODE_VISIBLE);
// Restore the previous mouse position when leaving freelook mode.
// This is done because leaving `Input.MOUSE_MODE_CAPTURED` will center the cursor
// due to OS limitations.
Input::get_singleton()->warp_mouse(previous_mouse_position);
}
}
void RuntimeNodeSelect::_cursor_scale_distance(real_t p_scale) {
ERR_FAIL_COND(!SceneTree::get_singleton()->get_root()->is_camera_3d_override_enabled());
real_t min_distance = MAX(camera_znear * 4, VIEW_3D_MIN_ZOOM);
real_t max_distance = MIN(camera_zfar / 4, VIEW_3D_MAX_ZOOM);
cursor.distance = CLAMP(cursor.distance * p_scale, min_distance, max_distance);
SceneTree::get_singleton()->get_root()->get_override_camera_3d()->set_transform(_get_cursor_transform());
}
void RuntimeNodeSelect::_scale_freelook_speed(real_t p_scale) {
real_t min_speed = MAX(camera_znear * 4, VIEW_3D_MIN_ZOOM);
real_t max_speed = MIN(camera_zfar / 4, VIEW_3D_MAX_ZOOM);
if (unlikely(min_speed > max_speed)) {
freelook_base_speed = (min_speed + max_speed) / 2;
} else {
freelook_base_speed = CLAMP(freelook_base_speed * p_scale, min_speed, max_speed);
}
}
void RuntimeNodeSelect::_cursor_look(Ref<InputEventWithModifiers> p_event) {
Window *root = SceneTree::get_singleton()->get_root();
ERR_FAIL_COND(!root->is_camera_3d_override_enabled());
const Vector2 relative = _get_warped_mouse_motion(p_event, Rect2(Vector2(), root->get_size()));
const Transform3D prev_camera_transform = _get_cursor_transform();
if (invert_y_axis) {
cursor.x_rot -= relative.y * freelook_sensitivity;
} else {
cursor.x_rot += relative.y * freelook_sensitivity;
}
// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
cursor.y_rot += relative.x * freelook_sensitivity;
// Look is like the opposite of Orbit: the focus point rotates around the camera.
Transform3D camera_transform = _get_cursor_transform();
Vector3 pos = camera_transform.xform(Vector3(0, 0, 0));
Vector3 prev_pos = prev_camera_transform.xform(Vector3(0, 0, 0));
Vector3 diff = prev_pos - pos;
cursor.pos += diff;
root->get_override_camera_3d()->set_transform(_get_cursor_transform());
}
void RuntimeNodeSelect::_cursor_pan(Ref<InputEventWithModifiers> p_event) {
Window *root = SceneTree::get_singleton()->get_root();
ERR_FAIL_COND(!root->is_camera_3d_override_enabled());
// Reduce all sides of the area by 1, so warping works when windows are maximized/fullscreen.
const Vector2 relative = _get_warped_mouse_motion(p_event, Rect2(Vector2(1, 1), root->get_size() - Vector2(2, 2)));
const real_t pan_speed = translation_sensitivity / 150.0;
Transform3D camera_transform;
camera_transform.translate_local(cursor.pos);
camera_transform.basis.rotate(Vector3(1, 0, 0), -cursor.x_rot);
camera_transform.basis.rotate(Vector3(0, 1, 0), -cursor.y_rot);
Vector3 translation(1 * -relative.x * pan_speed, relative.y * pan_speed, 0);
translation *= cursor.distance / 4;
camera_transform.translate_local(translation);
cursor.pos = camera_transform.origin;
root->get_override_camera_3d()->set_transform(_get_cursor_transform());
}
void RuntimeNodeSelect::_cursor_orbit(Ref<InputEventWithModifiers> p_event) {
Window *root = SceneTree::get_singleton()->get_root();
ERR_FAIL_COND(!root->is_camera_3d_override_enabled());
// Reduce all sides of the area by 1, so warping works when windows are maximized/fullscreen.
const Vector2 relative = _get_warped_mouse_motion(p_event, Rect2(Vector2(1, 1), root->get_size() - Vector2(2, 2)));
if (invert_y_axis) {
cursor.x_rot -= relative.y * orbit_sensitivity;
} else {
cursor.x_rot += relative.y * orbit_sensitivity;
}
// Clamp the Y rotation to roughly -90..90 degrees so the user can't look upside-down and end up disoriented.
cursor.x_rot = CLAMP(cursor.x_rot, -1.57, 1.57);
if (invert_x_axis) {
cursor.y_rot -= relative.x * orbit_sensitivity;
} else {
cursor.y_rot += relative.x * orbit_sensitivity;
}
root->get_override_camera_3d()->set_transform(_get_cursor_transform());
}
Point2 RuntimeNodeSelect::_get_warped_mouse_motion(const Ref<InputEventMouseMotion> &p_event, Rect2 p_area) const {
ERR_FAIL_COND_V(p_event.is_null(), Point2());
if (warped_mouse_panning_3d) {
return Input::get_singleton()->warp_mouse_motion(p_event, p_area);
}
return p_event->get_relative();
}
Transform3D RuntimeNodeSelect::_get_cursor_transform() {
Transform3D camera_transform;
camera_transform.translate_local(cursor.pos);
camera_transform.basis.rotate(Vector3(1, 0, 0), -cursor.x_rot);
camera_transform.basis.rotate(Vector3(0, 1, 0), -cursor.y_rot);
camera_transform.translate_local(0, 0, cursor.distance);
return camera_transform;
}
void RuntimeNodeSelect::_reset_camera_3d() {
camera_first_override = true;
cursor = Cursor();
View3DController::Cursor cursor;
Window *root = SceneTree::get_singleton()->get_root();
Camera3D *game_camera = root->is_camera_3d_override_enabled() ? root->get_overridden_camera_3d() : root->get_camera_3d();
if (game_camera) {
@ -1624,15 +1473,18 @@ void RuntimeNodeSelect::_reset_camera_3d() {
cursor.x_rot = -game_camera->get_global_rotation().x;
cursor.y_rot = -game_camera->get_global_rotation().y;
cursor.unsnapped_x_rot = cursor.x_rot;
cursor.unsnapped_y_rot = cursor.y_rot;
cursor.fov_scale = CLAMP(game_camera->get_fov() / camera_fov, CAMERA_MIN_FOV_SCALE, CAMERA_MAX_FOV_SCALE);
} else {
cursor.fov_scale = 1.0;
cursor.fov_scale = CLAMP(game_camera->get_fov() / camera_fov, View3DControllerConsts::CAMERA_MIN_FOV_SCALE, View3DControllerConsts::CAMERA_MAX_FOV_SCALE);
}
view_3d_controller->cursor = cursor;
if (root->is_camera_3d_override_enabled()) {
view_3d_controller->update_camera();
Camera3D *override_camera = root->get_override_camera_3d();
override_camera->set_transform(_get_cursor_transform());
override_camera->set_transform(view_3d_controller->to_camera_transform());
override_camera->set_perspective(camera_fov * cursor.fov_scale, camera_znear, camera_zfar);
}
}