/* * ColladaModel.cs * Authors: August Zinsser * Benjamin Nitschke (based on his tutorial project SkinningWithColladaModelsInXna at abi.exdream.com) * * Copyright August Zinsser 2007 * This program is distributed under the terms of the GNU General Public License */ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Xml; namespace Pina3D { /// /// Loads and stores all relevant data in a Collada (.dae) file. This includes boned animations for a single mesh with one material. /// public sealed class ColladaFile : IRenderableSourceData, IDisposable { public const string ColladaExtension = ".dae"; private string mName; // Name of this model private Matrix mMatrix; // A local transform for this model private Material mDefaultMaterial = null; // Default material for the main mesh. Only one material is supported. private Shader mShader = null; // An interface with the XNA Effect class private List mVertices = // Verticies for the mesh new List(); private int mNumVertices = 0; // Count of verts in the vertex buffer private int mNumIndices = 0; // Count of indicies in the index buffer private VertexBuffer mVertexBuffer = null; // Vertex buffer to send to the GPU private IndexBuffer mIndexBuffer = null; // Index '' private int mStrideSize = 0; // Remembers the stride size generated dynamically by ColladaVertex private List mBones = new List(); // Flat list of all the bones. Parents are guaranteed to appear before children. private int mNumAnimations = 1; // Number of values in the animationMatrices in each bone. private float mFrameRate = 30; // Rate of animation private int[] mReuseVertexPositions; // Helper to remember how to reuse vertices for OptimizeVertexBuffer. private List[] mReverseReuseVertexPositions; // Reverse reused vertex positions, used to get a list of used vertices for a shared vertex pos. private Dictionary mAnimationTargetValues = // Helper for loading animations matricies new Dictionary(); private int mLastAniMatrixNum = -1; // Remembers if the animation data was already constructed by last call to UpdateAnimation public string Name { get { return mName; } } /// /// Use this material when rendering unless the Renderable referring to this Model has a Material assigned /// public Material DefaultMaterial { set { mDefaultMaterial = value; mShader = new Shader(ref mDefaultMaterial); } get { return mDefaultMaterial; } } public Shader DefaultShader { get { return mShader; } } /// /// True if the model was loaded successfully /// public bool Loaded { get { return mVertexBuffer != null && mIndexBuffer != null; } } public int TotalNumberOfBones { get { return mBones.Count; } } /// /// Create a model from a collada file /// /// Location of the model asset public ColladaFile(string fileName) { // Set name to identify this model and build the filename mName = fileName; fileName += ColladaExtension; // Load file Stream file = File.OpenRead(fileName); string colladaXml = new StreamReader(file).ReadToEnd(); XmlNode colladaFile = XmlHelper.LoadXmlFromText(colladaXml); LoadMaterial(colladaFile); LoadBones(colladaFile); LoadMesh(colladaFile); LoadAnimation(colladaFile); file.Close(); } /// /// Dispose the stuff that needs to be disposed. /// public void Dispose() { if (mVertexBuffer != null) mVertexBuffer.Dispose(); mVertexBuffer = null; if (mIndexBuffer != null) mIndexBuffer.Dispose(); mIndexBuffer = null; if (mShader != null) mShader.Dispose(); } /// /// Create matrix from collada float value array. /// /// mat /// Matrix private static Matrix LoadColladaMatrix(float[] mat, int offset) { // Collada matricies are row major, wheras XNA's are column major. return new Matrix( mat[offset + 0], mat[offset + 4], mat[offset + 8], mat[offset + 12], mat[offset + 1], mat[offset + 5], mat[offset + 9], mat[offset + 13], mat[offset + 2], mat[offset + 6], mat[offset + 10], mat[offset + 14], mat[offset + 3], mat[offset + 7], mat[offset + 11], mat[offset + 15]); } /// /// Load material information from a collada file. Texture file paths are assumed to be relative to the model files. /// /// Collada file xml node private void LoadMaterial(XmlNode colladaFile) { // First get all textures XmlNode texturesNode = XmlHelper.GetChildNode(colladaFile, "library_images"); // If no texture was found, make a default material and leave if (texturesNode == null) { mDefaultMaterial = new Material(); return; } // Get the path to this model string modelName = StringHelper.ExtractFilename(mName, true); string modelPath = mName.Substring(0, mName.Length - modelName.Length); // Get all used texture images Dictionary textures = new Dictionary(); foreach (XmlNode textureNode in texturesNode) { if (textureNode.Name == "image") { string filename = XmlHelper.GetChildNode(textureNode, "init_from").InnerText; filename = StringHelper.CutExtension(filename); // Assume that the files are located relative to the model, but preceded by ".\" (the backslash is not an escape) filename = filename.Substring(2); string fullPath = modelPath + filename; textures.Add(XmlHelper.GetXmlAttribute(textureNode, "id"), Pina.Content.Load(fullPath)); } else throw new InvalidOperationException("Unknown Node " + textureNode.Name + " found in library_images"); } // Get all (1 supported) materials from the effects node XmlNode effectsNode = XmlHelper.GetChildNode(colladaFile, "library_effects"); if (effectsNode == null) throw new InvalidOperationException("library_effects node not found while loading Collada file " + mName); if (effectsNode.ChildNodes.Count < 1) throw new InvalidOperationException("library_effects node contains no child effect nodes in Collada file " + mName); // Use the first effect found and ignore the rest mDefaultMaterial = new Material(effectsNode.ChildNodes[0], textures); mShader = new Shader(ref mDefaultMaterial); } /// /// Load bones /// /// Collada file private void LoadBones(XmlNode colladaFile) { // Find the bones in the visual scene XmlNode visualSceneNode = XmlHelper.GetChildNode( XmlHelper.GetChildNode(colladaFile, "library_visual_scenes"), "visual_scene"); // Iterate through library_visual_scenes and collect all bones FillBoneNodes(null, visualSceneNode); } /// /// Fill bone nodes helper method for LoadBones. /// /// Parent bone /// Bone nodes as XmlNodes private void FillBoneNodes(ColladaBone parentBone, XmlNode boneNodes) { foreach (XmlNode boneNode in boneNodes) { if (boneNode.Name == "node" && (XmlHelper.GetXmlAttribute(boneNode, "id").Contains("Bone") || XmlHelper.GetXmlAttribute(boneNode, "type").Contains("JOINT"))) { Matrix matrix = Matrix.Identity; // Get all sub nodes for the matrix. Translate and Rotate nodes are unsupported. foreach (XmlNode subnode in boneNode) { switch (subnode.Name) { case "translate": case "rotate": throw new InvalidOperationException( "Unsupported bone data found for bone " + mBones.Count + ". Please make sure you save the collada file with baked " + "matrices!"); case "matrix": matrix = LoadColladaMatrix( StringHelper.ConvertStringToFloatArray(subnode.InnerText), 0); break; } } // Create this node, use the current number of bones as number. ColladaBone newBone = new ColladaBone(matrix, parentBone, mBones.Count, XmlHelper.GetXmlAttribute(boneNode, "sid")); // Add to the global bones list mBones.Add(newBone); // And to the parent if (parentBone != null) parentBone.children.Add(newBone); // Create all children (will do nothing if there are no sub bones) FillBoneNodes(newBone, boneNode); } } } /// /// Load mesh, must be called after we all bones are loaded. Also create /// the vertex and index buffers and optimize the vertices. /// /// Collada file private void LoadMesh(XmlNode colladaFile) { XmlNode geometries = XmlHelper.GetChildNode(colladaFile, "library_geometries"); if (geometries == null) throw new InvalidOperationException( "library_geometries node not found in collada file " + mName); foreach (XmlNode geometry in geometries) { if (geometry.Name == "geometry") { // Load everything from the mesh node LoadMeshGeometry(colladaFile, XmlHelper.GetChildNode(geometry, "mesh")); } } // Generate vertex buffer for rendering GenerateVertexAndIndexBuffers(); } /// /// Load mesh geometry /// /// private void LoadMeshGeometry(XmlNode colladaFile, XmlNode meshNode) { Dictionary> sources = new Dictionary>(); foreach (XmlNode node in meshNode) { if (node.Name != "source") continue; XmlNode floatArray = XmlHelper.GetChildNode(node, "float_array"); List floats = new List(StringHelper.ConvertStringToFloatArray(floatArray.InnerText)); // Fill up the array int count = Convert.ToInt32(XmlHelper.GetXmlAttribute(floatArray, "count"), NumberFormatInfo.InvariantInfo); while (floats.Count < count) floats.Add(0.0f); sources.Add(XmlHelper.GetXmlAttribute(node, "id"), floats); } // Also the vertices node, redirected to position node into sources XmlNode verticesNode = XmlHelper.GetChildNode(meshNode, "vertices"); XmlNode posInput = XmlHelper.GetChildNode(verticesNode, "input"); if (XmlHelper.GetXmlAttribute(posInput, "semantic").ToLower(CultureInfo.InvariantCulture) != "position") throw new InvalidOperationException("unsupported feature found in collada \"vertices\" node"); string verticesValueName = XmlHelper.GetXmlAttribute(posInput, "source").Substring(1); sources.Add(XmlHelper.GetXmlAttribute(verticesNode, "id"), sources[verticesValueName]); // Filled if a skin is found in the file List vertexSkinJoints = new List(); List vertexSkinWeights = new List(); // Only 1 skin is supported, so just load the first one found (if any) XmlNode libControllersNode = XmlHelper.GetChildNode(colladaFile, "library_controllers"); if (libControllersNode != null) { XmlNode skinNode = XmlHelper.GetChildNode(XmlHelper.GetChildNode(libControllersNode, "controller"), "skin"); mMatrix = LoadColladaMatrix(StringHelper.ConvertStringToFloatArray(XmlHelper.GetChildNode(skinNode, "bind_shape_matrix").InnerText), 0); // Get the order of the bones used in collada (can be different than ours) int[] boneArrayOrder = new int[mBones.Count]; int[] invBoneArrayOrder = new int[mBones.Count]; string boneNameArray = XmlHelper.GetChildNode(skinNode, "Name_array").InnerText; int arrayIndex = 0; foreach (string boneName in boneNameArray.Split(' ')) { boneArrayOrder[arrayIndex] = -1; foreach (ColladaBone bone in mBones) if (bone.id == boneName) { boneArrayOrder[arrayIndex] = bone.num; invBoneArrayOrder[bone.num] = arrayIndex; break; } if (boneArrayOrder[arrayIndex] == -1) throw new InvalidOperationException("Unable to find boneName=" + boneName + " in our bones array for skinning!"); arrayIndex++; } float[] weights = null; foreach (XmlNode sourceNode in skinNode) { // Get all inv bone skin matrices if (sourceNode.Name == "source" && XmlHelper.GetXmlAttribute(sourceNode, "id").Contains("bind_poses")) { // Get inner float array float[] mat = StringHelper.ConvertStringToFloatArray(XmlHelper.GetChildNode(sourceNode, "float_array").InnerText); for (int boneNum = 0; boneNum < mBones.Count; boneNum++) if (mat.Length / 16 > boneNum) { mBones[boneArrayOrder[boneNum]].invBoneSkinMatrix = LoadColladaMatrix(mat, boneNum * 16); } } // Get all weights if (sourceNode.Name == "source" && XmlHelper.GetXmlAttribute(sourceNode, "id").Contains("skin-weights")) { // Get inner float array weights = StringHelper.ConvertStringToFloatArray(XmlHelper.GetChildNode(sourceNode, "float_array").InnerText); } } if (weights == null) throw new InvalidOperationException("No weights were found in skin, unable to continue!"); // Helper to access the bones (first index) and weights (second index). // If there are more than 2 indices for an entry here, then there are multiple // weights used. For simplicity, just use the strongest 3. XmlNode vertexWeightsNode = XmlHelper.GetChildNode(skinNode, "vertex_weights"); int[] vcountArray = StringHelper.ConvertStringToIntArray(XmlHelper.GetChildNode(skinNode, "vcount").InnerText); int[] vArray = StringHelper.ConvertStringToIntArray(XmlHelper.GetChildNode(skinNode, "v").InnerText); // Build vertexSkinJoints and vertexSkinWeights for easier access. int vArrayIndex = 0; for (int num = 0; num < vcountArray.Length; num++) { int vcount = vcountArray[num]; List jointIndices = new List(); List weightIndices = new List(); for (int i = 0; i < vcount; i++) { // Make sure to convert the internal number to the bone numbers! jointIndices.Add(boneArrayOrder[vArray[vArrayIndex]]); weightIndices.Add(vArray[vArrayIndex + 1]); vArrayIndex += 2; } // If there are less than 3 values, add up to 3. This simplifies calculation below. while (jointIndices.Count < 3) jointIndices.Add(0); while (weightIndices.Count < 3) weightIndices.Add(-1); // Find the top 3 weights float[] weightValues = new float[weightIndices.Count]; int[] bestWeights = { 0, 1, 2 }; for (int i = 0; i < weightIndices.Count; i++) { // Use weight of zero for invalid indices. if (weightIndices[i] < 0 || weightValues[i] >= weights.Length) weightValues[i] = 0; else weightValues[i] = weights[weightIndices[i]]; if (i >= 3) { float lowestWeight = 1.0f; int lowestWeightOverride = 2; for (int b = 0; b < bestWeights.Length; b++) if (lowestWeight > weightValues[bestWeights[b]]) { lowestWeight = weightValues[bestWeights[b]]; lowestWeightOverride = b; } // Replace lowest weight bestWeights[lowestWeightOverride] = i; } } // Build 2 vectors from the best weights Vector3 boneIndicesVec = new Vector3(jointIndices[bestWeights[0]], jointIndices[bestWeights[1]], jointIndices[bestWeights[2]]); Vector3 weightsVec = new Vector3(weightValues[bestWeights[0]], weightValues[bestWeights[1]], weightValues[bestWeights[2]]); // Renormalize weight float totalWeights = weightsVec.X + weightsVec.Y + weightsVec.Z; if (totalWeights == 0) weightsVec.X = 1.0f; else { weightsVec.X /= totalWeights; weightsVec.Y /= totalWeights; weightsVec.Z /= totalWeights; } vertexSkinJoints.Add(boneIndicesVec); vertexSkinWeights.Add(weightsVec); } } // Construct and generate vertex lists. Every 3 vertices will // span one triangle polygon, but everything is optimized later. if (meshNode.ChildNodes.Count == 0) throw new InvalidOperationException("No mesh found in this collada file, unable to continue!"); foreach (XmlNode trianglenode in meshNode) { if (trianglenode.Name != "triangles") continue; // Declare source data List positions = null; List normals = null; List texcoords = null; List tangents = null; // Find data source nodes XmlNode positionsnode = XmlHelper.GetChildNode(trianglenode, "semantic", "VERTEX"); XmlNode normalsnode = XmlHelper.GetChildNode(trianglenode, "semantic", "NORMAL"); XmlNode texcoordsnode = XmlHelper.GetChildNode(trianglenode, "semantic", "TEXCOORD"); XmlNode tangentsnode = XmlHelper.GetChildNode(trianglenode, "semantic", "TEXTANGENT"); // Get the data of the sources (where applicable) positions = sources[XmlHelper.GetXmlAttribute(positionsnode, "source").Substring(1)]; if (normalsnode != null) normals = sources[XmlHelper.GetXmlAttribute(normalsnode, "source").Substring(1)]; if (texcoordsnode != null) texcoords = sources[XmlHelper.GetXmlAttribute( texcoordsnode, "source").Substring(1)]; if (tangentsnode != null) tangents = sources[XmlHelper.GetXmlAttribute(tangentsnode, "source").Substring(1)]; // Declare offset data int positionsoffset = 0; int normalsoffset = 0; int texcoordsoffset = 0; int tangentsoffset = 0; // Find the Offsets positionsoffset = Convert.ToInt32(XmlHelper.GetXmlAttribute(positionsnode, "offset"), NumberFormatInfo.InvariantInfo); if (normalsnode != null) normalsoffset = Convert.ToInt32(XmlHelper.GetXmlAttribute(normalsnode, "offset"), NumberFormatInfo.InvariantInfo); if (texcoordsnode != null) texcoordsoffset = Convert.ToInt32(XmlHelper.GetXmlAttribute(texcoordsnode, "offset"), NumberFormatInfo.InvariantInfo); if (tangentsnode != null) tangentsoffset = Convert.ToInt32(XmlHelper.GetXmlAttribute(tangentsnode, "offset"), NumberFormatInfo.InvariantInfo); // Get the indexlist XmlNode p = XmlHelper.GetChildNode(trianglenode, "p"); int[] pints = StringHelper.ConvertStringToIntArray(p.InnerText); int trianglecount = Convert.ToInt32(XmlHelper.GetXmlAttribute( trianglenode, "count"), NumberFormatInfo.InvariantInfo); // The number of ints that form one vertex: int vertexcomponentcount = pints.Length / trianglecount / 3; // Begin Constructing the data to store // Initialize reuseVertexPositions and reverseReuseVertexPositions // to make it easier to use them below int iRVP = 0; int iRRVP = 0; if (mReuseVertexPositions == null) { mReuseVertexPositions = new int[trianglecount * 3]; mReverseReuseVertexPositions = new List[positions.Count / 3]; } else { int[] tempRVP = new int[mReuseVertexPositions.Length + trianglecount * 3]; iRVP = mReuseVertexPositions.Length; for (int i = 0; i < iRVP; i++) tempRVP[i] = mReuseVertexPositions[i]; mReuseVertexPositions = tempRVP; List[] tempRRVP = new List[mReverseReuseVertexPositions.Length + positions.Count / 3]; iRRVP = mReverseReuseVertexPositions.Length; for (int i = 0; i < iRRVP; i++) tempRRVP[i] = mReverseReuseVertexPositions[i]; mReverseReuseVertexPositions = tempRRVP; } for (int i = iRRVP; i < mReverseReuseVertexPositions.Length; i++) mReverseReuseVertexPositions[i] = new List(); // Must use int indices here because models may have // more than 64k triangles, although models that big should be avoided! for (int i = 0; i < trianglecount * 3; i++) { Vector3? position = null; Vector3? normal = null; Vector2? uv = null; Vector3? tangent = null; bool foundPosition = false; bool foundNormal = false; bool foundUV = false; bool foundTangent = false; bool foundWeights = false; // Position int pos = pints[i * vertexcomponentcount + positionsoffset] * 3; position = new Vector3( positions[pos], positions[pos + 1], positions[pos + 2]); foundPosition = true; // Get vertex blending stuff (uses pos too) Vector3? blendWeights = null; Vector3? blendIndices = null; if (vertexSkinJoints.Count > 0) { blendWeights = vertexSkinWeights[pos / 3]; blendIndices = vertexSkinJoints[pos / 3]; // Pre-multiply all indices with 3, which optimizes some shaders blendIndices = new Vector3(blendIndices.Value.X * 3, blendIndices.Value.Y * 3, blendIndices.Value.Z * 3); foundWeights = true; } // Normal if (normals != null) { int nor = pints[i * vertexcomponentcount + normalsoffset] * 3; normal = new Vector3(normals[nor], normals[nor + 1], normals[nor + 2]); foundNormal = true; } // Texture Coordinates if (texcoords != null) { int tex = pints[i * vertexcomponentcount + texcoordsoffset] * 2; float u = texcoords[tex]; float v = 1 - texcoords[tex + 1]; // Maya's UVs are flipped uv = new Vector2(u, v); foundUV = true; } // Tangent if (tangents != null) { int tan = pints[i * vertexcomponentcount + tangentsoffset] * 3; tangent = new Vector3( tangents[tan], tangents[tan + 1], tangents[tan + 2]); foundTangent = true; } // Check the vertex type //if (foundPosition && foundNormal && foundUV && foundTangent && !foundWeights) //{ } //else if (foundPosition && foundNormal && foundUV && foundTangent && foundWeights) //{ } //else if (!foundPosition || !foundNormal || !foundUV || !foundTangent ) { // Unsupported, so invalidate the model and leave string vertexType = String.Empty; if (foundPosition) vertexType += "Position "; if (foundNormal) vertexType += "Normal "; if (foundUV) vertexType += "UV "; if (foundTangent) vertexType += "Tangent "; if (foundWeights) vertexType += "Skin Weights "; System.Diagnostics.Debug.Assert(false, "Unsupported Vertex type: " + vertexType + "."); mVertexBuffer = null; mIndexBuffer = null; return; } // Set the vertex mVertices.Add(new ColladaVertex(position, normal, uv, tangent, blendWeights, blendIndices)); // Remember pos for optimizing the vertices later mReuseVertexPositions[iRVP + i] = iRRVP + pos / 3; mReverseReuseVertexPositions[iRRVP + pos / 3].Add(iRVP + i); } // Save the stride size to optimize rendering mStrideSize = mVertices[0].SizeInBytes; // Only support one mesh node (per call to this function) return; } } /// /// Load Animation data from a collada file, ignoring timesteps, /// interpolation and multiple animations /// /// /// private void LoadAnimationTargets(XmlNode colladaFile) { // Get global frame rate try { mFrameRate = Convert.ToSingle(XmlHelper.GetChildNode(colladaFile, "frame_rate").InnerText); } catch { } // ignore if that fails XmlNode libraryanimation = XmlHelper.GetChildNode(colladaFile, "library_animations"); if (libraryanimation == null) return; LoadAnimationHelper(libraryanimation); } /// /// Load animation helper, goes over all animations in the animationnode, /// calls itself recursively for sub-animation-nodes /// /// Animationnode private void LoadAnimationHelper(XmlNode animationnode) { // go over all animation elements foreach (XmlNode node in animationnode) { if (node.Name == "animation") //not a channel but another animation node { LoadAnimationHelper(node); continue; } if (node.Name != "channel") continue; string samplername = XmlHelper.GetXmlAttribute(node, "source").Substring(1); string targetname = XmlHelper.GetXmlAttribute(node, "target"); // Find the sampler for the animation values. XmlNode sampler = XmlHelper.GetChildNode(animationnode, "id", samplername); // Find value xml node string valuename = XmlHelper.GetXmlAttribute(XmlHelper.GetChildNode(sampler, "semantic", "OUTPUT"), "source").Substring(1) + "-array"; XmlNode valuenode = XmlHelper.GetChildNode(animationnode, "id", valuename); // Parse values and add to dictionary float[] values = StringHelper.ConvertStringToFloatArray(valuenode.InnerText); mAnimationTargetValues.Add(targetname, values); // Set number of animations to use in all bones. // Leave last animation value out later, but make filling array // a little easier (last matrix is just unused then). mNumAnimations = values.Length; // If these are matrix values, devide by 16! if (XmlHelper.GetXmlAttribute(valuenode, "id").Contains("transform")) mNumAnimations /= 16; } } /// /// Fill bone animations, called from LoadAnimation after getting all animationTargetValues. /// /// Collada file private void FillBoneAnimations(XmlNode colladaFile) { foreach (ColladaBone bone in mBones) { // Loads animation data from bone node sid, links them // automatically, also generates animation matrices. // Note: Only support the transform node here. ignore // "RotX", "RotY", etc. // Build sid the way it is used in the collada file. string sid = bone.id + "/" + "transform"; int framecount = 0; if (mAnimationTargetValues.ContainsKey(sid)) // Transformation contains whole matrix (always 4x4). framecount = mAnimationTargetValues[sid].Length / 16; // Expand array and duplicate the initial matrix in case // there is no animation data present (often the case). for (int i = 0; i < mNumAnimations; i++) bone.animationMatrices.Add(bone.initialMatrix); if (framecount > 0) { float[] mat = mAnimationTargetValues[sid]; // Load all absolute animation matrices. If you want relative // data here you can use the invBoneMatrix (invert initialMatrix), // but this won't be required here because all animations are // already computed. Maybe you need it when doing your own animations. for (int num = 0; num < bone.animationMatrices.Count && num < framecount; num++) { bone.animationMatrices[num] = LoadColladaMatrix(mat, num * 16); } } } } /// /// Calculate absolute bone matrices for finalMatrix. Useful if using an animated model as a static one. /// private void CalculateAbsoluteBoneMatrices() { foreach (ColladaBone bone in mBones) { // Get absolute matrices and also use them for the initial finalMatrix // of each bone, which is used for rendering. bone.finalMatrix = bone.GetMatrixRecursively(); } } /// /// Load animation /// /// Collada file private void LoadAnimation(XmlNode colladaFile) { // Load and store all animation values before assigning them to the bones where they are used LoadAnimationTargets(colladaFile); // Fill animation matrices in each bone (if used or not, always fill) FillBoneAnimations(colladaFile); // Calculate all absolute matrices (There should only be relative mats so far) CalculateAbsoluteBoneMatrices(); } /// /// Flip indices from 0, 1, 2 to 0, 2, 1. /// Allows rendering with CullClockwiseFace (default for XNA). /// /// /// private int FlipIndexOrder(int oldIndex) { int polygonIndex = oldIndex % 3; if (polygonIndex == 0) return oldIndex; else if (polygonIndex == 1) return (ushort)(oldIndex + 1); else return (ushort)(oldIndex - 1); } /// /// Optimize vertex buffer. Also going create the indices /// for the index buffer here /// /// ushort array for the optimized indices private ushort[] OptimizeVertexBuffer() { List newVertices = new List(); List newIndices = new List(); // Helper to only search already added newVertices and for checking the // old position indices by transforming them into newVertices indices. List newVerticesPositions = new List(); // Go over all vertices (indices are currently 1:1 with the vertices) for (int num = 0; num < mVertices.Count; num++) { // Get current vertex ColladaVertex currentVertex = mVertices[num]; bool reusedExistingVertex = false; // Find out which position index was used, then we can compare // all other vertices that share this position. They will not // all be equal, but some of them can be merged. int sharedPos = mReuseVertexPositions[num]; foreach (int otherVertexIndex in mReverseReuseVertexPositions[sharedPos]) { // Only check the indices that have already been added! if (otherVertexIndex != num && // Only check against previously-visited verts otherVertexIndex < newIndices.Count && // Make sure this index has been added to newVertices newIndices[otherVertexIndex] < newVertices.Count && // Compare vertices (this call is slow, but it is not called that much) ColladaVertex.NearlyEquals(currentVertex, newVertices[newIndices[otherVertexIndex]])) { // Reuse the existing vertex newIndices.Add((ushort)newIndices[otherVertexIndex]); reusedExistingVertex = true; break; } } if (reusedExistingVertex == false) { // Add the currentVertex and set it as the current index newIndices.Add((ushort)newVertices.Count); newVertices.Add(currentVertex); } } // Flip order of all triangles to allow rendering // with CullCounterClockwiseFace (default for XNA) for (int num = 0; num < newIndices.Count / 3; num++) { ushort swap = newIndices[num * 3 + 1]; newIndices[num * 3 + 1] = newIndices[num * 3 + 2]; newIndices[num * 3 + 2] = swap; } // Update the vertex list mVertices = newVertices; // Return the index list return newIndices.ToArray(); } /// /// Generate vertex and index buffers /// private void GenerateVertexAndIndexBuffers() { // TODO: Optimize this crap! // Optimize vertices first and build index buffer from that //ushort[] indices = OptimizeVertexBuffer(); // TEMP BEGIN ushort[] indices = new ushort[mVertices.Count]; for (ushort i = 0; i < mVertices.Count; i++) indices[i] = i; for (int num = 0; num < indices.Length / 3; num++) { ushort swap = indices[num * 3 + 1]; indices[num * 3 + 1] = indices[num * 3 + 2]; indices[num * 3 + 2] = swap; } // TEMP END // Create the vertex buffers mVertexBuffer = ColladaVertex.GenerateVertexBuffer(mVertices); mVertexBuffer.SetData(ColladaVertex.GetRawArray(mVertices)); mNumVertices = mVertices.Count; // Only support up to 65535 optimized vertices if (mVertices.Count > ushort.MaxValue) throw new InvalidOperationException( "Too many vertices to index (Max = 65535), optimize vertices or use " + "fewer vertices. Vertices=" + mVertices.Count + ", Max Vertices for Index Buffer=" + ushort.MaxValue); // Create the index buffer from the indices (Note: While the indices // will point only to 16bit (ushort) vertices, it is possible to have // more indices in this list than 65535). mIndexBuffer = new IndexBuffer( Pina.Graphics.GraphicsDevice, typeof(ushort), indices.Length, ResourceUsage.WriteOnly, ResourceManagementMode.Automatic); mIndexBuffer.SetData(indices); mNumIndices = indices.Length; } /// /// Update animation. Do nothing if animation stayed the same since /// last call to this method. /// private void UpdateAnimation() { // Add some time to the animation depending on the position. int aniMatrixNum = ((int)(100 + Pina.TotalSeconds * mFrameRate)) % mNumAnimations; if (aniMatrixNum < 0) aniMatrixNum = 0; // No need to update if everything stayed the same if (aniMatrixNum == mLastAniMatrixNum) return; mLastAniMatrixNum = aniMatrixNum; foreach (ColladaBone bone in mBones) { // Assign the final matrix from the animation matrices. bone.finalMatrix = bone.animationMatrices[aniMatrixNum]; // Use parent matrix if present if (bone.parent != null) bone.finalMatrix *= bone.parent.finalMatrix; } } /// /// Get bone matrices for the skin shader. Apply the invBoneSkinMatrix /// to each final matrix, which is the recursively created matrix from /// all the animation data (see UpdateAnimation). /// /// private Matrix[] GetBoneMatrices() { // Update the animation data in case it is not up to date UpdateAnimation(); // Get all bone matrices, up to the supported max of 80 Matrix[] matrices = new Matrix[Math.Min(80, mBones.Count)]; for (int num = 0; num < matrices.Length; num++) // The matrices are constructed from the invBoneSkinMatrix and // the finalMatrix, which holds the recursively-added animation matrices matrices[num] = mBones[num].invBoneSkinMatrix * mBones[num].finalMatrix; return matrices; } /// /// Render the given model with the given shader at the given state /// public void Render(IRenderableSourceData sourceData, Shader shader, PinaState state) { // TODO: support rendering identical sourceData/shaders with a list of different states to optimize rendering // Set the world matrix for this instance Pina.WorldMatrix = state.Matrix; // Get the necessary data List verts = ((ColladaFile)sourceData).mVertices; List shaderPasses = shader.EffectPasses; // Make sure to use the correct vertex declaration for the shader. // Since the vertex declaration is stored in unmanaged space, the managed reference to it may get garbage collected. // To prevent this from happening, keep a reference to the VD in managed memory until the model is done rendering. VertexDeclaration vd = ColladaVertex.GenerateVertexDeclaration(verts); Pina.Graphics.GraphicsDevice.VertexDeclaration = vd; // Set all bone matrices if necessary, with a max of 80 (due to PS2 limitations) //Shader.SkinnedNormalMapping.SetBoneMatrices(GetBoneMatrices()); shader.Begin(); for (int i = 0; i < shaderPasses.Count; i++) { shaderPasses[i].Begin(); RenderVertices( ((ColladaFile)sourceData).mVertexBuffer, ((ColladaFile)sourceData).mIndexBuffer, ((ColladaFile)sourceData).mStrideSize, ((ColladaFile)sourceData).mNumVertices, ((ColladaFile)sourceData).mNumIndices); shaderPasses[i].End(); } shader.End(); } /// /// Render vertices /// private static void RenderVertices(VertexBuffer vertexBuffer, IndexBuffer indexBuffer, int strideSize, int numVertices, int numIndices) { Pina.Graphics.GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0, strideSize); Pina.Graphics.GraphicsDevice.Indices = indexBuffer; Pina.Graphics.GraphicsDevice.DrawIndexedPrimitives( PrimitiveType.TriangleList, 0, 0, numVertices, 0, numIndices / 3); } } }