/*
* 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);
}
}
}