| 1 | /**************************************************************************/ |
| 2 | /* cpu_particles_2d_editor_plugin.cpp */ |
| 3 | /**************************************************************************/ |
| 4 | /* This file is part of: */ |
| 5 | /* GODOT ENGINE */ |
| 6 | /* https://godotengine.org */ |
| 7 | /**************************************************************************/ |
| 8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
| 9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
| 10 | /* */ |
| 11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
| 12 | /* a copy of this software and associated documentation files (the */ |
| 13 | /* "Software"), to deal in the Software without restriction, including */ |
| 14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
| 15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
| 16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
| 17 | /* the following conditions: */ |
| 18 | /* */ |
| 19 | /* The above copyright notice and this permission notice shall be */ |
| 20 | /* included in all copies or substantial portions of the Software. */ |
| 21 | /* */ |
| 22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
| 23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
| 24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
| 25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
| 26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
| 27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
| 28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
| 29 | /**************************************************************************/ |
| 30 | |
| 31 | #include "cpu_particles_2d_editor_plugin.h" |
| 32 | |
| 33 | #include "canvas_item_editor_plugin.h" |
| 34 | #include "core/io/image_loader.h" |
| 35 | #include "editor/editor_node.h" |
| 36 | #include "editor/editor_undo_redo_manager.h" |
| 37 | #include "editor/gui/editor_file_dialog.h" |
| 38 | #include "editor/scene_tree_dock.h" |
| 39 | #include "scene/2d/cpu_particles_2d.h" |
| 40 | #include "scene/2d/gpu_particles_2d.h" |
| 41 | #include "scene/gui/check_box.h" |
| 42 | #include "scene/gui/menu_button.h" |
| 43 | #include "scene/gui/option_button.h" |
| 44 | #include "scene/gui/separator.h" |
| 45 | #include "scene/gui/spin_box.h" |
| 46 | #include "scene/resources/particle_process_material.h" |
| 47 | |
| 48 | void CPUParticles2DEditorPlugin::edit(Object *p_object) { |
| 49 | particles = Object::cast_to<CPUParticles2D>(p_object); |
| 50 | } |
| 51 | |
| 52 | bool CPUParticles2DEditorPlugin::handles(Object *p_object) const { |
| 53 | return p_object->is_class("CPUParticles2D" ); |
| 54 | } |
| 55 | |
| 56 | void CPUParticles2DEditorPlugin::make_visible(bool p_visible) { |
| 57 | if (p_visible) { |
| 58 | toolbar->show(); |
| 59 | } else { |
| 60 | toolbar->hide(); |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | void CPUParticles2DEditorPlugin::_file_selected(const String &p_file) { |
| 65 | source_emission_file = p_file; |
| 66 | emission_mask->popup_centered(); |
| 67 | } |
| 68 | |
| 69 | void CPUParticles2DEditorPlugin::_menu_callback(int p_idx) { |
| 70 | switch (p_idx) { |
| 71 | case MENU_LOAD_EMISSION_MASK: { |
| 72 | file->popup_file_dialog(); |
| 73 | } break; |
| 74 | case MENU_CLEAR_EMISSION_MASK: { |
| 75 | emission_mask->popup_centered(); |
| 76 | } break; |
| 77 | case MENU_RESTART: { |
| 78 | particles->restart(); |
| 79 | } break; |
| 80 | case MENU_CONVERT_TO_GPU_PARTICLES: { |
| 81 | GPUParticles2D *gpu_particles = memnew(GPUParticles2D); |
| 82 | gpu_particles->convert_from_particles(particles); |
| 83 | gpu_particles->set_name(particles->get_name()); |
| 84 | gpu_particles->set_transform(particles->get_transform()); |
| 85 | gpu_particles->set_visible(particles->is_visible()); |
| 86 | gpu_particles->set_process_mode(particles->get_process_mode()); |
| 87 | |
| 88 | EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); |
| 89 | ur->create_action(TTR("Convert to GPUParticles3D" )); |
| 90 | SceneTreeDock::get_singleton()->replace_node(particles, gpu_particles); |
| 91 | ur->commit_action(false); |
| 92 | } break; |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | void CPUParticles2DEditorPlugin::_generate_emission_mask() { |
| 97 | Ref<Image> img; |
| 98 | img.instantiate(); |
| 99 | Error err = ImageLoader::load_image(source_emission_file, img); |
| 100 | ERR_FAIL_COND_MSG(err != OK, "Error loading image '" + source_emission_file + "'." ); |
| 101 | |
| 102 | if (img->is_compressed()) { |
| 103 | img->decompress(); |
| 104 | } |
| 105 | img->convert(Image::FORMAT_RGBA8); |
| 106 | ERR_FAIL_COND(img->get_format() != Image::FORMAT_RGBA8); |
| 107 | Size2i s = img->get_size(); |
| 108 | ERR_FAIL_COND(s.width == 0 || s.height == 0); |
| 109 | |
| 110 | Vector<Point2> valid_positions; |
| 111 | Vector<Point2> valid_normals; |
| 112 | Vector<uint8_t> valid_colors; |
| 113 | |
| 114 | valid_positions.resize(s.width * s.height); |
| 115 | |
| 116 | EmissionMode emode = (EmissionMode)emission_mask_mode->get_selected(); |
| 117 | |
| 118 | if (emode == EMISSION_MODE_BORDER_DIRECTED) { |
| 119 | valid_normals.resize(s.width * s.height); |
| 120 | } |
| 121 | |
| 122 | bool capture_colors = emission_colors->is_pressed(); |
| 123 | |
| 124 | if (capture_colors) { |
| 125 | valid_colors.resize(s.width * s.height * 4); |
| 126 | } |
| 127 | |
| 128 | int vpc = 0; |
| 129 | |
| 130 | { |
| 131 | Vector<uint8_t> img_data = img->get_data(); |
| 132 | const uint8_t *r = img_data.ptr(); |
| 133 | |
| 134 | for (int i = 0; i < s.width; i++) { |
| 135 | for (int j = 0; j < s.height; j++) { |
| 136 | uint8_t a = r[(j * s.width + i) * 4 + 3]; |
| 137 | |
| 138 | if (a > 128) { |
| 139 | if (emode == EMISSION_MODE_SOLID) { |
| 140 | if (capture_colors) { |
| 141 | valid_colors.write[vpc * 4 + 0] = r[(j * s.width + i) * 4 + 0]; |
| 142 | valid_colors.write[vpc * 4 + 1] = r[(j * s.width + i) * 4 + 1]; |
| 143 | valid_colors.write[vpc * 4 + 2] = r[(j * s.width + i) * 4 + 2]; |
| 144 | valid_colors.write[vpc * 4 + 3] = r[(j * s.width + i) * 4 + 3]; |
| 145 | } |
| 146 | valid_positions.write[vpc++] = Point2(i, j); |
| 147 | |
| 148 | } else { |
| 149 | bool on_border = false; |
| 150 | for (int x = i - 1; x <= i + 1; x++) { |
| 151 | for (int y = j - 1; y <= j + 1; y++) { |
| 152 | if (x < 0 || y < 0 || x >= s.width || y >= s.height || r[(y * s.width + x) * 4 + 3] <= 128) { |
| 153 | on_border = true; |
| 154 | break; |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | if (on_border) { |
| 159 | break; |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | if (on_border) { |
| 164 | valid_positions.write[vpc] = Point2(i, j); |
| 165 | |
| 166 | if (emode == EMISSION_MODE_BORDER_DIRECTED) { |
| 167 | Vector2 normal; |
| 168 | for (int x = i - 2; x <= i + 2; x++) { |
| 169 | for (int y = j - 2; y <= j + 2; y++) { |
| 170 | if (x == i && y == j) { |
| 171 | continue; |
| 172 | } |
| 173 | |
| 174 | if (x < 0 || y < 0 || x >= s.width || y >= s.height || r[(y * s.width + x) * 4 + 3] <= 128) { |
| 175 | normal += Vector2(x - i, y - j).normalized(); |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | normal.normalize(); |
| 181 | valid_normals.write[vpc] = normal; |
| 182 | } |
| 183 | |
| 184 | if (capture_colors) { |
| 185 | valid_colors.write[vpc * 4 + 0] = r[(j * s.width + i) * 4 + 0]; |
| 186 | valid_colors.write[vpc * 4 + 1] = r[(j * s.width + i) * 4 + 1]; |
| 187 | valid_colors.write[vpc * 4 + 2] = r[(j * s.width + i) * 4 + 2]; |
| 188 | valid_colors.write[vpc * 4 + 3] = r[(j * s.width + i) * 4 + 3]; |
| 189 | } |
| 190 | |
| 191 | vpc++; |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | } |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | valid_positions.resize(vpc); |
| 200 | if (valid_normals.size()) { |
| 201 | valid_normals.resize(vpc); |
| 202 | } |
| 203 | |
| 204 | ERR_FAIL_COND_MSG(valid_positions.size() == 0, "No pixels with transparency > 128 in image..." ); |
| 205 | |
| 206 | if (capture_colors) { |
| 207 | PackedColorArray pca; |
| 208 | pca.resize(vpc); |
| 209 | Color *pcaw = pca.ptrw(); |
| 210 | for (int i = 0; i < vpc; i += 1) { |
| 211 | Color color; |
| 212 | color.r = valid_colors[i * 4 + 0] / 255.0f; |
| 213 | color.g = valid_colors[i * 4 + 1] / 255.0f; |
| 214 | color.b = valid_colors[i * 4 + 2] / 255.0f; |
| 215 | color.a = valid_colors[i * 4 + 3] / 255.0f; |
| 216 | pcaw[i] = color; |
| 217 | } |
| 218 | particles->set_emission_colors(pca); |
| 219 | } |
| 220 | |
| 221 | if (valid_normals.size()) { |
| 222 | particles->set_emission_shape(CPUParticles2D::EMISSION_SHAPE_DIRECTED_POINTS); |
| 223 | PackedVector2Array norms; |
| 224 | norms.resize(valid_normals.size()); |
| 225 | Vector2 *normsw = norms.ptrw(); |
| 226 | for (int i = 0; i < valid_normals.size(); i += 1) { |
| 227 | normsw[i] = valid_normals[i]; |
| 228 | } |
| 229 | particles->set_emission_normals(norms); |
| 230 | } else { |
| 231 | particles->set_emission_shape(CPUParticles2D::EMISSION_SHAPE_POINTS); |
| 232 | } |
| 233 | |
| 234 | { |
| 235 | Vector2 offset; |
| 236 | if (emission_mask_centered->is_pressed()) { |
| 237 | offset = Vector2(-s.width * 0.5, -s.height * 0.5); |
| 238 | } |
| 239 | |
| 240 | PackedVector2Array points; |
| 241 | points.resize(valid_positions.size()); |
| 242 | Vector2 *pointsw = points.ptrw(); |
| 243 | for (int i = 0; i < valid_positions.size(); i += 1) { |
| 244 | pointsw[i] = valid_positions[i] + offset; |
| 245 | } |
| 246 | particles->set_emission_points(points); |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | void CPUParticles2DEditorPlugin::_notification(int p_what) { |
| 251 | switch (p_what) { |
| 252 | case NOTIFICATION_ENTER_TREE: { |
| 253 | menu->get_popup()->connect("id_pressed" , callable_mp(this, &CPUParticles2DEditorPlugin::_menu_callback)); |
| 254 | menu->set_icon(epoints->get_editor_theme_icon(SNAME("CPUParticles2D" ))); |
| 255 | file->connect("file_selected" , callable_mp(this, &CPUParticles2DEditorPlugin::_file_selected)); |
| 256 | } break; |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | void CPUParticles2DEditorPlugin::_bind_methods() { |
| 261 | } |
| 262 | |
| 263 | CPUParticles2DEditorPlugin::CPUParticles2DEditorPlugin() { |
| 264 | particles = nullptr; |
| 265 | |
| 266 | toolbar = memnew(HBoxContainer); |
| 267 | add_control_to_container(CONTAINER_CANVAS_EDITOR_MENU, toolbar); |
| 268 | toolbar->hide(); |
| 269 | |
| 270 | menu = memnew(MenuButton); |
| 271 | menu->get_popup()->add_item(TTR("Restart" ), MENU_RESTART); |
| 272 | menu->get_popup()->add_item(TTR("Load Emission Mask" ), MENU_LOAD_EMISSION_MASK); |
| 273 | menu->get_popup()->add_item(TTR("Convert to GPUParticles2D" ), MENU_CONVERT_TO_GPU_PARTICLES); |
| 274 | menu->set_text(TTR("CPUParticles2D" )); |
| 275 | menu->set_switch_on_hover(true); |
| 276 | toolbar->add_child(menu); |
| 277 | |
| 278 | file = memnew(EditorFileDialog); |
| 279 | List<String> ext; |
| 280 | ImageLoader::get_recognized_extensions(&ext); |
| 281 | for (const String &E : ext) { |
| 282 | file->add_filter("*." + E, E.to_upper()); |
| 283 | } |
| 284 | file->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE); |
| 285 | toolbar->add_child(file); |
| 286 | |
| 287 | epoints = memnew(SpinBox); |
| 288 | epoints->set_min(1); |
| 289 | epoints->set_max(8192); |
| 290 | epoints->set_step(1); |
| 291 | epoints->set_value(512); |
| 292 | file->get_vbox()->add_margin_child(TTR("Generated Point Count:" ), epoints); |
| 293 | |
| 294 | emission_mask = memnew(ConfirmationDialog); |
| 295 | emission_mask->set_title(TTR("Load Emission Mask" )); |
| 296 | VBoxContainer *emvb = memnew(VBoxContainer); |
| 297 | emission_mask->add_child(emvb); |
| 298 | emission_mask_mode = memnew(OptionButton); |
| 299 | emvb->add_margin_child(TTR("Emission Mask" ), emission_mask_mode); |
| 300 | emission_mask_mode->add_item(TTR("Solid Pixels" ), EMISSION_MODE_SOLID); |
| 301 | emission_mask_mode->add_item(TTR("Border Pixels" ), EMISSION_MODE_BORDER); |
| 302 | emission_mask_mode->add_item(TTR("Directed Border Pixels" ), EMISSION_MODE_BORDER_DIRECTED); |
| 303 | VBoxContainer *optionsvb = memnew(VBoxContainer); |
| 304 | emvb->add_margin_child(TTR("Options" ), optionsvb); |
| 305 | emission_mask_centered = memnew(CheckBox); |
| 306 | emission_mask_centered->set_text(TTR("Centered" )); |
| 307 | optionsvb->add_child(emission_mask_centered); |
| 308 | emission_colors = memnew(CheckBox); |
| 309 | emission_colors->set_text(TTR("Capture Colors from Pixel" )); |
| 310 | optionsvb->add_child(emission_colors); |
| 311 | |
| 312 | toolbar->add_child(emission_mask); |
| 313 | |
| 314 | emission_mask->connect("confirmed" , callable_mp(this, &CPUParticles2DEditorPlugin::_generate_emission_mask)); |
| 315 | } |
| 316 | |
| 317 | CPUParticles2DEditorPlugin::~CPUParticles2DEditorPlugin() { |
| 318 | } |
| 319 | |