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 | |