How to optimise GHPython OBJ export?

I haven’t used ctypes besides using code snippets from others. How do you do that without ctypes?

Nathan,

How do I build the DLL for Mac in Windows Studio 2017?

Regards,
Terry.

A C/C++ DLL you can’t build for Mac using Windows VS 2017.

Nathan,

I need to use VS for Mac, correct? If so can I take my existing C++ source code and compile it on VS for Mac and then build the DLL? Or does the code need to be rewritten in some way?

Regards,
Terry.

If you don’t do anything platform-specific it shouldn’t be hard to have it compile on the Mac. You need a C/C++ compiler, VS for Mac is for .NET. Xcode is what you probably want to use, or if it is very simple project you may just want to do a Makefile and use clang as the compiler.

Hi everybody,

Since the DLL workflow still doesn’t work on macOS, I came up with another Pythonic approach today.
It uses the Rhino _Export command. Basically, a temporary, new layer is created in Rhino. The desired meshes are baked to that layer, and exported to an OBJ file. After that, the temporary layer and meshes get deleted, since they aren’t needed anymore.

In my most recent test, I was able to export a Mesh with 165K vertices and 160K faces in 4.07 seconds, which isn’t bad at all performance wise.
My first Python script (cf. above), which parses and writes the OBJ from Python, takes 18.9 seconds to complete the exact same task, and @stevebaer’s improved version of the same script oddly enough needs 19.4 seconds to complete the task.
@Terry_Chappell’s method, C++ with Python, is still far superior in terms of performance, however my new script allows you to fine tune the export with lots of options, like Rhino does. (I haven’t tested Terry’s script with this, since I have no Windows machine at home.)

Feel free to try it yourself:

import scriptcontext as sc
import System
import Rhino
import random
import string
import time
import os


def add_layer(layer_name=None, layer_color=None):
    """Adds a new layer to the active Rhino document.
    
    Args:
      layer_name (str): An optional layer name.
      layer_color (System.Drawing.Color): An optional layer color.
    Returns:
      The index of the new layer.
    """
    sc.doc = Rhino.RhinoDoc.ActiveDoc
    if layer_name != None:
        # Check whether the layer name is valid
        if not Rhino.DocObjects.Layer.IsValidName(layer_name):
            raise ValueError("{} is not a valid layer name."\
                .format(layer_name))
        # Check whether a layer with the same name already exists
        layer_index = sc.doc.Layers.Find(layer_name, True)
        if layer_index >= 0:
            raise ValueError("A layer with the name {} already exists."\
                .format(layer_name))
    else:
        layer_name = sc.doc.Layers.GetUnusedLayerName(False)
    
    # Check whether the layer color is valid
    if layer_color != None:
        if not isinstance(layer_color, System.Drawing.Color):
            raise ValueError("{} is not a valid layer color."\
                .format(layer_color))
    else:
        layer_color = System.Drawing.Color.Black # default layer color
    
    # Add a new layer to the active document
    layer_index = sc.doc.Layers.Add(layer_name, layer_color)
    if layer_index < 0:
        raise ValueError("Unable to add layer {} to document."\
            .format(layer_name))
    return layer_index
    

def delete_layer(layer):
    """Deletes an existing layer from the active Rhino document. 
    The layer to be removed cannot be the current layer and 
    it will be deleted even if it contains objects.
    
    Args:
      layer (str\id): A name or id of an existing layer.
    Returns:
      True or False, indicating success or failure.
    """
    sc.doc = Rhino.RhinoDoc.ActiveDoc
    layer_index = sc.doc.Layers.Find(layer, True)
    if layer_index < 0:
        raise ValueError("The layer {} does not exist.".format(layer))
    rc = sc.doc.Layers.Purge(layer_index, True)
    sc.doc.Views.Redraw()
    return rc
    

