chore: Drop support for SWTOR assets

This doesn't really add to the project.
This commit is contained in:
Vsevolod Kremianskii 2021-03-08 12:32:01 +07:00
parent 013f746cff
commit df8fec4193
11 changed files with 5 additions and 1152 deletions

View file

@ -236,21 +236,6 @@ set_target_properties(librender PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINA
## END librender static library
## libtor static library
set(TOR_HEADERS
src/experimental/tor/gr2file.h
src/experimental/tor/jbafile.h)
set(TOR_SOURCES
src/experimental/tor/gr2file.cpp
src/experimental/tor/jbafile.cpp)
add_library(libtor STATIC ${TOR_HEADERS} ${TOR_SOURCES})
set_target_properties(libtor PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
## END libtor static library
## libaudio static library
set(AUDIO_HEADERS
@ -686,7 +671,7 @@ add_executable(reone ${REONE_HEADERS} ${REONE_SOURCES})
set_target_properties(reone PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
target_link_libraries(reone PRIVATE
libmp libgame libscript libgui libscene libvideo libaudio libtor librender libresource libcommon
libmp libgame libscript libgui libscene libvideo libaudio librender libresource libcommon
libs3tc
${Boost_FILESYSTEM_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_SYSTEM_LIBRARY}
GLEW::GLEW
@ -751,7 +736,7 @@ if(BUILD_TESTS)
foreach(TEST_FILE ${TEST_FILES})
get_filename_component(TEST_NAME "${TEST_FILE}" NAME_WE)
add_executable(test_${TEST_NAME} ${TEST_FILE})
target_link_libraries(test_${TEST_NAME} PRIVATE libgame libscript libresource libcommon ${Boost_FILESYSTEM_LIBRARY})
target_link_libraries(test_${TEST_NAME} PRIVATE libgame libscript libresource libcommon ${Boost_FILESYSTEM_LIBRARY} ${Boost_SYSTEM_LIBRARY})
if(WIN32)
target_link_libraries(test_${TEST_NAME} PRIVATE SDL2::SDL2)

View file

@ -1 +1 @@
<mxfile host="app.diagrams.net" modified="2021-02-08T10:55:18.929Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.146 Safari/537.36" etag="Jn16g3SQKS4mHqmAybD-" version="14.3.0" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">3Zlbd6IwEMc/jY/2cPP2uK292x53e/H0aU9KRshuIDSEqv30GzQoSjyrrYhVfAj/hJD8ZjIToGafBeNLjiL/jmGgNcvA45rdrVmW6VhWLf0beKKUdsuZKR4nWGkL4YF8gBINpSYEQ7zUUDBGBYmWRZeFIbhiSUOcs9FysyGjy3eNkAcF4cFFtKgOCBb+TG1brYV+BcTzszubzc6sJkBZYzWT2EeYjXKSfV6zzzhjYlYKxmdAU3oZl8H1ZEB7f5uXNz/jN/R0evt4/1yfdXaxzSXzKXAIxae7TkbDm+jtGXcGHzAJ78c/ux/Duj3r+h3RRPFScxWTDKDHWRKpZsAFjHVmQ69Zc2PDwZpzgtL3gAUg+ERep3q3Oqof5Xb1duZPo4URrabS/LwBsxEg5TjevPMFHFlQfLZgZRVYwTgCTgI5UUQL3KS7RGkxCegFR4Esno58IuAhQm6qj+SCk5ovAjmKrplWq6mZhpFWZJOy07O1BsiDXm/iIuj/cjQ7J52l39exjh57j78j87af+Hd95+5lMnix65lhc1wpeQ2itUADhpNUO0WUeKEUKAzloE9jCZaEXm961rWMXAtXIgIuhRQgkcHhh6oQbMUG20DV2GOtQ5vL/uzYJ007fxScO3WCglEazknDyR8lubrWJILxPdpk5w4/T2nfyhLOoQbolfhsmo3KA3R7H6zWW+kTMbY0FKYmV4GbTKcWl5yqhpREV/Lka2mrXSFSfZ4qLkUOLISDzlPtNQbYNE9pFrU+HJZGvaGnXk8fItZ78vdk71QHXzuDInu5DeCIk9JjyEZxYxdpy9A8VzQcDWTbKcvFmzrKXopwY++e++5O/JuiV6B9FhNBmPbK3kqDgGA8Hc82NtpmZdgrZus0GxXHpZbOaC4LAgnkQILSbtA7q0um3dljWPLCQYwDmFzBE3DeQ716o6fZ3Ej2KMGEHRf64kuQytEX34GkGQFCDPt8NqyC/V5DjpZ98V3dlH3MEu4ezC60pKBTPf3i7l/SjyWqI0NvNg8u6Gi3oV5Cjhx89T6v3ZnGLieROHL2us8NpbH3/9Qj0g/7v26uQ68rd5cX8KR/BftOMOxzk7M33i2nPN7ydPGRblqX+9Zpn/8D</diagram></mxfile>
<mxfile host="app.diagrams.net" modified="2021-03-08T05:27:07.122Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36" etag="n5N0cfmXNYK2PiidXptk" version="14.4.4" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">3Zlbc+IgFMc/jY+ZyU2rj2vt3Xbs9uL0qUPDMWGXhJSQGvvplxjiLdixl6jrjA/wBwn8zuFwQhrOcZidcRQH1wwDbdgmzhpOr2Hblmvbjfxn4olSOo5VKD4nWGlz4Y68gxJNpaYEQ7LUUTBGBYmXRY9FEXhiSUOcs/FytxGjy0+NkQ8V4c5DtKoOCRZBobbto7l+DsQPyidbrU7REqKys1pJEiDMxguSc9JwjjljoiiF2THQnF7JZXgxGdL+39bZ5W3yih66V/c3j0Yx2Oln/jJbAodIfHnodDy6jF8fcWf4DpPoJrvtvY+MZjH0G6Kp4kXJC0ecSJMVixaTkqRcf5wX05CechTKYnccEAF3MfJyfSw9SGqBCKmsWXmzAm6ZZt5QYnby2oZrUmt/Ay4gW7CoWuMZsBAEn8guqrW0lvJXwzJLTxzPzd8sewULpndcJSLlcv5s8DlWWVBkP0HZqVCuwPU5S+NvMpltIvRSDmt+zKqzAqutYWU7GlaWXRcru8IKshg4CSUTRLfulB/ac2OnNHRc3bq4ju/798+xdTVIg+uBe/00GT45sxksb/UwXks0ZDjNtS6ixI+kQGEkZ9lNJFkS+f1prWebCz08iQ24FHIyRMbgX6pBsBUj1AXaspY9ulnlntu9wr1Zlzu7+7r1V8OkpUFlt7a69dtfZvUh+q+z2h0Kq4ICMvDS6ZTrPplHlMTnsvKJgNheg3pHSPUBsLoVObAI9joAbg62bN1d/NNTr2aYU+pG/hKw3pP/T/bunsFv6Y58Pw8QG3OfUf0R8hS9AB2whAjCtP/sr3QICcbT+XzzfFxrM2flHOy0mjs22pHOaB4LQwlkT7bLz6B3V3OQdmeLG8aPhgkOYXIOD8B5H/WNZl9z7Er2KMWEHRb66ovfztFX3/vymwiIsCR12Oy3GnK07Kv3E1P2CUu5tzf5UU1BZ/f0q3mppJ9IVAeG3mrtXdDRXn/6KTlw8Lv3eW1mmnicxOLA2euuWGtjH/wxYjKIBr8vLyK/J7PLU3gwtBeBbwTDNpOcrfE+cuvjLavzzz/TtoWvaM7JPw==</diagram></mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View file

@ -1,554 +0,0 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "gr2file.h"
#include <stdexcept>
#include <boost/algorithm/string.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include "glm/gtc/type_ptr.hpp"
#include "../../common/log.h"
#include "../../common/streamutil.h"
#include "../../render/textures.h"
#include "../../resource/resources.h"
#include "jbafile.h"
using namespace std;
using namespace reone::render;
using namespace reone::resource;
namespace pt = boost::property_tree;
namespace reone {
namespace tor {
static bool g_debugSkeleton = false;
Gr2File::Gr2File(string resRef, vector<shared_ptr<Animation>> animations, shared_ptr<Model> skeleton) :
BinaryFile(4, "GAWB"),
_resRef(move(resRef)),
_animations(move(animations)),
_skeleton(move(skeleton)) {
}
void Gr2File::doLoad() {
// Adapted from multiple sources:
//
// https://github.com/SWTOR-Extractors-Modders-Dataminers/Granny2-Plug-In-Blender-2.8x/blob/v1.0.0.1/io_scene_gr2/import_gr2.py
// https://forum.xentax.com/viewtopic.php?f=16&t=9703&start=30#p94880
// https://forum.xentax.com/viewtopic.php?f=16&t=11317&start=15#p128702
seek(0x10);
uint32_t numCachedOffsets = readUint32();
_fileType = static_cast<FileType>(readUint32());
_numMeshes = readUint16();
_numMaterials = readUint16();
_numBones = readUint16();
_numAttachments = readUint16();
seek(0x50);
uint32_t offsetCachedOffsets = readUint32();
_offsetMeshHeader = readUint32();
_offsetMaterialHeader = readUint32();
_offsetBoneStructure = readUint32();
_offsetAttachments = readUint32();
loadMaterials();
loadMeshes();
loadSkeletonBones();
loadAttachments();
loadModel();
}
void Gr2File::loadMeshes() {
for (uint16_t i = 0; i < _numMeshes; ++i) {
seek(_offsetMeshHeader + i * 0x28);
_meshes.push_back(readMesh());
}
}
unique_ptr<Gr2File::Gr2Mesh> Gr2File::readMesh() {
uint32_t offsetName = readUint32();
auto mesh = make_unique<Gr2Mesh>();
mesh->header.name = readCStringAt(offsetName);
ignore(4);
mesh->header.numPieces = readUint16();
mesh->header.numUsedBones = readUint16();
mesh->header.vertexMask = readUint16();
mesh->header.vertexSize = readUint16();
mesh->header.numVertices = readUint32();
mesh->header.numIndices = readUint32();
mesh->header.offsetVertices = readUint32();
mesh->header.offsetPieces = readUint32();
mesh->header.offsetIndices = readUint32();
mesh->header.offsetBones = readUint32();
for (uint16_t i = 0; i < mesh->header.numPieces; ++i) {
seek(mesh->header.offsetPieces + i * 0x30);
mesh->pieces.push_back(readMeshPiece());
}
mesh->mesh = readModelMesh(*mesh);
for (uint16_t i = 0; i < mesh->header.numUsedBones; ++i) {
seek(mesh->header.offsetBones + i * 0x1c);
mesh->bones.push_back(readMeshBone());
}
return move(mesh);
}
unique_ptr<Gr2File::MeshPiece> Gr2File::readMeshPiece() {
auto piece = make_unique<MeshPiece>();
piece->startFaceIdx = readUint32();
piece->numFaces = readUint32();
piece->materialIndex = readUint32();
piece->pieceIndex = readUint32();
ignore(0x24); // bounding box
return move(piece);
}
static float convertByteToFloat(uint8_t value) {
return 2.0f * value / 255.0f - 1.0f;
}
static float convertHalfFloatToFloat(uint16_t value) {
uint32_t sign = (value & 0x8000) ? 1 : 0;
uint32_t exponent = ((value & 0x7c00) >> 10) - 16;
uint32_t mantissa = value & 0x03ff;
uint32_t tmp = ((sign << 31) | ((exponent + 127) << 23) | (mantissa << 13));
float result = 2.0f * *reinterpret_cast<float *>(&tmp);
return result;
}
static glm::vec3 computeBitangent(float sign, const glm::vec3 &normal, const glm::vec3 &tangent) {
return sign * glm::cross(tangent, normal);
}
unique_ptr<ModelMesh> Gr2File::readModelMesh(const Gr2Mesh &mesh) {
int unkFlags = mesh.header.vertexMask & ~0x1f2;
if (unkFlags != 0) {
warn(boost::format("GR2: unrecognized vertex flags: 0x%x") % unkFlags);
}
Mesh::VertexOffsets offsets;
vector<float> vertices;
seek(mesh.header.offsetVertices);
vector<uint8_t> gr2Vertices(readArray<uint8_t>(static_cast<size_t>(mesh.header.numVertices) * mesh.header.vertexSize));
const uint8_t *gr2VerticesPtr = &gr2Vertices[0];
for (uint32_t i = 0; i < mesh.header.numVertices; ++i) {
int stride = 0;
int gr2Stride = 0;
// Vertex coordinates
vertices.push_back(*reinterpret_cast<const float *>(gr2VerticesPtr + gr2Stride + 0));
vertices.push_back(*reinterpret_cast<const float *>(gr2VerticesPtr + gr2Stride + 4));
vertices.push_back(*reinterpret_cast<const float *>(gr2VerticesPtr + gr2Stride + 8));
stride += 3 * sizeof(float);
gr2Stride += 3 * sizeof(float);
// Bone weights and indices
if (mesh.header.vertexMask & 0x100) {
vertices.push_back(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 0) / 255.0f);
vertices.push_back(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 1) / 255.0f);
vertices.push_back(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 2) / 255.0f);
vertices.push_back(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 3) / 255.0f);
vertices.push_back(static_cast<float>(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 4)));
vertices.push_back(static_cast<float>(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 5)));
vertices.push_back(static_cast<float>(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 6)));
vertices.push_back(static_cast<float>(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 7)));
offsets.boneWeights = stride;
offsets.boneIndices = stride + 4 * sizeof(float);
stride += 8 * sizeof(float);
gr2Stride += 8;
}
// Normal and tangent space
if (mesh.header.vertexMask & 0x2) {
glm::vec3 normal;
normal.x = convertByteToFloat(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 0));
normal.y = convertByteToFloat(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 1));
normal.z = convertByteToFloat(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 2));
vertices.push_back(normal.x);
vertices.push_back(normal.y);
vertices.push_back(normal.z);
glm::vec3 tangent;
tangent.x = convertByteToFloat(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 4));
tangent.y = convertByteToFloat(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 5));
tangent.z = convertByteToFloat(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 6));
float sign = convertByteToFloat(*reinterpret_cast<const uint8_t *>(gr2VerticesPtr + gr2Stride + 7));
vertices.push_back(tangent.x);
vertices.push_back(tangent.y);
vertices.push_back(tangent.z);
glm::vec3 bitangent(computeBitangent(sign, normal, tangent));
vertices.push_back(bitangent.x);
vertices.push_back(bitangent.y);
vertices.push_back(bitangent.z);
offsets.normals = stride;
offsets.tangents = stride + 3 * sizeof(float);
offsets.bitangents = stride + 6 * sizeof(float);
stride += 9 * sizeof(float);
gr2Stride += 8;
}
// Color
if (mesh.header.vertexMask & 0x10) {
gr2Stride += 4;
}
// Texture 1 coordinates
if (mesh.header.vertexMask & 0x20) {
vertices.push_back(convertHalfFloatToFloat(*reinterpret_cast<const uint16_t *>(gr2VerticesPtr + gr2Stride + 0)));
vertices.push_back(-1.0f * convertHalfFloatToFloat(*reinterpret_cast<const uint16_t *>(gr2VerticesPtr + gr2Stride + 2)));
offsets.texCoords1 = stride;
stride += 2 * sizeof(float);
gr2Stride += 2 * sizeof(uint16_t);
}
// Texture 2 coordinates
if (mesh.header.vertexMask & 0x40) {
gr2Stride += 4;
}
// Texture 3 coordinates
if (mesh.header.vertexMask & 0x80) {
gr2Stride += 4;
}
gr2VerticesPtr += mesh.header.vertexSize;
offsets.stride = stride;
}
vector<uint16_t> indices;
seek(mesh.header.offsetIndices);
for (uint16_t i = 0; i < mesh.header.numPieces; ++i) {
vector<uint16_t> pieceIndices(readArray<uint16_t>(3 * mesh.pieces[i]->numFaces));
indices.insert(indices.end(), pieceIndices.begin(), pieceIndices.end());
}
auto simpleMesh = make_unique<Mesh>(mesh.header.numVertices, move(vertices), move(indices), move(offsets));
simpleMesh->computeAABB();
auto modelMesh = make_unique<ModelMesh>(move(simpleMesh));
modelMesh->setRender(true);
modelMesh->setShadow(true);
modelMesh->setDiffuseColor(glm::vec3(0.8f));
modelMesh->setAmbientColor(glm::vec3(0.2f));
// Fill mesh textures from materials
if (!_materials.empty()) {
// TODO: add support for multiple materials
string materialResRef(_materials[0]);
if (materialResRef == "default") {
materialResRef = _resRef + "_v01";
}
shared_ptr<ByteArray> matData(Resources::instance().get(materialResRef, ResourceType::Mat));
if (matData) {
pt::ptree tree;
pt::read_xml(*wrap(matData), tree);
string diffuseResRef;
string bumpmapResRef;
for (auto &material : tree.get_child("Material")) {
if (material.first != "input") continue;
string semantic(material.second.get("semantic", ""));
string type(material.second.get("type", ""));
if (type != "texture") continue;
string value(material.second.get("value", ""));
int lastSlashIdx = static_cast<int>(value.find_last_of('\\'));
if (lastSlashIdx != -1) {
value = value.substr(lastSlashIdx + 1ll);
}
if (semantic == "DiffuseMap") {
diffuseResRef = value;
} else if (semantic == "RotationMap1") {
bumpmapResRef = value;
}
}
if (!diffuseResRef.empty()) {
modelMesh->setDiffuseTexture(Textures::instance().get(diffuseResRef, TextureUsage::Diffuse));
if (!bumpmapResRef.empty()) {
modelMesh->setBumpmapTexture(Textures::instance().get(bumpmapResRef, TextureUsage::Bumpmap), true);
}
}
}
}
return move(modelMesh);
}
unique_ptr<Gr2File::MeshBone> Gr2File::readMeshBone() {
uint32_t offsetName = readUint32();
auto bone = make_unique<MeshBone>();
bone->name = readCStringAt(offsetName);
bone->bounds = readArray<float>(6);
return move(bone);
}
void Gr2File::loadMaterials() {
if (_numMaterials == 0) {
for (auto &mesh : _meshes) {
if (mesh->header.vertexMask & 0x20) {
for (uint16_t i = 0; i < mesh->header.numPieces; ++i) {
_materials.push_back(str(boost::format("%s.%03d") % mesh->header.name % i));
}
}
}
return;
}
seek(_offsetMaterialHeader);
for (uint16_t i = 0; i < _numMaterials; ++i) {
uint32_t offsetName = readUint32();
_materials.push_back(readCStringAt(offsetName));
}
}
void Gr2File::loadSkeletonBones() {
for (uint16_t i = 0; i < _numBones; ++i) {
seek(_offsetBoneStructure + i * 0x88);
_bones.push_back(readSkeletonBone());
}
}
unique_ptr<Gr2File::SkeletonBone> Gr2File::readSkeletonBone() {
uint32_t offsetName = readUint32();
uint32_t parentIndex = readUint32();
ignore(0x40);
vector<float> rootToBoneValues(readArray<float>(16));
auto bone = make_unique<SkeletonBone>();
bone->name = readCStringAt(offsetName);
bone->parentIndex = parentIndex;
bone->rootToBone = glm::make_mat4(&rootToBoneValues[0]);
return move(bone);
}
void Gr2File::loadAttachments() {
for (uint16_t i = 0; i < _numAttachments; ++i) {
seek(_offsetAttachments + i * 0x48);
_attachments.push_back(readAttachment());
}
}
unique_ptr<Gr2File::Attachment> Gr2File::readAttachment() {
uint32_t offsetName = readUint32();
uint32_t offsetBoneName = readUint32();
vector<float> transformValues(readArray<float>(16));
auto attachment = make_unique<Attachment>();
attachment->name = readCStringAt(offsetName);
attachment->boneName = readCStringAt(offsetBoneName);
attachment->transform = glm::make_mat4(&transformValues[0]);
return move(attachment);
}
static glm::mat4 getModelMatrix() {
glm::mat4 transform(1.0f);
transform *= glm::mat4_cast(glm::angleAxis(glm::pi<float>(), glm::vec3(0.0f, 0.0f, 1.0f)));
transform *= glm::mat4_cast(glm::angleAxis(glm::half_pi<float>(), glm::vec3(1.0f, 0.0, 0.0f)));
transform = glm::scale(transform, glm::vec3(10.0f));
return move(transform);
}
static shared_ptr<Mesh> getBoneMesh() {
vector<float> vertices {
-0.005f, -0.005f, -0.005f,
-0.005f, -0.005f, 0.005f,
-0.005f, 0.005f, 0.005f,
-0.005f, 0.005f, -0.005f,
0.005f, 0.005f, 0.005f,
0.005f, 0.005f, -0.005f,
0.005f, -0.005f, -0.005f,
0.005f, -0.005f, 0.005f
};
vector<uint16_t> indices {
0, 1, 2, 2, 3, 0,
2, 4, 5, 5, 3, 2,
1, 7, 4, 4, 2, 1,
0, 6, 7, 7, 1, 0,
7, 6, 5, 5, 4, 7,
6, 0, 3, 3, 5, 6
};
Mesh::VertexOffsets offsets { 0, -1, -1, -1, -1, -1, -1, -1, 3 * sizeof(float) };
return make_shared<Mesh>(8, move(vertices), move(indices), move(offsets));
}
void Gr2File::loadModel() {
static shared_ptr<Mesh> boneMesh(getBoneMesh());
shared_ptr<ModelNode> rootNode;
int nodeIndex = 0;
if (_fileType == FileType::Skeleton) {
rootNode = make_shared<ModelNode>(nodeIndex++);
rootNode->setName(_resRef + "_root");
rootNode->setAbsoluteTransform(getModelMatrix());
shared_ptr<ModelMesh> modelMesh;
if (g_debugSkeleton) {
modelMesh = make_shared<ModelMesh>(boneMesh);
modelMesh->setRender(true);
modelMesh->setAmbientColor(glm::vec3(1.0f));
modelMesh->setDiffuseColor(glm::vec3(0.0f));
}
// Convert bones to model nodes
vector<shared_ptr<ModelNode>> nodes;
for (uint16_t i = 0; i < _numBones; ++i) {
auto node = make_shared<ModelNode>(nodeIndex);
node->setNodeNumber(nodeIndex);
node->setName(_bones[i]->name);
node->setMesh(modelMesh);
node->setAbsoluteTransform(rootNode->absoluteTransform() * glm::inverse(_bones[i]->rootToBone));
nodes.push_back(move(node));
++nodeIndex;
}
// Reparent model nodes
for (uint16_t i = 0; i < _numBones; ++i) {
if (_bones[i]->parentIndex == 0xffffffff) {
rootNode->addChild(nodes[i]);
} else {
nodes[_bones[i]->parentIndex]->addChild(nodes[i]);
}
}
} else {
// If skeleton is present, use it as the base
if (_skeleton) {
rootNode = _skeleton->rootNode();
nodeIndex = _skeleton->maxNodeIndex() + 1;
} else {
rootNode = make_shared<ModelNode>(nodeIndex++);
rootNode->setName(_resRef + "_root");
rootNode->setAbsoluteTransform(getModelMatrix());
}
// Convert meshes to model nodes
for (uint16_t i = 0; i < _numMeshes; ++i) {
auto node = make_shared<ModelNode>(nodeIndex);
node->setNodeNumber(nodeIndex);
node->setName(_meshes[i]->header.name);
node->setMesh(_meshes[i]->mesh);
// If skeleton is present configure the model node skin
if (_skeleton) {
auto skin = make_shared<ModelNode::Skin>();
for (uint16_t j = 0; j < _meshes[i]->header.numUsedBones; ++j) {
auto boneNode = _skeleton->findNodeByName(_meshes[i]->bones[j]->name);
skin->nodeIdxByBoneIdx.insert(make_pair(j, boneNode->index()));
}
node->setSkin(move(skin));
}
node->setAbsoluteTransform(rootNode->absoluteTransform());
rootNode->addChild(move(node));
++nodeIndex;
}
}
rootNode->computeLocalTransforms();
_model = make_shared<Model>(_resRef, Model::Classification::Character, move(rootNode), _animations);
}
static string getSkeletonByModel(const string &modelResRef) {
if (boost::starts_with(modelResRef, "rancor_rancor_")) return "rancor_skeleton";
return "";
}
static vector<string> getAnimationsBySkeleton(const string &skeletonResRef) {
vector<string> result;
if (skeletonResRef == "rancor_skeleton") {
result.push_back("dl_roar");
}
return move(result);
}
shared_ptr<Model> Gr2ModelLoader::loadModel(GameID gameId, const string &resRef) {
shared_ptr<Model> skeletonModel;
vector<shared_ptr<Animation>> animations;
string skeletonResRef(getSkeletonByModel(resRef));
if (!skeletonResRef.empty()) {
shared_ptr<ByteArray> skeletonData(Resources::instance().get(skeletonResRef, ResourceType::Gr2));
if (skeletonData) {
Gr2File skeleton(skeletonResRef, vector<shared_ptr<Animation>>());
skeleton.load(wrap(skeletonData));
skeletonModel = skeleton.model();
}
vector<string> animationResRefs(getAnimationsBySkeleton(skeletonResRef));
for (auto &animResRef : animationResRefs) {
shared_ptr<ByteArray> jbaData(Resources::instance().get(animResRef, ResourceType::Jba));
if (jbaData) {
JbaFile jba(animResRef, skeletonModel);
jba.load(wrap(jbaData));
shared_ptr<Animation> animation(jba.animation());
if (animation) {
// DEBUG: currently we only add a single animation and rename it to "cpause1"
animation->setName("cpause1");
animations.push_back(move(animation));
break;
}
}
}
}
shared_ptr<ByteArray> geometryData(Resources::instance().get(resRef, ResourceType::Gr2));
if (geometryData) {
Gr2File geometry(resRef, move(animations), move(skeletonModel));
geometry.load(wrap(geometryData));
return geometry.model();
}
return nullptr;
}
} // namespace tor
} // namespace reone

