Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to render particles in depth order in three.js?

Dislaimer: I am relatively new to three.js, WegGL and 3D graphics in general.

I am using three.js to display points from GPS tracks in 3D. The datasets we have to be able to visualize can be quite big(hundreds of thousands of points) so performance is quite important.

I use a Points object, that I populate with a BufferGeometry containing all the points. The points are added in the order of the track, so by chronological order.

Then, we use a PointsMaterial with a 2D texture (sprite) that represents the point as a circle, with the areas outside the circle being transparent. The 2D texture is drawn dynamically to a canvas because the color is dynamic.

The problem is, if we look at the points in the direction of the track, i.e. with the points further from the camera being the ones rendered after the ones closer, there are artifacts where the points overlap, with the transparent part of the closer points being drawn over the farther points:

points in 3d

When we look at the track in the other direction, i.e. with the points rendered from back to front, the problem disappears:

enter image description here

I tried the following two options to fix that problem:

  • Using alphaTest with a value between 0 and 1, which kind of works
    • but our points can also be partially transparent (it's a customer requirement) so the risk is that the alphaTest will clip parts of the points that should actually be rendered
    • and it creates a jagged edge where the points overlap which doesn't look nice

points in 3d

  • Using depthWrite: false for the points material, it renders nicely but then recent points are always drawn over older ones no matter the orientation of the camera and it looks weird and is wrong

points in 3d

What would a solution be to actually render the points in the depth order starting with the farthest and ending with the closest?

Here are the relevant parts of the code.

Building of the geometry. The timeline3d object contains all the points and comes from a XHR request:

  const particlesGeometry = new BufferGeometry();
  const vertices = [];

  for (let i = 0; i < timeline3d.points.length; i++) {
    const coordinates = timeline3d.points[i].sceneCoordinates;
    vertices.push(coordinates[0], coordinates[1], coordinates[2]);
  }

  particlesGeometry.addAttribute('position', new Float32BufferAttribute(vertices, 3));
  return new Points(particlesGeometry, buildTimelinePointsMaterial(timeline3d));

The material:

function buildTimelinePointsMaterial (timeline) {
  const pointTexture = new CanvasTexture(drawPointSprite(POINT_SPRITE_RADIUS, timeline.color));

  return new PointsMaterial({
    size: POINT_SIZE,
    sizeAttenuation: true,
    map: pointTexture,
    transparent: true,
    alphaTest: 0.4
  });
}
like image 980
Pierre Henry Avatar asked Jun 25 '19 17:06

Pierre Henry


1 Answers

Workaround:

This is a limitation of WebGL when rendering points. Just like @ScieCode mentioned above, I've personally been working around that issue by changing the material's blending mode. With it, each point blends over the other like a transparent Photoshop layer, and removes the overlapping effect. Typically if you have a light background, you'd want to use THREE.MultiplyBlending, and if you have dark backgrounds, you'd use THREE.AdditiveBlending, but it's all a matter of taste.

Solution:

There is a more complex solution that's implemented in this example that sorts all vertices by depth once per frame. If you look at its source code, you'll see that sortPoints() is called in the render loop, and it multiplies the camera matrices with the geometry's world matrix to look at each vertex depth and sort in order.

like image 172
Marquizzo Avatar answered Oct 07 '22 11:10

Marquizzo