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
================================================
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");
}
};
}