Repository: mkkellogg/TrailRendererJS Branch: master Commit: 671ab19ff706 Files: 6 Total size: 70.1 KB Directory structure: gitextract_vtl97efo/ ├── .gitignore ├── README.md ├── index.html └── js/ ├── Main.js ├── OrbitControls.js └── TrailRenderer.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ ================================================ FILE: README.md ================================================ # Three.js Trail Renderer Basic trail renderer for Three.js. This library allows for the straight-forward attachment of motion trails to any 3D object. The programmer simply has to specify the shape of the trail and its target (the target must be a Three.js Object3D instance). The update of the trail is handled automatically by the library. The shape of the trail is specified by supplying a list of 3D points. These points make up the local-space head of the trail. During the update phase for the trail, a new instance of the head is created by transforming the local points into world-space using the target Object3D instance's local-to-world transformation matrix. These points are then connected to the existing head of the trail to extend the trail's geometry. The trail renderer currently supports both textured and non-textured trails. Non-textured trails work well with many trail shapes in both translucent and opaque modes. Textured trails work well with many shapes as long as the trail is opaque; if the trail is not opaque, flat shapes work best. Demo of the effect can be seen [here](http://projects.markkellogg.org/threejs/demo_trail_renderer.php) The following code shows how to attach a trail renderer in a scene named 'scene' to an existing Object3D instance named 'trailTarget'. ```javascript // specify points to create planar trail-head geometry const trailHeadGeometry = []; trailHeadGeometry.push( new THREE.Vector3( -10.0, 0.0, 0.0 ), new THREE.Vector3( 0.0, 0.0, 0.0 ), new THREE.Vector3( 10.0, 0.0, 0.0 ) ); // create the trail renderer object const trail = new TrailRenderer( scene, false ); // set how often a new trail node will be added and existing nodes will be updated trail.setAdvanceFrequency(30); // create material for the trail renderer const trailMaterial = TrailRenderer.createBaseMaterial(); // specify length of trail const trailLength = 150; // initialize the trail trail.initialize( trailMaterial, trailLength, false, 0, trailHeadGeometry, trailTarget ); // activate the trail trail.activate(); function animate() { requestAnimationFrame( animate ); trail.update(); render(); } ``` ================================================ FILE: index.html ================================================ Three.js Trail Renderer
three.js - Trail Renderer by mkkellogg
================================================ FILE: js/Main.js ================================================ import * as THREE from 'three'; import { OrbitControls } from './OrbitControls.js'; import { TrailRenderer } from './TrailRenderer.js'; const TrailTypes = Object.freeze({ Basic : 1, Textured : 2 }); const TrailShapes = Object.freeze({ Plane : 1, Star : 2, Circle : 3 }); let screenWidth, screenHeight; let scene, gui, renderer, rendererContainer; let camera, pointLight, ambientLight, controls, stats; let options, lastFrameTime, elapsedSimTime; let starPoints, circlePoints, planePoints; let trailTarget; let trailHeadGeometry, trail, lastTrailUpdateTime, lastTrailResetTime; let trailMaterial, baseTrailMaterial, texturedTrailMaterial; window.addEventListener("load", function load(event) { window.removeEventListener("load", load, false); init(); }, false); function init() { elapsedSimTime = 0; lastTrailUpdateTime = performance.now(); lastTrailResetTime = performance.now(); getScreenDimensions(); initTrailOptions(); initScene(); initGUI(); initListeners(); initLights(); initSceneGeometry(function() { initTrailRenderers(function() { initRenderer(); initControls(); initStats(); animate(); }); }); } function initTrailOptions() { options = { headRed : 1.0, headGreen : 0.0, headBlue : 0.0, headAlpha : 1.0, tailRed : 1.0, tailGreen : 0.4, tailBlue : 1.0, tailAlpha : 1.0, trailLength : 700, trailType : TrailTypes.Textured, trailShape : TrailShapes.Star, textureTileFactorS : 10.0, textureTileFactorT : 0.8, dragTexture : true, depthWrite : true, pauseSim : false }; } function initGUI() { gui = new dat.GUI(); gui.add(options, 'trailType', {Basic: TrailTypes.Basic, Textured: TrailTypes.Textured}).name("Trail type").onChange(function() { options.trailType = parseInt(options.trailType); updateTrailType(); }); gui.add(options, 'trailShape', { Plane: TrailShapes.Plane, Star: TrailShapes.Star, Circle: TrailShapes.Circle }).name("Trail shape").onChange(function() { options.trailShape = parseInt(options.trailShape); updateTrailShape(); }); gui.add(options, "trailLength", 0, 1000).name("Trail length").onChange(updateTrailLength); gui.add(options, 'depthWrite').name("Depth write").onChange(updateTrailDepthWrite); gui.add(options, 'pauseSim').name("Pause simulation").onChange(pauseResumeSimulation); const headColor = gui.addFolder("Head color"); headColor.add(options, "headRed", 0.0, 1.0, 0.025).name("Red").onChange(updateTrailColors); headColor.add(options, "headGreen", 0.0, 1.0, 0.025).name("Green").onChange(updateTrailColors); headColor.add(options, "headBlue", 0.0, 1.0, 0.025).name("Blue").onChange(updateTrailColors); headColor.add(options, "headAlpha", 0.0, 1.0, 0.025).name("Alpha").onChange(updateTrailColors); const tailColor = gui.addFolder("Tail color"); tailColor.add(options, "tailRed", 0.0, 1.0, 0.025).name("Red").onChange(updateTrailColors); tailColor.add(options, "tailGreen", 0.0, 1.0, 0.025).name("Green").onChange(updateTrailColors); tailColor.add(options, "tailBlue", 0.0, 1.0, 0.025).name("Blue").onChange(updateTrailColors); tailColor.add(options, "tailAlpha", 0.0, 1.0, 0.025).name("Alpha").onChange(updateTrailColors); const textureOptions = gui.addFolder("Texture options"); textureOptions.add(options, "textureTileFactorS", 0, 25).name("Texture Tile S").onChange(updateTrailTextureTileSize); textureOptions.add(options, "textureTileFactorT", 0, 25).name("Texture Tile T").onChange(updateTrailTextureTileSize); textureOptions.add(options, 'dragTexture').name("Drag Texture").onChange(updateTrailTextureDrag); gui.domElement.parentNode.style.zIndex = 100; } function initListeners() { window.addEventListener('resize', onWindowResize, false); } function initRenderer() { renderer = new THREE.WebGLRenderer(); renderer.setSize(screenWidth, screenHeight); renderer.setClearColor(0x000000); renderer.sortObjects = false; rendererContainer = document.getElementById('renderingContainer'); rendererContainer.appendChild(renderer.domElement); } function initLights() { ambientLight = new THREE.AmbientLight(0x777777); scene.add(ambientLight); pointLight = new THREE.DirectionalLight(0xFFFFFF, 2); pointLight.position.set(5, 2, 5); scene.add(pointLight); } function initSceneGeometry(onFinished) { initTrailHeadGeometries(); initTrailTarget(); if(onFinished) { onFinished(); } } function initTrailHeadGeometries() { planePoints = []; planePoints.push(new THREE.Vector3(-14.0, 4.0, 0.0), new THREE.Vector3(0.0, 4.0, 0.0), new THREE.Vector3(14.0, 4.0, 0.0)); circlePoints = []; const twoPI = Math.PI * 2; let index = 0; const scale = 10.0; const inc = twoPI / 32.0; for (let i = 0; i <= twoPI + inc; i+= inc) { const vector = new THREE.Vector3(); vector.set(Math.cos(i) * scale, Math.sin(i) * scale, 0); circlePoints[ index ] = vector; index++; } starPoints = []; starPoints.push(new THREE.Vector3(0, 16)); starPoints.push(new THREE.Vector3(4, 4)); starPoints.push(new THREE.Vector3(16, 4)); starPoints.push(new THREE.Vector3(8, -4)); starPoints.push(new THREE.Vector3(12, -16)); starPoints.push(new THREE.Vector3( 0, -8)); starPoints.push(new THREE.Vector3(-12, -16)); starPoints.push(new THREE.Vector3(-8, -4)); starPoints.push(new THREE.Vector3(-16, 4)); starPoints.push(new THREE.Vector3(-4, 4)); starPoints.push(new THREE.Vector3(0, 16)); } function initTrailTarget() { const starShape = new THREE.Shape(starPoints); const extrusionSettings = { amount: 2, size: 2, height: 1, curveSegments: 3, bevelThickness: 1, bevelSize: 2, bevelEnabled: false, material: 0, extrudeMaterial: 1 }; const starGeometry = new THREE.ExtrudeGeometry(starShape, extrusionSettings); const trailTargetMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.7, metalness: 0.3 }); trailTarget = new THREE.Mesh(starGeometry, trailTargetMaterial); trailTarget.position.set(0, 0, 0); trailTarget.scale.multiplyScalar(1); trailTarget.receiveShadow = false; trailTarget.matrixAutoUpdate = false; scene.add(trailTarget); } function initTrailRenderers(callback) { trail = new TrailRenderer(scene, false); trail.setAdvanceFrequency(30); baseTrailMaterial = TrailRenderer.createBaseMaterial(); const textureLoader = new THREE.TextureLoader(); textureLoader.load("textures/sparkle4.jpg", function(tex) { tex.wrapS = THREE.RepeatWrapping; tex.wrapT = THREE.RepeatWrapping; texturedTrailMaterial = TrailRenderer.createTexturedMaterial(); texturedTrailMaterial.uniforms.trailTexture.value = tex; setTrailShapeFromOptions(); setTrailTypeFromOptions(); initializeTrail(); if (callback) { callback(); } }); } function updateTrailLength() { initializeTrail(); } function setTrailTypeFromOptions() { switch (options.trailType) { case TrailTypes.Basic: trailMaterial = baseTrailMaterial; break; case TrailTypes.Textured: trailMaterial = texturedTrailMaterial; break; } } function updateTrailType() { setTrailTypeFromOptions(); initializeTrail(); } function setTrailShapeFromOptions() { switch (options.trailShape) { case TrailShapes.Plane: trailHeadGeometry = planePoints; break; case TrailShapes.Star: trailHeadGeometry = starPoints; break; case TrailShapes.Circle: trailHeadGeometry = circlePoints; break; } } function updateTrailShape() { setTrailShapeFromOptions(); initializeTrail(); } function updateTrailTextureDrag() { initializeTrail(); } function updateTrailTextureTileSize() { trailMaterial.uniforms.textureTileFactor.value.set(options.textureTileFactorS, options.textureTileFactorT); } function updateTrailColors() { trailMaterial.uniforms.headColor.value.set(options.headRed, options.headGreen, options.headBlue, options.headAlpha); trailMaterial.uniforms.tailColor.value.set(options.tailRed, options.tailGreen, options.tailBlue, options.tailAlpha); } function updateTrailDepthWrite() { trailMaterial.depthWrite = options.depthWrite; } const updateTrailTarget = function updateTrailTarget() { const tempQuaternion = new THREE.Quaternion(); const baseForward = new THREE.Vector3(0, 0, -1); const tempForward = new THREE.Vector3(); const tempUp = new THREE.Vector3(); const tempRotationMatrix = new THREE.Matrix4(); const tempTranslationMatrix = new THREE.Matrix4(); const currentTargetPosition = new THREE.Vector3(); const lastTargetPosition = new THREE.Vector3(); const currentDirection = new THREE.Vector3(); const lastDirection = new THREE.Vector3(); const lastRotationMatrix = new THREE.Matrix4(); return function updateTrailTarget(time) { trail.update(); tempRotationMatrix.identity(); tempTranslationMatrix.identity(); const scaledTime = time * .001; const areaScale = 100; lastTargetPosition.copy(currentTargetPosition); currentTargetPosition.x = Math.sin(scaledTime) * areaScale; currentTargetPosition.y = Math.sin(scaledTime * 1.1) * areaScale; currentTargetPosition.z = Math.sin(scaledTime * 1.6) * areaScale; currentDirection.copy(currentTargetPosition); currentDirection.sub(lastTargetPosition); if (currentDirection.lengthSq() < .001) { currentDirection.copy(lastDirection); } else { currentDirection.normalize(); } tempUp.crossVectors(currentDirection, baseForward); const angle = baseForward.angleTo(currentDirection); if(Math.abs(angle) > .01 && tempUp.lengthSq() > .001) { tempQuaternion.setFromUnitVectors(baseForward, currentDirection); tempQuaternion.normalize(); tempRotationMatrix.makeRotationFromQuaternion(tempQuaternion); lastRotationMatrix.copy(tempRotationMatrix); } tempTranslationMatrix.makeTranslation (currentTargetPosition.x, currentTargetPosition.y, currentTargetPosition.z); tempTranslationMatrix.multiply(tempRotationMatrix); trailTarget.matrix.identity(); trailTarget.applyMatrix4(tempTranslationMatrix); trailTarget.updateMatrixWorld(); lastDirection.copy(currentDirection); } }(); function initializeTrail() { trail.initialize(trailMaterial, Math.floor(options.trailLength), options.dragTexture ? 1.0 : 0.0, 0, trailHeadGeometry, trailTarget); updateTrailColors(); updateTrailTextureTileSize(); updateTrailDepthWrite(); trail.activate(); } function initScene() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(45, 1.0, 2, 2000); scene.add(camera); resetCamera(); } function initStats() { stats = new Stats(); stats.domElement.style.position = 'absolute'; stats.domElement.style.bottom = '0px'; stats.domElement.style.zIndex = 100; rendererContainer.appendChild(stats.domElement); } function initControls() { controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, 0, 0); controls.update(); } function onWindowResize() { getScreenDimensions(); renderer.setSize(screenWidth, screenHeight); resetCamera(); } function getScreenDimensions() { screenWidth = window.innerWidth; screenHeight = window.innerHeight; } function resetCamera() { getScreenDimensions(); camera.aspect = screenWidth / screenHeight; camera.updateProjectionMatrix(); camera.position.set(0, 200, 400); camera.lookAt(scene.position); } function animate() { requestAnimationFrame(animate); update(); render(); } function pauseResumeSimulation() { if (trail.paused) { trail.pause(); } else { trail.resume(); } } function update() { const currentTime = performance.now(); const deltaTime = lastFrameTime ? currentTime - lastFrameTime : 0; if (!options.pauseSim){ elapsedSimTime += deltaTime; updateTrailTarget(elapsedSimTime); } lastFrameTime = currentTime; controls.update(); stats.update(); } function render() { renderer.render(scene, camera); } ================================================ FILE: js/OrbitControls.js ================================================ import { EventDispatcher, MOUSE, Quaternion, Spherical, TOUCH, Vector2, Vector3, Plane, Ray, MathUtils } from 'three'; // OrbitControls performs orbiting, dollying (zooming), and panning. // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). // // Orbit - left mouse / touch: one-finger move // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move const _changeEvent = { type: 'change' }; const _startEvent = { type: 'start' }; const _endEvent = { type: 'end' }; const _ray = new Ray(); const _plane = new Plane(); const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD ); class OrbitControls extends EventDispatcher { constructor( object, domElement ) { super(); this.object = object; this.domElement = domElement; this.domElement.style.touchAction = 'none'; // disable touch scroll // Set to false to disable this control this.enabled = true; // "target" sets the location of focus, where the object orbits around this.target = new Vector3(); // How far you can dolly in and out ( PerspectiveCamera only ) this.minDistance = 0; this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only ) this.minZoom = 0; this.maxZoom = Infinity; // How far you can orbit vertically, upper and lower limits. // Range is 0 to Math.PI radians. this.minPolarAngle = 0; // radians this.maxPolarAngle = Math.PI; // radians // How far you can orbit horizontally, upper and lower limits. // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) this.minAzimuthAngle = - Infinity; // radians this.maxAzimuthAngle = Infinity; // radians // Set to true to enable damping (inertia) // If damping is enabled, you must call controls.update() in your animation loop this.enableDamping = false; this.dampingFactor = 0.05; // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. // Set to false to disable zooming this.enableZoom = true; this.zoomSpeed = 1.0; // Set to false to disable rotating this.enableRotate = true; this.rotateSpeed = 1.0; // Set to false to disable panning this.enablePan = true; this.panSpeed = 1.0; this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up this.keyPanSpeed = 7.0; // pixels moved per arrow key push this.zoomToCursor = false; // Set to true to automatically rotate around the target // If auto-rotate is enabled, you must call controls.update() in your animation loop this.autoRotate = false; this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 // The four arrow keys this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; // Mouse buttons this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; // Touch fingers this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; // for reset this.target0 = this.target.clone(); this.position0 = this.object.position.clone(); this.zoom0 = this.object.zoom; // the target DOM element for key events this._domElementKeyEvents = null; // // public methods // this.getPolarAngle = function () { return spherical.phi; }; this.getAzimuthalAngle = function () { return spherical.theta; }; this.getDistance = function () { return this.object.position.distanceTo( this.target ); }; this.listenToKeyEvents = function ( domElement ) { domElement.addEventListener( 'keydown', onKeyDown ); this._domElementKeyEvents = domElement; }; this.stopListenToKeyEvents = function () { this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); this._domElementKeyEvents = null; }; this.saveState = function () { scope.target0.copy( scope.target ); scope.position0.copy( scope.object.position ); scope.zoom0 = scope.object.zoom; }; this.reset = function () { scope.target.copy( scope.target0 ); scope.object.position.copy( scope.position0 ); scope.object.zoom = scope.zoom0; scope.object.updateProjectionMatrix(); scope.dispatchEvent( _changeEvent ); scope.update(); state = STATE.NONE; }; // this method is exposed, but perhaps it would be better if we can make it private... this.update = function () { const offset = new Vector3(); // so camera.up is the orbit axis const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); const quatInverse = quat.clone().invert(); const lastPosition = new Vector3(); const lastQuaternion = new Quaternion(); const lastTargetPosition = new Vector3(); const twoPI = 2 * Math.PI; return function update() { const position = scope.object.position; offset.copy( position ).sub( scope.target ); // rotate offset to "y-axis-is-up" space offset.applyQuaternion( quat ); // angle from z-axis around y-axis spherical.setFromVector3( offset ); if ( scope.autoRotate && state === STATE.NONE ) { rotateLeft( getAutoRotationAngle() ); } if ( scope.enableDamping ) { spherical.theta += sphericalDelta.theta * scope.dampingFactor; spherical.phi += sphericalDelta.phi * scope.dampingFactor; } else { spherical.theta += sphericalDelta.theta; spherical.phi += sphericalDelta.phi; } // restrict theta to be between desired limits let min = scope.minAzimuthAngle; let max = scope.maxAzimuthAngle; if ( isFinite( min ) && isFinite( max ) ) { if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; if ( min <= max ) { spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); } else { spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? Math.max( min, spherical.theta ) : Math.min( max, spherical.theta ); } } // restrict phi to be between desired limits spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); spherical.makeSafe(); // move target to panned location if ( scope.enableDamping === true ) { scope.target.addScaledVector( panOffset, scope.dampingFactor ); } else { scope.target.add( panOffset ); } // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera // we adjust zoom later in these cases if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) { spherical.radius = clampDistance( spherical.radius ); } else { spherical.radius = clampDistance( spherical.radius * scale ); } offset.setFromSpherical( spherical ); // rotate offset back to "camera-up-vector-is-up" space offset.applyQuaternion( quatInverse ); position.copy( scope.target ).add( offset ); scope.object.lookAt( scope.target ); if ( scope.enableDamping === true ) { sphericalDelta.theta *= ( 1 - scope.dampingFactor ); sphericalDelta.phi *= ( 1 - scope.dampingFactor ); panOffset.multiplyScalar( 1 - scope.dampingFactor ); } else { sphericalDelta.set( 0, 0, 0 ); panOffset.set( 0, 0, 0 ); } // adjust camera position let zoomChanged = false; if ( scope.zoomToCursor && performCursorZoom ) { let newRadius = null; if ( scope.object.isPerspectiveCamera ) { // move the camera down the pointer ray // this method avoids floating point error const prevRadius = offset.length(); newRadius = clampDistance( prevRadius * scale ); const radiusDelta = prevRadius - newRadius; scope.object.position.addScaledVector( dollyDirection, radiusDelta ); scope.object.updateMatrixWorld(); } else if ( scope.object.isOrthographicCamera ) { // adjust the ortho camera position based on zoom changes const mouseBefore = new Vector3( mouse.x, mouse.y, 0 ); mouseBefore.unproject( scope.object ); scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); scope.object.updateProjectionMatrix(); zoomChanged = true; const mouseAfter = new Vector3( mouse.x, mouse.y, 0 ); mouseAfter.unproject( scope.object ); scope.object.position.sub( mouseAfter ).add( mouseBefore ); scope.object.updateMatrixWorld(); newRadius = offset.length(); } else { console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' ); scope.zoomToCursor = false; } // handle the placement of the target if ( newRadius !== null ) { if ( this.screenSpacePanning ) { // position the orbit target in front of the new camera position scope.target.set( 0, 0, - 1 ) .transformDirection( scope.object.matrix ) .multiplyScalar( newRadius ) .add( scope.object.position ); } else { // get the ray and translation plane to compute target _ray.origin.copy( scope.object.position ); _ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix ); // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid // extremely large values if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) { object.lookAt( scope.target ); } else { _plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target ); _ray.intersectPlane( _plane, scope.target ); } } } } else if ( scope.object.isOrthographicCamera ) { scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); scope.object.updateProjectionMatrix(); zoomChanged = true; } scale = 1; performCursorZoom = false; // update condition is: // min(camera displacement, camera rotation in radians)^2 > EPS // using small-angle approximation cos(x/2) = 1 - x^2 / 8 if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS || lastTargetPosition.distanceToSquared( scope.target ) > 0 ) { scope.dispatchEvent( _changeEvent ); lastPosition.copy( scope.object.position ); lastQuaternion.copy( scope.object.quaternion ); lastTargetPosition.copy( scope.target ); zoomChanged = false; return true; } return false; }; }(); this.dispose = function () { scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); scope.domElement.removeEventListener( 'pointercancel', onPointerUp ); scope.domElement.removeEventListener( 'wheel', onMouseWheel ); scope.domElement.removeEventListener( 'pointermove', onPointerMove ); scope.domElement.removeEventListener( 'pointerup', onPointerUp ); if ( scope._domElementKeyEvents !== null ) { scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); scope._domElementKeyEvents = null; } //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? }; // // internals // const scope = this; const STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_PAN: 4, TOUCH_DOLLY_PAN: 5, TOUCH_DOLLY_ROTATE: 6 }; let state = STATE.NONE; const EPS = 0.000001; // current position in spherical coordinates const spherical = new Spherical(); const sphericalDelta = new Spherical(); let scale = 1; const panOffset = new Vector3(); const rotateStart = new Vector2(); const rotateEnd = new Vector2(); const rotateDelta = new Vector2(); const panStart = new Vector2(); const panEnd = new Vector2(); const panDelta = new Vector2(); const dollyStart = new Vector2(); const dollyEnd = new Vector2(); const dollyDelta = new Vector2(); const dollyDirection = new Vector3(); const mouse = new Vector2(); let performCursorZoom = false; const pointers = []; const pointerPositions = {}; function getAutoRotationAngle() { return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; } function getZoomScale() { return Math.pow( 0.95, scope.zoomSpeed ); } function rotateLeft( angle ) { sphericalDelta.theta -= angle; } function rotateUp( angle ) { sphericalDelta.phi -= angle; } const panLeft = function () { const v = new Vector3(); return function panLeft( distance, objectMatrix ) { v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix v.multiplyScalar( - distance ); panOffset.add( v ); }; }(); const panUp = function () { const v = new Vector3(); return function panUp( distance, objectMatrix ) { if ( scope.screenSpacePanning === true ) { v.setFromMatrixColumn( objectMatrix, 1 ); } else { v.setFromMatrixColumn( objectMatrix, 0 ); v.crossVectors( scope.object.up, v ); } v.multiplyScalar( distance ); panOffset.add( v ); }; }(); // deltaX and deltaY are in pixels; right and down are positive const pan = function () { const offset = new Vector3(); return function pan( deltaX, deltaY ) { const element = scope.domElement; if ( scope.object.isPerspectiveCamera ) { // perspective const position = scope.object.position; offset.copy( position ).sub( scope.target ); let targetDistance = offset.length(); // half of the fov is center to top of screen targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); // we use only clientHeight here so aspect ratio does not distort speed panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); } else if ( scope.object.isOrthographicCamera ) { // orthographic panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); } else { // camera neither orthographic nor perspective console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); scope.enablePan = false; } }; }(); function dollyOut( dollyScale ) { if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { scale /= dollyScale; } else { console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); scope.enableZoom = false; } } function dollyIn( dollyScale ) { if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { scale *= dollyScale; } else { console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); scope.enableZoom = false; } } function updateMouseParameters( event ) { if ( ! scope.zoomToCursor ) { return; } performCursorZoom = true; const rect = scope.domElement.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const w = rect.width; const h = rect.height; mouse.x = ( x / w ) * 2 - 1; mouse.y = - ( y / h ) * 2 + 1; dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( object ).sub( object.position ).normalize(); } function clampDistance( dist ) { return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) ); } // // event callbacks - update the object state // function handleMouseDownRotate( event ) { rotateStart.set( event.clientX, event.clientY ); } function handleMouseDownDolly( event ) { updateMouseParameters( event ); dollyStart.set( event.clientX, event.clientY ); } function handleMouseDownPan( event ) { panStart.set( event.clientX, event.clientY ); } function handleMouseMoveRotate( event ) { rotateEnd.set( event.clientX, event.clientY ); rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); const element = scope.domElement; rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); rotateStart.copy( rotateEnd ); scope.update(); } function handleMouseMoveDolly( event ) { dollyEnd.set( event.clientX, event.clientY ); dollyDelta.subVectors( dollyEnd, dollyStart ); if ( dollyDelta.y > 0 ) { dollyOut( getZoomScale() ); } else if ( dollyDelta.y < 0 ) { dollyIn( getZoomScale() ); } dollyStart.copy( dollyEnd ); scope.update(); } function handleMouseMovePan( event ) { panEnd.set( event.clientX, event.clientY ); panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); pan( panDelta.x, panDelta.y ); panStart.copy( panEnd ); scope.update(); } function handleMouseWheel( event ) { updateMouseParameters( event ); if ( event.deltaY < 0 ) { dollyIn( getZoomScale() ); } else if ( event.deltaY > 0 ) { dollyOut( getZoomScale() ); } scope.update(); } function handleKeyDown( event ) { let needsUpdate = false; switch ( event.code ) { case scope.keys.UP: if ( event.ctrlKey || event.metaKey || event.shiftKey ) { rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); } else { pan( 0, scope.keyPanSpeed ); } needsUpdate = true; break; case scope.keys.BOTTOM: if ( event.ctrlKey || event.metaKey || event.shiftKey ) { rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); } else { pan( 0, - scope.keyPanSpeed ); } needsUpdate = true; break; case scope.keys.LEFT: if ( event.ctrlKey || event.metaKey || event.shiftKey ) { rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); } else { pan( scope.keyPanSpeed, 0 ); } needsUpdate = true; break; case scope.keys.RIGHT: if ( event.ctrlKey || event.metaKey || event.shiftKey ) { rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); } else { pan( - scope.keyPanSpeed, 0 ); } needsUpdate = true; break; } if ( needsUpdate ) { // prevent the browser from scrolling on cursor keys event.preventDefault(); scope.update(); } } function handleTouchStartRotate() { if ( pointers.length === 1 ) { rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); } else { const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); rotateStart.set( x, y ); } } function handleTouchStartPan() { if ( pointers.length === 1 ) { panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); } else { const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); panStart.set( x, y ); } } function handleTouchStartDolly() { const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; const distance = Math.sqrt( dx * dx + dy * dy ); dollyStart.set( 0, distance ); } function handleTouchStartDollyPan() { if ( scope.enableZoom ) handleTouchStartDolly(); if ( scope.enablePan ) handleTouchStartPan(); } function handleTouchStartDollyRotate() { if ( scope.enableZoom ) handleTouchStartDolly(); if ( scope.enableRotate ) handleTouchStartRotate(); } function handleTouchMoveRotate( event ) { if ( pointers.length == 1 ) { rotateEnd.set( event.pageX, event.pageY ); } else { const position = getSecondPointerPosition( event ); const x = 0.5 * ( event.pageX + position.x ); const y = 0.5 * ( event.pageY + position.y ); rotateEnd.set( x, y ); } rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); const element = scope.domElement; rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); rotateStart.copy( rotateEnd ); } function handleTouchMovePan( event ) { if ( pointers.length === 1 ) { panEnd.set( event.pageX, event.pageY ); } else { const position = getSecondPointerPosition( event ); const x = 0.5 * ( event.pageX + position.x ); const y = 0.5 * ( event.pageY + position.y ); panEnd.set( x, y ); } panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); pan( panDelta.x, panDelta.y ); panStart.copy( panEnd ); } function handleTouchMoveDolly( event ) { const position = getSecondPointerPosition( event ); const dx = event.pageX - position.x; const dy = event.pageY - position.y; const distance = Math.sqrt( dx * dx + dy * dy ); dollyEnd.set( 0, distance ); dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); dollyOut( dollyDelta.y ); dollyStart.copy( dollyEnd ); } function handleTouchMoveDollyPan( event ) { if ( scope.enableZoom ) handleTouchMoveDolly( event ); if ( scope.enablePan ) handleTouchMovePan( event ); } function handleTouchMoveDollyRotate( event ) { if ( scope.enableZoom ) handleTouchMoveDolly( event ); if ( scope.enableRotate ) handleTouchMoveRotate( event ); } // // event handlers - FSM: listen for events and reset state // function onPointerDown( event ) { if ( scope.enabled === false ) return; if ( pointers.length === 0 ) { scope.domElement.setPointerCapture( event.pointerId ); scope.domElement.addEventListener( 'pointermove', onPointerMove ); scope.domElement.addEventListener( 'pointerup', onPointerUp ); } // addPointer( event ); if ( event.pointerType === 'touch' ) { onTouchStart( event ); } else { onMouseDown( event ); } } function onPointerMove( event ) { if ( scope.enabled === false ) return; if ( event.pointerType === 'touch' ) { onTouchMove( event ); } else { onMouseMove( event ); } } function onPointerUp( event ) { removePointer( event ); if ( pointers.length === 0 ) { scope.domElement.releasePointerCapture( event.pointerId ); scope.domElement.removeEventListener( 'pointermove', onPointerMove ); scope.domElement.removeEventListener( 'pointerup', onPointerUp ); } scope.dispatchEvent( _endEvent ); state = STATE.NONE; } function onMouseDown( event ) { let mouseAction; switch ( event.button ) { case 0: mouseAction = scope.mouseButtons.LEFT; break; case 1: mouseAction = scope.mouseButtons.MIDDLE; break; case 2: mouseAction = scope.mouseButtons.RIGHT; break; default: mouseAction = - 1; } switch ( mouseAction ) { case MOUSE.DOLLY: if ( scope.enableZoom === false ) return; handleMouseDownDolly( event ); state = STATE.DOLLY; break; case MOUSE.ROTATE: if ( event.ctrlKey || event.metaKey || event.shiftKey ) { if ( scope.enablePan === false ) return; handleMouseDownPan( event ); state = STATE.PAN; } else { if ( scope.enableRotate === false ) return; handleMouseDownRotate( event ); state = STATE.ROTATE; } break; case MOUSE.PAN: if ( event.ctrlKey || event.metaKey || event.shiftKey ) { if ( scope.enableRotate === false ) return; handleMouseDownRotate( event ); state = STATE.ROTATE; } else { if ( scope.enablePan === false ) return; handleMouseDownPan( event ); state = STATE.PAN; } break; default: state = STATE.NONE; } if ( state !== STATE.NONE ) { scope.dispatchEvent( _startEvent ); } } function onMouseMove( event ) { switch ( state ) { case STATE.ROTATE: if ( scope.enableRotate === false ) return; handleMouseMoveRotate( event ); break; case STATE.DOLLY: if ( scope.enableZoom === false ) return; handleMouseMoveDolly( event ); break; case STATE.PAN: if ( scope.enablePan === false ) return; handleMouseMovePan( event ); break; } } function onMouseWheel( event ) { if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; event.preventDefault(); scope.dispatchEvent( _startEvent ); handleMouseWheel( event ); scope.dispatchEvent( _endEvent ); } function onKeyDown( event ) { if ( scope.enabled === false || scope.enablePan === false ) return; handleKeyDown( event ); } function onTouchStart( event ) { trackPointer( event ); switch ( pointers.length ) { case 1: switch ( scope.touches.ONE ) { case TOUCH.ROTATE: if ( scope.enableRotate === false ) return; handleTouchStartRotate(); state = STATE.TOUCH_ROTATE; break; case TOUCH.PAN: if ( scope.enablePan === false ) return; handleTouchStartPan(); state = STATE.TOUCH_PAN; break; default: state = STATE.NONE; } break; case 2: switch ( scope.touches.TWO ) { case TOUCH.DOLLY_PAN: if ( scope.enableZoom === false && scope.enablePan === false ) return; handleTouchStartDollyPan(); state = STATE.TOUCH_DOLLY_PAN; break; case TOUCH.DOLLY_ROTATE: if ( scope.enableZoom === false && scope.enableRotate === false ) return; handleTouchStartDollyRotate(); state = STATE.TOUCH_DOLLY_ROTATE; break; default: state = STATE.NONE; } break; default: state = STATE.NONE; } if ( state !== STATE.NONE ) { scope.dispatchEvent( _startEvent ); } } function onTouchMove( event ) { trackPointer( event ); switch ( state ) { case STATE.TOUCH_ROTATE: if ( scope.enableRotate === false ) return; handleTouchMoveRotate( event ); scope.update(); break; case STATE.TOUCH_PAN: if ( scope.enablePan === false ) return; handleTouchMovePan( event ); scope.update(); break; case STATE.TOUCH_DOLLY_PAN: if ( scope.enableZoom === false && scope.enablePan === false ) return; handleTouchMoveDollyPan( event ); scope.update(); break; case STATE.TOUCH_DOLLY_ROTATE: if ( scope.enableZoom === false && scope.enableRotate === false ) return; handleTouchMoveDollyRotate( event ); scope.update(); break; default: state = STATE.NONE; } } function onContextMenu( event ) { if ( scope.enabled === false ) return; event.preventDefault(); } function addPointer( event ) { pointers.push( event ); } function removePointer( event ) { delete pointerPositions[ event.pointerId ]; for ( let i = 0; i < pointers.length; i ++ ) { if ( pointers[ i ].pointerId == event.pointerId ) { pointers.splice( i, 1 ); return; } } } function trackPointer( event ) { let position = pointerPositions[ event.pointerId ]; if ( position === undefined ) { position = new Vector2(); pointerPositions[ event.pointerId ] = position; } position.set( event.pageX, event.pageY ); } function getSecondPointerPosition( event ) { const pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ]; return pointerPositions[ pointer.pointerId ]; } // scope.domElement.addEventListener( 'contextmenu', onContextMenu ); scope.domElement.addEventListener( 'pointerdown', onPointerDown ); scope.domElement.addEventListener( 'pointercancel', onPointerUp ); scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); // force an update at start this.update(); } } export { OrbitControls }; ================================================ FILE: js/TrailRenderer.js ================================================ /** * @author Mark Kellogg - http://www.github.com/mkkellogg */ import * as THREE from 'three'; //======================================= // Trail Renderer //======================================= export class TrailRenderer extends THREE.Object3D { constructor (scene, orientToMovement) { super(); this.active = false; this.orientToMovement = false; if (orientToMovement) this.orientToMovement = true; this.scene = scene; this.geometry = null; this.mesh = null; this.nodeCenters = null; this.lastNodeCenter = null; this.currentNodeCenter = null; this.lastOrientationDir = null; this.nodeIDs = null; this.currentLength = 0; this.currentEnd = 0; this.currentNodeID = 0; this.advanceFrequency = 60; this.advancePeriod = 1 / this.advanceFrequency; this.lastAdvanceTime = 0; this.paused = false; this.pauseAdvanceUpdateTimeDiff = 0; } setAdvanceFrequency(advanceFrequency) { this.advanceFrequency = advanceFrequency; this.advancePeriod = 1.0 / this.advanceFrequency; } initialize (material, length, dragTexture, localHeadWidth, localHeadGeometry, targetObject) { this.deactivate(); this.destroyMesh(); this.length = (length > 0) ? length + 1 : 0; this.dragTexture = (! dragTexture) ? 0 : 1; this.targetObject = targetObject; this.initializeLocalHeadGeometry(localHeadWidth, localHeadGeometry); this.nodeIDs = []; this.nodeCenters = []; for(let i = 0; i < this.length; i ++) { this.nodeIDs[ i ] = -1; this.nodeCenters[ i ] = new THREE.Vector3(); } this.material = material; this.initializeGeometry(); this.initializeMesh(); this.material.uniforms.trailLength.value = 0; this.material.uniforms.minID.value = 0; this.material.uniforms.maxID.value = 0; this.material.uniforms.dragTexture.value = this.dragTexture; this.material.uniforms.maxTrailLength.value = this.length; this.material.uniforms.verticesPerNode.value = this.VerticesPerNode; this.material.uniforms.textureTileFactor.value = new THREE.Vector2(1.0, 1.0); this.reset(); } initializeLocalHeadGeometry (localHeadWidth, localHeadGeometry) { this.localHeadGeometry = []; if (!localHeadGeometry) { const halfWidth = (localHeadWidth || 1.0) / 2.0; this.localHeadGeometry.push(new THREE.Vector3(-halfWidth, 0, 0)); this.localHeadGeometry.push(new THREE.Vector3(halfWidth, 0, 0)); this.VerticesPerNode = 2; } else { this.VerticesPerNode = 0; for (let i = 0; i < localHeadGeometry.length && i < TrailRenderer.MaxHeadVertices; i++) { const vertex = localHeadGeometry[ i ]; if (vertex && vertex instanceof THREE.Vector3) { const vertexCopy = new THREE.Vector3(); vertexCopy.copy(vertex); this.localHeadGeometry.push(vertexCopy); this.VerticesPerNode ++; } } } this.FacesPerNode = (this.VerticesPerNode - 1) * 2; this.FaceIndicesPerNode = this.FacesPerNode * 3; } initializeGeometry () { this.vertexCount = this.length * this.VerticesPerNode; this.faceCount = this.length * this.FacesPerNode; const geometry = new THREE.BufferGeometry(); const nodeIDs = new Float32Array(this.vertexCount); const nodeVertexIDs = new Float32Array(this.vertexCount * this.VerticesPerNode); const positions = new Float32Array(this.vertexCount * TrailRenderer.PositionComponentCount); const nodeCenters = new Float32Array(this.vertexCount * TrailRenderer.PositionComponentCount); const uvs = new Float32Array(this.vertexCount * TrailRenderer.UVComponentCount); const indices = new Uint32Array(this.faceCount * TrailRenderer.IndicesPerFace); const nodeIDAttribute = new THREE.BufferAttribute(nodeIDs, 1); nodeIDAttribute.dynamic = true; geometry.setAttribute('nodeID', nodeIDAttribute); const nodeVertexIDAttribute = new THREE.BufferAttribute(nodeVertexIDs, 1); nodeVertexIDAttribute.dynamic = true; geometry.setAttribute('nodeVertexID', nodeVertexIDAttribute); const nodeCenterAttribute = new THREE.BufferAttribute(nodeCenters, TrailRenderer.PositionComponentCount); nodeCenterAttribute.dynamic = true; geometry.setAttribute('nodeCenter', nodeCenterAttribute); const positionAttribute = new THREE.BufferAttribute(positions, TrailRenderer.PositionComponentCount); positionAttribute.dynamic = true; geometry.setAttribute('position', positionAttribute); const uvAttribute = new THREE.BufferAttribute(uvs, TrailRenderer.UVComponentCount); uvAttribute.dynamic = true; geometry.setAttribute('uv', uvAttribute); const indexAttribute = new THREE.BufferAttribute(indices, 1); indexAttribute.dynamic = true; geometry.setIndex(indexAttribute); this.geometry = geometry; } zeroVertices () { const positions = this.geometry.getAttribute('position'); for (let i = 0; i < this.vertexCount; i ++) { const index = i * 3; positions.array[ index ] = 0; positions.array[ index + 1 ] = 0; positions.array[ index + 2 ] = 0; } positions.needsUpdate = true; positions.updateRange.count = - 1; } zeroIndices () { const indices = this.geometry.getIndex(); for (let i = 0; i < this.faceCount; i ++) { const index = i * 3; indices.array[ index ] = 0; indices.array[ index + 1 ] = 0; indices.array[ index + 2 ] = 0; } indices.needsUpdate = true; indices.updateRange.count = - 1; } formInitialFaces () { this.zeroIndices(); const indices = this.geometry.getIndex(); for (let i = 0; i < this.length - 1; i ++) { this.connectNodes(i, i + 1); } indices.needsUpdate = true; indices.updateRange.count = - 1; } initializeMesh () { this.mesh = new THREE.Mesh(this.geometry, this.material); this.mesh.dynamic = true; this.mesh.matrixAutoUpdate = false; } destroyMesh () { if (this.mesh) { this.scene.remove(this.mesh); this.mesh = null; } } reset () { this.currentLength = 0; this.currentEnd = -1; this.lastNodeCenter = null; this.currentNodeCenter = null; this.lastOrientationDir = null; this.currentNodeID = 0; this.formInitialFaces(); this.zeroVertices(); this.geometry.setDrawRange(0, 0); } updateUniforms () { if (this.currentLength < this.length) { this.material.uniforms.minID.value = 0; } else { this.material.uniforms.minID.value = this.currentNodeID - this.length; } this.material.uniforms.maxID.value = this.currentNodeID; this.material.uniforms.trailLength.value = this.currentLength; this.material.uniforms.maxTrailLength.value = this.length; this.material.uniforms.verticesPerNode.value = this.VerticesPerNode; } advance = function() { const tempMatrix4 = new THREE.Matrix4(); return function advance() { this.targetObject.updateMatrixWorld(); tempMatrix4.copy(this.targetObject.matrixWorld); this.advanceWithTransform(tempMatrix4); this.updateUniforms(); }; }(); advanceWithPositionAndOrientation (nextPosition, orientationTangent) { this.advanceGeometry({ position : nextPosition, tangent : orientationTangent }, null); } advanceWithTransform (transformMatrix) { this.advanceGeometry(null, transformMatrix); } advanceGeometry = function() { return function advanceGeometry(positionAndOrientation, transformMatrix) { const nextIndex = this.currentEnd + 1 >= this.length ? 0 : this.currentEnd + 1; if(transformMatrix) { this.updateNodePositionsFromTransformMatrix(nextIndex, transformMatrix); } else { this.updateNodePositionsFromOrientationTangent(nextIndex, positionAndOrientation.position, positionAndOrientation.tangent); } if (this.currentLength >= 1) { this.connectNodes(this.currentEnd , nextIndex); if(this.currentLength >= this.length) { const disconnectIndex = this.currentEnd + 1 >= this.length ? 0 : this.currentEnd + 1; this.disconnectNodes(disconnectIndex); } } if(this.currentLength < this.length) { this.currentLength ++; } this.currentEnd++; if (this.currentEnd >= this.length) { this.currentEnd = 0; } if (this.currentLength >= 1) { if(this.currentLength < this.length) { this.geometry.setDrawRange(0, (this.currentLength - 1) * this.FaceIndicesPerNode); } else { this.geometry.setDrawRange(0, this.currentLength * this.FaceIndicesPerNode); } } this.updateNodeID(this.currentEnd, this.currentNodeID); this.currentNodeID ++; }; }(); currentTime() { return performance.now() / 1000; } pause() { if(!this.paused) { this.paused = true; this.pauseAdvanceUpdateTimeDiff = this.currentTime() - this.lastAdvanceTime; } } resume() { if(this.paused) { this.paused = false; this.lastAdvanceTime = this.currentTime() - this.pauseAdvanceUpdateTimeDiff; } } update() { if (!this.paused) { const time = this.currentTime(); if (!this.lastAdvanceTime) this.lastAdvanceTime = time; if (time - this.lastAdvanceTime > this.advancePeriod) { this.advance(); this.lastAdvanceTime = time; } else { this.updateHead(); } } } updateHead = function() { const tempMatrix4 = new THREE.Matrix4(); return function updateHead() { if(this.currentEnd < 0) return; this.targetObject.updateMatrixWorld(); tempMatrix4.copy(this.targetObject.matrixWorld); this.updateNodePositionsFromTransformMatrix(this.currentEnd, tempMatrix4); }; }(); updateNodeID (nodeIndex, id) { this.nodeIDs[ nodeIndex ] = id; const nodeIDs = this.geometry.getAttribute('nodeID'); const nodeVertexIDs = this.geometry.getAttribute('nodeVertexID'); for (let i = 0; i < this.VerticesPerNode; i ++) { const baseIndex = nodeIndex * this.VerticesPerNode + i ; nodeIDs.array[ baseIndex ] = id; nodeVertexIDs.array[ baseIndex ] = i; } nodeIDs.needsUpdate = true; nodeVertexIDs.needsUpdate = true; nodeIDs.updateRange.offset = nodeIndex * this.VerticesPerNode; nodeIDs.updateRange.count = this.VerticesPerNode; nodeVertexIDs.updateRange.offset = nodeIndex * this.VerticesPerNode; nodeVertexIDs.updateRange.count = this.VerticesPerNode; } updateNodeCenter (nodeIndex, nodeCenter) { this.lastNodeCenter = this.currentNodeCenter; this.currentNodeCenter = this.nodeCenters[ nodeIndex ]; this.currentNodeCenter.copy(nodeCenter); const nodeCenters = this.geometry.getAttribute('nodeCenter'); for (let i = 0; i < this.VerticesPerNode; i ++) { const baseIndex = (nodeIndex * this.VerticesPerNode + i) * 3; nodeCenters.array[ baseIndex ] = nodeCenter.x; nodeCenters.array[ baseIndex + 1 ] = nodeCenter.y; nodeCenters.array[ baseIndex + 2 ] = nodeCenter.z; } nodeCenters.needsUpdate = true; nodeCenters.updateRange.offset = nodeIndex * this.VerticesPerNode * TrailRenderer.PositionComponentCount; nodeCenters.updateRange.count = this.VerticesPerNode * TrailRenderer.PositionComponentCount; } updateNodePositionsFromOrientationTangent = function() { const tempQuaternion = new THREE.Quaternion(); const tempOffset = new THREE.Vector3(); const tempLocalHeadGeometry = []; for (let i = 0; i < TrailRenderer.MaxHeadVertices; i ++) { const vertex = new THREE.Vector3(); tempLocalHeadGeometry.push(vertex); } return function updateNodePositionsFromOrientationTangent(nodeIndex, nodeCenter, orientationTangent ) { const positions = this.geometry.getAttribute('position'); this.updateNodeCenter(nodeIndex, nodeCenter); tempOffset.copy(nodeCenter); tempOffset.sub(TrailRenderer.LocalHeadOrigin); tempQuaternion.setFromUnitVectors(TrailRenderer.LocalOrientationTangent, orientationTangent); for (let i = 0; i < this.localHeadGeometry.length; i ++) { const vertex = tempLocalHeadGeometry[ i ]; vertex.copy(this.localHeadGeometry[ i ]); vertex.applyQuaternion(tempQuaternion); vertex.add(tempOffset); } for (let i = 0; i < this.localHeadGeometry.length; i ++) { const positionIndex = ((this.VerticesPerNode * nodeIndex) + i) * TrailRenderer.PositionComponentCount; const transformedHeadVertex = tempLocalHeadGeometry[ i ]; positions.array[ positionIndex ] = transformedHeadVertex.x; positions.array[ positionIndex + 1 ] = transformedHeadVertex.y; positions.array[ positionIndex + 2 ] = transformedHeadVertex.z; } positions.needsUpdate = true; }; }(); updateNodePositionsFromTransformMatrix = function() { const tempMatrix3 = new THREE.Matrix3(); const tempQuaternion = new THREE.Quaternion(); const tempPosition = new THREE.Vector3(); const tempOffset = new THREE.Vector3(); const worldOrientation = new THREE.Vector3(); const tempDirection = new THREE.Vector3(); const tempLocalHeadGeometry = []; for (let i = 0; i < TrailRenderer.MaxHeadVertices; i ++) { const vertex = new THREE.Vector3(); tempLocalHeadGeometry.push(vertex); } function getMatrix3FromMatrix4(matrix3, matrix4) { const e = matrix4.elements; matrix3.set(e[0], e[1], e[2], e[4], e[5], e[6], e[8], e[9], e[10]); } return function updateNodePositionsFromTransformMatrix(nodeIndex, transformMatrix) { const positions = this.geometry.getAttribute('position'); tempPosition.set(0, 0, 0); tempPosition.applyMatrix4(transformMatrix); this.updateNodeCenter(nodeIndex, tempPosition); for (let i = 0; i < this.localHeadGeometry.length; i ++) { const vertex = tempLocalHeadGeometry[ i ]; vertex.copy(this.localHeadGeometry[ i ]); } for (let i = 0; i < this.localHeadGeometry.length; i ++) { const vertex = tempLocalHeadGeometry[ i ]; vertex.applyMatrix4(transformMatrix); } if(this.lastNodeCenter && this.orientToMovement) { getMatrix3FromMatrix4(tempMatrix3, transformMatrix); worldOrientation.set(0, 0, -1); worldOrientation.applyMatrix3(tempMatrix3); tempDirection.copy(this.currentNodeCenter); tempDirection.sub(this.lastNodeCenter); tempDirection.normalize(); if(tempDirection.lengthSq() <= .0001 && this.lastOrientationDir) { tempDirection.copy(this.lastOrientationDir); } if(tempDirection.lengthSq() > .0001) { if(! this.lastOrientationDir) this.lastOrientationDir = new THREE.Vector3(); tempQuaternion.setFromUnitVectors(worldOrientation, tempDirection); tempOffset.copy(this.currentNodeCenter); for (let i = 0; i < this.localHeadGeometry.length; i ++) { const vertex = tempLocalHeadGeometry[ i ]; vertex.sub(tempOffset); vertex.applyQuaternion(tempQuaternion); vertex.add(tempOffset); } } } for (let i = 0; i < this.localHeadGeometry.length; i ++) { const positionIndex = ((this.VerticesPerNode * nodeIndex) + i) * TrailRenderer.PositionComponentCount; const transformedHeadVertex = tempLocalHeadGeometry[ i ]; positions.array[ positionIndex ] = transformedHeadVertex.x; positions.array[ positionIndex + 1 ] = transformedHeadVertex.y; positions.array[ positionIndex + 2 ] = transformedHeadVertex.z; } positions.needsUpdate = true; positions.updateRange.offset = nodeIndex * this.VerticesPerNode * TrailRenderer.PositionComponentCount; positions.updateRange.count = this.VerticesPerNode * TrailRenderer.PositionComponentCount; }; }(); connectNodes = function() { const returnObj = { "attribute" : null, "offset" : 0, "count" : - 1 }; return function connectNodes(srcNodeIndex, destNodeIndex) { const indices = this.geometry.getIndex(); for (let i = 0; i < this.localHeadGeometry.length - 1; i ++) { const srcVertexIndex = (this.VerticesPerNode * srcNodeIndex) + i; const destVertexIndex = (this.VerticesPerNode * destNodeIndex) + i; const faceIndex = ((srcNodeIndex * this.FacesPerNode) + (i * TrailRenderer.FacesPerQuad )) * TrailRenderer.IndicesPerFace; indices.array[ faceIndex ] = srcVertexIndex; indices.array[ faceIndex + 1 ] = destVertexIndex; indices.array[ faceIndex + 2 ] = srcVertexIndex + 1; indices.array[ faceIndex + 3 ] = destVertexIndex; indices.array[ faceIndex + 4 ] = destVertexIndex + 1; indices.array[ faceIndex + 5 ] = srcVertexIndex + 1; } indices.needsUpdate = true; indices.updateRange.count = - 1; returnObj.attribute = indices; returnObj.offset = srcNodeIndex * this.FacesPerNode * TrailRenderer.IndicesPerFace; returnObj.count = this.FacesPerNode * TrailRenderer.IndicesPerFace; return returnObj; }; }(); disconnectNodes = function() { const returnObj = { "attribute" : null, "offset" : 0, "count" : - 1 }; return function disconnectNodes(srcNodeIndex) { const indices = this.geometry.getIndex(); for (let i = 0; i < this.localHeadGeometry.length - 1; i ++) { const faceIndex = ((srcNodeIndex * this.FacesPerNode) + (i * TrailRenderer.FacesPerQuad)) * TrailRenderer.IndicesPerFace; indices.array[ faceIndex ] = 0; indices.array[ faceIndex + 1 ] = 0; indices.array[ faceIndex + 2 ] = 0; indices.array[ faceIndex + 3 ] = 0; indices.array[ faceIndex + 4 ] = 0; indices.array[ faceIndex + 5 ] = 0; } indices.needsUpdate = true; indices.updateRange.count = - 1; returnObj.attribute = indices; returnObj.offset = srcNodeIndex * this.FacesPerNode * TrailRenderer.IndicesPerFace; returnObj.count = this.FacesPerNode * TrailRenderer.IndicesPerFace; return returnObj; }; }(); deactivate () { if (this.isActive) { this.scene.remove(this.mesh); this.isActive = false; } } activate () { if (! this.isActive) { this.scene.add(this.mesh); this.isActive = true; } } static createMaterial(vertexShader, fragmentShader, customUniforms) { customUniforms = customUniforms || {}; customUniforms.trailLength = { type: "f", value: null }; customUniforms.verticesPerNode = { type: "f", value: null }; customUniforms.minID = { type: "f", value: null }; customUniforms.maxID = { type: "f", value: null }; customUniforms.dragTexture = { type: "f", value: null }; customUniforms.maxTrailLength = { type: "f", value: null }; customUniforms.textureTileFactor = { type: "v2", value: null }; customUniforms.headColor = { type: "v4", value: new THREE.Vector4() }; customUniforms.tailColor = { type: "v4", value: new THREE.Vector4() }; vertexShader = vertexShader || TrailRenderer.Shader.BaseVertexShader; fragmentShader = fragmentShader || TrailRenderer.Shader.BaseFragmentShader; return new THREE.ShaderMaterial({ uniforms: customUniforms, vertexShader: vertexShader, fragmentShader: fragmentShader, transparent: true, alphaTest: 0.5, blending : THREE.CustomBlending, blendSrc : THREE.SrcAlphaFactor, blendDst : THREE.OneMinusSrcAlphaFactor, blendEquation : THREE.AddEquation, depthTest: true, depthWrite: false, side: THREE.DoubleSide }); } static createBaseMaterial(customUniforms) { return TrailRenderer.createMaterial(TrailRenderer.Shader.BaseVertexShader, TrailRenderer.Shader.BaseFragmentShader, customUniforms); } static createTexturedMaterial (customUniforms) { customUniforms = {}; customUniforms.trailTexture = { type: "t", value: null }; return TrailRenderer.createMaterial(TrailRenderer.Shader.TexturedVertexShader, TrailRenderer.Shader.TexturedFragmentShader, customUniforms); } static get MaxHeadVertices () { return 128; } static _LocalOrientationTangent = new THREE.Vector3(1, 0, 0); static get LocalOrientationTangent () { return _LocalOrientationTangent; } static _LocalHeadOrigin = new THREE.Vector3(0, 0, 0); static get LocalHeadOrigin () { return _LocalHeadOrigin; } static get PositionComponentCount () { return 3; } static get UVComponentCount () { return 2; } static get IndicesPerFace () { return 3; } static get FacesPerQuad () { return 2; } static Shader = { get BaseVertexVars() { return [ "attribute float nodeID;", "attribute float nodeVertexID;", "attribute vec3 nodeCenter;", "uniform float minID;", "uniform float maxID;", "uniform float trailLength;", "uniform float maxTrailLength;", "uniform float verticesPerNode;", "uniform vec2 textureTileFactor;", "uniform vec4 headColor;", "uniform vec4 tailColor;", "varying vec4 vColor;", ].join("\n") }, get TexturedVertexVars() { return [ this.BaseVertexVars, "varying vec2 vUV;", "uniform float dragTexture;", ].join("\n"); }, BaseFragmentVars: [ "varying vec4 vColor;", "uniform sampler2D trailTexture;", ].join("\n"), get TexturedFragmentVars() { return [ this.BaseFragmentVars, "varying vec2 vUV;" ].join("\n"); }, get VertexShaderCore() { return [ "float fraction = (maxID - nodeID) / (maxID - minID);", "vColor = (1.0 - fraction) * headColor + fraction * tailColor;", "vec4 realPosition = vec4((1.0 - fraction) * position.xyz + fraction * nodeCenter.xyz, 1.0); ", ].join("\n"); }, get BaseVertexShader() { return [ this.BaseVertexVars, "void main() { ", this.VertexShaderCore, "gl_Position = projectionMatrix * viewMatrix * realPosition;", "}" ].join("\n"); }, get BaseFragmentShader() { return [ this.BaseFragmentVars, "void main() { ", "gl_FragColor = vColor;", "}" ].join("\n"); }, get TexturedVertexShader() { return [ this.TexturedVertexVars, "void main() { ", this.VertexShaderCore, "float s = 0.0;", "float t = 0.0;", "if (dragTexture == 1.0) { ", " s = fraction * textureTileFactor.s; ", " t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;", "} else { ", " s = nodeID / maxTrailLength * textureTileFactor.s;", " t = (nodeVertexID / verticesPerNode) * textureTileFactor.t;", "}", "vUV = vec2(s, t); ", "gl_Position = projectionMatrix * viewMatrix * realPosition;", "}" ].join("\n"); }, get TexturedFragmentShader() { return [ this.TexturedFragmentVars, "void main() { ", "vec4 textureColor = texture2D(trailTexture, vUV);", "gl_FragColor = vColor * textureColor;", "}" ].join("\n"); } }; }