Files
healer-man/scripts/runewaker-pipeline/native/runewaker_model_exporter.cpp
T
2026-08-14 15:56:39 -04:00

1544 lines
54 KiB
C++

#include <windows.h>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#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<double>::infinity();
double minY = std::numeric_limits<double>::infinity();
double minZ = std::numeric_limits<double>::infinity();
double maxX = -std::numeric_limits<double>::infinity();
double maxY = -std::numeric_limits<double>::infinity();
double maxZ = -std::numeric_limits<double>::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<float> weights;
std::vector<UINT16> 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<PaperdollPartSpec> parts;
RUCOLOR skinColor = 0;
RUCOLOR hairColor = 0;
};
std::vector<std::string> SplitTabs(const std::string &line)
{
std::vector<std::string> 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<RUCOLOR>(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<std::string> 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<CRuPaperdoll *>(paperdoll)->BuildPaperdoll())
return false;
RuEntity_RefreshHierarchySubNodeBounds(entity);
return !paperdoll->IsPaperdollBuilding()
&& static_cast<CRuPaperdoll *>(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<unsigned char>(value[i]);
if(character >= 'A' && character <= 'Z')
character = static_cast<unsigned char>(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<unsigned char>(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<unsigned int>(character)
<< std::dec;
}
else
{
output << static_cast<char>(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<unsigned char>(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<IRuObject *> 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<CRuWorld_EntityDescriptor *>(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<INT32>(descriptor->GetDetailClass()) << ",\n";
report << " \"zoneType\": " << static_cast<INT32>(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<CRuACTEntity *>(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<CRuFrameHierarchy *>(entity);
if(entity->GetType().IsSubClassOf(CRuHierarchy_GR2::Type()))
{
CRuHierarchy_GR2 *candidate = static_cast<CRuHierarchy_GR2 *>(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<IRuEntity_Renderable *>(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<IRuMaterial *, MaterialInfo>::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<std::string> 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<double>((diffuse >> 16) & 0xFF) / 255.0;
const double green = static_cast<double>((diffuse >> 8) & 0xFF) / 255.0;
const double blue = static_cast<double>(diffuse & 0xFF) / 255.0;
const double alpha = static_cast<double>((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<REAL>(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<std::uint64_t>(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<IRuMaterial *, MaterialInfo>::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<std::uint64_t>(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<float>(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<RigBoneInfo> CollectRigBones(granny_skeleton *sourceSkeleton)
{
std::vector<RigBoneInfo> 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<RigMotionInfo> WriteRigMotions(
std::ofstream &binary,
granny_model_instance *hierarchyModelInstance,
granny_skeleton *sourceSkeleton)
{
std::vector<RigMotionInfo> 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<CRuController_Hierarchy *>(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<INT32>(std::ceil(exported.duration * exported.sampleRate)) + 1;
if(exported.frameCount < 2)
exported.frameCount = 2;
std::vector<float> times(exported.frameCount);
std::vector<float> translations(
static_cast<std::size_t>(exported.frameCount) * sourceSkeleton->BoneCount * 3);
std::vector<float> rotations(
static_cast<std::size_t>(exported.frameCount) * sourceSkeleton->BoneCount * 4);
std::vector<float> scales(
static_cast<std::size_t>(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<float>(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<std::size_t>(frameIndex) * sourceSkeleton->BoneCount + boneIndex) * 3];
float *rotation = &rotations[
(static_cast<std::size_t>(frameIndex) * sourceSkeleton->BoneCount + boneIndex) * 4];
float *scale = &scales[
(static_cast<std::size_t>(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, &times[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<RigMeshBinaryInfo> 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<RigBoneInfo> bones = CollectRigBones(sourceSkeleton);
const std::vector<RigMotionInfo> 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<std::size_t>(motion.frameCount) * boneCount * 3;
report << R"json(},"rotations":{"byteOffset":)json" << motion.rotationsByteOffset
<< R"json(,"count":)json"
<< static_cast<std::size_t>(motion.frameCount) * boneCount * 4
<< R"json(},"scales":{"byteOffset":)json" << motion.scalesByteOffset
<< R"json(,"count":)json"
<< static_cast<std::size_t>(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<CRuController_Hierarchy *>(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<IRuMaterial *, MaterialInfo> m_materials;
std::vector<RigMeshInfo> m_rigMeshes;
std::vector<EntityInfo> 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 <resource-root> <model-resource-path> <output-directory>\n"
<< " runewaker_model_exporter <resource-root> <model-resource-path> <output-directory> <paperdoll-assembly.tsv>\n"
<< " runewaker_model_exporter --wdb-report <wdb-path> <output-json>\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;
}