Notifications
Clear all

[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

12 Replies

Ok I found this, this work fine within max with some changes to the synatax I assume?..

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

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?

1 Reply
(@halfvector)
Joined: 11 months ago

Posts: 0

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.

cool thanks

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…

Page 1 / 2