Working examples of this type of code can be found in the examples directory.
The basic pattern is to iterate through the XRDisplay
instances to find the one that you want to use, based on whether it's external and whether it is a pass-through display. Once you have a display, you ask it for an XRSession
that is either for rendering a Reality
or rendering an augmentation on top of a Reality
.
let displays = null // A list of XRDisplay
let display = null // The display we'll use
let session = null // The XRSession we'll use
let canvas = document.createElement('canvas') // The canvas into which we'll render
let anchoredNodes = [] // An array of { anchorOffset: XRAnchorOffset, node: <scene node like THREE.Group> }
// Get displays and then request a session
navigator.XR.getDisplays().then(disps => {
if(disps.length == 0) {
// No displays are available
return
}
displays = disps
}).catch(err => {
console.error('Error getting XR displays', err)
})
Once you have the displays, you look for one that will support the type of session that you want to start:
// Set up the options for the type of session we want
let sessionInitOptions = {
exclusive: false, // True if you want only this session to have access to the display
type: 'augmentation' // do you want the session to create a 'reality' or offer an 'augmentation' on an existing `Reality`
outputContext: new XRPresentationContext(canvas) // Then canvas into which we'll render
}
// Now test each display
for(let disp of displays){
if(display.supportsSession(sessionInitOptions)){
display = disp
break
}
}
Once you have a display and the user has chosen to start using it, you ask the display for an XRSession
and request the first frame:
display.requestSession(sessionInitOptions).then(sess => {
session = sess
session.depthNear = 0.1
session.depthFar = 1000.0
session.requestFrame(handleFrame)
)}
The scene, camera, and renderer objects below are representative APIs that have equivalents in most WebGL libs like Three.js:
function handleFrame(frame){
// Set up for the next frame
session.requestFrame(frame => { handleFrame(frame) })
// Get the pose for the head
let headCoordinateSystem = frame.getCoordinateSystem(XRCoordinateSystem.HEAD_MODEL)
let headPose = frame.getDsiplayPose(frame.getCoordinateSystem(headCoordinateSystem)
// XXX Below we will add code here to add and manage anchors
// Displays can have one or more views. A magic window display has one, a headset has two (one for each eye), so iterate through each.
for(const view of frame.views){
// Each XRView has its own projection matrix, so set the camera to use that
camera.projectionMatrix = view.projectionMatrix
// Rotate the scene around the camera using the head pose
scene.matrix = headPose.getViewMatrix(view)
// Set up the renderer to the XRView's viewport and then render
const viewport = view.getViewport(session.baseLayer)
renderer.setViewport(viewport.x, viewport.y, viewport.width, viewport.height)
renderer.render(scene, camera)
}
}
Anchors are places in space that the AR system is tracking for you. They could be a surface like a floor or table, a feature like a door knob, or just a point in space relative to the world coordinate system. When you place virtual objects in XR, you find an XRAnchor
to attach it to, possibly with an XRAnchorOffset
to indicate a position relative to the anchor.
The reason that you use anchors instead of just placing objects in a global coordinate system is that AR systems may change their relative position over time as they sense the world. A table may shift. The system may refine its estimate of the location of the floor or a wall.
First, let's add an anchor just floated in space a meter in front of the current head position.
This code uses the XRPresentationFrame
, so it would live in the handleFrame
method above, where the '// XXX' comment is:
const sceneNode = createSceneNode() // if using Three.js, could be an Object3D or a Group
let anchorUID = frame.addAnchor(headCoordinateSystem, [0, 0, -1])
scene.add(sceneNode) // Now the node is in the scene
// Save this info for update during each frame
anchoredNodes.push({
anchorOffset: new XRAnchorOffset(anchor.uid),
node: sceneNode
})
Now search for an anchor on a surface like a floor or table:
frame.findAnchor(x, y).then(anchorOffset => {
if(anchorOffset === null){
// no surface was found to place the anchor
return
}
const node = createSceneNode()
// Add the node to the scene
scene.add(node)
// Save this info for update during each frame
anchoredNodes.push({
anchorOffset: anchorOffset,
node: node
})
})
You now have a couple of anchored nodes save in anchoredNodes
, so during each frame use the most recent anchor info to update the node position:
for(let anchoredNode of anchoredNodes){
// Get the updated anchor info
const anchor = frame.getAnchor(anchoredNode.anchorOffset.anchorUID)
// Get the offset coordinates relative to the anchor's coordinate system
let offsetTransform = anchoredNode.anchorOffset.getOffsetTransform(anchor.coordinateSystem)
// Now use the offset transform to position the anchored node in the scene
anchoredNode.node.matrix = offsetTransform
}