From 97f46bcca135ef2baffd48ba6a2ac8daf082ee38 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 9 Apr 2023 15:28:00 -0400 Subject: [PATCH] Introduce the parts system and EXD and MDL parts These parts (inspired by the KDE parts system) will allow the tooling to reuse GUI mechanisms. Right now the two supported parts are for Excel and Models, and exdviewer and mdlviewer will be retrofitted to them in future commits. --- CMakeLists.txt | 3 +- parts/CMakeLists.txt | 2 + parts/exd/CMakeLists.txt | 3 + parts/exd/exdpart.cpp | 97 ++++++++++ parts/exd/exdpart.h | 18 ++ parts/mdl/CMakeLists.txt | 3 + parts/mdl/mdlpart.cpp | 408 +++++++++++++++++++++++++++++++++++++++ parts/mdl/mdlpart.h | 62 ++++++ 8 files changed, 595 insertions(+), 1 deletion(-) create mode 100644 parts/CMakeLists.txt create mode 100644 parts/exd/CMakeLists.txt create mode 100644 parts/exd/exdpart.cpp create mode 100644 parts/exd/exdpart.h create mode 100644 parts/mdl/CMakeLists.txt create mode 100644 parts/mdl/mdlpart.cpp create mode 100644 parts/mdl/mdlpart.h 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