Browse Source

Loading FBX from AtomicTool

Josh Engebretson 10 years ago
parent
commit
4732c85d39

+ 1 - 5
Source/ToolCore/CMakeLists.txt

@@ -15,8 +15,4 @@ file (GLOB_RECURSE SOURCE_FILES *.cpp *.h)
 
 add_library(ToolCore ${SOURCE_FILES})
 
-if (LINUX)
-
-target_link_libraries(ToolCore Poco)
-
-endif()
+target_link_libraries(ToolCore Assimp Poco)

+ 30 - 17
Source/ToolCore/Command/ImportCmd.cpp

@@ -8,6 +8,7 @@
 
 #include "../Import/JSONSceneImporter.h"
 #include "../Import/JSONSceneProcess.h"
+#include "../Import/OpenAssetImporter.h"
 
 #include "ImportCmd.h"
 
@@ -43,37 +44,49 @@ bool ImportCmd::Parse(const Vector<String>& arguments, unsigned startIndex, Stri
         return false;
     }
 
-    sourceJSONFilename_ = value;
+    assetFilename_ = value;
 
     return true;
 }
 
 void ImportCmd::Run()
 {
+    //ToolSystem* tsystem = GetSubsystem<ToolSystem>();
+    //Project* project = tsystem->GetProject();
+    //String resourcePath = project->GetResourcePath();
 
-    Poco::File file(sourceJSONFilename_.CString());
+    String ext = GetExtension(assetFilename_);
 
-    if (!file.exists())
+    if (ext == ".json")
     {
-        Error(ToString("JSON source scene does not exist: %s", sourceJSONFilename_.CString()));
-        return;
-    }
+        Poco::File file(assetFilename_.CString());
+
+        if (!file.exists())
+        {
+            Error(ToString("JSON source scene does not exist: %s", assetFilename_.CString()));
+            return;
+        }
+
 
-    ToolSystem* tsystem = GetSubsystem<ToolSystem>();
-    Project* project = tsystem->GetProject();
+        LOGRAWF("Importing JSON: %s", assetFilename_.CString());
 
-    String resourcePath = project->GetResourcePath();
+        SharedPtr<JSONSceneImporter> jimporter;
+        jimporter = new JSONSceneImporter(context_);
+        jimporter->Import(assetFilename_);
 
-    LOGRAWF("Importing: %s", sourceJSONFilename_.CString());
+        SharedPtr<JSONSceneProcess> sceneProcess;
+        sceneProcess = new JSONSceneProcess(context_, jimporter);
+        //sceneProcess->Process(resourcePath);
+        //sceneProcess->Write();
+    }
+    else
+    {
+        SharedPtr<OpenAssetImporter> importer(new OpenAssetImporter(context_));
 
-    SharedPtr<JSONSceneImporter> jimporter;
-    jimporter = new JSONSceneImporter(context_);
-    jimporter->Import(sourceJSONFilename_);
+        importer->SetVerboseLog(true);
+        importer->Load(assetFilename_);
 
-    SharedPtr<JSONSceneProcess> sceneProcess;
-    sceneProcess = new JSONSceneProcess(context_, jimporter);
-    sceneProcess->Process(resourcePath);
-    sceneProcess->Write();
+    }
 
     Finished();
 }

+ 3 - 2
Source/ToolCore/Command/ImportCmd.h

@@ -18,12 +18,13 @@ public:
     virtual ~ImportCmd();
 
     bool Parse(const Vector<String>& arguments, unsigned startIndex, String& errorMsg);
-
     void Run();
 
+    bool RequiresProjectLoad() { return false; }
+
 private:
 
-    String sourceJSONFilename_;
+    String assetFilename_;
 
 };
 

+ 615 - 0
Source/ToolCore/Import/OpenAssetImporter.cpp

