Decreasing display time for a large amount of surfaces

Dear developers and users,

I have information of a shape in slices that I need to display. The amount of slices can go up to 10k. The way I am doing it now is to represent each slice as a face and build a compound shape with these slices. The compound is added to an AIS_Shape and added to the Context. 

The 'Context.Display(ais_shape, False)' takes some time (approximately 5 min).

Is there a way to do it faster? I am only interested in the visualisation, no algorithms need to happen after the display.

I know this question is asked already a couple of times throughout these past years, but I never found a straightforward answer to it...

I thank you!

Kind Regards,
Thieme

Thieme Vandeput's picture

Also at least 20GB is used when trying to display these shapes... 

Kirill Gavrilov's picture

That's quite impressive number! It would be helpful to elaborate in more details what exactly is displayed on the screen (what slices represent).

You may also activate Graphic3d_RenderingParams::ToShowStats and Graphic3d_RenderingParams::CollectedStats to see some general statistics on the screen.

Thieme Vandeput's picture

I can't figure the collectedstats out (I'm not using draw I realise now). I wrote a test script that does the following:

- #Polygons = 3,000
- #Points per polygon: 600

- Time to display the polygons: 450.842222s
- Memory needed while displaying: 6 GB

Code:

import time
import numpy as np

from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace
from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Builder
from OCC.Core.gp import gp_Pnt
from OCC.Core.AIS import AIS_Shape

totalHeight = 30
layerThickness = 0.01
polygonPointCount = 600

shape = TopoDS_Compound()
compoundBuilder = TopoDS_Builder()
compoundBuilder.MakeCompound(shape)

start = time.time()

for height in np.arange(0, totalHeight, layerThickness):
    polygonBuilder = BRepBuilderAPI_MakePolygon()
    polygonBuilder.Add(gp_Pnt(0, 0, float(height)))

    for polygonPointIndex in range(polygonPointCount):
        xPoint = polygonPointIndex/polygonPointCount
        yPoint = 1-xPoint**2
        polygonBuilder.Add(gp_Pnt(xPoint, yPoint, float(height)))

    polygonBuilder.Add(gp_Pnt(0, 0, float(height)))

    faceBuilder = BRepBuilderAPI_MakeFace(polygonBuilder.Wire())

    compoundBuilder.Add(shape, faceBuilder.Face())

stop = time.time()
print("Total time to create shape: %fs" % (stop-start))
print("Shape count = %d" % (totalHeight/layerThickness))

ais_Shape = AIS_Shape(shape)

if __name__ == "__main__":
    from OCC.Display.SimpleGui import init_display
    display, start_display, add_menu, add_function_to_menu = init_display()

    start = time.time()
    display.Context.Display(ais_Shape, True)
    stop = time.time()
    print("Total time to display shape: %fs" % (stop-start))

    start_display()

This script draws a stack of polygons in the shape of one quarter of a circle. Note that in real life the polygons are not the same. I am using PythonOCC. 
When decreasing the amount of points inside the polygon, the performance increases rapidly. When only displaying the wires instead of the faces the performance is way way better (10s needed to display).

Is there a way to display the polygons in another way, or is there a reason why this takes so much resources?

Kirill Gavrilov's picture

I didn't know PythonOCC wraps OCCT 3D Viewer - interesting to know.

Why are using BRepBuilderAPI_MakePolygon? This tool generates a valid B-Rep structure, which is, however, extremely inefficient from memory/performance point of view.
If you create this TopoDS_Shape only for displaying on the screen, it would be much better creating a single Poly_Triangulation from many polygons and displaying TopoDS_Face from this triangulation (see BRep_Builder::MakeFace() taking triangulation on input).

Thieme Vandeput's picture

I'm sorry for my late respons, I have other projects as well...

I've tested out your suggestion, now the display is almost immediately, BUT the triangulation of the faces take a lot of time (BRepMesh_IncrementalMesh is used). For 1700 polygons of each 600 points, meshing takes 3 minutes. The problem is actually shifted now to the meshing...

I'm using BRepBuilderAPI_MakePolygon as an example to create faces, but the input are faces that can also contain curved edges. What is strange for me are the following things:

