diff --git a/docs/components/anchored.md b/docs/components/anchored.md new file mode 100644 index 00000000000..1619a0270cd --- /dev/null +++ b/docs/components/anchored.md @@ -0,0 +1,28 @@ +--- +title: anchored +type: components +layout: docs +parent_section: components +source_code: src/components/anchored.js +examples: [] +--- + +[webxranchors]: https://immersive-web.github.io/anchors/ + +It requires a browser supporting the [WebXR Anchors module][webxranchors]. + +Fix any entity to a position and rotation in the real world. Apply the anchored component to an entity and call the method `el.components.createAnchor(position, quaternion)` to anchor it to a position and rotation corresponding to real world coordinates. + + +## Example + +```html + +``` + +## Properties + +| Properties | Description | +|-------------------|-------------------------------------------------------------------------| +| persistent | If the anchor persists on page reloads. The entity must have an id. | + diff --git a/examples/index.html b/examples/index.html index 26a8b639be3..08246e6675c 100644 --- a/examples/index.html +++ b/examples/index.html @@ -154,6 +154,7 @@

Examples

  • 360° Image
  • 360° Video
  • 3D Model (glTF)
  • +
  • Anchor (Mixed Reality)
  • Examples from Documentation

    diff --git a/examples/mixed-reality/anchor/anchor-grabbed-entity.js b/examples/mixed-reality/anchor/anchor-grabbed-entity.js new file mode 100644 index 00000000000..b9b209f0c21 --- /dev/null +++ b/examples/mixed-reality/anchor/anchor-grabbed-entity.js @@ -0,0 +1,23 @@ +/* global AFRAME */ +AFRAME.registerComponent('anchor-grabbed-entity', { + init: function () { + this.el.addEventListener('grabstarted', this.deleteAnchor.bind(this)); + this.el.addEventListener('grabended', this.updateAnchor.bind(this)); + }, + + updateAnchor: function (evt) { + var grabbedEl = evt.detail.grabbedEl; + var anchoredComponent = grabbedEl.components.anchored; + if (anchoredComponent) { + anchoredComponent.createAnchor(grabbedEl.object3D.position, grabbedEl.object3D.quaternion); + } + }, + + deleteAnchor: function (evt) { + var grabbedEl = evt.detail.grabbedEl; + var anchoredComponent = grabbedEl.components.anchored; + if (anchoredComponent) { + anchoredComponent.deleteAnchor(); + } + } +}); diff --git a/examples/mixed-reality/anchor/button.js b/examples/mixed-reality/anchor/button.js new file mode 100644 index 00000000000..92afbacf2ef --- /dev/null +++ b/examples/mixed-reality/anchor/button.js @@ -0,0 +1,35 @@ +/* global AFRAME */ +AFRAME.registerComponent('button', { + init: function () { + var buttonContainerEl = this.buttonContainerEl = document.createElement('div'); + var buttonEl = this.buttonEl = document.createElement('button'); + var style = document.createElement('style'); + var css = + '.a-button-container {box-sizing: border-box; display: inline-block; height: 34px; padding: 0;;' + + 'bottom: 20px; width: 150px; left: calc(50% - 75px); position: absolute; color: white;' + + 'font-size: 12px; line-height: 12px; border: none;' + + 'border-radius: 5px}' + + '.a-button {cursor: pointer; padding: 0px 10px 0 10px; font-weight: bold; color: #666; border: 3px solid #666; box-sizing: border-box; vertical-align: middle; max-width: 200px; border-radius: 10px; height: 34px; background-color: white; margin: 0;}' + + '.a-button:hover {border-color: #ef2d5e; color: #ef2d5e}'; + + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + document.getElementsByTagName('head')[0].appendChild(style); + + buttonContainerEl.classList.add('a-button-container'); + buttonEl.classList.add('a-button'); + buttonEl.addEventListener('click', this.onClick.bind(this)); + + buttonContainerEl.appendChild(buttonEl); + + this.el.sceneEl.appendChild(buttonContainerEl); + buttonEl.innerHTML = 'NEXT PAINTING'; + }, + + onClick: function () { + + } +}); diff --git a/examples/mixed-reality/anchor/index.html b/examples/mixed-reality/anchor/index.html new file mode 100644 index 00000000000..5fa45e90e3e --- /dev/null +++ b/examples/mixed-reality/anchor/index.html @@ -0,0 +1,47 @@ + + + + + Anchor (Mixed Reality) • A-Frame + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/mixed-reality/anchor/message.html b/examples/mixed-reality/anchor/message.html new file mode 100644 index 00000000000..ccd0d158377 --- /dev/null +++ b/examples/mixed-reality/anchor/message.html @@ -0,0 +1,11 @@ +

    +This demo requires a browser supporting the WebXR Anchors Module +

    + +

    +In AR mode the user can pinch to grab the painting and release it at any position and orientation. The painting will remain anchored to the real world coordinate where it was released by the user. The anchor will persists upon page reload. +

    + +

    +Paintings by Van Gogh, Alphonse Mucha, El Greco and Edvard Munch. Frame model by theGentleGiant +

    \ No newline at end of file diff --git a/examples/mixed-reality/anchor/painting-changer.js b/examples/mixed-reality/anchor/painting-changer.js new file mode 100644 index 00000000000..b7d8c60e5b7 --- /dev/null +++ b/examples/mixed-reality/anchor/painting-changer.js @@ -0,0 +1,26 @@ +/* global AFRAME */ +AFRAME.registerComponent('painting-changer', { + init: function () { + this.currentPaintingIndex = 0; + this.paintings = [ + '#greco', + '#helloaframe', + '#mucha', + '#scream', + '#vangogh' + ]; + this.el.sceneEl.addEventListener('loaded', this.onLoaded.bind(this)); + }, + + nextPainting: function () { + this.currentPaintingIndex++; + if (this.currentPaintingIndex === this.paintings.length) { + this.currentPaintingIndex = 0; + } + this.el.setAttribute('painting', 'src', this.paintings[this.currentPaintingIndex]); + }, + + onLoaded: function () { + document.querySelector('.a-button').addEventListener('click', this.nextPainting.bind(this)); + } +}); diff --git a/examples/mixed-reality/anchor/painting.js b/examples/mixed-reality/anchor/painting.js new file mode 100644 index 00000000000..51a300dea37 --- /dev/null +++ b/examples/mixed-reality/anchor/painting.js @@ -0,0 +1,28 @@ +/* global AFRAME, THREE */ +AFRAME.registerComponent('painting', { + schema: {src: {type: 'map'}}, + init: function () { + this.updateSrc = this.updateSrc.bind(this); + this.el.addEventListener('model-loaded', this.updateSrc); + }, + + update: function () { + if (this.data.src) { this.updateSrc(); } + }, + + updateSrc: function () { + var el = this.el; + var src = this.data.src; + var self = this; + if (!el.components['gltf-model'].model || !this.data.src) { return; } + el.sceneEl.systems.material.loadTexture(src, {src: src}, function textureLoaded (texture) { + var gltf = self.el.getObject3D('mesh'); + var gltfMaterial = gltf.children[0].children[0].children[0].children[0].children[0].material; + texture.colorSpace = THREE.SRGBColorSpace; + self.el.sceneEl.renderer.initTexture(texture); + self.texture = texture; + gltfMaterial.map = texture; + gltfMaterial.needsUpdate = true; + }); + } +}); diff --git a/src/components/anchored.js b/src/components/anchored.js new file mode 100644 index 00000000000..f8ccb17d109 --- /dev/null +++ b/src/components/anchored.js @@ -0,0 +1,134 @@ +/* global XRRigidTransform, localStorage */ +var registerComponent = require('../core/component').registerComponent; +var utils = require('../utils/'); +var warn = utils.debug('components:anchored:warn'); + +/** + * Anchored component. + * Feature only available in browsers that implement the WebXR anchors module. + * Once anchored the entity remains to a fixed position in real-world space. + * If the anchor is persistent, the anchor positioned remains across sessions or until the browser data is cleared. + */ +module.exports.Component = registerComponent('anchored', { + schema: { + persistent: {default: false} + }, + + init: function () { + var webxrData = this.el.sceneEl.getAttribute('webxr'); + var optionalFeaturesArray = webxrData.optionalFeatures; + if (optionalFeaturesArray.indexOf('anchors') === -1) { + optionalFeaturesArray.push('anchors'); + this.el.sceneEl.setAttribute('webxr', webxrData); + } + + this.requestPersistentAnchorPending = this.data.persistent; + }, + + tick: function () { + var sceneEl = this.el.sceneEl; + var xrManager = sceneEl.renderer.xr; + var frame; + var refSpace; + var pose; + var object3D = this.el.object3D; + + if ((!sceneEl.is('ar-mode') && !sceneEl.is('vr-mode'))) { return; } + if (!this.anchor && this.data.persistent && this.requestPersistentAnchorPending) { this.restorePersistentAnchor(); } + if (!this.anchor) { return; } + + frame = sceneEl.frame; + refSpace = xrManager.getReferenceSpace(); + + pose = frame.getPose(this.anchor.anchorSpace, refSpace); + object3D.matrix.elements = pose.transform.matrix; + object3D.matrix.decompose(object3D.position, object3D.rotation, object3D.scale); + }, + + createAnchor: async function createAnchor (position, quaternion) { + var sceneEl = this.el.sceneEl; + var xrManager = sceneEl.renderer.xr; + var frame; + var referenceSpace; + var anchorPose; + var anchor; + + if (!anchorsSupported(sceneEl)) { + warn('This browser doesn\'t support the WebXR anchors module'); + return; + } + + if (this.anchor) { this.deleteAnchor(); } + + frame = sceneEl.frame; + referenceSpace = xrManager.getReferenceSpace(); + anchorPose = new XRRigidTransform( + { + x: position.x, + y: position.y, + z: position.z + }, + { + x: quaternion.x, + y: quaternion.y, + z: quaternion.z, + w: quaternion.w + }); + anchor = await frame.createAnchor(anchorPose, referenceSpace); + if (this.data.persistent) { + if (this.el.id) { + this.persistentHandle = await anchor.requestPersistentHandle(); + localStorage.setItem(this.el.id, this.persistentHandle); + } else { + warn('The anchor won\'t be persisted because the entity has no assigned id.'); + } + } + sceneEl.object3D.attach(this.el.object3D); + this.anchor = anchor; + }, + + restorePersistentAnchor: async function restorePersistentAnchor () { + var xrManager = this.el.sceneEl.renderer.xr; + var session = xrManager.getSession(); + var persistentAnchors = session.persistentAnchors; + var storedPersistentHandle; + this.requestPersistentAnchorPending = false; + if (!this.el.id) { + warn('The entity associated to the persistent anchor cannot be retrieved because it doesn\'t have an assigned id.'); + return; + } + if (persistentAnchors.length) { + storedPersistentHandle = localStorage.getItem(this.el.id); + for (var i = 0; i < persistentAnchors.length; ++i) { + if (storedPersistentHandle !== persistentAnchors[i]) { continue; } + this.anchor = await session.restorePersistentAnchor(persistentAnchors[i]); + this.persistentHandle = persistentAnchors[i]; + break; + } + } else { + this.requestPersistentAnchorPending = true; + } + }, + + deleteAnchor: function () { + var xrManager; + var session; + var anchor = this.anchor; + + if (!anchor) { return; } + xrManager = this.el.sceneEl.renderer.xr; + session = xrManager.getSession(); + + anchor.delete(); + this.el.sceneEl.object3D.add(this.el.object3D); + if (this.persistentHandle) { session.deletePersistentAnchor(this.persistentHandle); } + this.anchor = undefined; + } +}); + +function anchorsSupported (sceneEl) { + var xrManager = sceneEl.renderer.xr; + var session = xrManager.getSession(); + return (session && session.restorePersistentAnchor); +} + diff --git a/src/components/index.js b/src/components/index.js index 339c10ed73c..492a67cf6e4 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,4 +1,5 @@ require('./animation'); +require('./anchored'); require('./camera'); require('./cursor'); require('./geometry');