python-3.xfbx

create mesh normals in FBX SDK


in python FBX SDK, I am trying to generate and export a cube. However, the normals in the exported model are completely off.

How I set the normals. I am setting a normal per control point (aka vertex) of the cube, so 8 normals in total. A normal for a given vertex is collinear with and has the same direction as the vector pointing from cube's center to the given vertex, so the cube normals are pointing 'outwards' with respect to cube's center. The full code I am using is below, it is fully reproduceable if the FBX library is installed. Also, this code is based on this example by FBX.

import fbx

memory_manager = fbx.FbxManager.Create()
scene = fbx.FbxScene.Create(memory_manager, '')

mesh_node = fbx.FbxNode.Create(memory_manager, 'cube')
scene.GetRootNode().AddChild(mesh_node)

# -- mesh
mesh_attribute: fbx.FbxMesh = fbx.FbxMesh.Create(memory_manager, '')
mesh_node.AddNodeAttribute(mesh_attribute)

# -- mesh -- vertices
mesh_attribute.InitControlPoints(8)
CUBE_VERTS = (
    (0,0,0),
    (0,0,1),
    (0,1,0),
    (0,1,1),
    (1,0,0),
    (1,0,1),
    (1,1,0),
    (1,1,1)
)
for i in range(0, 8):
    vert = CUBE_VERTS[i]
    mesh_attribute.SetControlPointAt(fbx.FbxVector4(vert[0], vert[1], vert[2], 0.0), i)

# -- mesh -- faces
CUBE_FACES = (
    (0,1,3,2),
    (3,2,6,7),
    (4,5,7,6),
    (0,1,5,4),
    (1,3,7,5),
    (0,2,6,4)
)
for i in range(0, len(CUBE_FACES)):
    mesh_attribute.BeginPolygon()
    mesh_attribute.AddPolygon(CUBE_FACES[i][0])
    mesh_attribute.AddPolygon(CUBE_FACES[i][1])
    mesh_attribute.AddPolygon(CUBE_FACES[i][2])
    mesh_attribute.AddPolygon(CUBE_FACES[i][3])
    mesh_attribute.EndPolygon()

# -- mesh -- normals
NORMALS = [
    [-0.57735, -0.57735, -0.57735, 1.0],
    [-0.57735, -0.57735, 0.57735, 1.0],
    [-0.57735, 0.57735, -0.57735, 1.0],
    [-0.57735, 0.57735, 0.57735, 1.0],
    [0.57735, -0.57735, -0.57735, 1.0],
    [0.57735, -0.57735, 0.57735, 1.0],
    [0.57735, 0.57735, -0.57735, 1.0],
    [0.57735, 0.57735, 0.57735, 1.0]
]

normal_element: fbx.FbxLayerElementNormal = mesh_attribute.CreateElementNormal()
normal_element.SetMappingMode(fbx.FbxLayerElementNormal.eByControlPoint)
normal_element.SetReferenceMode(fbx.FbxLayerElementNormal.eDirect)

for i in range(0, 8):
    normal_element.GetDirectArray().Add(fbx.FbxVector4(NORMALS[i][0], NORMALS[i][1], NORMALS[i][2]))


# EXPORT
exporter = fbx.FbxExporter.Create(memory_manager, '')
exporter.Initialize('cuby', -1, memory_manager.GetIOSettings())
result = exporter.Export(scene)

if result:
    print('exported succesfully')

memory_manager.Destroy()

What I expect. When I export the model, I expect to see per-face normals which are all pointing outwards with respect to the center, so a normal-looking cube with healthy normals. I expect the cube as shown below:

enter image description here

What actually happens. The normals are completely off, seems as if they had been generated with some random algorithm. I have examined the cube in the FBX Review software and in Blender. The results respectively as below:

enter image description here enter image description here

In the second picture (Blender), you can see that normals are pointing in pretty much random directions: both vertex and face normals.

I am completely sure that I am missing something in the code but I can't figure out what.

EDIT. I also tried adding the normal layer element to a geometry layer, as specified here, with no result... I did this like follows:

# layer exists in my case, else create
layer = mesh_attribute.GetLayer(0)
layer.SetNormals(normal_element)

Solution

  • To export face normals correctly, you have to define the face (a.k.a polygon) correctly. What this means:

    1. user is not allowed to set face normals. By definition, a face normal is perpendicular to a face, which means, more precisely, it is perpendicular to two vectors, defining a face (in 3d, three points, or two vectors, are sufficient to define a surface, any additional points/vectors will be redundant - that's why 3d works with triangles)
    2. because a normal is (always) perpendicular to a face, there's no point to allow a user free input - and, correctly so, fbx SDK does not allow it
    3. then, how to specify a face normal? well, it is calculated automatically by fbx SDK: to define a face in fbx SDK, you must define a sequence of points which form this face. fbx SDK will use three of these points (a.k.a vertices) to form two vectors and calculate their cross product, which will be the final normal. Depending on the order of multiplication of these vectors, the normal will be facing one or another direction.

    To illustrate: suppose, you have a list of vertices verts, and, suppose, you define a single triangle. Then, the below two options are not equivalent: the order of vertex inclusion matters.

    mesh.BeginPolygon()
    mesh.AddPolygon(verts[0])
    mesh.AddPolygon(verts[1])
    mesh.AddPolygon(verts[2])
    mesh.EndPolygon()
    

    will yield a different normal compared to

    mesh.BeginPolygon()
    mesh.AddPolygon(verts[2])
    mesh.AddPolygon(verts[1])
    mesh.AddPolygon(verts[0])
    mesh.EndPolygon()
    

    The order of definition of polygon vertices will decide upon the normal. A more thorough explanation deals with linear algebra and 3d graphics stuff, so is out of scope, but you can read here.

    Also, you may wonder what kind of normals I was defining in the code of the question. These were per-vertex normals, as opposed to per-face normals. Per-vertex normals are a different thing, although also important: they define whether surface looks smooth or triangulated, i.e. defines shading.