tpaviot / pythonocc-core

Python package for 3D geometry CAD/BIM/CAM
GNU Lesser General Public License v3.0
1.35k stars 376 forks source link

IndexedFaceSet vs. TriangleSet #816

Open andreasplesch opened 4 years ago

andreasplesch commented 4 years ago

The shape tesselator has an ExportShapeToX3DIndexedFaceSet() function:

https://github.com/tpaviot/pythonocc-core/blob/e87231d06087e2e2110a1b64a0240b023cd86165/src/Tesselator/ShapeTesselator.cpp#L373

But the function actual generates a TriangleSet, not an IndexedFaceSet.

The naming may be for historic reasons ?

It would be preferrable if the function actually would generate a IndexedFaceSet or an IndexedTriangleSet. By iterating over the triangles to get the vertex ids, and then iterating over locvertexcoord to get the coordinates for each id. This would more efficient. But perhaps there is another reason for using non-indexed geometry.

If so, renaming the function may be possible:

https://github.com/tpaviot/pythonocc-core/search?q=ExportShapeToX3DIndexedFaceSet%28%29&unscoped_q=ExportShapeToX3DIndexedFaceSet%28%29

tpaviot commented 4 years ago

@andreasplesch you're right, the method indeed creates an X3D TriangleSet. I don't remember exactly the history of this piece of code, but I guess I started with an IndexedFaceSet, and then changed the method body without renaming its name, certainly for backward compatibility reasons. I agree that the method name should be changed. I leave this issue open until this is done.

andreasplesch commented 4 years ago

thanks.

tpaviot commented 4 years ago

@andreasplesch I changed the method name to reflect the actual method content.

The TriangleSet vs IndexedTriangleSet discussion is interesting. If you do think the latter would result in a performance improvement, then it might be worth trying to move to this representation (the pythonocc x3dom exporter has very poor performances compared to 3js). But I did not work on the x3dom exporter for months or even years, diving back into this would be a kind of start from scratch. If you have any idea or suggestion to make this move fast, please let me know.

tpaviot commented 4 years ago

This might not be as difficult as I thought, I just have to add a

std::string ShapeTesselator::ExportShapeToX3DIndexedFaceSet()

and only modify the method body

tpaviot commented 4 years ago

This is related to issue #351

andreasplesch commented 4 years ago

In terms of performance, I think you are referring to the rendering performance ? Threejs is generally more efficient but there should not be a drastic difference. x3dom does pass through the indexed geometries to webgl. Indices should reduce the number of vertices to store by a large fraction (each vertex is shared perhaps by about 6 triangles) which helps with the generated file/object size, and should also help with rendering.

I found that the OCCT VrmlAPI actually generates IndexedFaceSets. So it may be possible to either reuse that functionality which requires translating to x3d xml, or reproduce the code in python to output x3d xml.

andreasplesch commented 4 years ago

Testing some more, I was reminded that what helps x3dom with performance is to limit the number of x3d shapes, eg. by combining geometries. Here is an example:

https://nbviewer.jupyter.org/github/andreasplesch/OCCToX3D/blob/77cda237a7055d796b48623712c0c0296d579973/notebooks/stp_to_x3d2.ipynb

[scroll to the end]

The first rendering has >1000 shapes and is a little slow. The second rendering has < 100 shapes and performs well.

The first rendering uses the Vrml generated by OCCT. The second rendering uses pythonocc generated x3d, the TriangleSet.

So more important than using indexed geometries is to limit the number of Shapes because each has to managed, updated for each frame by x3dom.

andreasplesch commented 4 years ago

It looks like OCCT now has glTF export as well but I could not figure out how to use it or find an example. There is a glTF loader for threejs and one for x3dom. glTF geometries are in a gpu friendly binary format and should load and render well.

tpaviot commented 4 years ago

The glTF exporter is being developed at OCCT, it has not been released yet

andreasplesch commented 4 years ago

I think I saw glTF export for CAD Assistant, but was perhaps misled by early announcements.

tpaviot commented 4 years ago

@andreasplesch I agree that Indexed geometry would result in a smaller file, thus reducing loading time. For instance, exporting a meshed box currently export 36 vertices (2 triangles per face, each triangle requires 3 vertices thus) and 6 faces -> 236 = 66=36. That is to say 363=108 floats. Indexed geometry would only require storing 8 vertices * 3 floats/vertex = 24 float and 36 integers.

