/**************************************************************************/ /* editor_preview_plugins.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 "editor_preview_plugins.h" #include "core/config/project_settings.h" #include "core/io/image.h" #include "core/io/resource_loader.h" #include "core/object/script_language.h" #include "editor/editor_node.h" #include "editor/editor_paths.h" #include "editor/editor_settings.h" #include "editor/themes/editor_scale.h" #include "main/main.h" #include "modules/gridmap/grid_map.h" #include "scene/2d/animated_sprite_2d.h" #include "scene/2d/camera_2d.h" #include "scene/2d/line_2d.h" #include "scene/2d/mesh_instance_2d.h" #include "scene/2d/multimesh_instance_2d.h" #include "scene/2d/polygon_2d.h" #include "scene/2d/sprite_2d.h" #include "scene/2d/tile_map_layer.h" #include "scene/2d/touch_screen_button.h" #include "scene/3d/cpu_particles_3d.h" #include "scene/3d/gpu_particles_3d.h" #include "scene/3d/light_3d.h" #include "scene/main/viewport.h" #include "scene/resources/atlas_texture.h" #include "scene/resources/bit_map.h" #include "scene/resources/font.h" #include "scene/resources/gradient_texture.h" #include "scene/resources/image_texture.h" #include "scene/resources/material.h" #include "scene/resources/mesh.h" #include "scene/resources/world_2d.h" #include "servers/audio/audio_stream.h" void post_process_preview(Ref p_image) { if (p_image->get_format() != Image::FORMAT_RGBA8) { p_image->convert(Image::FORMAT_RGBA8); } const int w = p_image->get_width(); const int h = p_image->get_height(); const int r = MIN(w, h) / 32; const int r2 = r * r; Color transparent = Color(0, 0, 0, 0); for (int i = 0; i < r; i++) { for (int j = 0; j < r; j++) { int dx = i - r; int dy = j - r; if (dx * dx + dy * dy > r2) { p_image->set_pixel(i, j, transparent); p_image->set_pixel(w - 1 - i, j, transparent); p_image->set_pixel(w - 1 - i, h - 1 - j, transparent); p_image->set_pixel(i, h - 1 - j, transparent); } else { break; } } } } bool EditorTexturePreviewPlugin::handles(const String &p_type) const { return ClassDB::is_parent_class(p_type, "Texture"); } bool EditorTexturePreviewPlugin::generate_small_preview_automatically() const { return true; } Ref EditorTexturePreviewPlugin::generate(const Ref &p_from, const Size2 &p_size, Dictionary &p_metadata) const { Ref img; Ref tex_atlas = p_from; Ref tex_3d = p_from; Ref tex_lyr = p_from; if (tex_atlas.is_valid()) { Ref tex = tex_atlas->get_atlas(); if (tex.is_null()) { return Ref(); } Ref atlas = tex->get_image(); if (atlas.is_null()) { return Ref(); } if (atlas->is_compressed()) { atlas = atlas->duplicate(); if (atlas->decompress() != OK) { return Ref(); } } if (!tex_atlas->get_region().has_area()) { return Ref(); } img = atlas->get_region(tex_atlas->get_region()); } else if (tex_3d.is_valid()) { if (tex_3d->get_depth() == 0) { return Ref(); } Vector> data = tex_3d->get_data(); if (data.size() != tex_3d->get_depth()) { return Ref(); } // Use the middle slice for the thumbnail. const int mid_depth = (tex_3d->get_depth() - 1) / 2; if (!data.is_empty() && data[mid_depth].is_valid()) { img = data[mid_depth]->duplicate(); } } else if (tex_lyr.is_valid()) { if (tex_lyr->get_layers() == 0) { return Ref(); } // Use the middle slice for the thumbnail. const int mid_layer = (tex_lyr->get_layers() - 1) / 2; Ref data = tex_lyr->get_layer_data(mid_layer); if (data.is_valid()) { img = data->duplicate(); } } else { Ref tex = p_from; if (tex.is_valid()) { img = tex->get_image(); if (img.is_valid()) { img = img->duplicate(); } } } if (img.is_null() || img->is_empty()) { return Ref(); } p_metadata["dimensions"] = img->get_size(); img->clear_mipmaps(); if (img->is_compressed()) { if (img->decompress() != OK) { return Ref(); } } else if (img->get_format() != Image::FORMAT_RGB8 && img->get_format() != Image::FORMAT_RGBA8) { img->convert(Image::FORMAT_RGBA8); } Vector2 new_size = img->get_size(); if (new_size.x > p_size.x) { new_size = Vector2(p_size.x, new_size.y * p_size.x / new_size.x); } if (new_size.y > p_size.y) { new_size = Vector2(new_size.x * p_size.y / new_size.y, p_size.y); } Vector2i new_size_i = Vector2i(new_size).maxi(1); img->resize(new_size_i.x, new_size_i.y, Image::INTERPOLATE_CUBIC); post_process_preview(img); return ImageTexture::create_from_image(img); } EditorTexturePreviewPlugin::EditorTexturePreviewPlugin() { } //////////////////////////////////////////////////////////////////////////// bool EditorImagePreviewPlugin::handles(const String &p_type) const { return p_type == "Image"; } Ref EditorImagePreviewPlugin::generate(const Ref &p_from, const Size2 &p_size, Dictionary &p_metadata) const { Ref img = p_from; if (img.is_null() || img->is_empty()) { return Ref(); } img = img->duplicate(); img->clear_mipmaps(); if (img->is_compressed()) { if (img->decompress() != OK) { return Ref(); } } else if (img->get_format() != Image::FORMAT_RGB8 && img->get_format() != Image::FORMAT_RGBA8) { img->convert(Image::FORMAT_RGBA8); } Vector2 new_size = img->get_size(); if (new_size.x > p_size.x) { new_size = Vector2(p_size.x, new_size.y * p_size.x / new_size.x); } if (new_size.y > p_size.y) { new_size = Vector2(new_size.x * p_size.y / new_size.y, p_size.y); } img->resize(new_size.x, new_size.y, Image::INTERPOLATE_CUBIC); post_process_preview(img); return ImageTexture::create_from_image(img); } EditorImagePreviewPlugin::EditorImagePreviewPlugin() { } bool EditorImagePreviewPlugin::generate_small_preview_automatically() const { return true; } //////////////////////////////////////////////////////////////////////////// bool EditorBitmapPreviewPlugin::handles(const String &p_type) const { return ClassDB::is_parent_class(p_type, "BitMap"); } Ref EditorBitmapPreviewPlugin::generate(const Ref &p_from, const Size2 &p_size, Dictionary &p_metadata) const { Ref bm = p_from; if (bm->get_size() == Size2()) { return Ref(); } Vector data; data.resize(bm->get_size().width * bm->get_size().height); { uint8_t *w = data.ptrw(); for (int i = 0; i < bm->get_size().width; i++) { for (int j = 0; j < bm->get_size().height; j++) { if (bm->get_bit(i, j)) { w[j * (int)bm->get_size().width + i] = 255; } else { w[j * (int)bm->get_size().width + i] = 0; } } } } Ref img = Image::create_from_data(bm->get_size().width, bm->get_size().height, false, Image::FORMAT_L8, data); if (img->is_compressed()) { if (img->decompress() != OK) { return Ref(); } } else if (img->get_format() != Image::FORMAT_RGB8 && img->get_format() != Image::FORMAT_RGBA8) { img->convert(Image::FORMAT_RGBA8); } Vector2 new_size = img->get_size(); if (new_size.x > p_size.x) { new_size = Vector2(p_size.x, new_size.y * p_size.x / new_size.x); } if (new_size.y > p_size.y) { new_size = Vector2(new_size.x * p_size.y / new_size.y, p_size.y); } img->resize(new_size.x, new_size.y, Image::INTERPOLATE_CUBIC); post_process_preview(img); return ImageTexture::create_from_image(img); } bool EditorBitmapPreviewPlugin::generate_small_preview_automatically() const { return true; } EditorBitmapPreviewPlugin::EditorBitmapPreviewPlugin() { } /////////////////////////////////////////////////////////////////////////// void EditorPackedScenePreviewPlugin::abort() { draw_requester.abort(); aborted = true; } bool EditorPackedScenePreviewPlugin::handles(const String &p_type) const { return ClassDB::is_parent_class(p_type, "PackedScene"); } Ref EditorPackedScenePreviewPlugin::generate(const Ref &p_from, const Size2 &p_size, Dictionary &p_metadata) const { return generate_from_path(p_from->get_path(), p_size, p_metadata); } Ref EditorPackedScenePreviewPlugin::generate_from_path(const String &p_path, const Size2 &p_size, Dictionary &p_metadata) const { // Safe checks, since this function interacts with EditorNode to render previews ERR_FAIL_COND_V_MSG(!Engine::get_singleton()->is_editor_hint(), Ref(), "This function can only be called from the editor."); ERR_FAIL_COND_V_MSG(EditorNode::get_singleton() == nullptr, Ref(), "EditorNode doesn't exist."); // Lower abort flag aborted = false; Error load_error; Ref pack = ResourceLoader::load(p_path, "PackedScene", ResourceFormatLoader::CACHE_MODE_IGNORE, &load_error); // no more cache issues? if (load_error != OK) { print_error(vformat("Failed to generate scene thumbnail for %s : Loaded with error code %d", p_path, int(load_error))); return Ref(); } if (!pack.is_valid()) { print_error(vformat("Failed to generate scene thumbnail for %s : Invalid scene file", p_path)); return Ref(); } bool rm_script_success = _remove_scripts_from_packed_scene(pack); // We don't want tool scripts to fire off when generating previews if (!rm_script_success) { print_error(vformat("Failed to generate scene thumbnail for %s : error in removing scripts from preview scene, thus not safe to create thumbnail image", p_path)); return Ref(); } Node *p_scene = pack->instantiate(); // The instantiated preview scene int count_2d = 0; int count_3d = 0; int count_light_3d = 0; _count_node_types(p_scene, count_2d, count_3d, count_light_3d); if (count_3d > 0) { // Is 3d scene RS::get_singleton()->viewport_set_size(viewport, Math::round(p_size.x), Math::round(p_size.y)); RS::get_singleton()->viewport_set_transparent_background(viewport, false); RS::get_singleton()->viewport_set_disable_2d(viewport, true); RS::get_singleton()->viewport_set_disable_3d(viewport, false); if (p_size.x < 2048 && p_size.y < 2048) { // Universal baseline for textures in Godot 4 is 4K RS::get_singleton()->viewport_set_scaling_3d_scale(viewport, 2.0); // Supersampling } RS::get_singleton()->viewport_set_msaa_3d(viewport, RS::ViewportMSAA::VIEWPORT_MSAA_8X); RID environment = RS::get_singleton()->environment_create(); Color default_clear_color = GLOBAL_GET("rendering/environment/defaults/default_clear_color"); RS::get_singleton()->environment_set_background(environment, RenderingServer::EnvironmentBG::ENV_BG_CLEAR_COLOR); RS::get_singleton()->environment_set_bg_color(environment, default_clear_color); RS::get_singleton()->scenario_set_environment(scenario, environment); RID camera_3d = RS::get_singleton()->camera_create(); RID camera_attributes = RS::get_singleton()->camera_attributes_create(); RS::get_singleton()->viewport_attach_camera(viewport, camera_3d); RS::get_singleton()->camera_set_perspective(camera_3d, preview_3d_fov, 0.05, 10000.0); RS::get_singleton()->camera_set_camera_attributes(camera_3d, camera_attributes); // Add scene to viewport _construct_scene_3d(p_scene); // Preview light RID light_1 = RS::get_singleton()->directional_light_create(); RID light_2 = RS::get_singleton()->directional_light_create(); RID light_inst_1 = RS::get_singleton()->instance_create(); RID light_inst_2 = RS::get_singleton()->instance_create(); if (count_light_3d == 0) { RS::get_singleton()->instance_set_scenario(light_inst_1, scenario); RS::get_singleton()->instance_set_scenario(light_inst_2, scenario); RS::get_singleton()->instance_set_base(light_inst_1, light_1); RS::get_singleton()->instance_set_base(light_inst_2, light_2); RS::get_singleton()->light_set_color(light_1, Color(1.0, 1.0, 1.0, 1.0)); RS::get_singleton()->light_set_color(light_2, Color(0.7, 0.7, 0.7, 1.0)); RS::get_singleton()->instance_set_transform(light_inst_1, Transform3D(Basis().rotated(Vector3(0, 1, 0), -Math_PI / 6), Vector3(0.0, 0.0, 0.0))); RS::get_singleton()->instance_set_transform(light_inst_2, Transform3D(Basis().rotated(Vector3(1, 0, 0), -Math_PI / 6), Vector3(0.0, 0.0, 0.0))); } // Move camera to fit scene AABB scene_aabb; _calculate_scene_aabb(p_scene, scene_aabb); float bound_sphere_radius = (scene_aabb.get_end() - scene_aabb.get_position()).length() / 2.0f; if (bound_sphere_radius <= 0.0f) { // The scene has zero volume, so just it give a literal bound_sphere_radius = 1.0f; } float cam_distance = bound_sphere_radius / Math::tan(Math::deg_to_rad(preview_3d_fov) / 2.0f); Transform3D thumbnail_cam_trans_3d; thumbnail_cam_trans_3d.set_origin(scene_aabb.get_center() + Vector3(1.0f, 0.25f, 1.0f).normalized() * cam_distance); thumbnail_cam_trans_3d.set_look_at(thumbnail_cam_trans_3d.origin, scene_aabb.get_center()); RS::get_singleton()->camera_set_transform(camera_3d, thumbnail_cam_trans_3d); // Wait for scene render draw_requester.request_and_wait(viewport); // HACK - Render again, this makes GPUParticles to render correctly to thumbnail. // Also prevents thumbnail image to be incorrectly assigned to next the asset, don't know why. RS::get_singleton()->viewport_set_update_mode(viewport, RS::ViewportUpdateMode::VIEWPORT_UPDATE_ONCE); _wait_frames(1); // Retrieve thumbnail image (if not aborted) Ref thumbnail = Ref(); if (!aborted) { Ref img = RS::get_singleton()->texture_2d_get(viewport_texture); thumbnail = ImageTexture::create_from_image(img); } // Clean up RS::get_singleton()->free(light_1); RS::get_singleton()->free(light_inst_1); RS::get_singleton()->free(light_2); RS::get_singleton()->free(light_inst_2); RS::get_singleton()->free(camera_attributes); RS::get_singleton()->free(camera_3d); RS::get_singleton()->free(environment); p_scene->queue_free(); return thumbnail; } if (count_2d > 0) { // Is 2d scene // NOTE - // At the time of writing, CanvasItem nodes can't be rendered outside of the tree (see CanvasItem::queue_redraw() and RenderingServer::draw()) // So we hack this by creating SubViewports under the EditorNode. SubViewport *sub_viewport = memnew(SubViewport); sub_viewport->set_update_mode(SubViewport::UpdateMode::UPDATE_DISABLED); sub_viewport->set_disable_3d(true); sub_viewport->set_transparent_background(false); sub_viewport->set_msaa_2d(Viewport::MSAA::MSAA_8X); Ref world; world.instantiate(); sub_viewport->set_world_2d(world); Node *preview_root = memnew(Node); // Nodes only used in preview is attached to this sub_viewport->add_child(p_scene); sub_viewport->add_child(preview_root); // Hide gui _hide_gui_in_scene(p_scene); // Preview camera Camera2D *camera = memnew(Camera2D); camera->set_name("ThumbnailCamera2D"); preview_root->add_child(camera); // Attach subviewport deferred (thread safe) EditorNode::get_singleton()->call_deferred("add_child", sub_viewport); _wait_frames(1); if (aborted) { sub_viewport->call_deferred("queue_free"); return Ref(); } // Make 2D camera current camera->make_current(); // Calculate scene rect Rect2 scene_rect; _calculate_scene_rect(p_scene, scene_rect); Vector2 scene_true_center = scene_rect.get_center(); // Place camera 2D camera->set_position(Point2(scene_true_center)); // Render viewport uint16_t scene_rect_long = MAX(scene_rect.get_size().x, scene_rect.get_size().y); sub_viewport->set_size(p_size); camera->set_zoom(Vector2(p_size.x / float(scene_rect_long), p_size.y / float(scene_rect_long))); sub_viewport->set_update_mode(SubViewport::UpdateMode::UPDATE_ONCE); _wait_frames(1); if (aborted) { sub_viewport->call_deferred("queue_free"); return Ref(); } // Retrieve thumbnail of 2D (No GUI) Ref capture_2d = ImageTexture::create_from_image(sub_viewport->get_texture()->get_image()); if (capture_2d->get_image()->get_size() != p_size) { capture_2d->get_image()->resize(p_size.x, p_size.y); } capture_2d->get_image()->convert(Image::Format::FORMAT_RGBA8); // ALPHA channel is needed for it to blend with other image, don't know why. // Prepare for gui render sub_viewport->call_deferred("remove_child", p_scene); p_scene->queue_free(); p_scene = pack->instantiate(); _hide_node_2d_in_scene(p_scene); SubViewport *sub_viewport_gui = memnew(SubViewport); sub_viewport_gui->set_size(Size2i(GLOBAL_GET("display/window/size/viewport_width"), GLOBAL_GET("display/window/size/viewport_height"))); sub_viewport_gui->set_update_mode(SubViewport::UpdateMode::UPDATE_DISABLED); sub_viewport_gui->set_transparent_background(true); sub_viewport_gui->set_msaa_2d(Viewport::MSAA::MSAA_8X); sub_viewport_gui->set_disable_3d(true); sub_viewport_gui->add_child(p_scene); // Render GUI EditorNode::get_singleton()->call_deferred("add_child", sub_viewport_gui); sub_viewport_gui->set_update_mode(SubViewport::UpdateMode::UPDATE_ONCE); _wait_frames(1); if (aborted) { sub_viewport->call_deferred("queue_free"); sub_viewport_gui->call_deferred("queue_free"); return Ref(); } // Retrieve thumbnail of gui Ref capture_gui = ImageTexture::create_from_image(sub_viewport_gui->get_texture()->get_image()); if (capture_gui->get_image()->get_size() != p_size) { capture_gui->get_image()->resize(p_size.x, p_size.y); } // Mix 2D, GUI thumbnail images into one Ref thumbnail = memnew(ImageTexture); Ref thumbnail_image = Image::create_empty(p_size.x, p_size.y, false, Image::Format::FORMAT_RGBA8); // blend_rect needs ALPHA channel to work thumbnail_image->blend_rect(capture_2d->get_image(), capture_2d->get_image()->get_used_rect(), Point2i(0, 0)); thumbnail_image->blend_rect(capture_gui->get_image(), capture_gui->get_image()->get_used_rect(), Point2i(0, 0)); thumbnail->set_image(thumbnail_image); // Clean up EditorNode::get_singleton()->call_deferred("remove_child", sub_viewport); EditorNode::get_singleton()->call_deferred("remove_child", sub_viewport_gui); sub_viewport->call_deferred("queue_free"); sub_viewport_gui->call_deferred("queue_free"); p_scene->queue_free(); return thumbnail; } // Is scene without any visuals (No Node2D, Node3D, Control found) return Ref(); } void EditorPackedScenePreviewPlugin::_construct_scene_3d(Node *p_node) const { // Create visual instance into scenario if (p_node->is_class("VisualInstance3D")) { VisualInstance3D *v3d = Object::cast_to(p_node); RS::get_singleton()->instance_set_scenario(v3d->get_instance(), scenario); RS::get_singleton()->instance_set_transform(v3d->get_instance(), _get_global_transform_3d(v3d)); } if (p_node->is_class("GridMap")) { GridMap *gm = Object::cast_to(p_node); Transform3D gm_t3d = _get_global_transform_3d(gm); Array meshes = gm->get_meshes(); // get_bake_meshes() will return [Transform3D, Ref, Transform3D...], to get filled cell count do (array.size() / 2) for (int i = 1; i <= meshes.size() / 2; i++) { Ref mesh = meshes[i * 2 - 1]; Transform3D mesh_t3d = meshes[(i - 1) * 2]; // Is in local space RID mesh_inst = RS::get_singleton()->instance_create(); RS::get_singleton()->instance_set_base(mesh_inst, mesh->get_rid()); RS::get_singleton()->instance_set_scenario(mesh_inst, scenario); RS::get_singleton()->instance_set_transform(mesh_inst, gm_t3d * mesh_t3d); } } // Specific class settings if (p_node->is_class("CPUParticles3D")) { CPUParticles3D *particles_node = Object::cast_to(p_node); particles_node->set_pre_process_time(particles_node->get_lifetime() * 0.5); // Fast forward the particle emission to make it render something particles_node->set_use_local_coordinates(true); // HACK - Now constructs scene outside of tree, using global coords will cause error, this may introduce visual bugs, but is the best solution now particles_node->restart(true); // Keep seed to make simulation persistent } if (p_node->is_class("GPUParticles3D")) { // The same as CPUParticles GPUParticles3D *particles_node = Object::cast_to(p_node); particles_node->set_pre_process_time(particles_node->get_lifetime() * 0.5); particles_node->set_use_local_coordinates(true); particles_node->restart(true); } for (int i = 0; i < p_node->get_child_count(); i++) { _construct_scene_3d(p_node->get_child(i)); } } void EditorPackedScenePreviewPlugin::_count_node_types(Node *p_node, int &c2d, int &c3d, int &clight3d) const { if (p_node->is_class("Control") || p_node->is_class("Node2D")) { c2d++; } if (p_node->is_class("Node3D")) { c3d++; } if (p_node->is_class("Light3D")) { clight3d++; } for (int i = 0; i < p_node->get_child_count(); i++) { _count_node_types(p_node->get_child(i), c2d, c3d, clight3d); } } void EditorPackedScenePreviewPlugin::_calculate_scene_rect(Node *p_node, Rect2 &scene_rect) const { // NOTE: There's no universal way to get the exact global rect as a Node2D, so we dig into subclasses one by one // NOTE: // 1. Sprite2D::position by default is at the **center** of the sprite. (with offset == (0,0) AND centered == true) // 2. Rect2::position is at the **up-left** of the rect // 3. AABB::position is at the **bottom-left-forward** of the bounding box // // calculation below is done with these in mind. Rect2 n2d_rect = Rect2(); // The rect of the current iterating node2d if (p_node->is_class("Sprite2D")) { Sprite2D *sprite = Object::cast_to(p_node); n2d_rect.size = sprite->get_global_scale() * sprite->get_rect().size; n2d_rect.position = sprite->get_global_position() + sprite->get_offset() * sprite->get_global_scale(); if (sprite->is_centered()) { n2d_rect.position -= n2d_rect.size / 2.0f; } } if (p_node->is_class("AnimatedSprite2D")) { AnimatedSprite2D *anim_sprite = Object::cast_to(p_node); if (anim_sprite->get_sprite_frames().is_valid()) { Ref current_frame_tex = anim_sprite->get_sprite_frames()->get_frame_texture(anim_sprite->get_animation(), anim_sprite->get_frame()); if (current_frame_tex.is_valid()) { n2d_rect.size = current_frame_tex->get_size() * anim_sprite->get_global_scale(); n2d_rect.position = anim_sprite->get_global_position() + anim_sprite->get_offset() * anim_sprite->get_global_scale(); if (anim_sprite->is_centered()) { n2d_rect.position -= n2d_rect.size / 2.0f; } } } } if (p_node->is_class("MeshInstance2D")) { // NOTE: Conversion is 1m = 1px (before 2d scale) MeshInstance2D *mesh2d = Object::cast_to(p_node); Ref mesh = mesh2d->get_mesh(); if (mesh.is_valid()) { // Discard z axis (depth) and only get length of mesh in x,y axis n2d_rect.size.x = (mesh->get_aabb().get_end() - mesh->get_aabb().position).x; n2d_rect.size.y = (mesh->get_aabb().get_end() - mesh->get_aabb().position).y; n2d_rect.size *= mesh2d->get_global_scale(); // Account for mesh offset in 3d space when calculating rect2 n2d_rect.position.x = mesh2d->get_global_position().x + mesh->get_aabb().position.x * mesh2d->get_global_scale().x; // AABB::position is bottom-left n2d_rect.position.y = mesh2d->get_global_position().y + mesh->get_aabb().position.y * mesh2d->get_global_scale().y; } } if (p_node->is_class("MultiMeshInstance2D")) { // Basically the same procedure as MeshInstance2D. MultiMeshInstance2D *mmesh2d = Object::cast_to(p_node); Ref mmesh = mmesh2d->get_multimesh(); if (mmesh.is_valid()) { n2d_rect.size.x = (mmesh->get_aabb().get_end() - mmesh->get_aabb().position).x; n2d_rect.size.y = (mmesh->get_aabb().get_end() - mmesh->get_aabb().position).y; n2d_rect.size *= mmesh2d->get_global_scale(); n2d_rect.position.x = mmesh2d->get_global_position().x + mmesh->get_aabb().position.x * mmesh2d->get_global_scale().x; n2d_rect.position.y = mmesh2d->get_global_position().y + mmesh->get_aabb().position.y * mmesh2d->get_global_scale().y; } } if (p_node->is_class("TileMapLayer")) { // NOTE: TileMapLayer::get_used_rect() only count cells, not their actual pixel size TileMapLayer *tile_map = Object::cast_to(p_node); if (tile_map->get_tile_set().is_valid()) { Size2 tile_size = Size2(tile_map->get_tile_set()->get_tile_size()); // tile map cell pixel size (x,y) Rect2 tile_rect = Rect2(tile_map->get_used_rect()); // unit is in cells, not pixels! n2d_rect.position = tile_map->get_global_position() + tile_rect.position * tile_size * tile_map->get_global_scale(); // accounts tilemap offset n2d_rect.size = tile_rect.size * tile_size * tile_map->get_global_scale(); } } if (p_node->is_class("Polygon2D")) { Polygon2D *poly2d = Object::cast_to(p_node); PackedVector2Array polygon = poly2d->get_polygon(); if (polygon.size() > 2) { // Abort if there's no surface (min = 3 verts) // Calculate bounds float max_x = polygon[0].x; float min_x = polygon[0].x; float max_y = polygon[0].y; float min_y = polygon[0].y; for (int i = 0; i < polygon.size(); i++) { if (polygon[i].x > max_x) { max_x = polygon[i].x; } if (polygon[i].x < min_x) { min_x = polygon[i].x; } if (polygon[i].y > max_y) { max_y = polygon[i].y; } if (polygon[i].y < min_y) { min_y = polygon[i].y; } } Rect2 poly_rect = Rect2(Point2(min_x, min_y), Size2(max_x - min_x, max_y - min_y)); n2d_rect.position = poly2d->get_global_position() + poly2d->get_offset() * poly2d->get_global_scale(); n2d_rect.position += poly_rect.position * poly2d->get_global_scale(); n2d_rect.size = poly_rect.size * poly2d->get_global_scale(); } } if (p_node->is_class("Line2D")) { // The same procedure as Polygon2D Line2D *line2d = Object::cast_to(p_node); PackedVector2Array points = line2d->get_points(); if (line2d->get_point_count() > 1) { // Abort if there's no line drawn // Calculate bounds float max_x = points[0].x; float min_x = points[0].x; float max_y = points[0].y; float min_y = points[0].y; for (int i = 0; i < points.size(); i++) { if (points[i].x > max_x) { max_x = points[i].x; } if (points[i].x < min_x) { min_x = points[i].x; } if (points[i].y > max_y) { max_y = points[i].y; } if (points[i].y < min_y) { min_y = points[i].y; } } Rect2 line2d_rect = Rect2(Point2(min_x, min_y), Size2(max_x - min_x, max_y - min_y)); n2d_rect.position = line2d->get_global_position(); n2d_rect.position += line2d_rect.position * line2d->get_global_scale(); n2d_rect.size = line2d_rect.size * line2d->get_global_scale(); n2d_rect.size += Size2(line2d->get_width(), line2d->get_width()) / 2.0f; // account for line width } } if (p_node->is_class("TouchScreenButton")) { TouchScreenButton *btn = Object::cast_to(p_node); Ref btn_tex = btn->get_texture_normal(); if (btn_tex.is_valid()) { // Abort if there's no normal texture for this button (won't display anything) n2d_rect.position = btn->get_global_position(); // It's not possible to offset image in this node n2d_rect.size = btn_tex->get_size() * btn->get_global_scale(); } } // Merge the calculated node 2d rect if (scene_rect.get_size().length() == 0.0f) { // Avoid accounting scene origin (0,0) into scene rect scene_rect = n2d_rect.abs(); } else { scene_rect = scene_rect.merge(n2d_rect.abs()); } for (int i = 0; i < p_node->get_child_count(); i++) { _calculate_scene_rect(p_node->get_child(i), scene_rect); } } void EditorPackedScenePreviewPlugin::_hide_node_2d_in_scene(Node *p_node) const { // NOTE: Irreversible (cannot unhide nodes after this) // We cannot simple hide() since it will affect all its children (may contain Control nodes) if (p_node->is_class("Node2D")) { Node2D *n2d = Object::cast_to(p_node); n2d->set_self_modulate(Color(0.0f, 0.0f, 0.0f, 0.0f)); } for (int i = 0; i < p_node->get_child_count(); i++) { _hide_node_2d_in_scene(p_node->get_child(i)); } } void EditorPackedScenePreviewPlugin::_hide_gui_in_scene(Node *p_node) const { // NOTE: Irreversible (cannot unhide nodes after this) // We cannot simply hide() since it will affect all its children (may contain Node2D nodes) if (p_node->is_class("Control")) { Control *ctrl = Object::cast_to(p_node); ctrl->set_self_modulate(Color(0.0f, 0.0f, 0.0f, 0.0f)); } for (int i = 0; i < p_node->get_child_count(); i++) { _hide_gui_in_scene(p_node->get_child(i)); } } void EditorPackedScenePreviewPlugin::_wait_frames(const uint64_t &n) const { if (n <= 0) { return; } const uint64_t prev_frame = Engine::get_singleton()->get_frames_drawn(); while (Engine::get_singleton()->get_frames_drawn() - prev_frame < n + 1) { // Wait for n frames == (n+1) frames has rendered if (!EditorResourcePreview::get_singleton()->is_threaded()) { // Is running this on main thread, iterate main loop (or will get stuck here forever) Main::iteration(); } if (aborted) { break; } continue; } } void EditorPackedScenePreviewPlugin::_calculate_scene_aabb(Node *p_node, AABB &aabb) const { if (p_node->is_class("GeometryInstance3D")) { // Use this because VisualInstance3D may have derived classes that are non-graphical (probes, volumes) GeometryInstance3D *g3d = Object::cast_to(p_node); AABB node_aabb = _get_global_transform_3d(g3d).xform(g3d->get_aabb()); aabb.merge_with(node_aabb); } if (p_node->is_class("CPUParticles3D")) { // CPUParticles3D does not calculate particle bounds, so do it here CPUParticles3D *particles = Object::cast_to(p_node); // Account the furthest position where particles can go Vector3 particle_destination = _get_global_transform_3d(particles).origin; particle_destination += particles->get_direction() * particles->get_param_max(CPUParticles3D::PARAM_INITIAL_LINEAR_VELOCITY); aabb.expand_to(particle_destination * 0.5); aabb.expand_to(particle_destination * -0.5); } if (p_node->is_class("GPUParticles3D")) { GPUParticles3D *particles = Object::cast_to(p_node); aabb.merge_with(_get_global_transform_3d(particles).xform(particles->get_visibility_aabb())); } for (int i = 0; i < p_node->get_child_count(); i++) { _calculate_scene_aabb(p_node->get_child(i), aabb); } } Transform3D EditorPackedScenePreviewPlugin::_get_global_transform_3d(Node *p_n3d) const { // Designed to work even if node is outside the tree (is_inside_tree() != true) Transform3D global_transform = Transform3D(); Array parents = Array(); if (!p_n3d->is_class("Node3D")) { ERR_PRINT("Expected a Node3D node as argument"); return global_transform; } Node *p_loop_node = p_n3d; while (p_loop_node != nullptr) { if (p_loop_node->is_class("Node3D")) { parents.append(p_loop_node); } p_loop_node = p_loop_node->get_parent(); } parents.reverse(); for (int i = 0; i < parents.size(); i++) { Node3D *p_parent = Object::cast_to(parents[i]); if (i == 0) { global_transform = p_parent->get_transform(); continue; } global_transform *= p_parent->get_transform(); } return global_transform; } bool EditorPackedScenePreviewPlugin::_remove_scripts_from_packed_scene(Ref pack) const { // Refer to SceneState in packed_scene.cpp to see how PackedScene is managed underhood. // Sanitize Dictionary bundle = pack->get_state()->get_bundled_scene(); ERR_FAIL_COND_V(!bundle.has("names"), false); ERR_FAIL_COND_V(!bundle.has("variants"), false); ERR_FAIL_COND_V(!bundle.has("node_count"), false); ERR_FAIL_COND_V(!bundle.has("nodes"), false); ERR_FAIL_COND_V(!bundle.has("conn_count"), false); ERR_FAIL_COND_V(!bundle.has("conns"), false); const uint8_t supported_version = 3; uint8_t current_version = 1; if (bundle.has("version")) { current_version = bundle["version"]; } if (current_version > supported_version) { WARN_PRINT_ONCE(vformat("Scene thumbnail creation was built upon PackedScene with version %d, but the version has changed to %d now.", supported_version, current_version)); // And assume it's safe to continue, there should have no reason to change the main structure of PackedScene } // Find and remove all scripts in scene Ref