@@ -0,0 +1,615 @@
+//
+// Copyright (c) 2008-2015 the Atomic project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+// Copyright (c) 2014-2015, THUNDERBEAST GAMES LLC All rights reserved
+// Please see LICENSE.md in repository root for license information
+// https://github.com/AtomicGameEngine/AtomicGameEngine
+
+#include <Atomic/Core/ProcessUtils.h>
+#include <Atomic/IO/Log.h>
+#include <Atomic/IO/File.h>
+#include <Atomic/IO/FileSystem.h>
+
+#include <Atomic/Atomic3D/AnimatedModel.h>
+#include <Atomic/Atomic3D/Animation.h>
+
+#include "OpenAssetImporter.h"
+
+namespace ToolCore
+{
+
+OpenAssetImporter::OpenAssetImporter(Context* context) : Object(context) ,
+    scene_(0),
+    rootNode_(0),
+    useSubdirs_(true),
+    localIDs_(false),
+    saveBinary_(false),
+    createZone_(true),
+    noAnimations_(false),
+    noHierarchy_(false),
+    noMaterials_(false),
+    noTextures_(false),
+    noMaterialDiffuseColor_(false),
+    noEmptyNodes_(false),
+    saveMaterialList_(false),
+    includeNonSkinningBones_(false),
+    verboseLog_(false),
+    emissiveAO_(false),
+    noOverwriteMaterial_(false),
+    noOverwriteTexture_(false),
+    noOverwriteNewerTexture_(false),
+    checkUniqueModel_(true),
+    defaultTicksPerSecond_(4800.0f)
+{
+
+    aiFlagsDefault_ =
+        aiProcess_ConvertToLeftHanded |
+        aiProcess_JoinIdenticalVertices |
+        aiProcess_Triangulate |
+        aiProcess_GenSmoothNormals |
+        aiProcess_LimitBoneWeights |
+        aiProcess_ImproveCacheLocality |
+        aiProcess_RemoveRedundantMaterials |
+        aiProcess_FixInfacingNormals |
+        aiProcess_FindInvalidData |
+        aiProcess_GenUVCoords |
+        aiProcess_FindInstances |
+        aiProcess_OptimizeMeshes;
+
+    aiCurrentFlags_ = aiFlagsDefault_;
+
+}
+
+bool OpenAssetImporter::Load(const String &assetPath)
+{
+    if (verboseLog_)
+        Assimp::DefaultLogger::create("", Assimp::Logger::VERBOSE, aiDefaultLogStream_STDOUT);
+
+    PrintLine("Reading file " + assetPath);
+
+    scene_ = aiImportFile(GetNativePath(assetPath).CString(), aiCurrentFlags_);
+
+    if (!scene_)
+        ErrorExit("Could not open or parse input file " + assetPath + ": " + String(aiGetErrorString()));
+
+    if (verboseLog_)
+        Assimp::DefaultLogger::kill();
+
+    return true;
+
+}
+
+String OpenAssetImporter::GetMeshMaterialName(aiMesh* mesh)
+{
+    aiMaterial* material = scene_->mMaterials[mesh->mMaterialIndex];
+    aiString matNameStr;
+    material->Get(AI_MATKEY_NAME, matNameStr);
+    String matName = SanitateAssetName(FromAIString(matNameStr));
+    if (matName.Trimmed().Empty())
+        matName = GenerateMaterialName(material);
+
+    return (useSubdirs_ ? "Materials/" : "") + matName + ".xml";
+}
+
+String OpenAssetImporter::GenerateMaterialName(aiMaterial* material)
+{
+    for (unsigned i = 0; i < scene_->mNumMaterials; ++i)
+    {
+        if (scene_->mMaterials[i] == material)
+            return inputName_ + "_Material" + String(i);
+    }
+
+    // Should not go here
+    return String::EMPTY;
+}
+
+String OpenAssetImporter::GetMaterialTextureName(const String& nameIn)
+{
+    // Detect assimp embedded texture
+    if (nameIn.Length() && nameIn[0] == '*')
+        return GenerateTextureName(ToInt(nameIn.Substring(1)));
+    else
+        return (useSubdirs_ ? "Textures/" : "") + nameIn;
+}
+
+String OpenAssetImporter::GenerateTextureName(unsigned texIndex)
+{
+    if (texIndex < scene_->mNumTextures)
+    {
+        // If embedded texture contains encoded data, use the format hint for file extension. Else save RGBA8 data as PNG
+        aiTexture* tex = scene_->mTextures[texIndex];
+        if (!tex->mHeight)
+            return (useSubdirs_ ? "Textures/" : "") + inputName_ + "_Texture" + String(texIndex) + "." + tex->achFormatHint;
+        else
+            return (useSubdirs_ ? "Textures/" : "") + inputName_ + "_Texture" + String(texIndex) + ".png";
+    }
+
+    // Should not go here
+    return String::EMPTY;
+}
+
+void OpenAssetImporter::CollectSceneModels(OutScene& scene, aiNode* node)
+{
+    Vector<Pair<aiNode*, aiMesh*> > meshes;
+    GetMeshesUnderNode(scene_, meshes, node);
+
+    if (meshes.Size())
+    {
+        OutModel model;
+        model.rootNode_ = node;
+        model.outName_ = resourcePath_ + (useSubdirs_ ? "Models/" : "") + SanitateAssetName(FromAIString(node->mName)) + ".mdl";
+        for (unsigned i = 0; i < meshes.Size(); ++i)
+        {
+            aiMesh* mesh = meshes[i].second_;
+            unsigned meshIndex = GetMeshIndex(scene_, mesh);
+            model.meshIndices_.Insert(meshIndex);
+            model.meshes_.Push(mesh);
+            model.meshNodes_.Push(meshes[i].first_);
+            model.totalVertices_ += mesh->mNumVertices;
+            model.totalIndices_ += GetNumValidFaces(mesh) * 3;
+        }
+
+        // Check if a model with identical mesh indices already exists. If yes, do not export twice
+        bool unique = true;
+        if (checkUniqueModel_)
+        {
+            for (unsigned i = 0; i < scene.models_.Size(); ++i)
+            {
+                if (scene.models_[i].meshIndices_ == model.meshIndices_)
+                {
+                    PrintLine("Added node " + FromAIString(node->mName));
+                    scene.nodes_.Push(node);
+                    scene.nodeModelIndices_.Push(i);
+                    unique = false;
+                    break;
+                }
+            }
+        }
+        if (unique)
+        {
+            PrintLine("Added model " + model.outName_);
+            PrintLine("Added node " + FromAIString(node->mName));
+            CollectBones(model);
+            BuildBoneCollisionInfo(model);
+            if (!noAnimations_)
+            {
+                CollectAnimations(&model);
+                BuildAndSaveAnimations(&model);
+            }
+
+            scene.models_.Push(model);
+            scene.nodes_.Push(node);
+            scene.nodeModelIndices_.Push(scene.models_.Size() - 1);
+        }
+    }
+
+    for (unsigned i = 0; i < node->mNumChildren; ++i)
+        CollectSceneModels(scene, node->mChildren[i]);
+}
+
+void OpenAssetImporter::CollectBones(OutModel& model, bool animationOnly)
+{
+    HashSet<aiNode*> necessary;
+    HashSet<aiNode*> rootNodes;
+
+    for (unsigned i = 0; i < model.meshes_.Size(); ++i)
+    {
+        aiMesh* mesh = model.meshes_[i];
+        aiNode* meshNode = model.meshNodes_[i];
+        aiNode* meshParentNode = meshNode->mParent;
+        aiNode* rootNode = 0;
+
+        for (unsigned j = 0; j < mesh->mNumBones; ++j)
+        {
+            aiBone* bone = mesh->mBones[j];
+            String boneName(FromAIString(bone->mName));
+            aiNode* boneNode = GetNode(boneName, scene_->mRootNode, true);
+            if (!boneNode)
+                ErrorExit("Could not find scene node for bone " + boneName);
+            necessary.Insert(boneNode);
+            rootNode = boneNode;
+
+            for (;;)
+            {
+                boneNode = boneNode->mParent;
+                if (!boneNode || ((boneNode == meshNode || boneNode == meshParentNode) && !animationOnly))
+                    break;
+                rootNode = boneNode;
+                necessary.Insert(boneNode);
+            }
+
+            if (rootNodes.Find(rootNode) == rootNodes.End())
+                rootNodes.Insert(rootNode);
+        }
+    }
+
+    // If we find multiple root nodes, try to remedy by using their parent instead
+    if (rootNodes.Size() > 1)
+    {
+        aiNode* commonParent = (*rootNodes.Begin())->mParent;
+        for (HashSet<aiNode*>::Iterator i = rootNodes.Begin(); i != rootNodes.End(); ++i)
+        {
+            if (*i != commonParent)
+            {
+                if (!commonParent || (*i)->mParent != commonParent)
+                    ErrorExit("Skeleton with multiple root nodes found, not supported");
+            }
+        }
+        rootNodes.Clear();
+        rootNodes.Insert(commonParent);
+        necessary.Insert(commonParent);
+    }
+
+    if (rootNodes.Empty())
+        return;
+
+    model.rootBone_ = *rootNodes.Begin();
+    CollectBonesFinal(model.bones_, necessary, model.rootBone_);
+    // Initialize the bone collision info
+    model.boneRadii_.Resize(model.bones_.Size());
+    model.boneHitboxes_.Resize(model.bones_.Size());
+    for (unsigned i = 0; i < model.bones_.Size(); ++i)
+    {
+        model.boneRadii_[i] = 0.0f;
+        model.boneHitboxes_[i] = BoundingBox(0.0f, 0.0f);
+    }
+}
+
+void OpenAssetImporter::CollectBonesFinal(PODVector<aiNode*>& dest, const HashSet<aiNode*>& necessary, aiNode* node)
+{
+    bool includeBone = necessary.Find(node) != necessary.End();
+    String boneName = FromAIString(node->mName);
+
+    // Check include/exclude filters for non-skinned bones
+    if (!includeBone && includeNonSkinningBones_)
+    {
+        // If no includes specified, include by default but check for excludes
+        if (nonSkinningBoneIncludes_.Empty())
+            includeBone = true;
+
+        // Check against includes/excludes
+        for (unsigned i = 0; i < nonSkinningBoneIncludes_.Size(); ++i)
+        {
+            if (boneName.Contains(nonSkinningBoneIncludes_[i], false))
+            {
+                includeBone = true;
+                break;
+            }
+        }
+        for (unsigned i = 0; i < nonSkinningBoneExcludes_.Size(); ++i)
+        {
+            if (boneName.Contains(nonSkinningBoneExcludes_[i], false))
+            {
+                includeBone = false;
+                break;
+            }
+        }
+
+        if (includeBone)
+            PrintLine("Including non-skinning bone " + boneName);
+    }
+
+    if (includeBone)
+        dest.Push(node);
+
+    for (unsigned i = 0; i < node->mNumChildren; ++i)
+        CollectBonesFinal(dest, necessary, node->mChildren[i]);
+}
+
+void OpenAssetImporter::CollectAnimations(OutModel* model)
+{
+    const aiScene* scene = scene_;
+    for (unsigned i = 0; i < scene->mNumAnimations; ++i)
+    {
+        aiAnimation* anim = scene->mAnimations[i];
+        if (allAnimations_.Contains(anim))
+            continue;
+
+        if (model)
+        {
+            bool modelBoneFound = false;
+            for (unsigned j = 0; j < anim->mNumChannels; ++j)
+            {
+                aiNodeAnim* channel = anim->mChannels[j];
+                String channelName = FromAIString(channel->mNodeName);
+                if (GetBoneIndex(*model, channelName) != M_MAX_UNSIGNED)
+                {
+                    modelBoneFound = true;
+                    break;
+                }
+            }
+            if (modelBoneFound)
+            {
+                model->animations_.Push(anim);
+                allAnimations_.Insert(anim);
+            }
+        }
+        else
+        {
+            sceneAnimations_.Push(anim);
+            allAnimations_.Insert(anim);
+        }
+    }
+
+    /// \todo Vertex morphs are ignored for now
+}
+
+void OpenAssetImporter::BuildBoneCollisionInfo(OutModel& model)
+{
+    for (unsigned i = 0; i < model.meshes_.Size(); ++i)
+    {
+        aiMesh* mesh = model.meshes_[i];
+        for (unsigned j = 0; j < mesh->mNumBones; ++j)
+        {
+            aiBone* bone = mesh->mBones[j];
+            String boneName = FromAIString(bone->mName);
+            unsigned boneIndex = GetBoneIndex(model, boneName);
+            if (boneIndex == M_MAX_UNSIGNED)
+                continue;
+            for (unsigned k = 0; k < bone->mNumWeights; ++k)
+            {
+                float weight = bone->mWeights[k].mWeight;
+                // Require skinning weight to be sufficiently large before vertex contributes to bone hitbox
+                if (weight > 0.33f)
+                {
+                    aiVector3D vertexBoneSpace = bone->mOffsetMatrix * mesh->mVertices[bone->mWeights[k].mVertexId];
+                    Vector3 vertex = ToVector3(vertexBoneSpace);
+                    float radius = vertex.Length();
+                    if (radius > model.boneRadii_[boneIndex])
+                        model.boneRadii_[boneIndex] = radius;
+                    model.boneHitboxes_[boneIndex].Merge(vertex);
+                }
+            }
+        }
+    }
+}
+
+void OpenAssetImporter::BuildAndSaveAnimations(OutModel* model)
+{
+    const PODVector<aiAnimation*>& animations = model ? model->animations_ : sceneAnimations_;
+
+    for (unsigned i = 0; i < animations.Size(); ++i)
+    {
+        aiAnimation* anim = animations[i];
+
+        float duration = (float)anim->mDuration;
+        String animName = FromAIString(anim->mName);
+        String animOutName;
+
+        if (animName.Empty())
+            animName = "Anim" + String(i + 1);
+        if (model)
+            animOutName = GetPath(model->outName_) + GetFileName(model->outName_) + "_" + SanitateAssetName(animName) + ".ani";
+        else
+            animOutName = outPath_ + SanitateAssetName(animName) + ".ani";
+
+        float ticksPerSecond = (float)anim->mTicksPerSecond;
+        // If ticks per second not specified, it's probably a .X file. In this case use the default tick rate
+        if (ticksPerSecond < M_EPSILON)
+            ticksPerSecond = defaultTicksPerSecond_;
+        float tickConversion = 1.0f / ticksPerSecond;
+
+        // Find out the start time of animation from each channel's first keyframe for adjusting the keyframe times
+        // to start from zero
+        float startTime = duration;
+        for (unsigned j = 0; j < anim->mNumChannels; ++j)
+        {
+            aiNodeAnim* channel = anim->mChannels[j];
+            if (channel->mNumPositionKeys > 0)
+                startTime = Min(startTime, (float)channel->mPositionKeys[0].mTime);
+            if (channel->mNumRotationKeys > 0)
+                startTime = Min(startTime, (float)channel->mRotationKeys[0].mTime);
+            if (channel->mScalingKeys > 0)
+                startTime = Min(startTime, (float)channel->mScalingKeys[0].mTime);
+        }
+        duration -= startTime;
+
+        SharedPtr<Animation> outAnim(new Animation(context_));
+        outAnim->SetAnimationName(animName);
+        outAnim->SetLength(duration * tickConversion);
+
+        PrintLine("Writing animation " + animName + " length " + String(outAnim->GetLength()));
+        Vector<AnimationTrack> tracks;
+        for (unsigned j = 0; j < anim->mNumChannels; ++j)
+        {
+            aiNodeAnim* channel = anim->mChannels[j];
+            String channelName = FromAIString(channel->mNodeName);
+            aiNode* boneNode = 0;
+            bool isRootBone = false;
+
+            if (model)
+            {
+                unsigned boneIndex = GetBoneIndex(*model, channelName);
+                if (boneIndex == M_MAX_UNSIGNED)
+                {
+                    PrintLine("Warning: skipping animation track " + channelName + " not found in model skeleton");
+                    continue;
+                }
+                boneNode = model->bones_[boneIndex];
+                isRootBone = boneIndex == 0;
+            }
+            else
+            {
+                boneNode = GetNode(channelName, scene_->mRootNode);
+                if (!boneNode)
+                {
+                    PrintLine("Warning: skipping animation track " + channelName + " whose scene node was not found");
+                    continue;
+                }
+            }
+
+            // To export single frame animation, check if first key frame is identical to bone transformation
+            aiVector3D bonePos, boneScale;
+            aiQuaternion boneRot;
+            boneNode->mTransformation.Decompose(boneScale, boneRot, bonePos);
+
+            bool posEqual = true;
+            bool scaleEqual = true;
+            bool rotEqual = true;
+
+            if (channel->mNumPositionKeys > 0 && !ToVector3(bonePos).Equals(ToVector3(channel->mPositionKeys[0].mValue)))
+                posEqual = false;
+            if (channel->mNumScalingKeys > 0 && !ToVector3(boneScale).Equals(ToVector3(channel->mScalingKeys[0].mValue)))
+                scaleEqual = false;
+            if (channel->mNumRotationKeys > 0 && !ToQuaternion(boneRot).Equals(ToQuaternion(channel->mRotationKeys[0].mValue)))
+                rotEqual = false;
+
+            AnimationTrack track;
+            track.name_ = channelName;
+            track.nameHash_ = channelName;
+
+            // Check which channels are used
+            track.channelMask_ = 0;
+            if (channel->mNumPositionKeys > 1 || !posEqual)
+                track.channelMask_ |= CHANNEL_POSITION;
+            if (channel->mNumRotationKeys > 1 || !rotEqual)
+                track.channelMask_ |= CHANNEL_ROTATION;
+            if (channel->mNumScalingKeys > 1 || !scaleEqual)
+                track.channelMask_ |= CHANNEL_SCALE;
+            // Check for redundant identity scale in all keyframes and remove in that case
+            if (track.channelMask_ & CHANNEL_SCALE)
+            {
+                bool redundantScale = true;
+                for (unsigned k = 0; k < channel->mNumScalingKeys; ++k)
+                {
+                    float SCALE_EPSILON = 0.000001f;
+                    Vector3 scaleVec = ToVector3(channel->mScalingKeys[k].mValue);
+                    if (fabsf(scaleVec.x_ - 1.0f) >= SCALE_EPSILON || fabsf(scaleVec.y_ - 1.0f) >= SCALE_EPSILON ||
+                            fabsf(scaleVec.z_ - 1.0f) >= SCALE_EPSILON)
+                    {
+                        redundantScale = false;
+                        break;
+                    }
+                }
+                if (redundantScale)
+                    track.channelMask_ &= ~CHANNEL_SCALE;
+            }
+
+            if (!track.channelMask_)
+                PrintLine("Warning: skipping animation track " + channelName + " with no keyframes");
+
+            // Currently only same amount of keyframes is supported
+            // Note: should also check the times of individual keyframes for match
+            if ((channel->mNumPositionKeys > 1 && channel->mNumRotationKeys > 1 && channel->mNumPositionKeys != channel->mNumRotationKeys) ||
+                    (channel->mNumPositionKeys > 1 && channel->mNumScalingKeys > 1 && channel->mNumPositionKeys != channel->mNumScalingKeys) ||
+                    (channel->mNumRotationKeys > 1 && channel->mNumScalingKeys > 1 && channel->mNumRotationKeys != channel->mNumScalingKeys))
+            {
+                PrintLine("Warning: differing amounts of channel keyframes, skipping animation track " + channelName);
+                continue;
+            }
+
+            unsigned keyFrames = channel->mNumPositionKeys;
+            if (channel->mNumRotationKeys > keyFrames)
+                keyFrames = channel->mNumRotationKeys;
+            if (channel->mNumScalingKeys > keyFrames)
+                keyFrames = channel->mNumScalingKeys;
+
+            for (unsigned k = 0; k < keyFrames; ++k)
+            {
+                AnimationKeyFrame kf;
+                kf.time_ = 0.0f;
+                kf.position_ = Vector3::ZERO;
+                kf.rotation_ = Quaternion::IDENTITY;
+                kf.scale_ = Vector3::ONE;
+
+                // Get time for the keyframe. Adjust with animation's start time
+                if (track.channelMask_ & CHANNEL_POSITION && k < channel->mNumPositionKeys)
+                    kf.time_ = ((float)channel->mPositionKeys[k].mTime - startTime) * tickConversion;
+                else if (track.channelMask_ & CHANNEL_ROTATION && k < channel->mNumRotationKeys)
+                    kf.time_ = ((float)channel->mRotationKeys[k].mTime - startTime) * tickConversion;
+                else if (track.channelMask_ & CHANNEL_SCALE && k < channel->mNumScalingKeys)
+                    kf.time_ = ((float)channel->mScalingKeys[k].mTime - startTime) * tickConversion;
+
+                // Make sure time stays positive
+                kf.time_ = Max(kf.time_, 0.0f);
+
+                // Start with the bone's base transform
+                aiMatrix4x4 boneTransform = boneNode->mTransformation;
+                aiVector3D pos, scale;
+                aiQuaternion rot;
+                boneTransform.Decompose(scale, rot, pos);
+                // Then apply the active channels
+                if (track.channelMask_ & CHANNEL_POSITION && k < channel->mNumPositionKeys)
+                    pos = channel->mPositionKeys[k].mValue;
+                if (track.channelMask_ & CHANNEL_ROTATION && k < channel->mNumRotationKeys)
+                    rot = channel->mRotationKeys[k].mValue;
+                if (track.channelMask_ & CHANNEL_SCALE && k < channel->mNumScalingKeys)
+                    scale = channel->mScalingKeys[k].mValue;
+
+                // If root bone, transform with the model root node transform
+                if (model && isRootBone)
+                {
+                    aiMatrix4x4 transMat, scaleMat, rotMat;
+                    aiMatrix4x4::Translation(pos, transMat);
+                    aiMatrix4x4::Scaling(scale, scaleMat);
+                    rotMat = aiMatrix4x4(rot.GetMatrix());
+                    aiMatrix4x4 tform = transMat * rotMat * scaleMat;
+                    tform = GetDerivedTransform(tform, boneNode, model->rootNode_);
+                    tform.Decompose(scale, rot, pos);
+                }
+
+                if (track.channelMask_ & CHANNEL_POSITION)
+                    kf.position_ = ToVector3(pos);
+                if (track.channelMask_ & CHANNEL_ROTATION)
+                    kf.rotation_ = ToQuaternion(rot);
+                if (track.channelMask_ & CHANNEL_SCALE)
+                    kf.scale_ = ToVector3(scale);
+
+                track.keyFrames_.Push(kf);
+            }
+
+            tracks.Push(track);
+        }
+
+        outAnim->SetTracks(tracks);
+
+        File outFile(context_);
+        if (!outFile.Open(animOutName, FILE_WRITE))
+            ErrorExit("Could not open output file " + animOutName);
+        outAnim->Save(outFile);
+    }
+}
+
+void OpenAssetImporter::DumpNodes(aiNode* rootNode, unsigned level)
+{
+    if (!rootNode)
+        return;
+
+    String indent(' ', level * 2);
+    Vector3 pos, scale;
+    Quaternion rot;
+    aiMatrix4x4 transform = GetDerivedTransform(rootNode, rootNode_);
+    GetPosRotScale(transform, pos, rot, scale);
+
+    PrintLine(indent + "Node " + FromAIString(rootNode->mName) + " pos " + String(pos));
+
+    if (rootNode->mNumMeshes == 1)
+        PrintLine(indent + "  " + String(rootNode->mNumMeshes) + " geometry");
+    if (rootNode->mNumMeshes > 1)
+        PrintLine(indent + "  " + String(rootNode->mNumMeshes) + " geometries");
+
+    for (unsigned i = 0; i < rootNode->mNumChildren; ++i)
+        DumpNodes(rootNode->mChildren[i], level + 1);
+}
+
+}
+

