// // Copyright (c) 2008-2014 the Urho3D 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 "AnimatedModel.h" #include "Animation.h" #include "Context.h" #include "DebugRenderer.h" #include "File.h" #include "FileSystem.h" #include "Geometry.h" #include "Graphics.h" #include "IndexBuffer.h" #include "Light.h" #include "Material.h" #include "Model.h" #include "Octree.h" #ifdef URHO3D_PHYSICS #include "PhysicsWorld.h" #endif #include "ProcessUtils.h" #include "Quaternion.h" #include "ResourceCache.h" #include "Scene.h" #include "StringUtils.h" #include "Vector3.h" #include "VertexBuffer.h" #include "WorkQueue.h" #include "XMLFile.h" #include "Zone.h" #include "Sort.h" #ifdef WIN32 #include #endif #include #include #include #include #include #include "DebugNew.h" using namespace Urho3D; struct OutModel { OutModel() : rootBone_(0), totalVertices_(0), totalIndices_(0) { } String outName_; aiNode* rootNode_; HashSet meshIndices_; PODVector meshes_; PODVector meshNodes_; PODVector bones_; PODVector animations_; PODVector boneRadii_; PODVector boneHitboxes_; aiNode* rootBone_; unsigned totalVertices_; unsigned totalIndices_; }; struct OutScene { String outName_; aiNode* rootNode_; Vector models_; PODVector nodes_; PODVector nodeModelIndices_; }; SharedPtr context_(new Context()); const aiScene* scene_ = 0; aiNode* rootNode_ = 0; String inputName_; String resourcePath_; String outPath_; bool useSubdirs_ = true; bool localIDs_ = false; bool saveBinary_ = false; bool createZone_ = true; bool noAnimations_ = false; bool noHierarchy_ = false; bool noMaterials_ = false; bool noTextures_ = false; bool noMaterialDiffuseColor_ = false; bool saveMaterialList_ = false; bool includeNonSkinningBones_ = false; bool verboseLog_ = false; bool emissiveAO_ = false; bool noOverwriteMaterial_ = false; bool noOverwriteTexture_ = false; bool noOverwriteNewerTexture_ = false; bool checkUniqueModel_ = true; Vector nonSkinningBoneIncludes_; Vector nonSkinningBoneExcludes_; HashSet allAnimations_; PODVector sceneAnimations_; float defaultTicksPerSecond_ = 4800.0f; int main(int argc, char** argv); void Run(const Vector& arguments); void DumpNodes(aiNode* rootNode, unsigned level); void ExportModel(const String& outName, bool animationOnly); void CollectMeshes(OutModel& model, aiNode* node); void CollectBones(OutModel& model, bool animationOnly = false); void CollectBonesFinal(PODVector& dest, const HashSet& necessary, aiNode* node); void CollectAnimations(OutModel* model = 0); void BuildBoneCollisionInfo(OutModel& model); void BuildAndSaveModel(OutModel& model); void BuildAndSaveAnimations(OutModel* model = 0); void ExportScene(const String& outName, bool asPrefab); void CollectSceneModels(OutScene& scene, aiNode* node); Node* CreateSceneNode(Scene* scene, aiNode* srcNode, HashMap >& nodeMapping); void BuildAndSaveScene(OutScene& scene, bool asPrefab); void ExportMaterials(HashSet& usedTextures); void BuildAndSaveMaterial(aiMaterial* material, HashSet& usedTextures); void CopyTextures(const HashSet& usedTextures, const String& sourcePath); void CombineLods(const PODVector& lodDistances, const Vector& modelNames, const String& outName); void GetMeshesUnderNode(Vector >& meshes, aiNode* node); unsigned GetMeshIndex(aiMesh* mesh); unsigned GetBoneIndex(OutModel& model, const String& boneName); aiBone* GetMeshBone(OutModel& model, const String& boneName); Matrix3x4 GetOffsetMatrix(OutModel& model, const String& boneName); void GetBlendData(OutModel& model, aiMesh* mesh, PODVector& boneMappings, Vector >& blendIndices, Vector >& blendWeights); String GetMeshMaterialName(aiMesh* mesh); String GetMaterialTextureName(const String& nameIn); String GenerateMaterialName(aiMaterial* material); String GenerateTextureName(unsigned texIndex); unsigned GetNumValidFaces(aiMesh* mesh); 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 >& blendIndices, Vector >& 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); int main(int argc, char** argv) { Vector arguments; #ifdef WIN32 arguments = ParseArguments(GetCommandLineW()); #else arguments = ParseArguments(argc, argv); #endif Run(arguments); return 0; } void Run(const Vector& arguments) { if (arguments.Size() < 2) { ErrorExit( "Usage: AssetImporter [options]\n" "See http://assimp.sourceforge.net/main_features_formats.html for input formats\n\n" "Commands:\n" "model Output a model\n" "scene Output a scene\n" "node Output a node and its children (prefab)\n" "dump Dump scene node structure. No output file is generated\n" "lod Combine several Urho3D models as LOD levels of the output model\n" " Syntax: lod ... \n" "\n" "Options:\n" "-b Save scene in binary format, default format is XML\n" "-h Generate hard instead of smooth normals if input file has no normals\n" "-i Use local ID's for scene nodes\n" "-l Output a material list file for models\n" "-na Do not output animations\n" "-nm Do not output materials\n" "-nt Do not output material textures\n" "-nc Do not use material diffuse color value, instead output white\n" "-nh Do not save full node hierarchy (scene mode only)\n" "-ns Do not create subdirectories for resources\n" "-nz Do not create a zone and a directional light (scene mode only)\n" "-nf Do not fix infacing normals\n" "-p Set path for scene resources. Default is output file path\n" "-r Use the named scene node as root node\n" "-f Animation tick frequency to use if unspecified. Default 4800\n" "-o Optimize redundant submeshes. Loses scene hierarchy and animations\n" "-s Include non-skinning bones in the model's skeleton. Can be given a\n" " case-insensitive semicolon separated filter list. Bone is included\n" " if its name contains any of the filters. Prefix filter with minus\n" " sign to use as an exclude. For example -s \"Bip01;-Dummy;-Helper\"\n" "-t Generate tangents\n" "-v Enable verbose Assimp library logging\n" "-eao Interpret material emissive texture as ambient occlusion\n" "-cm Check and do not overwrite if material exists\n" "-ct Check and do not overwrite if texture exists\n" "-ctn Check and do not overwrite if texture has newer timestamp\n" "-am Export all meshes even if identical (scene mode only)\n" ); } context_->RegisterSubsystem(new FileSystem(context_)); context_->RegisterSubsystem(new ResourceCache(context_)); context_->RegisterSubsystem(new WorkQueue(context_)); RegisterSceneLibrary(context_); RegisterGraphicsLibrary(context_); #ifdef URHO3D_PHYSICS RegisterPhysicsLibrary(context_); #endif String command = arguments[0].ToLower(); String rootNodeName; unsigned flags = aiProcess_ConvertToLeftHanded | aiProcess_JoinIdenticalVertices | aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_LimitBoneWeights | aiProcess_ImproveCacheLocality | aiProcess_RemoveRedundantMaterials | aiProcess_FixInfacingNormals | aiProcess_FindInvalidData | aiProcess_GenUVCoords | aiProcess_FindInstances | aiProcess_OptimizeMeshes; for (unsigned i = 2; i < arguments.Size(); ++i) { if (arguments[i].Length() > 1 && arguments[i][0] == '-') { String argument = arguments[i].Substring(1).ToLower(); String value = i + 1 < arguments.Size() ? arguments[i + 1] : String::EMPTY; if (argument == "b") saveBinary_ = true; else if (argument == "h") { flags &= ~aiProcess_GenSmoothNormals; flags |= aiProcess_GenNormals; } else if (argument == "i") localIDs_ = true; else if (argument == "l") saveMaterialList_ = true; else if (argument == "t") flags |= aiProcess_CalcTangentSpace; else if (argument == "o") flags |= aiProcess_PreTransformVertices; else if (argument.Length() == 2 && argument[0] == 'n') { switch (tolower(argument[1])) { case 'a': noAnimations_ = true; break; case 'c': noMaterialDiffuseColor_ = true; break; case 'm': noMaterials_ = true; break; case 'h': noHierarchy_ = true; break; case 's': useSubdirs_ = false; break; case 't': noTextures_ = true; break; case 'z': createZone_ = false; break; case 'f': flags &= ~aiProcess_FixInfacingNormals; break; } } else if (argument == "p" && !value.Empty()) { resourcePath_ = AddTrailingSlash(value); ++i; } else if (argument == "r" && !value.Empty()) { rootNodeName = value; ++i; } else if (argument == "f" && !value.Empty()) { defaultTicksPerSecond_ = ToFloat(value); ++i; } else if (argument == "s") { includeNonSkinningBones_ = true; if (value.Length() && (value[0] != '-' || value.Length() > 3)) { Vector filters = value.Split(';'); for (unsigned i = 0; i < filters.Size(); ++i) { if (filters[i][0] == '-') nonSkinningBoneExcludes_.Push(filters[i].Substring(1)); else nonSkinningBoneIncludes_.Push(filters[i]); } } } else if (argument == "v") verboseLog_ = true; else if (argument == "eao") emissiveAO_ = true; else if (argument == "cm") noOverwriteMaterial_ = true; else if (argument == "ct") noOverwriteTexture_ = true; else if (argument == "ctn") noOverwriteNewerTexture_ = true; else if (argument == "am") checkUniqueModel_ = false; } } if (command == "model" || command == "scene" || command == "node" || command == "dump") { String inFile = arguments[1]; String outFile; if (arguments.Size() > 2 && arguments[2][0] != '-') outFile = GetInternalPath(arguments[2]); inputName_ = GetFileName(inFile); outPath_ = GetPath(outFile); if (resourcePath_.Empty()) { resourcePath_ = outPath_; // If output file already has the Models/ path (model mode), do not take it into the resource path if (command == "model") { if (resourcePath_.EndsWith("Models/", false)) resourcePath_ = resourcePath_.Substring(0, resourcePath_.Length() - 7); } if (resourcePath_.Empty()) resourcePath_ = "./"; } resourcePath_ = AddTrailingSlash(resourcePath_); if (command != "dump" && outFile.Empty()) ErrorExit("No output file defined"); if (verboseLog_) Assimp::DefaultLogger::create("", Assimp::Logger::VERBOSE, aiDefaultLogStream_STDOUT); PrintLine("Reading file " + inFile); scene_ = aiImportFile(GetNativePath(inFile).CString(), flags); if (!scene_) ErrorExit("Could not open or parse input file " + inFile); if (verboseLog_) Assimp::DefaultLogger::kill(); rootNode_ = scene_->mRootNode; if (!rootNodeName.Empty()) { rootNode_ = GetNode(rootNodeName, rootNode_, false); if (!rootNode_) ErrorExit("Could not find scene node " + rootNodeName); } if (command == "dump") { DumpNodes(rootNode_, 0); return; } if (command == "model") ExportModel(outFile, scene_->mFlags & AI_SCENE_FLAGS_INCOMPLETE); if (command == "scene" || command == "node") { bool asPrefab = command == "node"; // Saving as prefab requires the hierarchy, especially the root node if (asPrefab) noHierarchy_ = false; ExportScene(outFile, asPrefab); } if (!noMaterials_) { HashSet usedTextures; ExportMaterials(usedTextures); if (!noTextures_) CopyTextures(usedTextures, GetPath(inFile)); } } else if (command == "lod") { PODVector lodDistances; Vector modelNames; String outFile; unsigned numLodArguments = 0; for (unsigned i = 1; i < arguments.Size(); ++i) { if (arguments[i][0] == '-') break; ++numLodArguments; } if (numLodArguments < 4) ErrorExit("Must define at least 2 LOD levels"); if (!(numLodArguments & 1)) ErrorExit("No output file defined"); for (unsigned i = 1; i < numLodArguments + 1; ++i) { if (i == numLodArguments) outFile = GetInternalPath(arguments[i]); else { if (i & 1) lodDistances.Push(Max(ToFloat(arguments[i]), 0.0f)); else modelNames.Push(GetInternalPath(arguments[i])); } } if (lodDistances[0] != 0.0f) { PrintLine("Warning: first LOD distance forced to 0"); lodDistances[0] = 0.0f; } CombineLods(lodDistances, modelNames, outFile); } else ErrorExit("Unrecognized command " + command); } void 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); } void ExportModel(const String& outName, bool animationOnly) { if (outName.Empty()) ErrorExit("No output file defined"); OutModel model; model.rootNode_ = rootNode_; model.outName_ = outName; CollectMeshes(model, model.rootNode_); CollectBones(model, animationOnly); BuildBoneCollisionInfo(model); BuildAndSaveModel(model); if (!noAnimations_) { CollectAnimations(&model); BuildAndSaveAnimations(&model); // Save scene-global animations CollectAnimations(); BuildAndSaveAnimations(); } } void CollectMeshes(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(model, node->mChildren[i]); } void CollectBones(OutModel& model, bool animationOnly) { HashSet necessary; HashSet 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::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 CollectBonesFinal(PODVector& dest, const HashSet& 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 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 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 BuildAndSaveModel(OutModel& model) { if (!model.rootNode_) ErrorExit("Null root node for model"); String rootNodeName = FromAIString(model.rootNode_->mName); if (!model.meshes_.Size()) ErrorExit("No geometries found starting from node " + rootNodeName); PrintLine("Writing model " + rootNodeName); SharedPtr outModel(new Model(context_)); Vector > allBoneMappings; BoundingBox box; unsigned numValidGeometries = 0; bool combineBuffers = true; // Check if buffers can be combined (same vertex element mask, under 65535 vertices) unsigned elementMask = GetElementMask(model.meshes_[0]); for (unsigned i = 0; i < model.meshes_.Size(); ++i) { if (GetNumValidFaces(model.meshes_[i])) { ++numValidGeometries; if (i > 0 && GetElementMask(model.meshes_[i]) != elementMask) combineBuffers = false; } } // Check if keeping separate buffers allows to avoid 32-bit indices if (combineBuffers && model.totalVertices_ > 65535) { bool allUnder65k = true; for (unsigned i = 0; i < model.meshes_.Size(); ++i) { if (GetNumValidFaces(model.meshes_[i])) { if (model.meshes_[i]->mNumVertices > 65535) allUnder65k = false; } } if (allUnder65k == true) combineBuffers = false; } SharedPtr ib; SharedPtr vb; Vector > vbVector; Vector > ibVector; unsigned startVertexOffset = 0; unsigned startIndexOffset = 0; unsigned destGeomIndex = 0; outModel->SetNumGeometries(numValidGeometries); for (unsigned i = 0; i < model.meshes_.Size(); ++i) { aiMesh* mesh = model.meshes_[i]; unsigned elementMask = GetElementMask(mesh); unsigned validFaces = GetNumValidFaces(mesh); if (!validFaces) continue; bool largeIndices; if (combineBuffers) largeIndices = model.totalIndices_ > 65535; else largeIndices = mesh->mNumVertices > 65535; // Create new buffers if necessary if (!combineBuffers || vbVector.Empty()) { vb = new VertexBuffer(context_); ib = new IndexBuffer(context_); if (combineBuffers) { ib->SetSize(model.totalIndices_, largeIndices); vb->SetSize(model.totalVertices_, elementMask); } else { ib->SetSize(validFaces * 3, largeIndices); vb->SetSize(mesh->mNumVertices, elementMask); } vbVector.Push(vb); ibVector.Push(ib); startVertexOffset = 0; startIndexOffset = 0; } // Get the world transform of the mesh for baking into the vertices Matrix3x4 vertexTransform; Matrix3 normalTransform; Vector3 pos, scale; Quaternion rot; GetPosRotScale(GetMeshBakingTransform(model.meshNodes_[i], model.rootNode_), pos, rot, scale); vertexTransform = Matrix3x4(pos, rot, scale); normalTransform = rot.RotationMatrix(); SharedPtr geom(new Geometry(context_)); PrintLine("Writing geometry " + String(i) + " with " + String(mesh->mNumVertices) + " vertices " + String(validFaces * 3) + " indices"); unsigned char* vertexData = vb->GetShadowData(); unsigned char* indexData = ib->GetShadowData(); // Build the index data if (!largeIndices) { unsigned short* dest = (unsigned short*)indexData + startIndexOffset; for (unsigned j = 0; j < mesh->mNumFaces; ++j) WriteShortIndices(dest, mesh, j, startVertexOffset); } else { unsigned* dest = (unsigned*)indexData + startIndexOffset; for (unsigned j = 0; j < mesh->mNumFaces; ++j) WriteLargeIndices(dest, mesh, j, startVertexOffset); } // Build the vertex data // If there are bones, get blend data Vector > blendIndices; Vector > blendWeights; PODVector boneMappings; if (model.bones_.Size()) GetBlendData(model, mesh, boneMappings, blendIndices, blendWeights); float* dest = (float*)((unsigned char*)vertexData + startVertexOffset * vb->GetVertexSize()); for (unsigned j = 0; j < mesh->mNumVertices; ++j) WriteVertex(dest, mesh, j, elementMask, box, vertexTransform, normalTransform, blendIndices, blendWeights); // Calculate the geometry center Vector3 center = Vector3::ZERO; if (validFaces) { for (unsigned j = 0; j < mesh->mNumFaces; ++j) { if (mesh->mFaces[j].mNumIndices == 3) { center += vertexTransform * ToVector3(mesh->mVertices[mesh->mFaces[j].mIndices[0]]); center += vertexTransform * ToVector3(mesh->mVertices[mesh->mFaces[j].mIndices[1]]); center += vertexTransform * ToVector3(mesh->mVertices[mesh->mFaces[j].mIndices[2]]); } } center /= (float)validFaces * 3; } // Define the geometry geom->SetIndexBuffer(ib); geom->SetVertexBuffer(0, vb); geom->SetDrawRange(TRIANGLE_LIST, startIndexOffset, validFaces * 3, true); outModel->SetNumGeometryLodLevels(destGeomIndex, 1); outModel->SetGeometry(destGeomIndex, 0, geom); outModel->SetGeometryCenter(destGeomIndex, center); if (model.bones_.Size() > MAX_SKIN_MATRICES) allBoneMappings.Push(boneMappings); startVertexOffset += mesh->mNumVertices; startIndexOffset += validFaces * 3; ++destGeomIndex; } // Define the model buffers and bounding box PODVector emptyMorphRange; outModel->SetVertexBuffers(vbVector, emptyMorphRange, emptyMorphRange); outModel->SetIndexBuffers(ibVector); outModel->SetBoundingBox(box); // Build skeleton if necessary if (model.bones_.Size() && model.rootBone_) { PrintLine("Writing skeleton with " + String(model.bones_.Size()) + " bones, rootbone " + FromAIString(model.rootBone_->mName)); Skeleton skeleton; Vector& bones = skeleton.GetModifiableBones(); for (unsigned i = 0; i < model.bones_.Size(); ++i) { aiNode* boneNode = model.bones_[i]; String boneName(FromAIString(boneNode->mName)); Bone newBone; newBone.name_ = boneName; aiMatrix4x4 transform = boneNode->mTransformation; // Make the root bone transform relative to the model's root node, if it is not already if (boneNode == model.rootBone_) transform = GetDerivedTransform(boneNode, model.rootNode_); GetPosRotScale(transform, newBone.initialPosition_, newBone.initialRotation_, newBone.initialScale_); // Get offset information if exists newBone.offsetMatrix_ = GetOffsetMatrix(model, boneName); newBone.radius_ = model.boneRadii_[i]; newBone.boundingBox_ = model.boneHitboxes_[i]; newBone.collisionMask_ = BONECOLLISION_SPHERE | BONECOLLISION_BOX; newBone.parentIndex_ = i; bones.Push(newBone); } // Set the bone hierarchy for (unsigned i = 1; i < model.bones_.Size(); ++i) { String parentName = FromAIString(model.bones_[i]->mParent->mName); for (unsigned j = 0; j < bones.Size(); ++j) { if (bones[j].name_ == parentName) { bones[i].parentIndex_ = j; break; } } } outModel->SetSkeleton(skeleton); if (model.bones_.Size() > MAX_SKIN_MATRICES) outModel->SetGeometryBoneMappings(allBoneMappings); } File outFile(context_); if (!outFile.Open(model.outName_, FILE_WRITE)) ErrorExit("Could not open output file " + model.outName_); outModel->Save(outFile); // If exporting materials, also save material list for use by the editor if (!noMaterials_ && saveMaterialList_) { String materialListName = ReplaceExtension(model.outName_, ".txt"); File listFile(context_); if (listFile.Open(materialListName, FILE_WRITE)) { for (unsigned i = 0; i < model.meshes_.Size(); ++i) listFile.WriteLine(GetMeshMaterialName(model.meshes_[i])); } else PrintLine("Warning: could not write material list file " + materialListName); } } void BuildAndSaveAnimations(OutModel* model) { const PODVector& 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 outAnim(new Animation(context_)); outAnim->SetAnimationName(animName); outAnim->SetLength(duration * tickConversion); PrintLine("Writing animation " + animName + " length " + String(outAnim->GetLength())); Vector 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; } } AnimationTrack track; track.name_ = channelName; track.nameHash_ = channelName; // Check which channels are used track.channelMask_ = 0; if (channel->mNumPositionKeys > 1) track.channelMask_ |= CHANNEL_POSITION; if (channel->mNumRotationKeys > 1) track.channelMask_ |= CHANNEL_ROTATION; if (channel->mNumScalingKeys > 1) 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 ExportScene(const String& outName, bool asPrefab) { OutScene outScene; outScene.outName_ = outName; outScene.rootNode_ = rootNode_; if (useSubdirs_) context_->GetSubsystem()->CreateDir(resourcePath_ + "Models"); CollectSceneModels(outScene, rootNode_); // Save models, their material lists and animations for (unsigned i = 0; i < outScene.models_.Size(); ++i) BuildAndSaveModel(outScene.models_[i]); // Save scene-global animations if (!noAnimations_) { CollectAnimations(); BuildAndSaveAnimations(); } // Save scene BuildAndSaveScene(outScene, asPrefab); } void CollectSceneModels(OutScene& scene, aiNode* node) { Vector > meshes; GetMeshesUnderNode(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(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]); } Node* CreateSceneNode(Scene* scene, aiNode* srcNode, HashMap& nodeMapping) { if (nodeMapping.Contains(srcNode)) return nodeMapping[srcNode]; // Flatten hierarchy if requested if (noHierarchy_) { Node* outNode = scene->CreateChild(FromAIString(srcNode->mName), localIDs_ ? LOCAL : REPLICATED); Vector3 pos, scale; Quaternion rot; GetPosRotScale(GetDerivedTransform(srcNode, rootNode_), pos, rot, scale); outNode->SetTransform(pos, rot, scale); nodeMapping[srcNode] = outNode; return outNode; } if (srcNode == rootNode_ || !srcNode->mParent) { Node* outNode = scene->CreateChild(FromAIString(srcNode->mName), localIDs_ ? LOCAL : REPLICATED); Vector3 pos, scale; Quaternion rot; GetPosRotScale(srcNode->mTransformation, pos, rot, scale); outNode->SetTransform(pos, rot, scale); nodeMapping[srcNode] = outNode; return outNode; } else { // Ensure the existence of the parent chain as in the original file if (!nodeMapping.Contains(srcNode->mParent)) CreateSceneNode(scene, srcNode->mParent, nodeMapping); Node* parent = nodeMapping[srcNode->mParent]; Node* outNode = parent->CreateChild(FromAIString(srcNode->mName), localIDs_ ? LOCAL : REPLICATED); Vector3 pos, scale; Quaternion rot; GetPosRotScale(srcNode->mTransformation, pos, rot, scale); outNode->SetTransform(pos, rot, scale); nodeMapping[srcNode] = outNode; return outNode; } } void BuildAndSaveScene(OutScene& scene, bool asPrefab) { if (!asPrefab) PrintLine("Writing scene"); else PrintLine("Writing node hierarchy"); SharedPtr outScene(new Scene(context_)); if (!asPrefab) { #ifdef URHO3D_PHYSICS /// \todo Make the physics properties configurable outScene->CreateComponent(); #endif /// \todo Make the octree properties configurable, or detect from the scene contents outScene->CreateComponent(); outScene->CreateComponent(); if (createZone_) { Node* zoneNode = outScene->CreateChild("Zone", localIDs_ ? LOCAL : REPLICATED); Zone* zone = zoneNode->CreateComponent(); zone->SetBoundingBox(BoundingBox(-1000.0f, 1000.f)); zone->SetAmbientColor(Color(0.25f, 0.25f, 0.25f)); // Create default light only if scene does not define them if (!scene_->HasLights()) { Node* lightNode = outScene->CreateChild("GlobalLight", localIDs_ ? LOCAL : REPLICATED); Light* light = lightNode->CreateComponent(); light->SetLightType(LIGHT_DIRECTIONAL); lightNode->SetRotation(Quaternion(60.0f, 30.0f, 0.0f)); } } } ResourceCache* cache = context_->GetSubsystem(); HashMap nodeMapping; Node* outRootNode = 0; if (asPrefab || !noHierarchy_) outRootNode = CreateSceneNode(outScene, rootNode_, nodeMapping); // Create geometry nodes for (unsigned i = 0; i < scene.nodes_.Size(); ++i) { const OutModel& model = scene.models_[scene.nodeModelIndices_[i]]; Node* modelNode = CreateSceneNode(outScene, scene.nodes_[i], nodeMapping); StaticModel* staticModel = model.bones_.Empty() ? modelNode->CreateComponent() : modelNode->CreateComponent(); // Create a dummy model so that the reference can be stored String modelName = (useSubdirs_ ? "Models/" : "") + GetFileNameAndExtension(model.outName_); if (!cache->Exists(modelName)) { Model* dummyModel = new Model(context_); dummyModel->SetName(modelName); dummyModel->SetNumGeometries(model.meshes_.Size()); cache->AddManualResource(dummyModel); } staticModel->SetModel(cache->GetResource(modelName)); // Set materials if they are known for (unsigned j = 0; j < model.meshes_.Size(); ++j) { String matName = GetMeshMaterialName(model.meshes_[j]); // Create a dummy material so that the reference can be stored if (!cache->Exists(matName)) { Material* dummyMat = new Material(context_); dummyMat->SetName(matName); cache->AddManualResource(dummyMat); } staticModel->SetMaterial(j, cache->GetResource(matName)); } } // Create lights if (!asPrefab) { for (unsigned i = 0; i < scene_->mNumLights; ++i) { aiLight* light = scene_->mLights[i]; aiNode* lightNode = GetNode(FromAIString(light->mName), rootNode_, true); if (!lightNode) continue; Node* outNode = CreateSceneNode(outScene, lightNode, nodeMapping); Vector3 lightAdjustPosition = ToVector3(light->mPosition); Vector3 lightAdjustDirection = ToVector3(light->mDirection); // If light is not aligned at the scene node, an adjustment node needs to be created if (!lightAdjustPosition.Equals(Vector3::ZERO) || (light->mType != aiLightSource_POINT && !lightAdjustDirection.Equals(Vector3::FORWARD))) { outNode = outNode->CreateChild("LightAdjust"); outNode->SetPosition(lightAdjustPosition); outNode->SetDirection(lightAdjustDirection); } Light* outLight = outNode->CreateComponent(); outLight->SetColor(Color(light->mColorDiffuse.r, light->mColorDiffuse.g, light->mColorDiffuse.b)); switch (light->mType) { case aiLightSource_DIRECTIONAL: outLight->SetLightType(LIGHT_DIRECTIONAL); break; case aiLightSource_SPOT: outLight->SetLightType(LIGHT_SPOT); outLight->SetFov(light->mAngleOuterCone * M_RADTODEG); break; case aiLightSource_POINT: outLight->SetLightType(LIGHT_POINT); break; default: break; } // Calculate range from attenuation parameters so that light intensity has been reduced to 10% at that distance if (light->mType != aiLightSource_DIRECTIONAL) { float a = light->mAttenuationQuadratic; float b = light->mAttenuationLinear; float c = -10.0f; if (!Equals(a, 0.0f)) { float root1 = (-b + sqrtf(b * b - 4.0f * a * c)) / (2.0f * a); float root2 = (-b - sqrtf(b * b - 4.0f * a * c)) / (2.0f * a); outLight->SetRange(Max(root1, root2)); } else if (!Equals(b, 0.0f)) outLight->SetRange(-c / b); } } } File file(context_); if (!file.Open(scene.outName_, FILE_WRITE)) ErrorExit("Could not open output file " + scene.outName_); if (!asPrefab) { if (!saveBinary_) outScene->SaveXML(file); else outScene->Save(file); } else { if (!saveBinary_) outRootNode->SaveXML(file); else outRootNode->Save(file); } } void ExportMaterials(HashSet& usedTextures) { if (useSubdirs_) context_->GetSubsystem()->CreateDir(resourcePath_ + "Materials"); for (unsigned i = 0; i < scene_->mNumMaterials; ++i) BuildAndSaveMaterial(scene_->mMaterials[i], usedTextures); } void BuildAndSaveMaterial(aiMaterial* material, HashSet& usedTextures) { aiString matNameStr; material->Get(AI_MATKEY_NAME, matNameStr); String matName = SanitateAssetName(FromAIString(matNameStr)); if (matName.Trimmed().Empty()) matName = GenerateMaterialName(material); // Do not actually create a material instance, but instead craft an xml file manually XMLFile outMaterial(context_); XMLElement materialElem = outMaterial.CreateRoot("material"); String diffuseTexName; String normalTexName; String specularTexName; String lightmapTexName; String emissiveTexName; Color diffuseColor = Color::WHITE; Color specularColor; Color emissiveColor = Color::BLACK; bool hasAlpha = false; bool twoSided = false; float specPower = 1.0f; aiString stringVal; float floatVal; int intVal; aiColor3D colorVal; if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_DIFFUSE, 0), stringVal) == AI_SUCCESS) diffuseTexName = GetFileNameAndExtension(FromAIString(stringVal)); if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_NORMALS, 0), stringVal) == AI_SUCCESS) normalTexName = GetFileNameAndExtension(FromAIString(stringVal)); if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_SPECULAR, 0), stringVal) == AI_SUCCESS) specularTexName = GetFileNameAndExtension(FromAIString(stringVal)); if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_LIGHTMAP, 0), stringVal) == AI_SUCCESS) specularTexName = GetFileNameAndExtension(FromAIString(stringVal)); if (material->Get(AI_MATKEY_TEXTURE(aiTextureType_EMISSIVE, 0), stringVal) == AI_SUCCESS) emissiveTexName = GetFileNameAndExtension(FromAIString(stringVal)); if (!noMaterialDiffuseColor_) { if (material->Get(AI_MATKEY_COLOR_DIFFUSE, colorVal) == AI_SUCCESS) diffuseColor = Color(colorVal.r, colorVal.g, colorVal.b); } if (material->Get(AI_MATKEY_COLOR_SPECULAR, colorVal) == AI_SUCCESS) specularColor = Color(colorVal.r, colorVal.g, colorVal.b); if (!emissiveAO_) { if (material->Get(AI_MATKEY_COLOR_EMISSIVE, colorVal) == AI_SUCCESS) emissiveColor = Color(colorVal.r, colorVal.g, colorVal.b); } if (material->Get(AI_MATKEY_OPACITY, floatVal) == AI_SUCCESS) { if (floatVal < 1.0f) hasAlpha = true; diffuseColor.a_ = floatVal; } if (material->Get(AI_MATKEY_SHININESS, floatVal) == AI_SUCCESS) specPower = floatVal; if (material->Get(AI_MATKEY_TWOSIDED, intVal) == AI_SUCCESS) twoSided = (intVal != 0); String techniqueName = "Techniques/NoTexture"; if (!diffuseTexName.Empty()) { techniqueName = "Techniques/Diff"; if (!normalTexName.Empty()) techniqueName += "Normal"; if (!specularTexName.Empty()) techniqueName += "Spec"; // For now lightmap does not coexist with normal & specular if (normalTexName.Empty() && specularTexName.Empty() && !lightmapTexName.Empty()) techniqueName += "LightMap"; if (lightmapTexName.Empty() && !emissiveTexName.Empty()) techniqueName += emissiveAO_ ? "AO" : "Emissive"; } if (hasAlpha) techniqueName += "Alpha"; XMLElement techniqueElem = materialElem.CreateChild("technique"); techniqueElem.SetString("name", techniqueName + ".xml"); if (!diffuseTexName.Empty()) { XMLElement diffuseElem = materialElem.CreateChild("texture"); diffuseElem.SetString("unit", "diffuse"); diffuseElem.SetString("name", GetMaterialTextureName(diffuseTexName)); usedTextures.Insert(diffuseTexName); } if (!normalTexName.Empty()) { XMLElement normalElem = materialElem.CreateChild("texture"); normalElem.SetString("unit", "normal"); normalElem.SetString("name", GetMaterialTextureName(normalTexName)); usedTextures.Insert(normalTexName); } if (!specularTexName.Empty()) { XMLElement specularElem = materialElem.CreateChild("texture"); specularElem.SetString("unit", "specular"); specularElem.SetString("name", GetMaterialTextureName(specularTexName)); usedTextures.Insert(specularTexName); } if (!lightmapTexName.Empty()) { XMLElement lightmapElem = materialElem.CreateChild("texture"); lightmapElem.SetString("unit", "emissive"); lightmapElem.SetString("name", GetMaterialTextureName(lightmapTexName)); usedTextures.Insert(lightmapTexName); } if (!emissiveTexName.Empty()) { XMLElement emissiveElem = materialElem.CreateChild("texture"); emissiveElem.SetString("unit", "emissive"); emissiveElem.SetString("name", GetMaterialTextureName(emissiveTexName)); usedTextures.Insert(emissiveTexName); } XMLElement diffuseColorElem = materialElem.CreateChild("parameter"); diffuseColorElem.SetString("name", "MatDiffColor"); diffuseColorElem.SetColor("value", diffuseColor); XMLElement specularElem = materialElem.CreateChild("parameter"); specularElem.SetString("name", "MatSpecColor"); specularElem.SetVector4("value", Vector4(specularColor.r_, specularColor.g_, specularColor.b_, specPower)); XMLElement emissiveColorElem = materialElem.CreateChild("parameter"); emissiveColorElem.SetString("name", "MatEmissiveColor"); emissiveColorElem.SetColor("value", emissiveColor); if (twoSided) { XMLElement cullElem = materialElem.CreateChild("cull"); XMLElement shadowCullElem = materialElem.CreateChild("shadowcull"); cullElem.SetString("value", "none"); shadowCullElem.SetString("value", "none"); } FileSystem* fileSystem = context_->GetSubsystem(); String outFileName = resourcePath_ + (useSubdirs_ ? "Materials/" : "" ) + matName + ".xml"; if (noOverwriteMaterial_ && fileSystem->FileExists(outFileName)) { PrintLine("Skipping save of existing material " + matName); return; } PrintLine("Writing material " + matName); File outFile(context_); if (!outFile.Open(outFileName, FILE_WRITE)) ErrorExit("Could not open output file " + outFileName); outMaterial.Save(outFile); } void CopyTextures(const HashSet& usedTextures, const String& sourcePath) { FileSystem* fileSystem = context_->GetSubsystem(); if (useSubdirs_) fileSystem->CreateDir(resourcePath_ + "Textures"); for (HashSet::ConstIterator i = usedTextures.Begin(); i != usedTextures.End(); ++i) { // Handle assimp embedded textures if (i->Length() && i->At(0) == '*') { unsigned texIndex = ToInt(i->Substring(1)); if (texIndex >= scene_->mNumTextures) PrintLine("Skipping out of range texture index " + String(texIndex)); else { aiTexture* tex = scene_->mTextures[texIndex]; String fullDestName = resourcePath_ + GenerateTextureName(texIndex); bool destExists = fileSystem->FileExists(fullDestName); if (destExists && noOverwriteTexture_) { PrintLine("Skipping copy of existing embedded texture " + GetFileNameAndExtension(fullDestName)); continue; } // Encoded texture if (!tex->mHeight) { PrintLine("Saving embedded texture " + GetFileNameAndExtension(fullDestName)); File dest(context_, fullDestName, FILE_WRITE); dest.Write((const void*)tex->pcData, tex->mWidth); } // RGBA8 texture else { PrintLine("Saving embedded RGBA texture " + GetFileNameAndExtension(fullDestName)); Image image(context_); image.SetSize(tex->mWidth, tex->mHeight, 4); memcpy(image.GetData(), (const void*)tex->pcData, tex->mWidth * tex->mHeight * 4); image.SavePNG(fullDestName); } } } else { String fullSourceName = sourcePath + *i; String fullDestName = resourcePath_ + (useSubdirs_ ? "Textures/" : "") + *i; if (!fileSystem->FileExists(fullSourceName)) { PrintLine("Skipping copy of nonexisting material texture " + *i); continue; } { File test(context_, fullSourceName); if (!test.GetSize()) { PrintLine("Skipping copy of zero-size material texture " + *i); continue; } } bool destExists = fileSystem->FileExists(fullDestName); if (destExists && noOverwriteTexture_) { PrintLine("Skipping copy of existing texture " + *i); continue; } if (destExists && noOverwriteNewerTexture_ && fileSystem->GetLastModifiedTime(fullDestName) > fileSystem->GetLastModifiedTime(fullSourceName)) { PrintLine("Skipping copying of material texture " + *i + ", destination is newer"); continue; } PrintLine("Copying material texture " + *i); fileSystem->Copy(fullSourceName, fullDestName); } } } void CombineLods(const PODVector& lodDistances, const Vector& modelNames, const String& outName) { // Load models Vector > srcModels; for (unsigned i = 0; i < modelNames.Size(); ++i) { PrintLine("Reading LOD level " + String(i) + ": model " + modelNames[i] + " distance " + String(lodDistances[i])); File srcFile(context_); srcFile.Open(modelNames[i]); SharedPtr srcModel(new Model(context_)); if (!srcModel->Load(srcFile)) ErrorExit("Could not load input model " + modelNames[i]); srcModels.Push(srcModel); } // Check that none of the models already has LOD levels for (unsigned i = 0; i < srcModels.Size(); ++i) { for (unsigned j = 0; j < srcModels[i]->GetNumGeometries(); ++j) { if (srcModels[i]->GetNumGeometryLodLevels(j) > 1) ErrorExit(modelNames[i] + " already has multiple LOD levels defined"); } } // Check for number of geometries (need to have same amount for now) for (unsigned i = 1; i < srcModels.Size(); ++i) { if (srcModels[i]->GetNumGeometries() != srcModels[0]->GetNumGeometries()) ErrorExit(modelNames[i] + " has different amount of geometries than " + modelNames[0]); } // If there are bones, check for compatibility (need to have exact match for now) for (unsigned i = 1; i < srcModels.Size(); ++i) { if (srcModels[i]->GetSkeleton().GetNumBones() != srcModels[0]->GetSkeleton().GetNumBones()) ErrorExit(modelNames[i] + " has different amount of bones than " + modelNames[0]); for (unsigned j = 0; j < srcModels[0]->GetSkeleton().GetNumBones(); ++j) { if (srcModels[i]->GetSkeleton().GetBone(j)->name_ != srcModels[0]->GetSkeleton().GetBone(j)->name_) ErrorExit(modelNames[i] + " has different bones than " + modelNames[0]); } if (srcModels[i]->GetGeometryBoneMappings() != srcModels[0]->GetGeometryBoneMappings()) ErrorExit(modelNames[i] + " has different per-geometry bone mappings than " + modelNames[0]); } Vector > vbVector; Vector > ibVector; PODVector emptyMorphRange; // Create the final model SharedPtr outModel(new Model(context_)); outModel->SetNumGeometries(srcModels[0]->GetNumGeometries()); for (unsigned i = 0; i < srcModels[0]->GetNumGeometries(); ++i) { outModel->SetNumGeometryLodLevels(i, srcModels.Size()); for (unsigned j = 0; j < srcModels.Size(); ++j) { Geometry* geometry = srcModels[j]->GetGeometry(i, 0); geometry->SetLodDistance(lodDistances[j]); outModel->SetGeometry(i, j, geometry); for (unsigned k = 0; k < geometry->GetNumVertexBuffers(); ++k) { SharedPtr vb(geometry->GetVertexBuffer(k)); if (!vbVector.Contains(vb)) vbVector.Push(vb); } SharedPtr ib(geometry->GetIndexBuffer()); if (!ibVector.Contains(ib)) ibVector.Push(ib); } } outModel->SetVertexBuffers(vbVector, emptyMorphRange, emptyMorphRange); outModel->SetIndexBuffers(ibVector); outModel->SetSkeleton(srcModels[0]->GetSkeleton()); outModel->SetGeometryBoneMappings(srcModels[0]->GetGeometryBoneMappings()); outModel->SetBoundingBox(srcModels[0]->GetBoundingBox()); /// \todo Vertex morphs are ignored for now // Save the final model PrintLine("Writing output model"); File outFile(context_); if (!outFile.Open(outName, FILE_WRITE)) ErrorExit("Could not open output file " + outName); outModel->Save(outFile); } void GetMeshesUnderNode(Vector >& dest, aiNode* node) { for (unsigned i = 0; i < node->mNumMeshes; ++i) dest.Push(MakePair(node, scene_->mMeshes[node->mMeshes[i]])); } unsigned GetMeshIndex(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; } void GetBlendData(OutModel& model, aiMesh* mesh, PODVector& boneMappings, Vector >& blendIndices, Vector >& blendWeights) { 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() > MAX_SKIN_MATRICES) { if (mesh->mNumBones > MAX_SKIN_MATRICES) ErrorExit("Geometry has too many bone influences"); 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 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 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 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 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; } 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 >& blendIndices, Vector >& 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); } 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; }