I've been trying to make a Roblox Mesh 1.0 exporter, and I'm nearly there, but I have encountered one problem that's causing a lot of annoying lighting issues: the float values for the Normals are not as I expected at first.

Current State of the Mesh Exporting

For comparison, the custom exporter that inspired me to make this is an anim8or plugin, and when I export a mesh from that, the normals are lit pretty well.

Anim8or export

I did also notice that importing an obj into anim8or, exporting it back into an obj, importing that back into blender, and then running the python code produces near identical results.

Import from anim8or, and then export as mesh

Here is the code, I hope it helps give some context.

import math
import bmesh
import bpy
from bpy import context

def triangulate_object(obj):
    me = obj.data
    bm = bmesh.new()

    bmesh.ops.triangulate(bm, faces=bm.faces[:])
def FixNumber(number):
    return str("{:.9f}".format(math.ceil(float('%.9g' % number) * 1000000000) / 1000000000)).rstrip('0').rstrip('.')

filename = "traffic2.mesh"
objects = context.scene.objects
numFaces = 0
iterator = 0
Output = ""
arr = []

for shape in objects:
    if (shape.type == "MESH"):
        data = shape.data.polygons
        tMatrix = shape.matrix_world
        numFaces = numFaces + sum([(len(p.vertices) - 2) for p in data])
        for face in data:
            for vert_idx, loop_idx in zip(reversed(face.vertices), reversed(face.loop_indices)):
                iterator = iterator + 1
                pointx, pointz, pointy = shape.matrix_basis @ shape.data.vertices[vert_idx].co
                pointx = FixNumber(pointx)
                pointy = FixNumber(pointy)
                pointz = FixNumber(-pointz)
                normal_local = shape.data.vertices[vert_idx].normal
                normalx, normaly, normalz = normal_local
                length = math.sqrt( normalx**2 + normaly**2 + normalz**2 )
                normalx = FixNumber(normalx / length)
                normaly = FixNumber(normaly / length)
                normalz = FixNumber(normalz / length)
                uvx, uvy = shape.data.uv_layers.active.data[loop_idx].uv
                uvx = FixNumber(uvx)
                uvy = FixNumber(uvy)
                arr.append(f"[{pointx}, {pointy}, {pointz}][{normalx}, {normaly}, {normalz}][{uvx}, {uvy}, 0]")
                if (iterator % 3 == 0):
                    arr.insert(iterator-2, arr.pop(iterator-1))
open(filename, "w").write(f"version 1.00\n{numFaces}\n")
for triangle in arr:
    open(filename, "a").write(triangle)

What I do know, is that anim8or is giving the obj some "Custom Split Normals", as what blender tells me. However all my efforts trying to change the Normal values in blender on the original model using modifiers have failed. Custom Split Normals

To elaborate on the Roblox mesh format, it's quite simple.

version 1.00
[VertexX, VertexY, VertexZ][NormalX, NormalY, NormalZ][UVX, UVY, unused]

The last line gets repeated for every Triangle

The Mesh 1.0 format does not support any kind of materials, or nodes. It only supports vertices, normals, and uvs.

Here is an archive of the anim8or script in question, and instructions on how to install it.

I want to be able to recreate these anim8or Normal values in python automatically, and then have it export those, but I'm not entirely sure what to do here.


1 Answer 1


You should fetch the triangulation with me.calc_loop_triangles() and then calculate per-corner normals with me.calc_normals_split(). This should give you the correct object space normals in me.loops[i].normal.

You also need to transform the normal from object space to world space, just like you do for positions, using the inverse-transpose.

I have no real way to test this, but I tried to clean up the code and handle normals correctly. Hopefully it works.

import bpy
import math

def FixNumber(number):
    rounded = math.ceil(float("%.9g" % number) * 1000000000) / 1000000000
    return "{:.9f}".format(rounded).rstrip('0').rstrip('.')

filename = "traffic2.mesh"


objects = bpy.context.scene.objects
numTris = 0
vertexLine = ""

for ob in objects:
    if ob.type != 'MESH':

    me = ob.data
    uvLayer = me.uv_layers.active.data

    # this matrix transforms points into world space
    vMatrix = ob.matrix_world

    # this matrix transforms normals into world space
    # (must re-normalize afterwards)
    nMatrix = vMatrix.to_3x3().inverted().transposed()

    # called to populate me.loop_triangles with triangulation

    # called to calc loop.normal for every loop

    numTris += len(me.loop_triangles)

    for tri in me.loop_triangles:
        for i in (0, 2, 1):
            v = me.vertices[tri.vertices[i]].co
            n = me.loops[tri.loops[i]].normal
            uv = uvLayer[tri.loops[i]].uv

            # transform point
            v = vMatrix @ v

            # transform normal
            n = nMatrix @ n

            vertexLine += (
                f"[{FixNumber(v[0])}, {FixNumber(v[1])}, {FixNumber(v[2])}]"
                f"[{FixNumber(n[0])}, {FixNumber(n[1])}, {FixNumber(n[2])}]"
                f"[{FixNumber(uv[0])}, {FixNumber(uv[1])}, 0]"

with open(filename, "w") as f:
    f.write("version 1.00\n")
  • $\begingroup$ That does seem to fix the issue with the incorrect normal floats! However now all the values are shuffled around and I'm unsure of how to fix it. In my original code, I reversed the values in the zip, and then I shuffled every 3 iterations back one. However it seems when I try to reverse the for loop in this, the values aren't quite accurate there, either. The mesh is also sideways, too, rather than standing up. $\endgroup$
    – MarioMan22
    Feb 25, 2022 at 3:13
  • $\begingroup$ Yeah, I couldn't understand what order you were trying to use. Does just using the (0, 1, 2) order work? In Blender, you get the verts in counter-clockwise order around the tri, but I don't know what your format expects. If the model is sideways, you need to use vMatrix = robloxMatrix @ ob.matrix_world where robloxMatrix transforms from Blender space to Roblox space. Of course I also don't know what that transform is (a decent first guess is probably X,Y,Z -> X,Z,-Y though). $\endgroup$
    – scurest
    Feb 25, 2022 at 3:34
  • $\begingroup$ The Euler seems to have fixed it being on an angle, but The values still seem to be in a completely different order than I can decipher. I know with the code I made, I put a reversed() on the zip values, and suddenly it started to make sense. However they were slightly out of order, so I had to shuffle every 3rd with the second. I should also mention that some of the normals are inverted, and if I try flipping them the uvs break. $\endgroup$
    – MarioMan22
    Feb 25, 2022 at 4:37
  • $\begingroup$ I got it working! I mixed the normal generation of your code with the vertex and uv mapping of mine. It's slower now, but It'll do. $\endgroup$
    – MarioMan22
    Feb 26, 2022 at 4:04

You must log in to answer this question.

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