1/**************************************************************************/
2/* audio_stream_player_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 "audio_stream_player_3d_gizmo_plugin.h"
32
33#include "editor/editor_node.h"
34#include "editor/editor_settings.h"
35#include "editor/editor_string_names.h"
36#include "editor/editor_undo_redo_manager.h"
37#include "editor/plugins/node_3d_editor_plugin.h"
38#include "scene/3d/audio_stream_player_3d.h"
39
40AudioStreamPlayer3DGizmoPlugin::AudioStreamPlayer3DGizmoPlugin() {
41 Color gizmo_color = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/stream_player_3d", Color(0.4, 0.8, 1));
42
43 create_icon_material("stream_player_3d_icon", EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("Gizmo3DSamplePlayer"), EditorStringName(EditorIcons)));
44 create_material("stream_player_3d_material_primary", gizmo_color);
45 create_material("stream_player_3d_material_secondary", gizmo_color * Color(1, 1, 1, 0.35));
46 // Enable vertex colors for the billboard material as the gizmo color depends on the
47 // AudioStreamPlayer3D attenuation type and source (Unit Size or Max Distance).
48 create_material("stream_player_3d_material_billboard", Color(1, 1, 1), true, false, true);
49 create_handle_material("handles");
50}
51
52bool AudioStreamPlayer3DGizmoPlugin::has_gizmo(Node3D *p_spatial) {
53 return Object::cast_to<AudioStreamPlayer3D>(p_spatial) != nullptr;
54}
55
56String AudioStreamPlayer3DGizmoPlugin::get_gizmo_name() const {
57 return "AudioStreamPlayer3D";
58}
59
60int AudioStreamPlayer3DGizmoPlugin::get_priority() const {
61 return -1;
62}
63
64String AudioStreamPlayer3DGizmoPlugin::get_handle_name(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary) const {
65 return "Emission Radius";
66}
67
68Variant AudioStreamPlayer3DGizmoPlugin::get_handle_value(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary) const {
69 AudioStreamPlayer3D *player = Object::cast_to<AudioStreamPlayer3D>(p_gizmo->get_node_3d());
70 return player->get_emission_angle();
71}
72
73void AudioStreamPlayer3DGizmoPlugin::set_handle(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary, Camera3D *p_camera, const Point2 &p_point) {
74 AudioStreamPlayer3D *player = Object::cast_to<AudioStreamPlayer3D>(p_gizmo->get_node_3d());
75
76 Transform3D gt = player->get_global_transform();
77 Transform3D gi = gt.affine_inverse();
78
79 Vector3 ray_from = p_camera->project_ray_origin(p_point);
80 Vector3 ray_dir = p_camera->project_ray_normal(p_point);
81 Vector3 ray_to = ray_from + ray_dir * 4096;
82
83 ray_from = gi.xform(ray_from);
84 ray_to = gi.xform(ray_to);
85
86 float closest_dist = 1e20;
87 float closest_angle = 1e20;
88
89 for (int i = 0; i < 180; i++) {
90 float a = Math::deg_to_rad((float)i);
91 float an = Math::deg_to_rad((float)(i + 1));
92
93 Vector3 from(Math::sin(a), 0, -Math::cos(a));
94 Vector3 to(Math::sin(an), 0, -Math::cos(an));
95
96 Vector3 r1, r2;
97 Geometry3D::get_closest_points_between_segments(from, to, ray_from, ray_to, r1, r2);
98 float d = r1.distance_to(r2);
99 if (d < closest_dist) {
100 closest_dist = d;
101 closest_angle = i;
102 }
103 }
104
105 if (closest_angle < 91) {
106 player->set_emission_angle(closest_angle);
107 }
108}
109
110void AudioStreamPlayer3DGizmoPlugin::commit_handle(const EditorNode3DGizmo *p_gizmo, int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel) {
111 AudioStreamPlayer3D *player = Object::cast_to<AudioStreamPlayer3D>(p_gizmo->get_node_3d());
112
113 if (p_cancel) {
114 player->set_emission_angle(p_restore);
115
116 } else {
117 EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
118 ur->create_action(TTR("Change AudioStreamPlayer3D Emission Angle"));
119 ur->add_do_method(player, "set_emission_angle", player->get_emission_angle());
120 ur->add_undo_method(player, "set_emission_angle", p_restore);
121 ur->commit_action();
122 }
123}
124
125void AudioStreamPlayer3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) {
126 p_gizmo->clear();
127
128 if (p_gizmo->is_selected()) {
129 const AudioStreamPlayer3D *player = Object::cast_to<AudioStreamPlayer3D>(p_gizmo->get_node_3d());
130
131 if (player->get_attenuation_model() != AudioStreamPlayer3D::ATTENUATION_DISABLED || player->get_max_distance() > CMP_EPSILON) {
132 // Draw a circle to represent sound volume attenuation.
133 // Use only a billboard circle to represent radius.
134 // This helps distinguish AudioStreamPlayer3D gizmos from OmniLight3D gizmos.
135 const Ref<Material> lines_billboard_material = get_material("stream_player_3d_material_billboard", p_gizmo);
136
137 // Soft distance cap varies depending on attenuation model, as some will fade out more aggressively than others.
138 // Multipliers were empirically determined through testing.
139 float soft_multiplier;
140 switch (player->get_attenuation_model()) {
141 case AudioStreamPlayer3D::ATTENUATION_INVERSE_DISTANCE:
142 soft_multiplier = 12.0;
143 break;
144 case AudioStreamPlayer3D::ATTENUATION_INVERSE_SQUARE_DISTANCE:
145 soft_multiplier = 4.0;
146 break;
147 case AudioStreamPlayer3D::ATTENUATION_LOGARITHMIC:
148 soft_multiplier = 3.25;
149 break;
150 default:
151 // Ensures Max Distance's radius visualization is not capped by Unit Size
152 // (when the attenuation mode is Disabled).
153 soft_multiplier = 10000.0;
154 break;
155 }
156
157 // Draw the distance at which the sound can be reasonably heard.
158 // This can be either a hard distance cap with the Max Distance property (if set above 0.0),
159 // or a soft distance cap with the Unit Size property (sound never reaches true zero).
160 // When Max Distance is 0.0, `r` represents the distance above which the
161 // sound can't be heard in *most* (but not all) scenarios.
162 float r;
163 if (player->get_max_distance() > CMP_EPSILON) {
164 r = MIN(player->get_unit_size() * soft_multiplier, player->get_max_distance());
165 } else {
166 r = player->get_unit_size() * soft_multiplier;
167 }
168 Vector<Vector3> points_billboard;
169
170 for (int i = 0; i < 120; i++) {
171 // Create a circle.
172 const float ra = Math::deg_to_rad((float)(i * 3));
173 const float rb = Math::deg_to_rad((float)((i + 1) * 3));
174 const Point2 a = Vector2(Math::sin(ra), Math::cos(ra)) * r;
175 const Point2 b = Vector2(Math::sin(rb), Math::cos(rb)) * r;
176
177 // Draw a billboarded circle.
178 points_billboard.push_back(Vector3(a.x, a.y, 0));
179 points_billboard.push_back(Vector3(b.x, b.y, 0));
180 }
181
182 Color color;
183 switch (player->get_attenuation_model()) {
184 // Pick cold colors for all attenuation models (except Disabled),
185 // so that soft caps can be easily distinguished from hard caps
186 // (which use warm colors).
187 case AudioStreamPlayer3D::ATTENUATION_INVERSE_DISTANCE:
188 color = Color(0.4, 0.8, 1);
189 break;
190 case AudioStreamPlayer3D::ATTENUATION_INVERSE_SQUARE_DISTANCE:
191 color = Color(0.4, 0.5, 1);
192 break;
193 case AudioStreamPlayer3D::ATTENUATION_LOGARITHMIC:
194 color = Color(0.4, 0.2, 1);
195 break;
196 default:
197 // Disabled attenuation mode.
198 // This is never reached when Max Distance is 0, but the
199 // hue-inverted form of this color will be used if Max Distance is greater than 0.
200 color = Color(1, 1, 1);
201 break;
202 }
203
204 if (player->get_max_distance() > CMP_EPSILON) {
205 // Sound is hard-capped by max distance. The attenuation model still matters,
206 // so invert the hue of the color that was chosen above.
207 color.set_h(color.get_h() + 0.5);
208 }
209
210 p_gizmo->add_lines(points_billboard, lines_billboard_material, true, color);
211 }
212
213 if (player->is_emission_angle_enabled()) {
214 const float pc = player->get_emission_angle();
215 const float ofs = -Math::cos(Math::deg_to_rad(pc));
216 const float radius = Math::sin(Math::deg_to_rad(pc));
217
218 Vector<Vector3> points_primary;
219 points_primary.resize(200);
220
221 real_t step = Math_TAU / 100.0;
222 for (int i = 0; i < 100; i++) {
223 const float a = i * step;
224 const float an = (i + 1) * step;
225
226 const Vector3 from(Math::sin(a) * radius, Math::cos(a) * radius, ofs);
227 const Vector3 to(Math::sin(an) * radius, Math::cos(an) * radius, ofs);
228
229 points_primary.write[i * 2 + 0] = from;
230 points_primary.write[i * 2 + 1] = to;
231 }
232
233 const Ref<Material> material_primary = get_material("stream_player_3d_material_primary", p_gizmo);
234 p_gizmo->add_lines(points_primary, material_primary);
235
236 Vector<Vector3> points_secondary;
237 points_secondary.resize(16);
238
239 for (int i = 0; i < 8; i++) {
240 const float a = i * (Math_TAU / 8.0);
241 const Vector3 from(Math::sin(a) * radius, Math::cos(a) * radius, ofs);
242
243 points_secondary.write[i * 2 + 0] = from;
244 points_secondary.write[i * 2 + 1] = Vector3();
245 }
246
247 const Ref<Material> material_secondary = get_material("stream_player_3d_material_secondary", p_gizmo);
248 p_gizmo->add_lines(points_secondary, material_secondary);
249
250 Vector<Vector3> handles;
251 const float ha = Math::deg_to_rad(player->get_emission_angle());
252 handles.push_back(Vector3(Math::sin(ha), 0, -Math::cos(ha)));
253 p_gizmo->add_handles(handles, get_material("handles"));
254 }
255 }
256
257 const Ref<Material> icon = get_material("stream_player_3d_icon", p_gizmo);
258 p_gizmo->add_unscaled_billboard(icon, 0.05);
259}
260