From ea29a1caeb56ca3a13c1c1b43f26f2be40511f29 Mon Sep 17 00:00:00 2001 From: Ykkrosh Date: Thu, 1 Mar 2007 00:24:34 +0000 Subject: [PATCH] # Updated COLLADA converter. Now uses FCollada 3.02. This was SVN commit r4933. --- source/collada/CommonConvert.cpp | 53 ++++++++++++++++----- source/collada/CommonConvert.h | 17 ++++++- source/collada/DLL.cpp | 2 +- source/collada/PMDConvert.cpp | 55 +++++++++++++-------- source/collada/PSAConvert.cpp | 82 ++++++++++++++++++++++---------- source/collada/tests/tests.py | 66 +++++++++++++++++++++---- 6 files changed, 204 insertions(+), 71 deletions(-) diff --git a/source/collada/CommonConvert.cpp b/source/collada/CommonConvert.cpp index 93dc42f1c8..3bbff9c9a1 100644 --- a/source/collada/CommonConvert.cpp +++ b/source/collada/CommonConvert.cpp @@ -8,12 +8,6 @@ #include -/** Throws a ColladaException unless the value is true. */ -#define REQUIRE(value, message) require_(__LINE__, value, "Assertion not satisfied", message) - -/** Throws a ColladaException unless the status is successful. */ -#define REQUIRE_SUCCESS(status) require_(__LINE__, status) - void require_(int line, bool value, const char* type, const char* message) { if (value) return; @@ -22,11 +16,6 @@ void require_(int line, bool value, const char* type, const char* message) throw ColladaException(std::string(type) + " (line " + linestr + "): " + message); } -void require_(int line, const FUStatus& status) -{ - require_(line, status, "FCollada error", status.GetErrorString()); -} - /** Error handler for libxml2 */ void errorHandler(void* ctx, const char* msg, ...) { @@ -40,8 +29,50 @@ void errorHandler(void* ctx, const char* msg, ...) *((std::string*)ctx) += buffer; } +FColladaErrorHandler::FColladaErrorHandler(std::string& xmlErrors_) +: xmlErrors(xmlErrors_) +{ + // Grab all the error output from libxml2, for useful error reporting + xmlSetGenericErrorFunc(&xmlErrors, &errorHandler); + + FUError::SetErrorCallback(FUError::DEBUG, new FUFunctor3(this, &FColladaErrorHandler::OnError)); + FUError::SetErrorCallback(FUError::WARNING, new FUFunctor3(this, &FColladaErrorHandler::OnError)); + FUError::SetErrorCallback(FUError::ERROR, new FUFunctor3(this, &FColladaErrorHandler::OnError)); +} + +FColladaErrorHandler::~FColladaErrorHandler() +{ + xmlSetGenericErrorFunc(NULL, NULL); + + FUError::SetErrorCallback(FUError::DEBUG, NULL); + FUError::SetErrorCallback(FUError::WARNING, NULL); + FUError::SetErrorCallback(FUError::ERROR, NULL); +} + +void FColladaErrorHandler::OnError(FUError::Level errorLevel, uint32 errorCode, uint32 UNUSED(lineNumber)) +{ + const char* errorString = FUError::GetErrorString((FUError::Code) errorCode); + if (! errorString) + errorString = "Unknown error code"; + + if (errorLevel == FUError::DEBUG) + Log(LOG_INFO, "FCollada message %d: %s", errorCode, errorString); + else if (errorLevel == FUError::WARNING) + Log(LOG_WARNING, "FCollada error %d: %s", errorCode, errorString); + else + throw ColladaException(errorString); +} + + + ////////////////////////////////////////////////////////////////////////// +// 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); +static float identity[] = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 }; +FMMatrix44 FMMatrix44::Identity(identity); + struct FoundInstance { FCDEntityInstance* instance; diff --git a/source/collada/CommonConvert.h b/source/collada/CommonConvert.h index 419409dc2d..cb2b16eb74 100644 --- a/source/collada/CommonConvert.h +++ b/source/collada/CommonConvert.h @@ -35,14 +35,27 @@ struct OutputCB virtual void operator() (const char* data, unsigned int length)=0; }; +class FColladaErrorHandler +{ +public: + FColladaErrorHandler(std::string& xmlErrors); + ~FColladaErrorHandler(); + +private: + void OnError(FUError::Level errorLevel, uint32 errorCode, uint32 lineNumber); + std::string& xmlErrors; + + void operator=(FColladaErrorHandler); +}; + /** Throws a ColladaException unless the value is true. */ #define REQUIRE(value, message) require_(__LINE__, value, "Assertion not satisfied", message) /** Throws a ColladaException unless the status is successful. */ -#define REQUIRE_SUCCESS(status) require_(__LINE__, status) +#define REQUIRE_SUCCESS(status) require_(__LINE__, status, "FCollada error", "Line " STRINGIFY(__LINE__)) +#define STRINGIFY(x) #x void require_(int line, bool value, const char* type, const char* message); -void require_(int line, const FUStatus& status); /** Outputs a structure, using sizeof to get the size. */ template void write(OutputCB& output, const T& data) diff --git a/source/collada/DLL.cpp b/source/collada/DLL.cpp index 62ca470daa..514128d5dc 100644 --- a/source/collada/DLL.cpp +++ b/source/collada/DLL.cpp @@ -88,7 +88,7 @@ int convert_dae_to_whatever(const char* dae, OutputFn writer, void* cb_data, voi { conv(dae, cb, xmlErrors); } - catch (ColladaException e) + catch (const ColladaException& e) { if (! xmlErrors.empty()) Log(LOG_ERROR, "%s", xmlErrors.c_str()); diff --git a/source/collada/PMDConvert.cpp b/source/collada/PMDConvert.cpp index 744f51cc4b..7d4c982244 100644 --- a/source/collada/PMDConvert.cpp +++ b/source/collada/PMDConvert.cpp @@ -50,17 +50,13 @@ public: */ static void ColladaToPMD(const char* input, OutputCB& output, std::string& xmlErrors) { - FUStatus ret; - - // Grab all the error output from libxml2. Be careful to never use - // libxml2 outside this function without having first set/reset the - // errorfunc (since xmlErrors won't be valid any more). - xmlSetGenericErrorFunc(&xmlErrors, &errorHandler); + FColladaErrorHandler err (xmlErrors); std::auto_ptr doc (FCollada::NewTopDocument()); REQUIRE_SUCCESS(doc->LoadFromText("", input)); FCDSceneNode* root = doc->GetVisualSceneRoot(); + REQUIRE(root != NULL, "has root object"); // Find the instance to convert FCDEntityInstance* instance; @@ -101,12 +97,26 @@ public: WritePMD(output, *indicesCombined, dataPosition, dataNormal, dataTexcoord, boneWeights, boneTransforms); } - else if (instance->GetEntity()->GetType() == FCDEntity::CONTROLLER) + else if (instance->GetType() == FCDEntityInstance::CONTROLLER) { + FCDControllerInstance* controllerInstance = (FCDControllerInstance*)instance; + + // (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(); - REQUIRE(controller->HasSkinController(), "has skin controller"); FCDSkinController* skin = controller->GetSkinController(); + REQUIRE(skin != NULL, "is skin controller"); + + // 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"); // Get the skinned mesh for this entity FCDEntity* baseTarget = controller->GetBaseTarget(); @@ -135,7 +145,12 @@ public: { uint32 jointIdx = vertexInfluences[i][j].jointIndex; REQUIRE(jointIdx <= 0xFF, "sensible number of joints"); - FCDSceneNode* joint = skin->GetJoint(jointIdx)->joint; + + // Find the joint on the skeleton, after checking it really exists + FCDSceneNode* joint = NULL; + if (jointIdx < controllerInstance->GetJointCount()) + joint = controllerInstance->GetJoint(jointIdx); + if (! joint) { if (! hasComplainedAboutNonexistentJoints) @@ -159,17 +174,9 @@ public: transform = skin->GetBindShapeTransform(); - for (size_t i = 0; i < skin->GetJointCount(); ++i) + for (size_t i = 0; i < jointCount; ++i) { - FCDJointMatrixPair* joint = skin->GetJoint(i); - - if (! joint->joint) - { - Log(LOG_WARNING, "Skin has nonexistent joint"); - continue; - } - - FMMatrix44 bindPose = joint->invertedBindPose.Inverted(); + FMMatrix44 bindPose = skin->GetBindPoses()[i].Inverted(); HMatrix matrix; memcpy(matrix, bindPose.Transposed().m, sizeof(matrix)); @@ -183,8 +190,14 @@ public: { parts.q.x, parts.q.y, parts.q.z, parts.q.w } }; - int boneId = StdSkeletons::FindStandardBoneID(joint->joint->GetName()); - REQUIRE(boneId >= 0, "recognised bone name"); + FCDSceneNode* joint = controllerInstance->GetJoint(i); + int boneId = StdSkeletons::FindStandardBoneID(joint->GetName()); + if (boneId < 0) + { + Log(LOG_WARNING, "Unrecognised bone name '%s'", joint->GetName().c_str()); + continue; + } + boneTransforms[boneId] = b; } diff --git a/source/collada/PSAConvert.cpp b/source/collada/PSAConvert.cpp index 600f00d98f..9b8f7a23ee 100644 --- a/source/collada/PSAConvert.cpp +++ b/source/collada/PSAConvert.cpp @@ -14,7 +14,6 @@ #include "FCDocument/FCDGeometryPolygons.h" #include "FCDocument/FCDGeometrySource.h" #include "FCDocument/FCDSceneNode.h" -#include "FCDocument/FCDSkinController.h" #include "StdSkeletons.h" #include "Decompose.h" @@ -23,6 +22,7 @@ #include #include +#include struct BoneTransform { @@ -44,17 +44,13 @@ public: */ static void ColladaToPSA(const char* input, OutputCB& output, std::string& xmlErrors) { - FUStatus ret; - - // Grab all the error output from libxml2. Be careful to never use - // libxml2 outside this function without having first set/reset the - // errorfunc (since xmlErrors won't be valid any more). - xmlSetGenericErrorFunc(&xmlErrors, &errorHandler); + FColladaErrorHandler err (xmlErrors); std::auto_ptr doc (FCollada::NewTopDocument()); REQUIRE_SUCCESS(doc->LoadFromText("", input)); FCDSceneNode* root = doc->GetVisualSceneRoot(); + REQUIRE(root != NULL, "has root object"); // Find the instance to convert FCDEntityInstance* instance; @@ -65,28 +61,33 @@ public: assert(instance); Log(LOG_INFO, "Converting '%s'", instance->GetEntity()->GetName().c_str()); - if (instance->GetEntity()->GetType() == FCDEntity::CONTROLLER) + if (instance->GetType() == FCDEntityInstance::CONTROLLER) { - FCDController* controller = (FCDController*)instance->GetEntity(); - - REQUIRE(controller->HasSkinController(), "has skin controller"); - FCDSkinController* skin = controller->GetSkinController(); + FCDControllerInstance* controllerInstance = (FCDControllerInstance*)instance; // Find the first and last times which have animations + // TODO: use the FCOLLADA start_time/end_time where available float timeStart = std::numeric_limits::max(); float timeEnd = -std::numeric_limits::max(); - for (size_t i = 0; i < skin->GetJointCount(); ++i) + for (size_t i = 0; i < controllerInstance->GetJointCount(); ++i) { - FCDJointMatrixPair* joint = skin->GetJoint(i); - REQUIRE(joint->joint != NULL, "joint exists"); + FCDSceneNode* joint = controllerInstance->GetJoint(i); + REQUIRE(joint != NULL, "joint exists"); + + int boneId = StdSkeletons::FindStandardBoneID(joint->GetName()); + if (boneId < 0) + { + Log(LOG_WARNING, "Unrecognised bone name '%s'", joint->GetName().c_str()); + continue; + } // Skip unanimated joints - if (joint->joint->GetTransformCount() == 0) + if (joint->GetTransformCount() == 0) continue; - REQUIRE(joint->joint->GetTransformCount() == 1, "joint has single transform"); + REQUIRE(joint->GetTransformCount() == 1, "joint has single transform"); - FCDTransform* transform = joint->joint->GetTransform(0); + FCDTransform* transform = joint->GetTransform(0); // Skip unanimated joints again. (TODO: Which of these happens in practice?) if (! transform->IsAnimated()) @@ -111,6 +112,7 @@ public: // 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(); @@ -124,18 +126,47 @@ public: std::vector frameBoneTransforms (boneCount, boneDefault); // Move the model into the new animated pose - for (size_t i = 0; i < skin->GetJointCount(); ++i) + for (size_t i = 0; i < controllerInstance->GetJointCount(); ++i) { - FCDTransform* transform = skin->GetJoint(i)->joint->GetTransform(0); + FCDSceneNode* joint = controllerInstance->GetJoint(i); + + int boneId = StdSkeletons::FindStandardBoneID(joint->GetName()); + if (boneId < 0) + continue; // already emitted a warning earlier + + FCDTransform* transform = joint->GetTransform(0); FCDAnimated* anim = transform->GetAnimated(); anim->Evaluate(time); } + // As well as the joints, we need to update all the ancestors + // of the skeleton (e.g. the Bip01 node, since the skeleton is + // hanging off Bip01-Pelvis instead). + // So choose an arbitrary joint, which is hopefully actually the + // top-most joint but it doesn't really matter, and evaluate all + // its ancestors; + if (controllerInstance->GetJointCount() >= 1) + { + FCDSceneNode* node = controllerInstance->GetJoint(0); + while (node->GetParentCount() == 1) // (I guess this should be true in sensible models) + { + node = node->GetParent(); + if (node->IsJoint() && node->GetTransformCount() == 1) + node->GetTransform(0)->GetAnimated()->Evaluate(time); + else + break; + } + } // Convert the pose into the form require by the game - for (size_t i = 0; i < skin->GetJointCount(); ++i) + for (size_t i = 0; i < controllerInstance->GetJointCount(); ++i) { - FCDSceneNode* jointNode = skin->GetJoint(i)->joint; - FMMatrix44 worldTransform = jointNode->CalculateWorldTransform(); + FCDSceneNode* joint = controllerInstance->GetJoint(i); + + int boneId = StdSkeletons::FindStandardBoneID(joint->GetName()); + if (boneId < 0) + continue; // already emitted a warning earlier + + FMMatrix44 worldTransform = joint->CalculateWorldTransform(); HMatrix matrix; memcpy(matrix, worldTransform.Transposed().m, sizeof(matrix)); @@ -148,13 +179,12 @@ public: { parts.q.x, parts.q.y, parts.q.z, parts.q.w } }; - int boneId = StdSkeletons::FindStandardBoneID(jointNode->GetName()); - REQUIRE(boneId >= 0, "recognised bone name"); frameBoneTransforms[boneId] = b; } // Push frameBoneTransforms onto the back of boneTransforms - copy(frameBoneTransforms.begin(), frameBoneTransforms.end(), inserter(boneTransforms, boneTransforms.end())); + copy(frameBoneTransforms.begin(), frameBoneTransforms.end(), + inserter(boneTransforms, boneTransforms.end())); } // Convert into game's coordinate space diff --git a/source/collada/tests/tests.py b/source/collada/tests/tests.py index 1055022079..fff63befa8 100644 --- a/source/collada/tests/tests.py +++ b/source/collada/tests/tests.py @@ -5,11 +5,17 @@ import xml.etree.ElementTree as ET binaries = '../../../binaries' +# Work out the platform-dependent library filename dll_filename = { 'posix': './libCollada_dbg.so', 'nt': 'Collada_dbg.dll', }[os.name] +# The DLL may need other DLLs which are in its directory, so set the path to that +# (Don't care about clobbering the old PATH - it doesn't have anything important) +os.environ['PATH'] = '%s/system/' % binaries + +# Load the actual library library = cdll.LoadLibrary('%s/system/%s' % (binaries, dll_filename)) def log(severity, message): @@ -19,16 +25,22 @@ clog = CFUNCTYPE(None, c_int, c_char_p)(log) # (the CFUNCTYPE must not be GC'd, so try to keep a reference) library.set_logger(clog) -def convert_dae_to_pmd(filename): +def _convert_dae(func, filename, expected_status=0): output = [] def cb(cbdata, str, len): output.append(string_at(str, len)) cbtype = CFUNCTYPE(None, POINTER(None), POINTER(c_char), c_uint) - status = library.convert_dae_to_pmd(filename, cbtype(cb), None) - assert(status == 0) + status = func(filename, cbtype(cb), None) + assert(status == expected_status) return ''.join(output) +def convert_dae_to_pmd(*args, **kwargs): + return _convert_dae(library.convert_dae_to_pmd, *args, **kwargs) + +def convert_dae_to_psa(*args, **kwargs): + return _convert_dae(library.convert_dae_to_psa, *args, **kwargs) + def clean_dir(path): # Remove all files first try: @@ -43,7 +55,7 @@ def clean_dir(path): except OSError: pass # (ignore errors if it already exists) -def create_actor(mesh, texture, idleanim, corpseanim, gatheranim): +def create_actor(mesh, texture, anims): actor = ET.Element('actor', version='1') ET.SubElement(actor, 'castshadow') group = ET.SubElement(actor, 'group') @@ -51,21 +63,41 @@ def create_actor(mesh, texture, idleanim, corpseanim, gatheranim): ET.SubElement(variant, 'mesh').text = mesh+'.pmd' ET.SubElement(variant, 'texture').text = texture+'.dds' animations = ET.SubElement(variant, 'animations') - ET.SubElement(animations, 'animation', file=idleanim+'.psa', name='Idle', speed='100') - ET.SubElement(animations, 'animation', file=corpseanim+'.psa', name='Corpse', speed='100') - ET.SubElement(animations, 'animation', file=gatheranim+'.psa', name='Build', speed='100') + for name, file in anims: + ET.SubElement(animations, 'animation', file=file+'.psa', name=name, speed='100') return ET.tostring(actor) +def create_actor_static(mesh, texture): + actor = ET.Element('actor', version='1') + ET.SubElement(actor, 'castshadow') + group = ET.SubElement(actor, 'group') + variant = ET.SubElement(group, 'variant', frequency='100', name='Base') + ET.SubElement(variant, 'mesh').text = mesh+'.pmd' + ET.SubElement(variant, 'texture').text = texture+'.dds' + return ET.tostring(actor) + ################################ +# Error handling + +convert_dae_to_pmd('This is not well-formed XML', expected_status=-2) + +convert_dae_to_pmd('This is not COLLADA', expected_status=-2) + +convert_dae_to_pmd('This is still not valid COLLADA', expected_status=-2) + +# Do some real conversions, so the output can be tested in the Actor Viewer + test_data = binaries + '/data/tests/collada' test_mod = binaries + '/data/mods/_test.collada' clean_dir(test_mod + '/art/meshes') clean_dir(test_mod + '/art/actors') +clean_dir(test_mod + '/art/animation') + +for test_file in ['cube', 'jav2', 'jav2b', 'teapot_basic', 'teapot_skin', 'plane_skin', 'dude_skin', 'mergenonbone', 'densemesh']: + print "* Converting PMD %s" % (test_file) -#for test_file in ['cube', 'jav2', 'teapot_basic', 'teapot_skin', 'plane_skin', 'dude_skin']: -for test_file in ['sphere']: input_filename = '%s/%s.dae' % (test_data, test_file) output_filename = '%s/art/meshes/%s.pmd' % (test_mod, test_file) @@ -73,5 +105,19 @@ for test_file in ['sphere']: output = convert_dae_to_pmd(input) open(output_filename, 'wb').write(output) - xml = create_actor(test_file, 'male', 'dudeidle', 'dudecorpse', 'dudechop') + xml = create_actor(test_file, 'male', [('Idle','dudeidle'),('Corpse','dudecorpse'),('Melee','jav2b')]) open('%s/art/actors/%s.xml' % (test_mod, test_file), 'w').write(xml) + + xml = create_actor_static(test_file, 'male') + open('%s/art/actors/%s_static.xml' % (test_mod, test_file), 'w').write(xml) + +for test_file in ['jav2b']: + print "* Converting PSA %s" % (test_file) + + input_filename = '%s/%s.dae' % (test_data, test_file) + output_filename = '%s/art/animation/%s.psa' % (test_mod, test_file) + + input = open(input_filename).read() + output = convert_dae_to_psa(input) + open(output_filename, 'wb').write(output) +