diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84bd417..a54ca9f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: run: | mkdir build conan profile detect --force - conan install . --output-folder=build --build=missing -s build_type=Release + conan install . --output-folder=build --build=missing -s build_type=Release -s compiler.cppstd=20 - name: Generate projects run: | diff --git a/BMEdit/Editor/CMakeLists.txt b/BMEdit/Editor/CMakeLists.txt index d0d1b3c..f52652b 100644 --- a/BMEdit/Editor/CMakeLists.txt +++ b/BMEdit/Editor/CMakeLists.txt @@ -67,6 +67,6 @@ target_compile_definitions(Editor # --- Required GameLib & Qt6 target_link_libraries(Editor PUBLIC Qt6::Widgets OpenGL::GL Qt6::OpenGL Qt6::OpenGLWidgets) target_link_libraries(Editor PRIVATE GameLib RenderDoc::AppInterface) -target_link_libraries(Editor PRIVATE ${libzip_LIBRARIES} ${ZLIB_LIBRARIES} ${libsquish_LIBRARIES}) +target_link_libraries(Editor PRIVATE ${libzip_LIBRARIES} ${ZLIB_LIBRARIES} ${libsquish_LIBRARIES} Freetype::Freetype) target_include_directories(Editor PRIVATE ${ZLIB_INCLUDE_DIRS} ${libzip_INCLUDE_DIRS} ${libsquish_INCLUDE_DIRS}) target_compile_definitions(Editor PRIVATE ${libzip_DEFINITIONS} ${libsquish_DEFINITIONS}) \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/GizmoRenderer.h b/BMEdit/Editor/Include/Render/GizmoRenderer.h index 4be8a16..da5173f 100644 --- a/BMEdit/Editor/Include/Render/GizmoRenderer.h +++ b/BMEdit/Editor/Include/Render/GizmoRenderer.h @@ -1,24 +1,37 @@ #pragma once -#include #include +#include #include #include +#include #include #include +#include +#include #include #include +#include +#include FT_FREETYPE_H + namespace render { class GizmoRenderer { public: - bool setup(GLFunctions *gapi); + bool setup(GLFunctions *gapi, int screenWidth, int screenHeight); + bool setFont(GLFunctions *gapi, QFile &fontFile, int pixelSize); + void setScreenSize(int w, int h); void clear(); void addLine(const glm::vec3& a, const glm::vec3& b, const glm::vec4& color); void addAABB(const gamelib::BoundingBox& box, const glm::vec4& fillColor, const glm::vec4& lineColor); - void render(GLFunctions *gapi, QOpenGLShaderProgram *shader, GLint cameraProjViewLoc, const glm::mat4 &projView); + void addText(const std::string& text, const glm::vec2& screenPos, float size); + void render(GLFunctions *gapi, + QOpenGLShaderProgram *shader, + GLint cameraProjViewLoc, + const glm::mat4 &projView, + QOpenGLShaderProgram *textShader); private: struct Vertex @@ -31,5 +44,35 @@ namespace render GLuint m_triVao {0}, m_triVbo {0}; std::vector m_lines; std::vector m_tris; + + struct Glyph { + glm::vec2 texCoordMin;// Top-left UV coordinate + glm::vec2 texCoordMax;// Bottom-right UV coordinate + glm::ivec2 size; // Width and height in pixels + glm::ivec2 bearing; // Offset from baseline to glyph origin + GLuint advance; // Horizontal advance to next glyph + }; + + struct TextVertex + { + glm::vec2 pos; + glm::vec2 uv; + glm::vec4 color; + }; + + struct TextGlyph + { + TextVertex verts[6]; + }; + + std::vector m_text; + std::map m_glyphs; + FT_Library m_ft { nullptr }; + FT_Face m_face { nullptr }; + GLuint m_textVao {0}, m_textVbo {0}; + int m_screenW {1}, m_screenH {1}; + + GLuint m_atlasTexture = 0; + QByteArray m_fontData; }; } diff --git a/BMEdit/Editor/Include/Widgets/SceneRenderWidget.h b/BMEdit/Editor/Include/Widgets/SceneRenderWidget.h index a24d222..87dcc3c 100644 --- a/BMEdit/Editor/Include/Widgets/SceneRenderWidget.h +++ b/BMEdit/Editor/Include/Widgets/SceneRenderWidget.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include class QOpenGLFunctions_3_3_Core; @@ -60,7 +62,9 @@ namespace widgets void addGizmoLine(const glm::vec3& a, const glm::vec3& b, const glm::vec4& color) { m_gizmo.addLine(a, b, color); } void addGizmoBox(const gamelib::BoundingBox &box, const glm::vec4 &color, const glm::vec4 &lineColor) { m_gizmo.addAABB(box, color, lineColor); } + void addGizmoText(const std::string& text, const glm::vec2& pos, float size) { m_gizmo.addText(text, pos, size); } void clearGizmos() { m_gizmo.clear(); } + bool setGizmoFont(QFile &file, int pixelSize); signals: void resourcesReady(); void resourceLoadFailed(const QString& reason); @@ -88,6 +92,7 @@ namespace widgets void updateViewLists(); void generateDrawCommands(); void drawScene(); + void drawGizmo(); void generateGizmosForEntity(gamelib::scene::SceneObject* pSceneObject); [[nodiscard]] glm::ivec2 getViewportSize() const { diff --git a/BMEdit/Editor/Resources/BMEdit.qrc b/BMEdit/Editor/Resources/BMEdit.qrc index b1d8078..6ebb68e 100644 --- a/BMEdit/Editor/Resources/BMEdit.qrc +++ b/BMEdit/Editor/Resources/BMEdit.qrc @@ -22,5 +22,9 @@ Shaders/Culling.csh Shaders/Gizmo_GL33.vsh Shaders/Gizmo_GL33.fsh + Shaders/GizmoText_GL33.vsh + Shaders/GizmoText_GL33.fsh + + Fonts/DejaVuSansMono.ttf \ No newline at end of file diff --git a/BMEdit/Editor/Resources/Fonts/DejaVuSansMono.ttf b/BMEdit/Editor/Resources/Fonts/DejaVuSansMono.ttf new file mode 100644 index 0000000..f578602 Binary files /dev/null and b/BMEdit/Editor/Resources/Fonts/DejaVuSansMono.ttf differ diff --git a/BMEdit/Editor/Resources/Shaders/GizmoText_GL33.fsh b/BMEdit/Editor/Resources/Shaders/GizmoText_GL33.fsh new file mode 100644 index 0000000..23590c5 --- /dev/null +++ b/BMEdit/Editor/Resources/Shaders/GizmoText_GL33.fsh @@ -0,0 +1,31 @@ +#version 450 core + +in vec2 fUV; +in vec4 fColor; + +out vec4 FragColor; + +uniform sampler2D textAtlas; +uniform float outlineThickness; + +void main() { + vec4 outlineColor = vec4(0.0); // NOTE: Maybe will pass from CPU as uniform, idk + float distance = texture(textAtlas, fUV).r; + + // Transform distance into world space + float smoothing = fwidth(distance); + + // Calculate alpha for main part + float alpha = smoothstep(0.5 - smoothing, 0.5 + smoothing, distance); + + // Calculate alpha for outline + float outlineDistance = 0.5 - outlineThickness; + float outlineAlpha = smoothstep(outlineDistance - smoothing, outlineDistance + smoothing, distance); + + // Combine all + vec4 textColor = fColor; + vec4 finalColor = mix(outlineColor, textColor, alpha); + finalColor.a = outlineAlpha; + + FragColor = finalColor; +} diff --git a/BMEdit/Editor/Resources/Shaders/GizmoText_GL33.vsh b/BMEdit/Editor/Resources/Shaders/GizmoText_GL33.vsh new file mode 100644 index 0000000..93861bd --- /dev/null +++ b/BMEdit/Editor/Resources/Shaders/GizmoText_GL33.vsh @@ -0,0 +1,12 @@ +#version 450 core +layout(location = 0) in vec2 vPos; +layout(location = 1) in vec2 vUV; +layout(location = 2) in vec4 vColor; +out vec2 fUV; +out vec4 fColor; +void main() +{ + gl_Position = vec4(vPos, 0.0, 1.0); + fUV = vUV; + fColor = vColor; +} diff --git a/BMEdit/Editor/Resources/Shaders/Gizmo_GL33.fsh b/BMEdit/Editor/Resources/Shaders/Gizmo_GL33.fsh index 1648dc7..7b95c75 100644 --- a/BMEdit/Editor/Resources/Shaders/Gizmo_GL33.fsh +++ b/BMEdit/Editor/Resources/Shaders/Gizmo_GL33.fsh @@ -1,4 +1,4 @@ -#version 330 core +#version 450 core in vec4 fColor; out vec4 FragColor; void main() diff --git a/BMEdit/Editor/Resources/Shaders/Gizmo_GL33.vsh b/BMEdit/Editor/Resources/Shaders/Gizmo_GL33.vsh index e513da3..6cdddab 100644 --- a/BMEdit/Editor/Resources/Shaders/Gizmo_GL33.vsh +++ b/BMEdit/Editor/Resources/Shaders/Gizmo_GL33.vsh @@ -1,4 +1,4 @@ -#version 330 core +#version 450 core layout(location = 0) in vec3 vPos; layout(location = 1) in vec4 vColor; uniform mat4 cameraProjView; diff --git a/BMEdit/Editor/Source/Edtor/EditorInstance.cpp b/BMEdit/Editor/Source/Edtor/EditorInstance.cpp index 632060c..40171fc 100644 --- a/BMEdit/Editor/Source/Edtor/EditorInstance.cpp +++ b/BMEdit/Editor/Source/Edtor/EditorInstance.cpp @@ -102,7 +102,7 @@ namespace editor { try #endif { - if (!m_currentLevel->loadSceneData()) + if (!m_currentLevel->loadSceneData(gamelib::LevelLoadCompatibilityLevel::CL_HitmanBloodMoney)) { levelLoadFailed(QString("Unable to load scene data!")); return; diff --git a/BMEdit/Editor/Source/Edtor/TextureProcessor.cpp b/BMEdit/Editor/Source/Edtor/TextureProcessor.cpp index 2bed69f..b9df9cc 100644 --- a/BMEdit/Editor/Source/Edtor/TextureProcessor.cpp +++ b/BMEdit/Editor/Source/Edtor/TextureProcessor.cpp @@ -29,13 +29,15 @@ namespace editor std::memcpy(result.get(), textureEntry.m_mipLevels.at(mipLevel).m_buffer.get(), realWidth * realHeight * 4); return result; } - else if (format == EntryType::ET_BITMAP_DXT1 || format == EntryType::ET_BITMAP_DXT3) + else if (format == EntryType::ET_BITMAP_DXT1 || format == EntryType::ET_BITMAP_DXT3 || format == EntryType::ET_BITMAP_DXT5) { // DXT1, DXT3: Process via squish int flags = 0; flags |= (format == EntryType::ET_BITMAP_DXT1 ? squish::kDxt1 : 0); flags |= (format == EntryType::ET_BITMAP_DXT3 ? squish::kDxt3 : 0); + flags |= (format == EntryType::ET_BITMAP_DXT5 ? squish::kDxt5 : 0); + auto result = std::make_unique(static_cast(realWidth) * static_cast(realHeight) * 4); squish::DecompressImage(result.get(), realWidth, realHeight, textureEntry.m_mipLevels.at(mipLevel).m_buffer.get(), flags); @@ -304,7 +306,8 @@ namespace editor break; case gamelib::tex::TEXEntryType::ET_BITMAP_DXT1: case gamelib::tex::TEXEntryType::ET_BITMAP_DXT3: - // Use libsquish to create DXT1/DXT3 buffers + case gamelib::tex::TEXEntryType::ET_BITMAP_DXT5: + // Use libsquish to create DXT1/DXT3/DXT5 buffers { uint32_t w = sourceMIP.width(); uint32_t h = sourceMIP.height(); @@ -312,6 +315,7 @@ namespace editor flags |= (targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_DXT1 ? squish::kDxt1 : 0); flags |= (targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_DXT3 ? squish::kDxt3 : 0); + flags |= (targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_DXT5 ? squish::kDxt5 : 0); // Calculate & allocate space textureMIP.m_mipLevelSize = squish::GetStorageRequirements(static_cast(w), static_cast(h), flags); diff --git a/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp b/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp index 0c3e4b8..3af0724 100644 --- a/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp +++ b/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp @@ -166,7 +166,7 @@ namespace models { const auto& root = m_level->getLevelLocalization()->localizationRoot; - return root->children.empty() ? 0 : static_cast(root->children.size()); + return !root || root->children.empty() ? 0 : static_cast(root->children.size()); } if (auto node = static_cast(parent.internalPointer())) diff --git a/BMEdit/Editor/Source/Models/SceneTexturesModel.cpp b/BMEdit/Editor/Source/Models/SceneTexturesModel.cpp index 0886b61..59ebade 100644 --- a/BMEdit/Editor/Source/Models/SceneTexturesModel.cpp +++ b/BMEdit/Editor/Source/Models/SceneTexturesModel.cpp @@ -86,6 +86,7 @@ QVariant SceneTexturesModel::data(const QModelIndex &index, int role) const case gamelib::tex::TEXEntryType::ET_BITMAP_U8V8: return "U8V8"; case gamelib::tex::TEXEntryType::ET_BITMAP_DXT1: return "DXT1"; case gamelib::tex::TEXEntryType::ET_BITMAP_DXT3: return "DXT3"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DXT5: return "DXT5"; default: assert(false && "Unknown entry"); return "Unknown"; diff --git a/BMEdit/Editor/Source/Render/GizmoRenderer.cpp b/BMEdit/Editor/Source/Render/GizmoRenderer.cpp index c2d35fe..f0225f3 100644 --- a/BMEdit/Editor/Source/Render/GizmoRenderer.cpp +++ b/BMEdit/Editor/Source/Render/GizmoRenderer.cpp @@ -1,12 +1,17 @@ #include #include +#include #include +static constexpr float kTextOutlineThickness = 0.35f; + namespace render { - bool GizmoRenderer::setup(GLFunctions* gapi) + bool GizmoRenderer::setup(GLFunctions* gapi, int screenWidth, int screenHeight) { if (!gapi) return false; + m_screenW = screenWidth; + m_screenH = screenHeight; gapi->glGenVertexArrays(1, &m_lineVao); gapi->glGenBuffers(1, &m_lineVbo); @@ -30,13 +35,243 @@ namespace render gapi->glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(sizeof(glm::vec3))); gapi->glBindVertexArray(0); + // --- Text rendering init --- + if (FT_Init_FreeType(&m_ft)) + return false; + + gapi->glGenVertexArrays(1, &m_textVao); + gapi->glGenBuffers(1, &m_textVbo); + gapi->glBindVertexArray(m_textVao); + gapi->glBindBuffer(GL_ARRAY_BUFFER, m_textVbo); + gapi->glBufferData(GL_ARRAY_BUFFER, sizeof(TextVertex) * 6, nullptr, GL_DYNAMIC_DRAW); + + gapi->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void *) 0); + gapi->glEnableVertexAttribArray(0); + + gapi->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void *) offsetof(TextVertex, uv)); + gapi->glEnableVertexAttribArray(1); + + gapi->glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(TextVertex), (void *) offsetof(TextVertex, color)); + gapi->glEnableVertexAttribArray(2); + + // Setup gizmo font + QFile fontFile{":/bmedit/gizmo_font_main.ttf"}; + setFont(gapi, fontFile, 24); + return true; } + bool GizmoRenderer::setFont(GLFunctions *gapi, QFile &fontFile, int pixelSize) + { + if (!m_ft || !gapi) + return false; + if (!fontFile.open(QIODevice::ReadOnly)) + return false; + QByteArray fontData = fontFile.readAll(); + fontFile.close(); + + // Store fontData as member variable to prevent deallocation + m_fontData = fontData; + + if (m_face) { + FT_Done_Face(m_face); + m_face = nullptr; + } + + // Clean up existing atlas texture + if (m_atlasTexture != 0) { + gapi->glDeleteTextures(1, &m_atlasTexture); + m_atlasTexture = 0; + } + m_glyphs.clear(); + + // Use stored fontData instead of local variable + if (FT_New_Memory_Face(m_ft, + reinterpret_cast(m_fontData.data()), + static_cast(m_fontData.size()), + 0, &m_face)) + return false; + + if (FT_Set_Pixel_Sizes(m_face, 0, pixelSize)) + return false; + + // Set pixel mode to normal (not SDF) for regular rendering + FT_Int32 load_flags = FT_LOAD_DEFAULT; + FT_Render_Mode render_mode = FT_RENDER_MODE_NORMAL; + + // First pass: calculate required atlas dimensions + struct GlyphInfo { + FT_Bitmap bitmap; + FT_Int bitmap_left; + FT_Int bitmap_top; + FT_Pos advance_x; + std::vector buffer;// Store processed bitmap data + }; + + std::map tempGlyphs; + int maxHeight = 0; + int totalWidth = 0; + int padding = 2;// Padding between glyphs + + // Load and process all glyphs first + for (unsigned char c = 32; c < 128; ++c) { + if (FT_Load_Char(m_face, c, load_flags)) + continue; + if (FT_Render_Glyph(m_face->glyph, render_mode)) + continue; + + FT_Bitmap &bmp = m_face->glyph->bitmap; + GlyphInfo info; + info.bitmap = bmp; + info.bitmap_left = m_face->glyph->bitmap_left; + info.bitmap_top = m_face->glyph->bitmap_top; + info.advance_x = m_face->glyph->advance.x; + + // Process bitmap data based on pixel mode + if (bmp.width > 0 && bmp.rows > 0) { + info.buffer.resize(bmp.width * bmp.rows); + + if (bmp.pixel_mode == FT_PIXEL_MODE_MONO) { + // Convert 1-bit bitmap to 8-bit grayscale + for (unsigned int y = 0; y < bmp.rows; ++y) { + for (unsigned int x = 0; x < bmp.width; ++x) { + unsigned char byte = bmp.buffer[y * bmp.pitch + x / 8]; + unsigned char bit = (byte >> (7 - (x % 8))) & 1; + info.buffer[y * bmp.width + x] = bit ? 255 : 0; + } + } + } + else if (bmp.pixel_mode == FT_PIXEL_MODE_GRAY) { + // Handle pitch properly for grayscale bitmaps + for (unsigned int y = 0; y < bmp.rows; ++y) { + memcpy(&info.buffer[y * bmp.width], + &bmp.buffer[y * bmp.pitch], + bmp.width); + } + } + else { + // Fallback for other formats - fill with white + std::fill(info.buffer.begin(), info.buffer.end(), 255); + } + + totalWidth += bmp.width + padding; + maxHeight = std::max(maxHeight, static_cast(bmp.rows)); + } + + tempGlyphs[c] = std::move(info); + } + + // Calculate atlas dimensions (power of 2 for better GPU compatibility) + int atlasWidth = 1; + while (atlasWidth < totalWidth) atlasWidth <<= 1; + + int atlasHeight = 1; + while (atlasHeight < maxHeight + padding * 2) atlasHeight <<= 1; + + // Create atlas buffer + std::vector atlasBuffer(atlasWidth * atlasHeight, 0); + + // Second pass: pack glyphs into atlas + int currentX = padding; + int currentY = padding; + + for (unsigned char c = 32; c < 128; ++c) { + auto it = tempGlyphs.find(c); + if (it == tempGlyphs.end()) { + // Store empty glyph info for characters that couldn't be loaded + m_glyphs[c] = { + glm::vec2(0.0f, 0.0f), // texCoordMin + glm::vec2(0.0f, 0.0f), // texCoordMax + glm::ivec2(0, 0), // size + glm::ivec2(0, 0), // bearing + static_cast(pixelSize / 2)// advance (rough estimate) + }; + continue; + } + + const GlyphInfo &info = it->second; + + // Check if glyph has actual bitmap data + if (info.bitmap.width == 0 || info.bitmap.rows == 0) { + // Store empty glyph info for spacing (like space character) + m_glyphs[c] = { + glm::vec2(0.0f, 0.0f), // texCoordMin + glm::vec2(0.0f, 0.0f), // texCoordMax + glm::ivec2(0, 0), // size + glm::ivec2(info.bitmap_left, info.bitmap_top),// bearing + static_cast(info.advance_x) // advance + }; + continue; + } + + // Check if we need to wrap to next row (simple left-to-right packing) + if (currentX + info.bitmap.width + padding > atlasWidth) { + currentX = padding; + currentY += maxHeight + padding; + + // Check if we exceed atlas height + if (currentY + info.bitmap.rows > atlasHeight) { + // Atlas too small, this is a fallback - in production you'd want to resize + break; + } + } + + // Copy glyph bitmap to atlas + for (unsigned int y = 0; y < info.bitmap.rows; ++y) { + for (unsigned int x = 0; x < info.bitmap.width; ++x) { + int atlasIndex = (currentY + y) * atlasWidth + (currentX + x); + int glyphIndex = y * info.bitmap.width + x; + if (atlasIndex < atlasBuffer.size() && glyphIndex < info.buffer.size()) { + atlasBuffer[atlasIndex] = info.buffer[glyphIndex]; + } + } + } + + // Calculate texture coordinates (normalized 0-1 range) + float texMinX = static_cast(currentX) / atlasWidth; + float texMinY = static_cast(currentY) / atlasHeight; + float texMaxX = static_cast(currentX + info.bitmap.width) / atlasWidth; + float texMaxY = static_cast(currentY + info.bitmap.rows) / atlasHeight; + + // Store glyph information + m_glyphs[c] = { + glm::vec2(texMinX, texMinY), // texCoordMin + glm::vec2(texMaxX, texMaxY), // texCoordMax + glm::ivec2(info.bitmap.width, info.bitmap.rows),// size + glm::ivec2(info.bitmap_left, info.bitmap_top), // bearing + static_cast(info.advance_x) // advance + }; + + currentX += info.bitmap.width + padding; + } + + // Create the atlas texture + gapi->glGenTextures(1, &m_atlasTexture); + gapi->glBindTexture(GL_TEXTURE_2D, m_atlasTexture); + + gapi->glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, atlasWidth, atlasHeight, + 0, GL_RED, GL_UNSIGNED_BYTE, atlasBuffer.data()); + + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + gapi->glBindTexture(GL_TEXTURE_2D, 0); + return true; + } + + void GizmoRenderer::setScreenSize(int w, int h) + { + m_screenW = w; + m_screenH = h; + } + void GizmoRenderer::clear() { m_lines.clear(); m_tris.clear(); + m_text.clear(); } void GizmoRenderer::addLine(const glm::vec3& a, const glm::vec3& b, const glm::vec4& color) @@ -69,33 +304,200 @@ namespace render tri(1,5,3); tri(3,5,7); int edges[24] = {0,1,1,3,3,2,2,0,4,5,5,7,7,6,6,4,0,4,1,5,2,6,3,7}; for (int i=0;i<24;i+=2) - addLine(p[edges[i]], p[edges[i + 1]], glm::vec4(lineColor.r, lineColor.g, lineColor.b, 1.0f)); + addLine(p[edges[i]], p[edges[i + 1]], glm::vec4(lineColor.r, lineColor.g, lineColor.b, 1.0f)); } - void GizmoRenderer::render(GLFunctions* gapi, QOpenGLShaderProgram *shader, GLint cameraProjViewLoc, const glm::mat4 &projView) - { - if (!gapi || !shader) return; - shader->bind(); - shader->setUniformValue(cameraProjViewLoc, QMatrix4x4(glm::value_ptr(projView)).transposed()); - if (!m_tris.empty()) + static glm::vec4 ParseColor(const std::string &hex) + { + std::string str = hex; + if (!str.empty() && str[0] == '#') + str.erase(0, 1); + + if (str.length() != 6 && str.length() != 8) + return {1.f, 1.f, 1.f, 1.f}; + + uint32_t value = static_cast(std::stoul(str, nullptr, 16)); + + if (str.length() == 6) + { + return { - gapi->glBindVertexArray(m_triVao); - gapi->glBindBuffer(GL_ARRAY_BUFFER, m_triVbo); - gapi->glBufferData(GL_ARRAY_BUFFER, static_cast(sizeof(Vertex) * m_tris.size()), m_tris.data(), GL_DYNAMIC_DRAW); - gapi->glEnable(GL_BLEND); - gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - gapi->glDrawArrays(GL_TRIANGLES, 0, static_cast(m_tris.size())); - gapi->glDisable(GL_BLEND); - } - if (!m_lines.empty()) + static_cast((value >> 16) & 0xFF) / 255.f, + static_cast((value >> 8) & 0xFF) / 255.f, + static_cast((value >> 0) & 0xFF) / 255.f, + 1.f + }; + } + else + { + return { - gapi->glBindVertexArray(m_lineVao); - gapi->glBindBuffer(GL_ARRAY_BUFFER, m_lineVbo); - gapi->glBufferData(GL_ARRAY_BUFFER, static_cast(sizeof(Vertex) * m_lines.size()), m_lines.data(), GL_DYNAMIC_DRAW); - gapi->glDrawArrays(GL_LINES, 0, static_cast(m_lines.size())); - } - gapi->glBindVertexArray(0); - shader->release(); - clear(); - } + static_cast((value >> 24) & 0xFF) / 255.f, + static_cast((value >> 16) & 0xFF) / 255.f, + static_cast((value >> 8) & 0xFF) / 255.f, + static_cast((value >> 0) & 0xFF) / 255.f + }; + } + } + + void GizmoRenderer::addText(const std::string &text, const glm::vec2 &screenPos, float size) + { + glm::vec4 currentColor(1.f, 1.f, 1.f, 1.f); + std::string buffer; + float x = screenPos.x; + float y = screenPos.y; + + auto Flush = [&]() { + float scale = size / 48.f; + for (char ch : buffer) { + auto gIt = m_glyphs.find(ch); + if (gIt == m_glyphs.end()) continue; + const Glyph &g = gIt->second; + + // Skip empty glyphs (like space) but still advance + if (g.size.x == 0 || g.size.y == 0) { + x += (g.advance >> 6) * scale; + continue; + } + + // Calculate glyph position in screen coordinates + // bearing.x: horizontal offset from pen position + // bearing.y: vertical offset from baseline (positive = above baseline) + float xpos = x + g.bearing.x * scale; + float ypos = y - g.bearing.y * scale;// Subtract because screen Y increases downward + float w = g.size.x * scale; + float h = g.size.y * scale; + + // Convert screen coordinates to NDC coordinates + // Screen: (0,0) = top-left, Y increases downward + // NDC: (-1,-1) = bottom-left, Y increases upward + float ndcX0 = (xpos / m_screenW) * 2.f - 1.f; + float ndcY0 = 1.f - (ypos / m_screenH) * 2.f;// Top edge + float ndcX1 = ((xpos + w) / m_screenW) * 2.f - 1.f; + float ndcY1 = 1.f - ((ypos + h) / m_screenH) * 2.f;// Bottom edge + + // Create quad vertices in NDC space + // The key is to match NDC vertex order with texture coordinate order + TextVertex verts[6] = { + // Triangle 1 + {{ndcX0, ndcY0}, {g.texCoordMin.x, g.texCoordMin.y}, currentColor},// top-left + {{ndcX1, ndcY0}, {g.texCoordMax.x, g.texCoordMin.y}, currentColor},// top-right + {{ndcX0, ndcY1}, {g.texCoordMin.x, g.texCoordMax.y}, currentColor},// bottom-left + + // Triangle 2 + {{ndcX0, ndcY1}, {g.texCoordMin.x, g.texCoordMax.y}, currentColor},// bottom-left + {{ndcX1, ndcY0}, {g.texCoordMax.x, g.texCoordMin.y}, currentColor},// top-right + {{ndcX1, ndcY1}, {g.texCoordMax.x, g.texCoordMax.y}, currentColor} // bottom-right + }; + + m_text.push_back({{verts[0], verts[1], verts[2], verts[3], verts[4], verts[5]}}); + x += (g.advance >> 6) * scale; + } + buffer.clear(); + }; + + for (size_t i = 0; i < text.size();) { + if (text[i] == '<') { + if (text.compare(i, 3, "

', end); + ++i; + continue; + } + } + else if (text.compare(i, 4, "

") == 0) { + Flush(); + currentColor = glm::vec4(1.f, 1.f, 1.f, 1.f);// Reset to white + i += 4; + continue; + } + } + buffer += text[i]; + ++i; + } + + // Flush remaining text + if (!buffer.empty()) { + Flush(); + } + } + + void GizmoRenderer::render(GLFunctions *gapi, + QOpenGLShaderProgram *shader, + GLint cameraProjViewLoc, + const glm::mat4 &projView, + QOpenGLShaderProgram *textShader) + { + if (!gapi || !shader) return; + + gapi->glEnable(GL_BLEND); + gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + gapi->glDisable(GL_DEPTH_TEST); + + shader->bind(); + shader->setUniformValue(cameraProjViewLoc, QMatrix4x4(glm::value_ptr(projView)).transposed()); + + if (!m_tris.empty()) + { + gapi->glBindVertexArray(m_triVao); + gapi->glBindBuffer(GL_ARRAY_BUFFER, m_triVbo); + gapi->glBufferData(GL_ARRAY_BUFFER, static_cast(sizeof(Vertex) * m_tris.size()), m_tris.data(), GL_DYNAMIC_DRAW); + gapi->glEnable(GL_BLEND); + gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + gapi->glDrawArrays(GL_TRIANGLES, 0, static_cast(m_tris.size())); + gapi->glDisable(GL_BLEND); + } + + if (!m_lines.empty()) + { + gapi->glBindVertexArray(m_lineVao); + gapi->glBindBuffer(GL_ARRAY_BUFFER, m_lineVbo); + gapi->glBufferData(GL_ARRAY_BUFFER, static_cast(sizeof(Vertex) * m_lines.size()), m_lines.data(), GL_DYNAMIC_DRAW); + gapi->glDrawArrays(GL_LINES, 0, static_cast(m_lines.size())); + } + + gapi->glBindVertexArray(0); + + // Text rendering + if (textShader && !m_text.empty() && m_atlasTexture != 0) { + gapi->glEnable(GL_BLEND); + gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + gapi->glDisable(GL_DEPTH_TEST); + + textShader->bind(); + + gapi->glBindVertexArray(m_textVao); + + // Bind the single atlas texture once + gapi->glActiveTexture(GL_TEXTURE0); + gapi->glBindTexture(GL_TEXTURE_2D, m_atlasTexture); + + // Setup vars + textShader->setUniformValue("outlineThickness", kTextOutlineThickness); + textShader->setUniformValue("textAtlas", 0); + + for (const auto &textQuad : m_text) + { + gapi->glBindBuffer(GL_ARRAY_BUFFER, m_textVbo); + gapi->glBufferData(GL_ARRAY_BUFFER, sizeof(TextVertex) * 6, textQuad.verts, GL_DYNAMIC_DRAW); + gapi->glDrawArrays(GL_TRIANGLES, 0, 6); + } + + gapi->glEnable(GL_DEPTH_TEST); + gapi->glDisable(GL_BLEND); + gapi->glBindVertexArray(0); + gapi->glBindTexture(GL_TEXTURE_2D, 0); + } + + clear(); + } } diff --git a/BMEdit/Editor/Source/Widgets/SceneRenderWidget.cpp b/BMEdit/Editor/Source/Widgets/SceneRenderWidget.cpp index 909d05f..fdabcaa 100644 --- a/BMEdit/Editor/Source/Widgets/SceneRenderWidget.cpp +++ b/BMEdit/Editor/Source/Widgets/SceneRenderWidget.cpp @@ -34,8 +34,11 @@ #include #include #include +#include +#define ON_SCREEN_GIZMO_FONT_SIZE 24.0f + #ifdef BMEDIT_DEBUG void BMEdit_OpenGLMessageCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { @@ -280,6 +283,7 @@ namespace widgets QSharedPointer DefaultShader = nullptr; QSharedPointer CullingShader = nullptr; QSharedPointer GizmoShader = nullptr; + QSharedPointer GizmoTextShader = nullptr; GLuint DefaultShaderUniformLocations[EUniformID::MAX_UNIFORM_INDEX] { 0 }; GLuint CullingShaderUniformLocations[EUniformID::MAX_UNIFORM_INDEX] { 0 }; GLuint GizmoShaderUniformLocations[EUniformID::MAX_UNIFORM_INDEX] { 0 }; @@ -345,16 +349,36 @@ namespace widgets return; } - m_pCommon->setup(); - m_gizmo.setup(m_pCommon->GL); + m_pCommon->setup(); + m_gizmo.setup(m_pCommon->GL, width(), height()); - qDebug() << "Base render stubs are inited"; - } + qDebug() << "Base render stubs are inited"; + } void SceneRenderWidget::paintGL() { - if (!m_pLevel) return; // Do nothing when level not presented yet - if (m_eLoaderState == ELevelLoadState::LLS_FAILED_TO_LOAD) return; // Don't render anything + if (!m_pLevel) + { + addGizmoText("

No selected level

", glm::vec2(45.f), ON_SCREEN_GIZMO_FONT_SIZE); + drawGizmo(); + return;// Do nothing when level not presented yet + } + + if (m_eLoaderState == ELevelLoadState::LLS_FAILED_TO_LOAD) + { + addGizmoText("

Failed to load level

", glm::vec2(45.f), ON_SCREEN_GIZMO_FONT_SIZE); + drawGizmo(); + + return;// Don't render anything + } + + // preapre mainscreen gizmos + QString gizmoText = QString("

Camera %1;%2;%3

") + .arg(m_camera.getPosition().x) + .arg(m_camera.getPosition().y) + .arg(m_camera.getPosition().z); + + addGizmoText(gizmoText.toStdString(), glm::vec2(45.0), ON_SCREEN_GIZMO_FONT_SIZE); if (m_eLoaderState == ELevelLoadState::LLS_READY) { @@ -371,28 +395,28 @@ namespace widgets m_bTransformsDirty = false; } + // Perform commands + drawScene(); + // Fill gizmo - if (m_pSelectedObject != nullptr) - { + if (m_pSelectedObject != nullptr) { generateGizmosForEntity(m_pSelectedObject); } - - // Perform commands - drawScene(); } else if (m_eLoaderState == ELevelLoadState::LLS_NONE) { loadLevelImpl(); } + + // And gizmo itself + drawGizmo(); } void SceneRenderWidget::resizeGL(int w, int h) { - Q_UNUSED(w) - Q_UNUSED(h) - - // Update projection - m_camera.setViewport(w, h); + // Update projection + m_camera.setViewport(w, h); + m_gizmo.setScreenSize(w, h); } void SceneRenderWidget::keyPressEvent(QKeyEvent* event) @@ -563,11 +587,18 @@ namespace widgets // bool isBit4Set = flags & (1 << 4); } - void SceneRenderWidget::resetSelectedObject() - { - m_pSelectedObject = nullptr; - repaint(); - } + void SceneRenderWidget::resetSelectedObject() + { + m_pSelectedObject = nullptr; + repaint(); + } + + bool SceneRenderWidget::setGizmoFont(QFile &file, int pixelSize) + { + if (!m_pCommon) + return false; + return m_gizmo.setFont(m_pCommon->GL, file, pixelSize); + } void SceneRenderWidget::moveCameraTo(const glm::vec3& position) { @@ -1140,44 +1171,57 @@ namespace widgets (const void *) (m_pContext->IndirectDrawNonTransparentCommands * sizeof(IndirectRenderDrawCommand)), static_cast(m_pContext->IndirectDrawNonTransparentCommandsCount), 0 /* stride */); - } - // Stage #4: Gizmo - m_gizmo.render(m_pCommon->GL, - m_pCommon->GizmoShader.get(), - static_cast(m_pCommon->GizmoShaderUniformLocations[RenderCommon::EUniformID::U_CAMERA_PROJ_VIEW]), - m_camera.getProjView()); - // Stage #5: Transparent commands - if (m_pContext->IndirectDrawTransparentCommandsCount) - { + } + + // Stage #4: Transparent commands + if (m_pContext->IndirectDrawTransparentCommandsCount) + { m_pContext->GL->glDepthMask(GL_TRUE); m_pContext->GL->glEnable(GL_BLEND); m_pContext->GL->glBlendFunc(GL_SRC_ALPHA, GL_ONE); m_pContext->GL->glMultiDrawElementsIndirect(GL_TRIANGLES, - GL_UNSIGNED_INT, - (const void *) (m_pContext->IndirectDrawTransparentCommands * sizeof(IndirectRenderDrawCommand)), - static_cast(m_pContext->IndirectDrawTransparentCommandsCount), - 0 /* stride */); + GL_UNSIGNED_INT, + (const void *) (m_pContext->IndirectDrawTransparentCommands * sizeof(IndirectRenderDrawCommand)), + static_cast(m_pContext->IndirectDrawTransparentCommandsCount), + 0 /* stride */); } } + void SceneRenderWidget::drawGizmo() + { + // Stage #5: Gizmo + m_gizmo.render( + m_pCommon->GL, + m_pCommon->GizmoShader.get(), + static_cast(m_pCommon->GizmoShaderUniformLocations[RenderCommon::EUniformID::U_CAMERA_PROJ_VIEW]), + m_camera.getProjView(), + m_pCommon->GizmoTextShader.get()); + } + void SceneRenderWidget::generateGizmosForEntity(gamelib::scene::SceneObject* pSceneObject) { if (auto objectToIndexIt = m_pContext->ObjectToTransformIndex.find(pSceneObject); objectToIndexIt != m_pContext->ObjectToTransformIndex.end()) { // Main gizmo (AABB) const auto &bounds = m_pContext->WorldBoundingBoxes[*objectToIndexIt]; - addGizmoBox(bounds.AsBoundsOrCube(pSceneObject->getPosition(), 50.0f), glm::vec4(0.f, 1.f, 0.0f, 0.15f), glm::vec4(0.f, 1.f, 0.0f, 1.0f)); + auto bbox = bounds.AsBoundsOrCube(pSceneObject->getPosition(), 50.0f); - // PathFollower gizmo - auto pathFolloweIt = std::find_if(pSceneObject->getControllers().begin(), pSceneObject->getControllers().end(), [](const gamelib::scene::SceneObject::Controller& sc) -> bool { - return sc.type->getName() == "ZPathFollower"; - }); + addGizmoBox(bbox, glm::vec4(0.f, 1.f, 0.0f, 0.15f), glm::vec4(0.f, 1.f, 0.0f, 1.0f)); - if (pathFolloweIt != pSceneObject->getControllers().end()) + glm::vec3 topCenter{(bbox.min.x + bbox.max.x) * 0.5f, + bbox.max.y, + (bbox.min.z + bbox.max.z) * 0.5f}; + glm::vec4 clip = m_camera.getProjView() * glm::vec4(topCenter, 1.0f); + if (clip.w > 0.0f) { - // Nice, we have path follower and a lot of gizmos! - // TODO: Extract "m_WayPointLists" property + glm::vec3 ndc = glm::vec3(clip) / clip.w; + float sx = (ndc.x * 0.5f + 0.5f) * width(); + float sy = (1.0f - (ndc.y * 0.5f + 0.5f)) * height(); + std::ostringstream ss; + const auto &pos = pSceneObject->getPosition(); + ss << "

" << pSceneObject->getName() << " (" << pos.x << ' ' << pos.y << ' ' << pos.z << ")

"; + addGizmoText(ss.str(), glm::vec2(sx, sy), ON_SCREEN_GIZMO_FONT_SIZE); } } } @@ -1432,8 +1476,29 @@ namespace widgets { if (!binder.renderStates.empty()) { - materialInstance.ZBiasOffset.x = static_cast(binder.renderStates[0].getZBias()); // NOTE: Need to convert it correctly (see PS2 build, method ZOldDrawPS2::BiasToScale) - materialInstance.ZBiasOffset.y = binder.renderStates[0].getZOffset(); + // exists 0, 1, 2, 3, 6, 7 + // actually, it's not reversed, just random offsets like in ZOldDrawPS2::BiasToScale but PS3, PC, XBox, iOS, Android and PS4 versions has different rendering core + const uint32_t zbias = binder.renderStates[0].getZBias(); + float zoffset = 0.0f; + + switch (zbias) + { + case 0: zoffset = 0.0f; + case 1: zoffset = 8.0f; + case 2: zoffset = 10.0f; + case 3: zoffset = 12.0f; + case 4: zoffset = 14.0f; + case 5: zoffset = 20.0f; + case 6: zoffset = 24.0f; + case 7: zoffset = 30.0f; + case 8: + default: + zoffset = 40.0f; + break; + } + + materialInstance.ZBiasOffset.x = binder.renderStates[0].isEnabled() ? 1.0f : 0.0f; + materialInstance.ZBiasOffset.y = zoffset + binder.renderStates[0].getZOffset(); } if (!binder.textures.empty()) @@ -1831,6 +1896,17 @@ namespace widgets CollectUniforms(&GizmoShaderUniformLocations[0], GizmoShader.get()); } + { + GizmoTextShader.reset(new QOpenGLShaderProgram(nullptr)); + GizmoTextShader->addShaderFromSourceCode(QOpenGLShader::ShaderTypeBit::Vertex, GetContents(":/bmedit/gizmo_text_gl33.vsh")); + GizmoTextShader->addShaderFromSourceCode(QOpenGLShader::ShaderTypeBit::Fragment, GetContents(":/bmedit/gizmo_text_gl33.fsh")); + if (!GizmoTextShader->link()) + { + QMessageBox::critical(nullptr, "Render error", "Failed to link GizmoTextShader!"); + return; + } + } + qDebug() << "GPU: Shaders are ready"; } } \ No newline at end of file diff --git a/BMEdit/Editor/UI/Source/ImportTextureDialog.cpp b/BMEdit/Editor/UI/Source/ImportTextureDialog.cpp index 82c4645..0480746 100644 --- a/BMEdit/Editor/UI/Source/ImportTextureDialog.cpp +++ b/BMEdit/Editor/UI/Source/ImportTextureDialog.cpp @@ -106,6 +106,7 @@ void ImportTextureDialog::resetState() ui->destinationFormatCombo->addItem("U8V8", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_U8V8)); ui->destinationFormatCombo->addItem("DXT1", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_DXT1)); ui->destinationFormatCombo->addItem("DXT3", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_DXT3)); + ui->destinationFormatCombo->addItem("DXT5", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_DXT5)); } // Reset mip level diff --git a/BMEdit/Editor/UI/Source/ViewTexturesDialog.cpp b/BMEdit/Editor/UI/Source/ViewTexturesDialog.cpp index c2052eb..7e6269b 100644 --- a/BMEdit/Editor/UI/Source/ViewTexturesDialog.cpp +++ b/BMEdit/Editor/UI/Source/ViewTexturesDialog.cpp @@ -31,6 +31,7 @@ static QString convertTextureTypeToQString(gamelib::tex::TEXEntryType entry) case gamelib::tex::TEXEntryType::ET_BITMAP_U8V8: return "U8V8"; case gamelib::tex::TEXEntryType::ET_BITMAP_DXT1: return "DXT1"; case gamelib::tex::TEXEntryType::ET_BITMAP_DXT3: return "DXT3"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DXT5: return "DXT5"; } return "Unknown"; diff --git a/BMEdit/GameLib/Include/GameLib/GMS/GMSHeader.h b/BMEdit/GameLib/Include/GameLib/GMS/GMSHeader.h index 3086db9..20b2b17 100644 --- a/BMEdit/GameLib/Include/GameLib/GMS/GMSHeader.h +++ b/BMEdit/GameLib/Include/GameLib/GMS/GMSHeader.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -25,7 +26,7 @@ namespace gamelib::gms [[nodiscard]] const GMSGeomStats &getGeomStats() const; [[nodiscard]] const GMSGroupsCluster &getGeomClusters() const; - static void deserialize(GMSHeader &header, ZBio::ZBinaryReader::BinaryReader *binaryReader, ZBio::ZBinaryReader::BinaryReader *bufFileReader); + static void deserialize(GMSHeader &header, ZBio::ZBinaryReader::BinaryReader *binaryReader, ZBio::ZBinaryReader::BinaryReader *bufFileReader, LevelLoadCompatibilityLevel eLevelCompat); private: static void buildSceneHierarchy(GMSHeader &header); diff --git a/BMEdit/GameLib/Include/GameLib/GMS/GMSReader.h b/BMEdit/GameLib/Include/GameLib/GMS/GMSReader.h index 138fde8..18444a1 100644 --- a/BMEdit/GameLib/Include/GameLib/GMS/GMSReader.h +++ b/BMEdit/GameLib/Include/GameLib/GMS/GMSReader.h @@ -4,6 +4,7 @@ #include #include +#include namespace gamelib::gms @@ -13,7 +14,7 @@ namespace gamelib::gms public: GMSReader(); - bool parse(const GMSHeader *header, const uint8_t *gmsBuffer, int64_t gmsBufferSize, const uint8_t *bufBuffer, int64_t bufBufferSize); + bool parse(const GMSHeader *header, const uint8_t *gmsBuffer, int64_t gmsBufferSize, const uint8_t *bufBuffer, int64_t bufBufferSize, LevelLoadCompatibilityLevel eLevelCompat); private: [[nodiscard]] static std::unique_ptr decompressGmsBuffer(const uint8_t *rawBuffer, uint32_t rawBufferSize, uint32_t uncompressedSize); @@ -21,5 +22,6 @@ namespace gamelib::gms private: const GMSHeader *m_header; + LevelLoadCompatibilityLevel m_eLevelCompat; }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/Level.h b/BMEdit/GameLib/Include/GameLib/Level.h index 2d7f436..c34d794 100644 --- a/BMEdit/GameLib/Include/GameLib/Level.h +++ b/BMEdit/GameLib/Include/GameLib/Level.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -79,7 +80,7 @@ namespace gamelib public: explicit Level(std::unique_ptr &&levelAssetsProvider); - [[nodiscard]] bool loadSceneData(); + [[nodiscard]] bool loadSceneData(LevelLoadCompatibilityLevel eCompatLevel = LevelLoadCompatibilityLevel::CL_Default); [[nodiscard]] const std::string &getLevelName() const; [[nodiscard]] const LevelProperties *getLevelProperties() const; @@ -111,7 +112,7 @@ namespace gamelib private: bool loadLevelProperties(); - bool loadLevelScene(); + bool loadLevelScene(LevelLoadCompatibilityLevel eCompatLevel); bool loadLevelPrimitives(); bool loadLevelTextures(); bool loadLevelMaterials(); diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATBlendMode.h b/BMEdit/GameLib/Include/GameLib/MAT/MATBlendMode.h index 5689c05..fdbc68c 100644 --- a/BMEdit/GameLib/Include/GameLib/MAT/MATBlendMode.h +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATBlendMode.h @@ -14,6 +14,9 @@ namespace gamelib::mat BM_ADD_ON_OPAQUE, BM_ADD, BM_SHADOW, - BM_STATICSHADOW + BM_STATICSHADOW, + + BM_MUL, // Since K&L + BM_SCREEN// Since K&L }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TEX/TEXEntryType.h b/BMEdit/GameLib/Include/GameLib/TEX/TEXEntryType.h index dc37efb..284ce4b 100644 --- a/BMEdit/GameLib/Include/GameLib/TEX/TEXEntryType.h +++ b/BMEdit/GameLib/Include/GameLib/TEX/TEXEntryType.h @@ -24,5 +24,6 @@ namespace gamelib::tex ET_BITMAP_U8V8 = 0x55385638u, //V8U8 ET_BITMAP_DXT1 = 0x44585431u, //DXT1 ET_BITMAP_DXT3 = 0x44585433u, //DXT3 + ET_BITMAP_DXT5 = 0x44585435u, //DXT5 [since K&L] }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/GMS/GMSHeader.cpp b/BMEdit/GameLib/Source/GameLib/GMS/GMSHeader.cpp index 6dfc614..61e3297 100644 --- a/BMEdit/GameLib/Source/GameLib/GMS/GMSHeader.cpp +++ b/BMEdit/GameLib/Source/GameLib/GMS/GMSHeader.cpp @@ -34,7 +34,7 @@ namespace gamelib::gms return m_geomClusters; } - void GMSHeader::deserialize(GMSHeader &header, ZBio::ZBinaryReader::BinaryReader *gmsFileReader, ZBio::ZBinaryReader::BinaryReader *bufFileReader) + void GMSHeader::deserialize(GMSHeader &header, ZBio::ZBinaryReader::BinaryReader *gmsFileReader, ZBio::ZBinaryReader::BinaryReader *bufFileReader, LevelLoadCompatibilityLevel eLevelCompat) { //TODO: https://github.com/ReGlacier/ReHitmanTools/issues/3#issuecomment-769654029 @@ -43,18 +43,33 @@ namespace gamelib::gms BinaryReaderSeekScope rootScope { gmsFileReader }; gmsFileReader->seek(4); // skip first block (Entities) - static constexpr std::array kExpectedSignature = { 0, 0, 4 }; + bool bIsOK = false; std::array sections = { 0, 0, 0 }; gmsFileReader->read(sections.data(), 3); - if (sections != kExpectedSignature) + if (eLevelCompat == LevelLoadCompatibilityLevel::CL_HitmanBloodMoney) + { + // +0x0 - zero + // +0x4 - zero + static constexpr std::array kExpectedSignature = {0, 0, 4}; + bIsOK = sections == kExpectedSignature; + } + if (eLevelCompat == LevelLoadCompatibilityLevel::CL_HitmanContracts) + { + // +0x0 - offset + // +0x4 - offset + bIsOK = sections[2] == 4; + } + + if (!bIsOK) { // Invalid format, stop deserialization throw GMSStructureError("Invalid GMS format: expected 0x4=0, 0x8=0, 0xC=4"); } } + if (eLevelCompat == LevelLoadCompatibilityLevel::CL_HitmanBloodMoney) { // Check physics data tag (for Hitman Blood Money should be 0xFFFFFFFFu) BinaryReaderSeekScope physicsScope { gmsFileReader }; diff --git a/BMEdit/GameLib/Source/GameLib/GMS/GMSReader.cpp b/BMEdit/GameLib/Source/GameLib/GMS/GMSReader.cpp index bfc5e39..3336dbe 100644 --- a/BMEdit/GameLib/Source/GameLib/GMS/GMSReader.cpp +++ b/BMEdit/GameLib/Source/GameLib/GMS/GMSReader.cpp @@ -10,9 +10,10 @@ namespace gamelib::gms { GMSReader::GMSReader() = default; - bool GMSReader::parse(const GMSHeader *header, const uint8_t *gmsBuffer, int64_t gmsBufferSize, const uint8_t *bufBuffer, int64_t bufBufferSize) + bool GMSReader::parse(const GMSHeader *header, const uint8_t *gmsBuffer, int64_t gmsBufferSize, const uint8_t *bufBuffer, int64_t bufBufferSize, LevelLoadCompatibilityLevel eLevelCompat) { m_header = header; + m_eLevelCompat = eLevelCompat; // Read RAW header (first 9 bytes) ZBio::ZBinaryReader::BinaryReader reader(reinterpret_cast(gmsBuffer), gmsBufferSize); @@ -88,7 +89,7 @@ namespace gamelib::gms ZBio::ZBinaryReader::BinaryReader bufBinaryReader { reinterpret_cast(bufBuffer), bufBufferSize }; // Now we have a pure GMS body and we are ready to read all data - GMSHeader::deserialize(*const_cast(m_header), &gmsBinaryReader, &bufBinaryReader); + GMSHeader::deserialize(*const_cast(m_header), &gmsBinaryReader, &bufBinaryReader, m_eLevelCompat); return true; } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/Level.cpp b/BMEdit/GameLib/Source/GameLib/Level.cpp index 049c616..a20fea2 100644 --- a/BMEdit/GameLib/Source/GameLib/Level.cpp +++ b/BMEdit/GameLib/Source/GameLib/Level.cpp @@ -48,47 +48,55 @@ namespace gamelib { } - bool Level::loadSceneData() + bool Level::loadSceneData(LevelLoadCompatibilityLevel eCompatLevel) { if (!m_assetProvider || !m_assetProvider->isValid()) { return false; } - if (!loadLevelProperties()) + if (eCompatLevel == LevelLoadCompatibilityLevel::CL_HitmanBloodMoney) { - return false; - } + if (!loadLevelProperties()) + { + return false; + } + } // otherwise need to fetch properties in loadLevelScene - if (!loadLevelScene()) + if (!loadLevelScene(eCompatLevel)) { return false; } - if (!loadLevelPrimitives()) - { - return false; - } + if (eCompatLevel == LevelLoadCompatibilityLevel::CL_HitmanBloodMoney) { + if (!loadLevelPrimitives()) { + return false; + } - if (!loadLevelTextures()) - { - return false; - } + if (!loadLevelTextures()) { + return false; + } - if (!loadLevelMaterials()) - { - return false; - } + if (!loadLevelMaterials()) { + return false; + } - if (!loadLevelRooms()) - { - return false; - } + if (!loadLevelRooms()) { + return false; + } - if (!loadLevelLocalization()) - { - return false; + if (!loadLevelLocalization()) { + return false; + } + } + else if (eCompatLevel == LevelLoadCompatibilityLevel::CL_HitmanContracts) { + // At least this supported well + if (!loadLevelTextures()) { + return false; + } } + else // unsupported + return false; // TODO: Load things (it's time to combine GMS, PRP & BUF files) m_isLevelLoaded = true; @@ -307,7 +315,7 @@ namespace gamelib return true; } - bool Level::loadLevelScene() + bool Level::loadLevelScene(LevelLoadCompatibilityLevel eCompatLevel) { int64_t gmsFileSize = 0; @@ -325,7 +333,7 @@ namespace gamelib } gms::GMSReader reader; - if (!reader.parse(&m_sceneProperties.header, gmsFileBuffer.get(), gmsFileSize, m_buf.data.get(), m_buf.size)) + if (!reader.parse(&m_sceneProperties.header, gmsFileBuffer.get(), gmsFileSize, m_buf.data.get(), m_buf.size, eCompatLevel)) { return false; } @@ -337,10 +345,9 @@ namespace gamelib m_sceneObjects.resize(entities.size()); // Create objects - for (std::size_t sceneObjectIndex = 0; sceneObjectIndex < entities.size(); ++sceneObjectIndex) - { - scene::SceneObject::Instructions propertyInstructions {}; - auto& currentGeom = entities[sceneObjectIndex]; + for (std::size_t sceneObjectIndex = 0; sceneObjectIndex < entities.size(); ++sceneObjectIndex) { + scene::SceneObject::Instructions propertyInstructions{}; + auto ¤tGeom = entities[sceneObjectIndex]; auto geomTypeId = currentGeom.getTypeId(); auto geomType = TypeRegistry::getInstance().findTypeByHash(geomTypeId); @@ -350,46 +357,31 @@ namespace gamelib geomTypeId, geomType, currentGeom, - propertyInstructions - ); + propertyInstructions); } // Visit properties - using scene::SceneObject; - using scene::SceneObject; - - scene::SceneObjectPropertiesLoader::load(Span(m_sceneObjects), Span(m_levelProperties.rawProperties)); - - // Scene hierarchy setup - for (const auto& sceneObject : m_sceneObjects) + if (eCompatLevel == LevelLoadCompatibilityLevel::CL_HitmanBloodMoney) { - auto parentIndex= sceneObject->getGeomInfo().getParentGeomIndex(); - if (parentIndex == gms::GMSGeomEntity::kInvalidParent) - { - continue; // No parent, probably ROOT - } + using scene::SceneObject; - sceneObject->setParent(m_sceneObjects[parentIndex]); + scene::SceneObjectPropertiesLoader::load(Span(m_sceneObjects), Span(m_levelProperties.rawProperties)); } - -#if 0 //TODO: Remove this code later - std::int32_t lowestPrimId = 0xFFFF; - - for (const auto& sceneObj: m_sceneObjects) + else if (eCompatLevel == LevelLoadCompatibilityLevel::CL_HitmanContracts) { - if (TypeRegistry::canCast<"ZGEOM">(sceneObj->getType())) - { - auto primId = sceneObj->getProperties()["PrimId"][0].getOperand().get(); - - if (primId == 0) - continue; + // Need to visit GMS again, lookup for lExtraData and parse that extra data + // TODO: Support me + } - if (primId < lowestPrimId) - lowestPrimId = primId; + // Scene hierarchy setup + for (const auto &sceneObject : m_sceneObjects) { + auto parentIndex = sceneObject->getGeomInfo().getParentGeomIndex(); + if (parentIndex == gms::GMSGeomEntity::kInvalidParent) { + continue;// No parent, probably ROOT } + + sceneObject->setParent(m_sceneObjects[parentIndex]); } - printf("Found minimal primId: %d\n", lowestPrimId); -#endif } return true; diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATRenderState.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATRenderState.cpp index 6014e81..80e1cc4 100644 --- a/BMEdit/GameLib/Source/GameLib/MAT/MATRenderState.cpp +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATRenderState.cpp @@ -112,6 +112,8 @@ namespace gamelib::mat else if (temp == "ADD") blendMode = MATBlendMode::BM_ADD; else if (temp == "SHADOW") blendMode = MATBlendMode::BM_SHADOW; else if (temp == "STATICSHADOW") blendMode = MATBlendMode::BM_STATICSHADOW; + else if (temp == "MUL") blendMode = MATBlendMode::BM_MUL; + else if (temp == "SCREEN") blendMode = MATBlendMode::BM_SCREEN; else { assert(false && "Unsupported mode!"); diff --git a/BMEdit/GameLib/Source/GameLib/TEX/TEXEntry.cpp b/BMEdit/GameLib/Source/GameLib/TEX/TEXEntry.cpp index 52d1a6f..618cab7 100644 --- a/BMEdit/GameLib/Source/GameLib/TEX/TEXEntry.cpp +++ b/BMEdit/GameLib/Source/GameLib/TEX/TEXEntry.cpp @@ -66,7 +66,7 @@ namespace gamelib::tex type != TEXEntryType::ET_BITMAP_DOT3 && type != TEXEntryType::ET_BITMAP_CUBE && type != TEXEntryType::ET_BITMAP_DMAP && type != TEXEntryType::ET_BITMAP_PAL && type != TEXEntryType::ET_BITMAP_PAL_OPAC && type != TEXEntryType::ET_BITMAP_32 && type != TEXEntryType::ET_BITMAP_U8V8 && - type != TEXEntryType::ET_BITMAP_DXT1 && type != TEXEntryType::ET_BITMAP_DXT3 + type != TEXEntryType::ET_BITMAP_DXT1 && type != TEXEntryType::ET_BITMAP_DXT3 && type != TEXEntryType::ET_BITMAP_DXT5 ) { return false; } diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a33180..99c1d83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ find_package(LibLZMA REQUIRED HINTS ${CMAKE_BINARY_DIR}) find_package(zstd REQUIRED HINTS ${CMAKE_BINARY_DIR}) find_package(libzip REQUIRED HINTS ${CMAKE_BINARY_DIR}) find_package(libsquish REQUIRED HINTS ${CMAKE_BINARY_DIR}) +find_package(Freetype REQUIRED HINTS ${CMAKE_BINARY_DIR}) # --- Global dependencies add_subdirectory(ThirdParty/fmt) diff --git a/README.md b/README.md index e07dcba..1d0b585 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ First of all you need to install [conan](https://conan.io) dependencies manager Download (or git clone) this repository and do ``` conan profile detect --force -conan install . --output-folder=cmake-build-debug --build=missing -s build_type=Debug +conan install . --output-folder=cmake-build-debug --build=missing -s build_type=Debug -s compiler.cppstd=20 ``` (replace `cmake-build-debug` to your ; replace Debug to Release for release build) diff --git a/conanfile.txt b/conanfile.txt index 7e2382c..65c1518 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -2,6 +2,13 @@ zlib/1.2.13 libzip/1.8.0 libsquish/1.15 +freetype/2.13.3 + +[options] +freetype/*:with_png=False +freetype/*:with_brotli=False +freetype/*:with_bzip2=False +freetype/*:with_zlib=False [generators] -CMakeDeps \ No newline at end of file +CMakeDeps