0
$\begingroup$

I'm writing an exporter and have some problems with my normals. Relevant Code:

def mesh_triangulate(me):
    import bmesh
    bm = bmesh.new()
    bm.from_mesh(me)
    bmesh.ops.triangulate(bm, faces=bm.faces)
    bm.to_mesh(me)
    bm.free()

meshes = [m for m in context.scene.objects if m.type=='MESH']
for obj in meshes:
    # create a new mesh with all modifiers applied
    mesh = obj.to_mesh(scene, True, 'RENDER', calc_tessface=False)

    mesh_triangulate(mesh)
    indices = list()
    for poly in mesh.polygons:
        for i in poly.loop_indices:
            indices.append(mesh.loops[i].vertex_index)

    positions = list()
    for v in mesh.vertices:
        positions.extend((v.co.x, v.co.y, v.co.z))

    normals = list()
    for n in mesh.vertices:
        normals.extend((n.normal.x, n.normal.y, n.normal.z))
    bpy.data.meshes.remove(mesh)

With this I always get flat shading. When I export the same object, once with smooth and once with flat shading, the indices, positions and normals are completely the same. I would expect to have each vertex multiple times for flat shading, to that I can have different normals at the same position, but this does not happen in my approach. I also need to have all modifiers applied and a triangulated mesh.

How can this be fixed?

$\endgroup$

3 Answers 3

1
$\begingroup$

Normals (except for normal maps, and some really strange formats) are always stored one-per-face or one-per-vertex. The renderer will calculate a normal for each pixel at display-time, but that normal is not stored in the mesh.

Smooth shading (interpolation) does not add data to the mesh, which is why it's so desirable. It is like a "post-processing" step. When rendering a given pixel (smooth shaded or not), the renderer has to use the normal to calculate incidence angle, to get a shading value. If smooth shading is off, then it just uses the normal that's stored for that face to calculate the incidence angle. If smooth shading is on, then it calculates an intermediate normal for that pixel that's a "blend" of the face's normal and the normals of the adjacent faces. That temporary "in between" normal is used to calculate shading for that pixel, and then discarded. Those "in between" values aren't stored anywhere. They're always calculated at display-time.

Some file types have a flag that indicates whether the receiving system should do this interpolation or not, but it's up to the receiving system to actually do the interpolation. That interpolation doesn't get stored in the mesh.

Some file formats (like OBJ) do store smoothing groups, but that only describes the borders where hard edges should NOT be smoothed.

More information on Smoothing Groups.

$\endgroup$
1
$\begingroup$

It took me a while longer to get this ready for you, sorry. here is my approach to export mesh data as json. I decided to leave you the whole code in context, so that you can really try to understand how to calculate the split normals. I also left how to extract uvs, vertices, positions and how to do this for subsets of polygons.

from bpy_extras.io_utils import unpack_list

    def parse_mesh(bl_mesh, faces=None):
    """
        Parses a blender mesh into json arrays
        Example:

        Non-interleaved:
            positions: [vx, vy, vz, ... ] size: multiple of 3
            normals: [nx, ny, nz, ...] size: multiple of 3
            uv: [u1, v1, ...] optional, multiple of 2
            uv2: [u2, v2, ...] optional, multiple of 2

        Interleaved: )
            (... TODO)

        Args:
            bl_mesh ('bpy.types.Mesh') - The mesh data block to parse.
        Kwargs:
            faces ('list(bpy.types.MeshPolygon)') - The subset of polygons to parse.
        Returns:
            al_mesh ('dict') - The json mesh dictionary.
    """

    # UV Textures by name
    texture_uvs = bl_mesh.tessface_uv_textures.get('UVMap')

    if faces is None:
        faces = bl_mesh.polygons

    _vertices = []
    _normals = []
    _uvs = []
    _uvs2 = []

    # Used for split normals export
    face_index_pairs = [(face, index) for index, face in enumerate(faces)]

    # Calculate the split normals from the shading
    bl_mesh.calc_normals_split()
    loops = bl_mesh.loops

    for face, face_index in face_index_pairs:
        # gather the face vertices
        face_vertices = [bl_mesh.vertices[v] for v in face.vertices]
        face_vertices_length = len(face_vertices)

        vertices = [(v.co.x, v.co.y, v.co.z) for v in face_vertices]

        # This is where you extract the split normals from the mesh
        normals = [(loops[l_idx].normal.x, loops[l_idx].normal.y, loops[l_idx].normal.z) for l_idx in face.loop_indices]

        uvs = [None] * face_vertices_length

        if texture_uvs:
            uv_layer = texture_uvs.data[face.index].uv
            uvs = [(uv[0], uv[1]) for uv in uv_layer]

        _vertices += vertices
        _normals += normals
        _uvs += uvs
        _uvs2 += uvs2


    al_mesh = OrderedDict()
    al_mesh['vertices'] = unpack_list(_vertices)
    al_mesh['normals'] = unpack_list(_normals)

    if texture_uvs:
        al_mesh['uvs'] = unpack_list(_uvs)

    return al_mesh

