UNPKG

aframe-ar

Version:

Basic A-Frame support for the new three.ar.js library and WebARonARKit/Core browsers, as well as WebXR Viewer.

1,161 lines (988 loc) 41.2 kB
/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { __webpack_require__(1); __webpack_require__(2); __webpack_require__(3); __webpack_require__(4); __webpack_require__(5); __webpack_require__(6); /***/ }), /* 1 */ /***/ (function(module, exports) { AFRAME.registerComponent('three-ar', { schema: { takeOverCamera: {default: true}, cameraUserHeight: {default: false} }, init: function () { this.posePosition = new THREE.Vector3(); this.poseQuaternion = new THREE.Quaternion(); this.poseEuler = new THREE.Euler(0, 0, 0, 'YXZ'); this.poseRotation = new THREE.Vector3(); this.projectionMatrix = new THREE.Matrix4(); this.onceSceneLoaded = this.onceSceneLoaded.bind(this); if (this.el.sceneEl.hasLoaded) { setTimeout(this.onceSceneLoaded); } else { this.el.sceneEl.addEventListener('loaded', this.onceSceneLoaded); } }, tick: function (t, dt) { if (!this.arDisplay || !this.arDisplay.getFrameData) { return; } // If we have an ARView, render it. if (this.arView) { this.arView.render(); } // Get the ARDisplay frame data with pose and projection matrix. if (!this.frameData) { this.frameData = new VRFrameData(); } this.arDisplay.getFrameData(this.frameData); // Get the pose information. this.posePosition.fromArray(this.frameData.pose.position); this.poseQuaternion.fromArray(this.frameData.pose.orientation); this.poseEuler.setFromQuaternion(this.poseQuaternion); this.poseRotation.set( THREE.Math.RAD2DEG * this.poseEuler.x, THREE.Math.RAD2DEG * this.poseEuler.y, THREE.Math.RAD2DEG * this.poseEuler.z); // If we control a camera, and should apply user height, do it. if (this.arCamera && this.data.cameraUserHeight) { this.posePosition.y += this.arCamera.el.components.camera.data.userHeight; } // For A-Painter, detect bogus pose and fire poseFound / poseLost. var poseValid = this.posePosition.x || this.posePosition.y || this.posePosition.z || this.poseQuaternion.x || this.poseQuaternion.y || this.poseQuaternion.z; if (poseValid) { if (this.poseLost !== false) { this.poseLost = false; this.el.emit('poseFound'); } } else { if (this.poseLost !== true) { this.poseLost = true; this.el.emit('poseLost', false); } } // Can use either left or right projection matrix; pick left for now. this.projectionMatrix.fromArray(this.frameData.leftProjectionMatrix); }, takeOverCamera: function (camera) { this.arCamera = camera; camera.isARPerspectiveCamera = true; // HACK - is this necessary? camera.vrDisplay = this.arDisplay; // HACK - is this necessary? camera.el.setAttribute('ar-camera', 'enabled', true); }, onceSceneLoaded: function () { // Add an event listener for ardisplayconnect, // to check for AR display if we don't have one yet. var self = this; window.addEventListener('ardisplayconnect', function () { if (!self.arDisplay) { self.checkForARDisplay(); } }); // Check now for AR display. this.checkForARDisplay(); }, checkForARDisplay: function () { // Get the ARDisplay, if any. var self = this; THREE.ARUtils.getARDisplay().then(function (display) { self.arDisplay = display; if (!display) { return; } // The scene is loaded, so scene components etc. should be available. var scene = self.el.sceneEl; // Take over the scene camera, if so directed. // But wait a tick, because otherwise injected camera will not be present. if (self.data.takeOverCamera) { setTimeout(function () { self.takeOverCamera(scene.camera); }); } // Modify the scene renderer to allow ARView video passthrough. scene.renderer.alpha = true; scene.renderer.autoClearColor = THREE.ARUtils.isARKit(display) && !window.WebARonARKitSendsCameraFrames; scene.renderer.autoClearDepth = true; // Create the ARView. self.arView = new THREE.ARView(display, scene.renderer); }); }, getPosition: function () { if (!this.arDisplay || !this.arDisplay.getFrameData) { return null; } return this.posePosition; }, getOrientation: function () { if (!this.arDisplay || !this.arDisplay.getFrameData) { return null; } return this.poseQuaternion; }, getRotation: function () { if (!this.arDisplay || !this.arDisplay.getFrameData) { return null; } return this.poseRotation; }, getProjectionMatrix: function () { if (!this.arDisplay || !this.arDisplay.getFrameData) { return null; } return this.projectionMatrix; }, hitAR: (function () { // Temporary variables, only within closure scope. var transform = new THREE.Matrix4(); var hitpoint = new THREE.Vector3(); var hitquat = new THREE.Quaternion(); var hitscale = new THREE.Vector3(); var worldpos = new THREE.Vector3(); // The desired function, which this returns. return function (x, y, el, raycasterEl) { var threear = this; if (!this.arDisplay || !this.arDisplay.hitTest) { return []; } var hit = this.arDisplay.hitTest(x, y); // Process AR hits. var hitsToReturn = []; for (var i = 0; hit && i < hit.length; i++) { transform.fromArray(hit[0].modelMatrix); transform.decompose(hitpoint, hitquat, hitscale); raycasterEl.object3D.getWorldPosition(worldpos); hitsToReturn.push({ distance: hitpoint.distanceTo(worldpos), point: hitpoint, // Vector3 object: (el && el.object3D) || this.el.sceneEl.object3D /* // We don't have any of these properties... face: undefined, // Face3 faceIndex: undefined, index: undefined, uv: undefined // Vector2 */ }); } return hitsToReturn; } })() }); /***/ }), /* 2 */ /***/ (function(module, exports) { AFRAME.registerComponent('mozilla-xr-ar', { schema: { takeOverCamera: {default: true}, cameraUserHeight: {default: false} }, init: function () { this.onInit = this.onInit.bind(this); this.onWatch = this.onWatch.bind(this); this.poseMatrix = new THREE.Matrix4(); this.posePosition = new THREE.Vector3(); this.poseQuaternion = new THREE.Quaternion(); this.poseEuler = new THREE.Euler(0, 0, 0, 'YXZ'); this.poseRotation = new THREE.Vector3(); this.projectionMatrix = new THREE.Matrix4(); this.viewMatrix = new THREE.Matrix4(); this.onceSceneLoaded = this.onceSceneLoaded.bind(this); if (this.el.sceneEl.hasLoaded) { console.log('mozilla-xr-ar: hasLoaded, setTimeout'); setTimeout(this.onceSceneLoaded); } else { console.log('mozilla-xr-ar: !hasLoaded, addEventListener'); this.el.sceneEl.addEventListener('loaded', this.onceSceneLoaded); } // Add planes handling, so we can do synchronous hit test. // From google-ar/WebARonARKit; also see webxr-polyfill/ARKitWrapper.js this.planes_ = new Map(); this.anchors_ = new Map(); }, // For WebXR Viewer, we are currently directly hooking the callback // used to provide frame data, so we don't need to do anything in tick! takeOverCamera: function (camera) { this.arCamera = camera; camera.el.setAttribute('ar-camera', 'enabled', true); }, onceSceneLoaded: function () { // Check if the low-level WebXR Viewer interfaces are there. if (!window.webkit || !window.webkit.messageHandlers) { return; } if (!window.webkit.messageHandlers.initAR) { return; } // Ugly hack to get around WebXR Viewer resizing issue. setTimeout(function () { var scene = AFRAME.scenes[0]; scene.canvas.style.position = "absolute !important"; scene.canvas.style.width = "100% !important"; scene.canvas.style.height = "100% !important"; setTimeout(function () { scene.resize(); }); }, 1000); window['arkitCallback' + 0] = this.onInit; window['arkitCallback' + 1] = this.onWatch; // Compose data to use with initAR. var data = { options: { ui: { browser: true, points: true, focus: false, rec: true, rec_time: true, mic: false, build: false, plane: true, warnings: true, anchors: false, debug: true, statistics: false } }, callback: 'arkitCallback0' // this.onInit as window callback }; // Call initAR. window.webkit.messageHandlers.initAR.postMessage(data); }, checkForARDisplay: function () { // Check if the low-level WebXR Viewer interfaces are there. if (!window.webkit || !window.webkit.messageHandlers) { return; } if (!window.webkit.messageHandlers.watchAR) { return; } // Mozilla WebXR Viewer detected. this.arDisplay = true; // Compose data to use with watchAR. var data = { options: { location: true, camera: true, objects: true, light_intensity: true }, callback: 'arkitCallback1' // this.onWatch as window callback }; // Add resize handling. window['arkitWindowResize'] = function () { setTimeout(function() { AFRAME.scenes[0].resize(); }, 100); }; // Start watching AR. window.webkit.messageHandlers.watchAR.postMessage(data); var self = this; // The scene is loaded, so scene components etc. should be available. var scene = self.el.sceneEl; // Take over the scene camera, if so directed. // But wait a tick, because otherwise injected camera will not be present. if (self.data.takeOverCamera) { setTimeout(function () { self.takeOverCamera(scene.camera); }); } // Modify the scene renderer to allow ARView video passthrough. scene.renderer.setPixelRatio(1); scene.renderer.autoClear = false; scene.renderer.setClearColor('#000', 0); scene.renderer.alpha = true; }, onInit: function (deviceId) { this.checkForARDisplay(); }, onWatch: function (data) { this.frameData = data; this.handleFrame(data); }, handleFrame: function (data) { var scene = this.el.sceneEl; // Decompose to get camera pose. this.poseMatrix.fromArray(data.camera_transform); this.poseMatrix.decompose(this.posePosition, this.poseQuaternion, this.poseRotation); // poseRotation is really scale, we redo below this.poseEuler.setFromQuaternion(this.poseQuaternion); this.poseRotation.set( THREE.Math.RAD2DEG * this.poseEuler.x, THREE.Math.RAD2DEG * this.poseEuler.y, THREE.Math.RAD2DEG * this.poseEuler.z); this.projectionMatrix.fromArray(data.projection_camera); this.viewMatrix.fromArray(data.camera_view); // If we control a camera, and should apply user height, do it. if (this.arCamera && this.data.cameraUserHeight) { this.posePosition.y += this.arCamera.el.components.camera.data.userHeight; } // For A-Painter, detect bogus pose and fire poseFound / poseLost. var poseValid = this.posePosition.x || this.posePosition.y || this.posePosition.z || this.poseQuaternion.x || this.poseQuaternion.y || this.poseQuaternion.z; if (poseValid) { if (this.poseLost !== false) { this.poseLost = false; this.el.emit('poseFound'); } } else { if (this.poseLost !== true) { this.poseLost = true; this.el.emit('poseLost', false); } } // Add planes handling, so we can do synchronous hit test. // From google-ar/WebARonARKit; also see webxr-polyfill/ARKitWrapper.js var i; if(data.newObjects && data.newObjects.length){ for (i = 0; i < data.newObjects.length; i++) { var element = data.newObjects[i]; if(element.h_plane_center){ this.planes_.set(element.uuid, { id: element.uuid, center: element.h_plane_center, extent: [element.h_plane_extent.x, element.h_plane_extent.z], modelMatrix: element.transform }); }else{ this.anchors_.set(element.uuid, { id: element.uuid, modelMatrix: element.transform }); } } } if(data.removedObjects && data.removedObjects.length){ for (i = 0; i < data.removedObjects.length; i++) { var element = data.removedObjects[i]; if(element.h_plane_center){ this.planes_.delete(element.uuid); }else{ this.anchors_.delete(element.uuid); } } } if(data.objects && data.objects.length){ for (i = 0; i < data.objects.length; i++) { var element = data.objects[i]; if(element.h_plane_center){ var plane = this.planes_.get(element.uuid); if(!plane){ this.planes_.set(element.uuid, { id: element.uuid, center: element.h_plane_center, extent: [element.h_plane_extent.x, element.h_plane_extent.z], transform: element.transform }); } else { plane.center = element.h_plane_center; plane.extent = [element.h_plane_extent.x, element.h_plane_extent.z]; plane.transform = element.transform; } }else{ var anchor = this.anchors_.get(element.uuid); if(!anchor){ this.anchors_.set(element.uuid, { id: element.uuid, transform: element.transform }); }else{ anchor.transform = element.transform; } } } } }, getPosition: function () { if (!this.arDisplay) { return null; } return this.posePosition; }, getOrientation: function () { if (!this.arDisplay) { return null; } return this.poseQuaternion; }, getRotation: function () { if (!this.arDisplay) { return null; } return this.poseRotation; }, getProjectionMatrix: function () { if (!this.arDisplay) { return null; } return this.projectionMatrix; }, // Use planes to do synchronous hit test. // From google-ar/WebARonARKit; also see webxr-polyfill/ARKitWrapper.js getPlanes: function () { return Array.from(this.planes_.values()); }, hitTestNoAnchor: (function () { // Temporary variables, only within closure scope. /** * The result of a raycast into the AR world encoded as a transform matrix. * This structure has a single property - modelMatrix - which encodes the * translation of the intersection of the hit in the form of a 4x4 matrix. * @constructor */ function VRHit() { this.modelMatrix = new Float32Array(16); return this; }; /** * Cached vec3, mat4, and quat structures needed for the hit testing to * avoid generating garbage. * @type {Object} */ var hitVars = { rayStart: new THREE.Vector3(), //vec3.create(), rayEnd: new THREE.Vector3(), //vec3.create(), cameraPosition: new THREE.Vector3(), //vec3.create(), cameraQuaternion: new THREE.Quaternion(), //quat.create(), //modelViewMatrix: new THREE.Matrix4(), //mat4.create(), //projectionMatrix: new THREE.Matrix4(), //mat4.create(), projViewMatrix: new THREE.Matrix4(), //mat4.create(), worldRayStart: new THREE.Vector3(), //vec3.create(), worldRayEnd: new THREE.Vector3(), //vec3.create(), worldRayDir: new THREE.Vector3(), //vec3.create(), planeMatrix: new THREE.Matrix4(), //mat4.create(), planeMatrixInverse: new THREE.Matrix4(), //mat4.create(), planeExtent: new THREE.Vector3(), //vec3.create(), planePosition: new THREE.Vector3(), //vec3.create(), planeCenter: new THREE.Vector3(), //vec3.create(), planeNormal: new THREE.Vector3(), //vec3.create(), planeIntersection: new THREE.Vector3(), //vec3.create(), planeIntersectionLocal: new THREE.Vector3(), //vec3.create(), planeHit: new THREE.Matrix4(), //mat4.create() //planeQuaternion: quat.create() }; /** * Tests whether the given ray intersects the given plane. * * @param {!vec3} planeNormal The normal of the plane. * @param {!vec3} planePosition Any point on the plane. * @param {!vec3} rayOrigin The origin of the ray. * @param {!vec3} rayDirection The direction of the ray (normalized). * @return {number} The t-value of the intersection (-1 for none). */ var rayIntersectsPlane = (function() { var rayToPlane = new THREE.Vector3(); return function(planeNormal, planePosition, rayOrigin, rayDirection) { // assuming vectors are all normalized var denom = planeNormal.dot(rayDirection); rayToPlane.subVectors(planePosition, rayOrigin); return rayToPlane.dot(planeNormal) / denom; }; })(); /** * Sorts based on the distance from the VRHits to the camera. * * @param {!VRHit} a The first hit to compare. * @param {!VRHit} b The second hit item to compare. * @returns {number} -1 if a is closer than b, otherwise 1. */ var sortFunction = function(a, b) { // Get the matrix of hit a. hitVars.planeMatrix.fromArray(a.modelMatrix); // Get the translation component of a's matrix. hitVars.planeIntersection.setFromMatrixPosition(hitVars.planeMatrix); // Get the distance from the intersection point to the camera. var distA = hitVars.planeIntersection.distanceTo(hitVars.cameraPosition); // Get the matrix of hit b. hitVars.planeMatrix.fromArray(b.modelMatrix); // Get the translation component of b's matrix. hitVars.planeIntersection.setFromMatrixPosition(hitVars.planeMatrix); // Get the distance from the intersection point to the camera. var distB = hitVars.planeIntersection.distanceTo(hitVars.cameraPosition); // Return comparison of distance from camera to a and b. return distA < distB ? -1 : 1; }; return function(x, y) { // Coordinates must be in normalized screen space. if (x < 0 || x > 1 || y < 0 || y > 1) { throw new Error( "hitTest - x and y values must be normalized [0,1]!") ; } var hits = []; // If there are no anchors detected, there will be no hits. var planes = this.getPlanes(); if (!planes || planes.length == 0) { return hits; } // Create a ray in screen space for the hit test ([-1, 1] with y flip). hitVars.rayStart.set(2 * x - 1, 2 * (1 - y) - 1, 0); hitVars.rayEnd.set(2 * x - 1, 2 * (1 - y) - 1, 1); // Set the projection matrix. //hitVars.projectionMatrix.fromArray(this.projectionMatrix); // Set the model view matrix. //hitVars.modelViewMatrix.fromArray(this.viewMatrix); // Combine the projection and model view matrices. hitVars.planeMatrix.multiplyMatrices( this.projectionMatrix, //hitVars.projectionMatrix, this.viewMatrix //hitVars.modelViewMatrix ); // Invert the combined matrix because we need to go from screen -> world. hitVars.projViewMatrix.getInverse(hitVars.planeMatrix); // Transform the screen-space ray start and end to world-space. hitVars.worldRayStart.copy(hitVars.rayStart) .applyMatrix4(hitVars.projViewMatrix); hitVars.worldRayEnd.copy(hitVars.rayEnd) .applyMatrix4(hitVars.projViewMatrix); // Subtract start from end to get the ray direction and then normalize. hitVars.worldRayDir.subVectors( hitVars.worldRayEnd, hitVars.worldRayStart ).normalize(); // Go through all the anchors and test for intersections with the ray. for (var i = 0; i < planes.length; i++) { var plane = planes[i]; // Get the anchor transform. hitVars.planeMatrix.fromArray(plane.modelMatrix); // Get the position of the anchor in world-space. hitVars.planeCenter.set(0, 0, 0); hitVars.planePosition.copy(hitVars.planeCenter) .applyMatrix4(hitVars.planeMatrix) // Get the plane normal. // TODO: use alignment to determine this. hitVars.planeNormal.set(0, 1, 0); // Check if the ray intersects the plane. var t = rayIntersectsPlane( hitVars.planeNormal, hitVars.planePosition, hitVars.worldRayStart, hitVars.worldRayDir ); // if t < 0, there is no intersection. if (t < 0) { continue; } // Calculate the actual intersection point. hitVars.planeIntersectionLocal.copy(hitVars.worldRayDir).multiplyScalar(t); hitVars.planeIntersection.addVectors( hitVars.worldRayStart, hitVars.planeIntersectionLocal ); // Get the plane extents (extents are in plane local space). hitVars.planeExtent.set(plane.extent[0], 0, plane.extent[1]); /* /////////////////////////////////////////////// // Test by converting extents to world-space. // TODO: get this working to avoid matrix inversion in method below. // Get the rotation component of the anchor transform. mat4.getRotation(hitVars.planeQuaternion, hitVars.planeMatrix); // Convert the extent into world space. vec3.transformQuat( hitVars.planeExtent, hitVars.planeExtent, hitVars.planeQuaternion); // Check if intersection is outside of the extent of the anchor. if (Math.abs(hitVars.planeIntersection[0] - hitVars.planePosition[0]) > hitVars.planeExtent[0] / 2) { continue; } if (Math.abs(hitVars.planeIntersection[2] - hitVars.planePosition[2]) > hitVars.planeExtent[2] / 2) { continue; } //////////////////////////////////////////////// */ //////////////////////////////////////////////// // Test by converting intersection into plane-space. hitVars.planeMatrixInverse.getInverse(hitVars.planeMatrix); hitVars.planeIntersectionLocal.copy(hitVars.planeIntersection) .applyMatrix4(hitVars.planeMatrixInverse); // Check if intersection is outside of the extent of the anchor. // Tolerance is added to match the behavior of the native hitTest call. var tolerance = 0.0075; if ( Math.abs(hitVars.planeIntersectionLocal.x) > hitVars.planeExtent.x / 2 + tolerance ) { continue; } if ( Math.abs(hitVars.planeIntersectionLocal.z) > hitVars.planeExtent.z / 2 + tolerance ) { continue; } //////////////////////////////////////////////// // The intersection is valid - create a matrix from hit position. hitVars.planeHit.makeTranslation( hitVars.planeIntersection.x, hitVars.planeIntersection.y, hitVars.planeIntersection.z); var hit = new VRHit(); for (var j = 0; j < 16; j++) { hit.modelMatrix[j] = hitVars.planeHit.elements[j]; } hit.i = i; hits.push(hit); } // Sort the hits by distance. hits.sort(sortFunction); return hits; }; })(), hitAR: (function () { // Temporary variables, only within closure scope. var transform = new THREE.Matrix4(); var hitpoint = new THREE.Vector3(); var hitquat = new THREE.Quaternion(); var hitscale = new THREE.Vector3(); var worldpos = new THREE.Vector3(); // The desired function, which this returns. return function (x, y, el, raycasterEl) { if (!this.arDisplay) { return []; } var hit = this.hitTestNoAnchor(x, y); // Process AR hits. var hitsToReturn = []; for (var i = 0; hit && i < hit.length; i++) { transform.fromArray(hit[0].modelMatrix); transform.decompose(hitpoint, hitquat, hitscale); raycasterEl.object3D.getWorldPosition(worldpos); hitsToReturn.push({ distance: hitpoint.distanceTo(worldpos), point: hitpoint, // Vector3 object: (el && el.object3D) || this.el.sceneEl.object3D /* // We don't have any of these properties... face: undefined, // Face3 faceIndex: undefined, index: undefined, uv: undefined // Vector2 */ }); } return hitsToReturn; } })() }); /***/ }), /* 3 */ /***/ (function(module, exports) { AFRAME.registerComponent('ar-planes', { getPlaneSource: function () { var whichar; if (!this.planeSource) { whichar = this.el.sceneEl.components['three-ar']; if (whichar && whichar.arDisplay) { this.planeSource = whichar.arDisplay; } } if (!this.planeSource) { whichar = this.el.sceneEl.components['mozilla-xr-ar']; if (whichar && whichar.arDisplay) { this.planeSource = whichar; } } return this.planeSource; }, getPlanes: function () { var planeSource = this.getPlaneSource(); if (!planeSource || !planeSource.getPlanes) return undefined; return planeSource.getPlanes(); }, init: function () { // Remember planes when we see them. this.planes = {}; this.anchorsAdded = []; this.anchorsAddedDetail = {type:'added', anchors: this.anchorsAdded}; this.anchorsUpdated = []; this.anchorsUpdatedDetail = {type:'updated', anchors: this.anchorsUpdated}; this.anchorsRemoved = []; this.anchorsRemovedDetail = {type:'removed', anchors: this.anchorsRemoved}; }, tick: (function (t, dt) { // Create the temporary variables we will reuse, if needed. var tempAlignment = 0; var tempScale = new THREE.Vector3(1, 1, 1); var tempExtent3 = new THREE.Vector3(); var tempMat4 = new THREE.Matrix4(); var tempPosition = new THREE.Vector3(); var tempRotation = new THREE.Vector3(); var tempQuaternion = new THREE.Quaternion(); var tempEuler = new THREE.Euler(0, 0, 0, 'YXZ'); // The actual function, which we return. return function (t, dt) { // Get the list of planes. var planes = this.getPlanes(); if (!planes) { return; } // Ideally we would have either events, or separate lists for added / updated / removed. var addedThese = []; var updatedThese = []; var removedThese = []; // Because we don't have an indication of added / updated / removed, // try to keep track ourselves. var seenThese = {}; var i; // Iterate over the available planes. for (i=0; planes && i<planes.length; i++) { var plane = planes[i]; // Force plane conformance to latest spec. // (Hopefully soon, this will no longer be required.) var planespec; // Get plane identifier and conform. var id = (plane.identifier !== undefined ? plane.identifier : plane.id).toString(); // Get plane timestamp, if available. var timestamp = plane.timestamp; // Note that we've seen it. seenThese[id] = true; var adding = !this.planes[id]; var hasTimestamp = timestamp !== undefined; if (!adding) { // We've seen this plane before. // If this plane has a timestamp, if (hasTimestamp) { // And the timestamp is identical, if (timestamp === this.planes[id].timestamp) { // Then we don't need to do any more work for this plane, // since it hasn't changed. continue; } else { // We have a timestamp, and it doesn't match, // so we'll be updating the previous plane spec. } } else { // This plane didn't have a timestamp, // so unfortunately we'll need to do brute force comparison. // We might update the previous plane spec afterward. } } else { // We haven't seen this plane before, so we'll be adding it. } // If we're still here, we need to finish building the plane spec. var planespec = {identifier: id}; if (timestamp !== undefined) { planespec.timestamp = timestamp; } // New API plane spec uses modelMatrix (same as transform). if (plane.modelMatrix || plane.transform) { planespec.modelMatrix = plane.modelMatrix || plane.transform; } else { // Create modelMatrix from position and orientation. tempPosition.fromArray(plane.position); tempQuaternion.fromArray(plane.orientation); tempScale.set(1, 1, 1); tempMat4.compose(tempPosition, tempQuaternion, tempScale); planespec.modelMatrix = tempMat4.elements.slice(); } planespec.extent = plane.extent; if (plane.center) { planespec.center = plane.center; } if (plane.polygon) { planespec.vertices = plane.polygon; } else if (plane.vertices) { planespec.vertices = plane.vertices; } // Figure out whether added or updated. // If we've seen it before, if (!adding) { // And it has a timestamp, if (hasTimestamp) { // We're updating it (because if not we'd be done already.) updatedThese.push(planespec); } else // If it didn't have a timestamp, do brute force comparison. // FIXME: better brute-force comparison! if (AFRAME.utils.deepEqual(planespec, this.planes[id])) { // It didn't change, so we're done with this one. continue; } else { // It changed, so we're updating it. // However, since we need to do brute force comparison, // we'll need to clone it when we remember. updatedThese.push(planespec); } } else { // We haven't see it, so we're adding it. addedThese.push(planespec) } // If we're still here, we need to remember the new planespec. // If we have timestamps, if (hasTimestamp) { // We only need to compare that, // so we don't need to copy or clone anything. // since we always make a new plane spec right now. this.planes[id] = planespec; } else { // Because the objects in the plane may be updated in place, // we need to clone those parts of the remembered plane spec. this.planes[id] = { identifier: planespec.identifier, modelMatrix: planespec.modelMatrix.slice(), extent: planespec.extent.slice() }; /* WebXR Viewer problem? WebARon___ doesn't use. if (planespec.center) { this.planes[id].center = planespec.center.slice(); } */ if (planespec.vertices) { this.planes[id].vertices = planespec.vertices.slice(); } } } // To find ones we've removed, we need to scan this.planes. var self = this; Object.keys(self.planes).forEach(function (key) { if (!seenThese[key]) { removedThese.push(self.planes[key]); delete self.planes[key]; } }); // OK, now we should have separate added / updated / removed lists, // with planes that match spec, // from which we can emit appropriate events downstream. // Replace the old list. this.anchorsAdded = addedThese; // Emit event if list isn't empty. if (addedThese.length > 0) { // Reuse the same event detail to avoid making garbage. // TODO: Reuse same CustomEvent? this.anchorsAddedDetail.anchors = addedThese; this.el.emit('anchorsadded', this.anchorsAddedDetail); } // Replace the old list. this.anchorsUpdated = updatedThese; // Emit event if list isn't empty. if (updatedThese.length > 0) { // Reuse the same event detail to avoid making garbage. // TODO: Reuse same CustomEvent? this.anchorsUpdatedDetail.anchors = updatedThese; this.el.emit('anchorsupdated', this.anchorsUpdatedDetail); } // Replace the old list. this.anchorsRemoved = removedThese; // Emit event if list isn't empty. if (removedThese.length > 0) { // Reuse the same event detail to avoid making garbage. // TODO: Reuse same CustomEvent? this.anchorsRemovedDetail.anchors = removedThese; this.el.emit('anchorsremoved', this.anchorsRemovedDetail); } }; })() }); /***/ }), /* 4 */ /***/ (function(module, exports) { AFRAME.registerComponent('ar', { schema: { takeOverCamera: {default: true}, cameraUserHeight: {default: false}, hideUI: {default: true} }, dependencies: ['three-ar', 'mozilla-xr-ar', 'ar-planes'], init: function () { this.el.setAttribute('three-ar', { takeOverCamera: this.data.takeOverCamera, cameraUserHeight: this.data.cameraUserHeight }); this.el.setAttribute('mozilla-xr-ar', { takeOverCamera: this.data.takeOverCamera, cameraUserHeight: this.data.cameraUserHeight }); if (this.data.hideUI) { this.el.sceneEl.setAttribute('vr-mode-ui', {enabled: false}); } // Ensure passthrough is visible, make sure A-Frame styles don't interfere. document.head.insertAdjacentHTML('beforeend', '<style>html,body {background-color: transparent !important;}</style>'); } }); /***/ }), /* 5 */ /***/ (function(module, exports) { AFRAME.registerComponent('ar-camera', { schema: { enabled: {default:true} }, init: function () { this.wasLookControlsEnabled = this.el.getAttribute('look-controls', 'enabled'); }, update: function (oldData) { if (!oldData || oldData.enabled !== this.data.enabled) { // Value changed, so react accordingly. if (this.data.enabled) { // Save camera look-controls enabled, and turn off for AR. this.wasLookControlsEnabled = this.el.getAttribute('look-controls', 'enabled'); this.el.setAttribute('look-controls', 'enabled', false); } else { // Restore camera look-controls enabled. this.el.setAttribute('look-controls', 'enabled', this.wasLookControlsEnabled === true); } } }, tick: function (t, dt) { if (!this.data.enabled) { return; } var whichar = this.checkWhichAR(); if (!whichar) { return; } // Apply the pose position via setAttribute, // so that other A-Frame components can see the values. this.el.setAttribute('position', whichar.getPosition()); // Apply the pose rotation via setAttribute, // so that other A-Frame components can see the values. this.el.setAttribute('rotation', whichar.getRotation()); // Apply the projection matrix, if we're not in VR. if (!this.el.sceneEl.is('vr-mode')) { this.el.components.camera.camera.projectionMatrix = whichar.getProjectionMatrix(); } }, checkWhichAR: function () { if (!this.whichar) { var whichar = this.el.sceneEl.components['three-ar']; if (!whichar || !whichar.arDisplay) { whichar = this.el.sceneEl.components['mozilla-xr-ar']; } if (!whichar || !whichar.arDisplay) { return; } this.whichar = whichar; } return this.whichar; } }); /***/ }), /* 6 */ /***/ (function(module, exports) { // ar-raycaster modifies raycaster to append AR hit, if any. // But note that current AR hit API does not support orientation as input. AFRAME.registerComponent('ar-raycaster', { dependencies: ['raycaster'], schema: { x: {default: 0.5}, y: {default: 0.5}, el: {type: 'selector'} }, init: function () { // HACK: monkey-patch raycaster to append AR hit result this.raycaster = this.el.components['raycaster'].raycaster; this.raycasterIntersectObjects = this.raycaster.intersectObjects.bind(this.raycaster); this.raycaster.intersectObjects = this.intersectObjects.bind(this); }, update: function (oldData) { if (!this.data.el) { // If not given some other element, return hit against the scene. // HACK: But that means we need its object3D to have an el. if (!this.el.sceneEl.object3D.el) { this.el.sceneEl.object3D.el = this.el.sceneEl; } } }, intersectObjects: function (objects, recursive) { var results = this.raycasterIntersectObjects(objects, recursive); // Tack on AR hit result, if any. return results.concat(this.hitAR()); }, hitAR: function () { var whichar = this.checkWhichAR(); if (!whichar || !whichar.arDisplay) { return []; } var x = this.data.x; var y = this.data.y; if (arguments.length >= 2) { x = arguments[0]; y = arguments[1]; } return whichar.hitAR(x, y, this.data.el, this.el); }, checkWhichAR: function () { if (!this.whichar) { var whichar = this.el.sceneEl.components['three-ar']; if (!whichar || !whichar.arDisplay) { whichar = this.el.sceneEl.components['mozilla-xr-ar']; } if (!whichar || !whichar.arDisplay) { return; } this.whichar = whichar; } return this.whichar; } }); /***/ }) /******/ ]);