diff --git a/.gitmodules b/.gitmodules index 45808f2..9088fb9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "extern/godot-cpp"] path = extern/godot-cpp url = https://github.com/godotengine/godot-cpp.git +[submodule "extern/DirectXTex"] + path = extern/DirectXTex + url = https://github.com/microsoft/DirectXTex.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ccb11e..6e702f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,17 +26,13 @@ project(swbf2 VERSION 0.1.0 ) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) # Create our library add_library( ${PROJECT_NAME} SHARED ) -target_compile_features( ${PROJECT_NAME} - PRIVATE - cxx_std_23 -) - # LIB_ARCH is the architecture being built. It is set to the build system's architecture. # For macOS, we build a universal library (both arm64 and x86_64). set( LIB_ARCH ${CMAKE_SYSTEM_PROCESSOR} ) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0f7b710..5ad840f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,9 +8,12 @@ target_sources(${PROJECT_NAME} "SWBF2/Native/Chunks/ModelChunk.cpp" "SWBF2/Native/Chunks/ModelSegmentChunk.cpp" "SWBF2/Native/Chunks/StreamReader.cpp" + "SWBF2/Native/Chunks/TextureChunk.cpp" "SWBF2/Native/Chunks/UcfbChunk.cpp" "SWBF2/Native/Chunks/WorldChunk.cpp" "SWBF2/Native/Models/Model.cpp" + "SWBF2/Native/Texture/Texture.cpp" + "SWBF2/Native/Texture/TextureUtils.cpp" "SWBF2/Native/Level.cpp" "SWBF2/Core.cpp" "SWBF2/Level.cpp" diff --git a/src/SWBF2/Core.cpp b/src/SWBF2/Core.cpp index 745bc55..3631673 100644 --- a/src/SWBF2/Core.cpp +++ b/src/SWBF2/Core.cpp @@ -21,8 +21,8 @@ namespace SWBF2 { godot::UtilityFunctions::print("hello world!"); - SWBF2::Native::UcfbChunk::ReadUcfbFile("data/_lvl_pc/common.lvl"); - SWBF2::Native::UcfbChunk::ReadUcfbFile("data/_lvl_pc/core.lvl"); + // SWBF2::Native::UcfbChunk::ReadUcfbFile("data/_lvl_pc/common.lvl"); + // SWBF2::Native::UcfbChunk::ReadUcfbFile("data/_lvl_pc/core.lvl"); add_child(memnew(Level)); } diff --git a/src/SWBF2/FNVHash.hpp b/src/SWBF2/FNVHash.hpp new file mode 100644 index 0000000..d92b33c --- /dev/null +++ b/src/SWBF2/FNVHash.hpp @@ -0,0 +1,31 @@ +#pragma once + +namespace SWBF2 +{ + typedef uint32_t FNVHash; + + constexpr FNVHash FNVGenerateHash(const std::string &str) + { + constexpr uint32_t FNV_prime = 16777619; + constexpr uint32_t offset_basis = 2166136261; + + uint32_t hash = offset_basis; + + const char *buffer = str.data(); + for (size_t i = 0; i < str.length(); ++i) + { + char c = buffer[i]; + c |= 0x20; + + hash ^= c; + hash *= FNV_prime; + } + + return hash; + } + + constexpr FNVHash operator""_fnv(const char *str, const std::size_t length) + { + return FNVGenerateHash({ str, length }); + } +} diff --git a/src/SWBF2/Level.cpp b/src/SWBF2/Level.cpp index 1f4d60d..d031d45 100644 --- a/src/SWBF2/Level.cpp +++ b/src/SWBF2/Level.cpp @@ -1,6 +1,8 @@ #include #include +#include +#include #include #include "Native/Chunks/ChunkProcessor.hpp" @@ -14,9 +16,28 @@ namespace SWBF2 { SWBF2::Native::UcfbChunk::ReadUcfbFile("data/_lvl_pc/cor/cor1.lvl"); + LoadTextures(); LoadMeshes(); } + void Level::LoadTextures() + { + for (auto const &[id, tex] : Native::Level::m_tex) + { + for (auto const format : tex->m_formats) + { + for (auto const faceLevel : format.m_faceLevels) + { + godot::Ref material; + material.instantiate(); + material->set_texture(godot::StandardMaterial3D::TEXTURE_ALBEDO, faceLevel.m_gdImageTexture); + + m_textureMaterials.insert_or_assign(id, material); + } + } + } + } + void Level::LoadMeshes() { for (auto const &[id, model] : Native::Level::m_models) @@ -37,6 +58,7 @@ namespace SWBF2 for (const auto &position : segment.m_verticesBuf.m_positions) { + const auto &position = segment.m_verticesBuf.m_positions[i]; vertices.push_back({ position.x, position.y, position.z }); } @@ -78,6 +100,20 @@ namespace SWBF2 godot::ArrayMesh *arrMesh = memnew(godot::ArrayMesh); arrMesh->add_surface_from_arrays(godot::Mesh::PRIMITIVE_TRIANGLE_STRIP, arrays); + auto tex_id = 0; + for (const auto &texName : segment.m_textureNames) + { + if (!m_textureMaterials.contains(texName)) + { + godot::UtilityFunctions::printerr(__FILE__, ":", __LINE__, ": No texture found for ", texName.c_str()); + continue; + } + + meshInstance->set_material_override(m_textureMaterials[texName]); + + tex_id++; + } + meshInstance->set_mesh(arrMesh); } } diff --git a/src/SWBF2/Level.hpp b/src/SWBF2/Level.hpp index 0939171..fbd0ed3 100644 --- a/src/SWBF2/Level.hpp +++ b/src/SWBF2/Level.hpp @@ -2,18 +2,22 @@ #pragma once #include +#include namespace SWBF2 { class Level : public godot::Node3D { GDCLASS(Level, godot::Node3D) + private: + std::unordered_map> m_textureMaterials; public: Level() {} ~Level() = default; virtual void _ready() override; + void LoadTextures(); void LoadMeshes(); void _process(double delta_time) override; diff --git a/src/SWBF2/Native/Chunks/ChunkProcessor.hpp b/src/SWBF2/Native/Chunks/ChunkProcessor.hpp index 3ecbfc8..0a07c78 100644 --- a/src/SWBF2/Native/Chunks/ChunkProcessor.hpp +++ b/src/SWBF2/Native/Chunks/ChunkProcessor.hpp @@ -9,6 +9,7 @@ #include "UcfbChunk.hpp" #include "WorldChunk.hpp" #include "LoclChunk.hpp" +#include "TextureChunk.hpp" namespace SWBF2::Native { @@ -21,7 +22,8 @@ namespace SWBF2::Native { "ucfb"_m, UcfbChunk::ProcessChunk }, { "wrld"_m, WorldChunk::ProcessChunk }, { "modl"_m, ModelChunk::ProcessChunk }, - { "Locl"_m, LoclChunk::ProcessChunk } + { "Locl"_m, LoclChunk::ProcessChunk }, + { "tex_"_m, TextureChunk::ProcessChunk } }; static void ProcessChunk(StreamReader &streamReader, StreamReader &parentReader); diff --git a/src/SWBF2/Native/Chunks/ModelSegmentChunk.cpp b/src/SWBF2/Native/Chunks/ModelSegmentChunk.cpp index c196e76..3f6f523 100644 --- a/src/SWBF2/Native/Chunks/ModelSegmentChunk.cpp +++ b/src/SWBF2/Native/Chunks/ModelSegmentChunk.cpp @@ -2,6 +2,8 @@ #include "StreamReader.hpp" +#include "FNVHash.hpp" + #include "ModelSegmentChunk.hpp" namespace SWBF2::Native @@ -60,10 +62,23 @@ namespace SWBF2::Native break; } + case "TNAM"_m: + { + uint32_t index; + *readerChild >> index; + + std::string texName; + *readerChild >> texName; + + string_tolower(texName); + + segment.m_textureNames.push_back(texName); + + break; + } case "BNAM"_m: case "SKIN"_m: case "BMAP"_m: - case "TNAM"_m: case "MNAM"_m: default: godot::UtilityFunctions::printerr(__FILE__, ":", __LINE__, ": ", readerChild->GetHeader().ToString().c_str(), " not implemented"); diff --git a/src/SWBF2/Native/Chunks/StreamReader.hpp b/src/SWBF2/Native/Chunks/StreamReader.hpp index 89fe070..8fbc3f2 100644 --- a/src/SWBF2/Native/Chunks/StreamReader.hpp +++ b/src/SWBF2/Native/Chunks/StreamReader.hpp @@ -91,7 +91,7 @@ namespace SWBF2::Native StreamReader &operator>>(std::string &value) { const char *str = reinterpret_cast(&m_data[m_head]); - std::size_t len = GetHeader().size; + std::size_t len = std::distance(str, std::find(str, str + GetHeader().size, '\0')); value = std::string_view(str, len); diff --git a/src/SWBF2/Native/Chunks/TextureChunk.cpp b/src/SWBF2/Native/Chunks/TextureChunk.cpp new file mode 100644 index 0000000..70987fa --- /dev/null +++ b/src/SWBF2/Native/Chunks/TextureChunk.cpp @@ -0,0 +1,113 @@ +#include + +#include "Native/Chunks/StreamReader.hpp" +#include "Native/Chunks/TextureChunk.hpp" + +#include "Native/Texture/TextureUtils.hpp" +#include "Native/Level.hpp" + +namespace SWBF2::Native +{ + void TextureChunk::ProcessChunk(StreamReader &streamReader) + { + std::unique_ptr tex = std::make_unique(); + + auto texNameReaderChild = streamReader.ReadChildWithHeader<"NAME"_m>(); + { + *texNameReaderChild >> tex->m_name; + } + + auto infoReaderChild = streamReader.ReadChildWithHeader<"INFO"_m>(); + { + *infoReaderChild >> tex->m_formatCount; + + tex->m_d3dFormats.resize(tex->m_formatCount); + *infoReaderChild >> tex->m_d3dFormats; + + std::optional fmtReaderChild; + while ((fmtReaderChild = streamReader.ReadChild()).has_value()) + { + switch (fmtReaderChild->GetHeader().m_Magic) + { + case "FMT_"_m: { + StreamReader r{ *fmtReaderChild }; + TextureChunk::ProcessFMTChunk(r, tex.get()); + break; + } + + default: + godot::UtilityFunctions::printerr(__FILE__, ":", __LINE__, ": ", fmtReaderChild->GetHeader().ToString().c_str(), " not implemented"); + break; + } + } + } + + Level::m_tex.try_emplace(tex->m_name, std::move(tex)); + } + + void TextureChunk::ProcessFMTChunk(StreamReader &streamReader, Texture *tex) + { + auto infoReaderChild = streamReader.ReadChildWithHeader<"INFO"_m>(); + + auto &fmt = tex->m_formats.emplace_back(TextureFormat()); + { + *infoReaderChild >> fmt.m_format; + *infoReaderChild >> fmt.m_width; + *infoReaderChild >> fmt.m_height; + *infoReaderChild >> fmt.m_depth; + *infoReaderChild >> fmt.m_mipmapCount; + *infoReaderChild >> fmt.m_typeDetailBias; + } + + auto faceReaderChild = streamReader.ReadChildWithHeader<"FACE"_m>(); + { + std::optional lvlReaderChild; + while ((lvlReaderChild = faceReaderChild->ReadChild()).has_value()) + { + switch (lvlReaderChild->GetHeader().m_Magic) + { + case "LVL_"_m: { + StreamReader r{ *lvlReaderChild }; + TextureChunk::ProcessTextureLevelChunk(r, fmt); + break; + } + + default: + godot::UtilityFunctions::printerr(__FILE__, ":", __LINE__, ": ", lvlReaderChild->GetHeader().ToString().c_str(), " not implemented"); + break; + } + } + } + } + + void TextureChunk::ProcessTextureLevelChunk(StreamReader &streamReader, TextureFormat &fmt) + { + TextureFormatFaceLevel lvl; + + auto infoReaderChild = streamReader.ReadChildWithHeader<"INFO"_m>(); + { + *infoReaderChild >> lvl.m_mipLevel; + *infoReaderChild >> lvl.m_bodySize; + } + + auto bodyReaderChild = streamReader.ReadChildWithHeader<"BODY"_m>(); + { + lvl.m_imageInBytes.resize(lvl.m_bodySize); + + *bodyReaderChild >> lvl.m_imageInBytes; + } + + if (TextureUtils::IsD3DFormatSupported(fmt.m_format)) + { + godot::PackedByteArray imageBuf; + imageBuf.resize(lvl.m_bodySize); + std::copy(lvl.m_imageInBytes.begin(), lvl.m_imageInBytes.end(), (std::byte *)imageBuf.ptrw()); + godot::Ref image{ godot::Image::create_from_data(fmt.m_width, fmt.m_height, fmt.m_mipmapCount > 1, TextureUtils::D3DToGLFormat(fmt.m_format), imageBuf) }; + + lvl.m_gdImageTexture.instantiate(); + lvl.m_gdImageTexture->create_from_image(image); + } + + fmt.m_faceLevels.push_back(lvl); + } +} diff --git a/src/SWBF2/Native/Chunks/TextureChunk.hpp b/src/SWBF2/Native/Chunks/TextureChunk.hpp new file mode 100644 index 0000000..9331bc4 --- /dev/null +++ b/src/SWBF2/Native/Chunks/TextureChunk.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "Native/Chunks/StreamReader.hpp" + +#include "Native/Texture/Texture.hpp" + +namespace SWBF2::Native +{ + class TextureChunk { + public: + static void ProcessChunk(StreamReader &streamReader); + static void ProcessFMTChunk(StreamReader &streamReader, Texture *tex); + static void ProcessTextureLevelChunk(StreamReader &streamReader, TextureFormat &fmt); + }; + +} diff --git a/src/SWBF2/Native/D3D9/d3dformat.hpp b/src/SWBF2/Native/D3D9/d3dformat.hpp new file mode 100644 index 0000000..b98811e --- /dev/null +++ b/src/SWBF2/Native/D3D9/d3dformat.hpp @@ -0,0 +1,103 @@ + +#pragma once + +#include "Types.hpp" + +namespace SWBF2::Native +{ +#ifndef MAKEFOURCC +#define MAKEFOURCC(ch0, ch1, ch2, ch3) \ + ((unsigned int)(std::byte)(ch0) | ((unsigned int)(std::byte)(ch1) << 8) | \ + ((unsigned int)(std::byte)(ch2) << 16) | ((unsigned int)(std::byte)(ch3) << 24 )) +#endif /* defined(MAKEFOURCC) */ + + typedef enum _D3DFORMAT { + D3DFMT_UNKNOWN = 0, + + D3DFMT_R8G8B8 = 20, + D3DFMT_A8R8G8B8 = 21, + D3DFMT_X8R8G8B8 = 22, + D3DFMT_R5G6B5 = 23, + D3DFMT_X1R5G5B5 = 24, + D3DFMT_A1R5G5B5 = 25, + D3DFMT_A4R4G4B4 = 26, + D3DFMT_R3G3B2 = 27, + D3DFMT_A8 = 28, + D3DFMT_A8R3G3B2 = 29, + D3DFMT_X4R4G4B4 = 30, + D3DFMT_A2B10G10R10 = 31, + D3DFMT_A8B8G8R8 = 32, + D3DFMT_X8B8G8R8 = 33, + D3DFMT_G16R16 = 34, + D3DFMT_A2R10G10B10 = 35, + D3DFMT_A16B16G16R16 = 36, + + D3DFMT_A8P8 = 40, + D3DFMT_P8 = 41, + + D3DFMT_L8 = 50, + D3DFMT_A8L8 = 51, + D3DFMT_A4L4 = 52, + + D3DFMT_V8U8 = 60, + D3DFMT_L6V5U5 = 61, + D3DFMT_X8L8V8U8 = 62, + D3DFMT_Q8W8V8U8 = 63, + D3DFMT_V16U16 = 64, + D3DFMT_A2W10V10U10 = 67, + + D3DFMT_UYVY = MAKEFOURCC('U', 'Y', 'V', 'Y'), + D3DFMT_R8G8_B8G8 = MAKEFOURCC('R', 'G', 'B', 'G'), + D3DFMT_YUY2 = MAKEFOURCC('Y', 'U', 'Y', '2'), + D3DFMT_G8R8_G8B8 = MAKEFOURCC('G', 'R', 'G', 'B'), + D3DFMT_DXT1 = MAKEFOURCC('D', 'X', 'T', '1'), + D3DFMT_DXT2 = MAKEFOURCC('D', 'X', 'T', '2'), + D3DFMT_DXT3 = MAKEFOURCC('D', 'X', 'T', '3'), + D3DFMT_DXT4 = MAKEFOURCC('D', 'X', 'T', '4'), + D3DFMT_DXT5 = MAKEFOURCC('D', 'X', 'T', '5'), + + D3DFMT_D16_LOCKABLE = 70, + D3DFMT_D32 = 71, + D3DFMT_D15S1 = 73, + D3DFMT_D24S8 = 75, + D3DFMT_D24X8 = 77, + D3DFMT_D24X4S4 = 79, + D3DFMT_D16 = 80, + + D3DFMT_D32F_LOCKABLE = 82, + D3DFMT_D24FS8 = 83, + +#if !defined(D3D_DISABLE_9EX) + D3DFMT_D32_LOCKABLE = 84, + D3DFMT_S8_LOCKABLE = 85, +#endif // !D3D_DISABLE_9EX + + D3DFMT_L16 = 81, + + D3DFMT_VERTEXDATA = 100, + D3DFMT_INDEX16 = 101, + D3DFMT_INDEX32 = 102, + + D3DFMT_Q16W16V16U16 = 110, + + D3DFMT_MULTI2_ARGB8 = MAKEFOURCC('M', 'E', 'T', '1'), + + D3DFMT_R16F = 111, + D3DFMT_G16R16F = 112, + D3DFMT_A16B16G16R16F = 113, + + D3DFMT_R32F = 114, + D3DFMT_G32R32F = 115, + D3DFMT_A32B32G32R32F = 116, + + D3DFMT_CxV8U8 = 117, + +#if !defined(D3D_DISABLE_9EX) + D3DFMT_A1 = 118, + D3DFMT_A2B10G10R10_XR_BIAS = 119, + D3DFMT_BINARYBUFFER = 199, +#endif // !D3D_DISABLE_9EX + + D3DFMT_FORCE_DWORD = 0x7fffffff + } D3DFORMAT; +} diff --git a/src/SWBF2/Native/Level.cpp b/src/SWBF2/Native/Level.cpp index 42ea3d7..b20a2aa 100644 --- a/src/SWBF2/Native/Level.cpp +++ b/src/SWBF2/Native/Level.cpp @@ -6,5 +6,6 @@ namespace SWBF2::Native World Level::m_world; std::unordered_map Level::m_models; + std::unordered_map> Level::m_tex; std::unordered_map Level::m_locl; } diff --git a/src/SWBF2/Native/Level.hpp b/src/SWBF2/Native/Level.hpp index 5dc7183..fcaa5c1 100644 --- a/src/SWBF2/Native/Level.hpp +++ b/src/SWBF2/Native/Level.hpp @@ -2,6 +2,7 @@ #pragma once #include "Models/Model.hpp" +#include "Texture/Texture.hpp" #include "World.hpp" @@ -12,6 +13,7 @@ namespace SWBF2::Native static World m_world; static std::unordered_map m_models; + static std::unordered_map> m_tex; using LoclEntriesMap = std::unordered_map; static std::unordered_map m_locl; diff --git a/src/SWBF2/Native/Models/ModelSegment.hpp b/src/SWBF2/Native/Models/ModelSegment.hpp index 4d9f337..66ab7b6 100644 --- a/src/SWBF2/Native/Models/ModelSegment.hpp +++ b/src/SWBF2/Native/Models/ModelSegment.hpp @@ -65,6 +65,7 @@ namespace SWBF2::Native Material m_material; std::string p_renderType; // TODO: enum? + std::vector m_textureNames; IndicesBuf m_indicesBuf; VerticesBuf m_verticesBuf; std::string m_parent; diff --git a/src/SWBF2/Native/Texture/Texture.cpp b/src/SWBF2/Native/Texture/Texture.cpp new file mode 100644 index 0000000..fbf133d --- /dev/null +++ b/src/SWBF2/Native/Texture/Texture.cpp @@ -0,0 +1,8 @@ + +#pragma once + +#include "Texture.hpp" + +namespace SWBF2::Native +{ +} diff --git a/src/SWBF2/Native/Texture/Texture.hpp b/src/SWBF2/Native/Texture/Texture.hpp new file mode 100644 index 0000000..5087072 --- /dev/null +++ b/src/SWBF2/Native/Texture/Texture.hpp @@ -0,0 +1,40 @@ + +#pragma once + +#include + +#include "Native/D3D9/d3dformat.hpp" + +namespace SWBF2::Native +{ + typedef struct _TEX_FMT_FACE_LVL + { + uint32_t m_mipLevel; + uint32_t m_bodySize; + std::vector m_imageInBytes; + + godot::Ref m_gdImageTexture; + } TextureFormatFaceLevel; + + typedef struct _TEX_FMT + { + D3DFORMAT m_format; + uint16_t m_width; + uint16_t m_height; + uint16_t m_depth; + uint16_t m_mipmapCount; + uint32_t m_typeDetailBias; + + std::vector m_faceLevels; + } TextureFormat; + + class Texture { + public: + std::string m_name; + + uint32_t m_formatCount; + std::vector m_d3dFormats; + + std::vector m_formats; + }; +} diff --git a/src/SWBF2/Native/Texture/TextureUtils.cpp b/src/SWBF2/Native/Texture/TextureUtils.cpp new file mode 100644 index 0000000..470f44a --- /dev/null +++ b/src/SWBF2/Native/Texture/TextureUtils.cpp @@ -0,0 +1,6 @@ + +#include "TextureUtils.hpp" + +namespace SWBF2::Native::TextureUtils +{ +} diff --git a/src/SWBF2/Native/Texture/TextureUtils.hpp b/src/SWBF2/Native/Texture/TextureUtils.hpp new file mode 100644 index 0000000..803e983 --- /dev/null +++ b/src/SWBF2/Native/Texture/TextureUtils.hpp @@ -0,0 +1,40 @@ + +#pragma once + +#include + +#include "Native/D3D9/d3dformat.hpp" + +namespace SWBF2::Native::TextureUtils +{ + inline godot::Image::Format D3DToGLFormat(D3DFORMAT format) + { + switch (format) + { + case D3DFMT_DXT1: + return godot::Image::FORMAT_DXT1; + case D3DFMT_DXT3: + return godot::Image::FORMAT_DXT3; + case D3DFMT_R5G6B5: + return godot::Image::FORMAT_RGB565; + case D3DFMT_L8: + return godot::Image::FORMAT_L8; + default: + throw std::runtime_error{ "unsupported image format" }; + } + } + + inline bool IsD3DFormatSupported(D3DFORMAT format) + { + switch (format) + { + case D3DFMT_DXT1: + case D3DFMT_DXT3: + case D3DFMT_R5G6B5: + case D3DFMT_L8: + return true; + default: + return false; + } + } +} diff --git a/src/SWBF2/Types.hpp b/src/SWBF2/Types.hpp index a3b4324..5188326 100644 --- a/src/SWBF2/Types.hpp +++ b/src/SWBF2/Types.hpp @@ -17,14 +17,13 @@ #include "Vector2.hpp" #include "Vector3.hpp" #include "RGBA.hpp" +#include "FNVHash.hpp" +#include "Utils.hpp" namespace SWBF2 { typedef uint32_t ChunkSize; - typedef uint32_t CRCChecksum; - typedef uint32_t FNVHash; - typedef uint16_t SWBF2Handle; constexpr auto SWBF2HANDLE_INVALID = 0xffff; diff --git a/src/SWBF2/Utils.hpp b/src/SWBF2/Utils.hpp new file mode 100644 index 0000000..7f25bc4 --- /dev/null +++ b/src/SWBF2/Utils.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace SWBF2 +{ + inline void string_tolower(std::string &str) + { + std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { return std::tolower(c); }); + } +}