def bake_mesh(layer, mesh, mesh_name=None):
    """Bakes a mesh object to the active Rhino document.
    
    Args:
      layer (str\id): The name or id of an existing layer.
      mesh (Rhino.Geometry.Mesh): A mesh object to bake.
      mesh_name (str): An optional mesh object name.
    Returns:
      The GUID of the baked mesh.
    """
    sc.doc = Rhino.RhinoDoc.ActiveDoc
    # Check whether the mesh is a mesh object
    if mesh.ObjectType != Rhino.DocObjects.ObjectType.Mesh:
        raise ValueError("{} is not a mesh.".format(mesh))
    # Create the mesh object attributes
    attr = Rhino.DocObjects.ObjectAttributes()
    # Set the mesh object layer index attribute
    layer_index = sc.doc.Layers.Find(layer, True)
    if layer_index < 0:
        if layer == "Default":
            raise ValueError("The layer {} can't be baked to.".format(layer))
        else:
            raise ValueError("The layer {} does not exist.".format(layer))    
    else:
        attr.LayerIndex = layer_index
    # Set the mesh object name attribute
    if mesh_name != None:
        attr.Name = mesh_name
    # Bake the mesh to the active Rhino documemnt
    mesh_id = sc.doc.Objects.AddMesh(mesh, attr)
    return mesh_id


def parse_obj_settings():
    """Returns the settings for the OBJ export command as a string."""
    # Formatting options
    cfg = "_Geometry=_Mesh "
    cfg += "_EndOfLine=CRLF "
    cfg += "_ExportRhinoObjectNames=_ExportObjectsAsOBJGroups "
    cfg += "_ExportMeshTextureCoordinates=_Yes "
    cfg += "_ExportMeshVertexNormals=_Yes "
    cfg += "_ExportMeshVertexColors=_Yes "
    cfg += "_CreateNGons=_No "
    cfg += "_ExportMaterialDefinitions=_No "
    cfg += "_YUp=_Yes "
    cfg += "_WrapLongLines=_No "
    cfg += "_VertexWelding=_Unmodified "
    cfg += "_WritePrecision=4 "
    cfg += "_Enter "
    # Detailed options
    cfg += "_DetailedOptions "
    cfg += "_JaggedSeams=_No "
    cfg += "_PackTextures=_No "
    cfg += "_Refine=_No "
    cfg += "_SimplePlane=_No "
    # Advanced options
    cfg += "_AdvancedOptions "
    cfg += "_Angle=0 "
    cfg += "_AspectRatio=0 "
    cfg += "_Distance=0.0 "
    cfg += "_Density=0.5 "
    cfg += "_Grid=0 "
    cfg += "_MaxEdgeLength=0 "
    cfg += "_MinEdgeLength=0.0001 "
    cfg += "_Enter _Enter" # remove the last _Enter to check if density is set
    return cfg


def export_obj(meshes, path, filename, debug=False):
    """Exports a collection of meshes to an OBJ file.
    
    Args:
      meshes (list): A list of Rhino.Geometry.Mesh objects.
      path (str): An absolute path pointing to a directory.
      filename (str): A filename (without extension).
      debug (bool): Optional True to print debug information.
    Returns:
      True or False, indicating success or failure.
    """
    sc.doc = Rhino.RhinoDoc.ActiveDoc
    sc.doc.Views.RedrawEnabled = False
    if debug: 
        now = time.clock()
        elapsed = 0.0
        
    # Create a temporary layer
    layer = "".join(random.choice(string.ascii_uppercase) for _ in range(9))
    layer_index = add_layer(layer)
    if debug: 
        then, now = now, time.clock()
        elapsed += now - then
        print "Creating temporary layer: {0:.4f} seconds".format(now-then)
        
    # Bake the temporary mesh(es)
    mesh_ids = [bake_mesh(layer, mesh) for mesh in meshes]
    if debug: 
        then, now = now, time.clock()
        elapsed += now - then
        print "Creating temporary mesh(es): {0:.4f} seconds".format(now-then)
    
    # Unselect all objects in the scene
    sc.doc.Objects.UnselectAll()
    # Select the baked mesh object(s)
    for mid in mesh_ids:
        sc.doc.Objects.Select(mid)
    if debug: 
        then, now = now, time.clock()
        elapsed += now - then
        print "Unselecting and selecting: {0:.4f} seconds".format(now-then)
    
    # Export the selected mesh object(s) to OBJ
    obj_fname = "{}.{}".format(filename, "obj")
    obj_fpath = os.path.join(path, obj_fname)
    obj_config = parse_obj_settings()
    cmd = '_-Export "{}" {}'.format(obj_fpath, obj_config)
    #start = Rhino.DocObjects.RhinoObject.NextRuntimeSerialNumber
    rc = Rhino.RhinoApp.RunScript(cmd, True)
    #end = Rhino.DocObjects.RhinoObject.NextRuntimeSerialNumber
    #global __command_serial_numbers
    #__command_serial_numbers = None
    #if start != end:
        #__command_serial_numbers = (start, end)
    
    if debug: 
        then, now = now, time.clock()
        elapsed += now - then
        print "Exporting OBJ: {0:.4f} seconds".format(now-then)
    
    # Delete the temporary layer and meshes
    delete_layer(layer)
    
    if debug: 
        then, now = now, time.clock()
        elapsed += now - then
        print "Cleaning up: {0:.4f} seconds\n".format(now-then)
        print "Total elapsed: {0:.4f} seconds".format(elapsed)

    sc.doc.Views.RedrawEnabled = True
    return rc
    

