diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f21c1639..204ceb9cbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Compatibility with CMake find_package (#1326, **@RiscadoA**). - A proper Nix package which can be used to install Cubos and Tesseratos (#1327, **RiscadoA**). - Added the option to use Shadow Normal Offset Bias algorithm (#1308, **@GalaxyCrush**) +- UI text element using MSDF for text rendering (#1300, **@mkuritsu**). ### Changed @@ -29,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Duplicated destructor call in AnyVector which caused double free crashes on multiple samples (**@RiscadoA**). - Compiler Error when using -O3 flag (#1351, **@SrGesus**). - Flipped documentation of SystemBuilder::before and SystemBuilder::after (#1371, **@RiscadoA**). +- Made canvas draw calls sorted by layer in order to prevent undeterministic behavior when drawing elements with transparency (**@mkuritsu**). ## [v0.4.0] - 2024-10-13 diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 9a40651a12..b6c0907800 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -116,10 +116,19 @@ set(CUBOS_ENGINE_SOURCE "src/ui/color_rect/color_rect.cpp" "src/ui/image/plugin.cpp" "src/ui/image/image.cpp" + "src/ui/text/plugin.cpp" + "src/ui/text/text_stretch.cpp" + "src/ui/text/text.cpp" "src/fixed_step/plugin.cpp" "src/fixed_step/fixed_delta_time.cpp" + "src/font/plugin.cpp" + "src/font/font.cpp" + "src/font/bridge.cpp" + "src/font/atlas.cpp" + "src/font/atlas_store.cpp" + "src/render/defaults/plugin.cpp" "src/render/defaults/target.cpp" "src/render/shader/plugin.cpp" @@ -285,6 +294,43 @@ target_include_directories(imgui PUBLIC # Finally, link the target we created for both ImGui and Implot target_link_libraries(cubos-engine PUBLIC imgui) +# freetype (msdfgen dependency) - fails to build on windows without this +set(FT_DISABLE_ZLIB ON) +set(FT_DISABLE_BZIP2 ON) +set(FT_DISABLE_PNG ON) +set(FT_DISABLE_HARFBUZZ ON) +set(FT_DISABLE_BROTLI ON) +FetchContent_Declare(freetype + GIT_REPOSITORY "https://gitlab.freedesktop.org/freetype/freetype.git" + GIT_TAG "VER-2-13-3" + SYSTEM + FIND_PACKAGE_ARGS +) +FetchContent_MakeAvailable(freetype) +if (NOT TARGET Freetype::Freetype) + add_library(Freetype::Freetype ALIAS freetype) +endif() +set_property(TARGET freetype PROPERTY POSITION_INDEPENDENT_CODE ON) + +# msdf-atlas-gen +set(MSDF_ATLAS_USE_VCPKG OFF) +set(MSDF_ATLAS_USE_SKIA OFF) +set(MSDF_ATLAS_BUILD_STANDALONE OFF) +set(MSDF_ATLAS_NO_ARTERY_FONT ON) +set(MSDFGEN_DISABLE_PNG ON) +set(MSDFGEN_BUILD_STANDALONE OFF) +FetchContent_Declare(msdf-atlas-gen + GIT_REPOSITORY "https://github.com/Chlumsky/msdf-atlas-gen.git" + SYSTEM + FIND_PACKAGE_ARGS +) +FetchContent_MakeAvailable(msdf-atlas-gen) +# To prevent reallocation errors when compiling engine as a shared library +set_property(TARGET msdf-atlas-gen PROPERTY POSITION_INDEPENDENT_CODE ON) +set_property(TARGET msdfgen-core PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_link_libraries(cubos-engine PRIVATE msdf-atlas-gen) + # ------------------------ Configure tests and samples ------------------------ if(CUBOS_ENGINE_TESTS) diff --git a/engine/assets/font/Roboto-Regular.ttf b/engine/assets/font/Roboto-Regular.ttf new file mode 100644 index 0000000000..2d116d9205 Binary files /dev/null and b/engine/assets/font/Roboto-Regular.ttf differ diff --git a/engine/assets/font/Roboto-Regular.ttf.meta b/engine/assets/font/Roboto-Regular.ttf.meta new file mode 100644 index 0000000000..fa8f7495da --- /dev/null +++ b/engine/assets/font/Roboto-Regular.ttf.meta @@ -0,0 +1,3 @@ +{ + "id": "93cbe82e-9c9b-4c25-aa55-5105c1afd0cc" +} \ No newline at end of file diff --git a/engine/assets/ui/text.fs b/engine/assets/ui/text.fs new file mode 100644 index 0000000000..a71d96d3d3 --- /dev/null +++ b/engine/assets/ui/text.fs @@ -0,0 +1,36 @@ +#version 330 core + +in vec2 texCoord; +out vec4 out_color; +uniform sampler2D fontAtlas; + +const float pxRange = 4.0; + +layout(std140) uniform PerElement +{ + vec2 xRange; + vec2 yRange; + vec4 color; + int depth; +}; + +float screenPxRange() +{ + vec2 unitRange = vec2(pxRange) / vec2(textureSize(fontAtlas, 0)); + vec2 screenTexSize = vec2(1.0) / fwidth(texCoord); + return max(0.5 * dot(unitRange, screenTexSize), 1.0); +} + +float median(float r, float g, float b) +{ + return max(min(r, g), min(max(r, g), b)); +} + +void main() +{ + vec3 msd = texture(fontAtlas, texCoord).rgb; + float sd = median(msd.r, msd.g, msd.b); + float screenPxDistance = screenPxRange() * (sd - 0.5); + float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); + out_color = mix(vec4(0.0), color, opacity); +} diff --git a/engine/assets/ui/text.fs.meta b/engine/assets/ui/text.fs.meta new file mode 100644 index 0000000000..6ff14a6b2b --- /dev/null +++ b/engine/assets/ui/text.fs.meta @@ -0,0 +1,3 @@ +{ + "id": "b5b43fcb-0ec3-4f3a-9e90-a7b0b9978cc5" +} diff --git a/engine/assets/ui/text_element.vs b/engine/assets/ui/text_element.vs new file mode 100644 index 0000000000..1c1f657ff5 --- /dev/null +++ b/engine/assets/ui/text_element.vs @@ -0,0 +1,25 @@ +#version 330 core + +in vec2 in_position; +in vec2 in_texCoord; + +layout(std140) uniform PerElement +{ + vec2 xRange; + vec2 yRange; + vec4 color; + int depth; +}; + +out vec2 texCoord; + +uniform MVP +{ + mat4 mvp; +}; + +void main() +{ + gl_Position = mvp * (vec4(xRange.x, yRange.x, 0, 0) + vec4(in_position, depth, 1)); + texCoord = in_texCoord; +} diff --git a/engine/assets/ui/text_element.vs.meta b/engine/assets/ui/text_element.vs.meta new file mode 100644 index 0000000000..f037a34244 --- /dev/null +++ b/engine/assets/ui/text_element.vs.meta @@ -0,0 +1,3 @@ +{ + "id": "51c11c57-c819-4a51-806c-853178ec686a" +} diff --git a/engine/include/cubos/engine/font/atlas.hpp b/engine/include/cubos/engine/font/atlas.hpp new file mode 100644 index 0000000000..744e7299c1 --- /dev/null +++ b/engine/include/cubos/engine/font/atlas.hpp @@ -0,0 +1,38 @@ +/// @file +/// @brief Struct @ref cubos::engine::FontAtlas. +/// @ingroup font-plugin + +#pragma once + +#include + +#include + +#include +#include + +#include + +namespace cubos::engine +{ + /// @brief Class that holds all the necessary data about a font atlas. This font atlas represents the texure + /// created from all the different glyphs in a font, that will be then used for drawing the text. + /// + /// @ingroup font-plugin + struct CUBOS_ENGINE_API FontAtlas + { + CUBOS_REFLECT; + + FontAtlas(msdfgen::FontHandle* font, core::gl::RenderDevice& rd); + + /// @brief GPU texture containing this font atlas. + cubos::core::gl::Texture2D texture; + + /// @brief Map from unicode characters to their respective glyph geometry data. + std::unordered_map glyphs; + + /// @brief Information about the bitmap used to generate the texture. + msdfgen::BitmapConstRef bitmap; + }; + +} // namespace cubos::engine diff --git a/engine/include/cubos/engine/font/atlas_store.hpp b/engine/include/cubos/engine/font/atlas_store.hpp new file mode 100644 index 0000000000..47bc840254 --- /dev/null +++ b/engine/include/cubos/engine/font/atlas_store.hpp @@ -0,0 +1,37 @@ +/// @file +/// @brief Resource @ref cubos::engine::FontAtlasStore. +/// @ingroup font-plugin + +#pragma once + +#include +#include + +#include + +#include + +#include +#include + +namespace cubos::engine +{ + struct CUBOS_ENGINE_API FontAtlasStore + { + CUBOS_REFLECT; + + std::weak_ptr retrieve(const uuids::uuid& assetId); + + bool contains(const uuids::uuid& assetId) const; + + void store(const uuids::uuid& assetId, const std::shared_ptr& atlas); + + void remove(const uuids::uuid& assetId); + + const std::unordered_map>& map() const; + + private: + std::unordered_map> mAtlas; + }; + +} // namespace cubos::engine diff --git a/engine/include/cubos/engine/font/bridge.hpp b/engine/include/cubos/engine/font/bridge.hpp new file mode 100644 index 0000000000..0695541c43 --- /dev/null +++ b/engine/include/cubos/engine/font/bridge.hpp @@ -0,0 +1,33 @@ +/// @file +/// @brief Class @ref cubos::engine::FontBridge. +/// @ingroup font-plugin + +#pragma once + +#include +#include + +namespace cubos::engine +{ + /// @brief Bridge which loads and saves @ref Font assets. + /// + /// @ingroup font-plugin + class CUBOS_ENGINE_API FontBridge : public FileBridge + { + public: + FontBridge() + : FileBridge(core::reflection::reflect()) + , mFtHandle(msdfgen::initializeFreetype()) + { + } + + ~FontBridge() override; + + protected: + bool loadFromFile(Assets& assets, const AnyAsset& handle, core::memory::Stream& stream) override; + bool saveToFile(const Assets& assets, const AnyAsset& handle, core::memory::Stream& stream) override; + + private: + msdfgen::FreetypeHandle* mFtHandle; + }; +} // namespace cubos::engine diff --git a/engine/include/cubos/engine/font/font.hpp b/engine/include/cubos/engine/font/font.hpp new file mode 100644 index 0000000000..2e8ad3439f --- /dev/null +++ b/engine/include/cubos/engine/font/font.hpp @@ -0,0 +1,30 @@ +/// @file +/// @brief Struct @ref cubos::engine::Font. +/// @ingroup font-plugin + +#pragma once + +#include + +#include +#include + +#include + +namespace cubos::engine +{ + /// @brief Asset containing font data. + /// + /// @ingroup font-plugin + struct CUBOS_ENGINE_API Font + { + CUBOS_REFLECT; + + /// @brief The handle to the loaded font. + msdfgen::FontHandle* fontHandle{nullptr}; + + explicit Font(msdfgen::FreetypeHandle* ft, core::memory::Stream& stream); + Font(Font&& other) noexcept; + ~Font(); + }; +} // namespace cubos::engine diff --git a/engine/include/cubos/engine/font/plugin.hpp b/engine/include/cubos/engine/font/plugin.hpp new file mode 100644 index 0000000000..7b83c3ccd0 --- /dev/null +++ b/engine/include/cubos/engine/font/plugin.hpp @@ -0,0 +1,28 @@ +/// @dir +/// @brief @ref font-plugin plugin directory. + +/// @file +/// @brief Plugin entry point. +/// @ingroup font-plugin + +#pragma once + +#include + +namespace cubos::engine +{ + /// @defgroup font-plugin Font + /// @ingroup engine + /// @brief Adds fonts to @b Cubos using msdfgen. + /// + /// ## Bridges + /// - @ref FontBridge - loads @ref Font assets. + /// + /// ## Dependencies + /// - @ref assets-plugin + + /// @brief Plugin entry function. + /// @param cubos @b Cubos main class. + /// @ingroup font-plugin + CUBOS_ENGINE_API void fontPlugin(Cubos& cubos); +} // namespace cubos::engine diff --git a/engine/include/cubos/engine/ui/text/plugin.hpp b/engine/include/cubos/engine/ui/text/plugin.hpp new file mode 100644 index 0000000000..2ea208e18e --- /dev/null +++ b/engine/include/cubos/engine/ui/text/plugin.hpp @@ -0,0 +1,23 @@ +/// @dir +/// @brief @ref ui-text-plugin plugin directory. + +/// @file +/// @brief Plugin entry point. +/// @ingroup ui-text-plugin + +#pragma once + +#include + +namespace cubos::engine +{ + /// @defgroup ui-text-plugin + /// @ingroup ui-plugins + /// @brief Adds text element to UI. + + /// @brief Plugin entry function. + /// @param cubos @b Cubos main class + /// @ingroup ui-text-plugin + CUBOS_ENGINE_API void uiTextPlugin(Cubos& cubos); + +} // namespace cubos::engine diff --git a/engine/include/cubos/engine/ui/text/text.hpp b/engine/include/cubos/engine/ui/text/text.hpp new file mode 100644 index 0000000000..31964ae1cc --- /dev/null +++ b/engine/include/cubos/engine/ui/text/text.hpp @@ -0,0 +1,48 @@ +/// @file +/// @brief Component @ref cubos::engine::UIText. +/// @ingroup ui-text-plugin + +#pragma once + +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace cubos::engine +{ + /// @brief Component used to draw text on the UI. + /// + /// @ingroup ui-text-plugin + struct CUBOS_ENGINE_API UIText + { + CUBOS_REFLECT; + + /// @brief The text of the element. + std::string text; + + /// @brief The color of the text. + glm::vec4 color{1}; + + /// @brief The font size to draw the characters. + float fontSize{24.0F}; + + /// @brief The font to be used. + Asset font{AnyAsset("93cbe82e-9c9b-4c25-aa55-5105c1afd0cc")}; + + core::gl::VertexArray va{nullptr}; + + std::shared_ptr atlas{nullptr}; + + size_t vertexCount{0}; + }; +} // namespace cubos::engine diff --git a/engine/include/cubos/engine/ui/text/text_stretch.hpp b/engine/include/cubos/engine/ui/text/text_stretch.hpp new file mode 100644 index 0000000000..937840cb2c --- /dev/null +++ b/engine/include/cubos/engine/ui/text/text_stretch.hpp @@ -0,0 +1,19 @@ +/// @file +/// @brief Component @ref cubos::engine::UITextStretch. +/// @ingroup ui-text-plugin + +#pragma once + +#include + +#include + +namespace cubos::engine +{ + /// @brief Component which makes a UI element fit the text it has. + /// @ingroup ui-text-plugin + struct CUBOS_ENGINE_API UITextStretch + { + CUBOS_REFLECT; + }; +} // namespace cubos::engine diff --git a/engine/samples/ui/main.cpp b/engine/samples/ui/main.cpp index 9562fdda67..5c662805b1 100644 --- a/engine/samples/ui/main.cpp +++ b/engine/samples/ui/main.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -22,6 +23,9 @@ #include #include #include +#include +#include +#include #include using namespace cubos::engine; @@ -43,6 +47,8 @@ int main(int argc, char** argv) cubos.plugin(imagePlugin); cubos.plugin(uiImagePlugin); cubos.plugin(colorRectPlugin); + cubos.plugin(fontPlugin); + cubos.plugin(uiTextPlugin); cubos.startupSystem("load and set the Input Bindings") .tagged(assetsTag) @@ -62,7 +68,7 @@ int main(int argc, char** argv) /// [Set up Canvas] /// [Set up Background] auto elementBg = commands.create() - .add(UIElement{}) + .add(UIElement{.layer = 0}) .add(UIHorizontalStretch{20, 20}) .add(UIVerticalStretch{20, 20}) .add(UIColorRect{{1, 1, 1, 1}}) @@ -73,28 +79,42 @@ int main(int argc, char** argv) /// [Set up Panel] auto elementPanel = commands.create() - .add(UIElement{.offset = {-50, 0}, .size = {200, 600}, .pivot = {1, 0.5F}, .anchor = {1, 0.5F}}) + .add(UIElement{ + .offset = {-50, 0}, .size = {200, 600}, .pivot = {1, 0.5F}, .anchor = {1, 0.5F}, .layer = 1}) .add(UIColorRect{{1, 0, 0, 1}}) .entity(); commands.relate(elementPanel, elementBg, ChildOf{}); /// [Set up Panel] /// [Set up Logo] - auto logo = commands.create() - .add(UIElement{.offset = {50, -50}, .size = {200, 200}, .pivot = {0, 1}, .anchor = {0, 1}}) - .add(UIImage{AnyAsset("50423317-a543-4614-9f4e-c2df975f5c0d")}) - .entity(); + auto logo = + commands.create() + .add(UIElement{.offset = {50, -50}, .size = {200, 200}, .pivot = {0, 1}, .anchor = {0, 1}, .layer = 1}) + .add(UIImage{AnyAsset("50423317-a543-4614-9f4e-c2df975f5c0d")}) + .entity(); commands.relate(logo, elementBg, ChildOf{}); /// [Set up Logo] /// [Set up Long Logo] - auto longLogo = commands.create() - .add(UIElement{.offset = {50, 50}, .size = {400, 400}, .pivot = {0, 0}, .anchor = {0, 0}}) - .add(UIImage{AnyAsset("6c24c031-2eac-47c9-be30-485238c3e355")}) - .add(UINativeAspectRatio{}) - .entity(); + auto longLogo = + commands.create() + .add(UIElement{.offset = {50, 50}, .size = {400, 400}, .pivot = {0, 0}, .anchor = {0, 0}, .layer = 1}) + .add(UIImage{AnyAsset("6c24c031-2eac-47c9-be30-485238c3e355")}) + .add(UINativeAspectRatio{}) + .entity(); commands.relate(longLogo, elementBg, ChildOf{}); /// [Set up Long Logo] + + /// [Setup title] + auto title = + commands.create() + .add(UIElement{ + .offset = {0, -20}, .size = {100, 100}, .pivot = {0.5F, 0.5F}, .anchor = {0.5F, 1.0F}, .layer = 2}) + .add(UIText{.text = "cubosengine.org", .color = {0.13, 0.14, 0.15, 1}, .fontSize = 48.0F}) + .add(UITextStretch{}) + .entity(); + commands.relate(title, elementBg, ChildOf{}); + /// [Setup title] }); cubos.system("change scale mode").call([](Commands cmds, const Input& input, Query query) { diff --git a/engine/src/defaults/plugin.cpp b/engine/src/defaults/plugin.cpp index d052f9eb3e..212710cbcd 100644 --- a/engine/src/defaults/plugin.cpp +++ b/engine/src/defaults/plugin.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #include @@ -32,6 +34,7 @@ void cubos::engine::defaultsPlugin(Cubos& cubos) cubos.plugin(assetsPlugin); cubos.plugin(collisionsPlugin); cubos.plugin(windowPlugin); + cubos.plugin(fontPlugin); cubos.plugin(scenePlugin); cubos.plugin(voxelsPlugin); @@ -43,6 +46,7 @@ void cubos::engine::defaultsPlugin(Cubos& cubos) cubos.plugin(uiCanvasPlugin); cubos.plugin(colorRectPlugin); + cubos.plugin(uiTextPlugin); cubos.plugin(gizmosPlugin); cubos.plugin(imguiPlugin); diff --git a/engine/src/font/atlas.cpp b/engine/src/font/atlas.cpp new file mode 100644 index 0000000000..2858792885 --- /dev/null +++ b/engine/src/font/atlas.cpp @@ -0,0 +1,68 @@ +#include + +#include + +#include +#include +#include +#include + +#include + +CUBOS_REFLECT_IMPL(cubos::engine::FontAtlas) +{ + return cubos::core::reflection::Type::create("cubos::engine::FontAtlas"); +} + +cubos::engine::FontAtlas::FontAtlas(msdfgen::FontHandle* font, core::gl::RenderDevice& rd) +{ + std::vector glyphs; + msdf_atlas::FontGeometry fontGeometry(&glyphs); + fontGeometry.loadCharset(font, 1.0, msdf_atlas::Charset::ASCII); + + const double maxCornerAngle = 3.0; + for (msdf_atlas::GlyphGeometry& glyph : glyphs) + { + glyph.edgeColoring(&msdfgen::edgeColoringInkTrap, maxCornerAngle, 0); + } + msdf_atlas::TightAtlasPacker packer; + packer.setDimensionsConstraint(msdf_atlas::DimensionsConstraint::SQUARE); + packer.setMinimumScale(24.0); + packer.setPixelRange(4.0); + packer.setMiterLimit(1.0); + packer.pack(glyphs.data(), static_cast(glyphs.size())); + int width = 0; + int height = 0; + packer.getDimensions(width, height); + + msdf_atlas::ImmediateAtlasGenerator> + generator(width, height); + msdf_atlas::GeneratorAttributes attributes; + generator.setAttributes(attributes); + generator.setThreadCount(4); + generator.generate(glyphs.data(), static_cast(glyphs.size())); + + this->bitmap = generator.atlasStorage(); + for (const msdf_atlas::GlyphGeometry& glyph : glyphs) + { + this->glyphs[glyph.getCodepoint()] = glyph; + } + auto bitmapWidth = static_cast(bitmap.width); + auto bitmapHeight = static_cast(bitmap.height); + std::vector pixels; + pixels.reserve(bitmapWidth * bitmapHeight * 4); + for (size_t i = 0; i < bitmapWidth * bitmapHeight * 3; i += 3) + { + pixels.push_back(bitmap.pixels[i]); + pixels.push_back(bitmap.pixels[i + 1]); + pixels.push_back(bitmap.pixels[i + 2]); + pixels.push_back(255); + } + core::gl::Texture2DDesc desc{.data = {pixels.data()}, + .width = bitmapWidth, + .height = bitmapHeight, + .usage = core::gl::Usage::Default, + .format = core::gl::TextureFormat::RGBA8UNorm}; + this->texture = rd.createTexture2D(desc); +} diff --git a/engine/src/font/atlas_store.cpp b/engine/src/font/atlas_store.cpp new file mode 100644 index 0000000000..1ddc81568d --- /dev/null +++ b/engine/src/font/atlas_store.cpp @@ -0,0 +1,43 @@ +#include + +#include + +#include +#include +#include +#include + +#include +#include + +using namespace cubos::engine; + +CUBOS_REFLECT_IMPL(FontAtlasStore) +{ + return cubos::core::ecs::TypeBuilder("cubos::engine::FontAtlasStore").build(); +} + +std::weak_ptr FontAtlasStore::retrieve(const uuids::uuid& assetId) +{ + return mAtlas[assetId]; +} + +bool FontAtlasStore::contains(const uuids::uuid& assetId) const +{ + return mAtlas.contains(assetId); +} + +void FontAtlasStore::store(const uuids::uuid& assetId, const std::shared_ptr& atlas) +{ + mAtlas[assetId] = atlas; +} + +void FontAtlasStore::remove(const uuids::uuid& assetId) +{ + mAtlas.erase(mAtlas.find(assetId)); +} + +const std::unordered_map>& FontAtlasStore::map() const +{ + return mAtlas; +} diff --git a/engine/src/font/bridge.cpp b/engine/src/font/bridge.cpp new file mode 100644 index 0000000000..558ec99454 --- /dev/null +++ b/engine/src/font/bridge.cpp @@ -0,0 +1,34 @@ +#include + +#include + +#include +#include +#include + +using namespace cubos::engine; + +FontBridge::~FontBridge() +{ + msdfgen::deinitializeFreetype(mFtHandle); +} + +bool FontBridge::loadFromFile(Assets& assets, const AnyAsset& handle, core::memory::Stream& stream) +{ + Font font{mFtHandle, stream}; + if (font.fontHandle == nullptr) + { + return false; + } + assets.store(handle, std::move(font)); + return true; +} + +bool FontBridge::saveToFile(const Assets& assets, const AnyAsset& handle, core::memory::Stream& stream) +{ + (void)assets; + (void)handle; + (void)stream; + CUBOS_ERROR("Saving fonts is currently unsupported!"); + return false; +} diff --git a/engine/src/font/font.cpp b/engine/src/font/font.cpp new file mode 100644 index 0000000000..f14162f305 --- /dev/null +++ b/engine/src/font/font.cpp @@ -0,0 +1,54 @@ +#include +#include +#include +#include +#include + +#include + +CUBOS_REFLECT_IMPL(cubos::engine::Font) +{ + return cubos::core::reflection::Type::create("cubos::engine::Font"); +} + +namespace cubos::engine +{ + Font::Font(msdfgen::FreetypeHandle* ft, core::memory::Stream& stream) + { + if (ft == nullptr) + { + CUBOS_ERROR("Couldn't load font: Invalid Freetype handle"); + return; + } + stream.seek(0, cubos::core::memory::SeekOrigin::End); + size_t streamSize = stream.tell(); + std::vector contents{}; + contents.reserve(streamSize); + stream.seek(0, cubos::core::memory::SeekOrigin::Begin); + if (stream.read(contents.data(), streamSize) < streamSize) + { + CUBOS_ERROR("Couldn't load font: Read less than stream length"); + fontHandle = nullptr; + return; + } + fontHandle = msdfgen::loadFontData(ft, contents.data(), static_cast(streamSize)); + if (fontHandle == nullptr) + { + CUBOS_ERROR("Couldn't load font: Failed to load data"); + } + } + + Font::Font(Font&& other) noexcept + : fontHandle(other.fontHandle) + { + other.fontHandle = nullptr; + } + + Font::~Font() + { + if (fontHandle != nullptr) + { + msdfgen::destroyFont(fontHandle); + } + } +} // namespace cubos::engine diff --git a/engine/src/font/plugin.cpp b/engine/src/font/plugin.cpp new file mode 100644 index 0000000000..c22d40757f --- /dev/null +++ b/engine/src/font/plugin.cpp @@ -0,0 +1,32 @@ +#include + +#include + +#include +#include +#include +#include +#include + +void cubos::engine::fontPlugin(Cubos& cubos) +{ + cubos.depends(assetsPlugin); + + cubos.resource(); + + cubos.startupSystem("setup Font assets bridge").tagged(assetsBridgeTag).call([](Assets& assets) { + auto bridge = std::make_shared(); + assets.registerBridge(".ttf", bridge); + assets.registerBridge(".otf", bridge); + }); + + cubos.system("cleanup atlas store").call([](FontAtlasStore& store) { + for (const auto& [assetId, atlas] : store.map()) + { + if (atlas.expired()) + { + store.remove(assetId); + } + } + }); +} diff --git a/engine/src/ui/canvas/plugin.cpp b/engine/src/ui/canvas/plugin.cpp index 2455487a94..c5b080f105 100644 --- a/engine/src/ui/canvas/plugin.cpp +++ b/engine/src/ui/canvas/plugin.cpp @@ -3,6 +3,7 @@ #include +#include #include #include @@ -38,21 +39,25 @@ namespace cubos::core::gl::DepthStencilState dss; cubos::core::gl::ConstantBuffer mvpBuffer; - State(cubos::core::gl::DepthStencilState depthStencilState, cubos::core::gl::ConstantBuffer mvpConstantBuffer) + cubos::core::gl::BlendState bs; + State(cubos::core::gl::DepthStencilState depthStencilState, cubos::core::gl::ConstantBuffer mvpConstantBuffer, + cubos::core::gl ::BlendState blendState) : dss(std::move(depthStencilState)) , mvpBuffer(std::move(mvpConstantBuffer)) + , bs(std::move(blendState)) { } }; } // namespace static void copyCommands(Query query, UIElement& element, Entity entity, - std::map& commands) + std::map>& commands) { for (size_t i = 0; i < element.drawList.size(); i++) { + int depth = element.layer * 100 + element.hierarchyDepth; auto entry = element.drawList.entry(i); - commands[&entry.type].push(entry.type, entry.command, element.drawList.data(i)); + commands[depth][&entry.type].push(entry.type, entry.command, element.drawList.data(i)); } element.drawList.clear(); for (auto [child, childElement, _] : query.pin(1, entity)) @@ -91,14 +96,26 @@ void cubos::engine::uiCanvasPlugin(Cubos& cubos) cubos.startupSystem("setup canvas state") .after(windowInitTag) .call([](Commands cmds, const core::io::Window& window) { - cubos::core::gl::DepthStencilStateDesc dssd; - dssd.depth.enabled = true; - dssd.depth.writeEnabled = true; - dssd.depth.near = -1.0F; - dssd.depth.compare = Compare::LEqual; + cubos::core::gl::DepthStencilStateDesc dssd = {.depth = {.enabled = false}, .stencil = {.enabled = false}}; + cubos::core::gl::BlendStateDesc bsd = { + .blendEnabled = true, + .color = + { + .src = BlendFactor::SrcAlpha, + .dst = BlendFactor::InvSrcAlpha, + .op = BlendOp::Add, + }, + .alpha = + { + .src = BlendFactor::One, + .dst = BlendFactor::Zero, + .op = BlendOp::Add, + }, + }; cmds.emplaceResource(window->renderDevice().createDepthStencilState(dssd), window->renderDevice().createConstantBuffer(sizeof(glm::mat4), nullptr, - cubos::core::gl::Usage::Dynamic)); + cubos::core::gl::Usage::Dynamic), + window->renderDevice().createBlendState(bsd)); }); cubos.system("scale canvas") @@ -139,7 +156,7 @@ void cubos::engine::uiCanvasPlugin(Cubos& cubos) canvas.virtualSize = canvas.referenceSize; } - canvas.mat = glm::ortho(0, canvas.virtualSize.x, 0, canvas.virtualSize.y); + canvas.mat = glm::ortho(0, canvas.virtualSize.x, 0, canvas.virtualSize.y, -1000.0F, 1000.0F); } }); @@ -256,7 +273,6 @@ void cubos::engine::uiCanvasPlugin(Cubos& cubos) window->renderDevice().clearColor(0.0F, 0.0F, 0.0F, 0.0F); target.cleared = true; } - window->renderDevice().clearDepth(1); } }); @@ -275,6 +291,7 @@ void cubos::engine::uiCanvasPlugin(Cubos& cubos) const State& state) { // For each render target with a UI canvas. window->renderDevice().setDepthStencilState(state.dss); + window->renderDevice().setBlendState(state.bs); for (auto [entity, canvas, renderTarget] : canvasQuery) { @@ -284,42 +301,44 @@ void cubos::engine::uiCanvasPlugin(Cubos& cubos) state.mvpBuffer->fill(&canvas.mat, sizeof(glm::mat4)); // Extract draw commands from all children UI elements, recursively. - std::map commands; + std::map> layerCommands; for (auto [child, childElement, _] : drawsToQuery.pin(1, entity)) { - copyCommands(childrenQuery, childElement, child, commands); + copyCommands(childrenQuery, childElement, child, layerCommands); } - // For each draw command type. - for (const auto& [type, list] : commands) + for (const auto& [layer, commands] : layerCommands) { - - // Prepare state common to all instances of this draw command type. - window->renderDevice().setShaderPipeline(type->pipeline); - type->pipeline->getBindingPoint("MVP")->bind(state.mvpBuffer); - type->constantBufferBindingPoint->bind(type->constantBuffer); - - // For all instances of this draw command type. - for (size_t i = 0; i < list.size(); i++) + // For each draw command type. + for (const auto& [type, list] : commands) { - // Prepare command-specific state and issue draw call. - auto entry = list.entry(i); - window->renderDevice().setVertexArray(entry.command.vertexArray); + // Prepare state common to all instances of this draw command type. + window->renderDevice().setShaderPipeline(type->pipeline); + type->pipeline->getBindingPoint("MVP")->bind(state.mvpBuffer); + type->constantBufferBindingPoint->bind(type->constantBuffer); - type->constantBuffer->fill(list.data(i), type->perElementSize); - - for (size_t j = 0; j < UIDrawList::Type::MaxTextures; j++) + // For all instances of this draw command type. + for (size_t i = 0; i < list.size(); i++) { - if (type->texBindingPoint[j] == nullptr) + // Prepare command-specific state and issue draw call. + auto entry = list.entry(i); + window->renderDevice().setVertexArray(entry.command.vertexArray); + + type->constantBuffer->fill(list.data(i), type->perElementSize); + + for (size_t j = 0; j < UIDrawList::Type::MaxTextures; j++) { - continue; + if (type->texBindingPoint[j] == nullptr) + { + continue; + } + type->texBindingPoint[j]->bind(entry.command.textures[j]); + type->texBindingPoint[j]->bind(entry.command.samplers[j]); } - type->texBindingPoint[j]->bind(entry.command.textures[j]); - type->texBindingPoint[j]->bind(entry.command.samplers[j]); + window->renderDevice().drawTriangles(entry.command.vertexOffset, entry.command.vertexCount); } - window->renderDevice().drawTriangles(entry.command.vertexOffset, entry.command.vertexCount); } } } }); -} \ No newline at end of file +} diff --git a/engine/src/ui/text/plugin.cpp b/engine/src/ui/text/plugin.cpp new file mode 100644 index 0000000000..a4e76972c9 --- /dev/null +++ b/engine/src/ui/text/plugin.cpp @@ -0,0 +1,202 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using cubos::core::io::Window; + +using namespace cubos::engine; + +namespace +{ + struct PerElement + { + glm::vec2 xRange; + glm::vec2 yRange; + glm::vec4 color; + int depth; + }; + + struct State + { + CUBOS_ANONYMOUS_REFLECT(State); + + UIDrawList::Type drawType; + Sampler sampler; + + State(RenderDevice& renderDevice, const ShaderPipeline& pipeline) + { + SamplerDesc sd = {.minFilter = TextureFilter::Linear, .magFilter = TextureFilter::Linear}; + sampler = renderDevice.createSampler(sd); + + drawType.constantBuffer = renderDevice.createConstantBuffer(sizeof(PerElement), nullptr, Usage::Dynamic); + drawType.texBindingPoint[0] = pipeline->getBindingPoint("fontAtlas"); + drawType.constantBufferBindingPoint = pipeline->getBindingPoint("PerElement"); + drawType.perElementSize = sizeof(PerElement); + drawType.pipeline = pipeline; + } + }; +} // namespace + +void cubos::engine::uiTextPlugin(Cubos& cubos) +{ + static const Asset VertexShader = AnyAsset("51c11c57-c819-4a51-806c-853178ec686a"); + static const Asset PixelShader = AnyAsset("b5b43fcb-0ec3-4f3a-9e90-a7b0b9978cc5"); + + cubos.component(); + cubos.component(); + + cubos.depends(uiCanvasPlugin); + cubos.depends(windowPlugin); + cubos.depends(shaderPlugin); + cubos.depends(assetsPlugin); + cubos.depends(fontPlugin); + + cubos.uninitResource(); + + cubos.startupSystem("setup UI text") + .tagged(assetsTag) + .after(windowInitTag) + .call([](Commands cmds, const Window& window, const Assets& assets) { + auto& rd = window->renderDevice(); + auto vs = assets.read(VertexShader)->shaderStage(); + auto ps = assets.read(PixelShader)->shaderStage(); + cmds.emplaceResource(rd, rd.createShaderPipeline(vs, ps)); + }); + + cubos.system("load UI text atlas") + .call([](const Window& window, const Assets& assets, FontAtlasStore& atlasStore, Query query) { + for (auto [uiText] : query) + { + if (uiText.atlas == nullptr) + { + auto fontAsset = assets.load(uiText.font); + uuids::uuid assetId = fontAsset.getId().value(); + if (atlasStore.contains(assetId)) + { + uiText.atlas = std::shared_ptr{atlasStore.retrieve(assetId)}; + } + else + { + auto font = assets.read(uiText.font); + uiText.atlas = std::make_shared(font->fontHandle, window->renderDevice()); + atlasStore.store(assetId, uiText.atlas); + } + } + } + }); + + cubos.system("load UI text vertex array") + .call([](const Window& window, const State& state, + Query> query) { + for (auto [element, uiText, optStretch] : query) + { + if (uiText.va == nullptr && uiText.atlas != nullptr) + { + std::size_t vertexCount = uiText.text.size() * 6; + uiText.vertexCount = vertexCount; + float xPos = 0; + float yPos = 0; + + VertexArrayDesc vad; + vad.elementCount = 2; + vad.elements[0].name = "in_position"; + vad.elements[0].type = cubos::core::gl::Type::Float; + vad.elements[0].size = 2; + vad.elements[0].buffer.index = 0; + vad.elements[0].buffer.offset = 0; + vad.elements[0].buffer.stride = 4 * sizeof(float); + vad.elements[1].name = "in_texCoord"; + vad.elements[1].type = cubos::core::gl::Type::Float; + vad.elements[1].size = 2; + vad.elements[1].buffer.index = 0; + vad.elements[1].buffer.offset = 2 * sizeof(float); + vad.elements[1].buffer.stride = 4 * sizeof(float); + vad.shaderPipeline = state.drawType.pipeline; + + std::vector verts; + verts.reserve(4 * vertexCount); + /// @todo make add support for Unicode + /// @note This repeats a lot of vertices because currently the UI system does not support drawing + /// Indexed, something that needs to be worked on + for (char c : uiText.text) + { + auto uChar = static_cast(c); + const msdf_atlas::GlyphGeometry& glyph = uiText.atlas->glyphs[uChar]; + auto advance = static_cast(glyph.getAdvance() * uiText.fontSize); + if (c == ' ') + { + xPos += advance; + continue; + } + double left; + double bottom; + double right; + double top; + glyph.getQuadAtlasBounds(left, bottom, right, top); + auto texCoordLeft = static_cast(left / uiText.atlas->bitmap.width); + auto texCoordBottom = static_cast(bottom / uiText.atlas->bitmap.height); + auto texCoordRight = static_cast(right / uiText.atlas->bitmap.width); + auto texCoordTop = static_cast(top / uiText.atlas->bitmap.height); + + glyph.getQuadPlaneBounds(left, bottom, right, top); + auto posLeft = static_cast(left * uiText.fontSize + xPos); + auto posBottom = static_cast(bottom * uiText.fontSize + yPos); + auto posRight = static_cast(right * uiText.fontSize + xPos); + auto posTop = static_cast(top * uiText.fontSize + yPos); + + std::array charVerts{ + posLeft, posBottom, texCoordLeft, texCoordBottom, posRight, posBottom, + texCoordRight, texCoordBottom, posLeft, posTop, texCoordLeft, texCoordTop, + posLeft, posTop, texCoordLeft, texCoordTop, posRight, posBottom, + texCoordRight, texCoordBottom, posRight, posTop, texCoordRight, texCoordTop}; + /// @todo maybe possible to optimize with a memcpy, instead of iterating/pushing_back? + for (float f : charVerts) + { + verts.push_back(f); + } + xPos += advance; + } + vad.buffers[0] = window->renderDevice().createVertexBuffer(verts.size() * sizeof(float), + verts.data(), Usage::Default); + uiText.va = window->renderDevice().createVertexArray(vad); + + if (optStretch.contains()) + { + element.size = {xPos, uiText.fontSize}; + } + } + } + }); + + cubos.system("draw UI text").tagged(uiDrawTag).call([](const State& state, Query query) { + for (auto [element, uiText] : query) + { + if (uiText.va != nullptr) + { + glm::vec2 min = element.position - element.size * element.pivot; + glm::vec2 max = element.position + element.size * (glm::vec2(1.0F, 1.0F) - element.pivot); + element + .draw( + state.drawType, uiText.va, 0, uiText.vertexCount, + PerElement{ + {min.x, max.x}, {min.y, max.y}, uiText.color, element.layer * 100 + element.hierarchyDepth}) + .withTexture(0, uiText.atlas->texture, state.sampler); + } + } + }); +} diff --git a/engine/src/ui/text/text.cpp b/engine/src/ui/text/text.cpp new file mode 100644 index 0000000000..8701a40721 --- /dev/null +++ b/engine/src/ui/text/text.cpp @@ -0,0 +1,19 @@ +#include +#include +#include +#include +#include + +#include + +using namespace cubos::engine; + +CUBOS_REFLECT_IMPL(UIText) +{ + return core::ecs::TypeBuilder("cubos::engine::UIText") + .withField("text", &UIText::text) + .withField("color", &UIText::color) + .withField("fontSize", &UIText::fontSize) + .withField("font", &UIText::font) + .build(); +} diff --git a/engine/src/ui/text/text_stretch.cpp b/engine/src/ui/text/text_stretch.cpp new file mode 100644 index 0000000000..b639a5ecf2 --- /dev/null +++ b/engine/src/ui/text/text_stretch.cpp @@ -0,0 +1,8 @@ +#include + +#include + +CUBOS_REFLECT_IMPL(cubos::engine::UITextStretch) +{ + return cubos::core::ecs::TypeBuilder("cubos::engine::UITextStretch").build(); +}