From c63e8be156bb03494890378ae0fdfcfa604c96b9 Mon Sep 17 00:00:00 2001 From: Daniel Hakim Date: Fri, 16 Aug 2019 12:11:21 -0700 Subject: [PATCH] Fast Animation (#724) * Fast animation by splitting tubes into static and dynamic portions such that only the dynamic portions require update each frame. animate.js - now tracks the previous frame. Should allow rewind in the future. animations-controller.js - now treats static and dynamic tube segments separately draw.js - Added method for creation of TubeBufferGeometry object for static tube segments. added method for updating draw range of TubeBufferGeometry sceneplotview3d.js - now adds both static and dynamic tubes to the scene. trajectory.js - New function to grab an interpolated tube segment / last two points of a trajectory for the current frame view.js - split tubes into static and dynamic * Updated tests for split of tubes field * Fix for potential divide by zero * Updated test for trajectory to check dynamic tube section equivalence * Updated comment for jsdoc * Moved variable declaration into for loop. Added utility method to view to retrieve all tubes * Added test for interpolating trajectories with duplicate points * Removed drawTrajectoryLine * Used preferred Material property setting through constructor. Removed the Blah. * Updated material constructor parameters for consistency. Added dispose functions for each tube type. Moving forward, project should decide if it wants to dispose all meshes the same, or each type of mesh differently, difference is whether or not materials and geometries should have potential to be reused. * animations-controller now disposes of tube objects when removing them from the scene. Draw no longer sets matrixAutoUpdate property on tube materials -- matrixAutoUpdate is a property of meshes. It's unclear what was intended by setting it previously * Ran fixjsstyle * Manually fixed lines over 80 characters --- emperor/support_files/js/animate.js | 8 ++ .../support_files/js/animations-controller.js | 58 ++++++++-- emperor/support_files/js/draw.js | 103 ++++++++++++++++-- emperor/support_files/js/sceneplotview3d.js | 5 +- emperor/support_files/js/trajectory.js | 29 +++++ emperor/support_files/js/view.js | 20 +++- .../test_decomposition_view.js | 12 +- tests/javascript_tests/test_trajectory.js | 51 ++++++++- 8 files changed, 257 insertions(+), 29 deletions(-) diff --git a/emperor/support_files/js/animate.js b/emperor/support_files/js/animate.js index fa8d926f..38aad867 100644 --- a/emperor/support_files/js/animate.js +++ b/emperor/support_files/js/animate.js @@ -137,6 +137,13 @@ function(_, trajectory) { * @default -1 */ this.currentFrame = -1; + + /** + * @type {Integer} + * The previous frame served by the director + */ + this.previousFrame = -1; + /** * @type {Array} * Array where each element in the trajectory is a trajectory with the @@ -276,6 +283,7 @@ function(_, trajectory) { */ AnimationDirector.prototype.updateFrame = function() { if (this.animationCycleFinished() === false) { + this.previousFrame = this.currentFrame; this.currentFrame = this.currentFrame + 1; } }; diff --git a/emperor/support_files/js/animations-controller.js b/emperor/support_files/js/animations-controller.js index 311d1c8f..0fb09c1b 100644 --- a/emperor/support_files/js/animations-controller.js +++ b/emperor/support_files/js/animations-controller.js @@ -10,7 +10,11 @@ define([ ], function($, _, DecompositionView, ViewControllers, AnimationDirector, draw, Color, ColorViewController) { var EmperorViewController = ViewControllers.EmperorViewController; - var drawTrajectoryLine = draw.drawTrajectoryLine; + var drawTrajectoryLineStatic = draw.drawTrajectoryLineStatic; + var drawTrajectoryLineDynamic = draw.drawTrajectoryLineDynamic; + var disposeTrajectoryLineStatic = draw.disposeTrajectoryLineStatic; + var disposeTrajectoryLineDynamic = draw.disposeTrajectoryLineDynamic; + var updateStaticTrajectoryDrawRange = draw.updateStaticTrajectoryDrawRange; var ColorEditor = Color.ColorEditor, ColorFormatter = Color.ColorFormatter; /** @@ -440,13 +444,22 @@ define([ this.playing = false; this.director = null; - view.tubes.forEach(function(tube) { - if (tube.parent !== null) { + view.staticTubes.forEach(function(tube) { + if (tube !== null && tube.parent !== null) { + tube.parent.remove(tube); + disposeTrajectoryLineStatic(tube); + } + }); + view.dynamicTubes.forEach(function(tube) { + if (tube !== null && tube.parent !== null) { tube.parent.remove(tube); + disposeTrajectoryLineDynamic(tube); } }); - view.tubes = []; + view.staticTubes = []; + view.dynamicTubes = []; + view.needsUpdate = true; this._updateButtons(); @@ -538,20 +551,43 @@ define([ var radius = view.getGeometryFactor(); radius *= 0.45 * this.getRadius(); - view.tubes.forEach(function(tube) { - if (tube === undefined) { + for (var i = 0; i < this.director.trajectories.length; i++) { + var trajectory = this.director.trajectories[i]; + + //Ensure static tubes are constructed + if (view.staticTubes[i] === null || view.staticTubes[i] === undefined) + { + var color = this._colors[trajectory.metadataCategoryName] || 'red'; + view.staticTubes[i] = drawTrajectoryLineStatic(trajectory, + color, + radius); + } + + //Ensure static tube draw ranges are set to visible segment + updateStaticTrajectoryDrawRange(trajectory, + this.director.currentFrame, + view.staticTubes[i]); + } + + //Remove any old dynamic tubes from the scene + view.dynamicTubes.forEach(function(tube) { + if (tube === undefined || tube === null) { return; } if (tube.parent !== null) { tube.parent.remove(tube); + disposeTrajectoryLineDynamic(tube); } }); - view.tubes = this.director.trajectories.map(function(trajectory) { - color = scope._colors[trajectory.metadataCategoryName] || 'red'; - - var tube = drawTrajectoryLine(trajectory, scope.director.currentFrame, - color, radius); + //Construct new dynamic tubes containing necessary + //interpolated segment for the current frame + view.dynamicTubes = this.director.trajectories.map(function(trajectory) { + var color = scope._colors[trajectory.metadataCategoryName] || 'red'; + var tube = drawTrajectoryLineDynamic(trajectory, + scope.director.currentFrame, + color, + radius); return tube; }); diff --git a/emperor/support_files/js/draw.js b/emperor/support_files/js/draw.js index 05001be1..f5684190 100644 --- a/emperor/support_files/js/draw.js +++ b/emperor/support_files/js/draw.js @@ -1,5 +1,9 @@ /** @module draw */ define(['underscore', 'three', 'jquery'], function(_, THREE, $) { + + var NUM_TUBE_SEGMENTS = 3; + var NUM_TUBE_CROSS_SECTION_POINTS = 10; + // useful for some calculations var ZERO = new THREE.Vector3(); @@ -275,16 +279,22 @@ define(['underscore', 'three', 'jquery'], function(_, THREE, $) { return arrow; } - function drawTrajectoryLine(trajectory, currentFrame, color, radius) { + /** + * Returns a new trajectory line dynamic mesh + */ + function drawTrajectoryLineDynamic(trajectory, currentFrame, color, radius) { // based on the example described in: // https://github.com/mrdoob/three.js/wiki/Drawing-lines var material, points = [], lineGeometry, limit = 0, path; - _trajectory = trajectory.representativeCoordinatesAtIndex(currentFrame); + _trajectory = trajectory.representativeInterpolatedCoordinatesAtIndex( + currentFrame); + if (_trajectory === null || _trajectory.length == 0) + return null; - material = new THREE.MeshPhongMaterial({color: color}); - material.matrixAutoUpdate = true; - material.transparent = false; + material = new THREE.MeshPhongMaterial({ + color: color, + transparent: false}); for (var index = 0; index < _trajectory.length; index++) { points.push(new THREE.Vector3(_trajectory[index].x, @@ -292,15 +302,88 @@ define(['underscore', 'three', 'jquery'], function(_, THREE, $) { } path = new THREE.EmperorTrajectory(points); + // the line will contain the two vertices and the described material // we increase the number of points to have a smoother transition on // edges i. e. where the trajectory changes the direction it is going - lineGeometry = new THREE.TubeGeometry(path, (points.length - 1) * 3, radius, - 10, false); + lineGeometry = new THREE.TubeGeometry(path, + (points.length - 1) * NUM_TUBE_SEGMENTS, + radius, + NUM_TUBE_CROSS_SECTION_POINTS, + false); return new THREE.Mesh(lineGeometry, material); } + /** + * Disposes a trajectory line dynamic mesh + */ + function disposeTrajectoryLineDynamic(mesh) { + mesh.geometry.dispose(); + mesh.material.dispose(); + } + + /** + * Returns a new trajectory line static mesh + */ + function drawTrajectoryLineStatic(trajectory, color, radius) { + var _trajectory = trajectory.coordinates; + + var material = new THREE.MeshPhongMaterial({ + color: color, + transparent: false} + ); + + var allPoints = []; + for (var index = 0; index < _trajectory.length; index++) { + allPoints.push(new THREE.Vector3(_trajectory[index].x, + _trajectory[index].y, _trajectory[index].z)); + } + + var path = new THREE.EmperorTrajectory(allPoints); + + //Tubes are straight segments, but adding vertices along them might change + //lighting effects under certain models and lighting conditions. + var tubeBufferGeom = new THREE.TubeBufferGeometry( + path, + (allPoints.length - 1) * NUM_TUBE_SEGMENTS, + radius, + NUM_TUBE_CROSS_SECTION_POINTS, + false); + + return new THREE.Mesh(tubeBufferGeom, material); + } + + /** + * Disposes a trajectory line static mesh + */ + function disposeTrajectoryLineStatic(mesh) { + mesh.geometry.dispose(); + mesh.material.dispose(); + } + + function updateStaticTrajectoryDrawRange(trajectory, currentFrame, threeMesh) + { + //Reverse engineering the number of points in a THREE tube is not fun, and + //may be implementation/version dependent. + //Number of points drawn per tube segment = + // 2 (triangles) * 3 (points per triangle) * NUM_TUBE_CROSS_SECTION_POINTS + //Number of tube segments per pair of consecutive points = + // NUM_TUBE_SEGMENTS + + var multiplier = 2 * 3 * NUM_TUBE_CROSS_SECTION_POINTS * NUM_TUBE_SEGMENTS; + if (currentFrame < trajectory._intervalValues.length) + { + var intervalValue = trajectory._intervalValues[currentFrame]; + threeMesh.geometry.setDrawRange(0, intervalValue * multiplier); + } + else + { + threeMesh.geometry.setDrawRange(0, + (trajectory.coordinates.length - 1) * multiplier); + } + } + /** * @@ -414,6 +497,10 @@ define(['underscore', 'three', 'jquery'], function(_, THREE, $) { return {'formatSVGLegend': formatSVGLegend, 'makeLine': makeLine, 'makeLabel': makeLabel, 'makeArrow': makeArrow, - 'drawTrajectoryLine': drawTrajectoryLine, + 'drawTrajectoryLineStatic': drawTrajectoryLineStatic, + 'disposeTrajectoryLineStatic': disposeTrajectoryLineStatic, + 'drawTrajectoryLineDynamic': drawTrajectoryLineDynamic, + 'disposeTrajectoryLineDynamic': disposeTrajectoryLineDynamic, + 'updateStaticTrajectoryDrawRange': updateStaticTrajectoryDrawRange, 'makeLineCollection': makeLineCollection}; }); diff --git a/emperor/support_files/js/sceneplotview3d.js b/emperor/support_files/js/sceneplotview3d.js index a77d852f..94cc02d9 100644 --- a/emperor/support_files/js/sceneplotview3d.js +++ b/emperor/support_files/js/sceneplotview3d.js @@ -677,8 +677,9 @@ define([ }); _.each(this.decViews, function(view) { - view.tubes.forEach(function(tube) { - scope.scene.add(tube); + view.getTubes().forEach(function(tube) { + if (tube !== null) + scope.scene.add(tube); }); }); diff --git a/emperor/support_files/js/trajectory.js b/emperor/support_files/js/trajectory.js index 2a678491..66a2ee30 100644 --- a/emperor/support_files/js/trajectory.js +++ b/emperor/support_files/js/trajectory.js @@ -187,6 +187,35 @@ define([ return output; }; + /** + * + * Grab only the interpolated portion of representativeCoordinatesAtIndex. + * + * @param {integer} idx Value for which to determine the required number of + * points. + * + * @return {Array[]} Array containing the representative float x, y, z + * coordinates needed to draw the interpolated portion of a trajectory at the + * given index. + */ + TrajectoryOfSamples.prototype.representativeInterpolatedCoordinatesAtIndex = + function(idx) { + if (idx === 0) + return null; + if (this.interpolatedCoordinates.length - 1 <= idx) + return null; + + lastStaticPoint = this.coordinates[this._intervalValues[idx]]; + interpPoint = this.interpolatedCoordinates[idx]; + if (lastStaticPoint.x === interpPoint.x && + lastStaticPoint.y === interpPoint.y && + lastStaticPoint.z === interpPoint.z) { + return null; //Shouldn't pass on a zero length segment + } + + return [lastStaticPoint, interpPoint]; + }; + /** * * Function to interpolate a certain number of steps between two three diff --git a/emperor/support_files/js/view.js b/emperor/support_files/js/view.js index 4328872c..ea567bb4 100644 --- a/emperor/support_files/js/view.js +++ b/emperor/support_files/js/view.js @@ -63,10 +63,18 @@ function DecompositionView(decomp, asPointCloud) { */ this.backgroundColor = '#000000'; /** - * Tube objects on screen (used for animations) + * Static tubes objects covering an entire trajectory. + * Can use setDrawRange on the underlying geometry to display + * just part of the trajectory. * @type {THREE.Mesh[]} */ - this.tubes = []; + this.staticTubes = []; + /** + * Dynamic tubes covering the final tube segment of a trajectory + * Must be rebuilt each frame by the animations controller + * @type {THREE.Mesh[]} + */ + this.dynamicTubes = []; /** * Array of THREE.Mesh objects on screen (represent samples). * @type {THREE.Mesh[]} @@ -120,6 +128,14 @@ DecompositionView.prototype.getGeometryFactor = function() { this.decomp.dimensionRanges.min[0]) * 0.012; }; +/** + * Retrieve a shallow copy of concatenated static and dynamic tube arrays + * @type {THREE.Mesh[]} + */ +DecompositionView.prototype.getTubes = function() { + return this.staticTubes.concat(this.dynamicTubes); +}; + /** * * Helper method to initialize the base THREE.js objects. diff --git a/tests/javascript_tests/test_decomposition_view.js b/tests/javascript_tests/test_decomposition_view.js index 0fd26253..1b3fe4f2 100644 --- a/tests/javascript_tests/test_decomposition_view.js +++ b/tests/javascript_tests/test_decomposition_view.js @@ -82,7 +82,8 @@ requirejs([ equal(dv.getVisibleCount(), 2, 'visibleCount set correctly'); deepEqual(dv.visibleDimensions, [0, 1, 2], 'visibleDimensions set correctly'); - deepEqual(dv.tubes, [], 'tubes set correctly'); + deepEqual(dv.staticTubes, [], 'tubes set correctly'); + deepEqual(dv.dynamicTubes, [], 'tubes set correctly'); equal(dv.axesColor, '#FFFFFF'); equal(dv.backgroundColor, '#000000'); @@ -138,7 +139,8 @@ requirejs([ equal(dv.getVisibleCount(), 2, 'visibleCount set correctly'); deepEqual(dv.visibleDimensions, [0, 1], 'visibleDimensions set correctly'); - deepEqual(dv.tubes, [], 'tubes set correctly'); + deepEqual(dv.staticTubes, [], 'tubes set correctly'); + deepEqual(dv.dynamicTubes, [], 'tubes set correctly'); equal(dv.axesColor, '#FFFFFF'); equal(dv.backgroundColor, '#000000'); @@ -160,7 +162,8 @@ requirejs([ equal(view.count, 2); equal(view.getVisibleCount(), 2); deepEqual(view.visibleDimensions, [0, 1, 2]); - deepEqual(view.tubes, []); + deepEqual(view.staticTubes, [], 'tubes set correctly'); + deepEqual(view.dynamicTubes, [], 'tubes set correctly'); equal(view.axesColor, '#FFFFFF'); equal(view.backgroundColor, '#000000'); deepEqual(view.axesOrientation, [1, 1, 1]); @@ -199,7 +202,8 @@ requirejs([ equal(view.count, 2); equal(view.getVisibleCount(), 2); deepEqual(view.visibleDimensions, [0, 1, 2]); - deepEqual(view.tubes, []); + deepEqual(view.staticTubes, [], 'tubes set correctly'); + deepEqual(view.dynamicTubes, [], 'tubes set correctly'); equal(view.axesColor, '#FFFFFF'); equal(view.backgroundColor, '#000000'); deepEqual(view.axesOrientation, [1, 1, 1]); diff --git a/tests/javascript_tests/test_trajectory.js b/tests/javascript_tests/test_trajectory.js index 77f72adb..c09614a2 100644 --- a/tests/javascript_tests/test_trajectory.js +++ b/tests/javascript_tests/test_trajectory.js @@ -361,16 +361,31 @@ requirejs(['underscore', 'trajectory'], function(_, trajectory) { {'x': 6.75, 'y': 6.75, 'z': 6.75}, {'x': 8, 'y': 8, 'z': 8}]; - deepEqual(trajectory.representativeCoordinatesAtIndex(3), + var fullCoordinates3 = trajectory.representativeCoordinatesAtIndex(3); + deepEqual(fullCoordinates3, [{'x': 0, 'y': 0, 'z': 0}, {'x': 0.75, 'y': 0.75, 'z': 0.75}], 'Coordinates are retrieved correctly at index 3'); - deepEqual(trajectory.representativeCoordinatesAtIndex(11), + + var fullCoordinates11 = trajectory.representativeCoordinatesAtIndex(11); + deepEqual(fullCoordinates11, [{'x': 0, 'y': 0, 'z': 0}, {'x': 1, 'y': 1, 'z': 1}, {'x': -9, 'y': -9, 'z': -9}, {'x': 3, 'y': 3, 'z': 3}, {'x': 4.25, 'y': 4.25, 'z': 4.25}], 'Coordinates are retrieved correctly at index 11'); + var dynamicCoordinates3 = + trajectory.representativeInterpolatedCoordinatesAtIndex(3); + var dynamicCoordinates11 = + trajectory.representativeInterpolatedCoordinatesAtIndex(11); + + deepEqual(fullCoordinates3.slice(-2), + dynamicCoordinates3, + 'Dynamic coordinates match on frame 3'); + deepEqual(fullCoordinates11.slice(-2), + dynamicCoordinates11, + 'Dynamic coordinates match on frame 11'); + }); /** @@ -525,5 +540,37 @@ requirejs(['underscore', 'trajectory'], function(_, trajectory) { 'category'); }); + /** + * + * Test trajectories with duplicate points. + * + */ + test('Test Duplicate Points In Trajectories', function() { + var result; + trajectory = new TrajectoryOfSamples(['A', 'B', 'C', 'D'], + 'Nonsense', + [1, 2, 3, 4], + [{'x': 0, 'y': 0, 'z': 0}, + {'x': 10, 'y': 10, 'z': 10}, + {'x': 10, 'y': 10, 'z': 10}, + {'x': 0, 'y': 0, 'z': 0}], + 2, + 5); + + for (var i = 0; i < 20; i++) { + var interpTubeCoords = + trajectory.representativeInterpolatedCoordinatesAtIndex(i); + if (interpTubeCoords !== null) { + var dx = interpTubeCoords[1].x - interpTubeCoords[0].x; + var dy = interpTubeCoords[1].y - interpTubeCoords[0].y; + var dz = interpTubeCoords[1].z - interpTubeCoords[0].z; + var lenSq = dx * dx + dy * dy + dz * dz; + notEqual(lenSq, 0, + 'Interpolated tube should never be built between' + + 'consecutive duplicate points in a trajectory'); + } + } + }); + }); });