When I fisrt started working on x3d and threejs exporters, I wanted to have the smallest granularity (down to the Face and the Edge), to be able to select such low level entities and request, for example, the computation of the mass, center of inertia etc. It's possible using, on the server side, a bijective mapping between the shape id and the x3d mesh. And thus roundtripping from OCCT to x3d. This is a drawback when rendering huge models where thousands of faces / edges are added to the Scene. But, on the other side, have many shapes also enable async loading from the server, then progressive loading of the scene.

Then, this has to be balanced between:

  1. few shapes/fast rendering/no async loading/poor granularity vs
  2. many shapes/low rendering/async loading/good granularity.

Solution 1 is suitable for big scene visualization I guess. Solution 2 is more suitable for detailed work on a part or small model. I made the choice of solution 2, because that's I needed, but I confess I forced all users to follow the same path ! The choice between 1 and 2 should be left to the final user.

So here is what could done IMO:

a. Move from a TriangleSet to IndexedTriangleSet representation, to save bandwith/loading time. This is independent from any of the solution 1 or 2. This is still to do. It can be done at the c++ level, or, at the python level, using numpy to convert the TriangleSet representation to indexed geometry (I prefer this solution, if it's not too resource consuming).

b. Let the user choose the granularity level when calling the X3D exporter (whether the Compound, Solid or Face level). I did it for the threejs based renderer for Jupyter. You can have a look at the helloworld.ipynb example from https://mybinder.org/v2/gh/tpaviot/pythonocc-binderhub/7.4.0, and just change topo_level="Face" to topo_level="Solid".

tpaviot commented 4 years ago

@andreasplesch Talking about a numpy based solution, I was thinking to something lke that:

from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
from OCC.Core.Tesselator import ShapeTesselator

import numpy as np

# use the tesselator to tesselate the shape
a_sphere = BRepPrimAPI_MakeBox(10, 20, 30).Shape()
sphere_tess = ShapeTesselator(a_sphere)
sphere_tess.Compute()

sphere_triangle_set_string = sphere_tess.ExportShapeToX3DTriangleSet()
print("Triangle Set representation:")
print(sphere_triangle_set_string)
triangle_set = sphere_tess.GetVerticesPositionAsTuple()

# convert vertex coordinates to a numpy array
triangle_set_np_array = np.array(triangle_set)
nb = len(triangle_set_np_array)
# following should be 108
print("Number of floats in the numpy array:", nb)
# reshape the array to get an array of 3 float arrays
vertices = triangle_set_np_array.reshape((int(nb / 3), 3))
print(vertices)
print("Length of vertices array:", len(vertices))
# among all these vertices, there should only be 8 different
unique_vertices = np.unique(vertices, axis=0)
print("Number of unique vertices:", len(unique_vertices))
print("Unique vertices:\n", unique_vertices)

indices = [np.where(np.all(unique_vertices==ar, axis=1))[0][0] for ar in vertices]
print("Vertices indices:\n", indices)

The original TriangleSet string is:

<TriangleSet solid='false'>
<Coordinate point='0 0 30 0 20 0 0 0 0 0 0 30 0 20 30 0 20 0 10 0 30 10 0 0 10 20 0 10 0 30 10 20 0 10 20 30 10 0 30 0 0 0 10 0 0 10 0 30 0 0 30 0 0 0 10 20 30 10 20 0 0 20 0 10 20 30 0 20 0 0 20 30 10 20 0 0 0 0 0 20 0 10 20 0 10 0 0 0 0 0 10 20 30 0 20 30 0 0 30 10 20 30 0 0 30 10 0 30 '></Coordinate>
<Normal vector='-1 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 0 -1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 '></Normal>
</TriangleSet>

After being processed by numpy, I get:

Number of unique vertices: 8
Unique vertices:
 [[ 0.  0.  0.]
 [ 0.  0. 30.]
 [ 0. 20.  0.]
 [ 0. 20. 30.]
 [10.  0.  0.]
 [10.  0. 30.]
 [10. 20.  0.]
 [10. 20. 30.]]
Vertices indices:
 [1, 2, 0, 1, 3, 2, 5, 4, 6, 5, 6, 7, 5, 0, 4, 5, 1, 0, 7, 6, 2, 7, 2, 3, 6, 0, 2, 6, 4, 0, 7, 3, 1, 7, 1, 5]

that would do it, no ?

tpaviot commented 4 years ago

Note that @aothms implemented something like that at the c++ level, using std::map but I'm less comfortable with c/c++ than python

tpaviot commented 4 years ago

Tested with a Torus, 4056 vertices in the TriangleSet, only 676 left in the IndexTriangleSet, performed by numpy in around 0.1s, that's pretty fast

aothms commented 4 years ago

For reference here is the IfcOpenShell OCCT glTF exporter https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.6.0/src/serializers/GltfSerializer.cpp But I don't think it's very useful as a starting point for sth Python-based.

Indexed geometry would only require storing 8 vertices * 3 floats/vertex = 24 float and 36 integers.

Note, if you need unique vertex,normal pairs the amount of vertices you can reuse is not so drastic for a cube. I don't know X3D but in WebGL this is typically the case if you need shading. So that's 8 vertices 3 normals 3 floats.

In my experience 3D performance is mostly bound by the number of draw calls. Indexed geometries definately help in reducing data throughput but not in reducing amount of draw calls, which can be accomplished by e..g merging objects.

tpaviot commented 4 years ago

I agree. However, the file size reduction is also important, especially when dealing with heavy models, and ascii meshes I use with x3dom or 3js are quite big, and as a consequence 200Mb to transfer from the server is much better than 500Mb.

@andreasplesch is there a kind of binary representation for x3dom ? and some related code to convert from ascii to binary ?

tpaviot commented 4 years ago

@aothms btw, did you experiment LOD ? I played with it a while ago, but never went further

aothms commented 4 years ago

@aothms btw, did you experiment LOD ? I played with it a while ago, but never went further

No not really. It seems the feature removal is a great candidate: https://dev.opencascade.org/index.php?q=node/1211 I thought this was closed source, but I might be mistaken or they changed it. For example removing a (small) inner boundary on a face results in a lot less triangles. And especially if that is a hole with a screw in it (or is otherwise obscured), it doesn't have major visual implications.

For the typical mesh reduction I don't think OCCT BRep model is suitable. We'd better look into CGAL's edge collapse for example https://doc.cgal.org/latest/Surface_mesh_simplification/index.html

tpaviot commented 4 years ago

Yes, I had a look at cgal and Vtk, and mesh decimation algorithm. Another possibility is to perform a mesh using basic OCCT meshing algorithm and high deviation value, resulting in a fewer number of triangles. This can be quickly achieved.

aothms commented 4 years ago

Yes, at least in my case though IFC files are full of very detailed explicit IfcFacetedBReps approximating a curved surface. So in these kind if 'pre-meshed' cases there is nothing that can be done anymore.

tpaviot commented 4 years ago

I don't understand your last statement. What I mean with LOD is to speed up rendering by computing several meshes from one single entity (from poor quality to best quality) and display each mesh according to the distance from the point of view. see https://threejs.org/examples/webgl_lod.html This could drastically improve the visualization for large scenes. Do we mean the same thing ?

andreasplesch commented 4 years ago

Another aspect to keep in mind is that X3D is not limited to the browser. There are higher performance, standalone viewers. If the X3D file is well structured and contains information about its entities, the structure and meta information can be used by X3D scenes built around the model, and the browsers.

@tpaviot yes, I think the numpy unique and remapping approach would work well.

x3dom has binary representations (SRC format) and in fact the glTF format is partially based on that. There is currently an effort to embrace glTF within X3D, eg. X3D viewers are starting to add support. The idea is to treat such a model as inert, a fixed entity. This is generally what is needed but if animation (for example for exploding a view) need to be added, it would not really be possible.

X3D has level of detail. One needs to provide all the levels. One could also think about more custom javascript to add and remove Inlined models on demand.

andreasplesch commented 4 years ago

If you are using Python to construct X3D, there is now a x3d.py (https://pypi.org/project/x3d/) which helps with generating spec. compliant, correct x3d.

andreasplesch commented 4 years ago

A small detail: if you use IndexedTriangleSet you do not need the '-1' end marker for each polygon, to save a few bytes.

On the other hand, if you can tesselate to quads rather than triangles, IndexedFaceSet would work with such polygons (and does the triangulation internally).

andreasplesch commented 4 years ago

https://x3dom.org/src/ has more info on SRC. SRC support is pretty much limited to x3dom.