Procedural binormals and tangents?

For my project I’ve got a procedurally generated model. The script constructs cylinderoid shapes by stacking rings of vertices. I also have a centroid to the ring that I use to calculate vertex normal. The script also writes texture coordinates for the vertices.

For the next step of the project I need to calculate binormals and tangents on those vertices. Any advice on how to calculate them?

I saw this topic: [Computing normals for procedural geometry?) but it seemed more concerned with normals and not the T and B values.

I guess I could use the egg-trans code, the problem is that the code will could be generating 100s of these structures and I’m concerned about speed.

It’s important for the project that the procedurally created models be generated from data during game play and not loaded into the environment from the disk.

Hi mavasher,

I’m starting to look into this exact same problem myself. I found this gamedev post about the topic (there’s also this stack overflow post that goes more in depth into the maths needed).

I didn’t test and i don’t have working code for that yet though, so please tell if you make some progress there.

From what I can see, the examples arikel posted are not very robust and they probably won’t work for all cases. At least that’s what my experience with this horrible thing that computing tangents and binormals is tells me.

When I was writing this stuff for my editor (my implementation is also not universal), I looked into Yabee’s code. It has a tangent/binormal generator.

That said, computing this is relatively complicated and computationally expensive, so don’t expect being able to do this fast enough for large amounts of triangles in Python. If you’re fine with using C++ then that’s a different story.

Here’s the code I wrote for my editor (again, probably not good for all cases) a while ago based on Yabee.

def tangentBinormalFromUV(self):
    def getTB(verts, coords):
        v0 = verts[0]
        v1 = verts[1]
        v2 = verts[2]
        
        q1 = v1 - v0
        q2 = v2 - v0
        
        uv0 = coords[0]
        uv1 = coords[1]
        uv2 = coords[2]
        
        st1 = uv1 - uv0
        st2 = uv2 - uv0
        
        s1 = st1.x; t1 = st1.y
        s2 = st2.x; t2 = st2.y
        
        r = 1.0 / (s1 * t2 - s2 * t1)
        
        matUV = []
        matUV.append([t2 * r, -t1 * r])
        matUV.append([-s2 * r, s1 * r])
        
        Tx = matUV[0][0] * q1.x + matUV[0][1] * q2.x
        Ty = matUV[0][0] * q1.y + matUV[0][1] * q2.y
        Tz = matUV[0][0] * q1.z + matUV[0][1] * q2.z
        
        Bx = matUV[1][0] * q1.x + matUV[1][1] * q2.x
        By = matUV[1][0] * q1.y + matUV[1][1] * q2.y
        Bz = matUV[1][0] * q1.z + matUV[1][1] * q2.z
        
        tangent = Vec3(Tx, Ty, Tz)
        binormal = Vec3(Bx, By, Bz)
        
        return tangent, binormal
    
    vertPositions = []
    vertUVs = []
    
    geomNode = self.node.node()
    geom = geomNode.getGeom(0)
    vdata = geom.getVertexData()
    
    vertex = GeomVertexReader(vdata, 'vertex')
    texcoord = GeomVertexReader(vdata, 'texcoord')
    
    while not vertex.isAtEnd():
        vert = vertex.getData3f()
        uv = texcoord.getData2f()
        
        vertPositions.append(vert)
        vertUVs.append(uv)
    
    tangentVec, binormalVec = getTB(vertPositions[0:3], vertUVs[0:3])
    
    geomNode = self.node.node()
    geom = geomNode.modifyGeom(0)
    vdata = geom.modifyVertexData()
    
    tangent = GeomVertexWriter(vdata, 'tangent')
    binormal = GeomVertexWriter(vdata, 'binormal')
    normalW = GeomVertexWriter(vdata, 'normal')
    normal = GeomVertexReader(vdata, 'normal')
    
    while not tangent.isAtEnd():
        N = normal.getData3f()
        
        T = Vec3(tangentVec)
        B = Vec3(binormalVec)
        
        T = T - N * N.dot(T)
        B = B - N * N.dot(B) - T * T.dot(B)
        
        T.normalize()
        B.normalize()
        
        def getClosestPoint(a, b, p):
            c = p - a
            v = b - a
            d = v.length()
            v.normalize()
            t = v.dot(c)
            
            if t < 0.0:
                return a
            if t > d:
                return b
            
            v *= t
            return a + v
        
        def orto(v1, v2):
            v2ProjV1 = getClosestPoint(v1, -v1, v2)
            res = v2 - v2ProjV1
            res.normalize()
            return res
        
        T = orto(N, T)
        B = orto(T, B)
        
        tangent.setData3f(T)
        binormal.setData3f(B)

Shouldn’t the 3d engine generate those itself, optionally? That would make even more sense for an engine which encourages Python.

Yes it would be nice, but let me play David: Are you willing to write a patch?

The usual “not a C++ programmer”

The usual “me neither”, but I’m thinking about diving into it. Not knowing it is just so limiting it hurts sometimes…

does this still work? tangent.isAtEnd() is returning True at the start for me

If it helps, here is a working code sample that uses my own method to compute tangent-space vectors:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase


def create_cube(vertex_format):

    vertex_data = GeomVertexData("cube_data", vertex_format, Geom.UH_static)
    tris_prim = GeomTriangles(Geom.UH_static)

    pos_writer = GeomVertexWriter(vertex_data, "vertex")
    normal_writer = GeomVertexWriter(vertex_data, "normal")
    color_writer = GeomVertexWriter(vertex_data, "color")
    uv_writer = GeomVertexWriter(vertex_data, "texcoord")

    vertex_count = 0
    # (left=purple, back=green, down=blue, right=red, front=yellow, up=white)
    colors = ((1., 0., 1.), (0., 1., 0.), (0., 0., 1.),
              (1., 0., 0.), (1., 1., 0.), (1., 1., 1.))

    for direction in (-1, 1):

        for i in range(3):

            normal = VBase3()
            normal[i] = direction
            r, g, b = colors[i if direction == -1 else i-3]
            color = (r, g, b, 1.)

            for a, b in ((-1., -1.), (-1., 1.), (1., 1.), (1., -1.)):

                pos = Point3()
                pos[i] = direction
                pos[(i + direction) % 3] = a
                pos[(i + direction * 2) % 3] = b
                uv = (max(0., a), max(0., b))

                pos_writer.add_data3(pos)
                normal_writer.add_data3(normal)
#                color_writer.add_data4(color)
                uv_writer.add_data2(uv)

            vertex_count += 4

            tris_prim.add_vertices(vertex_count - 2, vertex_count - 3, vertex_count - 4)
            tris_prim.add_vertices(vertex_count - 4, vertex_count - 1, vertex_count - 2)

    geom = Geom(vertex_data)
    geom.add_primitive(tris_prim)
    node = GeomNode("cube_node")
    node.add_geom(geom)

    return node


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        # set up a light source
        p_light = PointLight("point_light")
        p_light.set_color((1., 1., 1., 1.))
        self.light = self.camera.attach_new_node(p_light)
        self.light.set_pos(5., -10., 7.)
        self.render.set_light(self.light)
        self.render.set_shader_auto()

        # Define a vertex format that includes "tangent" and "binormal" columns

        array_format = GeomVertexArrayFormat()
        array_format.add_column(InternalName.make("vertex"), 3, Geom.NT_float32, Geom.C_point)
        array_format.add_column(InternalName.make("color"), 4, Geom.NT_uint8, Geom.C_color)
        array_format.add_column(InternalName.make("normal"), 3, Geom.NT_float32, Geom.C_normal)
        array_format.add_column(InternalName.make("tangent"), 3, Geom.NT_float32, Geom.C_vector)
        array_format.add_column(InternalName.make("binormal"), 3, Geom.NT_float32, Geom.C_vector)
        array_format.add_column(InternalName.make("texcoord"), 2, Geom.NT_float32, Geom.C_texcoord)

        vertex_format = GeomVertexFormat()
        vertex_format.add_array(array_format)
        vertex_format = GeomVertexFormat.register_format(vertex_format)

        # create a cube parented to the scene root
        cube_node = create_cube(vertex_format)
        cube = self.render.attach_new_node(cube_node)
        geom = cube.node().modify_geom(0)
        self.update_tangent_space(geom)
        tex = self.loader.load_texture("my_texture.jpg")
        cube.set_texture(tex)
        tex_stage = TextureStage("normal")
        tex_stage.set_mode(TextureStage.M_normal)
        tex = self.loader.load_texture("my_normal_map.png")
        cube.set_texture(tex_stage, tex)
