PDA

View Full Version : Finding bi-normals/tangents


Gibbz
09-21-2006, 08:02 AM
Hey Guys,

I need to find the binormals and tangents within max on my meshes.
As far as I can find max only supplys a normal...

What is the easiest way to go about calculating this data?

Cheers

Gibbz
09-21-2006, 08:27 AM
Ok I found this, this work fine within max with some changes to the synatax I assume?...

http://www.terathon.com/code/tangent.php

HalfVector
09-21-2006, 02:49 PM
I've ported the code I use to calculate the tangent space to MAXScript.

fn computeTangentSpace obj &tSpace = (

local theMesh = snapshotAsMesh obj

tSpace = #()

for nFace = 1 to theMesh.numFaces do (
local face = getFace theMesh nFace
local tface = getTVFace theMesh nFace

local v1 = getVert theMesh face[1]
local v2 = getVert theMesh face[2]
local v3 = getVert theMesh face[3]

local uv1 = getTVert theMesh tface[1]
local uv2 = getTVert theMesh tface[2]
local uv3 = getTVert theMesh tface[3]

local dV1 = v1 - v2
local dV2 = v1 - v3

local dUV1 = uv1 - uv2
local dUV2 = uv1 - uv3

local area = dUV1.x * dUV2.y - dUV1.y * dUV2.x
local sign = if area < 0 then -1 else 1

local tangent = [1,0,0]

tangent.x = dV1.x * dUV2.y - dUV1.y * dV2.x
tangent.y = dV1.y * dUV2.y - dUV1.y * dV2.y
tangent.z = dV1.z * dUV2.y - dUV1.y * dV2.z

tangent = (normalize tangent) * sign

local normal = getFaceNormal theMesh nFace
local binormal = (normalize (cross normal tangent)) * sign

append tSpace #(tangent, binormal, normal)
)

delete theMesh
)

It returns an array (the parameter tSpace) with the tangent, the binormal (or bitangent I should say...) and normal for each triangle.

To use it:

tSpace = #()
computeTangentSpace $Plane01 &tSpace

So now, if you want to acces to the vectors for face #1 you'll do:

tangent = tSpace[1][1]
binormal = tSpace[1][2]
normal = tSpace[1][3]

Keep in mind this data is not directly usable in a game engine as a graphics card expects per-vertex data and not per-face data.

Here's a screenshot showing the tangent space for an object:

http://img88.imageshack.us/img88/1381/tangentspacebx0.jpg

In red, the tangent. In green, the binormal. In blue, the normal.

Hope that helps.

Gibbz
09-21-2006, 11:20 PM
Sweet, thanks!

This looks perfect!

How did you get it to show the gizmo as the normals/binormal/tangents? Is that a special maxscript helper?

HalfVector
09-22-2006, 06:54 AM
How did you get it to show the gizmo as the normals/binormal/tangents? Is that a special maxscript helper?
Actually, it is a simple function I've done for that. Also, I've modified a bit the computeTangentSpace function. Now uses an array of matrices where row1 = tangent, row2 = binormal, row3 = normal and row4 = face center.

fn computeTangentSpace obj = (

local theMesh = snapshotAsMesh obj

local tSpace = #()

for nFace = 1 to theMesh.numFaces do (
local face = getFace theMesh nFace
local tface = getTVFace theMesh nFace

local v1 = getVert theMesh face[1]
local v2 = getVert theMesh face[2]
local v3 = getVert theMesh face[3]

local uv1 = getTVert theMesh tface[1]
local uv2 = getTVert theMesh tface[2]
local uv3 = getTVert theMesh tface[3]

local dV1 = v1 - v2
local dV2 = v1 - v3

local dUV1 = uv1 - uv2
local dUV2 = uv1 - uv3

local area = dUV1.x * dUV2.y - dUV1.y * dUV2.x
local sign = if area < 0 then -1 else 1

local tangent = [0,0,1]

tangent.x = dV1.x * dUV2.y - dUV1.y * dV2.x
tangent.y = dV1.y * dUV2.y - dUV1.y * dV2.y
tangent.z = dV1.z * dUV2.y - dUV1.y * dV2.z

tangent = (normalize tangent) * sign

local normal = normalize (getFaceNormal theMesh nFace)
local binormal = (normalize (cross normal tangent)) * sign

local fCenter = meshOp.getFaceCenter theMesh nFace

append tSpace (Matrix3 tangent binormal normal fCenter)
)

delete theMesh

return tSpace
)

fn showTangentSpace tSpace axisLength = (
gw.setTransform (matrix3 1)

for nFace = 1 to tSpace.count do (
local tbn = tSpace[nFace]

gw.setColor #line red
gw.polyLine #( tbn.row4, (tbn.row4 + tbn.row1 * axisLength) ) false
gw.setColor #line green
gw.polyLine #( tbn.row4, (tbn.row4 + tbn.row2 * axisLength) ) false
gw.setColor #line blue
gw.polyLine #( tbn.row4, (tbn.row4 + tbn.row3 * axisLength) ) false
)

gw.enlargeUpdateRect #whole
gw.updateScreen()
)

The tangent space rendering is not permanent. As soon as the viewport is redrawn, the vectors will disappear. To avoid that, you should make use of the registerRedrawViewsCallback system.

Greets.

Gibbz
09-22-2006, 09:25 AM
cool thanks :)

Gibbz
09-25-2006, 06:34 AM
Hey theres a problem with this. The binormal is calculated from the tangent/normal, what if the uv's are flipped? Then the binormal will not be correct....

Ill look into this more I havent had a good look over how the code works yet....

Thanks

HalfVector
09-25-2006, 12:34 PM
I forgot to multiply the binormal by the sign. So now the code looks like this:

fn computeTangentSpace obj = (

local theMesh = snapshotAsMesh obj

local tSpace = #()

for nFace = 1 to theMesh.numFaces do (
local face = getFace theMesh nFace
local tface = getTVFace theMesh nFace

local v1 = getVert theMesh face[1]
local v2 = getVert theMesh face[2]
local v3 = getVert theMesh face[3]

local uv1 = getTVert theMesh tface[1]
local uv2 = getTVert theMesh tface[2]
local uv3 = getTVert theMesh tface[3]

local dV1 = v1 - v2
local dV2 = v1 - v3

local dUV1 = uv1 - uv2
local dUV2 = uv1 - uv3

local area = dUV1.x * dUV2.y - dUV1.y * dUV2.x
local sign = if area < 0 then -1 else 1

local tangent = [0,0,1]

tangent.x = dV1.x * dUV2.y - dUV1.y * dV2.x
tangent.y = dV1.y * dUV2.y - dUV1.y * dV2.y
tangent.z = dV1.z * dUV2.y - dUV1.y * dV2.z

tangent = (normalize tangent) * sign

local normal = normalize (getFaceNormal theMesh nFace)
local binormal = (normalize (cross normal tangent)) * sign

local fCenter = meshOp.getFaceCenter theMesh nFace

append tSpace (Matrix3 tangent binormal normal fCenter)
)

delete theMesh

return tSpace
)

Anyway in the tests I've done there are no (apparent) differences. If you flip the U or the V component, the tangent or the binormal will be flipped too, to follow the UV flow.

Just in case, I've implemented the method of the page you mentioned in your second message (Terathon) and the results are exactly the same (at least for simple meshes):

