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
45void GPUParticles2DEditorPlugin::edit(Object *p_object) {
46 particles = Object::cast_to<GPUParticles2D>(p_object);
47}
48
49bool GPUParticles2DEditorPlugin::handles(Object *p_object) const {
50 return p_object->is_class("GPUParticles2D");
51}
52
53void GPUParticles2DEditorPlugin::make_visible(bool p_visible) {
54 if (p_visible) {
55 toolbar->show();
56 } else {
57 toolbar->hide();
58 }
59}
60
61void GPUParticles2DEditorPlugin::_file_selected(const String &p_file) {
62 source_emission_file = p_file;
63 emission_mask->popup_centered();
64}
65
66void 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
87void 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
129void 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
169void 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
351void 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
362void GPUParticles2DEditorPlugin::_bind_methods() {
363}
364
365GPUParticles2DEditorPlugin::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
435GPUParticles2DEditorPlugin::~GPUParticles2DEditorPlugin() {
436}
437