Home Reference Source

src/display/WebXRDisplay.js

import SceneDisplay from './SceneDisplay.js'
import * as displayConstants from './Constants.js'

/**
WebXRDisplay uses the WebXR Device API to render a scene
*/
const WebXRDisplay = class extends SceneDisplay {
	constructor(xrDevice, displayMode, domElement, camera, scene, tickCallback = null) {
		super(displayMode, camera, scene, tickCallback)
		this._render = this._render.bind(this)
		this._xrDevice = xrDevice
		this._dom = domElement

		this._renderer = null
		this._session = null // XRSession
		this._eyeLevelFrameOfReference = null

		this._bodyBackgroundColor = null

		// The context and renderer for our 3D scene
		this._glCanvas = null
		this._glContext = null // Will be created during session setup

		if (this._displayMode === displayConstants.PORTAL) {
			// Create the output context for the composited session render
			this._xrCanvas = dom.canvas({ class: 'xr-canvas' })
			this._xrContext = this._xrCanvas.getContext('xrpresent')
			if (this._xrContext === null) {
				throw new Error('Could not create the XR context')
			}
		} else {
			// immersive mode engines don't use this canvas as the composited results are rendered into the headset
			this._xrCanvas = null
			this._xrContext = null
		}
	}

	get renderer() {
		return this._renderer
	}

	get isStarted() {
		return this._session !== null
	}

	start() {
		return new Promise((resolve, reject) => {
			let sessionInitParamers
			if (this._displayMode === displayConstants.PORTAL) {
				sessionInitParamers = {
					outputContext: this._xrContext
				}
			} else {
				sessionInitParamers = {
					exclusive: true
				}
			}
			if (this._session !== null) {
				this._session.end()
				this._session = null
			}

			this._glCanvas = dom.canvas({ class: 'gl-canvas' })
			this._glContext = this._glCanvas.getContext('webgl', {
				compatibleXRDevice: this._xrDevice
			})
			if (!this._glContext) throw new Error('Could not create a webgl context')

			this._renderer = new THREE.WebGLRenderer({
				canvas: this._glCanvas,
				context: this._glContext,
				antialias: false,
				alpha: false
			})
			this._renderer.autoClear = false
			this._renderer.setPixelRatio(1)
			this._renderer.setClearColor('#000', 0)

			this._xrDevice
				.requestSession(sessionInitParamers)
				.then(session => {
					this._session = session
					// Set the session's base layer into which the app will render
					this._session.baseLayer = new XRWebGLLayer(this._session, this._glContext)

					if (this._displayMode === displayConstants.PORTAL) {
						this._dom.appendChild(this._xrCanvas)
					}

					this._session
						.requestFrameOfReference('eye-level')
						.then(frameOfReference => {
							this._eyeLevelFrameOfReference = frameOfReference
							this._session.requestAnimationFrame(this._render)
							this._bodyBackgroundColor = document.body.style['background-color']
							document.body.style['background-color'] = 'inherit'
							resolve(this._displayMode)
						})
						.catch(err => {
							document.body.style['background-color'] = this._bodyBackgroundColor
							reject(err)
						})
				})
				.catch(err => {
					reject(err)
				})
		})
	}

	stop() {
		return new Promise((resolve, reject) => {
			if (this._session !== null) {
				this._session.end()
				this._session = null
				if (this._displayMode === displayConstants.PORTAL) {
					this._dom.removeChild(this._xrCanvas)
				}
			}
			document.body.style['background-color'] = this._bodyBackgroundColor
			resolve(this._displayMode)
		})
	}

	_render(t, frame) {
		if (this._session === null || !this._session.baseLayer) {
			return
		}
		this._session.requestAnimationFrame(this._render)

		const pose = frame.getDevicePose(this._eyeLevelFrameOfReference)
		if (!pose) {
			console.log('No pose')
			return
		}

		if (this._tickCallback) {
			this._tickCallback()
		}
		if (this._session === null || !this._session.baseLayer) {
			return
		}

		this._renderer.setSize(this._session.baseLayer.framebufferWidth, this._session.baseLayer.framebufferHeight, false)
		this._renderer.clear()

		// Render each view into this._session.baseLayer.context
		for (const view of frame.views) {
			// Each XRView has its own projection matrix, so set the camera to use that
			this._camera.matrix.fromArray(pose.getViewMatrix(view))
			this._camera.updateMatrixWorld()
			this._camera.projectionMatrix.fromArray(view.projectionMatrix)

			// Set up the renderer to the XRView's viewport and then render
			this._renderer.clearDepth()
			const viewport = this._session.baseLayer.getViewport(view)
			this._renderer.setViewport(viewport.x, viewport.y, viewport.width, viewport.height)
			this._renderer.render(this._scene, this._camera)
		}
	}
}

export default WebXRDisplay