UNPKG

three.ar.js

Version:

A helper three.js library for building AR web experiences that run in WebARonARKit and WebARonARCore

653 lines (566 loc) 20.6 kB
<!-- /* * Copyright 2017 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ --> <!DOCTYPE html> <html lang="en"> <head> <title>three.ar.js - Graffiti</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <style> body { font-family: monospace; margin: 0; overflow: hidden; position: fixed; width: 100%; height: 100vh; -webkit-user-select: none; user-select: none; } #info { position: absolute; left: 50%; bottom: 0; transform: translate(-50%, 0); margin: 1em; z-index: 10; display: block; line-height: 2em; text-align: center; } #info * { color: #fff; } .title { background-color: rgba(40, 40, 40, 0.4); padding: 0.4em 0.6em; border-radius: 0.1em; } .links { background-color: rgba(40, 40, 40, 0.6); padding: 0.4em 0.6em; border-radius: 0.1em; } canvas { position: absolute; top: 0; left: 0; } </style> </head> <body> <div id="info"> <span class="title">Touch and hold to draw. Shake to undo.</span><br/> <span class="links"> <a href="https://github.com/google-ar/three.ar.js">three.ar.js</a> - <a href="https://developers.google.com/ar/develop/web/getting-started#examples">examples</a> </span> </div> <script src="../third_party/three.js/three.js"></script> <script src="../third_party/three.js/VRControls.js"></script> <script src="../dist/three.ar.js"></script> <script id="fragmentShader" type="shader"> precision mediump float; precision mediump int; varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUv; varying vec3 vVelocity; void main() { vec2 uv = vUv; uv *= 2.0; uv -= 1.0; float aa = 1.0 - abs( uv.y ); aa = smoothstep( 0.0, 0.05, aa ); gl_FragColor = vec4( vNormal, aa ); } </script> <script id="vertexShader" type="shader"> uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform mat3 normalMatrix; attribute vec3 position; attribute vec3 normal; attribute vec2 uv; attribute vec3 velocity; varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUv; varying vec3 vVelocity; void main() { vPosition = position; vNormal = normal; vUv = uv; vVelocity = velocity; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } </script> <script> var vrDisplay; var vrControls; var arView; var canvas; var camera; var scene; var renderer; // Drawing constants. var MINIMIM_POINT_DISTANCE = 0.001; var MINIMUM_STROKE_POINTS = 2; var MINIMUM_BRUSH_POINTS = 2; var STROKE_DISTANCE = 0.1; var STROKE_WIDTH_EASING = 0.05; var STROKE_WIDTH_MINIMUM = 0.00125; var STROKE_WIDTH_MAXIMUM = 0.1; var STROKE_WIDTH_MODIFIER = 1.25; var STROKE_POSITION_EASING = 0.5; var STROKE_VELOCITY_EASING = 0.5; var STROKE_ORTHOGONAL_EASING = 0.5; var STROKE_NORMAL_EASING = 0.95; var STROKE_NORMAL_QUAT_EASING = 0.5; var MINIMIM_ERASE_ENERGY = 0.005; var MINIMIM_ERASE_ENERGY_FRAMES = 15; // Setup drawing variables. var drawing = false; var stroke = []; var strokes = []; var strokeIndex = 0; var brush; var brushStroke = []; var drawMaterial = new THREE.RawShaderMaterial({ vertexShader: document.getElementById( 'vertexShader' ).textContent, fragmentShader: document.getElementById( 'fragmentShader' ).textContent, side: THREE.DoubleSide, transparent: true }); /** * Use the `getARDisplay()` utility to leverage the WebVR API * to see if there are any AR-capable WebVR VRDisplays. Returns * a valid display if found. Otherwise, display the unsupported * browser message. */ THREE.ARUtils.getARDisplay().then(function (display) { if (display) { vrDisplay = display; init(); } else { THREE.ARUtils.displayUnsupportedMessage(); } }); function init() { // Turn on the debugging panel //var arDebug = new THREE.ARDebug(vrDisplay); //document.body.appendChild(arDebug.getElement()); // Setup the three.js rendering environment renderer = new THREE.WebGLRenderer({ alpha: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.autoClear = false; canvas = renderer.domElement; document.body.appendChild(canvas); scene = new THREE.Scene(); // Creating the ARView, which is the object that handles // the rendering of the camera stream behind the three.js // scene arView = new THREE.ARView(vrDisplay, renderer); // The ARPerspectiveCamera is very similar to THREE.PerspectiveCamera, // except when using an AR-capable browser, the camera uses // the projection matrix provided from the device, so that the // perspective camera's depth planes and field of view matches // the physical camera on the device. camera = new THREE.ARPerspectiveCamera(vrDisplay, 60, window.innerWidth / window.innerHeight, 0.01, 100); // VRControls is a utility from three.js that applies the device's // orientation/position to the perspective camera, keeping our // real world and virtual world in sync. vrControls = new THREE.VRControls(camera); // Bind our event handlers window.addEventListener('resize', onWindowResize, false); // Start drawing when the user touches the screen. renderer.domElement.addEventListener('touchstart', function(event) { drawing = true; }); // Stop the current draw stroke when the user finishes the touch. renderer.domElement.addEventListener('touchend', function(event) { drawing = false; stroke.length = 0; strokeIndex += 1; }); update(); } // Get a point in front of the device to use as the drawing location. // Pass in a THREE.Vector3 to be populated with data to avoid creating garbage. // This strange function structure is a way of avoiding creating garbage while // generating the rotated forward vector. var getDrawPoint = (function() { // Setup a basic forward vector (scaled down so it's not so far away). var forward = new THREE.Vector3(0, 0, -1).multiplyScalar(STROKE_DISTANCE); // Create a scratch vector for generating the rotated forward vector. var rotatedForward = new THREE.Vector3(); return function(out) { // Start with the camera position, which is equivalent to device pose. out.copy(camera.position); // Rotate the forward vector by the pose orientation. rotatedForward.copy(forward); rotatedForward.applyQuaternion(camera.quaternion); out.add(rotatedForward); } })(); // Basic physics parameters to help smooth out input data for nicer // brsuh strokes var strokeWidth = STROKE_WIDTH_MINIMUM; var position = new THREE.Vector3(); var previousPosition = new THREE.Vector3(); var normalQuat = new THREE.Quaternion(); var previousNormalQuat = new THREE.Quaternion(); var normal = new THREE.Vector3(0, 0, 1); var previousNormal = new THREE.Vector3(0, 0, 1); var velocity = new THREE.Vector3(); var previousVelocity = new THREE.Vector3(); var acceleration = new THREE.Vector3(); // An array to keep track of the past accelerations var accelerationArray = []; /** * This function returns an object that represents a point in the stroke. * The position, normal, velocity and width values are used to help calculate * how the stroke will look. They are set by the physics values being * calculated every frame inside the updatePhysics function */ function getStroke() { return { position: new THREE.Vector3(position.x, position.y, position.z), normal: new THREE.Vector3(normal.x, normal.y, normal.z), velocity: new THREE.Vector3(velocity.x, velocity.y, velocity.z), width: strokeWidth }; } /** * This function is called when the user touches down and starts to draw * a stroke */ function processDraw() { // Add the draw point to the current stroke. stroke.push(getStroke()); // Check to see if you have enough points (2) to start drawing a stroke if(stroke.length < MINIMUM_STROKE_POINTS) { return; } // Get the last two points in the stroke var s0 = stroke[stroke.length - 2]; var s1 = stroke[stroke.length - 1]; // Check if the distance between the last two points is bigger // than a set amount, this avoids tiny strips in the brush stroke if(s0.position.distanceTo(s1.position) < MINIMIM_POINT_DISTANCE) { stroke.pop(); return; } // Remove the old stroke from the scene so we can regenerate the stroke. if (strokes[strokeIndex]) { scene.remove(strokes[strokeIndex]); } // Generate a new stroke and cache it in our array of strokes strokes[strokeIndex] = generateStroke(stroke); // Add the stroke to the threejs scene for rendering scene.add(strokes[strokeIndex]); } /** * This function is responsible for constantly rendering a brush reticle * when the user is not touching down on the screen. This helps to pre- * visualize the dynamics of the brush (color, stroke width, etc) */ function processDrawBrush() { // Add the draw point to the current stroke. brushStroke.push(getStroke()); // Don't use positions if they aren't far enough away from the previous point. if(brushStroke.length < MINIMUM_BRUSH_POINTS) { return; } // Remove the old brush reticle from the scene so we can regenerate it if (brush) { brushStroke.shift(); scene.remove(brush); } // Get the last two points in the stroke var b0 = brushStroke[brushStroke.length - 2]; var b1 = brushStroke[brushStroke.length - 1]; // Check if the distance between the last two points is bigger // than a set amount, this avoids tiny strips in the brush stroke if(b0.position.distanceTo(b1.position) < MINIMIM_POINT_DISTANCE) { return; } // Generate a new stroke and sets it to brush reticle brush = generateStroke(brushStroke); // Add the brush reticle to the threejs scene for rendering scene.add(brush); } /** * This function calculates the positions, normals, uvs and velocities * float32Arrays needed to create a buffer geometry so we can render our brush * strokes using threejs (WebGL) */ function generateStroke(strokeData) { // Create Float32Arrays of the proper size to hold vertex information // Each stroke is rendered by a triangle strip, thus each stroke point yields // two verticies, and each vertex contains three (x, y, z) or two // floats (u, v) depending on property. var positions = new Float32Array(strokeData.length * 2 * 3); var normals = new Float32Array(strokeData.length * 2 * 3); var uvs = new Float32Array(strokeData.length * 2 * 2); var velocities = new Float32Array(strokeData.length * 2 * 3); // Create a Vector3 to cache the vertex offset vector var v = new THREE.Vector3(); // Create a reference to the last stroke point's velocity var lastVelocity; // Create two Vector3s to cache the positions of the two vertex positions // calculated from the origin stroke position and its physics properties var p1 = new THREE.Vector3(); var p2 = new THREE.Vector3(); // Create a variable to keep track of whether the current stroke point's // velocity is in the opposite direction of the last point's velocity var flip = false; // loop through the stroke points and calculate its two emiited vertex // offset positions and set their other properties in the Float32Arrays // created above for (var i = 0; i < strokeData.length; i++) { // Get the current stroke point var entry = strokeData[i]; // Calculate the stroke point's offset by using the cross product of the // stroke point's normal and its velocity v.crossVectors(entry.normal, entry.velocity); // Normalize the vector and scale it by the precalculated stroke width v.normalize(); // This width is based on how fast the user was moving the device when the // stroke was drawn v.multiplyScalar(entry.width); // If this isn't the first point check for a velocity flip, otherwise create // a new Vector3 if( lastVelocity ) { // Use the dot product to check whether the current velocity is in the // direction as the previous stroke's velocity var directionChange = lastVelocity.dot( entry.velocity ); if( directionChange < 0 ) { flip = !flip; } } else { lastVelocity = new THREE.Vector3(); } lastVelocity.copy( entry.velocity ); // this is used to avoid improper drawing of triangles when the user // changes direction of a stroke if( flip ) { p1.addVectors(entry.position, v); p2.subVectors(entry.position, v); } else { p1.subVectors(entry.position, v); p2.addVectors(entry.position, v); } // Calculate the offset for the current vertex (i * 2 * 3) // Set the positions for the ith and ith + 1 verticies var index = i * 2 * 3; positions[index] = p1.x; positions[index + 1] = p1.y; positions[index + 2] = p1.z; positions[index + 3] = p2.x; positions[index + 4] = p2.y; positions[index + 5] = p2.z; // Calculate the offset for the current vertex (i * 2 * 3) index = i * 2 * 3; // Set the normals for the ith and ith + 1 verticies normals[index] = entry.normal.x; normals[index + 1] = entry.normal.y; normals[index + 2] = entry.normal.z; normals[index + 3] = entry.normal.x; normals[index + 4] = entry.normal.y; normals[index + 5] = entry.normal.z; // Calculate the offset for the current vertex (i * 2 * 3) index = i * 2 * 3; // Set the velocities for the ith and ith + 1 verticies velocities[index] = entry.velocity.x; velocities[index + 1] = entry.velocity.y; velocities[index + 2] = entry.velocity.z; velocities[index + 3] = entry.velocity.x; velocities[index + 4] = entry.velocity.y; velocities[index + 5] = entry.velocity.z; // Calculate the offset for the current vertex (i * 2 * 3) index = i * 2 * 2; // Set the uvs for the ith and ith + 1 verticies // Each complete stroke has vertex coordinates from (0,0) to (1,1) uvs[index] = i / (strokeData.length - 1); uvs[index + 1] = 0; uvs[index + 2] = i / (strokeData.length - 1); uvs[index + 3] = 1; } // Create a Threejs BufferGeometry var geometry = new THREE.BufferGeometry(); function disposeArray() { this.array = null; } // Add attributes to the buffer geometry using the Float32Arrays created // above. This will tell our shader pipeline information about each vertex // so we can create all types of dynamic strokes & paints geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3).onUpload(disposeArray)); geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3).onUpload(disposeArray)); geometry.addAttribute('uv', new THREE.BufferAttribute(uvs, 2).onUpload(disposeArray)); geometry.addAttribute('velocity', new THREE.BufferAttribute(velocities, 3).onUpload(disposeArray)); geometry.computeBoundingSphere(); // Create a Threejs mesh and set its draw mode to triangle strips var mesh = new THREE.Mesh(geometry, drawMaterial); mesh.drawMode = THREE.TriangleStripDrawMode; return mesh; } /** * Small utility function to sum the last n frames of acceleration to see if * the device was shaken. If the device was shaken, then the last stroke is * erased. */ function checkForShake() { //Calculate the current acceleration's magnitude and add it to the // acceleration array accelerationArray.push(acceleration.length()); var len = accelerationArray.length; // if the accelerationArray has enough frames to calculate whether the user // has shaken the device, then check for a shake if(len > MINIMIM_ERASE_ENERGY_FRAMES) { // Sum the "energy" total by looping through the accelerationArray values var energy = 0; for(var i = 0; i < len; i++) { energy += accelerationArray[i]; } // Check to see if the total energy is greate than a preset amount // this amount was calculated via user testing different shake thresholds if(energy > MINIMIM_ERASE_ENERGY * MINIMIM_ERASE_ENERGY_FRAMES) { // If a shake was detected, clear the accelerationArray so we don't get // multiple shakes in a small time frame accelerationArray.length = 0; // This is the action that happens when the user shakes the device, the // app clears or removes the last stroke clearLastStroke(); } else { // If the energy wasn't high enough pop off the oldest acceleration value accelerationArray.shift(); } } } /** * Clears all the strokes and essentially lets the user start over */ function clearStrokes() { // If the user if currently drawing, stop drawing = false; // Clear the current stroke array stroke.length = 0; // Reset the current stroke index strokeIndex = 0; // Remove all the threejs mesh strokes from the scene var len = strokes.length; for( var i = 0; i < len; i++ ) { scene.remove(strokes[i]); } } /** * Clears the last stroke, acts essentially as an UNDO */ function clearLastStroke() { // If the user if currently drawing, stop, and remove the stroke if( drawing ) { drawing = false; scene.remove(strokes[strokeIndex]); stroke.length = 0; } // Also remove the last drawn stroke strokeIndex -= 1; scene.remove(strokes[strokeIndex]); } /** * update physics function, called once per frame. Handles updating and * smoothing out rough input data from AR Session */ function updatePhysics() { // Cache previous positions & normals previousPosition.copy(position); previousNormal.copy(normal); previousNormalQuat.copy(normalQuat); previousVelocity.copy(velocity); // Update Drawing Stroke Position getDrawPoint(position); position.lerpVectors(previousPosition, position, STROKE_POSITION_EASING); normalQuat.slerp(camera.quaternion, STROKE_NORMAL_QUAT_EASING); // Update Drawing Stroke Normal normal.set(0, 0, 1); normal.applyQuaternion(normalQuat); normal.normalize(); normal.lerpVectors(previousNormal, normal, STROKE_NORMAL_EASING); // Update velocity velocity.subVectors(position, previousPosition); velocity.lerpVectors(previousVelocity, velocity, STROKE_VELOCITY_EASING); // Update acceleration acceleration.subVectors(velocity, previousVelocity); // Update Drawing Stroke Width strokeWidth = THREE.Math.lerp(strokeWidth, velocity.length() * STROKE_WIDTH_MODIFIER, STROKE_WIDTH_EASING); strokeWidth = Math.min( Math.max( strokeWidth, STROKE_WIDTH_MINIMUM ), STROKE_WIDTH_MAXIMUM ); } /** * The render loop, called once per frame. Handles updating * our scene and rendering. */ function update() { // Clears color from the frame before rendering the camera (arView) or scene. renderer.clearColor(); // Render the device's camera stream on screen first of all. // It allows to get the right pose synchronized with the right frame. arView.render(); // Update our camera projection matrix in the event that // the near or far planes have updated camera.updateProjectionMatrix(); // Update our perspective camera's positioning vrControls.update(); // Update Brush Physics updatePhysics(); // Check for shake to undo functionality checkForShake(); // Update the current graffiti stroke. if (drawing) { processDraw(); } // Uncomment to draw brush reticle // processDrawBrush(); // Render our three.js virtual scene renderer.clearDepth(); renderer.render(scene, camera); // Kick off the requestAnimationFrame to call this function // when a new VRDisplay frame is rendered vrDisplay.requestAnimationFrame(update); } /** * On window resize, update the perspective camera's aspect ratio, * and call `updateProjectionMatrix` so that we can get the latest * projection matrix provided from the device */ function onWindowResize () { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } </script> </body> </html>