| 1 | /**************************************************************************/ |
| 2 | /* gpu_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 "gpu_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/gui/menu_button.h" |
| 41 | #include "scene/gui/separator.h" |
| 42 | #include "scene/resources/image_texture.h" |
| 43 | #include "scene/resources/particle_process_material.h" |
| 44 | |
| 45 | void GPUParticles2DEditorPlugin::edit(Object *p_object) { |
| 46 | particles = Object::cast_to<GPUParticles2D>(p_object); |
| 47 | } |
| 48 | |
| 49 | bool GPUParticles2DEditorPlugin::handles(Object *p_object) const { |
| 50 | return p_object->is_class("GPUParticles2D" ); |
| 51 | } |
| 52 | |
| 53 | void GPUParticles2DEditorPlugin::make_visible(bool p_visible) { |
| 54 | if (p_visible) { |
| 55 | toolbar->show(); |
| 56 | } else { |
| 57 | toolbar->hide(); |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | void GPUParticles2DEditorPlugin::_file_selected(const String &p_file) { |
| 62 | source_emission_file = p_file; |
| 63 | emission_mask->popup_centered(); |
| 64 | } |
| 65 | |
| 66 | void GPUParticles2DEditorPlugin::_selection_changed() { |
| 67 | List<Node *> selected_nodes = EditorNode::get_singleton()->get_editor_selection()->get_selected_node_list(); |
| 68 | |
| 69 | if (selected_particles.is_empty() && selected_nodes.is_empty()) { |
| 70 | return; |
| 71 | } |
| 72 | |
| 73 | for (GPUParticles2D *SP : selected_particles) { |
| 74 | SP->set_show_visibility_rect(false); |
| 75 | } |
| 76 | selected_particles.clear(); |
| 77 | |
| 78 | for (Node *P : selected_nodes) { |
| 79 | GPUParticles2D *selected_particle = Object::cast_to<GPUParticles2D>(P); |
| 80 | if (selected_particle != nullptr) { |
| 81 | selected_particle->set_show_visibility_rect(true); |
| 82 | selected_particles.push_back(selected_particle); |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | void GPUParticles2DEditorPlugin::_menu_callback(int p_idx) { |
| 88 | switch (p_idx) { |
| 89 | case MENU_GENERATE_VISIBILITY_RECT: { |
| 90 | // Add one second to the default generation lifetime, since the progress is updated every second. |
| 91 | generate_seconds->set_value(MAX(1.0, trunc(particles->get_lifetime()) + 1.0)); |
| 92 | |
| 93 | if (generate_seconds->get_value() >= 11.0 + CMP_EPSILON) { |
| 94 | // Only pop up the time dialog if the particle's lifetime is long enough to warrant shortening it. |
| 95 | generate_visibility_rect->popup_centered(); |
| 96 | } else { |
| 97 | // Generate the visibility rect immediately. |
| 98 | _generate_visibility_rect(); |
| 99 | } |
| 100 | } break; |
| 101 | case MENU_LOAD_EMISSION_MASK: { |
| 102 | file->popup_file_dialog(); |
| 103 | |
| 104 | } break; |
| 105 | case MENU_CLEAR_EMISSION_MASK: { |
| 106 | emission_mask->popup_centered(); |
| 107 | } break; |
| 108 | case MENU_OPTION_CONVERT_TO_CPU_PARTICLES: { |
| 109 | CPUParticles2D *cpu_particles = memnew(CPUParticles2D); |
| 110 | cpu_particles->convert_from_particles(particles); |
| 111 | cpu_particles->set_name(particles->get_name()); |
| 112 | cpu_particles->set_transform(particles->get_transform()); |
| 113 | cpu_particles->set_visible(particles->is_visible()); |
| 114 | cpu_particles->set_process_mode(particles->get_process_mode()); |
| 115 | cpu_particles->set_z_index(particles->get_z_index()); |
| 116 | |
| 117 | EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); |
| 118 | ur->create_action(TTR("Convert to CPUParticles2D" )); |
| 119 | SceneTreeDock::get_singleton()->replace_node(particles, cpu_particles); |
| 120 | ur->commit_action(false); |
| 121 | |
| 122 | } break; |
| 123 | case MENU_RESTART: { |
| 124 | particles->restart(); |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | void GPUParticles2DEditorPlugin::_generate_visibility_rect() { |
| 130 | double time = generate_seconds->get_value(); |
| 131 | |
| 132 | float running = 0.0; |
| 133 | |
| 134 | EditorProgress ep("gen_vrect" , TTR("Generating Visibility Rect (Waiting for Particle Simulation)" ), int(time)); |
| 135 | |
| 136 | bool was_emitting = particles->is_emitting(); |
| 137 | if (!was_emitting) { |
| 138 | particles->set_emitting(true); |
| 139 | OS::get_singleton()->delay_usec(1000); |
| 140 | } |
| 141 | |
| 142 | Rect2 rect; |
| 143 | while (running < time) { |
| 144 | uint64_t ticks = OS::get_singleton()->get_ticks_usec(); |
| 145 | ep.step("Generating..." , int(running), true); |
| 146 | OS::get_singleton()->delay_usec(1000); |
| 147 | |
| 148 | Rect2 capture = particles->capture_rect(); |
| 149 | if (rect == Rect2()) { |
| 150 | rect = capture; |
| 151 | } else { |
| 152 | rect = rect.merge(capture); |
| 153 | } |
| 154 | |
| 155 | running += (OS::get_singleton()->get_ticks_usec() - ticks) / 1000000.0; |
| 156 | } |
| 157 | |
| 158 | if (!was_emitting) { |
| 159 | particles->set_emitting(false); |
| 160 | } |
| 161 | |
| 162 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
| 163 | undo_redo->create_action(TTR("Generate Visibility Rect" )); |
| 164 | undo_redo->add_do_method(particles, "set_visibility_rect" , rect); |
| 165 | undo_redo->add_undo_method(particles, "set_visibility_rect" , particles->get_visibility_rect()); |
| 166 | undo_redo->commit_action(); |
| 167 | } |
| 168 | |
| 169 | void GPUParticles2DEditorPlugin::_generate_emission_mask() { |
| 170 | Ref<ParticleProcessMaterial> pm = particles->get_process_material(); |
| 171 | if (!pm.is_valid()) { |
| 172 | EditorNode::get_singleton()->show_warning(TTR("Can only set point into a ParticleProcessMaterial process material" )); |
| 173 | return; |
| 174 | } |
| 175 | |
| 176 | Ref<Image> img; |
| 177 | img.instantiate(); |
| 178 | Error err = ImageLoader::load_image(source_emission_file, img); |
| 179 | ERR_FAIL_COND_MSG(err != OK, "Error loading image '" + source_emission_file + "'." ); |
| 180 | |
| 181 | if (img->is_compressed()) { |
| 182 | img->decompress(); |
| 183 | } |
| 184 | img->convert(Image::FORMAT_RGBA8); |
| 185 | ERR_FAIL_COND(img->get_format() != Image::FORMAT_RGBA8); |
| 186 | Size2i s = img->get_size(); |
| 187 | ERR_FAIL_COND(s.width == 0 || s.height == 0); |
| 188 | |
| 189 | Vector<Point2> valid_positions; |
| 190 | Vector<Point2> valid_normals; |
| 191 | Vector<uint8_t> valid_colors; |
| 192 | |
| 193 | valid_positions.resize(s.width * s.height); |
| 194 | |
| 195 | EmissionMode emode = (EmissionMode)emission_mask_mode->get_selected(); |
| 196 | |
| 197 | if (emode == EMISSION_MODE_BORDER_DIRECTED) { |
| 198 | valid_normals.resize(s.width * s.height); |
| 199 | } |
| 200 | |
| 201 | bool capture_colors = emission_colors->is_pressed(); |
| 202 | |
| 203 | if (capture_colors) { |
| 204 | valid_colors.resize(s.width * s.height * 4); |
| 205 | } |
| 206 | |
| 207 | int vpc = 0; |
| 208 | |
| 209 | { |
| 210 | Vector<uint8_t> img_data = img->get_data(); |
| 211 | const uint8_t *r = img_data.ptr(); |
| 212 | |
| 213 | for (int i = 0; i < s.width; i++) { |
| 214 | for (int j = 0; j < s.height; j++) { |
| 215 | uint8_t a = r[(j * s.width + i) * 4 + 3]; |
| 216 | |
| 217 | if (a > 128) { |
| 218 | if (emode == EMISSION_MODE_SOLID) { |
| 219 | if (capture_colors) { |
| 220 | valid_colors.write[vpc * 4 + 0] = r[(j * s.width + i) * 4 + 0]; |
| 221 | valid_colors.write[vpc * 4 + 1] = r[(j * s.width + i) * 4 + 1]; |
| 222 | valid_colors.write[vpc * 4 + 2] = r[(j * s.width + i) * 4 + 2]; |
| 223 | valid_colors.write[vpc * 4 + 3] = r[(j * s.width + i) * 4 + 3]; |
| 224 | } |
| 225 | valid_positions.write[vpc++] = Point2(i, j); |
| 226 | |
| 227 | } else { |
| 228 | bool on_border = false; |
| 229 | for (int x = i - 1; x <= i + 1; x++) { |
| 230 | for (int y = j - 1; y <= j + 1; y++) { |
| 231 | if (x < 0 || y < 0 || x >= s.width || y >= s.height || r[(y * s.width + x) * 4 + 3] <= 128) { |
| 232 | on_border = true; |
| 233 | break; |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | if (on_border) { |
| 238 | break; |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | if (on_border) { |
| 243 | valid_positions.write[vpc] = Point2(i, j); |
| 244 | |
| 245 | if (emode == EMISSION_MODE_BORDER_DIRECTED) { |
| 246 | Vector2 normal; |
| 247 | for (int x = i - 2; x <= i + 2; x++) { |
| 248 | for (int y = j - 2; y <= j + 2; y++) { |
| 249 | if (x == i && y == j) { |
| 250 | continue; |
| 251 | } |
| 252 | |
| 253 | if (x < 0 || y < 0 || x >= s.width || y >= s.height || r[(y * s.width + x) * 4 + 3] <= 128) { |
| 254 | normal += Vector2(x - i, y - j).normalized(); |
| 255 | } |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | normal.normalize(); |
| 260 | valid_normals.write[vpc] = normal; |
| 261 | } |
| 262 | |
| 263 | if (capture_colors) { |
| 264 | valid_colors.write[vpc * 4 + 0] = r[(j * s.width + i) * 4 + 0]; |
| 265 | valid_colors.write[vpc * 4 + 1] = r[(j * s.width + i) * 4 + 1]; |
| 266 | valid_colors.write[vpc * 4 + 2] = r[(j * s.width + i) * 4 + 2]; |
| 267 | valid_colors.write[vpc * 4 + 3] = r[(j * s.width + i) * 4 + 3]; |
| 268 | } |
| 269 | |
| 270 | vpc++; |
| 271 | } |
| 272 | } |
| 273 | } |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | valid_positions.resize(vpc); |
| 279 | if (valid_normals.size()) { |
| 280 | valid_normals.resize(vpc); |
| 281 | } |
| 282 | |
| 283 | ERR_FAIL_COND_MSG(valid_positions.size() == 0, "No pixels with transparency > 128 in image..." ); |
| 284 | |
| 285 | Vector<uint8_t> texdata; |
| 286 | |
| 287 | int w = 2048; |
| 288 | int h = (vpc / 2048) + 1; |
| 289 | |
| 290 | texdata.resize(w * h * 2 * sizeof(float)); |
| 291 | |
| 292 | { |
| 293 | Vector2 offset; |
| 294 | if (emission_mask_centered->is_pressed()) { |
| 295 | offset = Vector2(-s.width * 0.5, -s.height * 0.5); |
| 296 | } |
| 297 | |
| 298 | uint8_t *tw = texdata.ptrw(); |
| 299 | float *twf = reinterpret_cast<float *>(tw); |
| 300 | for (int i = 0; i < vpc; i++) { |
| 301 | twf[i * 2 + 0] = valid_positions[i].x + offset.x; |
| 302 | twf[i * 2 + 1] = valid_positions[i].y + offset.y; |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | img.instantiate(); |
| 307 | img->set_data(w, h, false, Image::FORMAT_RGF, texdata); |
| 308 | pm->set_emission_point_texture(ImageTexture::create_from_image(img)); |
| 309 | pm->set_emission_point_count(vpc); |
| 310 | |
| 311 | if (capture_colors) { |
| 312 | Vector<uint8_t> colordata; |
| 313 | colordata.resize(w * h * 4); //use RG texture |
| 314 | |
| 315 | { |
| 316 | uint8_t *tw = colordata.ptrw(); |
| 317 | for (int i = 0; i < vpc * 4; i++) { |
| 318 | tw[i] = valid_colors[i]; |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | img.instantiate(); |
| 323 | img->set_data(w, h, false, Image::FORMAT_RGBA8, colordata); |
| 324 | pm->set_emission_color_texture(ImageTexture::create_from_image(img)); |
| 325 | } |
| 326 | |
| 327 | if (valid_normals.size()) { |
| 328 | pm->set_emission_shape(ParticleProcessMaterial::EMISSION_SHAPE_DIRECTED_POINTS); |
| 329 | |
| 330 | Vector<uint8_t> normdata; |
| 331 | normdata.resize(w * h * 2 * sizeof(float)); //use RG texture |
| 332 | |
| 333 | { |
| 334 | uint8_t *tw = normdata.ptrw(); |
| 335 | float *twf = reinterpret_cast<float *>(tw); |
| 336 | for (int i = 0; i < vpc; i++) { |
| 337 | twf[i * 2 + 0] = valid_normals[i].x; |
| 338 | twf[i * 2 + 1] = valid_normals[i].y; |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | img.instantiate(); |
| 343 | img->set_data(w, h, false, Image::FORMAT_RGF, normdata); |
| 344 | pm->set_emission_normal_texture(ImageTexture::create_from_image(img)); |
| 345 | |
| 346 | } else { |
| 347 | pm->set_emission_shape(ParticleProcessMaterial::EMISSION_SHAPE_POINTS); |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | void GPUParticles2DEditorPlugin::_notification(int p_what) { |
| 352 | switch (p_what) { |
| 353 | case NOTIFICATION_ENTER_TREE: { |
| 354 | menu->get_popup()->connect("id_pressed" , callable_mp(this, &GPUParticles2DEditorPlugin::_menu_callback)); |
| 355 | menu->set_icon(menu->get_editor_theme_icon(SNAME("GPUParticles2D" ))); |
| 356 | file->connect("file_selected" , callable_mp(this, &GPUParticles2DEditorPlugin::_file_selected)); |
| 357 | EditorNode::get_singleton()->get_editor_selection()->connect("selection_changed" , callable_mp(this, &GPUParticles2DEditorPlugin::_selection_changed)); |
| 358 | } break; |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | void GPUParticles2DEditorPlugin::_bind_methods() { |
| 363 | } |
| 364 | |
| 365 | GPUParticles2DEditorPlugin::GPUParticles2DEditorPlugin() { |
| 366 | particles = nullptr; |
| 367 | |
| 368 | toolbar = memnew(HBoxContainer); |
| 369 | add_control_to_container(CONTAINER_CANVAS_EDITOR_MENU, toolbar); |
| 370 | toolbar->hide(); |
| 371 | |
| 372 | menu = memnew(MenuButton); |
| 373 | menu->get_popup()->add_item(TTR("Restart" ), MENU_RESTART); |
| 374 | menu->get_popup()->add_item(TTR("Generate Visibility Rect" ), MENU_GENERATE_VISIBILITY_RECT); |
| 375 | menu->get_popup()->add_item(TTR("Load Emission Mask" ), MENU_LOAD_EMISSION_MASK); |
| 376 | // menu->get_popup()->add_item(TTR("Clear Emission Mask"), MENU_CLEAR_EMISSION_MASK); |
| 377 | menu->get_popup()->add_item(TTR("Convert to CPUParticles2D" ), MENU_OPTION_CONVERT_TO_CPU_PARTICLES); |
| 378 | menu->set_text(TTR("GPUParticles2D" )); |
| 379 | menu->set_switch_on_hover(true); |
| 380 | toolbar->add_child(menu); |
| 381 | |
| 382 | file = memnew(EditorFileDialog); |
| 383 | List<String> ext; |
| 384 | ImageLoader::get_recognized_extensions(&ext); |
| 385 | for (const String &E : ext) { |
| 386 | file->add_filter("*." + E, E.to_upper()); |
| 387 | } |
| 388 | file->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE); |
| 389 | toolbar->add_child(file); |
| 390 | |
| 391 | epoints = memnew(SpinBox); |
| 392 | epoints->set_min(1); |
| 393 | epoints->set_max(8192); |
| 394 | epoints->set_step(1); |
| 395 | epoints->set_value(512); |
| 396 | file->get_vbox()->add_margin_child(TTR("Generated Point Count:" ), epoints); |
| 397 | |
| 398 | generate_visibility_rect = memnew(ConfirmationDialog); |
| 399 | generate_visibility_rect->set_title(TTR("Generate Visibility Rect" )); |
| 400 | VBoxContainer *genvb = memnew(VBoxContainer); |
| 401 | generate_visibility_rect->add_child(genvb); |
| 402 | generate_seconds = memnew(SpinBox); |
| 403 | genvb->add_margin_child(TTR("Generation Time (sec):" ), generate_seconds); |
| 404 | generate_seconds->set_min(0.1); |
| 405 | generate_seconds->set_max(25); |
| 406 | generate_seconds->set_value(2); |
| 407 | |
| 408 | toolbar->add_child(generate_visibility_rect); |
| 409 | |
| 410 | generate_visibility_rect->connect("confirmed" , callable_mp(this, &GPUParticles2DEditorPlugin::_generate_visibility_rect)); |
| 411 | |
| 412 | emission_mask = memnew(ConfirmationDialog); |
| 413 | emission_mask->set_title(TTR("Load Emission Mask" )); |
| 414 | VBoxContainer *emvb = memnew(VBoxContainer); |
| 415 | emission_mask->add_child(emvb); |
| 416 | emission_mask_mode = memnew(OptionButton); |
| 417 | emvb->add_margin_child(TTR("Emission Mask" ), emission_mask_mode); |
| 418 | emission_mask_mode->add_item(TTR("Solid Pixels" ), EMISSION_MODE_SOLID); |
| 419 | emission_mask_mode->add_item(TTR("Border Pixels" ), EMISSION_MODE_BORDER); |
| 420 | emission_mask_mode->add_item(TTR("Directed Border Pixels" ), EMISSION_MODE_BORDER_DIRECTED); |
| 421 | VBoxContainer *optionsvb = memnew(VBoxContainer); |
| 422 | emvb->add_margin_child(TTR("Options" ), optionsvb); |
| 423 | emission_mask_centered = memnew(CheckBox); |
| 424 | emission_mask_centered->set_text(TTR("Centered" )); |
| 425 | optionsvb->add_child(emission_mask_centered); |
| 426 | emission_colors = memnew(CheckBox); |
| 427 | emission_colors->set_text(TTR("Capture Colors from Pixel" )); |
| 428 | optionsvb->add_child(emission_colors); |
| 429 | |
| 430 | toolbar->add_child(emission_mask); |
| 431 | |
| 432 | emission_mask->connect("confirmed" , callable_mp(this, &GPUParticles2DEditorPlugin::_generate_emission_mask)); |
| 433 | } |
| 434 | |
| 435 | GPUParticles2DEditorPlugin::~GPUParticles2DEditorPlugin() { |
| 436 | } |
| 437 | |