View file

@ -1,140 +0,0 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "../../render/model/model.h"
#include "../../render/model/modelloader.h"
#include "../../resource/format/binfile.h"
namespace reone {
namespace tor {
/**
* Encapsulates loading GR2 model files, used by Star Wars: The Old Republic.
*/
class Gr2File : public resource::BinaryFile {
public:
enum class FileType {
Geometry = 0,
GeometryWithCollision = 1,
Skeleton = 2
};
/**
* @param resRef ResRef of the model
* @param animations list of animations to pass to the loaded model
* @param skeleton the skeleton model to append to the loaded model
*/
Gr2File(
std::string resRef,
std::vector<std::shared_ptr<render::Animation>> animations,
std::shared_ptr<render::Model> skeleton = nullptr);
std::shared_ptr<render::Model> model() const { return _model; }
private:
struct MeshHeader {
std::string name;
uint16_t numPieces { 0 };
uint16_t numUsedBones { 0 };
uint16_t vertexMask { 0 };
uint16_t vertexSize { 0 };
uint32_t numVertices { 0 };
uint32_t numIndices { 0 };
uint32_t offsetVertices { 0 };
uint32_t offsetPieces { 0 };
uint32_t offsetIndices { 0 };
uint32_t offsetBones { 0 };
};
struct MeshPiece {
uint32_t startFaceIdx { 0 };
uint32_t numFaces { 0 };
uint32_t materialIndex { 0 };
uint32_t pieceIndex { 0 };
};
struct MeshBone {
std::string name;
std::vector<float> bounds;
};
struct Gr2Mesh {
MeshHeader header;
std::vector<std::shared_ptr<MeshPiece>> pieces;
std::shared_ptr<render::ModelMesh> mesh;
std::vector<std::shared_ptr<MeshBone>> bones;
};
struct SkeletonBone {
std::string name;
uint32_t parentIndex { 0 };
glm::mat4 rootToBone { 1.0f };
};
struct Attachment {
std::string name;
std::string boneName;
glm::mat4 transform { 1.0f };
};
std::string _resRef;
std::vector<std::shared_ptr<render::Animation>> _animations;
std::shared_ptr<render::Model> _skeleton;
FileType _fileType { FileType::Geometry };
uint16_t _numMeshes { 0 };
uint16_t _numMaterials { 0 };
uint16_t _numBones { 0 };
uint16_t _numAttachments { 0 };
uint32_t _offsetMeshHeader { 0 };
uint32_t _offsetMaterialHeader { 0 };
uint32_t _offsetBoneStructure { 0 };
uint32_t _offsetAttachments { 0 };
std::vector<std::shared_ptr<Gr2Mesh>> _meshes;
std::vector<std::shared_ptr<SkeletonBone>> _bones;
std::vector<std::string> _materials;
std::vector<std::shared_ptr<Attachment>> _attachments;
std::shared_ptr<render::Model> _model;
void doLoad() override;
void loadMeshes();
void loadMaterials();
void loadSkeletonBones();
void loadAttachments();
void loadModel();
std::unique_ptr<Gr2Mesh> readMesh();
std::unique_ptr<MeshPiece> readMeshPiece();
std::unique_ptr<render::ModelMesh> readModelMesh(const Gr2Mesh &mesh);
std::unique_ptr<MeshBone> readMeshBone();
std::unique_ptr<SkeletonBone> readSkeletonBone();
std::unique_ptr<Attachment> readAttachment();
};
class Gr2ModelLoader : public render::IModelLoader {
public:
std::shared_ptr<render::Model> loadModel(resource::GameID gameId, const std::string &resRef) override;
};
} // namespace tor
} // namespace reone

