-
Notifications
You must be signed in to change notification settings - Fork 3.4k
/
webXRSessionManager.ts
282 lines (250 loc) · 11.7 KB
/
webXRSessionManager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import { Logger } from "../../Misc/logger";
import { Observable } from "../../Misc/observable";
import { Nullable } from "../../types";
import { IDisposable, Scene } from "../../scene";
import { InternalTexture, InternalTextureSource } from "../../Materials/Textures/internalTexture";
import { RenderTargetTexture } from "../../Materials/Textures/renderTargetTexture";
import { WebXRRenderTarget, WebXRState } from './webXRTypes';
import { WebXRManagedOutputCanvas, WebXRManagedOutputCanvasOptions } from './webXRManagedOutputCanvas';
interface IRenderTargetProvider {
getRenderTargetForEye(eye: XREye): RenderTargetTexture;
}
class RenderTargetProvider implements IRenderTargetProvider {
private _texture: RenderTargetTexture;
public constructor(texture: RenderTargetTexture) {
this._texture = texture;
}
public getRenderTargetForEye(eye: XREye): RenderTargetTexture {
return this._texture;
}
}
/**
* Manages an XRSession to work with Babylon's engine
* @see https://doc.babylonjs.com/how_to/webxr
*/
export class WebXRSessionManager implements IDisposable {
/**
* Fires every time a new xrFrame arrives which can be used to update the camera
*/
public onXRFrameObservable: Observable<any> = new Observable<any>();
/**
* Fires when the xr session is ended either by the device or manually done
*/
public onXRSessionEnded: Observable<any> = new Observable<any>();
/**
* Underlying xr session
*/
public session: XRSession;
/**
* Type of reference space used when creating the session
*/
public referenceSpace: XRReferenceSpace;
/**
* Current XR frame
*/
public currentFrame: Nullable<XRFrame>;
private _xrNavigator: any;
private baseLayer: Nullable<XRWebGLLayer> = null;
private _rttProvider: Nullable<IRenderTargetProvider>;
private _sessionEnded: boolean = false;
/**
* Constructs a WebXRSessionManager, this must be initialized within a user action before usage
* @param scene The scene which the session should be created for
*/
constructor(private scene: Scene) {
}
/**
* Initializes the manager
* After initialization enterXR can be called to start an XR session
* @returns Promise which resolves after it is initialized
*/
public initializeAsync(): Promise<void> {
Logger.Warn("The WebXR APIs are still under development and are subject to change in the future.");
// Check if the browser supports webXR
this._xrNavigator = navigator;
if (!this._xrNavigator.xr) {
return Promise.reject("webXR not supported by this browser");
}
return Promise.resolve();
}
/**
* Initializes an xr session
* @param xrSessionMode mode to initialize
* @param optionalFeatures defines optional values to pass to the session builder
* @returns a promise which will resolve once the session has been initialized
*/
public initializeSessionAsync(xrSessionMode: XRSessionMode, optionalFeatures: any = {}): Promise<XRSession> {
return this._xrNavigator.xr.requestSession(xrSessionMode, optionalFeatures).then((session: XRSession) => {
this.session = session;
this._sessionEnded = false;
// handle when the session is ended (By calling session.end or device ends its own session eg. pressing home button on phone)
this.session.addEventListener("end", () => {
this._sessionEnded = true;
// Remove render target texture and notify frame obervers
this._rttProvider = null;
// Restore frame buffer to avoid clear on xr framebuffer after session end
this.scene.getEngine().restoreDefaultFramebuffer();
// Need to restart render loop as after the session is ended the last request for new frame will never call callback
this.scene.getEngine().customAnimationFrameRequester = null;
this.onXRSessionEnded.notifyObservers(null);
this.scene.getEngine()._renderLoop();
}, { once: true });
return this.session;
});
}
/**
* Sets the reference space on the xr session
* @param referenceSpace space to set
* @returns a promise that will resolve once the reference space has been set
*/
public setReferenceSpaceAsync(referenceSpace: XRReferenceSpaceType) {
return this.session.requestReferenceSpace(referenceSpace).then((referenceSpace: XRReferenceSpace) => {
this.referenceSpace = referenceSpace;
}, (rejectionReason) => {
Logger.Error("XR.requestReferenceSpace failed for the following reason: ");
Logger.Error(rejectionReason);
Logger.Log("Defaulting to universally-supported \"viewer\" reference space type.");
return this.session.requestReferenceSpace("viewer").then((referenceSpace: XRReferenceSpace) => {
this.referenceSpace = referenceSpace;
}, (rejectionReason) => {
Logger.Error(rejectionReason);
throw "XR initialization failed: required \"viewer\" reference space type not supported.";
});
});
}
/**
* Updates the render state of the session
* @param state state to set
* @returns a promise that resolves once the render state has been updated
*/
public updateRenderStateAsync(state: XRRenderState) {
if (state.baseLayer) {
this.baseLayer = state.baseLayer;
}
return this.session.updateRenderState(state);
}
/**
* Starts rendering to the xr layer
* @returns a promise that will resolve once rendering has started
*/
public startRenderingToXRAsync() {
// Tell the engine's render loop to be driven by the xr session's refresh rate and provide xr pose information
this.scene.getEngine().customAnimationFrameRequester = {
requestAnimationFrame: this.session.requestAnimationFrame.bind(this.session),
renderFunction: (timestamp: number, xrFrame: Nullable<XRFrame>) => {
if (this._sessionEnded) {
return;
}
// Store the XR frame in the manager to be consumed by the XR camera to update pose
this.currentFrame = xrFrame;
this.onXRFrameObservable.notifyObservers(null);
this.scene.getEngine()._renderLoop();
}
};
if (this._xrNavigator.xr.native) {
this._rttProvider = this._xrNavigator.xr.getNativeRenderTargetProvider(this.session, (width: number, height: number) => {
return this.scene.getEngine().createRenderTargetTexture({ width: width, height: height }, false);
});
} else {
// Create render target texture from xr's webgl render target
this._rttProvider = new RenderTargetProvider(WebXRSessionManager._CreateRenderTargetTextureFromSession(this.session, this.scene, this.baseLayer!));
}
// Stop window's animation frame and trigger sessions animation frame
if (window.cancelAnimationFrame) { window.cancelAnimationFrame(this.scene.getEngine()._frameHandler); }
this.scene.getEngine()._renderLoop();
return Promise.resolve();
}
/**
* Gets the correct render target texture to be rendered this frame for this eye
* @param eye the eye for which to get the render target
* @returns the render target for the specified eye
*/
public getRenderTargetTextureForEye(eye: XREye): RenderTargetTexture {
return this._rttProvider!.getRenderTargetForEye(eye);
}
/**
* Stops the xrSession and restores the renderloop
* @returns Promise which resolves after it exits XR
*/
public exitXRAsync() {
if (this.session) {
try {
return this.session.end();
} catch (e) {
Logger.Warn("could not end XR session. It has ended already.");
}
}
return Promise.resolve();
}
/**
* Checks if a session would be supported for the creation options specified
* @param sessionMode session mode to check if supported eg. immersive-vr
* @returns true if supported
*/
public supportsSessionAsync(sessionMode: XRSessionMode) {
return WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
}
/**
* Creates a WebXRRenderTarget object for the XR session
* @param onStateChangedObservable optional, mechanism for enabling/disabling XR rendering canvas, used only on Web
* @param options optional options to provide when creating a new render target
* @returns a WebXR render target to which the session can render
*/
public getWebXRRenderTarget(onStateChangedObservable?: Observable<WebXRState>, options?: WebXRManagedOutputCanvasOptions): WebXRRenderTarget {
if (this._xrNavigator.xr.native) {
return this._xrNavigator.xr.getWebXRRenderTarget(this.scene.getEngine());
}
else {
return new WebXRManagedOutputCanvas(this.scene.getEngine(), this.scene.getEngine().getRenderingCanvas() as HTMLCanvasElement, onStateChangedObservable!, options);
}
}
/**
* @hidden
* Converts the render layer of xrSession to a render target
* @param session session to create render target for
* @param scene scene the new render target should be created for
*/
public static _CreateRenderTargetTextureFromSession(session: XRSession, scene: Scene, baseLayer: XRWebGLLayer) {
if (!baseLayer) {
throw "no layer";
}
// Create internal texture
var internalTexture = new InternalTexture(scene.getEngine(), InternalTextureSource.Unknown, true);
internalTexture.width = baseLayer.framebufferWidth;
internalTexture.height = baseLayer.framebufferHeight;
internalTexture._framebuffer = baseLayer.framebuffer;
// Create render target texture from the internal texture
var renderTargetTexture = new RenderTargetTexture("XR renderTargetTexture", { width: internalTexture.width, height: internalTexture.height }, scene, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true);
renderTargetTexture._texture = internalTexture;
return renderTargetTexture;
}
/**
* Disposes of the session manager
*/
public dispose() {
this.onXRFrameObservable.clear();
this.onXRSessionEnded.clear();
}
/**
* Gets a promise returning true when fullfiled if the given session mode is supported
* @param sessionMode defines the session to test
* @returns a promise
*/
public static IsSessionSupportedAsync(sessionMode: XRSessionMode): Promise<boolean> {
if (!(navigator as any).xr) {
return Promise.resolve(false);
}
// When the specs are final, remove supportsSession!
const functionToUse = (navigator as any).xr.isSessionSupported || (navigator as any).xr.supportsSession;
if (!functionToUse) {
return Promise.resolve(false);
} else {
return functionToUse.call((navigator as any).xr, sessionMode).then(() => {
return Promise.resolve(true);
}).catch((e: any) => {
Logger.Warn(e);
return Promise.resolve(false);
});
}
}
}