#!BPY """ Name: 'Ultimate 3D model file format (*.u3d)' Blender: 248a Group: 'Export' Tip: 'Exports models (geometry, materials and animations) to the Ultimate 3D model file format (*.u3d).' """ __author__="Christoph Peters"; __url__="http://Ultimate3D.org"; __version__="1.2.0"; __bpydoc__="""Exports geometry, materials and armatures with animations and constraints of models in Blender to the Ultimate 3D model file format."""; # ----------------------------- LICENSE ----------------------------- # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # Include the needed components of the Blender API import Blender from Blender import Scene from Blender import Group from Blender import Object from Blender import Material from Blender import Mathutils from Blender import Mesh from Blender import Armature from Blender import Ipo from Blender import Draw from Blender import Window # struct is needed to bring data into a binary format import struct # The math module is needed for the usual mathematical operations import math # This module is useful to copy files import shutil # This module is needed to create the directory to which the textures are output import os def Choose(Condition,Value1,Value2): """This function returns Value1, if the given Condition evaluates true or Value2 otherwise.""" if(Condition): return Value1; else: return Value2; class CChunkSizeWriter: """This class rembers the offset at which a chunk size should be written and writes the chunk size in this place on request. It is used to export fileformat version 2.0.""" def __init__(self,File): """This function remembers the current file pointer of the given file and moves it 4=sizeof(DWORD) bytes forward by writing zeroes. Later Write() can write a chunk size at this offset.""" self.oChunkSize=File.tell(); self.File=File; File.write(struct.pack("L",0)); def Write(self): """This function writes a DWORD chunk size at the recently marked position.""" oFilePointer=self.File.tell(); self.File.seek(self.oChunkSize); self.File.write(struct.pack("L",oFilePointer-self.oChunkSize-4)); self.File.seek(oFilePointer); oChunkSize=0; File=None; class C2DVector: """This class represents simple 2D vectors""" def __init__(self,x,y): """Constructs the vector from two components""" self.x=x; self.y=y; def SaveData(self,File): """Saves the data of this vector to the given file""" File.write(struct.pack("ff",self.x,self.y)); x=0.0; y=0.0; class C3DVector: """This class represents simple 3D vectors""" def __init__(self,x,y,z): """Constructs the vector from three components""" self.x=x; self.y=y; self.z=z; def SaveData(self,File): """Saves the data of this vector to the given file""" File.write(struct.pack("fff",self.x,self.y,self.z)); def SaveAsCompressedNormal(self,File,UseV2): """Saves a normalized copy of this vector as 16-bit or 32-bit latitude/longitude vector.""" self.Normalize(); Latitude=-math.asin(self.y); Longitude=math.atan2(self.x,self.z); if(UseV2): Latitude*=20860.12008; Longitude*=10430.06004; File.write(struct.pack("hh",int(Latitude),int(Longitude))); else: Latitude*=20.212678; Longitude*=40.4253555; File.write(struct.pack("bb",int(Latitude),int(Longitude))); def Normalize(self): """Normalizes this vector""" Length=math.sqrt(self.x*self.x+self.y*self.y+self.z*self.z); if(Length>0.0): InvLength=1.0/Length; self.x*=InvLength; self.y*=InvLength; self.z*=InvLength; def Minimize(self,RHS): """This function chooses the minimum of this vector and the given other vector on per-component base.""" self.x=min(self.x,RHS.x); self.y=min(self.y,RHS.y); self.z=min(self.z,RHS.z); def Maximize(self,RHS): """This function chooses the maximum of this vector and the given other vector on per-component base.""" self.x=max(self.x,RHS.x); self.y=max(self.y,RHS.y); self.z=max(self.z,RHS.z); x=0.0; y=0.0; z=0.0; class CMatrix4x4: """This class represents a 4x4 transformation matrix.""" def __init__(self,_11,_12,_13,_14,_21,_22,_23,_24,_31,_32,_33,_34,_41,_42,_43,_44): """This constructor initializes this matrix with the given entries.""" self._11=_11; self._12=_12; self._13=_13; self._14=_14; self._21=_21; self._22=_22; self._23=_23; self._24=_24; self._31=_31; self._32=_32; self._33=_33; self._34=_34; self._41=_41; self._42=_42; self._43=_43; self._44=_44; def CreateFromBlenderMatrix(self,BlenderMatrix): """Copies the given Blender matrix to this matrix.""" if(BlenderMatrix.colSize<4 or BlenderMatrix.rowSize<4): print("Failed to copy a Blender matrix object to a 4x4 matrix. The Blender matrix has a size of "+str(BlenderMatrix.rowSize)+"x"+str(BlenderMatrix.colSize)+"."); else: self._11=BlenderMatrix[0][0]; self._12=BlenderMatrix[0][1]; self._13=BlenderMatrix[0][2]; self._14=BlenderMatrix[0][3]; self._21=BlenderMatrix[1][0]; self._22=BlenderMatrix[1][1]; self._23=BlenderMatrix[1][2]; self._24=BlenderMatrix[1][3]; self._31=BlenderMatrix[2][0]; self._32=BlenderMatrix[2][1]; self._33=BlenderMatrix[2][2]; self._34=BlenderMatrix[2][3]; self._41=BlenderMatrix[3][0]; self._42=BlenderMatrix[3][1]; self._43=BlenderMatrix[3][2]; self._44=BlenderMatrix[3][3]; def ApplyTranslation(self,BlenderVector): self._41=BlenderVector[0]; self._42=BlenderVector[1]; self._43=BlenderVector[2]; def SaveData(self,File): """This function saves this matrix to a given binary file.""" File.write(struct.pack("16f",self._11,self._12,self._13,self._14,self._21,self._22,self._23,self._24,self._31,self._32,self._33,self._34,self._41,self._42,self._43,self._44)); def TransformVector(self,Vector): """This function returns a transformed version of the given vector.""" return C3DVector(Vector.x*self._11+Vector.y*self._21+Vector.z*self._31+self._41,Vector.x*self._12+Vector.y*self._22+Vector.z*self._32+self._42,Vector.x*self._13+Vector.y*self._23+Vector.z*self._33+self._43); def TransformMatrix(self,Matrix): """This function transforms the given matrix by this matrix. The resulting matrix will describe the transformation, that can be achieved by using first the given matrix and after that this matrix.""" return CMatrix4x4( self._11*Matrix._11+self._21*Matrix._12+self._31*Matrix._13+self._41*Matrix._14, self._12*Matrix._11+self._22*Matrix._12+self._32*Matrix._13+self._42*Matrix._14, self._13*Matrix._11+self._23*Matrix._12+self._33*Matrix._13+self._43*Matrix._14, self._14*Matrix._11+self._24*Matrix._12+self._34*Matrix._13+self._44*Matrix._14, self._11*Matrix._21+self._21*Matrix._22+self._31*Matrix._23+self._41*Matrix._24, self._12*Matrix._21+self._22*Matrix._22+self._32*Matrix._23+self._42*Matrix._24, self._13*Matrix._21+self._23*Matrix._22+self._33*Matrix._23+self._43*Matrix._24, self._14*Matrix._21+self._24*Matrix._22+self._34*Matrix._23+self._44*Matrix._24, self._11*Matrix._31+self._21*Matrix._32+self._31*Matrix._33+self._41*Matrix._34, self._12*Matrix._31+self._22*Matrix._32+self._32*Matrix._33+self._42*Matrix._34, self._13*Matrix._31+self._23*Matrix._32+self._33*Matrix._33+self._43*Matrix._34, self._14*Matrix._31+self._24*Matrix._32+self._34*Matrix._33+self._44*Matrix._34, self._11*Matrix._41+self._21*Matrix._42+self._31*Matrix._43+self._41*Matrix._44, self._12*Matrix._41+self._22*Matrix._42+self._32*Matrix._43+self._42*Matrix._44, self._13*Matrix._41+self._23*Matrix._42+self._33*Matrix._43+self._43*Matrix._44, self._14*Matrix._41+self._24*Matrix._42+self._34*Matrix._43+self._44*Matrix._44, ); def Compute3x3Determinant(self): """This function computes the determinant of the 3x3 sub-matrix of this matrix and returns it.""" return self._11*(self._22*self._33-self._32*self._23)-self._12*(self._21*self._33-self._31*self._23)+self._13*(self._21*self._32-self._31*self._22); def RotateAndScaleVector(self,Vector): """This function returns a rotated and scaled version of the given vector. Translation is not applied.""" return C3DVector(Vector.x*self._11+Vector.y*self._21+Vector.z*self._31,Vector.x*self._12+Vector.y*self._22+Vector.z*self._32,Vector.x*self._13+Vector.y*self._23+Vector.z*self._33); def Print(self): """This function outputs this matrix to the console.""" print("_11: "+str(self._11)+" _12: "+str(self._12)+" _13: "+str(self._13)+" _14: "+str(self._14)); print("_21: "+str(self._21)+" _22: "+str(self._22)+" _23: "+str(self._23)+" _24: "+str(self._24)); print("_31: "+str(self._31)+" _32: "+str(self._32)+" _33: "+str(self._33)+" _34: "+str(self._34)); print("_41: "+str(self._41)+" _42: "+str(self._42)+" _43: "+str(self._43)+" _44: "+str(self._44)); _11=1.0; _12=0.0; _13=0.0; _14=0.0; _21=0.0; _22=1.0; _23=0.0; _24=0.0; _31=0.0; _32=0.0; _33=1.0; _34=0.0; _41=0.0; _42=0.0; _43=0.0; _44=1.0; # This global matrix brings a vector from Blender space to Ultimate 3D space # by switching the y and the z axis. If skinning is used it will be set to # the identity matrix, because in this case the root bone does the # transformation. BlenderToU3DSpace=CMatrix4x4(1.0,0.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,0.0,1.0); class CQuaternion: """This class handles a quaternion describing a rotation.""" def __init__(self,x,y,z,w): """This constructor initializes this quaternion with the given entries and normalizes it. If x, y and z are zero an identity rotation is created.""" LengthSq=x*x+y*y+z*z+w*w; if(LengthSq>0.0): InvLength=1.0/math.sqrt(LengthSq); self.x=x*InvLength; self.y=y*InvLength; self.z=z*InvLength; self.w=w*InvLength; else: self.x=0.0; self.y=0.0; self.z=0.0; self.w=1.0; def SaveData(self,File): """This function saves the data of this quaternion to the given file.""" File.write(struct.pack("4f",self.x,self.y,self.z,self.w)); def AppendRotation(self,RHS): """This function multiplies this quaternion by the given quaternion. Transforming a vector with the resulting quaternion will be equivalent to transforming it first with this quaternion and after that with the given quaternion.""" x=self.x; y=self.y; z=self.z; w=self.w; self.x=RHS.w*x+w*RHS.x+RHS.y*z-RHS.z*y; self.y=RHS.w*y+w*RHS.y+RHS.z*x-RHS.x*z; self.z=RHS.w*z+w*RHS.z+RHS.x*y-RHS.y*x; self.w=RHS.w*w-RHS.x*x-RHS.y*y-RHS.z*z; def RotateVector(self,VectorX,VectorY,VectorZ): """This function returns a version of the given vector, to which the rotation described by this vector has been applied.""" return C3DVector( 2.0*(VectorX*(0.5-(self.y*self.y+self.z*self.z))+VectorY*(self.x*self.y-self.z*self.w)+VectorZ*(self.x*self.z+self.y*self.w)), 2.0*(VectorX*(self.x*self.y+self.z*self.w)+VectorY*(0.5-(self.x*self.x+self.z*self.z))+VectorZ*(self.y*self.z-self.x*self.w)), 2.0*(VectorX*(self.x*self.z-self.y*self.w)+VectorY*(self.y*self.z+self.x*self.w)+VectorZ*(0.5-(self.x*self.x+self.y*self.y))) ); # The member variables, describing an identity rotation x=0.0; y=0.0; z=0.0; w=1.0; class CTriangleIndices: """This class holds three vertex indices, describing a triangle.""" def __init__(self,A,B,C): """Constructs the triangle index set from three indices""" self.A=A; self.B=B; self.C=C; def SaveData(self,File): """Saves the data of this triangle index set to the given file""" File.write(struct.pack("HHH",self.A,self.B,self.C)); A=0; B=0; C=0; class CColor: """ This class represents an r, g, b, a color (four floats).""" def __init__(self,r,g,b,a): self.r=r; self.g=g; self.b=b; self.a=a; def SaveData(self,File): File.write(struct.pack("4f",self.r,self.g,self.b,self.a)); r=1.0; g=1.0; b=1.0; a=1.0; class CHeader: """This class collects header information for the *.u3d file""" def __init__(self,RelevantObjects,MaterialList,nBone,nFrame,UVLayerSet): """This constructor collects header data from the given scene.""" # Count the number of meshes for i in RelevantObjects: if(i.getType()=="Mesh"): self.nMesh+=1; if(nBone): self.nMesh=1; # Count the number of frames self.nFrame=max(1,nFrame); # Get the number of materials self.nMaterial=len(MaterialList); if(self.nMaterial==0): self.nMaterial=1; # Get the number of bones self.nBone=nBone; # Get the number of UV layers if(UVLayerSet!=None): self.nTexCoordSet=len(UVLayerSet); else: self.nTexCoordSet=1; print("The scene contains "+str(nBone)+" bones."); def SaveData(self,File,UseV2): """This function writes collected header data to the given file object""" if(UseV2): # Write the chunk header for the file header File.write("$U3D_FILE_HEADER\0"); # Remember the offset for the chunk size ChunkSize=CChunkSizeWriter(File); # Write the file format version File.write(struct.pack("LLL",2,0,0)); # Write the encryption version and the compression version File.write(struct.pack("LL",0,0)); # The chunk is complete ChunkSize.Write(); # Write the chunk header for the model header File.write("$U3D_MODEL_HEADER\0"); ChunkSize=CChunkSizeWriter(File); # Write the mesh counts File.write(struct.pack("LLLL",self.nMesh,self.nMesh,self.nFrame,1)); # Write the material count File.write(struct.pack("L",self.nMaterial)); # Write the bone count File.write(struct.pack("L",self.nBone)); # Vertex tweening is not used File.write(struct.pack("b",0)); # Write the maximal distance of the one and only LoD File.write(struct.pack("f",3.402823466e+38)); # Write the texture coordinate dimensions for i in range(max(1,self.nTexCoordSet)): File.write(struct.pack("L",2)); for i in range(max(1,self.nTexCoordSet),8): File.write(struct.pack("L",0)); # Write the skin weight count File.write(struct.pack("L",Choose(self.nBone>0,3,0))); # Write that there is no shader pack template File.write(struct.pack("b",0)); # The chunk is complete ChunkSize.Write(); else: # Write the null-terminated file format identification string File.write("U3DModelFile\0"); # Write the file version File.write(struct.pack("f",1.0)); #Write the mesh count (twice) File.write(struct.pack("HH",self.nMesh,self.nMesh)); # Write the frame count File.write(struct.pack("L",self.nFrame)); # Write the LoD count File.write(struct.pack("H",1)); # Write the material count File.write(struct.pack("H",self.nMaterial)); # Write the bone count File.write(struct.pack("H",self.nBone)); # Write the cel-shading line strength and the cel-shading color File.write(struct.pack("5f",0.0,0.0,0.0,0.0,0.0)); # Write whether this model uses alpha blending File.write(struct.pack("b",1)); # Write whether this model uses vertex tweening File.write(struct.pack("b",0)); # These variables save the collected header information nMesh=0; nFrame=0; nMaterial=0; nBone=0; nTexCoordSet=0; class CVertex: """This class represents a single vertex. It always contains a position and a normal. Additionally it may contain skin weights with skinning indices and an arbitrary number of texture coordinates.""" def __init__(self,VertexPosition=C3DVector(0.0,0.0,0.0),VertexNormal=C3DVector(0.0,0.0,0.0),VertexTextureCoordinate=None): """This constructor initializes this vertex with the given position, normal and optionally a single texture coordinate (may be None).""" self.Position=VertexPosition; self.Normal=VertexNormal; pTexCoord=[]; if(VertexTextureCoordinate!=None): self.pTexCoord.add(VertexTextureCoordinate); pSkinWeight=[]; piSkinBone=[]; def Compare(self,RHS): """This function returns true, if and only if all data held by the given vertex matches the data of this vertex exactly.""" Result=(self.Position.x==RHS.Position.x and self.Position.y==RHS.Position.y and self.Position.z==RHS.Position.z); Result=Result and (self.Normal.x==RHS.Normal.x and self.Normal.y==RHS.Normal.y and self.Normal.z==RHS.Normal.z); Result=Result and (len(self.pTexCoord)==len(RHS.pTexCoord)); Result=Result and (len(self.pSkinWeight)==len(RHS.pSkinWeight)); Result=Result and (len(self.piSkinBone)==len(RHS.piSkinBone)); if(Result): for i in range(len(self.pTexCoord)): Result=Result and (self.pTexCoord[i].x==RHS.pTexCoord[i].x and self.pTexCoord[i].y==RHS.pTexCoord[i].y); for i in range(len(self.pSkinWeight)): Result=Result and (self.pSkinWeight[i]==RHS.pSkinWeight[i]); for i in range(len(self.piSkinBone)): Result=Result and (self.piSkinBone[i]==RHS.piSkinBone[i]); return Result; Position=C3DVector(0.0,0.0,0.0); Normal=C3DVector(0.0,1.0,0.0); # Either zero or three skin weights for this vertex pSkinWeight=[]; # Either zero or four skinning indices for this vertex piSkinBone=[]; # An arbitrary number of texture coordinates for this vertex (using C2DVector) pTexCoord=[]; class CBranch: """This class represents a branch of an octree. A branch covers a cubic area and it holds either a list of appended vertices (if it is a leave branch) or a list of eight child branches.""" def __init__(self,CubeCenterIn,CubeSizeIn,DepthIn=0): """This constructor initializes this branch without child branches or appended vertices. It makes it cover the given axis aligned cubic area.""" # Save the input data self.CubeCenter=CubeCenterIn; self.CubeSize=CubeSizeIn; self.Depth=DepthIn; # Initialize the branch with no children and no vertices self.pChildBranch=[]; self.piVertex=[]; def Add(self,iVertex,pVertex): """This function adds the vertex with the given index to this branch. It fails changing nothing, if this branch is not a leaf branch! If the maximal number of vertices per branch is exceeded through this step, the branch is split up. For this reason it may need the vertex list.""" if(not self.IsLeaf()): return False; # Add the vertex self.piVertex.append(iVertex); # If necessary split the branch if(len(self.piVertex)>16): self.Split(pVertex); return True; def Split(self,pVertex): """This function splits this branch, meaning that it converts it from a leaf branch to a normal branch. Its vertices are moved to the newly created child branches. For non-leaf branches it does nothing. If the maximal depth would get exceeded by another split, it does nothing. It needs the vertex list to look up vertex positions.""" if(not self.IsLeaf() or self.Depth>=7): return False; # Create the eight child branches HalfCubeSize=self.CubeSize*0.5; P=C3DVector(self.CubeCenter.x+HalfCubeSize,self.CubeCenter.y+HalfCubeSize,self.CubeCenter.z+HalfCubeSize); N=C3DVector(self.CubeCenter.x-HalfCubeSize,self.CubeCenter.y-HalfCubeSize,self.CubeCenter.z-HalfCubeSize); self.pChildBranch=[ CBranch(C3DVector(N.x,N.y,N.z),HalfCubeSize,self.Depth+1), CBranch(C3DVector(N.x,N.y,P.z),HalfCubeSize,self.Depth+1), CBranch(C3DVector(N.x,P.y,N.z),HalfCubeSize,self.Depth+1), CBranch(C3DVector(N.x,P.y,P.z),HalfCubeSize,self.Depth+1), CBranch(C3DVector(P.x,N.y,N.z),HalfCubeSize,self.Depth+1), CBranch(C3DVector(P.x,N.y,P.z),HalfCubeSize,self.Depth+1), CBranch(C3DVector(P.x,P.y,N.z),HalfCubeSize,self.Depth+1), CBranch(C3DVector(P.x,P.y,P.z),HalfCubeSize,self.Depth+1) ]; # Pop the appended vertices from this branch and append them to the right # child branch while(len(self.piVertex)>0): iVertex=self.piVertex.pop(); self.GetChildBranch(pVertex[iVertex].Position).Add(iVertex,pVertex); return True; def GetChildBranch(self,Position): """This function returns the child branch, which contains the given position. It does not test, whether the position is in this branch, so the result may be meaningless. For leaf branches it causes an exception!""" XPositive=Position.x>self.CubeCenter.x; YPositive=Position.y>self.CubeCenter.y; ZPositive=Position.z>self.CubeCenter.z; return self.pChildBranch[4*XPositive+2*YPositive+1*ZPositive]; def IsLeaf(self): """This function returns True, if and only if this branch is a leaf branch.""" return len(self.pChildBranch)==0; # This C3DVector gives the center of the cubic area covered by this branch CubeCenter=C3DVector(0.0,0.0,0.0); # This floating point value gives the edge length of the cubic area covered by # this branch CubeSize=0.0; # This integer saves the depth of this branch. It is needed to avoid endless # splitting. Depth=0; # This is a list of the children of this branch. For leaf branches it is empty # otherwise it gives exactly eight child branches. pChildBranch=[]; # This is a list of the indices of the vertices which are appended to this # branch. It should be empty, if this branch is not a leaf branch. piVertex=[]; class COctree: """This class is used to sort vertices by their position. This makes it possible to find double vertices more efficiently. It is roughly an increasement from O(n^2) to O(n*log(n)).""" def __init__(self,CubeCenter,CubeSize): """This constructor initializes this octree so that it can cover an axis aligned cubic area with edge length CubeSize and its center at the given C3DVector. It associates it with the given vertex list. Vertex indices always refer to this list.""" # Initialize the root branch to cover the given area self.Root=CBranch(CubeCenter,CubeSize); def FindBranch(self,Position): """This function finds the leaf branch, which covers the given position and returns it. It does not verify that the position is in this octree.""" Result=self.Root; while(not Result.IsLeaf()): Result=Result.GetChildBranch(Position); return Result; # This is the root branch of the octree Root=None; class CMesh: """This class is used to collect and export mesh data""" def __init__(self,MeshObject,MaterialList,BoneList,MeshTransformation,iMeshPerFrame,UVLayerSet): """This constructor collects mesh data from the given mesh. The given material and bone list are used for finding indices, the mesh transformation is applied to the vertex data, double vertices are removed, the given index is saved and if UVLayerSet is not None all UV layers in the given set will be saved in the vertex data.""" self.CleanUp(); # Save the mesh index self.iMeshPerFrame=iMeshPerFrame; # Get the mesh name self.Name=MeshObject.name; # Get the number of texture coordinate sets and a list of UV layers to export if(MeshObject.vertexUV or MeshObject.faceUV): self.nTexCoordSet=1; else: self.nTexCoordSet=0; if(UVLayerSet!=None): self.nTexCoordSet=len(UVLayerSet); elif(self.nTexCoordSet==1): UVLayerSet=frozenset([MeshObject.getUVLayerNames()[0]]); elif(self.nTexCoordSet==0): UVLayerSet=frozenset([]); # Compute the complete mesh transformation if(len(BoneList)==0): MeshToU3DSpace=BlenderToU3DSpace.TransformMatrix(MeshTransformation); else: MeshToU3DSpace=MeshTransformation; # Determine whether the mesh is partially solid (in this case the # creation of the vertex list is more complicated) Solid=False; for Face in MeshObject.faces: if(not Face.smooth): Solid=True; break; # There are two different cases when it comes to mapping Blender vertex # indices to the indices used in the output: # 1) They are identical (if UseFaceVertices==False) # 2) The output indices are the unique face vertex indices. Face vertex # indices can be obtained by checking how many vertices are used by faces # with smaller index or earlier in the same face. To get to indices of # unique face vertices you need to map face vertex indices through # piFaceVertex. Multiple face vertex indices may correspond to the same # Blender vertex index and a single unique face vertex may refer to # multiple Blender vertex indices (if UseFaceVertices==True). # All of this is necessary because different faces may associate different # texture coordinates and normals with a single Blender vertex. piFaceVertex=[]; # The bounding box may be needed to remove double vertices efficiently using # an octree. Initialize vectors to determine it. MinVec=C3DVector( 1.175494351e-38, 1.175494351e-38, 1.175494351e-38); MaxVec=C3DVector(-1.175494351e-38,-1.175494351e-38,-1.175494351e-38); # If the texture coordinates and the normals are saved per vertex the # process is simple UseFaceVertices=((MeshObject.vertexUV==False and MeshObject.faceUV) or Solid); if(not UseFaceVertices): for v in MeshObject.verts: # Collect the data of the vertex and add it to the list NewVertex=CVertex(); NewVertex.Position=C3DVector(v.co.x,v.co.y,v.co.z); NewVertex.Normal=C3DVector(v.no.x,v.no.y,v.no.z); NewVertex.pTexCoord=[]; self.pVertex.append(NewVertex); # Update the bounding box MinVec.Minimize(NewVertex.Position); MaxVec.Maximize(NewVertex.Position); self.nVertex=len(self.pVertex); # Get the texture coordinates from all relevant UV layers PreviousUVLayer=MeshObject.activeUVLayer; for UVLayer in UVLayerSet: MeshObject.activeUVLayer=UVLayer; for j in range(self.nVertex): if(MeshObject.vertexUV): self.pVertex[j].pTexCoord.append(C2DVector(MeshObject.verts[j].uvco.x,MeshObject.verts[j].uvco.y)); else: self.pVertex[j].pTexCoord.append(C2DVector(0.0,0.0)); if(PreviousUVLayer!=None): MeshObject.activeUVLayer=PreviousUVLayer; # If the texture coordinates or the normals are saved on per triangle # base get them from there else: # This case is a little complicated. One vertex gets created for every # face vertex. Then the list gets reduced to unique combinations of # positions, texture coordinates and normals. During this process a # lookup table for creating the index buffer gets created. # For all faces iterate through all vertices and create a list of them pFaceVertex=[]; for i in range(len(MeshObject.faces)): for j in range(len(MeshObject.faces[i].verts)): NewVertex=CVertex(); # Get the position Position=MeshObject.faces[i].verts[j].co; NewVertex.Position=C3DVector(Position.x,Position.y,Position.z); # Get the normal if(MeshObject.faces[i].smooth): Normal=MeshObject.faces[i].verts[j].no; else: Normal=MeshObject.faces[i].no; NewVertex.Normal=C3DVector(Normal.x,Normal.y,Normal.z); NewVertex.pTexCoord=[]; pFaceVertex.append(NewVertex); # Update the bounding box MinVec.Minimize(NewVertex.Position); MaxVec.Maximize(NewVertex.Position); # Get the texture coordinates from all relevant UV layers PreviousUVLayer=MeshObject.activeUVLayer; for UVLayer in UVLayerSet: MeshObject.activeUVLayer=UVLayer; iFaceVertex=0; for i in range(len(MeshObject.faces)): for j in range(len(MeshObject.faces[i].verts)): # Get the texture coordinate or use a default value if(MeshObject.vertexUV): TexCoord=MeshObject.faces[i].verts[j].uvco elif(MeshObject.faceUV): TexCoord=MeshObject.faces[i].uv[j]; else: TexCoord=C2DVector(0.0,0.0); pFaceVertex[iFaceVertex].pTexCoord.append(C2DVector(TexCoord.x,TexCoord.y)); iFaceVertex+=1; if(PreviousUVLayer!=None): MeshObject.activeUVLayer=PreviousUVLayer; # Now remove double vertices # Inform the client print("Removing double vertices for the mesh \""+self.Name+"\"."); # Create an octree, which can contain the entire bounding box. All # unique vertices are added to it. This way it is possible to # determine uniqueness of vertices fast. BBoxSize=C3DVector(MaxVec.x-MinVec.x,MaxVec.y-MinVec.y,MaxVec.z-MinVec.z); Octree=COctree(C3DVector(MinVec.x+BBoxSize.x*0.5,MinVec.y+BBoxSize.y*0.5,MinVec.z+BBoxSize.z*0.5),max(BBoxSize.x,BBoxSize.y,BBoxSize.z)); # Cycle through all vertices and check whether they equal one of the # unique vertices found so far. If they do not append them. nUniqueVertex=0; for FaceVertex in pFaceVertex: # Search the vertex in the octree Found=False; Branch=Octree.FindBranch(FaceVertex.Position); for i in Branch.piVertex: if(FaceVertex.Compare(pFaceVertex[i])): # Use the existing identical vertex piFaceVertex.append(i); Found=True; break; if(not Found): # A new unique vertex has been found. Add it to the list and # to the octree. The right branch is already known. pFaceVertex[nUniqueVertex]=FaceVertex; piFaceVertex.append(nUniqueVertex); Branch.Add(nUniqueVertex,pFaceVertex); nUniqueVertex+=1; print("Removed "+str(len(pFaceVertex)-nUniqueVertex)+" vertices ("+str(100-int((100.0*nUniqueVertex)/len(pFaceVertex)))+"% and "+str(nUniqueVertex)+" remain)."); # Reducing the list has been done in the list. Slice away the # remaining duplicate vertices and set it up as mesh vertex list. self.pVertex=pFaceVertex[0:nUniqueVertex]; self.nVertex=nUniqueVertex; # Coordinates in Blender and U3D differ. Transform the vertex data as needed. for Vertex in self.pVertex: Vertex.Position=MeshToU3DSpace.TransformVector(Vertex.Position); Vertex.Normal=MeshToU3DSpace.RotateAndScaleVector(Vertex.Normal); for TexCoord in Vertex.pTexCoord: TexCoord.y=1.0-TexCoord.y; # Get the triangle count and the triangles with their material indices iFaceVertex=0; for Face in MeshObject.faces: # See the comment of piFaceVertex piVertex=[]; if(not UseFaceVertices): for FaceVertex in Face.verts: # Use Blender indices piVertex.append(FaceVertex.index); else: for i in range(len(Face.verts)): # Use indices of unique face vertices piVertex.append(piFaceVertex[iFaceVertex+i]); # Put the three or four face vertex indices togher to one or two # triangles (the code is ready for faces with more vertices) for i in range(1,len(Face.verts)-1): self.pTriangle.append(CTriangleIndices(piVertex[0],piVertex[i+1],piVertex[i])); if(len(MeshObject.materials)>0): if(MeshObject.materials[Face.mat] in MaterialList): self.piTriangleMaterial.append(MaterialList.index(MeshObject.materials[Face.mat])); else: self.piTriangleMaterial.append(0); else: self.piTriangleMaterial.append(0); self.nTriangle+=1; iFaceVertex+=len(Face.verts); # Get the skinning weights if necessary pSkinnedVertex=[]; if(len(BoneList)>0): self.SkinningUsed=True; self.iMaxBone=len(BoneList)-1; # Create a list representing the data of the skin weights for all Blender # vertices. If face vertices are not used they are written to the output # vertices immediately. for i in range(len(MeshObject.verts)): # Get the influences of this vertex pInfluence=MeshObject.getVertexInfluences(i); # Create a vertex. Only the skin weight members are actually used Weight=None; if(UseFaceVertices): pSkinnedVertex.append(CVertex()); Weight=pSkinnedVertex[i]; else: Weight=self.pVertex[i]; Weight.pSkinWeight=[]; Weight.piSkinBone=[]; # Find the four biggest bone influences while(len(Weight.pSkinWeight)<4 and len(pInfluence)>0): # Determine the biggest influence MaximumInfluence=0.0; iMaximumInfluence=0; for j in range(len(pInfluence)): if(MaximumInfluence0): # Cycle through all face vertices iFaceVertex=0; for i in range(len(MeshObject.faces)): for j in range(len(MeshObject.faces[i].verts)): # And assign the matching skinning data iVertex=piFaceVertex[iFaceVertex]; Destination=self.pVertex[iVertex]; Source=pSkinnedVertex[MeshObject.faces[i].verts[j].index]; Destination.pSkinWeight=Source.pSkinWeight; Destination.piSkinBone=Source.piSkinBone; iFaceVertex+=1; def SaveData(self,File,UseV2): """This function writes the collected data to the given file object""" # Check whether the model can be written if(self.nVertex>65535 or self.nTriangle>65535): print("The model can not be written to an Ultimate 3D model file, because one of the meshes has more than 65535 triangles or vertices."); Draw.PupMenu("Error%t|The model can not be exported because one mesh contains more than 65535 triangles/vertices."); return False; if(UseV2): # Write the chunk header File.write("$U3D_MESH\0"); ChunkSize=CChunkSizeWriter(File); # Write the mesh ID values File.write(struct.pack("LLL",self.iMeshPerFrame,0,0)); else: # Write the null-terminated chunk identifier File.write("$U3D_MESH\0"); # Write the null-terminated mesh name File.write(self.Name+"\0"); if(UseV2==False): # Write the frame index for the mesh (0) File.write(struct.pack("H",0)); # Write the lod of this mesh (1.0) File.write(struct.pack("f",1.0)); # Write the normal scalar NormalScalar=Choose(self.SkinningUsed,-1.0,1.0); File.write(struct.pack("f",NormalScalar)); # Write whether this mesh contains valid inverse tangent space matrices File.write(struct.pack("b",0)); # Write the vertex count File.write(struct.pack(Choose(UseV2,"L","H"),self.nVertex)); if(UseV2==False): # Write the texture coordinate dimension array for i in range(self.nTexCoordSet): File.write(struct.pack("H",2)); for i in range(self.nTexCoordSet,8): File.write(struct.pack("H",0)); # Write the number of skin weights per vertex File.write(struct.pack("H",3*self.SkinningUsed)); # Write the maximum index of the bones that are influencing this mesh File.write(struct.pack("B",self.iMaxBone)); # Write the vertex and triangle count of shadow optimized geometry File.write(struct.pack("HH",0,0)); # Write the vertex positions for Vertex in self.pVertex: Vertex.Position.SaveData(File); # Write the vertex normals if(UseV2): for Vertex in self.pVertex: # In version 2.0 the normal scalar is applied to the normals # in the file after loading. Since the normals are correct now # they need to be identical after loading. Scale them by the # normal scalar now (1*1=(-1)*(-1)=1). ScaledNormal=Vertex.Normal; ScaledNormal.x*=NormalScalar; ScaledNormal.y*=NormalScalar; ScaledNormal.z*=NormalScalar; ScaledNormal.SaveAsCompressedNormal(File,UseV2); else: for Vertex in self.pVertex: Vertex.Normal.SaveAsCompressedNormal(File,UseV2); if(UseV2==False): # Whether a texture coordinate buffer is to be used and whether the # buffer is owned File.write(struct.pack("b",0)); File.write(struct.pack("b",1)); # Write the vertex texture coordinates for i in range(self.nTexCoordSet): for Vertex in self.pVertex: Vertex.pTexCoord[i].SaveData(File); if(UseV2 and self.nTexCoordSet==0): NullTexCoord=C2DVector(0.0,0.0); for i in range(self.nVertex): NullTexCoord.SaveData(File); # Export skinning data, if necessary if(self.SkinningUsed): # Write the skin weights for Vertex in self.pVertex: File.write(struct.pack("3f",Vertex.pSkinWeight[0],Vertex.pSkinWeight[1],Vertex.pSkinWeight[2])); # Write the bone indices for each vertex for Vertex in self.pVertex: File.write(struct.pack("4B",Vertex.piSkinBone[0],Vertex.piSkinBone[1],Vertex.piSkinBone[2],Vertex.piSkinBone[3])); # Write the triangle count File.write(struct.pack(Choose(UseV2,"L","H"),self.nTriangle)); # Whether the triangle indices are owned by this mesh (true) File.write(struct.pack("b",1)); # Write the index buffer for Triangle in self.pTriangle: Triangle.SaveData(File); # Write the triangle material indices for i in range(self.nTriangle): File.write(struct.pack("H",self.piTriangleMaterial[i])); if(UseV2): # Whether there is shadow optimized geometry (false) File.write(struct.pack("b",0)); # The chunk is completed ChunkSize.Write(); # The function has succeeded return True; def AppendMesh(self,MeshObject): """This function appends the given mesh object to this mesh object.""" # Combine the names self.Name=self.Name+"+"+MeshObject.Name; # Combine the triangle lists for i in range(MeshObject.nTriangle): self.pTriangle.append(CTriangleIndices(MeshObject.pTriangle[i].A+self.nVertex,MeshObject.pTriangle[i].B+self.nVertex,MeshObject.pTriangle[i].C+self.nVertex)); self.piTriangleMaterial.append(MeshObject.piTriangleMaterial[i]); self.nTriangle+=MeshObject.nTriangle; # Combine the vertex lists self.pVertex.extend(MeshObject.pVertex); self.nVertex+=MeshObject.nVertex; def CleanUp(self): """This function cleans up this mesh object completely.""" self.iMeshPerFrame=0; self.Name=""; self.nVertex=self.nTriangle=0; self.nTexCoordSet=0; self.pVertex=[]; self.pTriangle=[]; self.piTriangleMaterial=[]; self.SkinningUsed=False; self.iMaxBone=0; # The index of this mesh iMeshPerFrame=0; # The name of this mesh Name=""; # The number of vertices and triangles nVertex=0; nTriangle=0; # This integer saves the number of texture coordinate sets available in this mesh nTexCoordSet=0; # The array of vertices pVertex=[]; # If this boolean is True the vertices have three skinning weights and four # skinning indices. Otherwise they have neither weights nor indices. SkinningUsed=False; # This integer saves the highest referenced bone index (according to the skinning # indices) iMaxBone=0; # The triangle index array pTriangle=[]; # The triangle material index array piTriangleMaterial=[]; def BlenderTexOpToD3D(BlendMode): """This is a little utility function, which converts a given Blender texture operation (resp. blend mode) to the D3DTEXTUREOP DWORD value, which matches it best.""" if(BlendMode==Blender.Texture.BlendModes["MIX"]): return 13; elif(BlendMode==Blender.Texture.BlendModes["MULTIPLY"]): return 4; elif(BlendMode==Blender.Texture.BlendModes["ADD"]): return 7; elif(BlendMode==Blender.Texture.BlendModes["SUBTRACT"]): return 10; elif(BlendMode==Blender.Texture.BlendModes["DIVIDE"]): return 4; elif(BlendMode==Blender.Texture.BlendModes["DARKEN"]): return 4; elif(BlendMode==Blender.Texture.BlendModes["DIFFERENCE"]): return 10; elif(BlendMode==Blender.Texture.BlendModes["LIGHTEN"]): return 7; elif(BlendMode==Blender.Texture.BlendModes["SCREEN"]): return 4; return 1; class CMaterial: """This class is used to collect data about materials and to write it to a *.u3d file.""" def __init__(self,MaterialObject,TexturePath,iMaterial,UVLayerSet): """This constructor collects data from the given material object.""" self.CleanUp(); # Get the material index and the name self.iMaterial=iMaterial; self.Name=MaterialObject.name; # Get the material colors self.AmbientColor=CColor(MaterialObject.R*MaterialObject.amb,MaterialObject.G*MaterialObject.amb,MaterialObject.B*MaterialObject.amb,MaterialObject.alpha); self.DiffuseColor=CColor(MaterialObject.R*MaterialObject.ref,MaterialObject.G*MaterialObject.ref,MaterialObject.B*MaterialObject.ref,MaterialObject.alpha); self.SpecularColor=CColor(MaterialObject.specR*MaterialObject.spec,MaterialObject.specG*MaterialObject.spec,MaterialObject.specB*MaterialObject.spec,1.0); self.EmissiveColor=CColor(MaterialObject.R*MaterialObject.emit,MaterialObject.G*MaterialObject.emit,MaterialObject.B*MaterialObject.emit,1.0); self.SpecularPower=MaterialObject.getHardness(); # Determine the number of used textures and get the list of texture # files (after unpacking them in case they are packed) self.nTexture=0; for iTexture in MaterialObject.enabledTextures: Texture=MaterialObject.getTextures()[iTexture]; if(Texture==None): continue; Image=Texture.tex.getImage(); if(Image==None): continue; # Determine the texture coordinate set iTexCoordSet=0; if(UVLayerSet): iUVLayer=0 for UVLayer in UVLayerSet: if(Texture.uvlayer==UVLayer): iTexCoordSet=iUVLayer; iUVLayer+=1; self.piTexCoordSet.append(iTexCoordSet); # Get the texture operation self.pTextureOperation.append(Texture.blendmode); # Get the file name (without path) FileName=Image.getFilename(); for i in range(len(FileName)-1,0,-1): if(FileName[i]=="/" or FileName[i]=="\\"): FileName=FileName[i+1:]; break; # Remove slashes from the beginning of the file name with path FileNameWithPath=Image.getFilename(); while FileNameWithPath[0]=="\\" or FileNameWithPath[0]=="/": FileNameWithPath=FileNameWithPath[1:]; # Unpack the texture if necessary and remember whether the resulting file # should be deleted after repacking Packed=Image.packed; DeleteTexture=not os.path.exists(FileNameWithPath); if(Image.packed): Image.unpack(Blender.UnpackModes["WRITE_LOCAL"]); # Copy the texture file to the texture path if(os.path.normcase(os.path.normpath(os.path.abspath(FileNameWithPath)))!=os.path.normcase(os.path.normpath(os.path.abspath(TexturePath+FileName)))): if(os.path.exists(TexturePath+FileName)): try: os.remove(TexturePath+FileName); except OSError: print("Failed to remove the recently exported texture file \""+TexturePath+FileName+"\"."); try: shutil.copyfile(FileNameWithPath,TexturePath+FileName); except IOError: print("Failed to copy the texture file \""+FileNameWithPath+"\" to the Textures folder (\""+TexturePath+FileName+"\"). You may be able to fix this by opening the file directly with Blender instead of opening it through File->Open."); # Add the texture to the list self.nTexture+=1; self.pTextureFile.append(FileName); # Repack the texture in case it has been unpacked if(Packed and os.path.exists(FileNameWithPath)): Image.pack(); # Ensure that the file is deleted, or in existance dependent on its # original state if(DeleteTexture): if(os.path.exists(FileNameWithPath)): try: os.remove(FileNameWithPath); except OSError: print("Failed to remove a temporarily unpacked texture file."); else: if(not os.path.exists(FileNameWithPath)): Image.save(); def SaveData(self,File,UseV2): """This function writes the collected data to the given file.""" # Write the null-terminated the chunk header or the material identifier if(UseV2): File.write("$U3D_MATERIAL\0"); ChunkSize=CChunkSizeWriter(File); # Write the material index File.write(struct.pack("L",self.iMaterial)); else: File.write("$U3D_MAT\0"); # Write the null-terminated material name File.write(self.Name+"\0"); # Write default material colors self.AmbientColor.SaveData(File); self.DiffuseColor.SaveData(File); self.SpecularColor.SaveData(File); self.EmissiveColor.SaveData(File); # Write the specular power File.write(struct.pack("f",self.SpecularPower)); # Write the parallax information if(UseV2): File.write(struct.pack("ff",0.0,1.0)); # Write the number of textures if(UseV2==False): self.nTexture=min(8,self.nTexture); File.write(struct.pack("H",self.nTexture)); # Write default texture operations pTextureOperation=[1,1,1,1,1,1,1,1]; if(self.nTexture>0): pTextureOperation[0]=5; for i in range(1,self.nTexture): pTextureOperation[i]=BlenderTexOpToD3D(self.pTextureOperation[i]); for i in range(0,8): File.write(struct.pack("L",pTextureOperation[i])); # Write texture coordinate sets for i in range(0,self.nTexture): File.write(struct.pack("L",self.piTexCoordSet[i])); for i in range(self.nTexture,8): File.write(struct.pack("L",0)); # Write whether the resources are part of the file (false) if(UseV2==False): File.write(struct.pack("b",0)); # Write the texture data if(UseV2): for i in range(8): if(i0): BoneToParentSpace=BoneObject.matrix["ARMATURESPACE"]*(BlenderBoneList[self.iParent-1].matrix["ARMATURESPACE"].copy().invert()); else: BoneToParentSpace=BoneObject.matrix["ARMATURESPACE"]*ArmatureObj.getMatrix(); # Create the keys for Action in pAction: if(len(Action.getFrameNumbers())==0): continue; if(ExportThroughIpo): self.AddKeysFromActionIpo(Action,BoneToParentSpace); else: self.AddKeysFromActionPose(Action,ArmatureObj); self.nFrame+=max(Action.getFrameNumbers())+1; # Make sure that there's at least one key of every type to have the # bone to parent space transformation included if(len(self.pTranslationKey)==0): BoneToParentTranslation=BoneToParentSpace.translationPart(); self.pTranslationKey.append(CTranslationKey(0,C3DVector(BoneToParentTranslation[0],BoneToParentTranslation[1],BoneToParentTranslation[2]))); if(len(self.pRotationKey)==0): BoneToParentRotation=BoneToParentSpace.toQuat(); self.pRotationKey.append(CRotationKey(0,CQuaternion(BoneToParentRotation.x,BoneToParentRotation.y,BoneToParentRotation.z,BoneToParentRotation.w))); if(len(self.pScalingKey)==0): BoneToParentScaling=BoneToParentSpace.scalePart(); self.pScalingKey.append(CScalingKey(0,C3DVector(BoneToParentScaling.x,BoneToParentScaling.y,BoneToParentScaling.z))); def AddKeysFromActionIpo(self,Action,BoneToParentSpace): """This function adds key frames from the given action to this bone. oFrame will be added to the frame values of the key frames.""" # Get the relevant Ipo try: AnimationIpo=Action.getChannelIpo(self.Name); except: # There's no animation for this bone, return return; # Get the transformation curves in the ipo TranslationXCurve=TranslationYCurve=TranslationZCurve=None; RotationXCurve=RotationYCurve=RotationZCurve=RotationWCurve=None; ScalingXCurve=ScalingYCurve=ScalingZCurve=None; for Curve in AnimationIpo: if(Curve.name=="PO_LOCX" or Curve.name=="LocX"): TranslationXCurve=Curve; elif(Curve.name=="PO_LOCY" or Curve.name=="LocY"): TranslationYCurve=Curve; elif(Curve.name=="PO_LOCZ" or Curve.name=="LocZ"): TranslationZCurve=Curve; elif(Curve.name=="PO_QUATX" or Curve.name=="QuatX"): RotationXCurve=Curve; elif(Curve.name=="PO_QUATY" or Curve.name=="QuatY"): RotationYCurve=Curve; elif(Curve.name=="PO_QUATZ" or Curve.name=="QuatZ"): RotationZCurve=Curve; elif(Curve.name=="PO_QUATW" or Curve.name=="QuatW"): RotationWCurve=Curve; elif(Curve.name=="PO_SIZEX" or Curve.name=="SizeX"): ScalingXCurve=Curve; elif(Curve.name=="PO_SIZEY" or Curve.name=="SizeY"): ScalingYCurve=Curve; elif(Curve.name=="PO_SIZEZ" or Curve.name=="SizeZ"): ScalingZCurve=Curve; # Create the keys (the transformation described by them needs to be # "multiplied from the right" with the bone to parent space matrix) SortedKeyFrameList=Action.getFrameNumbers(); SortedKeyFrameList.sort(); BoneToParentScaling=BoneToParentSpace.scalePart(); # If the scaling is not homogenous output a warning if(BoneToParentScaling.xBoneToParentScaling.y+0.001 or BoneToParentScaling.yBoneToParentScaling.z+0.001): print("WARNING: The bone "+self.Name+" has an inhomogenous scaling in relation to its parent. Since inhomogenous scaling (scaling that's not the same for all axes) is not supported the scaling has been made homogenous."); print("The inhomogenous scaling is X="+str(BoneToParentScaling.x)+" Y="+str(BoneToParentScaling.y)+" Z="+str(BoneToParentScaling.z)); BoneToParentScaling.x=BoneToParentScaling.y=BoneToParentScaling.z=(BoneToParentScaling.x+BoneToParentScaling.y+BoneToParentScaling.z)/3.0; BoneToParentTranslation=BoneToParentSpace.translationPart(); BoneToParentRotation=BoneToParentSpace.toQuat(); BoneToParentRotation=CQuaternion(BoneToParentRotation.x,BoneToParentRotation.y,BoneToParentRotation.z,BoneToParentRotation.w); for i in SortedKeyFrameList: KeyTranslation=C3DVector(0.0,0.0,0.0); KeyScaling=C3DVector(1.0,1.0,1.0); KeyRotation=CQuaternion(0.0,0.0,0.0,1.0); if(TranslationXCurve!=None): KeyTranslation=C3DVector(TranslationXCurve[i],TranslationYCurve[i],TranslationZCurve[i]); if(RotationXCurve!=None): KeyRotation=CQuaternion(RotationXCurve[i],RotationYCurve[i],RotationZCurve[i],RotationWCurve[i]); if(ScalingXCurve!=None): KeyScaling=C3DVector(ScalingXCurve[i],ScalingYCurve[i],ScalingZCurve[i]); # Transform the key data KeyScaling=C3DVector(KeyScaling.x*BoneToParentScaling.x,KeyScaling.y*BoneToParentScaling.y,KeyScaling.z*BoneToParentScaling.z); RotatedTranslation=BoneToParentRotation.RotateVector(KeyTranslation.x,KeyTranslation.y,KeyTranslation.z); KeyTranslation=C3DVector(RotatedTranslation.x+BoneToParentTranslation.x,RotatedTranslation.y+BoneToParentTranslation.y,RotatedTranslation.z+BoneToParentTranslation.z); KeyRotation.AppendRotation(BoneToParentRotation); # Save the transformed key data if(TranslationXCurve!=None): self.pTranslationKey.append(CTranslationKey(i+self.nFrame,KeyTranslation)); if(RotationXCurve!=None): self.pRotationKey.append(CRotationKey(i+self.nFrame,KeyRotation)); if(ScalingXCurve!=None): self.pScalingKey.append(CScalingKey(i+self.nFrame,KeyScaling)); def AddKeysFromActionPose(self,Action,ArmatureObj): """This function implements an important feature in the only possible way, which is very ugly. It makes it possible to retrieve animations, which use constraints like IKs. The keys will be appended to the array of this instance.""" # Get the pose Pose=ArmatureObj.getPose(); # Save the current pose and clear the pose bone transformations pPoseBoneTranslation=[]; pPoseBoneScaling=[]; pPoseBoneRotation=[]; for PoseBone in Pose.bones.values(): pPoseBoneTranslation.append(PoseBone.loc); pPoseBoneScaling.append(PoseBone.size); pPoseBoneRotation.append(PoseBone.quat); PoseBone.loc.x=PoseBone.loc.y=PoseBone.loc.z=0.0; PoseBone.size.x=PoseBone.size.y=PoseBone.size.z=1.0; PoseBone.quat.x=PoseBone.quat.y=PoseBone.quat.z=0.0; PoseBone.quat.w=1.0; # Save some information that needs to be restored PreviousAction=ArmatureObj.getAction(); PreviousFrame=Blender.Get("curframe"); # Activate the given action Action.setActive(ArmatureObj); # Cycle through all relevant frames # NOTE: It may be that additional frames are relevant. An alternative, # but also bad solution would be exporting one key for every frame. SortedKeyFrameList=Action.getFrameNumbers(); if(0 not in SortedKeyFrameList): SortedKeyFrameList.append(0); SortedKeyFrameList.sort(); for Frame in SortedKeyFrameList: # Set up the right frame for the pose ArmatureObj.evaluatePose(Frame); # Retrieve the transformation in relation to the parent PoseBoneTransformation=Pose.bones[self.Name].poseMatrix; if(Pose.bones[self.Name].parent!=None): PoseBoneTransformation*=Pose.bones[self.Name].parent.poseMatrix.copy().invert(); else: PoseBoneTransformation*=ArmatureObj.getMatrix(); # Create the transformation keys Rotation=PoseBoneTransformation.toQuat(); Scaling=PoseBoneTransformation.scalePart(); Translation=PoseBoneTransformation.translationPart(); RotationFactor=1; if(len(self.pRotationKey)>0): PreviousRotation=self.pRotationKey[len(self.pRotationKey)-1].Rotation; if(PreviousRotation.x*Rotation.x+PreviousRotation.y*Rotation.y+PreviousRotation.z*Rotation.z+PreviousRotation.w*Rotation.w<0.0): RotationFactor=-1; self.pRotationKey.append(CRotationKey(Frame+self.nFrame,CQuaternion(Rotation.x*RotationFactor,Rotation.y*RotationFactor,Rotation.z*RotationFactor,Rotation.w*RotationFactor))); self.pScalingKey.append(CScalingKey(Frame+self.nFrame,C3DVector(Scaling.x,Scaling.y,Scaling.z))); self.pTranslationKey.append(CTranslationKey(Frame+self.nFrame,C3DVector(Translation.x,Translation.y,Translation.z))); # Restore the pose transformation for PoseBone in Pose.bones.values(): PoseBone.loc=pPoseBoneTranslation.pop(0); PoseBone.size=pPoseBoneScaling.pop(0); PoseBone.quat=pPoseBoneRotation.pop(0); # Restore the previous action and the previous frame if(PreviousAction!=None): PreviousAction.setActive(ArmatureObj); else: ArmatureObj.action=None; Blender.Set("curframe",PreviousFrame); def SaveData(self,File,UseV2): """This function saves all data of this bone to the given file.""" # Write the chunk header or the null-terminated bone chunk identifier ChunkSize=None; if(UseV2): File.write("$U3D_BONE\0"); ChunkSize=CChunkSizeWriter(File); # Write the bone index if(UseV2 and self.iBone==65535): File.write(struct.pack("L",0xFFFFFFFF)); else: File.write(struct.pack("L",self.iBone)); else: File.write("$U3D_BONE\0"); # Write the null-terminated name of this bone File.write(self.Name+"\0"); # Write the index of the parent of this bone File.write(struct.pack(Choose(UseV2,"L","H"),self.iParent)); # Write the bone frame offset (-1.0) and whether this is to effect the # child frame File.write(struct.pack("f",-1.0)); File.write(struct.pack("B",0)); # Write the number of meshes that are appended to this bone File.write(struct.pack(Choose(UseV2,"L","H"),1)); # Write the index of the skinning mesh File.write(struct.pack(Choose(UseV2,"L","H"),0)); # Write the mesh transformation self.MeshTransformation.SaveData(File); # Write the scaling keys File.write(struct.pack(Choose(UseV2,"L","H"),len(self.pScalingKey))); for ScalingKey in self.pScalingKey: ScalingKey.SaveData(File,UseV2); # Write the translation keys File.write(struct.pack(Choose(UseV2,"L","H"),len(self.pTranslationKey))); for TranslationKey in self.pTranslationKey: TranslationKey.SaveData(File,UseV2); # Write the rotation keys File.write(struct.pack(Choose(UseV2,"L","H"),len(self.pRotationKey))); for RotationKey in self.pRotationKey: RotationKey.SaveData(File,UseV2); # The chunk is completed if(UseV2): ChunkSize.Write(); def MakeBlenderToU3DSpaceBone(self): self.CleanUp(); # Set up a bone index... self.iBone=0; # ... a descriptive name... self.Name="BlenderToU3DSpaceBone"; # ...no parent... self.iParent=65535; # ...an identity mesh transformation... self.MeshTransformation=CMatrix4x4(1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0); # ...and keys that describe the BlenderToU3DSpace matrix. self.pScalingKey=[CScalingKey(0,C3DVector(1.0,-1.0,1.0))]; self.pRotationKey=[CRotationKey(0,CQuaternion(1.0,0.0,0.0,-1.0))]; self.pTranslationKey=[]; self.nFrame=0; def CleanUp(self): """This function erases all data that's saved within this bone.""" self.iBone=0; self.Name=""; self.iParent=0; self.MeshTransformation=CMatrix4x4(1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0); self.pScalingKey=[]; self.pRotationKey=[]; self.pTranslationKey=[]; self.nFrame=0; # The index of this bone iBone=0; # The name of this bone Name=""; # The index of the parent bone iParent=0; # The transformation that is to be applied to the geometry before the # final bone transformation MeshTransformation=CMatrix4x4(1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0); # The scaling keys of this bone pScalingKey=[]; # The translation keys of this bone pRotationKey=[]; # The rotation keys of this bone pTranslationKey=[]; # The length of the animation of this bone nFrame=0; class CActionRange: """This class saves the range of a particular class and it can save it to a *.u3d file.""" def __init__(self,Act,iFirstFrameIn): """This init function initializes this action range using data from the given action and the given first frame.""" self.ActionName=Act.name; self.iFirstFrame=iFirstFrameIn; self.iLastFrame=iFirstFrameIn+max(Act.getFrameNumbers())+1; def SaveData(self,File): """This function writes the data of this object to the given file.""" File.write(self.ActionName+"\0"); File.write(struct.pack("LL",self.iFirstFrame,self.iLastFrame)); # The name of the action ActionName=""; # The first frame of the action iFirstFrame=0; # The last frame of the action iLastFrame=0; class CSkeleton: """This class is used to collect data about all armatures in a given scene and to write it to a *.u3d file.""" def __init__(self,RelevantObjects,ExportConstraints,ExportActionList,ActionListFileName): """This init function initializes this skeleton from the armatures in the given scene.""" # Create the Blender to U3D space root bone self.BoneList=[CBone()]; self.BoneList[0].MakeBlenderToU3DSpaceBone(); # Create a list of absolutely all bones in the scene for Obj in RelevantObjects: if(Obj.getType()=="Armature"): # Create a list of all blender bones iFirstArmatureBone=len(self.BlenderBoneList); for Bone in Obj.getData().bones.values(): self.BlenderBoneList.append(Bone); # Collect the needed information from the bones for i in range(iFirstArmatureBone,len(self.BlenderBoneList)): self.BoneList.append(CBone()); self.BoneList[i+1].Initialize(self.BlenderBoneList[i],self.BlenderBoneList,Armature.NLA.GetActions().values(),not ExportConstraints,Obj,i+1); # Output the association between frames and actions and determine the # number of frames that is needed self.nFrame=0; if(ExportActionList): ActionListFile=open(ActionListFileName,"w"); for Act in Armature.NLA.GetActions().values(): if(len(Act.getFrameNumbers())==0): continue; nActionFrame=max(Act.getFrameNumbers())+1; if(nActionFrame>1): print("Action \""+Act.name+"\" takes the range from frame "+str(self.nFrame)+" to frame "+str(self.nFrame+nActionFrame-1)+"."); if(ExportActionList): ActionListFile.write("\""+Act.name+"\" "+str(self.nFrame)+" "+str(self.nFrame+nActionFrame-1)+"\n"); self.ActionList.append(CActionRange(Act,self.nFrame)); self.nFrame+=nActionFrame; self.nFrame=max(1,self.nFrame); if(ExportActionList): ActionListFile.close(); def SaveData(self,File,UseV2): """Saves the skeleton to the given file and writes an action range chunk.""" for Bone in self.BoneList: Bone.SaveData(File,UseV2); if(UseV2): File.write("$U3D_ACTION_RANGE\0"); ChunkSize=CChunkSizeWriter(File); for Act in self.ActionList: Act.SaveData(File); ChunkSize.Write(); # A list of all Blender bone objects in the scene BlenderBoneList=[]; # A list of Ultimate 3D bone objects BoneList=[]; # The list of actions ActionList=[]; # The number of frames in the animation of this skeleton nFrame=1; def FileSelectorCallback(FileName): """This is the callback function for the file selector""" ExportPlugin.InputCompleted(FileName); class CInputWindow: """This class creates a window in which the user has to enter information about how he wants the file to be exported.""" def __init__(self): """This function creates a user interface to get the input.""" # Display a neat little pop up with the options FormContent=[]; FormContent.append(("Selection only",self.ExportSelectionOnly,"If checked only selected objects will be exported, otherwise the whole scene will be exported.")); FormContent.append(("Animation",self.ExportArmatures,"Export armatures, skin weights and acions?")); FormContent.append(("Constraints",self.ExportConstraints,"Cycle through all frames to export constraints?")); FormContent.append(("Action list",self.ExportActionList,"Export a list of all actions with their corresponding frame values?")); FormContent.append(("All UV layers",self.ExportAllUVLayers,"Export all UV layers or only the active layers. If enabled all meshes must have the same UV layers with the same names.")); FormContent.append(("Use v2.0 (U3D 3.0)",self.UseV2,"Use format version 2.0 instead of version 1.0? Version 2.0 is new in Ultimate 3D 3.0.")); PopUpReturnValue=Draw.PupBlock("Ultimate 3D export settings",FormContent); # If OK has been clicked show the file selection dialog if(PopUpReturnValue): # Suggest a filename that makes sense ExportFileSuggestion=Blender.Get("filename"); print(ExportFileSuggestion); for i in range(len(ExportFileSuggestion)-1,0,-1): if(ExportFileSuggestion[i]=="."): ExportFileSuggestion=ExportFileSuggestion[0:i]+".u3d"; break; if(len(ExportFileSuggestion)==0): ExportFileSuggestion="Model.u3d"; Window.FileSelector(FileSelectorCallback,"Export *.u3d model",ExportFileSuggestion); ExportSelectionOnly=Draw.Create(False); ExportArmatures=Draw.Create(True); ExportConstraints=Draw.Create(True); ExportActionList=Draw.Create(False); ExportAllUVLayers=Draw.Create(False); UseV2=Draw.Create(False); class CExportPlugin: """This class runs the whole export plugin.""" def __init__(self): """As the constructor gets called the plugin starts working.""" self.InputWindow=CInputWindow(); def InputCompleted(self,FileName): """When the input window has retrieved all input this function gets called.""" if(len(FileName)>3): self.ExportModel(FileName,self.InputWindow.ExportSelectionOnly.val,self.InputWindow.ExportArmatures.val,self.InputWindow.ExportConstraints.val,self.InputWindow.ExportActionList.val,self.InputWindow.ExportAllUVLayers.val,self.InputWindow.UseV2.val); def ExportModel(self,FileName,ExportSelectionOnly,ExportArmatures,ExportConstraints,ExportActionList,ExportAllUVLayers,UseV2): """This function initializes all processes that are necessary to export the model to a given file.""" print(""); print("*** Exporting to an Ultimate 3D model file.***"); # Prepare some information here: # Get the relevant scene SceneObject=Scene.GetCurrent(); # Create a list of all objects (including those, which are in groups) AllObjects=[]; for Obj in SceneObject.objects: AllObjects.append(Obj); for GroupObj in Group.Get(): for Obj in GroupObj.objects: AllObjects.append(Obj); # Create a list of the objects, which may need to be exported (if their # type is right) RelevantObjects=[]; if(ExportSelectionOnly): for Obj in AllObjects: if(Obj.isSelected()): RelevantObjects.append(Obj); if(len(RelevantObjects)==0): print("Error: No object selected for exporting."); Draw.PupMenu("Error%t|No objects selected for exporting."); return False; else: for Obj in AllObjects: RelevantObjects.append(Obj); if(len(RelevantObjects)==0): print("Error: No object contained in the scene."); Draw.PupMenu("Error%t|No object contained in the scene."); # Sort out meshes, which do not contain any geometry and count # the meshes nMesh=0; BlenderMeshList=[]; for Object in RelevantObjects: if(Object.getType()=="Mesh"): # Count the number of triangles nTriangle=0; for Face in Object.getData(False,True).faces: nTriangle+=len(Face.verts); # Remove the mesh if there are zero triangles if nTriangle==0: RelevantObjects.remove(Object); else: nMesh+=1; BlenderMeshList.append(Object); # The model is invalid, if it does not contain any meshes if(nMesh==0): print("The model you are trying to export does not contain any meshes. This is invalid. Every model has to contain at least one mesh with at least one triangle. Please add some dummy mesh and try again."); Draw.PupMenu("Error%t|No geometry found in the model. Please add at least one triangle."); return False; # If necessary check whether all meshes have identical UV layers UVLayerSet=None; if(ExportAllUVLayers): UVLayerSet=frozenset(BlenderMeshList[0].getData(False,True).getUVLayerNames()); for MeshObject in BlenderMeshList: if(frozenset(MeshObject.getData(False,True).getUVLayerNames())!=UVLayerSet): print("The mesh \""+MeshObject.getData(True)+"\" does not have the same UV layers as the mesh \""+BlenderMeshList[0].getData(True)+"\". Exporting UV layers has been disabled to export the model anyway."); Draw.PupMenu("Warning%t|UV layers of \""+MeshObject.getData(True)+"\" and \""+BlenderMeshList[0].getData(True)+"\" do not match. Exporting UV layers has been disabled."); UVLayerSet=None; ExportAllUVLayers=False; break; # If the UV layers can be exported output their order if(ExportAllUVLayers): print("All UV layers will be exported. Here is the mapping of UV layers to texture coordinate sets:"); iTexCoordSet=0; for UVLayer in UVLayerSet: print(UVLayer+" -> "+str(iTexCoordSet)); iTexCoordSet+=1; # Create a list of all materials (without double materials) MaterialList=[]; for Object in RelevantObjects: if(Object.getType()=="Mesh"): for Material in Object.getData().materials: if(Material not in MaterialList): MaterialList.append(Material); # Check whether there's an armature in the scene. If so skinning will # be used SkinningUsed=False; for Object in RelevantObjects: if(Object.getType()=="Armature"): SkinningUsed=ExportArmatures; break; # Create a skeleton object if necessary and get it's bone list BoneList=[]; if(SkinningUsed): print("Vertex skinning is being used."); ActionListFileName=""; if(ExportActionList): ActionListFileName=FileName[0:-3]+"txt"; Skeleton=CSkeleton(RelevantObjects,ExportConstraints,ExportActionList,ActionListFileName); BoneList=Skeleton.BoneList; if(len(BoneList)>=128): print("Your scene contains more than 126 bones. Since Ultimate 3D and the Ultimate 3D model file format can't handle that many bones in combination with skinning you have to reduce the number of bones. The exporter will terminate now. No file has been created."); Draw.PupMenu("Error%t|Too many bones found. There mustn't be more than 126."); return False; # Try to open the file File=open(FileName,"wb"); if File.closed: print("Failed to open the file \""+FileName+"\". Please make sure that you have write access and try again."); Draw.PupMenu("Error%t|Failed to open the file \""+FileName+"\"."); return False; print("Opened the file "+FileName+" successfully."); # Export the header if SkinningUsed: HeaderOut=CHeader(RelevantObjects,MaterialList,len(BoneList),Skeleton.nFrame,UVLayerSet); else: HeaderOut=CHeader(RelevantObjects,MaterialList,0,1,UVLayerSet); HeaderOut.SaveData(File,UseV2); # Export the meshes if(not SkinningUsed): # If no skinning is used export each mesh as one mesh chunk iMesh=0; for i in BlenderMeshList: ObjectMatrix=i.getMatrix(); MeshTransformation=CMatrix4x4(ObjectMatrix[0][0],ObjectMatrix[0][1],ObjectMatrix[0][2],ObjectMatrix[0][3],ObjectMatrix[1][0],ObjectMatrix[1][1],ObjectMatrix[1][2],ObjectMatrix[1][3],ObjectMatrix[2][0],ObjectMatrix[2][1],ObjectMatrix[2][2],ObjectMatrix[2][3],ObjectMatrix[3][0],ObjectMatrix[3][1],ObjectMatrix[3][2],ObjectMatrix[3][3]); MeshOut=CMesh(i.getData(False,True),MaterialList,BoneList,MeshTransformation,iMesh,UVLayerSet); if(not MeshOut.SaveData(File,UseV2)): File.close(); return False; iMesh+=1; print(str(HeaderOut.nMesh)+" meshes have been exported."); else: # If skinning is used combine all meshes into one mesh MeshList=[]; for i in BlenderMeshList: ObjectMatrix=i.getMatrix(); MeshTransformation=CMatrix4x4(ObjectMatrix[0][0],ObjectMatrix[0][1],ObjectMatrix[0][2],ObjectMatrix[0][3],ObjectMatrix[1][0],ObjectMatrix[1][1],ObjectMatrix[1][2],ObjectMatrix[1][3],ObjectMatrix[2][0],ObjectMatrix[2][1],ObjectMatrix[2][2],ObjectMatrix[2][3],ObjectMatrix[3][0],ObjectMatrix[3][1],ObjectMatrix[3][2],ObjectMatrix[3][3]); MeshList.append(CMesh(i.getData(False,True),MaterialList,BoneList,MeshTransformation,0,UVLayerSet)); for i in range(1,len(MeshList)): MeshList[0].AppendMesh(MeshList[i]); if(not MeshList[0].SaveData(File,UseV2)): File.close(); return; print("All meshes in the scene have been combined and the single resulting mesh has been exported."); # Export the material list iMaterial=0; for BlenderMaterial in MaterialList: TexturePath=FileName; for i in range(len(FileName)-1,0,-1): if(TexturePath[i]=="/" or TexturePath[i]=="\\"): TexturePath=TexturePath[0:i]+"/Textures/"; break; if(not os.path.exists(TexturePath)): os.mkdir(TexturePath); MaterialOut=CMaterial(BlenderMaterial,TexturePath,iMaterial,UVLayerSet); MaterialOut.SaveData(File,UseV2); iMaterial+=1; if(len(MaterialList)): print(str(HeaderOut.nMaterial)+" materials have been exported."); # If there's no material in the material list create a default material else: if(UseV2): # Write the material chunk header File.write("$U3D_MATERIAL\0"); ChunkSize=CChunkSizeWriter(File); # Write the material index and the name File.write(struct.pack("L",0)); File.write("DefaultMaterial\0"); # Write the color values (ambient, diffuse, specular, emissive) File.write(struct.pack("4f",1.0,1.0,1.0,1.0)); File.write(struct.pack("4f",1.0,1.0,1.0,1.0)); File.write(struct.pack("4f",0.0,0.0,0.0,1.0)); File.write(struct.pack("4f",0.0,0.0,0.0,1.0)); # Write the specular power File.write(struct.pack("f",0.0)); # Write the material depth and the parallax quality File.write(struct.pack("ff",0.0,0.0)); # Write the color operations and the texture coordinate sets File.write(struct.pack("LLLLLLLL",1,1,1,1,1,1,1,1)); File.write(struct.pack("LLLLLLLL",0,0,0,0,0,0,0,0)); # Write eight texture chunks without a texture for i in range(8): File.write("$U3D_TEXTURE\0"); ChunkSize2=CChunkSizeWriter(File); File.write(struct.pack("b",False)); ChunkSize2.Write(); # Write that there is no effect File.write(struct.pack("b",False)); # The chunk is completed ChunkSize.Write(); else: # Material chunk identifier (null-terminated) File.write("$U3D_MAT\0"); # Material name (null-terminated) File.write("DefaultMaterial"); File.write(struct.pack("b",0)); # Ambient color File.write(struct.pack("4f",1.0,1.0,1.0,1.0)); # Diffuse color File.write(struct.pack("4f",1.0,1.0,1.0,1.0)); # Specular color File.write(struct.pack("4f",0.0,0.0,0.0,1.0)); # Emissive color File.write(struct.pack("4f",0.0,0.0,0.0,1.0)); # Specular power File.write(struct.pack("f",0.0)); # Write the number of textures File.write(struct.pack("H",0)); # Write the texture operations File.write(struct.pack("8I",5,5,5,5,5,5,5,5)); # Write the texture coordinate indices File.write(struct.pack("8I",0,0,0,0,0,0,0,0)); # Write that resources don't get exported File.write(struct.pack("b",0)); # Write that there's no texture File.write(struct.pack("8b",0,0,0,0,0,0,0,0)); # Write that there's no effect File.write(struct.pack("b",0)); print("The blender model didn't contain any materials. A default material has been exported."); # Export the skeleton if(SkinningUsed): Skeleton.SaveData(File,UseV2); print("The skeleton has been exported successfully."); # Close the file File.close(); print("All data has been exported successfully."); Draw.PupMenu("Success%t|The model has been exported successfully."); return True; # Initialize an instance of CExportPlugin to run the plugin ExportPlugin=CExportPlugin();