def qdome2d(vertices, base, normal, precision=0.0001): """Build a convex dome from C{vertices} on top of the two C{base} vertices, in the plane with normal C{normal}. This is a helper function for L{qhull2d}, and should usually not be called directly. :param vertices: The vertices to construct the dome from. :param base: Two vertices that serve as a base for the dome. :param normal: Orientation of the projection plane used for calculating distances. :param precision: Distance used to decide whether points lie outside of the hull or not. :return: A list of vertices that make up a fan of the dome.""" vert0, vert1 = base outer = [(dist, vert) for dist, vert in zip((mathutils.vecDotProduct( mathutils.vecCrossProduct(normal, mathutils.vecSub(vert1, vert0)), mathutils.vecSub(vert, vert0)) for vert in vertices), vertices) if dist > precision] if outer: pivot = max(outer)[1] outer_verts = list(map(operator.itemgetter(1), outer)) return qdome2d(outer_verts, [vert0, pivot], normal, precision) + qdome2d( outer_verts, [pivot, vert1], normal, precision)[1:] else: return base
def ComputeNormal(self): # v = [[], [], []] v0 = self.vertex[0].Vert.Position v1 = self.vertex[1].Vert.Position v2 = self.vertex[2].Vert.Position # v.append(self.vertex[0].Vert.Position) # v.append(self.vertex[1].Vert.Position) # v.append(self.vertex[2].Vert.Position) # a = vecSub(v[1], v[0]) # b = vecSub(v[2], v[0]) a = vecSub(v1, v0) b = vecSub(v2, v0) _normal = self.normal self.normal = vecCrossProduct(a, b) if vecNorm(self.normal) < 0.001: return self.normal = vecNormalized(self.normal) length = vecDotProduct(self.normal, _normal) if length < 0: self.normal = vecscalarMul(self.normal, -1) return
def branchentry(self, branch): if not isinstance(branch, CgfFormat.MeshChunk): # keep recursing return True # get tangents and normals if not (branch.normals_data and branch.tangents_data): return True oldtangents = [tangent for tangent in branch.tangents_data.tangents] self.toaster.msg("recalculating new tangent space") branch.update_tangent_space() newtangents = [tangent for tangent in branch.tangents_data.tangents] self.toaster.msgblockbegin("validating and checking old with new") for norm, oldtangent, newtangent in zip(branch.normals_data.normals, oldtangents, newtangents): # self.toaster.msg("*** %s ***" % (norm,)) # check old norm = (norm.x, norm.y, norm.z) tan_norm = tuple(x / 32767.0 for x in (oldtangent[0].x, oldtangent[0].y, oldtangent[0].z)) bin_norm = tuple(x / 32767.0 for x in (oldtangent[1].x, oldtangent[1].y, oldtangent[1].z)) if abs(mathutils.vecNorm(norm) - 1) > self.SENSITIVITY: self.toaster.logger.warn("normal has non-unit norm") if abs(mathutils.vecNorm(tan_norm) - 1) > self.SENSITIVITY: self.toaster.logger.warn("oldtangent has non-unit norm") if abs(mathutils.vecNorm(bin_norm) - 1) > self.SENSITIVITY: self.toaster.logger.warn("oldbinormal has non-unit norm") if (oldtangent[0].w != oldtangent[1].w): raise ValueError( "inconsistent oldtangent w coordinate (%i != %i)" % (oldtangent[0].w, oldtangent[1].w)) if not (oldtangent[0].w in (-32767, 32767)): raise ValueError("invalid oldtangent w coordinate (%i)" % oldtangent[0].w) if oldtangent[0].w > 0: cross = mathutils.vecCrossProduct(tan_norm, bin_norm) else: cross = mathutils.vecCrossProduct(bin_norm, tan_norm) crossnorm = mathutils.vecNorm(cross) if abs(crossnorm - 1) > self.SENSITIVITY: # a lot of these... self.toaster.logger.warn( "tan_norm and bin_norm not orthogonal") self.toaster.logger.warn("%s %s" % (tan_norm, bin_norm)) self.toaster.logger.warn("(error is %f)" % abs(crossnorm - 1)) cross = mathutils.vecscalarMul(cross, 1.0 / crossnorm) if mathutils.vecDistance(norm, cross) > self.SENSITIVITY: self.toaster.logger.warn( "norm not cross product of tangent and binormal") # self.toaster.logger.warn("norm = %s" % (norm,)) # self.toaster.logger.warn("tan_norm = %s" % (tan_norm,)) # self.toaster.logger.warn("bin_norm = %s" % (bin_norm,)) # self.toaster.logger.warn("tan_norm bin_norm cross prod = %s" % (cross,)) self.toaster.logger.warn("(error is %f)" % mathutils.vecDistance(norm, cross)) # compare old with new if sum((abs(oldtangent[0].x - newtangent[0].x), abs(oldtangent[0].y - newtangent[0].y), abs(oldtangent[0].z - newtangent[0].z), abs(oldtangent[0].w - newtangent[0].w), abs(oldtangent[1].x - newtangent[1].x), abs(oldtangent[1].y - newtangent[1].y), abs(oldtangent[1].z - newtangent[1].z), abs(oldtangent[1].w - newtangent[1].w))) > self.SENSITIVITY * 32767.0: ntan = tuple(x / 32767.0 for x in (newtangent[0].x, newtangent[0].y, newtangent[0].z)) nbin = tuple(x / 32767.0 for x in (newtangent[1].x, newtangent[1].y, newtangent[1].z)) self.toaster.logger.warn( "old and new tangents differ substantially") self.toaster.logger.warn("old tangent") self.toaster.logger.warn("%s %s" % (tan_norm, bin_norm)) self.toaster.logger.warn("new tangent") self.toaster.logger.warn("%s %s" % (ntan, nbin)) self.toaster.msgblockend()
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_norm = [(0, 0, 0) for i in range(len(vertices))] tan_norm = [(0, 0, 0) for i in range(len(vertices))] orientations = [0 for i in range(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 = mathutils.vecSub(v2, v1) v3v1 = mathutils.vecSub(v3, v1) w2w1 = mathutils.vecSub(w2, w1) w3w1 = mathutils.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 = mathutils.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 = mathutils.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_norm[i] = mathutils.vecAdd(tan_norm[i], tdir) bin_norm[i] = mathutils.vecAdd(bin_norm[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 - mathutils.vecNorm(norm)) > 0.01: raise ValueError( "tangentspace: unnormalized normal in list of normals (%s, norm is %f)" % (norm, mathutils.vecNorm(norm))) try: # turn norm, bin_norm, tan_norm into a base via Gram-Schmidt bin_norm[i] = mathutils.vecSub( bin_norm[i], mathutils.vecscalarMul( norm, mathutils.vecDotProduct(norm, bin_norm[i]))) bin_norm[i] = mathutils.vecNormalized(bin_norm[i]) tan_norm[i] = mathutils.vecSub( tan_norm[i], mathutils.vecscalarMul( norm, mathutils.vecDotProduct(norm, tan_norm[i]))) tan_norm[i] = mathutils.vecSub( tan_norm[i], mathutils.vecscalarMul( bin_norm[i], mathutils.vecDotProduct(norm, bin_norm[i]))) tan_norm[i] = mathutils.vecNormalized(tan_norm[i]) except ZeroDivisionError: # insuffient data to set tangent space for this vertex # in that case pick a space bin_norm[i] = mathutils.vecCrossProduct(xvec, norm) try: bin_norm[i] = mathutils.vecNormalized(bin_norm[i]) except ZeroDivisionError: bin_norm[i] = mathutils.vecCrossProduct(yvec, norm) bin_norm[i] = mathutils.vecNormalized(bin_norm[i]) tan_norm[i] = mathutils.vecCrossProduct(norm, bin_norm[i]) # return result if orientation: return tan_norm, bin_norm, orientations else: return tan_norm, bin_norm
def get_mass_center_inertia_polyhedron(vertices, triangles, density=1, solid=True): """Return mass, center of gravity, and inertia matrix for a polyhedron. >>> from pyffi.utils.quickhull import qhull3d >>> box = [(0,0,0),(1,0,0),(0,2,0),(0,0,3),(1,2,0),(0,2,3),(1,0,3),(1,2,3)] >>> vertices, triangles = qhull3d(box) >>> mass, center, inertia = get_mass_center_inertia_polyhedron( ... vertices, triangles, density = 4) >>> mass 24.0 >>> center (0.5, 1.0, 1.5) >>> inertia ((26.0, 0.0, 0.0), (0.0, 20.0, 0.0), (0.0, 0.0, 10.0)) >>> poly = [(3,0,0),(0,3,0),(-3,0,0),(0,-3,0),(0,0,3),(0,0,-3)] # very rough approximation of a sphere of radius 2 >>> vertices, triangles = qhull3d(poly) >>> mass, center, inertia = get_mass_center_inertia_polyhedron( ... vertices, triangles, density = 3) >>> mass 108.0 >>> center (0.0, 0.0, 0.0) >>> abs(inertia[0][0] - 194.4) < 0.0001 True >>> abs(inertia[1][1] - 194.4) < 0.0001 True >>> abs(inertia[2][2] - 194.4) < 0.0001 True >>> abs(inertia[0][1]) < 0.0001 True >>> abs(inertia[0][2]) < 0.0001 True >>> abs(inertia[1][2]) < 0.0001 True >>> sphere = [] >>> N = 10 >>> for j in range(-N+1, N): ... theta = j * 0.5 * math.pi / N ... st, ct = math.sin(theta), math.cos(theta) ... M = max(3, int(ct * 2 * N + 0.5)) ... for i in range(0, M): ... phi = i * 2 * math.pi / M ... s, c = math.sin(phi), math.cos(phi) ... sphere.append((2*s*ct, 2*c*ct, 2*st)) # construct sphere of radius 2 >>> sphere.append((0,0,2)) >>> sphere.append((0,0,-2)) >>> vertices, triangles = qhull3d(sphere) >>> mass, center, inertia = get_mass_center_inertia_polyhedron( ... vertices, triangles, density = 3, solid = True) >>> abs(mass - 100.53) < 10 # 3*(4/3)*pi*2^3 = 100.53 True >>> sum(abs(x) for x in center) < 0.01 # is center at origin? True >>> abs(inertia[0][0] - 160.84) < 10 True >>> mass, center, inertia = get_mass_center_inertia_polyhedron( ... vertices, triangles, density = 3, solid = False) >>> abs(mass - 150.79) < 10 # 3*4*pi*2^2 = 150.79 True >>> abs(inertia[0][0] - mass*0.666*4) < 20 # m*(2/3)*2^2 True """ # 120 times the covariance matrix of the canonical tetrahedron # (0,0,0),(1,0,0),(0,1,0),(0,0,1) # integrate(integrate(integrate(z*z, x=0..1-y-z), y=0..1-z), z=0..1) = 1/120 # integrate(integrate(integrate(y*z, x=0..1-y-z), y=0..1-z), z=0..1) = 1/60 covariance_canonical = ((2, 1, 1), (1, 2, 1), (1, 1, 2)) covariance_correction = 1.0 / 120 covariances = [] masses = [] centers = [] # for each triangle # construct a tetrahedron from triangle + (0,0,0) # find its matrix, mass, and center (for density = 1, will be corrected at # the end of the algorithm) for triangle in triangles: # get vertices vert0, vert1, vert2 = mathutils.operator.itemgetter( *triangle)(vertices) # construct a transform matrix that converts the canonical tetrahedron # into (0,0,0),vert0,vert1,vert2 transform_transposed = (vert0, vert1, vert2) transform = mathutils.matTransposed(transform_transposed) # find the covariance matrix of the transformed tetrahedron/triangle if solid: # we shall be needing the determinant more than once, so # precalculate it determinant = mathutils.matDeterminant(transform) # C' = det(A) * A * C * A^T covariances.append( mathutils.matscalarMul( mathutils.matMul( mathutils.matMul(transform, covariance_canonical), transform_transposed), determinant)) # m = det(A) / 6.0 masses.append(determinant / 6.0) # find center of gravity of the tetrahedron centers.append( tuple(0.25 * sum(vert[i] for vert in (vert0, vert1, vert2)) for i in range(3))) else: # find center of gravity of the triangle centers.append( tuple( sum(vert[i] for vert in (vert0, vert1, vert2)) / 3.0 for i in range(3))) # find mass of triangle # mass is surface, which is half the norm of cross product # of two edges masses.append( mathutils.vecNorm( mathutils.vecCrossProduct(mathutils.vecSub( vert1, vert0), mathutils.vecSub(vert2, vert0))) / 2.0) # find covariance at center of this triangle # (this is approximate only as it replaces triangle with point mass # todo: find better way) covariances.append( tuple( tuple(masses[-1] * x * y for x in centers[-1]) for y in centers[-1])) # accumulate the results total_mass = sum(masses) if total_mass == 0: # dimension is probably badly chosen # raise ZeroDivisionError("mass is zero (consider calculating inertia with a lower dimension)") print("WARNING: mass is nearly zero (%f)" % total_mass) return 0, (0, 0, 0), ((0, 0, 0), (0, 0, 0), (0, 0, 0)) # weighed average of centers with masses total_center = (0, 0, 0) for center, mass in zip(centers, masses): total_center = mathutils.vecAdd( total_center, mathutils.vecscalarMul(center, mass / total_mass)) # add covariances, and correct the values total_covariance = ((0, 0, 0), (0, 0, 0), (0, 0, 0)) for covariance in covariances: total_covariance = mathutils.matAdd(total_covariance, covariance) if solid: total_covariance = mathutils.matscalarMul(total_covariance, covariance_correction) # translate covariance to center of gravity: # C' = C - m * ( x dx^T + dx x^T + dx dx^T ) # with x the translation vector and dx the center of gravity translate_correction = mathutils.matscalarMul( tuple(tuple(x * y for x in total_center) for y in total_center), total_mass) total_covariance = mathutils.matSub(total_covariance, translate_correction) # convert covariance matrix into inertia tensor trace = sum(total_covariance[i][i] for i in range(3)) trace_matrix = tuple( tuple((trace if i == j else 0) for i in range(3)) for j in range(3)) total_inertia = mathutils.matSub(trace_matrix, total_covariance) # correct for given density total_inertia = mathutils.matscalarMul(total_inertia, density) total_mass *= density # correct negative mass if total_mass < 0: total_mass = -total_mass total_inertia = tuple(tuple(-x for x in row) for row in total_inertia) return total_mass, total_center, total_inertia