View file

@ -1,324 +0,0 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "jbafile.h"
#include <stdexcept>
#include "glm/gtc/type_ptr.hpp"
#include "../../common/log.h"
#include "../../render/model/model.h"
using namespace std;
using namespace reone::render;
using namespace reone::resource;
namespace reone {
namespace tor {
JbaFile::JbaFile(const string &resRef, shared_ptr<Model> skeleton) :
BinaryFile(4, "\0\0\0\0"),
_resRef(resRef),
_skeleton(skeleton) {
if (!skeleton) {
throw invalid_argument("skeleton must not be null");
}
}
void JbaFile::doLoad() {
// This is a reference implementation of https://github.com/seedhartha/reone/wiki/SWTOR-Research
loadHeader();
loadPartHeaders();
loadBoneData();
loadPartData();
loadKeyframes();
loadBones();
loadAnimation();
}
void JbaFile::loadHeader() {
_length = 10.0f * readFloat();
_fps = readFloat();
_numParts = readUint32();
ignore(8);
_numBones = readUint32();
ignore(12);
debug(boost::format("JBA: length=%.06f numParts=%d, numBones=%d") % _length % _numParts % _numBones);
}
void JbaFile::loadPartHeaders() {
for (uint32_t i = 0; i < _numParts; ++i) {
JbaPart part;
part.keyframeIdx = readUint32();
part.keyframesSize = readUint32();
debug(boost::format("JBA: part %d header: %u %u") % i % part.keyframeIdx % part.keyframesSize);
_parts.push_back(move(part));
}
ignore(4 * _numParts);
}
static constexpr size_t alignAt80(size_t offset) {
size_t result = 0;
while (result < offset) {
result += 0x80;
}
return result;
}
static string describeVector(const glm::vec3 &vec) {
return str(boost::format("(%.06f, %.06f, %.06f)") % vec.x % vec.y % vec.z);
}
void JbaFile::loadBoneData() {
seek(alignAt80(tell()));
vector<uint8_t> data(readArray<uint8_t>(48ll * _numBones));
const float *dataPtr = reinterpret_cast<float *>(&data[0]);
for (uint32_t i = 0; i < _numBones; ++i) {
JbaBone bone;
bone.translationStride = glm::make_vec3(dataPtr + 0);
bone.translationBase = glm::make_vec3(dataPtr + 3);
bone.orientationStride = glm::make_vec3(dataPtr + 6);
bone.orientationBase = glm::make_vec3(dataPtr + 9);
debug(boost::format("JBA: bone %d data: %s, %s, %s, %s")
% i
% describeVector(bone.translationStride) % describeVector(bone.translationBase)
% describeVector(bone.orientationStride) % describeVector(bone.orientationBase), 2);
_bones.push_back(move(bone));
dataPtr += 12;
}
}
void JbaFile::loadPartData() {
for (uint32_t i = 0; i < _numParts; ++i) {
uint32_t start = static_cast<uint32_t>(alignAt80(tell()));
seek(start);
_parts[i].keyframes = readPartKeyframes();
seek(start + _parts[i].keyframesSize);
}
}
static glm::vec3 decompressPosition(uint32_t value, const glm::vec3 &base, const glm::vec3 &stride) {
float x = static_cast<float>((value >> 21) & 0x7ff);
float y = static_cast<float>((value >> 10) & 0x7ff);
float z = static_cast<float>(value & 0x3ff);
return glm::vec3(base + stride * glm::vec3(x, y, z));
}
static glm::quat decompressOrientation(const uint16_t *values, const glm::vec3 &base, const glm::vec3 &stride) {
bool sign = (values[0] & 0x8000) != 0;
float x = static_cast<float>(values[0] & 0x7fff);
float y = static_cast<float>(values[1] & 0xffff);
float z = static_cast<float>(values[2] & 0xffff);
glm::vec3 axis(base + stride * glm::vec3(x, y, z));
float w;
float dot = glm::dot(axis, axis);
if (dot > 1.0f) {
w = 0.0f;
} else {
w = glm::sqrt(1.0f - dot);
if (sign) {
w *= -1.0f;
}
}
return glm::normalize(glm::quat(w, axis));
}
static string describeQuaternion(const glm::quat &q) {
return str(boost::format("(%.06f, %.06f, %.06f, %.06f)") % q.x % q.y % q.z % q.w);
}
vector<vector<JbaFile::JbaKeyframe>> JbaFile::readPartKeyframes() {
vector<vector<JbaKeyframe>> keyframes;
ignore(8); // number of bones again
// Determine the size of the keyframes section
int dataSize = 0;
vector<uint32_t> keyframeLayout(readArray<uint32_t>(4 * _numBones));
for (uint32_t i = 0; i < 4 * _numBones; i += 4) {
debug(boost::format("JBA: bone %d keyframe layout: %u %u %u %u") % (i / 4) % keyframeLayout[i + 0] % keyframeLayout[i + 1ll] % keyframeLayout[i + 2ll] % keyframeLayout[i + 3ll], 2);
dataSize += 6 * keyframeLayout[i + 0] + 4 * keyframeLayout[i + 2ll];
}
uint32_t pos = static_cast<uint32_t>(tell());
debug(boost::format("JBA: part data: reading %d bytes from %X to %X") % dataSize % pos % (pos + dataSize - 1));
const uint32_t *keyframeLayoutPtr = &keyframeLayout[0];
for (uint32_t i = 0; i < _numBones; ++i) {
vector<JbaKeyframe> boneKeyframes;
boneKeyframes.resize(keyframeLayoutPtr[0]);
// Decompress 48-bit orientation poses
for (uint32_t j = 0; j < keyframeLayoutPtr[0]; ++j) {
vector<uint16_t> values(readArray<uint16_t>(3));
boneKeyframes[j].orientation = decompressOrientation(&values[0], _bones[i].orientationBase, _bones[i].orientationStride);
debug(boost::format("JBA: bone %u: keyframe %u: orientation: %04X %04X %04X -> %s") % i % j % values[0] % values[1] % values[2] % describeQuaternion(boneKeyframes[j].orientation), 2);
}
// Decompress 32-bit translation poses, if any
for (uint32_t j = 0; j < keyframeLayoutPtr[2]; ++j) {
uint32_t value = readUint32();
boneKeyframes[j].translation = decompressPosition(value, _bones[i].translationBase, _bones[i].translationStride);
debug(boost::format("JBA: bone %u: keyframe %u: translation: %08X -> %s") % i % j % value % describeVector(boneKeyframes[j].translation), 2);
}
keyframes.push_back(move(boneKeyframes));
keyframeLayoutPtr += 4;
}
return move(keyframes);
}
void JbaFile::loadKeyframes() {
uint32_t pos = static_cast<uint32_t>(alignAt80(tell()));
seek(pos);
ignore(8);
vector<float> valuesAt08(readArray<float>(12));
_translationStride = glm::make_vec3(&valuesAt08[0]);
_translationBase = glm::make_vec3(&valuesAt08[3]);
_orientationStride = glm::make_vec3(&valuesAt08[6]);
_orientationBase = glm::make_vec3(&valuesAt08[9]);
debug(boost::format("JBA: translationStride=%s translationBase=%s orientationStride=%s orientationBase=%s") %
describeVector(_translationStride) % describeVector(_translationBase) %
describeVector(_orientationStride) % describeVector(_orientationBase));
_numKeyframes = readUint32();
debug("JBA: numKeyframes=" + to_string(_numKeyframes));
_keyframes.resize(_numKeyframes);
ignore(3 * sizeof(uint32_t));
vector<uint16_t> valuesAt48(readArray<uint16_t>(3 * _numKeyframes));
const uint16_t *valuesAt48Ptr = &valuesAt48[0];
for (uint32_t i = 0; i < _numKeyframes; ++i) {
glm::quat orientation(decompressOrientation(valuesAt48Ptr, _orientationBase, _orientationStride));
debug(boost::format("JBA: keyframe %d orientation: %04X %04X %04X -> %s") % i %
valuesAt48Ptr[0] % valuesAt48Ptr[1] % valuesAt48Ptr[2] %
describeQuaternion(orientation), 2);
_keyframes[i].orientation = move(orientation);
valuesAt48Ptr += 3;
}
if (_numKeyframes % 2 != 0) {
ignore(2);
}
vector<uint32_t> keyframeValues(readArray<uint32_t>(_numKeyframes));
for (uint32_t i = 0; i < _numKeyframes; ++i) {
glm::vec3 translation(decompressPosition(keyframeValues[i], _translationBase, _translationStride));
debug(boost::format("JBA: keyframe %d translation: %08X -> %s") % i % keyframeValues[i] % describeVector(translation), 2);
_keyframes[i].translation = move(translation);
}
}
void JbaFile::loadBones() {
uint32_t numBones = readUint32();
ignore(16);
for (uint32_t i = 0; i < numBones; ++i) {
_bones[i].index = readUint32();
}
vector<uint32_t> nameOffsets(readArray<uint32_t>(numBones));
for (uint32_t i = 0; i < numBones; ++i) {
_bones[i].name = _reader->getCString();
debug(boost::format("JBA: bone %d: %s") % i % _bones[i].name);
}
}
void JbaFile::loadAnimation() {
if (_numParts == 0 || _parts[0].keyframes.empty()) return;
// Determine the total number of keyframes
int numKeyframes = 0;
for (uint32_t i = 0; i < _numParts; ++i) {
numKeyframes += static_cast<int>(_parts[i].keyframes[0].size());
}
float step = _length / static_cast<float>(numKeyframes - 1);
// Convert keyframes to model nodes
int index = 0;
auto rootNode = make_shared<ModelNode>(index++);
rootNode->setName(_resRef + "_root");
for (uint32_t i = 0; i < _numParts; ++i) {
vector<vector<JbaKeyframe>> partKeyframes(_parts[i].keyframes);
for (uint32_t j = 0; j < _numBones; ++j) {
auto node = make_shared<ModelNode>(index);
node->setNodeNumber(index);
node->setName(_bones[j].name);
auto skeletonNode = _skeleton->findNodeByName(_bones[j].name);
if (!skeletonNode) continue;
vector<JbaKeyframe> boneKeyframes(partKeyframes[j]);
for (size_t k = 0; k < boneKeyframes.size(); ++k) {
float time = k * step;
ModelNode::Keyframe position;
position.time = time;
position.translation = boneKeyframes[k].translation * _translationBase;
node->addTranslationKeyframe(move(position));
ModelNode::Keyframe orientation;
orientation.time = time;
orientation.orientation = skeletonNode->orientation() * boneKeyframes[k].orientation;
node->addOrientationKeyframe(move(orientation));
}
rootNode->addChild(move(node));
++index;
}
}
// Create the animation
vector<Animation::Event> events;
_animation = make_shared<Animation>(_resRef, _length, 0.5f * _length, move(events), move(rootNode));
}
} // namespace tor
} // namespace reone

View file

@ -1,92 +0,0 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "glm/gtc/quaternion.hpp"
#include "glm/vec3.hpp"
#include "../../render/model/animation.h"
#include "../../render/model/model.h"
#include "../../resource/format/binfile.h"
namespace reone {
namespace tor {
class JbaFile : public resource::BinaryFile {
public:
JbaFile(const std::string &resRef, std::shared_ptr<render::Model> skeleton);
std::shared_ptr<render::Animation> animation() const { return _animation; }
private:
struct JbaKeyframe {
glm::vec3 translation { 0.0f };
glm::quat orientation { 1.0f, 0.0f, 0.0, 0.0f };
};
struct JbaPart {
uint32_t keyframeIdx { 0 };
uint32_t keyframesSize;
std::vector<std::vector<JbaKeyframe>> keyframes;
};
struct JbaBone {
glm::vec3 translationStride { 0.0f };
glm::vec3 translationBase { 0.0f };
glm::vec3 orientationStride { 0.0f };
glm::vec3 orientationBase { 0.0f };
uint32_t index { 0 };
std::string name;
};
std::string _resRef;
std::shared_ptr<render::Model> _skeleton;
float _length { 0.0f };
float _fps { 0 };
uint32_t _numParts { 0 };
uint32_t _numKeyframes { 0 };
uint32_t _numBones { 0 };
glm::vec3 _translationStride { 0.0f };
glm::vec3 _translationBase { 0.0f };
glm::vec3 _orientationStride { 0.0f };
glm::vec3 _orientationBase { 0.0f };
std::vector<JbaPart> _parts;
std::vector<JbaKeyframe> _keyframes;
std::vector<JbaBone> _bones;
std::shared_ptr<render::Animation> _animation;
void doLoad();
void loadHeader();
void loadPartHeaders();
void loadBoneData();
void loadPartData();
void loadKeyframes();
void loadBones();
void loadAnimation();
std::vector<std::vector<JbaKeyframe>> readPartKeyframes();
};
} // namespace tor
} // namespace reone