- Displaying 10k polygons with 60 points each is almost instantaneously, displaying 10k polygons with 600 points but the same surface as the ones with 60 points takes 5 minutes and a lot of GB's to display.

- Displaying the wires of 10k polygons with 600 points is almost instantaneously

- The displaying time rises exponentially with the increasing amount of points

Does there exists a better solution (faster meshing/less resource requiring display) for this problem? Only the display is of interest, so no further calculations need to be done on the faces...

Thanks in advance,
Thieme 

Kirill Gavrilov's picture

I don't know what your surface looks alike, but I guess that exponential dependency is something you would expect for meshing an arbitrary UV parametric space into 3D via incremental (iterative) approach of BRepMesh_IncrementalMesh. You may further play with meshing parameters IMeshTools_Parameters - linear and angular deflection are configured first of all, but there are also multi-threaded flag and other advanced paramters. You may also try IMeshTools_MeshAlgoType_Delabella as alternative to current default (added by OCCT 7.5.0) to see if it makes any difference in performance in your particular case.

Of course, there are also alternative meshers like ExpressMesh to consider, if incremental mesh looks suboptimal. Or consider preprocessing the surface if there are some doubts that it is defined inefficiently.

Thieme Vandeput's picture

First of all, I'm very grateful for your responses. The incrementalmesh with additional parameters is faster. However, the faces can be very complex which makes meshing time still way too long (several minutes).
Do you have an idea why displaying a Poly_Triangulation Mesh is so much faster than displaying exactly the same thing but in BRep? Is there perhaps a rendering parameter that has such a big influence on performance? The quality of the display is less of importance. I would hate having to switch this late in my project to another 3D library...

Kirill Gavrilov's picture

And if you believe that meshing algorithm takes too much on your shapes, then it might be helpful registering an issue on Bugtracker with shared file samples, so that developers will have more samples for improving algorithm. Apart from general optimizations, of course, there is also a chance that your input also triggers some bug in OCCT algorithms.

And there are also Support Services to consider.

Kirill Gavrilov's picture

Do you have an idea why displaying a Poly_Triangulation Mesh is so much faster than displaying exactly the same thing but in BRep?

I don't quite understand the question. Graphic cards render only triangles, not B-Rep, so that displaying B-Rep would first require meshing. As soon as meshing is already done, displaying TopoDS_Shape via AIS_Shape should be as fast as displaying just triangulation (with Prs3d_Drawer::IsAutoTriangulation() disabled to avoid implicit remeshing of the same shape twice). This is not something specific to OCCT - any library working with B-Rep and B-Spline surfaces should perform meshing to display geometry on the screen.

It is important to understand, that parameters of BRepMesh_IncrementalMesh algorithm control not only a casual look of a shape on a screen in terms of "nice"/"rough". Their real purpose is to define a mesh following original surface close enough to be used in other algorithms with a trusted precision - visualization is only one of use cases. That's why linear and angular deflection parameters are main inputs of meshing algorithm - in this context an overtessellated geometry is better than undertessellated.

What make BRepMesh_IncrementalMesh slower?

  • Single-threaded execution.
    BRepMesh_IncrementalMesh supports multi-threaded execution, but it should be enabled explicitly. Parallelization is done on Faces level, so that to benefit meshing algorithm should be called on as much shapes as possible - e.g. by putting everything into a temporary TopoDS_Compound and passing it to meshing algorithm.
  • Overtessellation parameters.
    Passing too small deflection parameters will produce an overtessellated result taking a lot of time. It is desirable putting values actually meaningful for specific domain (e.g. based on application knowledge that shape is for a large vessel, a robot, or a microcontroller - each having very different precision criteria). It is important to understand the difference between a real (absolute) and relative (relative to bounding box) deflection parameter definition and use them appropriately.
  • Overcomplicated geometry/topology definition.
    Exactly the same geometry may be defined in numerous ways in B-Rep. Spherical surface may be defined by B-Spline, and B-Spline might be defined by redundant number of knots; continuous surface might be defined via single TopoDS_Face or may be split into large number of connected small Faces.
  • Broken or bad geometry.
    Invalid geometry may cause unexpected slowdown of the algorithm on trying to cope with unsolvable geometry issues.