fn computeTangentSpace_terathon obj = (

local theMesh = snapshotAsMesh obj

local tSpace = #()

for nFace = 1 to theMesh.numFaces do (
local face = getFace theMesh nFace
local tface = getTVFace theMesh nFace

local v1 = getVert theMesh face[1]
local v2 = getVert theMesh face[2]
local v3 = getVert theMesh face[3]

local w1 = getTVert theMesh tface[1]
local w2 = getTVert theMesh tface[2]
local w3 = getTVert theMesh tface[3]

local x1 = v2.x - v1.x
local x2 = v3.x - v1.x
local y1 = v2.y - v1.y
local y2 = v3.y - v1.y
local z1 = v2.z - v1.z
local z2 = v3.z - v1.z

local s1 = w2.x - w1.x
local s2 = w3.x - w1.x
local t1 = w2.y - w1.y
local t2 = w3.y - w1.y

local r = 1.0 / (s1 * t2 - s2 * t1)

local tan1 = [(t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r]
local tan2 = [(s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r]

local normal = normalize (getFaceNormal theMesh nFace)

local tangent = normalize (tan1 - normal * (dot normal tan1))

local handedness = if (dot (cross normal tan1) tan2) < 0.0 then -1.0 else 1.0

local binormal = (normalize (cross normal tangent)) * handedness

local fCenter = meshOp.getFaceCenter theMesh nFace

append tSpace (Matrix3 tangent binormal normal fCenter)
)

delete theMesh

return tSpace
)

Greets.

Gibbz
09-25-2006, 12:36 PM
Ok cheers :thumbsup:

I should be testing this out tommorow and do some tests with flipped uv's....

HalfVector
09-25-2006, 01:38 PM
Oh, by the way. The function showTangentSpace I've shown you earlier renders all vectors, even if they don't face the camera. So I've modified this and now it only renders the vectors facing the camera/view.

fn showTangentSpace tSpace axisLength = (
local worldMat = inverse (viewport.getTM())

gw.setTransform (matrix3 1)

for nFace = 1 to tSpace.count do (
local tbn = tSpace[nFace]

if (dot tbn.row3 worldMat.row3) >= 0.0 do (
gw.setColor #line red
gw.polyLine #( tbn.row4, (tbn.row4 + tbn.row1 * axisLength) ) false
gw.setColor #line green
gw.polyLine #( tbn.row4, (tbn.row4 + tbn.row2 * axisLength) ) false
gw.setColor #line blue
gw.polyLine #( tbn.row4, (tbn.row4 + tbn.row3 * axisLength) ) false
)
)

gw.enlargeUpdateRect #whole
gw.updateScreen()
)

HalfVector
09-25-2006, 04:19 PM
Hi.

I forgot that when you clone an object using the mirror tool, the faces and normals must be flipped. So I've modified the two versions of the script...again. :)

Version #1

fn computeTangentSpace obj = (

local theMesh = snapshotAsMesh obj

local tSpace = #()

-- Do we have to flip faces?
local flip = false
local indices = #(1, 2, 3)
if dot (cross obj.transform.row1 obj.transform.row2) obj.transform.row3 <= 0 do (
indices[2] = 3
indices[3] = 2
flip = true
)

for nFace = 1 to theMesh.numFaces do (
local face = getFace theMesh nFace
local tface = getTVFace theMesh nFace

local v1 = getVert theMesh face[indices[1]]
local v2 = getVert theMesh face[indices[2]]
local v3 = getVert theMesh face[indices[3]]

local uv1 = getTVert theMesh tface[indices[1]]
local uv2 = getTVert theMesh tface[indices[2]]
local uv3 = getTVert theMesh tface[indices[3]]

local dV1 = v1 - v2
local dV2 = v1 - v3

local dUV1 = uv1 - uv2
local dUV2 = uv1 - uv3

local area = dUV1.x * dUV2.y - dUV1.y * dUV2.x
local sign = if area < 0 then -1 else 1

local tangent = [0,0,1]

tangent.x = dV1.x * dUV2.y - dUV1.y * dV2.x
tangent.y = dV1.y * dUV2.y - dUV1.y * dV2.y
tangent.z = dV1.z * dUV2.y - dUV1.y * dV2.z

tangent = (normalize tangent) * sign

local normal = normalize (getFaceNormal theMesh nFace)
if flip do normal = -normal

local binormal = (normalize (cross normal tangent)) * sign

local fCenter = meshOp.getFaceCenter theMesh nFace

append tSpace (Matrix3 tangent binormal normal fCenter)
)

delete theMesh

return tSpace
)

Version #2

fn computeTangentSpace_terathon obj = (

local theMesh = snapshotAsMesh obj

local tSpace = #()

-- Do we have to flip faces?
local flip = false
local indices = #(1, 2, 3)
if dot (cross obj.transform.row1 obj.transform.row2) obj.transform.row3 <= 0 do (
indices[2] = 3
indices[3] = 2
flip = true
)

for nFace = 1 to theMesh.numFaces do (
local face = getFace theMesh nFace
local tface = getTVFace theMesh nFace

local v1 = getVert theMesh face[indices[1]]
local v2 = getVert theMesh face[indices[2]]
local v3 = getVert theMesh face[indices[3]]

local w1 = getTVert theMesh tface[indices[1]]
local w2 = getTVert theMesh tface[indices[2]]
local w3 = getTVert theMesh tface[indices[3]]

local x1 = v2.x - v1.x
local x2 = v3.x - v1.x
local y1 = v2.y - v1.y
local y2 = v3.y - v1.y
local z1 = v2.z - v1.z
local z2 = v3.z - v1.z

local s1 = w2.x - w1.x
local s2 = w3.x - w1.x
local t1 = w2.y - w1.y
local t2 = w3.y - w1.y

local r = 1.0 / (s1 * t2 - s2 * t1)

local tan1 = [(t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r]
local tan2 = [(s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r]

local normal = normalize (getFaceNormal theMesh nFace)
if flip do normal = -normal

local tangent = normalize (tan1 - normal * (dot normal tan1))

local handedness = if (dot (cross normal tan1) tan2) < 0.0 then -1.0 else 1.0

local binormal = (normalize (cross normal tangent)) * handedness

local fCenter = meshOp.getFaceCenter theMesh nFace

append tSpace (Matrix3 tangent binormal normal fCenter)
)

delete theMesh

return tSpace
)

Ok, hope everything is alright now... :D

Gibbz
09-26-2006, 09:19 AM
doing a snapshot should be the same as a xform reset, so it should not matter if the mesh has been mirrored correct?

HalfVector
09-26-2006, 10:03 AM
I think that when you make an snapshot or a xform reset, all the mesh vertices are transformed by the node's current transform matrix and (in the case of a xform reset) then this matrix is reset to the identity matrix. And because of this, when you do a mirror the object is scaled negatively and then, when vertices are transformed, the faces and normals are reversed. I've done a test and seems to work like that.

So doing the handedness check, you can take care of this situation. I do that in the exporter I did for our 3D engine and always worked properly. The good think about this is that you don't need to care about mirrored objects.

Anyway you can skip the check and try for yourself. :)

Greets.

CGTalk Moderation
09-26-2006, 10:03 AM
This thread has been automatically closed as it remained inactive for 12 months. If you wish to continue the discussion, please create a new thread in the appropriate forum.