MapBox GL JS marker/icon animation slow performanc

2019-07-29 03:50发布

问题:

I'm using mapbox-gl-js to animate many image from one coordinates to another on the map. It become slow frame rate when I try to add up to 15 image and above. Doing performance profile on chrome give me the hint to method Actor.receive, which cost the most computational time.

/**
 * Util.js
 */
 var linesDataSource = {
  "type": "FeatureCollection",
  "features": [{
    "type": "Feature",
    "properties": {},
    "geometry": {
      "type": "LineString",
      "coordinates": [
        [
          151.15684390068054, -33.89568424317427
        ],
        [
          151.15808844566345, -33.89606717952166
        ],
        [
          151.15779876708984, -33.89680633086413
        ],
        [
          151.15740180015564, -33.897794824453406
        ],
        [
          151.1582601070404, -33.8980085512904
        ],
        [
          151.1609423160553, -33.89863191817193
        ],
        [
          151.16222977638245, -33.89621857248702
        ],
        [
          151.16639256477356, -33.89771467675142
        ],
        [
          151.1694610118866, -33.898916884371395
        ],
        [
          151.17089867591858, -33.896298721595166
        ],
        [
          151.17217540740964, -33.899014841282515
        ],
        [
          151.1714780330658, -33.899192944468965
        ],
        [
          151.17132782936093, -33.89878330658397
        ],
        [
          151.1719822883606, -33.8985784869035
        ],
        [
          151.17339849472046, -33.89839147720036
        ],
        [
          151.17376327514648, -33.89825789858986
        ],
        [
          151.17332339286804, -33.897269410368615
        ],
        [
          151.1732053756714, -33.89697553328233
        ],
        [
          151.17341995239258, -33.89662822269281
        ],
        [
          151.17295861244202, -33.896263099778615
        ],
        [
          151.17225050926208, -33.89589797530112
        ],
        [
          151.17136001586914, -33.89561299901295
        ],
        [
          151.17184281349182, -33.894758064434605
        ],
        [
          151.17200374603271, -33.89455323508587
        ],
        [
          151.17257237434387, -33.89148073582115
        ],
        [
          151.17042660713196, -33.89132042847356
        ],
        [
          151.17168188095093, -33.88838140703873
        ],
        [
          151.1716067790985, -33.887606557247125
        ],
        [
          151.16321682929993, -33.888274531623864
        ],
        [
          151.16029858589172, -33.88777577791726
        ],
        [
          151.1591076850891, -33.88790937294604
        ],
        [
          151.15857124328613, -33.8892809364742
        ],
        [
          151.1584746837616, -33.89006467716016
        ],
        [
          151.15894675254822, -33.89009139546571
        ],
        [
          151.15893602371216, -33.889806399775104
        ]
      ]
    }
  }]
}


var PI = Math.PI;
var TWO_PI = Math.PI * 2;

function rotation(start, end) {
  var dx = end[0] - start[0];
  var dy = end[1] - start[1];
  return -Math.atan2(dy, dx) * (180 / PI);
};

function lerp(v0, v1, t) {
  return v0 * (1 - t) + v1 * t
}

function interpolateAngle(fromAngle, toAngle, t) {
  fromAngle = fromAngle * (PI / 180);
  toAngle = toAngle * (PI / 180);

  fromAngle = (fromAngle + TWO_PI) % TWO_PI;
  toAngle = (toAngle + TWO_PI) % TWO_PI;

  var diff = Math.abs(fromAngle - toAngle);
  if (diff < PI) {
    return lerp(fromAngle, toAngle, t) * (180 / PI);
  } else {
    if (fromAngle > toAngle) {
      fromAngle = fromAngle - TWO_PI;
      return lerp(fromAngle, toAngle, t) * (180 / PI);
    } else if (toAngle > fromAngle) {
      toAngle = toAngle - TWO_PI;
      return lerp(fromAngle, toAngle, t) * (180 / PI);
    }
  }
}





/**
 * Car.js
 */
function Car(name, map, path) {
  this.name = name;
  this.map = map;
  this.path = path;
  this.speed = 90; // 30 km/h
  this.accumulatedDistance = 0;
  this.previousPos = this.path.features[0].geometry.coordinates[0];
  this.previousAngle = 0;

  this.animate = function(frameInfo) {

    this.accumulatedDistance += ((frameInfo.deltaTime / 3600) * this.speed);
    var point = turf.along(this.path.features[0], this.accumulatedDistance, 'kilometers');

    this.map.getSource(this.name).setData(point);

    var newAngle = rotation(this.previousPos, point.geometry.coordinates);
    var rotate = interpolateAngle(this.previousAngle, newAngle, 0.1);

    this.map.setLayoutProperty(this.name, 'icon-rotate', rotate);

    this.previousAngle = rotate;
    this.previousPos = point.geometry.coordinates;
  };

  this.init = function() {
    this.map.addSource(this.name, {
      "type": "geojson",
      "data": {
        "type": "FeatureCollection",
        "features": [{
          "type": "Feature",
          "geometry": {
            "type": "Point",
            "coordinates": this.previousPos
          }
        }]
      }
    });

    this.map.addLayer({
      "id": this.name,
      "type": "symbol",
      "source": this.name,
      "layout": {
        "icon-image": "car",
        "icon-size": 1,
        "icon-rotate": 0,
        "icon-rotation-alignment": "map"
      }
    });
  };
}