If you are curious what might make  BRepMesh_IncrementalMesh faster or what could be an alternative, then it is better starting from understanding main meshing stages:

  • Tessellation of geometry boundaries (TopoDS_Edge).
    • This step is more involving (linear/angular deflection parameters are also involved), but relativelly fast on normal geometry.
  • Initial tessellation of TopoDS_Face in UV parametric space as 2D polygon with boundaries and holes.
    • There are different 2D polygon meshing algorithms varying in performance and robustness. OCCT has built-in algorithm called IMeshTools_MeshAlgoType_Watson and also provides an alternative IMeshTools_MeshAlgoType_Delabella.
  • Splitting initial triangulation into smaller triangles to meet surface/triangle deflections criteria (linear/angular deviation parameters) as well as a set of additional parameters.
    • This is an iterative process that might take a while due to numerous expensive computations (computing new 3D points on surface, classifying them to boundaries, estimating deviation).

Other meshing algorithms like ExpressMesh or Netgen use very different approaches to 2D polygon tessellation and sub-division, although they do this to generate a mesh structure more suitable to various solvers / simulation algorithms rather than for an algorithm speed up.

You may imagine what would happen if we stop meshing algorithm on earlier stages:

  • Ignoring Face boundaries.
    • Consider displaying a surface with natural boundaries or with trivial boundaries (e.g. TopoDS_Edge might be ignored).
      In this case 2D polygon tessellation can be defined as a trivial subdivision of rectangle (in UV parametric space) into desired number of triangles.
      This might work pretty well for displaying analytical surfaces with predictable appearance like a full sphere or cylinder.
      In fact, Draw Harness has a small command "tessellate" implementing this feature (src/MeshTest/MeshTest.cxx), basically intended for testing purposes, but implementation itself might have also practical cases.
  • Ignoring deflection parameters.
    • It could be technically possible to stop TopoDS_Face tessellation on 2D polygon step, and edges tessellation might be done using simple sub-division parameters or faster algorithms like GCPnts_QuasiUniformAbscissa/GCPnts_QuasiUniformDeflection as alternative to GCPnts_TangentialDeflection.
      Result triangulation might not meet deflection criteria (but knowing surface properties in advance it is technically possible defining predictable sub-division parameters resulting in desired mesh quality) but might be computed faster and have a tolerable quality for visualization purposes.
Thieme Vandeput's picture

Thanks again for this very clear explanation. I didn't know graphic cards only render triangles so meshing is done anyway...

My last question and then I'll close this topic. The wires of the faces are displayed almost instantaneously (so when only displaying the wires, not the whole face). They do not give a clear vision cause they are not 'filled'. Is there a way to fill the wires without having to create a face out of it, for displaying purposes only (See attached picture )?

Kind regards,
Thieme

Kirill Gavrilov's picture

No, you cannot display polygon Shaded without triangulating it.

However, if your polygon is planar, you may skip creation of complete B-Rep or/and avoid using BRepMesh_IncrementalMesh. Instead, it is possible using BRepMesh_Delaun directly to tessellate 2D polygon - this trick is used by readers like OBJ (RWObj_Reader) and VRML, which split polygons into triangles during mesh import.

Current development OCCT branch has a handy tool BRepMesh_Triangulator to simplify this interface.

Thieme Vandeput's picture

Yes, the polygons will always be planar! Just so you know, I've recently updated pythonocc to the most recent version of pythonocc but it looks like BRepMesh_Triangulator is not yet included in the python version. 

I'm trying out BRepMesh_FaceDiscret::Perform (loaded with the BRepMesh_DelabellaMeshAlgoFactory). It does something because it returns True, but I can't seem to get the tessellated shape out of the BRepMeshData_Model (the function takes in a BRepMeshData_Model with the 'polygon face' that I wan't to tessallate loaded into)... BRepMeshData_Model::GetShape() returns None after the Perform function.

Anyway, I'll be creative in my visualization to counter the performance issues. Many thanks for your time!