# Improved COLLADA skeletal animation support.

Moved skeleton definitions into XML format, to support multiple
different structures. (Not quite finished yet.)
Added support for rescaled models in XSI.
Minor refactoring.

This was SVN commit r4959.
This commit is contained in:
Ykkrosh 2007-03-16 18:00:58 +00:00
parent 29f44d66ed
commit a9feadc3ea
13 changed files with 573 additions and 295 deletions

View File

@ -2,6 +2,8 @@
#include "CommonConvert.h"
#include "StdSkeletons.h"
#include "FCollada.h"
#include "FCDocument/FCDSceneNode.h"
#include "FCDocument/FCDSkinController.h"
@ -135,6 +137,35 @@ void FColladaDocument::ReadExtras(xmlNode* colladaNode)
//////////////////////////////////////////////////////////////////////////
CommonConvert::CommonConvert(const char* text, std::string& xmlErrors)
: m_Err(xmlErrors)
{
m_Doc.LoadFromText(text);
FCDSceneNode* root = m_Doc.GetDocument()->GetVisualSceneRoot();
REQUIRE(root != NULL, "has root object");
// Find the instance to convert
if (! FindSingleInstance(root, m_Instance, m_EntityTransform))
throw ColladaException("Couldn't find object to convert");
assert(m_Instance);
Log(LOG_INFO, "Converting '%s'", m_Instance->GetEntity()->GetName().c_str());
m_IsXSI = false;
FCDAsset* asset = m_Doc.GetDocument()->GetAsset();
if (asset && asset->GetContributorCount() >= 1)
{
std::string tool = asset->GetContributor(0)->GetAuthoringTool();
if (tool.find("XSI") != tool.npos)
m_IsXSI = true;
}
FMVector3 upAxis = m_Doc.GetDocument()->GetAsset()->GetUpAxis();
m_YUp = (upAxis.y != 0); // assume either Y_UP or Z_UP (TODO: does anyone ever do X_UP?)
}
//////////////////////////////////////////////////////////////////////////
// HACK: These don't get exported properly from FCollada (3.02, DLL), so define
// them here instead of fixing it correctly.
const FMVector3 FMVector3::XAxis(1.0f, 0.0f, 0.0f);
@ -305,6 +336,9 @@ void SkinReduceInfluences(FCDSkinController* skin, size_t maxInfluenceCount, flo
newWeights.resize(maxInfluenceCount);
// Enforce the minimum weight per influence
// (This is done here rather than in the earlier loop, because several
// small weights for the same bone might add up to a value above the
// threshold)
while (!newWeights.empty() && newWeights.back().weight < minimumWeight)
newWeights.pop_back();
@ -323,17 +357,88 @@ void SkinReduceInfluences(FCDSkinController* skin, size_t maxInfluenceCount, flo
}
void FixSkeletonRoots(FCDControllerInstance* controllerInstance)
void FixSkeletonRoots(FCDControllerInstance& controllerInstance)
{
// HACK: The XSI exporter doesn't do a <skeleton> and FCollada doesn't
// seem to know where else to look, so just guess that it's somewhere
// under Scene_Root
if (controllerInstance->GetSkeletonRoots().empty())
if (controllerInstance.GetSkeletonRoots().empty())
{
// HACK (evil): SetSkeletonRoot is declared but not defined, and there's
// no other proper way to modify the skeleton-roots list, so cheat horribly
FUUriList& uriList = const_cast<FUUriList&>(controllerInstance->GetSkeletonRoots());
FUUriList& uriList = const_cast<FUUriList&>(controllerInstance.GetSkeletonRoots());
uriList.push_back(FUUri("Scene_Root"));
controllerInstance->LinkImport();
controllerInstance.LinkImport();
}
}
const Skeleton& FindSkeleton(const FCDControllerInstance& controllerInstance)
{
// I can't see any proper way to determine the real root of the skeleton,
// so just choose an arbitrary bone and search upwards until we find a
// recognised ancestor (or until we fall off the top of the tree)
const Skeleton* skeleton = NULL;
const FCDSceneNode* joint = controllerInstance.GetJoint(0);
while (joint && (skeleton = Skeleton::FindSkeleton(joint->GetName())) == NULL)
{
joint = joint->GetParent();
}
REQUIRE(skeleton != NULL, "recognised skeleton structure");
return *skeleton;
}
void TransformBones(std::vector<BoneTransform>& bones, const FMMatrix44& scaleTransform, bool yUp)
{
for (size_t i = 0; i < bones.size(); ++i)
{
// Apply the desired transformation to the bone coordinates
FMVector3 trans(bones[i].translation, 0);
trans = scaleTransform.TransformCoordinate(trans);
bones[i].translation[0] = trans.x;
bones[i].translation[1] = trans.y;
bones[i].translation[2] = trans.z;
// DON'T apply the transformation to orientation, because I can't get
// that kind of thing to work in practice (interacting nicely between
// the models and animations), so this function assumes the transform
// just does scaling, so there's no need to rotate anything. (But I think
// this code would work for rotation, though not very efficiently.)
/*
FMMatrix44 m = FMQuaternion(bones[i].orientation[0], bones[i].orientation[1], bones[i].orientation[2], bones[i].orientation[3]).ToMatrix();
m *= scaleTransform;
HMatrix matrix;
memcpy(matrix, m.Transposed().m, sizeof(matrix));
AffineParts parts;
decomp_affine(matrix, &parts);
bones[i].orientation[0] = parts.q.x;
bones[i].orientation[1] = parts.q.y;
bones[i].orientation[2] = parts.q.z;
bones[i].orientation[3] = parts.q.w;
*/
if (yUp)
{
// TODO: this is all just guesses which seem to work for data
// exported from XSI, rather than having been properly thought
// through
bones[i].translation[2] = -bones[i].translation[2];
bones[i].orientation[2] = -bones[i].orientation[2];
bones[i].orientation[3] = -bones[i].orientation[3];
}
else
{
// Convert bone translations from xyz into xzy axes:
std::swap(bones[i].translation[1], bones[i].translation[2]);
// To convert the quaternions: imagine you're using the axis/angle
// representation, then swap the y,z basis vectors and change the
// direction of rotation by negating the angle ( => negating sin(angle)
// => negating x,y,z => changing (x,y,z,w) to (-x,-z,-y,w)
// but then (-x,-z,-y,w) == (x,z,y,-w) so do that instead)
std::swap(bones[i].orientation[1], bones[i].orientation[2]);
bones[i].orientation[3] = -bones[i].orientation[3];
}
}
}

View File

@ -4,11 +4,13 @@
#include <exception>
#include <string>
class FUStatus;
class FCDSceneNode;
class FCDEntityInstance;
class FMMatrix44;
class FCDSceneNode;
class FCDSkinController;
class FMMatrix44;
class FUStatus;
class Skeleton;
class ColladaException : public std::exception
{
@ -49,7 +51,8 @@ private:
class FColladaDocument
{
public:
/** Loads the document from the given XML string. Should be the first function
/**
* Loads the document from the given XML string. Should be the first function
* called on this object, and should only be called once.
* @throws ColladaException if unable to load.
*/
@ -67,8 +70,32 @@ private:
std::auto_ptr<FCDExtra> extra;
};
/**
* Wrapper for code shared between the PMD and PSA converters. Loads the document
* and provides access to the relevant objects and values.
*/
class CommonConvert
{
public:
CommonConvert(const char* text, std::string& xmlErrors);
const FColladaDocument& GetDocument() const { return m_Doc; }
FCDSceneNode& GetRoot() { return *m_Doc.GetDocument()->GetVisualSceneRoot(); }
FCDEntityInstance& GetInstance() { return *m_Instance; }
const FMMatrix44& GetEntityTransform() const { return m_EntityTransform; }
bool IsYUp() const { return m_YUp; }
bool IsXSI() const { return m_IsXSI; }
private:
FColladaErrorHandler m_Err;
FColladaDocument m_Doc;
FCDEntityInstance* m_Instance;
FMMatrix44 m_EntityTransform;
bool m_YUp;
bool m_IsXSI;
};
/** Throws a ColladaException unless the value is true. */
#define REQUIRE(value, message) require_(__LINE__, value, "Assertion not satisfied", message)
#define REQUIRE(value, message) require_(__LINE__, value, "Assertion not satisfied", "failed requirement \"" message "\"")
/** Throws a ColladaException unless the status is successful. */
#define REQUIRE_SUCCESS(status) require_(__LINE__, status, "FCollada error", "Line " STRINGIFY(__LINE__))
@ -82,10 +109,6 @@ template<typename T> void write(OutputCB& output, const T& data)
output((char*)&data, sizeof(T));
}
/** Error handler for libxml2 */
void errorHandler(void* ctx, const char* msg, ...);
/**
* Tries to find a single suitable entity instance in the scene. Fails if there
* are none, or if there are too many and it's not clear which one should
@ -111,6 +134,25 @@ void SkinReduceInfluences(FCDSkinController* skin, size_t maxInfluenceCount, flo
* controller. (In particular, it's needed for models exported from XSI.)
* Should be called before extracting any joint information from the controller.
*/
void FixSkeletonRoots(FCDControllerInstance* controllerInstance);
void FixSkeletonRoots(FCDControllerInstance& controllerInstance);
/**
* Finds the skeleton definition which best matches the given controller.
* @throws ColladaException if none is found.
*/
const Skeleton& FindSkeleton(const FCDControllerInstance& controllerInstance);
/** Bone pose data */
struct BoneTransform
{
float translation[3];
float orientation[4];
};
/**
* Performs the standard transformations on bones, applying a scale matrix and
* moving them into the game's coordinate space.
*/
void TransformBones(std::vector<BoneTransform>& bones, const FMMatrix44& scaleTransform, bool yUp);
#endif // COMMONCONVERT_H__

View File

@ -3,6 +3,7 @@
#include "CommonConvert.h"
#include "PMDConvert.h"
#include "PSAConvert.h"
#include "StdSkeletons.h"
#include <cstdarg>
#include <cassert>
@ -117,3 +118,18 @@ EXPORT int convert_dae_to_psa(const char* dae, OutputFn psa_writer, void* cb_dat
{
return convert_dae_to_whatever(dae, psa_writer, cb_data, ColladaToPSA);
}
EXPORT int set_skeletons(const char* xml)
{
try
{
Skeleton::LoadSkeletonDataFromXml(xml);
}
catch (const ColladaException& e)
{
Log(LOG_ERROR, "%s", e.what());
return -2;
}
return 0;
}

View File

@ -25,6 +25,7 @@ typedef void (*OutputFn) (void* cb_data, const char* data, unsigned int length);
#define COLLADA_CONVERTER_VERSION 1
EXPORT void set_logger(LogFn logger);
EXPORT int set_skeletons(const char* xml);
EXPORT int convert_dae_to_pmd(const char* dae, OutputFn pmd_writer, void* cb_data);
EXPORT int convert_dae_to_psa(const char* dae, OutputFn psa_writer, void* cb_data);

View File

@ -131,19 +131,19 @@ void ReindexGeometry(FCDGeometryPolygons* polys, FCDSkinController* skin)
FCDGeometrySource* sourceNormal = inputNormal ->GetSource();
FCDGeometrySource* sourceTexcoord = inputTexcoord->GetSource();
const FloatList& dataPosition = sourcePosition->GetSourceData();
const FloatList& dataNormal = sourceNormal ->GetSourceData();
const FloatList& dataTexcoord = sourceTexcoord->GetSourceData();
const FloatList& dataPosition = sourcePosition->GetData();
const FloatList& dataNormal = sourceNormal ->GetData();
const FloatList& dataTexcoord = sourceTexcoord->GetData();
if (skin)
{
size_t numVertexPositions = dataPosition.size() / sourcePosition->GetSourceStride();
size_t numVertexPositions = dataPosition.size() / sourcePosition->GetStride();
assert(skin->GetVertexInfluenceCount() == numVertexPositions);
}
uint32 stridePosition = sourcePosition->GetSourceStride();
uint32 strideNormal = sourceNormal ->GetSourceStride();
uint32 strideTexcoord = sourceTexcoord->GetSourceStride();
uint32 stridePosition = sourcePosition->GetStride();
uint32 strideNormal = sourceNormal ->GetStride();
uint32 strideTexcoord = sourceTexcoord->GetStride();
UInt32List indicesCombined;
std::vector<VertexData> vertexes;

View File

@ -4,6 +4,25 @@
#include "FCollada.h"
void DumpMatrix(const FMMatrix44& m)
{
Log(LOG_INFO, "\n[%f %f %f %f]\n[%f %f %f %f]\n[%f %f %f %f]\n[%f %f %f %f]",
m.m[0][0], m.m[0][1], m.m[0][2], m.m[0][3],
m.m[1][0], m.m[1][1], m.m[1][2], m.m[1][3],
m.m[2][0], m.m[2][1], m.m[2][2], m.m[2][3],
m.m[3][0], m.m[3][1], m.m[3][2], m.m[3][3]
);
}
FMMatrix44 DecomposeToScaleMatrix(const FMMatrix44& m)
{
FMVector3 scale, rotation, translation;
float inverted;
m.Decompose(scale, rotation, translation, inverted);
return FMMatrix44::ScaleMatrix(scale);
}
/*
FMMatrix44 operator+ (const FMMatrix44& a, const FMMatrix44& b)
{
@ -49,4 +68,4 @@ FMMatrix44 QuatToMatrix(float x, float y, float z, float w)
return r;
}
*/
*/

View File

@ -3,6 +3,9 @@
class FMMatrix44;
extern void DumpMatrix(const FMMatrix44& m);
extern FMMatrix44 DecomposeToScaleMatrix(const FMMatrix44& m);
// (None of these are used any more)
// extern FMMatrix44 operator+ (const FMMatrix44& a, const FMMatrix44& b);
// extern FMMatrix44 operator/ (const FMMatrix44& a, const float b);

View File

@ -33,12 +33,6 @@ struct VertexBlend
};
VertexBlend defaultInfluences = { { 0xFF, 0xFF, 0xFF, 0xFF }, { 0, 0, 0, 0 } };
struct BoneTransform
{
float translation[3];
float orientation[4];
};
struct PropPoint
{
std::string name;
@ -61,32 +55,13 @@ public:
*/
static void ColladaToPMD(const char* input, OutputCB& output, std::string& xmlErrors)
{
FColladaErrorHandler err (xmlErrors);
CommonConvert converter(input, xmlErrors);
FColladaDocument doc;
doc.LoadFromText(input);
FCDSceneNode* root = doc.GetDocument()->GetVisualSceneRoot();
REQUIRE(root != NULL, "has root object");
// Find the instance to convert
FCDEntityInstance* instance;
FMMatrix44 transform;
if (! FindSingleInstance(root, instance, transform))
throw ColladaException("Couldn't find object to convert");
assert(instance);
Log(LOG_INFO, "Converting '%s'", instance->GetEntity()->GetName().c_str());
// StandardizeUpAxisAndLength completely mangles the skeletons, so don't use it
FMVector3 upAxis = doc.GetDocument()->GetAsset()->GetUpAxis();
bool yUp = (upAxis.y != 0); // assume either Y_UP or Z_UP (TODO: does anyone ever do X_UP?)
if (instance->GetEntity()->GetType() == FCDEntity::GEOMETRY)
if (converter.GetInstance().GetEntity()->GetType() == FCDEntity::GEOMETRY)
{
Log(LOG_INFO, "Found static geometry");
FCDGeometryPolygons* polys = GetPolysFromGeometry((FCDGeometry*)instance->GetEntity());
FCDGeometryPolygons* polys = GetPolysFromGeometry((FCDGeometry*)converter.GetInstance().GetEntity());
// Convert the geometry into a suitable form for the game
ReindexGeometry(polys);
@ -101,11 +76,11 @@ public:
FCDGeometrySource* sourceNormal = inputNormal ->GetSource();
FCDGeometrySource* sourceTexcoord = inputTexcoord->GetSource();
FloatList& dataPosition = sourcePosition->GetSourceData();
FloatList& dataNormal = sourceNormal ->GetSourceData();
FloatList& dataTexcoord = sourceTexcoord->GetSourceData();
FloatList& dataPosition = sourcePosition->GetData();
FloatList& dataNormal = sourceNormal ->GetData();
FloatList& dataTexcoord = sourceTexcoord->GetData();
TransformVertices(dataPosition, dataNormal, transform, yUp);
TransformVertices(dataPosition, dataNormal, converter.GetEntityTransform(), converter.IsYUp());
std::vector<VertexBlend> boneWeights;
std::vector<BoneTransform> boneTransforms;
@ -113,18 +88,18 @@ public:
WritePMD(output, *indicesCombined, dataPosition, dataNormal, dataTexcoord, boneWeights, boneTransforms, propPoints);
}
else if (instance->GetType() == FCDEntityInstance::CONTROLLER)
else if (converter.GetInstance().GetType() == FCDEntityInstance::CONTROLLER)
{
Log(LOG_INFO, "Found skinned geometry");
FCDControllerInstance* controllerInstance = (FCDControllerInstance*)instance;
FCDControllerInstance& controllerInstance = static_cast<FCDControllerInstance&>(converter.GetInstance());
// (NB: GetType is deprecated and should be replaced with HasType,
// except that has irritating linker errors when using a DLL, so don't
// bother)
assert(instance->GetEntity()->GetType() == FCDEntity::CONTROLLER); // assume this is always true?
FCDController* controller = (FCDController*)instance->GetEntity();
assert(converter.GetInstance().GetEntity()->GetType() == FCDEntity::CONTROLLER); // assume this is always true?
FCDController* controller = static_cast<FCDController*>(converter.GetInstance().GetEntity());
FCDSkinController* skin = controller->GetSkinController();
REQUIRE(skin != NULL, "is skin controller");
@ -134,14 +109,17 @@ public:
// Data for joints is stored in two places - avoid overflows by limiting
// to the minimum of the two sizes, and warn if they're different (which
// happens in practice for slightly-broken meshes)
size_t jointCount = std::min(skin->GetJointCount(), controllerInstance->GetJointCount());
if (skin->GetJointCount() != controllerInstance->GetJointCount())
Log(LOG_WARNING, "Mismatched bone counts (skin has %d, skeleton has %d)", skin->GetJointCount(), controllerInstance->GetJointCount());
size_t jointCount = std::min(skin->GetJointCount(), controllerInstance.GetJointCount());
if (skin->GetJointCount() != controllerInstance.GetJointCount())
{
Log(LOG_WARNING, "Mismatched bone counts (skin has %d, skeleton has %d)",
skin->GetJointCount(), controllerInstance.GetJointCount());
}
// Get the skinned mesh for this entity
FCDEntity* baseTarget = controller->GetBaseTarget();
REQUIRE(baseTarget->GetType() == FCDEntity::GEOMETRY, "base target is geometry");
FCDGeometryPolygons* polys = GetPolysFromGeometry((FCDGeometry*)baseTarget);
FCDGeometry* baseGeometry = controller->GetBaseGeometry();
REQUIRE(baseGeometry != NULL, "controller has base geometry");
FCDGeometryPolygons* polys = GetPolysFromGeometry(baseGeometry);
// Make sure it doesn't use more bones per vertex than the game can handle
SkinReduceInfluences(skin, maxInfluences, 0.001f);
@ -149,6 +127,8 @@ public:
// Convert the geometry into a suitable form for the game
ReindexGeometry(polys, skin);
const Skeleton& skeleton = FindSkeleton(controllerInstance);
// Convert the bone influences into VertexBlend structures for the PMD:
bool hasComplainedAboutNonexistentJoints = false; // because we want to emit a warning only once
@ -171,8 +151,8 @@ public:
// Find the joint on the skeleton, after checking it really exists
FCDSceneNode* joint = NULL;
if (jointIdx < controllerInstance->GetJointCount())
joint = controllerInstance->GetJoint(jointIdx);
if (jointIdx < controllerInstance.GetJointCount())
joint = controllerInstance.GetJoint(jointIdx);
// Complain on error
if (! joint)
@ -186,8 +166,15 @@ public:
}
// Store into the VertexBlend
int boneId = StdSkeletons::FindStandardBoneID(joint->GetName());
REQUIRE(boneId >= 0, "vertex influenced by recognised bone");
int boneId = skeleton.GetBoneID(joint->GetName());
if (boneId < 0)
{
// The relevant joint does exist, but it's not a recognised
// bone in our chosen skeleton structure
Log(LOG_ERROR, "Vertex influenced by unrecognised bone '%s'", joint->GetName().c_str());
continue;
}
influences.bones[j] = (uint8)boneId;
influences.weights[j] = vertexInfluences[i][j].weight;
}
@ -198,15 +185,13 @@ public:
// Convert the bind pose into BoneTransform structures for the PMD:
BoneTransform boneDefault = { { 0, 0, 0 }, { 0, 0, 0, 1 } }; // identity transform
std::vector<BoneTransform> boneTransforms (StdSkeletons::GetBoneCount(), boneDefault);
transform = skin->GetBindShapeTransform();
std::vector<BoneTransform> boneTransforms (skeleton.GetBoneCount(), boneDefault);
for (size_t i = 0; i < jointCount; ++i)
{
FCDSceneNode* joint = controllerInstance->GetJoint(i);
FCDSceneNode* joint = controllerInstance.GetJoint(i);
int boneId = StdSkeletons::FindStandardBoneID(joint->GetName());
int boneId = skeleton.GetRealBoneID(joint->GetName());
if (boneId < 0)
{
// unrecognised joint - it's probably just a prop point
@ -240,9 +225,9 @@ public:
for (size_t i = 0; i < jointCount; ++i)
{
FCDSceneNode* joint = controllerInstance->GetJoint(i);
FCDSceneNode* joint = controllerInstance.GetJoint(i);
int boneId = StdSkeletons::FindStandardBoneID(joint->GetName());
int boneId = skeleton.GetBoneID(joint->GetName());
if (boneId < 0)
{
// unrecognised joint name - ignore, same as before
@ -252,13 +237,13 @@ public:
for (size_t j = 0; j < joint->GetChildrenCount(); ++j)
{
FCDSceneNode* child = joint->GetChild(j);
if (StdSkeletons::FindStandardBoneID(child->GetName()) >= 0)
if (skeleton.GetBoneID(child->GetName()) >= 0)
{
// recognised as a bone, hence not a prop point, so skip it
continue;
}
//Log(LOG_INFO, "Adding prop point %s", child->GetName().c_str());
// Log(LOG_INFO, "Adding prop point %s", child->GetName().c_str());
// Get translation and orientation of local transform
@ -295,11 +280,13 @@ public:
FCDGeometrySource* sourceNormal = inputNormal ->GetSource();
FCDGeometrySource* sourceTexcoord = inputTexcoord->GetSource();
FloatList& dataPosition = sourcePosition->GetSourceData();
FloatList& dataNormal = sourceNormal ->GetSourceData();
FloatList& dataTexcoord = sourceTexcoord->GetSourceData();
FloatList& dataPosition = sourcePosition->GetData();
FloatList& dataNormal = sourceNormal ->GetData();
FloatList& dataTexcoord = sourceTexcoord->GetData();
TransformVertices(dataPosition, dataNormal, boneTransforms, propPoints, transform, yUp);
TransformVertices(dataPosition, dataNormal, boneTransforms, propPoints,
converter.GetEntityTransform(), skin->GetBindShapeTransform(),
converter.IsYUp(), converter.IsXSI());
WritePMD(output, *indicesCombined, dataPosition, dataNormal, dataTexcoord, boneWeights, boneTransforms, propPoints);
}
@ -444,8 +431,23 @@ public:
static void TransformVertices(FloatList& position, FloatList& normal,
std::vector<BoneTransform>& bones, std::vector<PropPoint>& propPoints,
const FMMatrix44& transform, bool yUp)
const FMMatrix44& transform, const FMMatrix44& bindTransform, bool yUp, bool isXSI)
{
FMMatrix44 scaledTransform; // for vertexes
FMMatrix44 scaleMatrix; // for bones
// HACK: see comment in PSAConvert::TransformVertices
if (isXSI)
{
scaleMatrix = DecomposeToScaleMatrix(transform);
scaledTransform = DecomposeToScaleMatrix(bindTransform) * transform;
}
else
{
scaleMatrix = FMMatrix44::Identity;
scaledTransform = bindTransform;
}
// Update the vertex positions and normals
assert(position.size() == normal.size());
for (size_t i = 0; i < position.size()/3; ++i)
@ -454,15 +456,15 @@ public:
FMVector3 norm (&normal[i*3], 0);
// Apply the scene-node transforms
pos = transform.TransformCoordinate(pos);
norm = transform.TransformVector(norm).Normalize();
pos = scaledTransform.TransformCoordinate(pos);
norm = scaledTransform.TransformVector(norm).Normalize();
// Convert from Y_UP or Z_UP to the game's coordinate system
if (yUp)
{
pos.x = -pos.x;
norm.x = -norm.x;
pos.z = -pos.z;
norm.z = -norm.z;
}
else
{
@ -481,34 +483,7 @@ public:
normal[i*3+2] = norm.z;
}
// We also need to change the bone rest states into the new coordinate
// system, so it'll look correct when displayed without any animation
// applied to the skeleton.
for (size_t i = 0; i < bones.size(); ++i)
{
if (yUp)
{
// TODO: this is all just guesses which seem to work for data
// exported from XSI, rather than having been properly thought
// through
bones[i].translation[0] = -bones[i].translation[0];
bones[i].orientation[0] = -bones[i].orientation[0];
bones[i].orientation[3] = -bones[i].orientation[3];
}
else
{
// Convert bone translations from xyz into xzy axes:
std::swap(bones[i].translation[1], bones[i].translation[2]);
// To convert the quaternions: imagine you're using the axis/angle
// representation, then swap the y,z basis vectors and change the
// direction of rotation by negating the angle ( => negating sin(angle)
// => negating x,y,z => changing (x,y,z,w) to (-x,-z,-y,w)
// but then (-x,-z,-y,w) == (x,z,y,-w) so do that instead)
std::swap(bones[i].orientation[1], bones[i].orientation[2]);
bones[i].orientation[3] = -bones[i].orientation[3];
}
}
TransformBones(bones, scaleMatrix, yUp);
// And do the same for prop points
for (size_t i = 0; i < propPoints.size(); ++i)

View File

@ -26,12 +26,6 @@
#include <vector>
#include <limits>
struct BoneTransform
{
float translation[3];
float orientation[4];
};
class PSAConvert
{
public:
@ -46,44 +40,34 @@ public:
*/
static void ColladaToPSA(const char* input, OutputCB& output, std::string& xmlErrors)
{
FColladaErrorHandler err (xmlErrors);
CommonConvert converter(input, xmlErrors);
FColladaDocument doc;
doc.LoadFromText(input);
FCDSceneNode* root = doc.GetDocument()->GetVisualSceneRoot();
REQUIRE(root != NULL, "has root object");
// Find the instance to convert
FCDEntityInstance* instance;
FMMatrix44 entityTransform;
if (! FindSingleInstance(root, instance, entityTransform))
throw ColladaException("Couldn't find object to convert");
assert(instance);
Log(LOG_INFO, "Converting '%s'", instance->GetEntity()->GetName().c_str());
FMVector3 upAxis = doc.GetDocument()->GetAsset()->GetUpAxis();
bool yUp = (upAxis.y != 0); // assume either Y_UP or Z_UP (TODO: does anyone ever do X_UP?)
if (instance->GetType() == FCDEntityInstance::CONTROLLER)
if (converter.GetInstance().GetType() == FCDEntityInstance::CONTROLLER)
{
FCDControllerInstance* controllerInstance = (FCDControllerInstance*)instance;
FCDControllerInstance& controllerInstance = static_cast<FCDControllerInstance&>(converter.GetInstance());
FixSkeletonRoots(controllerInstance);
assert(converter.GetInstance().GetEntity()->GetType() == FCDEntity::CONTROLLER); // assume this is always true?
FCDController* controller = static_cast<FCDController*>(converter.GetInstance().GetEntity());
FCDSkinController* skin = controller->GetSkinController();
REQUIRE(skin != NULL, "is skin controller");
const Skeleton& skeleton = FindSkeleton(controllerInstance);
float frameLength = 1.f / 30.f; // currently we always want to create PMDs at fixed 30fps
// Find the extents of the animation:
float timeStart, timeEnd;
GetAnimationRange(doc, controllerInstance, timeStart, timeEnd);
GetAnimationRange(converter.GetDocument(), skeleton, controllerInstance, timeStart, timeEnd);
// Count frames; don't include the last keyframe
size_t frameCount = (size_t)((timeEnd - timeStart) / frameLength - 0.5f);
// (TODO: sort out the timing/looping problems)
size_t boneCount = StdSkeletons::GetBoneCount();
size_t boneCount = skeleton.GetBoneCount();
std::vector<BoneTransform> boneTransforms;
@ -97,14 +81,14 @@ public:
// Move the model into the new animated pose
// (We can't tell exactly which nodes should be animated, so
// just update the entire world recursively)
EvaluateAnimations(root, time);
EvaluateAnimations(converter.GetRoot(), time);
// Convert the pose into the form require by the game
for (size_t i = 0; i < controllerInstance->GetJointCount(); ++i)
for (size_t i = 0; i < controllerInstance.GetJointCount(); ++i)
{
FCDSceneNode* joint = controllerInstance->GetJoint(i);
FCDSceneNode* joint = controllerInstance.GetJoint(i);
int boneId = StdSkeletons::FindStandardBoneID(joint->GetName());
int boneId = skeleton.GetRealBoneID(joint->GetName());
if (boneId < 0)
continue; // not a recognised bone - ignore it, same as before
@ -130,7 +114,7 @@ public:
}
// Convert into game's coordinate space
TransformVertices(boneTransforms, yUp);
TransformVertices(boneTransforms, skin->GetBindShapeTransform(), converter.IsYUp(), converter.IsXSI());
// Write out the file
WritePSA(output, frameCount, boneCount, boneTransforms);
@ -170,27 +154,25 @@ public:
}
}
static void TransformVertices(std::vector<BoneTransform>& bones, bool yUp)
static void TransformVertices(std::vector<BoneTransform>& bones,
const FMMatrix44& transform, bool yUp, bool isXSI)
{
// (See PMDConvert.cpp for explanatory comments)
for (size_t i = 0; i < bones.size(); ++i)
// HACK: we want to handle scaling in XSI because that makes it easy
// for artists to adjust the models to the right size. But this way
// doesn't work in Max, and I can't see how to make it do so, so this
// is only applied to models from XSI.
if (isXSI)
{
if (yUp)
{
bones[i].translation[0] = -bones[i].translation[0];
bones[i].orientation[0] = -bones[i].orientation[0];
bones[i].orientation[3] = -bones[i].orientation[3];
}
else
{
std::swap(bones[i].translation[1], bones[i].translation[2]);
std::swap(bones[i].orientation[1], bones[i].orientation[2]);
bones[i].orientation[3] = -bones[i].orientation[3];
}
TransformBones(bones, DecomposeToScaleMatrix(transform), yUp);
}
else
{
TransformBones(bones, FMMatrix44::Identity, yUp);
}
}
static void GetAnimationRange(FColladaDocument& doc, FCDControllerInstance* controllerInstance,
static void GetAnimationRange(const FColladaDocument& doc, const Skeleton& skeleton,
const FCDControllerInstance& controllerInstance,
float& timeStart, float& timeEnd)
{
// FCollada tools export <extra> info in the scene to specify the start
@ -212,12 +194,12 @@ public:
timeStart = std::numeric_limits<float>::max();
timeEnd = -std::numeric_limits<float>::max();
for (size_t i = 0; i < controllerInstance->GetJointCount(); ++i)
for (size_t i = 0; i < controllerInstance.GetJointCount(); ++i)
{
FCDSceneNode* joint = controllerInstance->GetJoint(i);
const FCDSceneNode* joint = controllerInstance.GetJoint(i);
REQUIRE(joint != NULL, "joint exists");
int boneId = StdSkeletons::FindStandardBoneID(joint->GetName());
int boneId = skeleton.GetBoneID(joint->GetName());
if (boneId < 0)
{
// unrecognised joint - it's probably just a prop point
@ -231,20 +213,20 @@ public:
for (size_t j = 0; j < joint->GetTransformCount(); ++j)
{
FCDTransform* transform = joint->GetTransform(j);
const FCDTransform* transform = joint->GetTransform(j);
if (! transform->IsAnimated())
continue;
// Iterate over all curves
FCDAnimated* anim = transform->GetAnimated();
FCDAnimationCurveListList& curvesList = anim->GetCurves();
const FCDAnimated* anim = transform->GetAnimated();
const FCDAnimationCurveListList& curvesList = anim->GetCurves();
for (size_t j = 0; j < curvesList.size(); ++j)
{
FCDAnimationCurveList& curves = curvesList[j];
const FCDAnimationCurveList& curves = curvesList[j];
for (size_t k = 0; k < curves.size(); ++k)
{
FCDAnimationCurve* curve = curves[k];
const FCDAnimationCurve* curve = curves[k];
timeStart = std::min(timeStart, curve->GetKeys().front());
timeEnd = std::max(timeEnd, curve->GetKeys().back());
}
@ -253,7 +235,7 @@ public:
}
}
static bool GetAnimationRange_XSI(FColladaDocument& doc, float& timeStart, float& timeEnd)
static bool GetAnimationRange_XSI(const FColladaDocument& doc, float& timeStart, float& timeEnd)
{
FCDExtra* extra = doc.GetExtra();
if (! extra) return false;
@ -291,18 +273,18 @@ public:
return false;
}
static void EvaluateAnimations(FCDSceneNode* node, float time)
static void EvaluateAnimations(FCDSceneNode& node, float time)
{
for (size_t i = 0; i < node->GetTransformCount(); ++i)
for (size_t i = 0; i < node.GetTransformCount(); ++i)
{
FCDTransform* transform = node->GetTransform(i);
FCDTransform* transform = node.GetTransform(i);
FCDAnimated* anim = transform->GetAnimated();
if (anim)
anim->Evaluate(time);
}
for (size_t i = 0; i < node->GetChildrenCount(); ++i)
EvaluateAnimations(node->GetChild(i), time);
for (size_t i = 0; i < node.GetChildrenCount(); ++i)
EvaluateAnimations(*node.GetChild(i), time);
}
};

View File

@ -2,116 +2,192 @@
#include "StdSkeletons.h"
#include "CommonConvert.h"
#include "FUtils/FUXmlParser.h"
namespace
{
const char* standardBoneNames0[] = {
/* */ "Bip01",
/* */ "Bip01_Pelvis",
/* */ "Bip01_Spine",
/* */ "Bip01_Spine1",
/* */ "Bip01_Neck",
/* */ "Bip01_Head",
/* */ "Bip01_HeadNub",
/* */ "Bip01_L_Clavicle",
/* */ "Bip01_L_UpperArm",
/* */ "Bip01_L_Forearm",
/* */ "Bip01_L_Hand",
/* */ "Bip01_L_Finger0",
/* */ "Bip01_L_Finger0Nub",
/* */ "Bip01_R_Clavicle",
/* */ "Bip01_R_UpperArm",
/* */ "Bip01_R_Forearm",
/* */ "Bip01_R_Hand",
/* */ "Bip01_R_Finger0",
/* */ "Bip01_R_Finger0Nub",
/* */ "Bip01_L_Thigh",
/* */ "Bip01_L_Calf",
/* */ "Bip01_L_Foot",
/* */ "Bip01_L_Toe0",
/* */ "Bip01_L_Toe0Nub",
/* */ "Bip01_R_Thigh",
/* */ "Bip01_R_Calf",
/* */ "Bip01_R_Foot",
/* */ "Bip01_R_Toe0",
/* */ "Bip01_R_Toe0Nub",
// (the above comments just stop the indentation being dropped by
// automatic code-formatting things...)
// NULL
// };
// (TODO (important): do this stuff properly)
// const char* standardBoneNames1[] = {
/* */ "Biped_GlobalSRT",
/* */ "Biped_Spineroot",
/* */ "Biped_Spine01",
/* */ "Biped_Spine02",
/* */ "Biped_Spine03",
/* */ "Biped_Spineeffector",
/* */ "Biped_Lshoulderroot",
/* */ "Biped_Lshoulder",
/* */ "Biped_Lshouldereffector",
/* */ "Biped_Larmroot",
/* */ "Biped_Lbicept",
/* */ "Biped_Lforearm",
/* */ "Biped_Larmupvector",
/* */ "Biped_Rshoulderroot",
/* */ "Biped_Rshoulder",
/* */ "Biped_Rshouldereffector",
/* */ "Biped_Rarmroot",
/* */ "Biped_Rbicept",
/* */ "Biped_Rforearm",
/* */ "Biped_Rarmupvector",
/* */ "Biped_neckroot",
/* */ "Biped_neck",
/* */ "Biped_head",
/* */ "Biped_headeffector",
/* */ "Biped_Llegroot",
/* */ "Biped_Lthigh",
/* */ "Biped_Lshin",
/* */ "Biped_Rlegroot",
/* */ "Biped_Rthigh",
/* */ "Biped_Rshin",
/* */ "Biped_Llegupvector",
/* */ "Biped_Rlegupvector",
/* */ "Biped_Larmeffector",
/* */ "Biped_Lhandroot",
/* */ "Biped_Lhand",
/* */ "Biped_Lfingers",
/* */ "Biped_Lhandeffector",
/* */ "Biped_Llegeffector",
/* */ "Biped_Lfooteffector",
/* */ "Biped_Lfoot",
/* */ "Biped_Ltoe",
/* */ "Biped_Ltoeeffector",
/* */ "Biped_Rarmeffector",
/* */ "Biped_Rhandroot",
/* */ "Biped_Rhand",
/* */ "Biped_Rfingers",
/* */ "Biped_Rhandeffector",
/* */ "Biped_Rlegeffector",
/* */ "Biped_Rfootroot",
/* */ "Biped_Rfoot",
/* */ "Biped_Rtoe",
/* */ "Biped_Rtoeeffector",
NULL
struct SkeletonMap : public std::map<std::string, const Skeleton*>
{
~SkeletonMap()
{
for (iterator it = begin(); it != end(); ++it)
delete it->second;
}
};
SkeletonMap g_StandardSkeletons;
SkeletonMap g_MappedSkeletons;
struct Bone
{
std::string parent;
std::string name;
int targetId;
int realTargetId;
};
}
namespace StdSkeletons
struct Skeleton_impl
{
int GetBoneCount()
std::string title;
std::vector<Bone> bones;
const Skeleton* target;
};
Skeleton::Skeleton() : m(new Skeleton_impl) { }
Skeleton::~Skeleton() { }
const Skeleton* Skeleton::FindSkeleton(const std::string& name)
{
return g_MappedSkeletons[name];
}
int Skeleton::GetBoneID(const std::string& name) const
{
for (size_t i = 0; i < m->bones.size(); ++i)
if (m->bones[i].name == name)
return m->bones[i].targetId;
return -1;
}
int Skeleton::GetRealBoneID(const std::string& name) const
{
for (size_t i = 0; i < m->bones.size(); ++i)
if (m->bones[i].name == name)
return m->bones[i].realTargetId;
return -1;
}
int Skeleton::GetBoneCount() const
{
return (int)m->target->m->bones.size();
}
namespace
{
bool AlreadyUsedTargetBone(const std::vector<Bone>& bones, int targetId)
{
int i = 0;
while (standardBoneNames0[i] != NULL)
++i;
return i;
for (size_t i = 0; i < bones.size(); ++i)
if (bones[i].targetId == targetId)
return true;
return false;
}
int FindStandardBoneID(const std::string& name)
// Recursive helper function used by LoadSkeletonData
void LoadSkeletonBones(xmlNode* parent, std::vector<Bone>& bones, const Skeleton* targetSkeleton, const std::string& targetName)
{
for (int i = 0; standardBoneNames0[i] != NULL; ++i)
if (standardBoneNames0[i] == name)
return i;
return -1;
xmlNodeList boneNodes;
FUXmlParser::FindChildrenByType(parent, "bone", boneNodes);
for (xmlNodeList::iterator boneNode = boneNodes.begin(); boneNode != boneNodes.end(); ++boneNode)
{
std::string name = FUXmlParser::ReadNodeProperty(*boneNode, "name");
Bone b;
b.name = name;
std::string newTargetName = targetName;
if (targetSkeleton)
{
xmlNode* targetNode = FUXmlParser::FindChildByType(*boneNode, "target");
if (targetNode)
newTargetName = FUXmlParser::ReadNodeContentFull(targetNode);
// else fall back to the parent node's target
b.targetId = targetSkeleton->GetBoneID(newTargetName);
REQUIRE(b.targetId != -1, "skeleton bone target matches some standard_skeleton bone name");
if (AlreadyUsedTargetBone(bones, b.targetId))
b.realTargetId = -1;
else
b.realTargetId = b.targetId;
}
else
{
b.targetId = (int)bones.size();
b.realTargetId = b.targetId;
}
bones.push_back(b);
LoadSkeletonBones(*boneNode, bones, targetSkeleton, newTargetName);
}
}
void LoadSkeletonData(xmlNode* root)
{
xmlNodeList skeletonNodes;
FUXmlParser::FindChildrenByType(root, "standard_skeleton", skeletonNodes);
FUXmlParser::FindChildrenByType(root, "skeleton", skeletonNodes);
for (xmlNodeList::iterator skeletonNode = skeletonNodes.begin();
skeletonNode != skeletonNodes.end(); ++skeletonNode)
{
std::auto_ptr<Skeleton> skeleton (new Skeleton());
std::string title = FUXmlParser::ReadNodeProperty(*skeletonNode, "title");
skeleton->m->title = title;
if (IsEquivalent((*skeletonNode)->name, "standard_skeleton"))
{
skeleton->m->target = NULL;
LoadSkeletonBones(*skeletonNode, skeleton->m->bones, NULL, "");
std::string id = FUXmlParser::ReadNodeProperty(*skeletonNode, "id");
REQUIRE(! id.empty(), "standard_skeleton has id");
g_StandardSkeletons[id] = skeleton.release();
}
else
{
// Non-standard skeletons need to choose a standard skeleton
// as their target to be mapped onto
std::string target = FUXmlParser::ReadNodeProperty(*skeletonNode, "target");
const Skeleton* targetSkeleton = g_StandardSkeletons[target];
REQUIRE(targetSkeleton != NULL, "skeleton target matches some standard_skeleton id");
skeleton->m->target = targetSkeleton;
LoadSkeletonBones(*skeletonNode, skeleton->m->bones, targetSkeleton, "");
// Currently the only supported identifier is a precise name match,
// so just look for that
xmlNode* identifier = FUXmlParser::FindChildByType(*skeletonNode, "identifier");
REQUIRE(identifier != NULL, "skeleton has <identifier>");
xmlNode* identRoot = FUXmlParser::FindChildByType(identifier, "root");
REQUIRE(identRoot != NULL, "skeleton identifier has <root>");
std::string identRootName = FUXmlParser::ReadNodeContentFull(identRoot);
g_MappedSkeletons[identRootName] = skeleton.release();
}
}
}
}
void Skeleton::LoadSkeletonDataFromXml(const char* text)
{
xmlDoc* doc = NULL;
try
{
doc = xmlParseDoc(reinterpret_cast<const unsigned char*>(text));
if (doc)
{
xmlNode* root = xmlDocGetRootElement(doc);
LoadSkeletonData(root);
xmlFreeDoc(doc);
doc = NULL;
}
xmlCleanupParser();
}
catch (const ColladaException&)
{
if (doc)
xmlFreeDoc(doc);
xmlCleanupParser();
throw;
}
}

View File

@ -3,10 +3,76 @@
#include <string>
namespace StdSkeletons
/*
Rough skeleton concept:
Different modelling/animation programs use different skeleton structures for
conceptually similar situations (e.g. humanoid bipeds). We'd like the game to
handle combinations of models and animations from different programs, so the
exported data must use a standard skeleton structure.
Rather than requiring awkward reconfiguration of those programs, we use an
XML file which defines a small set of standard skeletons (e.g. "biped") and
also a number of modelling-program-specific skeleton structures (e.g. "3ds Max
biped"). The non-standard ones define which bones we should expect to find,
and what standard bone they should get mapped onto. Sometimes multiple bones
will be mapped onto the same one (e.g. a series of fingers would all get mapped
onto a single hand bone), so the conversion is slightly lossy.
It is possible to have multiple independent standard skeletons - they just need
to be identified by using different bone names in the modelling program. The
game doesn't check that you're using models and animations with the same standard
skeleton, so that's up to the artists to take care of.
*/
struct Skeleton_impl;
class Skeleton
{
extern int GetBoneCount();
extern int FindStandardBoneID(const std::string& name);
}
public:
/** Default constructor - don't use; use FindSkeleton instead. */
Skeleton();
~Skeleton();
/**
* Returns the number of bones in the standard-skeleton which this current
* skeleton is mapped onto.
*/
int GetBoneCount() const;
/**
* Returns the ID number of the standard-skeleton bone, which corresponds
* to the named nonstandard-skeleton bone. Returns -1 if none is found.
* Can return the same value for multiple bones.
*/
int GetBoneID(const std::string& name) const;
/**
* Similar to GetBoneID, but only returns a value for the most important (first)
* nonstandard-skeleton bone for each standard bone; returns -1 in other cases.
* This is necessary when deciding which nonstandard-skeleton-bone's animation
* should be saved. (TODO: think of a better name.)
*/
int GetRealBoneID(const std::string& name) const;
/**
* Tries to find a skeleton that matches the given root bone name.
* Returns NULL if there is no match.
*/
static const Skeleton* FindSkeleton(const std::string& rootBoneName);
/**
* Initialises the global state with skeleton data loaded from the
* given XML data. Must only be called once.
* (TODO: don't do global state.)
* @throws ColladaException on failure.
*/
static void LoadSkeletonDataFromXml(const char* text);
std::auto_ptr<Skeleton_impl> m;
private:
Skeleton(Skeleton&);
};
#endif // STDSKELETONS_H__

View File

@ -31,7 +31,9 @@ extern void Log(int severity, const char* fmt, ...);
#include "FUtils/FUDaeSyntax.h"
#include "FUtils/FUFileManager.h"
#include "FUtils/FUXmlParser.h"
#include <cassert>
#include <cstdarg>
#include <string>
// FCollada pollutes the global namespace by defining these

View File

@ -8,7 +8,6 @@
#define ATLASOBJECT_H__
#include <wchar.h> // for wchar_t
#include <wx/string.h>
//////////////////////////////////////////////////////////////////////////
// Mostly-private bits:
@ -70,7 +69,7 @@ class AtIter
public:
// Increment the iterator; or make it undefined, if there weren't any
// AtObjs left to iterate over
AtIter& operator ++ ();
AtIter& operator++ ();
// Return whether this iterator has an AtObj to point to
bool defined() const;
// Return whether this iterator is pointing to a non-contentless AtObj
@ -78,21 +77,13 @@ public:
// Return an iterator to the children matching 'key'. (That is, children
// of the AtObj currently pointed to by this iterator)
const AtIter operator [] (const char* key) const;
const AtIter operator[] (const char* key) const;
// Return the AtObj currently pointed to by this iterator
//operator const AtObj () const;
const AtObj operator *() const;
const AtObj operator* () const;
// Return the string value of the AtObj currently pointed to by this iterator
operator const wchar_t* () const;
//operator const wxChar* () const { return (const wchar_t*)*this; }
#ifdef __WXWINDOWS__
// Wrapper function around 'operator wchar_t*', for convenience in wx programs
//operator wxString () const { return (const wchar_t*)*this; }
//wxString towxString() const { return (const wchar_t*)*this; }
#endif
// Private implementation. (But not 'private:', because it's a waste of time
// adding loads of friend functions)
@ -107,7 +98,7 @@ public:
AtObj(const AtObj& r) : p(r.p) {}
// Return an iterator to the children matching 'key'
const AtIter operator [] (const char* key) const;
const AtIter operator[] (const char* key) const;
// Return the string value of this object
operator const wchar_t* () const;