+ 106 - 0
Source/ToolCore/Import/OpenAssetImporter.h

@@ -0,0 +1,106 @@
+//
+// Copyright (c) 2008-2015 the Atomic project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+// Copyright (c) 2014-2015, THUNDERBEAST GAMES LLC All rights reserved
+// Please see LICENSE.md in repository root for license information
+// https://github.com/AtomicGameEngine/AtomicGameEngine
+
+#pragma once
+
+#include "OpenAssetUtils.h"
+
+#include <Atomic/Core/Object.h>
+
+using namespace Atomic;
+
+namespace ToolCore
+{
+
+class OpenAssetImporter : public Object
+{
+    OBJECT(OpenAssetImporter);
+
+public:
+
+    OpenAssetImporter(Context* context);
+    virtual ~OpenAssetImporter() {}
+
+    bool Load(const String& assetPath);
+
+    void SetVerboseLog(bool verboseLog) { verboseLog_ = verboseLog; }
+
+private:
+
+    void BuildAndSaveAnimations(OutModel* model);
+
+    void CollectSceneModels(OutScene& scene, aiNode* node);
+    void CollectBones(OutModel& model, bool animationOnly = false);
+    void CollectBonesFinal(PODVector<aiNode*>& dest, const HashSet<aiNode*>& necessary, aiNode* node);
+    void BuildBoneCollisionInfo(OutModel& model);
+    void CollectAnimations(OutModel* model);
+
+    String GetMeshMaterialName(aiMesh* mesh);
+    String GenerateMaterialName(aiMaterial* material);
+    String GetMaterialTextureName(const String& nameIn);
+    String GenerateTextureName(unsigned texIndex);
+
+    void DumpNodes(aiNode* rootNode, unsigned level);
+
+    const aiScene* scene_;
+    aiNode* rootNode_;
+
+    String inputName_;
+    String resourcePath_;
+    String outPath_;
+    bool useSubdirs_;
+    bool localIDs_;
+    bool saveBinary_;
+    bool createZone_;
+    bool noAnimations_;
+    bool noHierarchy_;
+    bool noMaterials_;
+    bool noTextures_;
+    bool noMaterialDiffuseColor_;
+    bool noEmptyNodes_;
+    bool saveMaterialList_;
+    bool includeNonSkinningBones_;
+    bool verboseLog_;
+    bool emissiveAO_;
+    bool noOverwriteMaterial_;
+    bool noOverwriteTexture_;
+    bool noOverwriteNewerTexture_;
+    bool checkUniqueModel_;
+
+    unsigned aiFlagsDefault_;
+    unsigned aiCurrentFlags_;
+
+    Vector<String> nonSkinningBoneIncludes_;
+    Vector<String> nonSkinningBoneExcludes_;
+
+    HashSet<aiAnimation*> allAnimations_;
+    PODVector<aiAnimation*> sceneAnimations_;
+
+    float defaultTicksPerSecond_;
+
+};
+
+}

