1
Fork 0
mirror of https://github.com/redstrate/Novus.git synced 2025-04-22 12:07:45 +00:00

Introduce the parts system and EXD and MDL parts

These parts (inspired by the KDE parts system) will allow the tooling
to reuse GUI mechanisms. Right now the two supported parts are for Excel
and Models, and exdviewer and mdlviewer will be retrofitted to them in
future commits.
This commit is contained in:
Joshua Goins 2023-04-09 15:28:00 -04:00
parent 792da6da6a
commit 97f46bcca1
8 changed files with 595 additions and 1 deletions

View file

@ -43,3 +43,4 @@ add_subdirectory(mdlviewer)
add_subdirectory(argcracker) add_subdirectory(argcracker)
add_subdirectory(explorer) add_subdirectory(explorer)
add_subdirectory(bonedecomp) add_subdirectory(bonedecomp)
add_subdirectory(parts)

2
parts/CMakeLists.txt Normal file
View file

@ -0,0 +1,2 @@
add_subdirectory(exd)
add_subdirectory(mdl)

3
parts/exd/CMakeLists.txt Normal file
View file

@ -0,0 +1,3 @@
add_library(exdpart STATIC exdpart.cpp)
target_link_libraries(exdpart PUBLIC physis z ${LIBRARIES} Qt5::Core Qt5::Widgets)
target_include_directories(exdpart PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

97
parts/exd/exdpart.cpp Normal file
View file

@ -0,0 +1,97 @@
#include "exdpart.h"
#include <physis.hpp>
#include <QTableWidget>
#include <QDebug>
#include <QVBoxLayout>
EXDPart::EXDPart(GameData* data) : data(data) {
pageTabWidget = new QTabWidget();
auto layout = new QVBoxLayout();
layout->addWidget(pageTabWidget);
setLayout(layout);
}
void EXDPart::loadSheet(const QString& name) {
qDebug() << "Loading" << name;
pageTabWidget->clear();
auto exh = physis_gamedata_read_excel_sheet_header(data, name.toStdString().c_str());
for(int i = 0; i < exh.page_count; i++) {
QTableWidget* tableWidget = new QTableWidget();
tableWidget->setColumnCount(exh.column_count);
auto exd = physis_gamedata_read_excel_sheet(data, name.toStdString().c_str(), &exh, exh.languages[0], i);
tableWidget->setRowCount(exd.row_count);
for (int j = 0; j < exd.row_count; j++) {
for(int z = 0; z < exd.column_count; z++) {
auto data = exd.row_data[j].column_data[z];
QString columnString;
QString columnType;
switch (data.tag) {
case physis_ColumnData::Tag::String:
columnString = QString(data.string._0);
columnType = "String";
break;
case physis_ColumnData::Tag::Bool:
columnString = data.bool_._0 ? "True" : "False";
columnType = "Bool";
break;
case physis_ColumnData::Tag::Int8:
columnString = QString::number(data.int8._0);
columnType = "Int8";
break;
case physis_ColumnData::Tag::UInt8:
columnString = QString::number(data.u_int8._0);
columnType = "UInt8";
break;
case physis_ColumnData::Tag::Int16:
columnString = QString::number(data.int16._0);
columnType = "Int16";
break;
case physis_ColumnData::Tag::UInt16:
columnString = QString::number(data.u_int16._0);
columnType = "UInt16";
break;
case physis_ColumnData::Tag::Int32:
columnString = QString::number(data.int32._0);
columnType = "Int32";
break;
case physis_ColumnData::Tag::UInt32:
columnString = QString::number(data.u_int32._0);
columnType = "UInt32";
break;
case physis_ColumnData::Tag::Float32:
columnString = QString::number(data.float32._0);
columnType = "Float32";
break;
case physis_ColumnData::Tag::Int64:
columnString = QString::number(data.int64._0);
columnType = "Int64";
break;
case physis_ColumnData::Tag::UInt64:
columnString = QString::number(data.u_int64._0);
columnType = "UInt64";
break;
}
auto newItem = new QTableWidgetItem(columnString);
tableWidget->setItem(i, j, newItem);
QTableWidgetItem* headerItem = new QTableWidgetItem();
headerItem->setText(columnType);
tableWidget->setHorizontalHeaderItem(j, headerItem);
}
}
pageTabWidget->addTab(tableWidget, QString("Page %1").arg(i));
}
}

18
parts/exd/exdpart.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#include <QWidget>
#include <QTabWidget>
struct GameData;
class EXDPart : public QWidget {
public:
explicit EXDPart(GameData* data);
void loadSheet(const QString& name);
private:
GameData* data = nullptr;
QTabWidget* pageTabWidget = nullptr;
};

3
parts/mdl/CMakeLists.txt Normal file
View file

@ -0,0 +1,3 @@
add_library(mdlpart STATIC mdlpart.cpp)
target_link_libraries(mdlpart PUBLIC physis z ${LIBRARIES} Qt5::Core Qt5::Widgets renderer)
target_include_directories(mdlpart PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

408
parts/mdl/mdlpart.cpp Normal file
View file

@ -0,0 +1,408 @@
#include "mdlpart.h"
#include "glm/gtx/transform.hpp"
#include <QWindow>
#include <QVulkanInstance>
#include <QVulkanWindow>
#include <QResizeEvent>
#include <fmt/core.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <assimp/Exporter.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include <QVBoxLayout>
#include <glm/gtc/type_ptr.inl>
#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
MDLPart::MDLPart(GameData *data) : data(data) {
auto viewportLayout = new QVBoxLayout();
setLayout(viewportLayout);
renderer = new Renderer();
#ifndef USE_STANDALONE_WINDOW
auto inst = new QVulkanInstance();
inst->setVkInstance(renderer->instance);
inst->setFlags(QVulkanInstance::Flag::NoDebugOutputRedirect);
inst->create();
vkWindow = new VulkanWindow(renderer, inst);
vkWindow->setVulkanInstance(inst);
auto widget = QWidget::createWindowContainer(vkWindow);
viewportLayout->addWidget(widget);
#else
standaloneWindow = new StandaloneWindow(renderer);
renderer->initSwapchain(standaloneWindow->getSurface(renderer->instance), 640, 480);
QTimer* timer = new QTimer();
connect(timer, &QTimer::timeout, this, [this] {
standaloneWindow->render();
});
timer->start(1000);
#endif
connect(this, &MDLPart::modelChanged, this, &MDLPart::reloadRenderer);
connect(this, &MDLPart::skeletonChanged, this, &MDLPart::reloadBoneData);
}
void MDLPart::exportModel(const QString &fileName) {
Assimp::Exporter exporter;
aiScene scene;
scene.mRootNode = new aiNode();
// TODO: hardcoded to the first model for now
scene.mRootNode->mNumChildren = models[0].model.lods[0].num_parts + 1; // plus one for the skeleton
scene.mRootNode->mChildren = new aiNode*[scene.mRootNode->mNumChildren];
scene.mNumMeshes = models[0].model.lods[0].num_parts;
scene.mMeshes = new aiMesh*[scene.mNumMeshes];
auto skeleton_node = new aiNode();
skeleton_node->mName = "Skeleton";
skeleton_node->mNumChildren = 1;
skeleton_node->mChildren = new aiNode*[skeleton_node->mNumChildren];
scene.mRootNode->mChildren[scene.mRootNode->mNumChildren - 1] = skeleton_node;
std::vector<aiNode*> skeletonNodes;
for(int i = 0; i < models[0].model.num_affected_bones; i++) {
auto& node = skeletonNodes.emplace_back();
node = new aiNode();
node->mName = models[0].model.affected_bone_names[i];
int real_bone_id = 0;
for(int k = 0; k < skeleton->num_bones; k++) {
if(strcmp(skeleton->bones[k].name, models[0].model.affected_bone_names[i]) == 0) {
real_bone_id = k;
}
}
node->mChildren = new aiNode*[models[0].model.num_affected_bones];
auto& real_bone = skeleton->bones[real_bone_id];
memcpy(&node->mTransformation, glm::value_ptr(boneData[real_bone.index].finalTransform), sizeof(aiMatrix4x4));
}
// setup parenting
for(int i = 0; i < models[0].model.num_affected_bones; i++) {
int real_bone_id = 0;
for(int k = 0; k < skeleton->num_bones; k++) {
if(strcmp(skeleton->bones[k].name, models[0].model.affected_bone_names[i]) == 0) {
real_bone_id = k;
}
}
auto& real_bone = skeleton->bones[real_bone_id];
if(real_bone.parent_bone != nullptr) {
for(int k = 0; k < models[0].model.num_affected_bones; k++) {
if(strcmp(models[0].model.affected_bone_names[k], real_bone.parent_bone->name) == 0) {
skeletonNodes[i]->mParent = skeletonNodes[k];
skeletonNodes[k]->mChildren[skeletonNodes[k]->mNumChildren++] = skeletonNodes[i];
}
}
}
}
skeleton_node->mChildren[0] = new aiNode();
skeleton_node->mChildren[0]->mName = "root";
skeleton_node->mChildren[0]->mChildren = new aiNode*[models[0].model.num_affected_bones];
for(int i = 0; i < skeletonNodes.size(); i++) {
if(skeletonNodes[i]->mParent == nullptr) {
skeleton_node->mChildren[0]->mChildren[skeleton_node->mChildren[0]->mNumChildren++] = skeletonNodes[i];
}
}
for(int i = 0; i < models[0].model.lods[0].num_parts; i++) {
scene.mMeshes[i] = new aiMesh();
scene.mMeshes[i]->mMaterialIndex = 0;
auto& node = scene.mRootNode->mChildren[i];
node = new aiNode();
node->mNumMeshes = 1;
node->mMeshes = new unsigned int [scene.mRootNode->mNumMeshes];
node->mMeshes[0] = i;
auto mesh = scene.mMeshes[i];
mesh->mNumVertices = models[0].model.lods[0].parts[i].num_vertices;
mesh->mVertices = new aiVector3D [mesh->mNumVertices];
mesh->mNormals = new aiVector3D [mesh->mNumVertices];
mesh->mTextureCoords[0] = new aiVector3D [mesh->mNumVertices];
mesh->mNumUVComponents[0] = 2;
for(int j = 0; j < mesh->mNumVertices; j++) {
auto vertex = models[0].model.lods[0].parts[i].vertices[j];
mesh->mVertices[j] = aiVector3D(vertex.position[0], vertex.position[1], vertex.position[2]);
mesh->mNormals[j] = aiVector3D (vertex.normal[0], vertex.normal[1], vertex.normal[2]);
mesh->mTextureCoords[0][j] = aiVector3D(vertex.uv[0], vertex.uv[1], 0.0f);
}
mesh->mNumBones = models[0].model.num_affected_bones;
mesh->mBones = new aiBone*[mesh->mNumBones];
for(int j = 0; j < mesh->mNumBones; j++) {
int real_bone_id = j;
// TODO: is this still relevant?5
/*for(int k = 0; k < skeleton.bones.size(); k++) {
if(skeleton.bones[k].name == model.affectedBoneNames[j]) {
real_bone_id = k;
}
}*/
mesh->mBones[j] = new aiBone();
mesh->mBones[j]->mName = models[0].model.affected_bone_names[j];
mesh->mBones[j]->mNumWeights = mesh->mNumVertices * 4;
mesh->mBones[j]->mWeights = new aiVertexWeight[mesh->mBones[j]->mNumWeights];
mesh->mBones[j]->mNode = skeleton_node->mChildren[j];
for(int k = 0; k < mesh->mNumVertices; k++) {
for(int z = 0; z < 4; z++) {
if (models[0].model.lods[0].parts[i].vertices[k].bone_id[z] == real_bone_id) {
auto &weight = mesh->mBones[j]->mWeights[k * 4 + z];
weight.mVertexId = k;
weight.mWeight = models[0].model.lods[0].parts[i].vertices[k].bone_weight[z];
}
}
}
}
mesh->mNumFaces = models[0].model.lods[0].parts[i].num_indices / 3;
mesh->mFaces = new aiFace[mesh->mNumFaces];
int lastFace = 0;
for(int j = 0; j < models[0].model.lods[0].parts[i].num_indices; j += 3) {
aiFace& face = mesh->mFaces[lastFace++];
face.mNumIndices = 3;
face.mIndices = new unsigned int[face.mNumIndices];
face.mIndices[0] = models[0].model.lods[0].parts[i].indices[j];
face.mIndices[1] = models[0].model.lods[0].parts[i].indices[j + 1];
face.mIndices[2] = models[0].model.lods[0].parts[i].indices[j + 2];
}
}
scene.mNumMaterials = 1;
scene.mMaterials = new aiMaterial*[1];
scene.mMaterials[0] = new aiMaterial();
exporter.Export(&scene, "fbx", fileName.toStdString());
}
void MDLPart::clear() {
models.clear();
Q_EMIT modelChanged();
}
void MDLPart::addModel(physis_MDL mdl, std::vector<physis_Material> materials, int lod) {
qDebug() << "Adding model to MDLPart";
auto model = renderer->addModel(mdl, lod);
std::transform(materials.begin(), materials.end(), std::back_inserter(model.materials), [this](const physis_Material& mat) {
return createMaterial(mat);
});
models.push_back(model);
Q_EMIT modelChanged();
}
void MDLPart::setSkeleton(physis_Skeleton newSkeleton) {
skeleton = newSkeleton;
Q_EMIT skeletonChanged();
}
void MDLPart::clearSkeleton() {
skeleton.reset();
Q_EMIT skeletonChanged();
}
void MDLPart::reloadRenderer() {
qDebug() << "Reloading render models...";
reloadBoneData();
#ifndef USE_STANDALONE_WINDOW
vkWindow->models = models;
#else
standaloneWindow->models = models;
#endif
}
void MDLPart::reloadBoneData() {
if(skeleton.has_value()) {
// first-time data, TODO split out
boneData.resize(skeleton->num_bones);
calculateBoneInversePose(*skeleton, *skeleton->root_bone, nullptr);
for(auto& bone : boneData) {
bone.inversePose = glm::inverse(bone.inversePose);
}
// update data
calculateBone(*skeleton, *skeleton->root_bone, nullptr);
for(auto& model : models) {
// we want to map the actual affected bones to bone ids
std::map<int, int> boneMapping;
for (int i = 0; i < model.model.num_affected_bones; i++) {
for (int k = 0; k < skeleton->num_bones; k++) {
if (strcmp(skeleton->bones[k].name, model.model.affected_bone_names[i]) == 0)
boneMapping[i] = k;
}
}
for (int i = 0; i < model.model.num_affected_bones; i++) {
model.boneData[i] = boneData[boneMapping[i]].finalTransform;
}
}
}
}
RenderMaterial MDLPart::createMaterial(const physis_Material &material) {
RenderMaterial newMaterial;
for (int i = 0; i < material.num_textures; i++) {
std::string t = material.textures[i];
if (t.find("skin") != std::string::npos) {
newMaterial.type = MaterialType::Skin;
}
char type = t[t.length() - 5];
switch(type) {
case 'm': {
auto texture = physis_texture_parse(physis_gamedata_extract_file(data, material.textures[i]));
auto tex = renderer->addTexture(texture.width, texture.height, texture.rgba, texture.rgba_size);
newMaterial.multiTexture = new RenderTexture(tex);
}
case 'd': {
auto texture = physis_texture_parse(physis_gamedata_extract_file(data, material.textures[i]));
auto tex = renderer->addTexture(texture.width, texture.height, texture.rgba, texture.rgba_size);
newMaterial.diffuseTexture = new RenderTexture(tex);
}
break;
case 'n': {
auto texture = physis_texture_parse(physis_gamedata_extract_file(data, material.textures[i]));
auto tex = renderer->addTexture(texture.width, texture.height, texture.rgba, texture.rgba_size);
newMaterial.normalTexture = new RenderTexture(tex);
}
break;
case 's': {
auto texture = physis_texture_parse(physis_gamedata_extract_file(data, material.textures[i]));
auto tex = renderer->addTexture(texture.width, texture.height, texture.rgba, texture.rgba_size);
newMaterial.specularTexture = new RenderTexture(tex);
}
break;
default:
qDebug() << "unhandled type" << type;
break;
}
}
return newMaterial;
}
void MDLPart::calculateBoneInversePose(physis_Skeleton& skeleton, physis_Bone& bone, physis_Bone* parent_bone) {
const glm::mat4 parentMatrix = parent_bone == nullptr ? glm::mat4(1.0f) : boneData[parent_bone->index].inversePose;
glm::mat4 local(1.0f);
local = glm::translate(local, glm::vec3(bone.position[0], bone.position[1], bone.position[2]));
local *= glm::mat4_cast(glm::quat(bone.rotation[3], bone.rotation[0], bone.rotation[1], bone.rotation[2]));
local = glm::scale(local, glm::vec3(bone.scale[0], bone.scale[1], bone.scale[2]));
boneData[bone.index].inversePose = parentMatrix * local;
for(int i = 0; i < skeleton.num_bones; i++) {
if(skeleton.bones[i].parent_bone != nullptr && strcmp(skeleton.bones[i].parent_bone->name, bone.name) == 0) {
calculateBoneInversePose(skeleton, skeleton.bones[i], &bone);
}
}
}
void MDLPart::calculateBone(physis_Skeleton& skeleton, physis_Bone& bone, const physis_Bone* parent_bone) {
const glm::mat4 parent_matrix = parent_bone == nullptr ? glm::mat4(1.0f) : boneData[parent_bone->index].localTransform;
glm::mat4 local = glm::mat4(1.0f);
local = glm::translate(local, glm::vec3(bone.position[0], bone.position[1], bone.position[2]));
local *= glm::mat4_cast(glm::quat(bone.rotation[3], bone.rotation[0], bone.rotation[1], bone.rotation[2]));
local = glm::scale(local, glm::vec3(bone.scale[0], bone.scale[1], bone.scale[2]));
boneData[bone.index].localTransform = parent_matrix * local;
boneData[bone.index].finalTransform = boneData[bone.index].localTransform * boneData[bone.index].inversePose;
for(int i = 0; i < skeleton.num_bones; i++) {
if(skeleton.bones[i].parent_bone != nullptr && strcmp(skeleton.bones[i].parent_bone->name, bone.name) == 0) {
calculateBone(skeleton, skeleton.bones[i], &bone);
}
}
}
#include "moc_mdlpart.cpp"

62
parts/mdl/mdlpart.h Normal file
View file

@ -0,0 +1,62 @@
#pragma once
#include <QWidget>
#include <optional>
#include <physis.hpp>
#include "renderer.hpp"
struct GameData;
class VulkanWindow;
class StandaloneWindow;
class MDLPart : public QWidget {
Q_OBJECT
public:
explicit MDLPart(GameData* data);
void exportModel(const QString& fileName);
Q_SIGNALS:
void modelChanged();
void skeletonChanged();
public Q_SLOTS:
/// Clears all stored MDLs.
void clear();
/// Adds a new MDL with a list of materials used.
void addModel(physis_MDL mdl, std::vector<physis_Material> materials, int lod);
/// Sets the skeleton any skinned MDLs should bind to.
void setSkeleton(physis_Skeleton skeleton);
/// Clears the current skeleton.
void clearSkeleton();
private Q_SLOTS:
void reloadRenderer();
void reloadBoneData();
private:
RenderMaterial createMaterial(const physis_Material& mat);
void calculateBoneInversePose(physis_Skeleton& skeleton, physis_Bone& bone, physis_Bone* parent_bone);
void calculateBone(physis_Skeleton& skeleton, physis_Bone& bone, const physis_Bone* parent_bone);
GameData* data = nullptr;
std::vector<RenderModel> models;
std::optional<physis_Skeleton> skeleton;
struct BoneData {
glm::mat4 localTransform, finalTransform, inversePose;
};
std::vector<BoneData> boneData;
Renderer* renderer;
VulkanWindow* vkWindow;
StandaloneWindow* standaloneWindow;
};