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:
When we look at the track in the other direction, i.e. with the points rendered from back to front, the problem disappears:
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
- 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
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
});
}
position
attribute on yourBufferGeometry
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.gAdditiveBlending
. This might have unwanted side effects though. – Rollie