14

So I've written a script which does this:

Before:

Input .stl After:

Output .stl

The code is:

#fname - filename of imported .stl
#thic - required thickness of output model
#txt - ascii or binary stl
#cyc - cycles of smoothing
#cut - times triangle divided
#fac - smoothing factor
#per - smoothing reps
#bbX - bounding box X
#bbY - bounding box Y
#bbZ - bounding box Z
#trX, trY, trZ - count of elements in array
def s_surface(fname, thic, txt, cyc, cut, fac, rep, bbX, bbY, bbZ, trX, trY, trZ ):
    bb = [[(bbX/2, 0, 0), (1,0,0), True, False], 
          [(-1*bbX/2, 0, 0), (1,0,0), False, True],
          [(0, bbY/2, 0), (0,1,0), True, False], 
          [(0,-1*bbY/2, 0), (0,1,0), False, True],
          [(0, 0, bbZ/2), (0,0,1), True, False], 
          [(0, 0, -1*bbZ/2), (0,0,1), False, True]
         ]
    tr = [trX, trY, trZ]
    bpy.ops.import_mesh.stl(filepath=fname)
    ob_new = bpy.context.selected_objects[0]
    bpy.context.scene.objects.active = ob_new
    bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN')
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.normals_make_consistent(inside=False)
    bpy.ops.mesh.remove_doubles(threshold=0.02)
    for i in range(cyc):
        bpy.ops.mesh.subdivide(number_cuts=cut)
        bpy.ops.mesh.vertices_smooth(factor=fac, repeat=rep, xaxis=True, yaxis=True, zaxis=True)
    bpy.ops.mesh.normals_make_consistent(inside=False)
    for i in range(len(bb)):
        bpy.ops.mesh.bisect(plane_co=bb[i][0],plane_no=bb[i][1], clear_outer=bb[i][2],clear_inner=bb[i][3]) 
        bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.normals_make_consistent(inside=False)
    bpy.ops.object.mode_set(mode='OBJECT')
    for i in range(3):
        bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN')
        bpy.ops.object.modifier_add(type='ARRAY')
        bpy.context.object.modifiers["Array"].count = tr[i]
        bpy.context.object.modifiers["Array"].relative_offset_displace[0] = 0
        bpy.context.object.modifiers["Array"].relative_offset_displace[i] = 1
        bpy.context.object.modifiers["Array"].use_merge_vertices = True
        bpy.context.object.modifiers["Array"].merge_threshold = 0.01
        bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Array")
    bpy.ops.object.origin_set(type='GEOMETRY_ORIGIN')
    bpy.ops.object.modifier_add(type='SOLIDIFY')
    bpy.context.object.modifiers["Solidify"].thickness = thic
    bpy.context.object.modifiers["Solidify"].use_quality_normals = True
    bpy.context.object.modifiers["Solidify"].use_even_offset = True
    bpy.ops.object.modifier_apply(apply_as='DATA', modifier="Solidify")
    nfname = ""
    nfname = fname[0:-4] + ".stl"
    bpy.ops.export_mesh.stl(filepath=nfname, ascii = txt)

Surfaces I am trying to smooth are supposed to be triply periodic minimal surfaces, so it means that after smoothing is applied mean curvature is meant to be 0 in all vertexes of a mesh. How can I calculate mean curvature to check if smoothing is done right?

Mike
  • 191
  • 1
  • 8
  • The amount of bpy.ops commands used is scary. You might attract a more proficient audience if you rewrote the codebase using the bmesh module. (However, I don't know if all commands [bisect e.g.] are directly translatable.) – Leander Aug 01 '19 at 13:02
  • @Leander The code part by @Mike containing all the bpy.ops is actually not defining or really relevant to the question, since this is a question which could be dealt with in a manner which is entirely independent from the surface shape. – Robert Roth Aug 01 '19 at 13:13
  • 1
    If the code is not relevant, he could simply strip it from the question. – Leander Aug 01 '19 at 13:17
  • 2
    Sounds like you're looking for an algorithm, in which case you may be better off asking on math.se or computergraphics.se. As far as I know Blender doesn't come with any tool which does what you're looking for. – gandalf3 Aug 01 '19 at 22:09
  • 1
    @gandalf3 Thank you! This sounds like a reasonable method: https://computergraphics.stackexchange.com/questions/1718/what-is-the-simplest-way-to-compute-principal-curvature-for-a-mesh-triangle – Robert Roth Aug 02 '19 at 07:00
  • @gandalf3 It seems like something like this is implemented in a python package called trimesh for example: discrete_mean_curvature_measure(mesh, points, radius) https://github.com/mikedh/trimesh/blob/master/trimesh/curvature.py – Robert Roth Aug 02 '19 at 07:07
  • @RobertRoth A link you provided is for calculating principal curvature. Are "mean" and "principle" curvature the same thing? (not good at english math defenitions) – Mike Aug 02 '19 at 07:23
  • @Leander Why using bpy.ops is bad? – Mike Aug 02 '19 at 07:24
  • 2
    As with all things "bad" is probably not the most correct description, there are cases where you want to use bpy.ops. Most coders write blender scripts without bpy.ops, since it is slower, context-sensitive and more difficult to read. This answer provides some generalized explanation. Every bpy.ops triggers a scene update. – Leander Aug 02 '19 at 07:29
  • @Leander thank you. – Mike Aug 02 '19 at 07:32
  • 1
    It seems that you have found a solution already. Please add it as an answer instead of into the question. It is not uncommon to answer your own question once you have found your solution. Adding it as an answer makes it easier for others to find it and enables them to upvote it. – Leander Aug 02 '19 at 10:32

3 Answers3

11

This is an answer based on this Computer Graphics SE indicated in the comments (the one by Nathan Reed).

The math is described in this answer, but in short the curvature is calculated by vertex so:

  • Get all edges from this vertex
  • And for each edge compare the projection along the edge of the normals at it extremities
  • Take the mean of all that

Now, as didn't want to just code a copy of it, I propose a little enhancement:

The calculation is also vertex centered, but we go along each of its surrounding face (need to be triangulated), to calculate a curvature weighted by the angle of this face at the vertex we are calculating around.

Once the sum done over all faces, we mean this sum by the total angle around the vertex.

Making so, the curvature of a large angle will be more important than for a small angle. This is like taking the integral of the curvatures around the vertex.

I think this may be close to the theoretical calculus as it consist of taking the curvature for each cutting plane turning around the vertex normal.

In both algorithms, the base calculus is the same and this corresponds to the CGSE answer:

def curvature_along_edge( vert, other ):
    normal_diff = other.normal - vert.normal
    vert_diff = other.co - vert.co
    return normal_diff.dot( vert_diff ) / vert_diff.length_squared

But in the case we want to use angles, we need to organize a surrounding loop (ring) over the vertex for which we want the curvature, to have the several angles with the good signs.

For instance here:

enter image description here

We want the ring/loop:

  • For vertex 0: 5, 8, 4
  • For vertex 8: 1, 6, 3, 7, 2, 4, 0, 5, (1)

etc.

These loops are counterclockwise, as vertices of Blender's polygons are.

This is done by:

# Get vertices in the face order but starting from a given vert
def following_verts_of_vert( vert, face ):
    i0 = index_of( vert, face.verts )
    i1 = (i0 + 1) % 3
    i2 = (i0 + 2) % 3
    return face.verts[i0], face.verts[i1], face.verts[i2]

# Create the oriented ring around vert
def ring_from_vert( vert ):
    vertices = []
    for face in vert.link_faces:
        i0, i1, i2 = following_verts_of_vert( vert, face )
        vertices.append( [i1, i2] )
    result = vertices[0]    
    added = True
    while added and len(vertices):
        added = False
        prev = search_link( result[0], vertices, 1 )
        if prev:
            result = [prev[0]] + result
            vertices.remove( prev )
            added = True
        next = search_link( result[-1], vertices, 0 )
        if next and next[1] not in result:
            result.append( next[1] )
            vertices.remove( next )
            added = True
    return result

(this code above is not really needed, or could be optimized, but was my first though to have a constant orientation reference for the curvature)

So that finally, the mean curvature around a vertex is calculated by:

def angle_between_edges( vert, other1, other2 ):
    edge1 = other1.co - vert.co
    edge2 = other2.co - vert.co
    product = edge1.cross( edge2 )
    sinus = product.length / (edge1.length * edge2.length)
    return asin( min(1.0, sinus) )

def mean_curvature_vert( vert ):
    ring = ring_from_vert( vert )
    ring_curvatures = [curvature_along_edge( vert, other ) for other in ring]
    total_angle = 0.0
    curvature = 0.0
    for i in range(len(ring)-1):
        angle = angle_between_edges( vert, ring[i], ring[i+1] )
        total_angle += angle
        curvature += angle * (ring_curvatures[i] + ring_curvatures[i+1])

    return curvature / (2.0 * total_angle)

Here is the compared results:

enter image description here

They are close, but the comparison I've used needs some explanation: the resulting curvatures are normalized so that they fit to the interval for vertex groups [0, 1]:

In short, this vertex group assignment compares the contrasts but not the values.

This is done so:

def assign_to_vertex_group( obj, group_name, curvatures ):
    vertex_group = ensure_vertex_group( obj, group_name )

    curvatures = [abs(c) for c in curvatures]

    min_curvature = min( curvatures )
    max_curvature = max( curvatures )
    vg_fac = 1.0 / (max_curvature - min_curvature) if max_curvature != min_curvature else 1.0

    for i, vert in enumerate( obj.data.vertices ):
        vertex_group.add( [vert.index], (curvatures[i] - min_curvature) * vg_fac, 'REPLACE' )

Here is the blend file with the 2 scripts named : cgseSimple and cgseSimple2.

Note:

Also included Mike's implementation (mike text) + the same rewritten a bit (mikesPaper), for understanding and comparison purpose. But from all that, it is hard to know what is the more accurate (how to determinate which is true?). I was not able to understand how this algo is able to determinate curvature orientation (convex vs. concave inclinations).

Also tested all that on big meshes (not included due to the blend file size limit). But that accentuate the diff between the different approaches.

lemon
  • 60,295
  • 3
  • 66
  • 136
  • Thank you @lemon! :) I will try it out tomorrow! – Robert Roth Aug 04 '19 at 16:41
  • @RobertRoth, you're welcome. Do you have meshes with known curvatures for testing? – lemon Aug 04 '19 at 17:32
  • 1
    Your code works extremely well! It is also very fast! You might want to post a comment on CG-SE with a link to your answer, as i'm certain that they will also like it! Here are some surfaces with known curvatures: https://imgur.com/a/UYxGIoP – Robert Roth Aug 05 '19 at 15:14
  • @RobertRoth, thanks. Could be optimized a bit (a part is unnecessary, that's indicated in the answer). Thank you for the image. Will try to check that when possible. For CGSE... maybe will ask them if this 'angle' is of some mean in their opinion... (I think all that is really dependent of the accuracy we need to have). – lemon Aug 05 '19 at 15:43
  • Excellent answer and extremely useful code, thank you @lemon – nantille Oct 12 '19 at 15:26
  • @lemon seeing this one pop up again has reminded me of a perennial question for me: roughly approximating the principal direction of curvature, (for guiding the direction of NPR hatching/gravure. If I posted that, as a Q, would there be any chance of an answer, do you think? – Robin Betts Dec 07 '21 at 10:53
  • @RobinBetts, in order to use it for shading, right? Getting the information from a precalculated vertex group (gn if possible or others)? – lemon Dec 07 '21 at 11:01
  • @Lemon Yup.. I'm thinking precalculated vertex colors, encoding XYZ in any space.. – Robin Betts Dec 07 '21 at 11:53
  • @RobinBetts, so the question may be about the shader? (as this answer gives a vertex group) – lemon Dec 07 '21 at 12:00
  • Oh vertex colors... well, same principle.. – lemon Dec 07 '21 at 12:08
  • I think this answer might be pretty close.. using the same basic mechanism, extract an approximation of the directions in which the curvature is steepest and shallowest .. – Robin Betts Dec 07 '21 at 12:18
  • have you tried fitting quadric surface on surrounding verts and calculate the curvature that way? it might fit better for the triangles? – JGrey Dec 21 '23 at 17:57
  • @JGrey... old subject, you know... maybe yes. Give it a try ?! – lemon Dec 21 '23 at 18:19
4

So I found an article on computing curvatures of triangular meshes.

And wrote this piece:

import bpy
import math
import mathutils

def create_pairs(k): tr = [] bpy.ops.object.mode_set(mode='OBJECT') obj = bpy.context.active_object bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_mode(type="VERT") bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') obj.data.vertices[k].select = True bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_more() bpy.ops.object.mode_set(mode='OBJECT') polys = [i.index for i in bpy.context.active_object.data.polygons if i.select]

polys2 =[]
for i in polys:
    tr = [obj.data.polygons[i].vertices[0],
          obj.data.polygons[i].vertices[1],
          obj.data.polygons[i].vertices[2]]
    tr.insert(0,tr.pop(tr.index(k)))      
    polys2.append(tr)
triang = []
tr = []
for i in range(len(polys2)):
    for j in range(len(polys2)):
        if len(set(polys2[i])&set(polys2[j])) == 2 and i != j:
           if  i not in triang: 
               triang.append(i)
               triang.append(j)
               tr.append([polys2[i], polys2[j]])
return tr

def cot(pair): obj = bpy.context.active_object p = list(set(pair[0]) & set(pair[1])) ab = list(set(pair[0]) - set(pair[1])) + list(set(pair[1]) - set(pair[0])) vec_a1 = obj.data.vertices[p[0]].co - obj.data.vertices[ab[0]].co vec_a2 = obj.data.vertices[ab[0]].co - obj.data.vertices[p[1]].co vec_b1 = obj.data.vertices[p[0]].co - obj.data.vertices[ab[1]].co vec_b2 = obj.data.vertices[ab[1]].co - obj.data.vertices[p[1]].co cos_a = (vec_a1.x * vec_a2.x + vec_a1.y * vec_a2.y + vec_a1.z * vec_a2.z)/(math.sqrt(vec_a1.x2 + vec_a1.y2 + vec_a1.z2)* math.sqrt(vec_a2.x2 + vec_a2.y2 + vec_a2.z2)) cos_b = (vec_b1.x * vec_b2.x + vec_b1.y * vec_b2.y + vec_b1.z * vec_b2.z)/(math.sqrt(vec_b1.x2 + vec_b1.y2 + vec_b1.z2)* math.sqrt(vec_b2.x2 + vec_b2.y2 + vec_b2.z2)) alpha = cos_a/(math.sqrt(1-cos_a2)) beta = cos_b/(math.sqrt(1-cos_b2)) return alpha + beta

def sq_norm(pair): obj = bpy.context.active_object p = list(set(pair[0]) & set(pair[1])) return (obj.matrix_world * (obj.data.vertices[p[0]].co - obj.data.vertices[p[1]].co)).length**2

def com_edge(pair): obj = bpy.context.active_object p = list(set(pair[0]) & set(pair[1])) return (obj.matrix_world * (obj.data.vertices[p[0]].co - obj.data.vertices[p[1]].co))

def v_area(ring): v_a = 0 for i in range(len(ring)): v_a = v_a + cot(ring[i]) * sq_norm(ring[i]) v_a = 0.125*v_a return v_a

def mean_curvature(k): ring = create_pairs(k) v_area(ring) mean = mathutils.Vector((0,0,0)) for i in range(len(ring)): mean = mean + cot(ring[i]) * com_edge(ring[i]) mean = 0.5 * (0.5 * v_area(ring) * mean).length return mean

obj = bpy.context.active_object for k in range(len(obj.data.vertices)): print(mean_curvature(k))

Computes mean curvature at each vertex of a mesh, needs tweaking - not working with obtuse triangles, other than that seems to get the job done.

Edit: fixed bugs, beatified code

Mike
  • 191
  • 1
  • 8
  • Two questions... what is the mean using obj.matrix_world (all is fine if the object is centered to origin, but if not...)? second point but more important: in a mesh like in the question, borders should have a big mean curvature, I think (in the way this algorithm is done), but how are they (border vertices) considered here, I don't see the point? – lemon Aug 02 '19 at 17:42
  • The code didn't work for me just yet. I added a small part which sets vertex weights depending on the calculated value but all i get is noise as seen in the picture. https://imgur.com/a/878bwcH Am i using the code wrongly? This is the code i used with your functions: `obj = bpy.context.active_object vg = obj.vertex_groups.new("VertexGroup")

    for k in range(len(obj.data.vertices)): meanCurvature = mean_curvature(k) print(meanCurvature) bpy.ops.object.mode_set(mode='OBJECT') vg.add([k], meanCurvature/10, "ADD")`

    – Robert Roth Aug 02 '19 at 18:39
  • @lemon for your first question - in my input data the object is always centered to origin. for - second - I don't really understand what you mean. – Mike Aug 02 '19 at 19:05
  • @RobertRoth Is your mesh triangular? Also the triangles should not be obtuse. And why are you dividing by 10? vg.add([k], meanCurvature/10, "ADD") – Mike Aug 02 '19 at 19:09
  • @Mike The mesh is entirely triangular and the triangles should also be obtuse everywhere (exept maybe on the sidefaces) https://imgur.com/a/jjhPuSy I divided by ten just to fit the results to the visible blue=0 to red=1 scale of vertex weight (rather than all vertices being red because many curvature values are larger than one) – Robert Roth Aug 02 '19 at 21:07
  • @Mike, what I mean is the borders/sides of the mesh can't have a low mean curvature, am I wrong? The ones marked in red (seam) here: https://i.stack.imgur.com/n8ii4.jpg (I mean they could if (min+max)/2 but in your algorithm this is not calculated like that, if understand well) – lemon Aug 03 '19 at 08:11
  • 1
    @RobertRoth, the randoms you have are due to 'obj.matrix_world *' which should be removed. No mean here to consider the object transformation matrix (except for scales but?) as we talk about vectors here, not locations. – lemon Aug 03 '19 at 14:21
  • @lemon While removing both of the obj.matrix_world * might be correct, this did not change the calculated values, the noise stays exacly the same... exactly like: Here – Robert Roth Aug 03 '19 at 16:24
  • 1
    @RobertRoth, ok, noticed matrix changes all is the object is not at the center. I've done few experiments and the DMSB_III paper is not so simple (for me). But CGSE answers are more simple and the first one gives coherent results. Mike, all this is nothing against your answer, just try to understand things. – lemon Aug 03 '19 at 17:00
  • 2
    @RobertRoth I don't understand what causing this noise, and I get the same result: here I am looking for what causing this issue – Mike Aug 03 '19 at 17:13
3

OpenMesh version of Nathan Reed and lemon: enter image description here

import numpy as np
import openmesh as om  
from vedo import *

def array_angle(array1, array2): """ INPUT: N x 2/3/... """

res = np.sum(array1 * array2, axis=1)
res /= np.linalg.norm(array1, axis=1)
res /= np.linalg.norm(array2, axis=1)
res = np.clip(res, -1.0, 1.0)
res = np.arccos(res)
return res


def run(path_mesh): mesh_om = om.read_trimesh(path_mesh, vertex_normal=True) v = mesh_om.points() f = mesh_om.face_vertex_indices() n = mesh_om.vertex_normals()

if True:
    """
    https://computergraphics.stackexchange.com/a/1719/17639
    """
    e = mesh_om.ev_indices()
    ev = v[e[:, 1]] - v[e[:, 0]]
    en = n[e[:, 1]] - n[e[:, 0]]
    e_curv = np.sum(ev * en, axis=1) / np.linalg.norm(ev, axis=1)
    v_curv = np.zeros(v.shape[0])
    for idx_e in range(e.shape[0]):
        v_curv[e[idx_e, 0]] += e_curv[idx_e]
        v_curv[e[idx_e, 1]] += e_curv[idx_e]
else:
    """
    https://blender.stackexchange.com/a/147371/82691
    """
    vf_indices = mesh_om.vf_indices()
    ev_indices = mesh_om.ev_indices()
    fe_indices = mesh_om.fe_indices()

    ev_01 = v[ev_indices[:, 1]] - v[ev_indices[:, 0]]
    en_01 = n[ev_indices[:, 1]] - n[ev_indices[:, 0]]
    e_curv = np.sum(ev_01 * en_01, axis=1) / np.linalg.norm(ev_01, axis=1)

    e_01 = v[f[:, 1]] - v[f[:, 0]]
    e_12 = v[f[:, 2]] - v[f[:, 1]]
    e_20 = v[f[:, 0]] - v[f[:, 2]]

    f_angles = np.zeros_like(f).astype(np.float32)
    f_angles[:, 0] = array_angle(e_01, -e_20)
    f_angles[:, 1] = array_angle(-e_01, e_12)
    f_angles[:, 2] = array_angle(e_20, -e_12)

    v_curv = np.zeros(v.shape[0])
    for idx_v in range(v.shape[0]):
        va = 0
        for i in range(vf_indices.shape[1]):
            idx_f = vf_indices[idx_v, i]
            if idx_f == -1:
                break
            idx_v_in_f = np.where(f[idx_f] == idx_v)[0][0]
            a = f_angles[idx_f, idx_v_in_f]
            va += a
            idx_e0 = fe_indices[idx_f, (0+idx_v_in_f) % 3]
            idx_e2 = fe_indices[idx_f, (2+idx_v_in_f) % 3]
            v_curv[idx_v] += a * (e_curv[idx_e0] + e_curv[idx_e2])
        v_curv[idx_v] /= (2 * va)

if True:
    #! vis
    mean_v_curv = np.mean(v_curv)
    std_v_curv = np.std(v_curv)
    k_min = mean_v_curv - 3 * std_v_curv
    k_max = mean_v_curv + 3 * std_v_curv
    v_curv = np.clip(v_curv, k_min, k_max)

    M = Mesh([v, f])
    M.cmap('jet', v_curv, vmin=k_min, vmax=k_max, on="points").addScalarBar()
    M.show(resetcam=True)


if name == "main": path_mesh = "/home/lab9/Mesh/bunny.obj" run(path_mesh)

When reading with Openmesh, the following message appears, indicating that the mesh may be flawed. At this time, the values of vertices and faces read by Openmesh and Blender may be different, and preprocessing may be required.

PolyMeshT::add_face: complex vertex
PolyMeshT::add_face: complex edge
PolyMeshT::add_face: patch re-linking failed

PolyMeshT::add_face: complex vertex is usually caused by the "wrong" faces, so we just need to delete some of the faces.

One possible solution is to use igl to read the mesh(keep the content of the file) with texcoord and normal(excluded here), and use openmesh to form the mesh, which will automatically delete the bad faces, we can calculate the mask and select valid f/ft/fn from the original array.

import numpy as np
import igl # conda install -c conda-forge igl
import openmesh as om

def write_mesh(path_mesh, v, vc=None, vt=None, vn=None, f=None, ft=None, fn=None, str_mtllib="", str_usemtl=""): assert v.ndim == 2 and v.shape[1] == 3, ("\033[1;31mv.shape=%s\n\033[0m") % str(v.shape) if vc is not None and len(vc) == 0: vc = None #! vc = [] if vt is not None and len(vt) == 0: vt = None if vn is not None and len(vn) == 0: vn = None if ft is not None and len(ft) == 0: ft = None if fn is not None and len(fn) == 0: fn = None

with open(path_mesh, "w") as fp:
    if str_mtllib != "":
        fp.write("mtllib " + str_mtllib + "\n")

    if vc is not None:
        v = np.concatenate([v, vc / 255.0], 1).reshape(-1, 6)
        fp.write(("v {:f} {:f} {:f} {:.3f} {:.3f} {:.3f}\n" * v.shape[0]).format(*v.reshape(-1)))
    else:
        fp.write(("v {:f} {:f} {:f}\n" * v.shape[0]).format(*v.reshape(-1)))

    if vt is not None:
        fp.write(("vt {:f} {:f}\n" * vt.shape[0]).format(*vt.reshape(-1)))

    if vn is not None:
        fp.write(("vn {:f} {:f} {:f}\n" * vn.shape[0]).format(*vn.reshape(-1)))

    if str_usemtl != "":
        fp.write("usemtl " + str_usemtl + "\n")

    if (ft is not None) and (fn is not None) and (f is not None):
        num_f = f.shape[0]
        # f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 ...
        for idx_f in range(num_f):
            fp.write(("f {:d}/{:d}/{:d} {:d}/{:d}/{:d} {:d}/{:d}/{:d}\n").format(
                f[idx_f, 0] + 1, ft[idx_f, 0] + 1, fn[idx_f, 0] + 1,
                f[idx_f, 1] + 1, ft[idx_f, 1] + 1, fn[idx_f, 1] + 1,
                f[idx_f, 2] + 1, ft[idx_f, 2] + 1, fn[idx_f, 2] + 1,
            ))
    elif (ft is None) and (fn is not None) and (f is not None):
        num_f = f.shape[0]
        # f v1//vn1 v2//vn2 v3//vn3 ...
        for idx_f in range(num_f):
            fp.write(("f {:d}//{:d} {:d}//{:d} {:d}//{:d}\n").format(
                f[idx_f, 0] + 1, fn[idx_f, 0] + 1,
                f[idx_f, 1] + 1, fn[idx_f, 1] + 1,
                f[idx_f, 2] + 1, fn[idx_f, 2] + 1,
            ))
    elif (ft is not None) and (fn is None) and (f is not None):
        num_f = f.shape[0]
        # f v1/vt1 v2/vt2 v3/vt3 ...
        for idx_f in range(num_f):
            fp.write(("f {:d}/{:d} {:d}/{:d} {:d}/{:d}\n").format(
                f[idx_f, 0] + 1, ft[idx_f, 0] + 1,
                f[idx_f, 1] + 1, ft[idx_f, 1] + 1,
                f[idx_f, 2] + 1, ft[idx_f, 2] + 1,
            ))
    elif (ft is None) and (fn is None) and (f is not None):
        # f v1 v2 v3 ....
        fp.write(("f {:d} {:d} {:d}\n" * f.shape[0]).format(*(f.reshape(-1) + 1)))


if name == "main": path_mesh = "XXX.obj" path_mesh_iglom = path_mesh[:-4] + "_IGLOM.obj"

v_igl, vt_igl, _, f_igl, ft_igl, _ = igl.read_obj(path_mesh)

#! check
if True:
    dbl_area = igl.doublearea(v_igl, f_igl)
    f_mask0 = (dbl_area > 2e-9).astype(np.bool_)  # ! 2e-9
    f_igl = f_igl[f_mask0]
    if len(vt_igl) != 0:
        ft_igl = ft_igl[f_mask0]        

f_mask = np.zeros(f_igl.shape[0]).astype(np.bool_)

mesh_iglom = om.PolyMesh(v_igl, f_igl)
v_iglom = mesh_iglom.points()
f_iglom = mesh_iglom.face_vertex_indices()

offset_f_igl = 0
for idx_f_iglom in range(f_iglom.shape[0]):
    is_true = True
    while is_true:
        is_equ = True
        is_equ &= f_igl[offset_f_igl, 0] == f_iglom[idx_f_iglom, 0]
        is_equ &= f_igl[offset_f_igl, 1] == f_iglom[idx_f_iglom, 1]
        is_equ &= f_igl[offset_f_igl, 2] == f_iglom[idx_f_iglom, 2]
        if is_equ:
            f_mask[offset_f_igl] = True
            offset_f_igl += 1
            is_true = False
        else:
            offset_f_igl += 1
if len(vt_igl) != 0:
    ft_iglom = ft_igl[f_mask]

if len(vt_igl) != 0:
    write_mesh(path_mesh_iglom, v=v_iglom, vt=vt_igl, f=f_iglom, ft=ft_iglom)
else:
    write_mesh(path_mesh_iglom, v=v_iglom, f=f_iglom)

LogWell
  • 139
  • 6