[Closed] Finding bi-normals/tangents
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
Ok I found this, this work fine within max with some changes to the synatax I assume?..
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:
In red, the tangent. In green, the binormal. In blue, the normal.
Hope that helps.
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?
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.
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
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.
Ok cheers
I should be testing this out tommorow and do some tests with flipped uv’s…
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()
)
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…