diff --git a/build/dependencies.txt b/build/dependencies.txt
index 4201f2cbe54..89e0193954b 100644
--- a/build/dependencies.txt
+++ b/build/dependencies.txt
@@ -111,6 +111,7 @@
../src/input/element-input.js
../src/vr/vr-manager.js
../src/vr/vr-display.js
+../src/xr/xr-manager.js
../src/net/http.js
../src/script/script-registry.js
../src/script/script.js
diff --git a/examples/examples.js b/examples/examples.js
index 7eb85ae6a3a..fdfefdb409c 100644
--- a/examples/examples.js
+++ b/examples/examples.js
@@ -71,5 +71,11 @@ var categories = [
"text-wrap",
"various"
]
+ }, {
+ name: "xr",
+ examples: [
+ 'augmented-reality-basic',
+ 'virtual-reality-basic'
+ ]
}
-];
\ No newline at end of file
+];
diff --git a/examples/style.css b/examples/style.css
index c35a8f7813f..cd40ee09016 100644
--- a/examples/style.css
+++ b/examples/style.css
@@ -4,7 +4,8 @@ body {
font-family: "Proxima Nova", Helvetica, arial, sans-serif;
font-size: 16px;
margin: 0;
- overscroll-behavior: none; }
+ overscroll-behavior: none;
+}
/* Sidebar menu */
.sidenav {
@@ -22,13 +23,17 @@ body {
display: flex;
flex-direction: column;
background: #364346;
- transition: all 240ms ease-in-out; }
- @media screen and (max-width: 1024px) {
- .sidenav {
- background: rgba(54, 67, 70, 0.98);
- width: 100%; }
- .sidenav.closed {
- left: -100%; } }
+ transition: all 240ms ease-in-out;
+}
+@media screen and (max-width: 1024px) {
+ .sidenav {
+ background: rgba(54, 67, 70, 0.98);
+ width: 100%;
+ }
+ .sidenav.closed {
+ left: -100%;
+ }
+}
.sidenav-toggle {
position: absolute;
@@ -42,39 +47,50 @@ body {
display: none;
background: #20292b;
border-radius: 300px;
- cursor: pointer; }
- .sidenav-toggle:hover {
- background: black; }
- @media screen and (max-width: 1024px) {
- .sidenav-toggle {
- display: block; } }
+ cursor: pointer;
+}
+.sidenav-toggle:hover {
+ background: black;
+}
+@media screen and (max-width: 1024px) {
+ .sidenav-toggle {
+ display: block;
+ }
+}
/* Navigation menu links */
.sidenav a,
p {
text-decoration: none;
- display: block; }
+ display: block;
+}
.sidenav a {
padding: 6px 8px 6px 32px;
font-size: 1rem;
color: #b1b8ba;
- cursor: pointer; }
- .sidenav a:hover {
- color: #ffffff; }
- .sidenav a.active {
- color: #f60; }
- @media screen and (max-width: 1024px) {
- .sidenav a {
- font-size: 1.2rem;
- padding: 6px 8px 12px 32px; } }
+ cursor: pointer;
+}
+.sidenav a:hover {
+ color: #ffffff;
+}
+.sidenav a.active {
+ color: #f60;
+}
+@media screen and (max-width: 1024px) {
+ .sidenav a {
+ font-size: 1.2rem;
+ padding: 6px 8px 12px 32px;
+ }
+}
.sidenav p {
padding: 24px 8px 8px 24px;
margin: 0;
font-size: 1.4rem;
font-weight: bold;
- color: #ffffff; }
+ color: #ffffff;
+}
#example,
iframe {
@@ -82,14 +98,17 @@ iframe {
border: 0px;
left: 0;
right: 0;
- padding-left: 160px;
- width: calc(100% - 160px);
+ padding-left: 240px;
+ width: calc(100% - 240px);
height: 100%;
- overflow: auto; }
- @media screen and (max-width: 1024px) {
- #example,
- iframe {
- padding-left: 0;
- width: 100%; } }
+ overflow: auto;
+}
+@media screen and (max-width: 1024px) {
+ #example,
+iframe {
+ padding-left: 0;
+ width: 100%;
+ }
+}
/*# sourceMappingURL=style.css.map */
diff --git a/examples/style.css.map b/examples/style.css.map
index ad59ed9f2e7..5bb4af39e5e 100644
--- a/examples/style.css.map
+++ b/examples/style.css.map
@@ -1,7 +1 @@
-{
-"version": 3,
-"mappings": "AAmBA,IAAK;EACH,gBAAgB,EAAE,IAAI;EACtB,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,4CAA4C;EACzD,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,CAAC;EACT,mBAAmB,EAAE,IAAI;;AAG3B,kBAAkB;AAClB,QAAS;EACP,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,KAAK;EACZ,QAAQ,EAAE,KAAK;EAAE,6CAA6C;EAC9D,OAAO,EAAE,CAAC;EAAE,iBAAiB;EAC7B,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,UAAU,EAAE,MAAM;EAAE,+BAA+B;EACnD,WAAW,EAAE,GAAG;EAChB,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,MAAM;EACtB,UAAU,EApCE,OAAO;EAqCnB,UAAU,EAAE,qBAAqB;EACjC,qCAA6C;IAb/C,QAAS;MAcL,UAAU,EAAE,sBAAsB;MAClC,KAAK,EAAE,IAAI;MACX,eAAS;QACP,IAAI,EAAE,KAAK;;AAKjB,eAAgB;EACd,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,GAAG,EAAE,IAAI;EACT,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,GAAG;EACZ,OAAO,EAAE,IAAI;EACb,UAAU,EA5DE,OAAO;EA6DnB,aAAa,EAAE,KAAK;EACpB,MAAM,EAAE,OAAO;EACf,qBAAQ;IACN,UAAU,EAAE,KAAK;EAEnB,qCAA6C;IAhB/C,eAAgB;MAiBZ,OAAO,EAAE,KAAK;;AAIlB,2BAA2B;AAC3B;CACE;EACA,eAAe,EAAE,IAAI;EACrB,OAAO,EAAE,KAAK;;AAGhB,UAAW;EACT,OAAO,EAAE,gBAAgB;EACzB,SAAS,EAAE,IAAI;EACf,KAAK,EA3EU,OAAO;EA4EtB,MAAM,EAAE,OAAO;EACf,gBAAQ;IACN,KAAK,EA7EM,OAAO;EA+EpB,iBAAS;IACP,KAAK,EA/EK,IAAI;EAiFhB,qCAA6C;IAX/C,UAAW;MAYP,SAAS,EAAE,MAAM;MACjB,OAAO,EAAE,iBAAiB;;AAI9B,UAAW;EACT,OAAO,EAAE,iBAAiB;EAC1B,MAAM,EAAE,CAAC;EACT,SAAS,EAAE,MAAM;EACjB,WAAW,EAAE,IAAI;EACjB,KAAK,EA7FQ,OAAO;;AAgGtB;MACO;EACL,QAAQ,EAAE,KAAK;EACf,MAAM,EAAE,GAAG;EACX,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,YAAY,EAAE,KAAK;EACnB,KAAK,EAAE,kBAAkB;EACzB,MAAM,EAAE,IAAI;EACZ,QAAQ,EAAE,IAAI;EACd,qCAA6C;IAV/C;UACO;MAUH,YAAY,EAAE,CAAC;MACf,KAAK,EAAE,IAAI",
-"sources": ["style.scss"],
-"names": [],
-"file": "style.css"
-}
\ No newline at end of file
+{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAmBA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;AAAiB;EACjB;AAAY;EACZ;EACA;EACA;AAAoB;EACpB;EACA;EACA;EACA,YApCY;EAqCZ;;AACA;EAbF;IAcI;IACA;;EACA;IACE;;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YA5DY;EA6DZ;EACA;;AACA;EACE;;AAEF;EAhBF;IAiBI;;;;AAIJ;AACA;AAAA;EAEE;EACA;;;AAGF;EACE;EACA;EACA,OA3Ee;EA4Ef;;AACA;EACE,OA7EW;;AA+Eb;EACE,OA/EU;;AAiFZ;EAXF;IAYI;IACA;;;;AAIJ;EACE;EACA;EACA;EACA;EACA,OA7Fa;;;AAgGf;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EAVF;AAAA;IAWI;IACA","file":"style.css"}
\ No newline at end of file
diff --git a/examples/style.scss b/examples/style.scss
index 373d90b8883..199ba6a2575 100644
--- a/examples/style.scss
+++ b/examples/style.scss
@@ -48,7 +48,7 @@ body {
}
}
}
-
+
.sidenav-toggle {
position: absolute;
right: 12px;
@@ -108,8 +108,8 @@ iframe {
border: 0px;
left: 0;
right: 0;
- padding-left: 160px;
- width: calc(100% - 160px);
+ padding-left: 240px;
+ width: calc(100% - 240px);
height: 100%;
overflow: auto;
@media screen and (max-width: $break-medium) {
diff --git a/examples/xr/augmented-reality-basic.html b/examples/xr/augmented-reality-basic.html
new file mode 100644
index 00000000000..28e38cc487d
--- /dev/null
+++ b/examples/xr/augmented-reality-basic.html
@@ -0,0 +1,170 @@
+
+
+
+ PlayCanvas Virtual Reality
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/xr/virtual-reality-basic.html b/examples/xr/virtual-reality-basic.html
new file mode 100644
index 00000000000..73c16def9bb
--- /dev/null
+++ b/examples/xr/virtual-reality-basic.html
@@ -0,0 +1,170 @@
+
+
+
+ PlayCanvas Virtual Reality
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/callbacks.js b/src/callbacks.js
index 50e9648c895..3acaa1aff73 100644
--- a/src/callbacks.js
+++ b/src/callbacks.js
@@ -171,3 +171,9 @@
* @callback pc.callbacks.VrFrame
* @description Callback used by {@link pc.VrDisplay#requestAnimationFrame}.
*/
+
+/**
+ * @callback pc.callbacks.XrError
+ * @description Callback used by {@link pc.XrManager#endXr} and {@link pc.XrManager#startXr}.
+ * @param {Error|null} err - The Error object or null if operation was successfull.
+ */
diff --git a/src/framework/application.js b/src/framework/application.js
index 3541b2b2ded..965e224e15a 100644
--- a/src/framework/application.js
+++ b/src/framework/application.js
@@ -124,6 +124,17 @@ Object.assign(pc, function () {
* this.app.systems.sound.volume = 0.5;
*/
+ /**
+ * @name pc.Application#xr
+ * @type {pc.XrManager}
+ * @description The XR Manager that provides ability to start VR/AR sessions.
+ * @example
+ * // check if VR is available
+ * if (app.xr.isAvailable(pc.XRTYPE_VR)) {
+ * // VR is available
+ * }
+ */
+
/**
* @name pc.Application#loader
* @type {pc.ResourceLoader}
@@ -260,6 +271,11 @@ Object.assign(pc, function () {
// for compatibility
this.context = this;
+ if (! options.graphicsDeviceOptions)
+ options.graphicsDeviceOptions = { };
+
+ options.graphicsDeviceOptions.xrCompatible = true;
+
this.graphicsDevice = new pc.GraphicsDevice(canvas, options.graphicsDeviceOptions);
this.stats = new pc.ApplicationStats(this.graphicsDevice);
this._audioManager = new pc.SoundManager(options);
@@ -536,6 +552,7 @@ Object.assign(pc, function () {
this.elementInput.app = this;
this.vr = null;
+ this.xr = new pc.XrManager(this);
this._inTools = false;
@@ -1381,6 +1398,10 @@ Object.assign(pc, function () {
resizeCanvas: function (width, height) {
if (!this._allowResize) return; // prevent resizing (e.g. if presenting in VR HMD)
+ // prevent resizing when in XR session
+ if (this.xr && this.xr.session)
+ return;
+
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
@@ -1744,6 +1765,7 @@ Object.assign(pc, function () {
this.vr.destroy();
this.vr = null;
}
+ this.xr.end();
this.graphicsDevice.destroy();
this.graphicsDevice = null;
@@ -1789,13 +1811,19 @@ Object.assign(pc, function () {
// create tick function to be wrapped in closure
var makeTick = function (_app) {
var app = _app;
- return function (timestamp) {
- if (!app.graphicsDevice) {
+ var frameRequest;
+
+ return function (timestamp, frame) {
+ if (!app.graphicsDevice)
return;
- }
Application._currentApplication = app;
+ if (frameRequest) {
+ cancelAnimationFrame(frameRequest);
+ frameRequest = null;
+ }
+
// have current application pointer in pc
pc.app = app;
@@ -1809,19 +1837,27 @@ Object.assign(pc, function () {
// Submit a request to queue up a new animation frame immediately
if (app.vr && app.vr.display) {
- app.vr.display.requestAnimationFrame(app.tick);
+ frameRequest = app.vr.display.requestAnimationFrame(app.tick);
+ } else if (app.xr.session) {
+ frameRequest = app.xr.session.requestAnimationFrame(app.tick);
} else {
- requestAnimationFrame(app.tick);
+ frameRequest = requestAnimationFrame(app.tick);
}
- if (app.graphicsDevice.contextLost) {
+ if (app.graphicsDevice.contextLost)
return;
- }
// #ifdef PROFILER
app._fillFrameStats(now, dt, ms);
// #endif
+ if (frame) {
+ app.xr.calculateViews(frame);
+ app.graphicsDevice.defaultFramebuffer = frame.session.renderState.baseLayer.framebuffer;
+ } else {
+ app.graphicsDevice.defaultFramebuffer = null;
+ }
+
app.update(dt);
if (app.autoRender || app.renderNextFrame) {
diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js
index f1564b2e1e0..4d57f31bf43 100644
--- a/src/framework/components/camera/component.js
+++ b/src/framework/components/camera/component.js
@@ -556,6 +556,51 @@ Object.assign(pc, function () {
} else {
callback("Not presenting VR");
}
+ },
+
+ /**
+ * @function
+ * @name pc.CameraComponent#startXr
+ * @description Attempt to start XR session with this camera
+ * @param {string} type - The type of session. Can be one of the following:
+ *
+ * * {@link pc.XRTYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element.
+ * * {@link pc.XRTYPE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features.
+ * * {@link pc.XRTYPE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment.
+ *
+ * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session.
+ * @example
+ * // On an entity with a camera component
+ * this.entity.camera.startXr(pc.XRTYPE_VR, function (err) {
+ * if (err) {
+ * // failed to start XR session
+ * } else {
+ * // in XR
+ * }
+ * });
+ */
+ startXr: function (type, callback) {
+ this.system.app.xr.start(this, type, callback);
+ },
+
+ /**
+ * @function
+ * @name pc.CameraComponent#endXr
+ * @description Attempt to end XR session of this camera
+ * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is ended. The callback has one argument Error - it is null if successfully ended XR session.
+ * @example
+ * // On an entity with a camera component
+ * this.entity.camera.endXr(function (err) {
+ * // not anymore in XR
+ * });
+ */
+ endXr: function (callback) {
+ if (! this.camera.xr) {
+ if (callback) callback(new Error("Camera is not in XR"));
+ return;
+ }
+
+ this.camera.xr.end(callback);
}
});
diff --git a/src/graphics/device.js b/src/graphics/device.js
index 5b8d498e6e3..c690817b207 100644
--- a/src/graphics/device.js
+++ b/src/graphics/device.js
@@ -186,6 +186,7 @@ Object.assign(pc, function () {
this._enableAutoInstancing = false;
this.autoInstancingMaxObjects = 16384;
this.attributesInvalidated = true;
+ this.defaultFramebuffer = null;
this.boundBuffer = null;
this.boundElementBuffer = null;
this.instancedAttribs = { };
@@ -1269,7 +1270,7 @@ Object.assign(pc, function () {
this.setFramebuffer(target._glFrameBuffer);
}
} else {
- this.setFramebuffer(null);
+ this.setFramebuffer(this.defaultFramebuffer);
}
},
diff --git a/src/scene/forward-renderer.js b/src/scene/forward-renderer.js
index b058a55e253..3bb58f6e56e 100644
--- a/src/scene/forward-renderer.js
+++ b/src/scene/forward-renderer.js
@@ -555,6 +555,10 @@ Object.assign(pc, function () {
viewInvMat.copy(viewMat).invert();
this.viewInvId.setValue(viewInvMat.data);
camera.frustum.update(projMat, viewMat);
+ } else if (camera.xr && camera.xr.views.length) {
+ // calculate frustum based on XR view
+ var view = camera.xr.views[0];
+ camera.frustum.update(view.projMat, view.viewOffMat);
return;
}
@@ -577,43 +581,9 @@ Object.assign(pc, function () {
// make sure colorWrite is set to true to all channels, if you want to fully clear the target
setCamera: function (camera, target, clear, cullBorder) {
var vrDisplay = camera.vrDisplay;
- if (!vrDisplay || !vrDisplay.presenting) {
- // Projection Matrix
- projMat = camera.getProjectionMatrix();
- if (camera.overrideCalculateProjection) camera.calculateProjection(projMat, pc.VIEW_CENTER);
- this.projId.setValue(projMat.data);
-
- // ViewInverse Matrix
- if (camera.overrideCalculateTransform) {
- camera.calculateTransform(viewInvMat, pc.VIEW_CENTER);
- } else {
- var pos = camera._node.getPosition();
- var rot = camera._node.getRotation();
- viewInvMat.setTRS(pos, rot, pc.Vec3.ONE);
- }
- this.viewInvId.setValue(viewInvMat.data);
-
- // View Matrix
- viewMat.copy(viewInvMat).invert();
- this.viewId.setValue(viewMat.data);
-
- // View 3x3
- mat3FromMat4(viewMat3, viewMat);
- this.viewId3.setValue(viewMat3.data);
-
- // ViewProjection Matrix
- viewProjMat.mul2(projMat, viewMat);
- this.viewProjId.setValue(viewProjMat.data);
-
- // View Position (world space)
- var cameraPos = camera._node.getPosition();
- this.viewPos[0] = cameraPos.x;
- this.viewPos[1] = cameraPos.y;
- this.viewPos[2] = cameraPos.z;
- this.viewPosId.setValue(this.viewPos);
+ var parent, transform;
- camera.frustum.update(projMat, viewMat);
- } else {
+ if (vrDisplay && vrDisplay.presenting) {
// Projection LR
projL = vrDisplay.leftProj;
projR = vrDisplay.rightProj;
@@ -632,9 +602,9 @@ Object.assign(pc, function () {
viewR.copy(viewInvR).invert();
viewMat.copy(viewInvMat).invert();
} else {
- var parent = camera._node.parent;
+ parent = camera._node.parent;
if (parent) {
- var transform = parent.getWorldTransform();
+ transform = parent.getWorldTransform();
// ViewInverse LR (parent)
viewInvL.mul2(transform, vrDisplay.leftViewInv);
@@ -677,6 +647,69 @@ Object.assign(pc, function () {
viewPosR.y = viewInvR.data[13];
viewPosR.z = viewInvR.data[14];
+ camera.frustum.update(projMat, viewMat);
+ } else if (camera.xr && camera.xr.session) {
+ parent = camera._node.parent;
+ if (parent) transform = parent.getWorldTransform();
+
+ var views = camera.xr.views;
+
+ for (var v = 0; v < views.length; v++) {
+ var view = views[v];
+
+ if (parent) {
+ view.viewInvOffMat.mul2(transform, view.viewInvMat);
+ view.viewOffMat.copy(view.viewInvOffMat).invert();
+ } else {
+ view.viewInvOffMat.copy(view.viewInvMat);
+ view.viewOffMat.copy(view.viewMat);
+ }
+
+ mat3FromMat4(view.viewMat3, view.viewOffMat);
+ view.projViewOffMat.mul2(view.projMat, view.viewOffMat);
+
+ view.positionOff.x = view.viewInvOffMat.data[12];
+ view.positionOff.y = view.viewInvOffMat.data[13];
+ view.positionOff.z = view.viewInvOffMat.data[14];
+
+ camera.frustum.update(view.projMat, view.viewOffMat);
+ }
+ } else {
+ // Projection Matrix
+ projMat = camera.getProjectionMatrix();
+ if (camera.overrideCalculateProjection) camera.calculateProjection(projMat, pc.VIEW_CENTER);
+ this.projId.setValue(projMat.data);
+
+ // ViewInverse Matrix
+ if (camera.overrideCalculateTransform) {
+ camera.calculateTransform(viewInvMat, pc.VIEW_CENTER);
+ } else {
+ var pos = camera._node.getPosition();
+ var rot = camera._node.getRotation();
+ viewInvMat.setTRS(pos, rot, pc.Vec3.ONE);
+ }
+ this.viewInvId.setValue(viewInvMat.data);
+
+ // View Matrix
+ viewMat.copy(viewInvMat).invert();
+ this.viewId.setValue(viewMat.data);
+
+ // View 3x3
+ mat3FromMat4(viewMat3, viewMat);
+ this.viewId3.setValue(viewMat3.data);
+
+ // ViewProjection Matrix
+ viewProjMat.mul2(projMat, viewMat);
+ this.viewProjId.setValue(viewProjMat.data);
+
+ // View Position (world space)
+ var cameraPos = camera._node.getPosition();
+ this.viewPos[0] = cameraPos.x;
+ this.viewPos[1] = cameraPos.y;
+ this.viewPos[2] = cameraPos.z;
+
+ this.viewPosId.setValue(this.viewPos);
+
camera.frustum.update(projMat, viewMat);
}
@@ -1728,6 +1761,29 @@ Object.assign(pc, function () {
this.viewPosId.setValue(this.viewPos);
i += this.drawInstance2(device, drawCall, mesh, style);
this._forwardDrawCalls++;
+ } else if (camera.xr && camera.xr.session && camera.xr.views.length) {
+ var views = camera.xr.views;
+
+ for (var v = 0; v < views.length; v++) {
+ var view = views[v];
+
+ device.setViewport(view.viewport.x, view.viewport.y, view.viewport.z, view.viewport.w);
+
+ this.projId.setValue(view.projMat.data);
+ this.viewId.setValue(view.viewOffMat.data);
+ this.viewInvId.setValue(view.viewInvOffMat.data);
+ this.viewId3.setValue(view.viewMat3.data);
+ this.viewProjId.setValue(view.projViewOffMat.data);
+ this.viewPosId.setValue(view.positionOff.data);
+
+ if (v === 0) {
+ i += this.drawInstance(device, drawCall, mesh, style, true);
+ } else {
+ i += this.drawInstance2(device, drawCall, mesh, style, true);
+ }
+
+ this._forwardDrawCalls++;
+ }
} else {
i += this.drawInstance(device, drawCall, mesh, style, true);
this._forwardDrawCalls++;
@@ -2280,7 +2336,6 @@ Object.assign(pc, function () {
}
for (pass = 0; pass < passes; pass++) {
-
if (type === pc.LIGHTTYPE_POINT) {
shadowCamNode.setRotation(pointLightRotations[pass]);
shadowCam.renderTarget = light._shadowCubeMap[pass];
diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js
new file mode 100644
index 00000000000..26763f9f340
--- /dev/null
+++ b/src/xr/xr-manager.js
@@ -0,0 +1,476 @@
+Object.assign(pc, function () {
+ var sessionTypes = {
+ /**
+ * @constant
+ * @type string
+ * @name pc.XRTYPE_INLINE
+ * @description Inline - always available type of session. It has limited features availability and is rendered into HTML element.
+ */
+ XRTYPE_INLINE: 'inline',
+
+ /**
+ * @constant
+ * @type string
+ * @name pc.XRTYPE_VR
+ * @description Immersive VR - session that provides exclusive access to VR device with best available tracking features.
+ */
+ XRTYPE_VR: 'immersive-vr',
+
+ /**
+ * @constant
+ * @type string
+ * @name pc.XRTYPE_AR
+ * @description Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment.
+ */
+ XRTYPE_AR: 'immersive-ar'
+ };
+
+
+ /**
+ * @class
+ * @name pc.XrManager
+ * @augments pc.EventHandler
+ * @classdesc Manage and update XR session and its states.
+ * @description Manage and update XR session and its states.
+ * @param {pc.Application} app - The main application.
+ * @property {boolean} supported Returns true if XR is supported.
+ * @property {boolean} active Returns true if XR session is running.
+ * @property {string|null} type Returns type of curently running XR session or null if no session is running.
+ */
+ var XrManager = function (app) {
+ pc.EventHandler.call(this);
+
+ var self = this;
+
+ this.app = app;
+
+ this._supported = !! navigator.xr;
+
+ this._available = { };
+ for (var key in sessionTypes) {
+ this._available[sessionTypes[key]] = false;
+ }
+
+ this._type = null;
+ this._session = null;
+ this._baseLayer = null;
+ this._referenceSpace = null;
+ this._inputSources = [];
+
+ this._camera = null;
+ this._pose = null;
+ this.views = [];
+ this.viewsPool = [];
+ this.position = new pc.Vec3();
+ this.rotation = new pc.Quat();
+
+ this._depthNear = 0.1;
+ this._depthFar = 1000;
+
+ this._width = 0;
+ this._height = 0;
+
+ // TODO
+ // 1. HMD class with its params
+ // 2. Space class
+ // 3. Controllers class
+
+ if (this._supported) {
+ navigator.xr.addEventListener('devicechange', function () {
+ self._deviceAvailabilityCheck();
+ });
+ this._deviceAvailabilityCheck();
+ }
+ };
+ XrManager.prototype = Object.create(pc.EventHandler.prototype);
+ XrManager.prototype.constructor = XrManager;
+
+ /**
+ * @event
+ * @name pc.XrManager#available
+ * @description Fired when availability of specific XR type is changed.
+ * @param {string} type - The session type that has changed availability.
+ * @param {boolean} available - True if specified session type is now available.
+ * @example
+ * app.xr.on('available', function (type, available) {
+ * console.log('"' + type + '" XR session is now ' + (available ? 'available' : 'unavailable'));
+ * });
+ */
+
+ /**
+ * @event
+ * @name pc.XrManager#available:[type]
+ * @description Fired when availability of specific XR type is changed.
+ * @param {boolean} available - True if specified session type is now available.
+ * @example
+ * app.xr.on('available:' + pc.XRTYPE_VR, function (available) {
+ * console.log('Immersive VR session is now ' + (available ? 'available' : 'unavailable'));
+ * });
+ */
+
+ /**
+ * @event
+ * @name pc.XrManager#start
+ * @description Fired when XR session is started
+ * @example
+ * app.xr.on('start', function () {
+ * // XR session has started
+ * });
+ */
+
+ /**
+ * @event
+ * @name pc.XrManager#end
+ * @description Fired when XR session is ended
+ * @example
+ * app.xr.on('end', function () {
+ * // XR session has ended
+ * });
+ */
+
+ /**
+ * @function
+ * @name pc.XrManager#start
+ * @description Attempts to start XR session for provided {@link pc.CameraComponent} and optionally fires callback when session is created or failed to create.
+ * @param {pc.CameraComponent} camera - it will be used to render XR session and manipulated based on pose tracking
+ * @param {string} type - session type. Can be one of the following:
+ *
+ * * {@link pc.XRTYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element.
+ * * {@link pc.XRTYPE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features.
+ * * {@link pc.XRTYPE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment.
+ *
+ * @example
+ * button.on('click', function () {
+ * app.xr.start(camera, PC.XRTYPE_VR);
+ * });
+ * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session.
+ */
+ XrManager.prototype.start = function (camera, type, callback) {
+ if (! this._available[type]) {
+ if (callback) callback(new Error('XR is not available'));
+ return;
+ }
+
+ if (this._session) {
+ if (callback) callback(new Error('XR session is already started'));
+ return;
+ }
+
+ var self = this;
+
+ this._camera = camera;
+ this._camera.camera.xr = this;
+ this._type = type;
+
+ this._setClipPlanes(camera.nearClip, camera.farClip);
+
+ // TODO
+ // makeXRCompatible
+ // scenario to test:
+ // 1. app is running on integrated GPU
+ // 2. XR device is connected, to another GPU
+ // 3. probably immersive-vr will fail to be created
+ // 4. call makeXRCompatible, very likely will lead to context loss
+
+ navigator.xr.requestSession(type).then(function (session) {
+ self._onSessionStart(session, callback);
+ });
+ };
+
+ /**
+ * @function
+ * @name pc.XrManager#end
+ * @description Attempts to end XR session and optionally fires callback when session is ended or failed to end.
+ * @example
+ * app.keyboard.on('keydown', function (evt) {
+ * if (evt.key === pc.KEY_ESCAPE && app.xr.active) {
+ * app.xr.end();
+ * }
+ * });
+ * @param {pc.callbacks.XrError} [callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session.
+ */
+ XrManager.prototype.end = function (callback) {
+ if (! this._session) {
+ if (callback) callback(new Error('XR Session is not initialized'));
+ return;
+ }
+
+ if (callback) this.once('end', callback);
+
+ this._session.end();
+ };
+
+ /**
+ * @function
+ * @name pc.XrManager#isAvailable
+ * @description Check if specific type of session is available
+ * @param {string} type - session type. Can be one of the following:
+ *
+ * * {@link pc.XRTYPE_INLINE}: Inline - always available type of session. It has limited features availability and is rendered into HTML element.
+ * * {@link pc.XRTYPE_VR}: Immersive VR - session that provides exclusive access to VR device with best available tracking features.
+ * * {@link pc.XRTYPE_AR}: Immersive AR - session that provides exclusive access to VR/AR device that is intended to be blended with real-world environment.
+ *
+ * @example
+ * if (app.xr.isAvailable(pc.XRTYPE_VR)) {
+ * // VR is available
+ * }
+ * @returns {boolean} True if specified session type is available.
+ */
+ XrManager.prototype.isAvailable = function (type) {
+ return this._available[type];
+ };
+
+ XrManager.prototype._deviceAvailabilityCheck = function () {
+ for (var key in this._available) {
+ this._sessionSupportCheck(key);
+ }
+ };
+
+ XrManager.prototype._sessionSupportCheck = function (type) {
+ var self = this;
+
+ navigator.xr.isSessionSupported(type).then(function (available) {
+ if (self._available[type] === available)
+ return;
+
+ self._available[type] = available;
+ self.fire('available', type, available);
+ self.fire('available:' + type, available);
+ });
+ };
+
+ XrManager.prototype._onSessionStart = function (session, callback) {
+ var self = this;
+
+ this._session = session;
+
+ var onVisibilityChange = function () {
+ self.fire('visibility:change', session.visibilityState);
+ };
+
+ var onInputSourcesChange = function (evt) {
+ var i;
+
+ for (i = 0; i < evt.removed.length; i++) {
+ self._inputSourceRemove(evt.removed[i]);
+ }
+ for (i = 0; i < evt.added.length; i++) {
+ self._inputSourceAdd(evt.added[i]);
+ }
+ };
+
+ var onClipPlanesChange = function () {
+ self._setClipPlanes(self._camera.nearClip, self._camera.farClip);
+ };
+
+ // clean up once session is ended
+ var onEnd = function () {
+ self._session = null;
+ self._referenceSpace = null;
+ self._inputSources = [];
+ self._pose = null;
+ self.views = [];
+ self._width = 0;
+ self._height = 0;
+ self._type = null;
+
+ if (self._camera) {
+ self._camera.off('set_nearClip', onClipPlanesChange);
+ self._camera.off('set_farClip', onClipPlanesChange);
+
+ self._camera.camera.xr = null;
+ self._camera = null;
+ }
+
+ session.removeEventListener('end', onEnd);
+ session.removeEventListener('visibilitychange', onVisibilityChange);
+ session.removeEventListener('inputsourceschange', onInputSourcesChange);
+
+ // old requestAnimationFrame will never be triggered,
+ // so queue up new tick
+ self.app.tick();
+
+ self.fire('end');
+ };
+
+ session.addEventListener('end', onEnd);
+ session.addEventListener('visibilitychange', onVisibilityChange);
+ session.addEventListener('inputsourceschange', onInputSourcesChange);
+
+ this._camera.on('set_nearClip', onClipPlanesChange);
+ this._camera.on('set_farClip', onClipPlanesChange);
+
+ this._baseLayer = new XRWebGLLayer(session, this.app.graphicsDevice.gl);
+
+ session.updateRenderState({
+ baseLayer: this._baseLayer,
+ depthNear: this._depthNear,
+ depthFar: this._depthFar
+ });
+
+ // request reference space
+ session.requestReferenceSpace('local').then(function (referenceSpace) {
+ self._referenceSpace = referenceSpace;
+
+ // old requestAnimationFrame will never be triggered,
+ // so queue up new tick
+ self.app.tick();
+
+ if (callback) callback(null);
+ self.fire('start');
+ });
+ };
+
+ XrManager.prototype._inputSourceAdd = function (inputSource) {
+ this._inputSources.push(inputSource);
+ this.fire('inputSource:add', inputSource);
+ };
+
+ XrManager.prototype._inputSourceRemove = function (inputSource) {
+ var ind = this._inputSources.indexOf(inputSource);
+ if (ind === -1) return;
+ this._inputSources.splice(ind, 1);
+ this.fire('inputSource:remove', inputSource);
+ };
+
+ XrManager.prototype._setClipPlanes = function (near, far) {
+ near = Math.min(0.0001, Math.max(0.1, near));
+ far = Math.max(1000, far);
+
+ if (this._depthNear === near && this._depthFar === far)
+ return;
+
+ this._depthNear = near;
+ this._depthFar = far;
+
+ if (! this._session)
+ return;
+
+ // if session is available,
+ // queue up render state update
+ this._session.updateRenderState({
+ depthNear: this._depthNear,
+ depthFar: this._depthFar
+ });
+ };
+
+ XrManager.prototype.calculateViews = function (frame) {
+ if (! this._session) return;
+
+ var i, view, viewRaw, layer, viewport;
+ var lengthNew;
+
+ // canvas resolution should be set on first frame availability or resolution changes
+ var width = frame.session.renderState.baseLayer.framebufferWidth;
+ var height = frame.session.renderState.baseLayer.framebufferHeight;
+ if (this._width !== width || this._height !== height) {
+ this._width = width;
+ this._height = height;
+ this.app.graphicsDevice.setResolution(width, height);
+ }
+
+ this._pose = frame.getViewerPose(this._referenceSpace);
+ lengthNew = this._pose ? this._pose.views.length : 0;
+
+ if (lengthNew > this.views.length) {
+ // add new views into list
+ for (i = 0; i <= (lengthNew - this.views.length); i++) {
+ view = this.viewsPool.pop();
+ if (! view) {
+ view = {
+ viewport: new pc.Vec4(),
+ projMat: new pc.Mat4(),
+ viewMat: new pc.Mat4(),
+ viewOffMat: new pc.Mat4(),
+ viewInvMat: new pc.Mat4(),
+ viewInvOffMat: new pc.Mat4(),
+ projViewOffMat: new pc.Mat4(),
+ viewMat3: new pc.Mat3(),
+ position: new pc.Vec3(),
+ positionOff: new pc.Vec3(),
+ rotation: new pc.Quat()
+ };
+ }
+
+ this.views.push(view);
+ }
+ } else if (lengthNew <= this.views.length) {
+ // remove views from list into pool
+ for (i = 0; i < (this.views.length - lengthNew); i++) {
+ this.viewsPool.push(this.views.pop());
+ }
+ }
+
+ // reset position
+ var posePosition = this._pose.transform.position;
+ var poseOrientation = this._pose.transform.orientation;
+ this.position.set(posePosition.x, posePosition.y, posePosition.z);
+ this.rotation.set(poseOrientation.x, poseOrientation.y, poseOrientation.z, poseOrientation.w);
+
+ if (this._pose) {
+ layer = frame.session.renderState.baseLayer;
+
+ for (i = 0; i < this._pose.views.length; i++) {
+ // for each view, calculate matrices
+ viewRaw = this._pose.views[i];
+ view = this.views[i];
+ viewport = layer.getViewport(viewRaw);
+
+ view.viewport.x = viewport.x;
+ view.viewport.y = viewport.y;
+ view.viewport.z = viewport.width;
+ view.viewport.w = viewport.height;
+
+ view.projMat.set(viewRaw.projectionMatrix);
+ view.viewMat.set(viewRaw.transform.inverse.matrix);
+ view.viewInvMat.set(viewRaw.transform.matrix);
+ }
+ }
+
+ // position and rotate camera based on calculated vectors
+ this._camera.camera._node.setLocalPosition(this.position);
+ this._camera.camera._node.setLocalRotation(this.rotation);
+ };
+
+ Object.defineProperty(XrManager.prototype, 'supported', {
+ get: function () {
+ return this._supported;
+ }
+ });
+
+ Object.defineProperty(XrManager.prototype, 'active', {
+ get: function () {
+ return !! this._session;
+ }
+ });
+
+ Object.defineProperty(XrManager.prototype, 'type', {
+ get: function () {
+ return this._type;
+ }
+ });
+
+ Object.defineProperty(XrManager.prototype, 'session', {
+ get: function () {
+ return this._session;
+ }
+ });
+
+ Object.defineProperty(XrManager.prototype, 'visibilityState', {
+ get: function () {
+ if (! this._session)
+ return null;
+
+ return this._session.visibilityState;
+ }
+ });
+
+
+ var obj = {
+ XrManager: XrManager
+ };
+ Object.assign(obj, sessionTypes);
+
+
+ return obj;
+}());