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