+ 398 - 0
Source/ToolCore/Import/OpenAssetUtils.cpp

@@ -0,0 +1,398 @@
+//
+// Copyright (c) 2008-2015 the Atomic project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include <Atomic/Core/ProcessUtils.h>
+#include <Atomic/Graphics/Geometry.h>
+
+#include "OpenAssetUtils.h"
+
+namespace ToolCore
+{
+
+void CollectMeshes(const aiScene* scene, OutModel& model, aiNode* node)
+{
+    for (unsigned i = 0; i < node->mNumMeshes; ++i)
+    {
+        aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
+        for (unsigned j = 0; j < model.meshes_.Size(); ++j)
+        {
+            if (mesh == model.meshes_[j])
+            {
+                PrintLine("Warning: same mesh found multiple times");
+                break;
+            }
+        }
+
+        model.meshIndices_.Insert(node->mMeshes[i]);
+        model.meshes_.Push(mesh);
+        model.meshNodes_.Push(node);
+        model.totalVertices_ += mesh->mNumVertices;
+        model.totalIndices_ += GetNumValidFaces(mesh) * 3;
+    }
+
+    for (unsigned i = 0; i < node->mNumChildren; ++i)
+        CollectMeshes(scene, model, node->mChildren[i]);
+}
+
+
+void GetMeshesUnderNode(const aiScene* scene, Vector<Pair<aiNode*, aiMesh*> >& dest, aiNode* node)
+{
+    for (unsigned i = 0; i < node->mNumMeshes; ++i)
+        dest.Push(MakePair(node, scene->mMeshes[node->mMeshes[i]]));
+}
+
+unsigned GetMeshIndex(const aiScene* scene, aiMesh* mesh)
+{
+    for (unsigned i = 0; i < scene->mNumMeshes; ++i)
+    {
+        if (scene->mMeshes[i] == mesh)
+            return i;
+    }
+    return M_MAX_UNSIGNED;
+}
+
+unsigned GetBoneIndex(OutModel& model, const String& boneName)
+{
+    for (unsigned i = 0; i < model.bones_.Size(); ++i)
+    {
+        if (boneName == model.bones_[i]->mName.data)
+            return i;
+    }
+    return M_MAX_UNSIGNED;
+}
+
+aiBone* GetMeshBone(OutModel& model, const String& boneName)
+{
+    for (unsigned i = 0; i < model.meshes_.Size(); ++i)
+    {
+        aiMesh* mesh = model.meshes_[i];
+        for (unsigned j = 0; j < mesh->mNumBones; ++j)
+        {
+            aiBone* bone = mesh->mBones[j];
+            if (boneName == bone->mName.data)
+                return bone;
+        }
+    }
+    return 0;
+}
+
+Matrix3x4 GetOffsetMatrix(OutModel& model, const String& boneName)
+{
+    for (unsigned i = 0; i < model.meshes_.Size(); ++i)
+    {
+        aiMesh* mesh = model.meshes_[i];
+        aiNode* node = model.meshNodes_[i];
+        for (unsigned j = 0; j < mesh->mNumBones; ++j)
+        {
+            aiBone* bone = mesh->mBones[j];
+            if (boneName == bone->mName.data)
+            {
+                aiMatrix4x4 offset = bone->mOffsetMatrix;
+                aiMatrix4x4 nodeDerivedInverse = GetMeshBakingTransform(node, model.rootNode_);
+                nodeDerivedInverse.Inverse();
+                offset *= nodeDerivedInverse;
+                return ToMatrix3x4(offset);
+            }
+        }
+    }
+    return Matrix3x4::IDENTITY;
+}
+
+
+unsigned GetNumValidFaces(aiMesh* mesh)
+{
+    unsigned ret = 0;
+
+    for (unsigned j = 0; j < mesh->mNumFaces; ++j)
+    {
+        if (mesh->mFaces[j].mNumIndices == 3)
+            ++ret;
+    }
+
+    return ret;
+}
+
+void WriteShortIndices(unsigned short*& dest, aiMesh* mesh, unsigned index, unsigned offset)
+{
+    if (mesh->mFaces[index].mNumIndices == 3)
+    {
+        *dest++ = mesh->mFaces[index].mIndices[0] + offset;
+        *dest++ = mesh->mFaces[index].mIndices[1] + offset;
+        *dest++ = mesh->mFaces[index].mIndices[2] + offset;
+    }
+}
+
+void WriteLargeIndices(unsigned*& dest, aiMesh* mesh, unsigned index, unsigned offset)
+{
+    if (mesh->mFaces[index].mNumIndices == 3)
+    {
+        *dest++ = mesh->mFaces[index].mIndices[0] + offset;
+        *dest++ = mesh->mFaces[index].mIndices[1] + offset;
+        *dest++ = mesh->mFaces[index].mIndices[2] + offset;
+    }
+}
+
+void WriteVertex(float*& dest, aiMesh* mesh, unsigned index, unsigned elementMask, BoundingBox& box,
+    const Matrix3x4& vertexTransform, const Matrix3& normalTransform, Vector<PODVector<unsigned char> >& blendIndices,
+    Vector<PODVector<float> >& blendWeights)
+{
+    Vector3 vertex = vertexTransform * ToVector3(mesh->mVertices[index]);
+    box.Merge(vertex);
+    *dest++ = vertex.x_;
+    *dest++ = vertex.y_;
+    *dest++ = vertex.z_;
+    if (elementMask & MASK_NORMAL)
+    {
+        Vector3 normal = normalTransform * ToVector3(mesh->mNormals[index]);
+        *dest++ = normal.x_;
+        *dest++ = normal.y_;
+        *dest++ = normal.z_;
+    }
+    if (elementMask & MASK_COLOR)
+    {
+        *((unsigned*)dest) = Color(mesh->mColors[0][index].r, mesh->mColors[0][index].g, mesh->mColors[0][index].b,
+            mesh->mColors[0][index].a).ToUInt();
+        ++dest;
+    }
+    if (elementMask & MASK_TEXCOORD1)
+    {
+        Vector3 texCoord = ToVector3(mesh->mTextureCoords[0][index]);
+        *dest++ = texCoord.x_;
+        *dest++ = texCoord.y_;
+    }
+    if (elementMask & MASK_TEXCOORD2)
+    {
+        Vector3 texCoord = ToVector3(mesh->mTextureCoords[1][index]);
+        *dest++ = texCoord.x_;
+        *dest++ = texCoord.y_;
+    }
+    if (elementMask & MASK_TANGENT)
+    {
+        Vector3 tangent = normalTransform * ToVector3(mesh->mTangents[index]);
+        Vector3 normal = normalTransform * ToVector3(mesh->mNormals[index]);
+        Vector3 bitangent = normalTransform * ToVector3(mesh->mBitangents[index]);
+        // Check handedness
+        float w = 1.0f;
+        if ((tangent.CrossProduct(normal)).DotProduct(bitangent) < 0.5f)
+            w = -1.0f;
+
+        *dest++ = tangent.x_;
+        *dest++ = tangent.y_;
+        *dest++ = tangent.z_;
+        *dest++ = w;
+    }
+    if (elementMask & MASK_BLENDWEIGHTS)
+    {
+        for (unsigned i = 0; i < 4; ++i)
+        {
+            if (i < blendWeights[index].Size())
+                *dest++ = blendWeights[index][i];
+            else
+                *dest++ = 0.0f;
+        }
+    }
+    if (elementMask & MASK_BLENDINDICES)
+    {
+        unsigned char* destBytes = (unsigned char*)dest;
+        ++dest;
+        for (unsigned i = 0; i < 4; ++i)
+        {
+            if (i < blendIndices[index].Size())
+                *destBytes++ = blendIndices[index][i];
+            else
+                *destBytes++ = 0;
+        }
+    }
+}
+
+unsigned GetElementMask(aiMesh* mesh)
+{
+    unsigned elementMask = MASK_POSITION;
+    if (mesh->HasNormals())
+        elementMask |= MASK_NORMAL;
+    if (mesh->HasTangentsAndBitangents())
+        elementMask |= MASK_TANGENT;
+    if (mesh->GetNumColorChannels() > 0)
+        elementMask |= MASK_COLOR;
+    if (mesh->GetNumUVChannels() > 0)
+        elementMask |= MASK_TEXCOORD1;
+    if (mesh->GetNumUVChannels() > 1)
+        elementMask |= MASK_TEXCOORD2;
+    if (mesh->HasBones())
+        elementMask |= (MASK_BLENDWEIGHTS | MASK_BLENDINDICES);
+    return elementMask;
+}
+
+aiNode* GetNode(const String& name, aiNode* rootNode, bool caseSensitive)
+{
+    if (!rootNode)
+        return 0;
+    if (!name.Compare(rootNode->mName.data, caseSensitive))
+        return rootNode;
+    for (unsigned i = 0; i < rootNode->mNumChildren; ++i)
+    {
+        aiNode* found = GetNode(name, rootNode->mChildren[i], caseSensitive);
+        if (found)
+            return found;
+    }
+    return 0;
+}
+
+aiMatrix4x4 GetDerivedTransform(aiNode* node, aiNode* rootNode, bool rootInclusive)
+{
+    return GetDerivedTransform(node->mTransformation, node, rootNode, rootInclusive);
+}
+
+aiMatrix4x4 GetDerivedTransform(aiMatrix4x4 transform, aiNode* node, aiNode* rootNode, bool rootInclusive)
+{
+    // If basenode is defined, go only up to it in the parent chain
+    while (node && node != rootNode)
+    {
+        node = node->mParent;
+        if (!rootInclusive && node == rootNode)
+            break;
+        if (node)
+            transform = node->mTransformation * transform;
+    }
+    return transform;
+}
+
+aiMatrix4x4 GetMeshBakingTransform(aiNode* meshNode, aiNode* modelRootNode)
+{
+    if (meshNode == modelRootNode)
+        return aiMatrix4x4();
+    else
+        return GetDerivedTransform(meshNode, modelRootNode);
+}
+
+void GetPosRotScale(const aiMatrix4x4& transform, Vector3& pos, Quaternion& rot, Vector3& scale)
+{
+    aiVector3D aiPos;
+    aiQuaternion aiRot;
+    aiVector3D aiScale;
+    transform.Decompose(aiScale, aiRot, aiPos);
+    pos = ToVector3(aiPos);
+    rot = ToQuaternion(aiRot);
+    scale = ToVector3(aiScale);
+}
+
+void GetBlendData(OutModel& model, aiMesh* mesh, PODVector<unsigned>& boneMappings, Vector<PODVector<unsigned char> >&
+    blendIndices, Vector<PODVector<float> >& blendWeights, unsigned maxBones)
+{
+    blendIndices.Resize(mesh->mNumVertices);
+    blendWeights.Resize(mesh->mNumVertices);
+    boneMappings.Clear();
+
+    // If model has more bones than can fit vertex shader parameters, write the per-geometry mappings
+    if (model.bones_.Size() > maxBones)
+    {
+        if (mesh->mNumBones > maxBones)
+        {
+            ErrorExit(
+                "Geometry (submesh) has over " + String(maxBones) + " bone influences. Try splitting to more submeshes\n"
+                "that each stay at " + String(maxBones) + " bones or below."
+            );
+        }
+        boneMappings.Resize(mesh->mNumBones);
+        for (unsigned i = 0; i < mesh->mNumBones; ++i)
+        {
+            aiBone* bone = mesh->mBones[i];
+            String boneName = FromAIString(bone->mName);
+            unsigned globalIndex = GetBoneIndex(model, boneName);
+            if (globalIndex == M_MAX_UNSIGNED)
+                ErrorExit("Bone " + boneName + " not found");
+            boneMappings[i] = globalIndex;
+            for (unsigned j = 0; j < bone->mNumWeights; ++j)
+            {
+                unsigned vertex = bone->mWeights[j].mVertexId;
+                blendIndices[vertex].Push(i);
+                blendWeights[vertex].Push(bone->mWeights[j].mWeight);
+                if (blendWeights[vertex].Size() > 4)
+                    ErrorExit("More than 4 bone influences on vertex");
+            }
+        }
+    }
+    else
+    {
+        for (unsigned i = 0; i < mesh->mNumBones; ++i)
+        {
+            aiBone* bone = mesh->mBones[i];
+            String boneName = FromAIString(bone->mName);
+            unsigned globalIndex = GetBoneIndex(model, boneName);
+            if (globalIndex == M_MAX_UNSIGNED)
+                ErrorExit("Bone " + boneName + " not found");
+            for (unsigned j = 0; j < bone->mNumWeights; ++j)
+            {
+                unsigned vertex = bone->mWeights[j].mVertexId;
+                blendIndices[vertex].Push(globalIndex);
+                blendWeights[vertex].Push(bone->mWeights[j].mWeight);
+                if (blendWeights[vertex].Size() > 4)
+                    ErrorExit("More than 4 bone influences on vertex");
+            }
+        }
+    }
+}
+
+String FromAIString(const aiString& str)
+{
+    return String(str.data);
+}
+
+Vector3 ToVector3(const aiVector3D& vec)
+{
+    return Vector3(vec.x, vec.y, vec.z);
+}
+
+Vector2 ToVector2(const aiVector2D& vec)
+{
+    return Vector2(vec.x, vec.y);
+}
+
+Quaternion ToQuaternion(const aiQuaternion& quat)
+{
+    return Quaternion(quat.w, quat.x, quat.y, quat.z);
+}
+
+Matrix3x4 ToMatrix3x4(const aiMatrix4x4& mat)
+{
+    Matrix3x4 ret;
+    memcpy(&ret.m00_, &mat.a1, sizeof(Matrix3x4));
+    return ret;
+}
+
+String SanitateAssetName(const String& name)
+{
+    String fixedName = name;
+    fixedName.Replace("<", "");
+    fixedName.Replace(">", "");
+    fixedName.Replace("?", "");
+    fixedName.Replace("*", "");
+    fixedName.Replace(":", "");
+    fixedName.Replace("\"", "");
+    fixedName.Replace("/", "");
+    fixedName.Replace("\\", "");
+    fixedName.Replace("|", "");
+
+    return fixedName;
+}
+
+}

