1
Fork 0
mirror of https://github.com/redstrate/Novus.git synced 2025-04-21 19:57:44 +00:00
novus/mdlviewer/src/mainwindow.cpp
Joshua Goins 9688c091af Add bone editing to mdlviewer
This is big, as it shows we are now correctly parsing the havok XML
sidecard data and you can edit the scale of the bones in the viewport.

This also pulls in a new libxiv version, which is required to fill out
the used bones list on a Model. Right now the bone editing is incredibly
basic, and the viewport suffers from a lack of depth testing still.
2022-04-28 17:50:05 -04:00

392 lines
No EOL
12 KiB
C++

#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 <glm/gtc/type_ptr.hpp>
#include "gamedata.h"
#include "exhparser.h"
#include "exdparser.h"
#include "mdlparser.h"
#include "equipment.h"
#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 calculate_bone_inverse_pose(Skeleton& skeleton, Bone& bone, Bone* parent_bone) {
const glm::mat4 parentMatrix = parent_bone == nullptr ? glm::mat4(1.0f) : parent_bone->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]));
bone.inversePose = parentMatrix * local;
for(auto& b : skeleton.bones) {
if(b.parent != nullptr && b.parent->name == bone.name)
calculate_bone_inverse_pose(skeleton, b, &bone);
}
}
MainWindow::MainWindow(GameData& data) : data(data) {
setWindowTitle("mdlviewer");
setMinimumSize(QSize(640, 480));
auto dummyWidget = new QWidget();
setCentralWidget(dummyWidget);
auto layout = new QHBoxLayout();
dummyWidget->setLayout(layout);
// smallclothes body
{
GearInfo info = {};
info.name = "Smallclothes Body";
info.slot = Slot::Body;
gears.push_back(info);
}
// smallclothes legs
{
GearInfo info = {};
info.name = "Smallclothes Legs";
info.slot = Slot::Legs;
gears.push_back(info);
}
auto exh = *data.readExcelSheet("Item");
auto path = getEXDFilename(exh, "item", getLanguageCode(Language::English), exh.pages[1]);
auto exd = readEXD(exh, *data.extractFile("exd/" + path), exh.pages[1]);
for(auto row : exd.rows) {
auto primaryModel = row.data[47].uint64Data;
auto secondaryModel = row.data[48].uint64Data;
int16_t parts[4];
memcpy(parts, &primaryModel, sizeof(int16_t) * 4);
GearInfo info = {};
info.name = row.data[9].data;
info.slot = *get_slot_from_id(row.data[17].uint64Data);
info.modelInfo.primaryID = parts[0];
gears.push_back(info);
}
auto listWidget = new QListWidget();
for(auto gear : gears)
listWidget->addItem(gear.name.c_str());
listWidget->setMaximumWidth(200);
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();
});
timer->start(1000);
#endif
QHBoxLayout* controlLayout = new QHBoxLayout();
viewportLayout->addLayout(controlLayout);
raceCombo = new QComboBox();
connect(raceCombo, qOverload<int>(&QComboBox::currentIndexChanged), [this](int index) {
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)"));
Model model;
#ifdef USE_STANDALONE_WINDOW
model = standaloneWindow->models[0].model;
#else
model = vkWindow->models[0].model;
#endif
exportModel(model, fileName);
});
controlLayout->addWidget(exportButton);
connect(listWidget, &QListWidget::itemClicked, [this](QListWidgetItem* item) {
for(auto& gear : gears) {
if(gear.name == item->text().toStdString()) {
loadInitialGearInfo(gear);
return;
}
}
});
skeleton = parseHavokXML("/home/josh/test.xml");
calculate_bone_inverse_pose(skeleton, *skeleton.root_bone, nullptr);
auto boneListWidget = new QListWidget();
for(auto& bone : skeleton.bones) {
bone.inversePose = glm::inverse(bone.inversePose);
boneListWidget->addItem(bone.name.c_str());
}
boneListWidget->setMaximumWidth(200);
connect(boneListWidget, &QListWidget::itemClicked, [this](QListWidgetItem* item) {
for(auto& bone : skeleton.bones) {
if(bone.name == item->text().toStdString()) {
currentScale = glm::make_vec3(bone.scale.data());
currentEditedBone = &bone;
}
}
});
layout->addWidget(boneListWidget);
Vector3Edit* scaleEdit = new Vector3Edit(currentScale);
connect(scaleEdit, &Vector3Edit::onValueChanged, [this] {
memcpy(currentEditedBone->scale.data(), glm::value_ptr(currentScale), sizeof(float) * 3);
reloadGearAppearance();
});
layout->addWidget(scaleEdit);
}
void MainWindow::exportModel(Model& model, QString fileName) {
Assimp::Exporter exporter;
aiScene scene;
scene.mRootNode = new aiNode();
scene.mRootNode->mNumChildren = model.lods[0].parts.size();
scene.mRootNode->mChildren = new aiNode*[scene.mRootNode->mNumChildren];
scene.mNumMeshes = model.lods[0].parts.size();
scene.mMeshes = new aiMesh*[scene.mNumMeshes];
for(int i = 0; i < model.lods[0].parts.size(); 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].vertices.size();
mesh->mVertices = new aiVector3D [mesh->mNumVertices];
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->mNumFaces = model.lods[0].parts[i].indices.size() / 3;
mesh->mFaces = new aiFace[mesh->mNumFaces];
int lastFace = 0;
for(int j = 0; j < model.lods[0].parts[i].indices.size(); 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>()) {
if(data.exists(build_equipment_path(loadedGear.gearInfo->modelInfo.primaryID, race, loadedGear.gearInfo->slot)))
raceCombo->addItem(race_name.data());
}
currentLod = 0;
currentRace = Race::HyurMidlanderMale;
reloadGearModel();
}
void MainWindow::reloadGearModel() {
auto mdl_data = data.extractFile(build_equipment_path(loadedGear.gearInfo->modelInfo.primaryID, currentRace, loadedGear.gearInfo->slot));
if(mdl_data == std::nullopt)
return;
loadedGear.model = parseMDL(*mdl_data);
lodCombo->clear();
for(int i = 0; i < loadedGear.model.lods.size(); i++)
lodCombo->addItem(QString::number(i));
reloadGearAppearance();
}
void calculate_bone(Skeleton& skeleton, Bone& bone, const Bone* parent_bone) {
glm::mat4 parent_matrix = glm::mat4(1.0f);
if(parent_bone != nullptr)
parent_matrix = parent_bone->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]));
bone.localTransform = parent_matrix * local;
bone.finalTransform = bone.localTransform * bone.inversePose;
for(auto& b : skeleton.bones) {
if(b.parent != nullptr && b.parent->name == bone.name)
calculate_bone(skeleton, b, &bone);
}
}
void MainWindow::reloadGearAppearance() {
loadedGear.renderModel = renderer->addModel(loadedGear.model, currentLod);
calculate_bone(skeleton, *skeleton.root_bone, nullptr);
for(int i = 0; i < 128; i++) {
loadedGear.renderModel.boneData[i] = glm::mat4(1.0f);
}
// we want to map the actual affected bones to bone ids
std::map<int, int> boneMapping;
for(int i = 0; i < loadedGear.model.affectedBoneNames.size(); i++) {
for(int k = 0; k < skeleton.bones.size(); k++) {
if(skeleton.bones[k].name == loadedGear.model.affectedBoneNames[i])
boneMapping[i] = k;
}
}
for(int i = 0; i < loadedGear.model.affectedBoneNames.size(); i++) {
loadedGear.renderModel.boneData[i] = skeleton.bones[boneMapping[i]].finalTransform;
}
#ifndef USE_STANDALONE_WINDOW
vkWindow->models = {loadedGear.renderModel};
#else
standaloneWindow->models = {loadedGear.renderModel};
#endif
}