if __name__ == "__main__":
    filename = "test_export" # OBJ filename
    path = "C:\Users\your_username\Documents" # folder

    rc = export_obj(Meshes, path, filename, True)
    if not rc:
        raise ValueError("OBJ export failed.")

The export settings can be changed inside the parse_obj_settings() method!

Have a nice weekend, guys!

5 Likes

Nathan,

I use VS on windows to create a DLL that can be called from a Python script running in Rhino. I am very new to using DLL with Python on Windows and do not own a Mac. Does this mean it is not possible for me to create a DLL in some version of VS on my Windows machine that will run on a Mac?

Regards,
Terry.

Just a note: All of the export options can also be setup to work inside my script.

I am continuing to investigate how to create a DLL for a Mac.

Regards,
Terry

A native DLL for Mac is called a dylib and if you don’t have a Mac to compile on then it is going to be hard.

If the python script I wrote is not fast enough, I would recommend using C# instead. There are some unsafe access functions available to C# that you could use that may improve performance. I’ll see if I can cook something up.

1 Like

Steve,

This sounds interesting. I have never used C# code with Rhino so if you could point me to the most relevant references this could help me spin up.

I look forward to seeing your C# example.

Regards,
Terry.

There is still a bug in my writing code as when I open this obj file back up in Rhino it has a hole. Maybe one of you can help find and fix the bug. Here’s what I have so far

The C# script uses multiple tasks to split up the load of generating the different chunks of data. These tasks run on separate threads.

mesh_to_obj.gh (11.3 KB)

2 Likes

I will take a look at this.

1 Like

I edited the slider and got up to a setting of 2000 before it stopped working. This resulted in a mesh with 4M faces in 9.1 sec or about 2 sec per million faces. My script, without parallel, is now running at about the same speed for this size of a mesh. So the parallelism is definitely a benefit as your Python script seems to be about 4X slower. I will be trying to get parallel to work with my script.

You are right about the C# exported mesh having some kind of problem, with bogus face indices being reported when I use Rhino to import it. I looked over the C# code but did not find anything yet. Is the original mesh sphere good?

Regards,
Terry.

1 Like

I think I know what I did wrong. Try adding one to all of the face indices. I think obj uses1 based indexing instead of 0

1 Like

Yes that is definitely correct. When you export you need to add the 1 to the face indices for vertices, textures and normals.

Yep, that was it. Here’s an updated version with the fix.
mesh_to_obj.gh (11.4 KB)

5 Likes

Steve,

I added parallel execution to my Python/DLL script and was able to export my 18.43M face mesh in only 25 sec. This is 0.72M faces/sec so a significant improvement over the 0.5M faces/sec of your new script and my old script.

Smaller meshes (5M faces) pop out in 5 sec. The higher rate, 1M faces/sec, is due to the shorter face lines (indices are smaller).

In terms of exporting my 18.4M face mesh, we have come a long ways, from around 200 sec for your first script to 25 sec for our latest script. For reference, Rhino Import takes 72 sec.

Regards,
Terry.

1 Like

Wow, you’re a beast @Terry_Chappell! Simply amazing performance.

1 Like

Thanks for your encouragement.

However, not owning a Mac, I see no clear path for getting my Python/DLL script to work on your Mac. Maybe if I coded the DLL in C# it would run nearly as fast as C++? What do you think @stevebaer , does the C# code execute nearly as fast as C++ code?

Regards,
Terry.

Don’t worry about it. Your scripts should help a lot of other people around here.
From what I’ve read, the performance of C++ comes mainly from the fact that it’s a compiled language and really fine-tuneable in many aspects. C# code is only semi-compiled which should make it a little slower?

2 Likes