Skip to content

Commit b7ebf9d

Browse files
klauswdmarcos
authored andcommitted
Add WebXR immersive-ar support (#4281)
* Add WebXR immersive-ar support This adds an extra "enter AR" button if WebXR support for "immersive-ar" is detected. While in AR mode, hide the default scene background (if any) for convenience. For the most part this is handled as a variation of VR mode. While in AR, both the 'vr-mode' and a new 'ar-mode' states are active. Also wire up a new 'update-vr-devices' event to refresh the enter button states after asynchronous availability changes. Add support for the new navigator.xr.isSessionSupported API, falling back to navigator.xr.supportsSession if not. Use a different image for the "Enter AR" button Conditionally show AR button when immersive-ar is available Use a fully-separate "enter AR" button, update-vr-devices event * Consider case when a-scene has not yet loaded
1 parent 97a1106 commit b7ebf9d

File tree

5 files changed

+170
-24
lines changed

5 files changed

+170
-24
lines changed

src/components/scene/vr-mode-ui.js

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ var utils = require('../../utils/');
44
var bind = utils.bind;
55

66
var ENTER_VR_CLASS = 'a-enter-vr';
7+
var ENTER_AR_CLASS = 'a-enter-ar';
78
var ENTER_VR_BTN_CLASS = 'a-enter-vr-button';
9+
var ENTER_AR_BTN_CLASS = 'a-enter-ar-button';
810
var HIDDEN_CLASS = 'a-hidden';
911
var ORIENTATION_MODAL_CLASS = 'a-orientation-modal';
1012

@@ -16,7 +18,8 @@ module.exports.Component = registerComponent('vr-mode-ui', {
1618

1719
schema: {
1820
enabled: {default: true},
19-
enterVRButton: {default: ''}
21+
enterVRButton: {default: ''},
22+
enterARButton: {default: ''}
2023
},
2124

2225
init: function () {
@@ -27,12 +30,14 @@ module.exports.Component = registerComponent('vr-mode-ui', {
2730

2831
this.insideLoader = false;
2932
this.enterVREl = null;
33+
this.enterAREl = null;
3034
this.orientationModalEl = null;
3135
this.bindMethods();
3236

3337
// Hide/show VR UI when entering/exiting VR mode.
34-
sceneEl.addEventListener('enter-vr', this.updateEnterVRInterface);
35-
sceneEl.addEventListener('exit-vr', this.updateEnterVRInterface);
38+
sceneEl.addEventListener('enter-vr', this.updateEnterInterfaces);
39+
sceneEl.addEventListener('exit-vr', this.updateEnterInterfaces);
40+
sceneEl.addEventListener('update-vr-devices', this.updateEnterInterfaces);
3641

3742
window.addEventListener('message', function (event) {
3843
if (event.data.type === 'loaderReady') {
@@ -47,9 +52,10 @@ module.exports.Component = registerComponent('vr-mode-ui', {
4752

4853
bindMethods: function () {
4954
this.onEnterVRButtonClick = bind(this.onEnterVRButtonClick, this);
55+
this.onEnterARButtonClick = bind(this.onEnterARButtonClick, this);
5056
this.onModalClick = bind(this.onModalClick, this);
5157
this.toggleOrientationModalIfNeeded = bind(this.toggleOrientationModalIfNeeded, this);
52-
this.updateEnterVRInterface = bind(this.updateEnterVRInterface, this);
58+
this.updateEnterInterfaces = bind(this.updateEnterInterfaces, this);
5359
},
5460

5561
/**
@@ -60,20 +66,28 @@ module.exports.Component = registerComponent('vr-mode-ui', {
6066
},
6167

6268
/**
63-
* Enter VR when modal clicked.
69+
* Enter VR when clicked.
6470
*/
6571
onEnterVRButtonClick: function () {
6672
this.el.enterVR();
6773
},
6874

75+
/**
76+
* Enter AR when clicked.
77+
*/
78+
onEnterARButtonClick: function () {
79+
this.el.enterAR();
80+
},
81+
6982
update: function () {
7083
var data = this.data;
7184
var sceneEl = this.el;
7285

7386
if (!data.enabled || this.insideLoader || utils.getUrlParameter('ui') === 'false') {
7487
return this.remove();
7588
}
76-
if (this.enterVREl || this.orientationModalEl) { return; }
89+
90+
if (this.enterVREl || this.enterAREl || this.orientationModalEl) { return; }
7791

7892
// Add UI if enabled and not already present.
7993
if (data.enterVRButton) {
@@ -84,23 +98,32 @@ module.exports.Component = registerComponent('vr-mode-ui', {
8498
this.enterVREl = createEnterVRButton(this.onEnterVRButtonClick);
8599
sceneEl.appendChild(this.enterVREl);
86100
}
101+
if (data.enterARButton) {
102+
// Custom button.
103+
this.enterAREl = document.querySelector(data.enterARButton);
104+
this.enterAREl.addEventListener('click', this.onEnterARButtonClick);
105+
} else {
106+
this.enterAREl = createEnterARButton(this.onEnterARButtonClick);
107+
sceneEl.appendChild(this.enterAREl);
108+
}
87109

88110
this.orientationModalEl = createOrientationModal(this.onModalClick);
89111
sceneEl.appendChild(this.orientationModalEl);
90112

91-
this.updateEnterVRInterface();
113+
this.updateEnterInterfaces();
92114
},
93115

94116
remove: function () {
95-
[this.enterVREl, this.orientationModalEl].forEach(function (uiElement) {
117+
[this.enterVREl, this.enterAREl, this.orientationModalEl].forEach(function (uiElement) {
96118
if (uiElement && uiElement.parentNode) {
97119
uiElement.parentNode.removeChild(uiElement);
98120
}
99121
});
100122
},
101123

102-
updateEnterVRInterface: function () {
124+
updateEnterInterfaces: function () {
103125
this.toggleEnterVRButtonIfNeeded();
126+
this.toggleEnterARButtonIfNeeded();
104127
this.toggleOrientationModalIfNeeded();
105128
},
106129

@@ -114,6 +137,17 @@ module.exports.Component = registerComponent('vr-mode-ui', {
114137
}
115138
},
116139

140+
toggleEnterARButtonIfNeeded: function () {
141+
var sceneEl = this.el;
142+
if (!this.enterAREl) { return; }
143+
// Hide the button while in a session, or if AR is not supported.
144+
if (sceneEl.is('vr-mode') || !utils.device.checkARSupport()) {
145+
this.enterAREl.classList.add(HIDDEN_CLASS);
146+
} else {
147+
this.enterAREl.classList.remove(HIDDEN_CLASS);
148+
}
149+
},
150+
117151
toggleOrientationModalIfNeeded: function () {
118152
var sceneEl = this.el;
119153
var orientationModalEl = this.orientationModalEl;
@@ -149,7 +183,6 @@ function createEnterVRButton (onClick) {
149183
'Enter VR mode with a headset or fullscreen mode on a desktop. ' +
150184
'Visit https://webvr.rocks or https://webvr.info for more information.');
151185
vrButton.setAttribute(constants.AFRAME_INJECTED, '');
152-
153186
// Insert elements.
154187
wrapper.appendChild(vrButton);
155188
vrButton.addEventListener('click', function (evt) {
@@ -159,6 +192,37 @@ function createEnterVRButton (onClick) {
159192
return wrapper;
160193
}
161194

195+
/**
196+
* Create a button that when clicked will enter into AR mode
197+
*
198+
* Structure: <div><button></div>
199+
*
200+
* @param {function} onClick - click event handler
201+
* @returns {Element} Wrapper <div>.
202+
*/
203+
function createEnterARButton (onClick) {
204+
var arButton;
205+
var wrapper;
206+
207+
// Create elements.
208+
wrapper = document.createElement('div');
209+
wrapper.classList.add(ENTER_AR_CLASS);
210+
wrapper.setAttribute(constants.AFRAME_INJECTED, '');
211+
arButton = document.createElement('button');
212+
arButton.className = ENTER_AR_BTN_CLASS;
213+
arButton.setAttribute('title',
214+
'Enter AR mode with a headset or handheld device. ' +
215+
'Visit https://webvr.rocks or https://webvr.info for more information.');
216+
arButton.setAttribute(constants.AFRAME_INJECTED, '');
217+
// Insert elements.
218+
wrapper.appendChild(arButton);
219+
arButton.addEventListener('click', function (evt) {
220+
onClick();
221+
evt.stopPropagation();
222+
});
223+
return wrapper;
224+
}
225+
162226
/**
163227
* Creates a modal dialog to request the user to switch to landscape orientation.
164228
*

src/core/scene/a-scene.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ module.exports.AScene = registerElement('a-scene', {
4242
this.isIOS = isIOS;
4343
this.isMobile = isMobile;
4444
this.hasWebXR = isWebXRAvailable;
45+
this.isAR = false;
4546
this.isScene = true;
4647
this.object3D = new THREE.Scene();
4748
var self = this;
@@ -245,15 +246,25 @@ module.exports.AScene = registerElement('a-scene', {
245246
writable: window.debug
246247
},
247248

249+
enterAR: {
250+
value: function () {
251+
if (!this.hasWebXR) {
252+
throw new Error('Failed to enter AR mode, WebXR not supported.');
253+
}
254+
this.enterVR(true);
255+
}
256+
},
257+
248258
/**
249259
* Call `requestPresent` if WebVR or WebVR polyfill.
250260
* Call `requestFullscreen` on desktop.
251261
* Handle events, states, fullscreen styles.
252262
*
263+
* @param {bool?} useAR - if true, try immersive-ar mode
253264
* @returns {Promise}
254265
*/
255266
enterVR: {
256-
value: function () {
267+
value: function (useAR) {
257268
var self = this;
258269
var vrDisplay;
259270
var vrManager = self.renderer.vr;
@@ -272,10 +283,16 @@ module.exports.AScene = registerElement('a-scene', {
272283
if (this.xrSession) {
273284
this.xrSession.removeEventListener('end', this.exitVRBound);
274285
}
275-
navigator.xr.requestSession('immersive-vr').then(function requestSuccess (xrSession) {
286+
navigator.xr.requestSession(useAR ? 'immersive-ar' : 'immersive-vr', {
287+
requiredFeatures: ['local-floor'],
288+
optionalFeatures: ['bounded-floor']
289+
}).then(function requestSuccess (xrSession) {
276290
self.xrSession = xrSession;
277291
vrManager.setSession(xrSession);
278292
xrSession.addEventListener('end', self.exitVRBound);
293+
if (useAR) {
294+
self.addState('ar-mode');
295+
}
279296
enterVRSuccess();
280297
});
281298
} else {
@@ -317,7 +334,7 @@ module.exports.AScene = registerElement('a-scene', {
317334
self.addState('vr-mode');
318335
self.emit('enter-vr', {target: self});
319336
// Lock to landscape orientation on mobile.
320-
if (self.isMobile && screen.orientation && screen.orientation.lock) {
337+
if (!navigator.xr && self.isMobile && screen.orientation && screen.orientation.lock) {
321338
screen.orientation.lock('landscape');
322339
}
323340
self.addFullScreenStyles();
@@ -383,6 +400,7 @@ module.exports.AScene = registerElement('a-scene', {
383400

384401
function exitVRSuccess () {
385402
self.removeState('vr-mode');
403+
self.removeState('ar-mode');
386404
// Lock to landscape orientation on mobile.
387405
if (self.isMobile && screen.orientation && screen.orientation.unlock) {
388406
screen.orientation.unlock();
@@ -735,8 +753,17 @@ module.exports.AScene = registerElement('a-scene', {
735753
this.time = this.clock.elapsedTime * 1000;
736754

737755
if (this.isPlaying) { this.tick(this.time, this.delta); }
738-
756+
var savedBackground = null;
757+
if (this.is('ar-mode')) {
758+
// In AR mode, don't render the default background. Hide it, then
759+
// restore it again after rendering.
760+
savedBackground = this.object3D.background;
761+
this.object3D.background = null;
762+
}
739763
renderer.render(this.object3D, this.camera);
764+
if (savedBackground) {
765+
this.object3D.background = savedBackground;
766+
}
740767
},
741768
writable: true
742769
}

src/style/aframe.css

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ html.a-fullscreen .a-canvas {
2626
position: fixed !important;
2727
}
2828

29-
html:not(.a-fullscreen) .a-enter-vr {
29+
html:not(.a-fullscreen) .a-enter-vr,
30+
html:not(.a-fullscreen) .a-enter-ar {
3031
right: 5px;
3132
bottom: 5px;
3233
}
@@ -130,7 +131,8 @@ a-scene audio {
130131
left: -4px;
131132
}
132133

133-
.a-enter-vr {
134+
.a-enter-vr,
135+
.a-enter-ar {
134136
font-family: sans-serif, monospace;
135137
font-size: 13px;
136138
width: 100%;
@@ -141,6 +143,10 @@ a-scene audio {
141143
bottom: 20px;
142144
}
143145

146+
.a-enter-ar {
147+
right: 80px;
148+
}
149+
144150
.a-enter-vr-button,
145151
.a-enter-vr-modal,
146152
.a-enter-vr-modal a {
@@ -149,6 +155,14 @@ a-scene audio {
149155

150156
.a-enter-vr-button {
151157
background: rgba(0, 0, 0, 0.35) url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20245.82%20141.73%22%3E%3Cdefs%3E%3Cstyle%3E.a%7Bfill%3A%23fff%3Bfill-rule%3Aevenodd%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3Emask%3C%2Ftitle%3E%3Cpath%20class%3D%22a%22%20d%3D%22M175.56%2C111.37c-22.52%2C0-40.77-18.84-40.77-42.07S153%2C27.24%2C175.56%2C27.24s40.77%2C18.84%2C40.77%2C42.07S198.08%2C111.37%2C175.56%2C111.37ZM26.84%2C69.31c0-23.23%2C18.25-42.07%2C40.77-42.07s40.77%2C18.84%2C40.77%2C42.07-18.26%2C42.07-40.77%2C42.07S26.84%2C92.54%2C26.84%2C69.31ZM27.27%2C0C11.54%2C0%2C0%2C12.34%2C0%2C28.58V110.9c0%2C16.24%2C11.54%2C30.83%2C27.27%2C30.83H99.57c2.17%2C0%2C4.19-1.83%2C5.4-3.7L116.47%2C118a8%2C8%2C0%2C0%2C1%2C12.52-.18l11.51%2C20.34c1.2%2C1.86%2C3.22%2C3.61%2C5.39%2C3.61h72.29c15.74%2C0%2C27.63-14.6%2C27.63-30.83V28.58C245.82%2C12.34%2C233.93%2C0%2C218.19%2C0H27.27Z%22%2F%3E%3C%2Fsvg%3E) 50% 50% no-repeat;
158+
}
159+
160+
.a-enter-ar-button {
161+
background: rgba(0, 0, 0, 0.35) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 245.8 141.7'%3E%3Cdefs%3E%3Cstyle%3E .a%7Bfill:%23fff;fill-rule:evenodd;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3E mask%3C/title%3E%3Cpath d='m115 104.7c-22.5 0-40.8-18.8-40.8-42.1s18.2-42.1 40.8-42.1 40.8 18.8 40.8 42.1-18.2 42.1-40.8 42.1zm-21.9-0.8c-0.4-14.7 27.8-23.8 50.3-23.8s50.3 8.8 50.8 23c0.4 12.7-26.8 22.2-49.3 22.2s-51.4-6.2-51.9-21.5zm-65.8-103.8c-15.7 0-27.3 12.3-27.3 28.6v82.3c0 16.2 11.5 30.8 27.3 30.8 74 0 146.7 0.9 190.9 0 15.7 0 27.6-14.6 27.6-30.8v-82.4c0-16.2-11.9-28.6-27.6-28.6z' class='a' fill='%23fff'/%3E%3C/svg%3E") 50% 50% no-repeat;
162+
}
163+
164+
.a-enter-vr-button,
165+
.a-enter-ar-button {
152166
background-size: 70% 70%;
153167
border: 0;
154168
bottom: 0;

src/systems/tracked-controls-webvr.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ module.exports.System = registerSystem('tracked-controls-webvr', {
1515
this.updateControllerList();
1616
this.throttledUpdateControllerList = utils.throttle(this.updateControllerList, 500, this);
1717

18+
// Don't use WebVR if WebXR is available?
19+
if (navigator.xr) { return; }
20+
1821
if (!navigator.getVRDisplays) { return; }
1922

2023
this.sceneEl.addEventListener('enter-vr', function () {

0 commit comments

Comments
 (0)