"""A module for tangent space calculation."""
# ***** BEGIN LICENSE BLOCK *****
#
# Copyright (c) 2007-2009, Python File Format Interface
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * Neither the name of the Python File Format Interface
# project nor the names of its contributors may be used to endorse
# or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# ***** END LICENSE BLOCK *****
from pyffi.utils.mathutils import *
def getTangentSpace(vertices = None, normals = None, uvs = None,
triangles = None, orientation = False,
orthogonal = True):
"""Calculate tangent space data.
>>> vertices = [(0,0,0), (0,1,0), (1,0,0)]
>>> normals = [(0,0,1), (0,0,1), (0,0,1)]
>>> uvs = [(0,0), (0,1), (1,0)]
>>> triangles = [(0,1,2)]
>>> getTangentSpace(vertices = vertices, normals = normals, uvs = uvs, triangles = triangles)
([(0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0)], [(1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0)])
:param vertices: A list of vertices (triples of floats/ints).
:param normals: A list of normals (triples of floats/ints).
:param uvs: A list of uvs (pairs of floats/ints).
:param triangles: A list of triangle indices (triples of ints).
:param orientation: Set to ``True`` to return orientation (this is used by
for instance Crysis).
:return: Two lists of vectors, tangents and binormals. If C{orientation}
is ``True``, then returns an extra list with orientations (containing
floats which describe the total signed surface of all faces sharing
the particular vertex).
"""
# validate input
if len(vertices) != len(normals) or len(vertices) != len(uvs):
raise ValueError(
"lists of vertices, normals, and uvs must have the same length")
bin = [(0,0,0) for i in xrange(len(vertices)) ]
tan = [(0,0,0) for i in xrange(len(vertices)) ]
orientations = [0 for i in xrange(len(vertices))]
# calculate tangents and binormals from vertex and texture coordinates
for t1, t2, t3 in triangles:
# skip degenerate triangles
if t1 == t2 or t2 == t3 or t3 == t1:
continue
# get vertices, uvs, and directions of the triangle
v1 = vertices[t1]
v2 = vertices[t2]
v3 = vertices[t3]
w1 = uvs[t1]
w2 = uvs[t2]
w3 = uvs[t3]
v2v1 = vecSub(v2, v1)
v3v1 = vecSub(v3, v1)
w2w1 = vecSub(w2, w1)
w3w1 = vecSub(w3, w1)
# surface of triangle in texture space
r = w2w1[0] * w3w1[1] - w3w1[0] * w2w1[1]
# sign of surface
r_sign = (1 if r >= 0 else -1)
# contribution of this triangle to tangents and binormals
sdir = (
r_sign * (w3w1[1] * v2v1[0] - w2w1[1] * v3v1[0]),
r_sign * (w3w1[1] * v2v1[1] - w2w1[1] * v3v1[1]),
r_sign * (w3w1[1] * v2v1[2] - w2w1[1] * v3v1[2]))
try:
sdir = vecNormalized(sdir)
except ZeroDivisionError: # catches zero vector
continue # skip triangle
except ValueError: # catches invalid data
continue # skip triangle
tdir = (
r_sign * (w2w1[0] * v3v1[0] - w3w1[0] * v2v1[0]),
r_sign * (w2w1[0] * v3v1[1] - w3w1[0] * v2v1[1]),
r_sign * (w2w1[0] * v3v1[2] - w3w1[0] * v2v1[2]))
try:
tdir = vecNormalized(tdir)
except ZeroDivisionError: # catches zero vector
continue # skip triangle
except ValueError: # catches invalid data
continue # skip triangle
# vector combination algorithm could possibly be improved
for i in (t1, t2, t3):
tan[i] = vecAdd(tan[i], tdir)
bin[i] = vecAdd(bin[i], sdir)
orientations[i] += r
# convert into orthogonal space
xvec = (1, 0, 0)
yvec = (0, 1, 0)
for i, norm in enumerate(normals):
if abs(1-vecNorm(norm)) > 0.01:
raise ValueError(
"tangentspace: unnormalized normal in list of normals (%s, norm is %f)" % (norm, vecNorm(norm)))
try:
# turn norm, bin, tan into a base via Gram-Schmidt
bin[i] = vecSub(bin[i],
vecscalarMul(
norm,
vecDotProduct(norm, bin[i])))
bin[i] = vecNormalized(bin[i])
tan[i] = vecSub(tan[i],
vecscalarMul(
norm,
vecDotProduct(norm, tan[i])))
tan[i] = vecSub(tan[i],
vecscalarMul(
bin[i],
vecDotProduct(norm, bin[i])))
tan[i] = vecNormalized(tan[i])
except ZeroDivisionError:
# insuffient data to set tangent space for this vertex
# in that case pick a space
bin[i] = vecCrossProduct(xvec, norm)
try:
bin[i] = vecNormalized(bin[i])
except ZeroDivisionError:
bin[i] = vecCrossProduct(yvec, norm)
bin[i] = vecNormalized(bin[i])
tan[i] = vecCrossProduct(norm, bin[i])
# return result
if orientation:
return tan, bin, orientations
else:
return tan, bin
if __name__ == "__main__":
import doctest
doctest.testmod()
|