Complete AnchorPositioning.js script that creates an AR anchor in Amazon Sumerian, based on the upcoming scripting API.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Import Sumerian scripts | |
import * as s from 'module://sumerian-common/api'; | |
import * as si from 'module://sumerian-common/internal'; | |
export default class AnchorPositioning extends s.Action { | |
static get PROPERTIES() { | |
return { | |
// Optional property to set the owner of the AR anchor | |
anchorEntity: { | |
// Property should be a reference to another entity in the scene | |
type: s.type.Entity, | |
// Description for the Amazon Sumerian web UI | |
description: 'Attach Anchor to this Entity' | |
} | |
}; | |
} | |
start(ctx) { | |
// Setting up global communication | |
// ------------------------------------------------------------------- | |
// Create / establish reference to global placeMode setting variable. | |
// This should be accessible by all components in the scene, so that | |
// others can for example pause their animations while the user is | |
// re-positioning the object. | |
// All variables created with this are stored in this object instance | |
// and are therefore persisted and accessible from other functions in | |
// this class. | |
this.placeMode = ctx.world.value('placeMode'); | |
// Initially, set the place mode to false. | |
this.placeMode.set(false); | |
// Get a reference to the WorldPlaced event. | |
// This is also a global (world) event. This script will emit the | |
// event once the user finished placing the scene contents. | |
this.worldPlacedEvent = ctx.world.event('WorldPlaced'); | |
// Check entity that will get the anchor component | |
// ------------------------------------------------------------------- | |
// Allow providing a specific entity in Sumerian where | |
// the anchor should be located in the scene hierarchy. | |
if (!this.anchorEntity) { | |
// Fallback: if the developer didn't set an entity in the editor, | |
// use this object (self). | |
console.log("no anchor entity provided - use self"); | |
this.anchorEntity = ctx.entity; | |
} | |
// World reference | |
// ------------------------------------------------------------------- | |
// The world is essentially the loaded scene. It allows interacting | |
// with the scene contents and reading / setting several properties | |
// of it. | |
const publicWorld = this.anchorEntity.world; | |
// Same as before, we actually need the more powerful internal world | |
// class, not the "normal" one. | |
this.internalWorld = si.World.forPublicWorld(publicWorld); | |
// Amazon Sumerian AR System | |
// ------------------------------------------------------------------- | |
// Retrieve the ArSystem of Amazon Sumerian. Note that this is only | |
// available if running on a platform that supports AR. | |
// So, you can't really test your code on the PC. | |
this.arSystem = this.internalWorld.getSystem('ArSystem'); | |
if (!this.arSystem) { | |
// No AR system is found - cancel further initialization. | |
// This usually happens on the computer so far. For testing, | |
// you can remove the return statement and continue execution | |
// to get a bit farther. | |
console.log("No AR system found"); | |
return; | |
} | |
console.log("Initializing AR anchoring"); | |
// Attach AR anchor component to entity | |
// ------------------------------------------------------------------- | |
// First, create the anchor component that is actually attached | |
// to the entity. | |
const arAnchorComponent = new si.ArAnchorComponent(); | |
// To work with this component, we also need the internal entity | |
// that extends the 'normal' entity and provides additional functionality. | |
this.internalAnchorEntity = si.Entity.forPublicEntity(this.anchorEntity); | |
// The internal entity allows us to add an additional component | |
// to its definition - in this case, the ArAnchorComponent we | |
// just created. | |
this.internalAnchorEntity.setComponent(arAnchorComponent); | |
// Listeners and Callbacks | |
// ------------------------------------------------------------------- | |
// First, create a variable that stores an arrow function to the | |
// performHitTest method of this class. This method takes care of | |
// starting the hit test in the native AR system. | |
// We need this as an arrow function to have access to "this" in | |
// the class scope. Creating this function in a variable allows | |
// to remove the event listener once the scene closes. | |
var performHitTestCallback = (evt) => this.performHitTest(evt); | |
// Add an event listener for touch events anywhere on the screen. | |
// Note: the MouseUpAction of Sumerian doesn't provide the coordinates | |
// -> use the HTML DOM event directly. This is accessible through | |
// the Renderer. | |
this.internalWorld.sumerianRunner.renderer.domElement.addEventListener('touchend', performHitTestCallback); | |
// Phones use touch events. To also develop / test on a PC (or on | |
// a mobile device with a mouse attached), you can also register for | |
// mouse events. | |
//this.internalWorld.sumerianRunner.renderer.domElement.addEventListener('mouseup', performHitTestCallback); | |
// Listen for the world event "StartPlaceWorld". This will be sent | |
// by another component in the scene when the placement mode should | |
// be active. When that event is received, this script sets placeMode to true, | |
// which actually allows this script to react to touch events. | |
this.startPlaceWorldEvent = ctx.world.event('StartPlaceWorld'); | |
this.startPlaceWorldEvent.listen(ctx, data => this.placeMode.set(true)); | |
// Cleanup. When this script stops, also remove the event listeners. | |
// This is especially important here, as Sumerian doesn't always reliably | |
// clean up all the runtime-registrations when you repeatedly start the scene. | |
ctx.onStop( | |
() => { | |
console.log("Removing touch event listener."); | |
this.internalWorld.sumerianRunner.renderer.domElement.removeEventListener('touchend', performHitTestCallback); | |
// For testing on the PC with a mouse | |
//this.internalWorld.sumerianRunner.renderer.domElement.removeEventListener('mouseup', performHitTestCallback); | |
} | |
); | |
} | |
// Touch handler. | |
// As the first step, performs a hit test at the currrent screen location. | |
performHitTest(evt) { | |
// First, check if this script is currently in an active place mode. | |
// That is triggered when another component in the scene sent the | |
// StartPlaceWorld event. | |
if (!this.placeMode.get()) { | |
// Currently not place mode - don't proceed! | |
console.log("Not in place mode - not proceeding"); | |
return; | |
} | |
console.log("Perform hit test: " + JSON.stringify(evt)); | |
var pixelRatio = this.internalWorld.sumerianRunner.renderer.devicePixelRatio; | |
// Touch | |
var touchX; | |
var touchY; | |
if (evt.changedTouches) { | |
touchX = evt.changedTouches[0].pageX * pixelRatio; | |
touchY = evt.changedTouches[0].pageY * pixelRatio; | |
} else { | |
// Mouse | |
touchX = evt.clientX * pixelRatio; | |
touchY = evt.clientY * pixelRatio; | |
} | |
var normalizedX = touchX / this.internalWorld.sumerianRunner.renderer.viewportWidth; | |
var normalizedY = touchY / this.internalWorld.sumerianRunner.renderer.viewportHeight; | |
if (this.arSystem) { | |
console.log("AR System found: calling hit test callback...") | |
this.arSystem.hitTest(normalizedX, normalizedY, (anchorTransform) => this.hitTestCallback(anchorTransform)); | |
} else { | |
// No AR mode - exit place mode right away | |
console.log("No AR system - exiting place mode.") | |
this.placeMode.set(false); | |
this.worldPlacedEvent.emit(); | |
} | |
} | |
// Hit test callback. If the hit test was successful (i.e., detected a | |
// point in the real world), registers an anchor with that point. | |
hitTestCallback(anchorTransform) { | |
if (anchorTransform) { | |
console.log("Transform from hit test: " + anchorTransform + ". Registering anchor..."); | |
this.arSystem.registerAnchor(anchorTransform, (anchorId) => this.registerAnchorCallback(anchorId)); | |
} else { | |
console.log("No transform received from hit test"); | |
} | |
} | |
// Anchor registration callback. Sets the anchor ID of the entity's | |
// ArAnchorComponent. The engine's ArSystem will automatically update | |
// the world position and orientation of entities with a valid anchor ID. | |
registerAnchorCallback(anchorId) { | |
if (anchorId) { | |
this.internalAnchorEntity.getComponent('ArAnchorComponent').anchorId = anchorId; | |
console.log("Registered AR anchor: " + anchorId); | |
this.placeMode.set(false); | |
this.worldPlacedEvent.emit(); | |
console.log("Quit place mode and sent WorldPlaced event"); | |
} else { | |
console.log("No Anchor ID received"); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment