1 | /**************************************************************************/ |
2 | /* light_3d_gizmo_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 "light_3d_gizmo_plugin.h" |
32 | |
33 | #include "core/config/project_settings.h" |
34 | #include "editor/editor_node.h" |
35 | #include "editor/editor_settings.h" |
36 | #include "editor/editor_string_names.h" |
37 | #include "editor/editor_undo_redo_manager.h" |
38 | #include "editor/plugins/node_3d_editor_plugin.h" |
39 | #include "scene/3d/light_3d.h" |
40 | |
41 | Light3DGizmoPlugin::Light3DGizmoPlugin() { |
42 | // Enable vertex colors for the materials below as the gizmo color depends on the light color. |
43 | create_material("lines_primary" , Color(1, 1, 1), false, false, true); |
44 | create_material("lines_secondary" , Color(1, 1, 1, 0.35), false, false, true); |
45 | create_material("lines_billboard" , Color(1, 1, 1), true, false, true); |
46 | |
47 | create_icon_material("light_directional_icon" , EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("GizmoDirectionalLight" ), EditorStringName(EditorIcons))); |
48 | create_icon_material("light_omni_icon" , EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("GizmoLight" ), EditorStringName(EditorIcons))); |
49 | create_icon_material("light_spot_icon" , EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("GizmoSpotLight" ), EditorStringName(EditorIcons))); |
50 | |
51 | create_handle_material("handles" ); |
52 | create_handle_material("handles_billboard" , true); |
53 | } |
54 | |
55 | bool Light3DGizmoPlugin::has_gizmo(Node3D *p_spatial) { |
56 | return Object::cast_to<Light3D>(p_spatial) != nullptr; |
57 | } |
58 | |
59 | String Light3DGizmoPlugin::get_gizmo_name() const { |
60 | return "Light3D" ; |
61 | } |
62 | |
63 | int Light3DGizmoPlugin::get_priority() const { |
64 | return -1; |
65 | } |
66 | |
67 | String Light3DGizmoPlugin::get_handle_name(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary) const { |
68 | if (p_id == 0) { |
69 | return "Radius" ; |
70 | } else { |
71 | return "Aperture" ; |
72 | } |
73 | } |
74 | |
75 | Variant Light3DGizmoPlugin::get_handle_value(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary) const { |
76 | Light3D *light = Object::cast_to<Light3D>(p_gizmo->get_node_3d()); |
77 | if (p_id == 0) { |
78 | return light->get_param(Light3D::PARAM_RANGE); |
79 | } |
80 | if (p_id == 1) { |
81 | return light->get_param(Light3D::PARAM_SPOT_ANGLE); |
82 | } |
83 | |
84 | return Variant(); |
85 | } |
86 | |
87 | void Light3DGizmoPlugin::set_handle(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary, Camera3D *p_camera, const Point2 &p_point) { |
88 | Light3D *light = Object::cast_to<Light3D>(p_gizmo->get_node_3d()); |
89 | Transform3D gt = light->get_global_transform(); |
90 | Transform3D gi = gt.affine_inverse(); |
91 | |
92 | Vector3 ray_from = p_camera->project_ray_origin(p_point); |
93 | Vector3 ray_dir = p_camera->project_ray_normal(p_point); |
94 | |
95 | Vector3 s[2] = { gi.xform(ray_from), gi.xform(ray_from + ray_dir * 4096) }; |
96 | if (p_id == 0) { |
97 | if (Object::cast_to<SpotLight3D>(light)) { |
98 | Vector3 ra, rb; |
99 | Geometry3D::get_closest_points_between_segments(Vector3(), Vector3(0, 0, -4096), s[0], s[1], ra, rb); |
100 | |
101 | float d = -ra.z; |
102 | if (Node3DEditor::get_singleton()->is_snap_enabled()) { |
103 | d = Math::snapped(d, Node3DEditor::get_singleton()->get_translate_snap()); |
104 | } |
105 | |
106 | if (d <= 0) { // Equal is here for negative zero. |
107 | d = 0; |
108 | } |
109 | |
110 | light->set_param(Light3D::PARAM_RANGE, d); |
111 | } else if (Object::cast_to<OmniLight3D>(light)) { |
112 | Plane cp = Plane(p_camera->get_transform().basis.get_column(2), gt.origin); |
113 | |
114 | Vector3 inters; |
115 | if (cp.intersects_ray(ray_from, ray_dir, &inters)) { |
116 | float r = inters.distance_to(gt.origin); |
117 | if (Node3DEditor::get_singleton()->is_snap_enabled()) { |
118 | r = Math::snapped(r, Node3DEditor::get_singleton()->get_translate_snap()); |
119 | } |
120 | |
121 | light->set_param(Light3D::PARAM_RANGE, r); |
122 | } |
123 | } |
124 | |
125 | } else if (p_id == 1) { |
126 | float a = _find_closest_angle_to_half_pi_arc(s[0], s[1], light->get_param(Light3D::PARAM_RANGE), gt); |
127 | light->set_param(Light3D::PARAM_SPOT_ANGLE, CLAMP(a, 0.01, 89.99)); |
128 | } |
129 | } |
130 | |
131 | void Light3DGizmoPlugin::commit_handle(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel) { |
132 | Light3D *light = Object::cast_to<Light3D>(p_gizmo->get_node_3d()); |
133 | if (p_cancel) { |
134 | light->set_param(p_id == 0 ? Light3D::PARAM_RANGE : Light3D::PARAM_SPOT_ANGLE, p_restore); |
135 | |
136 | } else if (p_id == 0) { |
137 | EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); |
138 | ur->create_action(TTR("Change Light Radius" )); |
139 | ur->add_do_method(light, "set_param" , Light3D::PARAM_RANGE, light->get_param(Light3D::PARAM_RANGE)); |
140 | ur->add_undo_method(light, "set_param" , Light3D::PARAM_RANGE, p_restore); |
141 | ur->commit_action(); |
142 | } else if (p_id == 1) { |
143 | EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); |
144 | ur->create_action(TTR("Change Light Radius" )); |
145 | ur->add_do_method(light, "set_param" , Light3D::PARAM_SPOT_ANGLE, light->get_param(Light3D::PARAM_SPOT_ANGLE)); |
146 | ur->add_undo_method(light, "set_param" , Light3D::PARAM_SPOT_ANGLE, p_restore); |
147 | ur->commit_action(); |
148 | } |
149 | } |
150 | |
151 | void Light3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) { |
152 | Light3D *light = Object::cast_to<Light3D>(p_gizmo->get_node_3d()); |
153 | |
154 | Color color = light->get_color().srgb_to_linear() * light->get_correlated_color().srgb_to_linear(); |
155 | color = color.linear_to_srgb(); |
156 | // Make the gizmo color as bright as possible for better visibility |
157 | color.set_hsv(color.get_h(), color.get_s(), 1); |
158 | |
159 | p_gizmo->clear(); |
160 | |
161 | if (Object::cast_to<DirectionalLight3D>(light)) { |
162 | if (p_gizmo->is_selected()) { |
163 | Ref<Material> material = get_material("lines_primary" , p_gizmo); |
164 | |
165 | const int arrow_points = 7; |
166 | const float arrow_length = 1.5; |
167 | |
168 | Vector3 arrow[arrow_points] = { |
169 | Vector3(0, 0, -1), |
170 | Vector3(0, 0.8, 0), |
171 | Vector3(0, 0.3, 0), |
172 | Vector3(0, 0.3, arrow_length), |
173 | Vector3(0, -0.3, arrow_length), |
174 | Vector3(0, -0.3, 0), |
175 | Vector3(0, -0.8, 0) |
176 | }; |
177 | |
178 | int arrow_sides = 2; |
179 | |
180 | Vector<Vector3> lines; |
181 | |
182 | for (int i = 0; i < arrow_sides; i++) { |
183 | for (int j = 0; j < arrow_points; j++) { |
184 | Basis ma(Vector3(0, 0, 1), Math_PI * i / arrow_sides); |
185 | |
186 | Vector3 v1 = arrow[j] - Vector3(0, 0, arrow_length); |
187 | Vector3 v2 = arrow[(j + 1) % arrow_points] - Vector3(0, 0, arrow_length); |
188 | |
189 | lines.push_back(ma.xform(v1)); |
190 | lines.push_back(ma.xform(v2)); |
191 | } |
192 | } |
193 | |
194 | p_gizmo->add_lines(lines, material, false, color); |
195 | } |
196 | |
197 | Ref<Material> icon = get_material("light_directional_icon" , p_gizmo); |
198 | p_gizmo->add_unscaled_billboard(icon, 0.05, color); |
199 | } |
200 | |
201 | if (Object::cast_to<OmniLight3D>(light)) { |
202 | if (p_gizmo->is_selected()) { |
203 | // Use both a billboard circle and 3 non-billboard circles for a better sphere-like representation |
204 | const Ref<Material> lines_material = get_material("lines_secondary" , p_gizmo); |
205 | const Ref<Material> lines_billboard_material = get_material("lines_billboard" , p_gizmo); |
206 | |
207 | OmniLight3D *on = Object::cast_to<OmniLight3D>(light); |
208 | const float r = on->get_param(Light3D::PARAM_RANGE); |
209 | Vector<Vector3> points; |
210 | Vector<Vector3> points_billboard; |
211 | |
212 | for (int i = 0; i < 120; i++) { |
213 | // Create a circle |
214 | const float ra = Math::deg_to_rad((float)(i * 3)); |
215 | const float rb = Math::deg_to_rad((float)((i + 1) * 3)); |
216 | const Point2 a = Vector2(Math::sin(ra), Math::cos(ra)) * r; |
217 | const Point2 b = Vector2(Math::sin(rb), Math::cos(rb)) * r; |
218 | |
219 | // Draw axis-aligned circles |
220 | points.push_back(Vector3(a.x, 0, a.y)); |
221 | points.push_back(Vector3(b.x, 0, b.y)); |
222 | points.push_back(Vector3(0, a.x, a.y)); |
223 | points.push_back(Vector3(0, b.x, b.y)); |
224 | points.push_back(Vector3(a.x, a.y, 0)); |
225 | points.push_back(Vector3(b.x, b.y, 0)); |
226 | |
227 | // Draw a billboarded circle |
228 | points_billboard.push_back(Vector3(a.x, a.y, 0)); |
229 | points_billboard.push_back(Vector3(b.x, b.y, 0)); |
230 | } |
231 | |
232 | p_gizmo->add_lines(points, lines_material, true, color); |
233 | p_gizmo->add_lines(points_billboard, lines_billboard_material, true, color); |
234 | |
235 | Vector<Vector3> handles; |
236 | handles.push_back(Vector3(r, 0, 0)); |
237 | p_gizmo->add_handles(handles, get_material("handles_billboard" ), Vector<int>(), true); |
238 | } |
239 | |
240 | const Ref<Material> icon = get_material("light_omni_icon" , p_gizmo); |
241 | p_gizmo->add_unscaled_billboard(icon, 0.05, color); |
242 | } |
243 | |
244 | if (Object::cast_to<SpotLight3D>(light)) { |
245 | if (p_gizmo->is_selected()) { |
246 | const Ref<Material> material_primary = get_material("lines_primary" , p_gizmo); |
247 | const Ref<Material> material_secondary = get_material("lines_secondary" , p_gizmo); |
248 | |
249 | Vector<Vector3> points_primary; |
250 | Vector<Vector3> points_secondary; |
251 | SpotLight3D *sl = Object::cast_to<SpotLight3D>(light); |
252 | |
253 | float r = sl->get_param(Light3D::PARAM_RANGE); |
254 | float w = r * Math::sin(Math::deg_to_rad(sl->get_param(Light3D::PARAM_SPOT_ANGLE))); |
255 | float d = r * Math::cos(Math::deg_to_rad(sl->get_param(Light3D::PARAM_SPOT_ANGLE))); |
256 | |
257 | for (int i = 0; i < 120; i++) { |
258 | // Draw a circle |
259 | const float ra = Math::deg_to_rad((float)(i * 3)); |
260 | const float rb = Math::deg_to_rad((float)((i + 1) * 3)); |
261 | const Point2 a = Vector2(Math::sin(ra), Math::cos(ra)) * w; |
262 | const Point2 b = Vector2(Math::sin(rb), Math::cos(rb)) * w; |
263 | |
264 | points_primary.push_back(Vector3(a.x, a.y, -d)); |
265 | points_primary.push_back(Vector3(b.x, b.y, -d)); |
266 | |
267 | if (i % 15 == 0) { |
268 | // Draw 8 lines from the cone origin to the sides of the circle |
269 | points_secondary.push_back(Vector3(a.x, a.y, -d)); |
270 | points_secondary.push_back(Vector3()); |
271 | } |
272 | } |
273 | |
274 | points_primary.push_back(Vector3(0, 0, -r)); |
275 | points_primary.push_back(Vector3()); |
276 | |
277 | p_gizmo->add_lines(points_primary, material_primary, false, color); |
278 | p_gizmo->add_lines(points_secondary, material_secondary, false, color); |
279 | |
280 | Vector<Vector3> handles = { |
281 | Vector3(0, 0, -r), |
282 | Vector3(w, 0, -d) |
283 | }; |
284 | |
285 | p_gizmo->add_handles(handles, get_material("handles" )); |
286 | } |
287 | |
288 | const Ref<Material> icon = get_material("light_spot_icon" , p_gizmo); |
289 | p_gizmo->add_unscaled_billboard(icon, 0.05, color); |
290 | } |
291 | } |
292 | |
293 | float Light3DGizmoPlugin::_find_closest_angle_to_half_pi_arc(const Vector3 &p_from, const Vector3 &p_to, float p_arc_radius, const Transform3D &p_arc_xform) { |
294 | //bleh, discrete is simpler |
295 | static const int arc_test_points = 64; |
296 | float min_d = 1e20; |
297 | Vector3 min_p; |
298 | |
299 | for (int i = 0; i < arc_test_points; i++) { |
300 | float a = i * Math_PI * 0.5 / arc_test_points; |
301 | float an = (i + 1) * Math_PI * 0.5 / arc_test_points; |
302 | Vector3 p = Vector3(Math::cos(a), 0, -Math::sin(a)) * p_arc_radius; |
303 | Vector3 n = Vector3(Math::cos(an), 0, -Math::sin(an)) * p_arc_radius; |
304 | |
305 | Vector3 ra, rb; |
306 | Geometry3D::get_closest_points_between_segments(p, n, p_from, p_to, ra, rb); |
307 | |
308 | float d = ra.distance_to(rb); |
309 | if (d < min_d) { |
310 | min_d = d; |
311 | min_p = ra; |
312 | } |
313 | } |
314 | |
315 | //min_p = p_arc_xform.affine_inverse().xform(min_p); |
316 | float a = (Math_PI * 0.5) - Vector2(min_p.x, -min_p.z).angle(); |
317 | return Math::rad_to_deg(a); |
318 | } |
319 | |