Modified OBJ Importer for Custom Vertex Normals

Geometry exported from NURBS-based modeling applications relies on custom vertex normals for accurate shading. Blender does not support this, it uses averaged normals that are constantly recalculated with editing operations.

However, it is possible to modify the normals through Python and these modifications should be retained as long as the mesh isn’t edited. Therefore, I have modified the OBJ importer to take the vertex normals into account:

Index: import_obj.py
===================================================================
--- import_obj.py    (revision 4618)
+++ import_obj.py    (working copy)
@@ -22,7 +22,9 @@
 # Contributors: Campbell Barton, Jiri Hnidek, Paolo Ciccone
 
 """
-This script imports a Wavefront OBJ files to Blender.
+This script imports a Wavefront OBJ files to Blender. This is a modified version that will
+import vertex normals. Caution: The imported normals will be overwritten when entering
+Edit Mode.
 
 Usage:
 Run this script from "File->Import" menu and then load the desired OBJ file.
@@ -383,17 +385,17 @@
             mtl.close()
 
 
-def split_mesh(verts_loc, faces, unique_materials, filepath, SPLIT_OB_OR_GROUP):
+def split_mesh(verts_loc,verts_nor,faces, unique_materials, filepath, SPLIT_OB_OR_GROUP):
     """
     Takes vert_loc and faces, and separates into multiple sets of
