/**************************************************************************/ /* 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(selected) : nullptr; if (se && se->gizmo.is_valid()) { for (const KeyValue &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 &selection = editor_selection->get_top_selected_node_list(); for (Node *E : selection) { Node3D *sp = Object::cast_to(E); if (!sp) { continue; } if (sp->has_meta("_edit_lock_")) { continue; } Node3DEditorSelectedItem *sel_item = editor_selection->get_node_editor_data(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(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(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 st = memnew(SurfaceTool); Ref st_xray = memnew(SurfaceTool); Ref active_st = memnew(SurfaceTool); Ref 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(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 &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> gizmos = selected->get_gizmos(); for (int i = 0; i < gizmos.size(); i++) { Ref seg = gizmos[i]; if (seg.is_null()) { continue; } seg->set_selected(false); } Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(selected); if (se) { se->gizmo.unref(); se->subgizmos.clear(); } selected->update_gizmos(); } selected = p_spatial; current_hover_gizmo = Ref(); current_hover_gizmo_handle = -1; current_hover_gizmo_handle_secondary = false; if (selected) { Vector> gizmos = selected->get_gizmos(); for (int i = 0; i < gizmos.size(); i++) { Ref 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 &selection = editor_selection->get_top_selected_node_list(); for (Node *E : selection) { Node3D *sp = Object::cast_to(E); if (!sp) { continue; } Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(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(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(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(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 &selection = editor_selection->get_top_selected_node_list(); for (Node *E : selection) { Node3D *spatial = Object::cast_to(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 &selection = editor_selection->get_top_selected_node_list(); for (Node *E : selection) { Node3D *spatial = Object::cast_to(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 &selection = editor_selection->get_top_selected_node_list(); for (Node *E : selection) { Node3D *spatial = Object::cast_to(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 &selection = editor_selection->get_top_selected_node_list(); for (Node *E : selection) { Node3D *spatial = Object::cast_to(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 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 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 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 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 mat; Ref 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 grid_colors[3]; static LocalVector grid_points[3]; static LocalVector 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 &ref_grid = grid_points[c]; LocalVector &ref_grid_normals = grid_normals[c]; LocalVector &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)grid_points[c]; d[RSE::ARRAY_COLOR] = (Vector)grid_colors[c]; d[RSE::ARRAY_NORMAL] = (Vector)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 &selection = editor_selection->get_selection(); for (const KeyValue &E : selection) { Node3D *sp = ObjectDB::get_instance(E.key); if (!sp) { continue; } Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(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> gizmos = selected->get_gizmos(); for (int i = 0; i < gizmos.size(); i++) { Ref seg = gizmos[i]; if (seg.is_null()) { continue; } seg->set_selected(false); } Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(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 &top_selected = editor_selection->get_top_selected_node_list(); if (top_selected.size() == 1) { Node3D *new_selected = Object::cast_to(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 &top_selected = editor_selection->get_top_selected_node_list(); if (top_selected.size() == 1) { Node3D *new_selected = Object::cast_to(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 &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(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 HashSet _get_child_nodes(Node *parent_node) { HashSet nodes = HashSet(); T *node = Node::cast_to(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 child_nodes = _get_child_nodes(child_node); for (T *I : child_nodes) { nodes.insert(I); } } return nodes; } HashSet _get_physics_bodies_rid(Node *node) { HashSet rids = HashSet(); PhysicsBody3D *pb = Node::cast_to(node); if (pb) { rids.insert(pb->get_rid()); } HashSet child_nodes = _get_child_nodes(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 &selection = editor_selection->get_top_selected_node_list(); Dictionary snap_data; for (Node *E : selection) { Node3D *sp = Object::cast_to(E); if (sp) { Vector3 from; Vector3 position_offset; // Priorities for snapping to floor are CollisionShapes, VisualInstances and then origin HashSet vi = _get_child_nodes(sp); HashSet cs = _get_child_nodes(sp); bool found_valid_shape = false; if (cs.size()) { AABB aabb; HashSet::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 &kv : snap_data) { Node *node = Object::cast_to(kv.key); Node3D *sp = Object::cast_to(node); Dictionary d = kv.value; Vector3 from = d["from"]; Vector3 to = from - Vector3(0.0, max_snap_height, 0.0); HashSet 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 &kv : snap_data) { Node *node = Object::cast_to(kv.key); Node3D *sp = Object::cast_to(node); Dictionary d = kv.value; Vector3 from = d["from"]; Vector3 to = from - Vector3(0.0, max_snap_height, 0.0); HashSet 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 &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(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(selected) : nullptr; if (se) { return se->gizmo == p_gizmo; } return false; } Vector Node3DEditor::get_subgizmo_selection() { Node3DEditorSelectedItem *se = selected ? editor_selection->get_node_editor_data(selected) : nullptr; Vector ret; if (se) { for (const KeyValue &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(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(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 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(p_id); if (node) { _request_gizmo(node); } } void Node3DEditor::_set_subgizmo_selection(Object *p_obj, Ref 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(p_obj); } else { sp = selected; } if (!sp) { return; } Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(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(p_obj); } else { sp = selected; } if (!sp) { return; } Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data(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(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(p_node)) { world_env_count++; if (world_env_count == 1) { _update_preview_environment(); } } else if (Object::cast_to(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(p_node)) { world_env_count--; if (world_env_count == 0) { _update_preview_environment(); } } else if (Object::cast_to(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(selected); if (se) { se->gizmo.unref(); se->subgizmos.clear(); } selected = nullptr; update_transform_gizmo(); } } void Node3DEditor::_register_all_gizmos() { add_gizmo_plugin(Ref(memnew(Camera3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(Light3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(AudioStreamPlayer3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(AudioListener3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(MeshInstance3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(OccluderInstance3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(SpriteBase3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(Label3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(GeometryInstance3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(Marker3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(SpringBoneCollision3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(SpringBoneSimulator3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(VisibleOnScreenNotifier3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(GPUParticles3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(GPUParticlesCollision3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(Particles3DEmissionShapeGizmoPlugin))); add_gizmo_plugin(Ref(memnew(CPUParticles3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(ReflectionProbeGizmoPlugin))); add_gizmo_plugin(Ref(memnew(DecalGizmoPlugin))); add_gizmo_plugin(Ref(memnew(VoxelGIGizmoPlugin))); add_gizmo_plugin(Ref(memnew(LightmapGIGizmoPlugin))); add_gizmo_plugin(Ref(memnew(LightmapProbeGizmoPlugin))); add_gizmo_plugin(Ref(memnew(FogVolumeGizmoPlugin))); add_gizmo_plugin(Ref(memnew(TwoBoneIK3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(ChainIK3DGizmoPlugin))); // Physics gizmo plugins. add_gizmo_plugin(Ref(memnew(CollisionObject3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(CollisionShape3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(CollisionPolygon3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(Joint3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(SoftBody3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(ShapeCast3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(SpringArm3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(PhysicalBone3DGizmoPlugin))); add_gizmo_plugin(Ref(memnew(VehicleWheel3DGizmoPlugin))); add_gizmo_plugin(Ref(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 &p_event) { Ref 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 &p_event) { const Ref 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.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(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(viewport); if (window) { return window->get_size(); } SubViewport *sub_viewport = Object::cast_to(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 &p_a, const Ref &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 &p_a, const Ref &p_b) const { return p_a->get_gizmo_name() < p_b->get_gizmo_name(); } }; void Node3DEditor::add_gizmo_plugin(Ref 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 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 Node3DEditor::gizmo_bvh_ray_query(const Vector3 &p_ray_start, const Vector3 &p_ray_end) { struct Result { Vector 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 Node3DEditor::gizmo_bvh_frustum_query(const Vector &p_frustum) { Vector points = Geometry3D::compute_convex_mesh_points(&p_frustum[0], p_frustum.size()); struct Result { Vector 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(); }