you should perform the following operations on the blender object before calling the parse_mesh function (this function returns an altered copy of the mesh datablock, with triangulated faces and applied modifiers, so if you for example use an edge split modifier on a smooth mesh with angle of 66°, you will get excactly this result in your export.):

def prepare_mesh(obj):
    # This applies all the modifiers (without altering the scene)
    mesh = obj.to_mesh(context.scene, apply_modifiers=True, settings='RENDER')

    # Triangulate for web export
    bm = bmesh.new()
    bm.from_mesh(mesh)
    bmesh.ops.triangulate(bm, faces=bm.faces)
    bm.to_mesh(mesh)
    bm.free()
    del bm

    mesh.calc_normals()
    mesh.calc_tessface()

    return mesh

if you need further information, ask me anytime. Or better, if you find improvements, I'd love to hear them!

$\endgroup$
4
  • $\begingroup$ I finally had time, to try it out. It works somewhat, I get hard faces now - but now every face is hard! From the GUI I use bpy.ops.mesh.faces_shade_{smooth, flat}() but the rendering looks the same both times, while Blender internal and Cycles produce the expected results. How can i access the normals these renderers use? $\endgroup$
    – JonathanK
    Mar 3, 2017 at 16:57
  • $\begingroup$ if you want to learn a bit more how split normals work you can check out this file I wrote once: custom split normals utility Especially how you can toggle between using the blender internal smoothing vs. custom split normals $\endgroup$ Mar 28, 2017 at 7:41
  • $\begingroup$ you probably have to set the polygons to actually use the split normals if you want to apply the split normals to the mesh: obj.data.use_auto_smooth = True i think i forgot to add this in my answer. I'm not sure if this will help you - also if you use split normals, face_shade_{smooth, flat} will not change anything for your rendering, because the smoothing information is overridden by the custom split normals (if use_auto_smooth is True). $\endgroup$ Mar 28, 2017 at 7:55
  • $\begingroup$ @MadlainaKalunder Can I ask, what do these functions calc_normals() and calc_tessface() do? Is it neccessary to call calc_normals() if you are later going to calculate split normals with calc_normals_split() anyway? $\endgroup$
    – user56873
    Apr 21, 2020 at 21:33
0
$\begingroup$

So it seems that Blender stores normals per vertex and per face and that I would have to check whether smooth or hard shading is enabled and based on that decide from where I take my normals. For hard shading I would need duplicated vertexes (so they can have different normals, depending what face they are for), and it seems, that I would have to do it on my own (also for flat surfaces vertexes could remain equal, in which case I most likely would want to join them again...).

But, when using the edge-split modifier, I get my doubled vertexes directly, and so my exporter works as expected without any modifications. For now, this simple solution works for me, however it sucks to require this additional modifier (added by the user!).

$\endgroup$
2
  • $\begingroup$ you can calculate vertex-split-normals upon export. This calculates the per-vertex-normals for the shading you applied. It's a bit more complex then your code but you don't have to add an edge split modifier anymore. (unless you want to use smooth angle based) i can give you a code example later if you like $\endgroup$ Jul 8, 2016 at 5:16
  • $\begingroup$ yes, I would be very interested to see, how this would be done properly. $\endgroup$
    – JonathanK
    Jul 8, 2016 at 11:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .