1544 lines
54 KiB
C++
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, ×[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;
|
|
}
|