How to optimise GHPython OBJ export?

I don’t know how this could be done efficiently in the context of Grasshopper.

Not with Grasshopper.
I mean GH will run the PyPy or Cython and the conversion will happen using RhinoIO.dll or Rhino3dm module. You can skip GH all together. Just have that mesh saved as 3dm

How are you getting the mesh from Grasshopper to these external python environments? That is what isn’t making sense to me.

subprocess.call()?
System.Diagnostics.Process.Start()? (in dotnet context)

I just recently found out about Cython (yesterday) I don’t know the syntax yet.

1 Like

I re-balanced the parallel tasks and improved the binary writes to reduced the export time to 13.4 sec for my 18.4M faces test case. The progression of improvements, measured in million of mesh faces per sec, now looks like this:

(1) 1.37M/sec with latest parallelPython/DLL script (= 5.1X Rhino Import)
(2) 0.74M/sec for my first parallel Python/DLL script
(3) 0.5M/sec for my best Python/DLL script and Steve’s C# parallel script inside GH
(4) 0.31M/sec for my best Python script
(4) 0.27M/sec for Rhino’s Import tool
(5) 0.093M/sec for Steve’s C# script without parallelism

Converting the mesh values to strings consumes a large portion of the time exporting a mesh to a file. This task lends itself well to parallelism in a Rhino IronPython/DLL implementation because the calls to the DLL’s are outside of the Python interpreter. Thus parallel requests do not get piled up waiting for the interpreter. If you try to run pure-Python commands in parallel, things run slower because of the interpreter bottleneck and the parallelism overhead. The DLL’s get 99% of the work done without using the interpreter so parallelism works great.

My machine has 18 cores so 36 threads are available. When I look at the Resource Monitor, I see that more than 18 threads threads are active when executing the script. I tested using only 8 cores (16 threads) and the performance drops by 8% which is not too bad (13.4 sec increases to 14.5 sec on the 18.4M faces test case). The timing breakdown of the script for my 18M faces test case is:

Time to load DLL = 0.0000 sec
Time to prep data = 6.7080 sec
Time to run parallel Tasks = 2.9272 sec
    Time to export vertices1 = 2.9262 sec.
    Time to export vertices2 = 2.6040 sec.
    Time to export vertices3 = 2.7317 sec.
    Time to export vertices4 = 2.7377 sec.
    Time to export textures1 = 2.8863 sec.
    Time to export textures2 = 2.9002 sec.
    Time to export normals1 = 2.7756 sec.
    Time to export normals2 = 2.2729 sec.
    Time to export normals3 = 2.3766 sec.
    Time to export normals4 = 2.2230 sec.
    Time to export faces1 = 2.2061 sec.
    Time to export faces2 = 2.1891 sec.
    Time to export faces3 = 2.1891 sec.
    Time to export faces4 = 2.7885 sec.
    Time to export faces5 = 2.2719 sec.
    Time to export faces6 = 2.2689 sec.
    Time to export faces7 = 2.2181 sec.
    Time to export faces8 = 2.2151 sec.
Time to write data to file = 3.7031 sec.
Total time = 13.3957 sec

From this we can see that the script achieves 18-way parallelism in converting the mesh number to strings. Without the parallelism, this task would take around 60 sec instead of 13.4 sec.

The timings show that 78% of the time (10.4 sec out of 13.4 sec) is spent in the non-parallel activities of extracting the data and writing the 2.2GB to file. These are the areas that need more work. The biggest offender is extracting the colors from the mesh; this takes over 3 sec because there is no ToIntArray method for colors like there is for faces and ToFloatArray for vertices, textures and normals. Compare the 3 sec required to extract the colors to the vertices ToFloatArray method which extracts the 27M values of X,Y,Z vertex data in only 0.17 sec. Quite a difference. If I had one Rhino 6/7 request for McNeel, it would be to add this method to Mesh.VertexColors.

Regards,
Terry.

1 Like

What does your current c++ implementation look like? Maybe it would give me some ideas to improve the C# implementation. Honestly, I didn’t really spend much time attempting different optimizations as I figured the sample I gave was good enough.

1 Like

Steve,

Right now the script is a bit torn up as I am still playing with adding more parallelism. Edit: I posted the final version in my later post below. I borrowed the core of the parallel code from an GH posting on the forum that does this:

# Size pieces input and results output lists for 10 entries.
pieces, results = range(10), range(10)
# Define helper procedure for feeding data list to function, result to output.
def helper(piece): results[piece] = function(piece)
# Execute function in parallel with all threads used.
Tasks.Parallel.ForEach(pieces, helper)

I this case pieces holds essentially nothing as I am just using it to index thru the case statement which selects which DLL to use (i == 0 for vertices1, i == 1 for vertices2, … , i = 9 for faces4). function holds the case statement for calling each DLL.

