From 7b28b574d24bbb8fae57bb9bf2f5becaced4238d Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 22 Sep 2024 16:59:31 +0200 Subject: [PATCH] feat: load iw5 materials from json --- .../IW5/AssetLoaders/AssetLoaderMaterial.cpp | 47 +- .../IW5/AssetLoaders/AssetLoaderMaterial.h | 7 +- .../Game/IW5/Material/JsonMaterialLoader.cpp | 447 ++++++++++++++++++ .../Game/IW5/Material/JsonMaterialLoader.h | 13 + 4 files changed, 509 insertions(+), 5 deletions(-) create mode 100644 src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.cpp create mode 100644 src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.h diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.cpp b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.cpp index 6075ad87..13170e20 100644 --- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.cpp +++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.cpp @@ -1,18 +1,57 @@ #include "AssetLoaderMaterial.h" #include "Game/IW5/IW5.h" -#include "ObjLoading.h" +#include "Game/IW5/Material/JsonMaterialLoader.h" #include "Pool/GlobalAssetPool.h" #include +#include +#include using namespace IW5; void* AssetLoaderMaterial::CreateEmptyAsset(const std::string& assetName, MemoryManager* memory) { - auto* material = memory->Create(); - memset(material, 0, sizeof(Material)); + auto* asset = memory->Alloc(); + asset->info.name = memory->Dup(assetName.c_str()); + return asset; +} + +bool AssetLoaderMaterial::CanLoadFromRaw() const +{ + return true; +} + +std::string AssetLoaderMaterial::GetFileNameForAsset(const std::string& assetName) +{ + std::string sanitizedFileName(assetName); + if (sanitizedFileName[0] == '*') + { + std::ranges::replace(sanitizedFileName, '*', '_'); + const auto parenthesisPos = sanitizedFileName.find('('); + if (parenthesisPos != std::string::npos) + sanitizedFileName.erase(parenthesisPos); + sanitizedFileName = "generated/" + sanitizedFileName; + } + + return std::format("materials/{}.json", sanitizedFileName); +} + +bool AssetLoaderMaterial::LoadFromRaw( + const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const +{ + const auto file = searchPath->Open(GetFileNameForAsset(assetName)); + if (!file.IsOpen()) + return false; + + auto* material = memory->Alloc(); material->info.name = memory->Dup(assetName.c_str()); - return material; + std::vector dependencies; + if (LoadMaterialAsJson(*file.m_stream, *material, memory, manager, dependencies)) + manager->AddAsset(assetName, material, std::move(dependencies)); + else + std::cerr << std::format("Failed to load material \"{}\"\n", assetName); + + return true; } diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.h b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.h index b61c8965..f9f45d71 100644 --- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.h +++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.h @@ -1,6 +1,6 @@ #pragma once - #include "AssetLoading/BasicAssetLoader.h" +#include "AssetLoading/IAssetLoadingManager.h" #include "Game/IW5/IW5.h" #include "SearchPath/ISearchPath.h" @@ -8,7 +8,12 @@ namespace IW5 { class AssetLoaderMaterial final : public BasicAssetLoader { + static std::string GetFileNameForAsset(const std::string& assetName); + public: _NODISCARD void* CreateEmptyAsset(const std::string& assetName, MemoryManager* memory) override; + _NODISCARD bool CanLoadFromRaw() const override; + bool + LoadFromRaw(const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const override; }; } // namespace IW5 diff --git a/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.cpp b/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.cpp new file mode 100644 index 00000000..a567e9dd --- /dev/null +++ b/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.cpp @@ -0,0 +1,447 @@ +#include "JsonMaterialLoader.h" + +#include "Game/IW5/CommonIW5.h" +#include "Game/IW5/Material/JsonMaterial.h" +#include "Impl/Base64.h" + +#include +#include +#include + +using namespace nlohmann; +using namespace IW5; + +namespace +{ + class JsonLoader + { + public: + JsonLoader(std::istream& stream, MemoryManager& memory, IAssetLoadingManager& manager, std::vector& dependencies) + : m_stream(stream), + m_memory(memory), + m_manager(manager), + m_dependencies(dependencies) + + { + } + + bool Load(Material& material) const + { + const auto jRoot = json::parse(m_stream); + std::string game; + std::string type; + unsigned version; + + jRoot.at("_type").get_to(type); + jRoot.at("_game").get_to(game); + jRoot.at("_version").get_to(version); + + if (type != "material" || version != 1u || game != "iw5") + { + std::cerr << std::format("Tried to load material \"{}\" but did not find expected type material of version 1\n", material.info.name); + return false; + } + + try + { + const auto jMaterial = jRoot.get(); + return CreateMaterialFromJson(jMaterial, material); + } + catch (const json::exception& e) + { + std::cerr << std::format("Failed to parse json of material: {}\n", e.what()); + } + + return false; + } + + private: + static void PrintError(const Material& material, const std::string& message) + { + std::cerr << std::format("Cannot load material \"{}\": {}\n", material.info.name, message); + } + + static bool CreateGameFlagsFromJson(const JsonMaterial& jMaterial, unsigned char& gameFlags) + { + for (const auto gameFlag : jMaterial.gameFlags) + gameFlags |= gameFlag; + + return true; + } + + static void CreateSamplerStateFromJson(const JsonSamplerState& jSamplerState, MaterialTextureDefSamplerState& samplerState) + { + samplerState.filter = jSamplerState.filter; + samplerState.mipMap = jSamplerState.mipMap; + samplerState.clampU = jSamplerState.clampU; + samplerState.clampV = jSamplerState.clampV; + samplerState.clampW = jSamplerState.clampW; + } + + bool CreateWaterFromJson(const JsonWater& jWater, water_t& water, const Material& material) const + { + water.writable.floatTime = jWater.floatTime; + water.M = jWater.m; + water.N = jWater.n; + water.Lx = jWater.lx; + water.Lz = jWater.lz; + water.gravity = jWater.gravity; + water.windvel = jWater.windvel; + water.winddir[0] = jWater.winddir[0]; + water.winddir[1] = jWater.winddir[1]; + water.amplitude = jWater.amplitude; + water.codeConstant[0] = jWater.codeConstant[0]; + water.codeConstant[1] = jWater.codeConstant[1]; + water.codeConstant[2] = jWater.codeConstant[2]; + water.codeConstant[3] = jWater.codeConstant[3]; + + const auto expectedH0Size = water.M * water.N * sizeof(complex_s); + if (expectedH0Size > 0) + { + water.H0 = m_memory.Alloc(water.M * water.N); + const auto h0Size = base64::DecodeBase64(jWater.h0.data(), jWater.h0.size(), water.H0, expectedH0Size); + if (h0Size != expectedH0Size) + { + PrintError(material, std::format("Water h0 size {} does not match expected {}", h0Size, expectedH0Size)); + return false; + } + } + + const auto expectedWTermSize = water.M * water.N * sizeof(float); + if (expectedWTermSize > 0) + { + water.wTerm = m_memory.Alloc(water.M * water.N); + auto wTermSize = base64::DecodeBase64(jWater.wTerm.data(), jWater.wTerm.size(), water.wTerm, expectedWTermSize); + if (wTermSize != expectedWTermSize) + { + PrintError(material, std::format("Water wTerm size {} does not match expected {}", wTermSize, expectedWTermSize)); + return false; + } + } + + return true; + } + + bool CreateTextureDefFromJson(const JsonTexture& jTexture, MaterialTextureDef& textureDef, const Material& material) const + { + if (jTexture.name) + { + if (jTexture.name->empty()) + { + PrintError(material, "textureDef name cannot be empty"); + return false; + } + + textureDef.nameStart = jTexture.name.value()[0]; + textureDef.nameEnd = jTexture.name.value()[jTexture.name->size() - 1]; + textureDef.nameHash = Common::R_HashString(jTexture.name.value().c_str(), 0); + } + else + { + if (!jTexture.nameStart || !jTexture.nameEnd || !jTexture.nameHash) + { + PrintError(material, "textureDefs without name must have nameStart, nameEnd and nameHash"); + return false; + } + + if (jTexture.nameStart->size() != 1 || jTexture.nameEnd->size() != 1) + { + PrintError(material, "nameStart and nameEnd must be a string of exactly one character"); + return false; + } + + textureDef.nameStart = jTexture.nameStart.value()[0]; + textureDef.nameEnd = jTexture.nameEnd.value()[0]; + textureDef.nameHash = jTexture.nameHash.value(); + } + + CreateSamplerStateFromJson(jTexture.samplerState, textureDef.samplerState); + + textureDef.semantic = jTexture.semantic; + + auto* imageAsset = m_manager.LoadDependency(jTexture.image); + if (!imageAsset) + { + PrintError(material, std::format("Could not find textureDef image: {}", jTexture.image)); + return false; + } + m_dependencies.push_back(imageAsset); + + if (jTexture.water) + { + if (jTexture.semantic != TS_WATER_MAP) + { + PrintError(material, "Only textureDefs with semantic waterMap can define water params"); + return false; + } + } + else + { + if (jTexture.semantic == TS_WATER_MAP) + { + PrintError(material, "TextureDefs with semantic waterMap must define water params"); + return false; + } + } + + if (jTexture.water) + { + auto* water = m_memory.Alloc(); + water->image = imageAsset->Asset(); + + if (!CreateWaterFromJson(*jTexture.water, *water, material)) + return false; + + textureDef.u.water = water; + } + else + textureDef.u.image = imageAsset->Asset(); + + return true; + } + + static bool CreateConstantDefFromJson(const JsonConstant& jConstant, MaterialConstantDef& constantDef, const Material& material) + { + if (jConstant.name) + { + const auto copyCount = std::min(jConstant.name->size() + 1, std::extent_v); + strncpy(constantDef.name, jConstant.name->c_str(), copyCount); + if (copyCount < std::extent_v) + memset(&constantDef.name[copyCount], 0, std::extent_v - copyCount); + constantDef.nameHash = Common::R_HashString(jConstant.name->c_str(), 0); + } + else + { + if (!jConstant.nameFragment || !jConstant.nameHash) + { + PrintError(material, "constantDefs without name must have nameFragment and nameHash"); + return false; + } + + const auto copyCount = std::min(jConstant.nameFragment->size() + 1, std::extent_v); + strncpy(constantDef.name, jConstant.nameFragment->c_str(), copyCount); + if (copyCount < std::extent_v) + memset(&constantDef.name[copyCount], 0, std::extent_v - copyCount); + constantDef.nameHash = jConstant.nameHash.value(); + } + + if (jConstant.literal.size() != 4) + { + PrintError(material, "constantDef literal must be array of size 4"); + return false; + } + + constantDef.literal.x = jConstant.literal[0]; + constantDef.literal.y = jConstant.literal[1]; + constantDef.literal.z = jConstant.literal[2]; + constantDef.literal.w = jConstant.literal[3]; + + return true; + } + + static bool + CreateStateBitsTableEntryFromJson(const JsonStateBitsTableEntry& jStateBitsTableEntry, GfxStateBits& stateBitsTableEntry, const Material& material) + { + auto& structured = stateBitsTableEntry.loadBits.structured; + + structured.srcBlendRgb = jStateBitsTableEntry.srcBlendRgb; + structured.dstBlendRgb = jStateBitsTableEntry.dstBlendRgb; + structured.blendOpRgb = jStateBitsTableEntry.blendOpRgb; + + if (jStateBitsTableEntry.alphaTest == JsonAlphaTest::DISABLED) + { + structured.alphaTestDisabled = 1; + structured.alphaTest = 0; + } + else if (jStateBitsTableEntry.alphaTest == JsonAlphaTest::GT0) + { + structured.alphaTestDisabled = 0; + structured.alphaTest = GFXS_ALPHA_TEST_GT_0; + } + else if (jStateBitsTableEntry.alphaTest == JsonAlphaTest::LT128) + { + structured.alphaTestDisabled = 0; + structured.alphaTest = GFXS_ALPHA_TEST_LT_128; + } + else if (jStateBitsTableEntry.alphaTest == JsonAlphaTest::GE128) + { + structured.alphaTestDisabled = 0; + structured.alphaTest = GFXS_ALPHA_TEST_GE_128; + } + else + { + PrintError(material, "Invalid value for alphaTest"); + return false; + } + + if (jStateBitsTableEntry.cullFace == JsonCullFace::NONE) + structured.cullFace = GFXS_CULL_NONE; + else if (jStateBitsTableEntry.cullFace == JsonCullFace::BACK) + structured.cullFace = GFXS_CULL_BACK; + else if (jStateBitsTableEntry.cullFace == JsonCullFace::FRONT) + structured.cullFace = GFXS_CULL_FRONT; + else + { + PrintError(material, "Invalid value for cull face"); + return false; + } + + structured.srcBlendAlpha = jStateBitsTableEntry.srcBlendAlpha; + structured.dstBlendAlpha = jStateBitsTableEntry.dstBlendAlpha; + structured.blendOpAlpha = jStateBitsTableEntry.blendOpAlpha; + structured.colorWriteRgb = jStateBitsTableEntry.colorWriteRgb; + structured.colorWriteAlpha = jStateBitsTableEntry.colorWriteAlpha; + structured.gammaWrite = jStateBitsTableEntry.gammaWrite; + structured.polymodeLine = jStateBitsTableEntry.polymodeLine; + structured.depthWrite = jStateBitsTableEntry.depthWrite; + + if (jStateBitsTableEntry.depthTest == JsonDepthTest::DISABLED) + structured.depthTestDisabled = 1; + else if (jStateBitsTableEntry.depthTest == JsonDepthTest::ALWAYS) + structured.depthTest = GFXS_DEPTHTEST_ALWAYS; + else if (jStateBitsTableEntry.depthTest == JsonDepthTest::LESS) + structured.depthTest = GFXS_DEPTHTEST_LESS; + else if (jStateBitsTableEntry.depthTest == JsonDepthTest::EQUAL) + structured.depthTest = GFXS_DEPTHTEST_EQUAL; + else if (jStateBitsTableEntry.depthTest == JsonDepthTest::LESS_EQUAL) + structured.depthTest = GFXS_DEPTHTEST_LESSEQUAL; + else + { + PrintError(material, "Invalid value for depth test"); + return false; + } + + structured.polygonOffset = jStateBitsTableEntry.polygonOffset; + + if (jStateBitsTableEntry.stencilFront) + { + structured.stencilFrontEnabled = 1; + structured.stencilFrontPass = jStateBitsTableEntry.stencilFront->pass; + structured.stencilFrontFail = jStateBitsTableEntry.stencilFront->fail; + structured.stencilFrontZFail = jStateBitsTableEntry.stencilFront->zfail; + structured.stencilFrontFunc = jStateBitsTableEntry.stencilFront->func; + } + + if (jStateBitsTableEntry.stencilBack) + { + structured.stencilBackEnabled = 1; + structured.stencilBackPass = jStateBitsTableEntry.stencilBack->pass; + structured.stencilBackFail = jStateBitsTableEntry.stencilBack->fail; + structured.stencilBackZFail = jStateBitsTableEntry.stencilBack->zfail; + structured.stencilBackFunc = jStateBitsTableEntry.stencilBack->func; + } + + return true; + } + + bool CreateMaterialFromJson(const JsonMaterial& jMaterial, Material& material) const + { + if (!CreateGameFlagsFromJson(jMaterial, material.info.gameFlags)) + return false; + + material.info.sortKey = static_cast(jMaterial.sortKey); + + if (jMaterial.textureAtlas) + { + material.info.textureAtlasRowCount = jMaterial.textureAtlas->rows; + material.info.textureAtlasColumnCount = jMaterial.textureAtlas->columns; + } + else + { + material.info.textureAtlasRowCount = 0; + material.info.textureAtlasColumnCount = 0; + } + + material.info.surfaceTypeBits = jMaterial.surfaceTypeBits; + + if (jMaterial.stateBitsEntry.size() != std::extent_v) + { + PrintError(material, std::format("StateBitsEntry size is not {}", std::extent_v)); + return false; + } + for (auto i = 0u; i < std::extent_v; i++) + material.stateBitsEntry[i] = jMaterial.stateBitsEntry[i]; + + material.stateFlags = static_cast(jMaterial.stateFlags); + material.cameraRegion = jMaterial.cameraRegion; + + auto* techniqueSet = m_manager.LoadDependency(jMaterial.techniqueSet); + if (!techniqueSet) + { + PrintError(material, "Could not find technique set"); + return false; + } + m_dependencies.push_back(techniqueSet); + material.techniqueSet = techniqueSet->Asset(); + + if (!jMaterial.textures.empty()) + { + material.textureCount = static_cast(jMaterial.textures.size()); + material.textureTable = m_memory.Alloc(material.textureCount); + + for (auto i = 0u; i < material.textureCount; i++) + { + if (!CreateTextureDefFromJson(jMaterial.textures[i], material.textureTable[i], material)) + return false; + } + } + else + { + material.textureCount = 0; + material.textureTable = nullptr; + } + + if (!jMaterial.constants.empty()) + { + material.constantCount = static_cast(jMaterial.constants.size()); + material.constantTable = m_memory.Alloc(material.constantCount); + + for (auto i = 0u; i < material.constantCount; i++) + { + if (!CreateConstantDefFromJson(jMaterial.constants[i], material.constantTable[i], material)) + return false; + } + } + else + { + material.constantCount = 0; + material.constantTable = nullptr; + } + + if (!jMaterial.stateBits.empty()) + { + material.stateBitsCount = static_cast(jMaterial.stateBits.size()); + material.stateBitsTable = m_memory.Alloc(material.stateBitsCount); + + for (auto i = 0u; i < material.stateBitsCount; i++) + { + if (!CreateStateBitsTableEntryFromJson(jMaterial.stateBits[i], material.stateBitsTable[i], material)) + return false; + } + } + else + { + material.stateBitsCount = 0; + material.stateBitsTable = nullptr; + } + + return true; + } + + std::istream& m_stream; + MemoryManager& m_memory; + IAssetLoadingManager& m_manager; + std::vector& m_dependencies; + }; +} // namespace + +namespace IW5 +{ + bool LoadMaterialAsJson( + std::istream& stream, Material& material, MemoryManager* memory, IAssetLoadingManager* manager, std::vector& dependencies) + { + const JsonLoader loader(stream, *memory, *manager, dependencies); + + return loader.Load(material); + } +} // namespace IW5 diff --git a/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.h b/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.h new file mode 100644 index 00000000..b498852d --- /dev/null +++ b/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.h @@ -0,0 +1,13 @@ +#pragma once + +#include "AssetLoading/IAssetLoadingManager.h" +#include "Game/IW5/IW5.h" +#include "Utils/MemoryManager.h" + +#include + +namespace IW5 +{ + bool LoadMaterialAsJson( + std::istream& stream, Material& material, MemoryManager* memory, IAssetLoadingManager* manager, std::vector& dependencies); +} // namespace IW5