diff --git a/CMakeLists.txt b/CMakeLists.txt index f208875..75272b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,4 +42,5 @@ add_subdirectory(exdviewer) add_subdirectory(mdlviewer) add_subdirectory(argcracker) add_subdirectory(explorer) -add_subdirectory(bonedecomp) \ No newline at end of file +add_subdirectory(bonedecomp) +add_subdirectory(parts) \ No newline at end of file diff --git a/parts/CMakeLists.txt b/parts/CMakeLists.txt new file mode 100644 index 0000000..a862e07 --- /dev/null +++ b/parts/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(exd) +add_subdirectory(mdl) \ No newline at end of file diff --git a/parts/exd/CMakeLists.txt b/parts/exd/CMakeLists.txt new file mode 100644 index 0000000..160861f --- /dev/null +++ b/parts/exd/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(exdpart STATIC exdpart.cpp) +target_link_libraries(exdpart PUBLIC physis z ${LIBRARIES} Qt5::Core Qt5::Widgets) +target_include_directories(exdpart PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) \ No newline at end of file diff --git a/parts/exd/exdpart.cpp b/parts/exd/exdpart.cpp new file mode 100644 index 0000000..b4a3d7e --- /dev/null +++ b/parts/exd/exdpart.cpp @@ -0,0 +1,97 @@ +#include "exdpart.h" + +#include +#include +#include +#include + +EXDPart::EXDPart(GameData* data) : data(data) { + pageTabWidget = new QTabWidget(); + + auto layout = new QVBoxLayout(); + layout->addWidget(pageTabWidget); + setLayout(layout); +} + +void EXDPart::loadSheet(const QString& name) { + qDebug() << "Loading" << name; + + pageTabWidget->clear(); + + auto exh = physis_gamedata_read_excel_sheet_header(data, name.toStdString().c_str()); + + for(int i = 0; i < exh.page_count; i++) { + QTableWidget* tableWidget = new QTableWidget(); + + tableWidget->setColumnCount(exh.column_count); + + auto exd = physis_gamedata_read_excel_sheet(data, name.toStdString().c_str(), &exh, exh.languages[0], i); + + tableWidget->setRowCount(exd.row_count); + + for (int j = 0; j < exd.row_count; j++) { + for(int z = 0; z < exd.column_count; z++) { + auto data = exd.row_data[j].column_data[z]; + + QString columnString; + QString columnType; + switch (data.tag) { + case physis_ColumnData::Tag::String: + columnString = QString(data.string._0); + columnType = "String"; + break; + case physis_ColumnData::Tag::Bool: + columnString = data.bool_._0 ? "True" : "False"; + columnType = "Bool"; + break; + case physis_ColumnData::Tag::Int8: + columnString = QString::number(data.int8._0); + columnType = "Int8"; + break; + case physis_ColumnData::Tag::UInt8: + columnString = QString::number(data.u_int8._0); + columnType = "UInt8"; + break; + case physis_ColumnData::Tag::Int16: + columnString = QString::number(data.int16._0); + columnType = "Int16"; + break; + case physis_ColumnData::Tag::UInt16: + columnString = QString::number(data.u_int16._0); + columnType = "UInt16"; + break; + case physis_ColumnData::Tag::Int32: + columnString = QString::number(data.int32._0); + columnType = "Int32"; + break; + case physis_ColumnData::Tag::UInt32: + columnString = QString::number(data.u_int32._0); + columnType = "UInt32"; + break; + case physis_ColumnData::Tag::Float32: + columnString = QString::number(data.float32._0); + columnType = "Float32"; + break; + case physis_ColumnData::Tag::Int64: + columnString = QString::number(data.int64._0); + columnType = "Int64"; + break; + case physis_ColumnData::Tag::UInt64: + columnString = QString::number(data.u_int64._0); + columnType = "UInt64"; + break; + } + + auto newItem = new QTableWidgetItem(columnString); + + tableWidget->setItem(i, j, newItem); + + QTableWidgetItem* headerItem = new QTableWidgetItem(); + headerItem->setText(columnType); + tableWidget->setHorizontalHeaderItem(j, headerItem); + } + } + + pageTabWidget->addTab(tableWidget, QString("Page %1").arg(i)); + } +} \ No newline at end of file diff --git a/parts/exd/exdpart.h b/parts/exd/exdpart.h new file mode 100644 index 0000000..36bba12 --- /dev/null +++ b/parts/exd/exdpart.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +struct GameData; + +class EXDPart : public QWidget { +public: + explicit EXDPart(GameData* data); + + void loadSheet(const QString& name); + +private: + GameData* data = nullptr; + + QTabWidget* pageTabWidget = nullptr; +}; \ No newline at end of file diff --git a/parts/mdl/CMakeLists.txt b/parts/mdl/CMakeLists.txt new file mode 100644 index 0000000..ca74645 --- /dev/null +++ b/parts/mdl/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(mdlpart STATIC mdlpart.cpp) +target_link_libraries(mdlpart PUBLIC physis z ${LIBRARIES} Qt5::Core Qt5::Widgets renderer) +target_include_directories(mdlpart PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) \ No newline at end of file diff --git a/parts/mdl/mdlpart.cpp b/parts/mdl/mdlpart.cpp new file mode 100644 index 0000000..28da417 --- /dev/null +++ b/parts/mdl/mdlpart.cpp @@ -0,0 +1,408 @@ +#include "mdlpart.h" +#include "glm/gtx/transform.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef USE_STANDALONE_WINDOW +class VulkanWindow : public QWindow +{ +public: + VulkanWindow(Renderer* renderer, QVulkanInstance* instance) : m_renderer(renderer), m_instance(instance) { + setSurfaceType(VulkanSurface); + setVulkanInstance(instance); + } + + void exposeEvent(QExposeEvent *) { + if (isExposed()) { + if (!m_initialized) { + m_initialized = true; + + auto surface = m_instance->surfaceForWindow(this); + if(!m_renderer->initSwapchain(surface, width(), height())) + m_initialized = false; + else + render(); + } + } + } + + bool event(QEvent *e) { + if (e->type() == QEvent::UpdateRequest) + render(); + + if (e->type() == QEvent::Resize) { + QResizeEvent* resizeEvent = (QResizeEvent*)e; + auto surface = m_instance->surfaceForWindow(this); + m_renderer->resize(surface, resizeEvent->size().width(), resizeEvent->size().height()); + } + + return QWindow::event(e); + } + + void render() { + m_renderer->render(models); + m_instance->presentQueued(this); + requestUpdate(); + } + + std::vector models; + +private: + bool m_initialized = false; + Renderer* m_renderer; + QVulkanInstance* m_instance; +}; +#else +#include "standalonewindow.h" +#include "equipment.h" + +#endif + +MDLPart::MDLPart(GameData *data) : data(data) { + auto viewportLayout = new QVBoxLayout(); + setLayout(viewportLayout); + + renderer = new Renderer(); + +#ifndef USE_STANDALONE_WINDOW + auto inst = new QVulkanInstance(); + inst->setVkInstance(renderer->instance); + inst->setFlags(QVulkanInstance::Flag::NoDebugOutputRedirect); + inst->create(); + + vkWindow = new VulkanWindow(renderer, inst); + vkWindow->setVulkanInstance(inst); + + auto widget = QWidget::createWindowContainer(vkWindow); + + viewportLayout->addWidget(widget); +#else + standaloneWindow = new StandaloneWindow(renderer); + renderer->initSwapchain(standaloneWindow->getSurface(renderer->instance), 640, 480); + + QTimer* timer = new QTimer(); + connect(timer, &QTimer::timeout, this, [this] { + standaloneWindow->render(); + }); + timer->start(1000); +#endif + + connect(this, &MDLPart::modelChanged, this, &MDLPart::reloadRenderer); + connect(this, &MDLPart::skeletonChanged, this, &MDLPart::reloadBoneData); +} + +void MDLPart::exportModel(const QString &fileName) { + Assimp::Exporter exporter; + + aiScene scene; + scene.mRootNode = new aiNode(); + + // TODO: hardcoded to the first model for now + scene.mRootNode->mNumChildren = models[0].model.lods[0].num_parts + 1; // plus one for the skeleton + scene.mRootNode->mChildren = new aiNode*[scene.mRootNode->mNumChildren]; + + scene.mNumMeshes = models[0].model.lods[0].num_parts; + scene.mMeshes = new aiMesh*[scene.mNumMeshes]; + + auto skeleton_node = new aiNode(); + skeleton_node->mName = "Skeleton"; + skeleton_node->mNumChildren = 1; + skeleton_node->mChildren = new aiNode*[skeleton_node->mNumChildren]; + + scene.mRootNode->mChildren[scene.mRootNode->mNumChildren - 1] = skeleton_node; + + std::vector skeletonNodes; + + for(int i = 0; i < models[0].model.num_affected_bones; i++) { + auto& node = skeletonNodes.emplace_back(); + node = new aiNode(); + node->mName = models[0].model.affected_bone_names[i]; + + int real_bone_id = 0; + for(int k = 0; k < skeleton->num_bones; k++) { + if(strcmp(skeleton->bones[k].name, models[0].model.affected_bone_names[i]) == 0) { + real_bone_id = k; + } + } + + node->mChildren = new aiNode*[models[0].model.num_affected_bones]; + + auto& real_bone = skeleton->bones[real_bone_id]; + memcpy(&node->mTransformation, glm::value_ptr(boneData[real_bone.index].finalTransform), sizeof(aiMatrix4x4)); + } + + // setup parenting + for(int i = 0; i < models[0].model.num_affected_bones; i++) { + int real_bone_id = 0; + for(int k = 0; k < skeleton->num_bones; k++) { + if(strcmp(skeleton->bones[k].name, models[0].model.affected_bone_names[i]) == 0) { + real_bone_id = k; + } + } + + auto& real_bone = skeleton->bones[real_bone_id]; + if(real_bone.parent_bone != nullptr) { + for(int k = 0; k < models[0].model.num_affected_bones; k++) { + if(strcmp(models[0].model.affected_bone_names[k], real_bone.parent_bone->name) == 0) { + skeletonNodes[i]->mParent = skeletonNodes[k]; + skeletonNodes[k]->mChildren[skeletonNodes[k]->mNumChildren++] = skeletonNodes[i]; + } + } + } + } + + skeleton_node->mChildren[0] = new aiNode(); + skeleton_node->mChildren[0]->mName = "root"; + skeleton_node->mChildren[0]->mChildren = new aiNode*[models[0].model.num_affected_bones]; + + for(int i = 0; i < skeletonNodes.size(); i++) { + if(skeletonNodes[i]->mParent == nullptr) { + skeleton_node->mChildren[0]->mChildren[skeleton_node->mChildren[0]->mNumChildren++] = skeletonNodes[i]; + } + } + + for(int i = 0; i < models[0].model.lods[0].num_parts; i++) { + scene.mMeshes[i] = new aiMesh(); + scene.mMeshes[i]->mMaterialIndex = 0; + + auto& node = scene.mRootNode->mChildren[i]; + node = new aiNode(); + node->mNumMeshes = 1; + node->mMeshes = new unsigned int [scene.mRootNode->mNumMeshes]; + node->mMeshes[0] = i; + + auto mesh = scene.mMeshes[i]; + mesh->mNumVertices = models[0].model.lods[0].parts[i].num_vertices; + mesh->mVertices = new aiVector3D [mesh->mNumVertices]; + mesh->mNormals = new aiVector3D [mesh->mNumVertices]; + mesh->mTextureCoords[0] = new aiVector3D [mesh->mNumVertices]; + mesh->mNumUVComponents[0] = 2; + + for(int j = 0; j < mesh->mNumVertices; j++) { + auto vertex = models[0].model.lods[0].parts[i].vertices[j]; + mesh->mVertices[j] = aiVector3D(vertex.position[0], vertex.position[1], vertex.position[2]); + mesh->mNormals[j] = aiVector3D (vertex.normal[0], vertex.normal[1], vertex.normal[2]); + mesh->mTextureCoords[0][j] = aiVector3D(vertex.uv[0], vertex.uv[1], 0.0f); + } + + mesh->mNumBones = models[0].model.num_affected_bones; + mesh->mBones = new aiBone*[mesh->mNumBones]; + for(int j = 0; j < mesh->mNumBones; j++) { + int real_bone_id = j; + // TODO: is this still relevant?5 + /*for(int k = 0; k < skeleton.bones.size(); k++) { + if(skeleton.bones[k].name == model.affectedBoneNames[j]) { + real_bone_id = k; + } + }*/ + + mesh->mBones[j] = new aiBone(); + mesh->mBones[j]->mName = models[0].model.affected_bone_names[j]; + mesh->mBones[j]->mNumWeights = mesh->mNumVertices * 4; + mesh->mBones[j]->mWeights = new aiVertexWeight[mesh->mBones[j]->mNumWeights]; + mesh->mBones[j]->mNode = skeleton_node->mChildren[j]; + + for(int k = 0; k < mesh->mNumVertices; k++) { + for(int z = 0; z < 4; z++) { + if (models[0].model.lods[0].parts[i].vertices[k].bone_id[z] == real_bone_id) { + auto &weight = mesh->mBones[j]->mWeights[k * 4 + z]; + weight.mVertexId = k; + weight.mWeight = models[0].model.lods[0].parts[i].vertices[k].bone_weight[z]; + } + } + } + } + + mesh->mNumFaces = models[0].model.lods[0].parts[i].num_indices / 3; + mesh->mFaces = new aiFace[mesh->mNumFaces]; + + int lastFace = 0; + for(int j = 0; j < models[0].model.lods[0].parts[i].num_indices; j += 3) { + aiFace& face = mesh->mFaces[lastFace++]; + + face.mNumIndices = 3; + face.mIndices = new unsigned int[face.mNumIndices]; + + face.mIndices[0] = models[0].model.lods[0].parts[i].indices[j]; + face.mIndices[1] = models[0].model.lods[0].parts[i].indices[j + 1]; + face.mIndices[2] = models[0].model.lods[0].parts[i].indices[j + 2]; + } + } + + scene.mNumMaterials = 1; + scene.mMaterials = new aiMaterial*[1]; + scene.mMaterials[0] = new aiMaterial(); + + exporter.Export(&scene, "fbx", fileName.toStdString()); +} + +void MDLPart::clear() { + models.clear(); + + Q_EMIT modelChanged(); +} + +void MDLPart::addModel(physis_MDL mdl, std::vector materials, int lod) { + qDebug() << "Adding model to MDLPart"; + + auto model = renderer->addModel(mdl, lod); + + std::transform(materials.begin(), materials.end(), std::back_inserter(model.materials), [this](const physis_Material& mat) { + return createMaterial(mat); + }); + + models.push_back(model); + + Q_EMIT modelChanged(); +} + +void MDLPart::setSkeleton(physis_Skeleton newSkeleton) { + skeleton = newSkeleton; + + Q_EMIT skeletonChanged(); +} + +void MDLPart::clearSkeleton() { + skeleton.reset(); + + Q_EMIT skeletonChanged(); +} + +void MDLPart::reloadRenderer() { + qDebug() << "Reloading render models..."; + + reloadBoneData(); + +#ifndef USE_STANDALONE_WINDOW + vkWindow->models = models; +#else + standaloneWindow->models = models; +#endif +} + +void MDLPart::reloadBoneData() { + if(skeleton.has_value()) { + // first-time data, TODO split out + boneData.resize(skeleton->num_bones); + calculateBoneInversePose(*skeleton, *skeleton->root_bone, nullptr); + + for(auto& bone : boneData) { + bone.inversePose = glm::inverse(bone.inversePose); + } + + // update data + calculateBone(*skeleton, *skeleton->root_bone, nullptr); + + for(auto& model : models) { + // we want to map the actual affected bones to bone ids + std::map boneMapping; + for (int i = 0; i < model.model.num_affected_bones; i++) { + for (int k = 0; k < skeleton->num_bones; k++) { + if (strcmp(skeleton->bones[k].name, model.model.affected_bone_names[i]) == 0) + boneMapping[i] = k; + } + } + + for (int i = 0; i < model.model.num_affected_bones; i++) { + model.boneData[i] = boneData[boneMapping[i]].finalTransform; + } + } + } +} + +RenderMaterial MDLPart::createMaterial(const physis_Material &material) { + RenderMaterial newMaterial; + + for (int i = 0; i < material.num_textures; i++) { + std::string t = material.textures[i]; + + if (t.find("skin") != std::string::npos) { + newMaterial.type = MaterialType::Skin; + } + + char type = t[t.length() - 5]; + + switch(type) { + case 'm': { + auto texture = physis_texture_parse(physis_gamedata_extract_file(data, material.textures[i])); + auto tex = renderer->addTexture(texture.width, texture.height, texture.rgba, texture.rgba_size); + + newMaterial.multiTexture = new RenderTexture(tex); + } + case 'd': { + auto texture = physis_texture_parse(physis_gamedata_extract_file(data, material.textures[i])); + auto tex = renderer->addTexture(texture.width, texture.height, texture.rgba, texture.rgba_size); + + newMaterial.diffuseTexture = new RenderTexture(tex); + } + break; + case 'n': { + auto texture = physis_texture_parse(physis_gamedata_extract_file(data, material.textures[i])); + auto tex = renderer->addTexture(texture.width, texture.height, texture.rgba, texture.rgba_size); + + newMaterial.normalTexture = new RenderTexture(tex); + } + break; + case 's': { + auto texture = physis_texture_parse(physis_gamedata_extract_file(data, material.textures[i])); + auto tex = renderer->addTexture(texture.width, texture.height, texture.rgba, texture.rgba_size); + + newMaterial.specularTexture = new RenderTexture(tex); + } + break; + default: + qDebug() << "unhandled type" << type; + break; + } + } + + return newMaterial; +} + +void MDLPart::calculateBoneInversePose(physis_Skeleton& skeleton, physis_Bone& bone, physis_Bone* parent_bone) { + const glm::mat4 parentMatrix = parent_bone == nullptr ? glm::mat4(1.0f) : boneData[parent_bone->index].inversePose; + + glm::mat4 local(1.0f); + local = glm::translate(local, glm::vec3(bone.position[0], bone.position[1], bone.position[2])); + local *= glm::mat4_cast(glm::quat(bone.rotation[3], bone.rotation[0], bone.rotation[1], bone.rotation[2])); + local = glm::scale(local, glm::vec3(bone.scale[0], bone.scale[1], bone.scale[2])); + + boneData[bone.index].inversePose = parentMatrix * local; + + for(int i = 0; i < skeleton.num_bones; i++) { + if(skeleton.bones[i].parent_bone != nullptr && strcmp(skeleton.bones[i].parent_bone->name, bone.name) == 0) { + calculateBoneInversePose(skeleton, skeleton.bones[i], &bone); + } + } +} + +void MDLPart::calculateBone(physis_Skeleton& skeleton, physis_Bone& bone, const physis_Bone* parent_bone) { + const glm::mat4 parent_matrix = parent_bone == nullptr ? glm::mat4(1.0f) : boneData[parent_bone->index].localTransform; + + glm::mat4 local = glm::mat4(1.0f); + local = glm::translate(local, glm::vec3(bone.position[0], bone.position[1], bone.position[2])); + local *= glm::mat4_cast(glm::quat(bone.rotation[3], bone.rotation[0], bone.rotation[1], bone.rotation[2])); + local = glm::scale(local, glm::vec3(bone.scale[0], bone.scale[1], bone.scale[2])); + + boneData[bone.index].localTransform = parent_matrix * local; + boneData[bone.index].finalTransform = boneData[bone.index].localTransform * boneData[bone.index].inversePose; + + for(int i = 0; i < skeleton.num_bones; i++) { + if(skeleton.bones[i].parent_bone != nullptr && strcmp(skeleton.bones[i].parent_bone->name, bone.name) == 0) { + calculateBone(skeleton, skeleton.bones[i], &bone); + } + } +} + +#include "moc_mdlpart.cpp" \ No newline at end of file diff --git a/parts/mdl/mdlpart.h b/parts/mdl/mdlpart.h new file mode 100644 index 0000000..226c3dc --- /dev/null +++ b/parts/mdl/mdlpart.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +#include "renderer.hpp" + +struct GameData; + +class VulkanWindow; +class StandaloneWindow; + +class MDLPart : public QWidget { + Q_OBJECT +public: + explicit MDLPart(GameData* data); + + void exportModel(const QString& fileName); + +Q_SIGNALS: + void modelChanged(); + void skeletonChanged(); + +public Q_SLOTS: + /// Clears all stored MDLs. + void clear(); + + /// Adds a new MDL with a list of materials used. + void addModel(physis_MDL mdl, std::vector materials, int lod); + + /// Sets the skeleton any skinned MDLs should bind to. + void setSkeleton(physis_Skeleton skeleton); + + /// Clears the current skeleton. + void clearSkeleton(); + +private Q_SLOTS: + void reloadRenderer(); + void reloadBoneData(); + +private: + RenderMaterial createMaterial(const physis_Material& mat); + + void calculateBoneInversePose(physis_Skeleton& skeleton, physis_Bone& bone, physis_Bone* parent_bone); + void calculateBone(physis_Skeleton& skeleton, physis_Bone& bone, const physis_Bone* parent_bone); + + GameData* data = nullptr; + + std::vector models; + std::optional skeleton; + + struct BoneData { + glm::mat4 localTransform, finalTransform, inversePose; + }; + + std::vector boneData; + + Renderer* renderer; + VulkanWindow* vkWindow; + StandaloneWindow* standaloneWindow; +}; \ No newline at end of file