I think you are probably familiar with this. As big a benefit comes from custom translation from the mesh numbers to strings. That looks like this for the case of converting the X-coordinate in a vertex:

    DLLEXPORT void vertex2string(int precision, int c_precision, float *verts, int *colors,
    	int num_verts, int num_colors, char *str, long &size_of_line) {
    	static int pow10[10] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 };
    	// Used to convert to string.
    	int a, b, c, k, l, m, i, j, i_begin, A, R, G, B;
    	float f;
    	// Process vertices.
    	// Save starting value of i.
    	i = 0;
    	i_begin = i;
    	for (j = 0; j < num_verts; j += 3) {
    		// Do first value.
    		f = verts[j];
    		// Start vertex line with 'v '.
    		str[i++] = 'v';
    		str[i++] = ' ';
    		// check for negative float
    		if (f < 0.0) {
    			str[i++] = '-';
    			f *= -1;
    		}
    		// Extract the whole number.
    		a = f;
    		// Extract the decimal part.
    		f -= a;
    		k = precision;
    		// Get the number of digits in the whole number = k+1.
    		// This loop finishes sooner when there are more digits.
    		while (k > -1) {
    			l = pow10[k];
    			m = a / l;
    			if (m > 0) { break; }
    			k--;
    		}
    		// Extract most significant digit and concatenate to string obtained as
    		// quotient by dividing number by 10^k where k = (number of digit - 1)
    		for (l = k + 1; l > 0; l--) {
    			b = pow10[l - 1];
    			c = a / b;
    			str[i++] = c + 48;
    			a %= b;
    		}
    		// Add decimal point.
    		str[i++] = '.';
    		// Extract decimal digits till precision met.
    		for (l = 0; l < precision - k - 1; l++) {
    			f *= 10.0;
    			b = f;
    			str[i++] = b + 48;
    			f -= b;
    		}
    		// Do second value.

The full details for converting the vertices, texture coordinates, normals and face indices to strings are shown in the DLL I posted earlier and the DLL posted below.

Regards,
Terry.

Steve,

Here are my Python script and DLL for quickly exporting a mesh to an .obj file using parallel tasks (1.37M faces/sec or 5.1X faster than Rhino Export). Edit: These were updated 8/27/2019 to correct an indexing error.

ExportObj_with_DLL_for_Steve.py (15.3 KB)

The text for the Python script is here:

dll_mesh2string = r'C:\Users\Terry\source\repos\Mesh2String\x64\Release\Mesh2String.dll'
#dll_name = r'C:\Users\Terry\Downloads\float2string.dll'
from scriptcontext import doc
from time import time
from ctypes import cdll, c_float, c_int, create_string_buffer, pointer, byref as c_byref
from System.Threading import Tasks
import os

def export_obj(mesh, path, filename):
	"""
	Exports the vertices with vertex colors, textures, normals and faces of a mesh to .obj file.
	Parameters:
		mesh: Geometry of mesh to be exported.
		path: Path to output file.
		filename: Name of output file.
	Returns:
		None
		The mesh data is written to an .obj file.
	"""
	# Add .obj to end of file name.
	obj_fname = "%s.%s" %(filename, "obj")
	# Add mtl to end of file name for mtl line.
	mtl_fname = "%s.%s" %(filename, "mtl")
	# Create full path to output file.  Data will be written to this file.
	obj_fpath = os.path.join(path, obj_fname)
	
	#
	# Write out header to .obj file while still in Python.
	#
	#header = "# Rhino\n\nmtllib %s\nusemtl diffuse_0_0_0_255\n" \
	#	%(mtl_fname)
	# This writes header without extra blank line.
	header = "# Rhino\n\nmtllib %s\nusemtl %s\n" %(mtl_fname,filename)
	with open(obj_fpath, 'w') as f: f.write(header)
	#
	# Export mesh data to file
	#
	timea = time()
	# Use DLL to speedup converting floating and integer numbers to string and writing to file.
	soMesh2String = cdll.LoadLibrary(dll_mesh2string)
	# Convert mesh data to array or list.
	timeb = time()
	print 'Time to load DLL = {0:.4f} sec'.format(timeb-timea)
	# Use precision of 8 so all digits of single-precision floating point numbers in arrays is preserved.
	precision = 8
	# Set precision for colors to 3 since RGB has max of 3 digits (256).
	c_precision = 3
	#
	# Extract data from mesh.
	#
	timep = time()
	# Get local reference to mesh colors.
	vcolors = mesh.VertexColors
	num_colors = vcolors.Count
	# Define function for use in parallel tasks.
	def extract_data(i):
		if i == 0: return mesh.Vertices.ToFloatArray()
		elif i == 1: return [color.ToArgb() for color in vcolors]
		elif i == 2: return mesh.TextureCoordinates.ToFloatArray()
		elif i == 3: return mesh.Normals.ToFloatArray()
		elif i == 4: return mesh.Faces.ToIntArray(mesh.Faces.QuadCount == 0)
	# Size input & output lists to hold 5 entries.
	pieces, results = range(5), range(5)
	# Define helper procedure for feeding data choice to function and results to output list.
	def helper(i): results[i] = extract_data(i)
	# Execute extra_data function in parallel.
	Tasks.Parallel.ForEach(pieces, helper)
	# Enable next line and comment out above line for debug.
	#for i in pieces: helper(i)
	# Define local variables for extracted data.
	verts = results[0]
	colors = results[1]
	tcs = results[2]
	normals = results[3]
	faces = results[4]
	print 'Time to prep data = {0:.4f} sec'.format(time() - timep)
	# Define data limits to be used during parallel execution.
	Qv = 3*((verts.Length//3)//4)
	Hv = 3*((verts.Length//3)//2)
	Qt = 3*((tcs.Length//3)//4)
	Ht = 3*((tcs.Length//3)//2)
	Qv = 3*((normals.Length//3)//4)
	Hv = 3*((normals.Length//3)//2)
	Qc = num_colors//4
	Hc = num_colors//2
	Qn = 3*((normals.Length//3)//4)
	Hn = 3*((normals.Length//3)//2)
	E = 4*((faces.Length//4)//8)
	Q = 4*((faces.Length//4)//4)
	H = 4*((faces.Length//4)//2)
	# Define lengths of string buffer for holding long string result from one parallel task.
	vert_str_len = mesh.Vertices.Count//4*((precision+2)*3 + (c_precision+1)*3 + 3 + 2)
	text_str_len = mesh.TextureCoordinates.Count//2*((precision+2)*2 + 4 + 2)
	norm_str_len = mesh.Normals.Count//4*((precision+2)*3 + 4 + 2)
	face_str_len = mesh.Faces.Count//8*((precision+1)*9 + 3 + 2)
	#
	# Define function that selects number-to-string DLL converter for each parallel task.
	#
	def function(i):
		global time0,time1,time2,time3,time4,time5,time6,time7,time8,time9,time10,time11,time12,time13,\
			time14,time15,time16,time17,time18,time19,time20,time21,time22,time23,time24,time25,time26,time27,\
			time28,time29,time30,time31,time32,time33,time34,time35
		if i == 0:
			time0 = time()
			cstrv1 = create_string_buffer(vert_str_len)
			cverts1 = (c_float * (Qv))(); cverts1[:] = verts[:Qv]
			ccolors1 = (c_int * (Qc))(); ccolors1[:] = colors[:Qc]
			csizev1 = c_int(0)
			soMesh2String.vertex2string1(precision, c_precision, cverts1, ccolors1, Qv, Qc, cstrv1, c_byref(csizev1))
			# Write results from first vertex group to file as part of parallel task.
			# Only this task can write to the file and be in the correct order.
			# Writes from following tasks have to wait until all tasks are complete.
			soMesh2String.verts2file(obj_fpath, cstrv1, csizev1.value)
			time1 = time()
			return [cstrv1,csizev1.value]
		elif i == 1:
			time2 = time()
			cstrv2 = create_string_buffer(vert_str_len)
			cverts2 = (c_float * (Hv - Qv))(); cverts2[:] = verts[Qv:Hv]
			ccolors2 = (c_int * (Hc - Qc))(); ccolors2[:] = colors[Qc:Hc]
			csizev2 = c_int(0)
			soMesh2String.vertex2string2(precision, c_precision, cverts2, ccolors2, Hv-Qv, Hc-Qc, cstrv2, c_byref(csizev2))
			time3 = time()
			return [cstrv2,csizev2.value]
		elif i == 2:
			time4 = time()
			cstrv3 = create_string_buffer(vert_str_len)
			cverts3 = (c_float * (Qv))(); cverts3[:] = verts[Hv:Hv+Qv]
			ccolors3 = (c_int * (Qc))(); ccolors3[:] = colors[Hc:Hc+Qc]
			csizev3 = c_int(0)
			soMesh2String.vertex2string3(precision, c_precision, cverts3, ccolors3, Qv, Qc, cstrv3, c_byref(csizev3))
			time5 = time()
			return [cstrv3,csizev3.value]
		elif i == 3:
			time6 = time()
			cstrv4 = create_string_buffer(vert_str_len)
			cverts4 = (c_float * (verts.Length - Hv - Qv))(); cverts4[:] = verts[Hv+Qv:verts.Length]
			ccolors4 = (c_int * (num_colors - Hc - Qc))(); ccolors4[:] = colors[Hc+Qc:num_colors]
			csizev4 = c_int(0)
			soMesh2String.vertex2string4(precision, c_precision, cverts4, ccolors4, verts.Length-Hv-Qv, num_colors-Hc-Qc, cstrv4, c_byref(csizev4))
			time7 = time()
			return [cstrv4,csizev4.value]
		elif i == 4:
			time8 = time()
			cstrt1 = create_string_buffer(text_str_len)
			ctcs1 = (c_float * (Ht))(); ctcs1[:] = tcs[:Ht]
			csizet1 = c_int(0)
			soMesh2String.textures2string1(precision, ctcs1, Ht, cstrt1, c_byref(csizet1))
			time9 = time()
			return [cstrt1,csizet1.value]
		elif i == 5:
			time10 = time()
			cstrt2 = create_string_buffer(text_str_len)
			ctcs2 = (c_float * (tcs.Length - Ht))(); ctcs2[:] = tcs[Ht:tcs.Length]
			csizet2 = c_int(0)
			soMesh2String.textures2string2(precision, ctcs2, tcs.Length-Ht, cstrt2, c_byref(csizet2))
			time11 = time()
			return [cstrt2,csizet2.value]
		elif i == 6:
			time12 = time()
			cstrn1 = create_string_buffer(norm_str_len)
			cnormals1 = (c_float * (Qn))(); cnormals1[:] = normals[:Qn]
			csizen1 = c_int(0)
			soMesh2String.normals2string1(precision, cnormals1, Qn, cstrn1, c_byref(csizen1))
			time13 = time()
			return [cstrn1,csizen1.value]
		elif i == 7:
			time14 = time()
			cstrn2 = create_string_buffer(norm_str_len)
			cnormals2 = (c_float * (Hn-Qn))(); cnormals2[:] = normals[Qn:Hn]
			csizen2 = c_int(0)
			soMesh2String.normals2string2(precision, cnormals2, Hn-Qn, cstrn2, c_byref(csizen2))
			time15 = time()
			return [cstrn2,csizen2.value]
		elif i == 8:
			time16 = time()
			cstrn3 = create_string_buffer(norm_str_len)
			cnormals3 = (c_float * (Qn))(); cnormals3[:] = normals[Hn:Hn+Qn]
			csizen3 = c_int(0)
			soMesh2String.normals2string3(precision, cnormals3, Qn, cstrn3, c_byref(csizen3))
			time17 = time()
			return [cstrn3,csizen3.value]
		elif i == 9:
			time18 = time()
			cstrn4 = create_string_buffer(norm_str_len)
			cnormals4 = (c_float * (normals.Length - Hn - Qn))(); cnormals4[:] = normals[Hn+Qn:normals.Length]
			csizen4 = c_int(0)
			soMesh2String.normals2string4(precision, cnormals4, normals.Length-Hn-Qn, cstrn4, c_byref(csizen4))
			time19 = time()
			return [cstrn4,csizen4.value]
		elif i == 10:
			time20 = time()
			cstrf1 = create_string_buffer(face_str_len)
			cfaces1 = (c_int * (E))(); cfaces1[:] = faces[:E]
			csizef1 = c_int(0)
			soMesh2String.faces2string1(precision, cfaces1, E, mesh.TextureCoordinates.Count, mesh.Normals.Count, cstrf1, c_byref(csizef1))
			time21 = time()
			return [cstrf1,csizef1.value]
		elif i == 11:
			time22 = time()
			cstrf2 = create_string_buffer(face_str_len)
			cfaces2 = (c_int * (Q-E))(); cfaces2[:] = faces[E:Q]
			csizef2 = c_int(0)
			soMesh2String.faces2string2(precision, cfaces2, Q-E, mesh.TextureCoordinates.Count, mesh.Normals.Count, cstrf2, c_byref(csizef2))
			time23 = time()
			return [cstrf2,csizef2.value]
		elif i == 12:
			time24 = time()
			cstrf3 = create_string_buffer(face_str_len)
			cfaces3 = (c_int * (E))(); cfaces3[:] = faces[Q:Q+E]
			csizef3 = c_int(0)
			soMesh2String.faces2string3(precision, cfaces3, E, mesh.TextureCoordinates.Count, mesh.Normals.Count, cstrf3, c_byref(csizef3))
			time25 = time()
			return [cstrf3,csizef3.value]
		elif i == 13:
			time26 = time()
			cstrf4 = create_string_buffer(face_str_len)
			cfaces4 = (c_int * (H-Q-E))(); cfaces4[:] = faces[Q+E:H]
			csizef4 = c_int(0)
			soMesh2String.faces2string4(precision, cfaces4, H-Q-E, mesh.TextureCoordinates.Count, mesh.Normals.Count, cstrf4, c_byref(csizef4))
			time27 = time()
			return [cstrf4,csizef4.value]
		elif i == 14:
			time28 = time()
			cstrf5 = create_string_buffer(face_str_len)
			cfaces5 = (c_int * (E))(); cfaces5[:] = faces[H:H+E]
			csizef5 = c_int(0)
			soMesh2String.faces2string5(precision, cfaces5, E, mesh.TextureCoordinates.Count, mesh.Normals.Count, cstrf5, c_byref(csizef5))
			time29 = time()
			return [cstrf5,csizef5.value]
		elif i == 15:
			time30 = time()
			cstrf6 = create_string_buffer(face_str_len)
			cfaces6 = (c_int * (Q-E))(); cfaces6[:] = faces[H+E:H+Q]
			csizef6 = c_int(0)
			soMesh2String.faces2string6(precision, cfaces6, Q-E, mesh.TextureCoordinates.Count, mesh.Normals.Count, cstrf6, c_byref(csizef6))
			time31 = time()
			return [cstrf6,csizef6.value]
		elif i == 16:
			time32 = time()
			cstrf7 = create_string_buffer(face_str_len)
			cfaces7 = (c_int * (E))(); cfaces7[:] = faces[H+Q:H+Q+E]
			csizef7 = c_int(0)
			soMesh2String.faces2string7(precision, cfaces7, E, mesh.TextureCoordinates.Count, mesh.Normals.Count, cstrf7, c_byref(csizef7))
			time33 = time()
			return [cstrf7,csizef7.value]
		elif i == 17:
			time34 = time()
			cstrf8 = create_string_buffer(face_str_len)
			cfaces8 = (c_int * (faces.Length-H-Q-E))(); cfaces8[:] = faces[H+Q+E:faces.Length]
			csizef8 = c_int(0)
			soMesh2String.faces2string8(precision, cfaces8, faces.Length-H-Q-E, mesh.TextureCoordinates.Count, mesh.Normals.Count, cstrf8, c_byref(csizef8))
			time35 = time()
			return [cstrf8,csizef8.value]
	timeb = time()
	# Size input and output lists for 18 entries.
	pieces, results = range(18), range(18)
	# Define helper procedure for feeding data selector to function and results to output list.
	def helper(piece): results[piece] = function(piece)
	#
	# Use parallel execution to translate mesh data to strings for output.
	# This is very effective because most of the time is spent in DLL's so
	# the Python interpreter is not a bottleneck to parallel execution.
	#
	Tasks.Parallel.ForEach(pieces, helper)
	# Use next line for debugging.
	#for piece in pieces: helper(piece)
	#
	# Show timing summary to show effectiveness of balancing parallel tasks.
	#
	print 'Time to run parallel Tasks = {0:.4f} sec'.format(time() - timeb)
	print '    Time to export vertices1 = {0:.4f} sec.'.format(time1 -time0)
	print '    Time to export vertices2 = {0:.4f} sec.'.format(time3 -time2)
	print '    Time to export vertices3 = {0:.4f} sec.'.format(time5 -time4)
	print '    Time to export vertices4 = {0:.4f} sec.'.format(time7 -time6)
	print '    Time to export textures1 = {0:.4f} sec.'.format(time9 -time8)
	print '    Time to export textures2 = {0:.4f} sec.'.format(time11 -time10)
	print '    Time to export normals1 = {0:.4f} sec.'.format(time13 -time12)
	print '    Time to export normals2 = {0:.4f} sec.'.format(time15 -time14)
	print '    Time to export normals3 = {0:.4f} sec.'.format(time17 -time16)
	print '    Time to export normals4 = {0:.4f} sec.'.format(time19 -time18)
	print '    Time to export faces1 = {0:.4f} sec.'.format(time21 -time20)
	print '    Time to export faces2 = {0:.4f} sec.'.format(time23 -time22)
	print '    Time to export faces3 = {0:.4f} sec.'.format(time25 -time24)
	print '    Time to export faces4 = {0:.4f} sec.'.format(time27 -time26)
	print '    Time to export faces5 = {0:.4f} sec.'.format(time29 -time28)
	print '    Time to export faces6 = {0:.4f} sec.'.format(time31 -time30)
	print '    Time to export faces7 = {0:.4f} sec.'.format(time33 -time32)
	print '    Time to export faces8 = {0:.4f} sec.'.format(time35 -time34)
	#
	# Call DLL to write data from tasks 1 to 17 to file.  Results from task0 have already been written.
	#
	timews = time()
	soMesh2String.data2file(obj_fpath, results[1][0], results[1][1], results[2][0],
	results[2][1], results[3][0], results[3][1], results[4][0], results[4][1],
	results[5][0], results[5][1], results[6][0], results[6][1], results[7][0],
	results[7][1], results[8][0], results[8][1], results[8][0], results[9][1],
	results[10][0], results[10][1], results[11][0], results[11][1],
	results[12][0], results[12][1], results[13][0], results[13][1],
	results[14][0], results[14][1], results[15][0], results[15][1],
	results[16][0], results[16][1], results[17][0], results[17][1])
	timew = time()
	print 'Time to write data to file = {0:.4f} sec.'.format(timew - timews)
	
	print 'Total time = {0:.4f} sec'.format(timew-timea)


# The simple code here for testing the export_obj script expects that 1 mesh is displayed.  This mesh is exported to the .obj file.
from rhinoscriptsyntax import ObjectsByType, VisibleObjects
# This is used in place of rs.coercegeometry.
def getGeo(id): return doc.Objects.Find(id).Geometry
# Get visible meshes.
meshes = [mesh for mesh in ObjectsByType(32, select=False) if (mesh in VisibleObjects())]
# Report error for too few or too many visible meshes.
if len(meshes) == 0: print 'ERROR: No visible mesh found. Please fix and try again.'; exit()
if len(meshes) > 1: print 'ERROR: {} visible meshes found. Only 1 mesh is supported. Please fix and try again.'.format(len(meshes)); exit()
# Get the 1 visible mesh and its geometry.
mesh = meshes[0]
meshGeo = getGeo(mesh)
# Export the mesh.
#export_obj(meshGeo, "C:\Users\Terry\Documents", "myobj")
export_obj(meshGeo, 'D:\Photoscan', 'myobj')

The text for the DLL is too large to include in its entirety here but here is its starting portion.

// Mesh2String.cpp : Defines the exported functions for the DLL application.
//

#include "stdafx.h"
#include <iostream>
#include <fstream>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

using namespace std;

#define DLLEXPORT extern "C" __declspec(dllexport)
#define UCLASS()
#define GENERATED_BODY()
#define UFUNCTION()

DLLEXPORT void vertex2string1(int precision, int c_precision, float *verts, int *colors, int num_verts, int num_colors, char *str, long &size_of_line) {
	static int pow10[10] = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 };
	// Used to convert to string.
	int a, b, c, k, l, m, i, j, i_begin, A, R, G, B;
	float f;
	// Process vertices.
	// Save starting value of i.
	i = 0;
	i_begin = i;
	for (j = 0; j < num_verts; j += 3) {
		// Do first value.
		f = verts[j];
		// Start vertex line with 'v '.
		str[i++] = 'v';
		str[i++] = ' ';
		// check for negative float
		if (f < 0.0) {
			str[i++] = '-';
			f *= -1;
		}
		// Extract the whole number.
		a = f;
		// Extract the decimal part.
		f -= a;
		k = precision;
		// Get the number of digits in the whole number = k+1.
		// This loop finishes sooner when there are more digits.
		while (k > -1) {
			l = pow10[k];
			m = a / l;
			if (m > 0) { break; }
			k--;
		}
		// Extract most significant digit and concatenate to string obtained as
		// quotient by dividing number by 10^k where k = (number of digit - 1)
		for (l = k + 1; l > 0; l--) {
			b = pow10[l - 1];
			c = a / b;
			str[i++] = c + 48;
			a %= b;
		}
		// Add decimal point.
		str[i++] = '.';
		// Extract decimal digits till precision met.
		for (l = 0; l < precision - k - 1; l++) {
			f *= 10.0;
			b = f;
			str[i++] = b + 48;
			f -= b;
		}
		// Do second value.
		f = verts[j + 1];
		str[i++] = ' ';
		// check for negative float
		if (f < 0.0) {
			str[i++] = '-';
			f *= -1;
		}
		// Extract the whole number.
		a = f;
		// Extract the decimal part.
		f -= a;
		k = precision;
		// Get the number of digits in the whole number = k+1.
		while (k > -1) {
			l = pow10[k];
			m = a / l;
			if (m > 0) { break; }
			k--;
		}
		// Extract most significant digit and concatenate to string obtained as
		// quotient by dividing number by 10^k where k = (number of digit - 1)
		for (l = k + 1; l > 0; l--) {
			b = pow10[l - 1];
			c = a / b;
			str[i++] = c + 48;
			a %= b;
		}
		// Add decimal point.
		str[i++] = '.';
		// Extract decimal digits till precision met.
		for (l = 0; l < precision - k - 1; l++) {
			f *= 10.0;
			b = f;
			str[i++] = b + 48;
			f -= b;
		}
		// Do third value.
		f = verts[j + 2];
		str[i++] = ' ';
		// check for negative float
		if (f < 0.0) {
			str[i++] = '-';
			f *= -1;
		}
		// Extract the whole number.
		a = f;
		// Extract the decimal part.
		f -= a;
		k = precision;
		// Get the number of digits in the whole number = k+1.
		while (k > -1) {
			l = pow10[k];
			m = a / l;
			if (m > 0) { break; }
			k--;
		}
		// Extract most significant digit and concatenate to string obtained as
		// quotient by dividing number by 10^k where k = (number of digit - 1)
		for (l = k + 1; l > 0; l--) {
			b = pow10[l - 1];
			c = a / b;
			str[i++] = c + 48;
			a %= b;
		}
		// Add decimal point.
		str[i++] = '.';
		// Extract decimal digits till precision met.
		for (l = 0; l < precision - k - 1; l++) {
			f *= 10.0;
			b = f;
			str[i++] = b + 48;
			f -= b;
		}
		// Add vertex color if present.
		if (num_colors) {
			// Put space after Z of vertex.
			str[i++] = ' ';
			// Get 32-bit color to convert.
			a = colors[j / 3];
			// Convert to normalize R,G,B.
			// Shift off possible A.
			A = a >> 24;
			a -= (A << 24);
			R = a >> 16;
			a -= (R << 16);
			G = a >> 8;
			B = a - (G << 8);
			// Do first value.
			a = R;
			k = c_precision;
			// Get the number of digits in the whole number = k+1.
			while (k > -1) {
				l = pow10[k];
				m = a / l;
				if (m > 0) { break; }
				k--;
			}
			// Extract most significant digit and concatenate to string obtained as
			// quotient by dividing number by 10^k where k = (number of digit - 1)
			for (l = k + 1; l > 0; l--) {
				b = pow10[l - 1];
				c = a / b;
				str[i++] = c + 48;
				a %= b;
			}
			// Do second value.
			// Put space after R.
			str[i++] = ' ';
			a = G;
			k = c_precision;
			// Get the number of digits in the whole number = k+1.
			while (k > -1) {
				l = pow10[k];
				m = a / l;
				if (m > 0) { break; }
				k--;
			}
			// Extract most significant digit and concatenate to string obtained as
			// quotient by dividing number by 10^k where k = (number of digit - 1)
			for (l = k + 1; l > 0; l--) {
				b = pow10[l - 1];
				c = a / b;
				str[i++] = c + 48;
				a %= b;
			}
			// Do third value.
			// Put space after G.
			str[i++] = ' ';
			a = B;
			k = c_precision;
			// Get the number of digits in the whole number = k+1.
			while (k > -1) {
				l = pow10[k];
				m = a / l;
				if (m > 0) { break; }
				k--;
			}
			// Extract most significant digit and concatenate to string obtained as
			// quotient by dividing number by 10^k where k = (number of digit - 1)
			for (l = k + 1; l > 0; l--) {
				b = pow10[l - 1];
				c = a / b;
				str[i++] = c + 48;
				a %= b;
			}
			// Add CR LF to terminate line with vertex and colors.
			str[i++] = '\r';
			str[i++] = '\n';
		}
		else {
			// Add CR LF when no color.
			str[i++] = '\r';
			str[i++] = '\n';
		}
	}
	// Write vertex lines to file.
	size_of_line = i - i_begin;
}

And here a downloadable version of the whole DLL:
Mesh2String.cpp (234.4 KB)

Let me know if you want more details.

Regards,
Terry.

1 Like

Steve,

An observation here on some of Rhino’s built-in methods for working with meshes.

The methods for extracting the mesh data vary widely in performance. While the .ToFloatArray methods for vertices, textures and normals are very quick, 0.2 sec for the 27M X,Y,Z components of the vertices in a 18.4M faces mesh, the .ToIntArray method for faces is glacial, taking 4 sec to provide 55M indices. The 20X slowdown for the.ToIntArray method does not make sense. Perhaps it was created by a different McNeel software developer at an earlier time compared to the fantastically fast .ToFloatArray methods?

It would be really great if mesh.Faces.ToIntArray() could be improved to be competitive with mesh.Vertices.ToFloatArray() and run in about 0.4 sec when generating the 55M indices of a 18.4M faces mesh with all triangular faces.

Also there is no .ToIntArray method for mesh.VertexColors which should be designed to create an array of 32-bit colors. Currently the way I do this is:

colors = [color.ToArgb() for color in mesh.VertexColors]

but this takes 3 sec for 9M colors, painfully slow compared to getting the 27M X,Y,Z components of 9M vertices in only 0.2 sec. This is a 45:1 ratio per value generated (27M in 0.2 vs 9M in 3). There is a significant opportunity for improvement here.

Combined, these two improvements might save 5.5 sec off the 13.4 sec for exporting a 18.4M faces mesh. The time would drop to 7.9 sec, a 170% speedup. This would go a long ways towards easing my pain in working with 100M faces meshes that require 73 sec to export with my best current script. It would drop this time to 43 sec, a nice improvement.

Our work on exporting a mesh to an .obj file can also be applied to importing an .obj file. Currently my best Python/DLL code imports the 18.4M faces mesh in 39 sec, 20X faster than Rhino Import. But now we have learned how effective parallelism can be for this type of task. I envision improving the import time to be similar to the export time, around 15 sec. Compared to Rhino Import, this would be more than a 50X improvement; Instead of waiting 790 sec to import the 18.4M faces mesh, it would take only 15 sec. A nice improvement. And the giving does not stop here as importing XYZRGB point clouds can also benefit. My current script for this is 15X faster than Rhino Import; getting to 50X may not be out of the question once parallelism is added.

On a related topic:

If you are just looking for a way to export/import meshes in Rhino, then it is possible to go even faster by storing the values in their native binary format. This is well known and it was easy to give it a try based upon my existing script.

To evaluate using binary format for storing the mesh value, I did the following experiment:
(1) Kept basic format of .obj file with string header and mtl file information and followed the .obj format of having each line start with v, vt, vn, f for vertex, texture coordinate, normal and face respectively.
(2) Wrote the values in 4-byte binary format without spaces between values (they are not needed for reading due to each value have a predictable fixed length).
(3) Wrote the faces without texture or normals indices since these are just a duplicate of the vertex indices.

With these changes I was able to export my 18.4M faces mesh in only 10.26 sec. The timing breakdown looks like this:

Time to load DLL = 0.0010 sec
Time to prep data = 6.6222 sec
Time to run parallel Tasks = 2.4325 sec
    Time to export vertices1 = 2.1901 sec.
    Time to export vertices2 = 2.1443 sec.
    Time to export vertices3 = 2.1074 sec.
    Time to export vertices4 = 2.1861 sec.
    Time to export textures1 = 2.4175 sec.
    Time to export textures2 = 2.4315 sec.
    Time to export normals1 = 1.9139 sec.
    Time to export normals2 = 1.9119 sec.
    Time to export normals3 = 2.0156 sec.
    Time to export normals4 = 1.9139 sec.
    Time to export faces1 = 1.7094 sec.
    Time to export faces2 = 1.7174 sec.
    Time to export faces3 = 1.7274 sec.
    Time to export faces4 = 1.7124 sec.
    Time to export faces5 = 1.7114 sec.
    Time to export faces6 = 1.7134 sec.
    Time to export faces7 = 1.7054 sec.
    Time to export faces8 = 1.7054 sec.
Time to write data to file = 1.1569 sec.
Total time = 10.2684 sec

The biggest improvement is in the file writing time. And the file size sees a big benefit, dropping from 2.2GB in size to 0.7GB. With the improvements I have outlined above for data prep, the export time could drop to 5 sec or 3.7M faces/sec of export speed.

As I have already mentioned, I plan to apply the parallel techniques to my .obj file import script and it would be easy for me to create a version that reads this format. So 5 sec export or import may be possible for this mesh. Contrast that to the 72 sec export, 790 sec import using current the Rhino Export/Import tools (which do a lot more that my script).

Of course this would be limited to Rhino-only use as no other tools support this format. I have not studied what standardized binary export formats are available for meshes. Maybe someone here on the forum knows. Perhaps we could develop a script to use that format and get similar performance to my scripts.

Please note that my scripts are incomplete as they do not handle all the details of the mtllib file. I would look to Steve for help on that part and how we can port this performance over to Rhino Mac.

Regards,
Terry.
P.S. For I/O of meshes and point clouds in Rhino, the best is yet to come.

2 Likes

:slight_smile: I can fix this

3 Likes

Great news!

RH-54455 is fixed in the latest Service Release Candidate

1 Like

Brian, @stevebaer, @diff-arch, @ivelin.peychev, @Dancergraham ,

I tested your improvement to Mesh.Faces.ToIntArray() and found it to be about 10X faster. With this speedup, my Obj export Python/DLL script has broken 10 sec for my 18M face test case:

Time to load DLL = 0.0000 sec
Time to prep data = 3.0444 sec
Time to run parallel Tasks = 2.8703 sec
    Time to export vertices1 = 2.8693 sec.
    Time to export vertices2 = 2.3427 sec.
    Time to export vertices3 = 2.7616 sec.
    Time to export vertices4 = 2.2719 sec.
    Time to export textures1 = 2.7357 sec.
    Time to export textures2 = 2.7467 sec.
    Time to export normals1 = 2.0086 sec.
    Time to export normals2 = 2.1004 sec.
    Time to export normals3 = 2.0086 sec.
    Time to export normals4 = 2.0226 sec.
    Time to export faces1 = 1.9049 sec.
    Time to export faces2 = 1.8999 sec.
    Time to export faces3 = 1.9209 sec.
    Time to export faces4 = 1.8999 sec.
    Time to export faces5 = 1.9029 sec.
    Time to export faces6 = 1.8999 sec.
    Time to export faces7 = 1.8999 sec.
    Time to export faces8 = 1.8999 sec.
 Time to write data to file D:\Photoscan\myobj.obj = 3.8832 sec.
 Total time = 9.8511 sec

This is a 26% speedup, making it faster than my old, experimental binary version that ran in 10.27 sec. The script now exports at a rate of 1.87M faces/sec! It is now 7X faster than Rhino’s Export tool.

Thanks for your wonderful, quick help in speeding up .OBJ file export. I cannot believe we broke 10 sec for my 18M face test case, quite an improvement from Rhino’s Export 72 sec.

My experimental binary version now runs in 7 sec:

Time to prep data = 3.1260 sec
Time to run parallel Tasks = 2.5931 sec
    Time to export vertices1 = 2.0256 sec.
    Time to export vertices2 = 2.0106 sec.
    Time to export vertices3 = 2.0126 sec.
    Time to export vertices4 = 2.0435 sec.
    Time to export textures1 = 2.5901 sec.
    Time to export textures2 = 2.3318 sec.
    Time to export normals1 = 2.2869 sec.
    Time to export normals2 = 1.8421 sec.
    Time to export normals3 = 1.8421 sec.
    Time to export normals4 = 1.6905 sec.
    Time to export faces1 = 2.1183 sec.
    Time to export faces2 = 1.5598 sec.
    Time to export faces3 = 1.5479 sec.
    Time to export faces4 = 1.5588 sec.
    Time to export faces5 = 1.5568 sec.
    Time to export faces6 = 1.5479 sec.
    Time to export faces7 = 2.1114 sec.
    Time to export faces8 = 1.5558 sec.
Time to write data to file = 1.2796 sec.
Total time = 7.0510 sec

I have applied similar parallel techniques to my .OBJ file import script and reduced its time from 38 to 20 sec on this 18M text case. Now it is 6X faster than Rhino’s Import tool 117 sec

Then I worked on my point cloud import script and improved its time from 49 to 25 sec on a 4GB, 74M point test case. This now makes it 30X times faster than Rhino’s Import tool which takes 790 sec. I notice that Rhino’s Export tool is also very slow to export point clouds. I do not have a script for that yet but estimate a 10-30X speed is possible.

These improved scripts greatly help me in processing the large .OBJ and Point Clouds created by Metashape from processing hundreds of drone photos to make a 3D model. Some of these models cover a 40 acres area and so require 10’s of millions of mesh faces to enable accurate measurement of construction piles and excavation volumes.

The one outstanding issue associated with my .OBJ file export that I mentioned before is:

Also there is no .ToIntArray method for mesh.VertexColors which should be designed to create an array of 32-bit colors. Currently the way I do this is:

ColorToArgb = Color.ToArgb
colors = map(ColorToArgb, mesh.VertexColors)

This takes 3 sec for 9M colors, painfully slow compared to getting the 27M X,Y,Z components of 9M vertices in only 0.2 sec. This is a 45:1 ratio per value generated (27M in 0.2 vs 9M in 3). There is a significant opportunity for improvement here.

Regards,
Terry.

5 Likes

Wow, you’ve accomplished quite an astounding performance, Terry! Maybe McNeel should implement this (and if they do, remunerate you somehow). In a couple of days, I’ll talk to a guy who might know how to port this to macOS, fingers crossed! I’ll get back to you, if we come up with a solution to this.

Thanks again for participating in this amazing discussion! It really went beyond everything that I had imagined.

well done Terry and May thanks Steve for making the toint() change so quickly! That’s astounding work! It’s amazing to see how much speed improvement is possible by selecting the right tools, methods and data structures for the task!

@diff-arch,

For the Mac, Steve indicated that the DLL needs to be replaced with a dylib. I found this reference about building a dylib:

An informative part of this reference is:

Creating the dylib

  1. Launch Xcode
    This tutorial assumes that you are using Xcode 3.1.4 from Apple.

  2. Choose “File->New Project”, or type Command-Shift-N.

  3. Select BSD Dynamic Library, and click “Choose”.

  4. Choose a project name. For this example, we’ll call it “SampleDylib”.

  5. Choose “File->New File…”

  6. Choose “C File”, from within the “Mac OS” section

  7. Name the file. For this example, we’ll call it “SampleDylib.c”. The default settings are generally correct, but if you have multiple projects open with multiple targets, make sure the proper project and target is selected. When done, click “Finish”.

  8. Open the SampleDylib.c source file.

  9. Add the code below (this is an example; place your real code here):

     #include <string.h>
     int addFunction( int a, int b ) {
        return a + b; } 
     int stringLength( char *str ) { 
        return strlen(str); }
    
  10. Save the file, then click the Build button, or press Command-B.

  11. If there were build errors, ensure that the code you entered looks exactly like it does above. Once the build is successful, continue to step 12.

  12. Find where the build was saved. If the builds folder location hasn’t been changed, it will be located in a “build” folder, next to your project file. It will be named libSampleDylib.dylib. (This is bceuase we’ve not altered the default install name in the build target.)
    Another way to locate the file is to locate the target by going to the project window, expanding libSampleDylib, then expanding Products. Select libSampleDylib.dylib, and control-click (or right-click, for those with two-button mice), and choose Reveal in Finder
    You will need to access this location later.

And here is a Realbasic example of calling this dylib:

Creating the Realbasic project

  1. Launch Real Studio. Create a new project, and choose “Desktop”

  2. Save the project to your Documents folder (so that it is next to the dylib file). For this example, we’ll name it, “Test Dylib.rb”

  3. Double click on “App” in the project window.

  4. Expand the “Events” section, and select “Open”

  5. Add the code below:

    CONST dylibLocation = "@executable_path/../Frameworks/libSampleDylib.dylib"
    Declare Function addFunction lib dylibLocation (a as integer, b as Integer) as Integer
    Declare Function stringLength lib dylibLocation (s as CString) as Integer
    msgBox "5 + 2 = " + str(AddFunction(5,2))
    msgBox "The length of ""asdf"" is " + str(stringLength("asdf"))

  6. Run your application using the “Run Paused” feature of the IDE.

  7. Copy the dylib from the Xcode build into the application bundle. You can do this by right clicking on the debug app & selecting “Show Package Contents”.
    Navigate to Contents > Mac OS & copy the dynamic library created by Xcode into the Frameworks directory in the bundle. (If the edition of Real Studio you use supports this you could use a Build Automation Step to do this for you on every compile)

  8. Now launch the debug application by double clicking it in the Finder. It should then connect to the debugger in the IDE.

  9. Notice that it correctly adds, and also correctly computes the length of the string.

Hopefully something similar to this can be put in the Python script to interface with the dylib.

Maybe this can help your Mac OS friend.

Regards,
Terry.

1 Like

@diff-arch, @stevebaer, @Dancergraham,

I added some more metrics to the mesh-to-OBJ file export script and discovered that the binary write of .OBJ file in the C++ DLL was slow, only 0.66 GB/s vs the 1.6 GB/s rating of my M.2 PCIe 3.0 x 4 SSD. I increased the default 4K write buffer-size so that about 64 transfers are needed:

const size_t bufsize = (2 << (int)log2(number_of_bytes >> 6);
unique_ptr<char[]> buf(new char[bufsize]);
ofstream out_file(file_name, ios::binary | ios::out | ios::app);
if (out_file.is_open()) {
	// Use large buffer size for 4.5X faster write, from 0.66 GB/s to 3.2 GB/s
	out_file.rdbuf()->pubsetbuf(buf.get(), bufsize); // IMPORTANT
	// Write data to file.
	out_file.write(str, number_of_bytes);

This increased the write speed to 3.2 GB/s. Using 64 transfers appears optimum over a wide range of data sizes, from 10 MB to 1000 MB. I am not sure why the write speed is now larger than the SSD rating but perhaps the buffering has something to do with it. In any case, the script now reports that exporting the 2.3 GB, 18M-faces test case to an .OBJ file takes only 5.9 sec:

    Time to get data from mesh = 2.5022 sec
    Time to run parallel Tasks = 2.6050 sec
    Time to convert 2,303,877 vertices1 to strings = 2.4265 sec.
    Time to convert 2,303,877 vertices2 to strings = 2.3138 sec.
    Time to convert 2,303,877 vertices3 to strings = 2.3487 sec.
    Time to convert 2,303,877 vertices4 to strings = 2.3378 sec.
    Time to convert 4,607,754 textures1 to strings = 2.6021 sec.
    Time to convert 4,607,754 textures2 to strings = 2.5661 sec.
    Time to convert 2,303,877 normals1 to strings = 1.9278 sec.
    Time to convert 2,303,877 normals2 to strings = 1.9189 sec.
    Time to convert 2,303,877 normals3 to strings = 1.9179 sec.
    Time to convert 2,303,877 normals4 to strings = 1.9677 sec.
    Time to convert 2,303,819 faces1 to strings = 1.9179 sec.
    Time to convert 2,303,819 faces2 to strings = 1.9298 sec.
    Time to convert 2,303,819 faces3 to strings = 1.8949 sec.
    Time to convert 2,303,819 faces4 to strings = 1.9199 sec.
    Time to convert 2,303,819 faces5 to strings = 1.9189 sec.
    Time to convert 2,303,819 faces6 to strings = 1.9179 sec.
    Time to convert 2,303,819 faces7 to strings = 1.9129 sec.
    Time to convert 2,303,819 faces8 to strings = 1.9129 sec.
Time to write 2.327 GB of data to file D:\Photoscan\myobj.obj = 0.7161 sec at 3.249 GB/sec.
Total time = 5.8753 sec

This ups the export rate to 3.14M faces/sec, a nice increase over the prior 1.87M faces/sec. This makes it 12X faster than Rhino’s Export tool; it drops a 72 sec export time to 5.9 sec!

I should caution that this script does not currently handle all cases of material export supported by Rhino’s Export Mesh-to-.OBJ tool; it will only create a .MTL file for 1 material which has to be a .jpg file. You could change this part of the script to cover additional materials or I can help you do this. Also the script will export the mesh colors, texture coordinates and vertex normals every time if they exist in the mesh. You will need to modify the script if you only want a subset of these. I am thinking of making an Eto GUI to enable selecting these before export.

Here are the updated Python script and DLL source code:

ExportObj_DLL_with_Parallel_Data_Conversion.py (16.9 KB)
Mesh2String.cpp (250.7 KB)

and here is a link to the completed DLL:

Significantly more improvement is possible if a new method is added to speed up the extraction of the mesh.Vertex.Colors. A method like .ToIntArray() that provides 32-bit colors would speed up the export 40%, dropping the 5.9 sec down to 3.5 sec or so. The .ToIntArray() method for mesh.Faces was recently improved by 10X-30X by Steve & Company, dropping the time to get 3-triangular indices/face indices to 0.4 sec and to only 0.13 sec for 4-quad indices/face for the 18M Face test case. But getting the 32-bit colors takes 2.7 sec making it a laggard. Adding mesh.VertexColors.ToIntArray() method would up the export rate to over 5M faces/sec making it really sing. This would bring the improvement over Rhino’s existing Mesh-to-.OBJ Export up to 21X, an attractive improvement.

Regards,
Terry.

2 Likes

I just found your script here… Awesome work Terry!

I was able to export a mesh with 2.5M faces in less than 2 seconds!
Unfortunately in my case the bottleneck is when importing the same mesh (specifically from Blender) which takes nearly a minute - and using a supposedly faster C++ importer did not help.

I will make some tests for medium-sized meshes, while for other cases it will be a balance for the overall export + import times.

However it’s a very remarkable achievement!

Best

Marco

Marco,

I have a another script for fast import of meshes using an .obj file. Would this be of help to you? I has two versions, an all Python version that runs 2X faster than Rhino’s import tool and a Python/C++ version for Windows that runs 30x faster. It has restrictions like my .obj file export script with regards to reading material information; it only supports .jpg texture files. The C++ version uses a DLL that could be downloaded and used in only Windows.

Do you have a .obj file from Blender that I could test? If I can get my script to work on it
then I could post the script for you to use.

I really like my script for my application of importing a mesh of terrain creater from drone photos. It has lots of X & Y variation but not so much Z. As a consequence I did some optimizations that take advantage of this. Thus it would be nice for me to see how they hold up with different types of mehes/

Regards,
Terry.

Hi Terry, thank you a lot for the reply and the additional information.

In my workflow I usually go from Rhino to Blender, so importing OBJs into Rhino does not happen very often, however it still occurs sometimes. As a a matter of fact, the biggest meshes that I work with are usually simulated milled panels that are almost flat in the Z axis, so the optimization could work in a way similar to your 3D scans.

If you’d like to share the script(s) I’ll be happy to make a few tests and give you some reports.

I’m also wondering if one of the import scripts could be somehow adapted to work from Blender (which uses Python as well), which at the moment is quite slow at importing big meshes.

By the way, the limitations of the scripts should not be a problem, as I’m working on Windows and I’m not transferring textures - just texture maps.

Thank you again!

Marco