#        cube.set_tex_rotate(TextureStage.get_default(), 180.)
#        cube.set_tex_rotate(tex_stage, 180.)

    def update_tangent_space(self, geom, flip_tangent=False, flip_bitangent=False):

        positions = []
        normals = []
        uvs = []
        vertex_data = geom.modify_vertex_data()
        pos_reader = GeomVertexReader(vertex_data, "vertex")
        normal_reader = GeomVertexReader(vertex_data, "normal")
        uv_reader = GeomVertexReader(vertex_data, "texcoord")
        tan_writer = GeomVertexWriter(vertex_data, "tangent")
        bitan_writer = GeomVertexWriter(vertex_data, "binormal")

        for _ in range(vertex_data.get_num_rows()):
            positions.append(pos_reader.get_data3())
            normals.append(normal_reader.get_data3())
            uvs.append(uv_reader.get_data2())

        prim = geom.get_primitive(0)
        verts = prim.get_vertex_list()
        triangles = [verts[n:n + 3] for n in range(0, len(verts), 3)]
        processed_verts = []
        epsilon = 1.e-010

        for rows in triangles:

            for row in rows:

                if row in processed_verts:
                    continue

                vert = verts[row]
                other_rows = list(rows)
                other_rows.remove(row)
                pos = positions[row]
                pos1, pos2 = [positions[r] for r in other_rows]
                pos_vec1 = pos1 - pos
                pos_vec2 = pos2 - pos
                uv = Point2(uvs[row])
                uv1, uv2 = [Point2(uvs[r]) for r in other_rows]
                uv_vec1 = uv1 - uv
                uv_vec2 = uv2 - uv

                # compute a vector pointing in the +U direction, in texture space
                # and in object space

                if abs(uv_vec1.y) < epsilon:
                    u_vec_tex = uv_vec1
                    u_vec_obj = Vec3(pos_vec1)
                elif abs(uv_vec2.y) < epsilon:
                    u_vec_tex = uv_vec2
                    u_vec_obj = Vec3(pos_vec2)
                else:
                    scale = (uv_vec1.y / uv_vec2.y)
                    u_vec_tex = uv_vec1 - uv_vec2 * scale
                    # u_vec_tex.y will be 0 and thus point in the -/+U direction;
                    # replacing the texture-space vectors with the corresponding
                    # object-space vectors will therefore yield an object-space U-vector
                    u_vec_obj = pos_vec1 - pos_vec2 * scale

                if u_vec_tex.x < 0.:
                    u_vec_obj *= -1.

                # compute a vector pointing in the +V direction, in texture space
                # and in object space

                if abs(uv_vec1.x) < epsilon:
                    v_vec_tex = uv_vec1
                    v_vec_obj = Vec3(pos_vec1)
                elif abs(uv_vec2.x) < epsilon:
                    v_vec_tex = uv_vec2
                    v_vec_obj = Vec3(pos_vec2)
                else:
                    scale = (uv_vec1.x / uv_vec2.x)
                    v_vec_tex = uv_vec1 - uv_vec2 * scale
                    # v_vec_tex.x will be 0 and thus point in the -/+V direction;
                    # replacing the texture-space vectors with the corresponding
                    # object-space vectors will therefore yield an object-space V-vector
                    v_vec_obj = pos_vec1 - pos_vec2 * scale

                if v_vec_tex.y < 0.:
                    v_vec_obj *= -1.

                normal = normals[row]
                tangent_plane = Plane(normal, Point3())
                # the tangent vector is the object-space U-vector projected onto
                # the tangent plane
                tangent = Vec3(tangent_plane.project(Point3(u_vec_obj)))

                if not tangent.normalize():
                    continue

                # the bitangent vector is the object-space V-vector projected onto
                # the tangent plane
                bitangent = Vec3(tangent_plane.project(Point3(v_vec_obj)))

                if not bitangent.normalize():
                    continue

                if flip_tangent:
                    tangent *= -1.

                if flip_bitangent:
                    bitangent *= -1.

                tan_writer.set_row(row)
                tan_writer.set_data3(tangent)
                bitan_writer.set_row(row)
                bitan_writer.set_data3(bitangent)
                processed_verts.append(row)


app = MyApp()
app.run()

But I agree with some of the comments in this thread that Panda3D itself should manage tangent-space vectors.
This could range from adjusting the vertex format of a model to include columns for these vectors whenever a normal map is applied to it, to automatically updating them when needed. Case in point, when a texture rotation effect is set on the model, the tangent-space vectors are not updated accordingly, such that an angle of 180 degrees makes “bumps” look like “dents” instead. You can test this with the sample.

3 Likes

I’m pretty fresh to all this so this helped me out a lot :smiley: Thank you!