View file

@ -24,7 +24,6 @@
#include "../audio/soundhandle.h"
#include "../common/log.h"
#include "../common/pathutil.h"
#include "../experimental/tor/gr2file.h"
#include "../render/featureutil.h"
#include "../render/lip/lips.h"
#include "../render/materials.h"
@ -54,7 +53,6 @@ using namespace reone::render;
using namespace reone::resource;
using namespace reone::scene;
using namespace reone::script;
using namespace reone::tor;
using namespace reone::video;
namespace fs = boost::filesystem;
@ -125,7 +123,6 @@ void Game::init() {
void Game::registerModelLoaders() {
Models::instance().registerLoader(ResourceType::Mdl, make_shared<MdlModelLoader>());
Models::instance().registerLoader(ResourceType::Gr2, make_shared<Gr2ModelLoader>());
}
void Game::setCursorType(CursorType type) {

View file

@ -40,11 +40,6 @@ namespace reone {
namespace game {
// This is a hook to replace MDL models with GR2 models (SWTOR).
static const unordered_map<string, string> g_gr2Models {
//{ "c_rancors", "rancor_rancor_a01" }
};
static const string g_headHookNode("headhook");
static const string g_maskHookNode("gogglehook");
@ -58,18 +53,10 @@ shared_ptr<ModelSceneNode> CreatureModelBuilder::build() {
string modelName(getBodyModelName());
if (modelName.empty()) return nullptr;
bool gr2Model = false;
auto maybeGr2Model = g_gr2Models.find(modelName);
if (maybeGr2Model != g_gr2Models.end()) {
modelName = maybeGr2Model->second;
gr2Model = true;
}
shared_ptr<Model> model(Models::instance().get(modelName, gr2Model ? ResourceType::Gr2 : ResourceType::Mdl));
shared_ptr<Model> model(Models::instance().get(modelName, ResourceType::Mdl));
if (!model) return nullptr;
auto modelSceneNode = make_unique<ModelSceneNode>(ModelSceneNode::Classification::Creature, model, &_creature->sceneGraph());
if (gr2Model) return move(modelSceneNode);
// Body texture

View file

@ -89,9 +89,6 @@ enum class ResourceType : uint16_t {
Mdx = 3008,
Mp3 = 0x1000,
Gr2 = 0x1001, // SWTOR model
Mat = 0x1002, // SWTOR material
Jba = 0x1003, // SWTOR animation
Invalid = 0xffff
};

View file

@ -82,10 +82,7 @@ static unordered_map<ResourceType, string> g_extByType {
{ ResourceType::Lip, "lip" },
{ ResourceType::Tpc, "tpc" },
{ ResourceType::Mdx, "mdx" },
{ ResourceType::Mp3, "mp3" },
{ ResourceType::Gr2, "gr2" },
{ ResourceType::Mat, "mat" },
{ ResourceType::Jba, "jba" }
{ ResourceType::Mp3, "mp3" }
};
static unordered_map<string, ResourceType> g_typeByExt;