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