/**
 * MapBoxTest.js
 */
var destination = {};
var cars = [];
var style = 'mapbox://styles/mapbox/streets-v9'; //'/TestEmptyProject/mapbox-gl-styles-master/styles/basic-v8.json';
//'http://localhost:8080/styles/osm-bright.json';  // 'http://localhost:8080/styles/fiord-color-gl.json'


mapboxgl.accessToken = 'pk.eyJ1IjoiZW1wZXJvcjE0MTIiLCJhIjoiY2ozYTYxdXFlMDM3dzJyczRsa2M5ZjE3aCJ9.9zQGtkSsjOw6npohN6ba3w';
var map = new mapboxgl.Map({
  container: 'map',
  style: style,
  center: [132.133333, -23.116667],
  zoom: 3
});

// Used to increment the value of the point measurement against the linesData.
var counter = 0;
var linesData = {};

function addCar() {
  var car = new Car("Car_" + counter, map, linesData);
  car.init();
  cars.push(car);
  ++counter;
}

var previousTimeStamp = 0;
// Add a source and layer displaying a point which will be animated in a circle.
function animate(timeStamp) {
  if (timeStamp <= previousTimeStamp) {
    console.log("Wrong timeStamp, now: " + timeStamp + "\t previous: " + previousTimeStamp);
    return;
  }

  var i;
  var frameInfo = {
    "timeStamp": timeStamp,
    "previousTimeStamp": previousTimeStamp,
    "deltaTime": (timeStamp - previousTimeStamp) / 1000
  };
  previousTimeStamp = timeStamp;

  for (i = 0; i < cars.length; ++i) {
    var car = cars[i];
    car.animate(frameInfo);
  }
  requestAnimationFrame(animate);
}

map.on('load', function() {

  console.log("map load");

  map.loadImage('https://maxcdn.icons8.com/office/PNG/40/Transport/railroad_car-40.png', function(error, image) {
    if (error) throw error;
    map.addImage('car', image);
  });

  //fetch('./lines.geojson', {
  //method: 'get'
//}).then(function(response) {
 // return response.json();
//}).then(function(data) {

  linesData = linesDataSource;
  var coordinates = linesData.features[0].geometry.coordinates;

  var bounds = coordinates.reduce(function(bounds, coord) {
    return bounds.extend(coord);
  }, new mapboxgl.LngLatBounds(coordinates[0], coordinates[0]));

  map.fitBounds(bounds, {
    padding: 20,
    duration: 2000
  });

  map.addSource('lines', {
    "type": "geojson",
    "data": linesData
  });

  map.addLayer({
    "id": "route",
    "source": "lines",
    "type": "line",
    "paint": {
      "line-width": 2,
      "line-color": "#007cbf"
    }
  });
  // }).catch(function(err) {
  //console.log("error: " + err);
//}); 

document.getElementById('addCar').addEventListener('click', function() {
addCar();
});


});


requestAnimationFrame(animate);
 body {
   margin: 0;
   padding: 0;
 }
 
 #map {
   position: absolute;
   top: 0;
   bottom: 0;
   width: 100%;
 }
 
 .overlay {
   position: absolute;
   top: 10px;
   left: 10px;
 }
 
 .overlay button {
   font: 600 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
   background-color: #3386c0;
   color: #fff;
   display: inline-block;
   margin: 0;
   padding: 10px 20px;
   border: none;
   cursor: pointer;
   border-radius: 3px;
 }
 
 .overlay button:hover {
   background-color: #4ea0da;
 }
<script src="https://master.fieldtec.com/vendor/custom-component-modules/car_tracking_animation/scripts/turf.min.js"></script>
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.css" rel="stylesheet"/>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.js"></script>

<body>
  <div id='map'></div>
  <div class='overlay'>
    <button id='addCar'>Add Car</button>
  </div>
</body>

回答1:

Do all of your animation with one source. Update the source each frame with setData(). Render one layer from the source using data-driven styles. This will use your GPU to render animations. This will improve performance considerably by reducing the number of layers and setData() calls.

Example code of animating in GL JS with one layer and one source: https://bl.ocks.org/ryanbaumann/9b9b52e09ff86d1ce8346fb76b681427



回答2:

To animate hundreds of icons, it is more efficient to do the animation in the shaders rather than in the javascript. This allows you to leverage the power of the GPU. Here is a demo : http://misterfresh.github.io/mapbox-animation/