Skip to content

Commit

Permalink
Add support for WebXR Anchors Module
Browse files Browse the repository at this point in the history
  • Loading branch information
dmarcos committed Nov 9, 2023
1 parent 6227622 commit da1ad3c
Show file tree
Hide file tree
Showing 10 changed files with 334 additions and 0 deletions.
28 changes: 28 additions & 0 deletions docs/components/anchored.md
Original file line number Diff line number Diff line change
@@ -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
<a-entity id="myBox" anchored="persistent: true" geometry="primitive: box" material="color: red"></a-entity>
```

## Properties

| Properties | Description |
|-------------------|-------------------------------------------------------------------------|
| persistent | If the anchor persists on page reloads. The entity must have an id. |

1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ <h2>Examples</h2>
<li><a href="boilerplate/panorama/">360&deg; Image</a></li>
<li><a href="boilerplate/360-video/">360&deg; Video</a></li>
<li><a href="boilerplate/3d-model/">3D Model (glTF)</a></li>
<li><a href="mixed-reality/anchor/">Anchor (Mixed Reality)</a></li>
</ul>

<h2>Examples from Documentation</h2>
Expand Down
23 changes: 23 additions & 0 deletions examples/mixed-reality/anchor/anchor-grabbed-entity.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
});
35 changes: 35 additions & 0 deletions examples/mixed-reality/anchor/button.js
Original file line number Diff line number Diff line change
@@ -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 () {

}
});
47 changes: 47 additions & 0 deletions examples/mixed-reality/anchor/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Anchor (Mixed Reality) • A-Frame</title>
<meta name="description" content="Anchor (Mixed Reality) • A-Frame">
<script src="../../dist/aframe-master.js"></script>
<script src="../../js/info-message.js"></script>
<script src="https://unpkg.com/[email protected]/dist/aframe-environment-component.min.js"></script>
<script src="anchor-grabbed-entity.js"></script>
<script src="painting-changer.js"></script>
<script src="painting.js"></script>
</head>
<body>
<a-scene
obb-collider="showColliders: false"
renderer="colorManagement: true;"
button
xr-mode-ui="XRMode: ar"
info-message="htmlSrc: #messageText">
<a-assets timeout="10000">
<!-- Model by theGentleGiant https://sketchfab.com/3d-models/vintage-painting-dani-3fdd92904c2b44028bef28b33e897d9f -->
<a-asset-item id="painting"
src="https://cdn.aframe.io/examples/mixed-reality/anchor/models/painting/scene.gltf"
response-type="arraybuffer" crossorigin="anonymous"></a-asset-item>
<a-asset-item id="messageText" src="message.html"></a-asset-item>
<img id="helloaframe" src="https://cdn.aframe.io/examples/mixed-reality/anchor/images/helloaframe.png" crossorigin="anonymous"/>
<img id="greco" src="https://cdn.aframe.io/examples/mixed-reality/anchor/images/greco.png" crossorigin="anonymous"/>
<img id="mucha" src="https://cdn.aframe.io/examples/mixed-reality/anchor/images/mucha.png" crossorigin="anonymous"/>
<img id="scream" src="https://cdn.aframe.io/examples/mixed-reality/anchor/images/scream.png" crossorigin="anonymous"/>
<img id="vangogh" src="https://cdn.aframe.io/examples/mixed-reality/anchor/images/vangogh.png" crossorigin="anonymous"/>
</a-assets>
<a-entity
id="painting"
position="0 1.6 -0.75"
gltf-model="#painting"
anchored="persistent: true"
painting
painting-changer
grabbable></a-entity>
<a-entity id="rightHand" hand-tracking-grab-controls="hand: right" anchor-grabbed-entity></a-entity>
<a-entity id="leftHand" hand-tracking-grab-controls="hand: left" anchor-grabbed-entity></a-entity>
</a-scene>
<script src="button.js"></script>
</body>
</html>

11 changes: 11 additions & 0 deletions examples/mixed-reality/anchor/message.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<p>
This demo requires a browser supporting the <a href="https://immersive-web.github.io/anchors/">WebXR Anchors Module</a>
</p>

<p>
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.
</p>

<p>
Paintings by <a href="https://en.wikipedia.org/wiki/Vincent_van_Gogh">Van Gogh</a>, <a href="https://en.wikipedia.org/wiki/Alphonse_Mucha">Alphonse Mucha</a>, <a href="https://en.wikipedia.org/wiki/El_Greco">El Greco</a> and <a href="https://en.wikipedia.org/wiki/Edvard_Munch">Edvard Munch</a>. Frame model by <a href="https://sketchfab.com/3d-models/vintage-painting-dani-3fdd92904c2b44028bef28b33e897d9f">theGentleGiant</a>
</p>
26 changes: 26 additions & 0 deletions examples/mixed-reality/anchor/painting-changer.js
Original file line number Diff line number Diff line change
@@ -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));
}
});
28 changes: 28 additions & 0 deletions examples/mixed-reality/anchor/painting.js
Original file line number Diff line number Diff line change
@@ -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;
});
}
});
134 changes: 134 additions & 0 deletions src/components/anchored.js
Original file line number Diff line number Diff line change
@@ -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) {

Check failure on line 48 in src/components/anchored.js

View workflow job for this annotation

GitHub Actions / Test Cases (16.x, latest)

Parsing error: Unexpected token function
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);
}

1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require('./animation');
require('./anchored');
require('./camera');
require('./cursor');
require('./geometry');
Expand Down

0 comments on commit da1ad3c

Please sign in to comment.