mirror of
https://github.com/redstrate/Novus.git
synced 2025-04-20 19:57:44 +00:00
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.
This commit is contained in:
parent
102ccd96ae
commit
7407d26247
10 changed files with 574 additions and 456 deletions
|
@ -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}")
|
||||
|
|
32
mdlviewer/include/fullmodelviewer.h
Normal file
32
mdlviewer/include/fullmodelviewer.h
Normal file
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#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<GearInfo> topSlot;
|
||||
std::optional<GearInfo> bottomSlot;\
|
||||
|
||||
GearView* gearView = nullptr;
|
||||
QComboBox* raceCombo, *genderCombo;
|
||||
|
||||
GameData* data = nullptr;
|
||||
};
|
76
mdlviewer/include/gearview.h
Normal file
76
mdlviewer/include/gearview.h
Normal file
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <physis.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <QComboBox>
|
||||
#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<Race> supportedRaces() const;
|
||||
|
||||
/// Returns an inclusive list of genders supported by the current gearset.
|
||||
std::vector<Gender> 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<GearInfo> gears;
|
||||
|
||||
MDLPart* mdlPart = nullptr;
|
||||
|
||||
GameData* data;
|
||||
};
|
|
@ -6,75 +6,21 @@
|
|||
#include <physis.hpp>
|
||||
#include <unordered_map>
|
||||
|
||||
#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<GearInfo> 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<BoneExtra> extraBone;
|
||||
};
|
48
mdlviewer/include/singlegearview.h
Normal file
48
mdlviewer/include/singlegearview.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QPushButton>
|
||||
#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<GearInfo> 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;
|
||||
};
|
93
mdlviewer/src/fullmodelviewer.cpp
Normal file
93
mdlviewer/src/fullmodelviewer.cpp
Normal file
|
@ -0,0 +1,93 @@
|
|||
#include "fullmodelviewer.h"
|
||||
|
||||
#include "magic_enum.hpp"
|
||||
#include <QVBoxLayout>
|
||||
|
||||
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<int>(&QComboBox::currentIndexChanged), [this](int index) {
|
||||
gearView->setRace((Race)index);
|
||||
});
|
||||
controlLayout->addWidget(raceCombo);
|
||||
|
||||
for (auto [race, race_name] : magic_enum::enum_entries<Race>()) {
|
||||
raceCombo->addItem(race_name.data());
|
||||
}
|
||||
|
||||
genderCombo = new QComboBox();
|
||||
connect(genderCombo, qOverload<int>(&QComboBox::currentIndexChanged), [this](int index) {
|
||||
gearView->setGender((Gender)index);
|
||||
});
|
||||
controlLayout->addWidget(genderCombo);
|
||||
|
||||
for (auto [gender, gender_name] : magic_enum::enum_entries<Gender>()) {
|
||||
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"
|
144
mdlviewer/src/gearview.cpp
Normal file
144
mdlviewer/src/gearview.cpp
Normal file
|
@ -0,0 +1,144 @@
|
|||
#include "gearview.h"
|
||||
#include "magic_enum.hpp"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QDebug>
|
||||
|
||||
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<Race> GearView::supportedRaces() const {
|
||||
std::vector<Race> races;
|
||||
for (const auto& gear : gears) {
|
||||
for(auto [race, race_name] : magic_enum::enum_entries<Race>()) {
|
||||
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<Gender> GearView::supportedGenders() const {
|
||||
std::vector<Gender> genders;
|
||||
for (const auto& gear : gears) {
|
||||
for(auto [gender, gender_name] : magic_enum::enum_entries<Gender>()) {
|
||||
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<physis_Material> 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"
|
|
@ -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();
|
||||
}
|
|
@ -1,103 +1,19 @@
|
|||
#include "mainwindow.h"
|
||||
|
||||
#include <QWindow>
|
||||
#include <QVulkanInstance>
|
||||
#include <QHBoxLayout>
|
||||
#include <QTableWidget>
|
||||
#include <fmt/core.h>
|
||||
#include <QListWidget>
|
||||
#include <QVulkanWindow>
|
||||
#include <QLineEdit>
|
||||
#include <QResizeEvent>
|
||||
#include <QComboBox>
|
||||
#include <QTimer>
|
||||
#include <assimp/Exporter.hpp>
|
||||
#include <assimp/scene.h>
|
||||
#include <assimp/postprocess.h>
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QFileDialog>
|
||||
#include <magic_enum.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <QMenuBar>
|
||||
#include <QAction>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
#include <QTreeWidget>
|
||||
#include <physis.hpp>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#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<RenderModel> 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<int>(&QComboBox::currentIndexChanged), [this](int index) {
|
||||
if(index != -1) {
|
||||
currentRace = (Race) index;
|
||||
reloadGearModel();
|
||||
}
|
||||
});
|
||||
|
||||
controlLayout->addWidget(raceCombo);
|
||||
|
||||
lodCombo = new QComboBox();
|
||||
|
||||
connect(lodCombo, qOverload<int>(&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);
|
||||
}
|
||||
|
||||
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<aiNode*> 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<Race>()) {
|
||||
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<int, int> 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
|
||||
layout->addWidget(scaleEdit);*/
|
||||
|
||||
fullModelViewer = new FullModelViewer(&data);
|
||||
fullModelViewer->show();
|
||||
}
|
149
mdlviewer/src/singlegearview.cpp
Normal file
149
mdlviewer/src/singlegearview.cpp
Normal file
|
@ -0,0 +1,149 @@
|
|||
#include "singlegearview.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QFileDialog>
|
||||
|
||||
#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<int>(&QComboBox::currentIndexChanged), [this](int index) {
|
||||
if(loadingComboData)
|
||||
return;
|
||||
|
||||
setRace((Race)index);
|
||||
});
|
||||
controlLayout->addWidget(raceCombo);
|
||||
|
||||
genderCombo = new QComboBox();
|
||||
connect(genderCombo, qOverload<int>(&QComboBox::currentIndexChanged), [this](int index) {
|
||||
if(loadingComboData)
|
||||
return;
|
||||
|
||||
setGender((Gender)index);
|
||||
});
|
||||
controlLayout->addWidget(genderCombo);
|
||||
|
||||
lodCombo = new QComboBox();
|
||||
connect(lodCombo, qOverload<int>(&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"
|
Loading…
Add table
Reference in a new issue