From 7407d26247fb6a112d96b9aed97bc1770e93eb6c Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 9 Apr 2023 15:31:19 -0400 Subject: [PATCH] Overhaul mdlviewer to use the MDL part, and add the full model viewer This is a major code overhaul for mdlviewer, which will make it easier to extend and modify in the future (trust me, the old code was garbage). The different views are now split up (SingleGearView, FullModelViewer, and MDLPart) which makes the functionality easier to handle, and less error-prone. Right now bone debugging is disabled (not that it worked that well anyway) but will be brought back in a future commit. --- mdlviewer/CMakeLists.txt | 8 +- mdlviewer/include/fullmodelviewer.h | 32 +++ mdlviewer/include/gearview.h | 76 ++++++ mdlviewer/include/mainwindow.h | 68 +---- mdlviewer/include/singlegearview.h | 48 ++++ mdlviewer/src/fullmodelviewer.cpp | 93 +++++++ mdlviewer/src/gearview.cpp | 144 ++++++++++ mdlviewer/src/main.cpp | 4 +- mdlviewer/src/mainwindow.cpp | 408 ++-------------------------- mdlviewer/src/singlegearview.cpp | 149 ++++++++++ 10 files changed, 574 insertions(+), 456 deletions(-) create mode 100644 mdlviewer/include/fullmodelviewer.h create mode 100644 mdlviewer/include/gearview.h create mode 100644 mdlviewer/include/singlegearview.h create mode 100644 mdlviewer/src/fullmodelviewer.cpp create mode 100644 mdlviewer/src/gearview.cpp create mode 100644 mdlviewer/src/singlegearview.cpp diff --git a/mdlviewer/CMakeLists.txt b/mdlviewer/CMakeLists.txt index acbda20..50eb27f 100644 --- a/mdlviewer/CMakeLists.txt +++ b/mdlviewer/CMakeLists.txt @@ -4,7 +4,10 @@ add_executable(mdlviewer src/main.cpp src/mainwindow.cpp src/vec3edit.cpp - include/vec3edit.h) + include/vec3edit.h + src/gearview.cpp + src/singlegearview.cpp + src/fullmodelviewer.cpp) target_include_directories(mdlviewer PUBLIC include) @@ -15,7 +18,8 @@ target_link_libraries(mdlviewer PUBLIC renderer assimp::assimp magic_enum - physis z) + physis z + mdlpart) install(TARGETS mdlviewer DESTINATION "${INSTALL_BIN_PATH}") diff --git a/mdlviewer/include/fullmodelviewer.h b/mdlviewer/include/fullmodelviewer.h new file mode 100644 index 0000000..19875c2 --- /dev/null +++ b/mdlviewer/include/fullmodelviewer.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include "gearview.h" + +struct GameData; + +class FullModelViewer : public QWidget { + Q_OBJECT +public: + explicit FullModelViewer(GameData* data); + +Q_SIGNALS: + void gearChanged(); + +public Q_SLOTS: + void clear(); + void addGear(GearInfo& info); + +private Q_SLOTS: + void reloadGear(); + +private: + std::optional topSlot; + std::optional bottomSlot;\ + + GearView* gearView = nullptr; + QComboBox* raceCombo, *genderCombo; + + GameData* data = nullptr; +}; \ No newline at end of file diff --git a/mdlviewer/include/gearview.h b/mdlviewer/include/gearview.h new file mode 100644 index 0000000..95d843b --- /dev/null +++ b/mdlviewer/include/gearview.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include "mdlpart.h" + +struct ModelInfo { + int primaryID; + int gearVersion = 1; +}; + +struct GearInfo { + std::string name; + Slot slot; + ModelInfo modelInfo; + + std::string getMtrlPath(int raceID) { + return fmt::format("chara/equipment/e{gearId:04d}/material/v{gearVersion:04d}/mt_c{raceId:04d}e{gearId:04d}_{slot}_a.mtrl", + fmt::arg("gearId", modelInfo.primaryID), + fmt::arg("gearVersion", modelInfo.gearVersion), + fmt::arg("raceId", raceID), + fmt::arg("slot", physis_get_slot_name(slot))); + } +}; + +struct GameData; + +class GearView : public QWidget { + Q_OBJECT +public: + explicit GearView(GameData* data); + + /// Returns an inclusive list of races supported by the current gearset. + std::vector supportedRaces() const; + + /// Returns an inclusive list of genders supported by the current gearset. + std::vector supportedGenders() const; + + /// Returns an inclusive list of LoDs supported by the current gearset. + int lodCount() const; + + void exportModel(const QString& fileName); + +Q_SIGNALS: + void gearChanged(); + + void raceChanged(); + void genderChanged(); + void levelOfDetailChanged(); + +public Q_SLOTS: + void clear(); + void addGear(GearInfo& gear); + + void setRace(Race race); + void setGender(Gender gender); + void setLevelOfDetail(int lod); + +private Q_SLOTS: + void reloadModel(); + +private: + Race currentRace = Race::Hyur; + Gender currentGender = Gender::Female; + int currentLod = 0; + + uint32_t maxLod = 0; + + std::vector gears; + + MDLPart* mdlPart = nullptr; + + GameData* data; +}; \ No newline at end of file diff --git a/mdlviewer/include/mainwindow.h b/mdlviewer/include/mainwindow.h index 0f50f7e..8f0050c 100644 --- a/mdlviewer/include/mainwindow.h +++ b/mdlviewer/include/mainwindow.h @@ -6,75 +6,21 @@ #include #include -#include "renderer.hpp" +#include "gearview.h" +#include "fullmodelviewer.h" +#include "singlegearview.h" -struct ModelInfo { - int primaryID; - int gearVersion = 1; -}; - -struct GearInfo { - std::string name; - Slot slot; - ModelInfo modelInfo; - - std::string getMtrlPath(int raceID) { - return fmt::format("chara/equipment/e{gearId:04d}/material/v{gearVersion:04d}/mt_c{raceId:04d}e{gearId:04d}_{slot}_a.mtrl", - fmt::arg("gearId", modelInfo.primaryID), - fmt::arg("gearVersion", modelInfo.gearVersion), - fmt::arg("raceId", raceID), - fmt::arg("slot", physis_get_slot_name(slot))); - } -}; - -class GameData; -class VulkanWindow; -class StandaloneWindow; +struct GameData; class MainWindow : public QMainWindow { public: - MainWindow(GameData* data); - - void exportModel(physis_MDL& model, physis_Skeleton& skeleton, QString fileName); + explicit MainWindow(GameData* data); private: - void loadInitialGearInfo(GearInfo& info); - void reloadGearModel(); - void reloadGearAppearance(); - void calculate_bone_inverse_pose(physis_Skeleton& skeleton, physis_Bone& bone, physis_Bone* parent_bone); - void calculate_bone(physis_Skeleton& skeleton, physis_Bone& bone, const physis_Bone* parent_bone); - std::vector gears; - struct LoadedGear { - GearInfo* gearInfo; - physis_MDL model; - physis_Material material; - RenderModel renderModel; - RenderTexture renderTexture; - }; - - struct BoneExtra { - glm::mat4 localTransform, finalTransform, inversePose; - }; - - LoadedGear loadedGear; - - QComboBox* raceCombo, *lodCombo; - - Race currentRace = Race::Hyur; - Subrace currentSubrace = Subrace::Midlander; - Gender currentGender = Gender::Male; - int currentLod = 0; - glm::vec3 currentScale = glm::vec3(1); - physis_Bone* currentEditedBone = nullptr; + SingleGearView* gearView = nullptr; + FullModelViewer* fullModelViewer = nullptr; GameData& data; - - Renderer* renderer; - VulkanWindow* vkWindow; - StandaloneWindow* standaloneWindow; - - physis_Skeleton skeleton; - std::vector extraBone; }; \ No newline at end of file diff --git a/mdlviewer/include/singlegearview.h b/mdlviewer/include/singlegearview.h new file mode 100644 index 0000000..8e1a33f --- /dev/null +++ b/mdlviewer/include/singlegearview.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include "gearview.h" + +struct GameData; + +class SingleGearView : public QWidget { + Q_OBJECT +public: + explicit SingleGearView(GameData* data); + +Q_SIGNALS: + void gearChanged(); + + void raceChanged(); + void genderChanged(); + void levelOfDetailChanged(); + + void addToFullModelViewer(GearInfo& info); + +public Q_SLOTS: + void clear(); + void setGear(GearInfo& info); + + void setRace(Race race); + void setGender(Gender gender); + void setLevelOfDetail(int lod); + +private Q_SLOTS: + void reloadGear(); + +private: + std::optional currentGear; + + Race currentRace = Race::Hyur; + Gender currentGender = Gender::Female; + int currentLod = 0; + + GearView* gearView = nullptr; + QComboBox* raceCombo, *genderCombo, *lodCombo; + QPushButton* addToFMVButton, *exportButton; + + bool loadingComboData = false; + + GameData* data = nullptr; +}; \ No newline at end of file diff --git a/mdlviewer/src/fullmodelviewer.cpp b/mdlviewer/src/fullmodelviewer.cpp new file mode 100644 index 0000000..7dc5548 --- /dev/null +++ b/mdlviewer/src/fullmodelviewer.cpp @@ -0,0 +1,93 @@ +#include "fullmodelviewer.h" + +#include "magic_enum.hpp" +#include + +FullModelViewer::FullModelViewer(GameData *data) : data(data) { + setWindowTitle("Full Model Viewer"); + setMinimumWidth(640); + setMinimumHeight(480); + + auto layout = new QVBoxLayout(); + setLayout(layout); + + gearView = new GearView(data); + layout->addWidget(gearView); + + auto controlLayout = new QHBoxLayout(); + layout->addLayout(controlLayout); + + raceCombo = new QComboBox(); + connect(raceCombo, qOverload(&QComboBox::currentIndexChanged), [this](int index) { + gearView->setRace((Race)index); + }); + controlLayout->addWidget(raceCombo); + + for (auto [race, race_name] : magic_enum::enum_entries()) { + raceCombo->addItem(race_name.data()); + } + + genderCombo = new QComboBox(); + connect(genderCombo, qOverload(&QComboBox::currentIndexChanged), [this](int index) { + gearView->setGender((Gender)index); + }); + controlLayout->addWidget(genderCombo); + + for (auto [gender, gender_name] : magic_enum::enum_entries()) { + genderCombo->addItem(gender_name.data()); + } + + connect(this, &FullModelViewer::gearChanged, this, &FullModelViewer::reloadGear); + + reloadGear(); +} + +void FullModelViewer::clear() { + topSlot.reset(); + bottomSlot.reset(); + + Q_EMIT gearChanged(); +} + +void FullModelViewer::addGear(GearInfo &info) { + switch(info.slot) { + case Slot::Body: + topSlot = info; + break; + case Slot::Legs: + bottomSlot = info; + break; + default: + break; + } + + Q_EMIT gearChanged(); +} + +void FullModelViewer::reloadGear() { + gearView->clear(); + + if (topSlot.has_value()) { + gearView->addGear(*topSlot); + } else { + // smallclothes body + GearInfo info = {}; + info.name = "Smallclothes Body"; + info.slot = Slot::Body; + + gearView->addGear(info); + } + + if (bottomSlot.has_value()) { + gearView->addGear(*bottomSlot); + } else { + // smallclothes legs + GearInfo info = {}; + info.name = "Smallclothes Legs"; + info.slot = Slot::Legs; + + gearView->addGear(info); + } +} + +#include "moc_fullmodelviewer.cpp" \ No newline at end of file diff --git a/mdlviewer/src/gearview.cpp b/mdlviewer/src/gearview.cpp new file mode 100644 index 0000000..3a2e717 --- /dev/null +++ b/mdlviewer/src/gearview.cpp @@ -0,0 +1,144 @@ +#include "gearview.h" +#include "magic_enum.hpp" + +#include +#include + +GearView::GearView(GameData *data) : data(data) { + mdlPart = new MDLPart(data); + + mdlPart->setSkeleton(physis_skeleton_from_skel(physis_read_file("c0101b0001.skel"))); + + auto layout = new QVBoxLayout(); + layout->addWidget(mdlPart); + setLayout(layout); + + connect(this, &GearView::gearChanged, this, &GearView::reloadModel); + + connect(this, &GearView::raceChanged, this, &GearView::reloadModel); + connect(this, &GearView::genderChanged, this, &GearView::reloadModel); + connect(this, &GearView::levelOfDetailChanged, this, &GearView::reloadModel); +} + +std::vector GearView::supportedRaces() const { + std::vector races; + for (const auto& gear : gears) { + for(auto [race, race_name] : magic_enum::enum_entries()) { + auto equip_path = physis_build_equipment_path(gear.modelInfo.primaryID, race, Subrace::Midlander, currentGender, gear.slot); + + if(physis_gamedata_exists(data, equip_path)) + races.push_back(race); + } + } + + return races; +} + +std::vector GearView::supportedGenders() const { + std::vector genders; + for (const auto& gear : gears) { + for(auto [gender, gender_name] : magic_enum::enum_entries()) { + auto equip_path = physis_build_equipment_path(gear.modelInfo.primaryID, currentRace, Subrace::Midlander, currentGender, gear.slot); + + if(physis_gamedata_exists(data, equip_path)) + genders.push_back(gender); + } + } + + return genders; +} + +int GearView::lodCount() const { + return maxLod; +} + +void GearView::exportModel(const QString &fileName) { + mdlPart->exportModel(fileName); +} + +void GearView::clear() { + gears.clear(); + + Q_EMIT gearChanged(); +} + +void GearView::addGear(GearInfo& gear) { + qDebug() << "Adding gear" << gear.name.c_str(); + + gears.push_back(gear); + + Q_EMIT gearChanged(); +} + +void GearView::setRace(Race race) { + if (currentRace == race) { + return; + } + + currentRace = race; + Q_EMIT raceChanged(); +} + +void GearView::setGender(Gender gender) { + if (currentGender == gender) { + return; + } + + currentGender = gender; + Q_EMIT genderChanged(); +} + +void GearView::setLevelOfDetail(int lod) { + if (currentLod == lod) { + return; + } + + currentLod = lod; + Q_EMIT levelOfDetailChanged(); +} + +void GearView::reloadModel() { + mdlPart->clear(); + + maxLod = 0; + + for (const auto& gear : gears) { + auto mdl_data = physis_gamedata_extract_file(data, physis_build_equipment_path(gear.modelInfo.primaryID, currentRace, Subrace::Midlander, currentGender, gear.slot)); + if (mdl_data.size > 0) { + auto mdl = physis_mdl_parse(mdl_data.size, mdl_data.data); + + std::vector materials; + for (int i = 0; i < mdl.num_material_names; i++) { + const char* material_name = mdl.material_names[i]; + + //std::string mtrl_path = loadedGear.gearInfo->getMtrlPath(201); + std::string mtrl_path = fmt::format("chara/equipment/e{gearId:04d}/material/v{gearVersion:04d}{}", material_name, + fmt::arg("gearId", gear.modelInfo.primaryID), + fmt::arg("gearVersion", gear.modelInfo.gearVersion)); + + int bodyCode = 1; + + // skin path + std::string skinmtrl_path = fmt::format("chara/human/c{raceCode:04d}/obj/body/b{bodyCode:04d}/material/v0001{}", material_name, + fmt::arg("raceCode", physis_get_race_code(currentRace, Subrace::Midlander, currentGender)), + fmt::arg("bodyCode", bodyCode)); + + if(physis_gamedata_exists(data, mtrl_path.c_str())) { + auto mat = physis_material_parse(physis_gamedata_extract_file(data, mtrl_path.c_str())); + materials.push_back(mat); + } + + if(physis_gamedata_exists(data, skinmtrl_path.c_str())) { + auto mat = physis_material_parse(physis_gamedata_extract_file(data, skinmtrl_path.c_str())); + materials.push_back(mat); + } + } + + maxLod = std::max(mdl.num_lod, maxLod); + + mdlPart->addModel(mdl, materials, currentLod); + } + } +} + +#include "moc_gearview.cpp" diff --git a/mdlviewer/src/main.cpp b/mdlviewer/src/main.cpp index ee450a6..530ddfe 100644 --- a/mdlviewer/src/main.cpp +++ b/mdlviewer/src/main.cpp @@ -6,8 +6,10 @@ int main(int argc, char* argv[]) { QApplication app(argc, argv); + physis_initialize_logging(); + MainWindow w(physis_gamedata_initialize(argv[1])); w.show(); - return app.exec(); + return QApplication::exec(); } \ No newline at end of file diff --git a/mdlviewer/src/mainwindow.cpp b/mdlviewer/src/mainwindow.cpp index e83c3c5..b1bb8ea 100644 --- a/mdlviewer/src/mainwindow.cpp +++ b/mdlviewer/src/mainwindow.cpp @@ -1,103 +1,19 @@ #include "mainwindow.h" -#include -#include #include #include -#include #include -#include #include -#include -#include #include -#include -#include -#include + #include #include #include -#include #include #include #include #include #include -#include - -#include "vec3edit.h" - -#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 - -void MainWindow::calculate_bone_inverse_pose(physis_Skeleton& skeleton, physis_Bone& bone, physis_Bone* parent_bone) { - const glm::mat4 parentMatrix = parent_bone == nullptr ? glm::mat4(1.0f) : extraBone[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])); - - extraBone[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) { - calculate_bone_inverse_pose(skeleton, skeleton.bones[i], &bone); - } - } -} void addItem(physis_Skeleton& skeleton, physis_Bone& bone, QTreeWidget* widget, QTreeWidgetItem* parent_item = nullptr) { auto item = new QTreeWidgetItem(); @@ -117,11 +33,12 @@ void addItem(physis_Skeleton& skeleton, physis_Bone& bone, QTreeWidget* widget, MainWindow::MainWindow(GameData* in_data) : data(*in_data) { setWindowTitle("mdlviewer"); - setMinimumSize(QSize(640, 480)); + setMinimumSize(QSize(800, 600)); auto fileMenu = menuBar()->addMenu("File"); - auto openMDLFile = fileMenu->addAction("Open MDL..."); + // TODO: move to a dedicated mdlview? + /*auto openMDLFile = fileMenu->addAction("Open MDL..."); connect(openMDLFile, &QAction::triggered, [=] { auto fileName = QFileDialog::getOpenFileName(nullptr, "Open MDL File", @@ -133,7 +50,7 @@ MainWindow::MainWindow(GameData* in_data) : data(*in_data) { loadedGear.model = physis_mdl_parse(buffer.size, buffer.data); reloadGearAppearance(); - }); + });*/ auto dummyWidget = new QWidget(); setCentralWidget(dummyWidget); @@ -160,7 +77,7 @@ MainWindow::MainWindow(GameData* in_data) : data(*in_data) { } auto exh = physis_gamedata_read_excel_sheet_header(&data, "Item"); - auto exd = physis_gamedata_read_excel_sheet(&data, "Item", exh, Language::English, 1); + auto exd = physis_gamedata_read_excel_sheet(&data, "Item", &exh, Language::English, 1); for(int i = 0; i < exd.row_count; i++) { const auto row = exd.row_data[i]; @@ -186,89 +103,23 @@ MainWindow::MainWindow(GameData* in_data) : data(*in_data) { layout->addWidget(listWidget); - renderer = new Renderer(); - - auto viewportLayout = new QVBoxLayout(); - layout->addLayout(viewportLayout); - -#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(); + gearView = new SingleGearView(&data); + connect(gearView, &SingleGearView::addToFullModelViewer, this, [=](GearInfo& info) { + fullModelViewer->addGear(info); }); - timer->start(1000); -#endif - - QHBoxLayout* controlLayout = new QHBoxLayout(); - viewportLayout->addLayout(controlLayout); - - raceCombo = new QComboBox(); - - connect(raceCombo, qOverload(&QComboBox::currentIndexChanged), [this](int index) { - if(index != -1) { - currentRace = (Race) index; - reloadGearModel(); - } - }); - - controlLayout->addWidget(raceCombo); - - lodCombo = new QComboBox(); - - connect(lodCombo, qOverload(&QComboBox::currentIndexChanged), [this](int index) { - currentLod = index; - reloadGearAppearance(); - }); - - controlLayout->addWidget(lodCombo); - - QPushButton* exportButton = new QPushButton("Export"); - - connect(exportButton, &QPushButton::clicked, [this] { - QString fileName = QFileDialog::getSaveFileName(this, tr("Save Model"), - "model.fbx", - tr("FBX Files (*.fbx)")); - - physis_MDL model; -#ifdef USE_STANDALONE_WINDOW - model = standaloneWindow->models[0].model; -#else - model = vkWindow->models[0].model; -#endif - exportModel(model, skeleton, fileName); - }); - - controlLayout->addWidget(exportButton); + layout->addWidget(gearView); connect(listWidget, &QListWidget::itemClicked, [this](QListWidgetItem* item) { for(auto& gear : gears) { if(gear.name == item->text().toStdString()) { - loadInitialGearInfo(gear); + gearView->setGear(gear); return; } } }); - skeleton = physis_skeleton_from_skel(physis_read_file("c0101b0001.skel")); - extraBone.resize(skeleton.num_bones); - calculate_bone_inverse_pose(skeleton, *skeleton.root_bone, nullptr); - - auto boneListWidget = new QTreeWidget(); + // TODO: reintroduce into SingleGearView + /*auto boneListWidget = new QTreeWidget(); for(auto& bone : extraBone) { bone.inversePose = glm::inverse(bone.inversePose); } @@ -293,235 +144,8 @@ MainWindow::MainWindow(GameData* in_data) : data(*in_data) { memcpy(currentEditedBone->scale, glm::value_ptr(currentScale), sizeof(float) * 3); reloadGearAppearance(); }); - layout->addWidget(scaleEdit); -} + layout->addWidget(scaleEdit);*/ -void MainWindow::exportModel(physis_MDL& model, physis_Skeleton& skeleton, QString fileName) { - Assimp::Exporter exporter; - - aiScene scene; - scene.mRootNode = new aiNode(); - - scene.mRootNode->mNumChildren = model.lods[0].num_parts + 1; // plus one for the skeleton - scene.mRootNode->mChildren = new aiNode*[scene.mRootNode->mNumChildren]; - - scene.mNumMeshes = 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 < model.num_affected_bones; i++) { - auto& node = skeletonNodes.emplace_back(); - node = new aiNode(); - node->mName = 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, model.affected_bone_names[i]) == 0) { - real_bone_id = k; - } - } - - node->mChildren = new aiNode*[model.num_affected_bones]; - - auto& real_bone = skeleton.bones[real_bone_id]; - memcpy(&node->mTransformation, glm::value_ptr(extraBone[real_bone.index].finalTransform), sizeof(aiMatrix4x4)); - } - - // setup parenting - for(int i = 0; i < 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, 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 < model.num_affected_bones; k++) { - if(strcmp(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*[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 < 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 = 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 = 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 = model.num_affected_bones; - mesh->mBones = new aiBone*[mesh->mNumBones]; - for(int j = 0; j < mesh->mNumBones; j++) { - int real_bone_id = j; - /*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 = 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 (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 = model.lods[0].parts[i].vertices[k].bone_weight[z]; - } - } - } - } - - mesh->mNumFaces = model.lods[0].parts[i].num_indices / 3; - mesh->mFaces = new aiFace[mesh->mNumFaces]; - - int lastFace = 0; - for(int j = 0; j < 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] = model.lods[0].parts[i].indices[j]; - face.mIndices[1] = model.lods[0].parts[i].indices[j + 1]; - face.mIndices[2] = 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 MainWindow::loadInitialGearInfo(GearInfo& info) { - loadedGear.gearInfo = &info; - - raceCombo->clear(); - for(auto [race, race_name] : magic_enum::enum_entries()) { - auto equip_path = physis_build_equipment_path(loadedGear.gearInfo->modelInfo.primaryID, race, currentSubrace, currentGender, loadedGear.gearInfo->slot); - - if(physis_gamedata_exists(&data, equip_path)) - raceCombo->addItem(race_name.data()); - } - - currentLod = 0; - currentRace = Race::Hyur; - - reloadGearModel(); -} - -void MainWindow::reloadGearModel() { - auto mdl_data = physis_gamedata_extract_file(&data, physis_build_equipment_path(loadedGear.gearInfo->modelInfo.primaryID, currentRace, currentSubrace, currentGender, loadedGear.gearInfo->slot)); - - loadedGear.model = physis_mdl_parse(mdl_data.size, mdl_data.data); - - std::string mtrl_path = loadedGear.gearInfo->getMtrlPath(101); - qDebug() << "MTRL path: " << mtrl_path.c_str(); - - if(physis_gamedata_exists(&data, mtrl_path.c_str())) { - qDebug() << "loading mtrl..."; - loadedGear.material = physis_material_parse(physis_gamedata_extract_file(&data, mtrl_path.c_str())); - } - - lodCombo->clear(); - for(int i = 0; i < loadedGear.model.num_lod; i++) - lodCombo->addItem(QString::number(i)); - - reloadGearAppearance(); -} - -void MainWindow::calculate_bone(physis_Skeleton& skeleton, physis_Bone& bone, const physis_Bone* parent_bone) { - const glm::mat4 parent_matrix = parent_bone == nullptr ? glm::mat4(1.0f) : extraBone[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])); - - extraBone[bone.index].localTransform = parent_matrix * local; - extraBone[bone.index].finalTransform = extraBone[bone.index].localTransform * extraBone[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) { - calculate_bone(skeleton, skeleton.bones[i], &bone); - } - } -} - -void MainWindow::reloadGearAppearance() { - loadedGear.renderModel = renderer->addModel(loadedGear.model, currentLod); - - if(loadedGear.material.num_textures > 0) { - auto texture = physis_texture_parse(physis_gamedata_extract_file(&data, loadedGear.material.textures[0])); - - loadedGear.renderTexture = renderer->addTexture(texture.width, texture.height, texture.rgba, texture.rgba_size); - - loadedGear.renderModel.texture = &loadedGear.renderTexture; - } - - calculate_bone(skeleton, *skeleton.root_bone, nullptr); - - // we want to map the actual affected bones to bone ids - std::map boneMapping; - for(int i = 0; i < loadedGear.model.num_affected_bones; i++) { - for(int k = 0; k < skeleton.num_bones; k++) { - if(strcmp(skeleton.bones[k].name, loadedGear.model.affected_bone_names[i]) == 0) - boneMapping[i] = k; - } - } - - for(int i = 0; i < loadedGear.model.num_affected_bones; i++) { - loadedGear.renderModel.boneData[i] = extraBone[boneMapping[i]].finalTransform; - } - -#ifndef USE_STANDALONE_WINDOW - vkWindow->models = {loadedGear.renderModel}; -#else - standaloneWindow->models = {loadedGear.renderModel}; -#endif + fullModelViewer = new FullModelViewer(&data); + fullModelViewer->show(); } \ No newline at end of file diff --git a/mdlviewer/src/singlegearview.cpp b/mdlviewer/src/singlegearview.cpp new file mode 100644 index 0000000..424f9d4 --- /dev/null +++ b/mdlviewer/src/singlegearview.cpp @@ -0,0 +1,149 @@ +#include "singlegearview.h" + +#include +#include +#include + +#include "magic_enum.hpp" + +SingleGearView::SingleGearView(GameData* data) : data(data) { + gearView = new GearView(data); + + auto layout = new QVBoxLayout(); + layout->addWidget(gearView); + setLayout(layout); + + auto controlLayout = new QHBoxLayout(); + layout->addLayout(controlLayout); + + raceCombo = new QComboBox(); + connect(raceCombo, qOverload(&QComboBox::currentIndexChanged), [this](int index) { + if(loadingComboData) + return; + + setRace((Race)index); + }); + controlLayout->addWidget(raceCombo); + + genderCombo = new QComboBox(); + connect(genderCombo, qOverload(&QComboBox::currentIndexChanged), [this](int index) { + if(loadingComboData) + return; + + setGender((Gender)index); + }); + controlLayout->addWidget(genderCombo); + + lodCombo = new QComboBox(); + connect(lodCombo, qOverload(&QComboBox::currentIndexChanged), [this](int index) { + if(loadingComboData) + return; + + setLevelOfDetail(index); + }); + controlLayout->addWidget(lodCombo); + + addToFMVButton = new QPushButton("Add to FMV"); + connect(addToFMVButton, &QPushButton::clicked, this, [this](bool) { + if(currentGear.has_value()) { + Q_EMIT addToFullModelViewer(*currentGear); + } + }); + controlLayout->addWidget(addToFMVButton); + + exportButton = new QPushButton("Export..."); + connect(exportButton, &QPushButton::clicked, this, [this](bool) { + QString fileName = QFileDialog::getSaveFileName(this, tr("Save Model"), + "model.fbx", + tr("FBX Files (*.fbx)")); + + gearView->exportModel(fileName); + }); + controlLayout->addWidget(exportButton); + + connect(this, &SingleGearView::gearChanged, this, &SingleGearView::reloadGear); + connect(this, &SingleGearView::raceChanged, this, [=] { + gearView->setRace(currentRace); + }); + connect(this, &SingleGearView::genderChanged, this, [=] { + gearView->setGender(currentGender); + }); + connect(this, &SingleGearView::levelOfDetailChanged, this, [=] { + gearView->setLevelOfDetail(currentLod); + }); + + reloadGear(); +} + +void SingleGearView::clear() { + currentGear.reset(); + + Q_EMIT gearChanged(); +} + +void SingleGearView::setGear(GearInfo &info) { + currentGear = info; + + Q_EMIT gearChanged(); +} + +void SingleGearView::setRace(Race race) { + if (currentRace == race) { + return; + } + + currentRace = race; + Q_EMIT raceChanged(); +} + +void SingleGearView::setGender(Gender gender) { + if (currentGender == gender) { + return; + } + + currentGender = gender; + Q_EMIT genderChanged(); +} + +void SingleGearView::setLevelOfDetail(int lod) { + if (currentLod == lod) { + return; + } + + currentLod = lod; + Q_EMIT levelOfDetailChanged(); +} + +void SingleGearView::reloadGear() { + gearView->clear(); + + raceCombo->setEnabled(currentGear.has_value()); + genderCombo->setEnabled(currentGear.has_value()); + lodCombo->setEnabled(currentGear.has_value()); + addToFMVButton->setEnabled(currentGear.has_value()); + exportButton->setEnabled(currentGear.has_value()); + + if (currentGear.has_value()) { + gearView->addGear(*currentGear); + + loadingComboData = true; + + raceCombo->clear(); + for(auto race : gearView->supportedRaces()) { + raceCombo->addItem(magic_enum::enum_name(race).data()); + } + + genderCombo->clear(); + for(auto gender : gearView->supportedGenders()) { + genderCombo->addItem(magic_enum::enum_name(gender).data()); + } + + lodCombo->clear(); + for(int i = 0; i < gearView->lodCount(); i++) + lodCombo->addItem(QString::number(i)); + + loadingComboData = false; + } +} + +#include "moc_singlegearview.cpp" \ No newline at end of file