#include #include #include #include #include #include #include #include #include #include #include #include #include "Rune Engine/Rune/Rune.h" #include "Rune Engine/Rune/Rune Engine NULL.h" #include "Rune Engine/Rune/Core/DataStore/RuDataStore_Disk.h" #include "Rune Engine/Rune/Scene/Base/RuEntityBase.h" #include "Rune Engine/Rune/Scene/Base/RuEntityACT.h" #include "Rune Engine/Rune/Scene/Base/RuEntityContainer.h" #include "Rune Engine/Rune/Scene/Base/RuHierarchy_GR2.h" #include "Rune Engine/Rune/Scene/Controller/RuController_Hierarchy.h" #include "Rune Engine/Rune/Scene/Terrain/RuWorld_Base.h" #include "Rune Engine/Rune/Scene/Terrain/RuWorld_EntitySystem.h" #include "Rune Engine/Rune/Helper/RuHelper_Entity.h" #include "Rune Engine/Rune/Scene/Paperdoll/RuEntityPaperdoll.h" #include "Rune Engine/Rune/Engine/Base/RuMaterialBase.h" #include "Rune Engine/Rune/Engine/Geometry/RuMeshBase.h" namespace { struct ExportStats { std::uint64_t entityCount = 0; std::uint64_t renderableCount = 0; std::uint64_t meshCount = 0; std::uint64_t vertexCount = 0; std::uint64_t triangleCount = 0; std::uint64_t skippedMeshCount = 0; std::uint64_t materialCount = 0; std::uint64_t copiedTextureCount = 0; std::uint64_t missingTextureCount = 0; double minX = std::numeric_limits::infinity(); double minY = std::numeric_limits::infinity(); double minZ = std::numeric_limits::infinity(); double maxX = -std::numeric_limits::infinity(); double maxY = -std::numeric_limits::infinity(); double maxZ = -std::numeric_limits::infinity(); }; struct MaterialInfo { std::string name; std::string shaderName; std::string sourceTexture; std::string resolvedTexture; std::string stagedTexture; bool textureCopied = false; }; struct RigMeshInfo { std::string name; INT32 vertexCount = 0; std::vector weights; std::vector joints; }; struct RigBoneInfo { std::string name; INT32 parent = -1; float translation[3] = { 0.0f, 0.0f, 0.0f }; float rotation[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; float scale[3] = { 1.0f, 1.0f, 1.0f }; }; struct RigMotionInfo { INT32 id = 0; std::string name; std::string animation; float duration = 0.0f; float sampleRate = 30.0f; INT32 frameCount = 0; std::uint64_t timesByteOffset = 0; std::uint64_t translationsByteOffset = 0; std::uint64_t rotationsByteOffset = 0; std::uint64_t scalesByteOffset = 0; }; struct RigMeshBinaryInfo { std::uint64_t jointsByteOffset = 0; std::uint64_t weightsByteOffset = 0; }; struct EntityInfo { std::string type; std::string name; }; struct PaperdollPartSpec { std::string part; std::string component; RUCOLOR mainColor = 0; RUCOLOR offColor = 0; }; struct PaperdollAssembly { std::vector parts; RUCOLOR skinColor = 0; RUCOLOR hairColor = 0; }; std::vector SplitTabs(const std::string &line) { std::vector fields; std::size_t begin = 0; for(;;) { const std::size_t tab = line.find('\t', begin); fields.push_back(line.substr(begin, tab == std::string::npos ? std::string::npos : tab - begin)); if(tab == std::string::npos) break; begin = tab + 1; } return fields; } RUCOLOR ParseColor(const std::string &value) { if(value.empty()) return 0; char *end = NULL; const unsigned long parsed = std::strtoul(value.c_str(), &end, 10); if(end == value.c_str() || *end != '\0') throw std::runtime_error("Invalid paperdoll color: " + value); return static_cast(parsed); } PaperdollAssembly ReadPaperdollAssembly(const std::string &fileName) { std::ifstream input(fileName.c_str()); if(!input) throw std::runtime_error("Unable to open paperdoll assembly: " + fileName); PaperdollAssembly assembly; std::string line; while(std::getline(input, line)) { if(!line.empty() && line[line.size() - 1] == '\r') line.erase(line.size() - 1); if(line.empty() || line[0] == '#') continue; const std::vector fields = SplitTabs(line); if(fields.size() != 4) throw std::runtime_error("Paperdoll assembly lines require four tab-separated fields."); if(fields[0] == "@skin") { assembly.skinColor = ParseColor(fields[2]); continue; } if(fields[0] == "@hair") { assembly.hairColor = ParseColor(fields[2]); continue; } PaperdollPartSpec spec; spec.part = fields[0]; spec.component = fields[1]; spec.mainColor = ParseColor(fields[2]); spec.offColor = ParseColor(fields[3]); assembly.parts.push_back(spec); } return assembly; } std::string DefaultPaperdollComponent(const std::string &part, const std::string &component) { if(!component.empty()) return component; if(part == "head" || part == "hair") return "type01"; if(part == "torso" || part == "hand" || part == "leg" || part == "foot") return "body000-001"; return ""; } void SetGlobalPaperdollColor(IRuPaperdoll *paperdoll, INT32 layer, RUCOLOR color) { CRuPaperdollTemplate *paperdollTemplate = paperdoll->GetPaperdollTemplate(); if(!paperdollTemplate) return; for(INT32 i = 0; i < paperdollTemplate->GetNumParts(); ++i) { BOOL activated[4] = { FALSE, FALSE, FALSE, FALSE }; RUCOLOR colors[4] = { 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF }; const char *partName = paperdollTemplate->GetPartName(i); if(partName && paperdoll->GetComponentColors(partName, activated, colors)) { activated[layer] = (color & 0xFF000000) == 0xFF000000; colors[layer] = color; paperdoll->SetComponentColors(partName, activated, colors); } } } bool ApplyPaperdollAssembly(CRuEntity *entity, const PaperdollAssembly &assembly) { IRuPaperdoll *paperdoll = RuEntity_FindFirstPaperdoll(entity); if(!paperdoll || !paperdoll->GetType().IsSubClassOf(CRuPaperdoll::Type())) return false; INT32 appliedComponents = 0; for(std::size_t i = 0; i < assembly.parts.size(); ++i) { const PaperdollPartSpec &spec = assembly.parts[i]; const std::string component = DefaultPaperdollComponent(spec.part, spec.component); if(paperdoll->SetComponent(spec.part.c_str(), component.c_str())) ++appliedComponents; } if(appliedComponents == 0) throw std::runtime_error("Paperdoll assembly did not match any template parts."); SetGlobalPaperdollColor(paperdoll, 0, assembly.skinColor); SetGlobalPaperdollColor(paperdoll, 1, assembly.hairColor); for(std::size_t i = 0; i < assembly.parts.size(); ++i) { const PaperdollPartSpec &spec = assembly.parts[i]; BOOL activated[4] = { FALSE, FALSE, FALSE, FALSE }; RUCOLOR colors[4] = { 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF }; if(paperdoll->GetComponentColors(spec.part.c_str(), activated, colors)) { activated[2] = (spec.mainColor & 0xFF000000) == 0xFF000000; activated[3] = (spec.offColor & 0xFF000000) == 0xFF000000; colors[2] = spec.mainColor; colors[3] = spec.offColor; paperdoll->SetComponentColors(spec.part.c_str(), activated, colors); } } if(!static_cast(paperdoll)->BuildPaperdoll()) return false; RuEntity_RefreshHierarchySubNodeBounds(entity); return !paperdoll->IsPaperdollBuilding() && static_cast(paperdoll)->IsPaperdollInValidState(); } std::string JoinPath(const std::string &left, const std::string &right) { if(left.empty()) return right; const char last = left[left.size() - 1]; if(last == '\\' || last == '/') return left + right; return left + "\\" + right; } std::string NormalizeResourcePath(std::string path) { for(std::size_t i = 0; i < path.size(); ++i) { if(path[i] == '/') path[i] = '\\'; } while(!path.empty() && (path[0] == '\\' || path[0] == '/')) path.erase(path.begin()); return path; } bool FileExists(const std::string &path) { const DWORD attributes = GetFileAttributesA(path.c_str()); return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; } void EnsureDirectory(const std::string &path) { if(CreateDirectoryA(path.c_str(), NULL) == FALSE) { const DWORD error = GetLastError(); if(error != ERROR_ALREADY_EXISTS) throw std::runtime_error("Unable to create output directory: " + path); } } std::uint32_t HashPath(const std::string &value) { std::uint32_t hash = 2166136261u; for(std::size_t i = 0; i < value.size(); ++i) { unsigned char character = static_cast(value[i]); if(character >= 'A' && character <= 'Z') character = static_cast(character - 'A' + 'a'); hash ^= character; hash *= 16777619u; } return hash; } std::string SafeFileName(const std::string &resourcePath) { const std::size_t slash = resourcePath.find_last_of("\\/"); std::string name = slash == std::string::npos ? resourcePath : resourcePath.substr(slash + 1); if(name.empty()) name = "texture"; for(std::size_t i = 0; i < name.size(); ++i) { const char character = name[i]; if(character == '<' || character == '>' || character == ':' || character == '"' || character == '/' || character == '\\' || character == '|' || character == '?' || character == '*') { name[i] = '_'; } } std::ostringstream output; output << std::hex << std::setw(8) << std::setfill('0') << HashPath(resourcePath) << "_" << name; return output.str(); } std::string JsonEscape(const std::string &value) { std::ostringstream output; for(std::size_t i = 0; i < value.size(); ++i) { const unsigned char character = static_cast(value[i]); switch(character) { case '\\': output << "\\\\"; break; case '"': output << "\\\""; break; case '\b': output << "\\b"; break; case '\f': output << "\\f"; break; case '\n': output << "\\n"; break; case '\r': output << "\\r"; break; case '\t': output << "\\t"; break; default: if(character < 0x20) { output << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast(character) << std::dec; } else { output << static_cast(character); } break; } } return output.str(); } std::string SafeObjectName(const char *objectName, std::uint64_t fallbackIndex) { std::string name = objectName && objectName[0] ? objectName : "entity"; for(std::size_t i = 0; i < name.size(); ++i) { const unsigned char character = static_cast(name[i]); if(character <= 0x20 || character == '#' || character == '/' || character == '\\') name[i] = '_'; } std::ostringstream output; output << name << "_" << fallbackIndex; return output.str(); } int WriteWdbReport(const std::string &wdbPath, const std::string &outputPath) { CRuDataStore_Disk *dataStore = ruNEW CRuDataStore_Disk(); if(dataStore->Open(wdbPath.c_str(), TRUE) == FALSE) { std::cerr << "Unable to open WDB data store: " << wdbPath << "\n"; ruSAFE_RELEASE(dataStore); return 9; } // Enumeration only touches the descriptor table, so these construction // bounds need not match the original terrain bounds. CRuWorld_ObjectContainer *container = ruNEW CRuWorld_ObjectContainer( CRuAABB(CRuVector3(-1.0f, -1.0f, -1.0f), CRuVector3(1.0f, 1.0f, 1.0f)), 2.0f); if(container->Attach(dataStore) == FALSE) { std::cerr << "Unable to attach the WDB object container.\n"; ruSAFE_RELEASE(container); ruSAFE_RELEASE(dataStore); return 10; } CRuArrayList objects; container->EnumerateAllObjectsByType(CRuWorld_EntityDescriptor::Type(), objects); std::ofstream report(outputPath.c_str(), std::ios::out | std::ios::trunc); if(!report) { std::cerr << "Unable to write WDB report: " << outputPath << "\n"; for(INT32 i = 0; i < objects.Count(); ++i) ruSAFE_RELEASE(objects[i]); ruSAFE_RELEASE(container); ruSAFE_RELEASE(dataStore); return 11; } report << std::setprecision(9); report << "{\n"; report << " \"schemaVersion\": 1,\n"; report << " \"sourceWdb\": \"" << JsonEscape(wdbPath) << "\",\n"; report << " \"coordinateSystem\": \"runewaker-left-handed-y-up\",\n"; report << " \"descriptorCount\": " << objects.Count() << ",\n"; report << " \"descriptors\": [\n"; for(INT32 i = 0; i < objects.Count(); ++i) { CRuWorld_EntityDescriptor *descriptor = static_cast(objects[i]); const CRuVector3 &scale = descriptor->GetScale(); const CRuQuaternion &rotation = descriptor->GetRotation(); const CRuVector3 &translation = descriptor->GetTranslation(); const CRuSphere &localBounds = descriptor->GetLocalBounds(); const CRuSphere &worldBounds = descriptor->GetWorldBounds(); char *guid = descriptor->GetGUID().ToString(); report << " {\n"; report << " \"guid\": \"" << JsonEscape(guid ? guid : "") << "\",\n"; report << " \"resource\": \"" << JsonEscape(descriptor->GetResourceName() ? descriptor->GetResourceName() : "") << "\",\n"; report << " \"scale\": [" << scale.x << ", " << scale.y << ", " << scale.z << "],\n"; report << " \"rotation\": [" << rotation.x << ", " << rotation.y << ", " << rotation.z << ", " << rotation.w << "],\n"; report << " \"translation\": [" << translation.x << ", " << translation.y << ", " << translation.z << "],\n"; report << " \"localBounds\": { \"center\": [" << localBounds.Center().x << ", " << localBounds.Center().y << ", " << localBounds.Center().z << "], \"radius\": " << localBounds.Radius() << " },\n"; report << " \"worldBounds\": { \"center\": [" << worldBounds.Center().x << ", " << worldBounds.Center().y << ", " << worldBounds.Center().z << "], \"radius\": " << worldBounds.Radius() << " },\n"; report << " \"zoneFlags\": " << descriptor->GetZoneFlags() << ",\n"; report << " \"detailClass\": " << static_cast(descriptor->GetDetailClass()) << ",\n"; report << " \"zoneType\": " << static_cast(descriptor->GetZoneType()) << "\n"; report << " }" << (i + 1 < objects.Count() ? "," : "") << "\n"; ruSAFE_DELETE_ARRAY(guid); } report << " ]\n"; report << "}\n"; report.flush(); const INT32 descriptorCount = objects.Count(); for(INT32 i = 0; i < objects.Count(); ++i) ruSAFE_RELEASE(objects[i]); ruSAFE_RELEASE(container); ruSAFE_RELEASE(dataStore); std::cout << "Reported " << descriptorCount << " WDB entity descriptors.\n"; return 0; } std::string FindMaterialTexture(IRuMaterial *material) { if(material == NULL) return std::string(); material->BindTextures(TRUE); if(material->ChannelExists(ruTEXCHANNEL_DIFFUSEMAP)) { IRuBaseTexture *texture = material->GetTexture(ruTEXCHANNEL_DIFFUSEMAP); if(texture && texture->GetTextureName() && texture->GetTextureName()[0]) return texture->GetTextureName(); } const INT32 channelCount = material->GetNumTextureChannels(); for(INT32 i = 0; i < channelCount; ++i) { const RuTextureChannel channel = material->GetTextureChannel(i); IRuBaseTexture *texture = material->GetTexture(channel); if(texture && texture->GetTextureName() && texture->GetTextureName()[0]) return texture->GetTextureName(); } return std::string(); } class ObjExporter { public: ObjExporter( const std::string &resourceRoot, const std::string &outputDirectory, const std::string &modelResourcePath) : m_resourceRoot(resourceRoot), m_outputDirectory(outputDirectory), m_modelResourcePath(modelResourcePath), m_actor(NULL), m_hierarchy(NULL), m_hierarchyGR2(NULL), m_nextVertexIndex(1), m_nextUvIndex(1), m_nextNormalIndex(1) { } bool Open() { EnsureDirectory(m_outputDirectory); EnsureDirectory(JoinPath(m_outputDirectory, "textures")); m_obj.open(JoinPath(m_outputDirectory, "scene.obj").c_str(), std::ios::out | std::ios::trunc); m_mtl.open(JoinPath(m_outputDirectory, "scene.mtl").c_str(), std::ios::out | std::ios::trunc); if(!m_obj || !m_mtl) return false; m_obj << "# Runewaker ROS export for HealerMan\n"; m_obj << "# Source: " << m_modelResourcePath << "\n"; m_obj << "mtllib scene.mtl\n"; m_obj << std::setprecision(9); m_mtl << "# Runewaker material export for HealerMan\n"; m_mtl << std::setprecision(9); return true; } void ExportEntityTree(CRuEntity *entity) { if(entity == NULL) return; ++m_stats.entityCount; entity->UpdateTransformation(); EntityInfo entityInfo; entityInfo.type = entity->GetType().GetTypeName(); entityInfo.name = entity->GetObjectName() ? entity->GetObjectName() : ""; m_entities.push_back(entityInfo); if(entity->GetType().IsSubClassOf(CRuACTEntity::Type())) { CRuACTEntity *candidate = static_cast(entity); const INT32 candidateMotions = candidate->GetTemplate() ? candidate->GetTemplate()->GetNumMotions() : 0; const INT32 selectedMotions = m_actor && m_actor->GetTemplate() ? m_actor->GetTemplate()->GetNumMotions() : -1; if(candidateMotions > selectedMotions) m_actor = candidate; } if(m_hierarchy == NULL && entity->GetType().IsSubClassOf(CRuFrameHierarchy::Type())) m_hierarchy = static_cast(entity); if(entity->GetType().IsSubClassOf(CRuHierarchy_GR2::Type())) { CRuHierarchy_GR2 *candidate = static_cast(entity); // Some ACT containers include a one-bone effects hierarchy before the // body hierarchy. Skin indices and ACT motions belong to the body rig, // so retain the most complete GR2 hierarchy in the entity tree. if(m_hierarchyGR2 == NULL || candidate->GetNumSubNodes() > m_hierarchyGR2->GetNumSubNodes()) m_hierarchyGR2 = candidate; } if(entity->GetType().IsSubClassOf(IRuEntity_Renderable::Type())) { ++m_stats.renderableCount; IRuEntity_Renderable *renderable = static_cast(entity); const INT32 meshCount = renderable->GetNumMeshes(); for(INT32 meshIndex = 0; meshIndex < meshCount; ++meshIndex) { IRuMesh *mesh = NULL; IRuMaterial *material = NULL; if(renderable->GetMesh(meshIndex, &mesh, &material) && mesh) ExportMesh(entity, mesh, material, meshIndex); else ++m_stats.skippedMeshCount; } } CRuEntity *child = entity->GetFirstChild(); while(child) { ExportEntityTree(child); child = child->GetNextSibling(); } } void Finish() { m_obj.flush(); m_mtl.flush(); WriteReport(); WriteRigReport(); } const ExportStats &Stats() const { return m_stats; } private: std::string EnsureMaterial(IRuMaterial *material) { std::map::iterator existing = m_materials.find(material); if(existing != m_materials.end()) return existing->second.name; MaterialInfo info; std::ostringstream name; name << "runewaker_material_" << (m_materials.size() + 1); info.name = name.str(); if(material) { const char *shaderName = material->GetShaderName(); if(shaderName) info.shaderName = shaderName; info.sourceTexture = NormalizeResourcePath(FindMaterialTexture(material)); } if(!info.sourceTexture.empty()) { std::vector candidates; candidates.push_back(info.sourceTexture); const std::string compositeMaskSuffix = "_compmask"; if(info.sourceTexture.size() > compositeMaskSuffix.size() && info.sourceTexture.compare( info.sourceTexture.size() - compositeMaskSuffix.size(), compositeMaskSuffix.size(), compositeMaskSuffix) == 0) { candidates.push_back(info.sourceTexture.substr( 0, info.sourceTexture.size() - compositeMaskSuffix.size())); } std::string sourcePath; for(std::size_t candidateIndex = 0; candidateIndex < candidates.size(); ++candidateIndex) { std::string candidate = candidates[candidateIndex]; const std::size_t slash = candidate.find_last_of("\\/"); const std::size_t period = candidate.find_last_of('.'); if(period == std::string::npos || (slash != std::string::npos && period < slash)) candidate += ".dds"; const std::string candidatePath = JoinPath(m_resourceRoot, candidate); if(FileExists(candidatePath)) { info.resolvedTexture = candidate; sourcePath = candidatePath; break; } } if(info.resolvedTexture.empty()) { info.resolvedTexture = candidates[0]; const std::size_t slash = info.resolvedTexture.find_last_of("\\/"); const std::size_t period = info.resolvedTexture.find_last_of('.'); if(period == std::string::npos || (slash != std::string::npos && period < slash)) info.resolvedTexture += ".dds"; sourcePath = JoinPath(m_resourceRoot, info.resolvedTexture); } const std::string stagedName = SafeFileName(info.resolvedTexture); const std::string stagedPath = JoinPath(JoinPath(m_outputDirectory, "textures"), stagedName); info.stagedTexture = "textures/" + stagedName; info.textureCopied = FileExists(sourcePath) && CopyFileA(sourcePath.c_str(), stagedPath.c_str(), FALSE) != FALSE; if(info.textureCopied) ++m_stats.copiedTextureCount; else ++m_stats.missingTextureCount; } DWORD diffuse = material ? material->GetDiffuse() : 0xFFFFFFFF; const double red = static_cast((diffuse >> 16) & 0xFF) / 255.0; const double green = static_cast((diffuse >> 8) & 0xFF) / 255.0; const double blue = static_cast(diffuse & 0xFF) / 255.0; const double alpha = static_cast((diffuse >> 24) & 0xFF) / 255.0; m_mtl << "\nnewmtl " << info.name << "\n"; m_mtl << "Ka 0 0 0\n"; m_mtl << "Kd " << red << " " << green << " " << blue << "\n"; m_mtl << "Ks 0 0 0\n"; m_mtl << "Ns 1\n"; m_mtl << "d " << alpha << "\n"; m_mtl << "illum 2\n"; if(info.textureCopied) m_mtl << "map_Kd " << info.stagedTexture << "\n"; m_materials[material] = info; ++m_stats.materialCount; return info.name; } void TrackBounds(const CRuVector3 &point) { if(point.x < m_stats.minX) m_stats.minX = point.x; if(point.y < m_stats.minY) m_stats.minY = point.y; if(point.z < m_stats.minZ) m_stats.minZ = point.z; if(point.x > m_stats.maxX) m_stats.maxX = point.x; if(point.y > m_stats.maxY) m_stats.maxY = point.y; if(point.z > m_stats.maxZ) m_stats.maxZ = point.z; } void WriteFace( INT32 first, INT32 second, INT32 third, INT32 vertexCount, bool hasUv, bool hasNormal, INT32 vertexBase, INT32 uvBase, INT32 normalBase) { if(first < 0 || second < 0 || third < 0 || first >= vertexCount || second >= vertexCount || third >= vertexCount || first == second || second == third || first == third) { return; } const INT32 indices[3] = { first, third, second }; m_obj << "f"; for(INT32 i = 0; i < 3; ++i) { const INT32 vertexIndex = vertexBase + indices[i]; m_obj << " " << vertexIndex; if(hasUv || hasNormal) { m_obj << "/"; if(hasUv) m_obj << (uvBase + indices[i]); if(hasNormal) m_obj << "/" << (normalBase + indices[i]); } } m_obj << "\n"; ++m_stats.triangleCount; } void ExportMesh(CRuEntity *entity, IRuMesh *mesh, IRuMaterial *material, INT32 meshIndex) { if(mesh->GetNumMorphTargets() <= 0) { ++m_stats.skippedMeshCount; return; } IRuMorphTarget *morphTarget = mesh->GetMorphTarget(0); const CRuVector3 *positions = morphTarget ? morphTarget->GetPosition() : NULL; if(!positions) { ++m_stats.skippedMeshCount; return; } const INT32 vertexCount = mesh->GetNumVertices(); if(vertexCount <= 0) { ++m_stats.skippedMeshCount; return; } const CRuVector3 *normals = morphTarget->GetNormal(); const float *uv = morphTarget->GetTextureCoordinate(ruTEXCHANNEL_DIFFUSEMAP); if(!uv && morphTarget->GetNumTextureCoordinates() > 0) uv = morphTarget->GetTextureCoordinateByIndex(0); const bool hasNormal = normals != NULL; const bool hasUv = uv != NULL; const INT32 vertexBase = m_nextVertexIndex; const INT32 uvBase = m_nextUvIndex; const INT32 normalBase = m_nextNormalIndex; std::ostringstream groupName; groupName << SafeObjectName(entity->GetObjectName(), m_stats.meshCount + 1) << "_mesh_" << meshIndex; m_obj << "\ng " << groupName.str() << "\n"; m_obj << "usemtl " << EnsureMaterial(material) << "\n"; const float *blendWeights = morphTarget->GetBlendWeight(); const UINT16 *blendJoints = morphTarget->GetBlendIndex(); if(blendWeights && blendJoints) { RigMeshInfo rigMesh; rigMesh.name = groupName.str(); rigMesh.vertexCount = vertexCount; rigMesh.weights.assign(blendWeights, blendWeights + vertexCount * 4); rigMesh.joints.assign(blendJoints, blendJoints + vertexCount * 4); m_rigMeshes.push_back(rigMesh); } const CRuMatrix4x4 &worldTransform = entity->GetWorldTransform(); for(INT32 vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { CRuVector3 worldPosition; worldTransform.TransformPoint(positions[vertexIndex], worldPosition); worldPosition.z = -worldPosition.z; TrackBounds(worldPosition); m_obj << "v " << worldPosition.x << " " << worldPosition.y << " " << worldPosition.z << "\n"; } if(hasUv) { for(INT32 vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) m_obj << "vt " << uv[vertexIndex * 2] << " " << (1.0f - uv[vertexIndex * 2 + 1]) << "\n"; } if(hasNormal) { for(INT32 vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { CRuVector3 worldNormal; worldTransform.TransformVector(normals[vertexIndex], worldNormal); worldNormal.z = -worldNormal.z; const REAL lengthSquared = worldNormal.x * worldNormal.x + worldNormal.y * worldNormal.y + worldNormal.z * worldNormal.z; if(lengthSquared > 0.0000001f) { const REAL inverseLength = 1.0f / static_cast(std::sqrt(lengthSquared)); worldNormal.x *= inverseLength; worldNormal.y *= inverseLength; worldNormal.z *= inverseLength; } m_obj << "vn " << worldNormal.x << " " << worldNormal.y << " " << worldNormal.z << "\n"; } } const UINT16 *indices = mesh->GetIndices(); const INT32 primitiveCount = mesh->GetNumPrimitives(); const RuPrimitiveType primitiveType = mesh->GetPrimitiveType(); if(primitiveType == ruPRIMTYPE_TRIANGLELIST) { for(INT32 primitiveIndex = 0; primitiveIndex < primitiveCount; ++primitiveIndex) { const INT32 first = indices ? indices[primitiveIndex * 3] : primitiveIndex * 3; const INT32 second = indices ? indices[primitiveIndex * 3 + 1] : primitiveIndex * 3 + 1; const INT32 third = indices ? indices[primitiveIndex * 3 + 2] : primitiveIndex * 3 + 2; WriteFace(first, second, third, vertexCount, hasUv, hasNormal, vertexBase, uvBase, normalBase); } } else if(primitiveType == ruPRIMTYPE_TRIANGLESTRIP) { for(INT32 primitiveIndex = 0; primitiveIndex < primitiveCount; ++primitiveIndex) { INT32 first = indices ? indices[primitiveIndex] : primitiveIndex; INT32 second = indices ? indices[primitiveIndex + 1] : primitiveIndex + 1; INT32 third = indices ? indices[primitiveIndex + 2] : primitiveIndex + 2; if((primitiveIndex & 1) != 0) { const INT32 temporary = first; first = second; second = temporary; } WriteFace(first, second, third, vertexCount, hasUv, hasNormal, vertexBase, uvBase, normalBase); } } else if(primitiveType == ruPRIMTYPE_TRIANGLEFAN) { const INT32 root = indices ? indices[0] : 0; for(INT32 primitiveIndex = 0; primitiveIndex < primitiveCount; ++primitiveIndex) { const INT32 second = indices ? indices[primitiveIndex + 1] : primitiveIndex + 1; const INT32 third = indices ? indices[primitiveIndex + 2] : primitiveIndex + 2; WriteFace(root, second, third, vertexCount, hasUv, hasNormal, vertexBase, uvBase, normalBase); } } else { ++m_stats.skippedMeshCount; } m_nextVertexIndex += vertexCount; if(hasUv) m_nextUvIndex += vertexCount; if(hasNormal) m_nextNormalIndex += vertexCount; ++m_stats.meshCount; m_stats.vertexCount += static_cast(vertexCount); } void WriteReport() { std::ofstream report(JoinPath(m_outputDirectory, "export-report.json").c_str(), std::ios::out | std::ios::trunc); if(!report) throw std::runtime_error("Unable to write export-report.json"); report << "{\n"; report << " \"schemaVersion\": 1,\n"; report << " \"sourceModel\": \"" << JsonEscape(m_modelResourcePath) << "\",\n"; report << " \"coordinateTransform\": \"three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)\",\n"; report << " \"windingReversed\": true,\n"; report << " \"stats\": {\n"; report << " \"entities\": " << m_stats.entityCount << ",\n"; report << " \"renderables\": " << m_stats.renderableCount << ",\n"; report << " \"meshes\": " << m_stats.meshCount << ",\n"; report << " \"vertices\": " << m_stats.vertexCount << ",\n"; report << " \"triangles\": " << m_stats.triangleCount << ",\n"; report << " \"skippedMeshes\": " << m_stats.skippedMeshCount << ",\n"; report << " \"materials\": " << m_stats.materialCount << ",\n"; report << " \"copiedTextures\": " << m_stats.copiedTextureCount << ",\n"; report << " \"missingTextures\": " << m_stats.missingTextureCount << "\n"; report << " },\n"; report << " \"bounds\": {\n"; report << " \"min\": [" << m_stats.minX << ", " << m_stats.minY << ", " << m_stats.minZ << "],\n"; report << " \"max\": [" << m_stats.maxX << ", " << m_stats.maxY << ", " << m_stats.maxZ << "]\n"; report << " },\n"; report << " \"materials\": [\n"; std::size_t materialIndex = 0; for(std::map::const_iterator material = m_materials.begin(); material != m_materials.end(); ++material, ++materialIndex) { const MaterialInfo &info = material->second; report << " {\n"; report << " \"name\": \"" << JsonEscape(info.name) << "\",\n"; report << " \"shader\": \"" << JsonEscape(info.shaderName) << "\",\n"; report << " \"sourceTexture\": \"" << JsonEscape(info.sourceTexture) << "\",\n"; report << " \"resolvedTexture\": \"" << JsonEscape(info.resolvedTexture) << "\",\n"; report << " \"stagedTexture\": \"" << JsonEscape(info.stagedTexture) << "\",\n"; report << " \"textureCopied\": " << (info.textureCopied ? "true" : "false") << "\n"; report << " }" << (materialIndex + 1 < m_materials.size() ? "," : "") << "\n"; } report << " ]\n"; report << "}\n"; } std::uint64_t WriteBinaryBlock(std::ofstream &binary, const void *data, std::size_t byteCount) { const std::streamoff offset = binary.tellp(); if(byteCount > 0) binary.write((const char *) data, byteCount); if(binary.fail()) throw std::runtime_error("Unable to write actor-rig.bin"); return static_cast(offset); } void CopyGrannyTransform(const granny_transform &source, float *translation, float *rotation, float *scale) { translation[0] = source.Position[0]; translation[1] = source.Position[1]; translation[2] = -source.Position[2]; // Positions move from RuneWaker to HealerMan through the Z reflection // S=diag(1,1,-1). Rotations therefore require R'=S*R*S, whose // quaternion representation is (-x,-y,z,w). Negating only quaternion // Z is not the same basis change; it made skinned vertices follow // unrelated-looking axes as soon as a clip started playing. rotation[0] = -source.Orientation[0]; rotation[1] = -source.Orientation[1]; rotation[2] = source.Orientation[2]; rotation[3] = source.Orientation[3]; const float length = static_cast(std::sqrt( rotation[0] * rotation[0] + rotation[1] * rotation[1] + rotation[2] * rotation[2] + rotation[3] * rotation[3])); if(length > 0.000001f) { rotation[0] /= length; rotation[1] /= length; rotation[2] /= length; rotation[3] /= length; } scale[0] = source.ScaleShear[0][0]; scale[1] = source.ScaleShear[1][1]; scale[2] = source.ScaleShear[2][2]; } std::vector CollectRigBones(granny_skeleton *sourceSkeleton) { std::vector bones; if(sourceSkeleton == NULL) return bones; bones.resize(sourceSkeleton->BoneCount); for(INT32 boneIndex = 0; boneIndex < sourceSkeleton->BoneCount; ++boneIndex) { const granny_bone &sourceBone = sourceSkeleton->Bones[boneIndex]; RigBoneInfo &bone = bones[boneIndex]; bone.name = sourceBone.Name ? sourceBone.Name : ""; bone.parent = sourceBone.ParentIndex; CopyGrannyTransform(sourceBone.LocalTransform, bone.translation, bone.rotation, bone.scale); } return bones; } std::vector WriteRigMotions( std::ofstream &binary, granny_model_instance *hierarchyModelInstance, granny_skeleton *sourceSkeleton) { std::vector motions; CRuACTTemplate *actorTemplate = m_actor ? m_actor->GetTemplate() : NULL; const INT32 motionCount = actorTemplate ? actorTemplate->GetNumMotions() : 0; for(INT32 motionIndex = 0; motionIndex < motionCount && sourceSkeleton; ++motionIndex) { CRuACTMotion *motion = actorTemplate->GetMotion(motionIndex); if(motion == NULL) continue; for(INT32 nodeIndex = 0; nodeIndex < motion->GetNumMotionNodes(); ++nodeIndex) { CRuACTMotionNode *motionNode = motion->GetMotionNodeByIndex(nodeIndex); IRuEntity_Controller *baseController = motionNode ? motionNode->Template_GetController() : NULL; if(baseController == NULL || !baseController->GetType().IsSubClassOf(CRuController_Hierarchy::Type())) continue; CRuController_Hierarchy *controller = static_cast(baseController); const std::string animationName = NormalizeResourcePath(controller->GetAnimationName()); if(animationName.empty()) continue; CRuAnimation_GR2 *animation = g_ruResourceManager->LoadAnimation_GR2(animationName.c_str()); if(animation == NULL || animation->GetGR2Animation() == NULL) { std::cerr << "Unable to load GR2 animation: " << animationName << "\n"; ruSAFE_RELEASE(animation); continue; } RigMotionInfo exported; exported.id = motion->GetMotionID(); exported.name = motion->GetMotionName() ? motion->GetMotionName() : ""; exported.animation = animationName; exported.duration = animation->GetDuration(); exported.sampleRate = 30.0f; exported.frameCount = static_cast(std::ceil(exported.duration * exported.sampleRate)) + 1; if(exported.frameCount < 2) exported.frameCount = 2; std::vector times(exported.frameCount); std::vector translations( static_cast(exported.frameCount) * sourceSkeleton->BoneCount * 3); std::vector rotations( static_cast(exported.frameCount) * sourceSkeleton->BoneCount * 4); std::vector scales( static_cast(exported.frameCount) * sourceSkeleton->BoneCount * 3); bool sampled = true; RuExtLink_Granny_GlobalCS()->Enter(); granny_model *sourceModel = GrannyGetSourceModel(hierarchyModelInstance); granny_model_instance *sampleInstance = sourceModel ? GrannyInstantiateModel(sourceModel) : NULL; granny_control *control = NULL; granny_local_pose *pose = NULL; if(sampleInstance) { granny_controlled_animation_builder *builder = GrannyBeginControlledAnimation(0.0f, animation->GetGR2Animation()); GrannySetTrackGroupTarget(builder, 0, sampleInstance); control = GrannyEndControlledAnimation(builder); if(control) { GrannySetControlLoopCount(control, 1); GrannySetControlForceClampedLooping(control, true); pose = GrannyNewLocalPose(sourceSkeleton->BoneCount); } } if(sampleInstance == NULL || control == NULL || pose == NULL) sampled = false; for(INT32 frameIndex = 0; frameIndex < exported.frameCount && sampled; ++frameIndex) { float time = static_cast(frameIndex) / exported.sampleRate; if(time > exported.duration) time = exported.duration; times[frameIndex] = time; GrannySetControlClockOnly(control, time); GrannySetControlRawLocalClock(control, time); sampled = GrannySampleSingleModelAnimation( sampleInstance, control, 0, sourceSkeleton->BoneCount, pose); for(INT32 boneIndex = 0; boneIndex < sourceSkeleton->BoneCount && sampled; ++boneIndex) { granny_transform *transform = GrannyGetLocalPoseTransform(pose, boneIndex); if(transform == NULL) { sampled = false; break; } float *translation = &translations[ (static_cast(frameIndex) * sourceSkeleton->BoneCount + boneIndex) * 3]; float *rotation = &rotations[ (static_cast(frameIndex) * sourceSkeleton->BoneCount + boneIndex) * 4]; float *scale = &scales[ (static_cast(frameIndex) * sourceSkeleton->BoneCount + boneIndex) * 3]; CopyGrannyTransform(*transform, translation, rotation, scale); if(frameIndex > 0) { const float *previous = rotation - sourceSkeleton->BoneCount * 4; const float dot = rotation[0] * previous[0] + rotation[1] * previous[1] + rotation[2] * previous[2] + rotation[3] * previous[3]; if(dot < 0.0f) { for(INT32 component = 0; component < 4; ++component) rotation[component] = -rotation[component]; } } } } if(pose) GrannyFreeLocalPose(pose); if(control) GrannyFreeControl(control); if(sampleInstance) GrannyFreeModelInstance(sampleInstance); RuExtLink_Granny_GlobalCS()->Leave(); if(sampled) { exported.timesByteOffset = WriteBinaryBlock(binary, ×[0], times.size() * sizeof(float)); exported.translationsByteOffset = WriteBinaryBlock(binary, &translations[0], translations.size() * sizeof(float)); exported.rotationsByteOffset = WriteBinaryBlock(binary, &rotations[0], rotations.size() * sizeof(float)); exported.scalesByteOffset = WriteBinaryBlock(binary, &scales[0], scales.size() * sizeof(float)); motions.push_back(exported); } else { std::cerr << "Unable to sample GR2 animation: " << animationName << "\n"; } ruSAFE_RELEASE(animation); break; } } return motions; } void WriteRigReport() { std::ofstream binary( JoinPath(m_outputDirectory, "actor-rig.bin").c_str(), std::ios::out | std::ios::binary | std::ios::trunc); if(binary.fail()) throw std::runtime_error("Unable to write actor-rig.bin"); std::vector meshBinary(m_rigMeshes.size()); for(std::size_t meshIndex = 0; meshIndex < m_rigMeshes.size(); ++meshIndex) { const RigMeshInfo &mesh = m_rigMeshes[meshIndex]; meshBinary[meshIndex].jointsByteOffset = WriteBinaryBlock( binary, mesh.joints.empty() ? NULL : &mesh.joints[0], mesh.joints.size() * sizeof(UINT16)); meshBinary[meshIndex].weightsByteOffset = WriteBinaryBlock( binary, mesh.weights.empty() ? NULL : &mesh.weights[0], mesh.weights.size() * sizeof(float)); } granny_model_instance *hierarchyModelInstance = m_hierarchyGR2 ? m_hierarchyGR2->GetGR2ModelInstance() : NULL; granny_skeleton *sourceSkeleton = hierarchyModelInstance ? GrannyGetSourceSkeleton(hierarchyModelInstance) : NULL; const std::vector bones = CollectRigBones(sourceSkeleton); const std::vector motions = WriteRigMotions(binary, hierarchyModelInstance, sourceSkeleton); binary.flush(); binary.close(); if(binary.fail()) throw std::runtime_error("Unable to finalize actor-rig.bin"); std::ofstream report( JoinPath(m_outputDirectory, "actor-rig.json").c_str(), std::ios::out | std::ios::trunc); if(report.fail()) throw std::runtime_error("Unable to write actor-rig.json"); report << std::setprecision(9); report << R"json({ "schemaVersion": 2, )json"; report << R"json( "sourceModel": ")json" << JsonEscape(m_modelResourcePath) << R"json(", "binary": "actor-rig.bin", "status": ")json" << (!motions.empty() ? "animated" : (!bones.empty() ? "skinned" : "static")) << R"json(", "hierarchyType": ")json" << (m_hierarchy ? "frame" : (m_hierarchyGR2 ? "gr2" : "none")) << R"json(", )json"; report << R"json( "entities": [ )json"; for(std::size_t entityIndex = 0; entityIndex < m_entities.size(); ++entityIndex) { const EntityInfo &entity = m_entities[entityIndex]; report << R"json( {"type":")json" << JsonEscape(entity.type) << R"json(","name":")json" << JsonEscape(entity.name) << R"json("})json" << (entityIndex + 1 < m_entities.size() ? "," : "") << "\n"; } report << R"json( ], )json"; report << R"json( "meshes": [ )json"; for(std::size_t meshIndex = 0; meshIndex < m_rigMeshes.size(); ++meshIndex) { const RigMeshInfo &mesh = m_rigMeshes[meshIndex]; const RigMeshBinaryInfo &offsets = meshBinary[meshIndex]; report << R"json( {"name":")json" << JsonEscape(mesh.name) << R"json(","vertexCount":)json" << mesh.vertexCount << R"json(,"joints":{"byteOffset":)json" << offsets.jointsByteOffset << R"json(,"count":)json" << mesh.joints.size() << R"json(,"componentType":"uint16"},"weights":{"byteOffset":)json" << offsets.weightsByteOffset << R"json(,"count":)json" << mesh.weights.size() << R"json(,"componentType":"float32"}})json" << (meshIndex + 1 < m_rigMeshes.size() ? "," : "") << "\n"; } report << R"json( ], )json"; report << R"json( "skeleton": {"boneCount":)json" << bones.size() << R"json(,"bones":[ )json"; for(std::size_t boneIndex = 0; boneIndex < bones.size(); ++boneIndex) { const RigBoneInfo &bone = bones[boneIndex]; report << R"json( {"index":)json" << boneIndex << R"json(,"name":")json" << JsonEscape(bone.name) << R"json(","parent":)json" << bone.parent; report << R"json(,"translation":[)json" << bone.translation[0] << "," << bone.translation[1] << "," << bone.translation[2] << R"json(],"rotation":[)json" << bone.rotation[0] << "," << bone.rotation[1] << "," << bone.rotation[2] << "," << bone.rotation[3]; report << R"json(],"scale":[)json" << bone.scale[0] << "," << bone.scale[1] << "," << bone.scale[2] << R"json(]})json" << (boneIndex + 1 < bones.size() ? "," : "") << "\n"; } report << R"json( ]}, )json"; report << R"json( "motions": [ )json"; for(std::size_t motionIndex = 0; motionIndex < motions.size(); ++motionIndex) { const RigMotionInfo &motion = motions[motionIndex]; const std::size_t boneCount = bones.size(); report << R"json( {"id":)json" << motion.id << R"json(,"name":")json" << JsonEscape(motion.name) << R"json(","animation":")json" << JsonEscape(motion.animation) << R"json(","duration":)json" << motion.duration << R"json(,"sampleRate":)json" << motion.sampleRate << R"json(,"frameCount":)json" << motion.frameCount; report << R"json(,"times":{"byteOffset":)json" << motion.timesByteOffset << R"json(,"count":)json" << motion.frameCount << R"json(},"translations":{"byteOffset":)json" << motion.translationsByteOffset << R"json(,"count":)json" << static_cast(motion.frameCount) * boneCount * 3; report << R"json(},"rotations":{"byteOffset":)json" << motion.rotationsByteOffset << R"json(,"count":)json" << static_cast(motion.frameCount) * boneCount * 4 << R"json(},"scales":{"byteOffset":)json" << motion.scalesByteOffset << R"json(,"count":)json" << static_cast(motion.frameCount) * boneCount * 3 << R"json(}})json" << (motionIndex + 1 < motions.size() ? "," : "") << "\n"; } report << R"json( ] )json"; report << "}\n"; report.flush(); report.close(); if(report.fail()) throw std::runtime_error("Unable to finalize actor-rig.json"); std::cout << "Exported " << bones.size() << " bones and " << motions.size() << " original GR2 motion clips.\n"; } void WriteRigReportLegacy() { std::ofstream report(JoinPath(m_outputDirectory, "actor-rig.json").c_str(), std::ios::out | std::ios::trunc); if(!report) throw std::runtime_error("Unable to write actor-rig.json"); report << std::setprecision(9); report << "{\n"; report << " \"schemaVersion\": 1,\n"; report << " \"sourceModel\": \"" << JsonEscape(m_modelResourcePath) << "\",\n"; report << " \"coordinateTransform\": \"three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)\",\n"; report << " \"status\": \"" << ((m_hierarchy || m_hierarchyGR2) && !m_rigMeshes.empty() ? "skinned" : "static") << "\",\n"; report << " \"hierarchyType\": \"" << (m_hierarchy ? "frame" : (m_hierarchyGR2 ? "gr2" : "none")) << "\",\n"; report << " \"entities\": [\n"; for(std::size_t entityIndex = 0; entityIndex < m_entities.size(); ++entityIndex) { const EntityInfo &entity = m_entities[entityIndex]; report << " {\"type\":\"" << JsonEscape(entity.type) << "\",\"name\":\"" << JsonEscape(entity.name) << "\"}" << (entityIndex + 1 < m_entities.size() ? "," : "") << "\n"; } report << " ],\n"; report << " \"meshes\": [\n"; for(std::size_t meshIndex = 0; meshIndex < m_rigMeshes.size(); ++meshIndex) { const RigMeshInfo &mesh = m_rigMeshes[meshIndex]; report << " {\n"; report << " \"name\": \"" << JsonEscape(mesh.name) << "\",\n"; report << " \"vertexCount\": " << mesh.vertexCount << ",\n"; report << " \"joints\": ["; for(std::size_t i = 0; i < mesh.joints.size(); ++i) report << (i ? "," : "") << mesh.joints[i]; report << "],\n"; report << " \"weights\": ["; for(std::size_t i = 0; i < mesh.weights.size(); ++i) report << (i ? "," : "") << mesh.weights[i]; report << "]\n"; report << " }" << (meshIndex + 1 < m_rigMeshes.size() ? "," : "") << "\n"; } report << " ],\n"; const INT32 boneCount = m_hierarchy ? m_hierarchy->GetNumSubNodes() : (m_hierarchyGR2 ? m_hierarchyGR2->GetNumSubNodes() : 0); report << " \"skeleton\": {\n"; report << " \"boneCount\": " << boneCount << ",\n"; report << " \"bones\": [\n"; CRuAnimKeyFrame *bindFrames = m_hierarchy ? m_hierarchy->GetInterpolator()->GetKeyFrames() : NULL; const INT32 serializedBoneCount = m_hierarchy ? boneCount : 0; for(INT32 boneIndex = 0; boneIndex < serializedBoneCount; ++boneIndex) { const CRuAnimKeyFrame &frame = bindFrames[boneIndex]; report << " {\"index\":" << boneIndex << ",\"parent\":" << m_hierarchy->GetSubNodeParentIndex(boneIndex) << ",\"translation\":[" << frame.m_translation.x << "," << frame.m_translation.y << "," << frame.m_translation.z << "]" << ",\"rotation\":[" << frame.m_rotation.x << "," << frame.m_rotation.y << "," << frame.m_rotation.z << "," << frame.m_rotation.w << "]" << ",\"scale\":[" << frame.m_scale.x << "," << frame.m_scale.y << "," << frame.m_scale.z << "]}" << (boneIndex + 1 < serializedBoneCount ? "," : "") << "\n"; } report << " ]\n"; report << " },\n"; report << " \"motions\": [\n"; bool wroteMotion = false; CRuACTTemplate *actorTemplate = m_actor ? m_actor->GetTemplate() : NULL; const INT32 motionCount = actorTemplate ? actorTemplate->GetNumMotions() : 0; for(INT32 motionIndex = 0; motionIndex < motionCount; ++motionIndex) { CRuACTMotion *motion = actorTemplate->GetMotion(motionIndex); if(!motion) continue; for(INT32 nodeIndex = 0; nodeIndex < motion->GetNumMotionNodes(); ++nodeIndex) { CRuACTMotionNode *motionNode = motion->GetMotionNodeByIndex(nodeIndex); IRuEntity_Controller *baseController = motionNode ? motionNode->Template_GetController() : NULL; if(!baseController || !baseController->GetType().IsSubClassOf(CRuController_Hierarchy::Type())) continue; CRuController_Hierarchy *controller = static_cast(baseController); const std::string animationName = NormalizeResourcePath(controller->GetAnimationName()); if(animationName.empty()) continue; CRuAnimation *animation = g_ruResourceManager->LoadAnimation(animationName.c_str()); if(!animation) continue; if(wroteMotion) report << ",\n"; wroteMotion = true; const float duration = animation->GetDuration(); const INT32 frameCount = animation->GetNumKeyFrames(); const INT32 animationNodeCount = animation->GetNumNodes(); CRuAnimKeyFrame *frames = animation->GetKeyFrames(); const char *motionName = motion->GetMotionName(); report << " {\n"; report << " \"id\": " << motion->GetMotionID() << ",\n"; report << " \"name\": \"" << JsonEscape(motionName ? motionName : "") << "\",\n"; report << " \"animation\": \"" << JsonEscape(animationName) << "\",\n"; report << " \"duration\": " << duration << ",\n"; report << " \"sourceNodeCount\": " << animationNodeCount << ",\n"; report << " \"updateNodes\": ["; for(INT32 updateIndex = 0; updateIndex < animation->m_numUpdateNodes; ++updateIndex) report << (updateIndex ? "," : "") << animation->m_updateNodes[updateIndex]; report << "],\n"; report << " \"tracks\": [\n"; INT32 frameCursor = 0; const INT32 exportedNodeCount = min(boneCount, animationNodeCount); for(INT32 animationNodeIndex = 0; animationNodeIndex < exportedNodeCount; ++animationNodeIndex) { const INT32 trackStart = frameCursor; while(frameCursor < frameCount) { const bool isLast = frames[frameCursor].m_time >= duration - 0.000001f; ++frameCursor; if(isLast) break; } const INT32 trackEnd = frameCursor; report << " {\"bone\":" << animationNodeIndex << ",\"times\":["; for(INT32 frameIndex = trackStart; frameIndex < trackEnd; ++frameIndex) report << (frameIndex > trackStart ? "," : "") << frames[frameIndex].m_time; report << "],\"translation\":["; for(INT32 frameIndex = trackStart; frameIndex < trackEnd; ++frameIndex) { const CRuVector3 &value = frames[frameIndex].m_translation; report << (frameIndex > trackStart ? "," : "") << value.x << "," << value.y << "," << value.z; } report << "],\"rotation\":["; for(INT32 frameIndex = trackStart; frameIndex < trackEnd; ++frameIndex) { const CRuQuaternion &value = frames[frameIndex].m_rotation; report << (frameIndex > trackStart ? "," : "") << value.x << "," << value.y << "," << value.z << "," << value.w; } report << "],\"scale\":["; for(INT32 frameIndex = trackStart; frameIndex < trackEnd; ++frameIndex) { const CRuVector3 &value = frames[frameIndex].m_scale; report << (frameIndex > trackStart ? "," : "") << value.x << "," << value.y << "," << value.z; } report << "]}" << (animationNodeIndex + 1 < exportedNodeCount ? "," : "") << "\n"; } report << " ]\n"; report << " }"; ruSAFE_RELEASE(animation); break; } } report << "\n ]\n"; report << "}\n"; } std::string m_resourceRoot; std::string m_outputDirectory; std::string m_modelResourcePath; std::ofstream m_obj; std::ofstream m_mtl; std::map m_materials; std::vector m_rigMeshes; std::vector m_entities; ExportStats m_stats; CRuACTEntity *m_actor; CRuFrameHierarchy *m_hierarchy; CRuHierarchy_GR2 *m_hierarchyGR2; INT32 m_nextVertexIndex; INT32 m_nextUvIndex; INT32 m_nextNormalIndex; }; } int main(int argc, char **argv) { const bool wdbReportMode = argc == 4 && std::string(argv[1]) == "--wdb-report"; const bool modelExportMode = argc == 4 || argc == 5; if(!modelExportMode) { std::cerr << "Usage:\n" << " runewaker_model_exporter \n" << " runewaker_model_exporter \n" << " runewaker_model_exporter --wdb-report \n"; return 2; } if(RuInitialize_Core() == FALSE) { std::cerr << "RuInitialize_Core failed.\n"; return 3; } if(RuInitialize_NULL() == FALSE) { std::cerr << "RuInitialize_NULL failed.\n"; RuShutdown(); return 4; } if(wdbReportMode) { const int exitCode = WriteWdbReport(argv[2], argv[3]); RuShutdown(); return exitCode; } const std::string resourceRoot = argv[1]; const std::string modelResourcePath = NormalizeResourcePath(argv[2]); const std::string outputDirectory = argv[3]; g_ruResourceManager->SetRootDirectory(resourceRoot.c_str()); CRuEntity *entity = g_ruResourceManager->LoadEntity(modelResourcePath.c_str()); if(entity == NULL) { std::cerr << "Unable to load ROS entity: " << modelResourcePath << "\n"; RuShutdown(); return 5; } if(argc == 5) { try { const PaperdollAssembly assembly = ReadPaperdollAssembly(argv[4]); if(!ApplyPaperdollAssembly(entity, assembly)) { std::cerr << "Paperdoll assembly did not produce a valid synchronous model.\n"; ruSAFE_RELEASE(entity); RuShutdown(); return 9; } } catch(const std::exception &error) { std::cerr << "Paperdoll assembly failed: " << error.what() << "\n"; ruSAFE_RELEASE(entity); RuShutdown(); return 9; } } int exitCode = 0; try { ObjExporter exporter(resourceRoot, outputDirectory, modelResourcePath); if(!exporter.Open()) { std::cerr << "Unable to open OBJ/MTL output files in: " << outputDirectory << "\n"; exitCode = 6; } else { exporter.ExportEntityTree(entity); exporter.Finish(); const ExportStats &stats = exporter.Stats(); std::cout << "Exported " << stats.meshCount << " meshes, " << stats.vertexCount << " vertices, " << stats.triangleCount << " triangles, and " << stats.copiedTextureCount << " textures.\n"; if(stats.meshCount == 0 || stats.triangleCount == 0) { std::cerr << "The entity loaded, but no triangle meshes were exported.\n"; exitCode = 7; } } } catch(const std::exception &error) { std::cerr << "Export failed: " << error.what() << "\n"; exitCode = 8; } ruSAFE_RELEASE(entity); RuShutdown(); return exitCode; }