+ 113 - 0
Source/ToolCore/Import/OpenAssetUtils.h

@@ -0,0 +1,113 @@
+//
+// Copyright (c) 2008-2015 the Atomic project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include <ThirdParty/Assimp/include/assimp/cimport.h>
+#include <ThirdParty/Assimp/include/assimp/scene.h>
+#include <ThirdParty/Assimp/include/assimp/postprocess.h>
+#include <ThirdParty/Assimp/include/assimp/DefaultLogger.hpp>
+
+#include <cstring>
+
+#include <Atomic/Container/Str.h>
+#include <Atomic/Container/Sort.h>
+#include <Atomic/Container/Hash.h>
+#include <Atomic/Container/HashSet.h>
+
+#include <Atomic/Core/StringUtils.h>
+
+#include <Atomic/Math/Vector3.h>
+#include <Atomic/Math/Quaternion.h>
+#include <Atomic/Math/Matrix3x4.h>
+#include <Atomic/Math/BoundingBox.h>
+
+using namespace Atomic;
+
+namespace ToolCore
+{
+
+struct OutModel
+{
+    OutModel() :
+        rootBone_(0),
+        totalVertices_(0),
+        totalIndices_(0)
+    {
+    }
+
+    String outName_;
+    aiNode* rootNode_;
+    HashSet<unsigned> meshIndices_;
+    PODVector<aiMesh*> meshes_;
+    PODVector<aiNode*> meshNodes_;
+    PODVector<aiNode*> bones_;
+    PODVector<aiAnimation*> animations_;
+    PODVector<float> boneRadii_;
+    PODVector<BoundingBox> boneHitboxes_;
+    aiNode* rootBone_;
+    unsigned totalVertices_;
+    unsigned totalIndices_;
+};
+
+struct OutScene
+{
+    String outName_;
+    aiNode* rootNode_;
+    Vector<OutModel> models_;
+    PODVector<aiNode*> nodes_;
+    PODVector<unsigned> nodeModelIndices_;
+};
+
+void WriteShortIndices(unsigned short*& dest, aiMesh* mesh, unsigned index, unsigned offset);
+void WriteLargeIndices(unsigned*& dest, aiMesh* mesh, unsigned index, unsigned offset);
+void WriteVertex(float*& dest, aiMesh* mesh, unsigned index, unsigned elementMask, BoundingBox& box,
+    const Matrix3x4& vertexTransform, const Matrix3& normalTransform, Vector<PODVector<unsigned char> >& blendIndices,
+    Vector<PODVector<float> >& blendWeights);
+unsigned GetElementMask(aiMesh* mesh);
+
+aiNode* GetNode(const String& name, aiNode* rootNode, bool caseSensitive = true);
+aiMatrix4x4 GetDerivedTransform(aiNode* node, aiNode* rootNode, bool rootInclusive = true);
+aiMatrix4x4 GetDerivedTransform(aiMatrix4x4 transform, aiNode* node, aiNode* rootNode, bool rootInclusive = true);
+aiMatrix4x4 GetMeshBakingTransform(aiNode* meshNode, aiNode* modelRootNode);
+void GetPosRotScale(const aiMatrix4x4& transform, Vector3& pos, Quaternion& rot, Vector3& scale);
+
+String FromAIString(const aiString& str);
+Vector3 ToVector3(const aiVector3D& vec);
+Vector2 ToVector2(const aiVector2D& vec);
+Quaternion ToQuaternion(const aiQuaternion& quat);
+Matrix3x4 ToMatrix3x4(const aiMatrix4x4& mat);
+String SanitateAssetName(const String& name);
+
+void GetMeshesUnderNode(const aiScene* scene, Vector<Pair<aiNode*, aiMesh*> >& dest, aiNode* node);
+unsigned GetMeshIndex(const aiScene* scene, aiMesh* mesh);
+unsigned GetBoneIndex(OutModel& model, const String& boneName);
+aiBone* GetMeshBone(OutModel& model, const String& boneName);
+Matrix3x4 GetOffsetMatrix(OutModel& model, const String& boneName);
+unsigned GetNumValidFaces(aiMesh* mesh);
+
+void GetBlendData(OutModel& model, aiMesh* mesh, PODVector<unsigned>& boneMappings, Vector<PODVector<unsigned char> >&
+    blendIndices, Vector<PODVector<float> >& blendWeights, unsigned maxBones = 64);
+
+void DumpNodes(aiNode* rootNode, unsigned level);
+
+}