How to display a million of interactive line strings in Three.js
Asked Answered
B

2

6

I would like to display a circuit using react-three-fiber (or with the Drei library).

  • A circuit is composed of segments and turns.
  • A segment is defined thanks to two points.
  • A turn is defined thanks to a list of points.

Segments and turns

I'd like my user to be able to construct the circuit by himself. It means that he will be able to modify (drag/drop/delete and so on...) the turns and the segments. It will result with around 10,000 of segments and turns and so in more than 100,000 points.

How would you proceed? (see the below for more details about my tests)


Considered Solutions and difficulties

Segments

  • r3f ((react-three-fiber) instanceMesh component (with a rectangle): I would need to store a map of the instanceId linked to my turn/segments ids, it does not seem impossible. It seems to be a good solution and is still performant with more than 100k elements. I implemented a quick example here to test this logic.

  • drei Instance component: With more than 1k elements, it starts to be laggy. You can play with an example here (adapted from an r3f example)

Turns

  • r3f instanceMesh component: The turn can be seen as a list of segments. With this solution, if we zoom too much, we can clearly see each segments. It is maybe not ideal, but it seems to be working. Another issue is that a turn can be composed of 100+points. If I have 1k turns, it will draw 100k segments, which starts being a lot

  • line: We could create only one geometry with all the turns inside. However, currently all my lines are linked (like if I was drawing them with a pen in one shot). Moreover, I do not think it is easily possible to manipulate a specific turn with this solution.

  • Line v2: We could also create as much geometries as turns. Each turn would be an entity instead of lots of segments. However, having 1K+ geometries is not recommended. With this solution, the performances were not good at all.

  • Limiting the number of objects: Another solution I would like to try is to draw all my turns in one geometry. In this case I would have no interaction. When zoomed enough, with only a maximum of 50 visible, I add real line with all the interactions. It is a bit tricky, but it also seems to be a good compromise.

The ideal would be to have an instance of the turns (such as the segments). However, each turn seems to be unique and I do not see how it would be possible to create only one geometry in an instance to display all the possible turns.


What would be the best way to implement this logic?


I tried this method by drawing only the closest turns. It stays quite fluid. I will try to go further with this solution: Here is a GIF example of the implementation:

Enter image description here

Bughouse answered 19/12, 2022 at 13:35 Comment(1)
Have you tried LineSegments and putting all the lines/turns in one big BufferGeometry? For the interactivity, it seems the raycaster does interact with LineSegments, and with the face index you could find the individual line segment.Schmeltzer
S
3

I made a PR for a LineSegments component, and the component is now available in Drei 9.53.0. You can use it like <Line segments />.

This component can render lines (with gaps) and have a line width (other than 1px) in one call. That would probably be the most performant way of drawing it.

The LineSegments is basically Drei's Line component, but using Three.js LineSegments2 instead of Line2. LineSegments2 or Line2 is what enables us to draw lines with a line width less than one pixel.

For interaction, you can still use the raycaster. The pointer events of R3F also exposed a faceIndex which you can use to determine which line.

If you want to draw various width, group your lines based on width, and create a LineSegments2 per width.

If the frame rate is too low, combine it with LOD and frustum culling.

Finally, your frame rate can also drop if the number of rerenders is high. Do add some console logs, for example, to see if you are rerendering a lot on the Canvas, but also outside of the Canvas! I've noticed that when I also have a high number of CPU cycles—not related to Three.js—rerender, calculation, etc., the frame rate also drops.

Schmeltzer answered 16/1, 2023 at 15:54 Comment(8)
Thank you very much ! I think we could combine this solution with the one from @Schulz to have the full advantage of each solutions!Bughouse
would you have an exemple ? Is there another solution rather than duplicating the coordinates of the vertices (such as indexes for example? )Bughouse
@Bughouse I don't understand your question... can you rephrase it or in more detail?Schmeltzer
I am use <Line segment />, with the following coordinates: [[x1,y1],[x2,y2],[x3,y3],...], it will draw a dashed line. The solution is to modify the coordinates like the following: [[x1,y1],[x2,y2],[x2,y2],[x3,y3]...]. If I use R3F, I can use indexes as an alternative solution. Would you have another solution using Drei?Bughouse
First of all, <Line segment /> is also Drei. The component (without segments) is one continuous line consisting of multiple points that draws in one draw call, which is why is performs so good. A segmented line also consists of multiple points, but with gaps. It seems to me you don't want a dashed line (gaps), so you should drop the segments (it will still draw it in one draw call). A different way of drawing, would be several lines - all just with two points, a begin and end - where each end would match the following start and drawing one continuous line. This would be less performantSchmeltzer
Yes, the issue with Line without segment is that I can not use ìndex (like in R3F) to remove the link between each turns. What I said is that even with segment, I can have the desired output by tweeking the coordinates as mentioned above. According to your answer, I do not think there is another solution yetBughouse
Ah, I see what you mean! I don't think the Line components bothers with indices, but the underlying LineSegmentsGeometry does support it. I think you could copy the code, and play with it a bitSchmeltzer
Cheers for your reply ! Absolutely, it could also be a PR for the Drei library !Bughouse
S
4

Using LOD + spatial indexing for massive data plots

I'd suggest using the LOD (Level Of Detail) method. Three-dimensional vector positions over 200k~300k+ can easily cause lag for mid-low level computers. I've tested in one of my previous experiments. This is a rough idea, but I think it'll definitely clear some performance issues.

  1. Assign each vector positions per its coordinate area: make a low-res map and put coordinates in each area

    X1Y1 X2Y1
    |---|---|
    |xxx|xx |
    |   |  x|
    |---|---|
    |   |  x|
    |   |   |
    |---|---|
    X1Y2 Y2Y2
    
  2. Pick a limited number of vector points (e.g., first, middle and last points) in each coordinate area and define them as low-resolution meshes.

  3. Define them as LOD.

  4. When interacting, make the interaction only work with the points under current low-resolution coordinate. This is basically a simple spatial indexing method.

    # When the mouse is in the X1Y1 coordinate, just ignore all other coordinates.
    # Currently activated location: X1Y1
    X1Y1 X2Y1
    |---|---|
    |xxx|###|
    |   |###|
    |---|---|
    |###|###|
    |###|###|
    |---|---|
    X1Y2 Y2Y2
    
Schulz answered 16/1, 2023 at 4:1 Comment(1)
Thanks for your schema, I will check that in details !Bughouse
S
3

I made a PR for a LineSegments component, and the component is now available in Drei 9.53.0. You can use it like <Line segments />.

This component can render lines (with gaps) and have a line width (other than 1px) in one call. That would probably be the most performant way of drawing it.

The LineSegments is basically Drei's Line component, but using Three.js LineSegments2 instead of Line2. LineSegments2 or Line2 is what enables us to draw lines with a line width less than one pixel.

For interaction, you can still use the raycaster. The pointer events of R3F also exposed a faceIndex which you can use to determine which line.

If you want to draw various width, group your lines based on width, and create a LineSegments2 per width.

If the frame rate is too low, combine it with LOD and frustum culling.

Finally, your frame rate can also drop if the number of rerenders is high. Do add some console logs, for example, to see if you are rerendering a lot on the Canvas, but also outside of the Canvas! I've noticed that when I also have a high number of CPU cycles—not related to Three.js—rerender, calculation, etc., the frame rate also drops.

Schmeltzer answered 16/1, 2023 at 15:54 Comment(8)
Thank you very much ! I think we could combine this solution with the one from @Schulz to have the full advantage of each solutions!Bughouse
would you have an exemple ? Is there another solution rather than duplicating the coordinates of the vertices (such as indexes for example? )Bughouse
@Bughouse I don't understand your question... can you rephrase it or in more detail?Schmeltzer
I am use <Line segment />, with the following coordinates: [[x1,y1],[x2,y2],[x3,y3],...], it will draw a dashed line. The solution is to modify the coordinates like the following: [[x1,y1],[x2,y2],[x2,y2],[x3,y3]...]. If I use R3F, I can use indexes as an alternative solution. Would you have another solution using Drei?Bughouse
First of all, <Line segment /> is also Drei. The component (without segments) is one continuous line consisting of multiple points that draws in one draw call, which is why is performs so good. A segmented line also consists of multiple points, but with gaps. It seems to me you don't want a dashed line (gaps), so you should drop the segments (it will still draw it in one draw call). A different way of drawing, would be several lines - all just with two points, a begin and end - where each end would match the following start and drawing one continuous line. This would be less performantSchmeltzer
Yes, the issue with Line without segment is that I can not use ìndex (like in R3F) to remove the link between each turns. What I said is that even with segment, I can have the desired output by tweeking the coordinates as mentioned above. According to your answer, I do not think there is another solution yetBughouse
Ah, I see what you mean! I don't think the Line components bothers with indices, but the underlying LineSegmentsGeometry does support it. I think you could copy the code, and play with it a bitSchmeltzer
Cheers for your reply ! Absolutely, it could also be a PR for the Drei library !Bughouse

© 2022 - 2025 — McMap. All rights reserved.