diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a2866..8674ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ ## [0.8.0] - UNRELEASED - Added pgtools.exe modding tools +- Added pgtools patcher to convert particle lights to light placer lights +- Parallax maps included in the pbr subdirectory will be considered a different "height pbr" texture type +- Closing mod sort dialog will now close the whole app +- BSMeshLODTriShapes will also be patched now +- Existing TXST records will no longer be patched, only new ones will be created +- Added support for PBR fuzz +- Added support for PBR hair - Removed simplicity of snow warning as the mod is not inherently incompatible ## [0.7.3] - 2024-12-09 diff --git a/CMakeLists.txt b/CMakeLists.txt index b7e19b9..879c9e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,12 +22,12 @@ add_compile_definitions(_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR) set(PROJECT_NAME "ParallaxGen") # Initialize Project -set(PARALLAXGEN_VERSION 0.7.3) +set(PARALLAXGEN_VERSION 0.8.0) project(${PROJECT_NAME} VERSION ${PARALLAXGEN_VERSION}) add_compile_definitions(PARALLAXGEN_VERSION="${PARALLAXGEN_VERSION}") # Set test version (set this to 0 for prod releases) -add_compile_definitions(PARALLAXGEN_TEST_VERSION=0) +add_compile_definitions(PARALLAXGEN_TEST_VERSION=1) # Setup Folders set(EXTRN_BUILD_DIR ${CMAKE_BINARY_DIR}/external/blds) diff --git a/PGTools/src/main.cpp b/PGTools/src/main.cpp index 9055ef1..61c0f09 100644 --- a/PGTools/src/main.cpp +++ b/PGTools/src/main.cpp @@ -17,6 +17,7 @@ #include "patchers/PatcherTruePBR.hpp" #include "patchers/PatcherUpgradeParallaxToCM.hpp" #include "patchers/PatcherVanillaParallax.hpp" +#include "patchers/PatcherParticleLightsToLP.hpp" using namespace std; @@ -136,9 +137,17 @@ void mainRunner(PGToolsCLIArgs &Args) { Patchers.ShaderTransformPatchers[PatcherUpgradeParallaxToCM::getFromShader()].emplace( PatcherUpgradeParallaxToCM::getToShader(), PatcherUpgradeParallaxToCM::getFactory()); } + if (Args.Patch.Patchers.contains("particlelightstolp")) { + Patchers.GlobalPatchers.emplace_back(PatcherParticleLightsToLP::getFactory()); + } PG.patchMeshes(Patchers, nullptr, Args.Multithreading, false); + // Finalize step + if (Args.Patch.Patchers.contains("particlelightstolp")) { + PatcherParticleLightsToLP::finalize(); + } + // Release cached files, if any PGD.clearCache(); diff --git a/ParallaxGen/include/GUI/ModSortDialog.hpp b/ParallaxGen/include/GUI/ModSortDialog.hpp index 702eac9..5dc2f6f 100644 --- a/ParallaxGen/include/GUI/ModSortDialog.hpp +++ b/ParallaxGen/include/GUI/ModSortDialog.hpp @@ -41,6 +41,8 @@ class ModSortDialog : public wxDialog { bool SortAscending; /** Stores whether the list is in asc or desc order */ public: + static inline bool UIExitTriggered = false; + /** * @brief Construct a new Mod Sort Dialog object * @@ -103,6 +105,13 @@ class ModSortDialog : public wxDialog { */ void onTimer(wxTimerEvent &Event); + /** + * @brief Resets indices for the list after drag or sort + * + * @param Event wxWidgets event object + */ + void onClose(wxCloseEvent &Event); + /** * @brief Get the Header Height for positioning * diff --git a/ParallaxGen/src/GUI/ModSortDialog.cpp b/ParallaxGen/src/GUI/ModSortDialog.cpp index 2a5ef6c..11d4129 100644 --- a/ParallaxGen/src/GUI/ModSortDialog.cpp +++ b/ParallaxGen/src/GUI/ModSortDialog.cpp @@ -75,6 +75,7 @@ ModSortDialog::ModSortDialog(const std::vector &Mods, const std::v // Add OK button auto *OkButton = new wxButton(this, wxID_OK, "OK"); MainSizer->Add(OkButton, 0, wxALIGN_CENTER_HORIZONTAL | wxALL, 10); + Bind(wxEVT_CLOSE_WINDOW, &ModSortDialog::onClose, this); SetSizer(MainSizer); @@ -373,3 +374,9 @@ auto ModSortDialog::getSortedItems() const -> std::vector { return SortedItems; } + +void ModSortDialog::onClose( // NOLINT(readability-convert-member-functions-to-static) + [[maybe_unused]] wxCloseEvent &Event) { + UIExitTriggered = true; + wxTheApp->Exit(); +} diff --git a/ParallaxGen/src/main.cpp b/ParallaxGen/src/main.cpp index 68532ea..c0d4072 100644 --- a/ParallaxGen/src/main.cpp +++ b/ParallaxGen/src/main.cpp @@ -25,6 +25,7 @@ #include "BethesdaGame.hpp" #include "GUI/LauncherWindow.hpp" +#include "GUI/ModSortDialog.hpp" #include "ModManagerDirectory.hpp" #include "NIFUtil.hpp" #include "ParallaxGen.hpp" @@ -326,7 +327,7 @@ void mainRunner(ParallaxGenCLIArgs &Args, const filesystem::path &ExePath) { } void exitBlocking() { - if (LauncherWindow::UIExitTriggered) { + if (LauncherWindow::UIExitTriggered || ModSortDialog::UIExitTriggered) { return; } diff --git a/ParallaxGenLib/CMakeLists.txt b/ParallaxGenLib/CMakeLists.txt index cc6f0a6..8e40fc4 100644 --- a/ParallaxGenLib/CMakeLists.txt +++ b/ParallaxGenLib/CMakeLists.txt @@ -20,6 +20,8 @@ set(HEADERS "include/patchers/PatcherUpgradeParallaxToCM.hpp" "include/patchers/Patcher.hpp" "include/patchers/PatcherUtil.hpp" + "include/patchers/PatcherGlobal.hpp" + "include/patchers/PatcherParticleLightsToLP.hpp" ) set(SOURCES @@ -43,6 +45,8 @@ set(SOURCES "src/patchers/PatcherUpgradeParallaxToCM.cpp" "src/patchers/Patcher.cpp" "src/patchers/PatcherUtil.cpp" + "src/patchers/PatcherGlobal.cpp" + "src/patchers/PatcherParticleLightsToLP.cpp" ) include_directories("include") diff --git a/ParallaxGenLib/include/NIFUtil.hpp b/ParallaxGenLib/include/NIFUtil.hpp index f82db4e..0c516f9 100644 --- a/ParallaxGenLib/include/NIFUtil.hpp +++ b/ParallaxGenLib/include/NIFUtil.hpp @@ -41,12 +41,14 @@ enum class TextureType { SKINTINT, SUBSURFACECOLOR, HEIGHT, + HEIGHTPBR, CUBEMAP, ENVIRONMENTMASK, COMPLEXMATERIAL, RMAOS, SUBSURFACETINT, INNERLAYER, + FUZZPBR, COATNORMALROUGHNESS, BACKLIGHT, SPECULAR, diff --git a/ParallaxGenLib/include/patchers/PatcherGlobal.hpp b/ParallaxGenLib/include/patchers/PatcherGlobal.hpp new file mode 100644 index 0000000..d2952d1 --- /dev/null +++ b/ParallaxGenLib/include/patchers/PatcherGlobal.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include "patchers/Patcher.hpp" + +/** + * @class PrePatcher + * @brief Base class for prepatchers + */ +class PatcherGlobal : public Patcher { +public: + // type definitions + using PatcherGlobalFactory = std::function(std::filesystem::path, nifly::NifFile *)>; + using PatcherGlobalObject = std::unique_ptr; + + // Constructors + PatcherGlobal(std::filesystem::path NIFPath, nifly::NifFile *NIF, std::string PatcherName); + virtual ~PatcherGlobal() = default; + PatcherGlobal(const PatcherGlobal &Other) = default; + auto operator=(const PatcherGlobal &Other) -> PatcherGlobal & = default; + PatcherGlobal(PatcherGlobal &&Other) noexcept = default; + auto operator=(PatcherGlobal &&Other) noexcept -> PatcherGlobal & = default; + + /** + * @brief Apply the patch to the NIFShape if able + * + * @param NIFShape Shape to apply patch to + * @param NIFModified Whether the NIF was modified + * @param ShapeDeleted Whether the shape was deleted + * @return true Patch was applied + * @return false Patch was not applied + */ + virtual auto applyPatch(bool &NIFModified) -> bool = 0; +}; diff --git a/ParallaxGenLib/include/patchers/PatcherParticleLightsToLP.hpp b/ParallaxGenLib/include/patchers/PatcherParticleLightsToLP.hpp new file mode 100644 index 0000000..b830744 --- /dev/null +++ b/ParallaxGenLib/include/patchers/PatcherParticleLightsToLP.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "patchers/PatcherGlobal.hpp" +#include + +/** + * @class PrePatcherParticleLightsToLP + * @brief patcher to transform particle lights to LP + */ +class PatcherParticleLightsToLP : public PatcherGlobal { +private: + static nlohmann::json LPJsonData; /** < LP JSON data */ + static std::mutex LPJsonDataMutex; /** < Mutex for LP JSON data */ + +public: + /** + * @brief Get the Factory object + * + * @return PatcherShaderTransform::PatcherShaderTransformFactory + */ + static auto getFactory() -> PatcherGlobal::PatcherGlobalFactory; + + /** + * @brief Construct a new PrePatcher Particle Lights To LP patcher + * + * @param NIFPath NIF path to be patched + * @param NIF NIF object to be patched + */ + PatcherParticleLightsToLP(std::filesystem::path NIFPath, nifly::NifFile *NIF); + + /** + * @brief Apply this patcher to shape + * + * @param NIFShape Shape to patch + * @param NIFModified Whether NIF was modified + * @param ShapeDeleted Whether shape was deleted + * @return true Shape was patched + * @return false Shape was not patched + */ + auto applyPatch(bool &NIFModified) -> bool override; + + /** + * @brief Save output JSON + */ + static void finalize(); + +private: + /** + * @brief Apply patch to single particle light in mesh + * + * @param NiAlphaProperty Alpha property to patch + * @return true patch was applied + * @return false patch was not applied + */ + auto applySinglePatch(nifly::NiBillboardNode *Node, nifly::NiShape *Shape, nifly::BSEffectShaderProperty *EffectShader) -> bool; + + /** + * @brief Get LP JSON for a specific NIF controller + * + * @param Controller Controller to get JSON for + * @param JSONField JSON field to store controller JSON in LP + * @return nlohmann::json JSON for controller + */ + auto getControllerJSON(nifly::NiTimeController *Controller, std::string &JSONField) -> nlohmann::json; +}; diff --git a/ParallaxGenLib/include/patchers/PatcherUtil.hpp b/ParallaxGenLib/include/patchers/PatcherUtil.hpp index 29c779e..183d9ad 100644 --- a/ParallaxGenLib/include/patchers/PatcherUtil.hpp +++ b/ParallaxGenLib/include/patchers/PatcherUtil.hpp @@ -2,6 +2,7 @@ #include +#include "patchers/PatcherGlobal.hpp" #include "patchers/PatcherShader.hpp" #include "patchers/PatcherShaderTransform.hpp" @@ -18,6 +19,7 @@ class PatcherUtil { * @brief Stores the patcher objects for a given run */ struct PatcherObjectSet { + std::vector GlobalPatchers; std::unordered_map ShaderPatchers; std::unordered_map> @@ -29,6 +31,7 @@ class PatcherUtil { * @brief Stores the patcher factories for a given run */ struct PatcherSet { + std::vector GlobalPatchers; std::unordered_map ShaderPatchers; std::unordered_map> diff --git a/ParallaxGenLib/src/NIFUtil.cpp b/ParallaxGenLib/src/NIFUtil.cpp index b8e4800..879e831 100644 --- a/ParallaxGenLib/src/NIFUtil.cpp +++ b/ParallaxGenLib/src/NIFUtil.cpp @@ -20,7 +20,6 @@ #include #include - #include #include @@ -70,6 +69,7 @@ auto NIFUtil::getTexSuffixMap() -> map map string { - static unordered_map StrFromTexMap = {{TextureType::DIFFUSE, "diffuse"}, - {TextureType::NORMAL, "normal"}, - {TextureType::MODELSPACENORMAL, "model space normal"}, - {TextureType::EMISSIVE, "emissive"}, - {TextureType::SKINTINT, "skin tint"}, - {TextureType::SUBSURFACECOLOR, "subsurface color"}, - {TextureType::HEIGHT, "height"}, - {TextureType::CUBEMAP, "cubemap"}, - {TextureType::ENVIRONMENTMASK, "environment mask"}, - {TextureType::COMPLEXMATERIAL, "complex material"}, - {TextureType::RMAOS, "rmaos"}, - {TextureType::SUBSURFACETINT, "subsurface tint"}, - {TextureType::INNERLAYER, "inner layer"}, - {TextureType::COATNORMALROUGHNESS, "coat normal roughness"}, - {TextureType::BACKLIGHT, "backlight"}, - {TextureType::SPECULAR, "specular"}, - {TextureType::SUBSURFACEPBR, "subsurface pbr"}, - {TextureType::UNKNOWN, "unknown"}}; + static unordered_map StrFromTexMap = { + {TextureType::DIFFUSE, "diffuse"}, + {TextureType::NORMAL, "normal"}, + {TextureType::MODELSPACENORMAL, "model space normal"}, + {TextureType::EMISSIVE, "emissive"}, + {TextureType::SKINTINT, "skin tint"}, + {TextureType::SUBSURFACECOLOR, "subsurface color"}, + {TextureType::HEIGHT, "height"}, + {TextureType::HEIGHTPBR, "height pbr"}, + {TextureType::CUBEMAP, "cubemap"}, + {TextureType::ENVIRONMENTMASK, "environment mask"}, + {TextureType::COMPLEXMATERIAL, "complex material"}, + {TextureType::RMAOS, "rmaos"}, + {TextureType::SUBSURFACETINT, "subsurface tint"}, + {TextureType::INNERLAYER, "inner layer"}, + {TextureType::FUZZPBR, "fuzz pbr"}, + {TextureType::COATNORMALROUGHNESS, "coat normal roughness"}, + {TextureType::BACKLIGHT, "backlight"}, + {TextureType::SPECULAR, "specular"}, + {TextureType::SUBSURFACEPBR, "subsurface pbr"}, + {TextureType::UNKNOWN, "unknown"}}; if (StrFromTexMap.find(Type) != StrFromTexMap.end()) { return StrFromTexMap[Type]; @@ -114,24 +117,27 @@ auto NIFUtil::getStrFromTexType(const TextureType &Type) -> string { } auto NIFUtil::getTexTypeFromStr(const string &Type) -> TextureType { - static unordered_map TexFromStrMap = {{"diffuse", TextureType::DIFFUSE}, - {"normal", TextureType::NORMAL}, - {"model space normal", TextureType::MODELSPACENORMAL}, - {"emissive", TextureType::EMISSIVE}, - {"skin tint", TextureType::SKINTINT}, - {"subsurface color", TextureType::SUBSURFACECOLOR}, - {"height", TextureType::HEIGHT}, - {"cubemap", TextureType::CUBEMAP}, - {"environment mask", TextureType::ENVIRONMENTMASK}, - {"complex material", TextureType::COMPLEXMATERIAL}, - {"rmaos", TextureType::RMAOS}, - {"subsurface tint", TextureType::SUBSURFACETINT}, - {"inner layer", TextureType::INNERLAYER}, - {"coat normal roughness", TextureType::COATNORMALROUGHNESS}, - {"backlight", TextureType::BACKLIGHT}, - {"specular", TextureType::SPECULAR}, - {"subsurface pbr", TextureType::SUBSURFACEPBR}, - {"unknown", TextureType::UNKNOWN}}; + static unordered_map TexFromStrMap = { + {"diffuse", TextureType::DIFFUSE}, + {"normal", TextureType::NORMAL}, + {"model space normal", TextureType::MODELSPACENORMAL}, + {"emissive", TextureType::EMISSIVE}, + {"skin tint", TextureType::SKINTINT}, + {"subsurface color", TextureType::SUBSURFACECOLOR}, + {"height", TextureType::HEIGHT}, + {"height pbr", TextureType::HEIGHTPBR}, + {"cubemap", TextureType::CUBEMAP}, + {"environment mask", TextureType::ENVIRONMENTMASK}, + {"complex material", TextureType::COMPLEXMATERIAL}, + {"rmaos", TextureType::RMAOS}, + {"subsurface tint", TextureType::SUBSURFACETINT}, + {"inner layer", TextureType::INNERLAYER}, + {"fuzz pbr", TextureType::FUZZPBR}, + {"coat normal roughness", TextureType::COATNORMALROUGHNESS}, + {"backlight", TextureType::BACKLIGHT}, + {"specular", TextureType::SPECULAR}, + {"subsurface pbr", TextureType::SUBSURFACEPBR}, + {"unknown", TextureType::UNKNOWN}}; const auto SearchKey = boost::to_lower_copy(Type); if (TexFromStrMap.find(Type) != TexFromStrMap.end()) { @@ -143,15 +149,26 @@ auto NIFUtil::getTexTypeFromStr(const string &Type) -> TextureType { auto NIFUtil::getSlotFromTexType(const TextureType &Type) -> TextureSlots { static unordered_map TexTypeToSlotMap = { - {TextureType::DIFFUSE, TextureSlots::DIFFUSE}, {TextureType::NORMAL, TextureSlots::NORMAL}, - {TextureType::MODELSPACENORMAL, TextureSlots::NORMAL}, {TextureType::EMISSIVE, TextureSlots::GLOW}, - {TextureType::SKINTINT, TextureSlots::GLOW}, {TextureType::SUBSURFACECOLOR, TextureSlots::GLOW}, - {TextureType::HEIGHT, TextureSlots::PARALLAX}, {TextureType::CUBEMAP, TextureSlots::CUBEMAP}, - {TextureType::ENVIRONMENTMASK, TextureSlots::ENVMASK}, {TextureType::COMPLEXMATERIAL, TextureSlots::ENVMASK}, - {TextureType::RMAOS, TextureSlots::ENVMASK}, {TextureType::SUBSURFACETINT, TextureSlots::MULTILAYER}, - {TextureType::INNERLAYER, TextureSlots::MULTILAYER}, {TextureType::COATNORMALROUGHNESS, TextureSlots::MULTILAYER}, - {TextureType::BACKLIGHT, TextureSlots::BACKLIGHT}, {TextureType::SPECULAR, TextureSlots::BACKLIGHT}, - {TextureType::SUBSURFACEPBR, TextureSlots::BACKLIGHT}, {TextureType::UNKNOWN, TextureSlots::UNKNOWN}}; + {TextureType::DIFFUSE, TextureSlots::DIFFUSE}, + {TextureType::NORMAL, TextureSlots::NORMAL}, + {TextureType::MODELSPACENORMAL, TextureSlots::NORMAL}, + {TextureType::EMISSIVE, TextureSlots::GLOW}, + {TextureType::SKINTINT, TextureSlots::GLOW}, + {TextureType::SUBSURFACECOLOR, TextureSlots::GLOW}, + {TextureType::HEIGHT, TextureSlots::PARALLAX}, + {TextureType::HEIGHTPBR, TextureSlots::PARALLAX}, + {TextureType::CUBEMAP, TextureSlots::CUBEMAP}, + {TextureType::ENVIRONMENTMASK, TextureSlots::ENVMASK}, + {TextureType::COMPLEXMATERIAL, TextureSlots::ENVMASK}, + {TextureType::RMAOS, TextureSlots::ENVMASK}, + {TextureType::SUBSURFACETINT, TextureSlots::MULTILAYER}, + {TextureType::INNERLAYER, TextureSlots::MULTILAYER}, + {TextureType::FUZZPBR, TextureSlots::MULTILAYER}, + {TextureType::COATNORMALROUGHNESS, TextureSlots::MULTILAYER}, + {TextureType::BACKLIGHT, TextureSlots::BACKLIGHT}, + {TextureType::SPECULAR, TextureSlots::BACKLIGHT}, + {TextureType::SUBSURFACEPBR, TextureSlots::BACKLIGHT}, + {TextureType::UNKNOWN, TextureSlots::UNKNOWN}}; if (TexTypeToSlotMap.find(Type) != TexTypeToSlotMap.end()) { return TexTypeToSlotMap[Type]; @@ -170,6 +187,12 @@ auto NIFUtil::getDefaultsFromSuffix(const std::filesystem::path &Path) for (const auto &[Suffix, Slot] : SuffixMap) { if (boost::iends_with(PathStr, Suffix)) { + // check if PBR in prefix + if (get<1>(Slot) == TextureType::HEIGHT && boost::istarts_with(PathStr, L"textures\\pbr")) { + // This is a PBR heightmap so it gets a different texture type + return {TextureSlots::PARALLAX, TextureType::HEIGHTPBR}; + } + return Slot; } } @@ -180,9 +203,10 @@ auto NIFUtil::getDefaultsFromSuffix(const std::filesystem::path &Path) auto NIFUtil::getTexTypesStr() -> vector { static const vector TexTypesStr = { - "diffuse", "normal", "model space normal", "emissive", "skin tint", "subsurface color", "height", "cubemap", - "environment mask", "complex material", "rmaos", "subsurface tint", "inner layer", "coat normal roughness", - "backlight", "specular", "subsurface pbr", "unknown"}; + "diffuse", "normal", "model space normal", "emissive", "skin tint", + "subsurface color", "height", "height pbr", "cubemap", "environment mask", + "complex material", "rmaos", "subsurface tint", "inner layer", "fuzz pbr", "coat normal roughness", + "backlight", "specular", "subsurface pbr", "unknown"}; return TexTypesStr; } @@ -359,7 +383,8 @@ auto NIFUtil::getTexBase(const std::filesystem::path &Path) -> std::wstring { } auto NIFUtil::getTexMatch(const wstring &Base, const TextureType &DesiredType, - const map> &SearchMap) -> vector { + const map> &SearchMap) + -> vector { // Binary search on base list const wstring BaseLower = boost::to_lower_copy(Base); const auto It = SearchMap.find(BaseLower); @@ -391,7 +416,7 @@ auto NIFUtil::getTexMatch(const wstring &Base, const TextureType &DesiredType, return {}; } -auto NIFUtil::getSearchPrefixes(NifFile const& NIF, nifly::NiShape *NIFShape) -> array { +auto NIFUtil::getSearchPrefixes(NifFile const &NIF, nifly::NiShape *NIFShape) -> array { array OutPrefixes; // Loop through each texture Slot diff --git a/ParallaxGenLib/src/ParallaxGen.cpp b/ParallaxGenLib/src/ParallaxGen.cpp index 356f837..ef5172c 100644 --- a/ParallaxGenLib/src/ParallaxGen.cpp +++ b/ParallaxGenLib/src/ParallaxGen.cpp @@ -187,7 +187,7 @@ void ParallaxGen::zipMeshes() const { } void ParallaxGen::deleteOutputDir(const bool &PreOutput) const { - static const unordered_set FoldersToDelete = {"meshes", "textures"}; + static const unordered_set FoldersToDelete = {"meshes", "textures", "LightPlacer"}; static const unordered_set FilesToDelete = {"ParallaxGen.esp", getDiffJSONName()}; static const unordered_set FilesToIgnore = {"meta.ini"}; static const unordered_set FilesToDeletePreOutput = {getOutputZipName()}; @@ -296,6 +296,10 @@ auto ParallaxGen::processNIF( PatcherObjects.ShaderTransformPatchers[Shader].emplace(TransformShader, std::move(Transform)); } } + for (const auto &Factory : Patchers.GlobalPatchers) { + auto Patcher = Factory(NIFFile, &NIF); + PatcherObjects.GlobalPatchers.emplace_back(std::move(Patcher)); + } // Patch each shape in NIF size_t NumShapes = 0; @@ -358,6 +362,15 @@ auto ParallaxGen::processNIF( return Result; } + // Run global patchers + for (const auto &GlobalPatcher : PatcherObjects.GlobalPatchers) { + Logger::Prefix PrefixPatches(UTF8toUTF16(GlobalPatcher->getPatcherName())); + GlobalPatcher->applyPatch(NIFModified); + } + + // Delete unreferenced blocks + NIF.DeleteUnreferencedBlocks(); + // Save patched NIF if it was modified if (NIFModified && !Dry) { // Sort blocks and set plugin indices @@ -437,7 +450,7 @@ auto ParallaxGen::processShape( // Check for exclusions // only allow BSLightingShaderProperty blocks string NIFShapeName = NIFShape->GetBlockName(); - if (NIFShapeName != "NiTriShape" && NIFShapeName != "BSTriShape" && NIFShapeName != "BSLODTriShape") { + if (NIFShapeName != "NiTriShape" && NIFShapeName != "BSTriShape" && NIFShapeName != "BSLODTriShape" && NIFShapeName != "BSMeshLODTriShape") { Logger::trace(L"Rejecting: Incorrect shape block type"); return Result; } diff --git a/ParallaxGenLib/src/ParallaxGenPlugin.cpp b/ParallaxGenLib/src/ParallaxGenPlugin.cpp index 44a4280..31781e4 100644 --- a/ParallaxGenLib/src/ParallaxGenPlugin.cpp +++ b/ParallaxGenLib/src/ParallaxGenPlugin.cpp @@ -292,18 +292,14 @@ void ParallaxGenPlugin::processShape(const NIFUtil::ShapeShader &AppliedShader, // loop through matches for (const auto &[TXSTIndex, AltTexIndex] : Matches) { int TXSTId = 0; - bool NewTXST = false; + // Check if the TXST is already modified { lock_guard Lock(TXSTModMapMutex); if (TXSTModMap.find(TXSTIndex) != TXSTModMap.end()) { // TXST set was already patched, check to see which shader - if (TXSTModMap[TXSTIndex].find(AppliedShader) == TXSTModMap[TXSTIndex].end()) { - // TXST was patched, but not for the current shader. We need to make a new TXST record - NewTXST = true; - spdlog::trace(L"Plugin Patching | {} | {} | {} | New TXST record needed", NIFPath, Name3D, Index3D); - } else { + if (TXSTModMap[TXSTIndex].find(AppliedShader) != TXSTModMap[TXSTIndex].end()) { // TXST was patched, and for the current shader. We need to determine if AltTex is set correctly spdlog::trace(L"Plugin Patching | {} | {} | {} | TXST record already patched correctly", NIFPath, Name3D, Index3D); @@ -423,24 +419,13 @@ void ParallaxGenPlugin::processShape(const NIFUtil::ShapeShader &AppliedShader, continue; } - // Patch record - if (NewTXST) { - // Create a new TXST record - spdlog::trace(L"Plugin Patching | {} | {} | {} | Creating a new TXST record and patching", NIFPath, Name3D, - Index3D); - TXSTId = libCreateNewTXSTPatch(AltTexIndex, NewSlots); - { - lock_guard Lock(TXSTModMapMutex); - TXSTModMap[TXSTIndex][AppliedShader] = TXSTId; - } - } else { - // Update the existing TXST record - spdlog::trace(L"Plugin Patching | {} | {} | {} | Patching an existing TXST record", NIFPath, Name3D, Index3D); - libCreateTXSTPatch(TXSTIndex, NewSlots); - { - lock_guard Lock(TXSTModMapMutex); - TXSTModMap[TXSTIndex][AppliedShader] = TXSTIndex; - } + // Create a new TXST record + spdlog::trace(L"Plugin Patching | {} | {} | {} | Creating a new TXST record and patching", NIFPath, Name3D, + Index3D); + TXSTId = libCreateNewTXSTPatch(AltTexIndex, NewSlots); + { + lock_guard Lock(TXSTModMapMutex); + TXSTModMap[TXSTIndex][AppliedShader] = TXSTId; } } } diff --git a/ParallaxGenLib/src/patchers/PatcherGlobal.cpp b/ParallaxGenLib/src/patchers/PatcherGlobal.cpp new file mode 100644 index 0000000..f23be65 --- /dev/null +++ b/ParallaxGenLib/src/patchers/PatcherGlobal.cpp @@ -0,0 +1,6 @@ +#include "patchers/PatcherGlobal.hpp" + +using namespace std; + +PatcherGlobal::PatcherGlobal(std::filesystem::path NIFPath, nifly::NifFile *NIF, std::string PatcherName) + : Patcher(std::move(NIFPath), NIF, std::move(PatcherName)) {} diff --git a/ParallaxGenLib/src/patchers/PatcherParticleLightsToLP.cpp b/ParallaxGenLib/src/patchers/PatcherParticleLightsToLP.cpp new file mode 100644 index 0000000..bf86003 --- /dev/null +++ b/ParallaxGenLib/src/patchers/PatcherParticleLightsToLP.cpp @@ -0,0 +1,455 @@ +#include "patchers/PatcherParticleLightsToLP.hpp" +#include "NIFUtil.hpp" +#include "ParallaxGenUtil.hpp" +#include "patchers/PatcherGlobal.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "Logger.hpp" + +using namespace std; + +// statics +nlohmann::json PatcherParticleLightsToLP::LPJsonData; +mutex PatcherParticleLightsToLP::LPJsonDataMutex; + +PatcherParticleLightsToLP::PatcherParticleLightsToLP(std::filesystem::path NIFPath, nifly::NifFile *NIF) + : PatcherGlobal(std::move(NIFPath), NIF, "ParticleLightsToLP") {} + +auto PatcherParticleLightsToLP::getFactory() -> PatcherGlobal::PatcherGlobalFactory { + return [](const filesystem::path &NIFPath, nifly::NifFile *NIF) -> unique_ptr { + return make_unique(NIFPath, NIF); + }; +} + +auto PatcherParticleLightsToLP::applyPatch(bool &NIFModified) -> bool { + // Loop through all blocks to find alpha properties + // Determine if NIF has attached havok animations + vector NIFBlockTree; + getNIF()->GetTree(NIFBlockTree); + + bool AppliedPatch = false; + + for (NiObject *NIFBlock : NIFBlockTree) { + if (!boost::iequals(NIFBlock->GetBlockName(), "NiBillboardNode")) { + continue; + } + auto *const BillboardNode = dynamic_cast(NIFBlock); + + // Get children + set ChildRefs; + BillboardNode->GetChildRefs(ChildRefs); + + if (ChildRefs.empty()) { + // no children + continue; + } + + // get relevant objects + nifly::NiShape *Shape = nullptr; + nifly::NiAlphaProperty *AlphaProperty = nullptr; + nifly::BSEffectShaderProperty *EffectShader = nullptr; + nifly::NiParticleSystem *ParticleSystem = nullptr; + + // Loop through children and assign whatever is found + for (auto *ChildRef : ChildRefs) { + if (Shape == nullptr) { + Shape = getNIF()->GetHeader().GetBlock(ChildRef); + } + + if (ParticleSystem == nullptr) { + ParticleSystem = getNIF()->GetHeader().GetBlock(ChildRef); + } + } + + if (Shape == nullptr) { + // No shape available + continue; + } + + if (ParticleSystem != nullptr) { + // This node has a particle system + // Find the alpha property and effect shader from particle system + const auto PSAlphaPropertyRef = ParticleSystem->alphaPropertyRef; + if (!PSAlphaPropertyRef.IsEmpty() && AlphaProperty == nullptr) { + AlphaProperty = getNIF()->GetHeader().GetBlock(PSAlphaPropertyRef); + } + + const auto PSShaderPropertyRef = ParticleSystem->shaderPropertyRef; + if (!PSShaderPropertyRef.IsEmpty() && EffectShader == nullptr) { + EffectShader = getNIF()->GetHeader().GetBlock(PSShaderPropertyRef); + } + } + + if (ParticleSystem == nullptr) { + // This node does not have a particle system + // Find the alpha property and effect shader from shape + auto *const ShapeAlphaPropertyRef = Shape->AlphaPropertyRef(); + if (!ShapeAlphaPropertyRef->IsEmpty() && AlphaProperty == nullptr) { + AlphaProperty = getNIF()->GetHeader().GetBlock(ShapeAlphaPropertyRef); + } + + auto *const ShapeShaderPropertyRef = Shape->ShaderPropertyRef(); + if (!ShapeShaderPropertyRef->IsEmpty() && EffectShader == nullptr) { + EffectShader = getNIF()->GetHeader().GetBlock(ShapeShaderPropertyRef); + } + } + + if (AlphaProperty == nullptr || EffectShader == nullptr) { + // No alpha property or effect shader + continue; + } + + // Validate that all parameters are met for a particle light + if (AlphaProperty->flags != 4109) { + // no additive blending + continue; + } + + if ((EffectShader->shaderFlags1 & SLSF1_SOFT_EFFECT) == 0U) { + // no soft effect shader flag + continue; + } + + Logger::trace(L"Found Particle Light: {}", ParallaxGenUtil::ASCIItoUTF16(NIFBlock->GetBlockName())); + + // Apply patch to this particle light + if (applySinglePatch(BillboardNode, Shape, EffectShader)) { + // Delete block if patch was applied + NIFModified = true; + const auto NIFBlockRef = nifly::NiRef(getNIF()->GetBlockID(NIFBlock)); + getNIF()->GetHeader().DeleteBlock(NIFBlockRef); + } + } + + return AppliedPatch; +} + +auto PatcherParticleLightsToLP::applySinglePatch(nifly::NiBillboardNode *Node, nifly::NiShape *Shape, + nifly::BSEffectShaderProperty *EffectShader) -> bool { + // Start generating LP JSON + nlohmann::json LPJson; + + // Set model path + LPJson["models"] = nlohmann::json::array(); + + // Remove "meshes\\" from start of path + const auto NIFPath = boost::ireplace_first_copy(getNIFPath().string(), "meshes\\", ""); + LPJson["models"].push_back(NIFPath); + + // Create lights array + LPJson["lights"] = nlohmann::json::array(); + + // LightEntry will hold all JSON data for light + nlohmann::json LightEntry; + LightEntry["data"] = nlohmann::json::object(); + + // Set position + MatTransform GlobalPosition; + getNIF()->GetNodeTransformToGlobal(Node->name.get(), GlobalPosition); + + // Setup points array + LightEntry["points"] = nlohmann::json::array(); + LightEntry["points"].push_back(nlohmann::json::array({round(GlobalPosition.translation.x * 100.0) / 100.0, + round(GlobalPosition.translation.y * 100.0) / 100.0, + round(GlobalPosition.translation.z * 100.0) / 100.0})); + + // Set Light + LightEntry["data"]["light"] = "MagicLightWhite01"; // Placeholder light that will be overridden + + // BSTriShape conversion + auto *const ShapeBSTriShape = dynamic_cast(Shape); + if (ShapeBSTriShape == nullptr) { + // not a BSTriShape + // TODO support other shape types? + return false; + } + + const auto VertData = ShapeBSTriShape->vertData; + const auto NumVerts = VertData.size(); + + // set color + auto BaseColor = EffectShader->baseColor; + bool GetColorFromVertex = abs(BaseColor.r - 1.0) < 1e-5 && abs(BaseColor.g - 1.0) < 1e-5 && + abs(BaseColor.b - 1.0) < 1e-5 && Shape->HasVertexColors(); + if (GetColorFromVertex) { + // Reset basecolor + BaseColor = {0, 0, 0, 0}; + + // Get color from vertex + for (const auto &Vertex : VertData) { + BaseColor.r += static_cast(Vertex.colorData[0]); + BaseColor.g += static_cast(Vertex.colorData[1]); + BaseColor.b += static_cast(Vertex.colorData[2]); + } + + BaseColor.r /= static_cast(NumVerts); + BaseColor.g /= static_cast(NumVerts); + BaseColor.b /= static_cast(NumVerts); + } else { + // Convert to 0-255 + BaseColor *= 255.0; + } + + LightEntry["data"]["color"] = nlohmann::json::array( + {static_cast(BaseColor.r), static_cast(BaseColor.g), static_cast(BaseColor.b)}); + + // Calculate radius from average of vertex distances + double Radius = 0.0; + for (const auto &Vertex : VertData) { + // Find distance with pythagorean theorem + const auto CurDistance = sqrt(pow(Vertex.vert.x, 2) + pow(Vertex.vert.y, 2) + pow(Vertex.vert.z, 2)); + // update radius with average + Radius += CurDistance; + } + + Radius /= static_cast(NumVerts); + + LightEntry["data"]["radius"] = static_cast(Radius); + + LightEntry["data"]["fade"] = round(EffectShader->baseColorScale * 100.0) / 100.0; + + // Get controllers + auto ControllerRef = EffectShader->controllerRef; + while (!ControllerRef.IsEmpty()) { + auto *const Controller = getNIF()->GetHeader().GetBlock(ControllerRef); + if (Controller == nullptr) { + break; + } + + string JSONField; + const auto ControllerJson = getControllerJSON(Controller, JSONField); + if (!ControllerJson.empty()) { + LightEntry["data"][JSONField] = ControllerJson; + } + + ControllerRef = Controller->nextControllerRef; + } + + LPJson["lights"].push_back(LightEntry); + + // Save JSON + { + lock_guard Lock(LPJsonDataMutex); + PatcherParticleLightsToLP::LPJsonData.push_back(LPJson); + } + + return true; +} + +auto PatcherParticleLightsToLP::getControllerJSON(nifly::NiTimeController *Controller, + string &JSONField) -> nlohmann::json { + nlohmann::json ControllerJson = nlohmann::json::object(); + + if (Controller == nullptr) { + return ControllerJson; + } + + auto *const FloatController = dynamic_cast(Controller); + if (FloatController != nullptr) { + if (FloatController->typeOfControlledVariable != 0) { + // We don't care about this controller (not controlling emissive mult) + return ControllerJson; + } + + JSONField = "fadeController"; + } + + auto *const ColorController = dynamic_cast(Controller); + if (ColorController != nullptr) { + JSONField = "colorController"; + } + + if ((FloatController == nullptr && ColorController == nullptr) || + (FloatController != nullptr && ColorController != nullptr)) { + // We don't care about this controller + return ControllerJson; + } + + const auto InterpRef = FloatController->interpolatorRef; + if (InterpRef.IsEmpty()) { + return ControllerJson; + } + + // Find data block + nifly::NiFloatData *FadeDataBlock = nullptr; + nifly::NiPosData *ColorDataBlock = nullptr; + + auto *const FloatInterpolator = getNIF()->GetHeader().GetBlock(InterpRef); + if (FloatInterpolator != nullptr) { + const auto DataRef = FloatInterpolator->dataRef; + FadeDataBlock = getNIF()->GetHeader().GetBlock(DataRef); + } + + auto *const NiPoint3Interpolator = dynamic_cast(FloatInterpolator); + if (NiPoint3Interpolator != nullptr) { + const auto DataRef = NiPoint3Interpolator->dataRef; + ColorDataBlock = getNIF()->GetHeader().GetBlock(DataRef); + } + + if ((FadeDataBlock == nullptr && FloatController != nullptr) || + (ColorDataBlock == nullptr && ColorController != nullptr)) { + // Could not find data block + return ControllerJson; + } + + ControllerJson["keys"] = nlohmann::json::array(); + + nifly::NiKeyType InterpolationType = nifly::NiKeyType::LINEAR_KEY; + if (FloatController != nullptr) { + InterpolationType = FadeDataBlock->data.GetInterpolationType(); + } else if (ColorController != nullptr) { + InterpolationType = ColorDataBlock->data.GetInterpolationType(); + } + + // Get interpolation method + switch (InterpolationType) { + case nifly::NiKeyType::LINEAR_KEY: + ControllerJson["interpolation"] = "Linear"; + break; + case nifly::NiKeyType::QUADRATIC_KEY: + ControllerJson["interpolation"] = "Cubic"; + break; + case nifly::NiKeyType::NO_INTERP: + ControllerJson["interpolation"] = "Step"; + break; + default: + // Linear interpolation default + ControllerJson["interpolation"] = "Linear"; + break; + } + + // Add keys to JSON + uint32_t NumKeys = 0; + if (FloatController != nullptr) { + NumKeys = FadeDataBlock->data.GetNumKeys(); + } else if (ColorController != nullptr) { + NumKeys = ColorDataBlock->data.GetNumKeys(); + } + + for (uint32_t I = 0; I < NumKeys; I++) { + if (FloatController != nullptr) { + const auto CurKey = FadeDataBlock->data.GetKey(static_cast(I)); + + ControllerJson["keys"].push_back( + nlohmann::json::object({{"time", round(CurKey.time * 1000000.0) / 1000000.0}, + {"value", round(CurKey.value * 1000000.0) / 1000000.0}, + {"forward", round(CurKey.forward * 1000000.0) / 1000000.0}, + {"backward", round(CurKey.backward * 1000000.0) / 1000000.0}})); + } else if (ColorController != nullptr) { + const auto CurKey = ColorDataBlock->data.GetKey(static_cast(I)); + + ControllerJson["keys"].push_back(nlohmann::json::object( + {{"time", round(CurKey.time * 1000000.0) / 1000000.0}, + {"color", nlohmann::json::array({static_cast(CurKey.value.x), static_cast(CurKey.value.y), + static_cast(CurKey.value.z)})}, + {"forward", nlohmann::json::array({static_cast(CurKey.forward.x), static_cast(CurKey.forward.y), + static_cast(CurKey.forward.z)})}, + {"backward", nlohmann::json::array({static_cast(CurKey.backward.x), static_cast(CurKey.backward.y), + static_cast(CurKey.backward.z)})}})); + } + } + + return ControllerJson; +} + +void PatcherParticleLightsToLP::finalize() { + lock_guard Lock(LPJsonDataMutex); + + // Check if output JSON is empty + if (LPJsonData.empty()) { + return; + } + + // Key type for grouping: the stringified "models" array. + std::unordered_map>> ModelsToDataPoints; + + // --- Step 1: Group by "models". --- + for (auto &Obj : LPJsonData) { + // Convert the entire "models" array to a string key for grouping. + std::string ModelsKey = Obj["models"].dump(); + + // Retrieve or create the DataGroup for these models. + auto &DataGroup = ModelsToDataPoints[ModelsKey]; + + // --- Step 2: For each light, group by "data". --- + for (auto &Light : Obj["lights"]) { + // The entire "data" object as a string key: + std::string DataKey = Light["data"].dump(); + + // The "points" array might have multiple points, + // but typically in your example there's only one per object. + // We'll append them all to the vector in dataGroup. + for (auto &P : Light["points"]) { + DataGroup[DataKey].push_back(P); + } + } + } + + // --- Step 3: Build the merged output. --- + // We'll produce a JSON array of objects, each with "models" and "lights". + nlohmann::json MergedOutput = nlohmann::json::array(); + + for (auto &Kv : ModelsToDataPoints) { + const auto &ModelsKey = Kv.first; + const auto &DataGroup = Kv.second; + + // Reconstruct the "models" array from the key. + nlohmann::json ModelsJson = nlohmann::json::parse(ModelsKey); + + // Build "lights" by iterating over each distinct dataKey + nlohmann::json LightsArray = nlohmann::json::array(); + + for (const auto &DataKv : DataGroup) { + std::string DataKey = DataKv.first; + // Reconstruct the data object from string + nlohmann::json DataObj = nlohmann::json::parse(DataKey); + + // The merged points for this data + const auto &AllPoints = DataKv.second; // vector + + // Create the light entry + nlohmann::json LightEntry; + LightEntry["data"] = DataObj; + LightEntry["points"] = nlohmann::json::array(); + + // Append all points + for (const auto &Pt : AllPoints) { + LightEntry["points"].push_back(Pt); + } + + // Add this light entry to the lights array + LightsArray.push_back(LightEntry); + } + + // Now assemble the final group object + nlohmann::json GroupObj; + GroupObj["models"] = ModelsJson; + GroupObj["lights"] = LightsArray; + + // Add to the final merged output + MergedOutput.push_back(GroupObj); + } + + const auto OutputJSON = getPGD()->getGeneratedPath() / "LightPlacer/parallaxgen.json"; + + // Create directories for parent path + filesystem::create_directories(OutputJSON.parent_path()); + + // Save JSON to file + ofstream LPJsonFile(OutputJSON); + LPJsonFile << MergedOutput.dump(2) << endl; + LPJsonFile.close(); +} diff --git a/ParallaxGenLib/src/patchers/PatcherTruePBR.cpp b/ParallaxGenLib/src/patchers/PatcherTruePBR.cpp index 8166acc..4adf519 100644 --- a/ParallaxGenLib/src/patchers/PatcherTruePBR.cpp +++ b/ParallaxGenLib/src/patchers/PatcherTruePBR.cpp @@ -672,6 +672,11 @@ void PatcherTruePBR::applyOnePatchSlots(std::array(NIFUtil::TextureSlots::MULTILAYER)] = NewCNR; } @@ -730,6 +735,11 @@ void PatcherTruePBR::enableTruePBROnShape(NiShape *NIFShape, NiShader *NIFShader NIFModified); } + // "hair" attribute + if (flag(TruePBRData, "hair")) { + NIFUtil::setShaderFlag(NIFShaderBSLSP, SLSF2_BACK_LIGHTING, NIFModified); + } + // "multilayer" attribute bool EnableMultiLayer = false; if (TruePBRData.contains("multilayer") && TruePBRData["multilayer"]) { @@ -819,6 +829,32 @@ void PatcherTruePBR::enableTruePBROnShape(NiShape *NIFShape, NiShader *NIFShader NIFUtil::setShaderFloat(NIFShaderBSLSP->parallaxInnerLayerTextureScale.v, GlintParams["density_randomization"], NIFModified); } + } else if(TruePBRData.contains("fuzz")) { + // fuzz is enabled + const auto &FuzzParams = TruePBRData["fuzz"]; + + // Set shader type to MLP + NIFUtil::setShaderType(NIFShader, BSLSP_MULTILAYERPARALLAX, NIFModified); + // Enable Fuzz with soft lighting flag + NIFUtil::setShaderFlag(NIFShaderBSLSP, SLSF2_SOFT_LIGHTING, NIFModified); + + // get color + auto FuzzColor = vector{0.0F, 0.0F, 0.0F}; + if (FuzzParams.contains("color")) { + FuzzColor = FuzzParams["color"].get>(); + } + + NIFUtil::setShaderFloat(NIFShaderBSLSP->parallaxInnerLayerThickness, FuzzColor[0], NIFModified); + NIFUtil::setShaderFloat(NIFShaderBSLSP->parallaxRefractionScale, FuzzColor[1], NIFModified); + NIFUtil::setShaderFloat(NIFShaderBSLSP->parallaxInnerLayerTextureScale.u, FuzzColor[2], NIFModified); + + // get weight + auto FuzzWeight = 1.0F; + if (FuzzParams.contains("weight")) { + FuzzWeight = FuzzParams["weight"].get(); + } + + NIFUtil::setShaderFloat(NIFShaderBSLSP->parallaxInnerLayerTextureScale.v, FuzzWeight, NIFModified); } else { // Revert to default NIFShader type NIFUtil::setShaderType(NIFShader, BSLSP_DEFAULT, NIFModified); @@ -827,8 +863,14 @@ void PatcherTruePBR::enableTruePBROnShape(NiShape *NIFShape, NiShader *NIFShader if (!EnableMultiLayer) { // Clear multilayer flags NIFUtil::clearShaderFlag(NIFShaderBSLSP, SLSF2_MULTI_LAYER_PARALLAX, NIFModified); - NIFUtil::clearShaderFlag(NIFShaderBSLSP, SLSF2_BACK_LIGHTING, NIFModified); - NIFUtil::clearShaderFlag(NIFShaderBSLSP, SLSF2_SOFT_LIGHTING, NIFModified); + + if (!flag(TruePBRData, "hair")) { + NIFUtil::clearShaderFlag(NIFShaderBSLSP, SLSF2_BACK_LIGHTING, NIFModified); + } + + if (!TruePBRData.contains("fuzz")) { + NIFUtil::clearShaderFlag(NIFShaderBSLSP, SLSF2_SOFT_LIGHTING, NIFModified); + } } }