-    (verts_loc, faces, unique_materials, dataname)
+    (verts_loc, verts_nor, faces, unique_materials, dataname)
     """
 
     filename = os.path.splitext((os.path.basename(filepath)))[0]
 
     if not SPLIT_OB_OR_GROUP:
         # use the filename for the object name since we arnt chopping up the mesh.
-        return [(verts_loc, faces, unique_materials, filename)]
+        return [(verts_loc, verts_nor, faces, unique_materials, filename)]
 
     def key_to_name(key):
         # if the key is a tuple, join it to make a string
@@ -413,19 +415,21 @@
         if oldkey != key:
             # Check the key has changed.
             try:
-                verts_split, faces_split, unique_materials_split, vert_remap = face_split_dict[key]
+                verts_split, nors_split,faces_split, unique_materials_split, vert_remap = face_split_dict[key]
             except KeyError:
                 faces_split = []
                 verts_split = []
+                nors_split=[]
                 unique_materials_split = {}
                 vert_remap = {}
 
-                face_split_dict[key] = (verts_split, faces_split, unique_materials_split, vert_remap)
+                face_split_dict[key] = (verts_split,nors_split,faces_split, unique_materials_split, vert_remap)
 
             oldkey = key
 
         face_vert_loc_indices = face[0]
 
+        use_nor = len(verts_nor)==len(verts_loc)
         # Remap verts to new vert list and add where needed
         for enum, i in enumerate(face_vert_loc_indices):
             map_index = vert_remap.get(i)
@@ -433,7 +437,8 @@
                 map_index = len(verts_split)
                 vert_remap<i> = map_index  # set the new remapped index so we only add once and can reference next time.
                 verts_split.append(verts_loc[i])  # add the vert to the local verts
-
+                if use_nor:
+                    nors_split.append(verts_nor[i])
             face_vert_loc_indices[enum] = map_index  # remap to the local index
 
             matname = face[2]
@@ -443,7 +448,9 @@
         faces_split.append(face)
 
     # remove one of the itemas and reorder
-    return [(value[0], value[1], value[2], key_to_name(key)) for key, value in list(face_split_dict.items())]
+    return [(verts_split, nors_split, faces_split, unique_materials_split,key_to_name(key)) \
+                for key, (verts_split,nors_split,faces_split,unique_materials_split,vert_remap) \
+                in list(face_split_dict.items())]
 
 
 def create_mesh(new_objects,
@@ -451,6 +458,7 @@
                 use_ngons,
                 use_edges,
                 verts_loc,
+                verts_nor,
                 verts_tex,
                 faces,
                 unique_materials,
@@ -589,6 +597,7 @@
 
     # verts_loc is a list of (x, y, z) tuples
     me.vertices.foreach_set("co", unpack_list(verts_loc))
+    
 
     # faces is a list of (vert_indices, texco_indices, ...) tuples
     # XXX faces should contain either 3 or 4 verts
@@ -671,6 +680,7 @@
         me.edges.foreach_set("vertices", unpack_list(edges))
 #         me_edges.extend( edges )
 
+     
 #     del me_edges
 
     # Add edge faces.
@@ -702,6 +712,10 @@
 
     mesh_untessellate(me, fgon_edges)
 
+    if verts_nor:
+        if len(verts_nor)==len(verts_loc):
+            me.vertices.foreach_set("normal", unpack_list(verts_nor))
+
     # XXX slow
 #     if unique_smooth_groups and sharp_edges:
 #         for sharp_edge in sharp_edges.keys():
@@ -862,6 +876,7 @@
     time_main = time.time()
 
     verts_loc = []
+    verts_nor = []
     verts_tex = []
     faces = []  # tuples of the faces
     material_libs = []  # filanems to material libs this uses
@@ -913,7 +928,7 @@
             verts_loc.append((float_func(line_split[1]), float_func(line_split[2]), float_func(line_split[3])))
 
         elif line_start == b'vn':
-            pass
+            verts_nor.append((float_func(line_split[1]), float_func(line_split[2]), float_func(line_split[3])))
 
         elif line_start == b'vt':
             verts_tex.append((float_func(line_split[1]), float_func(line_split[2])))
@@ -1125,13 +1140,14 @@
     else:
         SPLIT_OB_OR_GROUP = False
 
-    for verts_loc_split, faces_split, unique_materials_split, dataname in split_mesh(verts_loc, faces, unique_materials, filepath, SPLIT_OB_OR_GROUP):
+    for verts_loc_split, verts_nor_split, faces_split, unique_materials_split, dataname in split_mesh(verts_loc,verts_nor,faces, unique_materials, filepath, SPLIT_OB_OR_GROUP):
         # Create meshes from the data, warning 'vertex_groups' wont support splitting
         create_mesh(new_objects,
                     has_ngons,
                     use_ngons,
                     use_edges,
                     verts_loc_split,
+                    verts_nor_split,
                     verts_tex,
                     faces_split,
                     unique_materials_split,

Averaged Normals, showing artifacts:

Custom Normals, using the modified importer:


Usage:
You can download the modified importer script here. You [I]must overwrite the existing import_obj.py file in the Blender script directory:
(Assuming version 2.67)
<Blender Path>/2.67/scripts/addons/io_scene_obj

Simply import an OBJ with vertex normals, as usual. For the custom normals to be visible, the mesh must be in “Smooth” mode. Do not enter Edit Mode with this mesh, or the normals will be lost! You can use the example file shown above (created in MoI) for testing.

Warning:
This experimental modification of the OBJ is largely untested and comes with no warranties, whatsoever. It is almost guaranteed to set your dog on fire and kill your house - use it at your own risk! If you find any bugs, you can report them here and I might consider fixing them.

Update (July 4th):
I fixed the obvious issue where import fails with a mesh that doesn’t define vertex normals (I’m not kidding when I say this is largely untested).
Also: It is legal for OBJ files to define faces that use vertex normals which do not match the vertex coordinates by index. Supporting this would make things a bit more complicated, so unless someone actually needs this, I won’t bother. Such files will currently fail to import normals, silently.

Update (September 26th):
Maccesh posted a improved version of the script. Thanks!

Holy moly blender happiness !!!

Nice job, I should try it out

Zalamander: can I hug you?

Seriously, you rock. No more Moi artefacts!!!

Excellent! Thanks Zalamander :slight_smile:

This is great! Thank you very much, Zalamander.
This is totally bringing new life to my relationship with blender - and the vray for blender license I bought years ago…:wink:

What is this witchcraft? I’ve modified the importer myself before but the vertex normals never got transferred to the renderer, it only worked in the viewport!

Thank you, good sir!

I’ve modified the importer myself before but the vertex normals never got transferred to the renderer, it only worked in the viewport!

Up until a while ago (up until Bmesh?) the internal renderer did a recalc, but not that is apparently not the case anymore. I’m not sure about Cycles, it probably would’ve worked all along. Had I known that this was the case, I would’ve done this much earlier…

Yeah, it’s really useful now, we’re nearly there. A next step could be to have a script that iterates over the vertices and stores the normals so they can be reset later after small edits or just as a backup. But it’s not really needed. How is this in other applications, editing a mesh with explicit vertex normals doesn’t make sense anyway, does it?

I’ll just parent my meshes to empties and set them to non-selectable.

Yes! This works wonderfully. Finally can bring my MoI models directly into Blender. Thank you, Zalamander!

I also think that is not really needed and I actually hope for some “official” support, such as a “normal freeze” property that will disable automatic recalculation. Editing mesh normals explicitly does make sense in some cases (like lowpoly game assets) and some applications do support it. Also, averaged normals with area weight give arguably better results, so there could be an option for that.

Hi Zalamander,

I tested your Script, but it doesn’t work for me.
The mesh is ripped.
I have not enter Edit-Mode.

http://www.pasteall.org/pic/55709

Blender r58189
import_obj.py - Uploaded: 2013-07-03 20:27:33
custom_normal_test.zip - Uploaded: 2013-07-02 21:01:14

Cheers
Hans

Thanx Zalamander!
But if you want to deal with UV, you must enter the edit-mode so…
How do you prepare the UV on these models…

Great Script! Unfortunately my Maya.obj - normals do not work, your example does work well though!
I have to investigate my file…

best regards,
tencars

This is normal, meshes converted from NURBS are usually not watertight. If you increase the tesselation during export, it becomes less noticable.
In any case, this has nothing to do with the importer (it doesn’t touch the geometry at all), it depends on the tesselator of whatever program you use to export. In this case, I used MoI, but I didn’t use very high settings.

You don’t. If you export from a program like MoI, the model comes with the NURBS UV parameterization already (which isn’t necessarily what you want, either). With industrial design applications, UV texturing isn’t even that commonly used.

It’s probably one of those files I described in the first post.

Hi Zalamander,

I tested YOUR custom_normal_test.zip.
And the mesh is ripped.

I don’t think that’s normal.

Let me repeat myself:
This is normal, meshes converted from NURBS are usually not watertight. If you increase the tesselation during export, it becomes less noticable.[…]
In this case, I used MoI, but I didn’t use very high settings.

Hi Zalamdner, thanks for the great script.

We implemented some enhancements that we needed and we thought maybe someone else would like it.

We added the option to split meshes by materials and #QNAN error handling.

You can download the script here.

Replace the files in scripts/addons/io_scene_obj

does anyone still have the improved version of this script? the link is not valid anymore, sadly

Edit: Maccesh has updated the link. Thank you so much!

Hi Rkia,

you don’t need this Script anymore.
Blender supports now custom vertex normals.