How to get exact normals when using Bevel modifier with "Harden normals"? (WYSIWYG)

I want to export meshes to my own file format, exactly the way they look in the editor. I already figured out how to get a bmesh with modifiers applied, which I also triangulate right then and there so I don’t have to deal with quads/ngons later:

depsgraph = bpy.context.evaluated_depsgraph_get()
myObjectEvaluated = myObject.evaluated_get(depsgraph)
bm = bmesh.new()
bm.from_mesh(myObjectEvaluated.data)
bmesh.ops.triangulate(bm, faces=bm.faces[:])
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
bm.faces.ensure_lookup_table()
// ...
bm.free()

When trying to figure out vertex normals, I end up here:

for face in bm.faces:
    for loop in face.loops:
        normal = face.normal // ALWAYS flat
        if face.smooth:
            normal = loop.vert.normal // ALWAYS smooth, angle-weighted

This breaks as soon as I use the Bevel modifier’s normal hardening, and will probably break when using custom split normals as well. How do I get exactly those normals which I see in the 3D view?

The loop.vert.normal value is always just the value that you would get by calculating the normal of the vertex as a function of the normals of the surrounding faces, I believe. Note that it cannot therefore represent what happens when you split normals (as Bevel harden normals does, among other operations). Because then you get different normals (possibly) at the vertex for each face around the vertex. Often it looks like there is only one (if you enable “Display Split Normals” in the overlap) but in reality there can be one per face around a vertex. So you need to access the vertex-per-face normals.

A way to do that is to find the Loop’s for a face : a Loop represents one vertex+edge as you go around a the polygon representing the face. Each loop has a “normal” property which is the normal you want. Though it seems you have to call the mesh’s calc_normal_split() function before accessing normals from Loops.

E.g., with the default cube, do a bevel with harden normals (don’t forget to enable autosmooth), then apply it, then you can play in the Python Console like:

>>> cube = bpy.data.objects['Cube']
>>> mesh = cube.data
>>> mesh.calc_normals_split()
>>> f20 = mesh.polygons[20]  # one of the bevel edge quads, between +x and +z faces
>>> list(f20.vertices)
[1, 7, 6, 2]

>>> list(f20.loop_indices)
[72, 73, 74, 75]

>>> f20loops = [mesh.loops[i] for i in f20.loop_indices]
>>> f20loops
[bpy.data.meshes['Cube'].loops[72], bpy.data.meshes['Cube'].loops[73], bpy.data.meshes['Cube'].loops[74], bpy.data.meshes['Cube'].loops[75]]

>>> f20norms = [l.normal for l in f20loops]
>>> f20norms
[Vector((2.4139881134033203e-05, -3.3938635169761255e-05, 1.0)), Vector((2.41696834564209e-05, 3.3778484066715464e-05, 1.0)), Vector((1.0, 3.393863880774006e-05, 2.3871660232543945e-05)), Vector((1.0, -3.377848770469427e-05, 2.3931264877319336e-05))]

2 Likes

Yeah it seems only mesh.polygons has the data. Bmesh seems to throw away custom normal data. I got it working now. Only have to add I have to get the mesh with to_mesh() function of object so that modifiers are applied.

oEval = myObject.evaluated_get(depsgraph)
finalMesh = oEval.to_mesh()
finalMesh.calc_normals_split() # always needed or normals are (0, 0, 0)

I hear there are dodgy scenarios where even this does not work, related to depsgraph. Haven’t encountered them yet though, so this is solved for now. Thanks!