1 | /**************************************************************************/ |
2 | /* webxr_interface_js.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 "webxr_interface_js.h" |
32 | |
33 | #ifdef WEB_ENABLED |
34 | |
35 | #include "godot_webxr.h" |
36 | |
37 | #include "core/input/input.h" |
38 | #include "core/os/os.h" |
39 | #include "drivers/gles3/storage/texture_storage.h" |
40 | #include "scene/main/scene_tree.h" |
41 | #include "scene/main/window.h" |
42 | #include "servers/rendering/renderer_compositor.h" |
43 | #include "servers/rendering/rendering_server_globals.h" |
44 | |
45 | #include <emscripten.h> |
46 | #include <stdlib.h> |
47 | |
48 | void _emwebxr_on_session_supported(char *p_session_mode, int p_supported) { |
49 | XRServer *xr_server = XRServer::get_singleton(); |
50 | ERR_FAIL_NULL(xr_server); |
51 | |
52 | Ref<XRInterface> interface = xr_server->find_interface("WebXR" ); |
53 | ERR_FAIL_COND(interface.is_null()); |
54 | |
55 | String session_mode = String(p_session_mode); |
56 | interface->emit_signal(SNAME("session_supported" ), session_mode, p_supported ? true : false); |
57 | } |
58 | |
59 | void _emwebxr_on_session_started(char *p_reference_space_type) { |
60 | XRServer *xr_server = XRServer::get_singleton(); |
61 | ERR_FAIL_NULL(xr_server); |
62 | |
63 | Ref<XRInterface> interface = xr_server->find_interface("WebXR" ); |
64 | ERR_FAIL_COND(interface.is_null()); |
65 | |
66 | String reference_space_type = String(p_reference_space_type); |
67 | static_cast<WebXRInterfaceJS *>(interface.ptr())->_set_reference_space_type(reference_space_type); |
68 | interface->emit_signal(SNAME("session_started" )); |
69 | } |
70 | |
71 | void _emwebxr_on_session_ended() { |
72 | XRServer *xr_server = XRServer::get_singleton(); |
73 | ERR_FAIL_NULL(xr_server); |
74 | |
75 | Ref<XRInterface> interface = xr_server->find_interface("WebXR" ); |
76 | ERR_FAIL_COND(interface.is_null()); |
77 | |
78 | interface->uninitialize(); |
79 | interface->emit_signal(SNAME("session_ended" )); |
80 | } |
81 | |
82 | void _emwebxr_on_session_failed(char *p_message) { |
83 | XRServer *xr_server = XRServer::get_singleton(); |
84 | ERR_FAIL_NULL(xr_server); |
85 | |
86 | Ref<XRInterface> interface = xr_server->find_interface("WebXR" ); |
87 | ERR_FAIL_COND(interface.is_null()); |
88 | |
89 | interface->uninitialize(); |
90 | |
91 | String message = String(p_message); |
92 | interface->emit_signal(SNAME("session_failed" ), message); |
93 | } |
94 | |
95 | extern "C" EMSCRIPTEN_KEEPALIVE void _emwebxr_on_input_event(int p_event_type, int p_input_source_id) { |
96 | XRServer *xr_server = XRServer::get_singleton(); |
97 | ERR_FAIL_NULL(xr_server); |
98 | |
99 | Ref<XRInterface> interface = xr_server->find_interface("WebXR" ); |
100 | ERR_FAIL_COND(interface.is_null()); |
101 | |
102 | ((WebXRInterfaceJS *)interface.ptr())->_on_input_event(p_event_type, p_input_source_id); |
103 | } |
104 | |
105 | extern "C" EMSCRIPTEN_KEEPALIVE void _emwebxr_on_simple_event(char *p_signal_name) { |
106 | XRServer *xr_server = XRServer::get_singleton(); |
107 | ERR_FAIL_NULL(xr_server); |
108 | |
109 | Ref<XRInterface> interface = xr_server->find_interface("WebXR" ); |
110 | ERR_FAIL_COND(interface.is_null()); |
111 | |
112 | StringName signal_name = StringName(p_signal_name); |
113 | interface->emit_signal(signal_name); |
114 | } |
115 | |
116 | void WebXRInterfaceJS::is_session_supported(const String &p_session_mode) { |
117 | godot_webxr_is_session_supported(p_session_mode.utf8().get_data(), &_emwebxr_on_session_supported); |
118 | } |
119 | |
120 | void WebXRInterfaceJS::set_session_mode(String p_session_mode) { |
121 | session_mode = p_session_mode; |
122 | } |
123 | |
124 | String WebXRInterfaceJS::get_session_mode() const { |
125 | return session_mode; |
126 | } |
127 | |
128 | void WebXRInterfaceJS::set_required_features(String p_required_features) { |
129 | required_features = p_required_features; |
130 | } |
131 | |
132 | String WebXRInterfaceJS::get_required_features() const { |
133 | return required_features; |
134 | } |
135 | |
136 | void WebXRInterfaceJS::set_optional_features(String p_optional_features) { |
137 | optional_features = p_optional_features; |
138 | } |
139 | |
140 | String WebXRInterfaceJS::get_optional_features() const { |
141 | return optional_features; |
142 | } |
143 | |
144 | void WebXRInterfaceJS::set_requested_reference_space_types(String p_requested_reference_space_types) { |
145 | requested_reference_space_types = p_requested_reference_space_types; |
146 | } |
147 | |
148 | String WebXRInterfaceJS::get_requested_reference_space_types() const { |
149 | return requested_reference_space_types; |
150 | } |
151 | |
152 | void WebXRInterfaceJS::_set_reference_space_type(String p_reference_space_type) { |
153 | reference_space_type = p_reference_space_type; |
154 | } |
155 | |
156 | String WebXRInterfaceJS::get_reference_space_type() const { |
157 | return reference_space_type; |
158 | } |
159 | |
160 | bool WebXRInterfaceJS::is_input_source_active(int p_input_source_id) const { |
161 | ERR_FAIL_INDEX_V(p_input_source_id, input_source_count, false); |
162 | return input_sources[p_input_source_id].active; |
163 | } |
164 | |
165 | Ref<XRPositionalTracker> WebXRInterfaceJS::get_input_source_tracker(int p_input_source_id) const { |
166 | ERR_FAIL_INDEX_V(p_input_source_id, input_source_count, Ref<XRPositionalTracker>()); |
167 | return input_sources[p_input_source_id].tracker; |
168 | } |
169 | |
170 | WebXRInterface::TargetRayMode WebXRInterfaceJS::get_input_source_target_ray_mode(int p_input_source_id) const { |
171 | ERR_FAIL_INDEX_V(p_input_source_id, input_source_count, WebXRInterface::TARGET_RAY_MODE_UNKNOWN); |
172 | if (!input_sources[p_input_source_id].active) { |
173 | return WebXRInterface::TARGET_RAY_MODE_UNKNOWN; |
174 | } |
175 | return input_sources[p_input_source_id].target_ray_mode; |
176 | } |
177 | |
178 | String WebXRInterfaceJS::get_visibility_state() const { |
179 | char *c_str = godot_webxr_get_visibility_state(); |
180 | if (c_str) { |
181 | String visibility_state = String(c_str); |
182 | free(c_str); |
183 | |
184 | return visibility_state; |
185 | } |
186 | return String(); |
187 | } |
188 | |
189 | PackedVector3Array WebXRInterfaceJS::get_play_area() const { |
190 | PackedVector3Array ret; |
191 | |
192 | float *points; |
193 | int point_count = godot_webxr_get_bounds_geometry(&points); |
194 | if (point_count > 0) { |
195 | ret.resize(point_count); |
196 | for (int i = 0; i < point_count; i++) { |
197 | float *js_vector3 = points + (i * 3); |
198 | ret.set(i, Vector3(js_vector3[0], js_vector3[1], js_vector3[2])); |
199 | } |
200 | free(points); |
201 | } |
202 | |
203 | return ret; |
204 | } |
205 | |
206 | float WebXRInterfaceJS::get_display_refresh_rate() const { |
207 | return godot_webxr_get_frame_rate(); |
208 | } |
209 | |
210 | void WebXRInterfaceJS::set_display_refresh_rate(float p_refresh_rate) { |
211 | godot_webxr_update_target_frame_rate(p_refresh_rate); |
212 | } |
213 | |
214 | Array WebXRInterfaceJS::get_available_display_refresh_rates() const { |
215 | Array ret; |
216 | |
217 | float *rates; |
218 | int rate_count = godot_webxr_get_supported_frame_rates(&rates); |
219 | if (rate_count > 0) { |
220 | ret.resize(rate_count); |
221 | for (int i = 0; i < rate_count; i++) { |
222 | ret[i] = rates[i]; |
223 | } |
224 | free(rates); |
225 | } |
226 | |
227 | return ret; |
228 | } |
229 | |
230 | StringName WebXRInterfaceJS::get_name() const { |
231 | return "WebXR" ; |
232 | }; |
233 | |
234 | uint32_t WebXRInterfaceJS::get_capabilities() const { |
235 | return XRInterface::XR_STEREO | XRInterface::XR_MONO | XRInterface::XR_VR | XRInterface::XR_AR; |
236 | }; |
237 | |
238 | uint32_t WebXRInterfaceJS::get_view_count() { |
239 | return godot_webxr_get_view_count(); |
240 | }; |
241 | |
242 | bool WebXRInterfaceJS::is_initialized() const { |
243 | return (initialized); |
244 | }; |
245 | |
246 | bool WebXRInterfaceJS::initialize() { |
247 | XRServer *xr_server = XRServer::get_singleton(); |
248 | ERR_FAIL_NULL_V(xr_server, false); |
249 | |
250 | if (!initialized) { |
251 | if (!godot_webxr_is_supported()) { |
252 | return false; |
253 | } |
254 | |
255 | if (requested_reference_space_types.size() == 0) { |
256 | return false; |
257 | } |
258 | |
259 | // we must create a tracker for our head |
260 | head_transform.basis = Basis(); |
261 | head_transform.origin = Vector3(); |
262 | head_tracker.instantiate(); |
263 | head_tracker->set_tracker_type(XRServer::TRACKER_HEAD); |
264 | head_tracker->set_tracker_name("head" ); |
265 | head_tracker->set_tracker_desc("Players head" ); |
266 | xr_server->add_tracker(head_tracker); |
267 | |
268 | // make this our primary interface |
269 | xr_server->set_primary_interface(this); |
270 | |
271 | // Clear render_targetsize to make sure it gets reset to the new size. |
272 | // Clearing in uninitialize() doesn't work because a frame can still be |
273 | // rendered after it's called, which will fill render_targetsize again. |
274 | render_targetsize.width = 0; |
275 | render_targetsize.height = 0; |
276 | |
277 | initialized = true; |
278 | |
279 | godot_webxr_initialize( |
280 | session_mode.utf8().get_data(), |
281 | required_features.utf8().get_data(), |
282 | optional_features.utf8().get_data(), |
283 | requested_reference_space_types.utf8().get_data(), |
284 | &_emwebxr_on_session_started, |
285 | &_emwebxr_on_session_ended, |
286 | &_emwebxr_on_session_failed, |
287 | &_emwebxr_on_input_event, |
288 | &_emwebxr_on_simple_event); |
289 | }; |
290 | |
291 | return true; |
292 | }; |
293 | |
294 | void WebXRInterfaceJS::uninitialize() { |
295 | if (initialized) { |
296 | XRServer *xr_server = XRServer::get_singleton(); |
297 | if (xr_server != nullptr) { |
298 | if (head_tracker.is_valid()) { |
299 | xr_server->remove_tracker(head_tracker); |
300 | |
301 | head_tracker.unref(); |
302 | } |
303 | |
304 | if (xr_server->get_primary_interface() == this) { |
305 | // no longer our primary interface |
306 | xr_server->set_primary_interface(nullptr); |
307 | } |
308 | } |
309 | |
310 | godot_webxr_uninitialize(); |
311 | |
312 | GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage); |
313 | if (texture_storage != nullptr) { |
314 | for (KeyValue<unsigned int, RID> &E : texture_cache) { |
315 | // Forcibly mark as not part of a render target so we can free it. |
316 | GLES3::Texture *texture = texture_storage->get_texture(E.value); |
317 | texture->is_render_target = false; |
318 | |
319 | texture_storage->texture_free(E.value); |
320 | } |
321 | } |
322 | |
323 | texture_cache.clear(); |
324 | reference_space_type = "" ; |
325 | initialized = false; |
326 | }; |
327 | }; |
328 | |
329 | Dictionary WebXRInterfaceJS::get_system_info() { |
330 | Dictionary dict; |
331 | |
332 | // TODO get actual information from WebXR to return here |
333 | dict[SNAME("XRRuntimeName" )] = String("WebXR" ); |
334 | dict[SNAME("XRRuntimeVersion" )] = String("" ); |
335 | |
336 | return dict; |
337 | } |
338 | |
339 | Transform3D WebXRInterfaceJS::_js_matrix_to_transform(float *p_js_matrix) { |
340 | Transform3D transform; |
341 | |
342 | transform.basis.rows[0].x = p_js_matrix[0]; |
343 | transform.basis.rows[1].x = p_js_matrix[1]; |
344 | transform.basis.rows[2].x = p_js_matrix[2]; |
345 | transform.basis.rows[0].y = p_js_matrix[4]; |
346 | transform.basis.rows[1].y = p_js_matrix[5]; |
347 | transform.basis.rows[2].y = p_js_matrix[6]; |
348 | transform.basis.rows[0].z = p_js_matrix[8]; |
349 | transform.basis.rows[1].z = p_js_matrix[9]; |
350 | transform.basis.rows[2].z = p_js_matrix[10]; |
351 | transform.origin.x = p_js_matrix[12]; |
352 | transform.origin.y = p_js_matrix[13]; |
353 | transform.origin.z = p_js_matrix[14]; |
354 | |
355 | return transform; |
356 | } |
357 | |
358 | Size2 WebXRInterfaceJS::get_render_target_size() { |
359 | if (render_targetsize.width != 0 && render_targetsize.height != 0) { |
360 | return render_targetsize; |
361 | } |
362 | |
363 | int js_size[2]; |
364 | bool has_size = godot_webxr_get_render_target_size(js_size); |
365 | |
366 | if (!initialized || !has_size) { |
367 | // As a temporary default (until WebXR is fully initialized), use the |
368 | // window size. |
369 | return DisplayServer::get_singleton()->window_get_size(); |
370 | } |
371 | |
372 | render_targetsize.width = (float)js_size[0]; |
373 | render_targetsize.height = (float)js_size[1]; |
374 | |
375 | return render_targetsize; |
376 | }; |
377 | |
378 | Transform3D WebXRInterfaceJS::get_camera_transform() { |
379 | Transform3D camera_transform; |
380 | |
381 | XRServer *xr_server = XRServer::get_singleton(); |
382 | ERR_FAIL_NULL_V(xr_server, camera_transform); |
383 | |
384 | if (initialized) { |
385 | double world_scale = xr_server->get_world_scale(); |
386 | |
387 | Transform3D _head_transform = head_transform; |
388 | _head_transform.origin *= world_scale; |
389 | |
390 | camera_transform = (xr_server->get_reference_frame()) * _head_transform; |
391 | } |
392 | |
393 | return camera_transform; |
394 | }; |
395 | |
396 | Transform3D WebXRInterfaceJS::get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) { |
397 | XRServer *xr_server = XRServer::get_singleton(); |
398 | ERR_FAIL_NULL_V(xr_server, p_cam_transform); |
399 | ERR_FAIL_COND_V(!initialized, p_cam_transform); |
400 | |
401 | float js_matrix[16]; |
402 | bool has_transform = godot_webxr_get_transform_for_view(p_view, js_matrix); |
403 | if (!has_transform) { |
404 | return p_cam_transform; |
405 | } |
406 | |
407 | Transform3D transform_for_view = _js_matrix_to_transform(js_matrix); |
408 | |
409 | double world_scale = xr_server->get_world_scale(); |
410 | transform_for_view.origin *= world_scale; |
411 | |
412 | return p_cam_transform * xr_server->get_reference_frame() * transform_for_view; |
413 | }; |
414 | |
415 | Projection WebXRInterfaceJS::get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) { |
416 | Projection view; |
417 | |
418 | ERR_FAIL_COND_V(!initialized, view); |
419 | |
420 | float js_matrix[16]; |
421 | bool has_projection = godot_webxr_get_projection_for_view(p_view, js_matrix); |
422 | if (!has_projection) { |
423 | return view; |
424 | } |
425 | |
426 | int k = 0; |
427 | for (int i = 0; i < 4; i++) { |
428 | for (int j = 0; j < 4; j++) { |
429 | view.columns[i][j] = js_matrix[k++]; |
430 | } |
431 | } |
432 | |
433 | // Copied from godot_oculus_mobile's ovr_mobile_session.cpp |
434 | view.columns[2][2] = -(p_z_far + p_z_near) / (p_z_far - p_z_near); |
435 | view.columns[3][2] = -(2.0f * p_z_far * p_z_near) / (p_z_far - p_z_near); |
436 | |
437 | return view; |
438 | } |
439 | |
440 | bool WebXRInterfaceJS::pre_draw_viewport(RID p_render_target) { |
441 | GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage); |
442 | if (texture_storage == nullptr) { |
443 | return false; |
444 | } |
445 | |
446 | GLES3::RenderTarget *rt = texture_storage->get_render_target(p_render_target); |
447 | if (rt == nullptr) { |
448 | return false; |
449 | } |
450 | |
451 | // Cache the resources so we don't have to get them from JS twice. |
452 | color_texture = _get_color_texture(); |
453 | depth_texture = _get_depth_texture(); |
454 | |
455 | // Per the WebXR spec, it returns "opaque textures" to us, which may be the |
456 | // same WebGLTexture object (which would be the same GLuint in C++) but |
457 | // represent a different underlying resource (probably the next texture in |
458 | // the XR device's swap chain). In order to render to this texture, we need |
459 | // to re-attach it to the FBO, otherwise we get an "incomplete FBO" error. |
460 | // |
461 | // See: https://immersive-web.github.io/layers/#xropaquetextures |
462 | // |
463 | // This is why we're doing this sort of silly check: if the color and depth |
464 | // textures are the same this frame as last frame, we need to attach them |
465 | // again, despite the fact that the GLuint for them hasn't changed. |
466 | if (rt->overridden.is_overridden && rt->overridden.color == color_texture && rt->overridden.depth == depth_texture) { |
467 | GLES3::Config *config = GLES3::Config::get_singleton(); |
468 | bool use_multiview = rt->view_count > 1 && config->multiview_supported; |
469 | |
470 | glBindFramebuffer(GL_FRAMEBUFFER, rt->fbo); |
471 | if (use_multiview) { |
472 | glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, rt->color, 0, 0, rt->view_count); |
473 | glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, rt->depth, 0, 0, rt->view_count); |
474 | } else { |
475 | glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, rt->color, 0); |
476 | glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, rt->depth, 0); |
477 | } |
478 | glBindFramebuffer(GL_FRAMEBUFFER, texture_storage->system_fbo); |
479 | } |
480 | |
481 | return true; |
482 | } |
483 | |
484 | Vector<BlitToScreen> WebXRInterfaceJS::post_draw_viewport(RID p_render_target, const Rect2 &p_screen_rect) { |
485 | Vector<BlitToScreen> blit_to_screen; |
486 | |
487 | // We don't need to do anything here. |
488 | |
489 | return blit_to_screen; |
490 | }; |
491 | |
492 | RID WebXRInterfaceJS::_get_color_texture() { |
493 | unsigned int texture_id = godot_webxr_get_color_texture(); |
494 | if (texture_id == 0) { |
495 | return RID(); |
496 | } |
497 | |
498 | return _get_texture(texture_id); |
499 | } |
500 | |
501 | RID WebXRInterfaceJS::_get_depth_texture() { |
502 | unsigned int texture_id = godot_webxr_get_depth_texture(); |
503 | if (texture_id == 0) { |
504 | return RID(); |
505 | } |
506 | |
507 | return _get_texture(texture_id); |
508 | } |
509 | |
510 | RID WebXRInterfaceJS::_get_texture(unsigned int p_texture_id) { |
511 | RBMap<unsigned int, RID>::Element *cache = texture_cache.find(p_texture_id); |
512 | if (cache != nullptr) { |
513 | return cache->get(); |
514 | } |
515 | |
516 | GLES3::TextureStorage *texture_storage = dynamic_cast<GLES3::TextureStorage *>(RSG::texture_storage); |
517 | if (texture_storage == nullptr) { |
518 | return RID(); |
519 | } |
520 | |
521 | uint32_t view_count = godot_webxr_get_view_count(); |
522 | Size2 texture_size = get_render_target_size(); |
523 | |
524 | RID texture = texture_storage->texture_create_external( |
525 | view_count == 1 ? GLES3::Texture::TYPE_2D : GLES3::Texture::TYPE_LAYERED, |
526 | Image::FORMAT_RGBA8, |
527 | p_texture_id, |
528 | (int)texture_size.width, |
529 | (int)texture_size.height, |
530 | 1, |
531 | view_count); |
532 | |
533 | texture_cache.insert(p_texture_id, texture); |
534 | |
535 | return texture; |
536 | } |
537 | |
538 | RID WebXRInterfaceJS::get_color_texture() { |
539 | return color_texture; |
540 | } |
541 | |
542 | RID WebXRInterfaceJS::get_depth_texture() { |
543 | return depth_texture; |
544 | } |
545 | |
546 | RID WebXRInterfaceJS::get_velocity_texture() { |
547 | unsigned int texture_id = godot_webxr_get_velocity_texture(); |
548 | if (texture_id == 0) { |
549 | return RID(); |
550 | } |
551 | |
552 | return _get_texture(texture_id); |
553 | } |
554 | |
555 | void WebXRInterfaceJS::process() { |
556 | if (initialized) { |
557 | // Get the "head" position. |
558 | float js_matrix[16]; |
559 | if (godot_webxr_get_transform_for_view(-1, js_matrix)) { |
560 | head_transform = _js_matrix_to_transform(js_matrix); |
561 | } |
562 | if (head_tracker.is_valid()) { |
563 | head_tracker->set_pose("default" , head_transform, Vector3(), Vector3()); |
564 | } |
565 | |
566 | // Update all input sources. |
567 | for (int i = 0; i < input_source_count; i++) { |
568 | _update_input_source(i); |
569 | } |
570 | }; |
571 | }; |
572 | |
573 | void WebXRInterfaceJS::_update_input_source(int p_input_source_id) { |
574 | XRServer *xr_server = XRServer::get_singleton(); |
575 | ERR_FAIL_NULL(xr_server); |
576 | |
577 | InputSource &input_source = input_sources[p_input_source_id]; |
578 | |
579 | float target_pose[16]; |
580 | int tmp_target_ray_mode; |
581 | int touch_index; |
582 | int has_grip_pose; |
583 | float grip_pose[16]; |
584 | int has_standard_mapping; |
585 | int button_count; |
586 | float buttons[10]; |
587 | int axes_count; |
588 | float axes[10]; |
589 | |
590 | input_source.active = godot_webxr_update_input_source( |
591 | p_input_source_id, |
592 | target_pose, |
593 | &tmp_target_ray_mode, |
594 | &touch_index, |
595 | &has_grip_pose, |
596 | grip_pose, |
597 | &has_standard_mapping, |
598 | &button_count, |
599 | buttons, |
600 | &axes_count, |
601 | axes); |
602 | |
603 | if (!input_source.active) { |
604 | if (input_source.tracker.is_valid()) { |
605 | xr_server->remove_tracker(input_source.tracker); |
606 | input_source.tracker.unref(); |
607 | } |
608 | return; |
609 | } |
610 | |
611 | input_source.target_ray_mode = (WebXRInterface::TargetRayMode)tmp_target_ray_mode; |
612 | input_source.touch_index = touch_index; |
613 | |
614 | Ref<XRPositionalTracker> &tracker = input_source.tracker; |
615 | |
616 | if (tracker.is_null()) { |
617 | tracker.instantiate(); |
618 | |
619 | StringName tracker_name; |
620 | if (input_source.target_ray_mode == WebXRInterface::TargetRayMode::TARGET_RAY_MODE_SCREEN) { |
621 | tracker_name = touch_names[touch_index]; |
622 | } else { |
623 | tracker_name = tracker_names[p_input_source_id]; |
624 | } |
625 | |
626 | // Input source id's 0 and 1 are always the left and right hands. |
627 | if (p_input_source_id < 2) { |
628 | tracker->set_tracker_type(XRServer::TRACKER_CONTROLLER); |
629 | tracker->set_tracker_name(tracker_name); |
630 | tracker->set_tracker_desc(p_input_source_id == 0 ? "Left hand controller" : "Right hand controller" ); |
631 | tracker->set_tracker_hand(p_input_source_id == 0 ? XRPositionalTracker::TRACKER_HAND_LEFT : XRPositionalTracker::TRACKER_HAND_RIGHT); |
632 | } else { |
633 | tracker->set_tracker_name(tracker_name); |
634 | tracker->set_tracker_desc(tracker_name); |
635 | } |
636 | xr_server->add_tracker(tracker); |
637 | } |
638 | |
639 | Transform3D aim_transform = _js_matrix_to_transform(target_pose); |
640 | tracker->set_pose(SNAME("default" ), aim_transform, Vector3(), Vector3()); |
641 | tracker->set_pose(SNAME("aim" ), aim_transform, Vector3(), Vector3()); |
642 | if (has_grip_pose) { |
643 | tracker->set_pose(SNAME("grip" ), _js_matrix_to_transform(grip_pose), Vector3(), Vector3()); |
644 | } |
645 | |
646 | for (int i = 0; i < button_count; i++) { |
647 | StringName button_name = has_standard_mapping ? standard_button_names[i] : unknown_button_names[i]; |
648 | StringName button_pressure_name = has_standard_mapping ? standard_button_pressure_names[i] : unknown_button_pressure_names[i]; |
649 | float value = buttons[i]; |
650 | bool state = value > 0.0; |
651 | tracker->set_input(button_name, state); |
652 | tracker->set_input(button_pressure_name, value); |
653 | } |
654 | |
655 | for (int i = 0; i < axes_count; i++) { |
656 | StringName axis_name = has_standard_mapping ? standard_axis_names[i] : unknown_axis_names[i]; |
657 | float value = axes[i]; |
658 | if (has_standard_mapping && (i == 1 || i == 3)) { |
659 | // Invert the Y-axis on thumbsticks and trackpads, in order to |
660 | // match OpenXR and other XR platform SDKs. |
661 | value = -value; |
662 | } |
663 | tracker->set_input(axis_name, value); |
664 | } |
665 | |
666 | // Also create Vector2's for the thumbstick and trackpad when we have the |
667 | // standard mapping. |
668 | if (has_standard_mapping) { |
669 | if (axes_count >= 2) { |
670 | tracker->set_input(standard_vector_names[0], Vector2(axes[0], -axes[1])); |
671 | } |
672 | if (axes_count >= 4) { |
673 | tracker->set_input(standard_vector_names[1], Vector2(axes[2], -axes[3])); |
674 | } |
675 | } |
676 | |
677 | if (input_source.target_ray_mode == WebXRInterface::TARGET_RAY_MODE_SCREEN) { |
678 | if (touch_index < 5 && axes_count >= 2) { |
679 | Vector2 joy_vector = Vector2(axes[0], axes[1]); |
680 | Vector2 position = _get_screen_position_from_joy_vector(joy_vector); |
681 | |
682 | if (touches[touch_index].is_touching) { |
683 | Vector2 delta = position - touches[touch_index].position; |
684 | |
685 | // If position has changed by at least 1 pixel, generate a drag event. |
686 | if (abs(delta.x) >= 1.0 || abs(delta.y) >= 1.0) { |
687 | Ref<InputEventScreenDrag> event; |
688 | event.instantiate(); |
689 | event->set_index(touch_index); |
690 | event->set_position(position); |
691 | event->set_relative(delta); |
692 | Input::get_singleton()->parse_input_event(event); |
693 | } |
694 | } |
695 | |
696 | touches[touch_index].position = position; |
697 | } |
698 | } |
699 | } |
700 | |
701 | void WebXRInterfaceJS::_on_input_event(int p_event_type, int p_input_source_id) { |
702 | // Get the latest data for this input source. For transient input sources, |
703 | // we may not have any data at all yet! |
704 | _update_input_source(p_input_source_id); |
705 | |
706 | if (p_event_type == WEBXR_INPUT_EVENT_SELECTSTART || p_event_type == WEBXR_INPUT_EVENT_SELECTEND) { |
707 | const InputSource &input_source = input_sources[p_input_source_id]; |
708 | if (input_source.target_ray_mode == WebXRInterface::TARGET_RAY_MODE_SCREEN) { |
709 | int touch_index = input_source.touch_index; |
710 | if (touch_index >= 0 && touch_index < 5) { |
711 | touches[touch_index].is_touching = (p_event_type == WEBXR_INPUT_EVENT_SELECTSTART); |
712 | |
713 | Ref<InputEventScreenTouch> event; |
714 | event.instantiate(); |
715 | event->set_index(touch_index); |
716 | event->set_position(touches[touch_index].position); |
717 | event->set_pressed(p_event_type == WEBXR_INPUT_EVENT_SELECTSTART); |
718 | |
719 | Input::get_singleton()->parse_input_event(event); |
720 | } |
721 | } |
722 | } |
723 | |
724 | switch (p_event_type) { |
725 | case WEBXR_INPUT_EVENT_SELECTSTART: |
726 | emit_signal("selectstart" , p_input_source_id); |
727 | break; |
728 | |
729 | case WEBXR_INPUT_EVENT_SELECTEND: |
730 | emit_signal("selectend" , p_input_source_id); |
731 | // Emit the 'select' event on our own (rather than intercepting the |
732 | // one from JavaScript) so that we don't have to needlessly call |
733 | // _update_input_source() a second time. |
734 | emit_signal("select" , p_input_source_id); |
735 | break; |
736 | |
737 | case WEBXR_INPUT_EVENT_SQUEEZESTART: |
738 | emit_signal("squeezestart" , p_input_source_id); |
739 | break; |
740 | |
741 | case WEBXR_INPUT_EVENT_SQUEEZEEND: |
742 | emit_signal("squeezeend" , p_input_source_id); |
743 | // Again, we emit the 'squeeze' event on our own to avoid extra work. |
744 | emit_signal("squeeze" , p_input_source_id); |
745 | break; |
746 | } |
747 | } |
748 | |
749 | Vector2 WebXRInterfaceJS::_get_screen_position_from_joy_vector(const Vector2 &p_joy_vector) { |
750 | SceneTree *scene_tree = Object::cast_to<SceneTree>(OS::get_singleton()->get_main_loop()); |
751 | if (!scene_tree) { |
752 | return Vector2(); |
753 | } |
754 | |
755 | Window *viewport = scene_tree->get_root(); |
756 | |
757 | Vector2 position_percentage((p_joy_vector.x + 1.0f) / 2.0f, ((p_joy_vector.y) + 1.0f) / 2.0f); |
758 | Vector2 position = (Size2)viewport->get_size() * position_percentage; |
759 | |
760 | return position; |
761 | } |
762 | |
763 | WebXRInterfaceJS::WebXRInterfaceJS() { |
764 | initialized = false; |
765 | session_mode = "inline" ; |
766 | requested_reference_space_types = "local" ; |
767 | }; |
768 | |
769 | WebXRInterfaceJS::~WebXRInterfaceJS() { |
770 | // and make sure we cleanup if we haven't already |
771 | if (initialized) { |
772 | uninitialize(); |
773 | }; |
774 | }; |
775 | |
776 | #endif // WEB_ENABLED |
777 | |