How to render particles in depth order in three.js?
Asked Answered
X

1

5

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
  });
}
Xenon answered 25/6, 2019 at 17:39 Comment(1)
There's no built-in solution for this problem. You can update the position attribute on your BufferGeometry sorting by depth, but depending on the number of points this is not a practical / efficient solution. A different approach would be to use a different blending option, e.g AdditiveBlending. This might have unwanted side effects though.Rollie
D
6

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.

Donniedonnish answered 25/6, 2019 at 20:39 Comment(4)
Complement: If the points/camera are static in the scene, you don't need to sort the vertices every frame. Only when, either, the camera or the points move.Rollie
Thanks for the answer. Multiply blending doesn't work for me, I have a scene with a transparent background so the points are not visible unless there is something else (like the ground map) behind them. Additive blending changes the color of the points where they overlap, so not great either. But I got an acceptable result with alphaTest and will resort to the custom sorting if really needed.Xenon
@ScieCode: yes, actually the whole scene is static so I basically don't even render unless the camera has moved.Xenon
@Marquizzo I did some tests with this example, and is doesn't scae well, because the sorting is done by the CPU and it becomes overloaded already with a few tens of thousands of points. Any idea how I could delegate this to the GPU?Xenon

© 2022 - 2024 — McMap. All rights reserved.