BrunoLevy / GraphiteThree

Experimental 3D modeler
GNU General Public License v2.0
213 stars 18 forks source link

How does union work in gompy? #19

Closed Nicolaus93 closed 2 weeks ago

Nicolaus93 commented 2 weeks ago

Hello! I'm Nico_Campolongo from Twitter, decided to move my issues here as it might be easier to track and expand on them :smile:

I'm now trying to compute the union between two meshes, but I don't know if I'm doing something wrong or it's just supposed to work like it is now. I have some meshes (coming from FreeCAD, but it shouldn't matter) and would like to compute their union. Below you can find an example with 2 faces of a cube. If I visualize the faces I can see they're touching (I also double checked the points manually), but their union is empty. Is this the intended behaviour? How is the union defined?

image

Here's the code:

import FreeCAD
import Mesh
import MeshPart
import Part
import polyscope as ps
import numpy as np
import gompy
from scipy.spatial.distance import cdist

def gompy_union():
    scene_graph = gom.create(classname='OGF::SceneGraph', interpreter=gom)
    scene_graph.clear()

    faces = []
    for i in range(6):
        face_m = scene_graph.load_object(f"/tmp/cube_face_{i}.obj")
        faces.append(face_m)

    faces[0].I.Surface.compute_union(other=faces[2].name, result="union")
    union = scene_graph.resolve('union')

    pts0 = np.asarray(faces[0].I.Editor.find_attribute('vertices.point'))
    pts1 = np.asarray(faces[2].I.Editor.find_attribute('vertices.point'))
    dists = cdist(pts0, pts1)
    res = np.where(dists < 1e-3)
    print(res)

    ps.init()
    for f in (faces[0], faces[2]):
        ps.register_surface_mesh(
            f.name,
            np.asarray(f.I.Editor.find_attribute('vertices.point')),
            np.asarray(f.I.Editor.get_triangles()),
        )
    ps.register_surface_mesh(
        "union",
        np.asarray(union.I.Editor.find_attribute('vertices.point')),
        np.asarray(union.I.Editor.get_triangles()),
    )
    ps.show()

if __name__ == "__main__":

    cube = Part.makeBox(2, 2, 2)
    for idx, face in enumerate(cube.Faces):
        print(f"Processing face {idx}")
        m = MeshPart.meshFromShape(face, 0.1)
        m.write(f"/tmp/cube_face_{idx}.obj")

    cube.exportStep("/tmp/cube.step")
    gompy_union()

I cannot attach the obj files, I'll post them directly below:

# cube_face_0.obj
v 0.000000 0.000000 0.000000
v 0.000000 0.000000 2.000000
v 0.000000 2.000000 0.000000
v 0.000000 2.000000 2.000000
vn -1.000000 0.000000 0.000000
vn -1.000000 0.000000 0.000000
f 1//1 2//1 3//1
f 3//2 2//2 4//2

and:

# cube_face_2.obj
v 2.000000 0.000000 0.000000
v 2.000000 0.000000 2.000000
v 0.000000 0.000000 0.000000
v 0.000000 0.000000 2.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 -1.000000 0.000000
f 1//1 2//1 3//1
f 3//2 2//2 4//2
BrunoLevy commented 2 weeks ago

Hello @Nicolaus93 ! It is because union is a volumetric operation: it only works when both operands are closed meshes, that define an interior and an exterior. What you want to do is to merge meshes. To (blindly) merge the content of two meshes, you can use:

S = ... # some mesh
T = ..  # another mesh
S.I.Mesh.merge(T)

After that, you may want to merge duplicated vertices:

S.I.Surface.merge_vertices()

Or maybe you want to compute intersections:

S.I.Surface.intersect(remove_internal_shells=False)

Note: if you want to have more information about a function (and if you updated geogram and Graphite this morning), you can use, for instance:

S.I.Surface.help()

and it will say:

============
OGF::MeshGrobSurfaceCommands::intersect(remove_internal_shells,simplify_coplanar_facets,coplanar_angle_tolerance,interpolate_attributes)
Computes intersections in a surface mesh
Parameters
==========
remove_internal_shells : bool = true
keep only facets on external hull
simplify_coplanar_facets : bool = true
retriangulates planar zones
coplanar_angle_tolerance : double = 0.001
in degrees
interpolate_attributes : bool = false
interpolate facet corner attributes on generated intersections. Deactivates coplanar facets simplification if set.
Nicolaus93 commented 2 weeks ago

Thanks for the quick reply! That's what I suspected, but I couldn't find merge. I've updated my function (will update graphite as well later) but still cannot make it work. Here's the code:

def gompy_merge():
    scene_graph = gom.create(classname='OGF::SceneGraph', interpreter=gom)
    scene_graph.clear()

    faces = [scene_graph.load_object(f"/tmp/cube_face_{i}.obj") for i in range(6)]

    faces[0].I.Mesh.merge(faces[2])
    faces[0].I.Surface.merge_vertices()

    ps.init()
    ps.register_surface_mesh(
        "union",
        np.asarray(faces[0].I.Editor.find_attribute('vertices.point')),
        np.asarray(faces[0].I.Editor.get_triangles()),
    )
    ps.show()

If I run help(faces[0].I.Mesh) I get:

Help on Object:
<graphite.Object object>
    OGF::MeshGrobMeshCommands
    Commands that manipulate the mesh in a MeshGrob

while dir(faces[0].I.Mesh) gives me:

['append',
 'chrono',
 'copy',
 'disable_signals',
 'disable_slots',
 'disconnect',
 'display_statistics',
 'display_topology',
 'enable_signals',
 'enable_slots',
 'gather',
 'grob',
 'meta_class',
 'nb_elements',
 'normalize_mesh',
 'normalize_mesh_box',
 'remove_isolated_vertices',
 'remove_mesh_elements',
 'set_properties',
 'set_property',
 'signals_enabled',
 'slots_enabled',
 'string_id']

so it looks I'm looking in the wrong place :sweat_smile: If I define S = faces[0].I.Surface I can see merge_vertices but not just merge

['bake_colors',
 'bake_normals',
 'bake_texture',
 'chrono',
 'compute_boolean_operation',
 'compute_difference',
 'compute_intersection',
 'compute_union',
 'decimate',
 'disable_signals',
 'disable_slots',
 'disconnect',
 'enable_signals',
 'enable_slots',
 'expand_border',
 'fill_holes',
 'fix_facets_orientation',
 'get_charts',
 'grob',
 'intersect',
 'make_texture_atlas',
 'merge_vertices',
 'meta_class',
 'nb_elements',
 'pack_texture_space',
 'parameterize_chart',
 'remesh_feature_sensitive',
 'remesh_quad_dominant',
 'remesh_smooth',
 'remove_charts',
 'remove_invisible_facets',
 'repair_surface',
 'segment',
 'set_properties',
 'set_property',
 'signals_enabled',
 'slots_enabled',
 'smooth',
 'split_catmull_clark',
 'split_quads',
 'split_triangles',
 'string_id',
 'tessellate_facets',
 'triangulate',
 'triangulate_center_vertex',
 'unglue_charts',
 'unglue_sharp_edges']
BrunoLevy commented 2 weeks ago

It is S.I.Mesh.merge(...), not S.I.Surface.merge(...). If you do not have merge in S.I.Mesh maybe your Graphite is not up to date (added it recently).

BrunoLevy commented 2 weeks ago

Oooo, I'm stupid ! It is S.I.Mesh.append(T), not merge, my bad !!!

BrunoLevy commented 2 weeks ago

BTW, I have just committed a gom.search("string to search") feature, it searches all classes and all functions of Graphite and prints everything that contains the string to search as a substring:

Type "help", "copyright", "credits" or "license" for more information.
>>> import gompy
o-[ModuleMgr   ] Loading module: luagrob
o-[ModuleMgr   ] Loading module: mesh
o-[ModuleMgr   ] Loading module: voxel
o-[ModuleMgr   ] Loading module: Experiment
o-[ModuleMgr   ] Loading module: RayTracing
o-[ModuleMgr   ] Loading module: WarpDrive
>>> gom.search("merge")
o-[GOM         ] gom.meta_types.OGF.MeshGrobSurfaceCommands.merge_vertices
>>> gom.search("append")
                 gom.meta_types.OGF.CompositeGrob.append
                 gom.meta_types.OGF.Grob.append
                 gom.meta_types.OGF.Interpreter.append_dynamic_libraries_path
                 gom.meta_types.OGF.LuaGrob.append
                 gom.meta_types.OGF.LuaInterpreter.append_dynamic_libraries_path
                 gom.meta_types.OGF.MeshGrob.append
                 gom.meta_types.OGF.MeshGrobMeshCommands.append
                 gom.meta_types.OGF.MeshGrobTransportCommands.append_points
                 gom.meta_types.OGF.PythonInterpreter.append_dynamic_libraries_path
                 gom.meta_types.OGF.SceneGraph.append
                 gom.meta_types.OGF.VoxelGrob.append
>>> gom.search("SceneGraph")
                 gom.meta_types.OGF.SceneGraph
                 gom.meta_types.OGF.SceneGraphCommands
                 gom.meta_types.OGF.SceneGraphSceneCommands
                 gom.meta_types.OGF.SceneGraphShaderManager
                 gom.meta_types.OGF.SceneGraphToolsManager
Nicolaus93 commented 2 weeks ago

No worries! I was wondering what was wrong after updating Graphite.. Now it works, thanks! image

One more question: is merge_vertices actually needed or does it happen automatically when calling append? I updated my function and it seems there are no duplicate vertices after all the operations

def gompy_append():
    scene_graph = gom.create(classname='OGF::SceneGraph', interpreter=gom)
    scene_graph.clear()

    faces = [scene_graph.load_object(f"/tmp/cube_face_{i}.obj") for i in range(6)]

    for i in range(1, 4):
        faces[0].I.Mesh.append(faces[i])

    vertices = np.asarray(faces[0].I.Editor.find_attribute('vertices.point'))
    print(f"{len(vertices)} vertices before merging")

    faces[0].I.Surface.merge_vertices()
    vertices = np.asarray(faces[0].I.Editor.find_attribute('vertices.point'))
    print(f"{len(vertices)} vertices after merging")

    ps.init()
    ps.register_surface_mesh(
        "union",
        np.asarray(faces[0].I.Editor.find_attribute('vertices.point')),
        np.asarray(faces[0].I.Editor.get_triangles()),
    )
    ps.show()
BrunoLevy commented 2 weeks ago

Yes, I confirm, append() calls mesh_repair() that merges vertices and deletes duplicated faces. I can add an option to deactivate it if need be.

Nicolaus93 commented 2 weeks ago

Personally I don't need it. Thanks again for all the help!