diff options
Diffstat (limited to 'libs/preview_nif/src')
| -rw-r--r-- | libs/preview_nif/src/CMakeLists.txt | 36 | ||||
| -rw-r--r-- | libs/preview_nif/src/Camera.cpp | 50 | ||||
| -rw-r--r-- | libs/preview_nif/src/Camera.h | 37 | ||||
| -rw-r--r-- | libs/preview_nif/src/NifExtensions.h | 236 | ||||
| -rw-r--r-- | libs/preview_nif/src/NifWidget.cpp | 254 | ||||
| -rw-r--r-- | libs/preview_nif/src/NifWidget.h | 71 | ||||
| -rw-r--r-- | libs/preview_nif/src/OpenGLShape.cpp | 403 | ||||
| -rw-r--r-- | libs/preview_nif/src/OpenGLShape.h | 105 | ||||
| -rw-r--r-- | libs/preview_nif/src/PreviewNif.cpp | 114 | ||||
| -rw-r--r-- | libs/preview_nif/src/PreviewNif.h | 39 | ||||
| -rw-r--r-- | libs/preview_nif/src/ShaderManager.cpp | 74 | ||||
| -rw-r--r-- | libs/preview_nif/src/ShaderManager.h | 48 | ||||
| -rw-r--r-- | libs/preview_nif/src/TextureManager.cpp | 448 | ||||
| -rw-r--r-- | libs/preview_nif/src/TextureManager.h | 53 | ||||
| -rw-r--r-- | libs/preview_nif/src/preview_nif_en.ts | 33 | ||||
| -rw-r--r-- | libs/preview_nif/src/previewnif.json | 1 |
16 files changed, 2002 insertions, 0 deletions
diff --git a/libs/preview_nif/src/CMakeLists.txt b/libs/preview_nif/src/CMakeLists.txt new file mode 100644 index 0000000..28bb30f --- /dev/null +++ b/libs/preview_nif/src/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.22) + +find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets OpenGL OpenGLWidgets) +find_package(fmt CONFIG REQUIRED) + +file(GLOB preview_nif_SOURCES CONFIGURE_DEPENDS + ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/*.h +) + +add_library(preview_nif SHARED ${preview_nif_SOURCES}) + +mo2_configure_plugin(preview_nif NO_SOURCES WARNINGS OFF) + +target_include_directories(preview_nif PRIVATE + ${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase/game_features + ${CMAKE_SOURCE_DIR}/libs/libbsarch/include/libbsarch +) + +target_link_libraries(preview_nif PRIVATE + mo2::uibase + libbsarch + Qt6::Widgets + Qt6::OpenGL + Qt6::OpenGLWidgets + fmt::fmt +) + +mo2_install_plugin(preview_nif) + +# Stage shader files next to the plugin under plugins/data/shaders/. +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../data/shaders + DESTINATION plugins/data + FILES_MATCHING PATTERN "*.vert" PATTERN "*.frag" +) diff --git a/libs/preview_nif/src/Camera.cpp b/libs/preview_nif/src/Camera.cpp new file mode 100644 index 0000000..0a9ce73 --- /dev/null +++ b/libs/preview_nif/src/Camera.cpp @@ -0,0 +1,50 @@ +#include "Camera.h" + +#include <cmath> + +void Camera::setDistance(float distance) +{ + m_Distance = qBound(MinDistance, distance, MaxDistance); + cameraMoved(); +} + +void Camera::setLookAt(QVector3D lookAt) +{ + m_LookAt = lookAt; + cameraMoved(); +} + +void Camera::pan(QVector3D distance) +{ + m_LookAt += distance; + cameraMoved(); +} + +void Camera::rotate(float yaw, float pitch) +{ + m_Yaw = repeat(m_Yaw + yaw, 0.0f, 360.0f); + m_Pitch = repeat(m_Pitch + pitch, 0.0f, 360.0f); + + cameraMoved(); +} + +void Camera::zoomDistance(float distance) +{ + m_Distance += distance; + m_Distance = qBound(MinDistance, m_Distance, MaxDistance); + + cameraMoved(); +} + +void Camera::zoomFactor(float factor) +{ + m_Distance *= factor; + m_Distance = qBound(MinDistance, m_Distance, MaxDistance); + + cameraMoved(); +} + +float Camera::repeat(float value, float min, float max) +{ + return fmod(fmod(value, max - min) + (max - min), max - min) + min; +} diff --git a/libs/preview_nif/src/Camera.h b/libs/preview_nif/src/Camera.h new file mode 100644 index 0000000..3d685ea --- /dev/null +++ b/libs/preview_nif/src/Camera.h @@ -0,0 +1,37 @@ +#pragma once + +#include <QObject> +#include <QVector3D> + +class Camera : public QObject +{ + Q_OBJECT + +public: + QVector3D lookAt() { return m_LookAt; } + float pitch() { return m_Pitch; } + float yaw() { return m_Yaw; } + float distance() { return m_Distance; } + + void setDistance(float distance); + void setLookAt(QVector3D lookAt); + + void pan(QVector3D delta); + void rotate(float yaw, float pitch); + void zoomDistance(float distance); + void zoomFactor(float factor); + +private: + inline static constexpr float MinDistance = 1.0f; + inline static constexpr float MaxDistance = 5000.0f; + + static float repeat(float value, float min, float max); + + QVector3D m_LookAt; + float m_Pitch = 0.0f; + float m_Yaw = 0.0f; + float m_Distance = 100.0f; + +signals: + void cameraMoved(); +}; diff --git a/libs/preview_nif/src/NifExtensions.h b/libs/preview_nif/src/NifExtensions.h new file mode 100644 index 0000000..cd529e5 --- /dev/null +++ b/libs/preview_nif/src/NifExtensions.h @@ -0,0 +1,236 @@ +#pragma once + +#include <QOpenGLFunctions> +#include <NifFile.hpp> +#include <cstdint> +#include <cstring> + +struct SLSF1 +{ + SLSF1() = delete; + + enum BSLightingShaderFlags1 : std::uint32_t + { + Specular = 1U << 0, + Skinned = 1U << 1, + TempRefraction = 1U << 2, + VertexAlpha = 1U << 3, + GreyscaleToPaletteColor = 1U << 4, + GreyscaleToPaletteAlpha = 1U << 5, + UseFalloff = 1U << 6, + EnvironmentMapping = 1U << 7, + ReceiveShadows = 1U << 8, + CastShadows = 1U << 9, + FacegenDetailMap = 1U << 10, + Parallax = 1U << 11, + ModelSpaceNormals = 1U << 12, + NonProjectiveShadows = 1U << 13, + Landscape = 1U << 14, + Refraction = 1U << 15, + FireRefraction = 1U << 16, + EyeEnvironmentMapping = 1U << 17, + HairSoftLighting = 1U << 18, + ScreendoorAlphaFade = 1U << 19, + LocalmapHideSecret = 1U << 20, + FaceGenRGBTint = 1U << 21, + OwnEmit = 1U << 22, + ProjectedUV = 1U << 23, + MultipleTextures = 1U << 24, + RemappableTextures = 1U << 25, + Decal = 1U << 26, + DynamicDecal = 1U << 27, + ParallaxOcclusion = 1U << 28, + ExternalEmittance = 1U << 29, + SoftEffect = 1U << 30, + ZBufferTest = 1U << 31, + }; +}; + +struct SLSF2 +{ + SLSF2() = delete; + + enum BSLightingShaderFlags2 : std::uint32_t + { + ZBufferWrite = 1U << 0, + LODLandscape = 1U << 1, + LODObjects = 1U << 2, + NoFade = 1U << 3, + DoubleSided = 1U << 4, + VertexColors = 1U << 5, + GlowMap = 1U << 6, + AssumeShadowmask = 1U << 7, + PackedTangent = 1U << 8, + MultiIndexSnow = 1U << 9, + VertexLighting = 1U << 10, + UniformScale = 1U << 11, + FitSlope = 1U << 12, + Billboard = 1U << 13, + NoLODLandBlend = 1U << 14, + EnvMapLightFade = 1U << 15, + Wireframe = 1U << 16, + WeaponBlood = 1U << 17, + HideOnLocalMap = 1U << 18, + PremultAlpha = 1U << 19, + CloudLOD = 1U << 20, + AnisotropicLighting = 1U << 21, + NoTransparencyMultisampling = 1U << 22, + Unused01 = 1U << 23, + MultiLayerParallax = 1U << 24, + SoftLighting = 1U << 25, + RimLighting = 1U << 26, + BackLighting = 1U << 27, + Unused02 = 1U << 28, + TreeAnim = 1U << 29, + EffectLighting = 1U << 30, + HDLODObjects = 1U << 31, + }; +}; + +struct TriShape +{ + TriShape() = delete; + + enum NiAVObjectFlags : std::uint32_t + { + Hidden = 1U << 0, + SelectiveUpdate = 1U << 1, + SelectiveUpdateTransforms = 1U << 2, + SelectiveUpdateController = 1U << 3, + SelectiveUpdateRigid = 1U << 4, + DisplayUIObject = 1U << 5, + DisableSorting = 1U << 6, + SelectiveUpdateTransformsOverride = 1U << 7, + SaveExternalGeomData = 1U << 9, + NoDecals = 1U << 10, + AlwaysDraw = 1U << 11, + MeshLODFO4 = 1U << 12, + FixedBound = 1U << 13, + TopFadeNode = 1U << 14, + IgnoreFade = 1U << 15, + NoAnimSyncX = 1U << 16, + NoAnimSyncY = 1U << 17, + NoAnimSyncZ = 1U << 18, + NoAnimSyncS = 1U << 19, + NoDismember = 1U << 20, + NoDismemberValidity = 1U << 21, + RenderUse = 1U << 22, + MaterialsApplied = 1U << 23, + HighDetail = 1U << 24, + ForceUpdate = 1U << 25, + PreProcessedNode = 1U << 26, + MeshLODSkyrim = 1U << 27, + }; +}; + +struct NiAlphaPropertyFlags +{ +public: + NiAlphaPropertyFlags(std::uint16_t flags = 0) + { + std::memcpy(this, &flags, sizeof(NiAlphaPropertyFlags)); + } + + bool isAlphaBlendEnabled() + { + return m_AlphaBlendEnable; + } + + GLenum sourceBlendingFactor() + { + return getBlendMode(m_SrcBlendMode); + } + + GLenum destinationBlendingFactor() + { + return getBlendMode(m_DstBlendMode); + } + + bool isAlphaTestEnabled() + { + return m_AlphaTestEnable; + } + + GLenum alphaTestMode() + { + return getTestMode(m_AlphaTestMode); + } + + bool isTriangleSortDisabled() + { + return m_NoSort; + } + +private: + static std::uint32_t getBlendMode(std::uint16_t flags) + { + switch (flags) { + case 0: return GL_ONE; + case 1: return GL_ZERO; + case 2: return GL_SRC_COLOR; + case 3: return GL_ONE_MINUS_SRC_COLOR; + case 4: return GL_DST_COLOR; + case 5: return GL_ONE_MINUS_DST_COLOR; + case 6: return GL_SRC_ALPHA; + case 7: return GL_ONE_MINUS_SRC_ALPHA; + case 8: return GL_DST_ALPHA; + case 9: return GL_ONE_MINUS_DST_ALPHA; + default: return GL_ONE; + }; + } + + static std::uint32_t getTestMode(std::uint16_t flags) + { + switch (flags) { + case 0: return GL_ALWAYS; + case 1: return GL_LESS; + case 2: return GL_EQUAL; + case 3: return GL_LEQUAL; + case 4: return GL_GREATER; + case 5: return GL_NOTEQUAL; + case 6: return GL_GEQUAL; + case 7: return GL_NEVER; + default: return GL_ALWAYS; + } + }; + + std::uint16_t m_AlphaBlendEnable : 1; + std::uint16_t m_SrcBlendMode : 4; + std::uint16_t m_DstBlendMode : 4; + std::uint16_t m_AlphaTestEnable : 1; + std::uint16_t m_AlphaTestMode : 3; + std::uint16_t m_NoSort : 1; +}; +static_assert(sizeof(NiAlphaPropertyFlags) == 2); + +inline nifly::MatTransform GetShapeTransformToGlobal( + nifly::NifFile* nifFile, + nifly::NiShape* niShape) +{ + nifly::MatTransform xform = niShape->GetTransformToParent(); + nifly::NiNode* parent = nifFile->GetParentNode(niShape); + while (parent) { + xform = parent->GetTransformToParent().ComposeTransforms(xform); + parent = nifFile->GetParentNode(parent); + } + + return xform; +} + +inline nifly::BoundingSphere GetBoundingSphere( + nifly::NifFile* nifFile, + nifly::NiShape* niShape) +{ + if (auto vertices = nifFile->GetVertsForShape(niShape)) { + auto bounds = nifly::BoundingSphere(*vertices); + + auto xform = GetShapeTransformToGlobal(nifFile, niShape); + + bounds.center = xform.ApplyTransform(bounds.center); + bounds.radius = xform.ApplyTransformToDist(bounds.radius); + return bounds; + } + else { + return nifly::BoundingSphere(); + } +} diff --git a/libs/preview_nif/src/NifWidget.cpp b/libs/preview_nif/src/NifWidget.cpp new file mode 100644 index 0000000..d224d04 --- /dev/null +++ b/libs/preview_nif/src/NifWidget.cpp @@ -0,0 +1,254 @@ +#include "NifWidget.h" +#include "NifExtensions.h" + +#include <QMouseEvent> +#include <QWheelEvent> +#include <QOpenGLContext> +#include <QOpenGLFunctions_2_1> +#include <QOpenGLVersionFunctionsFactory> +using OpenGLFunctions = QOpenGLFunctions_2_1; + +NifWidget::NifWidget( + std::shared_ptr<nifly::NifFile> nifFile, + MOBase::IOrganizer* moInfo, + bool debugContext, + QWidget* parent, + Qt::WindowFlags f) + : QOpenGLWidget(parent, f), + m_NifFile{ nifFile }, + m_MOInfo{ moInfo }, + m_TextureManager{ std::make_unique<TextureManager>(moInfo) }, + m_ShaderManager{ std::make_unique<ShaderManager>(moInfo) } +{ + QSurfaceFormat format; + // CompatibilityProfile — CoreProfile only exists for 3.2+, so the old + // CoreProfile+2.1 combo was getting silently downgraded, leaving Qt + // to pick a driver path we don't control. Compatibility 2.1 is the + // actual legal pair for the fixed-function bits this widget uses. + format.setVersion(2, 1); + format.setProfile(QSurfaceFormat::CompatibilityProfile); + format.setSwapBehavior(QSurfaceFormat::DoubleBuffer); + format.setSwapInterval(1); + format.setDepthBufferSize(24); + // RGBX framebuffer — no alpha channel so compositor can't see through. + format.setAlphaBufferSize(0); + + if (debugContext) { + format.setOption(QSurfaceFormat::DebugContext); + m_Logger = new QOpenGLDebugLogger(this); + } + + setFormat(format); +} + +NifWidget::~NifWidget() +{ + cleanup(); +} + +void NifWidget::mousePressEvent(QMouseEvent* event) +{ + m_MousePos = event->globalPosition().toPoint(); +} + +void NifWidget::mouseMoveEvent(QMouseEvent* event) +{ + auto pos = event->globalPosition().toPoint(); + auto delta = pos - m_MousePos; + m_MousePos = pos; + + switch (event->buttons()) { + case Qt::LeftButton: + { + m_Camera->rotate(delta.x() * 0.5, delta.y() * 0.5); + } break; + case Qt::MiddleButton: + { + float viewDX = m_Camera->distance() / m_ViewportWidth; + float viewDY = m_Camera->distance() / m_ViewportHeight; + + QMatrix4x4 r; + r.rotate(-m_Camera->yaw(), 0.0f, 1.0f, 0.0f); + r.rotate(-m_Camera->pitch(), 1.0f, 0.0f, 0.0f); + + auto pan = r * QVector4D(-delta.x() * viewDX, delta.y() * viewDY, 0.0f, 0.0f); + + m_Camera->pan(QVector3D(pan)); + } break; + case Qt::RightButton: + { + if (event->modifiers() == Qt::ShiftModifier) { + m_Camera->zoomDistance(delta.y() * 0.1f); + } + } break; + } +} + +void NifWidget::wheelEvent(QWheelEvent* event) +{ + m_Camera->zoomFactor(1.0f - (event->angleDelta().y() / 120.0f * 0.38f)); +} + +void NifWidget::initializeGL() +{ + if (m_Logger) { + m_Logger->initialize(); + connect( + m_Logger, + &QOpenGLDebugLogger::messageLogged, + this, + [](const QOpenGLDebugMessage& debugMessage){ + auto msg = tr("OpenGL debug message: %1").arg(debugMessage.message()); + qDebug(qUtf8Printable(msg)); + }); + } + + auto shapes = m_NifFile->GetShapes(); + for (auto& shape : shapes) { + if (shape->flags & TriShape::Hidden) { + continue; + } + + m_GLShapes.emplace_back(m_NifFile.get(), shape, m_TextureManager.get()); + } + + m_Camera = SharedCamera; + if (m_Camera.isNull()) { + m_Camera = { new Camera(), &Camera::deleteLater }; + SharedCamera = m_Camera; + + float largestRadius = 0.0f; + for (auto& shape : shapes) { + auto bounds = GetBoundingSphere(m_NifFile.get(), shape); + + if (bounds.radius > largestRadius) { + largestRadius = bounds.radius; + + m_Camera->setDistance(bounds.radius * 2.4f); + m_Camera->setLookAt({ -bounds.center.x, bounds.center.z, bounds.center.y }); + } + } + } + + updateCamera(); + + connect( + m_Camera.get(), + &Camera::cameraMoved, + this, + [this](){ + updateCamera(); + update(); + }); + + auto f = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_2_1>( + QOpenGLContext::currentContext()); + + f->glEnable(GL_DEPTH_TEST); + f->glDepthFunc(GL_LEQUAL); + f->glClearColor(0.18, 0.18, 0.18, 1.0); + + // Persistent polygon offset state — actual per-shape bias is set in + // paintGL() by draw order so coplanar overlays tie-break deterministically. + f->glEnable(GL_POLYGON_OFFSET_FILL); +} + +void NifWidget::paintGL() +{ + auto f = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_2_1>( + QOpenGLContext::currentContext()); + + // Force the framebuffer to be fully opaque. Sequence: + // 1. Unmask alpha + clear → writes RGB=dark grey, A=1.0 + // 2. Mask alpha off → subsequent shape draws can't touch FB alpha + // Without this, alpha-blended shapes mutate FB alpha and Qt composites + // the dialog background through the preview area (see-through bug). + f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + f->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); + + for (auto& shape : m_GLShapes) { + // Small polygon offset only on decal-flagged shapes to break z-ties + // with their coincident base mesh. Progressive/large offsets push + // depth out of the valid [0,1] range at far camera distances, + // which culls decals (the moss/road at zoom-out bug). + if (shape.isDecal) { + f->glPolygonOffset(-1.0f, -1.0f); + } else { + f->glPolygonOffset(0.0f, 0.0f); + } + + auto program = m_ShaderManager->getProgram(shape.shaderType); + if (program && program->isLinked() && program->bind()) { + auto binder = QOpenGLVertexArrayObject::Binder(shape.vertexArray); + + auto& modelMatrix = shape.modelMatrix; + auto modelViewMatrix = m_ViewMatrix * modelMatrix; + auto mvpMatrix = m_ProjectionMatrix * modelViewMatrix; + + program->setUniformValue("worldMatrix", modelMatrix); + program->setUniformValue("viewMatrix", m_ViewMatrix); + program->setUniformValue("modelViewMatrix", modelViewMatrix); + program->setUniformValue("modelViewMatrixInverse", modelViewMatrix.inverted()); + program->setUniformValue("normalMatrix", modelViewMatrix.normalMatrix()); + program->setUniformValue("mvpMatrix", mvpMatrix); + program->setUniformValue("lightDirection", QVector3D(0, 0, 1)); + + shape.setupShaders(program); + + if (shape.indexBuffer && shape.indexBuffer->isCreated()) { + shape.indexBuffer->bind(); + f->glDrawElements(GL_TRIANGLES, shape.elements, GL_UNSIGNED_SHORT, nullptr); + shape.indexBuffer->release(); + } + + program->release(); + } + } +} + +void NifWidget::resizeGL(int w, int h) +{ + QMatrix4x4 m; + m.perspective(40.0f, static_cast<float>(w) / h, 0.1f, 10000.0f); + + m_ProjectionMatrix = m; + m_ViewportWidth = w; + m_ViewportHeight = h; +} + +void NifWidget::cleanup() +{ + // Must run from ~NifWidget (not from the QOpenGLContext::aboutToBeDestroyed + // signal), because the signal fires after derived-class members have + // already been torn down — iterating m_GLShapes from a signal slot then + // walks freed memory. Keep cleanup synchronous in the dtor. + if (!context()) { + return; + } + + makeCurrent(); + + for (auto& shape : m_GLShapes) { + shape.destroy(); + } + m_GLShapes.clear(); + + m_TextureManager->cleanup(); +} + +void NifWidget::updateCamera() +{ + QMatrix4x4 m; + m.translate(0.0f, 0.0f, -m_Camera->distance()); + m.rotate(m_Camera->pitch(), 1.0f, 0.0f, 0.0f); + m.rotate(m_Camera->yaw(), 0.0f, 1.0f, 0.0f); + m.translate(-m_Camera->lookAt()); + m *= QMatrix4x4{ + -1, 0, 0, 0, + 0, 0, 1, 0, + 0, 1, 0, 0, + 0, 0, 0, 1, + }; + m_ViewMatrix = m; +} diff --git a/libs/preview_nif/src/NifWidget.h b/libs/preview_nif/src/NifWidget.h new file mode 100644 index 0000000..34f2de0 --- /dev/null +++ b/libs/preview_nif/src/NifWidget.h @@ -0,0 +1,71 @@ +#pragma once + +#include "Camera.h" +#include "OpenGLShape.h" +#include "ShaderManager.h" +#include "TextureManager.h" + +#include <QOpenGLBuffer> +#include <QOpenGLDebugLogger> +#include <QOpenGLShaderProgram> +#include <QOpenGLVertexArrayObject> +#include <QOpenGLWidget> +#include <QSharedPointer> + +#include <imoinfo.h> +#include <NifFile.hpp> + +#include <memory> + +class NifWidget : public QOpenGLWidget +{ + Q_OBJECT + +public: + NifWidget( + std::shared_ptr<nifly::NifFile> nifFile, + MOBase::IOrganizer* organizer, + bool debugContext = false, + QWidget* parent = nullptr, + Qt::WindowFlags f = {0}); + + ~NifWidget(); + NifWidget(const NifWidget&) = delete; + NifWidget(NifWidget&&) = delete; + NifWidget& operator=(const NifWidget&) = delete; + NifWidget& operator=(NifWidget&&) = delete; + +protected: + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + + void initializeGL() override; + void paintGL() override; + void resizeGL(int w, int h) override; + +private: + void cleanup(); + void updateCamera(); + + inline static QWeakPointer<Camera> SharedCamera; + + std::shared_ptr<nifly::NifFile> m_NifFile; + MOBase::IOrganizer* m_MOInfo = nullptr; + + std::unique_ptr<TextureManager> m_TextureManager; + std::unique_ptr<ShaderManager> m_ShaderManager; + + QOpenGLDebugLogger* m_Logger = nullptr; + + std::vector<OpenGLShape> m_GLShapes; + + QSharedPointer<Camera> m_Camera; + + QMatrix4x4 m_ViewMatrix; + QMatrix4x4 m_ProjectionMatrix; + + int m_ViewportWidth; + int m_ViewportHeight; + QPoint m_MousePos; +}; diff --git a/libs/preview_nif/src/OpenGLShape.cpp b/libs/preview_nif/src/OpenGLShape.cpp new file mode 100644 index 0000000..ac7e678 --- /dev/null +++ b/libs/preview_nif/src/OpenGLShape.cpp @@ -0,0 +1,403 @@ +#include "OpenGLShape.h" +#include "NifExtensions.h" + +#include <QOpenGLContext> +#include <QOpenGLFunctions_2_1> +#include <QOpenGLVersionFunctionsFactory> + +template <typename T> +inline static QOpenGLBuffer* makeVertexBuffer(const std::vector<T>* data, GLuint attrib) +{ + QOpenGLBuffer* buffer = nullptr; + + if (data) { + buffer = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + if (buffer->create() && buffer->bind()) { + buffer->allocate(data->data(), data->size() * sizeof(T)); + + auto f = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_2_1>( + QOpenGLContext::currentContext()); + + f->glEnableVertexAttribArray(attrib); + + f->glVertexAttribPointer(attrib, sizeof(T) / sizeof(float), GL_FLOAT, + GL_FALSE, sizeof(T), nullptr); + + buffer->release(); + } + } + + return buffer; +} + +OpenGLShape::OpenGLShape(nifly::NifFile* nifFile, nifly::NiShape* niShape, + TextureManager* textureManager) +{ + auto f = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_2_1>( + QOpenGLContext::currentContext()); + + auto shader = nifFile->GetShader(niShape); + auto& version = nifFile->GetHeader().GetVersion(); + if (version.IsFO4()) { + if (shader && shader->HasType<nifly::BSEffectShaderProperty>()) { + shaderType = ShaderManager::FO4EffectShader; + } + else { + shaderType = ShaderManager::FO4Default; + } + } + else { + if (shader && shader->HasType<nifly::BSEffectShaderProperty>()) { + shaderType = ShaderManager::SKEffectShader; + } + else { + if (shader && shader->IsModelSpace()) { + shaderType = ShaderManager::SKMSN; + } + else if (shader && + shader->GetShaderType() == nifly::BSLSP_MULTILAYERPARALLAX) { + shaderType = ShaderManager::SKMultilayer; + } + else { + shaderType = ShaderManager::SKDefault; + } + } + } + + vertexArray = new QOpenGLVertexArrayObject(); + vertexArray->create(); + auto binder = QOpenGLVertexArrayObject::Binder(vertexArray); + + auto xform = GetShapeTransformToGlobal(nifFile, niShape); + modelMatrix = convertTransform(xform); + + f->glVertexAttrib2f(AttribTexCoord, 0.0f, 0.0f); + f->glVertexAttrib4f(AttribColor, 1.0f, 1.0f, 1.0f, 1.0f); + + // AMD GPU fails to render without vertex data + if (!niShape->HasNormals()) { + niShape->SetNormals(true); + if (auto geomData = niShape->GetGeomData()) { + geomData->RecalcNormals(); + } + } + + if (!niShape->HasTangents()) { + niShape->SetTangents(true); + if (auto geomData = niShape->GetGeomData()) { + geomData->CalcTangentSpace(); + } + } + + if (!niShape->HasUVs()) { + niShape->SetUVs(true); + } + + if (!niShape->HasVertexColors()) { + niShape->SetVertexColors(true); + } + + if (auto verts = nifFile->GetVertsForShape(niShape)) { + vertexBuffers[AttribPosition] = makeVertexBuffer(verts, AttribPosition); + } + + if (auto normals = nifFile->GetNormalsForShape(niShape)) { + vertexBuffers[AttribNormal] = makeVertexBuffer(normals, AttribNormal); + } + + if (auto tangents = nifFile->GetTangentsForShape(niShape)) { + vertexBuffers[AttribTangent] = makeVertexBuffer(tangents, AttribTangent); + } + + if (auto bitangents = nifFile->GetBitangentsForShape(niShape)) { + vertexBuffers[AttribBitangent] = makeVertexBuffer(bitangents, AttribBitangent); + } + + if (auto uvs = nifFile->GetUvsForShape(niShape)) { + vertexBuffers[AttribTexCoord] = makeVertexBuffer(uvs, AttribTexCoord); + } + + if (std::vector<nifly::Color4> colors; + nifFile->GetColorsForShape(niShape, colors)) { + vertexBuffers[AttribColor] = makeVertexBuffer(&colors, AttribColor); + } + + indexBuffer = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer); + if (indexBuffer->create() && indexBuffer->bind()) { + + if (std::vector<nifly::Triangle> tris; niShape->GetTriangles(tris)) { + indexBuffer->allocate(tris.data(), tris.size() * sizeof(nifly::Triangle)); + } + + elements = niShape->GetNumTriangles() * 3; + indexBuffer->release(); + } + + if (shader) { + if (shader->HasTextureSet()) { + auto textureSetRef = shader->TextureSetRef(); + auto textureSet = nifFile->GetHeader().GetBlock(textureSetRef); + + for (std::size_t i = 0; i < textureSet->textures.size(); i++) { + auto texturePath = textureSet->textures[i].get(); + if (!texturePath.empty()) { + textures[i] = textureManager->getTexture(texturePath); + } + + if (textures[i] == nullptr) { + switch (i) { + case TextureSlot::BaseMap: + textures[i] = textureManager->getErrorTexture(); + break; + case TextureSlot::NormalMap: + textures[i] = textureManager->getFlatNormalTexture(); + break; + case TextureSlot::GlowMap: + if (shader->HasGlowmap()) { + textures[i] = textureManager->getBlackTexture(); + } + else { + textures[i] = textureManager->getWhiteTexture(); + } + break; + default: + textures[i] = nullptr; + break; + } + } + } + } + + specColor = convertVector3(shader->GetSpecularColor()); + specStrength = shader->GetSpecularStrength(); + specGlossiness = qBound(0.0f, shader->GetGlossiness(), 128.0f); + fresnelPower = shader->GetFresnelPower(); + paletteScale = shader->GetGrayscaleToPaletteScale(); + + hasGlowMap = shader->HasGlowmap(); + glowColor = convertColor(shader->GetEmissiveColor()); + glowMult = shader->GetEmissiveMultiple(); + + alpha = shader->GetAlpha(); + uvScale = convertVector2(shader->GetUVScale()); + uvOffset = convertVector2(shader->GetUVOffset()); + + hasEmit = shader->IsEmissive(); + hasSoftlight = shader->HasSoftlight(); + hasBacklight = shader->HasBacklight(); + hasRimlight = shader->HasRimlight(); + + softlight = shader->GetSoftlight(); + backlightPower = shader->GetBacklightPower(); + rimPower = shader->GetRimlightPower(); + doubleSided = shader->IsDoubleSided(); + envReflection = shader->GetEnvironmentMapScale(); + + if (auto alphaProperty = nifFile->GetAlphaProperty(niShape)) { + + NiAlphaPropertyFlags flags = alphaProperty->flags; + + alphaBlendEnable = flags.isAlphaBlendEnabled(); + srcBlendMode = flags.sourceBlendingFactor(); + dstBlendMode = flags.destinationBlendingFactor(); + alphaTestEnable = flags.isAlphaTestEnabled(); + alphaTestMode = flags.alphaTestMode(); + + alphaThreshold = alphaProperty->threshold / 255.0f; + } + + if (auto bslsp = dynamic_cast<nifly::BSLightingShaderProperty*>(shader)) { + zBufferTest = bslsp->shaderFlags1 & SLSF1::ZBufferTest; + zBufferWrite = bslsp->shaderFlags2 & SLSF2::ZBufferWrite; + // SLSF1_Decal = 0x04000000, SLSF1_Dynamic_Decal = 0x08000000. + // Bethesda marks decals (roads, blood splats, graffiti) with + // these flags. We use them to drive polygon offset so decals + // don't z-fight with the coincident base mesh. + isDecal = bslsp->shaderFlags1 & 0x0C000000; + + auto bslspType = bslsp->GetShaderType(); + if (bslspType == nifly::BSLSP_SKINTINT || bslspType == nifly::BSLSP_FACE) { + tintColor = convertVector3(bslsp->skinTintColor); + hasTintColor = true; + } + else if (bslspType == nifly::BSLSP_HAIRTINT) { + tintColor = convertVector3(bslsp->hairTintColor); + hasTintColor = true; + } + + if (bslspType == nifly::BSLSP_MULTILAYERPARALLAX) { + innerScale = convertVector2(bslsp->parallaxInnerLayerTextureScale); + innerThickness = bslsp->parallaxInnerLayerThickness; + outerRefraction = bslsp->parallaxRefractionScale; + outerReflection = bslsp->parallaxEnvmapStrength; + } + } + + if (auto effectShader = dynamic_cast<nifly::BSEffectShaderProperty*>(shader)) { + hasWeaponBlood = effectShader->shaderFlags2 & SLSF2::WeaponBlood; + } + } + else { + textures[BaseMap] = textureManager->getWhiteTexture(); + textures[NormalMap] = textureManager->getFlatNormalTexture(); + } +} + +void OpenGLShape::destroy() +{ + for (std::size_t i = 0; i < ATTRIB_COUNT; i++) { + if (vertexBuffers[i]) { + vertexBuffers[i]->destroy(); + delete vertexBuffers[i]; + vertexBuffers[i] = nullptr; + } + } + + if (indexBuffer) { + indexBuffer->destroy(); + delete indexBuffer; + indexBuffer = nullptr; + } + + if (vertexArray) { + vertexArray->destroy(); + vertexArray->deleteLater(); + } +} + +void OpenGLShape::setupShaders(QOpenGLShaderProgram* program) +{ + program->setUniformValue("BaseMap", BaseMap + 1); + program->setUniformValue("NormalMap", NormalMap + 1); + program->setUniformValue("GlowMap", GlowMap + 1); + program->setUniformValue("LightMask", LightMask + 1); + program->setUniformValue("hasGlowMap", hasGlowMap && textures[GlowMap] != nullptr); + program->setUniformValue("HeightMap", HeightMap + 1); + program->setUniformValue("hasHeightMap", textures[HeightMap] != nullptr); + program->setUniformValue("DetailMask", DetailMask + 1); + program->setUniformValue("hasDetailMask", textures[DetailMask] != nullptr); + program->setUniformValue("CubeMap", EnvironmentMap + 1); + program->setUniformValue("hasCubeMap", textures[EnvironmentMap] != nullptr); + program->setUniformValue("EnvironmentMap", EnvironmentMask + 1); + program->setUniformValue("hasEnvMask", textures[EnvironmentMask] != nullptr); + program->setUniformValue("TintMask", TintMask + 1); + program->setUniformValue("hasTintMask", textures[TintMask] != nullptr); + program->setUniformValue("InnerMap", InnerMap + 1); + program->setUniformValue("BacklightMap", BacklightMap + 1); + program->setUniformValue("SpecularMap", SpecularMap + 1); + program->setUniformValue("hasSpecularMap", textures[SpecularMap] != nullptr); + + for (int i = 0; i < textures.size(); i++) { + if (textures[i]) { + textures[i]->bind(i + 1); + } + } + + program->setUniformValue("ambientColor", QVector4D(0.2f, 0.2f, 0.2f, 1.0f)); + program->setUniformValue("diffuseColor", QVector4D(1.0f, 1.0f, 1.0f, 1.0f)); + + program->setUniformValue("alpha", alpha); + program->setUniformValue("tintColor", tintColor); + program->setUniformValue("uvScale", uvScale); + program->setUniformValue("uvOffset", uvOffset); + program->setUniformValue("specColor", specColor); + program->setUniformValue("specStrength", specStrength); + program->setUniformValue("specGlossiness", specGlossiness); + program->setUniformValue("fresnelPower", fresnelPower); + + program->setUniformValue("paletteScale", paletteScale); + + program->setUniformValue("hasEmit", hasEmit); + program->setUniformValue("hasSoftlight", hasSoftlight); + program->setUniformValue("hasBacklight", hasBacklight); + program->setUniformValue("hasRimlight", hasRimlight); + program->setUniformValue("hasTintColor", hasTintColor); + program->setUniformValue("hasWeaponBlood", hasWeaponBlood); + + program->setUniformValue("softlight", softlight); + program->setUniformValue("backlightPower", backlightPower); + program->setUniformValue("rimPower", rimPower); + program->setUniformValue("subsurfaceRolloff", subsurfaceRolloff); + program->setUniformValue("doubleSided", doubleSided); + + program->setUniformValue("envReflection", envReflection); + + if (shaderType == ShaderManager::SKMultilayer) { + program->setUniformValue("innerScale", innerScale); + program->setUniformValue("innerThickness", innerThickness); + program->setUniformValue("outerRefraction", outerRefraction); + program->setUniformValue("outerReflection", outerReflection); + } + + auto f = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_2_1>( + QOpenGLContext::currentContext()); + + for (std::size_t i = 0; i < ATTRIB_COUNT; i++) { + if (vertexBuffers[i]) { + f->glEnableVertexAttribArray(i); + } + else { + f->glDisableVertexAttribArray(i); + } + } + + f->glDepthMask(zBufferWrite ? GL_TRUE : GL_FALSE); + + if (zBufferTest) { + f->glEnable(GL_DEPTH_TEST); + } + else { + f->glDisable(GL_DEPTH_TEST); + } + + // Polygon offset bias is set per-shape by the draw loop in + // NifWidget::paintGL — don't touch it here. + + if (doubleSided) { + f->glDisable(GL_CULL_FACE); + } + else { + f->glEnable(GL_CULL_FACE); + f->glCullFace(GL_BACK); + } + + if (alphaBlendEnable) { + f->glEnable(GL_BLEND); + f->glBlendFunc(srcBlendMode, dstBlendMode); + } + else { + f->glDisable(GL_BLEND); + } + + if (alphaTestEnable) { + f->glEnable(GL_ALPHA_TEST); + f->glAlphaFunc(alphaTestMode, alphaThreshold); + } + else { + f->glDisable(GL_ALPHA_TEST); + } +} + +QVector2D OpenGLShape::convertVector2(nifly::Vector2 vector) +{ + return {vector.u, vector.v}; +} + +QVector3D OpenGLShape::convertVector3(nifly::Vector3 vector) +{ + return {vector.x, vector.y, vector.z}; +} + +QColor OpenGLShape::convertColor(nifly::Color4 color) +{ + return QColor::fromRgbF(color.r, color.g, color.b, color.a); +} + +QMatrix4x4 OpenGLShape::convertTransform(nifly::MatTransform transform) +{ + auto mat = transform.ToMatrix(); + return QMatrix4x4{ + mat[0], mat[1], mat[2], mat[3], mat[4], mat[5], mat[6], mat[7], + mat[8], mat[9], mat[10], mat[11], mat[12], mat[13], mat[14], mat[15], + }; +} diff --git a/libs/preview_nif/src/OpenGLShape.h b/libs/preview_nif/src/OpenGLShape.h new file mode 100644 index 0000000..b53e96d --- /dev/null +++ b/libs/preview_nif/src/OpenGLShape.h @@ -0,0 +1,105 @@ +#pragma once + +#include "ShaderManager.h" +#include "TextureManager.h" + +#include <Geometry.hpp> +#include <NifFile.hpp> + +#include <QOpenGLBuffer> +#include <QOpenGLShaderProgram> +#include <QOpenGLTexture> +#include <QOpenGLVertexArrayObject> +#include <QWidget> + +enum TextureSlot +{ + BaseMap = 0, + NormalMap = 1, + GlowMap = 2, + LightMask = 2, + HeightMap = 3, + DetailMask = 3, + EnvironmentMap = 4, + EnvironmentMask = 5, + TintMask = 6, + InnerMap = 6, + BacklightMap = 7, + SpecularMap = 7, +}; + +struct OpenGLShape +{ +public: + OpenGLShape( + nifly::NifFile* nifFile, + nifly::NiShape* niShape, + TextureManager* textureManager); + + void destroy(); + void setupShaders(QOpenGLShaderProgram* program); + + static QVector2D convertVector2(nifly::Vector2 vector); + static QVector3D convertVector3(nifly::Vector3 vector); + static QColor convertColor(nifly::Color4 color); + static QMatrix4x4 convertTransform(nifly::MatTransform transform); + + ShaderManager::ShaderType shaderType = ShaderManager::SKDefault; + + QOpenGLVertexArrayObject* vertexArray = nullptr; + + QOpenGLBuffer* vertexBuffers[ATTRIB_COUNT] { nullptr }; + + QOpenGLBuffer* indexBuffer = nullptr; + GLsizei elements = 0; + + std::array<QOpenGLTexture*, 13> textures { nullptr }; + + QMatrix4x4 modelMatrix; + QVector3D specColor{ 1.0f, 1.0f, 1.0f }; + float specStrength = 1.0f ; + float specGlossiness = 1.0f; + float fresnelPower; + + float paletteScale; + + bool hasGlowMap = false; + QColor glowColor = QColorConstants::White; + float glowMult = 1.0f; + + float alpha = 1.0f; + QVector3D tintColor{ 1.0f, 1.0f, 1.0f }; + + QVector2D uvScale{ 1.0f, 1.0f }; + QVector2D uvOffset{ 0.0f, 0.0f }; + + bool hasEmit = false; + bool hasSoftlight = false; + bool hasBacklight = false; + bool hasRimlight = false; + bool hasTintColor = false; + bool hasWeaponBlood = false; + + bool doubleSided = false; + float softlight = 0.3f; + float backlightPower = 0.0f; + float rimPower = 2.0f; + float subsurfaceRolloff; + float envReflection = 1.0f; + + QVector2D innerScale; + float innerThickness; + float outerRefraction; + float outerReflection; + + bool zBufferWrite = true; + bool zBufferTest = true; + bool isDecal = false; + + bool alphaBlendEnable = false; + GLenum srcBlendMode = GL_ONE; + GLenum dstBlendMode = GL_ONE; + bool alphaTestEnable = false; + GLenum alphaTestMode = GL_GREATER; + float alphaThreshold = 0.0f; +}; diff --git a/libs/preview_nif/src/PreviewNif.cpp b/libs/preview_nif/src/PreviewNif.cpp new file mode 100644 index 0000000..8e7e03a --- /dev/null +++ b/libs/preview_nif/src/PreviewNif.cpp @@ -0,0 +1,114 @@ +#include <NifFile.hpp> + +#include "PreviewNif.h" +#include "NifExtensions.h" +#include "NifWidget.h" + +#include <QGridLayout> +#include <filesystem> +#include <sstream> +#include <string> + +bool PreviewNif::init(MOBase::IOrganizer* moInfo) +{ + m_MOInfo = moInfo; + return true; +} + +QString PreviewNif::name() const +{ + return "Preview NIF"; +} + +QString PreviewNif::author() const +{ + return "Parapets"; +} + +QString PreviewNif::description() const +{ + return "Supports previewing NIF files"; +} + +MOBase::VersionInfo PreviewNif::version() const +{ + return MOBase::VersionInfo(0, 2, 0, 0, MOBase::VersionInfo::RELEASE_BETA); +} + +QList<MOBase::PluginSetting> PreviewNif::settings() const +{ + return QList<MOBase::PluginSetting>(); +} + +bool PreviewNif::enabledByDefault() const +{ + return true; +} + +std::set<QString> PreviewNif::supportedExtensions() const +{ + return { "bto", "btr", "nif" }; +} + +QWidget* PreviewNif::genFilePreview(const QString& fileName, const QSize&) const +{ + auto path = std::filesystem::path(fileName.toStdString()); + auto nifFile = std::make_shared<nifly::NifFile>(path); + + if (!nifFile->IsValid()) { + qWarning(qUtf8Printable(tr("Failed to load file: %1").arg(fileName))); + return nullptr; + } + + return makeWidget(nifFile); +} + +QWidget* PreviewNif::genDataPreview(const QByteArray& fileData, + const QString& fileName, const QSize&) const +{ + std::string data(fileData.constData(), fileData.size()); + std::istringstream stream(std::move(data), std::ios::binary); + + auto nifFile = std::make_shared<nifly::NifFile>(); + if (nifFile->Load(stream) != 0 || !nifFile->IsValid()) { + qWarning(qUtf8Printable(tr("Failed to load file: %1").arg(fileName))); + return nullptr; + } + + return makeWidget(nifFile); +} + +QWidget* PreviewNif::makeWidget(std::shared_ptr<nifly::NifFile> nifFile) const +{ + auto layout = new QGridLayout(); + layout->setRowStretch(0, 1); + layout->setColumnStretch(0, 1); + + layout->addWidget(makeLabel(nifFile.get()), 1, 0, 1, 1); + + auto nifWidget = new NifWidget(nifFile, m_MOInfo); + layout->addWidget(nifWidget, 0, 0, 1, 1); + + auto widget = new QWidget(); + widget->setLayout(layout); + return widget; +} + +QLabel* PreviewNif::makeLabel(nifly::NifFile* nifFile) const +{ + int shapes = 0; + int faces = 0; + int verts = 0; + + for (auto& shape : nifFile->GetShapes()) { + shapes++; + faces += shape->GetNumTriangles(); + verts += shape->GetNumVertices(); + } + + auto text = tr("Verts: %1 | Faces: %2 | Shapes: %3").arg(verts).arg(faces).arg(shapes); + auto label = new QLabel(text); + label->setWordWrap(true); + label->setTextInteractionFlags(Qt::TextSelectableByMouse); + return label; +} diff --git a/libs/preview_nif/src/PreviewNif.h b/libs/preview_nif/src/PreviewNif.h new file mode 100644 index 0000000..cd5175d --- /dev/null +++ b/libs/preview_nif/src/PreviewNif.h @@ -0,0 +1,39 @@ +#pragma once + +#include <ipluginpreview.h> +#include <QLabel> +#include <NifFile.hpp> + +class PreviewNif : public MOBase::IPluginPreview +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginPreview) + Q_PLUGIN_METADATA(IID "org.tannin.PreviewNif" FILE "previewnif.json") + +public: + PreviewNif() = default; + + // IPlugin Interface + + bool init(MOBase::IOrganizer* moInfo) override; + QString name() const override; + QString author() const override; + QString description() const override; + MOBase::VersionInfo version() const override; + QList<MOBase::PluginSetting> settings() const override; + bool enabledByDefault() const override; + + // IPluginPreview interface + + std::set<QString> supportedExtensions() const override; + bool supportsArchives() const override { return true; } + QWidget* genFilePreview(const QString& fileName, const QSize& maxSize) const override; + QWidget* genDataPreview(const QByteArray& fileData, const QString& fileName, + const QSize& maxSize) const override; + +private: + QWidget* makeWidget(std::shared_ptr<nifly::NifFile> nifFile) const; + QLabel* makeLabel(nifly::NifFile* nifFile) const; + + MOBase::IOrganizer* m_MOInfo; +}; diff --git a/libs/preview_nif/src/ShaderManager.cpp b/libs/preview_nif/src/ShaderManager.cpp new file mode 100644 index 0000000..2109b44 --- /dev/null +++ b/libs/preview_nif/src/ShaderManager.cpp @@ -0,0 +1,74 @@ +#include "ShaderManager.h" + +#include <QOpenGLContext> + +ShaderManager::ShaderManager(MOBase::IOrganizer* moInfo) : m_MOInfo{ moInfo } +{} + +QOpenGLShaderProgram* ShaderManager::getProgram(ShaderType type) +{ + if (type == None) { + return nullptr; + } + + if (m_Programs[type] == nullptr) { + m_Programs[type] = loadProgram(type); + } + + return m_Programs[type]; +} + +QOpenGLShaderProgram* ShaderManager::loadProgram(ShaderType type) +{ + QString vert; + QString frag; + + switch (type) { + case SKDefault: + vert = "default.vert"; + frag = "sk_default.frag"; + break; + case SKMSN: + vert = "sk_msn.vert"; + frag = "sk_msn.frag"; + break; + case SKMultilayer: + vert = "default.vert"; + frag = "sk_multilayer.frag"; + break; + case SKEffectShader: + vert = "sk_effectshader.vert"; + frag = "sk_effectshader.frag"; + break; + case FO4Default: + vert = "default.vert"; + frag = "fo4_default.frag"; + break; + case FO4EffectShader: + vert = "default.vert"; + frag = "fo4_effectshader.frag"; + break; + default: + return nullptr; + } + + auto game = m_MOInfo->managedGame(); + auto dataPath = MOBase::IOrganizer::getPluginDataPath(); + auto vertexShader = QString("%1/shaders/%2").arg(dataPath).arg(vert); + auto fragmentShader = QString("%1/shaders/%2").arg(dataPath).arg(frag); + + auto program = new QOpenGLShaderProgram(QOpenGLContext::currentContext()); + program->addShaderFromSourceFile(QOpenGLShader::Vertex, vertexShader); + program->addShaderFromSourceFile(QOpenGLShader::Fragment, fragmentShader); + + program->bindAttributeLocation("position", AttribPosition); + program->bindAttributeLocation("normal", AttribNormal); + program->bindAttributeLocation("tangent", AttribTangent); + program->bindAttributeLocation("bitangent", AttribBitangent); + program->bindAttributeLocation("texCoord", AttribTexCoord); + program->bindAttributeLocation("color", AttribColor); + + program->link(); + + return program; +} diff --git a/libs/preview_nif/src/ShaderManager.h b/libs/preview_nif/src/ShaderManager.h new file mode 100644 index 0000000..3c38ba1 --- /dev/null +++ b/libs/preview_nif/src/ShaderManager.h @@ -0,0 +1,48 @@ +#pragma once + +#include <imoinfo.h> +#include <QOpenGLShaderProgram> + +enum VertexAttrib +{ + AttribPosition = 0, + AttribNormal = 1, + AttribTangent = 2, + AttribBitangent = 3, + AttribTexCoord = 4, + AttribColor = 5, + + ATTRIB_COUNT, +}; + +class ShaderManager +{ +public: + enum ShaderType + { + None = -1, + SKDefault, + SKMSN, + SKMultilayer, + SKEffectShader, + FO4Default, + FO4EffectShader, + + SHADER_COUNT, + }; + + ShaderManager(MOBase::IOrganizer* moInfo); + ~ShaderManager() = default; + ShaderManager(const ShaderManager&) = delete; + ShaderManager(ShaderManager&&) = delete; + ShaderManager& operator=(const ShaderManager&) = delete; + ShaderManager& operator=(ShaderManager&&) = delete; + + QOpenGLShaderProgram* getProgram(ShaderType type); + +private: + QOpenGLShaderProgram* loadProgram(ShaderType type); + + MOBase::IOrganizer* m_MOInfo; + QOpenGLShaderProgram* m_Programs[SHADER_COUNT] { nullptr }; +}; diff --git a/libs/preview_nif/src/TextureManager.cpp b/libs/preview_nif/src/TextureManager.cpp new file mode 100644 index 0000000..2135ec6 --- /dev/null +++ b/libs/preview_nif/src/TextureManager.cpp @@ -0,0 +1,448 @@ +#include "TextureManager.h" +#include "PreviewNif.h" + +#include <dataarchives.h> +#include <igamefeatures.h> +#include <imodinterface.h> +#include <imodlist.h> +#include <imoinfo.h> +#include <ipluginlist.h> +#include <iplugingame.h> + +#include <gli/gli.hpp> +#include <libbsarch.h> + +#include <QDir> +#include <QDirIterator> +#include <QFileInfo> +#include <QSet> + +#include <QOpenGLContext> +#include <QOpenGLFunctions_2_1> +#include <QOpenGLVersionFunctionsFactory> +#include <QVector4D> + +#include <exception> +#include <memory> + +TextureManager::TextureManager(MOBase::IOrganizer* moInfo) : m_MOInfo{moInfo} {} + +void TextureManager::cleanup() +{ + for (auto it = m_Textures.cbegin(); it != m_Textures.cend();) { + auto texture = it->second; + m_Textures.erase(it++); + delete texture; + } + + if (m_ErrorTexture) { + delete m_ErrorTexture; + m_ErrorTexture = nullptr; + } + + if (m_BlackTexture) { + delete m_BlackTexture; + m_BlackTexture = nullptr; + } + + if (m_WhiteTexture) { + delete m_WhiteTexture; + m_WhiteTexture = nullptr; + } + + if (m_FlatNormalTexture) { + delete m_FlatNormalTexture; + m_FlatNormalTexture = nullptr; + } +} + +QOpenGLTexture* TextureManager::getTexture(const std::string& texturePath) +{ + return getTexture(QString::fromStdString(texturePath)); +} + +QOpenGLTexture* TextureManager::getTexture(QString texturePath) +{ + if (texturePath.isEmpty()) { + return nullptr; + } + + auto key = texturePath.toLower().toStdWString(); + + auto cached = m_Textures.find(key); + if (cached != m_Textures.end()) { + return cached->second; + } + + auto texture = loadTexture(texturePath); + + m_Textures[key] = texture; + return texture; +} + +QOpenGLTexture* TextureManager::getErrorTexture() +{ + if (!m_ErrorTexture) { + m_ErrorTexture = makeSolidColor({1.0f, 0.0f, 1.0f, 1.0f}); + } + + return m_ErrorTexture; +} + +QOpenGLTexture* TextureManager::getBlackTexture() +{ + if (!m_BlackTexture) { + m_BlackTexture = makeSolidColor({0.0f, 0.0f, 0.0f, 1.0f}); + } + + return m_BlackTexture; +} + +QOpenGLTexture* TextureManager::getWhiteTexture() +{ + if (!m_WhiteTexture) { + m_WhiteTexture = makeSolidColor({1.0f, 1.0f, 1.0f, 1.0f}); + } + + return m_WhiteTexture; +} + +QOpenGLTexture* TextureManager::getFlatNormalTexture() +{ + if (!m_FlatNormalTexture) { + m_FlatNormalTexture = makeSolidColor({0.5f, 0.5f, 1.0f, 1.0f}); + } + + return m_FlatNormalTexture; +} + +QOpenGLTexture* TextureManager::loadTexture(QString texturePath) +{ + if (texturePath.isEmpty()) { + return nullptr; + } + + auto game = m_MOInfo->managedGame(); + + if (!game) { + return nullptr; + } + + // 1) Loose file via MO2's virtual tree (honors mod priority + Overwrite). + auto realPath = resolvePath(game, texturePath); + if (!realPath.isEmpty()) { + return makeTexture(gli::load(realPath.toStdString())); + } + + // 2) BSA fallback. Walk cached priority-sorted candidate list. + auto tryExtract = [&](const QString& bsaPath) -> QOpenGLTexture* { + using bsa_ptr = std::unique_ptr<void, decltype(&bsa_free)>; + auto bsa = bsa_ptr(bsa_create(), bsa_free); + + const std::wstring bsaPathW = bsaPath.toStdWString(); + const std::wstring texturePathW = texturePath.toStdWString(); + + bsa_result_message_t result = + bsa_load_from_file(bsa.get(), bsaPathW.c_str()); + if (result.code == BSA_RESULT_EXCEPTION) { + return nullptr; + } + + auto result_buffer = + bsa_extract_file_data_by_filename(bsa.get(), texturePathW.c_str()); + if (result_buffer.message.code == BSA_RESULT_EXCEPTION || + !result_buffer.buffer.data) { + return nullptr; + } + + auto buffer_free = [&bsa](bsa_result_buffer_t* buffer) { + bsa_file_data_free(bsa.get(), *buffer); + }; + using buffer_ptr = + std::unique_ptr<bsa_result_buffer_t, decltype(buffer_free)>; + auto buffer = buffer_ptr(&result_buffer.buffer, buffer_free); + + return makeTexture( + gli::load(static_cast<char*>(buffer->data), buffer->size)); + }; + + for (const QString& bsaPath : bsaCandidates()) { + if (auto* tex = tryExtract(bsaPath)) { + return tex; + } + } + + return nullptr; +} + +// Process-wide BSA candidate cache. Keyed by profile directory path so +// switching instances invalidates automatically. Built once, reused across +// every NIF preview until the profile changes. +namespace { + QString g_cachedProfileKey; + QStringList g_cachedBsaCandidates; +} + +const QStringList& TextureManager::bsaCandidates() +{ + QString profileKey; + if (auto profile = m_MOInfo->profile()) { + profileKey = profile->absolutePath(); + } + + if (profileKey == g_cachedProfileKey && !g_cachedBsaCandidates.isEmpty()) { + return g_cachedBsaCandidates; + } + + g_cachedBsaCandidates.clear(); + rebuildBsaCandidates(); + g_cachedProfileKey = profileKey; + return g_cachedBsaCandidates; +} + +void TextureManager::rebuildBsaCandidates() +{ + QSet<QString> seen; + + auto addCandidate = [&](const QString& p) { + QString norm = QDir::cleanPath(p); + if (!norm.isEmpty() && !seen.contains(norm)) { + seen.insert(norm); + g_cachedBsaCandidates.append(norm); + } + }; + + try { + auto game = m_MOInfo->managedGame(); + + if (auto* modList = m_MOInfo->modList()) { + auto profile = m_MOInfo->profile(); + QStringList modNames = + modList->allModsByProfilePriority(profile.get()); + + for (auto it = modNames.rbegin(); it != modNames.rend(); ++it) { + if (!(modList->state(*it) & MOBase::IModList::STATE_ACTIVE)) + continue; + + MOBase::IModInterface* mod = modList->getMod(*it); + if (!mod) continue; + + QDir modDir(mod->absolutePath()); + if (!modDir.exists()) continue; + + const QStringList bsas = + modDir.entryList(QStringList{"*.bsa", "*.ba2"}, + QDir::Files | QDir::NoDotAndDotDot); + for (const QString& name : bsas) { + addCandidate(modDir.absoluteFilePath(name)); + } + } + } + + // Vanilla/game archives from DataArchives ini list. + if (auto* features = m_MOInfo->gameFeatures()) { + if (auto gameArchives = + features->gameFeature<MOBase::DataArchives>()) { + auto profile = m_MOInfo->profile(); + auto archives = gameArchives->archives(profile.get()); + for (auto it = archives.rbegin(); it != archives.rend(); ++it) { + QString resolved = resolvePath(game, *it); + if (!resolved.isEmpty()) addCandidate(resolved); + } + } + } + } catch (...) { + } +} + + +QOpenGLTexture* TextureManager::makeTexture(const gli::texture& texture) +{ + if (texture.empty()) { + return nullptr; + } + + gli::gl GL(gli::gl::PROFILE_GL32); + const gli::gl::format format = GL.translate(texture.format(), texture.swizzles()); + GLenum target = GL.translate(texture.target()); + + auto f = QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_2_1>( + QOpenGLContext::currentContext()); + QOpenGLTexture* glTexture = + new QOpenGLTexture(static_cast<QOpenGLTexture::Target>(target)); + + glTexture->create(); + glTexture->bind(); + glTexture->setMipLevels(texture.levels()); + glTexture->setMipBaseLevel(0); + glTexture->setMipMaxLevel(texture.levels() - 1); + glTexture->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, + QOpenGLTexture::Linear); + glTexture->setSwizzleMask( + static_cast<QOpenGLTexture::SwizzleValue>(format.Swizzles[0]), + static_cast<QOpenGLTexture::SwizzleValue>(format.Swizzles[1]), + static_cast<QOpenGLTexture::SwizzleValue>(format.Swizzles[2]), + static_cast<QOpenGLTexture::SwizzleValue>(format.Swizzles[3])); + + glTexture->setWrapMode(QOpenGLTexture::Repeat); + + auto extent = texture.extent(); + const GLsizei faceTotal = texture.layers() * texture.faces(); + + glTexture->setSize(extent.x, extent.y, extent.z); + glTexture->setFormat(static_cast<QOpenGLTexture::TextureFormat>(format.Internal)); + glTexture->allocateStorage( + static_cast<QOpenGLTexture::PixelFormat>(format.External), + static_cast<QOpenGLTexture::PixelType>(format.Type)); + + for (std::size_t layer = 0; layer < texture.layers(); layer++) + for (std::size_t face = 0; face < texture.faces(); face++) + for (std::size_t level = 0; level < texture.levels(); level++) { + auto extent = texture.extent(level); + + target = + gli::is_target_cube(texture.target()) + ? static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face) + : target; + + // Qt's upload functions lag badly so we just use the GL API + switch (texture.target()) { + case gli::TARGET_1D: + if (gli::is_compressed(texture.format())) { + f->glCompressedTexSubImage1D( + target, level, 0, extent.x, format.Internal, + texture.size(level), texture.data(layer, face, level)); + } + else { + f->glTexSubImage1D(target, level, 0, extent.x, format.External, + format.Type, + texture.data(layer, face, level)); + } + break; + case gli::TARGET_1D_ARRAY: + case gli::TARGET_2D: + case gli::TARGET_CUBE: + if (gli::is_compressed(texture.format())) { + f->glCompressedTexSubImage2D( + target, level, 0, 0, extent.x, + texture.target() == gli::TARGET_1D_ARRAY ? layer : extent.y, + format.Internal, texture.size(level), + texture.data(layer, face, level)); + } + else { + f->glTexSubImage2D( + target, level, 0, 0, extent.x, + texture.target() == gli::TARGET_1D_ARRAY ? layer : extent.y, + format.External, format.Type, + texture.data(layer, face, level)); + } + break; + case gli::TARGET_2D_ARRAY: + case gli::TARGET_3D: + case gli::TARGET_CUBE_ARRAY: + if (gli::is_compressed(texture.format())) { + f->glCompressedTexSubImage3D( + target, level, 0, 0, 0, extent.x, extent.y, + texture.target() == gli::TARGET_3D ? extent.z : layer, + format.Internal, texture.size(level), + texture.data(layer, face, level)); + } + else { + f->glTexSubImage3D(target, level, 0, 0, 0, extent.x, extent.y, + texture.target() == gli::TARGET_3D ? extent.z + : layer, + format.External, format.Type, + texture.data(layer, face, level)); + } + break; + } + } + + glTexture->release(); + + return glTexture; +} + +QOpenGLTexture* TextureManager::makeSolidColor(QVector4D color) +{ + QOpenGLTexture* glTexture = new QOpenGLTexture(QOpenGLTexture::Target2D); + glTexture->create(); + glTexture->bind(); + + glTexture->setSize(1, 1); + glTexture->setFormat(QOpenGLTexture::RGBA32F); + glTexture->allocateStorage(QOpenGLTexture::RGBA, QOpenGLTexture::Float32); + + glTexture->setData(QOpenGLTexture::RGBA, QOpenGLTexture::Float32, &color); + + glTexture->release(); + + return glTexture; +} + +// Case-insensitive walk of a path relative to a root dir. Used because the +// Linux FS is case-sensitive but Bethesda NIFs reference textures with +// arbitrary case. We fix the case component-by-component against the real +// on-disk filenames. +static QString resolveCaseInsensitive(const QString& rootAbs, const QString& relPath) +{ + QStringList parts = QDir::cleanPath(relPath).split('/', Qt::SkipEmptyParts); + QString cursor = rootAbs; + for (const QString& part : parts) { + QDir cur(cursor); + if (!cur.exists()) return ""; + const QStringList entries = + cur.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + QString match; + for (const QString& entry : entries) { + if (entry.compare(part, Qt::CaseInsensitive) == 0) { + match = entry; + break; + } + } + if (match.isEmpty()) return ""; + cursor = cur.absoluteFilePath(match); + } + return QFileInfo::exists(cursor) ? cursor : QString(); +} + +QString TextureManager::resolvePath(const MOBase::IPluginGame* game, QString path) +{ + // NIF texture paths use Windows backslashes. On Linux, IOrganizer's + // virtual tree keys on forward-slash paths, so normalize here before + // lookup. BSA lookups keep the raw backslash form separately. + QString normalized = path; + normalized.replace('\\', '/'); + + // MO2's virtual tree may return a path that matches case-insensitively + // against its internal index but doesn't match the real on-disk case. + // Verify existence — if the returned path doesn't actually exist, do a + // case-insensitive walk against the containing mod directory to recover + // the correct case. + auto virtPath = m_MOInfo->resolvePath(normalized); + if (!virtPath.isEmpty()) { + if (QFileInfo::exists(virtPath)) { + return virtPath; + } + // Path wasn't valid — walk up to find the mod root, then re-resolve + // case-insensitively under it. We detect the mod root by looking for + // the "textures/" segment (NIF texture paths are always rooted at + // textures/). + const int texIdx = virtPath.indexOf("/textures/", 0, Qt::CaseInsensitive); + if (texIdx > 0) { + const QString modRoot = virtPath.left(texIdx); + const QString rel = virtPath.mid(texIdx + 1); + const QString fixed = resolveCaseInsensitive(modRoot, rel); + if (!fixed.isEmpty()) return fixed; + } + } + + auto dataDir = game->dataDirectory(); + auto dataPath = dataDir.absoluteFilePath(QDir::cleanPath(normalized)); + if (QFileInfo::exists(dataPath)) { + return dataPath; + } + + // Final fallback: case-insensitive walk under the game data directory. + return resolveCaseInsensitive(dataDir.absolutePath(), normalized); +} diff --git a/libs/preview_nif/src/TextureManager.h b/libs/preview_nif/src/TextureManager.h new file mode 100644 index 0000000..7ba1c9a --- /dev/null +++ b/libs/preview_nif/src/TextureManager.h @@ -0,0 +1,53 @@ +#pragma once + +#include <imoinfo.h> +#include <gli/gli.hpp> +#include <QOpenGLTexture> +#include <QSet> +#include <QStringList> +#include <map> + +class TextureManager +{ +public: + TextureManager(MOBase::IOrganizer* organizer); + ~TextureManager() = default; + TextureManager(const TextureManager&) = delete; + TextureManager(TextureManager&&) = delete; + TextureManager& operator=(const TextureManager&) = delete; + TextureManager& operator=(TextureManager&&) = delete; + + void cleanup(); + + QOpenGLTexture* getTexture(const std::string& texturePath); + QOpenGLTexture* getTexture(QString texturePath); + + QOpenGLTexture* getErrorTexture(); + QOpenGLTexture* getBlackTexture(); + QOpenGLTexture* getWhiteTexture(); + QOpenGLTexture* getFlatNormalTexture(); + +private: + QOpenGLTexture* loadTexture(QString texturePath); + QOpenGLTexture* makeTexture(const gli::texture& texture); + QOpenGLTexture* makeSolidColor(QVector4D color); + + QString resolvePath(const MOBase::IPluginGame* game, QString path); + + // Build priority-sorted BSA candidate list. Shared process-wide, keyed by + // the current instance's profile path so switching instances rebuilds. + // Conflict semantics: only active mods contribute, highest profile + // priority first, then vanilla archives. No plugin-attachment filter — + // matches BodySlide's "load all BSAs" approach which is what MO2's + // virtual data tree effectively does. + const QStringList& bsaCandidates(); + void rebuildBsaCandidates(); + + MOBase::IOrganizer* m_MOInfo; + QOpenGLTexture* m_ErrorTexture = nullptr; + QOpenGLTexture* m_BlackTexture = nullptr; + QOpenGLTexture* m_WhiteTexture = nullptr; + QOpenGLTexture* m_FlatNormalTexture = nullptr; + + std::map<std::wstring, QOpenGLTexture*> m_Textures; +}; diff --git a/libs/preview_nif/src/preview_nif_en.ts b/libs/preview_nif/src/preview_nif_en.ts new file mode 100644 index 0000000..10b8b07 --- /dev/null +++ b/libs/preview_nif/src/preview_nif_en.ts @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="en_US"> +<context> + <name>NifWidget</name> + <message> + <location filename="NifWidget.cpp" line="92"/> + <source>OpenGL debug message: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PreviewNif</name> + <message> + <location filename="PreviewNif.cpp" line="57"/> + <source>Failed to load file: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="PreviewNif.cpp" line="87"/> + <source>Verts: %1 | Faces: %2 | Shapes: %3</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>QObject</name> + <message> + <location filename="TextureManager.cpp" line="118"/> + <source>Failed to interface with managed game plugin</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/libs/preview_nif/src/previewnif.json b/libs/preview_nif/src/previewnif.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/preview_nif/src/previewnif.json @@ -0,0 +1 @@ +{} |
