Files
godot/editor/scene/3d/node_3d_editor_plugin.cpp
T
2026-06-20 19:40:06 -07:00

4128 lines
167 KiB
C++

/**************************************************************************/
/* node_3d_editor_plugin.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "node_3d_editor_plugin.h"
#include "core/config/project_settings.h"
#include "core/input/input.h"
#include "core/math/geometry_3d.h"
#include "core/math/math_funcs.h"
#include "core/math/projection.h"
#include "core/object/callable_mp.h"
#include "core/object/class_db.h"
#include "core/os/keyboard.h"
#include "core/os/os.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/docks/scene_tree_dock.h"
#include "editor/editor_main_screen.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/editor_undo_redo_manager.h"
#include "editor/gui/editor_spin_slider.h"
#include "editor/plugins/editor_plugin_list.h"
#include "editor/run/editor_run_bar.h"
#include "editor/scene/3d/gizmos/audio_listener_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/audio_stream_player_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/camera_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/chain_ik_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/cpu_particles_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/decal_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/fog_volume_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/geometry_instance_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/gpu_particles_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/gpu_particles_collision_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/label_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/light_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/lightmap_gi_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/lightmap_probe_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/marker_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/mesh_instance_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/occluder_instance_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/particles_3d_emission_shape_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/collision_object_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/collision_polygon_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/collision_shape_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/joint_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/physics_bone_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/ray_cast_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/shape_cast_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/soft_body_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/spring_arm_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/physics/vehicle_body_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/reflection_probe_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/spring_bone_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/sprite_base_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/two_bone_ik_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/visible_on_screen_notifier_3d_gizmo_plugin.h"
#include "editor/scene/3d/gizmos/voxel_gi_gizmo_plugin.h"
#include "editor/scene/3d/node_3d_editor_constants.h"
#include "editor/scene/3d/node_3d_editor_gizmos.h"
#include "editor/scene/3d/node_3d_editor_viewport.h"
#include "editor/settings/editor_settings.h"
#include "editor/translations/editor_translation_preview_menu.h"
#include "scene/3d/camera_3d.h"
#include "scene/3d/light_3d.h"
#include "scene/3d/physics/collision_shape_3d.h"
#include "scene/3d/physics/physics_body_3d.h"
#include "scene/3d/world_environment.h"
#include "scene/gui/button.h"
#include "scene/gui/center_container.h"
#include "scene/gui/color_picker.h"
#include "scene/gui/flow_container.h"
#include "scene/gui/menu_button.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/separator.h"
#include "scene/gui/spin_box.h"
#include "scene/gui/split_container.h"
#include "scene/main/scene_tree.h"
#include "scene/resources/3d/sky_material.h"
#include "scene/resources/sky.h"
#include "scene/resources/surface_tool.h"
#include "servers/rendering/rendering_server.h"
using namespace Node3DEditorConstants;
///////////////////////////////////////////////////////////////////
Node3DEditor *Node3DEditor::singleton = nullptr;
void Node3DEditor::select_gizmo_highlight_axis(int p_axis) {
for (int i = 0; i < 3; i++) {
move_gizmo[i]->surface_set_material(0, i == p_axis ? gizmo_color_hl[i] : gizmo_color[i]);
move_plane_gizmo[i]->surface_set_material(0, (i + 6) == p_axis ? plane_gizmo_color_hl[i] : plane_gizmo_color[i]);
scale_gizmo[i]->surface_set_material(0, (i + 9) == p_axis ? gizmo_color_hl[i] : gizmo_color[i]);
scale_plane_gizmo[i]->surface_set_material(0, (i + 12) == p_axis ? plane_gizmo_color_hl[i] : plane_gizmo_color[i]);
}
for (int i = 0; i < 4; i++) {
bool highlight;
if (i == 3) {
highlight = (p_axis == GIZMO_HIGHLIGHT_AXIS_VIEW_ROTATION);
} else {
highlight = (i + 3) == p_axis;
}
rotate_gizmo[i]->surface_set_material(0, highlight ? rotate_gizmo_color_hl[i] : rotate_gizmo_color[i]);
}
bool highlight_trackball = (p_axis == GIZMO_HIGHLIGHT_AXIS_TRACKBALL);
trackball_sphere_gizmo->surface_set_material(0, highlight_trackball ? trackball_sphere_material_hl : trackball_sphere_material);
}
void Node3DEditor::update_transform_gizmo() {
int count = 0;
bool local_gizmo_coords = are_local_coords_enabled();
Vector3 gizmo_center;
Basis gizmo_basis;
Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(selected) : nullptr;
if (se && se->gizmo.is_valid()) {
for (const KeyValue<int, Transform3D> &E : se->subgizmos) {
Transform3D xf = se->sp->get_global_transform() * se->gizmo->get_subgizmo_transform(E.key);
if (!xf.is_finite()) {
continue;
}
gizmo_center += xf.origin;
if ((unsigned int)count == se->subgizmos.size() - 1 && local_gizmo_coords) {
gizmo_basis = xf.basis;
}
count++;
}
} else {
const List<Node *> &selection = editor_selection->get_top_selected_node_list();
for (Node *E : selection) {
Node3D *sp = Object::cast_to<Node3D>(E);
if (!sp) {
continue;
}
if (sp->has_meta("_edit_lock_")) {
continue;
}
Node3DEditorSelectedItem *sel_item = editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(sp);
if (!sel_item) {
continue;
}
Transform3D xf = sel_item->sp->get_global_transform();
if (!xf.is_finite()) {
continue;
}
gizmo_center += xf.origin;
if (count == selection.size() - 1 && local_gizmo_coords) {
gizmo_basis = xf.basis;
}
count++;
}
}
gizmo.visible = count > 0;
gizmo.transform.origin = (count > 0) ? gizmo_center / count : Vector3();
gizmo.transform.basis = gizmo_basis;
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
viewports[i]->update_transform_gizmo_view();
}
}
void _update_all_gizmos(Node *p_node) {
for (int i = p_node->get_child_count() - 1; 0 <= i; --i) {
Node3D *spatial_node = Object::cast_to<Node3D>(p_node->get_child(i));
if (spatial_node) {
spatial_node->update_gizmos();
}
_update_all_gizmos(p_node->get_child(i));
}
}
void Node3DEditor::update_all_gizmos(Node *p_node) {
if (!p_node && is_inside_tree()) {
p_node = get_tree()->get_edited_scene_root();
}
if (!p_node) {
// No edited scene, so nothing to update.
return;
}
_update_all_gizmos(p_node);
}
Object *Node3DEditor::_get_editor_data(Object *p_what) {
Node3D *sp = Object::cast_to<Node3D>(p_what);
if (!sp) {
return nullptr;
}
Node3DEditorSelectedItem *si = memnew(Node3DEditorSelectedItem);
si->sp = sp;
si->sbox_instance = RenderingServer::get_singleton()->instance_create2(
selection_box->get_rid(),
sp->get_world_3d()->get_scenario());
si->sbox_instance_offset = RenderingServer::get_singleton()->instance_create2(
selection_box->get_rid(),
sp->get_world_3d()->get_scenario());
RS::get_singleton()->instance_geometry_set_cast_shadows_setting(
si->sbox_instance,
RSE::SHADOW_CASTING_SETTING_OFF);
RS::get_singleton()->instance_geometry_set_cast_shadows_setting(
si->sbox_instance_offset,
RSE::SHADOW_CASTING_SETTING_OFF);
// Use the Edit layer to hide the selection box when View Gizmos is disabled, since it is a bit distracting.
// It's still possible to approximately guess what is selected by looking at the manipulation gizmo position.
RS::get_singleton()->instance_set_layer_mask(si->sbox_instance, 1 << Node3DEditorViewport::GIZMO_EDIT_LAYER);
RS::get_singleton()->instance_set_layer_mask(si->sbox_instance_offset, 1 << Node3DEditorViewport::GIZMO_EDIT_LAYER);
RS::get_singleton()->instance_geometry_set_flag(si->sbox_instance, RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
RS::get_singleton()->instance_geometry_set_flag(si->sbox_instance, RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
RS::get_singleton()->instance_geometry_set_flag(si->sbox_instance_offset, RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
RS::get_singleton()->instance_geometry_set_flag(si->sbox_instance_offset, RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
si->sbox_instance_xray = RenderingServer::get_singleton()->instance_create2(
selection_box_xray->get_rid(),
sp->get_world_3d()->get_scenario());
si->sbox_instance_xray_offset = RenderingServer::get_singleton()->instance_create2(
selection_box_xray->get_rid(),
sp->get_world_3d()->get_scenario());
RS::get_singleton()->instance_geometry_set_cast_shadows_setting(
si->sbox_instance_xray,
RSE::SHADOW_CASTING_SETTING_OFF);
RS::get_singleton()->instance_geometry_set_cast_shadows_setting(
si->sbox_instance_xray_offset,
RSE::SHADOW_CASTING_SETTING_OFF);
// Use the Edit layer to hide the selection box when View Gizmos is disabled, since it is a bit distracting.
// It's still possible to approximately guess what is selected by looking at the manipulation gizmo position.
RS::get_singleton()->instance_set_layer_mask(si->sbox_instance_xray, 1 << Node3DEditorViewport::GIZMO_EDIT_LAYER);
RS::get_singleton()->instance_set_layer_mask(si->sbox_instance_xray_offset, 1 << Node3DEditorViewport::GIZMO_EDIT_LAYER);
RS::get_singleton()->instance_geometry_set_flag(si->sbox_instance_xray, RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
RS::get_singleton()->instance_geometry_set_flag(si->sbox_instance_xray, RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
RS::get_singleton()->instance_geometry_set_flag(si->sbox_instance_xray_offset, RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
RS::get_singleton()->instance_geometry_set_flag(si->sbox_instance_xray_offset, RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
return si;
}
void Node3DEditor::_generate_selection_boxes() {
// Use two AABBs to create the illusion of a slightly thicker line.
AABB aabb(Vector3(), Vector3(1, 1, 1));
// Create a x-ray (visible through solid surfaces) and standard version of the selection box.
// Both will be drawn at the same position, but with different opacity.
// This lets the user see where the selection is while still having a sense of depth.
Ref<SurfaceTool> st = memnew(SurfaceTool);
Ref<SurfaceTool> st_xray = memnew(SurfaceTool);
Ref<SurfaceTool> active_st = memnew(SurfaceTool);
Ref<SurfaceTool> active_st_xray = memnew(SurfaceTool);
st->begin(Mesh::PRIMITIVE_LINES);
st_xray->begin(Mesh::PRIMITIVE_LINES);
active_st->begin(Mesh::PRIMITIVE_LINES);
active_st_xray->begin(Mesh::PRIMITIVE_LINES);
for (int i = 0; i < 12; i++) {
Vector3 a, b;
aabb.get_edge(i, a, b);
st->add_vertex(a);
st->add_vertex(b);
active_st->add_vertex(a);
active_st->add_vertex(b);
st_xray->add_vertex(a);
st_xray->add_vertex(b);
active_st_xray->add_vertex(a);
active_st_xray->add_vertex(b);
}
const Color selection_box_color = EDITOR_GET("editors/3d/selection_box_color");
const Color active_selection_box_color = EDITOR_GET("editors/3d/active_selection_box_color");
selection_box_mat.instantiate();
selection_box_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
selection_box_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
selection_box_mat->set_albedo(selection_box_color);
selection_box_mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
st->set_material(selection_box_mat);
selection_box = st->commit();
selection_box_mat_xray.instantiate();
selection_box_mat_xray->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
selection_box_mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
selection_box_mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true);
selection_box_mat_xray->set_albedo(selection_box_color * Color(1, 1, 1, 0.15));
selection_box_mat_xray->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
st_xray->set_material(selection_box_mat_xray);
selection_box_xray = st_xray->commit();
active_selection_box_mat.instantiate();
active_selection_box_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
active_selection_box_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
active_selection_box_mat->set_albedo(active_selection_box_color);
active_selection_box_mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
active_st->set_material(active_selection_box_mat);
active_selection_box = active_st->commit();
active_selection_box_mat_xray.instantiate();
active_selection_box_mat_xray->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
active_selection_box_mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
active_selection_box_mat_xray->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true);
active_selection_box_mat_xray->set_albedo(active_selection_box_color * Color(1, 1, 1, 0.15));
active_selection_box_mat_xray->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
active_st_xray->set_material(active_selection_box_mat_xray);
active_selection_box_xray = active_st_xray->commit();
}
Dictionary Node3DEditor::get_state() const {
Dictionary d;
d["snap_enabled"] = snap_enabled;
d["trackball_enabled"] = trackball_enabled;
d["translate_snap"] = snap_translate_value;
d["rotate_snap"] = snap_rotate_value;
d["scale_snap"] = snap_scale_value;
d["local_coords"] = tool_option_button[TOOL_OPT_LOCAL_COORDS]->is_pressed();
d["preserve_children_transform"] = tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->is_pressed();
int vc = 0;
if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT))) {
vc = 1;
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS))) {
vc = 2;
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS))) {
vc = 3;
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_4_VIEWPORTS))) {
vc = 4;
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS_ALT))) {
vc = 5;
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS_ALT))) {
vc = 6;
}
d["viewport_mode"] = vc;
d["viewport_splits"] = viewport_base->get_split_state();
Array vpdata;
for (int i = 0; i < 4; i++) {
vpdata.push_back(viewports[i]->get_state());
}
d["viewports"] = vpdata;
d["vertex_snap_origin_mode"] = vertex_snap_origin_mode;
d["vertex_snap_use_collision"] = vertex_snap_use_collision;
d["show_grid"] = view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_GRID));
d["show_origin"] = view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_ORIGIN));
d["fov"] = get_fov();
d["znear"] = get_znear();
d["zfar"] = get_zfar();
Dictionary gizmos_status;
for (int i = 0; i < gizmo_plugins_by_name.size(); i++) {
if (!gizmo_plugins_by_name[i]->can_be_hidden()) {
continue;
}
int state = gizmos_menu->get_item_state(gizmos_menu->get_item_index(i));
String name = gizmo_plugins_by_name[i]->get_gizmo_name();
gizmos_status[name] = state;
}
d["gizmos_status"] = gizmos_status;
{
Dictionary pd;
pd["sun_rotation"] = sun_rotation;
pd["environ_sky_color"] = environ_sky_color->get_pick_color();
pd["environ_ground_color"] = environ_ground_color->get_pick_color();
pd["environ_energy"] = environ_energy->get_value();
pd["environ_glow_enabled"] = environ_glow_button->is_pressed();
pd["environ_tonemap_enabled"] = environ_tonemap_button->is_pressed();
pd["environ_ao_enabled"] = environ_ao_button->is_pressed();
pd["environ_gi_enabled"] = environ_gi_button->is_pressed();
pd["sun_shadow_max_distance"] = sun_shadow_max_distance->get_value();
pd["sun_color"] = sun_color->get_pick_color();
pd["sun_energy"] = sun_energy->get_value();
pd["sun_enabled"] = sun_button->is_pressed();
pd["environ_enabled"] = environ_button->is_pressed();
d["preview_sun_env"] = pd;
}
return d;
}
void Node3DEditor::set_state(const Dictionary &p_state) {
Dictionary d = p_state;
if (d.has("snap_enabled")) {
snap_enabled = d["snap_enabled"];
tool_option_button[TOOL_OPT_USE_SNAP]->set_pressed(d["snap_enabled"]);
}
if (d.has("trackball_enabled")) {
trackball_enabled = d["trackball_enabled"];
tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_pressed(d["trackball_enabled"]);
}
if (d.has("translate_snap")) {
snap_translate_value = d["translate_snap"];
}
if (d.has("rotate_snap")) {
snap_rotate_value = d["rotate_snap"];
}
if (d.has("scale_snap")) {
snap_scale_value = d["scale_snap"];
}
_snap_update();
if (d.has("preserve_children_transform")) {
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_pressed(d["preserve_children_transform"]);
}
if (d.has("vertex_snap_origin_mode")) {
vertex_snap_origin_mode = d["vertex_snap_origin_mode"];
int idx_vertex = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_BASE_VERTEX);
int idx_origin = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_BASE_ORIGIN);
transform_menu->get_popup()->set_item_checked(idx_vertex, !vertex_snap_origin_mode);
transform_menu->get_popup()->set_item_checked(idx_origin, vertex_snap_origin_mode);
}
if (d.has("vertex_snap_use_collision")) {
vertex_snap_use_collision = d["vertex_snap_use_collision"];
int idx_mesh = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH);
int idx_collision = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_COLLISION);
transform_menu->get_popup()->set_item_checked(idx_mesh, !vertex_snap_use_collision);
transform_menu->get_popup()->set_item_checked(idx_collision, vertex_snap_use_collision);
}
if (d.has("local_coords")) {
tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_pressed(d["local_coords"]);
update_transform_gizmo();
}
if (d.has("viewport_mode")) {
int vc = d["viewport_mode"];
if (vc == 1) {
_menu_item_pressed(MENU_VIEW_USE_1_VIEWPORT);
} else if (vc == 2) {
_menu_item_pressed(MENU_VIEW_USE_2_VIEWPORTS);
} else if (vc == 3) {
_menu_item_pressed(MENU_VIEW_USE_3_VIEWPORTS);
} else if (vc == 4) {
_menu_item_pressed(MENU_VIEW_USE_4_VIEWPORTS);
} else if (vc == 5) {
_menu_item_pressed(MENU_VIEW_USE_2_VIEWPORTS_ALT);
} else if (vc == 6) {
_menu_item_pressed(MENU_VIEW_USE_3_VIEWPORTS_ALT);
}
}
if (d.has("zfar")) {
settings_zfar->set_value(double(d["zfar"]));
}
if (d.has("znear")) {
settings_znear->set_value(double(d["znear"]));
}
if (d.has("fov")) {
settings_fov->set_value(double(d["fov"]));
}
if (d.has("viewport_splits")) {
viewport_base->set_split_state(d["viewport_splits"]);
}
if (d.has("viewports")) {
Array vp = d["viewports"];
uint32_t vp_size = static_cast<uint32_t>(vp.size());
if (vp_size > VIEWPORTS_COUNT) {
WARN_PRINT("Ignoring superfluous viewport settings from spatial editor state.");
vp_size = VIEWPORTS_COUNT;
}
for (uint32_t i = 0; i < vp_size; i++) {
viewports[i]->set_state(vp[i]);
}
}
if (d.has("show_grid")) {
bool use = d["show_grid"];
if (use != view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_GRID))) {
_menu_item_pressed(MENU_VIEW_GRID);
}
}
if (d.has("show_origin")) {
bool use = d["show_origin"];
if (use != view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_ORIGIN))) {
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_ORIGIN), use);
RenderingServer::get_singleton()->instance_set_visible(origin_instance, use);
}
}
if (d.has("gizmos_status")) {
Dictionary gizmos_status = d["gizmos_status"];
for (int j = 0; j < gizmo_plugins_by_name.size(); ++j) {
if (!gizmo_plugins_by_name[j]->can_be_hidden()) {
continue;
}
int state = EditorNode3DGizmoPlugin::VISIBLE;
for (const KeyValue<Variant, Variant> &kv : gizmos_status) {
if (gizmo_plugins_by_name.write[j]->get_gizmo_name() == String(kv.key)) {
state = kv.value;
break;
}
}
gizmo_plugins_by_name.write[j]->set_state(state);
}
_update_gizmos_menu();
}
if (d.has("preview_sun_env")) {
sun_environ_updating = true;
Dictionary pd = d["preview_sun_env"];
sun_rotation = pd["sun_rotation"];
environ_sky_color->set_pick_color(pd["environ_sky_color"]);
environ_ground_color->set_pick_color(pd["environ_ground_color"]);
environ_energy->set_value_no_signal(pd["environ_energy"]);
environ_glow_button->set_pressed_no_signal(pd["environ_glow_enabled"]);
environ_tonemap_button->set_pressed_no_signal(pd["environ_tonemap_enabled"]);
environ_ao_button->set_pressed_no_signal(pd["environ_ao_enabled"]);
environ_gi_button->set_pressed_no_signal(pd["environ_gi_enabled"]);
sun_shadow_max_distance->set_value_no_signal(pd["sun_shadow_max_distance"]);
sun_color->set_pick_color(pd["sun_color"]);
sun_energy->set_value_no_signal(pd["sun_energy"]);
sun_button->set_pressed(pd["sun_enabled"]);
environ_button->set_pressed(pd["environ_enabled"]);
sun_environ_updating = false;
_preview_settings_changed();
_update_preview_environment();
} else {
_load_default_preview_settings();
sun_button->set_pressed(true);
environ_button->set_pressed(true);
_preview_settings_changed();
_update_preview_environment();
}
}
void Node3DEditor::edit(Node3D *p_spatial) {
if (p_spatial != selected) {
if (selected) {
Vector<Ref<Node3DGizmo>> gizmos = selected->get_gizmos();
for (int i = 0; i < gizmos.size(); i++) {
Ref<EditorNode3DGizmo> seg = gizmos[i];
if (seg.is_null()) {
continue;
}
seg->set_selected(false);
}
Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(selected);
if (se) {
se->gizmo.unref();
se->subgizmos.clear();
}
selected->update_gizmos();
}
selected = p_spatial;
current_hover_gizmo = Ref<EditorNode3DGizmo>();
current_hover_gizmo_handle = -1;
current_hover_gizmo_handle_secondary = false;
if (selected) {
Vector<Ref<Node3DGizmo>> gizmos = selected->get_gizmos();
for (int i = 0; i < gizmos.size(); i++) {
Ref<EditorNode3DGizmo> seg = gizmos[i];
if (seg.is_null()) {
continue;
}
seg->set_selected(true);
}
selected->update_gizmos();
}
}
}
void Node3DEditor::_snap_changed() {
snap_translate_value = snap_translate->get_value();
snap_rotate_value = snap_rotate->get_value();
snap_scale_value = snap_scale->get_value();
EditorSettings::get_singleton()->set_project_metadata("3d_editor", "snap_translate_value", snap_translate_value);
EditorSettings::get_singleton()->set_project_metadata("3d_editor", "snap_rotate_value", snap_rotate_value);
EditorSettings::get_singleton()->set_project_metadata("3d_editor", "snap_scale_value", snap_scale_value);
}
void Node3DEditor::_snap_update() {
snap_translate->set_value(snap_translate_value);
snap_rotate->set_value(snap_rotate_value);
snap_scale->set_value(snap_scale_value);
}
void Node3DEditor::_update_vertex_snap_tooltips() {
String snap_key = ED_GET_SHORTCUT("spatial_editor/vertex_snap")->get_as_text();
PopupMenu *p = transform_menu->get_popup();
p->set_item_tooltip(p->get_item_index(MENU_VERTEX_SNAP_BASE_VERTEX),
vformat(TTR("Hold %s to highlight a vertex on the currently selected node,\nthen drag to move the node and snap it to vertices on neighboring nodes.\n\nFor nodes without a vertex-based representation,\nSnap Origin to Vertex is always used instead."), snap_key));
p->set_item_tooltip(p->get_item_index(MENU_VERTEX_SNAP_BASE_ORIGIN),
vformat(TTR("Hold %s to highlight another node's vertex,\nthen click to teleport the selected node to the highlighted vertex."), snap_key));
p->set_item_tooltip(p->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH),
TTR("Snap to vertices of visual meshes.\nHold Shift while vertex snapping to temporarily snap to collision shapes instead."));
p->set_item_tooltip(p->get_item_index(MENU_VERTEX_SNAP_SOURCE_COLLISION),
TTR("Snap to vertices of collision shapes.\nHold Shift while vertex snapping to temporarily snap to mesh vertices instead."));
}
void Node3DEditor::_xform_dialog_action() {
Transform3D t;
//translation
Vector3 scale;
Vector3 rotate;
Vector3 translate;
for (int i = 0; i < 3; i++) {
translate[i] = xform_translate[i]->get_text().to_float();
rotate[i] = Math::deg_to_rad(xform_rotate[i]->get_text().to_float());
scale[i] = xform_scale[i]->get_text().to_float();
}
t.basis.scale(scale);
t.basis.rotate(rotate);
t.origin = translate;
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("XForm Dialog"));
const List<Node *> &selection = editor_selection->get_top_selected_node_list();
for (Node *E : selection) {
Node3D *sp = Object::cast_to<Node3D>(E);
if (!sp) {
continue;
}
Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(sp);
if (!se) {
continue;
}
bool post = xform_type->get_selected() > 0;
Transform3D tr = sp->get_global_gizmo_transform();
if (post) {
tr = tr * t;
} else {
tr.basis = t.basis * tr.basis;
tr.origin += t.origin;
}
Node3D *parent = sp->get_parent_node_3d();
Transform3D local_tr = parent ? parent->get_global_transform().affine_inverse() * tr : tr;
undo_redo->add_do_method(sp, "set_transform", local_tr);
undo_redo->add_undo_method(sp, "set_transform", sp->get_transform());
}
undo_redo->commit_action();
}
void Node3DEditor::_menu_item_toggled(bool pressed, int p_option) {
switch (p_option) {
case MENU_TOOL_LOCAL_COORDS: {
tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_pressed(pressed);
update_transform_gizmo();
} break;
case MENU_TOOL_USE_SNAP: {
tool_option_button[TOOL_OPT_USE_SNAP]->set_pressed(pressed);
snap_enabled = pressed;
} break;
case MENU_TOOL_USE_TRACKBALL: {
tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_pressed(pressed);
trackball_enabled = pressed;
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
viewports[i]->update_transform_gizmo_highlight();
}
} break;
case MENU_TOOL_PRESERVE_CHILDREN_TRANSFORM: {
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_pressed(pressed);
if (pressed) {
EditorNode::get_editor_data().add_undo_redo_inspector_hook_callback(callable_mp(this, &Node3DEditor::_undo_redo_inspector_callback));
} else {
EditorNode::get_editor_data().remove_undo_redo_inspector_hook_callback(callable_mp(this, &Node3DEditor::_undo_redo_inspector_callback));
}
} break;
}
}
void Node3DEditor::_undo_redo_inspector_callback(Object *p_undo_redo, Object *p_edited, const String &p_property, const Variant &p_new_value) {
Node3D *node = Object::cast_to<Node3D>(p_edited);
if (!node) {
return;
}
static const char *transform_properties[] = { "position", "rotation", "scale", "quaternion", "basis", "transform", nullptr };
bool is_transform_prop = false;
for (int i = 0; transform_properties[i]; i++) {
if (p_property == transform_properties[i]) {
is_transform_prop = true;
break;
}
}
if (!is_transform_prop) {
return;
}
EditorUndoRedoManager *undo_redo = Object::cast_to<EditorUndoRedoManager>(p_undo_redo);
ERR_FAIL_NULL(undo_redo);
int child_count = node->get_child_count();
for (int i = 0; i < child_count; i++) {
Node3D *child = Object::cast_to<Node3D>(node->get_child(i));
if (child) {
Transform3D child_global = child->get_global_transform();
undo_redo->add_do_method(child, "set_global_transform", child_global);
undo_redo->add_undo_method(child, "set_global_transform", child_global);
}
}
}
void Node3DEditor::_menu_gizmo_toggled(int p_option) {
const int idx = gizmos_menu->get_item_index(p_option);
gizmos_menu->toggle_item_multistate(idx);
// Change icon
const int state = gizmos_menu->get_item_state(idx);
switch (state) {
case EditorNode3DGizmoPlugin::VISIBLE:
gizmos_menu->set_item_icon(idx, get_editor_theme_icon(SNAME("GuiVisibilityVisible")));
break;
case EditorNode3DGizmoPlugin::ON_TOP:
gizmos_menu->set_item_icon(idx, get_editor_theme_icon(SNAME("GuiVisibilityXray")));
break;
case EditorNode3DGizmoPlugin::HIDDEN:
gizmos_menu->set_item_icon(idx, get_editor_theme_icon(SNAME("GuiVisibilityHidden")));
break;
}
gizmo_plugins_by_name.write[p_option]->set_state(state);
update_all_gizmos();
}
void Node3DEditor::_menu_item_pressed(int p_option) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
switch (p_option) {
case MENU_TOOL_TRANSFORM:
case MENU_TOOL_MOVE:
case MENU_TOOL_ROTATE:
case MENU_TOOL_SCALE:
case MENU_TOOL_SELECT:
case MENU_TOOL_LIST_SELECT: {
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
if (viewports[i]->_edit.mode != Node3DEditorViewport::TRANSFORM_NONE) {
viewports[i]->commit_transform();
}
}
for (int i = 0; i < TOOL_MAX; i++) {
tool_button[i]->set_pressed(i == p_option);
}
tool_mode = (ToolMode)p_option;
update_transform_gizmo();
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
viewports[i]->update_transform_gizmo_highlight();
}
} break;
case MENU_TRANSFORM_CONFIGURE_SNAP: {
snap_dialog->popup_centered(Size2(200, 180));
} break;
case MENU_VERTEX_SNAP_BASE_VERTEX: {
vertex_snap_origin_mode = false;
int idx_vertex = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_BASE_VERTEX);
int idx_origin = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_BASE_ORIGIN);
transform_menu->get_popup()->set_item_checked(idx_vertex, true);
transform_menu->get_popup()->set_item_checked(idx_origin, false);
} break;
case MENU_VERTEX_SNAP_BASE_ORIGIN: {
vertex_snap_origin_mode = true;
int idx_vertex = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_BASE_VERTEX);
int idx_origin = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_BASE_ORIGIN);
transform_menu->get_popup()->set_item_checked(idx_vertex, false);
transform_menu->get_popup()->set_item_checked(idx_origin, true);
} break;
case MENU_VERTEX_SNAP_SOURCE_MESH: {
vertex_snap_use_collision = false;
int idx_mesh = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH);
int idx_collision = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_COLLISION);
transform_menu->get_popup()->set_item_checked(idx_mesh, true);
transform_menu->get_popup()->set_item_checked(idx_collision, false);
} break;
case MENU_VERTEX_SNAP_SOURCE_COLLISION: {
vertex_snap_use_collision = true;
int idx_mesh = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH);
int idx_collision = transform_menu->get_popup()->get_item_index(MENU_VERTEX_SNAP_SOURCE_COLLISION);
transform_menu->get_popup()->set_item_checked(idx_mesh, false);
transform_menu->get_popup()->set_item_checked(idx_collision, true);
} break;
case MENU_TRANSFORM_DIALOG: {
for (int i = 0; i < 3; i++) {
xform_translate[i]->set_text("0");
xform_rotate[i]->set_text("0");
xform_scale[i]->set_text("1");
}
xform_dialog->popup_centered(Size2(320, 240) * EDSCALE);
} break;
case MENU_VIEW_USE_1_VIEWPORT: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_1_VIEWPORT);
if (last_used_viewport > 0) {
last_used_viewport = 0;
}
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), true);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_4_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS_ALT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS_ALT), false);
} break;
case MENU_VIEW_USE_2_VIEWPORTS: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_2_VIEWPORTS);
if (last_used_viewport > 1) {
last_used_viewport = 0;
}
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), true);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_4_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS_ALT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS_ALT), false);
} break;
case MENU_VIEW_USE_2_VIEWPORTS_ALT: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_2_VIEWPORTS_ALT);
if (last_used_viewport > 1) {
last_used_viewport = 0;
}
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_4_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS_ALT), true);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS_ALT), false);
} break;
case MENU_VIEW_USE_3_VIEWPORTS: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_3_VIEWPORTS);
if (last_used_viewport > 2) {
last_used_viewport = 0;
}
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS), true);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_4_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS_ALT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS_ALT), false);
} break;
case MENU_VIEW_USE_3_VIEWPORTS_ALT: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_3_VIEWPORTS_ALT);
if (last_used_viewport > 2) {
last_used_viewport = 0;
}
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_4_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS_ALT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS_ALT), true);
} break;
case MENU_VIEW_USE_4_VIEWPORTS: {
viewport_base->set_view(Node3DEditorViewportContainer::VIEW_USE_4_VIEWPORTS);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_4_VIEWPORTS), true);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS_ALT), false);
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS_ALT), false);
} break;
case MENU_VIEW_ORIGIN: {
bool is_checked = view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(p_option));
origin_enabled = !is_checked;
RenderingServer::get_singleton()->instance_set_visible(origin_instance, origin_enabled);
// Update the grid since its appearance depends on whether the origin is enabled
_finish_grid();
_init_grid();
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(p_option), origin_enabled);
} break;
case MENU_VIEW_GRID: {
bool is_checked = view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(p_option));
grid_enabled = !is_checked;
for (int i = 0; i < 3; ++i) {
if (grid_enable[i]) {
grid_visible[i] = grid_enabled;
}
}
_finish_grid();
_init_grid();
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(p_option), grid_enabled);
} break;
case MENU_VIEW_CAMERA_SETTINGS: {
settings_dialog->popup_centered(settings_vbc->get_combined_minimum_size() + Size2(50, 50));
} break;
case MENU_SNAP_TO_FLOOR: {
snap_selected_nodes_to_floor();
} break;
case MENU_LOCK_SELECTED: {
undo_redo->create_action(TTR("Lock Selected"));
const List<Node *> &selection = editor_selection->get_top_selected_node_list();
for (Node *E : selection) {
Node3D *spatial = Object::cast_to<Node3D>(E);
if (!spatial || !spatial->is_inside_tree()) {
continue;
}
undo_redo->add_do_method(spatial, "set_meta", "_edit_lock_", true);
undo_redo->add_undo_method(spatial, "remove_meta", "_edit_lock_");
undo_redo->add_do_method(this, "emit_signal", "item_lock_status_changed");
undo_redo->add_undo_method(this, "emit_signal", "item_lock_status_changed");
}
undo_redo->add_do_method(this, "_refresh_menu_icons");
undo_redo->add_undo_method(this, "_refresh_menu_icons");
undo_redo->add_do_method(this, "update_transform_gizmo");
undo_redo->add_undo_method(this, "update_transform_gizmo");
undo_redo->commit_action();
} break;
case MENU_UNLOCK_SELECTED: {
undo_redo->create_action(TTR("Unlock Selected"));
const List<Node *> &selection = editor_selection->get_top_selected_node_list();
for (Node *E : selection) {
Node3D *spatial = Object::cast_to<Node3D>(E);
if (!spatial || !spatial->is_inside_tree()) {
continue;
}
undo_redo->add_do_method(spatial, "remove_meta", "_edit_lock_");
undo_redo->add_undo_method(spatial, "set_meta", "_edit_lock_", true);
undo_redo->add_do_method(this, "emit_signal", "item_lock_status_changed");
undo_redo->add_undo_method(this, "emit_signal", "item_lock_status_changed");
}
undo_redo->add_do_method(this, "_refresh_menu_icons");
undo_redo->add_undo_method(this, "_refresh_menu_icons");
undo_redo->add_do_method(this, "update_transform_gizmo");
undo_redo->add_undo_method(this, "update_transform_gizmo");
undo_redo->commit_action();
} break;
case MENU_GROUP_SELECTED: {
undo_redo->create_action(TTR("Group Selected"));
const List<Node *> &selection = editor_selection->get_top_selected_node_list();
for (Node *E : selection) {
Node3D *spatial = Object::cast_to<Node3D>(E);
if (!spatial || !spatial->is_inside_tree()) {
continue;
}
undo_redo->add_do_method(spatial, "set_meta", "_edit_group_", true);
undo_redo->add_undo_method(spatial, "remove_meta", "_edit_group_");
undo_redo->add_do_method(this, "emit_signal", "item_group_status_changed");
undo_redo->add_undo_method(this, "emit_signal", "item_group_status_changed");
}
undo_redo->add_do_method(this, "_refresh_menu_icons");
undo_redo->add_undo_method(this, "_refresh_menu_icons");
undo_redo->commit_action();
} break;
case MENU_UNGROUP_SELECTED: {
undo_redo->create_action(TTR("Ungroup Selected"));
const List<Node *> &selection = editor_selection->get_top_selected_node_list();
for (Node *E : selection) {
Node3D *spatial = Object::cast_to<Node3D>(E);
if (!spatial || !spatial->is_inside_tree()) {
continue;
}
undo_redo->add_do_method(spatial, "remove_meta", "_edit_group_");
undo_redo->add_undo_method(spatial, "set_meta", "_edit_group_", true);
undo_redo->add_do_method(this, "emit_signal", "item_group_status_changed");
undo_redo->add_undo_method(this, "emit_signal", "item_group_status_changed");
}
undo_redo->add_do_method(this, "_refresh_menu_icons");
undo_redo->add_undo_method(this, "_refresh_menu_icons");
undo_redo->commit_action();
} break;
case MENU_RULER: {
for (int i = 0; i < TOOL_MAX; i++) {
tool_button[i]->set_pressed(i == p_option);
}
tool_button[TOOL_RULER]->set_pressed(true);
tool_mode = ToolMode::TOOL_RULER;
update_transform_gizmo();
} break;
}
}
void Node3DEditor::_init_indicators() {
{
origin_enabled = true;
grid_enabled = true;
Ref<Shader> origin_shader = memnew(Shader);
origin_shader->set_code(R"(
// 3D editor origin line shader.
shader_type spatial;
render_mode blend_mix, cull_disabled, unshaded, fog_disabled;
void vertex() {
vec3 point_a = MODEL_MATRIX[3].xyz;
// Encoded in scale.
vec3 point_b = vec3(MODEL_MATRIX[0].x, MODEL_MATRIX[1].y, MODEL_MATRIX[2].z);
// Points are already in world space, so no need for MODEL_MATRIX anymore.
vec4 clip_a = PROJECTION_MATRIX * (VIEW_MATRIX * vec4(point_a, 1.0));
vec4 clip_b = PROJECTION_MATRIX * (VIEW_MATRIX * vec4(point_b, 1.0));
vec2 screen_a = VIEWPORT_SIZE * (0.5 * clip_a.xy / clip_a.w + 0.5);
vec2 screen_b = VIEWPORT_SIZE * (0.5 * clip_b.xy / clip_b.w + 0.5);
vec2 x_basis = normalize(screen_b - screen_a);
vec2 y_basis = vec2(-x_basis.y, x_basis.x);
float width = 3.0;
vec2 screen_point_a = screen_a + width * (VERTEX.x * x_basis + VERTEX.y * y_basis);
vec2 screen_point_b = screen_b + width * (VERTEX.x * x_basis + VERTEX.y * y_basis);
vec2 screen_point_final = mix(screen_point_a, screen_point_b, VERTEX.z);
vec4 clip_final = mix(clip_a, clip_b, VERTEX.z);
POSITION = vec4(clip_final.w * ((2.0 * screen_point_final) / VIEWPORT_SIZE - 1.0), clip_final.z, clip_final.w);
UV = VERTEX.yz * clip_final.w;
if (!OUTPUT_IS_SRGB) {
COLOR.rgb = mix(pow((COLOR.rgb + vec3(0.055)) * (1.0 / (1.0 + 0.055)), vec3(2.4)), COLOR.rgb * (1.0 / 12.92), lessThan(COLOR.rgb, vec3(0.04045)));
}
}
void fragment() {
// Multiply by 0.5 since UV is actually UV is [-1, 1].
float line_width = fwidth(UV.x * 0.5);
float line_uv = abs(UV.x * 0.5);
float line = smoothstep(line_width * 1.0, line_width * 0.25, line_uv);
ALBEDO = COLOR.rgb;
ALPHA *= COLOR.a * line;
}
)");
origin_mat.instantiate();
origin_mat->set_shader(origin_shader);
Vector<Vector3> origin_points;
origin_points.resize(6);
origin_points.set(0, Vector3(0.0, -0.5, 0.0));
origin_points.set(1, Vector3(0.0, -0.5, 1.0));
origin_points.set(2, Vector3(0.0, 0.5, 1.0));
origin_points.set(3, Vector3(0.0, -0.5, 0.0));
origin_points.set(4, Vector3(0.0, 0.5, 1.0));
origin_points.set(5, Vector3(0.0, 0.5, 0.0));
Array d;
d.resize(RSE::ARRAY_MAX);
d[RSE::ARRAY_VERTEX] = origin_points;
origin_mesh = RenderingServer::get_singleton()->mesh_create();
RenderingServer::get_singleton()->mesh_add_surface_from_arrays(origin_mesh, RSE::PRIMITIVE_TRIANGLES, d);
RenderingServer::get_singleton()->mesh_surface_set_material(origin_mesh, 0, origin_mat->get_rid());
origin_multimesh = RenderingServer::get_singleton()->multimesh_create();
RenderingServer::get_singleton()->multimesh_set_mesh(origin_multimesh, origin_mesh);
RenderingServer::get_singleton()->multimesh_allocate_data(origin_multimesh, 12, RSE::MultimeshTransformFormat::MULTIMESH_TRANSFORM_3D, true, false);
RenderingServer::get_singleton()->multimesh_set_visible_instances(origin_multimesh, -1);
LocalVector<float> distances;
distances.resize(5);
distances[0] = -1000000.0;
distances[1] = -1000.0;
distances[2] = 0.0;
distances[3] = 1000.0;
distances[4] = 1000000.0;
for (int i = 0; i < 3; i++) {
Color origin_color;
switch (i) {
case 0:
origin_color = get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor));
break;
case 1:
origin_color = get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor));
break;
case 2:
origin_color = get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor));
break;
default:
origin_color = Color();
break;
}
Vector3 axis;
axis[i] = 1;
for (int j = 0; j < 4; j++) {
Transform3D t = Transform3D();
if (distances[j] > 0.0) {
t = t.scaled(axis * distances[j + 1]);
t = t.translated(axis * distances[j]);
} else {
t = t.scaled(axis * distances[j]);
t = t.translated(axis * distances[j + 1]);
}
RenderingServer::get_singleton()->multimesh_instance_set_transform(origin_multimesh, i * 4 + j, t);
RenderingServer::get_singleton()->multimesh_instance_set_color(origin_multimesh, i * 4 + j, origin_color);
}
}
origin_instance = RenderingServer::get_singleton()->instance_create2(origin_multimesh, get_tree()->get_root()->get_world_3d()->get_scenario());
RS::get_singleton()->instance_set_layer_mask(origin_instance, 1 << Node3DEditorViewport::GIZMO_GRID_LAYER);
RS::get_singleton()->instance_geometry_set_flag(origin_instance, RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
RS::get_singleton()->instance_geometry_set_flag(origin_instance, RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
RenderingServer::get_singleton()->instance_geometry_set_cast_shadows_setting(origin_instance, RSE::SHADOW_CASTING_SETTING_OFF);
Ref<Shader> grid_shader = memnew(Shader);
grid_shader->set_code(R"(
// 3D editor grid shader.
shader_type spatial;
render_mode unshaded, fog_disabled;
uniform bool orthogonal;
uniform float grid_size;
void vertex() {
// From FLAG_SRGB_VERTEX_COLOR.
if (!OUTPUT_IS_SRGB) {
COLOR.rgb = mix(pow((COLOR.rgb + vec3(0.055)) * (1.0 / (1.0 + 0.055)), vec3(2.4)), COLOR.rgb * (1.0 / 12.92), lessThan(COLOR.rgb, vec3(0.04045)));
}
}
void fragment() {
ALBEDO = COLOR.rgb;
vec3 dir = orthogonal ? -vec3(0, 0, 1) : VIEW;
float angle_fade = abs(dot(dir, NORMAL));
angle_fade = smoothstep(0.05, 0.2, angle_fade);
vec3 world_pos = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
vec3 world_normal = (INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz;
vec3 camera_world_pos = INV_VIEW_MATRIX[3].xyz;
vec3 camera_world_pos_on_plane = camera_world_pos * (1.0 - world_normal);
float dist_fade = 1.0 - (distance(world_pos, camera_world_pos_on_plane) / grid_size);
dist_fade = smoothstep(0.02, 0.3, dist_fade);
ALPHA = COLOR.a * dist_fade * angle_fade;
}
)");
for (int i = 0; i < 3; i++) {
grid_mat[i].instantiate();
grid_mat[i]->set_shader(grid_shader);
}
grid_enable[0] = EDITOR_GET("editors/3d/grid_xy_plane");
grid_enable[1] = EDITOR_GET("editors/3d/grid_yz_plane");
grid_enable[2] = EDITOR_GET("editors/3d/grid_xz_plane");
grid_visible[0] = grid_enable[0];
grid_visible[1] = grid_enable[1];
grid_visible[2] = grid_enable[2];
_init_grid();
}
{
//move gizmo
// Inverted zxy.
Vector3 ivec = Vector3(0, 0, -1);
Vector3 nivec = Vector3(-1, -1, 0);
Vector3 ivec2 = Vector3(-1, 0, 0);
Vector3 ivec3 = Vector3(0, -1, 0);
for (int i = 0; i < 4; i++) {
Color col;
switch (i) {
case 0:
col = get_theme_color(SNAME("axis_x_color"), EditorStringName(Editor));
break;
case 1:
col = get_theme_color(SNAME("axis_y_color"), EditorStringName(Editor));
break;
case 2:
col = get_theme_color(SNAME("axis_z_color"), EditorStringName(Editor));
break;
case 3:
col = get_theme_color(SNAME("axis_view_plane_color"), EditorStringName(Editor));
break;
default:
col = Color();
break;
}
col.a = col.a * (float)EDITOR_GET("editors/3d/manipulator_gizmo_opacity");
if (i < 3) {
move_gizmo[i].instantiate();
move_plane_gizmo[i].instantiate();
scale_gizmo[i].instantiate();
scale_plane_gizmo[i].instantiate();
axis_gizmo[i].instantiate();
}
rotate_gizmo[i].instantiate();
const Color albedo = col.from_hsv(col.get_h(), col.get_s() * 0.25, 1.0, 1);
Ref<StandardMaterial3D> mat;
Ref<StandardMaterial3D> mat_hl;
if (i < 3) {
// Only create standard materials for X, Y, Z axes (move/scale gizmos).
mat.instantiate();
mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
mat->set_on_top_of_alpha();
mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
mat->set_albedo(col);
gizmo_color[i] = mat;
mat_hl = mat->duplicate();
mat_hl->set_albedo(albedo);
gizmo_color_hl[i] = mat_hl;
}
// Only create translate gizmo for X, Y, Z axes (not view rotation).
if (i < 3) {
//translate
{
Ref<SurfaceTool> surftool;
surftool.instantiate();
surftool->begin(Mesh::PRIMITIVE_TRIANGLES);
// Arrow profile
const int arrow_points = 5;
Vector3 arrow[5] = {
nivec * 0.0 + ivec * 0.0,
nivec * 0.01 + ivec * 0.0,
nivec * 0.01 + ivec * GIZMO_ARROW_OFFSET,
nivec * 0.065 + ivec * GIZMO_ARROW_OFFSET,
nivec * 0.0 + ivec * (GIZMO_ARROW_OFFSET + GIZMO_ARROW_SIZE),
};
int arrow_sides = 16;
const real_t arrow_sides_step = Math::TAU / arrow_sides;
for (int k = 0; k < arrow_sides; k++) {
Basis ma(ivec, k * arrow_sides_step);
Basis mb(ivec, (k + 1) * arrow_sides_step);
for (int j = 0; j < arrow_points - 1; j++) {
Vector3 points[4] = {
ma.xform(arrow[j]),
mb.xform(arrow[j]),
mb.xform(arrow[j + 1]),
ma.xform(arrow[j + 1]),
};
surftool->add_vertex(points[0]);
surftool->add_vertex(points[1]);
surftool->add_vertex(points[2]);
surftool->add_vertex(points[0]);
surftool->add_vertex(points[2]);
surftool->add_vertex(points[3]);
}
}
surftool->set_material(mat);
surftool->commit(move_gizmo[i]);
}
// Plane Translation
{
Ref<SurfaceTool> surftool;
surftool.instantiate();
surftool->begin(Mesh::PRIMITIVE_TRIANGLES);
Vector3 vec = ivec2 - ivec3;
Vector3 plane[4] = {
vec * GIZMO_PLANE_DST,
vec * GIZMO_PLANE_DST + ivec2 * GIZMO_PLANE_SIZE,
vec * (GIZMO_PLANE_DST + GIZMO_PLANE_SIZE),
vec * GIZMO_PLANE_DST - ivec3 * GIZMO_PLANE_SIZE
};
Basis ma(ivec, Math::PI / 2);
Vector3 points[4] = {
ma.xform(plane[0]),
ma.xform(plane[1]),
ma.xform(plane[2]),
ma.xform(plane[3]),
};
surftool->add_vertex(points[0]);
surftool->add_vertex(points[1]);
surftool->add_vertex(points[2]);
surftool->add_vertex(points[0]);
surftool->add_vertex(points[2]);
surftool->add_vertex(points[3]);
Ref<StandardMaterial3D> plane_mat;
plane_mat.instantiate();
plane_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
plane_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
plane_mat->set_on_top_of_alpha();
plane_mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
plane_mat->set_cull_mode(StandardMaterial3D::CULL_DISABLED);
plane_mat->set_albedo(col);
plane_gizmo_color[i] = plane_mat; // Needed, so we can draw planes from both sides.
surftool->set_material(plane_mat);
surftool->commit(move_plane_gizmo[i]);
Ref<StandardMaterial3D> plane_mat_hl = plane_mat->duplicate();
plane_mat_hl->set_albedo(albedo);
plane_gizmo_color_hl[i] = plane_mat_hl; // Needed, so we can draw planes from both sides.
}
}
// Rotate - for all 4 indices (X, Y, Z, and view rotation).
{
Ref<SurfaceTool> surftool;
surftool.instantiate();
surftool->begin(Mesh::PRIMITIVE_TRIANGLES);
int n = 128; // number of circle segments
int m = 3; // number of thickness segments
real_t step = Math::TAU / n;
for (int j = 0; j < n; ++j) {
Basis basis = Basis(ivec, j * step);
Vector3 vertex = basis.xform(ivec2 * GIZMO_CIRCLE_SIZE);
for (int k = 0; k < m; ++k) {
Vector2 ofs = Vector2(Math::cos((Math::TAU * k) / m), Math::sin((Math::TAU * k) / m));
Vector3 normal = ivec * ofs.x + ivec2 * ofs.y;
surftool->set_normal(basis.xform(normal));
surftool->add_vertex(vertex);
}
}
for (int j = 0; j < n; ++j) {
for (int k = 0; k < m; ++k) {
int current_ring = j * m;
int next_ring = ((j + 1) % n) * m;
int current_segment = k;
int next_segment = (k + 1) % m;
surftool->add_index(current_ring + next_segment);
surftool->add_index(current_ring + current_segment);
surftool->add_index(next_ring + current_segment);
surftool->add_index(next_ring + current_segment);
surftool->add_index(next_ring + next_segment);
surftool->add_index(current_ring + next_segment);
}
}
Ref<Shader> rotate_shader = memnew(Shader);
// Use special shader for view rotation (index 3) with camera-relative transformation.
if (i == 3) {
rotate_shader->set_code(R"(
shader_type spatial;
render_mode unshaded, depth_test_disabled, fog_disabled;
uniform vec4 albedo;
mat3 orthonormalize(mat3 m) {
vec3 x = normalize(m[0]);
vec3 y = normalize(m[1] - x * dot(x, m[1]));
vec3 z = m[2] - x * dot(x, m[2]);
z = normalize(z - y * (dot(y, m[2])));
return mat3(x,y,z);
}
void vertex() {
mat3 mv = orthonormalize(mat3(MODELVIEW_MATRIX));
mv = inverse(mv);
VERTEX += NORMAL * 0.008;
vec3 camera_dir_local = mv * vec3(0.0, 0.0, 1.0);
vec3 camera_up_local = mv * vec3(0.0, 1.0, 0.0);
mat3 rotation_matrix = mat3(cross(camera_dir_local, camera_up_local), camera_up_local, camera_dir_local);
VERTEX = rotation_matrix * VERTEX;
}
void fragment() {
ALBEDO = albedo.rgb;
ALPHA = albedo.a;
}
)");
} else {
// Standard shader for X, Y, Z rotation gizmos.
rotate_shader->set_code(R"(
// 3D editor rotation manipulator gizmo shader.
shader_type spatial;
render_mode unshaded, depth_test_disabled, fog_disabled;
uniform vec4 albedo;
mat3 orthonormalize(mat3 m) {
vec3 x = normalize(m[0]);
vec3 y = normalize(m[1] - x * dot(x, m[1]));
vec3 z = m[2] - x * dot(x, m[2]);
z = normalize(z - y * (dot(y, m[2])));
return mat3(x, y, z);
}
void vertex() {
mat3 mv = orthonormalize(mat3(MODELVIEW_MATRIX));
vec3 n = mv * VERTEX;
float orientation = dot(vec3(0.0, 0.0, -1.0), n);
if (orientation <= 0.005) {
VERTEX += NORMAL * 0.02;
}
}
void fragment() {
ALBEDO = albedo.rgb;
ALPHA = albedo.a;
}
)");
}
Ref<ShaderMaterial> rotate_mat;
rotate_mat.instantiate();
rotate_mat->set_render_priority(Material::RENDER_PRIORITY_MAX);
rotate_mat->set_shader(rotate_shader);
rotate_mat->set_shader_parameter("albedo", col);
rotate_gizmo_color[i] = rotate_mat;
Array arrays = surftool->commit_to_arrays();
rotate_gizmo[i]->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays);
rotate_gizmo[i]->surface_set_material(0, rotate_mat);
Ref<ShaderMaterial> rotate_mat_hl = rotate_mat->duplicate();
rotate_mat_hl->set_shader_parameter("albedo", albedo);
rotate_gizmo_color_hl[i] = rotate_mat_hl;
}
// Only create scale gizmo for X, Y, Z axes (not view rotation).
if (i < 3) {
// Scale.
{
Ref<SurfaceTool> surftool;
surftool.instantiate();
surftool->begin(Mesh::PRIMITIVE_TRIANGLES);
// Cube arrow profile.
const int arrow_points = 6;
Vector3 arrow[6] = {
nivec * 0.0 + ivec * 0.0,
nivec * 0.01 + ivec * 0.0,
nivec * 0.01 + ivec * 1.0 * GIZMO_SCALE_OFFSET,
nivec * 0.07 + ivec * 1.0 * GIZMO_SCALE_OFFSET,
nivec * 0.07 + ivec * 1.11 * GIZMO_SCALE_OFFSET,
nivec * 0.0 + ivec * 1.11 * GIZMO_SCALE_OFFSET,
};
int arrow_sides = 4;
const real_t arrow_sides_step = Math::TAU / arrow_sides;
for (int k = 0; k < 4; k++) {
Basis ma(ivec, k * arrow_sides_step);
Basis mb(ivec, (k + 1) * arrow_sides_step);
for (int j = 0; j < arrow_points - 1; j++) {
Vector3 points[4] = {
ma.xform(arrow[j]),
mb.xform(arrow[j]),
mb.xform(arrow[j + 1]),
ma.xform(arrow[j + 1]),
};
surftool->add_vertex(points[0]);
surftool->add_vertex(points[1]);
surftool->add_vertex(points[2]);
surftool->add_vertex(points[0]);
surftool->add_vertex(points[2]);
surftool->add_vertex(points[3]);
}
}
surftool->set_material(mat);
surftool->commit(scale_gizmo[i]);
}
// Plane Scale.
{
Ref<SurfaceTool> surftool;
surftool.instantiate();
surftool->begin(Mesh::PRIMITIVE_TRIANGLES);
Vector3 vec = ivec2 - ivec3;
Vector3 plane[4] = {
vec * GIZMO_PLANE_DST,
vec * GIZMO_PLANE_DST + ivec2 * GIZMO_PLANE_SIZE,
vec * (GIZMO_PLANE_DST + GIZMO_PLANE_SIZE),
vec * GIZMO_PLANE_DST - ivec3 * GIZMO_PLANE_SIZE
};
Basis ma(ivec, Math::PI / 2);
Vector3 points[4] = {
ma.xform(plane[0]),
ma.xform(plane[1]),
ma.xform(plane[2]),
ma.xform(plane[3]),
};
surftool->add_vertex(points[0]);
surftool->add_vertex(points[1]);
surftool->add_vertex(points[2]);
surftool->add_vertex(points[0]);
surftool->add_vertex(points[2]);
surftool->add_vertex(points[3]);
Ref<StandardMaterial3D> plane_mat;
plane_mat.instantiate();
plane_mat->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
plane_mat->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
plane_mat->set_on_top_of_alpha();
plane_mat->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
plane_mat->set_cull_mode(StandardMaterial3D::CULL_DISABLED);
plane_mat->set_albedo(col);
plane_gizmo_color[i] = plane_mat; // needed, so we can draw planes from both sides.
surftool->set_material(plane_mat);
surftool->commit(scale_plane_gizmo[i]);
Ref<StandardMaterial3D> plane_mat_hl = plane_mat->duplicate();
plane_mat_hl->set_albedo(col.from_hsv(col.get_h(), col.get_s() * 0.25, 1.0, 1));
plane_gizmo_color_hl[i] = plane_mat_hl; // needed, so we can draw planes from both sides.
}
// Lines to visualize transforms locked to an axis/plane.
{
Ref<SurfaceTool> surftool;
surftool.instantiate();
surftool->begin(Mesh::PRIMITIVE_LINE_STRIP);
Vector3 vec;
vec[i] = 1;
// Line extending through like infinity.
surftool->add_vertex(vec * -1048576);
surftool->add_vertex(Vector3());
surftool->add_vertex(vec * 1048576);
surftool->set_material(mat_hl);
surftool->commit(axis_gizmo[i]);
}
}
}
}
// Create trackball sphere
{
trackball_sphere_gizmo.instantiate();
Ref<SurfaceTool> surftool;
surftool.instantiate();
surftool->begin(Mesh::PRIMITIVE_TRIANGLES);
const int sphere_rings = TRACKBALL_SPHERE_RINGS;
const int sphere_sectors = TRACKBALL_SPHERE_SECTORS;
const real_t sphere_radius = GIZMO_CIRCLE_SIZE;
for (int r = 0; r <= sphere_rings; ++r) {
for (int s = 0; s <= sphere_sectors; ++s) {
real_t ring_angle = Math::PI * r / sphere_rings;
real_t sector_angle = 2.0 * Math::PI * s / sphere_sectors;
Vector3 vertex(
sphere_radius * Math::sin(ring_angle) * Math::cos(sector_angle),
sphere_radius * Math::cos(ring_angle),
sphere_radius * Math::sin(ring_angle) * Math::sin(sector_angle));
surftool->set_normal(vertex.normalized());
surftool->add_vertex(vertex);
}
}
for (int r = 0; r < sphere_rings; ++r) {
for (int s = 0; s < sphere_sectors; ++s) {
int current = r * (sphere_sectors + 1) + s;
int next = current + sphere_sectors + 1;
surftool->add_index(current);
surftool->add_index(next);
surftool->add_index(current + 1);
surftool->add_index(current + 1);
surftool->add_index(next);
surftool->add_index(next + 1);
}
}
trackball_sphere_material.instantiate();
trackball_sphere_material->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
trackball_sphere_material->set_flag(StandardMaterial3D::FLAG_DISABLE_FOG, true);
trackball_sphere_material->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA);
trackball_sphere_material->set_cull_mode(StandardMaterial3D::CULL_DISABLED);
trackball_sphere_material->set_albedo(Color(1.0, 1.0, 1.0, 0.0));
trackball_sphere_material->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true);
trackball_sphere_material_hl = trackball_sphere_material->duplicate();
trackball_sphere_material_hl->set_albedo(Color(1.0, 1.0, 1.0, TRACKBALL_HIGHLIGHT_ALPHA));
surftool->set_material(trackball_sphere_material);
surftool->commit(trackball_sphere_gizmo);
}
_generate_selection_boxes();
}
void Node3DEditor::_update_gizmos_menu() {
gizmos_menu->clear();
for (int i = 0; i < gizmo_plugins_by_name.size(); ++i) {
if (!gizmo_plugins_by_name[i]->can_be_hidden()) {
continue;
}
String plugin_name = gizmo_plugins_by_name[i]->get_gizmo_name();
const int plugin_state = gizmo_plugins_by_name[i]->get_state();
gizmos_menu->add_multistate_item(plugin_name, 3, plugin_state, i);
const int idx = gizmos_menu->get_item_index(i);
gizmos_menu->set_item_tooltip(
idx,
TTR("Click to toggle between visibility states.\n\nOpen eye: Gizmo is visible.\nClosed eye: Gizmo is hidden.\nHalf-open eye: Gizmo is also visible through opaque surfaces (\"x-ray\")."));
switch (plugin_state) {
case EditorNode3DGizmoPlugin::VISIBLE:
gizmos_menu->set_item_icon(idx, get_editor_theme_icon(SNAME("GuiVisibilityVisible")));
break;
case EditorNode3DGizmoPlugin::ON_TOP:
gizmos_menu->set_item_icon(idx, get_editor_theme_icon(SNAME("GuiVisibilityXray")));
break;
case EditorNode3DGizmoPlugin::HIDDEN:
gizmos_menu->set_item_icon(idx, get_editor_theme_icon(SNAME("GuiVisibilityHidden")));
break;
}
}
}
void Node3DEditor::_update_gizmos_menu_theme() {
for (int i = 0; i < gizmo_plugins_by_name.size(); ++i) {
if (!gizmo_plugins_by_name[i]->can_be_hidden()) {
continue;
}
const int plugin_state = gizmo_plugins_by_name[i]->get_state();
const int idx = gizmos_menu->get_item_index(i);
switch (plugin_state) {
case EditorNode3DGizmoPlugin::VISIBLE:
gizmos_menu->set_item_icon(idx, get_editor_theme_icon(SNAME("GuiVisibilityVisible")));
break;
case EditorNode3DGizmoPlugin::ON_TOP:
gizmos_menu->set_item_icon(idx, get_editor_theme_icon(SNAME("GuiVisibilityXray")));
break;
case EditorNode3DGizmoPlugin::HIDDEN:
gizmos_menu->set_item_icon(idx, get_editor_theme_icon(SNAME("GuiVisibilityHidden")));
break;
}
}
}
void Node3DEditor::_init_grid() {
if (!grid_enabled) {
return;
}
Camera3D *camera = get_editor_viewport(0)->camera;
Vector3 camera_position = camera->get_position();
if (camera_position == Vector3()) {
return; // Camera3D is invalid, don't draw the grid.
}
bool orthogonal = camera->get_projection() == Camera3D::PROJECTION_ORTHOGONAL;
static LocalVector<Color> grid_colors[3];
static LocalVector<Vector3> grid_points[3];
static LocalVector<Vector3> grid_normals[3];
for (uint32_t n = 0; n < 3; n++) {
grid_colors[n].clear();
grid_points[n].clear();
grid_normals[n].clear();
}
Color primary_grid_color = EDITOR_GET("editors/3d/primary_grid_color");
Color secondary_grid_color = EDITOR_GET("editors/3d/secondary_grid_color");
int grid_size = EDITOR_GET("editors/3d/grid_size");
int primary_grid_steps = EDITOR_GET("editors/3d/primary_grid_steps");
// Which grid planes are enabled? Which should we generate?
grid_enable[0] = grid_visible[0] = orthogonal || EDITOR_GET("editors/3d/grid_xy_plane");
grid_enable[1] = grid_visible[1] = orthogonal || EDITOR_GET("editors/3d/grid_yz_plane");
grid_enable[2] = grid_visible[2] = orthogonal || EDITOR_GET("editors/3d/grid_xz_plane");
// Offsets division_level for bigger or smaller grids.
// Default value is -0.2. -1.0 gives Blender-like behavior, 0.5 gives huge grids.
real_t division_level_bias = EDITOR_GET("editors/3d/grid_division_level_bias");
// Default largest grid size is 8^2 (default value is 2) when primary_grid_steps is 8 (64m apart, so primary grid lines are 512m apart).
int division_level_max = EDITOR_GET("editors/3d/grid_division_level_max");
// Default smallest grid size is 8^0 (default value is 0) when primary_grid_steps is 8.
int division_level_min = EDITOR_GET("editors/3d/grid_division_level_min");
ERR_FAIL_COND_MSG(division_level_max < division_level_min, "The 3D grid's maximum division level cannot be lower than its minimum division level.");
if (primary_grid_steps != 10) { // Log10 of 10 is 1.
// Change of base rule, divide by ln(10).
real_t div = Math::log((real_t)primary_grid_steps) / (real_t)2.302585092994045901094;
// Truncation (towards zero) is intentional.
division_level_max = (int)(division_level_max / div);
division_level_min = (int)(division_level_min / div);
}
for (int a = 0; a < 3; a++) {
if (!grid_enable[a]) {
continue; // If this grid plane is disabled, skip generation.
}
int b = (a + 1) % 3;
int c = (a + 2) % 3;
Vector3 normal;
normal[c] = 1.0;
real_t camera_distance = Math::abs(camera_position[c]);
if (orthogonal) {
camera_distance = camera->get_size() / 2.0;
Vector3 camera_direction = -camera->get_global_transform().get_basis().get_column(2);
Plane grid_plane = Plane(normal);
Vector3 intersection;
if (grid_plane.intersects_ray(camera_position, camera_direction, &intersection)) {
camera_position = intersection;
}
}
real_t division_level = Math::log(Math::abs(camera_distance)) / Math::log((double)primary_grid_steps) + division_level_bias;
real_t clamped_division_level = CLAMP(division_level, division_level_min, division_level_max);
real_t division_level_floored = Math::floor(clamped_division_level);
real_t division_level_decimals = clamped_division_level - division_level_floored;
real_t small_step_size = Math::pow(primary_grid_steps, division_level_floored);
real_t large_step_size = small_step_size * primary_grid_steps;
real_t center_a = large_step_size * (int)(camera_position[a] / large_step_size);
real_t center_b = large_step_size * (int)(camera_position[b] / large_step_size);
real_t bgn_a = center_a - grid_size * small_step_size;
real_t end_a = center_a + grid_size * small_step_size;
real_t bgn_b = center_b - grid_size * small_step_size;
real_t end_b = center_b + grid_size * small_step_size;
real_t fade_size = Math::pow(primary_grid_steps, division_level - 1.0);
real_t min_fade_size = Math::pow(primary_grid_steps, float(division_level_min));
real_t max_fade_size = Math::pow(primary_grid_steps, float(division_level_max));
fade_size = CLAMP(fade_size, min_fade_size, max_fade_size);
real_t grid_fade_size = (grid_size - primary_grid_steps) * fade_size;
grid_mat[c]->set_shader_parameter("grid_size", grid_fade_size);
grid_mat[c]->set_shader_parameter("orthogonal", orthogonal);
LocalVector<Vector3> &ref_grid = grid_points[c];
LocalVector<Vector3> &ref_grid_normals = grid_normals[c];
LocalVector<Color> &ref_grid_colors = grid_colors[c];
// Count our elements same as code below it.
int expected_size = 0;
for (int i = -grid_size; i <= grid_size; i++) {
const real_t position_a = center_a + i * small_step_size;
const real_t position_b = center_b + i * small_step_size;
// Don't draw lines over the origin if it's enabled.
if (!(origin_enabled && Math::is_zero_approx(position_a))) {
expected_size += 2;
}
if (!(origin_enabled && Math::is_zero_approx(position_b))) {
expected_size += 2;
}
}
int idx = 0;
ref_grid.resize(expected_size);
ref_grid_normals.resize(expected_size);
ref_grid_colors.resize(expected_size);
// In each iteration of this loop, draw one line in each direction (so two lines per loop, in each if statement).
for (int i = -grid_size; i <= grid_size; i++) {
Color line_color;
// Is this a primary line? Set the appropriate color.
if (i % primary_grid_steps == 0) {
line_color = primary_grid_color.lerp(secondary_grid_color, division_level_decimals);
} else {
line_color = secondary_grid_color;
line_color.a = line_color.a * (1 - division_level_decimals);
}
real_t position_a = center_a + i * small_step_size;
real_t position_b = center_b + i * small_step_size;
// Don't draw lines over the origin if it's enabled.
if (!(origin_enabled && Math::is_zero_approx(position_a))) {
Vector3 line_bgn;
Vector3 line_end;
line_bgn[a] = position_a;
line_end[a] = position_a;
line_bgn[b] = bgn_b;
line_end[b] = end_b;
ref_grid[idx] = line_bgn;
ref_grid[idx + 1] = line_end;
ref_grid_colors[idx] = line_color;
ref_grid_colors[idx + 1] = line_color;
ref_grid_normals[idx] = normal;
ref_grid_normals[idx + 1] = normal;
idx += 2;
}
if (!(origin_enabled && Math::is_zero_approx(position_b))) {
Vector3 line_bgn;
Vector3 line_end;
line_bgn[b] = position_b;
line_end[b] = position_b;
line_bgn[a] = bgn_a;
line_end[a] = end_a;
ref_grid[idx] = line_bgn;
ref_grid[idx + 1] = line_end;
ref_grid_colors[idx] = line_color;
ref_grid_colors[idx + 1] = line_color;
ref_grid_normals[idx] = normal;
ref_grid_normals[idx + 1] = normal;
idx += 2;
}
}
// Create a mesh from the pushed vector points and colors.
grid[c] = RenderingServer::get_singleton()->mesh_create();
Array d;
d.resize(RSE::ARRAY_MAX);
d[RSE::ARRAY_VERTEX] = (Vector<Vector3>)grid_points[c];
d[RSE::ARRAY_COLOR] = (Vector<Color>)grid_colors[c];
d[RSE::ARRAY_NORMAL] = (Vector<Vector3>)grid_normals[c];
RenderingServer::get_singleton()->mesh_add_surface_from_arrays(grid[c], RSE::PRIMITIVE_LINES, d);
RenderingServer::get_singleton()->mesh_surface_set_material(grid[c], 0, grid_mat[c]->get_rid());
grid_instance[c] = RenderingServer::get_singleton()->instance_create2(grid[c], get_tree()->get_root()->get_world_3d()->get_scenario());
// Yes, the end of this line is supposed to be a.
RenderingServer::get_singleton()->instance_set_visible(grid_instance[c], grid_visible[a]);
RenderingServer::get_singleton()->instance_geometry_set_cast_shadows_setting(grid_instance[c], RSE::SHADOW_CASTING_SETTING_OFF);
RS::get_singleton()->instance_set_layer_mask(grid_instance[c], 1 << Node3DEditorViewport::GIZMO_GRID_LAYER);
RS::get_singleton()->instance_geometry_set_flag(grid_instance[c], RSE::INSTANCE_FLAG_IGNORE_OCCLUSION_CULLING, true);
RS::get_singleton()->instance_geometry_set_flag(grid_instance[c], RSE::INSTANCE_FLAG_USE_BAKED_LIGHT, false);
}
}
void Node3DEditor::_finish_indicators() {
RenderingServer::get_singleton()->free_rid(origin_instance);
RenderingServer::get_singleton()->free_rid(origin_multimesh);
RenderingServer::get_singleton()->free_rid(origin_mesh);
_finish_grid();
}
void Node3DEditor::_finish_grid() {
for (int i = 0; i < 3; i++) {
RenderingServer::get_singleton()->free_rid(grid_instance[i]);
RenderingServer::get_singleton()->free_rid(grid[i]);
}
}
void Node3DEditor::update_gizmo_opacity() {
if (!origin_instance.is_valid()) {
return;
}
const float opacity = EDITOR_GET("editors/3d/manipulator_gizmo_opacity");
for (int i = 0; i < 3; i++) {
Color col = gizmo_color[i]->get_albedo();
col.a = opacity;
gizmo_color[i]->set_albedo(col);
col = gizmo_color_hl[i]->get_albedo();
col.a = 1.0;
gizmo_color_hl[i]->set_albedo(col);
col = plane_gizmo_color[i]->get_albedo();
col.a = opacity;
plane_gizmo_color[i]->set_albedo(col);
col = plane_gizmo_color_hl[i]->get_albedo();
col.a = 1.0;
plane_gizmo_color_hl[i]->set_albedo(col);
}
}
void Node3DEditor::update_grid() {
const Camera3D::ProjectionType current_projection = viewports[0]->camera->get_projection();
if (current_projection != grid_camera_last_update_perspective) {
grid_init_draw = false; // redraw
grid_camera_last_update_perspective = current_projection;
}
// Gets a orthogonal or perspective position correctly (for the grid comparison)
const Vector3 camera_position = get_editor_viewport(0)->camera->get_position();
if (!grid_init_draw || grid_camera_last_update_position.distance_squared_to(camera_position) >= 100.0f) {
_finish_grid();
_init_grid();
grid_init_draw = true;
grid_camera_last_update_position = camera_position;
}
}
void Node3DEditor::_selection_changed() {
_refresh_menu_icons();
const HashMap<ObjectID, Object *> &selection = editor_selection->get_selection();
for (const KeyValue<ObjectID, Object *> &E : selection) {
Node3D *sp = ObjectDB::get_instance<Node3D>(E.key);
if (!sp) {
continue;
}
Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(sp);
if (!se) {
continue;
}
if (sp == editor_selection->get_top_selected_node_list().back()->get()) {
RenderingServer::get_singleton()->instance_set_base(se->sbox_instance, active_selection_box->get_rid());
RenderingServer::get_singleton()->instance_set_base(se->sbox_instance_xray, active_selection_box_xray->get_rid());
RenderingServer::get_singleton()->instance_set_base(se->sbox_instance_offset, active_selection_box->get_rid());
RenderingServer::get_singleton()->instance_set_base(se->sbox_instance_xray_offset, active_selection_box_xray->get_rid());
} else {
RenderingServer::get_singleton()->instance_set_base(se->sbox_instance, selection_box->get_rid());
RenderingServer::get_singleton()->instance_set_base(se->sbox_instance_xray, selection_box_xray->get_rid());
RenderingServer::get_singleton()->instance_set_base(se->sbox_instance_offset, selection_box->get_rid());
RenderingServer::get_singleton()->instance_set_base(se->sbox_instance_xray_offset, selection_box_xray->get_rid());
}
}
if (selected && editor_selection->get_top_selected_node_list().size() != 1) {
Vector<Ref<Node3DGizmo>> gizmos = selected->get_gizmos();
for (int i = 0; i < gizmos.size(); i++) {
Ref<EditorNode3DGizmo> seg = gizmos[i];
if (seg.is_null()) {
continue;
}
seg->set_selected(false);
}
Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(selected);
if (se) {
se->gizmo.unref();
se->subgizmos.clear();
}
selected->update_gizmos();
selected = nullptr;
}
// Ensure gizmo updates are performed when the selection changes
// outside of the 3D view (see GH-106713).
if (!is_visible()) {
const List<Node *> &top_selected = editor_selection->get_top_selected_node_list();
if (top_selected.size() == 1) {
Node3D *new_selected = Object::cast_to<Node3D>(top_selected.back()->get());
if (new_selected != selected) {
gizmos_dirty = true;
}
}
}
update_transform_gizmo();
}
void Node3DEditor::refresh_dirty_gizmos() {
if (!gizmos_dirty) {
return;
}
const List<Node *> &top_selected = editor_selection->get_top_selected_node_list();
if (top_selected.size() == 1) {
Node3D *new_selected = Object::cast_to<Node3D>(top_selected.back()->get());
if (new_selected != selected) {
edit(new_selected);
}
}
gizmos_dirty = false;
}
void Node3DEditor::_refresh_menu_icons() {
bool all_locked = true;
bool all_grouped = true;
bool has_node3d_item = false;
const List<Node *> &selection = editor_selection->get_top_selected_node_list();
if (selection.is_empty()) {
all_locked = false;
all_grouped = false;
} else {
for (Node *E : selection) {
Node3D *node = Object::cast_to<Node3D>(E);
if (node) {
if (all_locked && !node->has_meta("_edit_lock_")) {
all_locked = false;
}
if (all_grouped && !node->has_meta("_edit_group_")) {
all_grouped = false;
}
has_node3d_item = true;
}
if (!all_locked && !all_grouped) {
break;
}
}
}
all_locked = all_locked && has_node3d_item;
all_grouped = all_grouped && has_node3d_item;
tool_button[TOOL_LOCK_SELECTED]->set_visible(!all_locked);
tool_button[TOOL_LOCK_SELECTED]->set_disabled(!has_node3d_item);
tool_button[TOOL_UNLOCK_SELECTED]->set_visible(all_locked);
tool_button[TOOL_UNLOCK_SELECTED]->set_disabled(!has_node3d_item);
tool_button[TOOL_GROUP_SELECTED]->set_visible(!all_grouped);
tool_button[TOOL_GROUP_SELECTED]->set_disabled(!has_node3d_item);
tool_button[TOOL_UNGROUP_SELECTED]->set_visible(all_grouped);
tool_button[TOOL_UNGROUP_SELECTED]->set_disabled(!has_node3d_item);
}
template <typename T>
HashSet<T *> _get_child_nodes(Node *parent_node) {
HashSet<T *> nodes = HashSet<T *>();
T *node = Node::cast_to<T>(parent_node);
if (node) {
nodes.insert(node);
}
for (int i = 0; i < parent_node->get_child_count(); i++) {
Node *child_node = parent_node->get_child(i);
HashSet<T *> child_nodes = _get_child_nodes<T>(child_node);
for (T *I : child_nodes) {
nodes.insert(I);
}
}
return nodes;
}
HashSet<RID> _get_physics_bodies_rid(Node *node) {
HashSet<RID> rids = HashSet<RID>();
PhysicsBody3D *pb = Node::cast_to<PhysicsBody3D>(node);
if (pb) {
rids.insert(pb->get_rid());
}
HashSet<PhysicsBody3D *> child_nodes = _get_child_nodes<PhysicsBody3D>(node);
for (const PhysicsBody3D *I : child_nodes) {
rids.insert(I->get_rid());
}
return rids;
}
void Node3DEditor::snap_selected_nodes_to_floor() {
do_snap_selected_nodes_to_floor = true;
}
void Node3DEditor::_snap_selected_nodes_to_floor() {
const List<Node *> &selection = editor_selection->get_top_selected_node_list();
Dictionary snap_data;
for (Node *E : selection) {
Node3D *sp = Object::cast_to<Node3D>(E);
if (sp) {
Vector3 from;
Vector3 position_offset;
// Priorities for snapping to floor are CollisionShapes, VisualInstances and then origin
HashSet<VisualInstance3D *> vi = _get_child_nodes<VisualInstance3D>(sp);
HashSet<CollisionShape3D *> cs = _get_child_nodes<CollisionShape3D>(sp);
bool found_valid_shape = false;
if (cs.size()) {
AABB aabb;
HashSet<CollisionShape3D *>::Iterator I = cs.begin();
if ((*I)->get_shape().is_valid()) {
CollisionShape3D *collision_shape = *cs.begin();
aabb = collision_shape->get_global_transform().xform(collision_shape->get_shape()->get_debug_mesh()->get_aabb());
found_valid_shape = true;
}
for (++I; I; ++I) {
CollisionShape3D *col_shape = *I;
if (col_shape->get_shape().is_valid()) {
aabb.merge_with(col_shape->get_global_transform().xform(col_shape->get_shape()->get_debug_mesh()->get_aabb()));
found_valid_shape = true;
}
}
if (found_valid_shape) {
Vector3 size = aabb.size * Vector3(0.5, 0.0, 0.5);
from = aabb.position + size;
position_offset.y = from.y - sp->get_global_transform().origin.y;
}
}
if (!found_valid_shape && vi.size()) {
VisualInstance3D *begin = *vi.begin();
AABB aabb = begin->get_global_transform().xform(begin->get_aabb());
for (const VisualInstance3D *I : vi) {
aabb.merge_with(I->get_global_transform().xform(I->get_aabb()));
}
Vector3 size = aabb.size * Vector3(0.5, 0.0, 0.5);
from = aabb.position + size;
position_offset.y = from.y - sp->get_global_transform().origin.y;
} else if (!found_valid_shape) {
from = sp->get_global_transform().origin;
}
// We add a bit of margin to the from position to avoid it from snapping
// when the spatial is already on a floor and there's another floor under
// it
from = from + Vector3(0.0, 1, 0.0);
Dictionary d;
d["from"] = from;
d["position_offset"] = position_offset;
snap_data[sp] = d;
}
}
PhysicsDirectSpaceState3D *ss = get_tree()->get_root()->get_world_3d()->get_direct_space_state();
PhysicsDirectSpaceState3D::RayResult result;
// The maximum height an object can travel to be snapped
const float max_snap_height = 500.0;
// Will be set to `true` if at least one node from the selection was successfully snapped
bool snapped_to_floor = false;
if (!snap_data.is_empty()) {
// For snapping to be performed, there must be solid geometry under at least one of the selected nodes.
// We need to check this before snapping to register the undo/redo action only if needed.
for (const KeyValue<Variant, Variant> &kv : snap_data) {
Node *node = Object::cast_to<Node>(kv.key);
Node3D *sp = Object::cast_to<Node3D>(node);
Dictionary d = kv.value;
Vector3 from = d["from"];
Vector3 to = from - Vector3(0.0, max_snap_height, 0.0);
HashSet<RID> excluded = _get_physics_bodies_rid(sp);
PhysicsDirectSpaceState3D::RayParameters ray_params;
ray_params.from = from;
ray_params.to = to;
ray_params.exclude = excluded;
if (ss->intersect_ray(ray_params, result)) {
snapped_to_floor = true;
}
}
if (snapped_to_floor) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Snap Nodes to Floor"));
// Perform snapping if at least one node can be snapped
for (const KeyValue<Variant, Variant> &kv : snap_data) {
Node *node = Object::cast_to<Node>(kv.key);
Node3D *sp = Object::cast_to<Node3D>(node);
Dictionary d = kv.value;
Vector3 from = d["from"];
Vector3 to = from - Vector3(0.0, max_snap_height, 0.0);
HashSet<RID> excluded = _get_physics_bodies_rid(sp);
PhysicsDirectSpaceState3D::RayParameters ray_params;
ray_params.from = from;
ray_params.to = to;
ray_params.exclude = excluded;
if (ss->intersect_ray(ray_params, result)) {
Vector3 position_offset = d["position_offset"];
Transform3D new_transform = sp->get_global_transform();
new_transform.origin.y = result.position.y;
new_transform.origin = new_transform.origin - position_offset;
Node3D *parent = sp->get_parent_node_3d();
Transform3D new_local_xform = parent ? parent->get_global_transform().affine_inverse() * new_transform : new_transform;
undo_redo->add_do_method(sp, "set_transform", new_local_xform);
undo_redo->add_undo_method(sp, "set_transform", sp->get_transform());
}
}
undo_redo->commit_action();
} else {
EditorNode::get_singleton()->show_warning(TTR("Couldn't find a solid floor to snap the selection to."));
}
}
}
void Node3DEditor::shortcut_input(const Ref<InputEvent> &p_event) {
ERR_FAIL_COND(p_event.is_null());
if (!is_visible_in_tree()) {
return;
}
snap_key_enabled = Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL);
}
void Node3DEditor::_sun_environ_settings_pressed() {
Vector2 pos = sun_environ_settings->get_screen_position() + sun_environ_settings->get_size();
sun_environ_popup->set_position(pos - Vector2(sun_environ_popup->get_contents_minimum_size().width / 2, 0));
sun_environ_popup->reset_size();
sun_environ_popup->popup();
// Grabbing the focus is required for Shift modifier checking to be functional
// (when the Add sun/environment buttons are pressed).
sun_environ_popup->grab_focus();
}
void Node3DEditor::_add_sun_to_scene(bool p_already_added_environment) {
sun_environ_popup->hide();
if (!p_already_added_environment && world_env_count == 0 && Input::get_singleton()->is_key_pressed(Key::SHIFT)) {
// Prevent infinite feedback loop between the sun and environment methods.
_add_environment_to_scene(true);
}
Node *base = get_tree()->get_edited_scene_root();
if (!base) {
// Create a root node so we can add child nodes to it.
SceneTreeDock::get_singleton()->add_root_node(memnew(Node3D));
base = get_tree()->get_edited_scene_root();
}
ERR_FAIL_NULL(base);
Node *new_sun = preview_sun->duplicate();
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Add Preview Sun to Scene"));
undo_redo->add_do_method(base, "add_child", new_sun, true);
// Move to the beginning of the scene tree since more "global" nodes
// generally look better when placed at the top.
undo_redo->add_do_method(base, "move_child", new_sun, 0);
undo_redo->add_do_method(new_sun, "set_owner", base);
undo_redo->add_undo_method(base, "remove_child", new_sun);
undo_redo->add_do_reference(new_sun);
undo_redo->commit_action();
}
void Node3DEditor::_add_environment_to_scene(bool p_already_added_sun) {
sun_environ_popup->hide();
if (!p_already_added_sun && directional_light_count == 0 && Input::get_singleton()->is_key_pressed(Key::SHIFT)) {
// Prevent infinite feedback loop between the sun and environment methods.
_add_sun_to_scene(true);
}
Node *base = get_tree()->get_edited_scene_root();
if (!base) {
// Create a root node so we can add child nodes to it.
SceneTreeDock::get_singleton()->add_root_node(memnew(Node3D));
base = get_tree()->get_edited_scene_root();
}
ERR_FAIL_NULL(base);
WorldEnvironment *new_env = memnew(WorldEnvironment);
new_env->set_environment(preview_environment->get_environment()->duplicate(true));
if (GLOBAL_GET("rendering/lights_and_shadows/use_physical_light_units")) {
new_env->set_camera_attributes(preview_environment->get_camera_attributes()->duplicate(true));
}
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Add Preview Environment to Scene"));
undo_redo->add_do_method(base, "add_child", new_env, true);
// Move to the beginning of the scene tree since more "global" nodes
// generally look better when placed at the top.
undo_redo->add_do_method(base, "move_child", new_env, 0);
undo_redo->add_do_method(new_env, "set_owner", base);
undo_redo->add_undo_method(base, "remove_child", new_env);
undo_redo->add_do_reference(new_env);
undo_redo->commit_action();
}
void Node3DEditor::_update_theme() {
tool_button[TOOL_MODE_TRANSFORM]->set_button_icon(get_editor_theme_icon(SNAME("ToolTransform")));
tool_button[TOOL_MODE_MOVE]->set_button_icon(get_editor_theme_icon(SNAME("ToolMove")));
tool_button[TOOL_MODE_ROTATE]->set_button_icon(get_editor_theme_icon(SNAME("ToolRotate")));
tool_button[TOOL_MODE_SCALE]->set_button_icon(get_editor_theme_icon(SNAME("ToolScale")));
tool_button[TOOL_MODE_SELECT]->set_button_icon(get_editor_theme_icon(SNAME("ToolSelect")));
tool_button[TOOL_MODE_LIST_SELECT]->set_button_icon(get_editor_theme_icon(SNAME("ListSelect")));
tool_button[TOOL_LOCK_SELECTED]->set_button_icon(get_editor_theme_icon(SNAME("Lock")));
tool_button[TOOL_UNLOCK_SELECTED]->set_button_icon(get_editor_theme_icon(SNAME("Unlock")));
tool_button[TOOL_GROUP_SELECTED]->set_button_icon(get_editor_theme_icon(SNAME("Group")));
tool_button[TOOL_UNGROUP_SELECTED]->set_button_icon(get_editor_theme_icon(SNAME("Ungroup")));
tool_button[TOOL_RULER]->set_button_icon(get_editor_theme_icon(SNAME("Ruler")));
tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_button_icon(get_editor_theme_icon(SNAME("Object")));
tool_option_button[TOOL_OPT_USE_SNAP]->set_button_icon(get_editor_theme_icon(SNAME("Snap")));
tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_button_icon(get_editor_theme_icon(SNAME("Trackball")));
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_button_icon(get_editor_theme_icon(SNAME("Pin")));
view_layout_menu->get_popup()->set_item_icon(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), get_editor_theme_icon(SNAME("Panels1")));
view_layout_menu->get_popup()->set_item_icon(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), get_editor_theme_icon(SNAME("Panels2")));
view_layout_menu->get_popup()->set_item_icon(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS_ALT), get_editor_theme_icon(SNAME("Panels2Alt")));
view_layout_menu->get_popup()->set_item_icon(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS), get_editor_theme_icon(SNAME("Panels3")));
view_layout_menu->get_popup()->set_item_icon(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS_ALT), get_editor_theme_icon(SNAME("Panels3Alt")));
view_layout_menu->get_popup()->set_item_icon(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_4_VIEWPORTS), get_editor_theme_icon(SNAME("Panels4")));
sun_button->set_button_icon(get_editor_theme_icon(SNAME("PreviewSun")));
environ_button->set_button_icon(get_editor_theme_icon(SNAME("PreviewEnvironment")));
sun_environ_settings->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));
sun_title->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("title_font"), SNAME("Window")));
environ_title->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("title_font"), SNAME("Window")));
sun_color->set_custom_minimum_size(Size2(0, get_theme_constant(SNAME("inspector_property_height"), EditorStringName(Editor))));
environ_sky_color->set_custom_minimum_size(Size2(0, get_theme_constant(SNAME("inspector_property_height"), EditorStringName(Editor))));
environ_ground_color->set_custom_minimum_size(Size2(0, get_theme_constant(SNAME("inspector_property_height"), EditorStringName(Editor))));
context_toolbar_panel->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("ContextualToolbar"), EditorStringName(EditorStyles)));
}
void Node3DEditor::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_TRANSLATION_CHANGED: {
const String show_list_tooltip = TTR("Alt+RMB: Show list of all nodes at position clicked, including locked.");
tool_button[TOOL_MODE_TRANSFORM]->set_tooltip_text(vformat(TTR("%s+Drag: Rotate selected node around pivot."), keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL)) + "\n" + show_list_tooltip);
tool_button[TOOL_MODE_MOVE]->set_tooltip_text(vformat(TTR("%s+Drag: Use snap."), keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL)) + "\n" + show_list_tooltip);
tool_button[TOOL_MODE_ROTATE]->set_tooltip_text(vformat(TTR("%s+Drag: Use snap."), keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL)) + "\n" + show_list_tooltip);
tool_button[TOOL_MODE_SCALE]->set_tooltip_text(vformat(TTR("%s+Drag: Use snap."), keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL)) + "\n" + show_list_tooltip);
tool_button[TOOL_MODE_SELECT]->set_tooltip_text(show_list_tooltip);
tool_button[TOOL_MODE_LIST_SELECT]->set_tooltip_text(TTR("Show list of selectable nodes at position clicked.") + "\n" + show_list_tooltip);
tool_button[TOOL_RULER]->set_tooltip_text(TTR("LMB+Drag: Measure the distance between two points in 3D space.") + "\n" + show_list_tooltip);
_update_gizmos_menu();
_update_vertex_snap_tooltips();
} break;
case NOTIFICATION_READY: {
_menu_item_pressed(MENU_VIEW_USE_1_VIEWPORT);
_refresh_menu_icons();
get_tree()->connect("node_removed", callable_mp(this, &Node3DEditor::_node_removed));
get_tree()->connect("node_added", callable_mp(this, &Node3DEditor::_node_added));
SceneTreeDock::get_singleton()->get_tree_editor()->connect("node_changed", callable_mp(this, &Node3DEditor::_refresh_menu_icons));
editor_selection->connect("selection_changed", callable_mp(this, &Node3DEditor::_selection_changed));
_update_preview_environment();
sun_state->set_custom_minimum_size(sun_vb->get_combined_minimum_size());
environ_state->set_custom_minimum_size(environ_vb->get_combined_minimum_size());
ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &Node3DEditor::update_all_gizmos).bind(Variant()));
} break;
case NOTIFICATION_ENTER_TREE: {
_update_theme();
_register_all_gizmos();
_init_indicators();
update_all_gizmos();
} break;
case NOTIFICATION_EXIT_TREE: {
_finish_indicators();
} break;
case NOTIFICATION_THEME_CHANGED: {
_update_theme();
_update_gizmos_menu_theme();
sun_title->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("title_font"), SNAME("Window")));
environ_title->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("title_font"), SNAME("Window")));
} break;
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
if (EditorSettings::get_singleton()->check_changed_settings_in_group("editors/3d")) {
const Color selection_box_color = EDITOR_GET("editors/3d/selection_box_color");
const Color active_selection_box_color = EDITOR_GET("editors/3d/active_selection_box_color");
if (selection_box_color != selection_box_mat->get_albedo()) {
selection_box_mat->set_albedo(selection_box_color);
selection_box_mat_xray->set_albedo(selection_box_color * Color(1, 1, 1, 0.15));
}
if (active_selection_box_color != active_selection_box_mat->get_albedo()) {
active_selection_box_mat->set_albedo(active_selection_box_color);
active_selection_box_mat_xray->set_albedo(active_selection_box_color * Color(1, 1, 1, 0.15));
}
gizmo_view_rotation_scale = GIZMO_CIRCLE_SIZE * (float)EDITOR_GET("editors/3d/view_plane_rotation_gizmo_scale");
// Update grid color by rebuilding grid.
_finish_grid();
_init_grid();
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
viewports[i]->update_transform_gizmo_view();
}
update_gizmo_opacity();
}
if (EditorSettings::get_singleton()->check_changed_settings_in_group("editors/3d_gizmos/gizmo_settings")) {
CollisionShape3DGizmoPlugin::set_show_only_when_selected(EDITOR_GET("editors/3d_gizmos/gizmo_settings/show_collision_shapes_only_when_selected"));
update_all_gizmos();
}
_update_vertex_snap_tooltips();
} break;
case NOTIFICATION_PHYSICS_PROCESS: {
if (do_snap_selected_nodes_to_floor) {
_snap_selected_nodes_to_floor();
do_snap_selected_nodes_to_floor = false;
}
}
}
}
bool Node3DEditor::is_subgizmo_selected(int p_id) {
Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(selected) : nullptr;
if (se) {
return se->subgizmos.has(p_id);
}
return false;
}
bool Node3DEditor::is_current_selected_gizmo(const EditorNode3DGizmo *p_gizmo) {
Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(selected) : nullptr;
if (se) {
return se->gizmo == p_gizmo;
}
return false;
}
Vector<int> Node3DEditor::get_subgizmo_selection() {
Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(selected) : nullptr;
Vector<int> ret;
if (se) {
for (const KeyValue<int, Transform3D> &E : se->subgizmos) {
ret.push_back(E.key);
}
}
return ret;
}
void Node3DEditor::clear_subgizmo_selection(Object *p_obj) {
_clear_subgizmo_selection(p_obj);
}
void Node3DEditor::add_control_to_menu_panel(Control *p_control) {
ERR_FAIL_NULL(p_control);
ERR_FAIL_COND(p_control->get_parent());
VSeparator *sep = memnew(VSeparator);
context_toolbar_hbox->add_child(sep);
context_toolbar_hbox->add_child(p_control);
context_toolbar_separators[p_control] = sep;
p_control->connect(SceneStringName(visibility_changed), callable_mp(this, &Node3DEditor::_update_context_toolbar));
_update_context_toolbar();
}
void Node3DEditor::remove_control_from_menu_panel(Control *p_control) {
ERR_FAIL_NULL(p_control);
ERR_FAIL_COND(p_control->get_parent() != context_toolbar_hbox);
p_control->disconnect(SceneStringName(visibility_changed), callable_mp(this, &Node3DEditor::_update_context_toolbar));
VSeparator *sep = context_toolbar_separators[p_control];
context_toolbar_hbox->remove_child(sep);
context_toolbar_hbox->remove_child(p_control);
context_toolbar_separators.erase(p_control);
memdelete(sep);
_update_context_toolbar();
}
void Node3DEditor::_update_context_toolbar() {
bool has_visible = false;
bool first_visible = false;
for (int i = 0; i < context_toolbar_hbox->get_child_count(); i++) {
Control *child = Object::cast_to<Control>(context_toolbar_hbox->get_child(i));
if (!child || !context_toolbar_separators.has(child)) {
continue;
}
if (child->is_visible()) {
first_visible = !has_visible;
has_visible = true;
}
VSeparator *sep = context_toolbar_separators[child];
sep->set_visible(!first_visible && child->is_visible());
}
context_toolbar_panel->set_visible(has_visible);
}
void Node3DEditor::set_can_preview(Camera3D *p_preview) {
for (int i = 0; i < 4; i++) {
viewports[i]->set_can_preview(p_preview);
}
viewports[last_used_viewport]->switch_preview_camera(p_preview);
}
VSplitContainer *Node3DEditor::get_shader_split() {
return shader_split;
}
Node3DEditorViewport *Node3DEditor::get_last_used_viewport() {
return viewports[last_used_viewport];
}
void Node3DEditor::set_freelook_viewport(Node3DEditorViewport *p_viewport) {
freelook_viewport = p_viewport;
}
Node3DEditorViewport *Node3DEditor::get_freelook_viewport() const {
return freelook_viewport;
}
void Node3DEditor::add_control_to_left_panel(Control *p_control) {
left_panel_split->add_child(p_control);
left_panel_split->move_child(p_control, 0);
}
void Node3DEditor::add_control_to_right_panel(Control *p_control) {
right_panel_split->add_child(p_control);
right_panel_split->move_child(p_control, 1);
}
void Node3DEditor::remove_control_from_left_panel(Control *p_control) {
left_panel_split->remove_child(p_control);
}
void Node3DEditor::remove_control_from_right_panel(Control *p_control) {
right_panel_split->remove_child(p_control);
}
void Node3DEditor::move_control_to_left_panel(Control *p_control) {
ERR_FAIL_NULL(p_control);
if (p_control->get_parent() == left_panel_split) {
return;
}
ERR_FAIL_COND(p_control->get_parent() != right_panel_split);
right_panel_split->remove_child(p_control);
add_control_to_left_panel(p_control);
}
void Node3DEditor::move_control_to_right_panel(Control *p_control) {
ERR_FAIL_NULL(p_control);
if (p_control->get_parent() == right_panel_split) {
return;
}
ERR_FAIL_COND(p_control->get_parent() != left_panel_split);
left_panel_split->remove_child(p_control);
add_control_to_right_panel(p_control);
}
void Node3DEditor::_request_gizmo(Object *p_obj) {
Node3D *sp = Object::cast_to<Node3D>(p_obj);
if (!sp) {
return;
}
bool is_selected = (sp == selected);
Node *edited_scene = EditorNode::get_singleton()->get_edited_scene();
if (edited_scene && (sp == edited_scene || (sp->get_owner() && edited_scene->is_ancestor_of(sp)))) {
for (int i = 0; i < gizmo_plugins_by_priority.size(); ++i) {
Ref<EditorNode3DGizmo> seg = gizmo_plugins_by_priority.write[i]->get_gizmo(sp);
if (seg.is_valid()) {
sp->add_gizmo(seg);
if (is_selected != seg->is_selected()) {
seg->set_selected(is_selected);
}
}
}
if (!sp->get_gizmos().is_empty()) {
sp->update_gizmos();
}
}
}
void Node3DEditor::_request_gizmo_for_id(ObjectID p_id) {
Node3D *node = ObjectDB::get_instance<Node3D>(p_id);
if (node) {
_request_gizmo(node);
}
}
void Node3DEditor::_set_subgizmo_selection(Object *p_obj, Ref<Node3DGizmo> p_gizmo, int p_id, Transform3D p_transform) {
if (p_id == -1) {
_clear_subgizmo_selection(p_obj);
return;
}
Node3D *sp = nullptr;
if (p_obj) {
sp = Object::cast_to<Node3D>(p_obj);
} else {
sp = selected;
}
if (!sp) {
return;
}
Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(sp);
if (se) {
se->subgizmos.clear();
se->subgizmos.insert(p_id, p_transform);
se->gizmo = p_gizmo;
sp->update_gizmos();
update_transform_gizmo();
}
}
void Node3DEditor::_clear_subgizmo_selection(Object *p_obj) {
Node3D *sp = nullptr;
if (p_obj) {
sp = Object::cast_to<Node3D>(p_obj);
} else {
sp = selected;
}
if (!sp) {
return;
}
Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(sp);
if (se) {
se->subgizmos.clear();
se->gizmo.unref();
sp->update_gizmos();
update_transform_gizmo();
}
}
void Node3DEditor::_toggle_maximize_view(Object *p_viewport) {
if (!p_viewport) {
return;
}
Node3DEditorViewport *current_viewport = Object::cast_to<Node3DEditorViewport>(p_viewport);
if (!current_viewport) {
return;
}
int index = -1;
bool maximized = false;
for (int i = 0; i < 4; i++) {
if (viewports[i] == current_viewport) {
index = i;
if (current_viewport->get_global_rect() == viewport_base->get_global_rect()) {
maximized = true;
}
break;
}
}
if (index == -1) {
return;
}
if (!maximized) {
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
if (i == (uint32_t)index) {
viewports[i]->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
} else {
viewports[i]->hide();
}
}
} else {
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
viewports[i]->show();
}
if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT))) {
_menu_item_pressed(MENU_VIEW_USE_1_VIEWPORT);
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS))) {
_menu_item_pressed(MENU_VIEW_USE_2_VIEWPORTS);
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS_ALT))) {
_menu_item_pressed(MENU_VIEW_USE_2_VIEWPORTS_ALT);
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS))) {
_menu_item_pressed(MENU_VIEW_USE_3_VIEWPORTS);
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_3_VIEWPORTS_ALT))) {
_menu_item_pressed(MENU_VIEW_USE_3_VIEWPORTS_ALT);
} else if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_4_VIEWPORTS))) {
_menu_item_pressed(MENU_VIEW_USE_4_VIEWPORTS);
}
}
}
void Node3DEditor::_viewport_clicked(int p_viewport_idx) {
last_used_viewport = p_viewport_idx;
}
void Node3DEditor::_node_added(Node *p_node) {
if (EditorNode::get_singleton()->get_scene_root()->is_ancestor_of(p_node)) {
if (Object::cast_to<WorldEnvironment>(p_node)) {
world_env_count++;
if (world_env_count == 1) {
_update_preview_environment();
}
} else if (Object::cast_to<DirectionalLight3D>(p_node)) {
directional_light_count++;
if (directional_light_count == 1) {
_update_preview_environment();
}
}
}
}
void Node3DEditor::_node_removed(Node *p_node) {
if (EditorNode::get_singleton()->get_scene_root()->is_ancestor_of(p_node)) {
if (Object::cast_to<WorldEnvironment>(p_node)) {
world_env_count--;
if (world_env_count == 0) {
_update_preview_environment();
}
} else if (Object::cast_to<DirectionalLight3D>(p_node)) {
directional_light_count--;
if (directional_light_count == 0) {
_update_preview_environment();
}
}
}
if (p_node == selected) {
Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(selected);
if (se) {
se->gizmo.unref();
se->subgizmos.clear();
}
selected = nullptr;
update_transform_gizmo();
}
}
void Node3DEditor::_register_all_gizmos() {
add_gizmo_plugin(Ref<Camera3DGizmoPlugin>(memnew(Camera3DGizmoPlugin)));
add_gizmo_plugin(Ref<Light3DGizmoPlugin>(memnew(Light3DGizmoPlugin)));
add_gizmo_plugin(Ref<AudioStreamPlayer3DGizmoPlugin>(memnew(AudioStreamPlayer3DGizmoPlugin)));
add_gizmo_plugin(Ref<AudioListener3DGizmoPlugin>(memnew(AudioListener3DGizmoPlugin)));
add_gizmo_plugin(Ref<MeshInstance3DGizmoPlugin>(memnew(MeshInstance3DGizmoPlugin)));
add_gizmo_plugin(Ref<OccluderInstance3DGizmoPlugin>(memnew(OccluderInstance3DGizmoPlugin)));
add_gizmo_plugin(Ref<SpriteBase3DGizmoPlugin>(memnew(SpriteBase3DGizmoPlugin)));
add_gizmo_plugin(Ref<Label3DGizmoPlugin>(memnew(Label3DGizmoPlugin)));
add_gizmo_plugin(Ref<GeometryInstance3DGizmoPlugin>(memnew(GeometryInstance3DGizmoPlugin)));
add_gizmo_plugin(Ref<Marker3DGizmoPlugin>(memnew(Marker3DGizmoPlugin)));
add_gizmo_plugin(Ref<SpringBoneCollision3DGizmoPlugin>(memnew(SpringBoneCollision3DGizmoPlugin)));
add_gizmo_plugin(Ref<SpringBoneSimulator3DGizmoPlugin>(memnew(SpringBoneSimulator3DGizmoPlugin)));
add_gizmo_plugin(Ref<VisibleOnScreenNotifier3DGizmoPlugin>(memnew(VisibleOnScreenNotifier3DGizmoPlugin)));
add_gizmo_plugin(Ref<GPUParticles3DGizmoPlugin>(memnew(GPUParticles3DGizmoPlugin)));
add_gizmo_plugin(Ref<GPUParticlesCollision3DGizmoPlugin>(memnew(GPUParticlesCollision3DGizmoPlugin)));
add_gizmo_plugin(Ref<Particles3DEmissionShapeGizmoPlugin>(memnew(Particles3DEmissionShapeGizmoPlugin)));
add_gizmo_plugin(Ref<CPUParticles3DGizmoPlugin>(memnew(CPUParticles3DGizmoPlugin)));
add_gizmo_plugin(Ref<ReflectionProbeGizmoPlugin>(memnew(ReflectionProbeGizmoPlugin)));
add_gizmo_plugin(Ref<DecalGizmoPlugin>(memnew(DecalGizmoPlugin)));
add_gizmo_plugin(Ref<VoxelGIGizmoPlugin>(memnew(VoxelGIGizmoPlugin)));
add_gizmo_plugin(Ref<LightmapGIGizmoPlugin>(memnew(LightmapGIGizmoPlugin)));
add_gizmo_plugin(Ref<LightmapProbeGizmoPlugin>(memnew(LightmapProbeGizmoPlugin)));
add_gizmo_plugin(Ref<FogVolumeGizmoPlugin>(memnew(FogVolumeGizmoPlugin)));
add_gizmo_plugin(Ref<TwoBoneIK3DGizmoPlugin>(memnew(TwoBoneIK3DGizmoPlugin)));
add_gizmo_plugin(Ref<ChainIK3DGizmoPlugin>(memnew(ChainIK3DGizmoPlugin)));
// Physics gizmo plugins.
add_gizmo_plugin(Ref<CollisionObject3DGizmoPlugin>(memnew(CollisionObject3DGizmoPlugin)));
add_gizmo_plugin(Ref<CollisionShape3DGizmoPlugin>(memnew(CollisionShape3DGizmoPlugin)));
add_gizmo_plugin(Ref<CollisionPolygon3DGizmoPlugin>(memnew(CollisionPolygon3DGizmoPlugin)));
add_gizmo_plugin(Ref<Joint3DGizmoPlugin>(memnew(Joint3DGizmoPlugin)));
add_gizmo_plugin(Ref<SoftBody3DGizmoPlugin>(memnew(SoftBody3DGizmoPlugin)));
add_gizmo_plugin(Ref<ShapeCast3DGizmoPlugin>(memnew(ShapeCast3DGizmoPlugin)));
add_gizmo_plugin(Ref<SpringArm3DGizmoPlugin>(memnew(SpringArm3DGizmoPlugin)));
add_gizmo_plugin(Ref<PhysicalBone3DGizmoPlugin>(memnew(PhysicalBone3DGizmoPlugin)));
add_gizmo_plugin(Ref<VehicleWheel3DGizmoPlugin>(memnew(VehicleWheel3DGizmoPlugin)));
add_gizmo_plugin(Ref<RayCast3DGizmoPlugin>(memnew(RayCast3DGizmoPlugin)));
}
void Node3DEditor::_bind_methods() {
ClassDB::bind_method("_get_editor_data", &Node3DEditor::_get_editor_data);
ClassDB::bind_method("_request_gizmo", &Node3DEditor::_request_gizmo);
ClassDB::bind_method("_request_gizmo_for_id", &Node3DEditor::_request_gizmo_for_id);
ClassDB::bind_method("_set_subgizmo_selection", &Node3DEditor::_set_subgizmo_selection);
ClassDB::bind_method("_clear_subgizmo_selection", &Node3DEditor::_clear_subgizmo_selection);
ClassDB::bind_method("_refresh_menu_icons", &Node3DEditor::_refresh_menu_icons);
ClassDB::bind_method("_preview_settings_changed", &Node3DEditor::_preview_settings_changed);
ClassDB::bind_method("update_all_gizmos", &Node3DEditor::update_all_gizmos);
ClassDB::bind_method("update_transform_gizmo", &Node3DEditor::update_transform_gizmo);
ADD_SIGNAL(MethodInfo("transform_3d_key_request"));
ADD_SIGNAL(MethodInfo("item_lock_status_changed"));
ADD_SIGNAL(MethodInfo("item_group_status_changed"));
}
void Node3DEditor::clear() {
settings_fov->set_value(EDITOR_GET("editors/3d/default_fov"));
settings_znear->set_value(EDITOR_GET("editors/3d/default_z_near"));
settings_zfar->set_value(EDITOR_GET("editors/3d/default_z_far"));
snap_translate_value = EditorSettings::get_singleton()->get_project_metadata("3d_editor", "snap_translate_value", 1);
snap_rotate_value = EditorSettings::get_singleton()->get_project_metadata("3d_editor", "snap_rotate_value", 15);
snap_scale_value = EditorSettings::get_singleton()->get_project_metadata("3d_editor", "snap_scale_value", 10);
_snap_update();
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
viewports[i]->reset();
}
if (origin_instance.is_valid()) {
RenderingServer::get_singleton()->instance_set_visible(origin_instance, true);
}
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_ORIGIN), true);
for (int i = 0; i < 3; ++i) {
if (grid_enable[i]) {
grid_visible[i] = true;
}
}
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
viewports[i]->view_display_menu->get_popup()->set_item_checked(viewports[i]->view_display_menu->get_popup()->get_item_index(Node3DEditorViewport::VIEW_AUDIO_LISTENER), i == 0);
viewports[i]->viewport->set_as_audio_listener_3d(i == 0);
}
view_layout_menu->get_popup()->set_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_GRID), true);
grid_enabled = true;
grid_init_draw = false;
}
void Node3DEditor::_sun_direction_draw() {
sun_direction->draw_rect(Rect2(Vector2(), sun_direction->get_size()), Color(1, 1, 1, 1));
Vector3 z_axis = preview_sun->get_transform().basis.get_column(Vector3::AXIS_Z);
z_axis = get_editor_viewport(0)->camera->get_camera_transform().basis.xform_inv(z_axis);
sun_direction_material->set_shader_parameter("sun_direction", Vector3(z_axis.x, -z_axis.y, z_axis.z));
Color color = sun_color->get_pick_color() * sun_energy->get_value();
sun_direction_material->set_shader_parameter("sun_color", Vector3(color.r, color.g, color.b));
}
void Node3DEditor::_preview_settings_changed() {
if (sun_environ_updating) {
return;
}
{ // preview sun
sun_rotation.x = Math::deg_to_rad(-sun_angle_altitude->get_value());
sun_rotation.y = Math::deg_to_rad(180.0 - sun_angle_azimuth->get_value());
Transform3D t;
t.basis = Basis::from_euler(Vector3(sun_rotation.x, sun_rotation.y, 0));
preview_sun->set_transform(t);
sun_direction->queue_redraw();
preview_sun->set_param(Light3D::PARAM_ENERGY, sun_energy->get_value());
preview_sun->set_param(Light3D::PARAM_SHADOW_MAX_DISTANCE, sun_shadow_max_distance->get_value());
preview_sun->set_color(sun_color->get_pick_color());
}
{ //preview env
sky_material->set_energy_multiplier(environ_energy->get_value());
Color hz_color = environ_sky_color->get_pick_color().lerp(environ_ground_color->get_pick_color(), 0.5);
float hz_lum = hz_color.get_luminance() * 3.333;
hz_color = hz_color.lerp(Color(hz_lum, hz_lum, hz_lum), 0.5);
sky_material->set_sky_top_color(environ_sky_color->get_pick_color());
sky_material->set_sky_horizon_color(hz_color);
sky_material->set_ground_bottom_color(environ_ground_color->get_pick_color());
sky_material->set_ground_horizon_color(hz_color);
environment->set_ssao_enabled(environ_ao_button->is_pressed());
environment->set_glow_enabled(environ_glow_button->is_pressed());
environment->set_sdfgi_enabled(environ_gi_button->is_pressed());
environment->set_tonemapper(environ_tonemap_button->is_pressed() ? Environment::TONE_MAPPER_FILMIC : Environment::TONE_MAPPER_LINEAR);
}
}
void Node3DEditor::_load_default_preview_settings() {
sun_environ_updating = true;
// These default rotations place the preview sun at an angular altitude
// of 60 degrees (must be negative) and an azimuth of 30 degrees clockwise
// from north (or 150 CCW from south), from north east, facing south west.
// On any not-tidally-locked planet, a sun would have an angular altitude
// of 60 degrees as the average of all points on the sphere at noon.
// The azimuth choice is arbitrary, but ideally shouldn't be on an axis.
sun_rotation = Vector2(-Math::deg_to_rad(60.0), Math::deg_to_rad(150.0));
sun_angle_altitude->set_value_no_signal(-Math::rad_to_deg(sun_rotation.x));
sun_angle_azimuth->set_value_no_signal(180.0 - Math::rad_to_deg(sun_rotation.y));
sun_direction->queue_redraw();
environ_sky_color->set_pick_color(Color(0.385, 0.454, 0.55));
environ_ground_color->set_pick_color(Color(0.2, 0.169, 0.133));
environ_energy->set_value_no_signal(1.0);
if (OS::get_singleton()->get_current_rendering_method() != "gl_compatibility" && OS::get_singleton()->get_current_rendering_method() != "dummy") {
environ_glow_button->set_pressed_no_signal(true);
}
environ_tonemap_button->set_pressed_no_signal(false);
environ_ao_button->set_pressed_no_signal(false);
environ_gi_button->set_pressed_no_signal(false);
sun_shadow_max_distance->set_value_no_signal(100);
sun_color->set_pick_color(Color(1, 1, 1));
sun_energy->set_value_no_signal(1.0);
sun_environ_updating = false;
}
void Node3DEditor::_update_preview_environment() {
bool disable_light = directional_light_count > 0 || !sun_button->is_pressed();
sun_button->set_disabled(directional_light_count > 0);
if (disable_light) {
if (preview_sun->get_parent()) {
preview_sun->get_parent()->remove_child(preview_sun);
sun_state->show();
sun_vb->hide();
preview_sun_dangling = true;
}
if (directional_light_count > 0) {
sun_state->set_text(TTRC("Scene contains\nDirectionalLight3D.\nPreview disabled."));
} else {
sun_state->set_text(TTRC("Preview disabled."));
}
} else {
if (!preview_sun->get_parent()) {
add_child(preview_sun, true);
sun_state->hide();
sun_vb->show();
preview_sun_dangling = false;
}
}
sun_angle_altitude->set_value_no_signal(-Math::rad_to_deg(sun_rotation.x));
sun_angle_azimuth->set_value_no_signal(180.0 - Math::rad_to_deg(sun_rotation.y));
bool disable_env = world_env_count > 0 || !environ_button->is_pressed();
environ_button->set_disabled(world_env_count > 0);
if (disable_env) {
if (preview_environment->get_parent()) {
preview_environment->get_parent()->remove_child(preview_environment);
environ_state->show();
environ_vb->hide();
preview_env_dangling = true;
}
if (world_env_count > 0) {
environ_state->set_text(TTRC("Scene contains\nWorldEnvironment.\nPreview disabled."));
} else {
environ_state->set_text(TTRC("Preview disabled."));
}
} else {
if (!preview_environment->get_parent()) {
add_child(preview_environment);
environ_state->hide();
environ_vb->show();
preview_env_dangling = false;
}
}
}
void Node3DEditor::_sun_direction_input(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseMotion> mm = p_event;
if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT)) {
sun_rotation.x += mm->get_relative().y * (0.02 * EDSCALE);
sun_rotation.y -= mm->get_relative().x * (0.02 * EDSCALE);
sun_rotation.x = CLAMP(sun_rotation.x, -Math::TAU / 4, Math::TAU / 4);
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Sun Direction"), UndoRedo::MergeMode::MERGE_ENDS);
undo_redo->add_do_method(sun_angle_altitude, "set_value_no_signal", -Math::rad_to_deg(sun_rotation.x));
undo_redo->add_undo_method(sun_angle_altitude, "set_value_no_signal", sun_angle_altitude->get_value());
undo_redo->add_do_method(sun_angle_azimuth, "set_value_no_signal", 180.0 - Math::rad_to_deg(sun_rotation.y));
undo_redo->add_undo_method(sun_angle_azimuth, "set_value_no_signal", sun_angle_azimuth->get_value());
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
}
void Node3DEditor::_sun_direction_set_altitude(float p_altitude) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Sun Altitude"), UndoRedo::MergeMode::MERGE_ENDS);
undo_redo->add_do_method(sun_angle_altitude, "set_value_no_signal", p_altitude);
undo_redo->add_undo_method(sun_angle_altitude, "set_value_no_signal", -Math::rad_to_deg(sun_rotation.x));
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_sun_direction_set_azimuth(float p_azimuth) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Sun Azimuth"), UndoRedo::MergeMode::MERGE_ENDS);
undo_redo->add_do_method(sun_angle_azimuth, "set_value_no_signal", p_azimuth);
undo_redo->add_undo_method(sun_angle_azimuth, "set_value_no_signal", 180.0 - Math::rad_to_deg(sun_rotation.y));
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_sun_set_color(const Color &p_color) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Sun Color"), UndoRedo::MergeMode::MERGE_ENDS);
undo_redo->add_do_method(sun_color, "set_pick_color", p_color);
undo_redo->add_undo_method(sun_color, "set_pick_color", preview_sun->get_color());
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_sun_set_energy(float p_energy) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Sun Energy"), UndoRedo::MergeMode::MERGE_ENDS);
undo_redo->add_do_method(sun_energy, "set_value_no_signal", p_energy);
undo_redo->add_undo_method(sun_energy, "set_value_no_signal", preview_sun->get_param(Light3D::PARAM_ENERGY));
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_sun_set_shadow_max_distance(float p_shadow_max_distance) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Sun Max Shadow Distance"), UndoRedo::MergeMode::MERGE_ENDS);
undo_redo->add_do_method(sun_shadow_max_distance, "set_value_no_signal", p_shadow_max_distance);
undo_redo->add_undo_method(sun_shadow_max_distance, "set_value_no_signal", preview_sun->get_param(Light3D::PARAM_SHADOW_MAX_DISTANCE));
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_environ_set_sky_color(const Color &p_color) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Environment Sky Color"), UndoRedo::MergeMode::MERGE_ENDS);
undo_redo->add_do_method(environ_sky_color, "set_pick_color", p_color);
undo_redo->add_undo_method(environ_sky_color, "set_pick_color", sky_material->get_sky_top_color());
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_environ_set_ground_color(const Color &p_color) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Environment Ground Color"), UndoRedo::MergeMode::MERGE_ENDS);
undo_redo->add_do_method(environ_ground_color, "set_pick_color", p_color);
undo_redo->add_undo_method(environ_ground_color, "set_pick_color", sky_material->get_ground_bottom_color());
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_environ_set_sky_energy(float p_energy) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Environment Energy"), UndoRedo::MergeMode::MERGE_ENDS);
undo_redo->add_do_method(environ_energy, "set_value_no_signal", p_energy);
undo_redo->add_undo_method(environ_energy, "set_value_no_signal", sky_material->get_energy_multiplier());
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_environ_set_ao() {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Environment Ambient Occlusion"));
undo_redo->add_do_method(environ_ao_button, "set_pressed", environ_ao_button->is_pressed());
undo_redo->add_undo_method(environ_ao_button, "set_pressed", !environ_ao_button->is_pressed());
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_environ_set_glow() {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Environment Glow"));
undo_redo->add_do_method(environ_glow_button, "set_pressed", environ_glow_button->is_pressed());
undo_redo->add_undo_method(environ_glow_button, "set_pressed", !environ_glow_button->is_pressed());
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_environ_set_tonemap() {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Environment Tonemap"));
undo_redo->add_do_method(environ_tonemap_button, "set_pressed", environ_tonemap_button->is_pressed());
undo_redo->add_undo_method(environ_tonemap_button, "set_pressed", !environ_tonemap_button->is_pressed());
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::_environ_set_gi() {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Preview Environment Global Illumination"));
undo_redo->add_do_method(environ_gi_button, "set_pressed", environ_gi_button->is_pressed());
undo_redo->add_undo_method(environ_gi_button, "set_pressed", !environ_gi_button->is_pressed());
undo_redo->add_do_method(this, "_preview_settings_changed");
undo_redo->add_undo_method(this, "_preview_settings_changed");
undo_redo->commit_action();
}
void Node3DEditor::PreviewSunEnvPopup::shortcut_input(const Ref<InputEvent> &p_event) {
const Ref<InputEventKey> k = p_event;
if (k.is_valid() && k->is_pressed()) {
bool handled = false;
if (ED_IS_SHORTCUT("ui_undo", p_event)) {
EditorNode::get_singleton()->undo();
handled = true;
}
if (ED_IS_SHORTCUT("ui_redo", p_event)) {
EditorNode::get_singleton()->redo();
handled = true;
}
if (handled) {
set_input_as_handled();
}
}
}
Node3DEditor::Node3DEditor() {
gizmo.visible = true;
gizmo.scale = 1.0;
gizmo_view_rotation_scale = GIZMO_CIRCLE_SIZE * (float)EDITOR_GET("editors/3d/view_plane_rotation_gizmo_scale");
viewport_environment.instantiate();
VBoxContainer *vbc = this;
ERR_FAIL_COND_MSG(singleton != nullptr, "A Node3DEditor singleton already exists.");
singleton = this;
editor_selection = EditorNode::get_singleton()->get_editor_selection();
editor_selection->add_editor_plugin(this);
MarginContainer *toolbar_margin = memnew(MarginContainer);
toolbar_margin->set_theme_type_variation("MainToolBarMargin");
vbc->add_child(toolbar_margin);
// A fluid container for all toolbars.
HFlowContainer *main_flow = memnew(HFlowContainer);
toolbar_margin->add_child(main_flow);
// Main toolbars.
HBoxContainer *main_menu_hbox = memnew(HBoxContainer);
main_flow->add_child(main_menu_hbox);
String sct;
tool_button[TOOL_MODE_TRANSFORM] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_MODE_TRANSFORM]);
tool_button[TOOL_MODE_TRANSFORM]->set_toggle_mode(true);
tool_button[TOOL_MODE_TRANSFORM]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_MODE_TRANSFORM]->set_pressed(true);
tool_button[TOOL_MODE_TRANSFORM]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_TOOL_TRANSFORM));
tool_button[TOOL_MODE_TRANSFORM]->set_shortcut(ED_SHORTCUT("spatial_editor/tool_transform", TTRC("Transform Mode"), Key::Q, true));
tool_button[TOOL_MODE_TRANSFORM]->set_shortcut_context(this);
tool_button[TOOL_MODE_TRANSFORM]->set_accessibility_name(TTRC("Transform Mode"));
tool_button[TOOL_MODE_MOVE] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_MODE_MOVE]);
tool_button[TOOL_MODE_MOVE]->set_toggle_mode(true);
tool_button[TOOL_MODE_MOVE]->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
tool_button[TOOL_MODE_MOVE]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_MODE_MOVE]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_TOOL_MOVE));
tool_button[TOOL_MODE_MOVE]->set_shortcut(ED_SHORTCUT("spatial_editor/tool_move", TTRC("Move Mode"), Key::W, true));
tool_button[TOOL_MODE_MOVE]->set_shortcut_context(this);
tool_button[TOOL_MODE_MOVE]->set_accessibility_name(TTRC("Move Mode"));
tool_button[TOOL_MODE_ROTATE] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_MODE_ROTATE]);
tool_button[TOOL_MODE_ROTATE]->set_toggle_mode(true);
tool_button[TOOL_MODE_ROTATE]->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
tool_button[TOOL_MODE_ROTATE]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_MODE_ROTATE]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_TOOL_ROTATE));
tool_button[TOOL_MODE_ROTATE]->set_shortcut(ED_SHORTCUT("spatial_editor/tool_rotate", TTRC("Rotate Mode"), Key::E, true));
tool_button[TOOL_MODE_ROTATE]->set_shortcut_context(this);
tool_button[TOOL_MODE_ROTATE]->set_accessibility_name(TTRC("Rotate Mode"));
tool_button[TOOL_MODE_SCALE] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_MODE_SCALE]);
tool_button[TOOL_MODE_SCALE]->set_toggle_mode(true);
tool_button[TOOL_MODE_SCALE]->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
tool_button[TOOL_MODE_SCALE]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_MODE_SCALE]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_TOOL_SCALE));
tool_button[TOOL_MODE_SCALE]->set_shortcut(ED_SHORTCUT("spatial_editor/tool_scale", TTRC("Scale Mode"), Key::R, true));
tool_button[TOOL_MODE_SCALE]->set_shortcut_context(this);
tool_button[TOOL_MODE_SCALE]->set_accessibility_name(TTRC("Scale Mode"));
tool_button[TOOL_MODE_SELECT] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_MODE_SELECT]);
tool_button[TOOL_MODE_SELECT]->set_toggle_mode(true);
tool_button[TOOL_MODE_SELECT]->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
tool_button[TOOL_MODE_SELECT]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_MODE_SELECT]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_TOOL_SELECT));
tool_button[TOOL_MODE_SELECT]->set_shortcut(ED_SHORTCUT("spatial_editor/tool_select", TTRC("Select Mode"), Key::V, true));
tool_button[TOOL_MODE_SELECT]->set_shortcut_context(this);
tool_button[TOOL_MODE_SELECT]->set_accessibility_name(TTRC("Select Mode"));
main_menu_hbox->add_child(memnew(VSeparator));
tool_button[TOOL_MODE_LIST_SELECT] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_MODE_LIST_SELECT]);
tool_button[TOOL_MODE_LIST_SELECT]->set_toggle_mode(true);
tool_button[TOOL_MODE_LIST_SELECT]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_MODE_LIST_SELECT]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_TOOL_LIST_SELECT));
tool_button[TOOL_MODE_LIST_SELECT]->set_tooltip_text(TTR("Show list of selectable nodes at position clicked.") + "\n" + TTR("Alt+RMB: Show list of all nodes at position clicked, including locked."));
tool_button[TOOL_MODE_LIST_SELECT]->set_accessibility_name(TTRC("Show List of Selectable Nodes"));
tool_button[TOOL_LOCK_SELECTED] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_LOCK_SELECTED]);
tool_button[TOOL_LOCK_SELECTED]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_LOCK_SELECTED]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_LOCK_SELECTED));
tool_button[TOOL_LOCK_SELECTED]->set_tooltip_text(TTRC("Lock selected node, preventing selection and movement."));
// Define the shortcut globally (without a context) so that it works if the Scene tree dock is currently focused.
tool_button[TOOL_LOCK_SELECTED]->set_shortcut(ED_GET_SHORTCUT("editor/lock_selected_nodes"));
tool_button[TOOL_LOCK_SELECTED]->set_accessibility_name(TTRC("Lock"));
tool_button[TOOL_UNLOCK_SELECTED] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_UNLOCK_SELECTED]);
tool_button[TOOL_UNLOCK_SELECTED]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_UNLOCK_SELECTED]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_UNLOCK_SELECTED));
tool_button[TOOL_UNLOCK_SELECTED]->set_tooltip_text(TTRC("Unlock selected node, allowing selection and movement."));
// Define the shortcut globally (without a context) so that it works if the Scene tree dock is currently focused.
tool_button[TOOL_UNLOCK_SELECTED]->set_shortcut(ED_GET_SHORTCUT("editor/unlock_selected_nodes"));
tool_button[TOOL_UNLOCK_SELECTED]->set_accessibility_name(TTRC("Unlock"));
tool_button[TOOL_GROUP_SELECTED] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_GROUP_SELECTED]);
tool_button[TOOL_GROUP_SELECTED]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_GROUP_SELECTED]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_GROUP_SELECTED));
tool_button[TOOL_GROUP_SELECTED]->set_tooltip_text(TTRC("Groups the selected node with its children. This selects the parent when any child node is clicked in 2D and 3D view."));
// Define the shortcut globally (without a context) so that it works if the Scene tree dock is currently focused.
tool_button[TOOL_GROUP_SELECTED]->set_shortcut(ED_GET_SHORTCUT("editor/group_selected_nodes"));
tool_button[TOOL_GROUP_SELECTED]->set_accessibility_name(TTRC("Group"));
tool_button[TOOL_UNGROUP_SELECTED] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_UNGROUP_SELECTED]);
tool_button[TOOL_UNGROUP_SELECTED]->set_theme_type_variation(SceneStringName(FlatButton));
tool_button[TOOL_UNGROUP_SELECTED]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_UNGROUP_SELECTED));
tool_button[TOOL_UNGROUP_SELECTED]->set_tooltip_text(TTRC("Ungroups the selected node from its children. Child nodes will be individual items in 2D and 3D view."));
// Define the shortcut globally (without a context) so that it works if the Scene tree dock is currently focused.
tool_button[TOOL_UNGROUP_SELECTED]->set_shortcut(ED_GET_SHORTCUT("editor/ungroup_selected_nodes"));
tool_button[TOOL_UNGROUP_SELECTED]->set_accessibility_name(TTRC("Ungroup"));
tool_button[TOOL_RULER] = memnew(Button);
main_menu_hbox->add_child(tool_button[TOOL_RULER]);
tool_button[TOOL_RULER]->set_toggle_mode(true);
tool_button[TOOL_RULER]->set_theme_type_variation("FlatButton");
tool_button[TOOL_RULER]->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed).bind(MENU_RULER));
tool_button[TOOL_RULER]->set_tooltip_text(TTRC("LMB+Drag: Measure distance between two points.\nShift+LMB+Drag: Show component measurements."));
// Define the shortcut globally (without a context) so that it works if the Scene tree dock is currently focused.
tool_button[TOOL_RULER]->set_shortcut(ED_SHORTCUT("spatial_editor/measure", TTRC("Ruler Mode"), Key::M));
tool_button[TOOL_RULER]->set_accessibility_name(TTRC("Ruler Mode"));
main_menu_hbox->add_child(memnew(VSeparator));
tool_option_button[TOOL_OPT_LOCAL_COORDS] = memnew(Button);
main_menu_hbox->add_child(tool_option_button[TOOL_OPT_LOCAL_COORDS]);
tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_toggle_mode(true);
tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_theme_type_variation(SceneStringName(FlatButton));
tool_option_button[TOOL_OPT_LOCAL_COORDS]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_LOCAL_COORDS));
tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_shortcut(ED_SHORTCUT("spatial_editor/local_coords", TTRC("Use Local Space"), Key::T));
tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_shortcut_context(this);
tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_accessibility_name(TTRC("Use Local Space"));
tool_option_button[TOOL_OPT_USE_SNAP] = memnew(Button);
main_menu_hbox->add_child(tool_option_button[TOOL_OPT_USE_SNAP]);
tool_option_button[TOOL_OPT_USE_SNAP]->set_toggle_mode(true);
tool_option_button[TOOL_OPT_USE_SNAP]->set_theme_type_variation(SceneStringName(FlatButton));
tool_option_button[TOOL_OPT_USE_SNAP]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_USE_SNAP));
tool_option_button[TOOL_OPT_USE_SNAP]->set_shortcut(ED_SHORTCUT("spatial_editor/snap", TTRC("Use Snap"), Key::Y));
tool_option_button[TOOL_OPT_USE_SNAP]->set_shortcut_context(this);
tool_option_button[TOOL_OPT_USE_SNAP]->set_accessibility_name(TTRC("Use Snap"));
tool_option_button[TOOL_OPT_USE_TRACKBALL] = memnew(Button);
main_menu_hbox->add_child(tool_option_button[TOOL_OPT_USE_TRACKBALL]);
tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_toggle_mode(true);
tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_theme_type_variation(SceneStringName(FlatButton));
tool_option_button[TOOL_OPT_USE_TRACKBALL]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_USE_TRACKBALL));
tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_shortcut(ED_SHORTCUT("spatial_editor/trackball", TTRC("Use Trackball"), Key::U));
tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_shortcut_context(this);
tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_accessibility_name(TTRC("Use Trackball"));
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM] = memnew(Button);
main_menu_hbox->add_child(tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]);
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_toggle_mode(true);
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_theme_type_variation(SceneStringName(FlatButton));
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_PRESERVE_CHILDREN_TRANSFORM));
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_shortcut(ED_SHORTCUT("spatial_editor/preserve_children_transform", TTRC("Preserve Children Transform"), Key::P));
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_shortcut_context(this);
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_accessibility_name(TTRC("Preserve Children Transform"));
tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_tooltip_text(TTRC("When enabled, transforming a node will preserve the global transform of its children.\nThis also applies when editing transform properties in the Inspector."));
main_menu_hbox->add_child(memnew(VSeparator));
sun_button = memnew(Button);
sun_button->set_tooltip_text(TTRC("Toggle preview sunlight.\nIf a DirectionalLight3D node is added to the scene, preview sunlight is disabled."));
sun_button->set_toggle_mode(true);
sun_button->set_accessibility_name(TTRC("Toggle preview sunlight."));
sun_button->set_theme_type_variation(SceneStringName(FlatButton));
sun_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_update_preview_environment), CONNECT_DEFERRED);
// Preview is enabled by default - ensure this applies on editor startup when there is no state yet.
sun_button->set_pressed(true);
main_menu_hbox->add_child(sun_button);
environ_button = memnew(Button);
environ_button->set_tooltip_text(TTRC("Toggle preview environment.\nIf a WorldEnvironment node is added to the scene, preview environment is disabled."));
environ_button->set_toggle_mode(true);
environ_button->set_accessibility_name(TTRC("Toggle preview environment."));
environ_button->set_theme_type_variation(SceneStringName(FlatButton));
environ_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_update_preview_environment), CONNECT_DEFERRED);
// Preview is enabled by default - ensure this applies on editor startup when there is no state yet.
environ_button->set_pressed(true);
main_menu_hbox->add_child(environ_button);
sun_environ_settings = memnew(Button);
sun_environ_settings->set_tooltip_text(TTRC("Edit Sun and Environment settings."));
sun_environ_settings->set_theme_type_variation(SceneStringName(FlatButton));
sun_environ_settings->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_sun_environ_settings_pressed));
main_menu_hbox->add_child(sun_environ_settings);
main_menu_hbox->add_child(memnew(VSeparator));
// Drag and drop support;
preview_node = memnew(Node3D);
preview_bounds = AABB();
ED_SHORTCUT("spatial_editor/bottom_view", TTRC("Bottom View"), KeyModifierMask::ALT + Key::KP_7);
ED_SHORTCUT("spatial_editor/top_view", TTRC("Top View"), Key::KP_7);
ED_SHORTCUT("spatial_editor/rear_view", TTRC("Rear View"), KeyModifierMask::ALT + Key::KP_1);
ED_SHORTCUT("spatial_editor/front_view", TTRC("Front View"), Key::KP_1);
ED_SHORTCUT("spatial_editor/left_view", TTRC("Left View"), KeyModifierMask::ALT + Key::KP_3);
ED_SHORTCUT("spatial_editor/right_view", TTRC("Right View"), Key::KP_3);
ED_SHORTCUT("spatial_editor/orbit_view_down", TTRC("Orbit View Down"), Key::KP_2);
ED_SHORTCUT("spatial_editor/orbit_view_left", TTRC("Orbit View Left"), Key::KP_4);
ED_SHORTCUT("spatial_editor/orbit_view_right", TTRC("Orbit View Right"), Key::KP_6);
ED_SHORTCUT("spatial_editor/orbit_view_up", TTRC("Orbit View Up"), Key::KP_8);
ED_SHORTCUT("spatial_editor/orbit_view_180", TTRC("Orbit View 180"), Key::KP_9);
ED_SHORTCUT("spatial_editor/switch_perspective_orthogonal", TTRC("Switch Perspective/Orthogonal View"), Key::KP_5);
ED_SHORTCUT("spatial_editor/insert_anim_key", TTRC("Insert Animation Key"), Key::K);
ED_SHORTCUT("spatial_editor/focus_origin", TTRC("Focus Origin"), Key::O);
ED_SHORTCUT("spatial_editor/focus_selection", TTRC("Focus Selection"), Key::F);
ED_SHORTCUT_ARRAY("spatial_editor/align_transform_with_view", TTRC("Align Transform with View"),
{ int32_t(KeyModifierMask::ALT | KeyModifierMask::CTRL | Key::KP_0),
int32_t(KeyModifierMask::ALT | KeyModifierMask::CTRL | Key::M),
int32_t(KeyModifierMask::ALT | KeyModifierMask::CTRL | Key::G) });
ED_SHORTCUT_OVERRIDE_ARRAY("spatial_editor/align_transform_with_view", "macos",
{ int32_t(KeyModifierMask::ALT | KeyModifierMask::META | Key::KP_0),
int32_t(KeyModifierMask::ALT | KeyModifierMask::META | Key::G) });
ED_SHORTCUT("spatial_editor/align_rotation_with_view", TTRC("Align Rotation with View"), KeyModifierMask::ALT + KeyModifierMask::CMD_OR_CTRL + Key::F);
ED_SHORTCUT("spatial_editor/freelook_toggle", TTRC("Toggle Freelook"), KeyModifierMask::SHIFT + Key::F);
ED_SHORTCUT("spatial_editor/decrease_fov", TTRC("Decrease Field of View"), KeyModifierMask::CMD_OR_CTRL + Key::EQUAL); // Usually direct access key for `KEY_PLUS`.
ED_SHORTCUT("spatial_editor/increase_fov", TTRC("Increase Field of View"), KeyModifierMask::CMD_OR_CTRL + Key::MINUS);
ED_SHORTCUT("spatial_editor/reset_fov", TTRC("Reset Field of View to Default"), KeyModifierMask::CMD_OR_CTRL + Key::KEY_0);
PopupMenu *p;
transform_menu = memnew(MenuButton);
transform_menu->set_flat(false);
transform_menu->set_theme_type_variation("FlatMenuButton");
transform_menu->set_text(TTRC("Transform"));
transform_menu->set_switch_on_hover(true);
transform_menu->set_shortcut_context(this);
main_menu_hbox->add_child(transform_menu);
p = transform_menu->get_popup();
p->add_shortcut(ED_SHORTCUT("spatial_editor/snap_to_floor", TTRC("Snap Object to Floor"), Key::PAGEDOWN), MENU_SNAP_TO_FLOOR);
p->add_shortcut(ED_SHORTCUT("spatial_editor/transform_dialog", TTRC("Transform Dialog...")), MENU_TRANSFORM_DIALOG);
p->add_separator();
ED_SHORTCUT("spatial_editor/vertex_snap", TTRC("Vertex Snap"), Key::B);
p->add_radio_check_item(TTRC("Snap Vertex to Vertex"), MENU_VERTEX_SNAP_BASE_VERTEX);
p->set_item_checked(p->get_item_index(MENU_VERTEX_SNAP_BASE_VERTEX), true);
p->add_radio_check_item(TTRC("Snap Origin to Vertex"), MENU_VERTEX_SNAP_BASE_ORIGIN);
p->add_separator();
p->add_radio_check_item(TTRC("Snap to Mesh Vertices"), MENU_VERTEX_SNAP_SOURCE_MESH);
p->set_item_checked(p->get_item_index(MENU_VERTEX_SNAP_SOURCE_MESH), true);
p->add_radio_check_item(TTRC("Snap to Collision Vertices"), MENU_VERTEX_SNAP_SOURCE_COLLISION);
_update_vertex_snap_tooltips();
p->add_separator();
p->add_shortcut(ED_SHORTCUT("spatial_editor/configure_snap", TTRC("Configure Snap...")), MENU_TRANSFORM_CONFIGURE_SNAP);
p->connect(SceneStringName(id_pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed));
view_layout_menu = memnew(MenuButton);
view_layout_menu->set_flat(false);
view_layout_menu->set_theme_type_variation("FlatMenuButton");
// TRANSLATORS: Noun, name of the 2D/3D View menus.
view_layout_menu->set_text(TTRC("View"));
view_layout_menu->set_switch_on_hover(true);
view_layout_menu->set_shortcut_context(this);
main_menu_hbox->add_child(view_layout_menu);
main_menu_hbox->add_child(memnew(VSeparator));
context_toolbar_panel = memnew(PanelContainer);
context_toolbar_hbox = memnew(HBoxContainer);
context_toolbar_panel->add_child(context_toolbar_hbox);
main_flow->add_child(context_toolbar_panel);
// Get the view menu popup and have it stay open when a checkable item is selected
p = view_layout_menu->get_popup();
p->set_hide_on_checkable_item_selection(false);
accept = memnew(AcceptDialog);
EditorNode::get_singleton()->get_gui_base()->add_child(accept);
p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/1_viewport", TTRC("1 Viewport"), KeyModifierMask::CMD_OR_CTRL + Key::KEY_1, true), MENU_VIEW_USE_1_VIEWPORT);
p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/2_viewports", TTRC("2 Viewports"), KeyModifierMask::CMD_OR_CTRL + Key::KEY_2, true), MENU_VIEW_USE_2_VIEWPORTS);
p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/2_viewports_alt", TTRC("2 Viewports (Alt)"), KeyModifierMask::ALT + KeyModifierMask::CMD_OR_CTRL + Key::KEY_2, true), MENU_VIEW_USE_2_VIEWPORTS_ALT);
p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/3_viewports", TTRC("3 Viewports"), KeyModifierMask::CMD_OR_CTRL + Key::KEY_3, true), MENU_VIEW_USE_3_VIEWPORTS);
p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/3_viewports_alt", TTRC("3 Viewports (Alt)"), KeyModifierMask::ALT + KeyModifierMask::CMD_OR_CTRL + Key::KEY_3, true), MENU_VIEW_USE_3_VIEWPORTS_ALT);
p->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/4_viewports", TTRC("4 Viewports"), KeyModifierMask::CMD_OR_CTRL + Key::KEY_4, true), MENU_VIEW_USE_4_VIEWPORTS);
p->add_separator();
gizmos_menu = memnew(PopupMenu);
gizmos_menu->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
gizmos_menu->set_hide_on_checkable_item_selection(false);
p->add_submenu_node_item(TTRC("Gizmos"), gizmos_menu);
gizmos_menu->connect(SceneStringName(id_pressed), callable_mp(this, &Node3DEditor::_menu_gizmo_toggled));
p->add_separator();
p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_origin", TTRC("View Origin")), MENU_VIEW_ORIGIN);
p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_grid", TTRC("View Grid"), Key::NUMBERSIGN), MENU_VIEW_GRID);
p->add_separator();
p->add_submenu_node_item(TTRC("Preview Translation"), memnew(EditorTranslationPreviewMenu));
p->add_separator();
p->add_shortcut(ED_SHORTCUT("spatial_editor/settings", TTRC("Settings...")), MENU_VIEW_CAMERA_SETTINGS);
p->set_item_checked(p->get_item_index(MENU_VIEW_ORIGIN), true);
p->set_item_checked(p->get_item_index(MENU_VIEW_GRID), true);
p->connect(SceneStringName(id_pressed), callable_mp(this, &Node3DEditor::_menu_item_pressed));
/* REST OF MENU */
left_panel_split = memnew(HSplitContainer);
left_panel_split->set_v_size_flags(SIZE_EXPAND_FILL);
vbc->add_child(left_panel_split);
right_panel_split = memnew(HSplitContainer);
right_panel_split->set_v_size_flags(SIZE_EXPAND_FILL);
left_panel_split->add_child(right_panel_split);
shader_split = memnew(VSplitContainer);
shader_split->set_h_size_flags(SIZE_EXPAND_FILL);
right_panel_split->add_child(shader_split);
viewport_base = memnew(Node3DEditorViewportContainer);
shader_split->add_child(viewport_base);
viewport_base->set_v_size_flags(SIZE_EXPAND_FILL);
for (uint32_t i = 0; i < VIEWPORTS_COUNT; i++) {
viewports[i] = memnew(Node3DEditorViewport(this, i));
viewports[i]->connect("toggle_maximize_view", callable_mp(this, &Node3DEditor::_toggle_maximize_view));
viewports[i]->connect("clicked", callable_mp(this, &Node3DEditor::_viewport_clicked).bind(i));
viewports[i]->assign_pending_data_pointers(preview_node, &preview_bounds, accept);
viewports[i]->set_h_size_flags(SIZE_EXPAND_FILL);
viewports[i]->set_v_size_flags(SIZE_EXPAND_FILL);
viewports[i]->set_custom_minimum_size(Size2(39, 39));
viewport_base->add_viewport(viewports[i], i);
}
/* SNAP DIALOG */
snap_dialog = memnew(ConfirmationDialog);
snap_dialog->set_title(TTRC("Snap Settings"));
add_child(snap_dialog);
snap_dialog->connect(SceneStringName(confirmed), callable_mp(this, &Node3DEditor::_snap_changed));
snap_dialog->get_cancel_button()->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_snap_update));
VBoxContainer *snap_dialog_vbc = memnew(VBoxContainer);
snap_dialog->add_child(snap_dialog_vbc);
snap_translate = memnew(EditorSpinSlider);
snap_translate->set_min(0.0);
snap_translate->set_step(0.001);
snap_translate->set_max(10.0);
snap_translate->set_suffix("m");
snap_translate->set_allow_greater(true);
snap_translate->set_accessibility_name(TTRC("Translate Snap"));
snap_dialog_vbc->add_margin_child(TTR("Translate Snap:"), snap_translate);
snap_rotate = memnew(EditorSpinSlider);
snap_rotate->set_min(0.0);
snap_rotate->set_step(0.1);
snap_rotate->set_max(360);
snap_rotate->set_suffix(U"°");
snap_rotate->set_accessibility_name(TTRC("Rotate Snap"));
snap_dialog_vbc->add_margin_child(TTR("Rotate Snap:"), snap_rotate);
snap_scale = memnew(EditorSpinSlider);
snap_scale->set_min(0.0);
snap_scale->set_step(1.0);
snap_scale->set_max(100);
snap_scale->set_suffix("%");
snap_scale->set_accessibility_name(TTRC("Scale Snap"));
snap_dialog_vbc->add_margin_child(TTR("Scale Snap:"), snap_scale);
/* SETTINGS DIALOG */
settings_dialog = memnew(ConfirmationDialog);
settings_dialog->set_title(TTRC("Viewport Settings"));
add_child(settings_dialog);
settings_vbc = memnew(VBoxContainer);
settings_vbc->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
settings_dialog->add_child(settings_vbc);
settings_fov = memnew(SpinBox);
settings_fov->set_max(MAX_FOV);
settings_fov->set_min(MIN_FOV);
settings_fov->set_step(0.1);
settings_fov->set_value(EDITOR_GET("editors/3d/default_fov"));
settings_fov->set_select_all_on_focus(true);
settings_fov->set_tooltip_text(TTRC("FOV is defined as a vertical value, as the editor camera always uses the Keep Height aspect mode."));
settings_fov->set_accessibility_name(TTRC("Perspective VFOV (deg.):"));
settings_vbc->add_margin_child(TTRC("Perspective VFOV (deg.):"), settings_fov);
settings_znear = memnew(SpinBox);
settings_znear->set_max(MAX_Z);
settings_znear->set_min(MIN_Z);
settings_znear->set_step(0.01);
settings_znear->set_accessibility_name(TTRC("View Z-Near:"));
settings_znear->set_value(EDITOR_GET("editors/3d/default_z_near"));
settings_znear->set_select_all_on_focus(true);
settings_vbc->add_margin_child(TTRC("View Z-Near:"), settings_znear);
settings_zfar = memnew(SpinBox);
settings_zfar->set_max(MAX_Z);
settings_zfar->set_min(MIN_Z);
settings_zfar->set_step(0.1);
settings_zfar->set_accessibility_name(TTRC("View Z-Far:"));
settings_zfar->set_value(EDITOR_GET("editors/3d/default_z_far"));
settings_zfar->set_select_all_on_focus(true);
settings_vbc->add_margin_child(TTRC("View Z-Far:"), settings_zfar);
for (uint32_t i = 0; i < VIEWPORTS_COUNT; ++i) {
settings_dialog->connect(SceneStringName(confirmed), callable_mp(viewports[i], &Node3DEditorViewport::_view_settings_confirmed).bind(0.0));
}
/* XFORM DIALOG */
xform_dialog = memnew(ConfirmationDialog);
xform_dialog->set_title(TTRC("Transform Change"));
add_child(xform_dialog);
VBoxContainer *xform_vbc = memnew(VBoxContainer);
xform_dialog->add_child(xform_vbc);
HBoxContainer *translate_hb = memnew(HBoxContainer);
xform_vbc->add_margin_child(TTRC("Translate:"), translate_hb);
HBoxContainer *rotate_hb = memnew(HBoxContainer);
xform_vbc->add_margin_child(TTRC("Rotate (deg.):"), rotate_hb);
HBoxContainer *scale_hb = memnew(HBoxContainer);
xform_vbc->add_margin_child(TTRC("Scale (ratio):"), scale_hb);
for (int i = 0; i < 3; i++) {
xform_translate[i] = memnew(LineEdit);
xform_translate[i]->set_h_size_flags(SIZE_EXPAND_FILL);
xform_translate[i]->set_select_all_on_focus(true);
translate_hb->add_child(xform_translate[i]);
xform_rotate[i] = memnew(LineEdit);
xform_rotate[i]->set_h_size_flags(SIZE_EXPAND_FILL);
xform_rotate[i]->set_select_all_on_focus(true);
rotate_hb->add_child(xform_rotate[i]);
xform_scale[i] = memnew(LineEdit);
xform_scale[i]->set_h_size_flags(SIZE_EXPAND_FILL);
xform_scale[i]->set_select_all_on_focus(true);
scale_hb->add_child(xform_scale[i]);
}
xform_type = memnew(OptionButton);
xform_type->set_h_size_flags(SIZE_EXPAND_FILL);
xform_type->set_accessibility_name(TTRC("Transform Type"));
xform_type->add_item(TTRC("Pre"));
xform_type->add_item(TTRC("Post"));
xform_vbc->add_margin_child(TTRC("Transform Type"), xform_type);
xform_dialog->connect(SceneStringName(confirmed), callable_mp(this, &Node3DEditor::_xform_dialog_action));
selected = nullptr;
set_process_shortcut_input(true);
add_to_group(SceneStringName(_spatial_editor_group));
current_hover_gizmo_handle = -1;
current_hover_gizmo_handle_secondary = false;
{
// Sun/preview environment popup.
sun_environ_popup = memnew(PreviewSunEnvPopup);
add_child(sun_environ_popup);
HBoxContainer *sun_environ_hb = memnew(HBoxContainer);
sun_environ_popup->add_child(sun_environ_hb);
sun_vb = memnew(VBoxContainer);
sun_environ_hb->add_child(sun_vb);
sun_vb->set_custom_minimum_size(Size2(200 * EDSCALE, 0));
sun_vb->hide();
sun_title = memnew(Label);
sun_title->set_theme_type_variation("HeaderMedium");
sun_vb->add_child(sun_title);
sun_title->set_text(TTRC("Preview Sun"));
sun_title->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
CenterContainer *sun_direction_center = memnew(CenterContainer);
sun_direction = memnew(Control);
sun_direction->set_custom_minimum_size(Size2(128, 128) * EDSCALE);
sun_direction_center->add_child(sun_direction);
sun_vb->add_margin_child(TTRC("Sun Direction"), sun_direction_center);
sun_direction->connect(SceneStringName(gui_input), callable_mp(this, &Node3DEditor::_sun_direction_input));
sun_direction->connect(SceneStringName(draw), callable_mp(this, &Node3DEditor::_sun_direction_draw));
sun_direction->set_default_cursor_shape(CURSOR_MOVE);
sun_direction_shader.instantiate();
sun_direction_shader->set_code(R"(
// 3D editor Preview Sun direction shader.
shader_type canvas_item;
uniform vec3 sun_direction;
uniform vec3 sun_color;
void fragment() {
vec3 n;
n.xy = UV * 2.0 - 1.0;
n.z = sqrt(max(0.0, 1.0 - dot(n.xy, n.xy)));
COLOR.rgb = dot(n, sun_direction) * sun_color;
COLOR.a = 1.0 - smoothstep(0.99, 1.0, length(n.xy));
}
)");
sun_direction_material.instantiate();
sun_direction_material->set_shader(sun_direction_shader);
sun_direction_material->set_shader_parameter("sun_direction", Vector3(0, 0, 1));
sun_direction_material->set_shader_parameter("sun_color", Vector3(1, 1, 1));
sun_direction->set_material(sun_direction_material);
HBoxContainer *sun_angle_hbox = memnew(HBoxContainer);
sun_angle_hbox->set_h_size_flags(SIZE_EXPAND_FILL);
VBoxContainer *sun_angle_altitude_vbox = memnew(VBoxContainer);
sun_angle_altitude_vbox->set_h_size_flags(SIZE_EXPAND_FILL);
Label *sun_angle_altitude_label = memnew(Label);
sun_angle_altitude_label->set_text(TTRC("Angular Altitude"));
sun_angle_altitude_vbox->add_child(sun_angle_altitude_label);
sun_angle_altitude = memnew(EditorSpinSlider);
sun_angle_altitude->set_suffix(U"\u00B0");
sun_angle_altitude->set_max(90);
sun_angle_altitude->set_min(-90);
sun_angle_altitude->set_step(0.1);
sun_angle_altitude->connect(SceneStringName(value_changed), callable_mp(this, &Node3DEditor::_sun_direction_set_altitude));
sun_angle_altitude_vbox->add_child(sun_angle_altitude);
sun_angle_hbox->add_child(sun_angle_altitude_vbox);
VBoxContainer *sun_angle_azimuth_vbox = memnew(VBoxContainer);
sun_angle_azimuth_vbox->set_h_size_flags(SIZE_EXPAND_FILL);
sun_angle_azimuth_vbox->set_custom_minimum_size(Vector2(100, 0));
Label *sun_angle_azimuth_label = memnew(Label);
sun_angle_azimuth_label->set_text(TTRC("Azimuth"));
sun_angle_azimuth_vbox->add_child(sun_angle_azimuth_label);
sun_angle_azimuth = memnew(EditorSpinSlider);
sun_angle_azimuth->set_suffix(U"\u00B0");
sun_angle_azimuth->set_max(180);
sun_angle_azimuth->set_min(-180);
sun_angle_azimuth->set_step(0.1);
sun_angle_azimuth->set_allow_greater(true);
sun_angle_azimuth->set_allow_lesser(true);
sun_angle_azimuth->connect(SceneStringName(value_changed), callable_mp(this, &Node3DEditor::_sun_direction_set_azimuth));
sun_angle_azimuth_vbox->add_child(sun_angle_azimuth);
sun_angle_hbox->add_child(sun_angle_azimuth_vbox);
sun_angle_hbox->add_theme_constant_override("separation", 10);
sun_vb->add_child(sun_angle_hbox);
sun_color = memnew(ColorPickerButton);
sun_color->set_edit_alpha(false);
sun_vb->add_margin_child(TTRC("Sun Color"), sun_color);
sun_color->connect("color_changed", callable_mp(this, &Node3DEditor::_sun_set_color));
sun_color->get_popup()->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(sun_color->get_picker()));
sun_energy = memnew(EditorSpinSlider);
sun_energy->set_max(64.0);
sun_energy->set_min(0);
sun_energy->set_step(0.05);
sun_vb->add_margin_child(TTRC("Sun Energy"), sun_energy);
sun_energy->connect(SceneStringName(value_changed), callable_mp(this, &Node3DEditor::_sun_set_energy));
sun_shadow_max_distance = memnew(EditorSpinSlider);
sun_vb->add_margin_child(TTRC("Shadow Max Distance"), sun_shadow_max_distance);
sun_shadow_max_distance->connect(SceneStringName(value_changed), callable_mp(this, &Node3DEditor::_sun_set_shadow_max_distance));
sun_shadow_max_distance->set_min(1);
sun_shadow_max_distance->set_max(4096);
sun_add_to_scene = memnew(Button);
sun_add_to_scene->set_text(TTRC("Add Sun to Scene"));
sun_add_to_scene->set_tooltip_text(TTRC("Adds a DirectionalLight3D node matching the preview sun settings to the current scene.\nHold Shift while clicking to also add the preview environment to the current scene."));
sun_add_to_scene->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_add_sun_to_scene).bind(false));
sun_vb->add_spacer();
sun_vb->add_child(sun_add_to_scene);
sun_state = memnew(Label);
sun_environ_hb->add_child(sun_state);
sun_state->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
sun_state->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
sun_state->set_h_size_flags(SIZE_EXPAND_FILL);
VSeparator *sc = memnew(VSeparator);
sc->set_custom_minimum_size(Size2(10 * EDSCALE, 0));
sc->set_v_size_flags(SIZE_EXPAND_FILL);
sun_environ_hb->add_child(sc);
environ_vb = memnew(VBoxContainer);
sun_environ_hb->add_child(environ_vb);
environ_vb->set_custom_minimum_size(Size2(200 * EDSCALE, 0));
environ_vb->hide();
environ_title = memnew(Label);
environ_title->set_theme_type_variation("HeaderMedium");
environ_vb->add_child(environ_title);
environ_title->set_text(TTRC("Preview Environment"));
environ_title->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
environ_sky_color = memnew(ColorPickerButton);
environ_sky_color->set_edit_alpha(false);
environ_sky_color->connect("color_changed", callable_mp(this, &Node3DEditor::_environ_set_sky_color));
environ_sky_color->get_popup()->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(environ_sky_color->get_picker()));
environ_vb->add_margin_child(TTRC("Sky Color"), environ_sky_color);
environ_ground_color = memnew(ColorPickerButton);
environ_ground_color->connect("color_changed", callable_mp(this, &Node3DEditor::_environ_set_ground_color));
environ_ground_color->set_edit_alpha(false);
environ_ground_color->get_popup()->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(environ_ground_color->get_picker()));
environ_vb->add_margin_child(TTRC("Ground Color"), environ_ground_color);
environ_energy = memnew(EditorSpinSlider);
environ_energy->set_max(8.0);
environ_energy->set_min(0);
environ_energy->set_step(0.05);
environ_energy->connect(SceneStringName(value_changed), callable_mp(this, &Node3DEditor::_environ_set_sky_energy));
environ_vb->add_margin_child(TTRC("Sky Energy"), environ_energy);
HBoxContainer *fx_vb = memnew(HBoxContainer);
fx_vb->set_h_size_flags(SIZE_EXPAND_FILL);
environ_ao_button = memnew(Button);
environ_ao_button->set_text(TTRC("AO"));
environ_ao_button->set_h_size_flags(SIZE_EXPAND_FILL);
environ_ao_button->set_toggle_mode(true);
environ_ao_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_environ_set_ao), CONNECT_DEFERRED);
fx_vb->add_child(environ_ao_button);
environ_glow_button = memnew(Button);
environ_glow_button->set_text(TTRC("Glow"));
environ_glow_button->set_h_size_flags(SIZE_EXPAND_FILL);
environ_glow_button->set_toggle_mode(true);
environ_glow_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_environ_set_glow), CONNECT_DEFERRED);
fx_vb->add_child(environ_glow_button);
environ_tonemap_button = memnew(Button);
environ_tonemap_button->set_text(TTRC("Tonemap"));
environ_tonemap_button->set_h_size_flags(SIZE_EXPAND_FILL);
environ_tonemap_button->set_toggle_mode(true);
environ_tonemap_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_environ_set_tonemap), CONNECT_DEFERRED);
fx_vb->add_child(environ_tonemap_button);
environ_gi_button = memnew(Button);
environ_gi_button->set_text(TTRC("GI"));
environ_gi_button->set_h_size_flags(SIZE_EXPAND_FILL);
environ_gi_button->set_toggle_mode(true);
environ_gi_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_environ_set_gi), CONNECT_DEFERRED);
fx_vb->add_child(environ_gi_button);
environ_vb->add_margin_child(TTRC("Post Process"), fx_vb);
environ_add_to_scene = memnew(Button);
environ_add_to_scene->set_text(TTRC("Add Environment to Scene"));
environ_add_to_scene->set_tooltip_text(TTRC("Adds a WorldEnvironment node matching the preview environment settings to the current scene.\nHold Shift while clicking to also add the preview sun to the current scene."));
environ_add_to_scene->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_add_environment_to_scene).bind(false));
environ_vb->add_spacer();
environ_vb->add_child(environ_add_to_scene);
environ_state = memnew(Label);
sun_environ_hb->add_child(environ_state);
environ_state->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
environ_state->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
environ_state->set_h_size_flags(SIZE_EXPAND_FILL);
preview_sun = memnew(DirectionalLight3D);
preview_sun->set_shadow(true);
preview_sun->set_shadow_mode(DirectionalLight3D::SHADOW_PARALLEL_4_SPLITS);
preview_environment = memnew(WorldEnvironment);
environment.instantiate();
preview_environment->set_environment(environment);
if (GLOBAL_GET("rendering/lights_and_shadows/use_physical_light_units")) {
camera_attributes.instantiate();
preview_environment->set_camera_attributes(camera_attributes);
}
Ref<Sky> sky;
sky.instantiate();
sky_material.instantiate();
sky->set_material(sky_material);
environment->set_sky(sky);
environment->set_background(Environment::BG_SKY);
sun_environ_popup->set_process_shortcut_input(true);
_load_default_preview_settings();
_preview_settings_changed();
}
clear(); // Make sure values are initialized. Will call _snap_update() for us.
}
Node3DEditor::~Node3DEditor() {
singleton = nullptr;
memdelete(preview_node);
if (preview_sun_dangling && preview_sun) {
memdelete(preview_sun);
}
if (preview_env_dangling && preview_environment) {
memdelete(preview_environment);
}
}
void Node3DEditorPlugin::edited_scene_changed() {
for (uint32_t i = 0; i < Node3DEditor::VIEWPORTS_COUNT; i++) {
Node3DEditorViewport *viewport = Node3DEditor::get_singleton()->get_editor_viewport(i);
if (viewport->is_visible()) {
viewport->notification(Control::NOTIFICATION_VISIBILITY_CHANGED);
}
}
}
void Node3DEditorPlugin::make_visible(bool p_visible) {
if (p_visible) {
spatial_editor->show();
spatial_editor->set_process(true);
spatial_editor->set_physics_process(true);
spatial_editor->refresh_dirty_gizmos();
} else {
spatial_editor->hide();
spatial_editor->set_process(false);
spatial_editor->set_physics_process(false);
}
}
void Node3DEditorPlugin::edit(Object *p_object) {
spatial_editor->edit(Object::cast_to<Node3D>(p_object));
}
bool Node3DEditorPlugin::handles(Object *p_object) const {
return p_object->is_class("Node3D");
}
Dictionary Node3DEditorPlugin::get_state() const {
return spatial_editor->get_state();
}
void Node3DEditorPlugin::set_state(const Dictionary &p_state) {
spatial_editor->set_state(p_state);
}
Size2i Node3DEditor::get_camera_viewport_size(Camera3D *p_camera) {
Viewport *viewport = p_camera->get_viewport();
Window *window = Object::cast_to<Window>(viewport);
if (window) {
return window->get_size();
}
SubViewport *sub_viewport = Object::cast_to<SubViewport>(viewport);
ERR_FAIL_NULL_V(sub_viewport, Size2i());
if (sub_viewport == EditorNode::get_singleton()->get_scene_root()) {
return Size2(GLOBAL_GET("display/window/size/viewport_width"), GLOBAL_GET("display/window/size/viewport_height"));
}
return sub_viewport->get_size();
}
Vector3 Node3DEditor::snap_point(Vector3 p_target, Vector3 p_start) const {
if (is_snap_enabled()) {
real_t snap = get_translate_snap();
p_target.snapf(snap);
}
return p_target;
}
float Node3DEditor::get_znear() const {
return settings_znear->get_value();
}
float Node3DEditor::get_zfar() const {
return settings_zfar->get_value();
}
float Node3DEditor::get_fov() const {
return settings_fov->get_value();
}
bool Node3DEditor::is_gizmo_visible() const {
if (selected) {
return gizmo.visible && selected->is_transform_gizmo_visible();
}
return gizmo.visible;
}
bool Node3DEditor::are_local_coords_enabled() const {
return tool_option_button[Node3DEditor::TOOL_OPT_LOCAL_COORDS]->is_pressed();
}
void Node3DEditor::set_local_coords_enabled(bool on) const {
tool_option_button[Node3DEditor::TOOL_OPT_LOCAL_COORDS]->set_pressed(on);
}
bool Node3DEditor::is_preserve_children_transform_enabled() const {
return tool_option_button[Node3DEditor::TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->is_pressed();
}
bool Node3DEditor::is_vertex_snap_use_collision() const {
return vertex_snap_use_collision != Input::get_singleton()->is_key_pressed(Key::SHIFT);
}
real_t Node3DEditor::get_translate_snap() const {
real_t snap_value = snap_translate_value;
if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) {
snap_value /= 10.0f;
}
return snap_value;
}
real_t Node3DEditor::get_rotate_snap() const {
real_t snap_value = snap_rotate_value;
if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) {
snap_value /= 3.0f;
}
return snap_value;
}
real_t Node3DEditor::get_scale_snap() const {
real_t snap_value = snap_scale_value;
if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) {
snap_value /= 2.0f;
}
return snap_value;
}
struct _GizmoPluginPriorityComparator {
bool operator()(const Ref<EditorNode3DGizmoPlugin> &p_a, const Ref<EditorNode3DGizmoPlugin> &p_b) const {
if (p_a->get_priority() == p_b->get_priority()) {
return p_a->get_gizmo_name() < p_b->get_gizmo_name();
}
return p_a->get_priority() > p_b->get_priority();
}
};
struct _GizmoPluginNameComparator {
bool operator()(const Ref<EditorNode3DGizmoPlugin> &p_a, const Ref<EditorNode3DGizmoPlugin> &p_b) const {
return p_a->get_gizmo_name() < p_b->get_gizmo_name();
}
};
void Node3DEditor::add_gizmo_plugin(Ref<EditorNode3DGizmoPlugin> p_plugin) {
ERR_FAIL_COND(p_plugin.is_null());
gizmo_plugins_by_priority.push_back(p_plugin);
gizmo_plugins_by_priority.sort_custom<_GizmoPluginPriorityComparator>();
gizmo_plugins_by_name.push_back(p_plugin);
gizmo_plugins_by_name.sort_custom<_GizmoPluginNameComparator>();
_update_gizmos_menu();
}
void Node3DEditor::remove_gizmo_plugin(Ref<EditorNode3DGizmoPlugin> p_plugin) {
gizmo_plugins_by_priority.erase(p_plugin);
gizmo_plugins_by_name.erase(p_plugin);
_update_gizmos_menu();
}
DynamicBVH::ID Node3DEditor::insert_gizmo_bvh_node(Node3D *p_node, const AABB &p_aabb) {
return gizmo_bvh.insert(p_aabb, p_node);
}
void Node3DEditor::update_gizmo_bvh_node(DynamicBVH::ID p_id, const AABB &p_aabb) {
gizmo_bvh.update(p_id, p_aabb);
gizmo_bvh.optimize_incremental(1);
}
void Node3DEditor::remove_gizmo_bvh_node(DynamicBVH::ID p_id) {
gizmo_bvh.remove(p_id);
}
Vector<Node3D *> Node3DEditor::gizmo_bvh_ray_query(const Vector3 &p_ray_start, const Vector3 &p_ray_end) {
struct Result {
Vector<Node3D *> nodes;
bool operator()(void *p_data) {
nodes.append((Node3D *)p_data);
return false;
}
} result;
gizmo_bvh.ray_query(p_ray_start, p_ray_end, result);
return result.nodes;
}
Vector<Node3D *> Node3DEditor::gizmo_bvh_frustum_query(const Vector<Plane> &p_frustum) {
Vector<Vector3> points = Geometry3D::compute_convex_mesh_points(&p_frustum[0], p_frustum.size());
struct Result {
Vector<Node3D *> nodes;
bool operator()(void *p_data) {
nodes.append((Node3D *)p_data);
return false;
}
} result;
gizmo_bvh.convex_query(p_frustum.ptr(), p_frustum.size(), points.ptr(), points.size(), result);
return result.nodes;
}
Node3DEditorPlugin::Node3DEditorPlugin() {
spatial_editor = memnew(Node3DEditor);
spatial_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL);
EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(spatial_editor);
spatial_editor->hide();
}