From 9d58c8c9473e7cd909873850767b6e05bf1c7635 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Sat, 7 Jun 2025 11:00:13 +0900 Subject: [PATCH 01/30] rework text instance and type setting --- CMakeLists.txt | 2 + include/limitless/core/texture/texture.hpp | 3 + include/limitless/core/vertex.hpp | 17 +- include/limitless/core/vertex_array.hpp | 1 + include/limitless/models/text_model.hpp | 16 +- .../limitless/models/text_selection_model.hpp | 23 ++ include/limitless/text/font_atlas.hpp | 40 +- include/limitless/text/text_instance.hpp | 41 +- include/limitless/text/type_setter.hpp | 91 +++++ shaders/text/icon_text.frag | 12 + shaders/text/icon_text.vert | 19 + shaders/text/text.frag | 6 +- shaders/text/text.vert | 3 + src/limitless/core/profiler.cpp | 9 +- src/limitless/core/texture/texture.cpp | 102 +++++ src/limitless/core/vertex_array.cpp | 8 +- src/limitless/models/text_model.cpp | 2 +- src/limitless/models/text_selection_model.cpp | 42 ++ src/limitless/shader_storage.cpp | 1 + src/limitless/text/font_atlas.cpp | 374 ++++++++++++------ src/limitless/text/text_instance.cpp | 85 ++-- src/limitless/text/type_setter.cpp | 291 ++++++++++++++ 22 files changed, 1003 insertions(+), 185 deletions(-) create mode 100644 include/limitless/models/text_selection_model.hpp create mode 100644 include/limitless/text/type_setter.hpp create mode 100644 shaders/text/icon_text.frag create mode 100644 shaders/text/icon_text.vert create mode 100644 src/limitless/models/text_selection_model.cpp create mode 100644 src/limitless/text/type_setter.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 21b51626..df0aae97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,6 +101,7 @@ set(ENGINE_LOADERS set(ENGINE_MODELS src/limitless/models/elementary_model.cpp src/limitless/models/text_model.cpp + src/limitless/models/text_selection_model.cpp src/limitless/models/skeletal_model.cpp src/limitless/models/abstract_model.cpp src/limitless/models/cube.cpp @@ -141,6 +142,7 @@ set(ENGINE_MS set(ENGINE_TEXT src/limitless/text/text_instance.cpp src/limitless/text/font_atlas.cpp + src/limitless/text/type_setter.cpp ) set(ENGINE_FX diff --git a/include/limitless/core/texture/texture.hpp b/include/limitless/core/texture/texture.hpp index 040008d1..7e4588a8 100644 --- a/include/limitless/core/texture/texture.hpp +++ b/include/limitless/core/texture/texture.hpp @@ -131,6 +131,8 @@ namespace Limitless { bool mipmap {false}; bool compressed {false}; bool immutable {false}; + + size_t getBytesPerPixel() const noexcept; protected: Texture() = default; public: @@ -164,6 +166,7 @@ namespace Limitless { [[nodiscard]] auto isCubemapArray() const noexcept { return target == Type::TexCubeMapArray; } [[nodiscard]] uint32_t getId() const noexcept; [[nodiscard]] auto& getExtensionTexture() noexcept { return *texture; } + [[nodiscard]] std::vector getPixels() noexcept; Texture& setMinFilter(Filter filter); Texture& setMagFilter(Filter filter); diff --git a/include/limitless/core/vertex.hpp b/include/limitless/core/vertex.hpp index cb1a3588..2a1ff085 100644 --- a/include/limitless/core/vertex.hpp +++ b/include/limitless/core/vertex.hpp @@ -14,14 +14,27 @@ namespace Limitless { struct TextVertex { glm::vec2 position; glm::vec2 uv; + glm::vec4 color; - TextVertex(glm::vec2 _position, glm::vec2 _uv) noexcept + TextVertex(glm::vec2 _position, glm::vec2 _uv, glm::vec4 _color) noexcept : position{_position} - , uv{_uv} {} + , uv{_uv} + , color{_color} + {} TextVertex() = default; }; + struct TextSelectionVertex { + glm::vec2 position; + + TextSelectionVertex(glm::vec2 _position) noexcept + : position{_position} + {} + + TextSelectionVertex() = default; + }; + struct VertexNormal { glm::vec3 position; glm::vec3 normal; diff --git a/include/limitless/core/vertex_array.hpp b/include/limitless/core/vertex_array.hpp index 5b947e5a..0b45be93 100644 --- a/include/limitless/core/vertex_array.hpp +++ b/include/limitless/core/vertex_array.hpp @@ -75,6 +75,7 @@ namespace Limitless { VertexArray& operator<<(const std::pair&>& attribute) noexcept; VertexArray& operator<<(const std::pair&>& attribute) noexcept; VertexArray& operator<<(const std::pair&>& attribute) noexcept; + VertexArray& operator<<(const std::pair&>& attribute) noexcept; }; void swap(VertexArray& lhs, VertexArray& rhs); diff --git a/include/limitless/models/text_model.hpp b/include/limitless/models/text_model.hpp index aef3bc17..efa7da6a 100644 --- a/include/limitless/models/text_model.hpp +++ b/include/limitless/models/text_model.hpp @@ -2,22 +2,24 @@ #include #include #include +#include namespace Limitless { class Buffer; class TextModel { - private: - VertexArray vertex_array; - std::shared_ptr buffer; - std::vector vertices; - - void initialize(size_t count); public: explicit TextModel(std::vector&& vertices); explicit TextModel(size_t count); void update(std::vector&& vertices); void draw() const; + + private: + VertexArray vertex_array; + std::shared_ptr buffer; + std::vector vertices; + + void initialize(size_t count); }; -} \ No newline at end of file +} diff --git a/include/limitless/models/text_selection_model.hpp b/include/limitless/models/text_selection_model.hpp new file mode 100644 index 00000000..207be3ed --- /dev/null +++ b/include/limitless/models/text_selection_model.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +namespace Limitless { + class TextSelectionModel { + private: + VertexArray vertex_array; + std::shared_ptr buffer; + std::vector vertices; + + void initialize(size_t count); + public: + explicit TextSelectionModel(std::vector&& vertices); + explicit TextSelectionModel(size_t count); + + void update(std::vector&& vertices); + void draw() const; + }; +} \ No newline at end of file diff --git a/include/limitless/text/font_atlas.hpp b/include/limitless/text/font_atlas.hpp index d3c0cd42..c92b313b 100644 --- a/include/limitless/text/font_atlas.hpp +++ b/include/limitless/text/font_atlas.hpp @@ -37,6 +37,11 @@ namespace Limitless { * UVs of this character glyph on font atlas texture. */ std::array uvs; + + /** + * Whether this character is an icon, meaning that it does not get colored. + */ + bool is_icon; }; struct font_error : public std::runtime_error { @@ -46,13 +51,32 @@ namespace Limitless { class FontAtlas { public: - FontAtlas(const fs::path& path, uint32_t pixel_size); + static std::shared_ptr load( + const fs::path& path, + uint32_t pixel_size + ); + + static std::shared_ptr make( + uint32_t font_size_in_pixels, + std::unordered_map> icons + ); + + FontAtlas( + std::unordered_map chars, + std::shared_ptr texture, + uint32_t pixel_size, + bool is_icon + ); + + FontAtlas(const FontAtlas&) = delete; + FontAtlas& operator=(const FontAtlas&) = delete; + ~FontAtlas(); /** * Return font vertical size in pixels. */ - [[nodiscard]] auto getFontSize() const noexcept { return font_size; } + [[nodiscard]] auto getFontSize() const noexcept { return pixel_size; } /** * Return font character for given Unicode codepoint. @@ -62,10 +86,12 @@ namespace Limitless { [[nodiscard]] const auto& getTexture() const { return texture; } + [[nodiscard]] auto isIconAtlas() const noexcept { return is_icon; } + /** * Return vertices for UTF-8 encoded string. */ - [[nodiscard]] std::vector generate(const std::string& text) const; + [[nodiscard]] std::vector generate(const std::string& text, const glm::vec4& color = glm::vec4(1.0f)) const; /** * Return bounding box for UTF-8 encoded string. @@ -75,14 +101,12 @@ namespace Limitless { /** * Return selection geometry for UTF-8 encoded string, from [@begin; @end) range position runes. */ - std::vector getSelectionGeometry(std::string_view text, size_t begin, size_t end) const; + [[nodiscard]] std::vector getSelectionGeometry(std::string_view text, size_t begin, size_t end) const; private: std::unordered_map chars; std::shared_ptr texture; - FT_Face face {}; - uint32_t font_size; - - static constexpr auto TAB_WIDTH_IN_SPACES = 4; + uint32_t pixel_size; + bool is_icon; }; } diff --git a/include/limitless/text/text_instance.hpp b/include/limitless/text/text_instance.hpp index e6ce14ab..71739e1f 100644 --- a/include/limitless/text/text_instance.hpp +++ b/include/limitless/text/text_instance.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include @@ -11,28 +13,33 @@ namespace Limitless { class TextInstance { private: - TextModel text_model; - std::string text; - glm::vec2 position; + struct FontTextModel { + TextModel text_model; + std::shared_ptr font_atlas; + }; + + std::vector font_text_models; + std::optional selection_model {std::nullopt}; + + std::vector formatted_text_parts; + glm::vec2 position {0.0f}; glm::vec4 color {1.0f}; glm::vec2 size {1.0f}; - std::shared_ptr font; + std::pair bounding_box {glm::vec2(0.f),glm::vec2(0.f)}; - std::optional selection_model {}; glm::vec4 selection_color {}; - bool hidden {}; + bool hidden {false}; glm::mat4 model_matrix {1.0f}; void calculateModelMatrix() noexcept; public: - TextInstance(std::string text, const glm::vec2& position, std::shared_ptr font); - TextInstance(size_t count, const glm::vec2& position, std::shared_ptr font); + TextInstance(std::vector formatted_text_parts, const glm::vec2& position); void hide() noexcept { hidden = true; } void reveal() noexcept { hidden = false; } - TextInstance& setText(std::string text); - TextInstance& setColor(const glm::vec4& color) noexcept; + TextInstance& setText(std::string text, TextFormat text_format); + TextInstance& setText(std::vector new_formatted_text_parts); TextInstance& setPosition(const glm::vec2& position) noexcept; TextInstance& setSize(const glm::vec2& size) noexcept; TextInstance& setSelectionColor(const glm::vec4& color) noexcept; @@ -45,6 +52,18 @@ namespace Limitless { [[nodiscard]] auto isHidden() const noexcept { return hidden; } [[nodiscard]] auto isVisible() const noexcept { return !hidden; } + /** + * In model space. + */ + [[nodiscard]] const auto getBoundingBox() const noexcept { + return std::make_pair( + bounding_box.first * size, + bounding_box.second * size + ); + } + + [[nodiscard]] glm::vec2 getBoundingBoxDimensions() const noexcept; + void draw(Context& context, const Assets& assets); }; -} \ No newline at end of file +} diff --git a/include/limitless/text/type_setter.hpp b/include/limitless/text/type_setter.hpp new file mode 100644 index 00000000..aa577124 --- /dev/null +++ b/include/limitless/text/type_setter.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace Limitless { + class FontAtlas; + + struct TextFormat { + glm::vec4 color; + std::shared_ptr font; + std::optional wrap_width; + + TextFormat(glm::vec4 _color, std::shared_ptr _font, std::optional _wrap_width) + : color (_color) + , font (_font) + , wrap_width (_wrap_width) + { + } + + friend bool operator==(const TextFormat& lhs, const TextFormat& rhs) { + return lhs.color == rhs.color + && lhs.font.get() == rhs.font.get() + && lhs.wrap_width == rhs.wrap_width; + } + + friend bool operator!=(const TextFormat& lhs, const TextFormat& rhs) { + return !(lhs == rhs); + } + }; + + struct FormattedText { + std::string text; + TextFormat format; + + FormattedText(std::string _text, TextFormat _text_format) + : text (std::move(_text)) + , format (std::move(_text_format)) + { + } + + friend bool operator==(const FormattedText& a, const FormattedText& b) { + return a.text == b.text && a.format == b.format; + } + }; + + struct FontVertices { + std::shared_ptr font; + std::vector vertices; + + FontVertices(std::shared_ptr _font, std::vector _vertices) + : font (std::move(_font)) + , vertices (std::move(_vertices)) + { + + } + }; + + struct TypeSetResult { + std::vector vertices_of_fonts; + std::pair bounding_box; + + TypeSetResult(std::vector _vertices_of_fonts, std::pair _bounding_box) + : vertices_of_fonts(std::move(_vertices_of_fonts)) + , bounding_box(std::move(_bounding_box)) + { + + } + }; + + class TypeSetter { + public: + static TypeSetResult + typeSet( + const std::vector& formatted_text_parts + ); + + static std::vector typeSetSelection( + const std::vector& formatted_text_parts, + size_t start_codepoint_index, + size_t end_codepoint_index + ); + + static glm::vec2 getBoundingBoxSize(const std::vector& formatted_text_parts); + }; +} diff --git a/shaders/text/icon_text.frag b/shaders/text/icon_text.frag new file mode 100644 index 00000000..a20daa66 --- /dev/null +++ b/shaders/text/icon_text.frag @@ -0,0 +1,12 @@ +ENGINE::COMMON + +in vec2 vs_uv; +in vec4 vs_color; + +out vec4 FragColor; + +uniform sampler2D bitmap; + +void main() { + FragColor = vs_color * texture(bitmap, vs_uv); +} \ No newline at end of file diff --git a/shaders/text/icon_text.vert b/shaders/text/icon_text.vert new file mode 100644 index 00000000..f6bccc0c --- /dev/null +++ b/shaders/text/icon_text.vert @@ -0,0 +1,19 @@ +ENGINE::COMMON + +layout (location = 0) in vec2 position; +layout (location = 1) in vec2 uv; +layout (location = 2) in vec4 color; + +out vec2 vs_uv; +out vec4 vs_color; + +uniform mat4 model; + +uniform mat4 proj; + +void main() +{ + gl_Position = proj * model * vec4(position, 0.0, 1.0); + vs_uv = uv; + vs_color = color; +} diff --git a/shaders/text/text.frag b/shaders/text/text.frag index 6fcc542a..0ef819bb 100644 --- a/shaders/text/text.frag +++ b/shaders/text/text.frag @@ -1,12 +1,12 @@ ENGINE::COMMON in vec2 vs_uv; +in vec4 vs_color; out vec4 FragColor; uniform sampler2D bitmap; -uniform vec4 color; void main() { - FragColor = vec4(color.rgb, texture(bitmap, vs_uv).r * color.a); -} \ No newline at end of file + FragColor = vec4(vs_color.rgb, texture(bitmap, vs_uv).r * vs_color.a); +} diff --git a/shaders/text/text.vert b/shaders/text/text.vert index 95a4f8d5..cdb4a36d 100644 --- a/shaders/text/text.vert +++ b/shaders/text/text.vert @@ -2,8 +2,10 @@ ENGINE::COMMON layout (location = 0) in vec2 position; layout (location = 1) in vec2 uv; +layout (location = 2) in vec4 color; out vec2 vs_uv; +out vec4 vs_color; uniform mat4 model; @@ -13,4 +15,5 @@ void main() { gl_Position = proj * model * vec4(position, 0.0, 1.0); vs_uv = uv; + vs_color = color; } \ No newline at end of file diff --git a/src/limitless/core/profiler.cpp b/src/limitless/core/profiler.cpp index 197e1f0e..fd7a1bf9 100644 --- a/src/limitless/core/profiler.cpp +++ b/src/limitless/core/profiler.cpp @@ -5,11 +5,16 @@ using namespace Limitless; void Profiler::draw(Context& ctx, const Assets& assets) { - TextInstance text {"text", glm::vec2{0.0f}, assets.fonts.at("nunito")}; + const auto text_format = TextFormat( + /* color = */glm::vec4(1.f), + assets.fonts.at("nunito"), + /* wrap_width =*/ std::nullopt + ); + auto text = TextInstance({{"text", text_format}}, glm::vec2(0.f)); text.setSize(glm::vec2{0.5f}); glm::vec2 position = {400, 400}; for (const auto& [name, query] : queries) { - text.setText(name + " " + std::to_string(query.getDuration())); + text.setText(name + " " + std::to_string(query.getDuration()), text_format); text.setPosition(position); text.draw(ctx, assets); diff --git a/src/limitless/core/texture/texture.cpp b/src/limitless/core/texture/texture.cpp index 6fa8107f..12877d05 100644 --- a/src/limitless/core/texture/texture.cpp +++ b/src/limitless/core/texture/texture.cpp @@ -4,6 +4,8 @@ #include #include +#include + using namespace Limitless; void Texture::storage(const void* data) { @@ -271,3 +273,103 @@ bool Texture::isImmutable() const noexcept { Texture::Builder Texture::builder() { return {}; } + +static std::string formatToString(GLenum format) { + switch (format) { + case GL_RED: return "GL_RED"; + case GL_RG: return "GL_RG"; + case GL_RGB: return "GL_RGB"; + case GL_RGBA: return "GL_RGBA"; + case GL_DEPTH_COMPONENT: return "GL_DEPTH_COMPONENT"; + case GL_STENCIL_INDEX: return "GL_STENCIL_INDEX"; + case GL_DEPTH_STENCIL: return "GL_DEPTH_STENCIL"; + default: return "Unknown"; + } +} + +static std::string dataTypeToString(GLenum dataType) { + switch (dataType) { + case GL_UNSIGNED_BYTE: return "GL_UNSIGNED_BYTE"; + case GL_FLOAT: return "GL_FLOAT"; + case GL_INT: return "GL_INT"; + case GL_UNSIGNED_INT: return "GL_UNSIGNED_INT"; + case GL_SHORT: return "GL_SHORT"; + case GL_UNSIGNED_SHORT: return "GL_UNSIGNED_SHORT"; + case GL_BYTE: return "GL_BYTE"; + case GL_UNSIGNED_INT_24_8: return "GL_UNSIGNED_INT_24_8"; + default: return "Unknown"; + } +} + +static std::string targetToString(GLenum target) { + switch (target) { + case GL_TEXTURE_2D: return "GL_TEXTURE_2D"; + case GL_TEXTURE_3D: return "GL_TEXTURE_3D"; + case GL_TEXTURE_CUBE_MAP: return "GL_TEXTURE_CUBE_MAP"; + case GL_TEXTURE_CUBE_MAP_ARRAY: return "GL_TEXTURE_CUBE_MAP_ARRAY"; + default: return "Unknown"; + } +} + +static std::string bytesToHexString(const std::vector& bytes) { + std::stringstream ss; + for (const auto& byte : bytes) { + ss << std::hex << static_cast(static_cast(byte)) << " "; + } + return ss.str(); +} + +std::vector Texture::getPixels() noexcept { + std::vector pixels; + pixels.resize(size.x * size.y * getBytesPerPixel()); + + std::cerr << "getPixels size: " << size.x << " " << size.y << " " << getBytesPerPixel() << std::endl; + std::cerr << "getPixels format: " << formatToString(static_cast(format)) << " " << dataTypeToString(static_cast(data_type)) << std::endl; + std::cerr << "getPixels target: " << targetToString(static_cast(target)) << std::endl; + + bind(0); + glGetTexImage(static_cast(target), + 0, // mipmap level + static_cast(format), + static_cast(data_type), + pixels.data()); + + std::cerr << "getPixels pixels: " << bytesToHexString(pixels) << std::endl; + + return pixels; +} + +size_t Texture::getBytesPerPixel() const noexcept { + const auto channels = [&]() -> size_t { + switch (format) { + case Format::DepthComponent: return 1; + case Format::StencilIndex: return 1; + case Format::DepthStencil: return 2; + case Format::Red: return 1; + case Format::Green: return 1; + case Format::Blue: return 1; + case Format::RG: return 2; + case Format::RGInt: return 2; + case Format::RGB: return 3; + case Format::RGBInt: return 3; + case Format::RGBA: return 4; + }; + throw std::runtime_error("Invalid format"); + }(); + + const auto bytes_per_channel = [&]() -> size_t { + switch (data_type) { + case DataType::UnsignedByte: return 1; + case DataType::Float: return 4; + case DataType::Int: return 4; + case DataType::UnsignedInt: return 4; + case DataType::Short: return 2; + case DataType::UnsignedShort: return 2; + case DataType::Byte: return 1; + case DataType::Uint24_8: return 3; + }; + throw std::runtime_error("Invalid data type"); + }(); + + return channels * bytes_per_channel; +} diff --git a/src/limitless/core/vertex_array.cpp b/src/limitless/core/vertex_array.cpp index 57d6a9f1..92bd5680 100644 --- a/src/limitless/core/vertex_array.cpp +++ b/src/limitless/core/vertex_array.cpp @@ -93,7 +93,13 @@ VertexArray& VertexArray::operator<<(const std::pair&>& attribute) noexcept { setAttribute(0, false, sizeof(TextVertex), (GLvoid*)offsetof(TextVertex, position), attribute.second); setAttribute(1, false, sizeof(TextVertex), (GLvoid*)offsetof(TextVertex, uv), attribute.second); - return *this; + setAttribute(2, false, sizeof(TextVertex), (GLvoid*)offsetof(TextVertex, color), attribute.second); + return *this; +} + +VertexArray& VertexArray::operator<<(const std::pair&>& attribute) noexcept { + setAttribute(0, false, sizeof(TextSelectionVertex), (GLvoid*)offsetof(TextSelectionVertex, position), attribute.second); + return *this; } void VertexArray::setAttribute(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* pointer, const std::shared_ptr& buffer) { diff --git a/src/limitless/models/text_model.cpp b/src/limitless/models/text_model.cpp index fcfe2af1..9aa97a6b 100644 --- a/src/limitless/models/text_model.cpp +++ b/src/limitless/models/text_model.cpp @@ -44,4 +44,4 @@ void TextModel::draw() const { vertex_array.bind(); glDrawArrays(GL_TRIANGLES, 0, vertices.size()); -} \ No newline at end of file +} diff --git a/src/limitless/models/text_selection_model.cpp b/src/limitless/models/text_selection_model.cpp new file mode 100644 index 00000000..0dca4de1 --- /dev/null +++ b/src/limitless/models/text_selection_model.cpp @@ -0,0 +1,42 @@ +#include + +#include + +using namespace Limitless; + +TextSelectionModel::TextSelectionModel(std::vector&& vertices) + : vertices{std::move(vertices)} +{ + initialize(vertices.size()); +} + +TextSelectionModel::TextSelectionModel(size_t count) { + initialize(count); +} + +void TextSelectionModel::initialize(size_t count) { + buffer = Buffer::builder() + .target(Buffer::Type::Array) + .data(vertices.empty() ? nullptr : vertices.data()) + .size(count * sizeof(TextSelectionVertex)) + .usage(Buffer::Usage::DynamicDraw) + .access(Buffer::MutableAccess::WriteOrphaning) + .build(); + + vertex_array << std::pair&>(TextSelectionVertex{}, buffer); +} + +void TextSelectionModel::update(std::vector&& _vertices) { + vertices = std::move(_vertices); + + if (vertices.size() * sizeof(TextSelectionVertex) > buffer->getSize()) { + buffer->resize(vertices.size() * sizeof(TextSelectionVertex)); + } + + buffer->mapData(vertices.data(), vertices.size() * sizeof(TextSelectionVertex)); +} + +void TextSelectionModel::draw() const { + vertex_array.bind(); + glDrawArrays(GL_TRIANGLES, 0, vertices.size()); +} diff --git a/src/limitless/shader_storage.cpp b/src/limitless/shader_storage.cpp index f885432a..4af79fc1 100644 --- a/src/limitless/shader_storage.cpp +++ b/src/limitless/shader_storage.cpp @@ -137,6 +137,7 @@ void ShaderStorage::initialize(Context& ctx, const RendererSettings& settings, c add("text", compiler.compile(shader_dir / "text/text")); add("text_selection", compiler.compile(shader_dir / "text/text_selection")); + add("icon_text", compiler.compile(shader_dir / "text/icon_text")); } void ShaderStorage::clear() { diff --git a/src/limitless/text/font_atlas.cpp b/src/limitless/text/font_atlas.cpp index f6968343..82a65722 100644 --- a/src/limitless/text/font_atlas.cpp +++ b/src/limitless/text/font_atlas.cpp @@ -11,142 +11,163 @@ using namespace std::literals::string_literals; static constexpr const uint32_t UNDEFINED_GLYPH_INDEX = 0; static constexpr const uint32_t UNDEFINED_GLYPH_CHAR_CODE = 0; -FontAtlas::FontAtlas(const fs::path& path, uint32_t pixel_size) - : font_size {pixel_size} { - static FT_Library ft {nullptr}; +static constexpr const size_t TAB_WIDTH_IN_SPACES = 4; - if (!ft) { - if (FT_Init_FreeType(&ft)) { - throw font_error{"Failed to initialize Freetype"}; - } - } +static constexpr const size_t MAX_ATLAS_DIM_SIZE = 4096; - if (FT_New_Face(ft, path.string().c_str(), 0, &face)) { - throw font_error{"Failed to load the font at path"s + path.string()}; - } +struct GlyphInfo { + glm::ivec2 bearing; + uint32_t advance; + glm::uvec2 size; + std::vector bitmap; + bool is_icon; +}; - FT_Set_Pixel_Sizes(face, 0, font_size); - - struct GlyphInfo { - glm::ivec2 bearing; - uint32_t advance; - std::vector bitmap; - }; - - std::unordered_map glyph_for_char; +struct AtlasPackResult { + std::unordered_map utf32_char_pos; + glm::uvec2 atlas_size; +}; +static AtlasPackResult packAtlas(const std::unordered_map& glyph_for_char) { stbrp_context context; std::vector packer_rects; std::vector packer_nodes; - FT_ULong char_code; - FT_UInt glyph_index; + static_assert(sizeof(int) >= sizeof(uint32_t), "int must be at least as big as uint32_t"); - auto loadGlyphFrom = [&](const FT_GlyphSlot ft_glyph, uint32_t char_code){ - const auto& glyph_bitmap = ft_glyph->bitmap; - if (glyph_bitmap.pitch < 0) { - throw font_error {"font has negative glyph bitmap pitch, which is not supported"}; - } - - auto char_bitmap = std::vector(glyph_bitmap.pitch * glyph_bitmap.rows); - memcpy(char_bitmap.data(), glyph_bitmap.buffer, char_bitmap.size()); - - auto [_, emplaced] = glyph_for_char.emplace(char_code, GlyphInfo { - {ft_glyph->bitmap_left, ft_glyph->bitmap_top}, - static_cast(ft_glyph->advance.x), - std::move(char_bitmap) - }); - - // TODO: support char glyph variants? - if (emplaced) { - packer_rects.emplace_back(stbrp_rect{(int)char_code, (int)glyph_bitmap.width, (int)glyph_bitmap.rows, 0, 0, 0}); - } - }; - - if (FT_Load_Glyph(face, UNDEFINED_GLYPH_INDEX, FT_LOAD_RENDER) != 0) { - throw font_error {"Failed to load tofu (missing) glyph"}; + for (const auto& [char_code, glyph_info] : glyph_for_char) { + packer_rects.emplace_back(stbrp_rect{(int)char_code, (int)glyph_info.size.x, (int)glyph_info.size.y, 0, 0, 0}); } - // Put "tofu" as 0 char code glyph -- null terminators (\0) are not normally rendered anyway. - loadGlyphFrom(face->glyph, UNDEFINED_GLYPH_CHAR_CODE); - - for ( - char_code = FT_Get_First_Char(face, &glyph_index); - glyph_index != 0; - char_code = FT_Get_Next_Char(face, char_code, &glyph_index) - ) { - if (FT_Load_Char(face, char_code, FT_LOAD_RENDER) != 0) { - throw font_error {"Failed to load char with code " + std::to_string(char_code)}; + packer_nodes.resize(packer_rects.size()); + + size_t atlas_dim_size = 256; + + while (atlas_dim_size <= MAX_ATLAS_DIM_SIZE) { + stbrp_init_target(&context, atlas_dim_size, atlas_dim_size, packer_nodes.data(), packer_nodes.size()); + + if (!stbrp_pack_rects(&context, packer_rects.data(), packer_rects.size())) { + atlas_dim_size *= 2; + continue; + } + + std::unordered_map utf32_char_pos; + for (const auto& rect : packer_rects) { + uint32_t char_code = *(reinterpret_cast(&rect.id)); + utf32_char_pos.emplace(char_code, glm::uvec2{rect.x, rect.y}); } - loadGlyphFrom(face->glyph, char_code); + return AtlasPackResult { + std::move(utf32_char_pos), + glm::uvec2{atlas_dim_size, atlas_dim_size} + }; } - packer_nodes.resize(packer_rects.size()); - // TODO: pass atlas size as parameter. - constexpr const size_t atlas_dim_size = 4096; - const glm::uvec2 atlas_size = glm::uvec2(atlas_dim_size); - - stbrp_init_target(&context, atlas_size.x, atlas_size.y, packer_nodes.data(), packer_nodes.size()); - stbrp_pack_rects(&context, packer_rects.data(), packer_rects.size()); - - std::vector data(atlas_size.x * atlas_size.y); + throw font_error {"failed to pack atlas into " + std::to_string(MAX_ATLAS_DIM_SIZE) + "x" + std::to_string(MAX_ATLAS_DIM_SIZE) + " texture"}; +} - for (const auto& rect : packer_rects) { - if (!rect.was_packed) { - // TODO: better to try again with larger size. - throw font_error {"failed to pack chars into atlas"}; +static std::shared_ptr makeAtlas( + std::unordered_map glyph_for_char, + uint32_t pixel_size, + size_t bytes_per_pixel, + bool is_icon +) { + std::unordered_map chars; + + auto atlas_pack_result = packAtlas(glyph_for_char); + const auto atlas_size = atlas_pack_result.atlas_size; + const auto& char_pos = atlas_pack_result.utf32_char_pos; + + std::vector data(atlas_size.x * atlas_size.y * bytes_per_pixel); + + for (const auto& [char_code, glyph] : glyph_for_char) { + auto it = char_pos.find(char_code); + if (it == char_pos.end()) { + // TODO: show actual char code as hex. + throw font_error {"failed to find char " + std::to_string(char_code) + " in generated atlas"}; } - uint32_t char_code = *(reinterpret_cast(&rect.id)); - auto it = glyph_for_char.find(char_code); - if (it == glyph_for_char.end()) { - throw font_error {"failed to find glyph for char " + std::to_string(char_code)}; - } + const auto& glyph_pos = it->second; - const auto& glyph_info = it->second; + for (size_t row = 0; row < static_cast(glyph.size.y); ++row) { + const auto& char_bitmap = glyph.bitmap; + const size_t glyph_pitch = char_bitmap.size() / glyph.size.y; // as bitmap size = pitch * rows + const size_t atlas_pitch = data.size() / atlas_size.y; - for (size_t row = 0; row < static_cast(rect.h); ++row) { - const auto& char_bitmap = glyph_info.bitmap; - const size_t glyph_pitch = char_bitmap.size() / rect.h; // because bitmap size = pitch * rows - const ptrdiff_t input_offset = glyph_pitch * row; - const ptrdiff_t output_offset = (rect.y + row)*atlas_size.y + rect.x; + const ptrdiff_t input_offset = is_icon + ? glyph_pitch * (glyph.size.y - 1 - row) + : glyph_pitch * row; + const ptrdiff_t output_offset = (glyph_pos.y + row)*atlas_pitch + (glyph_pos.x * bytes_per_pixel); - memcpy(data.data() + output_offset, char_bitmap.data() + input_offset, rect.w); + memcpy(data.data() + output_offset, char_bitmap.data() + input_offset, glyph.size.x * bytes_per_pixel); } const glm::vec2 atlas_rect_tl_pos = { - static_cast(rect.x) / static_cast(atlas_size.x), - static_cast(rect.y) / static_cast(atlas_size.y) + static_cast(glyph_pos.x) / static_cast(atlas_size.x), + static_cast(glyph_pos.y) / static_cast(atlas_size.y) }; const glm::vec2 atlas_rect_br_pos = { - static_cast(rect.x + rect.w) / static_cast(atlas_size.x), - static_cast(rect.y + rect.h) / static_cast(atlas_size.y) + static_cast(glyph_pos.x + glyph.size.x) / static_cast(atlas_size.x), + static_cast(glyph_pos.y + glyph.size.y) / static_cast(atlas_size.y) }; chars.emplace(char_code, FontChar{ - {rect.w, rect.h}, - glyph_info.bearing, - glyph_info.advance, + {glyph.size.x, glyph.size.y}, + glyph.bearing, + glyph.advance, { glm::vec2 { atlas_rect_tl_pos.x, atlas_rect_br_pos.y}, glm::vec2 { atlas_rect_br_pos.x, atlas_rect_br_pos.y}, glm::vec2 { atlas_rect_tl_pos.x, atlas_rect_tl_pos.y}, glm::vec2 { atlas_rect_br_pos.x, atlas_rect_tl_pos.y} - }} - ); + }, + glyph.is_icon + }); } + if (chars.find(' ') == chars.end()) { + // add fallback whitespace definition if missing, + // otherwise the font atlas is too cursed. + chars.emplace(' ', FontChar{ + {0, 0}, + {0, 0}, + pixel_size, + { + glm::vec2(0.f), + glm::vec2(0.f), + glm::vec2(0.f), + glm::vec2(0.f) + }, + /* is_icon = */ true // so that whitespace is not colored : ) + }); + } + // TODO: remove as tabulation has to be handled by type setter. chars.emplace('\t', chars.at(' ')); chars.at('\t').advance *= TAB_WIDTH_IN_SPACES; + const auto internal_format = [bytes_per_pixel] { + switch (bytes_per_pixel) { + case 1: return Texture::InternalFormat::R8; + case 4: return Texture::InternalFormat::RGBA8; + default: throw font_error {"unsupported bytes per pixel: " + std::to_string(bytes_per_pixel)}; + } + }(); + + const auto format = [bytes_per_pixel] { + switch (bytes_per_pixel) { + case 1: return Texture::Format::Red; + case 4: return Texture::Format::RGBA; + default: throw font_error {"unsupported bytes per pixel: " + std::to_string(bytes_per_pixel)}; + } + }(); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - texture = Texture::builder() + auto texture = Texture::builder() .target(Texture::Type::Tex2D) - .internal_format(Texture::InternalFormat::R) + .internal_format(internal_format) .size(atlas_size) - .format(Texture::Format::Red) + .format(format) .data_type(Texture::DataType::UnsignedByte) .data(data.data()) .wrap_s(Texture::Wrap::ClampToEdge) @@ -156,14 +177,127 @@ FontAtlas::FontAtlas(const fs::path& path, uint32_t pixel_size) .mipmap(false) .buildMutable(); glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + + return std::make_shared(std::move(chars), std::move(texture), pixel_size, is_icon); +} + +FontAtlas::FontAtlas( + std::unordered_map chars, + std::shared_ptr texture, + uint32_t pixel_size, + bool _is_icon +) + : chars {std::move(chars)} + , texture {std::move(texture)} + , pixel_size {pixel_size} + , is_icon (_is_icon) +{ + } -FontAtlas::~FontAtlas() { - if (face) { - FT_Done_Face(face); +std::shared_ptr FontAtlas::make( + uint32_t font_size_in_pixels, + std::unordered_map> icons +) { + if (icons.empty()) { + throw font_error {"no icons provided"}; } + + std::unordered_map glyph_for_char; + + const auto bytes_per_pixel = [&] { + switch (icons.begin()->second->getInternalFormat()) { + case Texture::InternalFormat::R8: + std::cerr << "internal format r8\n"; + return 1; + case Texture::InternalFormat::RGBA8: + std::cerr << "internal format rgba8\n"; + return 4; + default: throw font_error {"unsupported texture format"}; + } + }(); + + for (const auto& [char_code, texture] : icons) { + glyph_for_char.emplace(char_code, GlyphInfo { + {0, 3*texture->getSize().y/4}, + texture->getSize().x << 6, + {texture->getSize().x, texture->getSize().y}, + texture->getPixels(), + /* is_icon = */true + }); + } + + return makeAtlas(std::move(glyph_for_char), font_size_in_pixels, bytes_per_pixel, /* is_icon = */ true); } +std::shared_ptr FontAtlas::load( + const fs::path& path, + uint32_t pixel_size +) { + static FT_Library ft {nullptr}; + + if (!ft) { + if (FT_Init_FreeType(&ft)) { + throw font_error{"Failed to initialize Freetype"}; + } + } + + FT_Face face; + if (FT_New_Face(ft, path.string().c_str(), 0, &face)) { + throw font_error{"Failed to load the font at path"s + path.string()}; + } + + FT_Set_Pixel_Sizes(face, 0, pixel_size); + + std::unordered_map glyph_for_char; + + FT_ULong char_code; + FT_UInt glyph_index; + + auto loadGlyphFrom = [&](const FT_GlyphSlot ft_glyph, uint32_t char_code){ + const auto& glyph_bitmap = ft_glyph->bitmap; + if (glyph_bitmap.pitch < 0) { + throw font_error {"font has negative glyph bitmap pitch, which is not supported"}; + } + + auto char_bitmap = std::vector(glyph_bitmap.pitch * glyph_bitmap.rows); + memcpy(char_bitmap.data(), glyph_bitmap.buffer, char_bitmap.size()); + + auto [_, emplaced] = glyph_for_char.emplace(char_code, GlyphInfo { + {ft_glyph->bitmap_left, ft_glyph->bitmap_top}, + static_cast(ft_glyph->advance.x), + {static_cast(glyph_bitmap.width), static_cast(glyph_bitmap.rows)}, + std::move(char_bitmap), + /* is_icon = */false // TODO: check if its emoji or w/e. + }); + }; + + if (FT_Load_Glyph(face, UNDEFINED_GLYPH_INDEX, FT_LOAD_RENDER) != 0) { + throw font_error {"Failed to load tofu (missing) glyph"}; + } + + // Put "tofu" as 0 char code glyph -- null terminators (\0) are not normally rendered anyway. + loadGlyphFrom(face->glyph, UNDEFINED_GLYPH_CHAR_CODE); + + for ( + char_code = FT_Get_First_Char(face, &glyph_index); + glyph_index != 0; + char_code = FT_Get_Next_Char(face, char_code, &glyph_index) + ) { + if (FT_Load_Char(face, char_code, FT_LOAD_RENDER) != 0) { + throw font_error {"Failed to load char with code " + std::to_string(char_code)}; + } + + loadGlyphFrom(face->glyph, char_code); + } + + FT_Done_Face(face); + + return makeAtlas(std::move(glyph_for_char), pixel_size, /* bytes_per_pixel = */ 1, /* is_icon = */ false); +} + +FontAtlas::~FontAtlas() = default; + static size_t utf8CharLength(char c) { if ((c & 0x80) == 0) { return 1; @@ -212,7 +346,7 @@ static void forEachUnicodeCodepoint(std::string_view str, T&& func) { } } -std::vector FontAtlas::generate(const std::string& text) const { +std::vector FontAtlas::generate(const std::string& text, const glm::vec4& color) const { std::vector vertices; vertices.reserve(text.size()); @@ -220,7 +354,7 @@ std::vector FontAtlas::generate(const std::string& text) const { forEachUnicodeCodepoint(text, [&](uint32_t cp) { if (cp == '\n') { - offset.y -= font_size; + offset.y -= pixel_size; offset.x = 0; return true; } @@ -230,13 +364,15 @@ std::vector FontAtlas::generate(const std::string& text) const { float x = offset.x + fc.bearing.x; float y = offset.y + fc.bearing.y - fc.size.y; - vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2]); - vertices.emplace_back(glm::vec2{x, y}, fc.uvs[0]); - vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1]); + const auto vertex_color = fc.is_icon ? glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) : color; - vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2]); - vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1]); - vertices.emplace_back(glm::vec2{x + fc.size.x, y + fc.size.y}, fc.uvs[3]); + vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); + vertices.emplace_back(glm::vec2{x, y}, fc.uvs[0], vertex_color); + vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); + + vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); + vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); + vertices.emplace_back(glm::vec2{x + fc.size.x, y + fc.size.y}, fc.uvs[3], vertex_color); offset.x += (fc.advance >> 6); return true; @@ -246,7 +382,7 @@ std::vector FontAtlas::generate(const std::string& text) const { } glm::vec2 FontAtlas::getTextSize(const std::string& text) const { - glm::vec2 max_size {0, font_size}; + glm::vec2 max_size {0, pixel_size}; float size {}; forEachUnicodeCodepoint(text, [&](uint32_t cp) { @@ -255,7 +391,7 @@ glm::vec2 FontAtlas::getTextSize(const std::string& text) const { size += (fc.advance >> 6); if (cp == '\n') { - max_size.y += font_size; + max_size.y += pixel_size; size = 0; } @@ -266,17 +402,17 @@ glm::vec2 FontAtlas::getTextSize(const std::string& text) const { return max_size; } -std::vector FontAtlas::getSelectionGeometry(std::string_view text, size_t begin, size_t end) const { - std::vector vertices; +std::vector FontAtlas::getSelectionGeometry(std::string_view text, size_t begin, size_t end) const { + std::vector vertices; auto addRect = [&] (glm::vec2 pos, glm::vec2 size) { - vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}, glm::vec2{}); - vertices.emplace_back(glm::vec2{pos.x, -pos.y}, glm::vec2{}); - vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}, glm::vec2{}); + vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}); + vertices.emplace_back(glm::vec2{pos.x, -pos.y}); + vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}); - vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}, glm::vec2{}); - vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}, glm::vec2{}); - vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y + size.y}, glm::vec2{}); + vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}); + vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}); + vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y + size.y}); }; glm::ivec2 offset{}; @@ -295,7 +431,7 @@ std::vector FontAtlas::getSelectionGeometry(std::string_view text, s offset.x += getFontChar(cp).advance >> 6; if (cp == '\n') { offset.x = 0; - offset.y += font_size; + offset.y += pixel_size; } ++pos; return true; @@ -304,16 +440,16 @@ std::vector FontAtlas::getSelectionGeometry(std::string_view text, s if (cp == '\n') { if (size != 0) { // Finish this selection line. - addRect({offset.x, offset.y}, glm::vec2{size, font_size}); + addRect({offset.x, offset.y}, glm::vec2{size, pixel_size}); size = 0; offset.x = 0; - offset.y += font_size; + offset.y += pixel_size; } else { // An empty line, still add small selection geometry for it. size = getFontChar(' ').advance >> 6; } } else { - size += getFontChar(cp).advance >> 6; + size += getFontChar(cp).advance >> 6; } ++pos; return true; @@ -323,7 +459,7 @@ std::vector FontAtlas::getSelectionGeometry(std::string_view text, s } }); - addRect({offset.x, offset.y}, glm::vec2{size, font_size}); + addRect({offset.x, offset.y}, glm::vec2{size, pixel_size}); return vertices; } diff --git a/src/limitless/text/text_instance.cpp b/src/limitless/text/text_instance.cpp index 2b795422..f503259a 100644 --- a/src/limitless/text/text_instance.cpp +++ b/src/limitless/text/text_instance.cpp @@ -5,25 +5,27 @@ #include "limitless/core/uniform/uniform.hpp" #include #include +#include #include #include using namespace Limitless; -TextInstance::TextInstance(std::string _text, const glm::vec2& _position, std::shared_ptr _font) - : text_model {_font->generate(_text)} - , text {std::move(_text)} +TextInstance::TextInstance( + std::vector _formatted_text_parts, + const glm::vec2& _position +) + : formatted_text_parts {std::move(_formatted_text_parts)} , position {_position} - , font {std::move(_font)} { - calculateModelMatrix(); -} - -TextInstance::TextInstance(size_t count, const glm::vec2& _position, std::shared_ptr _font) - : text_model {count} - , text {} - , position {_position} - , font {std::move(_font)} { +{ + auto type_set_result = TypeSetter::typeSet(formatted_text_parts); + bounding_box = type_set_result.bounding_box; + for (auto& fv : type_set_result.vertices_of_fonts) { + font_text_models.push_back(FontTextModel{ + TextModel(std::move(fv.vertices)), fv.font + }); + } calculateModelMatrix(); } @@ -34,16 +36,24 @@ void TextInstance::calculateModelMatrix() noexcept { model_matrix = translation_matrix * scaling_matrix; } -TextInstance& TextInstance::setText(std::string _text) { - if (text != _text) { - text = std::move(_text); - text_model.update(font->generate(text)); - } - return *this; +TextInstance& TextInstance::setText(std::string _text, TextFormat _text_format) { + return setText({FormattedText(std::move(_text), std::move(_text_format))}); } -TextInstance& TextInstance::setColor(const glm::vec4& _color) noexcept { - color = _color; +TextInstance& TextInstance::setText(std::vector _formatted_text_parts) { + if (formatted_text_parts != _formatted_text_parts) { + formatted_text_parts = std::move(_formatted_text_parts); + + auto type_set_result = TypeSetter::typeSet(formatted_text_parts); + font_text_models.clear(); + + bounding_box = type_set_result.bounding_box; + for (auto& fv : type_set_result.vertices_of_fonts) { + font_text_models.push_back(FontTextModel{ + TextModel(std::move(fv.vertices)), fv.font + }); + } + } return *this; } @@ -53,7 +63,7 @@ TextInstance& TextInstance::setSelectionColor(const glm::vec4& _color) noexcept } TextInstance& TextInstance::setSelection(size_t begin, size_t end) { - selection_model = TextModel{font->getSelectionGeometry(text, begin, end)}; + selection_model = TextSelectionModel{TypeSetter::typeSetSelection(formatted_text_parts, begin, end)}; return *this; } @@ -74,9 +84,21 @@ TextInstance& TextInstance::setSize(const glm::vec2& _size) noexcept { return *this; } +glm::vec2 TextInstance::getBoundingBoxDimensions() const noexcept { + const auto& [min, max] = bounding_box; + return (max - min) * size; +} + void TextInstance::draw(Context& ctx, const Assets& assets) { ctx.disable(Capabilities::DepthTest); + const auto ortho_projection = glm::ortho( + 0.0f, + static_cast(ctx.getSize().x), + 0.0f, + static_cast(ctx.getSize().y) + ); + // draw selection if (selection_model) { ctx.disable(Capabilities::Blending); @@ -84,7 +106,7 @@ void TextInstance::draw(Context& ctx, const Assets& assets) { auto& shader = assets.shaders.get("text_selection"); shader.setUniform("model", model_matrix) - .setUniform("proj", glm::ortho(0.0f, static_cast(ctx.getSize().x), 0.0f, static_cast(ctx.getSize().y))) + .setUniform("proj", ortho_projection) .setUniform("color", selection_color); shader.use(); @@ -93,18 +115,19 @@ void TextInstance::draw(Context& ctx, const Assets& assets) { } // draw text - { - auto& shader = assets.shaders.get("text"); - + for (auto& ftm : font_text_models) { + auto& text_shader = ftm.font_atlas->isIconAtlas() + ? assets.shaders.get("icon_text") + : assets.shaders.get("text"); setBlendingMode(ms::Blending::Text); - shader.setUniform("bitmap", font->getTexture()) + text_shader + .setUniform("bitmap", ftm.font_atlas->getTexture()) .setUniform("model", model_matrix) - .setUniform("proj", glm::ortho(0.0f, static_cast(ctx.getSize().x), 0.0f, static_cast(ctx.getSize().y))) - .setUniform("color", color); + .setUniform("proj", ortho_projection); - shader.use(); + text_shader.use(); - text_model.draw(); + ftm.text_model.draw(); } -} \ No newline at end of file +} diff --git a/src/limitless/text/type_setter.cpp b/src/limitless/text/type_setter.cpp new file mode 100644 index 00000000..7aa37d97 --- /dev/null +++ b/src/limitless/text/type_setter.cpp @@ -0,0 +1,291 @@ +#include + +#include + +using namespace Limitless; + +static size_t utf8CharLength(char c) { + if ((c & 0x80) == 0) { + return 1; + + } else if ((c & 0xE0) == 0xC0) { + return 2; + + } else if ((c & 0xF0) == 0xE0) { + return 3; + + } else if ((c & 0xF8) == 0xF0) { + return 4; + } + + return 0; +} + +/** + * Invokes bool(uint32_t) function for each Unicode codepoint of a UTF-8 encoded string. + * If that function returns false, then iteration is stopped. + */ +template +static void forEachUnicodeCodepoint(std::string_view str, T&& func) { + size_t i = 0; + while (i < str.size()) { + size_t char_len = utf8CharLength(str[i]); + if (char_len == 0) { + throw font_error {"invalid UTF-8 char"}; + } + + uint32_t codepoint = str[i] & (0xFF >> char_len); + for (size_t j = 1; j < char_len; ++j) { + char continuation_byte = str[i + j]; + + if ((continuation_byte & 0xC0) != 0x80) { + throw font_error {"invalid UTF-8 byte sequence at " + std::to_string(i + j)}; + } + codepoint = (codepoint << 6) | (continuation_byte & 0x3F); + } + + if (!func(codepoint)) { + return; + } + + i += char_len; + } +} + +static std::vector split(const std::string& str, char separator) { + std::vector tokens; + std::stringstream ss(str); + std::string token; + + while (std::getline(ss, token, separator)) { + tokens.emplace_back(std::move(token)); + } + + return tokens; +} + +static std::string wordWrap( + const std::string& original_text, + const FontAtlas& font, + float wrap_width, + float curr_width +) { + std::string word_wrapped_text; + auto lines = split(original_text, '\n'); + + for (const auto& line : lines) { + auto words = split(line, ' '); + + for (auto& word : words) { + if (&word != &words.back()) { + word += ' '; + } + + const auto word_width = [&](){ + float result = 0.f; + + forEachUnicodeCodepoint(word, [&](uint32_t cp){ + const auto& fc = font.getFontChar(cp); + + result += (fc.advance >> 6); + + return true; + }); + + return result; + }(); + + if (curr_width + word_width <= wrap_width) { + word_wrapped_text += word; + curr_width += word_width; + + } else { + if (curr_width != 0.f) { + word_wrapped_text += '\n'; + } + word_wrapped_text += word; + curr_width = word_width; + } + } + + if (&line != &lines.back()) { + curr_width = 0.f; + word_wrapped_text += '\n'; + } + } + + return word_wrapped_text; +} + +TypeSetResult TypeSetter::typeSet( + const std::vector& formatted_text_parts +) { + std::unordered_map font_vertices; + std::pair bounding_box {{0.f, 0.f}, {0.f, 0.f}}; + glm::vec2& min_pos = bounding_box.first; + glm::vec2& max_pos = bounding_box.second; + + if (formatted_text_parts.empty()) { + return TypeSetResult({}, bounding_box); + } + + glm::vec2 offset {0.f, 0.f}; + + for (const auto& formatted_text : formatted_text_parts) { + const auto& color = formatted_text.format.color; + const auto& font = formatted_text.format.font; + const auto& wrap_width = formatted_text.format.wrap_width; + + const auto& text = wrap_width + ? wordWrap(formatted_text.text, *font, *wrap_width, offset.x) + : formatted_text.text; + + auto& vertices = [&]() -> std::vector& { + auto it = font_vertices.find(font.get()); + if (it != font_vertices.end()) { + return it->second.vertices; + } + + font_vertices.emplace(font.get(), FontVertices(font, {})); + return font_vertices.at(font.get()).vertices; + }(); + + forEachUnicodeCodepoint(text, [&](uint32_t cp) -> bool { + if (cp == '\n') { + offset.y -= font->getFontSize(); + offset.x = 0; + // ++cp_index; + return true; + } + + const auto& fc = font->getFontChar(cp); + + float x = offset.x + fc.bearing.x; + float y = offset.y + fc.bearing.y - fc.size.y; + + min_pos = glm::vec2(std::min(min_pos.x, x), std::min(min_pos.y, y)); + max_pos = glm::vec2(std::max(max_pos.x, x + fc.size.x), std::max(max_pos.y, y + fc.size.y)); + + const auto vertex_color = fc.is_icon ? glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) : color; + + vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); + vertices.emplace_back(glm::vec2{x, y}, fc.uvs[0], vertex_color); + vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); + + vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); + vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); + vertices.emplace_back(glm::vec2{x + fc.size.x, y + fc.size.y}, fc.uvs[3], vertex_color); + + offset.x += (fc.advance >> 6); + // ++cp_index; + return true; + }); + }; + + std::vector vertices_per_font; + + for (auto& [font_atlas_ptr, vertices] : font_vertices) { + vertices_per_font.emplace_back(std::move(vertices)); + } + + return TypeSetResult(std::move(vertices_per_font), bounding_box); +} + +std::vector TypeSetter::typeSetSelection( + const std::vector& formatted_text_parts, + size_t start_codepoint_index, + size_t end_codepoint_index +) { + std::vector vertices; + + auto addRect = [&] (glm::vec2 pos, glm::vec2 size) { + vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}); + vertices.emplace_back(glm::vec2{pos.x, -pos.y}); + vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}); + + vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}); + vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}); + vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y + size.y}); + }; + + glm::ivec2 offset {}; + size_t cp_index = 0u; + uint32_t line_width = 0u; + uint32_t line_height = 0u; + + for (const auto& formatted_text : formatted_text_parts) { + const auto& text = formatted_text.text; + const auto& font = formatted_text.format.font; + + forEachUnicodeCodepoint(text, [&](uint32_t cp) { + line_height = std::max(line_height, font->getFontSize()); + if (cp_index < start_codepoint_index) { + // Skip until selection range starts. + offset.x += font->getFontChar(cp).advance >> 6; + if (cp == '\n') { + offset.x = 0; + offset.y += line_height; + line_height = 0u; + } + ++cp_index; + return true; + + } else if (cp_index < end_codepoint_index) { + if (cp == '\n') { + if (line_width != 0) { + // Finish this selection line. + addRect({offset.x, offset.y}, glm::vec2{line_width, line_height}); + line_width = 0; + offset.x = 0; + offset.y += line_height; + line_height = 0u; + } else { + // An empty line, still add small selection geometry for it. + line_width = font->getFontChar(' ').advance >> 6; + } + } else { + line_width += font->getFontChar(cp).advance >> 6; + } + ++cp_index; + return true; + + } else { + return false; + } + }); + } + + + return vertices; +} + +glm::vec2 TypeSetter::getBoundingBoxSize(const std::vector& formatted_text_parts) { + if (formatted_text_parts.empty()) { + return glm::vec2(0.f, 0.f); + } + + // TODO: compute first line height. + glm::vec2 max_size {0, formatted_text_parts[0].format.font->getFontSize()}; + float line_width = 0.f; + + for (const auto& formatted_text : formatted_text_parts) { + const auto& text = formatted_text.text; + const auto& font = formatted_text.format.font; + + forEachUnicodeCodepoint(text, [&](uint32_t cp) { + const auto& fc = font->getFontChar(cp); + + line_width += (fc.advance >> 6); + + if (cp == '\n') { + max_size.y += font->getFontSize(); + line_width = 0; + } + + max_size.x = std::max(max_size.x, line_width); + return true; + }); + } + + return max_size; +} From fb68f8a024d8e6b699e633e588404615c65d388c Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Mon, 9 Jun 2025 12:58:08 +0900 Subject: [PATCH 02/30] implement font stack support --- include/limitless/text/font_atlas.hpp | 8 +- include/limitless/text/type_setter.hpp | 17 +- src/limitless/core/profiler.cpp | 2 +- src/limitless/text/font_atlas.cpp | 217 ++++++++++++++----------- src/limitless/text/type_setter.cpp | 90 +++++----- 5 files changed, 189 insertions(+), 145 deletions(-) diff --git a/include/limitless/text/font_atlas.hpp b/include/limitless/text/font_atlas.hpp index c92b313b..fcff0589 100644 --- a/include/limitless/text/font_atlas.hpp +++ b/include/limitless/text/font_atlas.hpp @@ -53,7 +53,8 @@ namespace Limitless { public: static std::shared_ptr load( const fs::path& path, - uint32_t pixel_size + uint32_t pixel_size, + std::vector> codepoint_ranges = {} ); static std::shared_ptr make( @@ -82,7 +83,10 @@ namespace Limitless { * Return font character for given Unicode codepoint. * If font does not have it, then ""missing/tofu" font character is returned. */ - [[nodiscard]] const FontChar& getFontChar(uint32_t utf32_codepoint) const noexcept; + [[nodiscard]] const FontChar& getFontCharOrTofu(uint32_t utf32_codepoint) const noexcept; + + [[nodiscard]] std::optional> + getFontChar(uint32_t utf32_codepoint) const noexcept; [[nodiscard]] const auto& getTexture() const { return texture; } diff --git a/include/limitless/text/type_setter.hpp b/include/limitless/text/type_setter.hpp index aa577124..0ada3485 100644 --- a/include/limitless/text/type_setter.hpp +++ b/include/limitless/text/type_setter.hpp @@ -13,25 +13,32 @@ namespace Limitless { struct TextFormat { glm::vec4 color; - std::shared_ptr font; + std::vector> font_stack; std::optional wrap_width; - TextFormat(glm::vec4 _color, std::shared_ptr _font, std::optional _wrap_width) + TextFormat( + glm::vec4 _color, + std::vector> _font_stack, + std::optional _wrap_width + ) : color (_color) - , font (_font) + , font_stack (std::move(_font_stack)) , wrap_width (_wrap_width) { } friend bool operator==(const TextFormat& lhs, const TextFormat& rhs) { return lhs.color == rhs.color - && lhs.font.get() == rhs.font.get() + && sameFontStack(lhs, rhs) && lhs.wrap_width == rhs.wrap_width; } friend bool operator!=(const TextFormat& lhs, const TextFormat& rhs) { return !(lhs == rhs); } + + private: + static bool sameFontStack(const TextFormat& lhs, const TextFormat& rhs); }; struct FormattedText { @@ -85,7 +92,5 @@ namespace Limitless { size_t start_codepoint_index, size_t end_codepoint_index ); - - static glm::vec2 getBoundingBoxSize(const std::vector& formatted_text_parts); }; } diff --git a/src/limitless/core/profiler.cpp b/src/limitless/core/profiler.cpp index fd7a1bf9..09ceea3d 100644 --- a/src/limitless/core/profiler.cpp +++ b/src/limitless/core/profiler.cpp @@ -7,7 +7,7 @@ using namespace Limitless; void Profiler::draw(Context& ctx, const Assets& assets) { const auto text_format = TextFormat( /* color = */glm::vec4(1.f), - assets.fonts.at("nunito"), + {assets.fonts.at("nunito")}, /* wrap_width =*/ std::nullopt ); auto text = TextInstance({{"text", text_format}}, glm::vec2(0.f)); diff --git a/src/limitless/text/font_atlas.cpp b/src/limitless/text/font_atlas.cpp index 82a65722..0534c6f6 100644 --- a/src/limitless/text/font_atlas.cpp +++ b/src/limitless/text/font_atlas.cpp @@ -232,7 +232,8 @@ std::shared_ptr FontAtlas::make( std::shared_ptr FontAtlas::load( const fs::path& path, - uint32_t pixel_size + uint32_t pixel_size, + std::vector> codepoint_ranges ) { static FT_Library ft {nullptr}; @@ -279,16 +280,30 @@ std::shared_ptr FontAtlas::load( // Put "tofu" as 0 char code glyph -- null terminators (\0) are not normally rendered anyway. loadGlyphFrom(face->glyph, UNDEFINED_GLYPH_CHAR_CODE); - for ( - char_code = FT_Get_First_Char(face, &glyph_index); - glyph_index != 0; - char_code = FT_Get_Next_Char(face, char_code, &glyph_index) - ) { - if (FT_Load_Char(face, char_code, FT_LOAD_RENDER) != 0) { - throw font_error {"Failed to load char with code " + std::to_string(char_code)}; - } + if (codepoint_ranges.empty()) { + for ( + char_code = FT_Get_First_Char(face, &glyph_index); + glyph_index != 0; + char_code = FT_Get_Next_Char(face, char_code, &glyph_index) + ) { + if (FT_Load_Char(face, char_code, FT_LOAD_RENDER) != 0) { + throw font_error {"Failed to load char with code " + std::to_string(char_code)}; + } - loadGlyphFrom(face->glyph, char_code); + loadGlyphFrom(face->glyph, char_code); + } + } else { + for (const auto& [cp_start, cp_end] : codepoint_ranges) { + for (uint32_t cp = cp_start; cp <= cp_end; ++cp) { + const auto glyph_index = FT_Get_Char_Index(face, cp); + if (glyph_index != 0) { + FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT); + FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL); + + loadGlyphFrom(face->glyph, cp); + } + } + } } FT_Done_Face(face); @@ -346,47 +361,47 @@ static void forEachUnicodeCodepoint(std::string_view str, T&& func) { } } -std::vector FontAtlas::generate(const std::string& text, const glm::vec4& color) const { - std::vector vertices; - vertices.reserve(text.size()); +// std::vector FontAtlas::generate(const std::string& text, const glm::vec4& color) const { +// std::vector vertices; +// vertices.reserve(text.size()); - glm::vec2 offset {0.f, 0.f}; +// glm::vec2 offset {0.f, 0.f}; - forEachUnicodeCodepoint(text, [&](uint32_t cp) { - if (cp == '\n') { - offset.y -= pixel_size; - offset.x = 0; - return true; - } +// forEachUnicodeCodepoint(text, [&](uint32_t cp) { +// if (cp == '\n') { +// offset.y -= pixel_size; +// offset.x = 0; +// return true; +// } - const auto& fc = getFontChar(cp); +// const auto& fc = getFontCharOrTofu(cp); - float x = offset.x + fc.bearing.x; - float y = offset.y + fc.bearing.y - fc.size.y; +// float x = offset.x + fc.bearing.x; +// float y = offset.y + fc.bearing.y - fc.size.y; - const auto vertex_color = fc.is_icon ? glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) : color; +// const auto vertex_color = fc.is_icon ? glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) : color; - vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); - vertices.emplace_back(glm::vec2{x, y}, fc.uvs[0], vertex_color); - vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); +// vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); +// vertices.emplace_back(glm::vec2{x, y}, fc.uvs[0], vertex_color); +// vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); - vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); - vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); - vertices.emplace_back(glm::vec2{x + fc.size.x, y + fc.size.y}, fc.uvs[3], vertex_color); +// vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); +// vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); +// vertices.emplace_back(glm::vec2{x + fc.size.x, y + fc.size.y}, fc.uvs[3], vertex_color); - offset.x += (fc.advance >> 6); - return true; - }); +// offset.x += (fc.advance >> 6); +// return true; +// }); - return vertices; -} +// return vertices; +// } glm::vec2 FontAtlas::getTextSize(const std::string& text) const { glm::vec2 max_size {0, pixel_size}; float size {}; forEachUnicodeCodepoint(text, [&](uint32_t cp) { - const auto& fc = getFontChar(cp); + const auto& fc = getFontCharOrTofu(cp); size += (fc.advance >> 6); @@ -402,72 +417,82 @@ glm::vec2 FontAtlas::getTextSize(const std::string& text) const { return max_size; } -std::vector FontAtlas::getSelectionGeometry(std::string_view text, size_t begin, size_t end) const { - std::vector vertices; - - auto addRect = [&] (glm::vec2 pos, glm::vec2 size) { - vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}); - vertices.emplace_back(glm::vec2{pos.x, -pos.y}); - vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}); - - vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}); - vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}); - vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y + size.y}); - }; - - glm::ivec2 offset{}; - - // finds character max y value - for (const auto& [c, fc] : chars) { - offset.y = std::max(offset.y, fc.size.y - fc.bearing.y); +// std::vector FontAtlas::getSelectionGeometry(std::string_view text, size_t begin, size_t end) const { +// std::vector vertices; + +// auto addRect = [&] (glm::vec2 pos, glm::vec2 size) { +// vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}); +// vertices.emplace_back(glm::vec2{pos.x, -pos.y}); +// vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}); + +// vertices.emplace_back(glm::vec2{pos.x, -pos.y + size.y}); +// vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y}); +// vertices.emplace_back(glm::vec2{pos.x + size.x, -pos.y + size.y}); +// }; + +// glm::ivec2 offset{}; + +// // finds character max y value +// for (const auto& [c, fc] : chars) { +// offset.y = std::max(offset.y, fc.size.y - fc.bearing.y); +// } + +// size_t pos = 0; +// size_t size = 0; + +// forEachUnicodeCodepoint(text, [&](uint32_t cp) { +// if (pos < begin) { +// // Skip until selection range starts. +// offset.x += getFontChar(cp).advance >> 6; +// if (cp == '\n') { +// offset.x = 0; +// offset.y += pixel_size; +// } +// ++pos; +// return true; + +// } else if (pos < end) { +// if (cp == '\n') { +// if (size != 0) { +// // Finish this selection line. +// addRect({offset.x, offset.y}, glm::vec2{size, pixel_size}); +// size = 0; +// offset.x = 0; +// offset.y += pixel_size; +// } else { +// // An empty line, still add small selection geometry for it. +// size = getFontChar(' ').advance >> 6; +// } +// } else { +// size += getFontChar(cp).advance >> 6; +// } +// ++pos; +// return true; + +// } else { +// return false; +// } +// }); + +// addRect({offset.x, offset.y}, glm::vec2{size, pixel_size}); + +// return vertices; +// } + +const FontChar& FontAtlas::getFontCharOrTofu(uint32_t utf32_codepoint) const noexcept { + auto it = chars.find(utf32_codepoint); + if (it == chars.end()) { + return chars.at(UNDEFINED_GLYPH_CHAR_CODE); } - size_t pos = 0; - size_t size = 0; - - forEachUnicodeCodepoint(text, [&](uint32_t cp) { - if (pos < begin) { - // Skip until selection range starts. - offset.x += getFontChar(cp).advance >> 6; - if (cp == '\n') { - offset.x = 0; - offset.y += pixel_size; - } - ++pos; - return true; - - } else if (pos < end) { - if (cp == '\n') { - if (size != 0) { - // Finish this selection line. - addRect({offset.x, offset.y}, glm::vec2{size, pixel_size}); - size = 0; - offset.x = 0; - offset.y += pixel_size; - } else { - // An empty line, still add small selection geometry for it. - size = getFontChar(' ').advance >> 6; - } - } else { - size += getFontChar(cp).advance >> 6; - } - ++pos; - return true; - - } else { - return false; - } - }); - - addRect({offset.x, offset.y}, glm::vec2{size, pixel_size}); - - return vertices; + return it->second; } -const FontChar& FontAtlas::getFontChar(uint32_t utf32_codepoint) const noexcept { +std::optional> +FontAtlas::getFontChar(uint32_t utf32_codepoint) const noexcept { auto it = chars.find(utf32_codepoint); if (it == chars.end()) { - return chars.at(UNDEFINED_GLYPH_CHAR_CODE); + return std::nullopt; } return it->second; diff --git a/src/limitless/text/type_setter.cpp b/src/limitless/text/type_setter.cpp index 7aa37d97..d91279db 100644 --- a/src/limitless/text/type_setter.cpp +++ b/src/limitless/text/type_setter.cpp @@ -64,9 +64,22 @@ static std::vector split(const std::string& str, char separator) { return tokens; } +static const std::shared_ptr& getFontForChar( + uint32_t utf32_codepoint, + const std::vector>& font_stack +) { + for (const auto& font_sptr : font_stack) { + auto maybe_fc = font_sptr->getFontChar(utf32_codepoint); + if (maybe_fc) { + return font_sptr; + } + } + return font_stack[0]; +} + static std::string wordWrap( const std::string& original_text, - const FontAtlas& font, + const std::vector>& font_stack, float wrap_width, float curr_width ) { @@ -85,7 +98,8 @@ static std::string wordWrap( float result = 0.f; forEachUnicodeCodepoint(word, [&](uint32_t cp){ - const auto& fc = font.getFontChar(cp); + const auto& font = getFontForChar(cp, font_stack); + const auto& fc = font->getFontCharOrTofu(cp); result += (fc.advance >> 6); @@ -133,14 +147,18 @@ TypeSetResult TypeSetter::typeSet( for (const auto& formatted_text : formatted_text_parts) { const auto& color = formatted_text.format.color; - const auto& font = formatted_text.format.font; + const auto& font_stack = formatted_text.format.font_stack; const auto& wrap_width = formatted_text.format.wrap_width; + if (font_stack.empty()) { + throw font_error("empty font stack"); + } + const auto& text = wrap_width - ? wordWrap(formatted_text.text, *font, *wrap_width, offset.x) + ? wordWrap(formatted_text.text, font_stack, *wrap_width, offset.x) : formatted_text.text; - auto& vertices = [&]() -> std::vector& { + auto getVertices = [&](const std::shared_ptr& font) -> std::vector& { auto it = font_vertices.find(font.get()); if (it != font_vertices.end()) { return it->second.vertices; @@ -148,17 +166,18 @@ TypeSetResult TypeSetter::typeSet( font_vertices.emplace(font.get(), FontVertices(font, {})); return font_vertices.at(font.get()).vertices; - }(); + }; forEachUnicodeCodepoint(text, [&](uint32_t cp) -> bool { if (cp == '\n') { - offset.y -= font->getFontSize(); + offset.y -= font_stack[0]->getFontSize(); offset.x = 0; - // ++cp_index; + return true; } - const auto& fc = font->getFontChar(cp); + const auto& font = getFontForChar(cp, font_stack); + const auto& fc = font->getFontCharOrTofu(cp); float x = offset.x + fc.bearing.x; float y = offset.y + fc.bearing.y - fc.size.y; @@ -168,6 +187,8 @@ TypeSetResult TypeSetter::typeSet( const auto vertex_color = fc.is_icon ? glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) : color; + auto& vertices = getVertices(font); + vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); vertices.emplace_back(glm::vec2{x, y}, fc.uvs[0], vertex_color); vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); @@ -177,7 +198,7 @@ TypeSetResult TypeSetter::typeSet( vertices.emplace_back(glm::vec2{x + fc.size.x, y + fc.size.y}, fc.uvs[3], vertex_color); offset.x += (fc.advance >> 6); - // ++cp_index; + return true; }); }; @@ -215,13 +236,19 @@ std::vector TypeSetter::typeSetSelection( for (const auto& formatted_text : formatted_text_parts) { const auto& text = formatted_text.text; - const auto& font = formatted_text.format.font; + const auto& font_stack = formatted_text.format.font_stack; + if (font_stack.empty()) { + throw font_error("empty font stack"); + } forEachUnicodeCodepoint(text, [&](uint32_t cp) { - line_height = std::max(line_height, font->getFontSize()); + const auto& font = getFontForChar(cp, font_stack); + + line_height = std::max(line_height, font_stack[0]->getFontSize()); if (cp_index < start_codepoint_index) { // Skip until selection range starts. - offset.x += font->getFontChar(cp).advance >> 6; + + offset.x += font->getFontCharOrTofu(cp).advance >> 6; if (cp == '\n') { offset.x = 0; offset.y += line_height; @@ -241,10 +268,10 @@ std::vector TypeSetter::typeSetSelection( line_height = 0u; } else { // An empty line, still add small selection geometry for it. - line_width = font->getFontChar(' ').advance >> 6; + line_width = font->getFontCharOrTofu(' ').advance >> 6; } } else { - line_width += font->getFontChar(cp).advance >> 6; + line_width += font->getFontCharOrTofu(cp).advance >> 6; } ++cp_index; return true; @@ -259,33 +286,16 @@ std::vector TypeSetter::typeSetSelection( return vertices; } -glm::vec2 TypeSetter::getBoundingBoxSize(const std::vector& formatted_text_parts) { - if (formatted_text_parts.empty()) { - return glm::vec2(0.f, 0.f); +bool TextFormat::sameFontStack(const TextFormat& lhs, const TextFormat& rhs) { + if (lhs.font_stack.size() != rhs.font_stack.size()) { + return false; } - // TODO: compute first line height. - glm::vec2 max_size {0, formatted_text_parts[0].format.font->getFontSize()}; - float line_width = 0.f; - - for (const auto& formatted_text : formatted_text_parts) { - const auto& text = formatted_text.text; - const auto& font = formatted_text.format.font; - - forEachUnicodeCodepoint(text, [&](uint32_t cp) { - const auto& fc = font->getFontChar(cp); - - line_width += (fc.advance >> 6); - - if (cp == '\n') { - max_size.y += font->getFontSize(); - line_width = 0; - } - - max_size.x = std::max(max_size.x, line_width); - return true; - }); + for (size_t i = 0; i < lhs.font_stack.size(); ++i) { + if (lhs.font_stack[i].get() != rhs.font_stack[i].get()) { + return false; + } } - return max_size; + return true; } From d0501be22e38f6b797ff9d5572d1b132adf411fd Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Mon, 9 Jun 2025 19:27:21 +0900 Subject: [PATCH 03/30] fix text selection --- src/limitless/core/texture/texture.cpp | 6 ------ src/limitless/text/text_instance.cpp | 8 +++++++- src/limitless/text/type_setter.cpp | 4 +++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/limitless/core/texture/texture.cpp b/src/limitless/core/texture/texture.cpp index 12877d05..280f4b6e 100644 --- a/src/limitless/core/texture/texture.cpp +++ b/src/limitless/core/texture/texture.cpp @@ -323,10 +323,6 @@ std::vector Texture::getPixels() noexcept { std::vector pixels; pixels.resize(size.x * size.y * getBytesPerPixel()); - std::cerr << "getPixels size: " << size.x << " " << size.y << " " << getBytesPerPixel() << std::endl; - std::cerr << "getPixels format: " << formatToString(static_cast(format)) << " " << dataTypeToString(static_cast(data_type)) << std::endl; - std::cerr << "getPixels target: " << targetToString(static_cast(target)) << std::endl; - bind(0); glGetTexImage(static_cast(target), 0, // mipmap level @@ -334,8 +330,6 @@ std::vector Texture::getPixels() noexcept { static_cast(data_type), pixels.data()); - std::cerr << "getPixels pixels: " << bytesToHexString(pixels) << std::endl; - return pixels; } diff --git a/src/limitless/text/text_instance.cpp b/src/limitless/text/text_instance.cpp index f503259a..2db1eb1e 100644 --- a/src/limitless/text/text_instance.cpp +++ b/src/limitless/text/text_instance.cpp @@ -63,7 +63,13 @@ TextInstance& TextInstance::setSelectionColor(const glm::vec4& _color) noexcept } TextInstance& TextInstance::setSelection(size_t begin, size_t end) { - selection_model = TextSelectionModel{TypeSetter::typeSetSelection(formatted_text_parts, begin, end)}; + auto vertices = TypeSetter::typeSetSelection(formatted_text_parts, begin, end); + if (!selection_model) { + selection_model = TextSelectionModel(std::move(vertices)); + } else { + selection_model->update(std::move(vertices)); + } + return *this; } diff --git a/src/limitless/text/type_setter.cpp b/src/limitless/text/type_setter.cpp index d91279db..76dc394d 100644 --- a/src/limitless/text/type_setter.cpp +++ b/src/limitless/text/type_setter.cpp @@ -261,7 +261,7 @@ std::vector TypeSetter::typeSetSelection( if (cp == '\n') { if (line_width != 0) { // Finish this selection line. - addRect({offset.x, offset.y}, glm::vec2{line_width, line_height}); + addRect({offset.x, offset.y + line_height/4}, glm::vec2{line_width, line_height}); line_width = 0; offset.x = 0; offset.y += line_height; @@ -282,6 +282,8 @@ std::vector TypeSetter::typeSetSelection( }); } + addRect({offset.x, offset.y + line_height/4}, glm::vec2{line_width, line_height}); + return vertices; } From 995a7e7c2838b6d804b7d8268ae480586212974c Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Wed, 11 Jun 2025 09:10:04 +0900 Subject: [PATCH 04/30] add support for CJK variant selection in text format --- include/limitless/text/font_atlas.hpp | 15 ++++++++++-- include/limitless/text/type_setter.hpp | 8 ++++-- src/limitless/core/profiler.cpp | 3 ++- src/limitless/text/font_atlas.cpp | 34 +++++++++++++++++++++----- src/limitless/text/type_setter.cpp | 22 ++++++++++++++--- 5 files changed, 67 insertions(+), 15 deletions(-) diff --git a/include/limitless/text/font_atlas.hpp b/include/limitless/text/font_atlas.hpp index fcff0589..89dbe791 100644 --- a/include/limitless/text/font_atlas.hpp +++ b/include/limitless/text/font_atlas.hpp @@ -44,6 +44,12 @@ namespace Limitless { bool is_icon; }; + enum class CjkVariant { + CHINESE, + JAPANESE, + KOREAN + }; + struct font_error : public std::runtime_error { explicit font_error(const char* error) : runtime_error{error} { } explicit font_error(const std::string& error) : runtime_error{error} {} @@ -54,7 +60,8 @@ namespace Limitless { static std::shared_ptr load( const fs::path& path, uint32_t pixel_size, - std::vector> codepoint_ranges = {} + std::vector> codepoint_ranges = {}, + std::optional _cjk_variant = std::nullopt ); static std::shared_ptr make( @@ -66,7 +73,8 @@ namespace Limitless { std::unordered_map chars, std::shared_ptr texture, uint32_t pixel_size, - bool is_icon + bool is_icon, + std::optional cjk_variant ); FontAtlas(const FontAtlas&) = delete; @@ -92,6 +100,8 @@ namespace Limitless { [[nodiscard]] auto isIconAtlas() const noexcept { return is_icon; } + [[nodiscard]] auto getCjkVariant() const noexcept { return cjk_variant; } + /** * Return vertices for UTF-8 encoded string. */ @@ -112,5 +122,6 @@ namespace Limitless { std::shared_ptr texture; uint32_t pixel_size; bool is_icon; + std::optional cjk_variant; }; } diff --git a/include/limitless/text/type_setter.hpp b/include/limitless/text/type_setter.hpp index 0ada3485..d25f3810 100644 --- a/include/limitless/text/type_setter.hpp +++ b/include/limitless/text/type_setter.hpp @@ -15,22 +15,26 @@ namespace Limitless { glm::vec4 color; std::vector> font_stack; std::optional wrap_width; + std::optional cjk_variant; TextFormat( glm::vec4 _color, std::vector> _font_stack, - std::optional _wrap_width + std::optional _wrap_width, + std::optional _cjk_variant ) : color (_color) , font_stack (std::move(_font_stack)) , wrap_width (_wrap_width) + , cjk_variant (_cjk_variant) { } friend bool operator==(const TextFormat& lhs, const TextFormat& rhs) { return lhs.color == rhs.color && sameFontStack(lhs, rhs) - && lhs.wrap_width == rhs.wrap_width; + && lhs.wrap_width == rhs.wrap_width + && lhs.cjk_variant == rhs.cjk_variant; } friend bool operator!=(const TextFormat& lhs, const TextFormat& rhs) { diff --git a/src/limitless/core/profiler.cpp b/src/limitless/core/profiler.cpp index 09ceea3d..6bc62af7 100644 --- a/src/limitless/core/profiler.cpp +++ b/src/limitless/core/profiler.cpp @@ -8,7 +8,8 @@ void Profiler::draw(Context& ctx, const Assets& assets) { const auto text_format = TextFormat( /* color = */glm::vec4(1.f), {assets.fonts.at("nunito")}, - /* wrap_width =*/ std::nullopt + /* wrap_width =*/ std::nullopt, + /* cjk_variant =*/std::nullopt ); auto text = TextInstance({{"text", text_format}}, glm::vec2(0.f)); text.setSize(glm::vec2{0.5f}); diff --git a/src/limitless/text/font_atlas.cpp b/src/limitless/text/font_atlas.cpp index 0534c6f6..85aa46a5 100644 --- a/src/limitless/text/font_atlas.cpp +++ b/src/limitless/text/font_atlas.cpp @@ -70,7 +70,8 @@ static std::shared_ptr makeAtlas( std::unordered_map glyph_for_char, uint32_t pixel_size, size_t bytes_per_pixel, - bool is_icon + bool is_icon, + std::optional cjk_variant ) { std::unordered_map chars; @@ -178,19 +179,27 @@ static std::shared_ptr makeAtlas( .buildMutable(); glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - return std::make_shared(std::move(chars), std::move(texture), pixel_size, is_icon); + return std::make_shared( + std::move(chars), + std::move(texture), + pixel_size, + is_icon, + cjk_variant + ); } FontAtlas::FontAtlas( std::unordered_map chars, std::shared_ptr texture, uint32_t pixel_size, - bool _is_icon + bool _is_icon, + std::optional _cjk_variant ) : chars {std::move(chars)} , texture {std::move(texture)} , pixel_size {pixel_size} , is_icon (_is_icon) + , cjk_variant (_cjk_variant) { } @@ -227,13 +236,20 @@ std::shared_ptr FontAtlas::make( }); } - return makeAtlas(std::move(glyph_for_char), font_size_in_pixels, bytes_per_pixel, /* is_icon = */ true); + return makeAtlas( + std::move(glyph_for_char), + font_size_in_pixels, + bytes_per_pixel, + /* is_icon = */ true, + /* cjk_variant = */std::nullopt + ); } std::shared_ptr FontAtlas::load( const fs::path& path, uint32_t pixel_size, - std::vector> codepoint_ranges + std::vector> codepoint_ranges, + std::optional cjk_variant ) { static FT_Library ft {nullptr}; @@ -308,7 +324,13 @@ std::shared_ptr FontAtlas::load( FT_Done_Face(face); - return makeAtlas(std::move(glyph_for_char), pixel_size, /* bytes_per_pixel = */ 1, /* is_icon = */ false); + return makeAtlas( + std::move(glyph_for_char), + pixel_size, + /* bytes_per_pixel = */ 1, + /* is_icon = */ false, + cjk_variant + ); } FontAtlas::~FontAtlas() = default; diff --git a/src/limitless/text/type_setter.cpp b/src/limitless/text/type_setter.cpp index 76dc394d..3dcacdb8 100644 --- a/src/limitless/text/type_setter.cpp +++ b/src/limitless/text/type_setter.cpp @@ -66,9 +66,18 @@ static std::vector split(const std::string& str, char separator) { static const std::shared_ptr& getFontForChar( uint32_t utf32_codepoint, - const std::vector>& font_stack + const std::vector>& font_stack, + std::optional cjk_variant ) { for (const auto& font_sptr : font_stack) { + if (cjk_variant) { + const auto font_cjk_variant = font_sptr->getCjkVariant(); + if (font_cjk_variant && *cjk_variant != *font_cjk_variant) { + // skip font if its for wrong CJK variant. + continue; + } + } + auto maybe_fc = font_sptr->getFontChar(utf32_codepoint); if (maybe_fc) { return font_sptr; @@ -84,6 +93,7 @@ static std::string wordWrap( float curr_width ) { std::string word_wrapped_text; + // TODO: investigate word split for CJK. auto lines = split(original_text, '\n'); for (const auto& line : lines) { @@ -98,7 +108,8 @@ static std::string wordWrap( float result = 0.f; forEachUnicodeCodepoint(word, [&](uint32_t cp){ - const auto& font = getFontForChar(cp, font_stack); + // TODO: this might result in wrong word wrap if diff fonts are selected due to CJK variance. + const auto& font = getFontForChar(cp, font_stack, /* cjk_variant = */std::nullopt); const auto& fc = font->getFontCharOrTofu(cp); result += (fc.advance >> 6); @@ -149,6 +160,7 @@ TypeSetResult TypeSetter::typeSet( const auto& color = formatted_text.format.color; const auto& font_stack = formatted_text.format.font_stack; const auto& wrap_width = formatted_text.format.wrap_width; + const auto& cjk_variant = formatted_text.format.cjk_variant; if (font_stack.empty()) { throw font_error("empty font stack"); @@ -176,7 +188,7 @@ TypeSetResult TypeSetter::typeSet( return true; } - const auto& font = getFontForChar(cp, font_stack); + const auto& font = getFontForChar(cp, font_stack, cjk_variant); const auto& fc = font->getFontCharOrTofu(cp); float x = offset.x + fc.bearing.x; @@ -237,12 +249,14 @@ std::vector TypeSetter::typeSetSelection( for (const auto& formatted_text : formatted_text_parts) { const auto& text = formatted_text.text; const auto& font_stack = formatted_text.format.font_stack; + const auto& cjk_variant = formatted_text.format.cjk_variant; + if (font_stack.empty()) { throw font_error("empty font stack"); } forEachUnicodeCodepoint(text, [&](uint32_t cp) { - const auto& font = getFontForChar(cp, font_stack); + const auto& font = getFontForChar(cp, font_stack, cjk_variant); line_height = std::max(line_height, font_stack[0]->getFontSize()); if (cp_index < start_codepoint_index) { From 68f5b78ce66c4b70a391600a7c817aa03d4ec9b3 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Wed, 11 Jun 2025 20:53:37 +0900 Subject: [PATCH 05/30] implement line spacing modifier --- include/limitless/text/type_setter.hpp | 8 ++++++-- src/limitless/core/profiler.cpp | 3 ++- src/limitless/text/type_setter.cpp | 8 +++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/include/limitless/text/type_setter.hpp b/include/limitless/text/type_setter.hpp index d25f3810..8b4c572b 100644 --- a/include/limitless/text/type_setter.hpp +++ b/include/limitless/text/type_setter.hpp @@ -16,17 +16,20 @@ namespace Limitless { std::vector> font_stack; std::optional wrap_width; std::optional cjk_variant; + float line_spacing_modifier; TextFormat( glm::vec4 _color, std::vector> _font_stack, std::optional _wrap_width, - std::optional _cjk_variant + std::optional _cjk_variant, + float _line_spacing_modifier ) : color (_color) , font_stack (std::move(_font_stack)) , wrap_width (_wrap_width) , cjk_variant (_cjk_variant) + , line_spacing_modifier (_line_spacing_modifier) { } @@ -34,7 +37,8 @@ namespace Limitless { return lhs.color == rhs.color && sameFontStack(lhs, rhs) && lhs.wrap_width == rhs.wrap_width - && lhs.cjk_variant == rhs.cjk_variant; + && lhs.cjk_variant == rhs.cjk_variant + && lhs.line_spacing_modifier == rhs.line_spacing_modifier; } friend bool operator!=(const TextFormat& lhs, const TextFormat& rhs) { diff --git a/src/limitless/core/profiler.cpp b/src/limitless/core/profiler.cpp index 6bc62af7..2c5d5936 100644 --- a/src/limitless/core/profiler.cpp +++ b/src/limitless/core/profiler.cpp @@ -9,7 +9,8 @@ void Profiler::draw(Context& ctx, const Assets& assets) { /* color = */glm::vec4(1.f), {assets.fonts.at("nunito")}, /* wrap_width =*/ std::nullopt, - /* cjk_variant =*/std::nullopt + /* cjk_variant = */std::nullopt, + /* line_spacing_modifier = */1.f ); auto text = TextInstance({{"text", text_format}}, glm::vec2(0.f)); text.setSize(glm::vec2{0.5f}); diff --git a/src/limitless/text/type_setter.cpp b/src/limitless/text/type_setter.cpp index 3dcacdb8..77a61fbd 100644 --- a/src/limitless/text/type_setter.cpp +++ b/src/limitless/text/type_setter.cpp @@ -161,6 +161,7 @@ TypeSetResult TypeSetter::typeSet( const auto& font_stack = formatted_text.format.font_stack; const auto& wrap_width = formatted_text.format.wrap_width; const auto& cjk_variant = formatted_text.format.cjk_variant; + const auto line_spacing_modifier = formatted_text.format.line_spacing_modifier; if (font_stack.empty()) { throw font_error("empty font stack"); @@ -182,7 +183,7 @@ TypeSetResult TypeSetter::typeSet( forEachUnicodeCodepoint(text, [&](uint32_t cp) -> bool { if (cp == '\n') { - offset.y -= font_stack[0]->getFontSize(); + offset.y -= font_stack[0]->getFontSize() * line_spacing_modifier; offset.x = 0; return true; @@ -250,6 +251,7 @@ std::vector TypeSetter::typeSetSelection( const auto& text = formatted_text.text; const auto& font_stack = formatted_text.format.font_stack; const auto& cjk_variant = formatted_text.format.cjk_variant; + const auto line_spacing_modifier = formatted_text.format.line_spacing_modifier; if (font_stack.empty()) { throw font_error("empty font stack"); @@ -265,7 +267,7 @@ std::vector TypeSetter::typeSetSelection( offset.x += font->getFontCharOrTofu(cp).advance >> 6; if (cp == '\n') { offset.x = 0; - offset.y += line_height; + offset.y += line_height * line_spacing_modifier; line_height = 0u; } ++cp_index; @@ -278,7 +280,7 @@ std::vector TypeSetter::typeSetSelection( addRect({offset.x, offset.y + line_height/4}, glm::vec2{line_width, line_height}); line_width = 0; offset.x = 0; - offset.y += line_height; + offset.y += line_height * line_spacing_modifier; line_height = 0u; } else { // An empty line, still add small selection geometry for it. From 33094d617e316cfb3fde3f635dd4eacdfd75cc88 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Thu, 12 Jun 2025 21:19:42 +0900 Subject: [PATCH 06/30] fix LightContainer::add --- src/limitless/lighting/light_container.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/limitless/lighting/light_container.cpp b/src/limitless/lighting/light_container.cpp index 808bcb10..b8934277 100644 --- a/src/limitless/lighting/light_container.cpp +++ b/src/limitless/lighting/light_container.cpp @@ -77,7 +77,7 @@ Light& LightContainer::add(Light&& light) { Light& LightContainer::add(const Light& light) { // add new light to all lights auto copy = light; - lights.emplace(light.getId(), std::move(copy)); + lights.emplace(copy.getId(), std::move(copy)); // add corresponding internal presentation internal_lights.emplace(copy.getId(), light); From 6e7dffce0b001d02944534bfc0496e7abbbf27bd Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Mon, 16 Jun 2025 20:21:04 +0900 Subject: [PATCH 07/30] add pixel size override for type setter --- include/limitless/text/type_setter.hpp | 8 ++++-- src/limitless/core/profiler.cpp | 3 +- src/limitless/text/type_setter.cpp | 39 +++++++++++++++----------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/include/limitless/text/type_setter.hpp b/include/limitless/text/type_setter.hpp index 8b4c572b..d1bfdac5 100644 --- a/include/limitless/text/type_setter.hpp +++ b/include/limitless/text/type_setter.hpp @@ -17,19 +17,22 @@ namespace Limitless { std::optional wrap_width; std::optional cjk_variant; float line_spacing_modifier; + std::optional pixel_size; TextFormat( glm::vec4 _color, std::vector> _font_stack, std::optional _wrap_width, std::optional _cjk_variant, - float _line_spacing_modifier + float _line_spacing_modifier, + std::optional _pixel_size ) : color (_color) , font_stack (std::move(_font_stack)) , wrap_width (_wrap_width) , cjk_variant (_cjk_variant) , line_spacing_modifier (_line_spacing_modifier) + , pixel_size (_pixel_size) { } @@ -38,7 +41,8 @@ namespace Limitless { && sameFontStack(lhs, rhs) && lhs.wrap_width == rhs.wrap_width && lhs.cjk_variant == rhs.cjk_variant - && lhs.line_spacing_modifier == rhs.line_spacing_modifier; + && lhs.line_spacing_modifier == rhs.line_spacing_modifier + && lhs.pixel_size == rhs.pixel_size; } friend bool operator!=(const TextFormat& lhs, const TextFormat& rhs) { diff --git a/src/limitless/core/profiler.cpp b/src/limitless/core/profiler.cpp index 2c5d5936..07841a55 100644 --- a/src/limitless/core/profiler.cpp +++ b/src/limitless/core/profiler.cpp @@ -10,7 +10,8 @@ void Profiler::draw(Context& ctx, const Assets& assets) { {assets.fonts.at("nunito")}, /* wrap_width =*/ std::nullopt, /* cjk_variant = */std::nullopt, - /* line_spacing_modifier = */1.f + /* line_spacing_modifier = */1.f, + /* pixel_size = */std::nullopt ); auto text = TextInstance({{"text", text_format}}, glm::vec2(0.f)); text.setSize(glm::vec2{0.5f}); diff --git a/src/limitless/text/type_setter.cpp b/src/limitless/text/type_setter.cpp index 77a61fbd..4ac76db9 100644 --- a/src/limitless/text/type_setter.cpp +++ b/src/limitless/text/type_setter.cpp @@ -182,35 +182,41 @@ TypeSetResult TypeSetter::typeSet( }; forEachUnicodeCodepoint(text, [&](uint32_t cp) -> bool { + const auto& font = getFontForChar(cp, font_stack, cjk_variant); + const auto scale = formatted_text.format.pixel_size + ? static_cast(*formatted_text.format.pixel_size) / font->getFontSize() : 1.0f; + if (cp == '\n') { - offset.y -= font_stack[0]->getFontSize() * line_spacing_modifier; + offset.y -= font->getFontSize() * line_spacing_modifier * scale; offset.x = 0; return true; } - const auto& font = getFontForChar(cp, font_stack, cjk_variant); const auto& fc = font->getFontCharOrTofu(cp); - float x = offset.x + fc.bearing.x; - float y = offset.y + fc.bearing.y - fc.size.y; + const auto width = fc.size.x * scale; + const auto height = fc.size.y * scale; + + float x = offset.x + fc.bearing.x * scale; + float y = offset.y + fc.bearing.y * scale - height; min_pos = glm::vec2(std::min(min_pos.x, x), std::min(min_pos.y, y)); - max_pos = glm::vec2(std::max(max_pos.x, x + fc.size.x), std::max(max_pos.y, y + fc.size.y)); + max_pos = glm::vec2(std::max(max_pos.x, x + width), std::max(max_pos.y, y + height)); const auto vertex_color = fc.is_icon ? glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) : color; auto& vertices = getVertices(font); - vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); + vertices.emplace_back(glm::vec2{x, y + height}, fc.uvs[2], vertex_color); vertices.emplace_back(glm::vec2{x, y}, fc.uvs[0], vertex_color); - vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); + vertices.emplace_back(glm::vec2{x + width, y}, fc.uvs[1], vertex_color); - vertices.emplace_back(glm::vec2{x, y + fc.size.y}, fc.uvs[2], vertex_color); - vertices.emplace_back(glm::vec2{x + fc.size.x, y}, fc.uvs[1], vertex_color); - vertices.emplace_back(glm::vec2{x + fc.size.x, y + fc.size.y}, fc.uvs[3], vertex_color); + vertices.emplace_back(glm::vec2{x, y + height}, fc.uvs[2], vertex_color); + vertices.emplace_back(glm::vec2{x + width, y}, fc.uvs[1], vertex_color); + vertices.emplace_back(glm::vec2{x + width, y + height}, fc.uvs[3], vertex_color); - offset.x += (fc.advance >> 6); + offset.x += (fc.advance >> 6) * scale; return true; }); @@ -259,12 +265,14 @@ std::vector TypeSetter::typeSetSelection( forEachUnicodeCodepoint(text, [&](uint32_t cp) { const auto& font = getFontForChar(cp, font_stack, cjk_variant); + const auto scale = formatted_text.format.pixel_size + ? static_cast(*formatted_text.format.pixel_size) / font->getFontSize() : 1.0f; - line_height = std::max(line_height, font_stack[0]->getFontSize()); + line_height = std::max(line_height, static_cast(font->getFontSize() * scale)); if (cp_index < start_codepoint_index) { // Skip until selection range starts. - offset.x += font->getFontCharOrTofu(cp).advance >> 6; + offset.x += (font->getFontCharOrTofu(cp).advance >> 6) * scale; if (cp == '\n') { offset.x = 0; offset.y += line_height * line_spacing_modifier; @@ -284,10 +292,10 @@ std::vector TypeSetter::typeSetSelection( line_height = 0u; } else { // An empty line, still add small selection geometry for it. - line_width = font->getFontCharOrTofu(' ').advance >> 6; + line_width = (font->getFontCharOrTofu(' ').advance >> 6) * scale; } } else { - line_width += font->getFontCharOrTofu(cp).advance >> 6; + line_width += (font->getFontCharOrTofu(cp).advance >> 6) * scale; } ++cp_index; return true; @@ -300,7 +308,6 @@ std::vector TypeSetter::typeSetSelection( addRect({offset.x, offset.y + line_height/4}, glm::vec2{line_width, line_height}); - return vertices; } From 14765958d820c169894342e2aa3291178db0f7ac Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Mon, 16 Jun 2025 22:22:49 +0900 Subject: [PATCH 08/30] add links support to text instance --- include/limitless/text/text_instance.hpp | 4 +++ include/limitless/text/type_setter.hpp | 16 ++++++++-- src/limitless/core/profiler.cpp | 3 +- src/limitless/text/text_instance.cpp | 2 ++ src/limitless/text/type_setter.cpp | 37 ++++++++++++++++++++++-- 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/include/limitless/text/text_instance.hpp b/include/limitless/text/text_instance.hpp index 71739e1f..57343565 100644 --- a/include/limitless/text/text_instance.hpp +++ b/include/limitless/text/text_instance.hpp @@ -26,6 +26,7 @@ namespace Limitless { glm::vec4 color {1.0f}; glm::vec2 size {1.0f}; std::pair bounding_box {glm::vec2(0.f),glm::vec2(0.f)}; + std::unordered_map>> link_rectangles; glm::vec4 selection_color {}; bool hidden {false}; @@ -52,6 +53,9 @@ namespace Limitless { [[nodiscard]] auto isHidden() const noexcept { return hidden; } [[nodiscard]] auto isVisible() const noexcept { return !hidden; } + // TODO: take scale into account. + [[nodiscard]] const auto& getLinkRectangles() const noexcept { return link_rectangles; } + /** * In model space. */ diff --git a/include/limitless/text/type_setter.hpp b/include/limitless/text/type_setter.hpp index d1bfdac5..8a2224e6 100644 --- a/include/limitless/text/type_setter.hpp +++ b/include/limitless/text/type_setter.hpp @@ -18,6 +18,7 @@ namespace Limitless { std::optional cjk_variant; float line_spacing_modifier; std::optional pixel_size; + std::optional link_id; TextFormat( glm::vec4 _color, @@ -25,7 +26,8 @@ namespace Limitless { std::optional _wrap_width, std::optional _cjk_variant, float _line_spacing_modifier, - std::optional _pixel_size + std::optional _pixel_size, + std::optional _link_id ) : color (_color) , font_stack (std::move(_font_stack)) @@ -33,6 +35,7 @@ namespace Limitless { , cjk_variant (_cjk_variant) , line_spacing_modifier (_line_spacing_modifier) , pixel_size (_pixel_size) + , link_id (_link_id) { } @@ -42,7 +45,8 @@ namespace Limitless { && lhs.wrap_width == rhs.wrap_width && lhs.cjk_variant == rhs.cjk_variant && lhs.line_spacing_modifier == rhs.line_spacing_modifier - && lhs.pixel_size == rhs.pixel_size; + && lhs.pixel_size == rhs.pixel_size + && lhs.link_id == rhs.link_id; } friend bool operator!=(const TextFormat& lhs, const TextFormat& rhs) { @@ -83,10 +87,16 @@ namespace Limitless { struct TypeSetResult { std::vector vertices_of_fonts; std::pair bounding_box; + std::unordered_map>> link_rectangles; - TypeSetResult(std::vector _vertices_of_fonts, std::pair _bounding_box) + TypeSetResult( + std::vector _vertices_of_fonts, + std::pair _bounding_box, + std::unordered_map>> _link_rectangles + ) : vertices_of_fonts(std::move(_vertices_of_fonts)) , bounding_box(std::move(_bounding_box)) + , link_rectangles(std::move(_link_rectangles)) { } diff --git a/src/limitless/core/profiler.cpp b/src/limitless/core/profiler.cpp index 07841a55..0c07c2c3 100644 --- a/src/limitless/core/profiler.cpp +++ b/src/limitless/core/profiler.cpp @@ -11,7 +11,8 @@ void Profiler::draw(Context& ctx, const Assets& assets) { /* wrap_width =*/ std::nullopt, /* cjk_variant = */std::nullopt, /* line_spacing_modifier = */1.f, - /* pixel_size = */std::nullopt + /* pixel_size = */std::nullopt, + /* link_id = */std::nullopt ); auto text = TextInstance({{"text", text_format}}, glm::vec2(0.f)); text.setSize(glm::vec2{0.5f}); diff --git a/src/limitless/text/text_instance.cpp b/src/limitless/text/text_instance.cpp index 2db1eb1e..357f1b04 100644 --- a/src/limitless/text/text_instance.cpp +++ b/src/limitless/text/text_instance.cpp @@ -21,6 +21,7 @@ TextInstance::TextInstance( { auto type_set_result = TypeSetter::typeSet(formatted_text_parts); bounding_box = type_set_result.bounding_box; + link_rectangles = std::move(type_set_result.link_rectangles); for (auto& fv : type_set_result.vertices_of_fonts) { font_text_models.push_back(FontTextModel{ TextModel(std::move(fv.vertices)), fv.font @@ -48,6 +49,7 @@ TextInstance& TextInstance::setText(std::vector _formatted_text_p font_text_models.clear(); bounding_box = type_set_result.bounding_box; + link_rectangles = std::move(type_set_result.link_rectangles); for (auto& fv : type_set_result.vertices_of_fonts) { font_text_models.push_back(FontTextModel{ TextModel(std::move(fv.vertices)), fv.font diff --git a/src/limitless/text/type_setter.cpp b/src/limitless/text/type_setter.cpp index 4ac76db9..34011066 100644 --- a/src/limitless/text/type_setter.cpp +++ b/src/limitless/text/type_setter.cpp @@ -150,11 +150,15 @@ TypeSetResult TypeSetter::typeSet( glm::vec2& min_pos = bounding_box.first; glm::vec2& max_pos = bounding_box.second; + std::unordered_map>> link_rectangles; + if (formatted_text_parts.empty()) { - return TypeSetResult({}, bounding_box); + return TypeSetResult({}, bounding_box, {}); } glm::vec2 offset {0.f, 0.f}; + std::optional line_min_y = std::nullopt; + std::optional line_max_y = std::nullopt; for (const auto& formatted_text : formatted_text_parts) { const auto& color = formatted_text.format.color; @@ -162,6 +166,9 @@ TypeSetResult TypeSetter::typeSet( const auto& wrap_width = formatted_text.format.wrap_width; const auto& cjk_variant = formatted_text.format.cjk_variant; const auto line_spacing_modifier = formatted_text.format.line_spacing_modifier; + const auto& link_id = formatted_text.format.link_id; + + float part_x = offset.x; if (font_stack.empty()) { throw font_error("empty font stack"); @@ -187,8 +194,19 @@ TypeSetResult TypeSetter::typeSet( ? static_cast(*formatted_text.format.pixel_size) / font->getFontSize() : 1.0f; if (cp == '\n') { + const auto rect_width = offset.x - part_x; + if (link_id && rect_width > 0 && line_min_y && line_max_y) { + link_rectangles[*link_id].emplace_back( + glm::vec2{part_x, *line_min_y}, + glm::vec2{rect_width, *line_max_y - *line_min_y} + ); + } offset.y -= font->getFontSize() * line_spacing_modifier * scale; offset.x = 0; + part_x = 0.f; + + line_min_y = std::nullopt; + line_max_y = std::nullopt; return true; } @@ -201,6 +219,14 @@ TypeSetResult TypeSetter::typeSet( float x = offset.x + fc.bearing.x * scale; float y = offset.y + fc.bearing.y * scale - height; + if (!line_min_y || *line_min_y > y) { + line_min_y = y; + } + + if (!line_max_y || *line_max_y < (y + height)) { + line_max_y = y + height; + } + min_pos = glm::vec2(std::min(min_pos.x, x), std::min(min_pos.y, y)); max_pos = glm::vec2(std::max(max_pos.x, x + width), std::max(max_pos.y, y + height)); @@ -220,6 +246,13 @@ TypeSetResult TypeSetter::typeSet( return true; }); + + if (link_id && (offset.x - part_x) > 0 && line_min_y && line_max_y) { + link_rectangles[*link_id].emplace_back( + glm::vec2{part_x, *line_min_y}, + glm::vec2{offset.x - part_x, *line_max_y - *line_min_y} + ); + } }; std::vector vertices_per_font; @@ -228,7 +261,7 @@ TypeSetResult TypeSetter::typeSet( vertices_per_font.emplace_back(std::move(vertices)); } - return TypeSetResult(std::move(vertices_per_font), bounding_box); + return TypeSetResult(std::move(vertices_per_font), bounding_box, std::move(link_rectangles)); } std::vector TypeSetter::typeSetSelection( From 4fcf0651fde185b53e60af447d1a40156e71fbbc Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Sat, 21 Jun 2025 09:40:16 +0900 Subject: [PATCH 09/30] fix includes --- include/limitless/serialization/asset_deserializer.hpp | 1 + include/limitless/serialization/distribution_serializer.hpp | 1 + include/limitless/serialization/effect_serializer.hpp | 1 + include/limitless/serialization/emitter_serializer.hpp | 1 + include/limitless/serialization/material_serializer.hpp | 1 + include/limitless/serialization/module_serializer.hpp | 2 ++ include/limitless/serialization/uniform_serializer.hpp | 1 + 7 files changed, 8 insertions(+) diff --git a/include/limitless/serialization/asset_deserializer.hpp b/include/limitless/serialization/asset_deserializer.hpp index 430842f8..1f1092c2 100644 --- a/include/limitless/serialization/asset_deserializer.hpp +++ b/include/limitless/serialization/asset_deserializer.hpp @@ -2,6 +2,7 @@ #include +#include #include namespace Limitless { diff --git a/include/limitless/serialization/distribution_serializer.hpp b/include/limitless/serialization/distribution_serializer.hpp index 666e49f3..da8d87f1 100644 --- a/include/limitless/serialization/distribution_serializer.hpp +++ b/include/limitless/serialization/distribution_serializer.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace Limitless { template class Distribution; diff --git a/include/limitless/serialization/effect_serializer.hpp b/include/limitless/serialization/effect_serializer.hpp index 2fc33d85..d363aef5 100644 --- a/include/limitless/serialization/effect_serializer.hpp +++ b/include/limitless/serialization/effect_serializer.hpp @@ -2,6 +2,7 @@ #include #include +#include namespace Limitless { class EffectInstance; diff --git a/include/limitless/serialization/emitter_serializer.hpp b/include/limitless/serialization/emitter_serializer.hpp index 71539426..fa895389 100644 --- a/include/limitless/serialization/emitter_serializer.hpp +++ b/include/limitless/serialization/emitter_serializer.hpp @@ -2,6 +2,7 @@ #include #include +#include namespace Limitless { class ByteBuffer; diff --git a/include/limitless/serialization/material_serializer.hpp b/include/limitless/serialization/material_serializer.hpp index 25d061e7..7a37016c 100644 --- a/include/limitless/serialization/material_serializer.hpp +++ b/include/limitless/serialization/material_serializer.hpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace Limitless { class Context; diff --git a/include/limitless/serialization/module_serializer.hpp b/include/limitless/serialization/module_serializer.hpp index 75621172..911ea8af 100644 --- a/include/limitless/serialization/module_serializer.hpp +++ b/include/limitless/serialization/module_serializer.hpp @@ -31,6 +31,8 @@ #include #include +#include + namespace Limitless { template class ModuleSerializer { diff --git a/include/limitless/serialization/uniform_serializer.hpp b/include/limitless/serialization/uniform_serializer.hpp index 1d0c3ba5..f6c37b98 100644 --- a/include/limitless/serialization/uniform_serializer.hpp +++ b/include/limitless/serialization/uniform_serializer.hpp @@ -2,6 +2,7 @@ #include #include +#include namespace Limitless { class Uniform; From e990091b17e5cec9260299b355d12c3a99220bf1 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Tue, 8 Jul 2025 12:36:44 +0900 Subject: [PATCH 10/30] split composite pass into bloom enabled version --- include/limitless/renderer/composite_pass.hpp | 35 ++++++++------ include/limitless/renderer/renderer.hpp | 1 + shaders/pipeline/composite.frag | 5 +- shaders/pipeline/composite_with_bloom.frag | 29 ++++++++++++ shaders/pipeline/composite_with_bloom.vert | 11 +++++ src/limitless/renderer/composite_pass.cpp | 46 +++++++++++++++++-- src/limitless/renderer/deferred.cpp | 6 ++- src/limitless/renderer/renderer.cpp | 11 ++++- src/limitless/renderer/screen_pass.cpp | 2 +- src/limitless/shader_storage.cpp | 6 ++- 10 files changed, 125 insertions(+), 27 deletions(-) create mode 100644 shaders/pipeline/composite_with_bloom.frag create mode 100644 shaders/pipeline/composite_with_bloom.vert diff --git a/include/limitless/renderer/composite_pass.hpp b/include/limitless/renderer/composite_pass.hpp index 3a0b4ddf..b6572704 100644 --- a/include/limitless/renderer/composite_pass.hpp +++ b/include/limitless/renderer/composite_pass.hpp @@ -5,17 +5,7 @@ #include namespace Limitless { - /** - * CompositePass combines results from previous passes together - * - * result from transparent pass (lighting image + transparent objects) combined with bloom pass result - */ - class CompositePass final : public RendererPass { - private: - /** - * Result framebuffer - */ - Framebuffer framebuffer; + class CompositePass : public RendererPass { public: float tone_mapping_exposure = 1.0f; float gamma = 2.2f; @@ -24,14 +14,31 @@ namespace Limitless { std::shared_ptr getResult(); - /** - * - */ void render(InstanceRenderer &renderer, Scene &scene, Context &ctx, const Assets &assets, const Camera &camera, UniformSetter &setter) override; /** * Updates framebuffer size */ void onFramebufferChange(glm::uvec2 size) override; + + ~CompositePass() override = default; + + protected: + /** + * Result framebuffer + */ + Framebuffer framebuffer; + }; + + /** + * CompositeWithBloomPass combines results from previous passes together + * + * result from transparent pass (lighting image + transparent objects) combined with bloom pass result + */ + class CompositeWithBloomPass final : public CompositePass { + public: + explicit CompositeWithBloomPass(Renderer& renderer); + + void render(InstanceRenderer &renderer, Scene &scene, Context &ctx, const Assets &assets, const Camera &camera, UniformSetter &setter) override; }; } \ No newline at end of file diff --git a/include/limitless/renderer/renderer.hpp b/include/limitless/renderer/renderer.hpp index cab2c38b..d146846b 100644 --- a/include/limitless/renderer/renderer.hpp +++ b/include/limitless/renderer/renderer.hpp @@ -124,6 +124,7 @@ namespace Limitless { Builder& addTranslucentPass(); Builder& addBloomPass(); Builder& addOutlinePass(); + Builder& addCompositeWithBloomPass(); Builder& addCompositePass(); Builder& addFXAAPass(); Builder& addScreenPass(); diff --git a/shaders/pipeline/composite.frag b/shaders/pipeline/composite.frag index 3c63fbcf..6a9fcb90 100644 --- a/shaders/pipeline/composite.frag +++ b/shaders/pipeline/composite.frag @@ -8,15 +8,12 @@ out vec3 color; uniform sampler2D lightened; -uniform sampler2D bloom; uniform sampler2D outline; -uniform float bloom_strength; uniform float tone_mapping_exposure; uniform float gamma; void main() { - vec3 bloom_color = texture(bloom, uv).rgb * bloom_strength; - color = texture(lightened, uv).rgb + bloom_color; + color = texture(lightened, uv).rgb; // apply tone mapping function to HDR color = toneMapping(color, tone_mapping_exposure); diff --git a/shaders/pipeline/composite_with_bloom.frag b/shaders/pipeline/composite_with_bloom.frag new file mode 100644 index 00000000..3c63fbcf --- /dev/null +++ b/shaders/pipeline/composite_with_bloom.frag @@ -0,0 +1,29 @@ +ENGINE::COMMON + +#include "../functions/tone_mapping.glsl" + +in vec2 uv; + +out vec3 color; + +uniform sampler2D lightened; + +uniform sampler2D bloom; +uniform sampler2D outline; +uniform float bloom_strength; +uniform float tone_mapping_exposure; +uniform float gamma; + +void main() { + vec3 bloom_color = texture(bloom, uv).rgb * bloom_strength; + color = texture(lightened, uv).rgb + bloom_color; + + // apply tone mapping function to HDR + color = toneMapping(color, tone_mapping_exposure); + + // apply gamma correction + color = pow(color, vec3(1.0 / gamma)); + + // add objects outlining + color += texture(outline, uv).rgb; +} \ No newline at end of file diff --git a/shaders/pipeline/composite_with_bloom.vert b/shaders/pipeline/composite_with_bloom.vert new file mode 100644 index 00000000..2dade937 --- /dev/null +++ b/shaders/pipeline/composite_with_bloom.vert @@ -0,0 +1,11 @@ +ENGINE::COMMON + +layout (location = 0) in vec3 vertex_position; +layout (location = 1) in vec2 vertex_uv; + +out vec2 uv; + +void main() { + uv = vertex_uv; + gl_Position = vec4(vertex_position, 1.0); +} \ No newline at end of file diff --git a/src/limitless/renderer/composite_pass.cpp b/src/limitless/renderer/composite_pass.cpp index 21c0241b..518a4908 100644 --- a/src/limitless/renderer/composite_pass.cpp +++ b/src/limitless/renderer/composite_pass.cpp @@ -20,6 +20,46 @@ std::shared_ptr CompositePass::getResult() { } void CompositePass::render( + [[maybe_unused]] InstanceRenderer &instance_renderer, + [[maybe_unused]] Scene &scene, + Context &ctx, + const Assets &assets, + [[maybe_unused]] const Camera &camera, + [[maybe_unused]] UniformSetter &setter +) { + + ctx.disable(Capabilities::DepthTest); + ctx.disable(Capabilities::Blending); + + { + ctx.setViewPort(getResult()->getSize()); + framebuffer.clear(); + + auto& shader = assets.shaders.get("composite"); + + shader.setUniform("lightened", renderer.getPass().getResult()); + + { + shader.setUniform("outline", renderer.getPass().getResult()) + .setUniform("tone_mapping_exposure", tone_mapping_exposure) + .setUniform("gamma", gamma); + } + + shader.use(); + + assets.meshes.at("quad")->draw(); + } +} + +void CompositePass::onFramebufferChange(glm::uvec2 size) { + framebuffer.onFramebufferChange(size); +} + +CompositeWithBloomPass::CompositeWithBloomPass(Renderer& renderer) + : CompositePass {renderer} { +} + +void CompositeWithBloomPass::render( [[maybe_unused]] InstanceRenderer &instance_renderer, [[maybe_unused]] Scene &scene, Context &ctx, @@ -34,7 +74,7 @@ void CompositePass::render( ctx.setViewPort(getResult()->getSize()); framebuffer.clear(); - auto& shader = assets.shaders.get("composite"); + auto& shader = assets.shaders.get("composite_with_bloom"); shader.setUniform("lightened", renderer.getPass().getResult()); @@ -55,7 +95,3 @@ void CompositePass::render( assets.meshes.at("quad")->draw(); } } - -void CompositePass::onFramebufferChange(glm::uvec2 size) { - framebuffer.onFramebufferChange(size); -} diff --git a/src/limitless/renderer/deferred.cpp b/src/limitless/renderer/deferred.cpp index 0f0b92df..12acf9b5 100644 --- a/src/limitless/renderer/deferred.cpp +++ b/src/limitless/renderer/deferred.cpp @@ -108,7 +108,11 @@ void Deferred::build(Context& ctx, const RendererSettings& settings) { /* * Combines shaded translucent result and bloom */ - add(size); + if (settings.bloom) { + add(size); + } else { + add(size); + } // if (settings.fast_approximate_antialiasing) { // add(size); diff --git a/src/limitless/renderer/renderer.cpp b/src/limitless/renderer/renderer.cpp index 8681f3ee..e5160fff 100644 --- a/src/limitless/renderer/renderer.cpp +++ b/src/limitless/renderer/renderer.cpp @@ -132,6 +132,11 @@ Renderer::Builder &Renderer::Builder::addOutlinePass() { return *this; } +Renderer::Builder &Renderer::Builder::addCompositeWithBloomPass() { + renderer->passes.emplace_back(std::make_unique(*renderer)); + return *this; +} + Renderer::Builder &Renderer::Builder::addCompositePass() { renderer->passes.emplace_back(std::make_unique(*renderer)); return *this; @@ -175,7 +180,11 @@ Renderer::Builder &Renderer::Builder::deferred() { addBloomPass(); } addOutlinePass(); - addCompositePass(); + if (renderer->settings.bloom) { + addCompositeWithBloomPass(); + } else { + addCompositePass(); + } if (renderer->settings.fast_approximate_antialiasing) { addFXAAPass(); } diff --git a/src/limitless/renderer/screen_pass.cpp b/src/limitless/renderer/screen_pass.cpp index 9e853cd2..a500ac20 100644 --- a/src/limitless/renderer/screen_pass.cpp +++ b/src/limitless/renderer/screen_pass.cpp @@ -39,7 +39,7 @@ void ScreenPass::render( if (renderer.isPresent()) { screen = renderer.getPass().getResult(); } else { - screen = renderer.getPass().getResult();; + screen = renderer.getPass().getResult(); } shader.setUniform("screen_texture", screen); diff --git a/src/limitless/shader_storage.cpp b/src/limitless/shader_storage.cpp index 4af79fc1..098072e1 100644 --- a/src/limitless/shader_storage.cpp +++ b/src/limitless/shader_storage.cpp @@ -112,7 +112,11 @@ void ShaderStorage::initialize(Context& ctx, const RendererSettings& settings, c } add("deferred", compiler.compile(shader_dir / "pipeline/deferred")); - add("composite", compiler.compile(shader_dir / "pipeline/composite")); + if (settings.bloom) { + add("composite_with_bloom", compiler.compile(shader_dir / "pipeline/composite_with_bloom")); + } else { + add("composite", compiler.compile(shader_dir / "pipeline/composite")); + } add("outline", compiler.compile(shader_dir / "pipeline/outline")); From dd853bcd1a246c15271947174bc08181e4359ff5 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Tue, 8 Jul 2025 19:18:21 +0900 Subject: [PATCH 11/30] fix effect shader --- shaders/instance/instance_fs.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shaders/instance/instance_fs.glsl b/shaders/instance/instance_fs.glsl index a88329c9..1635a005 100644 --- a/shaders/instance/instance_fs.glsl +++ b/shaders/instance/instance_fs.glsl @@ -8,7 +8,7 @@ struct InstanceData { }; // REGULAR MODEL -#if defined (ENGINE_MATERIAL_REGULAR_MODEL) || defined (ENGINE_MATERIAL_SKELETAL_MODEL) || defined (ENGINE_MATERIAL_DECAL_MODEL) || defined (ENGINE_MATERIAL_TERRAIN_MODEL) +#if defined (ENGINE_MATERIAL_REGULAR_MODEL) || defined (ENGINE_MATERIAL_SKELETAL_MODEL) || defined (ENGINE_MATERIAL_DECAL_MODEL) || defined (ENGINE_MATERIAL_TERRAIN_MODEL) || (defined (ENGINE_MATERIAL_EFFECT_MODEL) && !defined (MeshEmitter)) layout (std140) uniform INSTANCE_BUFFER { InstanceData _instance_data; }; From caedf1890c6dc10bbb3fcf080396a9383e427e36 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Thu, 10 Jul 2025 19:58:39 +0900 Subject: [PATCH 12/30] fix light removal --- src/limitless/lighting/light_container.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/limitless/lighting/light_container.cpp b/src/limitless/lighting/light_container.cpp index b8934277..52a2247b 100644 --- a/src/limitless/lighting/light_container.cpp +++ b/src/limitless/lighting/light_container.cpp @@ -87,13 +87,22 @@ Light& LightContainer::add(const Light& light) { void LightContainer::update() { // check if there were an update to lights - bool changed {}; - for (auto& [id, light]: lights) { - // if changed since last update - if (light.isChanged()) { - // update corresponding internal presentation - internal_lights.at(id).update(light); + bool changed = false; + + auto it = lights.begin(); + while (it != lights.end()) { + auto& [id, light] = *it; + if (light.isRemoved()) { + internal_lights.erase(id); + it = lights.erase(it); changed = true; + + } else { + if (light.isChanged()) { + internal_lights.at(id).update(light); + changed = true; + } + ++it; } } From 38b41048cd99c8fcc8dfe61d8dcf88841d9537e5 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Sat, 12 Jul 2025 14:28:04 +0900 Subject: [PATCH 13/30] fix glb loading --- src/limitless/loaders/gltf_model_loader.cpp | 48 +++++++++++++-------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/limitless/loaders/gltf_model_loader.cpp b/src/limitless/loaders/gltf_model_loader.cpp index 18fc3e70..8c2301f3 100644 --- a/src/limitless/loaders/gltf_model_loader.cpp +++ b/src/limitless/loaders/gltf_model_loader.cpp @@ -828,26 +828,40 @@ static std::shared_ptr loadMaterial( flags.wrapping = *wrap_t_mode; } - if (strncmp(img.uri, "data:", 5) == 0) { - const char* comma = strchr(img.uri, ','); - - if (comma && comma - img.uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) { - auto buffer = bytesFromBase64(comma + 1); - - return TextureLoader::load( - assets, - name, - buffer.data(), - buffer.size(), - flags - ); - } else { - throw ModelLoadError {"unknown data uri"}; + if (img.uri == nullptr) { + if (!img.buffer_view) { + throw ModelLoadError {"texture has no uri and no buffer view"}; } + return TextureLoader::load( + assets, + name, + cgltf_buffer_view_data(img.buffer_view), + img.buffer_view->size + ); + } else { - const auto path = base_path / fs::path(img.uri); - return TextureLoader::load(assets, path, flags); + if (strncmp(img.uri, "data:", 5) == 0) { + const char* comma = strchr(img.uri, ','); + + if (comma && comma - img.uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) { + auto buffer = bytesFromBase64(comma + 1); + + return TextureLoader::load( + assets, + name, + buffer.data(), + buffer.size(), + flags + ); + } else { + throw ModelLoadError {"unknown data uri"}; + } + + } else { + const auto path = base_path / fs::path(img.uri); + return TextureLoader::load(assets, path, flags); + } } }; From 2541f5666b8565daa313b7b11bbd551f419e51f9 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Tue, 15 Jul 2025 12:10:36 +0900 Subject: [PATCH 14/30] add CPU profiler --- CMakeLists.txt | 1 + include/limitless/core/cpu_profiler.hpp | 57 +++++++++++++++ src/limitless/core/cpu_profiler.cpp | 73 +++++++++++++++++++ src/limitless/instances/instance.cpp | 2 + .../instances/instanced_instance.cpp | 2 + src/limitless/renderer/composite_pass.cpp | 3 +- src/limitless/renderer/decal_pass.cpp | 3 + .../renderer/deferred_framebuffer_pass.cpp | 3 + src/limitless/renderer/depth_pass.cpp | 3 + src/limitless/renderer/gbuffer_pass.cpp | 3 + src/limitless/renderer/instance_renderer.cpp | 4 + src/limitless/renderer/outline_pass.cpp | 3 + src/limitless/renderer/renderer.cpp | 23 ++++-- src/limitless/renderer/sceneupdate_pass.cpp | 3 +- src/limitless/renderer/translucent_pass.cpp | 3 + src/limitless/scene.cpp | 5 ++ 16 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 include/limitless/core/cpu_profiler.hpp create mode 100644 src/limitless/core/cpu_profiler.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index df0aae97..f8728b61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ set(ENGINE_CORE src/limitless/core/context_observer.cpp src/limitless/core/state_query.cpp src/limitless/core/profiler.cpp + src/limitless/core/cpu_profiler.cpp src/limitless/core/time_query.cpp src/limitless/core/texture/texture.cpp diff --git a/include/limitless/core/cpu_profiler.hpp b/include/limitless/core/cpu_profiler.hpp new file mode 100644 index 00000000..fb563aa7 --- /dev/null +++ b/include/limitless/core/cpu_profiler.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +namespace Limitless { + struct CpuProfiler { + using MonotonicTime = std::chrono::steady_clock::time_point; + using Duration = std::chrono::steady_clock::duration; + + struct Frame { + void record(Duration period); + + Duration getMinDuration() const noexcept; + Duration getMaxDuration() const noexcept; + Duration getAverageDuration() const noexcept; + Duration getLastDuration() const noexcept; + size_t getCount() const noexcept; + + Duration getTotalPerFrame() const noexcept; + size_t getCountPerFrame() const noexcept; + + void startFrame(); + + private: + Duration min {Duration::max()}; + Duration max {Duration::min()}; + Duration avg {Duration::zero()}; + Duration last {Duration::zero()}; + Duration total_per_frame {Duration::zero()}; + size_t n_per_frame {0}; + size_t n {0}; + }; + + std::unordered_map frames; + + void startFrame(); + }; + + extern CpuProfiler global_profiler; + + struct CpuProfileScope { + CpuProfiler::Frame& frame; + // Is a const char* to avoid dynamically allocating a std::sting for + // every profile frame. + const char* identifier; + CpuProfiler::MonotonicTime start; + + CpuProfileScope(CpuProfiler& profiler, const char* id) noexcept; + + ~CpuProfileScope(); + + private: + // TODO: consider taking a clock instance. + static CpuProfiler::MonotonicTime now() noexcept; + }; +} diff --git a/src/limitless/core/cpu_profiler.cpp b/src/limitless/core/cpu_profiler.cpp new file mode 100644 index 00000000..41cc81f6 --- /dev/null +++ b/src/limitless/core/cpu_profiler.cpp @@ -0,0 +1,73 @@ +#include "limitless/core/cpu_profiler.hpp" + +using namespace Limitless; + +CpuProfiler Limitless::global_profiler; + +void CpuProfiler::Frame::record(Duration period) { + if (period < min) { + min = period; + } + if (period > max) { + max = period; + } + + avg = decltype(avg)((avg.count() * n + period.count()) / (n + 1)); + last = period; + ++n; + total_per_frame += period; + ++n_per_frame; +} + +CpuProfiler::Duration CpuProfiler::Frame::getMinDuration() const noexcept { + return min; +} + +CpuProfiler::Duration CpuProfiler::Frame::getMaxDuration() const noexcept { + return max; +} + +CpuProfiler::Duration CpuProfiler::Frame::getAverageDuration() const noexcept { + return avg; +} + +CpuProfiler::Duration CpuProfiler::Frame::getLastDuration() const noexcept { + return last; +} + +size_t CpuProfiler::Frame::getCount() const noexcept { + return n; +} + +CpuProfiler::Duration CpuProfiler::Frame::getTotalPerFrame() const noexcept { + return total_per_frame; +} + +size_t CpuProfiler::Frame::getCountPerFrame() const noexcept { + return n_per_frame; +} + +void CpuProfiler::Frame::startFrame() { + total_per_frame = Duration::zero(); + n_per_frame = 0; +} + +void CpuProfiler::startFrame() { + for (auto& [name, frame] : frames) { + frame.startFrame(); + } +} + +CpuProfileScope::CpuProfileScope(CpuProfiler& profiler, const char* id) noexcept + : frame {profiler.frames[id]} + , identifier {id} + , start {now()} { +} + +CpuProfileScope::~CpuProfileScope() { + frame.record(now() - start); +} + +CpuProfiler::MonotonicTime CpuProfileScope::now() noexcept { + return std::chrono::steady_clock::now(); +} diff --git a/src/limitless/instances/instance.cpp b/src/limitless/instances/instance.cpp index 6c2c5240..84c87d4d 100644 --- a/src/limitless/instances/instance.cpp +++ b/src/limitless/instances/instance.cpp @@ -1,6 +1,7 @@ #include #include #include +#include using namespace Limitless; @@ -155,6 +156,7 @@ Instance& Instance::setBoundingBox(const Box& box) noexcept { } void Instance::update(const Camera &camera) { + CpuProfileScope scope(global_profiler, "Instance::update"); // updates current model matrices updateModelMatrix(); updateFinalMatrix(); diff --git a/src/limitless/instances/instanced_instance.cpp b/src/limitless/instances/instanced_instance.cpp index 039b9bce..a118c10a 100644 --- a/src/limitless/instances/instanced_instance.cpp +++ b/src/limitless/instances/instanced_instance.cpp @@ -1,6 +1,7 @@ #include #include #include +#include using namespace Limitless; @@ -65,6 +66,7 @@ void InstancedInstance::updateInstanceBuffer() { } void InstancedInstance::update(const Camera &camera) { + CpuProfileScope scope(global_profiler, "InstancedInstance::update"); if (instances.empty()) { return; } diff --git a/src/limitless/renderer/composite_pass.cpp b/src/limitless/renderer/composite_pass.cpp index 518a4908..4ff2b265 100644 --- a/src/limitless/renderer/composite_pass.cpp +++ b/src/limitless/renderer/composite_pass.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace Limitless; @@ -27,7 +28,7 @@ void CompositePass::render( [[maybe_unused]] const Camera &camera, [[maybe_unused]] UniformSetter &setter ) { - + CpuProfileScope scope(global_profiler, "CompositePass::render"); ctx.disable(Capabilities::DepthTest); ctx.disable(Capabilities::Blending); diff --git a/src/limitless/renderer/decal_pass.cpp b/src/limitless/renderer/decal_pass.cpp index ff2085d4..0a46b4a3 100644 --- a/src/limitless/renderer/decal_pass.cpp +++ b/src/limitless/renderer/decal_pass.cpp @@ -3,6 +3,7 @@ #include #include #include +#include using namespace Limitless; @@ -18,6 +19,8 @@ void DecalPass::render( [[maybe_unused]] const Camera &camera, UniformSetter &setter) { + CpuProfileScope scope(global_profiler, "DecalPass::render"); + auto& gbuffer = renderer.getPass(); gbuffer.getFramebuffer().readBuffer(FramebufferAttachment::None); diff --git a/src/limitless/renderer/deferred_framebuffer_pass.cpp b/src/limitless/renderer/deferred_framebuffer_pass.cpp index 049abe52..b5d5b51a 100644 --- a/src/limitless/renderer/deferred_framebuffer_pass.cpp +++ b/src/limitless/renderer/deferred_framebuffer_pass.cpp @@ -3,6 +3,7 @@ #include #include #include +#include using namespace Limitless; @@ -39,6 +40,8 @@ void DeferredFramebufferPass::render( [[maybe_unused]] const Assets &assets, [[maybe_unused]] const Camera &camera, [[maybe_unused]] UniformSetter &setter) { + CpuProfileScope scope(global_profiler, "DeferredFramebufferPass::render"); + ctx.setViewPort(framebuffer.get(FramebufferAttachment::Color0).texture->getSize()); ctx.setDepthMask(DepthMask::True); ctx.disable(Capabilities::Blending); diff --git a/src/limitless/renderer/depth_pass.cpp b/src/limitless/renderer/depth_pass.cpp index 671bdb35..f287a0a8 100644 --- a/src/limitless/renderer/depth_pass.cpp +++ b/src/limitless/renderer/depth_pass.cpp @@ -9,6 +9,7 @@ #include #include #include +#include using namespace Limitless; @@ -24,6 +25,8 @@ void DepthPass::render( [[maybe_unused]] const Camera &camera, UniformSetter &setter) { + CpuProfileScope scope(global_profiler, "DepthPass::render"); + ctx.enable(Capabilities::DepthTest); ctx.enable(Capabilities::StencilTest); ctx.disable(Capabilities::Blending); diff --git a/src/limitless/renderer/gbuffer_pass.cpp b/src/limitless/renderer/gbuffer_pass.cpp index e4b1d6c0..d83dd058 100644 --- a/src/limitless/renderer/gbuffer_pass.cpp +++ b/src/limitless/renderer/gbuffer_pass.cpp @@ -6,6 +6,7 @@ #include #include #include +#include using namespace Limitless; @@ -21,6 +22,8 @@ void GBufferPass::render( [[maybe_unused]] const Camera &camera, UniformSetter &setter) { + CpuProfileScope scope(global_profiler, "GBufferPass::render"); + ctx.enable(Capabilities::DepthTest); ctx.disable(Capabilities::Blending); ctx.setDepthFunc(DepthFunc::Equal); diff --git a/src/limitless/renderer/instance_renderer.cpp b/src/limitless/renderer/instance_renderer.cpp index 87cb5eb5..6fd5eb1b 100644 --- a/src/limitless/renderer/instance_renderer.cpp +++ b/src/limitless/renderer/instance_renderer.cpp @@ -1,4 +1,5 @@ #include +#include using namespace Limitless; @@ -57,6 +58,7 @@ bool InstanceRenderer::shouldBeRendered(const Instance &instance, const DrawPara } void InstanceRenderer::renderScene(const DrawParameters& drawp) { + CpuProfileScope scope(global_profiler, "InstanceRenderer::renderScene"); // renders common instances except decals // because decals rendered projected on everything else for (const auto& instance: frustum_culling.getVisibleInstances()) { @@ -246,6 +248,8 @@ void InstanceRenderer::renderVisible(Instance &instance, const DrawParameters &d } void InstanceRenderer::update(Scene& scene, Camera& camera) { + CpuProfileScope scope(global_profiler, "InstanceRenderer::update"); + frustum_culling.update(scene, camera); effect_renderer.update(frustum_culling.getVisibleInstances()); } diff --git a/src/limitless/renderer/outline_pass.cpp b/src/limitless/renderer/outline_pass.cpp index 5dae18e4..857c486b 100644 --- a/src/limitless/renderer/outline_pass.cpp +++ b/src/limitless/renderer/outline_pass.cpp @@ -6,6 +6,7 @@ #include #include #include +#include using namespace Limitless; @@ -29,6 +30,8 @@ void OutlinePass::render( [[maybe_unused]] const Camera &camera, [[maybe_unused]] UniformSetter &setter) { + CpuProfileScope scope(global_profiler, "OutlinePass::render"); + ctx.disable(Capabilities::DepthTest); ctx.disable(Capabilities::Blending); diff --git a/src/limitless/renderer/renderer.cpp b/src/limitless/renderer/renderer.cpp index e5160fff..0975436b 100644 --- a/src/limitless/renderer/renderer.cpp +++ b/src/limitless/renderer/renderer.cpp @@ -25,20 +25,31 @@ #include #include #include +#include using namespace Limitless; void Renderer::render(Context& context, const Assets& assets, Scene& scene, Camera& camera) { + CpuProfileScope scope(global_profiler, "Renderer::render"); + instance_renderer.update(scene, camera); - for (const auto& pass: passes) { - pass->update(scene, camera); + { + CpuProfileScope scope(global_profiler, "Renderer::render_update_passes"); + + for (const auto& pass: passes) { + pass->update(scene, camera); + } } - UniformSetter setter; - for (const auto& pass: passes) { - pass->render(instance_renderer, scene, context, assets, camera, setter); - pass->addUniformSetter(setter); + { + CpuProfileScope scope(global_profiler, "Renderer::render_render_passes"); + + UniformSetter setter; + for (const auto& pass: passes) { + pass->render(instance_renderer, scene, context, assets, camera, setter); + pass->addUniformSetter(setter); + } } } diff --git a/src/limitless/renderer/sceneupdate_pass.cpp b/src/limitless/renderer/sceneupdate_pass.cpp index 77ed40b1..3da54a6a 100644 --- a/src/limitless/renderer/sceneupdate_pass.cpp +++ b/src/limitless/renderer/sceneupdate_pass.cpp @@ -1,5 +1,5 @@ #include - +#include #include using namespace Limitless; @@ -10,6 +10,7 @@ SceneUpdatePass::SceneUpdatePass(Renderer& renderer) } void SceneUpdatePass::update(Scene &scene, const Camera &camera) { + CpuProfileScope scope(global_profiler, "SceneUpdatePass::update"); scene.update(camera); scene_data.update(camera); } diff --git a/src/limitless/renderer/translucent_pass.cpp b/src/limitless/renderer/translucent_pass.cpp index 89d397a9..8e81688f 100644 --- a/src/limitless/renderer/translucent_pass.cpp +++ b/src/limitless/renderer/translucent_pass.cpp @@ -13,6 +13,7 @@ #include #include #include +#include using namespace Limitless; @@ -29,6 +30,8 @@ void TranslucentPass::render( [[maybe_unused]] const Camera &camera, UniformSetter &setter) { + CpuProfileScope scope(global_profiler, "TranslucentPass::render"); + std::array transparent = { ms::Blending::Additive, ms::Blending::Modulate, diff --git a/src/limitless/scene.cpp b/src/limitless/scene.cpp index 76179a85..28dd5100 100644 --- a/src/limitless/scene.cpp +++ b/src/limitless/scene.cpp @@ -1,6 +1,7 @@ #include #include #include +#include using namespace Limitless; @@ -110,17 +111,21 @@ void Scene::setSkybox(const std::shared_ptr& skybox_) { } void Scene::update(const Camera& camera) { + CpuProfileScope scope(global_profiler, "Scene::update"); + lighting.update(); removeDeadInstances(); for (auto& [_, instance] : instances) { + CpuProfileScope scope(global_profiler, "Scene::update_instance"); if (instance->getInstanceType() != InstanceType::Effect) { instance->update(camera); } } for (auto& [_, instance] : instances) { + CpuProfileScope scope(global_profiler, "Scene::update_effect_instance"); if (instance->getInstanceType() == InstanceType::Effect) { instance->update(camera); } From a79c8f1f6671c99d8108af12be135ccf97a39a3f Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Tue, 19 Aug 2025 20:20:38 +0900 Subject: [PATCH 15/30] fix text selection driver crash --- .../limitless/models/text_selection_model.hpp | 29 ++++++++++++++----- include/limitless/text/text_instance.hpp | 2 +- src/limitless/models/text_selection_model.cpp | 23 +++++++++++++++ src/limitless/text/text_instance.cpp | 19 +++++------- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/include/limitless/models/text_selection_model.hpp b/include/limitless/models/text_selection_model.hpp index 207be3ed..fe1f3950 100644 --- a/include/limitless/models/text_selection_model.hpp +++ b/include/limitless/models/text_selection_model.hpp @@ -6,18 +6,33 @@ #include namespace Limitless { - class TextSelectionModel { - private: - VertexArray vertex_array; - std::shared_ptr buffer; - std::vector vertices; - - void initialize(size_t count); + class TextSelectionModel { public: explicit TextSelectionModel(std::vector&& vertices); explicit TextSelectionModel(size_t count); + TextSelectionModel(const TextSelectionModel&) = delete; + TextSelectionModel& operator=(const TextSelectionModel&) = delete; + + TextSelectionModel(TextSelectionModel&&) noexcept; + TextSelectionModel& operator=(TextSelectionModel&&) noexcept; + void update(std::vector&& vertices); void draw() const; + + [[nodiscard]] bool empty() const noexcept { return vertices.empty(); } + + ~TextSelectionModel(); + + private: + VertexArray vertex_array; + std::shared_ptr buffer; + std::vector vertices; + + void initialize(size_t count); + + friend void swap(TextSelectionModel& lhs, TextSelectionModel& rhs) noexcept; }; + + void swap(TextSelectionModel& lhs, TextSelectionModel& rhs) noexcept; } \ No newline at end of file diff --git a/include/limitless/text/text_instance.hpp b/include/limitless/text/text_instance.hpp index 57343565..6c7731ba 100644 --- a/include/limitless/text/text_instance.hpp +++ b/include/limitless/text/text_instance.hpp @@ -19,7 +19,7 @@ namespace Limitless { }; std::vector font_text_models; - std::optional selection_model {std::nullopt}; + TextSelectionModel selection_model; std::vector formatted_text_parts; glm::vec2 position {0.0f}; diff --git a/src/limitless/models/text_selection_model.cpp b/src/limitless/models/text_selection_model.cpp index 0dca4de1..77b2cd0c 100644 --- a/src/limitless/models/text_selection_model.cpp +++ b/src/limitless/models/text_selection_model.cpp @@ -4,6 +4,27 @@ using namespace Limitless; +TextSelectionModel::TextSelectionModel(TextSelectionModel&& other) noexcept { + swap(*this, other); +} + +TextSelectionModel& TextSelectionModel::operator=(TextSelectionModel&& other) noexcept { + if (this == &other) { + return *this; + } + + swap(*this, other); + return *this; +} + +void Limitless::swap(TextSelectionModel& lhs, TextSelectionModel& rhs) noexcept { + using std::swap; + swap(lhs.vertex_array, rhs.vertex_array); + swap(lhs.buffer, rhs.buffer); + swap(lhs.vertices, rhs.vertices); +} + + TextSelectionModel::TextSelectionModel(std::vector&& vertices) : vertices{std::move(vertices)} { @@ -40,3 +61,5 @@ void TextSelectionModel::draw() const { vertex_array.bind(); glDrawArrays(GL_TRIANGLES, 0, vertices.size()); } + +TextSelectionModel::~TextSelectionModel() = default; diff --git a/src/limitless/text/text_instance.cpp b/src/limitless/text/text_instance.cpp index 357f1b04..8a57153f 100644 --- a/src/limitless/text/text_instance.cpp +++ b/src/limitless/text/text_instance.cpp @@ -16,7 +16,8 @@ TextInstance::TextInstance( std::vector _formatted_text_parts, const glm::vec2& _position ) - : formatted_text_parts {std::move(_formatted_text_parts)} + : selection_model(0) + , formatted_text_parts {std::move(_formatted_text_parts)} , position {_position} { auto type_set_result = TypeSetter::typeSet(formatted_text_parts); @@ -66,17 +67,13 @@ TextInstance& TextInstance::setSelectionColor(const glm::vec4& _color) noexcept TextInstance& TextInstance::setSelection(size_t begin, size_t end) { auto vertices = TypeSetter::typeSetSelection(formatted_text_parts, begin, end); - if (!selection_model) { - selection_model = TextSelectionModel(std::move(vertices)); - } else { - selection_model->update(std::move(vertices)); - } + selection_model.update(std::move(vertices)); return *this; } TextInstance& TextInstance::removeSelection() noexcept { - selection_model = std::nullopt; + selection_model.update({}); return *this; } @@ -108,18 +105,18 @@ void TextInstance::draw(Context& ctx, const Assets& assets) { ); // draw selection - if (selection_model) { + if (!selection_model.empty()) { ctx.disable(Capabilities::Blending); auto& shader = assets.shaders.get("text_selection"); shader.setUniform("model", model_matrix) - .setUniform("proj", ortho_projection) - .setUniform("color", selection_color); + .setUniform("proj", ortho_projection) + .setUniform("color", selection_color); shader.use(); - selection_model->draw(); + selection_model.draw(); } // draw text From d3f50d271b8100767c4a64318133f1ae667ffe2b Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Tue, 26 Aug 2025 08:53:55 +0900 Subject: [PATCH 16/30] profile cursor pos retrieval --- src/limitless/core/context.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/limitless/core/context.cpp b/src/limitless/core/context.cpp index 19b5598d..a4a13104 100644 --- a/src/limitless/core/context.cpp +++ b/src/limitless/core/context.cpp @@ -1,6 +1,7 @@ #include #include #include +#include using namespace Limitless; @@ -158,6 +159,7 @@ glm::uvec2 Context::getSize() const noexcept { } glm::vec2 Context::getCursorPos() const noexcept { + CpuProfileScope ps {global_profiler, "Context::getCursorPos"}; double x, y; glfwGetCursorPos(window, &x, &y); From 2e3e712bbcf2fb9a2da5524f221fba33e211cbb4 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Tue, 26 Aug 2025 15:56:57 +0900 Subject: [PATCH 17/30] support monospace digits --- src/limitless/text/font_atlas.cpp | 23 +++++++++++++++++++++++ src/limitless/text/type_setter.cpp | 6 ++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/limitless/text/font_atlas.cpp b/src/limitless/text/font_atlas.cpp index 85aa46a5..dd1e3780 100644 --- a/src/limitless/text/font_atlas.cpp +++ b/src/limitless/text/font_atlas.cpp @@ -147,6 +147,29 @@ static std::shared_ptr makeAtlas( chars.emplace('\t', chars.at(' ')); chars.at('\t').advance *= TAB_WIDTH_IN_SPACES; + { + uint32_t max_digit_advance = 0; + for (int i = 0; i < 10; ++i) { + uint32_t char_code = '0' + i; + if (chars.find(char_code) != chars.end()) { + max_digit_advance = std::max(max_digit_advance, chars.at(char_code).advance); + } + } + + auto addMonospaceDigit = [&](int digit) { + const uint32_t monospace_char_code = 0x1D7F6 + digit; + const uint32_t ascii_char_code = '0' + digit; + if (chars.find(monospace_char_code) == chars.end() && chars.find(ascii_char_code) != chars.end()) { + chars.emplace(monospace_char_code, chars.at(ascii_char_code)); + chars.at(monospace_char_code).advance = max_digit_advance; + } + }; + + for (int i = 0; i < 10; ++i) { + addMonospaceDigit(i); + } + } + const auto internal_format = [bytes_per_pixel] { switch (bytes_per_pixel) { case 1: return Texture::InternalFormat::R8; diff --git a/src/limitless/text/type_setter.cpp b/src/limitless/text/type_setter.cpp index 34011066..3436a351 100644 --- a/src/limitless/text/type_setter.cpp +++ b/src/limitless/text/type_setter.cpp @@ -215,6 +215,7 @@ TypeSetResult TypeSetter::typeSet( const auto width = fc.size.x * scale; const auto height = fc.size.y * scale; + const auto advance = (fc.advance >> 6) * scale; float x = offset.x + fc.bearing.x * scale; float y = offset.y + fc.bearing.y * scale - height; @@ -228,7 +229,8 @@ TypeSetResult TypeSetter::typeSet( } min_pos = glm::vec2(std::min(min_pos.x, x), std::min(min_pos.y, y)); - max_pos = glm::vec2(std::max(max_pos.x, x + width), std::max(max_pos.y, y + height)); + // perhaps x + width is more correct, but advance is more correct for monospace fonts. + max_pos = glm::vec2(std::max(max_pos.x, x + advance), std::max(max_pos.y, y + height)); const auto vertex_color = fc.is_icon ? glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) : color; @@ -242,7 +244,7 @@ TypeSetResult TypeSetter::typeSet( vertices.emplace_back(glm::vec2{x + width, y}, fc.uvs[1], vertex_color); vertices.emplace_back(glm::vec2{x + width, y + height}, fc.uvs[3], vertex_color); - offset.x += (fc.advance >> 6) * scale; + offset.x += advance; return true; }); From 87de46047c18b496efa09b5e4bf2050b601c3b92 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Tue, 26 Aug 2025 22:06:30 +0900 Subject: [PATCH 18/30] expire emitter once particles are dead --- src/limitless/fx/emitters/emitter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/limitless/fx/emitters/emitter.cpp b/src/limitless/fx/emitters/emitter.cpp index ba9d860d..d9bd5955 100644 --- a/src/limitless/fx/emitters/emitter.cpp +++ b/src/limitless/fx/emitters/emitter.cpp @@ -75,7 +75,7 @@ std::chrono::duration& Emitter

::getDuration() noexcept { template bool Emitter

::isDone() const noexcept { - return done; + return done && particles.empty(); } template From 8b0ae1f25a50d53be06489d7aa8a2d47d4ec0242 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Wed, 3 Sep 2025 08:56:40 +0900 Subject: [PATCH 19/30] simplify camera --- include/limitless/camera.hpp | 37 ++++----- samples/effects/main.cpp | 31 ++++---- samples/gltf_viewer/gltf_viewer.cpp | 19 +++-- samples/lighting/main.cpp | 29 +++---- samples/materials/main.cpp | 29 +++---- samples/terrain/main.cpp | 29 +++---- src/limitless/camera.cpp | 114 ++++++---------------------- 7 files changed, 113 insertions(+), 175 deletions(-) diff --git a/include/limitless/camera.hpp b/include/limitless/camera.hpp index d581c1d8..68b414b7 100644 --- a/include/limitless/camera.hpp +++ b/include/limitless/camera.hpp @@ -6,31 +6,27 @@ #include namespace Limitless { - enum class CameraMovement { Forward, Backward, Left, Right, Up, Down }; - enum class CameraMode { Free, Panning }; - class Camera { private: glm::vec3 position; - glm::vec3 front, up, right, world_up; + glm::vec3 front; + glm::vec3 up; + glm::vec3 right; + glm::vec3 world_up; glm::mat4 projection; glm::mat4 view; glm::mat4 view_to_screen; - CameraMode mode {}; - - float pitch; + float pitch; // degrees float yaw; float fov {90}; // degrees float near_distance {0.01f}; float far_distance {100.0f}; - float move_speed {2.0f}; - float mouse_sence {0.5f}; public: - explicit Camera(glm::uvec2 window_size) noexcept; + explicit Camera(glm::uvec2 screen_size) noexcept; [[nodiscard]] const auto& getViewToScreen() const noexcept { return view_to_screen; } [[nodiscard]] const auto& getProjection() const noexcept { return projection; } @@ -42,21 +38,16 @@ namespace Limitless { [[nodiscard]] const auto& getNear() const noexcept { return near_distance; } [[nodiscard]] const auto& getFar() const noexcept { return far_distance; } [[nodiscard]] const auto& getFov() const noexcept { return fov; } - - [[nodiscard]] auto& getMoveSpeed() noexcept { return move_speed; } - [[nodiscard]] auto& getMouseSence() noexcept { return mouse_sence; } - [[nodiscard]] auto& getMode() noexcept { return mode; } + [[nodiscard]] const auto& getPitch() const noexcept { return pitch; } + [[nodiscard]] const auto& getYaw() const noexcept { return yaw; } void setPosition(const glm::vec3& position) noexcept; - void setFront(const glm::vec3& front) noexcept; - void setFov(glm::uvec2 size, float fov) noexcept; - void setMode(CameraMode mode) noexcept; - - void mouseMove(glm::dvec2 offset) noexcept; - void mouseScroll(float yoffset) noexcept; - void movement(CameraMovement move, float delta) noexcept; + void setFov(glm::uvec2 screen_size, float fov) noexcept; + void setPitch(float pitch) noexcept; + void setYaw(float yaw) noexcept; + void setRotation(float pitch, float yaw) noexcept; void updateView() noexcept; - void updateProjection(glm::uvec2 size) noexcept; + void updateProjection(glm::uvec2 screen_size) noexcept; }; -} \ No newline at end of file +} diff --git a/samples/effects/main.cpp b/samples/effects/main.cpp index ae0b24b5..938b5436 100644 --- a/samples/effects/main.cpp +++ b/samples/effects/main.cpp @@ -41,7 +41,7 @@ namespace LimitlessMaterials { onKey(key, scancode, state, modifier); }) .build() - } + } , camera {window_size} , render {Limitless::Renderer::builder() .resolution(window_size) @@ -59,7 +59,10 @@ namespace LimitlessMaterials { auto offset = glm::vec2{pos.x - last_move.x, last_move.y - pos.y}; last_move = pos; - camera.mouseMove(offset); + camera.setRotation( + camera.getPitch() + float(offset.y), + camera.getYaw() + float(offset.x) + ); } void onKey(int key, [[maybe_unused]] int scancode, Limitless::InputState state, [[maybe_unused]] Limitless::Modifier modifier) { @@ -68,14 +71,6 @@ namespace LimitlessMaterials { done = true; } - if (key == GLFW_KEY_SPACE && state == Limitless::InputState::Pressed) { - camera.getMoveSpeed() *= 5.0f; - } - - if (key == GLFW_KEY_SPACE && state == Limitless::InputState::Released) { - camera.getMoveSpeed() /= 5.0f; - } - if (key == GLFW_KEY_GRAVE_ACCENT && state == Limitless::InputState::Released) { hidden_text = !hidden_text; } @@ -90,19 +85,27 @@ namespace LimitlessMaterials { using namespace Limitless; if (context.isPressed(GLFW_KEY_W)) { - camera.movement(CameraMovement::Forward, delta); + camera.setPosition(camera.getPosition() + camera.getFront() * delta); } if (context.isPressed(GLFW_KEY_S)) { - camera.movement(CameraMovement::Backward, delta); + camera.setPosition(camera.getPosition() - camera.getFront() * delta); } if (context.isPressed(GLFW_KEY_A)) { - camera.movement(CameraMovement::Left, delta); + camera.setPosition(camera.getPosition() - camera.getRight() * delta); } if (context.isPressed(GLFW_KEY_D)) { - camera.movement(CameraMovement::Right, delta); + camera.setPosition(camera.getPosition() + camera.getRight() * delta); + } + + if (context.isPressed(GLFW_KEY_SPACE)) { + camera.setPosition(camera.getPosition() + camera.getUp() * delta); + } + + if (context.isPressed(GLFW_KEY_Z)) { + camera.setPosition(camera.getPosition() - camera.getUp() * delta); } } diff --git a/samples/gltf_viewer/gltf_viewer.cpp b/samples/gltf_viewer/gltf_viewer.cpp index 0468e428..3d098bd7 100644 --- a/samples/gltf_viewer/gltf_viewer.cpp +++ b/samples/gltf_viewer/gltf_viewer.cpp @@ -42,7 +42,10 @@ class CameraMouseHandler { }; last_mouse_pos = mouse_pos; - camera.mouseMove(offset); + camera.setRotation( + camera.getPitch() + float(offset.y), + camera.getYaw() + float(offset.x) + ); } private: @@ -107,7 +110,7 @@ int main(int argc, char* argv[]) { } camera.setPosition({0.0f, 0.0f, 0.0f}); - camera.setFront({1.f, 0.f, 0.f}); + camera.setRotation(0.0f, 0.0f); if (!Limitless::ContextInitializer::checkMinimumRequirements()) { std::cerr << "Minimum graphics card requirements are not met!" << std::endl; @@ -139,27 +142,27 @@ int main(int argc, char* argv[]) { ctx.pollEvents(); if (ctx.isPressed(GLFW_KEY_W)) { - camera.movement(CameraMovement::Forward, delta); + camera.setPosition(camera.getPosition() + camera.getFront() * delta); } if (ctx.isPressed(GLFW_KEY_S)) { - camera.movement(CameraMovement::Backward, delta); + camera.setPosition(camera.getPosition() - camera.getFront() * delta); } if (ctx.isPressed(GLFW_KEY_A)) { - camera.movement(CameraMovement::Left, delta); + camera.setPosition(camera.getPosition() - camera.getRight() * delta); } if (ctx.isPressed(GLFW_KEY_D)) { - camera.movement(CameraMovement::Right, delta); + camera.setPosition(camera.getPosition() + camera.getRight() * delta); } if (ctx.isPressed(GLFW_KEY_SPACE)) { - camera.movement(CameraMovement::Up, delta); + camera.setPosition(camera.getPosition() + camera.getUp() * delta); } if (ctx.isPressed(GLFW_KEY_Z)) { - camera.movement(CameraMovement::Down, delta); + camera.setPosition(camera.getPosition() - camera.getUp() * delta); } if (ctx.isPressed(GLFW_KEY_Q)) { diff --git a/samples/lighting/main.cpp b/samples/lighting/main.cpp index 2d5e516e..ff84fb62 100644 --- a/samples/lighting/main.cpp +++ b/samples/lighting/main.cpp @@ -59,7 +59,10 @@ namespace LimitlessMaterials { auto offset = glm::vec2{pos.x - last_move.x, last_move.y - pos.y}; last_move = pos; - camera.mouseMove(offset); + camera.setRotation( + camera.getPitch() + float(offset.y), + camera.getYaw() + float(offset.x) + ); } void onKey(int key, [[maybe_unused]] int scancode, Limitless::InputState state, [[maybe_unused]] Limitless::Modifier modifier) { @@ -68,14 +71,6 @@ namespace LimitlessMaterials { done = true; } - if (key == GLFW_KEY_SPACE && state == Limitless::InputState::Pressed) { - camera.getMoveSpeed() *= 5.0f; - } - - if (key == GLFW_KEY_SPACE && state == Limitless::InputState::Released) { - camera.getMoveSpeed() /= 5.0f; - } - if (key == GLFW_KEY_GRAVE_ACCENT && state == Limitless::InputState::Released) { hidden_text = !hidden_text; } @@ -90,19 +85,27 @@ namespace LimitlessMaterials { using namespace Limitless; if (context.isPressed(GLFW_KEY_W)) { - camera.movement(CameraMovement::Forward, delta); + camera.setPosition(camera.getPosition() + camera.getFront() * delta); } if (context.isPressed(GLFW_KEY_S)) { - camera.movement(CameraMovement::Backward, delta); + camera.setPosition(camera.getPosition() - camera.getFront() * delta); } if (context.isPressed(GLFW_KEY_A)) { - camera.movement(CameraMovement::Left, delta); + camera.setPosition(camera.getPosition() - camera.getRight() * delta); } if (context.isPressed(GLFW_KEY_D)) { - camera.movement(CameraMovement::Right, delta); + camera.setPosition(camera.getPosition() + camera.getRight() * delta); + } + + if (context.isPressed(GLFW_KEY_SPACE)) { + camera.setPosition(camera.getPosition() + camera.getUp() * delta); + } + + if (context.isPressed(GLFW_KEY_Z)) { + camera.setPosition(camera.getPosition() - camera.getUp() * delta); } } diff --git a/samples/materials/main.cpp b/samples/materials/main.cpp index 58acbe1d..8175e080 100644 --- a/samples/materials/main.cpp +++ b/samples/materials/main.cpp @@ -60,7 +60,10 @@ namespace LimitlessMaterials { auto offset = glm::vec2{pos.x - last_move.x, last_move.y - pos.y}; last_move = pos; - camera.mouseMove(offset); + camera.setRotation( + camera.getPitch() + float(offset.y), + camera.getYaw() + float(offset.x) + ); } void onKey(int key, [[maybe_unused]] int scancode, Limitless::InputState state, [[maybe_unused]] Limitless::Modifier modifier) { @@ -69,14 +72,6 @@ namespace LimitlessMaterials { done = true; } - if (key == GLFW_KEY_SPACE && state == Limitless::InputState::Pressed) { - camera.getMoveSpeed() *= 5.0f; - } - - if (key == GLFW_KEY_SPACE && state == Limitless::InputState::Released) { - camera.getMoveSpeed() /= 5.0f; - } - if (key == GLFW_KEY_GRAVE_ACCENT && state == Limitless::InputState::Released) { hidden_text = !hidden_text; } @@ -92,19 +87,27 @@ namespace LimitlessMaterials { using namespace Limitless; if (context.isPressed(GLFW_KEY_W)) { - camera.movement(CameraMovement::Forward, delta); + camera.setPosition(camera.getPosition() + camera.getFront() * delta); } if (context.isPressed(GLFW_KEY_S)) { - camera.movement(CameraMovement::Backward, delta); + camera.setPosition(camera.getPosition() - camera.getFront() * delta); } if (context.isPressed(GLFW_KEY_A)) { - camera.movement(CameraMovement::Left, delta); + camera.setPosition(camera.getPosition() - camera.getRight() * delta); } if (context.isPressed(GLFW_KEY_D)) { - camera.movement(CameraMovement::Right, delta); + camera.setPosition(camera.getPosition() + camera.getRight() * delta); + } + + if (context.isPressed(GLFW_KEY_SPACE)) { + camera.setPosition(camera.getPosition() + camera.getUp() * delta); + } + + if (context.isPressed(GLFW_KEY_Z)) { + camera.setPosition(camera.getPosition() - camera.getUp() * delta); } } diff --git a/samples/terrain/main.cpp b/samples/terrain/main.cpp index 979339c4..fd5745fa 100644 --- a/samples/terrain/main.cpp +++ b/samples/terrain/main.cpp @@ -56,7 +56,10 @@ namespace LimitlessMaterials { auto offset = glm::vec2{pos.x - last_move.x, last_move.y - pos.y}; last_move = pos; - camera.mouseMove(offset); + camera.setRotation( + camera.getPitch() + float(offset.y), + camera.getYaw() + float(offset.x) + ); } void onKey(int key, [[maybe_unused]] int scancode, Limitless::InputState state, [[maybe_unused]] Limitless::Modifier modifier) { @@ -65,14 +68,6 @@ namespace LimitlessMaterials { done = true; } - if (key == GLFW_KEY_SPACE && state == Limitless::InputState::Pressed) { - camera.getMoveSpeed() *= 5.0f; - } - - if (key == GLFW_KEY_SPACE && state == Limitless::InputState::Released) { - camera.getMoveSpeed() /= 5.0f; - } - if (key == GLFW_KEY_GRAVE_ACCENT && state == Limitless::InputState::Released) { hidden_text = !hidden_text; } @@ -87,19 +82,27 @@ namespace LimitlessMaterials { using namespace Limitless; if (context.isPressed(GLFW_KEY_W)) { - camera.movement(CameraMovement::Forward, delta); + camera.setPosition(camera.getPosition() + camera.getFront() * delta); } if (context.isPressed(GLFW_KEY_S)) { - camera.movement(CameraMovement::Backward, delta); + camera.setPosition(camera.getPosition() - camera.getFront() * delta); } if (context.isPressed(GLFW_KEY_A)) { - camera.movement(CameraMovement::Left, delta); + camera.setPosition(camera.getPosition() - camera.getRight() * delta); } if (context.isPressed(GLFW_KEY_D)) { - camera.movement(CameraMovement::Right, delta); + camera.setPosition(camera.getPosition() + camera.getRight() * delta); + } + + if (context.isPressed(GLFW_KEY_SPACE)) { + camera.setPosition(camera.getPosition() + camera.getUp() * delta); + } + + if (context.isPressed(GLFW_KEY_Z)) { + camera.setPosition(camera.getPosition() - camera.getUp() * delta); } } diff --git a/src/limitless/camera.cpp b/src/limitless/camera.cpp index 6038e871..d3e99938 100644 --- a/src/limitless/camera.cpp +++ b/src/limitless/camera.cpp @@ -1,8 +1,9 @@ #include +#include using namespace Limitless; -Camera::Camera(glm::uvec2 window_size) noexcept +Camera::Camera(glm::uvec2 screen_size) noexcept : position{0.0f} , front {1.0f, 0.0f, 0.0f} , up {0.0f, 1.0f, 0.0f} @@ -13,87 +14,23 @@ Camera::Camera(glm::uvec2 window_size) noexcept , pitch {-60.0f} , yaw {270.0f} { - updateView(); - updateProjection(window_size); + updateProjection(screen_size); } -void Camera::mouseMove(glm::dvec2 offset) noexcept { - switch (mode) { - case CameraMode::Free: - offset *= mouse_sence; - - yaw += offset.x; - pitch += offset.y; - - pitch = pitch >= 89.0f ? 89.0f : pitch; - pitch = pitch <= -89.0f ? -89.0f : pitch; - break; - case CameraMode::Panning: - break; - } - +void Camera::setPitch(float _pitch) noexcept { + pitch = std::clamp(_pitch, -89.0f, 89.0f); updateView(); } -void Camera::mouseScroll(float yoffset) noexcept { - fov -= yoffset; - - fov = fov >= 150.0f ? 150.0f : fov; - fov = fov <= 1.0f ? 1.0f : fov; +void Camera::setYaw(float _yaw) noexcept { + yaw = std::fmod(_yaw, 360.0f); + updateView(); } - -void Camera::movement(CameraMovement move, float delta) noexcept { - auto velocity = move_speed * delta; - - switch (mode) { - case CameraMode::Free: - switch (move) { - case CameraMovement::Forward: - position += front * velocity; - break; - case CameraMovement::Backward: - position -= front * velocity; - break; - case CameraMovement::Left: - position -= right * velocity; - break; - case CameraMovement::Right: - position += right * velocity; - break; - case CameraMovement::Up: - position += world_up * velocity; - break; - case CameraMovement::Down: - position -= world_up * velocity; - break; - } - break; - case CameraMode::Panning: - switch (move) { - case CameraMovement::Forward: - position.z -= velocity; - break; - case CameraMovement::Backward: - position.z += velocity; - break; - case CameraMovement::Left: - position.x -= velocity; - break; - case CameraMovement::Right: - position.x += velocity; - break; - case CameraMovement::Up: - position.y += velocity; - break; - case CameraMovement::Down: - position.y -= velocity; - break; - } - break; - } - +void Camera::setRotation(float _pitch, float _yaw) noexcept { + pitch = std::clamp(_pitch, -89.0f, 89.0f); + yaw = std::fmod(_yaw, 360.0f); updateView(); } @@ -116,16 +53,21 @@ void Camera::updateView() noexcept { view = glm::lookAt(position, position + front, up); } -void Camera::updateProjection(glm::uvec2 size) noexcept { - projection = glm::perspective(glm::radians(fov), static_cast(size.x) / static_cast(size.y), near_distance, far_distance); +void Camera::updateProjection(glm::uvec2 screen_size) noexcept { + projection = glm::perspective( + glm::radians(fov), + static_cast(screen_size.x) / static_cast(screen_size.y), + near_distance, + far_distance + ); // matrix that converts from // clip space [-1, 1] to screen space [0, screen size] glm::mat4 clip_to_screen = glm::mat4( - 0.5 * size.x, 0.0, 0.0, 0.0, - 0.0, 0.5 * size.y, 0.0, 0.0, + 0.5 * screen_size.x, 0.0, 0.0, 0.0, + 0.0, 0.5 * screen_size.y, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.5 * size.x, 0.5 * size.y, 0.0, 1.0 + 0.5 * screen_size.x, 0.5 * screen_size.y, 0.0, 1.0 ); view_to_screen = clip_to_screen * projection; @@ -137,17 +79,7 @@ void Camera::setPosition(const glm::vec3& _position) noexcept { updateView(); } -void Camera::setFront(const glm::vec3& _front) noexcept { - front = _front; - - updateView(); -} - -void Camera::setFov(glm::uvec2 size, float _fov) noexcept { +void Camera::setFov(glm::uvec2 screen_size, float _fov) noexcept { fov = _fov; - updateProjection(size); -} - -void Camera::setMode(CameraMode new_mode) noexcept { - mode = new_mode; + updateProjection(screen_size); } From 1c33bc13ccfef36ead2dd860bd7e8609be03348c Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Sun, 7 Sep 2025 19:51:17 +0900 Subject: [PATCH 20/30] meme snow --- shaders/terrain/terrain.glsl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shaders/terrain/terrain.glsl b/shaders/terrain/terrain.glsl index 7c8400e7..18d9c5db 100644 --- a/shaders/terrain/terrain.glsl +++ b/shaders/terrain/terrain.glsl @@ -75,6 +75,10 @@ void calculateTerrain(inout MaterialContext mctx) { // vec3 variation = MacroContrast(MacroVariation()); vec3 diffuse = StochasticTexture(terrain_uv, getVertexTileCurrent(), _terrain_diffuse_texture); + if (getVertexTileCurrent() == 1u) { + vec3 diffuse_snow = StochasticTexture(terrain_uv, 4u, _terrain_diffuse_texture); + diffuse = (1.0 - winter ) * diffuse + winter * diffuse_snow; + } vec3 normal = StochasticTexture(terrain_uv, getVertexTileCurrent(), _terrain_normal_texture); vec3 orm = StochasticTexture(terrain_uv, getVertexTileCurrent(), _terrain_orm_texture); @@ -103,6 +107,10 @@ void calculateTerrain(inout MaterialContext mctx) { if (mask[i] == 1u) { // fetch adjacent type vec3 adjacent_diffuse = StochasticTexture(terrain_uv, types[i], _terrain_diffuse_texture).rgb; + if (types[i] == 1u) { + vec3 adjacent_diffuse_snow = StochasticTexture(terrain_uv, 4u, _terrain_diffuse_texture).rgb; + adjacent_diffuse = (1.0 - winter ) * adjacent_diffuse + winter * adjacent_diffuse_snow; + } vec3 adjacent_normal = StochasticTexture(terrain_uv, types[i], _terrain_normal_texture).xyz; vec3 adjacent_orm = StochasticTexture(terrain_uv, types[i], _terrain_orm_texture).rgb; From abee503b0cfbbb7d4e7d0183c2f6e6725778ad5f Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Mon, 8 Sep 2025 18:10:26 +0900 Subject: [PATCH 21/30] fix mutable texture image loading --- include/limitless/core/texture/texture_builder.hpp | 8 ++++---- src/limitless/core/texture/state_texture.cpp | 3 ++- src/limitless/core/texture/texture_builder.cpp | 12 ++++++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/include/limitless/core/texture/texture_builder.hpp b/include/limitless/core/texture/texture_builder.hpp index 50215849..15c03aae 100644 --- a/include/limitless/core/texture/texture_builder.hpp +++ b/include/limitless/core/texture/texture_builder.hpp @@ -45,10 +45,10 @@ namespace Limitless { Builder& wrap_t(Texture::Wrap wrap); Builder& wrap_r(Texture::Wrap wrap); - void useStateExtensionTexture(); - void useNamedExtensionTexture(); - void useBindlessExtensionTexture(); - void useBestSupportedExtensionTexture(); + Builder& useStateExtensionTexture(); + Builder& useNamedExtensionTexture(); + Builder& useBindlessExtensionTexture(); + Builder& useBestSupportedExtensionTexture(); std::shared_ptr buildMutable(); std::shared_ptr buildImmutable(); diff --git a/src/limitless/core/texture/state_texture.cpp b/src/limitless/core/texture/state_texture.cpp index f2d18ef7..4b27291c 100644 --- a/src/limitless/core/texture/state_texture.cpp +++ b/src/limitless/core/texture/state_texture.cpp @@ -83,8 +83,9 @@ void StateTexture::bind(GLenum target, GLuint index) const { throw std::logic_error{"Failed to bind texture to unit greater than accessible"}; } + activate(index); + if (ctx->texture_bound[index] != id) { - activate(index); glBindTexture(target, id); ctx->texture_bound[index] = id; } diff --git a/src/limitless/core/texture/texture_builder.cpp b/src/limitless/core/texture/texture_builder.cpp index 5d32e232..aec27b68 100644 --- a/src/limitless/core/texture/texture_builder.cpp +++ b/src/limitless/core/texture/texture_builder.cpp @@ -247,21 +247,23 @@ std::shared_ptr Texture::Builder::asDepth32F(glm::uvec2 size) { .build(); } -void Texture::Builder::useStateExtensionTexture() { +Texture::Builder& Texture::Builder::useStateExtensionTexture() { texture->texture = std::make_unique(); texture->texture->generateId(); + return *this; } -void Texture::Builder::useNamedExtensionTexture() { +Texture::Builder& Texture::Builder::useNamedExtensionTexture() { if (!ContextInitializer::isExtensionSupported("GL_ARB_direct_state_access")) { throw std::runtime_error{"NamedExtensionTexture is not supported!"}; } texture->texture = std::make_unique(static_cast(texture->target)); texture->texture->generateId(); + return *this; } -void Texture::Builder::useBindlessExtensionTexture() { +Texture::Builder& Texture::Builder::useBindlessExtensionTexture() { if (!ContextInitializer::isExtensionSupported("GL_ARB_bindless_texture")) { throw std::runtime_error{"BindlessTexture is not supported!"}; } @@ -271,12 +273,14 @@ void Texture::Builder::useBindlessExtensionTexture() { } texture->texture = std::make_unique(texture->texture.release()); + return *this; } -void Texture::Builder::useBestSupportedExtensionTexture() { +Texture::Builder& Texture::Builder::useBestSupportedExtensionTexture() { ContextInitializer::isExtensionSupported("GL_ARB_direct_state_access") ? useNamedExtensionTexture() : useStateExtensionTexture(); if (ContextInitializer::isExtensionSupported("GL_ARB_bindless_texture")) { useBindlessExtensionTexture(); } + return *this; } From a2a5cbf15c6decbc7681c3d4498c9d9f4fdb49c2 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Wed, 24 Sep 2025 20:00:27 +0900 Subject: [PATCH 22/30] more profiling --- src/limitless/core/shader/shader_program.cpp | 16 +++- .../shader/shader_program_texture_setter.cpp | 31 ++++--- src/limitless/core/texture/texture_binder.cpp | 2 + src/limitless/renderer/instance_renderer.cpp | 89 +++++++++++++------ 4 files changed, 100 insertions(+), 38 deletions(-) diff --git a/src/limitless/core/shader/shader_program.cpp b/src/limitless/core/shader/shader_program.cpp index e8ec65b9..949b6051 100644 --- a/src/limitless/core/shader/shader_program.cpp +++ b/src/limitless/core/shader/shader_program.cpp @@ -6,6 +6,7 @@ #include #include #include +#include using namespace Limitless; @@ -16,6 +17,7 @@ ShaderProgram::ShaderProgram(GLuint id) noexcept } void ShaderProgram::bindIndexedBuffers() { + CpuProfileScope scope(global_profiler, "ShaderProgram::bindIndexedBuffers"); for (auto& [target, name, block_index, bound_point, connected] : indexed_binds) { // connects index block inside program with state binding point if (!connected) { @@ -33,6 +35,7 @@ void ShaderProgram::bindIndexedBuffers() { if (auto* ctx = Context::getCurrentContext(); ctx) { // binds buffer to state binding point try { + CpuProfileScope scope(global_profiler, "ShaderProgram::bindIndexedBuffers::bindBuffer"); auto buffer = ctx->getIndexedBuffers().get(name); Buffer::Type program_target {}; @@ -72,14 +75,21 @@ ShaderProgram::~ShaderProgram() { void ShaderProgram::use() { if (auto* state = Context::getCurrentContext(); state) { if (state->shader_id != id) { + CpuProfileScope scope(global_profiler, "ShaderProgram::use::useProgram"); state->shader_id = id; glUseProgram(id); } - bindResources(); + { + CpuProfileScope scope(global_profiler, "ShaderProgram::use::bindResources"); + bindResources(); + } - for (auto& [name, uniform] : uniforms) { - uniform->set(); + { + CpuProfileScope scope(global_profiler, "ShaderProgram::use::setUniforms"); + for (auto& [name, uniform] : uniforms) { + uniform->set(); + } } } } diff --git a/src/limitless/core/shader/shader_program_texture_setter.cpp b/src/limitless/core/shader/shader_program_texture_setter.cpp index 469193b5..86d50b1f 100644 --- a/src/limitless/core/shader/shader_program_texture_setter.cpp +++ b/src/limitless/core/shader/shader_program_texture_setter.cpp @@ -4,32 +4,43 @@ #include #include #include +#include using namespace Limitless; void ShaderProgramTextureSetter::bindTextures(const std::map>& uniforms) { + CpuProfileScope scope(global_profiler, "ShaderProgramTextureSetter::bindTextures"); // first we collect all passed Texture Samplers std::vector samplers; - for (const auto& [_, uniform] : uniforms) { - if (uniform->getType() == UniformType::Sampler) { - samplers.emplace_back(static_cast(*uniform).getSampler().get()); //NOLINT + { + CpuProfileScope scope(global_profiler, "ShaderProgramTextureSetter::bindTextures::collectSamplers"); + for (const auto& [_, uniform] : uniforms) { + if (uniform->getType() == UniformType::Sampler) { + samplers.emplace_back(static_cast(*uniform).getSampler().get()); //NOLINT + } } } // then we collect only state textures // because not state (bindless) textures do not need this set up std::vector state_samplers; - TextureExtensionCapturer capturer {state_samplers}; - for (const auto& texture : samplers) { - texture->accept(capturer); + { + CpuProfileScope scope(global_profiler, "ShaderProgramTextureSetter::bindTextures::captureStateSamplers"); + TextureExtensionCapturer capturer {state_samplers}; + for (const auto& texture : samplers) { + texture->accept(capturer); + } } // then we determine to which units should we bind these textures std::vector se_samplers; - for (const auto& sampler : state_samplers) { - se_samplers.emplace_back(*std::find_if(samplers.begin(), samplers.end(), [&] (Texture* texture) { - return texture->getId() == sampler->getId(); - })); + { + CpuProfileScope scope(global_profiler, "ShaderProgramTextureSetter::bindTextures::collectSESamplers"); + for (const auto& sampler : state_samplers) { + se_samplers.emplace_back(*std::find_if(samplers.begin(), samplers.end(), [&] (Texture* texture) { + return texture->getId() == sampler->getId(); + })); + } } const auto units = TextureBinder::bind(se_samplers); diff --git a/src/limitless/core/texture/texture_binder.cpp b/src/limitless/core/texture/texture_binder.cpp index 06517e71..8171d6ae 100644 --- a/src/limitless/core/texture/texture_binder.cpp +++ b/src/limitless/core/texture/texture_binder.cpp @@ -3,6 +3,7 @@ #include #include #include +#include using namespace Limitless; // @@ -32,6 +33,7 @@ using namespace Limitless; //} std::vector TextureBinder::bind(const std::vector& textures) { + CpuProfileScope scope(global_profiler, "TextureBinder::bind"); if (textures.size() > static_cast(ContextInitializer::limits.max_texture_units)) { throw std::runtime_error("Failed to bind textures which more than texture units."); } diff --git a/src/limitless/renderer/instance_renderer.cpp b/src/limitless/renderer/instance_renderer.cpp index 6fd5eb1b..af96bbdc 100644 --- a/src/limitless/renderer/instance_renderer.cpp +++ b/src/limitless/renderer/instance_renderer.cpp @@ -4,37 +4,63 @@ using namespace Limitless; void InstanceRenderer::setRenderState(const Instance& instance, const MeshInstance& mesh, const DrawParameters& drawp) { - // sets culling based on two-sideness - if (mesh.getMaterial()->getTwoSided()) { - drawp.ctx.disable(Capabilities::CullFace); - } else { - drawp.ctx.enable(Capabilities::CullFace); + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::setRenderState::culling"); + // sets culling based on two-sideness + if (mesh.getMaterial()->getTwoSided()) { + drawp.ctx.disable(Capabilities::CullFace); + } else { + drawp.ctx.enable(Capabilities::CullFace); + } } - // front cullfacing for shadows helps prevent peter panning - if (drawp.type == ShaderType::DirectionalShadow) { - drawp.ctx.setCullFace(CullFace::Front); - } else { - drawp.ctx.setCullFace(CullFace::Back); + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::setRenderState::cullface"); + // front cullfacing for shadows helps prevent peter panning + if (drawp.type == ShaderType::DirectionalShadow) { + drawp.ctx.setCullFace(CullFace::Front); + } else { + drawp.ctx.setCullFace(CullFace::Back); + } } - setBlendingMode(mesh.getMaterial()->getBlending()); + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::setRenderState::blending"); + setBlendingMode(mesh.getMaterial()->getBlending()); + } // gets required shader from storage - auto& shader = drawp.assets.shaders.get(drawp.type, instance.getInstanceType(), mesh.getMaterial()->getShaderIndex()); - - instance.getInstanceBuffer()->bindBase(drawp.ctx.getIndexedBuffers().getBindingPoint(IndexedBuffer::Type::UniformBuffer, "INSTANCE_BUFFER")); + auto& shader = [&]() -> ShaderProgram& { + CpuProfileScope scope(global_profiler, "InstanceRenderer::setRenderState::getShader"); + return drawp.assets.shaders.get(drawp.type, instance.getInstanceType(), mesh.getMaterial()->getShaderIndex()); + }(); + + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::setRenderState::bindInstanceBuffer"); + instance.getInstanceBuffer()->bindBase(drawp.ctx.getIndexedBuffers().getBindingPoint(IndexedBuffer::Type::UniformBuffer, "INSTANCE_BUFFER")); + } - shader - .setMaterial(*mesh.getMaterial()); + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::setRenderState::setMaterial"); + shader.setMaterial(*mesh.getMaterial()); + } // sets custom pass-dependent uniforms - drawp.setter(shader); + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::setRenderState::setPassUniforms"); + drawp.setter(shader); + } // sets custom instance-dependent uniforms - drawp.isetter(shader, instance); + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::setRenderState::setInstanceUniforms"); + drawp.isetter(shader, instance); + } - shader.use(); + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::setRenderState::useShader"); + shader.use(); + } } bool InstanceRenderer::shouldBeRendered(const Instance &instance, const DrawParameters& drawp) { @@ -199,8 +225,13 @@ void InstanceRenderer::render(InstancedInstance &instance, const DrawParameters return; } - // bind buffer for instanced data - instance.getBuffer()->bindBase(drawp.ctx.getIndexedBuffers().getBindingPoint(IndexedBuffer::Type::ShaderStorage, "model_buffer")); + CpuProfileScope scope(global_profiler, "InstanceRenderer::renderInstancedInstance"); + + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::renderInstancedInstance::bindBuffer"); + // bind buffer for instanced data + instance.getBuffer()->bindBase(drawp.ctx.getIndexedBuffers().getBindingPoint(IndexedBuffer::Type::ShaderStorage, "model_buffer")); + } for (const auto& [_, mesh]: instance.getInstances()[0]->getMeshes()) { // skip mesh if blending is different @@ -208,11 +239,19 @@ void InstanceRenderer::render(InstancedInstance &instance, const DrawParameters return; } - // set render state: shaders, material, blending, etc - setRenderState(instance, mesh, drawp); + CpuProfileScope scope(global_profiler, "InstanceRenderer::renderInstancedInstance::renderMesh"); - // draw vertices - mesh.getMesh()->draw_instanced(instance.getVisibleInstances().size()); + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::renderInstancedInstance::renderMesh::setRenderState"); + // set render state: shaders, material, blending, etc + setRenderState(instance, mesh, drawp); + } + + { + CpuProfileScope scope(global_profiler, "InstanceRenderer::renderInstancedInstance::renderMesh::draw_instanced"); + // draw vertices + mesh.getMesh()->draw_instanced(instance.getVisibleInstances().size()); + } } } From cff36a79a86959fa948be55f68a6d65458656772 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Sun, 12 Oct 2025 21:56:44 +0900 Subject: [PATCH 23/30] pass tex loader flags to gltf loader --- .../limitless/loaders/gltf_model_loader.hpp | 13 ++++++-- include/limitless/loaders/texture_loader.hpp | 31 +++++++++++++++++++ src/limitless/loaders/gltf_model_loader.cpp | 27 +++++++++++----- src/limitless/loaders/texture_loader.cpp | 2 +- 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/include/limitless/loaders/gltf_model_loader.hpp b/include/limitless/loaders/gltf_model_loader.hpp index e07a8b4b..66882278 100644 --- a/include/limitless/loaders/gltf_model_loader.hpp +++ b/include/limitless/loaders/gltf_model_loader.hpp @@ -8,6 +8,7 @@ namespace Limitless { class AbstractModel; + class TextureLoaderFlags; enum class ModelLoaderOption { FlipUV, @@ -27,6 +28,7 @@ namespace Limitless { std::set options; float scale_factor {1.0f}; InstanceTypes additional_instance_types; + TextureLoaderFlags base_tex_flags; auto isPresent(ModelLoaderOption option) const { return options.count(option) != 0; } @@ -39,17 +41,24 @@ namespace Limitless { additional_instance_types.emplace(InstanceType::Instanced); return *this; } + + ModelLoaderFlags& baseTextureLoaderFlags(TextureLoaderFlags tex_flags) { + base_tex_flags = std::move(tex_flags); + return *this; + } }; class GltfModelLoader { public: - // Load a 3D model from given file. + // Load a GLTF 3D model from given file. // Will also attempt to load materials referenced in model definition. // Returns a shared pointer to resulting model on success. // Provided assets are modified. // On failure, a ModelLoadError exception is thrown. static std::shared_ptr loadModel( - Assets& assets, const fs::path& path, const ModelLoaderFlags& flags + Assets& assets, + const fs::path& path, + const ModelLoaderFlags& flags ); }; } diff --git a/include/limitless/loaders/texture_loader.hpp b/include/limitless/loaders/texture_loader.hpp index 112d0166..e2a8d8b2 100644 --- a/include/limitless/loaders/texture_loader.hpp +++ b/include/limitless/loaders/texture_loader.hpp @@ -39,6 +39,11 @@ namespace Limitless { glm::vec4 border_color {0.0f}; TextureLoaderFlags() = default; + // TextureLoaderFlags(const TextureLoaderFlags&) = default; + // TextureLoaderFlags(TextureLoaderFlags&&) = default; + // TextureLoaderFlags& operator=(const TextureLoaderFlags&) = default; + // TextureLoaderFlags& operator=(TextureLoaderFlags&&) = default; + TextureLoaderFlags(Origin _origin) noexcept : origin { _origin } {} TextureLoaderFlags(Origin _origin, Filter _filter) noexcept : origin { _origin }, filter { _filter } {} TextureLoaderFlags(Origin _origin, Space _space) noexcept : origin { _origin }, space {_space} {} @@ -46,6 +51,32 @@ namespace Limitless { TextureLoaderFlags(Filter _filter, Texture::Wrap _wrapping) noexcept : filter { _filter }, wrapping { _wrapping } {} TextureLoaderFlags(Space _space) noexcept : space { _space } {} TextureLoaderFlags(Texture::Wrap _wrapping) noexcept : wrapping { _wrapping } {} + + TextureLoaderFlags withSpace(Space new_space) const noexcept { + auto new_flags = *this; + new_flags.space = new_space; + return new_flags; + } + + TextureLoaderFlags withSrgb() const noexcept { + return withSpace(Space::sRGB); + } + + TextureLoaderFlags withLinearSpace() const noexcept { + return withSpace(Space::Linear); + } + + TextureLoaderFlags withNoMipmaps() const noexcept { + auto new_flags = *this; + new_flags.mipmap = false; + return new_flags; + } + + TextureLoaderFlags withDownscale(DownScale new_downscale) const noexcept { + auto new_flags = *this; + new_flags.downscale = new_downscale; + return new_flags; + } }; class texture_loader_exception : public std::runtime_error { diff --git a/src/limitless/loaders/gltf_model_loader.cpp b/src/limitless/loaders/gltf_model_loader.cpp index 8c2301f3..07b239dc 100644 --- a/src/limitless/loaders/gltf_model_loader.cpp +++ b/src/limitless/loaders/gltf_model_loader.cpp @@ -664,7 +664,7 @@ static std::shared_ptr loadMaterial( const cgltf_material& material, const std::string& model_name, size_t material_index, - [[maybe_unused]] const ModelLoaderFlags& flags + const ModelLoaderFlags& model_flags ) { ms::Material::Builder builder = ms::Material::builder(); const auto material_name = model_name + (material.name @@ -837,7 +837,8 @@ static std::shared_ptr loadMaterial( assets, name, cgltf_buffer_view_data(img.buffer_view), - img.buffer_view->size + img.buffer_view->size, + flags ); } else { @@ -879,8 +880,8 @@ static std::shared_ptr loadMaterial( // The base color texture MUST contain 8-bit values encoded with the // sRGB opto-electronic transfer function. - // TODO: deduce other flags from cgltf sampler. - const auto flags = TextureLoaderFlags(TextureLoaderFlags::Space::sRGB); + const auto flags = TextureLoaderFlags(model_flags.base_tex_flags) + .withSpace(TextureLoaderFlags::Space::sRGB); builder.diffuse(*loadTextureFrom(*base_color_tex, material_name + "_base_color", flags)); builder.color(toVec4(pbr_mr.base_color_factor)); @@ -900,7 +901,8 @@ static std::shared_ptr loadMaterial( auto* normal_tex = material.normal_texture.texture; if (normal_tex && normal_tex->image) { // These values MUST be encoded with a linear transfer function. - const auto flags = TextureLoaderFlags(TextureLoaderFlags::Space::Linear); + const auto flags = TextureLoaderFlags(model_flags.base_tex_flags) + .withSpace(TextureLoaderFlags::Space::Linear); builder.normal(*loadTextureFrom(*normal_tex, material_name + "_normal", flags)); } @@ -914,7 +916,8 @@ static std::shared_ptr loadMaterial( if (emissive_tex && emissive_tex->image) { // This texture contains RGB components encoded with the sRGB transfer // function - const auto flags = TextureLoaderFlags(TextureLoaderFlags::Space::sRGB); + const auto flags = TextureLoaderFlags(model_flags.base_tex_flags) + .withSpace(TextureLoaderFlags::Space::sRGB); builder.emissive_mask(*loadTextureFrom(*emissive_tex, material_name + "_emissive_mask", flags)); } @@ -989,7 +992,11 @@ static void fixMissingMaterials( } static SkeletalModel* loadSkeletalModel( - Assets& assets, const fs::path& path, const cgltf_data& src, const std::string& model_name, const ModelLoaderFlags& flags + Assets& assets, + const fs::path& path, + const cgltf_data& src, + const std::string& model_name, + const ModelLoaderFlags& flags ) { std::vector bones; std::unordered_map bone_indice_map; @@ -1091,7 +1098,11 @@ static SkeletalModel* loadSkeletalModel( } static Model* loadPlainModel( - Assets& assets, const fs::path& path, const cgltf_data& src, const std::string& model_name, const ModelLoaderFlags& flags + Assets& assets, + const fs::path& path, + const cgltf_data& src, + const std::string& model_name, + const ModelLoaderFlags& flags ) { std::vector> meshes; std::vector> mesh_materials; diff --git a/src/limitless/loaders/texture_loader.cpp b/src/limitless/loaders/texture_loader.cpp index f393f9be..be7bf33a 100644 --- a/src/limitless/loaders/texture_loader.cpp +++ b/src/limitless/loaders/texture_loader.cpp @@ -156,7 +156,7 @@ std::shared_ptr TextureLoader::load(Assets& assets, const fs::path& _pa #if LIMITLESS_OPENGL_DEBUG if (!isPowerOfTwo(width, height)) { - std::cerr << path.string() << " has not 2^n size, its not recommended to have it!" << std::endl; + std::cerr << path.string() << " size is not a power of 2, please resize!" << std::endl; } #endif From 58fa0e1dbde484f62c02792851b8a6082d1a6fa1 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Mon, 13 Oct 2025 16:09:52 +0900 Subject: [PATCH 24/30] add ASTC compression support --- .../limitless/core/context_initializer.hpp | 5 +- include/limitless/core/texture/texture.hpp | 6 +- include/limitless/loaders/texture_loader.hpp | 19 ++++- include/limitless/util/resource_container.hpp | 8 +++ src/limitless/assets.cpp | 70 +++++++++++++------ src/limitless/core/context_initializer.cpp | 6 +- src/limitless/loaders/gltf_model_loader.cpp | 17 ++++- src/limitless/loaders/texture_loader.cpp | 21 ++++++ 8 files changed, 120 insertions(+), 32 deletions(-) diff --git a/include/limitless/core/context_initializer.hpp b/include/limitless/core/context_initializer.hpp index 055533bb..6e98f6ea 100644 --- a/include/limitless/core/context_initializer.hpp +++ b/include/limitless/core/context_initializer.hpp @@ -26,7 +26,7 @@ namespace Limitless { static void initializeGLEW(); static void initializeGLFW(); - static void getExtensions() noexcept; + static void discoverExtensions() noexcept; static void getLimits() noexcept; ContextInitializer(); @@ -41,6 +41,9 @@ namespace Limitless { static void printExtensions() noexcept; static bool isExtensionSupported(std::string_view name) noexcept; static bool checkMinimumRequirements() noexcept; + static const auto& getExtensions() noexcept { + return extensions; + } static bool isProgramInterfaceQuerySupported() noexcept; static bool isBindlessTextureSupported() noexcept; diff --git a/include/limitless/core/texture/texture.hpp b/include/limitless/core/texture/texture.hpp index 7e4588a8..20343a6b 100644 --- a/include/limitless/core/texture/texture.hpp +++ b/include/limitless/core/texture/texture.hpp @@ -67,7 +67,11 @@ namespace Limitless { // GL_ARB_texture_compression_rgtc R_RGTC = GL_COMPRESSED_RED_RGTC1, - RG_RGTC = GL_COMPRESSED_RG_RGTC2 + RG_RGTC = GL_COMPRESSED_RG_RGTC2, + + // GL_KHR_texture_compression_astc_ldr + RGBA_ASTC_6x6 = GL_COMPRESSED_RGBA_ASTC_6x6_KHR, + sRGBA8_ASTC_6x6 = GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR }; enum class Format { diff --git a/include/limitless/loaders/texture_loader.hpp b/include/limitless/loaders/texture_loader.hpp index e2a8d8b2..f8761b11 100644 --- a/include/limitless/loaders/texture_loader.hpp +++ b/include/limitless/loaders/texture_loader.hpp @@ -14,7 +14,7 @@ namespace Limitless { // does not work for DDS formats enum class Origin { TopLeft, BottomLeft }; enum class Filter { Linear, Nearest }; - enum class Compression { None, Default, DXT1, DXT5, BC7, RGTC }; + enum class Compression { None, Default, DXT1, DXT5, BC7, RGTC, ASTC }; // works for dds with precomputed mipmaps only enum class DownScale { None = 0, x2, x4, x8, x16 }; enum class Space { sRGB, Linear }; @@ -58,6 +58,22 @@ namespace Limitless { return new_flags; } + TextureLoaderFlags withCompression(bool use_compression) const noexcept { + auto new_flags = *this; + new_flags.compression = use_compression + ? TextureLoaderFlags::Compression::Default + : TextureLoaderFlags::Compression::None; + return new_flags; + } + + TextureLoaderFlags withBestCompression() const noexcept { + return withCompression(true); + } + + TextureLoaderFlags withNoCompression() const noexcept { + return withCompression(false); + } + TextureLoaderFlags withSrgb() const noexcept { return withSpace(Space::sRGB); } @@ -82,6 +98,7 @@ namespace Limitless { class texture_loader_exception : public std::runtime_error { public: explicit texture_loader_exception(const char* msg) : std::runtime_error(msg) {} + explicit texture_loader_exception(std::string msg) : std::runtime_error(std::move(msg)) {} }; class TextureLoader final { diff --git a/include/limitless/util/resource_container.hpp b/include/limitless/util/resource_container.hpp index 08ca13fc..2b0c98e5 100644 --- a/include/limitless/util/resource_container.hpp +++ b/include/limitless/util/resource_container.hpp @@ -45,6 +45,10 @@ namespace Limitless { } } + /** + * Add a resource to the container. + * If the resource already exists, a resource_container_error exception is thrown. + */ void add(const std::string& name, std::shared_ptr res) { std::unique_lock lock(mutex); const auto result = resource.emplace(name, std::move(res)); @@ -53,6 +57,10 @@ namespace Limitless { } } + /** + * Remove a resource from the container. + * If the resource does not exist, nothing happens. + */ void remove(const std::string& name) { std::unique_lock lock(mutex); resource.erase(name); diff --git a/src/limitless/assets.cpp b/src/limitless/assets.cpp index 0621f7d2..ba1f1653 100644 --- a/src/limitless/assets.cpp +++ b/src/limitless/assets.cpp @@ -29,55 +29,79 @@ Assets::Assets(fs::path _base_dir, fs::path _shader_dir) noexcept void Assets::load([[maybe_unused]] Context& context) { // builds default materials for every model type - ms::Material::builder() - .name("default") - .shading(ms::Shading::Unlit) - .color({0.7f, 0.0f, 0.7f, 1.0f}) - .models({InstanceType::Model, InstanceType::Skeletal, InstanceType::Effect, InstanceType::Instanced }) - .two_sided(true) - .build(*this); - - ms::Material::builder() + if (!materials.contains("default")) { + ms::Material::builder() + .name("default") + .shading(ms::Shading::Unlit) + .color({0.7f, 0.0f, 0.7f, 1.0f}) + .models({InstanceType::Model, InstanceType::Skeletal, InstanceType::Effect, InstanceType::Instanced }) + .two_sided(true) + .build(*this); + } + + if (!materials.contains("red")) { + ms::Material::builder() .name("red") .color({1.0f, 0.0f, 0.0f, 1.0f}) .models({InstanceType::Model, InstanceType::Skeletal, InstanceType::Effect, InstanceType::Instanced, InstanceType::Decal }) .two_sided(true) .build(*this); + } - ms::Material::builder() + if (!materials.contains("blue")) { + ms::Material::builder() .name("blue") .color({0.0f, 0.0f, 1.0f, 1.0f}) .models({InstanceType::Model, InstanceType::Skeletal, InstanceType::Effect, InstanceType::Instanced }) .two_sided(true) .build(*this); + } - ms::Material::builder() + if (!materials.contains("green")) { + ms::Material::builder() .name("green") .color({0.0f, 1.0f, 0.0f, 1.0f}) .models({InstanceType::Model, InstanceType::Skeletal, InstanceType::Effect, InstanceType::Instanced }) .two_sided(true) .build(*this); + } // used in render as point light model - models.add("sphere", std::make_shared(glm::uvec2{32})); - meshes.add("sphere", models.at("sphere")->getMeshes().at(0)); + if (!models.contains("sphere")) { + models.add("sphere", std::make_shared(glm::uvec2{32})); + meshes.add("sphere", models.at("sphere")->getMeshes().at(0)); + } // used in postprocessing - models.add("quad", std::make_shared()); - meshes.add("quad", models.at("quad")->getMeshes().at(0)); + if (!models.contains("quad")) { + models.add("quad", std::make_shared()); + meshes.add("quad", models.at("quad")->getMeshes().at(0)); + } // used in skybox render - models.add("cube", std::make_shared()); - meshes.add("cube", models.at("cube")->getMeshes().at(0)); + if (!models.contains("cube")) { + models.add("cube", std::make_shared()); + meshes.add("cube", models.at("cube")->getMeshes().at(0)); + } + + if (!models.contains("plane")) { + models.add("plane", std::make_shared()); + meshes.add("plane", models.at("plane")->getMeshes().at(0)); + } - models.add("plane", std::make_shared()); - meshes.add("plane", models.at("plane")->getMeshes().at(0)); + if (!models.contains("planequad")) { + models.add("planequad", std::make_shared()); + meshes.add("planequad", models.at("planequad")->getMeshes().at(0)); + } - models.add("planequad", std::make_shared()); - meshes.add("planequad", models.at("planequad")->getMeshes().at(0)); + if (!models.contains("line")) { + models.add("line", std::make_shared(glm::vec3{0.0f}, glm::vec3{1.0f})); + } - models.add("line", std::make_shared(glm::vec3{0.0f}, glm::vec3{1.0f})); - models.add("cylinder", std::make_shared()); + if (!models.contains("cylinder")) { + models.add("cylinder", std::make_shared()); + meshes.add("cylinder", models.at("cylinder")->getMeshes().at(0)); + } } void Assets::initialize(Context& ctx, const RendererSettings& settings) { diff --git a/src/limitless/core/context_initializer.cpp b/src/limitless/core/context_initializer.cpp index ce9ff881..e63aeada 100644 --- a/src/limitless/core/context_initializer.cpp +++ b/src/limitless/core/context_initializer.cpp @@ -15,7 +15,7 @@ void ContextInitializer::initializeGLEW() { activate_debug(); #endif - getExtensions(); + discoverExtensions(); getLimits(); #ifdef LIMITLESS_OPENGL_DEBUG @@ -59,7 +59,7 @@ ContextInitializer::~ContextInitializer() { } } -void ContextInitializer::getExtensions() noexcept { +void ContextInitializer::discoverExtensions() noexcept { GLint count; glGetIntegerv(GL_NUM_EXTENSIONS, &count); @@ -70,7 +70,7 @@ void ContextInitializer::getExtensions() noexcept { } #ifdef LIMITLESS_OPENGL_NO_EXTENSIONS - std::cerr << "OpenGL toster mode" << std::endl; + std::cerr << "OpenGL toaster mode" << std::endl; extensions.clear(); extensions.emplace_back("GL_ARB_shader_storage_buffer_object"); extensions.emplace_back("GL_ARB_shading_language_420pack"); diff --git a/src/limitless/loaders/gltf_model_loader.cpp b/src/limitless/loaders/gltf_model_loader.cpp index 07b239dc..de6c921c 100644 --- a/src/limitless/loaders/gltf_model_loader.cpp +++ b/src/limitless/loaders/gltf_model_loader.cpp @@ -671,6 +671,8 @@ static std::shared_ptr loadMaterial( ? std::string(material.name) : generateMaterialName(model_name, material_index)); + // assets.materials.remove(material_name); + builder .name(material_name) .shading(material.unlit ? ms::Shading::Unlit : ms::Shading::Lit) @@ -833,6 +835,9 @@ static std::shared_ptr loadMaterial( throw ModelLoadError {"texture has no uri and no buffer view"}; } + // TODO: check if removal is needed. + // assets.textures.remove(name); + return TextureLoader::load( assets, name, @@ -848,6 +853,9 @@ static std::shared_ptr loadMaterial( if (comma && comma - img.uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) { auto buffer = bytesFromBase64(comma + 1); + // TODO: check if removal is needed. + // assets.textures.remove(name); + return TextureLoader::load( assets, name, @@ -861,6 +869,8 @@ static std::shared_ptr loadMaterial( } else { const auto path = base_path / fs::path(img.uri); + // TODO: check if removal is needed. + // assets.textures.remove(path.stem().string()); return TextureLoader::load(assets, path, flags); } } @@ -881,7 +891,7 @@ static std::shared_ptr loadMaterial( // The base color texture MUST contain 8-bit values encoded with the // sRGB opto-electronic transfer function. const auto flags = TextureLoaderFlags(model_flags.base_tex_flags) - .withSpace(TextureLoaderFlags::Space::sRGB); + .withSrgb(); builder.diffuse(*loadTextureFrom(*base_color_tex, material_name + "_base_color", flags)); builder.color(toVec4(pbr_mr.base_color_factor)); @@ -902,7 +912,8 @@ static std::shared_ptr loadMaterial( if (normal_tex && normal_tex->image) { // These values MUST be encoded with a linear transfer function. const auto flags = TextureLoaderFlags(model_flags.base_tex_flags) - .withSpace(TextureLoaderFlags::Space::Linear); + .withLinearSpace() + .withNoCompression(); builder.normal(*loadTextureFrom(*normal_tex, material_name + "_normal", flags)); } @@ -917,7 +928,7 @@ static std::shared_ptr loadMaterial( // This texture contains RGB components encoded with the sRGB transfer // function const auto flags = TextureLoaderFlags(model_flags.base_tex_flags) - .withSpace(TextureLoaderFlags::Space::sRGB); + .withSrgb(); builder.emissive_mask(*loadTextureFrom(*emissive_tex, material_name + "_emissive_mask", flags)); } diff --git a/src/limitless/loaders/texture_loader.cpp b/src/limitless/loaders/texture_loader.cpp index be7bf33a..a2318dbb 100644 --- a/src/limitless/loaders/texture_loader.cpp +++ b/src/limitless/loaders/texture_loader.cpp @@ -21,6 +21,7 @@ namespace { constexpr auto S3TC_EXTENSION = "GL_EXT_texture_compression_s3tc"; constexpr auto BPTC_EXTENSION = "GL_ARB_texture_compression_bptc"; constexpr auto RGTC_EXTENSION = "GL_ARB_texture_compression_rgtc"; + constexpr auto ASTC_EXTENSION = "GL_KHR_texture_compression_astc_ldr"; } void TextureLoader::setFormat(Texture::Builder& builder, const TextureLoaderFlags& flags, int channels) { @@ -91,11 +92,31 @@ void TextureLoader::setFormat(Texture::Builder& builder, const TextureLoaderFlag case 2: internal = Texture::InternalFormat::RG_RGTC; break; } break; + case TextureLoaderFlags::Compression::ASTC: + astc: + if (channels != 4) { + throw texture_loader_exception("ASTC compression is only supported for 4 channels, got " + std::to_string(channels)); + } + + if (!ContextInitializer::isExtensionSupported(ASTC_EXTENSION)) { + throw texture_loader_exception("ASTC compression is not supported!"); + } + + internal = (flags.space == TextureLoaderFlags::Space::sRGB) + ? Texture::InternalFormat::sRGBA8_ASTC_6x6 + : Texture::InternalFormat::RGBA_ASTC_6x6; + break; + case TextureLoaderFlags::Compression::Default: + // RGTC is good for normals / masks, bad for color. if ((channels == 1 || channels == 2) && ContextInitializer::isExtensionSupported(RGTC_EXTENSION)) { goto rgtc; } + if (channels == 4 && ContextInitializer::isExtensionSupported(ASTC_EXTENSION)) { + goto astc; + } + if ((channels == 3 || channels == 4) && ContextInitializer::isExtensionSupported(BPTC_EXTENSION)) { goto bc7; } From dcae8e85d9e4f5f3da92a8ec85e31a7f56f36e08 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Mon, 13 Oct 2025 17:53:00 +0900 Subject: [PATCH 25/30] remove ASTC compression support as it cannot be used online --- include/limitless/core/texture/texture.hpp | 4 ++-- include/limitless/loaders/texture_loader.hpp | 2 +- src/limitless/loaders/texture_loader.cpp | 18 ------------------ 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/include/limitless/core/texture/texture.hpp b/include/limitless/core/texture/texture.hpp index 20343a6b..1ca2d338 100644 --- a/include/limitless/core/texture/texture.hpp +++ b/include/limitless/core/texture/texture.hpp @@ -70,8 +70,8 @@ namespace Limitless { RG_RGTC = GL_COMPRESSED_RG_RGTC2, // GL_KHR_texture_compression_astc_ldr - RGBA_ASTC_6x6 = GL_COMPRESSED_RGBA_ASTC_6x6_KHR, - sRGBA8_ASTC_6x6 = GL_COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR + RGBA_ASTC_4x4 = GL_COMPRESSED_RGBA_ASTC_4x4_KHR, + sRGBA8_ASTC_4x4 = GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR }; enum class Format { diff --git a/include/limitless/loaders/texture_loader.hpp b/include/limitless/loaders/texture_loader.hpp index f8761b11..c6f9aadd 100644 --- a/include/limitless/loaders/texture_loader.hpp +++ b/include/limitless/loaders/texture_loader.hpp @@ -14,7 +14,7 @@ namespace Limitless { // does not work for DDS formats enum class Origin { TopLeft, BottomLeft }; enum class Filter { Linear, Nearest }; - enum class Compression { None, Default, DXT1, DXT5, BC7, RGTC, ASTC }; + enum class Compression { None, Default, DXT1, DXT5, BC7, RGTC }; // works for dds with precomputed mipmaps only enum class DownScale { None = 0, x2, x4, x8, x16 }; enum class Space { sRGB, Linear }; diff --git a/src/limitless/loaders/texture_loader.cpp b/src/limitless/loaders/texture_loader.cpp index a2318dbb..06bc9120 100644 --- a/src/limitless/loaders/texture_loader.cpp +++ b/src/limitless/loaders/texture_loader.cpp @@ -92,20 +92,6 @@ void TextureLoader::setFormat(Texture::Builder& builder, const TextureLoaderFlag case 2: internal = Texture::InternalFormat::RG_RGTC; break; } break; - case TextureLoaderFlags::Compression::ASTC: - astc: - if (channels != 4) { - throw texture_loader_exception("ASTC compression is only supported for 4 channels, got " + std::to_string(channels)); - } - - if (!ContextInitializer::isExtensionSupported(ASTC_EXTENSION)) { - throw texture_loader_exception("ASTC compression is not supported!"); - } - - internal = (flags.space == TextureLoaderFlags::Space::sRGB) - ? Texture::InternalFormat::sRGBA8_ASTC_6x6 - : Texture::InternalFormat::RGBA_ASTC_6x6; - break; case TextureLoaderFlags::Compression::Default: // RGTC is good for normals / masks, bad for color. @@ -113,10 +99,6 @@ void TextureLoader::setFormat(Texture::Builder& builder, const TextureLoaderFlag goto rgtc; } - if (channels == 4 && ContextInitializer::isExtensionSupported(ASTC_EXTENSION)) { - goto astc; - } - if ((channels == 3 || channels == 4) && ContextInitializer::isExtensionSupported(BPTC_EXTENSION)) { goto bc7; } From c89a964e32e7487cd7dff8ae49cbe19be657b1fa Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Tue, 14 Oct 2025 19:03:38 +0900 Subject: [PATCH 26/30] improve skeletal rendering --- .../instances/instanced_instance.hpp | 1 + src/limitless/renderer/instance_renderer.cpp | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/include/limitless/instances/instanced_instance.hpp b/include/limitless/instances/instanced_instance.hpp index b425b45f..a24b61b4 100644 --- a/include/limitless/instances/instanced_instance.hpp +++ b/include/limitless/instances/instanced_instance.hpp @@ -34,6 +34,7 @@ namespace Limitless { void update(const Camera &camera) override; auto& getInstances() noexcept { return instances; } + const auto& getInstances() const noexcept { return instances; } auto& getVisibleInstances() noexcept { return visible_instances; } auto& getBuffer() noexcept { return buffer ; } diff --git a/src/limitless/renderer/instance_renderer.cpp b/src/limitless/renderer/instance_renderer.cpp index af96bbdc..cc760f61 100644 --- a/src/limitless/renderer/instance_renderer.cpp +++ b/src/limitless/renderer/instance_renderer.cpp @@ -80,6 +80,50 @@ bool InstanceRenderer::shouldBeRendered(const Instance &instance, const DrawPara return false; } + { + bool has_blending_match = false; + + switch (instance.getInstanceType()) { + case InstanceType::Model: + case InstanceType::Skeletal: { + for (const auto& [_, mesh]: static_cast(instance).getMeshes()) { + if (mesh.getMaterial()->getBlending() == drawp.blending) { + has_blending_match = true; + break; + } + } + } + break; + case InstanceType::Instanced: { + for (const auto& [_, mesh]: static_cast(instance).getInstances()[0]->getMeshes()) { + if (mesh.getMaterial()->getBlending() == drawp.blending) { + has_blending_match = true; + break; + } + } + } + break; + case InstanceType::Decal: + if (static_cast(instance).getMaterial()->getBlending() == drawp.blending) { + has_blending_match = true; + } + break; + case InstanceType::SkeletalInstanced: + break; + case InstanceType::Effect: + break; + case InstanceType::Terrain: + if (static_cast(instance).getMeshes().begin()->second.getMaterial()->getBlending() == drawp.blending) { + has_blending_match = true; + } + break; + } + + if (!has_blending_match) { + return false; + } + } + return true; } From 7c21d04c9d60352bab7870c00959c65257167c73 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Sat, 18 Oct 2025 20:48:38 +0900 Subject: [PATCH 27/30] implement GLTF saver via cgltf plus extension support --- CMakeLists.txt | 2 + include/limitless/core/texture/texture.hpp | 2 +- .../limitless/loaders/gltf_model_saver.hpp | 17 + src/limitless/core/texture/texture.cpp | 2 +- src/limitless/loaders/cgltf.h | 726 ++++++-- src/limitless/loaders/cgltf_write.c | 2 + src/limitless/loaders/cgltf_write.h | 1651 +++++++++++++++++ src/limitless/loaders/gltf_model_loader.cpp | 97 +- src/limitless/loaders/gltf_model_saver.cpp | 644 +++++++ 9 files changed, 2935 insertions(+), 208 deletions(-) create mode 100644 include/limitless/loaders/gltf_model_saver.hpp create mode 100644 src/limitless/loaders/cgltf_write.c create mode 100644 src/limitless/loaders/cgltf_write.h create mode 100644 src/limitless/loaders/gltf_model_saver.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f8728b61..ee0ee7b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,8 @@ set(ENGINE_LOADERS src/limitless/loaders/dds_loader.cpp src/limitless/loaders/cgltf.c src/limitless/loaders/gltf_model_loader.cpp + src/limitless/loaders/cgltf_write.c + src/limitless/loaders/gltf_model_saver.cpp ) set(ENGINE_MODELS diff --git a/include/limitless/core/texture/texture.hpp b/include/limitless/core/texture/texture.hpp index 1ca2d338..59cebada 100644 --- a/include/limitless/core/texture/texture.hpp +++ b/include/limitless/core/texture/texture.hpp @@ -170,7 +170,7 @@ namespace Limitless { [[nodiscard]] auto isCubemapArray() const noexcept { return target == Type::TexCubeMapArray; } [[nodiscard]] uint32_t getId() const noexcept; [[nodiscard]] auto& getExtensionTexture() noexcept { return *texture; } - [[nodiscard]] std::vector getPixels() noexcept; + [[nodiscard]] std::vector getPixels() const noexcept; Texture& setMinFilter(Filter filter); Texture& setMagFilter(Filter filter); diff --git a/include/limitless/loaders/gltf_model_saver.hpp b/include/limitless/loaders/gltf_model_saver.hpp new file mode 100644 index 00000000..8fa3f4f9 --- /dev/null +++ b/include/limitless/loaders/gltf_model_saver.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +namespace Limitless { + struct ModelSaveError : public std::runtime_error { + explicit ModelSaveError(const std::string& msg) noexcept + : std::runtime_error(msg) {} + }; + + class GltfModelSaver { + public: + static void saveModel(const std::filesystem::path& output_path, const Model& model); + }; +} diff --git a/src/limitless/core/texture/texture.cpp b/src/limitless/core/texture/texture.cpp index 280f4b6e..8e13c136 100644 --- a/src/limitless/core/texture/texture.cpp +++ b/src/limitless/core/texture/texture.cpp @@ -319,7 +319,7 @@ static std::string bytesToHexString(const std::vector& bytes) { return ss.str(); } -std::vector Texture::getPixels() noexcept { +std::vector Texture::getPixels() const noexcept { std::vector pixels; pixels.resize(size.x * size.y * getBytesPerPixel()); diff --git a/src/limitless/loaders/cgltf.h b/src/limitless/loaders/cgltf.h index af24c65e..e0ae0e91 100644 --- a/src/limitless/loaders/cgltf.h +++ b/src/limitless/loaders/cgltf.h @@ -1,7 +1,7 @@ /** * cgltf - a single-file glTF 2.0 parser written in C99. * - * Version: 1.13 + * Version: 1.15 * * Website: https://github.com/jkuhlmann/cgltf * @@ -63,9 +63,14 @@ * By passing null for the output pointer, users can find out how many floats are required in the * output buffer. * + * `cgltf_accessor_unpack_indices` reads in the index data from an accessor. Assumes that + * `cgltf_load_buffers` has already been called. By passing null for the output pointer, users can + * find out how many indices are required in the output buffer. Returns 0 if the accessor is + * sparse or if the output component size is less than the accessor's component size. + * * `cgltf_num_components` is a tiny utility that tells you the dimensionality of * a certain accessor type. This can be used before `cgltf_accessor_unpack_floats` to help allocate - * the necessary amount of memory. `cgltf_component_size` and `cgltf_calc_size` exist for + * the necessary amount of memory. `cgltf_component_size` and `cgltf_calc_size` exist for * similar purposes. * * `cgltf_accessor_read_float` reads a certain element from a non-sparse accessor and converts it to @@ -75,7 +80,7 @@ * * `cgltf_accessor_read_uint` is similar to its floating-point counterpart, but limited to reading * vector types and does not support matrix types. The passed-in element size is the number of uints - * in the output buffer, which should be in the range [1, 4]. Returns false if the passed-in + * in the output buffer, which should be in the range [1, 4]. Returns false if the passed-in * element_size is too small, or if the accessor is sparse. * * `cgltf_accessor_read_index` is similar to its floating-point counterpart, but it returns size_t @@ -197,6 +202,7 @@ typedef enum cgltf_type typedef enum cgltf_primitive_type { + cgltf_primitive_type_invalid, cgltf_primitive_type_points, cgltf_primitive_type_lines, cgltf_primitive_type_line_loop, @@ -328,15 +334,6 @@ typedef struct cgltf_accessor_sparse cgltf_component_type indices_component_type; cgltf_buffer_view* values_buffer_view; cgltf_size values_byte_offset; - cgltf_extras extras; - cgltf_extras indices_extras; - cgltf_extras values_extras; - cgltf_size extensions_count; - cgltf_extension* extensions; - cgltf_size indices_extensions_count; - cgltf_extension* indices_extensions; - cgltf_size values_extensions_count; - cgltf_extension* values_extensions; } cgltf_accessor_sparse; typedef struct cgltf_accessor @@ -379,13 +376,29 @@ typedef struct cgltf_image cgltf_extension* extensions; } cgltf_image; +typedef enum cgltf_filter_type { + cgltf_filter_type_undefined = 0, + cgltf_filter_type_nearest = 9728, + cgltf_filter_type_linear = 9729, + cgltf_filter_type_nearest_mipmap_nearest = 9984, + cgltf_filter_type_linear_mipmap_nearest = 9985, + cgltf_filter_type_nearest_mipmap_linear = 9986, + cgltf_filter_type_linear_mipmap_linear = 9987 +} cgltf_filter_type; + +typedef enum cgltf_wrap_mode { + cgltf_wrap_mode_clamp_to_edge = 33071, + cgltf_wrap_mode_mirrored_repeat = 33648, + cgltf_wrap_mode_repeat = 10497 +} cgltf_wrap_mode; + typedef struct cgltf_sampler { char* name; - cgltf_int mag_filter; - cgltf_int min_filter; - cgltf_int wrap_s; - cgltf_int wrap_t; + cgltf_filter_type mag_filter; + cgltf_filter_type min_filter; + cgltf_wrap_mode wrap_s; + cgltf_wrap_mode wrap_t; cgltf_extras extras; cgltf_size extensions_count; cgltf_extension* extensions; @@ -398,6 +411,8 @@ typedef struct cgltf_texture cgltf_sampler* sampler; cgltf_bool has_basisu; cgltf_image* basisu_image; + cgltf_bool has_webp; + cgltf_image* webp_image; cgltf_extras extras; cgltf_size extensions_count; cgltf_extension* extensions; @@ -419,9 +434,6 @@ typedef struct cgltf_texture_view cgltf_float scale; /* equivalent to strength for occlusion_texture */ cgltf_bool has_transform; cgltf_texture_transform transform; - cgltf_extras extras; - cgltf_size extensions_count; - cgltf_extension* extensions; } cgltf_texture_view; typedef struct cgltf_pbr_metallic_roughness @@ -504,6 +516,14 @@ typedef struct cgltf_iridescence cgltf_texture_view iridescence_thickness_texture; } cgltf_iridescence; +typedef struct cgltf_diffuse_transmission +{ + cgltf_texture_view diffuse_transmission_texture; + cgltf_float diffuse_transmission_factor; + cgltf_float diffuse_transmission_color_factor[3]; + cgltf_texture_view diffuse_transmission_color_texture; +} cgltf_diffuse_transmission; + typedef struct cgltf_anisotropy { cgltf_float anisotropy_strength; @@ -511,6 +531,53 @@ typedef struct cgltf_anisotropy cgltf_texture_view anisotropy_texture; } cgltf_anisotropy; +typedef struct cgltf_dispersion +{ + cgltf_float dispersion; +} cgltf_dispersion; + +typedef enum cgltf_uniform_type { + cgltf_uniform_type_value, + cgltf_uniform_type_sampler, + cgltf_uniform_type_time, +} cgltf_uniform_type; + +typedef enum cgltf_uniform_value_type { + cgltf_uniform_value_type_int, + cgltf_uniform_value_type_uint, + cgltf_uniform_value_type_float, + cgltf_uniform_value_type_vec2, + cgltf_uniform_value_type_vec3, + cgltf_uniform_value_type_vec4, + cgltf_uniform_value_type_mat3, + cgltf_uniform_value_type_mat4, + cgltf_uniform_value_type_texture +} cgltf_uniform_value_type; + +typedef union cgltf_uniform_value +{ + cgltf_int int_value; + cgltf_uint uint_value; + cgltf_float float_value; + cgltf_float vec2_value[2]; + cgltf_float vec3_value[3]; + cgltf_float vec4_value[4]; + cgltf_float mat3_value[3][3]; + cgltf_float mat4_value[4][4]; + cgltf_texture* texture_value; +} cgltf_uniform_value; + +typedef struct cgltf_uniform +{ + char* name; + cgltf_uniform_type type; + cgltf_uniform_value_type value_type; + cgltf_uniform_value value; +} cgltf_uniform; + +#define CGLTF_LIMITLESS_MATERIAL_MODELS_MODEL 1 +#define CGLTF_LIMITLESS_MATERIAL_MODELS_INSTANCED 2 + typedef struct cgltf_material { char* name; @@ -524,7 +591,9 @@ typedef struct cgltf_material cgltf_bool has_sheen; cgltf_bool has_emissive_strength; cgltf_bool has_iridescence; + cgltf_bool has_diffuse_transmission; cgltf_bool has_anisotropy; + cgltf_bool has_dispersion; cgltf_pbr_metallic_roughness pbr_metallic_roughness; cgltf_pbr_specular_glossiness pbr_specular_glossiness; cgltf_clearcoat clearcoat; @@ -535,7 +604,9 @@ typedef struct cgltf_material cgltf_volume volume; cgltf_emissive_strength emissive_strength; cgltf_iridescence iridescence; + cgltf_diffuse_transmission diffuse_transmission; cgltf_anisotropy anisotropy; + cgltf_dispersion dispersion; cgltf_texture_view normal_texture; cgltf_texture_view occlusion_texture; cgltf_texture_view emissive_texture; @@ -544,6 +615,10 @@ typedef struct cgltf_material cgltf_float alpha_cutoff; cgltf_bool double_sided; cgltf_bool unlit; + cgltf_uniform* uniforms; + cgltf_size uniforms_count; + char* fragment; + unsigned int models; cgltf_extras extras; cgltf_size extensions_count; cgltf_extension* extensions; @@ -841,6 +916,8 @@ void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix) const uint8_t* cgltf_buffer_view_data(const cgltf_buffer_view* view); +const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int index); + cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size); cgltf_bool cgltf_accessor_read_uint(const cgltf_accessor* accessor, cgltf_size index, cgltf_uint* out, cgltf_size element_size); cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index); @@ -850,7 +927,7 @@ cgltf_size cgltf_component_size(cgltf_component_type component_type); cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type); cgltf_size cgltf_accessor_unpack_floats(const cgltf_accessor* accessor, cgltf_float* out, cgltf_size float_count); -cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, cgltf_uint* out, cgltf_size index_count); +cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, void* out, cgltf_size out_component_size, cgltf_size index_count); /* this function is deprecated and will be removed in the future; use cgltf_extras::data instead */ cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size); @@ -950,8 +1027,8 @@ static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t #ifndef CGLTF_CONSTS -static const cgltf_size GlbHeaderSize = 12; -static const cgltf_size GlbChunkHeaderSize = 8; +#define GlbHeaderSize 12 +#define GlbChunkHeaderSize 8 static const uint32_t GlbVersion = 2; static const uint32_t GlbMagic = 0x46546C67; static const uint32_t GlbMagicJsonChunk = 0x4E4F534A; @@ -1045,7 +1122,7 @@ static cgltf_result cgltf_default_file_read(const struct cgltf_memory_options* m fclose(file); return cgltf_result_out_of_memory; } - + cgltf_size read_size = fread(file_data, 1, file_size, file); fclose(file); @@ -1153,7 +1230,7 @@ cgltf_result cgltf_parse(const cgltf_options* options, const void* data, cgltf_s // JSON chunk: length uint32_t json_length; memcpy(&json_length, json_chunk, 4); - if (GlbHeaderSize + GlbChunkHeaderSize + json_length > size) + if (json_length > size - GlbHeaderSize - GlbChunkHeaderSize) { return cgltf_result_data_too_short; } @@ -1167,10 +1244,10 @@ cgltf_result cgltf_parse(const cgltf_options* options, const void* data, cgltf_s json_chunk += GlbChunkHeaderSize; - const void* bin = 0; + const void* bin = NULL; cgltf_size bin_size = 0; - if (GlbHeaderSize + GlbChunkHeaderSize + json_length + GlbChunkHeaderSize <= size) + if (GlbChunkHeaderSize <= size - GlbHeaderSize - GlbChunkHeaderSize - json_length) { // We can read another chunk const uint8_t* bin_chunk = json_chunk + json_length; @@ -1178,7 +1255,7 @@ cgltf_result cgltf_parse(const cgltf_options* options, const void* data, cgltf_s // Bin chunk: length uint32_t bin_length; memcpy(&bin_length, bin_chunk, 4); - if (GlbHeaderSize + GlbChunkHeaderSize + json_length + GlbChunkHeaderSize + bin_length > size) + if (bin_length > size - GlbHeaderSize - GlbChunkHeaderSize - json_length - GlbChunkHeaderSize) { return cgltf_result_data_too_short; } @@ -1564,6 +1641,9 @@ cgltf_result cgltf_validate(cgltf_data* data) { cgltf_accessor* accessor = &data->accessors[i]; + CGLTF_ASSERT_IF(data->accessors[i].component_type == cgltf_component_type_invalid, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->accessors[i].type == cgltf_type_invalid, cgltf_result_invalid_gltf); + cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type); if (accessor->buffer_view) @@ -1577,7 +1657,7 @@ cgltf_result cgltf_validate(cgltf_data* data) { cgltf_accessor_sparse* sparse = &accessor->sparse; - cgltf_size indices_component_size = cgltf_calc_size(cgltf_type_scalar, sparse->indices_component_type); + cgltf_size indices_component_size = cgltf_component_size(sparse->indices_component_type); cgltf_size indices_req_size = sparse->indices_byte_offset + indices_component_size * sparse->count; cgltf_size values_req_size = sparse->values_byte_offset + element_size * sparse->count; @@ -1643,43 +1723,48 @@ cgltf_result cgltf_validate(cgltf_data* data) for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j) { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].type == cgltf_primitive_type_invalid, cgltf_result_invalid_gltf); CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets_count != data->meshes[i].primitives[0].targets_count, cgltf_result_invalid_gltf); - if (data->meshes[i].primitives[j].attributes_count) + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes_count == 0, cgltf_result_invalid_gltf); + + cgltf_accessor* first = data->meshes[i].primitives[j].attributes[0].data; + + CGLTF_ASSERT_IF(first->count == 0, cgltf_result_invalid_gltf); + + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) { - cgltf_accessor* first = data->meshes[i].primitives[j].attributes[0].data; + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + } - for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k) + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) + { + for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) { - CGLTF_ASSERT_IF(data->meshes[i].primitives[j].attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets[k].attributes[m].data->count != first->count, cgltf_result_invalid_gltf); } + } - for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k) - { - for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m) - { - CGLTF_ASSERT_IF(data->meshes[i].primitives[j].targets[k].attributes[m].data->count != first->count, cgltf_result_invalid_gltf); - } - } + cgltf_accessor* indices = data->meshes[i].primitives[j].indices; - cgltf_accessor* indices = data->meshes[i].primitives[j].indices; + CGLTF_ASSERT_IF(indices && + indices->component_type != cgltf_component_type_r_8u && + indices->component_type != cgltf_component_type_r_16u && + indices->component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); - CGLTF_ASSERT_IF(indices && - indices->component_type != cgltf_component_type_r_8u && - indices->component_type != cgltf_component_type_r_16u && - indices->component_type != cgltf_component_type_r_32u, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(indices && indices->type != cgltf_type_scalar, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(indices && indices->stride != cgltf_component_size(indices->component_type), cgltf_result_invalid_gltf); - if (indices && indices->buffer_view && indices->buffer_view->buffer->data) - { - cgltf_size index_bound = cgltf_calc_index_bound(indices->buffer_view, indices->offset, indices->component_type, indices->count); + if (indices && indices->buffer_view && indices->buffer_view->buffer->data) + { + cgltf_size index_bound = cgltf_calc_index_bound(indices->buffer_view, indices->offset, indices->component_type, indices->count); - CGLTF_ASSERT_IF(index_bound >= first->count, cgltf_result_data_too_short); - } + CGLTF_ASSERT_IF(index_bound >= first->count, cgltf_result_data_too_short); + } - for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) - { - CGLTF_ASSERT_IF(data->meshes[i].primitives[j].mappings[k].variant >= data->variants_count, cgltf_result_invalid_gltf); - } + for (cgltf_size k = 0; k < data->meshes[i].primitives[j].mappings_count; ++k) + { + CGLTF_ASSERT_IF(data->meshes[i].primitives[j].mappings[k].variant >= data->variants_count, cgltf_result_invalid_gltf); } } } @@ -1688,7 +1773,20 @@ cgltf_result cgltf_validate(cgltf_data* data) { if (data->nodes[i].weights && data->nodes[i].mesh) { - CGLTF_ASSERT_IF (data->nodes[i].mesh->primitives_count && data->nodes[i].mesh->primitives[0].targets_count != data->nodes[i].weights_count, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->nodes[i].mesh->primitives_count && data->nodes[i].mesh->primitives[0].targets_count != data->nodes[i].weights_count, cgltf_result_invalid_gltf); + } + + if (data->nodes[i].has_mesh_gpu_instancing) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh == NULL, cgltf_result_invalid_gltf); + CGLTF_ASSERT_IF(data->nodes[i].mesh_gpu_instancing.attributes_count == 0, cgltf_result_invalid_gltf); + + cgltf_accessor* first = data->nodes[i].mesh_gpu_instancing.attributes[0].data; + + for (cgltf_size k = 0; k < data->nodes[i].mesh_gpu_instancing.attributes_count; ++k) + { + CGLTF_ASSERT_IF(data->nodes[i].mesh_gpu_instancing.attributes[k].data->count != first->count, cgltf_result_invalid_gltf); + } } } @@ -1736,10 +1834,15 @@ cgltf_result cgltf_validate(cgltf_data* data) cgltf_size values = channel->sampler->interpolation == cgltf_interpolation_type_cubic_spline ? 3 : 1; - CGLTF_ASSERT_IF(channel->sampler->input->count * components * values != channel->sampler->output->count, cgltf_result_data_too_short); + CGLTF_ASSERT_IF(channel->sampler->input->count * components * values != channel->sampler->output->count, cgltf_result_invalid_gltf); } } + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + CGLTF_ASSERT_IF(!data->variants[i].name, cgltf_result_invalid_gltf); + } + return cgltf_result_success; } @@ -1786,12 +1889,6 @@ static void cgltf_free_extensions(cgltf_data* data, cgltf_extension* extensions, data->memory.free_func(data->memory.user_data, extensions); } -static void cgltf_free_texture_view(cgltf_data* data, cgltf_texture_view* view) -{ - cgltf_free_extensions(data, view->extensions, view->extensions_count); - cgltf_free_extras(data, &view->extras); -} - void cgltf_free(cgltf_data* data) { if (!data) @@ -1813,15 +1910,6 @@ void cgltf_free(cgltf_data* data) { data->memory.free_func(data->memory.user_data, data->accessors[i].name); - if(data->accessors[i].is_sparse) - { - cgltf_free_extensions(data, data->accessors[i].sparse.extensions, data->accessors[i].sparse.extensions_count); - cgltf_free_extensions(data, data->accessors[i].sparse.indices_extensions, data->accessors[i].sparse.indices_extensions_count); - cgltf_free_extensions(data, data->accessors[i].sparse.values_extensions, data->accessors[i].sparse.values_extensions_count); - cgltf_free_extras(data, &data->accessors[i].sparse.extras); - cgltf_free_extras(data, &data->accessors[i].sparse.indices_extras); - cgltf_free_extras(data, &data->accessors[i].sparse.values_extras); - } cgltf_free_extensions(data, data->accessors[i].extensions, data->accessors[i].extensions_count); cgltf_free_extras(data, &data->accessors[i].extras); } @@ -1923,61 +2011,13 @@ void cgltf_free(cgltf_data* data) { data->memory.free_func(data->memory.user_data, data->materials[i].name); - if(data->materials[i].has_pbr_metallic_roughness) - { - cgltf_free_texture_view(data, &data->materials[i].pbr_metallic_roughness.metallic_roughness_texture); - cgltf_free_texture_view(data, &data->materials[i].pbr_metallic_roughness.base_color_texture); - } - if(data->materials[i].has_pbr_specular_glossiness) - { - cgltf_free_texture_view(data, &data->materials[i].pbr_specular_glossiness.diffuse_texture); - cgltf_free_texture_view(data, &data->materials[i].pbr_specular_glossiness.specular_glossiness_texture); - } - if(data->materials[i].has_clearcoat) - { - cgltf_free_texture_view(data, &data->materials[i].clearcoat.clearcoat_texture); - cgltf_free_texture_view(data, &data->materials[i].clearcoat.clearcoat_roughness_texture); - cgltf_free_texture_view(data, &data->materials[i].clearcoat.clearcoat_normal_texture); - } - if(data->materials[i].has_specular) - { - cgltf_free_texture_view(data, &data->materials[i].specular.specular_texture); - cgltf_free_texture_view(data, &data->materials[i].specular.specular_color_texture); - } - if(data->materials[i].has_transmission) - { - cgltf_free_texture_view(data, &data->materials[i].transmission.transmission_texture); - } - if (data->materials[i].has_volume) - { - cgltf_free_texture_view(data, &data->materials[i].volume.thickness_texture); - } - if(data->materials[i].has_sheen) - { - cgltf_free_texture_view(data, &data->materials[i].sheen.sheen_color_texture); - cgltf_free_texture_view(data, &data->materials[i].sheen.sheen_roughness_texture); - } - if(data->materials[i].has_iridescence) - { - cgltf_free_texture_view(data, &data->materials[i].iridescence.iridescence_texture); - cgltf_free_texture_view(data, &data->materials[i].iridescence.iridescence_thickness_texture); - } - if (data->materials[i].has_anisotropy) - { - cgltf_free_texture_view(data, &data->materials[i].anisotropy.anisotropy_texture); - } - - cgltf_free_texture_view(data, &data->materials[i].normal_texture); - cgltf_free_texture_view(data, &data->materials[i].occlusion_texture); - cgltf_free_texture_view(data, &data->materials[i].emissive_texture); - cgltf_free_extensions(data, data->materials[i].extensions, data->materials[i].extensions_count); cgltf_free_extras(data, &data->materials[i].extras); } data->memory.free_func(data->memory.user_data, data->materials); - for (cgltf_size i = 0; i < data->images_count; ++i) + for (cgltf_size i = 0; i < data->images_count; ++i) { data->memory.free_func(data->memory.user_data, data->images[i].name); data->memory.free_func(data->memory.user_data, data->images[i].uri); @@ -2225,8 +2265,6 @@ static cgltf_ssize cgltf_component_read_integer(const void* in, cgltf_component_ return *((const uint16_t*) in); case cgltf_component_type_r_32u: return *((const uint32_t*) in); - case cgltf_component_type_r_32f: - return (cgltf_ssize)*((const float*) in); case cgltf_component_type_r_8: return *((const int8_t*) in); case cgltf_component_type_r_8u: @@ -2244,8 +2282,6 @@ static cgltf_size cgltf_component_read_index(const void* in, cgltf_component_typ return *((const uint16_t*) in); case cgltf_component_type_r_32u: return *((const uint32_t*) in); - case cgltf_component_type_r_32f: - return (cgltf_size)((cgltf_ssize)*((const float*) in)); case cgltf_component_type_r_8u: return *((const uint8_t*) in); default: @@ -2350,6 +2386,18 @@ const uint8_t* cgltf_buffer_view_data(const cgltf_buffer_view* view) return result; } +const cgltf_accessor* cgltf_find_accessor(const cgltf_primitive* prim, cgltf_attribute_type type, cgltf_int index) +{ + for (cgltf_size i = 0; i < prim->attributes_count; ++i) + { + const cgltf_attribute* attr = &prim->attributes[i]; + if (attr->type == type && attr->index == index) + return attr->data; + } + + return NULL; +} + cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size) { if (accessor->is_sparse) @@ -2629,7 +2677,7 @@ cgltf_size cgltf_animation_channel_index(const cgltf_animation* animation, const return (cgltf_size)(object - animation->channels); } -cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, cgltf_uint* out, cgltf_size index_count) +cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, void* out, cgltf_size out_component_size, cgltf_size index_count) { if (out == NULL) { @@ -2637,6 +2685,7 @@ cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, cgltf_u } index_count = accessor->count < index_count ? accessor->count : index_count; + cgltf_size index_component_size = cgltf_component_size(accessor->component_type); if (accessor->is_sparse) { @@ -2646,6 +2695,10 @@ cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, cgltf_u { return 0; } + if (index_component_size > out_component_size) + { + return 0; + } const uint8_t* element = cgltf_buffer_view_data(accessor->buffer_view); if (element == NULL) { @@ -2653,18 +2706,29 @@ cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, cgltf_u } element += accessor->offset; - if (accessor->component_type == cgltf_component_type_r_32u && accessor->stride == sizeof(cgltf_uint)) + if (index_component_size == out_component_size && accessor->stride == out_component_size) { - memcpy(out, element, index_count * sizeof(cgltf_uint)); + memcpy(out, element, index_count * index_component_size); + return index_count; } - else - { - cgltf_uint* dest = out; - for (cgltf_size index = 0; index < index_count; index++, dest++, element += accessor->stride) + // The component size of the output array is larger than the component size of the index data, so index data will be padded. + switch (out_component_size) + { + case 2: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) { - *dest = (cgltf_uint)cgltf_component_read_index(element, accessor->component_type); + ((uint16_t*)out)[index] = (uint16_t)cgltf_component_read_index(element, accessor->component_type); } + break; + case 4: + for (cgltf_size index = 0; index < index_count; index++, element += accessor->stride) + { + ((uint32_t*)out)[index] = (uint32_t)cgltf_component_read_index(element, accessor->component_type); + } + break; + default: + break; } return index_count; @@ -2675,7 +2739,7 @@ cgltf_size cgltf_accessor_unpack_indices(const cgltf_accessor* accessor, cgltf_u #define CGLTF_ERROR_LEGACY -3 #define CGLTF_CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return CGLTF_ERROR_JSON; } -#define CGLTF_CHECK_TOKTYPE_RETTYPE(tok_, type_, ret_) if ((tok_).type != (type_)) { return (ret_)CGLTF_ERROR_JSON; } +#define CGLTF_CHECK_TOKTYPE_RET(tok_, type_, ret_) if ((tok_).type != (type_)) { return ret_; } #define CGLTF_CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return CGLTF_ERROR_JSON; } /* checking size for 0 verifies that a value follows the key */ #define CGLTF_PTRINDEX(type, idx) (type*)((cgltf_size)idx + 1) @@ -2702,12 +2766,13 @@ static int cgltf_json_to_int(jsmntok_t const* tok, const uint8_t* json_chunk) static cgltf_size cgltf_json_to_size(jsmntok_t const* tok, const uint8_t* json_chunk) { - CGLTF_CHECK_TOKTYPE_RETTYPE(*tok, JSMN_PRIMITIVE, cgltf_size); + CGLTF_CHECK_TOKTYPE_RET(*tok, JSMN_PRIMITIVE, 0); char tmp[128]; int size = (size_t)(tok->end - tok->start) < sizeof(tmp) ? (int)(tok->end - tok->start) : (int)(sizeof(tmp) - 1); strncpy(tmp, (const char*)json_chunk + tok->start, size); tmp[size] = 0; - return (cgltf_size)CGLTF_ATOLL(tmp); + long long res = CGLTF_ATOLL(tmp); + return res < 0 ? 0 : (cgltf_size)res; } static cgltf_float cgltf_json_to_float(jsmntok_t const* tok, const uint8_t* json_chunk) @@ -2889,6 +2954,11 @@ static void cgltf_parse_attribute_type(const char* name, cgltf_attribute_type* o if (us && *out_type != cgltf_attribute_type_invalid) { *out_index = CGLTF_ATOI(us + 1); + if (*out_index < 0) + { + *out_type = cgltf_attribute_type_invalid; + *out_index = 0; + } } } @@ -3221,6 +3291,31 @@ static int cgltf_parse_json_material_mappings(cgltf_options* options, jsmntok_t return i; } +static cgltf_primitive_type cgltf_json_to_primitive_type(jsmntok_t const* tok, const uint8_t* json_chunk) +{ + int type = cgltf_json_to_int(tok, json_chunk); + + switch (type) + { + case 0: + return cgltf_primitive_type_points; + case 1: + return cgltf_primitive_type_lines; + case 2: + return cgltf_primitive_type_line_loop; + case 3: + return cgltf_primitive_type_line_strip; + case 4: + return cgltf_primitive_type_triangles; + case 5: + return cgltf_primitive_type_triangle_strip; + case 6: + return cgltf_primitive_type_triangle_fan; + default: + return cgltf_primitive_type_invalid; + } +} + static int cgltf_parse_json_primitive(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim) { CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); @@ -3237,9 +3332,7 @@ static int cgltf_parse_json_primitive(cgltf_options* options, jsmntok_t const* t if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) { ++i; - out_prim->type - = (cgltf_primitive_type) - cgltf_json_to_int(tokens+i, json_chunk); + out_prim->type = cgltf_json_to_primitive_type(tokens+i, json_chunk); ++i; } else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) @@ -3475,7 +3568,7 @@ static cgltf_component_type cgltf_json_to_component_type(jsmntok_t const* tok, c } } -static int cgltf_parse_json_accessor_sparse(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor_sparse* out_sparse) +static int cgltf_parse_json_accessor_sparse(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor_sparse* out_sparse) { CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); @@ -3489,7 +3582,7 @@ static int cgltf_parse_json_accessor_sparse(cgltf_options* options, jsmntok_t co if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) { ++i; - out_sparse->count = cgltf_json_to_int(tokens + i, json_chunk); + out_sparse->count = cgltf_json_to_size(tokens + i, json_chunk); ++i; } else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0) @@ -3522,14 +3615,6 @@ static int cgltf_parse_json_accessor_sparse(cgltf_options* options, jsmntok_t co out_sparse->indices_component_type = cgltf_json_to_component_type(tokens + i, json_chunk); ++i; } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) - { - i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_sparse->indices_extras); - } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) - { - i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sparse->indices_extensions_count, &out_sparse->indices_extensions); - } else { i = cgltf_skip_json(tokens, i+1); @@ -3565,14 +3650,6 @@ static int cgltf_parse_json_accessor_sparse(cgltf_options* options, jsmntok_t co out_sparse->values_byte_offset = cgltf_json_to_size(tokens + i, json_chunk); ++i; } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) - { - i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_sparse->values_extras); - } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) - { - i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sparse->values_extensions_count, &out_sparse->values_extensions); - } else { i = cgltf_skip_json(tokens, i+1); @@ -3584,14 +3661,6 @@ static int cgltf_parse_json_accessor_sparse(cgltf_options* options, jsmntok_t co } } } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) - { - i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_sparse->extras); - } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) - { - i = cgltf_parse_json_unprocessed_extensions(options, tokens, i, json_chunk, &out_sparse->extensions_count, &out_sparse->extensions); - } else { i = cgltf_skip_json(tokens, i+1); @@ -3649,8 +3718,7 @@ static int cgltf_parse_json_accessor(cgltf_options* options, jsmntok_t const* to else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) { ++i; - out_accessor->count = - cgltf_json_to_int(tokens+i, json_chunk); + out_accessor->count = cgltf_json_to_size(tokens+i, json_chunk); ++i; } else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0) @@ -3705,7 +3773,7 @@ static int cgltf_parse_json_accessor(cgltf_options* options, jsmntok_t const* to else if (cgltf_json_strcmp(tokens + i, json_chunk, "sparse") == 0) { out_accessor->is_sparse = 1; - i = cgltf_parse_json_accessor_sparse(options, tokens, i + 1, json_chunk, &out_accessor->sparse); + i = cgltf_parse_json_accessor_sparse(tokens, i + 1, json_chunk, &out_accessor->sparse); } else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) { @@ -3777,6 +3845,8 @@ static int cgltf_parse_json_texture_transform(jsmntok_t const* tokens, int i, co static int cgltf_parse_json_texture_view(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_view* out_texture_view) { + (void)options; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); out_texture_view->scale = 1.0f; @@ -3801,7 +3871,7 @@ static int cgltf_parse_json_texture_view(cgltf_options* options, jsmntok_t const out_texture_view->texcoord = cgltf_json_to_int(tokens + i, json_chunk); ++i; } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) + else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) { ++i; out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); @@ -3813,28 +3883,12 @@ static int cgltf_parse_json_texture_view(cgltf_options* options, jsmntok_t const out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk); ++i; } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) - { - i = cgltf_parse_json_extras(options, tokens, i + 1, json_chunk, &out_texture_view->extras); - } else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0) { ++i; CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); - if(out_texture_view->extensions) - { - return CGLTF_ERROR_JSON; - } - int extensions_size = tokens[i].size; - out_texture_view->extensions_count = 0; - out_texture_view->extensions = (cgltf_extension*)cgltf_calloc(options, sizeof(cgltf_extension), extensions_size); - - if (!out_texture_view->extensions) - { - return CGLTF_ERROR_NOMEM; - } ++i; @@ -3849,7 +3903,7 @@ static int cgltf_parse_json_texture_view(cgltf_options* options, jsmntok_t const } else { - i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_texture_view->extensions[out_texture_view->extensions_count++])); + i = cgltf_skip_json(tokens, i + 1); } if (i < 0) @@ -3886,11 +3940,11 @@ static int cgltf_parse_json_pbr_metallic_roughness(cgltf_options* options, jsmnt if (cgltf_json_strcmp(tokens+i, json_chunk, "metallicFactor") == 0) { ++i; - out_pbr->metallic_factor = + out_pbr->metallic_factor = cgltf_json_to_float(tokens + i, json_chunk); ++i; } - else if (cgltf_json_strcmp(tokens+i, json_chunk, "roughnessFactor") == 0) + else if (cgltf_json_strcmp(tokens+i, json_chunk, "roughnessFactor") == 0) { ++i; out_pbr->roughness_factor = @@ -3903,13 +3957,11 @@ static int cgltf_parse_json_pbr_metallic_roughness(cgltf_options* options, jsmnt } else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorTexture") == 0) { - i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, - &out_pbr->base_color_texture); + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->base_color_texture); } else if (cgltf_json_strcmp(tokens + i, json_chunk, "metallicRoughnessTexture") == 0) { - i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, - &out_pbr->metallic_roughness_texture); + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_pbr->metallic_roughness_texture); } else { @@ -4312,6 +4364,52 @@ static int cgltf_parse_json_iridescence(cgltf_options* options, jsmntok_t const* return i; } +static int cgltf_parse_json_diffuse_transmission(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_diffuse_transmission* out_diff_transmission) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + // Defaults + cgltf_fill_float_array(out_diff_transmission->diffuse_transmission_color_factor, 3, 1.0f); + out_diff_transmission->diffuse_transmission_factor = 0.f; + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionFactor") == 0) + { + ++i; + out_diff_transmission->diffuse_transmission_factor = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_diff_transmission->diffuse_transmission_texture); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionColorFactor") == 0) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_diff_transmission->diffuse_transmission_color_factor, 3); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "diffuseTransmissionColorTexture") == 0) + { + i = cgltf_parse_json_texture_view(options, tokens, i + 1, json_chunk, &out_diff_transmission->diffuse_transmission_color_texture); + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + static int cgltf_parse_json_anisotropy(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_anisotropy* out_anisotropy) { CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); @@ -4353,6 +4451,186 @@ static int cgltf_parse_json_anisotropy(cgltf_options* options, jsmntok_t const* return i; } +static int cgltf_parse_json_dispersion(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_dispersion* out_dispersion) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i].size; + ++i; + + + for (int j = 0; j < size; ++j) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "dispersion") == 0) + { + ++i; + out_dispersion->dispersion = cgltf_json_to_float(tokens + i, json_chunk); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + + if (i < 0) + { + return i; + } + } + + return i; +} + +static int cgltf_parse_json_uniforms(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_uniform** out_uniform, cgltf_size* out_uniform_count) +{ + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY); + int size = tokens[i].size; + ++i; + + *out_uniform = (cgltf_uniform*)cgltf_calloc(options, sizeof(cgltf_uniform), size); + *out_uniform_count = size; + + for (int j = 0; j < size; ++j) + { + cgltf_uniform* uniform = &(*out_uniform)[j]; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + for (int k = 0; k < num_properties; ++k) + { + CGLTF_CHECK_KEY(tokens[i]); + if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &uniform->name); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "value") == 0) + { + uniform->type = cgltf_uniform_type_value; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "sampler") == 0) + { + uniform->type = cgltf_uniform_type_sampler; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "time") == 0) + { + uniform->type = cgltf_uniform_type_time; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "value_type") == 0) + { + ++i; + if (cgltf_json_strcmp(tokens + i, json_chunk, "int") == 0) + { + uniform->value_type = cgltf_uniform_value_type_int; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "uint") == 0) + { + uniform->value_type = cgltf_uniform_value_type_uint; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "float") == 0) + { + uniform->value_type = cgltf_uniform_value_type_float; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "vec2") == 0) + { + uniform->value_type = cgltf_uniform_value_type_vec2; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "vec3") == 0) + { + uniform->value_type = cgltf_uniform_value_type_vec3; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "vec4") == 0) + { + uniform->value_type = cgltf_uniform_value_type_vec4; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "mat3") == 0) + { + uniform->value_type = cgltf_uniform_value_type_mat3; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "mat4") == 0) + { + uniform->value_type = cgltf_uniform_value_type_mat4; + } + ++i; + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "value") == 0) + { + ++i; + if (uniform->value_type == cgltf_uniform_value_type_int) + { + uniform->value.int_value = cgltf_json_to_int(tokens + i, json_chunk); + } + else if (uniform->value_type == cgltf_uniform_value_type_uint) + { + uniform->value.uint_value = cgltf_json_to_int(tokens + i, json_chunk); + } + else if (uniform->value_type == cgltf_uniform_value_type_float) + { + uniform->value.float_value = cgltf_json_to_float(tokens + i, json_chunk); + } + else if (uniform->value_type == cgltf_uniform_value_type_vec2) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, uniform->value.vec2_value, 2); + } + else if (uniform->value_type == cgltf_uniform_value_type_vec3) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, uniform->value.vec3_value, 3); + } + else if (uniform->value_type == cgltf_uniform_value_type_vec4) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, uniform->value.vec4_value, 4); + } + else if (uniform->value_type == cgltf_uniform_value_type_mat3) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, uniform->value.mat3_value[0], 9); + } + else if (uniform->value_type == cgltf_uniform_value_type_mat4) + { + i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, uniform->value.mat4_value[0], 16); + } + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + if (i < 0) + { + return i; + } + } + } + + return i; +} + +static int cgltf_parse_json_limitless_material(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material* out_material) { + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + for (int k = 0; k < num_properties; ++k) { + CGLTF_CHECK_KEY(tokens[i]); + if (cgltf_json_strcmp(tokens + i, json_chunk, "fragment") == 0) + { + i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_material->fragment); + } else if (cgltf_json_strcmp(tokens + i, json_chunk, "uniforms") == 0) { + i = cgltf_parse_json_uniforms(options, tokens, i + 1, json_chunk, &out_material->uniforms, &out_material->uniforms_count); + } else { + i = cgltf_skip_json(tokens, i + 1); + } + if (i < 0) + { + return i; + } + } + return i; +} + static int cgltf_parse_json_image(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_image* out_image) { CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); @@ -4360,11 +4638,11 @@ static int cgltf_parse_json_image(cgltf_options* options, jsmntok_t const* token int size = tokens[i].size; ++i; - for (int j = 0; j < size; ++j) + for (int j = 0; j < size; ++j) { CGLTF_CHECK_KEY(tokens[i]); - if (cgltf_json_strcmp(tokens + i, json_chunk, "uri") == 0) + if (cgltf_json_strcmp(tokens + i, json_chunk, "uri") == 0) { i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->uri); } @@ -4409,8 +4687,8 @@ static int cgltf_parse_json_sampler(cgltf_options* options, jsmntok_t const* tok (void)options; CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); - out_sampler->wrap_s = 10497; - out_sampler->wrap_t = 10497; + out_sampler->wrap_s = cgltf_wrap_mode_repeat; + out_sampler->wrap_t = cgltf_wrap_mode_repeat; int size = tokens[i].size; ++i; @@ -4427,28 +4705,28 @@ static int cgltf_parse_json_sampler(cgltf_options* options, jsmntok_t const* tok { ++i; out_sampler->mag_filter - = cgltf_json_to_int(tokens + i, json_chunk); + = (cgltf_filter_type)cgltf_json_to_int(tokens + i, json_chunk); ++i; } else if (cgltf_json_strcmp(tokens + i, json_chunk, "minFilter") == 0) { ++i; out_sampler->min_filter - = cgltf_json_to_int(tokens + i, json_chunk); + = (cgltf_filter_type)cgltf_json_to_int(tokens + i, json_chunk); ++i; } else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapS") == 0) { ++i; out_sampler->wrap_s - = cgltf_json_to_int(tokens + i, json_chunk); + = (cgltf_wrap_mode)cgltf_json_to_int(tokens + i, json_chunk); ++i; } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapT") == 0) + else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapT") == 0) { ++i; out_sampler->wrap_t - = cgltf_json_to_int(tokens + i, json_chunk); + = (cgltf_wrap_mode)cgltf_json_to_int(tokens + i, json_chunk); ++i; } else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0) @@ -4494,7 +4772,7 @@ static int cgltf_parse_json_texture(cgltf_options* options, jsmntok_t const* tok out_texture->sampler = CGLTF_PTRINDEX(cgltf_sampler, cgltf_json_to_int(tokens + i, json_chunk)); ++i; } - else if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + else if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) { ++i; out_texture->image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); @@ -4556,6 +4834,34 @@ static int cgltf_parse_json_texture(cgltf_options* options, jsmntok_t const* tok } } } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "EXT_texture_webp") == 0) + { + out_texture->has_webp = 1; + ++i; + CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int num_properties = tokens[i].size; + ++i; + + for (int t = 0; t < num_properties; ++t) + { + CGLTF_CHECK_KEY(tokens[i]); + + if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) + { + ++i; + out_texture->webp_image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk)); + ++i; + } + else + { + i = cgltf_skip_json(tokens, i + 1); + } + if (i < 0) + { + return i; + } + } + } else { i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_texture->extensions[out_texture->extensions_count++])); @@ -4741,11 +5047,25 @@ static int cgltf_parse_json_material(cgltf_options* options, jsmntok_t const* to out_material->has_iridescence = 1; i = cgltf_parse_json_iridescence(options, tokens, i + 1, json_chunk, &out_material->iridescence); } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_diffuse_transmission") == 0) + { + out_material->has_diffuse_transmission = 1; + i = cgltf_parse_json_diffuse_transmission(options, tokens, i + 1, json_chunk, &out_material->diffuse_transmission); + } else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_anisotropy") == 0) { out_material->has_anisotropy = 1; i = cgltf_parse_json_anisotropy(options, tokens, i + 1, json_chunk, &out_material->anisotropy); } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "KHR_materials_dispersion") == 0) + { + out_material->has_dispersion = 1; + i = cgltf_parse_json_dispersion(tokens, i + 1, json_chunk, &out_material->dispersion); + } + else if (cgltf_json_strcmp(tokens + i, json_chunk, "limitless_material") == 0) + { + i = cgltf_parse_json_limitless_material(options, tokens, i + 1, json_chunk, out_material); + } else { i = cgltf_parse_json_unprocessed_extension(options, tokens, i, json_chunk, &(out_material->extensions[out_material->extensions_count++])); @@ -4905,7 +5225,7 @@ static int cgltf_parse_json_meshopt_compression(cgltf_options* options, jsmntok_ else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0) { ++i; - out_meshopt_compression->count = cgltf_json_to_int(tokens+i, json_chunk); + out_meshopt_compression->count = cgltf_json_to_size(tokens+i, json_chunk); ++i; } else if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0) @@ -6561,6 +6881,7 @@ static int cgltf_fixup_pointers(cgltf_data* data) { CGLTF_PTRFIXUP(data->textures[i].image, data->images, data->images_count); CGLTF_PTRFIXUP(data->textures[i].basisu_image, data->images, data->images_count); + CGLTF_PTRFIXUP(data->textures[i].webp_image, data->images, data->images_count); CGLTF_PTRFIXUP(data->textures[i].sampler, data->samplers, data->samplers_count); } @@ -6598,6 +6919,9 @@ static int cgltf_fixup_pointers(cgltf_data* data) CGLTF_PTRFIXUP(data->materials[i].iridescence.iridescence_texture.texture, data->textures, data->textures_count); CGLTF_PTRFIXUP(data->materials[i].iridescence.iridescence_thickness_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].diffuse_transmission.diffuse_transmission_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].diffuse_transmission.diffuse_transmission_color_texture.texture, data->textures, data->textures_count); + CGLTF_PTRFIXUP(data->materials[i].anisotropy.anisotropy_texture.texture, data->textures, data->textures_count); } diff --git a/src/limitless/loaders/cgltf_write.c b/src/limitless/loaders/cgltf_write.c new file mode 100644 index 00000000..fa0de2ef --- /dev/null +++ b/src/limitless/loaders/cgltf_write.c @@ -0,0 +1,2 @@ +#define CGLTF_WRITE_IMPLEMENTATION +#include "cgltf_write.h" \ No newline at end of file diff --git a/src/limitless/loaders/cgltf_write.h b/src/limitless/loaders/cgltf_write.h new file mode 100644 index 00000000..64c0d9bb --- /dev/null +++ b/src/limitless/loaders/cgltf_write.h @@ -0,0 +1,1651 @@ +/** + * cgltf_write - a single-file glTF 2.0 writer written in C99. + * + * Version: 1.15 + * + * Website: https://github.com/jkuhlmann/cgltf + * + * Distributed under the MIT License, see notice at the end of this file. + * + * Building: + * Include this file where you need the struct and function + * declarations. Have exactly one source file where you define + * `CGLTF_WRITE_IMPLEMENTATION` before including this file to get the + * function definitions. + * + * Reference: + * `cgltf_result cgltf_write_file(const cgltf_options* options, const char* + * path, const cgltf_data* data)` writes a glTF data to the given file path. + * If `options->type` is `cgltf_file_type_glb`, both JSON content and binary + * buffer of the given glTF data will be written in a GLB format. + * Otherwise, only the JSON part will be written. + * External buffers and images are not written out. `data` is not deallocated. + * + * `cgltf_size cgltf_write(const cgltf_options* options, char* buffer, + * cgltf_size size, const cgltf_data* data)` writes JSON into the given memory + * buffer. Returns the number of bytes written to `buffer`, including a null + * terminator. If buffer is null, returns the number of bytes that would have + * been written. `data` is not deallocated. + */ +#ifndef CGLTF_WRITE_H_INCLUDED__ +#define CGLTF_WRITE_H_INCLUDED__ + +#include "cgltf.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +cgltf_result cgltf_write_file(const cgltf_options* options, const char* path, const cgltf_data* data); +cgltf_size cgltf_write(const cgltf_options* options, char* buffer, cgltf_size size, const cgltf_data* data); + +#ifdef __cplusplus +} +#endif + +#endif /* #ifndef CGLTF_WRITE_H_INCLUDED__ */ + +/* + * + * Stop now, if you are only interested in the API. + * Below, you find the implementation. + * + */ + +#if defined(__INTELLISENSE__) || defined(__JETBRAINS_IDE__) +/* This makes MSVC/CLion intellisense work. */ +#define CGLTF_WRITE_IMPLEMENTATION +#endif + +#ifdef CGLTF_WRITE_IMPLEMENTATION + +#include +#include +#include +#include +#include +#include + +#define CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM (1 << 0) +#define CGLTF_EXTENSION_FLAG_MATERIALS_UNLIT (1 << 1) +#define CGLTF_EXTENSION_FLAG_SPECULAR_GLOSSINESS (1 << 2) +#define CGLTF_EXTENSION_FLAG_LIGHTS_PUNCTUAL (1 << 3) +#define CGLTF_EXTENSION_FLAG_DRACO_MESH_COMPRESSION (1 << 4) +#define CGLTF_EXTENSION_FLAG_MATERIALS_CLEARCOAT (1 << 5) +#define CGLTF_EXTENSION_FLAG_MATERIALS_IOR (1 << 6) +#define CGLTF_EXTENSION_FLAG_MATERIALS_SPECULAR (1 << 7) +#define CGLTF_EXTENSION_FLAG_MATERIALS_TRANSMISSION (1 << 8) +#define CGLTF_EXTENSION_FLAG_MATERIALS_SHEEN (1 << 9) +#define CGLTF_EXTENSION_FLAG_MATERIALS_VARIANTS (1 << 10) +#define CGLTF_EXTENSION_FLAG_MATERIALS_VOLUME (1 << 11) +#define CGLTF_EXTENSION_FLAG_TEXTURE_BASISU (1 << 12) +#define CGLTF_EXTENSION_FLAG_MATERIALS_EMISSIVE_STRENGTH (1 << 13) +#define CGLTF_EXTENSION_FLAG_MESH_GPU_INSTANCING (1 << 14) +#define CGLTF_EXTENSION_FLAG_MATERIALS_IRIDESCENCE (1 << 15) +#define CGLTF_EXTENSION_FLAG_MATERIALS_ANISOTROPY (1 << 16) +#define CGLTF_EXTENSION_FLAG_MATERIALS_DISPERSION (1 << 17) +#define CGLTF_EXTENSION_FLAG_TEXTURE_WEBP (1 << 18) +#define CGLTF_EXTENSION_FLAG_MATERIALS_DIFFUSE_TRANSMISSION (1 << 19) +#define CGLTF_EXTENSION_FLAG_LIMITLESS_MATERIAL (1 << 20) + +typedef struct { + char* buffer; + cgltf_size buffer_size; + cgltf_size remaining; + char* cursor; + cgltf_size tmp; + cgltf_size chars_written; + const cgltf_data* data; + int depth; + const char* indent; + int needs_comma; + uint32_t extension_flags; + uint32_t required_extension_flags; +} cgltf_write_context; + +#define CGLTF_MIN(a, b) (a < b ? a : b) + +#ifdef FLT_DECIMAL_DIG + // FLT_DECIMAL_DIG is C11 + #define CGLTF_DECIMAL_DIG (FLT_DECIMAL_DIG) +#else + #define CGLTF_DECIMAL_DIG 9 +#endif + +#define CGLTF_SPRINTF(...) { \ + assert(context->cursor || (!context->cursor && context->remaining == 0)); \ + context->tmp = snprintf ( context->cursor, context->remaining, __VA_ARGS__ ); \ + context->chars_written += context->tmp; \ + if (context->cursor) { \ + context->cursor += context->tmp; \ + context->remaining -= context->tmp; \ + } } + +#define CGLTF_SNPRINTF(length, ...) { \ + assert(context->cursor || (!context->cursor && context->remaining == 0)); \ + context->tmp = snprintf ( context->cursor, CGLTF_MIN(length + 1, context->remaining), __VA_ARGS__ ); \ + context->chars_written += length; \ + if (context->cursor) { \ + context->cursor += length; \ + context->remaining -= length; \ + } } + +#define CGLTF_WRITE_IDXPROP(label, val, start) if (val) { \ + cgltf_write_indent(context); \ + CGLTF_SPRINTF("\"%s\": %d", label, (int) (val - start)); \ + context->needs_comma = 1; } + +#define CGLTF_WRITE_IDXARRPROP(label, dim, vals, start) if (vals) { \ + cgltf_write_indent(context); \ + CGLTF_SPRINTF("\"%s\": [", label); \ + for (int i = 0; i < (int)(dim); ++i) { \ + int idx = (int) (vals[i] - start); \ + if (i != 0) CGLTF_SPRINTF(","); \ + CGLTF_SPRINTF(" %d", idx); \ + } \ + CGLTF_SPRINTF(" ]"); \ + context->needs_comma = 1; } + +#define CGLTF_WRITE_TEXTURE_INFO(label, info) if (info.texture) { \ + cgltf_write_line(context, "\"" label "\": {"); \ + CGLTF_WRITE_IDXPROP("index", info.texture, context->data->textures); \ + cgltf_write_intprop(context, "texCoord", info.texcoord, 0); \ + if (info.has_transform) { \ + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM; \ + cgltf_write_texture_transform(context, &info.transform); \ + } \ + cgltf_write_line(context, "}"); } + +#define CGLTF_WRITE_NORMAL_TEXTURE_INFO(label, info) if (info.texture) { \ + cgltf_write_line(context, "\"" label "\": {"); \ + CGLTF_WRITE_IDXPROP("index", info.texture, context->data->textures); \ + cgltf_write_intprop(context, "texCoord", info.texcoord, 0); \ + cgltf_write_floatprop(context, "scale", info.scale, 1.0f); \ + if (info.has_transform) { \ + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM; \ + cgltf_write_texture_transform(context, &info.transform); \ + } \ + cgltf_write_line(context, "}"); } + +#define CGLTF_WRITE_OCCLUSION_TEXTURE_INFO(label, info) if (info.texture) { \ + cgltf_write_line(context, "\"" label "\": {"); \ + CGLTF_WRITE_IDXPROP("index", info.texture, context->data->textures); \ + cgltf_write_intprop(context, "texCoord", info.texcoord, 0); \ + cgltf_write_floatprop(context, "strength", info.scale, 1.0f); \ + if (info.has_transform) { \ + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM; \ + cgltf_write_texture_transform(context, &info.transform); \ + } \ + cgltf_write_line(context, "}"); } + +#ifndef CGLTF_CONSTS +#define GlbHeaderSize 12 +#define GlbChunkHeaderSize 8 +static const uint32_t GlbVersion = 2; +static const uint32_t GlbMagic = 0x46546C67; +static const uint32_t GlbMagicJsonChunk = 0x4E4F534A; +static const uint32_t GlbMagicBinChunk = 0x004E4942; +#define CGLTF_CONSTS +#endif + +static void cgltf_write_indent(cgltf_write_context* context) +{ + if (context->needs_comma) + { + CGLTF_SPRINTF(",\n"); + context->needs_comma = 0; + } + else + { + CGLTF_SPRINTF("\n"); + } + for (int i = 0; i < context->depth; ++i) + { + CGLTF_SPRINTF("%s", context->indent); + } +} + +static void cgltf_write_line(cgltf_write_context* context, const char* line) +{ + if (line[0] == ']' || line[0] == '}') + { + --context->depth; + context->needs_comma = 0; + } + cgltf_write_indent(context); + CGLTF_SPRINTF("%s", line); + cgltf_size last = (cgltf_size)(strlen(line) - 1); + if (line[0] == ']' || line[0] == '}') + { + context->needs_comma = 1; + } + if (line[last] == '[' || line[last] == '{') + { + ++context->depth; + context->needs_comma = 0; + } +} + +static void cgltf_write_strprop(cgltf_write_context* context, const char* label, const char* val) +{ + if (val) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": \"%s\"", label, val); + context->needs_comma = 1; + } +} + +static void cgltf_write_extras(cgltf_write_context* context, const cgltf_extras* extras) +{ + if (extras->data) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"extras\": %s", extras->data); + context->needs_comma = 1; + } + else + { + cgltf_size length = extras->end_offset - extras->start_offset; + if (length > 0 && context->data->json) + { + char* json_string = ((char*) context->data->json) + extras->start_offset; + cgltf_write_indent(context); + CGLTF_SPRINTF("%s", "\"extras\": "); + CGLTF_SNPRINTF(length, "%.*s", (int)(extras->end_offset - extras->start_offset), json_string); + context->needs_comma = 1; + } + } +} + +static void cgltf_write_stritem(cgltf_write_context* context, const char* item) +{ + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\"", item); + context->needs_comma = 1; +} + +static void cgltf_write_intprop(cgltf_write_context* context, const char* label, int val, int def) +{ + if (val != def) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": %d", label, val); + context->needs_comma = 1; + } +} + +static void cgltf_write_sizeprop(cgltf_write_context* context, const char* label, cgltf_size val, cgltf_size def) +{ + if (val != def) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": %zu", label, val); + context->needs_comma = 1; + } +} + +static void cgltf_write_floatprop(cgltf_write_context* context, const char* label, float val, float def) +{ + if (val != def) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": ", label); + CGLTF_SPRINTF("%.*g", CGLTF_DECIMAL_DIG, val); + context->needs_comma = 1; + + if (context->cursor) + { + char *decimal_comma = strchr(context->cursor - context->tmp, ','); + if (decimal_comma) + { + *decimal_comma = '.'; + } + } + } +} + +static void cgltf_write_boolprop_optional(cgltf_write_context* context, const char* label, bool val, bool def) +{ + if (val != def) + { + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": %s", label, val ? "true" : "false"); + context->needs_comma = 1; + } +} + +static void cgltf_write_floatarrayprop(cgltf_write_context* context, const char* label, const cgltf_float* vals, cgltf_size dim) +{ + cgltf_write_indent(context); + CGLTF_SPRINTF("\"%s\": [", label); + for (cgltf_size i = 0; i < dim; ++i) + { + if (i != 0) + { + CGLTF_SPRINTF(", %.*g", CGLTF_DECIMAL_DIG, vals[i]); + } + else + { + CGLTF_SPRINTF("%.*g", CGLTF_DECIMAL_DIG, vals[i]); + } + } + CGLTF_SPRINTF("]"); + context->needs_comma = 1; +} + +static bool cgltf_check_floatarray(const float* vals, int dim, float val) { + while (dim--) + { + if (vals[dim] != val) + { + return true; + } + } + return false; +} + +static int cgltf_int_from_component_type(cgltf_component_type ctype) +{ + switch (ctype) + { + case cgltf_component_type_r_8: return 5120; + case cgltf_component_type_r_8u: return 5121; + case cgltf_component_type_r_16: return 5122; + case cgltf_component_type_r_16u: return 5123; + case cgltf_component_type_r_32u: return 5125; + case cgltf_component_type_r_32f: return 5126; + default: return 0; + } +} + +static int cgltf_int_from_primitive_type(cgltf_primitive_type ctype) +{ + switch (ctype) + { + case cgltf_primitive_type_points: return 0; + case cgltf_primitive_type_lines: return 1; + case cgltf_primitive_type_line_loop: return 2; + case cgltf_primitive_type_line_strip: return 3; + case cgltf_primitive_type_triangles: return 4; + case cgltf_primitive_type_triangle_strip: return 5; + case cgltf_primitive_type_triangle_fan: return 6; + default: return -1; + } +} + +static const char* cgltf_str_from_alpha_mode(cgltf_alpha_mode alpha_mode) +{ + switch (alpha_mode) + { + case cgltf_alpha_mode_mask: return "MASK"; + case cgltf_alpha_mode_blend: return "BLEND"; + default: return NULL; + } +} + +static const char* cgltf_str_from_type(cgltf_type type) +{ + switch (type) + { + case cgltf_type_scalar: return "SCALAR"; + case cgltf_type_vec2: return "VEC2"; + case cgltf_type_vec3: return "VEC3"; + case cgltf_type_vec4: return "VEC4"; + case cgltf_type_mat2: return "MAT2"; + case cgltf_type_mat3: return "MAT3"; + case cgltf_type_mat4: return "MAT4"; + default: return NULL; + } +} + +static cgltf_size cgltf_dim_from_type(cgltf_type type) +{ + switch (type) + { + case cgltf_type_scalar: return 1; + case cgltf_type_vec2: return 2; + case cgltf_type_vec3: return 3; + case cgltf_type_vec4: return 4; + case cgltf_type_mat2: return 4; + case cgltf_type_mat3: return 9; + case cgltf_type_mat4: return 16; + default: return 0; + } +} + +static const char* cgltf_str_from_camera_type(cgltf_camera_type camera_type) +{ + switch (camera_type) + { + case cgltf_camera_type_perspective: return "perspective"; + case cgltf_camera_type_orthographic: return "orthographic"; + default: return NULL; + } +} + +static const char* cgltf_str_from_light_type(cgltf_light_type light_type) +{ + switch (light_type) + { + case cgltf_light_type_directional: return "directional"; + case cgltf_light_type_point: return "point"; + case cgltf_light_type_spot: return "spot"; + default: return NULL; + } +} + +static const char* cgltf_str_from_uniform_type(cgltf_uniform_type type) +{ + switch (type) + { + case cgltf_uniform_type_value: return "value"; + case cgltf_uniform_type_sampler: return "sampler"; + case cgltf_uniform_type_time: return "time"; + default: return NULL; + } +} + +static const char* cgltf_str_from_uniform_value_type(cgltf_uniform_value_type value_type) +{ + switch (value_type) + { + case cgltf_uniform_value_type_int: return "int"; + case cgltf_uniform_value_type_uint: return "uint"; + case cgltf_uniform_value_type_float: return "float"; + case cgltf_uniform_value_type_vec2: return "vec2"; + case cgltf_uniform_value_type_vec3: return "vec3"; + case cgltf_uniform_value_type_vec4: return "vec4"; + case cgltf_uniform_value_type_mat3: return "mat3"; + case cgltf_uniform_value_type_mat4: return "mat4"; + default: return NULL; + } +} + +static void cgltf_write_texture_transform(cgltf_write_context* context, const cgltf_texture_transform* transform) +{ + cgltf_write_line(context, "\"extensions\": {"); + cgltf_write_line(context, "\"KHR_texture_transform\": {"); + if (cgltf_check_floatarray(transform->offset, 2, 0.0f)) + { + cgltf_write_floatarrayprop(context, "offset", transform->offset, 2); + } + cgltf_write_floatprop(context, "rotation", transform->rotation, 0.0f); + if (cgltf_check_floatarray(transform->scale, 2, 1.0f)) + { + cgltf_write_floatarrayprop(context, "scale", transform->scale, 2); + } + if (transform->has_texcoord) + { + cgltf_write_intprop(context, "texCoord", transform->texcoord, -1); + } + cgltf_write_line(context, "}"); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_asset(cgltf_write_context* context, const cgltf_asset* asset) +{ + cgltf_write_line(context, "\"asset\": {"); + cgltf_write_strprop(context, "copyright", asset->copyright); + cgltf_write_strprop(context, "generator", asset->generator); + cgltf_write_strprop(context, "version", asset->version); + cgltf_write_strprop(context, "min_version", asset->min_version); + cgltf_write_extras(context, &asset->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_primitive(cgltf_write_context* context, const cgltf_primitive* prim) +{ + cgltf_write_intprop(context, "mode", cgltf_int_from_primitive_type(prim->type), 4); + CGLTF_WRITE_IDXPROP("indices", prim->indices, context->data->accessors); + CGLTF_WRITE_IDXPROP("material", prim->material, context->data->materials); + cgltf_write_line(context, "\"attributes\": {"); + for (cgltf_size i = 0; i < prim->attributes_count; ++i) + { + const cgltf_attribute* attr = prim->attributes + i; + CGLTF_WRITE_IDXPROP(attr->name, attr->data, context->data->accessors); + } + cgltf_write_line(context, "}"); + + if (prim->targets_count) + { + cgltf_write_line(context, "\"targets\": ["); + for (cgltf_size i = 0; i < prim->targets_count; ++i) + { + cgltf_write_line(context, "{"); + for (cgltf_size j = 0; j < prim->targets[i].attributes_count; ++j) + { + const cgltf_attribute* attr = prim->targets[i].attributes + j; + CGLTF_WRITE_IDXPROP(attr->name, attr->data, context->data->accessors); + } + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "]"); + } + cgltf_write_extras(context, &prim->extras); + + if (prim->has_draco_mesh_compression || prim->mappings_count > 0) + { + cgltf_write_line(context, "\"extensions\": {"); + + if (prim->has_draco_mesh_compression) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_DRACO_MESH_COMPRESSION; + if (prim->attributes_count == 0 || prim->indices == 0) + { + context->required_extension_flags |= CGLTF_EXTENSION_FLAG_DRACO_MESH_COMPRESSION; + } + + cgltf_write_line(context, "\"KHR_draco_mesh_compression\": {"); + CGLTF_WRITE_IDXPROP("bufferView", prim->draco_mesh_compression.buffer_view, context->data->buffer_views); + cgltf_write_line(context, "\"attributes\": {"); + for (cgltf_size i = 0; i < prim->draco_mesh_compression.attributes_count; ++i) + { + const cgltf_attribute* attr = prim->draco_mesh_compression.attributes + i; + CGLTF_WRITE_IDXPROP(attr->name, attr->data, context->data->accessors); + } + cgltf_write_line(context, "}"); + cgltf_write_line(context, "}"); + } + + if (prim->mappings_count > 0) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_VARIANTS; + cgltf_write_line(context, "\"KHR_materials_variants\": {"); + cgltf_write_line(context, "\"mappings\": ["); + for (cgltf_size i = 0; i < prim->mappings_count; ++i) + { + const cgltf_material_mapping* map = prim->mappings + i; + cgltf_write_line(context, "{"); + CGLTF_WRITE_IDXPROP("material", map->material, context->data->materials); + + cgltf_write_indent(context); + CGLTF_SPRINTF("\"variants\": [%d]", (int)map->variant); + context->needs_comma = 1; + + cgltf_write_extras(context, &map->extras); + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "]"); + cgltf_write_line(context, "}"); + } + + cgltf_write_line(context, "}"); + } +} + +static void cgltf_write_mesh(cgltf_write_context* context, const cgltf_mesh* mesh) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", mesh->name); + + cgltf_write_line(context, "\"primitives\": ["); + for (cgltf_size i = 0; i < mesh->primitives_count; ++i) + { + cgltf_write_line(context, "{"); + cgltf_write_primitive(context, mesh->primitives + i); + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "]"); + + if (mesh->weights_count > 0) + { + cgltf_write_floatarrayprop(context, "weights", mesh->weights, mesh->weights_count); + } + + cgltf_write_extras(context, &mesh->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_buffer_view(cgltf_write_context* context, const cgltf_buffer_view* view) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", view->name); + CGLTF_WRITE_IDXPROP("buffer", view->buffer, context->data->buffers); + cgltf_write_sizeprop(context, "byteLength", view->size, (cgltf_size)-1); + cgltf_write_sizeprop(context, "byteOffset", view->offset, 0); + cgltf_write_sizeprop(context, "byteStride", view->stride, 0); + // NOTE: We skip writing "target" because the spec says its usage can be inferred. + cgltf_write_extras(context, &view->extras); + cgltf_write_line(context, "}"); +} + + +static void cgltf_write_buffer(cgltf_write_context* context, const cgltf_buffer* buffer) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", buffer->name); + cgltf_write_strprop(context, "uri", buffer->uri); + cgltf_write_sizeprop(context, "byteLength", buffer->size, (cgltf_size)-1); + cgltf_write_extras(context, &buffer->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_material(cgltf_write_context* context, const cgltf_material* material) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", material->name); + if (material->alpha_mode == cgltf_alpha_mode_mask) + { + cgltf_write_floatprop(context, "alphaCutoff", material->alpha_cutoff, 0.5f); + } + cgltf_write_boolprop_optional(context, "doubleSided", (bool)material->double_sided, false); + // cgltf_write_boolprop_optional(context, "unlit", material->unlit, false); + + if (material->unlit) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_UNLIT; + } + + if (material->has_pbr_specular_glossiness) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_SPECULAR_GLOSSINESS; + } + + if (material->has_clearcoat) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_CLEARCOAT; + } + + if (material->has_transmission) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_TRANSMISSION; + } + + if (material->has_volume) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_VOLUME; + } + + if (material->has_ior) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_IOR; + } + + if (material->has_specular) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_SPECULAR; + } + + if (material->has_sheen) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_SHEEN; + } + + if (material->has_emissive_strength) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_EMISSIVE_STRENGTH; + } + + if (material->has_iridescence) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_IRIDESCENCE; + } + + if (material->has_diffuse_transmission) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_DIFFUSE_TRANSMISSION; + } + + if (material->has_anisotropy) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_ANISOTROPY; + } + + if (material->has_dispersion) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_DISPERSION; + } + + if (material->uniforms_count > 0 || material->fragment) { + context->extension_flags |= CGLTF_EXTENSION_FLAG_LIMITLESS_MATERIAL; + } + + if (material->has_pbr_metallic_roughness) + { + const cgltf_pbr_metallic_roughness* params = &material->pbr_metallic_roughness; + cgltf_write_line(context, "\"pbrMetallicRoughness\": {"); + CGLTF_WRITE_TEXTURE_INFO("baseColorTexture", params->base_color_texture); + CGLTF_WRITE_TEXTURE_INFO("metallicRoughnessTexture", params->metallic_roughness_texture); + cgltf_write_floatprop(context, "metallicFactor", params->metallic_factor, 1.0f); + cgltf_write_floatprop(context, "roughnessFactor", params->roughness_factor, 1.0f); + if (cgltf_check_floatarray(params->base_color_factor, 4, 1.0f)) + { + cgltf_write_floatarrayprop(context, "baseColorFactor", params->base_color_factor, 4); + } + cgltf_write_line(context, "}"); + } + + if (material->unlit || material->has_pbr_specular_glossiness || material->has_clearcoat || material->has_ior || material->has_specular || material->has_transmission || material->has_sheen || material->has_volume || material->has_emissive_strength || material->has_iridescence || material->has_anisotropy || material->has_dispersion || material->has_diffuse_transmission || material->uniforms_count > 0 || material->fragment) + { + cgltf_write_line(context, "\"extensions\": {"); + if (material->has_clearcoat) + { + const cgltf_clearcoat* params = &material->clearcoat; + cgltf_write_line(context, "\"KHR_materials_clearcoat\": {"); + CGLTF_WRITE_TEXTURE_INFO("clearcoatTexture", params->clearcoat_texture); + CGLTF_WRITE_TEXTURE_INFO("clearcoatRoughnessTexture", params->clearcoat_roughness_texture); + CGLTF_WRITE_NORMAL_TEXTURE_INFO("clearcoatNormalTexture", params->clearcoat_normal_texture); + cgltf_write_floatprop(context, "clearcoatFactor", params->clearcoat_factor, 0.0f); + cgltf_write_floatprop(context, "clearcoatRoughnessFactor", params->clearcoat_roughness_factor, 0.0f); + cgltf_write_line(context, "}"); + } + if (material->has_ior) + { + const cgltf_ior* params = &material->ior; + cgltf_write_line(context, "\"KHR_materials_ior\": {"); + cgltf_write_floatprop(context, "ior", params->ior, 1.5f); + cgltf_write_line(context, "}"); + } + if (material->has_specular) + { + const cgltf_specular* params = &material->specular; + cgltf_write_line(context, "\"KHR_materials_specular\": {"); + CGLTF_WRITE_TEXTURE_INFO("specularTexture", params->specular_texture); + CGLTF_WRITE_TEXTURE_INFO("specularColorTexture", params->specular_color_texture); + cgltf_write_floatprop(context, "specularFactor", params->specular_factor, 1.0f); + if (cgltf_check_floatarray(params->specular_color_factor, 3, 1.0f)) + { + cgltf_write_floatarrayprop(context, "specularColorFactor", params->specular_color_factor, 3); + } + cgltf_write_line(context, "}"); + } + if (material->has_transmission) + { + const cgltf_transmission* params = &material->transmission; + cgltf_write_line(context, "\"KHR_materials_transmission\": {"); + CGLTF_WRITE_TEXTURE_INFO("transmissionTexture", params->transmission_texture); + cgltf_write_floatprop(context, "transmissionFactor", params->transmission_factor, 0.0f); + cgltf_write_line(context, "}"); + } + if (material->has_volume) + { + const cgltf_volume* params = &material->volume; + cgltf_write_line(context, "\"KHR_materials_volume\": {"); + CGLTF_WRITE_TEXTURE_INFO("thicknessTexture", params->thickness_texture); + cgltf_write_floatprop(context, "thicknessFactor", params->thickness_factor, 0.0f); + if (cgltf_check_floatarray(params->attenuation_color, 3, 1.0f)) + { + cgltf_write_floatarrayprop(context, "attenuationColor", params->attenuation_color, 3); + } + if (params->attenuation_distance < FLT_MAX) + { + cgltf_write_floatprop(context, "attenuationDistance", params->attenuation_distance, FLT_MAX); + } + cgltf_write_line(context, "}"); + } + if (material->has_sheen) + { + const cgltf_sheen* params = &material->sheen; + cgltf_write_line(context, "\"KHR_materials_sheen\": {"); + CGLTF_WRITE_TEXTURE_INFO("sheenColorTexture", params->sheen_color_texture); + CGLTF_WRITE_TEXTURE_INFO("sheenRoughnessTexture", params->sheen_roughness_texture); + if (cgltf_check_floatarray(params->sheen_color_factor, 3, 0.0f)) + { + cgltf_write_floatarrayprop(context, "sheenColorFactor", params->sheen_color_factor, 3); + } + cgltf_write_floatprop(context, "sheenRoughnessFactor", params->sheen_roughness_factor, 0.0f); + cgltf_write_line(context, "}"); + } + if (material->has_pbr_specular_glossiness) + { + const cgltf_pbr_specular_glossiness* params = &material->pbr_specular_glossiness; + cgltf_write_line(context, "\"KHR_materials_pbrSpecularGlossiness\": {"); + CGLTF_WRITE_TEXTURE_INFO("diffuseTexture", params->diffuse_texture); + CGLTF_WRITE_TEXTURE_INFO("specularGlossinessTexture", params->specular_glossiness_texture); + if (cgltf_check_floatarray(params->diffuse_factor, 4, 1.0f)) + { + cgltf_write_floatarrayprop(context, "diffuseFactor", params->diffuse_factor, 4); + } + if (cgltf_check_floatarray(params->specular_factor, 3, 1.0f)) + { + cgltf_write_floatarrayprop(context, "specularFactor", params->specular_factor, 3); + } + cgltf_write_floatprop(context, "glossinessFactor", params->glossiness_factor, 1.0f); + cgltf_write_line(context, "}"); + } + if (material->unlit) + { + cgltf_write_line(context, "\"KHR_materials_unlit\": {}"); + } + if (material->has_emissive_strength) + { + cgltf_write_line(context, "\"KHR_materials_emissive_strength\": {"); + const cgltf_emissive_strength* params = &material->emissive_strength; + cgltf_write_floatprop(context, "emissiveStrength", params->emissive_strength, 1.f); + cgltf_write_line(context, "}"); + } + if (material->has_iridescence) + { + cgltf_write_line(context, "\"KHR_materials_iridescence\": {"); + const cgltf_iridescence* params = &material->iridescence; + cgltf_write_floatprop(context, "iridescenceFactor", params->iridescence_factor, 0.f); + CGLTF_WRITE_TEXTURE_INFO("iridescenceTexture", params->iridescence_texture); + cgltf_write_floatprop(context, "iridescenceIor", params->iridescence_ior, 1.3f); + cgltf_write_floatprop(context, "iridescenceThicknessMinimum", params->iridescence_thickness_min, 100.f); + cgltf_write_floatprop(context, "iridescenceThicknessMaximum", params->iridescence_thickness_max, 400.f); + CGLTF_WRITE_TEXTURE_INFO("iridescenceThicknessTexture", params->iridescence_thickness_texture); + cgltf_write_line(context, "}"); + } + if (material->has_diffuse_transmission) + { + const cgltf_diffuse_transmission* params = &material->diffuse_transmission; + cgltf_write_line(context, "\"KHR_materials_diffuse_transmission\": {"); + CGLTF_WRITE_TEXTURE_INFO("diffuseTransmissionTexture", params->diffuse_transmission_texture); + cgltf_write_floatprop(context, "diffuseTransmissionFactor", params->diffuse_transmission_factor, 0.f); + if (cgltf_check_floatarray(params->diffuse_transmission_color_factor, 3, 1.f)) + { + cgltf_write_floatarrayprop(context, "diffuseTransmissionColorFactor", params->diffuse_transmission_color_factor, 3); + } + CGLTF_WRITE_TEXTURE_INFO("diffuseTransmissionColorTexture", params->diffuse_transmission_color_texture); + cgltf_write_line(context, "}"); + } + if (material->has_anisotropy) + { + cgltf_write_line(context, "\"KHR_materials_anisotropy\": {"); + const cgltf_anisotropy* params = &material->anisotropy; + cgltf_write_floatprop(context, "anisotropyFactor", params->anisotropy_strength, 0.f); + cgltf_write_floatprop(context, "anisotropyRotation", params->anisotropy_rotation, 0.f); + CGLTF_WRITE_TEXTURE_INFO("anisotropyTexture", params->anisotropy_texture); + cgltf_write_line(context, "}"); + } + if (material->has_dispersion) + { + cgltf_write_line(context, "\"KHR_materials_dispersion\": {"); + const cgltf_dispersion* params = &material->dispersion; + cgltf_write_floatprop(context, "dispersion", params->dispersion, 0.f); + cgltf_write_line(context, "}"); + } + if (material->uniforms_count > 0 || material->fragment) + { + cgltf_write_line(context, "\"limitless_material\": {"); + if (material->fragment) { + cgltf_write_strprop(context, "fragment", material->fragment); + } + cgltf_write_line(context, "\"uniforms\": ["); + for (size_t i = 0; i < material->uniforms_count; ++i) + { + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", material->uniforms[i].name); + cgltf_write_strprop(context, "type", cgltf_str_from_uniform_type(material->uniforms[i].type)); + cgltf_write_strprop(context, "value_type", cgltf_str_from_uniform_value_type(material->uniforms[i].value_type)); + switch (material->uniforms[i].value_type) + { + case cgltf_uniform_value_type_int: + cgltf_write_intprop(context, "value", material->uniforms[i].value.int_value, 0); + break; + + case cgltf_uniform_value_type_uint: + cgltf_write_intprop(context, "value", material->uniforms[i].value.uint_value, 0); + break; + case cgltf_uniform_value_type_float: + cgltf_write_floatprop(context, "value", material->uniforms[i].value.float_value, 0.0f); + break; + case cgltf_uniform_value_type_vec2: + cgltf_write_floatarrayprop(context, "value", material->uniforms[i].value.vec2_value, 2); + break; + case cgltf_uniform_value_type_vec3: + cgltf_write_floatarrayprop(context, "value", material->uniforms[i].value.vec3_value, 3); + break; + case cgltf_uniform_value_type_vec4: + cgltf_write_floatarrayprop(context, "value", material->uniforms[i].value.vec4_value, 4); + break; + case cgltf_uniform_value_type_mat3: + cgltf_write_floatarrayprop(context, "value", &material->uniforms[i].value.mat3_value[0][0], 9); + break; + case cgltf_uniform_value_type_mat4: + cgltf_write_floatarrayprop(context, "value", &material->uniforms[i].value.mat4_value[0][0], 16); + break; + case cgltf_uniform_value_type_texture: + CGLTF_WRITE_IDXPROP("value", material->uniforms[i].value.texture_value, context->data->textures); + break; + default: + break; + } + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "]"); + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "}"); + } + + CGLTF_WRITE_NORMAL_TEXTURE_INFO("normalTexture", material->normal_texture); + CGLTF_WRITE_OCCLUSION_TEXTURE_INFO("occlusionTexture", material->occlusion_texture); + CGLTF_WRITE_TEXTURE_INFO("emissiveTexture", material->emissive_texture); + if (cgltf_check_floatarray(material->emissive_factor, 3, 0.0f)) + { + cgltf_write_floatarrayprop(context, "emissiveFactor", material->emissive_factor, 3); + } + cgltf_write_strprop(context, "alphaMode", cgltf_str_from_alpha_mode(material->alpha_mode)); + cgltf_write_extras(context, &material->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_image(cgltf_write_context* context, const cgltf_image* image) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", image->name); + cgltf_write_strprop(context, "uri", image->uri); + CGLTF_WRITE_IDXPROP("bufferView", image->buffer_view, context->data->buffer_views); + cgltf_write_strprop(context, "mimeType", image->mime_type); + cgltf_write_extras(context, &image->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_texture(cgltf_write_context* context, const cgltf_texture* texture) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", texture->name); + CGLTF_WRITE_IDXPROP("source", texture->image, context->data->images); + CGLTF_WRITE_IDXPROP("sampler", texture->sampler, context->data->samplers); + + if (texture->has_basisu || texture->has_webp) + { + cgltf_write_line(context, "\"extensions\": {"); + if (texture->has_basisu) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_BASISU; + cgltf_write_line(context, "\"KHR_texture_basisu\": {"); + CGLTF_WRITE_IDXPROP("source", texture->basisu_image, context->data->images); + cgltf_write_line(context, "}"); + } + if (texture->has_webp) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_TEXTURE_WEBP; + cgltf_write_line(context, "\"EXT_texture_webp\": {"); + CGLTF_WRITE_IDXPROP("source", texture->webp_image, context->data->images); + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "}"); + } + cgltf_write_extras(context, &texture->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_skin(cgltf_write_context* context, const cgltf_skin* skin) +{ + cgltf_write_line(context, "{"); + CGLTF_WRITE_IDXPROP("skeleton", skin->skeleton, context->data->nodes); + CGLTF_WRITE_IDXPROP("inverseBindMatrices", skin->inverse_bind_matrices, context->data->accessors); + CGLTF_WRITE_IDXARRPROP("joints", skin->joints_count, skin->joints, context->data->nodes); + cgltf_write_strprop(context, "name", skin->name); + cgltf_write_extras(context, &skin->extras); + cgltf_write_line(context, "}"); +} + +static const char* cgltf_write_str_path_type(cgltf_animation_path_type path_type) +{ + switch (path_type) + { + case cgltf_animation_path_type_translation: + return "translation"; + case cgltf_animation_path_type_rotation: + return "rotation"; + case cgltf_animation_path_type_scale: + return "scale"; + case cgltf_animation_path_type_weights: + return "weights"; + default: + break; + } + return "invalid"; +} + +static const char* cgltf_write_str_interpolation_type(cgltf_interpolation_type interpolation_type) +{ + switch (interpolation_type) + { + case cgltf_interpolation_type_linear: + return "LINEAR"; + case cgltf_interpolation_type_step: + return "STEP"; + case cgltf_interpolation_type_cubic_spline: + return "CUBICSPLINE"; + default: + break; + } + return "invalid"; +} + +static void cgltf_write_path_type(cgltf_write_context* context, const char *label, cgltf_animation_path_type path_type) +{ + cgltf_write_strprop(context, label, cgltf_write_str_path_type(path_type)); +} + +static void cgltf_write_interpolation_type(cgltf_write_context* context, const char *label, cgltf_interpolation_type interpolation_type) +{ + cgltf_write_strprop(context, label, cgltf_write_str_interpolation_type(interpolation_type)); +} + +static void cgltf_write_animation_sampler(cgltf_write_context* context, const cgltf_animation_sampler* animation_sampler) +{ + cgltf_write_line(context, "{"); + cgltf_write_interpolation_type(context, "interpolation", animation_sampler->interpolation); + CGLTF_WRITE_IDXPROP("input", animation_sampler->input, context->data->accessors); + CGLTF_WRITE_IDXPROP("output", animation_sampler->output, context->data->accessors); + cgltf_write_extras(context, &animation_sampler->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_animation_channel(cgltf_write_context* context, const cgltf_animation* animation, const cgltf_animation_channel* animation_channel) +{ + cgltf_write_line(context, "{"); + CGLTF_WRITE_IDXPROP("sampler", animation_channel->sampler, animation->samplers); + cgltf_write_line(context, "\"target\": {"); + CGLTF_WRITE_IDXPROP("node", animation_channel->target_node, context->data->nodes); + cgltf_write_path_type(context, "path", animation_channel->target_path); + cgltf_write_line(context, "}"); + cgltf_write_extras(context, &animation_channel->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_animation(cgltf_write_context* context, const cgltf_animation* animation) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", animation->name); + + if (animation->samplers_count > 0) + { + cgltf_write_line(context, "\"samplers\": ["); + for (cgltf_size i = 0; i < animation->samplers_count; ++i) + { + cgltf_write_animation_sampler(context, animation->samplers + i); + } + cgltf_write_line(context, "]"); + } + if (animation->channels_count > 0) + { + cgltf_write_line(context, "\"channels\": ["); + for (cgltf_size i = 0; i < animation->channels_count; ++i) + { + cgltf_write_animation_channel(context, animation, animation->channels + i); + } + cgltf_write_line(context, "]"); + } + cgltf_write_extras(context, &animation->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_sampler(cgltf_write_context* context, const cgltf_sampler* sampler) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", sampler->name); + cgltf_write_intprop(context, "magFilter", sampler->mag_filter, 0); + cgltf_write_intprop(context, "minFilter", sampler->min_filter, 0); + cgltf_write_intprop(context, "wrapS", sampler->wrap_s, 10497); + cgltf_write_intprop(context, "wrapT", sampler->wrap_t, 10497); + cgltf_write_extras(context, &sampler->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_node(cgltf_write_context* context, const cgltf_node* node) +{ + cgltf_write_line(context, "{"); + CGLTF_WRITE_IDXARRPROP("children", node->children_count, node->children, context->data->nodes); + CGLTF_WRITE_IDXPROP("mesh", node->mesh, context->data->meshes); + cgltf_write_strprop(context, "name", node->name); + if (node->has_matrix) + { + cgltf_write_floatarrayprop(context, "matrix", node->matrix, 16); + } + if (node->has_translation) + { + cgltf_write_floatarrayprop(context, "translation", node->translation, 3); + } + if (node->has_rotation) + { + cgltf_write_floatarrayprop(context, "rotation", node->rotation, 4); + } + if (node->has_scale) + { + cgltf_write_floatarrayprop(context, "scale", node->scale, 3); + } + if (node->skin) + { + CGLTF_WRITE_IDXPROP("skin", node->skin, context->data->skins); + } + + bool has_extension = node->light || (node->has_mesh_gpu_instancing && node->mesh_gpu_instancing.attributes_count > 0); + if(has_extension) + cgltf_write_line(context, "\"extensions\": {"); + + if (node->light) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_LIGHTS_PUNCTUAL; + cgltf_write_line(context, "\"KHR_lights_punctual\": {"); + CGLTF_WRITE_IDXPROP("light", node->light, context->data->lights); + cgltf_write_line(context, "}"); + } + + if (node->has_mesh_gpu_instancing && node->mesh_gpu_instancing.attributes_count > 0) + { + context->extension_flags |= CGLTF_EXTENSION_FLAG_MESH_GPU_INSTANCING; + context->required_extension_flags |= CGLTF_EXTENSION_FLAG_MESH_GPU_INSTANCING; + + cgltf_write_line(context, "\"EXT_mesh_gpu_instancing\": {"); + { + cgltf_write_line(context, "\"attributes\": {"); + { + for (cgltf_size i = 0; i < node->mesh_gpu_instancing.attributes_count; ++i) + { + const cgltf_attribute* attr = node->mesh_gpu_instancing.attributes + i; + CGLTF_WRITE_IDXPROP(attr->name, attr->data, context->data->accessors); + } + } + cgltf_write_line(context, "}"); + } + cgltf_write_line(context, "}"); + } + + if (has_extension) + cgltf_write_line(context, "}"); + + if (node->weights_count > 0) + { + cgltf_write_floatarrayprop(context, "weights", node->weights, node->weights_count); + } + + if (node->camera) + { + CGLTF_WRITE_IDXPROP("camera", node->camera, context->data->cameras); + } + + cgltf_write_extras(context, &node->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_scene(cgltf_write_context* context, const cgltf_scene* scene) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", scene->name); + CGLTF_WRITE_IDXARRPROP("nodes", scene->nodes_count, scene->nodes, context->data->nodes); + cgltf_write_extras(context, &scene->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_accessor(cgltf_write_context* context, const cgltf_accessor* accessor) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", accessor->name); + CGLTF_WRITE_IDXPROP("bufferView", accessor->buffer_view, context->data->buffer_views); + cgltf_write_intprop(context, "componentType", cgltf_int_from_component_type(accessor->component_type), 0); + cgltf_write_strprop(context, "type", cgltf_str_from_type(accessor->type)); + cgltf_size dim = cgltf_dim_from_type(accessor->type); + cgltf_write_boolprop_optional(context, "normalized", (bool)accessor->normalized, false); + cgltf_write_sizeprop(context, "byteOffset", (int)accessor->offset, 0); + cgltf_write_intprop(context, "count", (int)accessor->count, -1); + if (accessor->has_min) + { + cgltf_write_floatarrayprop(context, "min", accessor->min, dim); + } + if (accessor->has_max) + { + cgltf_write_floatarrayprop(context, "max", accessor->max, dim); + } + if (accessor->is_sparse) + { + cgltf_write_line(context, "\"sparse\": {"); + cgltf_write_intprop(context, "count", (int)accessor->sparse.count, 0); + cgltf_write_line(context, "\"indices\": {"); + cgltf_write_sizeprop(context, "byteOffset", (int)accessor->sparse.indices_byte_offset, 0); + CGLTF_WRITE_IDXPROP("bufferView", accessor->sparse.indices_buffer_view, context->data->buffer_views); + cgltf_write_intprop(context, "componentType", cgltf_int_from_component_type(accessor->sparse.indices_component_type), 0); + cgltf_write_line(context, "}"); + cgltf_write_line(context, "\"values\": {"); + cgltf_write_sizeprop(context, "byteOffset", (int)accessor->sparse.values_byte_offset, 0); + CGLTF_WRITE_IDXPROP("bufferView", accessor->sparse.values_buffer_view, context->data->buffer_views); + cgltf_write_line(context, "}"); + cgltf_write_line(context, "}"); + } + cgltf_write_extras(context, &accessor->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_camera(cgltf_write_context* context, const cgltf_camera* camera) +{ + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "type", cgltf_str_from_camera_type(camera->type)); + if (camera->name) + { + cgltf_write_strprop(context, "name", camera->name); + } + + if (camera->type == cgltf_camera_type_orthographic) + { + cgltf_write_line(context, "\"orthographic\": {"); + cgltf_write_floatprop(context, "xmag", camera->data.orthographic.xmag, -1.0f); + cgltf_write_floatprop(context, "ymag", camera->data.orthographic.ymag, -1.0f); + cgltf_write_floatprop(context, "zfar", camera->data.orthographic.zfar, -1.0f); + cgltf_write_floatprop(context, "znear", camera->data.orthographic.znear, -1.0f); + cgltf_write_extras(context, &camera->data.orthographic.extras); + cgltf_write_line(context, "}"); + } + else if (camera->type == cgltf_camera_type_perspective) + { + cgltf_write_line(context, "\"perspective\": {"); + + if (camera->data.perspective.has_aspect_ratio) { + cgltf_write_floatprop(context, "aspectRatio", camera->data.perspective.aspect_ratio, -1.0f); + } + + cgltf_write_floatprop(context, "yfov", camera->data.perspective.yfov, -1.0f); + + if (camera->data.perspective.has_zfar) { + cgltf_write_floatprop(context, "zfar", camera->data.perspective.zfar, -1.0f); + } + + cgltf_write_floatprop(context, "znear", camera->data.perspective.znear, -1.0f); + cgltf_write_extras(context, &camera->data.perspective.extras); + cgltf_write_line(context, "}"); + } + cgltf_write_extras(context, &camera->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_light(cgltf_write_context* context, const cgltf_light* light) +{ + context->extension_flags |= CGLTF_EXTENSION_FLAG_LIGHTS_PUNCTUAL; + + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "type", cgltf_str_from_light_type(light->type)); + if (light->name) + { + cgltf_write_strprop(context, "name", light->name); + } + if (cgltf_check_floatarray(light->color, 3, 1.0f)) + { + cgltf_write_floatarrayprop(context, "color", light->color, 3); + } + cgltf_write_floatprop(context, "intensity", light->intensity, 1.0f); + cgltf_write_floatprop(context, "range", light->range, 0.0f); + + if (light->type == cgltf_light_type_spot) + { + cgltf_write_line(context, "\"spot\": {"); + cgltf_write_floatprop(context, "innerConeAngle", light->spot_inner_cone_angle, 0.0f); + cgltf_write_floatprop(context, "outerConeAngle", light->spot_outer_cone_angle, 3.14159265358979323846f/4.0f); + cgltf_write_line(context, "}"); + } + cgltf_write_extras( context, &light->extras ); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_variant(cgltf_write_context* context, const cgltf_material_variant* variant) +{ + context->extension_flags |= CGLTF_EXTENSION_FLAG_MATERIALS_VARIANTS; + + cgltf_write_line(context, "{"); + cgltf_write_strprop(context, "name", variant->name); + cgltf_write_extras(context, &variant->extras); + cgltf_write_line(context, "}"); +} + +static void cgltf_write_glb(FILE* file, const void* json_buf, const cgltf_size json_size, const void* bin_buf, const cgltf_size bin_size) +{ + char header[GlbHeaderSize]; + char chunk_header[GlbChunkHeaderSize]; + char json_pad[3] = { 0x20, 0x20, 0x20 }; + char bin_pad[3] = { 0, 0, 0 }; + + cgltf_size json_padsize = (json_size % 4 != 0) ? 4 - json_size % 4 : 0; + cgltf_size bin_padsize = (bin_size % 4 != 0) ? 4 - bin_size % 4 : 0; + cgltf_size total_size = GlbHeaderSize + GlbChunkHeaderSize + json_size + json_padsize; + if (bin_buf != NULL && bin_size > 0) { + total_size += GlbChunkHeaderSize + bin_size + bin_padsize; + } + + // Write a GLB header + memcpy(header, &GlbMagic, 4); + memcpy(header + 4, &GlbVersion, 4); + memcpy(header + 8, &total_size, 4); + fwrite(header, 1, GlbHeaderSize, file); + + // Write a JSON chunk (header & data) + uint32_t json_chunk_size = (uint32_t)(json_size + json_padsize); + memcpy(chunk_header, &json_chunk_size, 4); + memcpy(chunk_header + 4, &GlbMagicJsonChunk, 4); + fwrite(chunk_header, 1, GlbChunkHeaderSize, file); + + fwrite(json_buf, 1, json_size, file); + fwrite(json_pad, 1, json_padsize, file); + + if (bin_buf != NULL && bin_size > 0) { + // Write a binary chunk (header & data) + uint32_t bin_chunk_size = (uint32_t)(bin_size + bin_padsize); + memcpy(chunk_header, &bin_chunk_size, 4); + memcpy(chunk_header + 4, &GlbMagicBinChunk, 4); + fwrite(chunk_header, 1, GlbChunkHeaderSize, file); + + fwrite(bin_buf, 1, bin_size, file); + fwrite(bin_pad, 1, bin_padsize, file); + } +} + +cgltf_result cgltf_write_file(const cgltf_options* options, const char* path, const cgltf_data* data) +{ + cgltf_size expected = cgltf_write(options, NULL, 0, data); + char* buffer = (char*) malloc(expected); + cgltf_size actual = cgltf_write(options, buffer, expected, data); + if (expected != actual) { + fprintf(stderr, "Error: expected %zu bytes but wrote %zu bytes.\n", expected, actual); + } + FILE* file = fopen(path, "wb"); + if (!file) + { + return cgltf_result_file_not_found; + } + // Note that cgltf_write() includes a null terminator, which we omit from the file content. + if (options->type == cgltf_file_type_glb) { + cgltf_write_glb(file, buffer, actual - 1, data->bin, data->bin_size); + } else { + // Write a plain JSON file. + fwrite(buffer, actual - 1, 1, file); + } + fclose(file); + free(buffer); + return cgltf_result_success; +} + +static void cgltf_write_extensions(cgltf_write_context* context, uint32_t extension_flags) +{ + if (extension_flags & CGLTF_EXTENSION_FLAG_TEXTURE_TRANSFORM) { + cgltf_write_stritem(context, "KHR_texture_transform"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_UNLIT) { + cgltf_write_stritem(context, "KHR_materials_unlit"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_SPECULAR_GLOSSINESS) { + cgltf_write_stritem(context, "KHR_materials_pbrSpecularGlossiness"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_LIGHTS_PUNCTUAL) { + cgltf_write_stritem(context, "KHR_lights_punctual"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_DRACO_MESH_COMPRESSION) { + cgltf_write_stritem(context, "KHR_draco_mesh_compression"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_CLEARCOAT) { + cgltf_write_stritem(context, "KHR_materials_clearcoat"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_IOR) { + cgltf_write_stritem(context, "KHR_materials_ior"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_SPECULAR) { + cgltf_write_stritem(context, "KHR_materials_specular"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_TRANSMISSION) { + cgltf_write_stritem(context, "KHR_materials_transmission"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_SHEEN) { + cgltf_write_stritem(context, "KHR_materials_sheen"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_VARIANTS) { + cgltf_write_stritem(context, "KHR_materials_variants"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_VOLUME) { + cgltf_write_stritem(context, "KHR_materials_volume"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_TEXTURE_BASISU) { + cgltf_write_stritem(context, "KHR_texture_basisu"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_TEXTURE_WEBP) { + cgltf_write_stritem(context, "EXT_texture_webp"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_EMISSIVE_STRENGTH) { + cgltf_write_stritem(context, "KHR_materials_emissive_strength"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_IRIDESCENCE) { + cgltf_write_stritem(context, "KHR_materials_iridescence"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_DIFFUSE_TRANSMISSION) { + cgltf_write_stritem(context, "KHR_materials_diffuse_transmission"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_ANISOTROPY) { + cgltf_write_stritem(context, "KHR_materials_anisotropy"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MESH_GPU_INSTANCING) { + cgltf_write_stritem(context, "EXT_mesh_gpu_instancing"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_MATERIALS_DISPERSION) { + cgltf_write_stritem(context, "KHR_materials_dispersion"); + } + if (extension_flags & CGLTF_EXTENSION_FLAG_LIMITLESS_MATERIAL) { + cgltf_write_stritem(context, "limitless_material"); + } +} + +cgltf_size cgltf_write(const cgltf_options* options, char* buffer, cgltf_size size, const cgltf_data* data) +{ + (void)options; + cgltf_write_context ctx; + ctx.buffer = buffer; + ctx.buffer_size = size; + ctx.remaining = size; + ctx.cursor = buffer; + ctx.chars_written = 0; + ctx.data = data; + ctx.depth = 1; + ctx.indent = " "; + ctx.needs_comma = 0; + ctx.extension_flags = 0; + ctx.required_extension_flags = 0; + + cgltf_write_context* context = &ctx; + + CGLTF_SPRINTF("{"); + + if (data->accessors_count > 0) + { + cgltf_write_line(context, "\"accessors\": ["); + for (cgltf_size i = 0; i < data->accessors_count; ++i) + { + cgltf_write_accessor(context, data->accessors + i); + } + cgltf_write_line(context, "]"); + } + + cgltf_write_asset(context, &data->asset); + + if (data->buffer_views_count > 0) + { + cgltf_write_line(context, "\"bufferViews\": ["); + for (cgltf_size i = 0; i < data->buffer_views_count; ++i) + { + cgltf_write_buffer_view(context, data->buffer_views + i); + } + cgltf_write_line(context, "]"); + } + + if (data->buffers_count > 0) + { + cgltf_write_line(context, "\"buffers\": ["); + for (cgltf_size i = 0; i < data->buffers_count; ++i) + { + cgltf_write_buffer(context, data->buffers + i); + } + cgltf_write_line(context, "]"); + } + + if (data->images_count > 0) + { + cgltf_write_line(context, "\"images\": ["); + for (cgltf_size i = 0; i < data->images_count; ++i) + { + cgltf_write_image(context, data->images + i); + } + cgltf_write_line(context, "]"); + } + + if (data->meshes_count > 0) + { + cgltf_write_line(context, "\"meshes\": ["); + for (cgltf_size i = 0; i < data->meshes_count; ++i) + { + cgltf_write_mesh(context, data->meshes + i); + } + cgltf_write_line(context, "]"); + } + + if (data->materials_count > 0) + { + cgltf_write_line(context, "\"materials\": ["); + for (cgltf_size i = 0; i < data->materials_count; ++i) + { + cgltf_write_material(context, data->materials + i); + } + cgltf_write_line(context, "]"); + } + + if (data->nodes_count > 0) + { + cgltf_write_line(context, "\"nodes\": ["); + for (cgltf_size i = 0; i < data->nodes_count; ++i) + { + cgltf_write_node(context, data->nodes + i); + } + cgltf_write_line(context, "]"); + } + + if (data->samplers_count > 0) + { + cgltf_write_line(context, "\"samplers\": ["); + for (cgltf_size i = 0; i < data->samplers_count; ++i) + { + cgltf_write_sampler(context, data->samplers + i); + } + cgltf_write_line(context, "]"); + } + + CGLTF_WRITE_IDXPROP("scene", data->scene, data->scenes); + + if (data->scenes_count > 0) + { + cgltf_write_line(context, "\"scenes\": ["); + for (cgltf_size i = 0; i < data->scenes_count; ++i) + { + cgltf_write_scene(context, data->scenes + i); + } + cgltf_write_line(context, "]"); + } + + if (data->textures_count > 0) + { + cgltf_write_line(context, "\"textures\": ["); + for (cgltf_size i = 0; i < data->textures_count; ++i) + { + cgltf_write_texture(context, data->textures + i); + } + cgltf_write_line(context, "]"); + } + + if (data->skins_count > 0) + { + cgltf_write_line(context, "\"skins\": ["); + for (cgltf_size i = 0; i < data->skins_count; ++i) + { + cgltf_write_skin(context, data->skins + i); + } + cgltf_write_line(context, "]"); + } + + if (data->animations_count > 0) + { + cgltf_write_line(context, "\"animations\": ["); + for (cgltf_size i = 0; i < data->animations_count; ++i) + { + cgltf_write_animation(context, data->animations + i); + } + cgltf_write_line(context, "]"); + } + + if (data->cameras_count > 0) + { + cgltf_write_line(context, "\"cameras\": ["); + for (cgltf_size i = 0; i < data->cameras_count; ++i) + { + cgltf_write_camera(context, data->cameras + i); + } + cgltf_write_line(context, "]"); + } + + if (data->lights_count > 0 || data->variants_count > 0) + { + cgltf_write_line(context, "\"extensions\": {"); + + if (data->lights_count > 0) + { + cgltf_write_line(context, "\"KHR_lights_punctual\": {"); + cgltf_write_line(context, "\"lights\": ["); + for (cgltf_size i = 0; i < data->lights_count; ++i) + { + cgltf_write_light(context, data->lights + i); + } + cgltf_write_line(context, "]"); + cgltf_write_line(context, "}"); + } + + if (data->variants_count) + { + cgltf_write_line(context, "\"KHR_materials_variants\": {"); + cgltf_write_line(context, "\"variants\": ["); + for (cgltf_size i = 0; i < data->variants_count; ++i) + { + cgltf_write_variant(context, data->variants + i); + } + cgltf_write_line(context, "]"); + cgltf_write_line(context, "}"); + } + + cgltf_write_line(context, "}"); + } + + if (context->extension_flags != 0) + { + cgltf_write_line(context, "\"extensionsUsed\": ["); + cgltf_write_extensions(context, context->extension_flags); + cgltf_write_line(context, "]"); + } + + if (context->required_extension_flags != 0) + { + cgltf_write_line(context, "\"extensionsRequired\": ["); + cgltf_write_extensions(context, context->required_extension_flags); + cgltf_write_line(context, "]"); + } + + cgltf_write_extras(context, &data->extras); + + CGLTF_SPRINTF("\n}\n"); + + // snprintf does not include the null terminator in its return value, so be sure to include it + // in the returned byte count. + return 1 + ctx.chars_written; +} + +#endif /* #ifdef CGLTF_WRITE_IMPLEMENTATION */ + +/* cgltf is distributed under MIT license: + * + * Copyright (c) 2019-2021 Philip Rideout + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ diff --git a/src/limitless/loaders/gltf_model_loader.cpp b/src/limitless/loaders/gltf_model_loader.cpp index de6c921c..2270ff58 100644 --- a/src/limitless/loaders/gltf_model_loader.cpp +++ b/src/limitless/loaders/gltf_model_loader.cpp @@ -134,14 +134,18 @@ static std::vector copyFromAccessor(const cgltf_accessor& accessor) { return result; } -static glm::vec4 toVec4(const float (&src)[4]) { - return glm::vec4 {src[0], src[1], src[2], src[3]}; +static glm::vec2 toVec2(const float (&src)[2]) { + return glm::vec2 {src[0], src[1]}; } static glm::vec3 toVec3(const float (&src)[3]) { return glm::vec3 {src[0], src[1], src[2]}; } +static glm::vec4 toVec4(const float (&src)[4]) { + return glm::vec4 {src[0], src[1], src[2], src[3]}; +} + static glm::quat toQuat(const float (&src)[4]) { glm::quat result; // GLM quaternion storage depends on GLM_FORCE_QUAT_DATA_XYZW macro @@ -168,6 +172,20 @@ static glm::quat toQuat(const std::array& src) { return result; } +static glm::mat3 toMat3(const float (&src)[3][3]) { + return glm::mat3 { + src[0][0], + src[0][1], + src[0][2], + src[1][0], + src[1][1], + src[1][2], + src[2][0], + src[2][1], + src[2][2] + }; +} + static glm::mat4 toMat4(const float (&src)[16]) { return glm::mat4 { src[0], @@ -188,6 +206,27 @@ static glm::mat4 toMat4(const float (&src)[16]) { src[15]}; } +static glm::mat3 toMat4(const float (&mat4)[4][4]) { + return glm::mat4 { + mat4[0][0], + mat4[0][1], + mat4[0][2], + mat4[0][3], + mat4[1][0], + mat4[1][1], + mat4[1][2], + mat4[1][3], + mat4[2][0], + mat4[2][1], + mat4[2][2], + mat4[2][3], + mat4[3][0], + mat4[3][1], + mat4[3][2], + mat4[3][3] + }; +} + static glm::mat4 toMat4(const std::array& src) { return glm::mat4 { src[0], @@ -664,7 +703,8 @@ static std::shared_ptr loadMaterial( const cgltf_material& material, const std::string& model_name, size_t material_index, - const ModelLoaderFlags& model_flags + const ModelLoaderFlags& model_flags, + const cgltf_texture* textures ) { ms::Material::Builder builder = ms::Material::builder(); const auto material_name = model_name + (material.name @@ -738,7 +778,7 @@ static std::shared_ptr loadMaterial( return output; }; - auto loadTextureFrom = [&](cgltf_texture& tex, std::string name, TextureLoaderFlags flags) -> std::optional> { + auto loadTextureFrom = [&](const cgltf_texture& tex, std::string name, TextureLoaderFlags flags) -> std::optional> { if (!tex.image) { return std::nullopt; } @@ -943,6 +983,53 @@ static std::shared_ptr loadMaterial( builder.emissive_color(emissive_color); } + for (size_t i = 0; i < material.uniforms_count; ++i) { + const auto& uniform = material.uniforms[i]; + switch (uniform.type) { + case cgltf_uniform_type_sampler: + builder.custom(uniform.name, *loadTextureFrom(textures[uniform.value.uint_value], textures[uniform.value.uint_value].name, model_flags.base_tex_flags)); + continue; + case cgltf_uniform_type_time: + builder.time(); + continue; + case cgltf_uniform_type_value: + switch (uniform.value_type) { + case cgltf_uniform_value_type_int: + builder.custom(uniform.name, uniform.value.int_value); + continue; + case cgltf_uniform_value_type_uint: + builder.custom(uniform.name, uniform.value.uint_value); + continue; + case cgltf_uniform_value_type_float: + builder.custom(uniform.name, uniform.value.float_value); + continue; + case cgltf_uniform_value_type_vec2: + builder.custom(uniform.name, toVec2(uniform.value.vec2_value)); + continue; + case cgltf_uniform_value_type_vec3: + builder.custom(uniform.name, toVec3(uniform.value.vec3_value)); + continue; + case cgltf_uniform_value_type_vec4: + builder.custom(uniform.name, toVec4(uniform.value.vec4_value)); + continue; + case cgltf_uniform_value_type_mat3: + builder.custom(uniform.name, toMat3(uniform.value.mat3_value)); + continue; + case cgltf_uniform_value_type_mat4: + builder.custom(uniform.name, toMat4(uniform.value.mat4_value)); + continue; + case cgltf_uniform_value_type_texture: + throw ModelLoadError("invalid uniform value type"); + } + throw ModelLoadError("unknown uniform value type"); + } + throw ModelLoadError("unknown uniform type"); + } + + if (material.fragment) { + builder.fragment(material.fragment); + } + return builder.models(instance_types).build(assets); } @@ -958,7 +1045,7 @@ static std::vector> loadMaterials( for (size_t i = 0; i < src.materials_count; ++i) { materials.emplace_back(loadMaterial( - assets, instance_types, path.parent_path(), src.materials[i], model_name, i, flags + assets, instance_types, path.parent_path(), src.materials[i], model_name, i, flags, src.textures )); } diff --git a/src/limitless/loaders/gltf_model_saver.cpp b/src/limitless/loaders/gltf_model_saver.cpp new file mode 100644 index 00000000..aa589d0d --- /dev/null +++ b/src/limitless/loaders/gltf_model_saver.cpp @@ -0,0 +1,644 @@ +#include "cgltf_write.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Limitless; + +template +T* safeMalloc(size_t count = 1) { + T* ptr = static_cast(malloc(sizeof(T) * count)); + if (ptr == nullptr) { + throw ModelSaveError("Failed to allocate memory for " + std::string(typeid(T).name())); + } + memset(ptr, 0, sizeof(T) * count); + return ptr; +} + +template +T* safeAllocOneMore(T** ptr, size_t* curr_count) { + const auto prev_count = *curr_count; + const auto new_count = prev_count + 1; + T* result = *ptr + prev_count; + *curr_count = new_count; + return result; +} + + +// static std::string base64Encode(const std::vector& data) { +// const char* base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +// if (data.empty()) { +// return ""; +// } + +// std::string result; +// result.reserve(((data.size() + 2) / 3) * 4); + +// for (size_t i = 0; i < data.size(); i += 3) { +// uint32_t value = 0; +// int padding = 0; + +// // Pack 3 bytes into 24-bit value +// value |= static_cast(data[i]) << 16; +// if (i + 1 < data.size()) { +// value |= static_cast(data[i + 1]) << 8; +// } else { +// padding = 2; +// } +// if (i + 2 < data.size()) { +// value |= static_cast(data[i + 2]); +// } else if (padding == 0) { +// padding = 1; +// } + +// // Extract 4 base64 characters +// result += base64_chars[(value >> 18) & 0x3F]; +// result += base64_chars[(value >> 12) & 0x3F]; +// result += (padding >= 2) ? '=' : base64_chars[(value >> 6) & 0x3F]; +// result += (padding >= 1) ? '=' : base64_chars[value & 0x3F]; +// } + +// return result; +// } + +static cgltf_texture* cgltfTextureFrom(const Texture& texture, const std::string& name, cgltf_data* data, std::vector* embed_buffer) { + auto* texture_data = safeAllocOneMore(&data->textures, &data->textures_count); + texture_data->name = strdup(name.c_str()); + + std::cout << "saving texture " << texture_data->name << '\n'; + + texture_data->image = safeAllocOneMore(&data->images, &data->images_count); + const auto& maybe_path = texture.getPath(); + std::string mime_type; + if (!embed_buffer) { + if (!maybe_path) { + // not supported because we need to write texture data in a format supported by stbi (png, jpg, etc.) + throw ModelSaveError("Texture " + name + " has no path"); + } + // mime_type = "image/" + maybe_path->extension().string().erase(0, 1); + texture_data->image->uri = strdup(maybe_path->string().c_str()); + std::cout << "saving image of " << texture_data->image->uri << '\n'; + + } else { + if (!maybe_path) { + throw ModelSaveError("base64 encoding not supported yet"); + } else { + auto file = std::fstream {*maybe_path, std::ios::in | std::ios::binary}; + if (!file.is_open()) { + throw ModelSaveError("failed to open " + maybe_path->string() + " for embedding"); + } + + std::cout << "embedding " + maybe_path->string() + "\n"; + + const auto buffer_start_pos = embed_buffer->size(); + + char buffer[8192]; + while (file) { + file.read(buffer, sizeof(buffer)); + const auto read = file.gcount(); + const auto prev_buffer_size = embed_buffer->size(); + embed_buffer->resize(prev_buffer_size + read); + std::memcpy(embed_buffer->data() + prev_buffer_size, buffer, read); + } + + auto* buffer_view = safeAllocOneMore(&data->buffer_views, &data->buffer_views_count); + buffer_view->offset = buffer_start_pos; + buffer_view->buffer = data->buffers; // TODO: this might not be populated yet. + buffer_view->size = embed_buffer->size() - buffer_start_pos; + texture_data->image->buffer_view = buffer_view; + texture_data->image->mime_type = strdup(("image/" + maybe_path->extension().string().erase(0, 1)).c_str()); + + std::cout << "total " << buffer_view->size << " bytes\n"; + } + } + + texture_data->image->name = strdup((name + "_image").c_str()); + // texture_data->image->mime_type = strdup("image/png"); // TODO: determine mime type from texture + + auto*& sampler = texture_data->sampler; + sampler = safeAllocOneMore(&data->samplers, &data->samplers_count); + sampler->name = strdup((name + "_sampler").c_str()); + + auto cgltfFilterTypeFrom = [](Texture::Filter filter) -> cgltf_filter_type { + switch (filter) { + case Texture::Filter::Linear: + return cgltf_filter_type_linear; + case Texture::Filter::LinearMipmapLinear: + return cgltf_filter_type_linear_mipmap_linear; + case Texture::Filter::LinearMipMapNearest: + return cgltf_filter_type_linear_mipmap_nearest; + case Texture::Filter::Nearest: + return cgltf_filter_type_nearest; + case Texture::Filter::NearestMipmapNearest: + return cgltf_filter_type_nearest_mipmap_nearest; + case Texture::Filter::NearestMipMapLinear: + return cgltf_filter_type_nearest_mipmap_linear; + } + throw ModelSaveError("Unsupported texture filter: " + std::to_string(static_cast(filter))); + }; + + auto cgltfWrapModeFrom = [](Texture::Wrap wrap) -> cgltf_wrap_mode { + switch (wrap) { + case Texture::Wrap::ClampToEdge: + return cgltf_wrap_mode_clamp_to_edge; + case Texture::Wrap::MirroredRepeat: + return cgltf_wrap_mode_mirrored_repeat; + case Texture::Wrap::Repeat: + return cgltf_wrap_mode_repeat; + case Texture::Wrap::ClampToBorder: + return cgltf_wrap_mode_clamp_to_edge; + } + throw ModelSaveError("Unsupported texture wrap mode: " + std::to_string(static_cast(wrap))); + }; + + sampler->mag_filter = cgltfFilterTypeFrom(texture.getMag()); + sampler->min_filter = cgltfFilterTypeFrom(texture.getMin()); + sampler->wrap_s = cgltfWrapModeFrom(texture.getWrapS()); + sampler->wrap_t = cgltfWrapModeFrom(texture.getWrapT()); + + return texture_data; +} + +static cgltf_data* makeData(const Model& model, bool embed_textures) { + cgltf_data* data = safeMalloc(); + data->buffers_count = 1; + data->buffers = safeMalloc(); + + data->asset.generator = strdup("limitless GLTF model saver"); + data->asset.version = strdup("2.0"); + + std::vector buffer; + auto* embed_buffer = embed_textures ? &buffer : nullptr; + + data->meshes_count = model.getMeshes().size(); + data->meshes = safeMalloc(data->meshes_count); + + data->materials_count = model.getMaterials().size(); + data->materials = safeMalloc(data->materials_count); + + const auto accessors_per_mesh = 4; // position, normal, uv, indices + data->accessors_count = data->meshes_count * accessors_per_mesh; + data->accessors = safeMalloc(data->accessors_count); + + data->scenes_count = 1; + data->scenes = safeMalloc(); + data->scene = data->scenes; + data->scene->name = strdup("scene"); + + data->nodes_count = data->meshes_count; + data->nodes = safeMalloc(data->nodes_count); + data->scene->nodes_count = data->nodes_count; + data->scene->nodes = safeMalloc(data->nodes_count); + + for (size_t i = 0; i < data->nodes_count; ++i) { + auto& node_data = data->nodes[i]; + node_data.mesh = &data->meshes[i]; + node_data.name = strdup(("mesh" + std::to_string(i)).c_str()); + data->scene->nodes[i] = &data->nodes[i]; + } + + data->buffer_views = safeMalloc(data->meshes_count * (2 + 4)); // vertices, indices and up to 4 textures + + data->images = safeMalloc(16); + data->textures = safeMalloc(16); + data->samplers = safeMalloc(16); + + for (size_t i = 0; i < model.getMeshes().size(); ++i) { + const auto& amesh = model.getMeshes()[i]; + const auto* mesh = dynamic_cast(amesh.get()); + if (mesh == nullptr) { + continue; + } + const auto& material = model.getMaterials()[i]; + + const auto& stream = mesh->getVertexStream(); + const auto* indexed_stream = dynamic_cast*>(&stream); + if (indexed_stream == nullptr) { + continue; + } + const auto& vertices = indexed_stream->getVertices(); + const auto& indices = indexed_stream->getIndices(); + + struct GltfVertex { + glm::vec3 position; + glm::vec3 normal; + // glm::vec4 tangent; // this differs from VertexNormalTangent, which has glm::vec3. + glm::vec2 texCoord0; + }; + + auto min_position = glm::vec3(std::numeric_limits::max()); + auto max_position = glm::vec3(std::numeric_limits::min()); + + std::vector gltf_vertices; + for (const auto& vertice : vertices) { + min_position.x = std::min(min_position.x, vertice.position.x); + min_position.y = std::min(min_position.y, vertice.position.y); + min_position.z = std::min(min_position.z, vertice.position.z); + + max_position.x = std::max(max_position.x, vertice.position.x); + max_position.y = std::max(max_position.y, vertice.position.y); + max_position.z = std::max(max_position.z, vertice.position.z); + + gltf_vertices.push_back(GltfVertex{ + vertice.position, + vertice.normal, + // glm::vec4(vertice.tangent, 1.f), // w indicates handedness, -1 or +1 (no idea whats that) + vertice.uv + }); + } + + const auto alignment = 4; + if (buffer.size() % alignment != 0) { + const auto padding_size = alignment - (buffer.size() % alignment); + buffer.resize(buffer.size() + padding_size); + std::memset(buffer.data() + buffer.size() - padding_size, 0, padding_size); + } + const auto vertices_pos = buffer.size(); + const auto vertices_bytes = gltf_vertices.size() * sizeof(GltfVertex); + const auto indices_bytes = indices.size() * sizeof(uint32_t); + buffer.resize(buffer.size() + vertices_bytes + indices_bytes); + std::memcpy(buffer.data() + vertices_pos, gltf_vertices.data(), vertices_bytes); + const auto indices_pos = vertices_pos + vertices_bytes; + std::memcpy(buffer.data() + indices_pos, indices.data(), indices_bytes); + + auto* mesh_data = &data->meshes[i]; + mesh_data->name = strdup(mesh->getName().c_str()); + mesh_data->primitives_count = 1; + mesh_data->primitives = safeMalloc(); + + std::cout << "mesh->getName(): " << mesh->getName() << std::endl; + std::cout << "vertices.size(): " << vertices.size() << std::endl; + std::cout << "indices.size(): " << indices.size() << std::endl; + std::cout << "buffer.size(): " << buffer.size() << std::endl; + + auto& primitive = mesh_data->primitives[0]; + primitive.type = cgltf_primitive_type_triangles; + primitive.attributes_count = 3; + primitive.attributes = safeMalloc(primitive.attributes_count); + + auto& position = primitive.attributes[0]; + position.name = strdup("POSITION"); + position.type = cgltf_attribute_type_position; + position.index = 0; + position.data = &data->accessors[accessors_per_mesh*i + 0]; + position.data->component_type = cgltf_component_type_r_32f; + position.data->type = cgltf_type_vec3; + position.data->count = gltf_vertices.size(); + position.data->normalized = false; + position.data->offset = 0; + position.data->stride = sizeof(GltfVertex); + // vertex position attribute accessors MUST have accessor.min and accessor.max defined. + position.data->has_min = true; + position.data->min[0] = min_position[0]; + position.data->min[1] = min_position[1]; + position.data->min[2] = min_position[2]; + position.data->has_max = true; + position.data->max[0] = max_position[0]; + position.data->max[1] = max_position[1]; + position.data->max[2] = max_position[2]; + + position.data->buffer_view = safeAllocOneMore(&data->buffer_views, &data->buffer_views_count); + auto*& vertex_buffer_view = position.data->buffer_view; + vertex_buffer_view->buffer = data->buffers; + vertex_buffer_view->offset = vertices_pos; + vertex_buffer_view->size = gltf_vertices.size() * sizeof(GltfVertex); + vertex_buffer_view->stride = sizeof(GltfVertex); + vertex_buffer_view->type = cgltf_buffer_view_type_vertices; + + auto& normal = primitive.attributes[1]; + normal.name = strdup("NORMAL"); + normal.type = cgltf_attribute_type_normal; + normal.index = 1; + normal.data = &data->accessors[accessors_per_mesh*i + normal.index]; + normal.data->component_type = cgltf_component_type_r_32f; + normal.data->count = gltf_vertices.size(); + normal.data->type = cgltf_type_vec3; + normal.data->offset = offsetof(GltfVertex, GltfVertex::normal); + normal.data->stride = sizeof(GltfVertex); + normal.data->buffer_view = vertex_buffer_view; + + // auto& tangent = primitive.attributes[2]; + // tangent.name = strdup("TANGENT"); + // tangent.type = cgltf_attribute_type_tangent; + // tangent.index = 2; + // tangent.data = &data->accessors[accessors_per_mesh*i + 2]; + // tangent.data->component_type = cgltf_component_type_r_32f; + // tangent.data->count = gltf_vertices.size(); + // tangent.data->type = cgltf_type_vec4; + // tangent.data->normalized = false; + // tangent.data->offset = offsetof(GltfVertex, GltfVertex::tangent); + // tangent.data->stride = sizeof(GltfVertex); + // tangent.data->buffer_view = vertex_buffer_view; + + auto& uv = primitive.attributes[2]; + uv.name = strdup("TEXCOORD_0"); + uv.type = cgltf_attribute_type_texcoord; + uv.index = 2; + uv.data = &data->accessors[accessors_per_mesh*i + uv.index]; + uv.data->component_type = cgltf_component_type_r_32f; + uv.data->count = gltf_vertices.size(); + uv.data->type = cgltf_type_vec2; + uv.data->normalized = false; + uv.data->offset = offsetof(GltfVertex, GltfVertex::texCoord0); + uv.data->stride = sizeof(GltfVertex); + uv.data->buffer_view = vertex_buffer_view; + + primitive.indices = &data->accessors[accessors_per_mesh*i + 3]; + primitive.indices->component_type = cgltf_component_type_r_32u; + primitive.indices->type = cgltf_type_scalar; + primitive.indices->count = indices.size(); + primitive.indices->stride = sizeof(uint32_t); + primitive.indices->buffer_view = safeAllocOneMore(&data->buffer_views, &data->buffer_views_count); + primitive.indices->buffer_view->buffer = data->buffers; + primitive.indices->buffer_view->offset = indices_pos; + primitive.indices->buffer_view->size = indices.size() * sizeof(uint32_t); + // primitive.indices->buffer_view->stride = sizeof(uint32_t); + primitive.indices->buffer_view->type = cgltf_buffer_view_type_indices; + + primitive.material = &data->materials[i]; + std::cout << "saving material " << material->getName() << '\n'; + primitive.material->name = strdup((material->getName()).c_str()); + auto& pmaterial = *primitive.material; + for (const auto& property : material->getProperties()) { + switch (property.first) { + case ms::Property::Color: { + const auto& color = static_cast&>(*property.second).getValue(); + pmaterial.has_pbr_metallic_roughness = true; + pmaterial.pbr_metallic_roughness.base_color_factor[0] = color.r; + pmaterial.pbr_metallic_roughness.base_color_factor[1] = color.g; + pmaterial.pbr_metallic_roughness.base_color_factor[2] = color.b; + pmaterial.pbr_metallic_roughness.base_color_factor[3] = color.a; + break; + } + case ms::Property::EmissiveColor: { + const auto& emissive_color = static_cast&>(*property.second).getValue(); + pmaterial.emissive_factor[0] = emissive_color.r; + pmaterial.emissive_factor[1] = emissive_color.g; + pmaterial.emissive_factor[2] = emissive_color.b; + break; + } + case ms::Property::Diffuse: { + const auto& diffuse_texture = static_cast(*property.second).getSampler(); + auto& color_texture_view = pmaterial.pbr_metallic_roughness.base_color_texture; + pmaterial.has_pbr_metallic_roughness = true; + color_texture_view.texture = cgltfTextureFrom(*diffuse_texture, material->getName() + property.second->getName(), data, embed_buffer); + color_texture_view.texcoord = 0; + color_texture_view.scale = 1.0f; + color_texture_view.has_transform = false; + } + break; + + case ms::Property::Normal: { + const auto& normal_texture = static_cast(*property.second).getSampler(); + auto& normal_texture_view = pmaterial.normal_texture; + normal_texture_view.texture = cgltfTextureFrom(*normal_texture, material->getName() + property.second->getName(), data, embed_buffer); + normal_texture_view.texcoord = 0; + normal_texture_view.scale = 1.0f; + normal_texture_view.has_transform = false; + } + break; + case ms::Property::EmissiveMask: { + const auto& emissive_mask_texture = static_cast(*property.second).getSampler(); + auto& emissive_mask_texture_view = pmaterial.emissive_texture; + emissive_mask_texture_view.texture = cgltfTextureFrom(*emissive_mask_texture, material->getName() + property.second->getName(), data, embed_buffer); + emissive_mask_texture_view.texcoord = 0; + emissive_mask_texture_view.scale = 1.f; + emissive_mask_texture_view.has_transform = false; + } + break; + case ms::Property::BlendMask: + // not supported yet + break; + case ms::Property::MetallicTexture: + // not supported yet + break; + case ms::Property::RoughnessTexture: + // not supported yet + break; + case ms::Property::AmbientOcclusionTexture: + // not supported yet + break; + case ms::Property::ORM: + // not supported yet + break; + case ms::Property::Metallic: + // not supported yet + break; + case ms::Property::Roughness: + // not supported yet + break; + case ms::Property::IoR: + pmaterial.has_ior = true; + pmaterial.ior.ior = static_cast&>(*property.second).getValue(); + break; + case ms::Property::Absorption: + // not supported yet + break; + case ms::Property::MicroThickness: + // not supported yet + break; + case ms::Property::Thickness: + // not supported yet + break; + case ms::Property::Reflectance: + // not supported yet + break; + case ms::Property::Transmission: + // not supported yet + break; + default: + throw ModelSaveError("Unsupported material property: " + std::to_string(static_cast(property.first))); + } + } + pmaterial.alpha_mode = [&]() { + switch (material->getBlending()) { + case ms::Blending::Opaque: { + const auto alpha_uniform_it = material->getUniforms().find("alpha_cutoff"); + if (alpha_uniform_it != material->getUniforms().end()) { + const auto& alpha_cutoff = static_cast&>(*alpha_uniform_it->second).getValue(); + pmaterial.alpha_cutoff = alpha_cutoff; + return cgltf_alpha_mode_mask; + } else { + return cgltf_alpha_mode_opaque; + } + } + case ms::Blending::Translucent: + return cgltf_alpha_mode_blend; + case ms::Blending::Additive: + throw ModelSaveError("Additive blending is not supported yet"); + case ms::Blending::Modulate: + throw ModelSaveError("Modulate blending is not supported yet"); + case ms::Blending::Text: + throw ModelSaveError("Text blending is not supported"); + } + throw ModelSaveError("Unsupported blending mode: " + std::to_string(static_cast(material->getBlending()))); + }(); + pmaterial.double_sided = material->getTwoSided(); + pmaterial.unlit = material->getShading() == ms::Shading::Unlit; + + pmaterial.fragment = material->getFragmentSnippet() != "" ? strdup(material->getFragmentSnippet().c_str()) : nullptr; + pmaterial.models = [&](){ + unsigned int result = 0; + for (const auto instance_type : material->getModelShaders()) { + switch (instance_type) { + case Limitless::InstanceType::Model: + result |= CGLTF_LIMITLESS_MATERIAL_MODELS_MODEL; + continue; + + case Limitless::InstanceType::Instanced: + result |= CGLTF_LIMITLESS_MATERIAL_MODELS_INSTANCED; + continue; + + case Limitless::InstanceType::Decal: + case Limitless::InstanceType::Effect: + case Limitless::InstanceType::Skeletal: + case Limitless::InstanceType::SkeletalInstanced: + case Limitless::InstanceType::Terrain: + throw ModelSaveError("unsupported material instance type"); + } + throw ModelSaveError("unknown material instance type"); + } + return result; + }(); + + pmaterial.uniforms_count = material->getUniforms().size(); + pmaterial.uniforms = safeMalloc(pmaterial.uniforms_count); + size_t uniform_index = 0; + for (const auto& [uniform_name, uniform_value] : material->getUniforms()) { + auto& uniform_data = pmaterial.uniforms[uniform_index]; + uniform_data.name = strdup(uniform_name.c_str()); + + std::cout << "saving uniform " << uniform_name << '\n'; + + uniform_data.type = [&]() { + switch (uniform_value->getType()) { + case UniformType::Value: + return cgltf_uniform_type_value; + case UniformType::Sampler: + return cgltf_uniform_type_sampler; + case UniformType::Time: + return cgltf_uniform_type_time; + } + throw ModelSaveError("Unsupported uniform type: " + std::to_string(static_cast(uniform_value->getType()))); + }(); + + if (uniform_value->getType() == UniformType::Sampler) { + uniform_data.value_type = cgltf_uniform_value_type_texture; + uniform_data.value.texture_value = cgltfTextureFrom(*static_cast(*uniform_value).getSampler().get(), uniform_name, data, embed_buffer); + } else switch (uniform_value->getValueType()) { + case UniformValueType::Float: + uniform_data.value_type = cgltf_uniform_value_type_float; + uniform_data.value.float_value = static_cast&>(*uniform_value).getValue(); + break; + case UniformValueType::Int: + uniform_data.value_type = cgltf_uniform_value_type_int; + uniform_data.value.int_value = static_cast&>(*uniform_value).getValue(); + break; + case UniformValueType::Uint: + uniform_data.value_type = cgltf_uniform_value_type_uint; + uniform_data.value.uint_value = static_cast&>(*uniform_value).getValue(); + break; + case UniformValueType::Vec2: { + uniform_data.value_type = cgltf_uniform_value_type_vec2; + const auto& vec2 = static_cast&>(*uniform_value).getValue(); + uniform_data.value.vec2_value[0] = vec2.x; + uniform_data.value.vec2_value[1] = vec2.y; + break; + } + case UniformValueType::Vec3: { + uniform_data.value_type = cgltf_uniform_value_type_vec3; + const auto& vec3 = static_cast&>(*uniform_value).getValue(); + uniform_data.value.vec3_value[0] = vec3.x; + uniform_data.value.vec3_value[1] = vec3.y; + uniform_data.value.vec3_value[2] = vec3.z; + break; + } + case UniformValueType::Vec4: { + uniform_data.value_type = cgltf_uniform_value_type_vec4; + const auto& vec4 = static_cast&>(*uniform_value).getValue(); + uniform_data.value.vec4_value[0] = vec4.x; + uniform_data.value.vec4_value[1] = vec4.y; + uniform_data.value.vec4_value[2] = vec4.z; + uniform_data.value.vec4_value[3] = vec4.w; + break; + } + case UniformValueType::Mat3: { + uniform_data.value_type = cgltf_uniform_value_type_mat3; + const auto& mat3 = static_cast&>(*uniform_value).getValue(); + uniform_data.value.mat3_value[0][0] = mat3[0][0]; + uniform_data.value.mat3_value[0][1] = mat3[0][1]; + uniform_data.value.mat3_value[0][2] = mat3[0][2]; + uniform_data.value.mat3_value[1][0] = mat3[1][0]; + uniform_data.value.mat3_value[1][1] = mat3[1][1]; + uniform_data.value.mat3_value[1][2] = mat3[1][2]; + uniform_data.value.mat3_value[2][0] = mat3[2][0]; + uniform_data.value.mat3_value[2][1] = mat3[2][1]; + uniform_data.value.mat3_value[2][2] = mat3[2][2]; + break; + } + case UniformValueType::Mat4: { + uniform_data.value_type = cgltf_uniform_value_type_mat4; + const auto& mat4 = static_cast&>(*uniform_value).getValue(); + uniform_data.value.mat4_value[0][0] = mat4[0][0]; + uniform_data.value.mat4_value[0][1] = mat4[0][1]; + uniform_data.value.mat4_value[0][2] = mat4[0][2]; + uniform_data.value.mat4_value[0][3] = mat4[0][3]; + uniform_data.value.mat4_value[1][0] = mat4[1][0]; + uniform_data.value.mat4_value[1][1] = mat4[1][1]; + uniform_data.value.mat4_value[1][2] = mat4[1][2]; + uniform_data.value.mat4_value[1][3] = mat4[1][3]; + uniform_data.value.mat4_value[2][0] = mat4[2][0]; + uniform_data.value.mat4_value[2][1] = mat4[2][1]; + uniform_data.value.mat4_value[2][2] = mat4[2][2]; + uniform_data.value.mat4_value[2][3] = mat4[2][3]; + uniform_data.value.mat4_value[3][0] = mat4[3][0]; + uniform_data.value.mat4_value[3][1] = mat4[3][1]; + uniform_data.value.mat4_value[3][2] = mat4[3][2]; + uniform_data.value.mat4_value[3][3] = mat4[3][3]; + break; + } + default: + throw ModelSaveError("Unsupported uniform value type: " + std::to_string(static_cast(uniform_value->getValueType()))); + } + uniform_index++; + } + } + auto* bin = static_cast(malloc(buffer.size())); + if (bin == nullptr) { + throw ModelSaveError("Failed to allocate memory for binary data"); + } + std::memcpy(bin, buffer.data(), buffer.size()); + data->bin_size = buffer.size(); + data->bin = bin; + data->buffers[0].data = bin; + data->buffers[0].size = buffer.size(); + return data; +} + +void GltfModelSaver::saveModel(const std::filesystem::path& output_path, const Model& model) { + cgltf_options options = cgltf_options { + cgltf_file_type_glb, + 0, // auto json token count + cgltf_memory_options {nullptr, nullptr, nullptr}, + cgltf_file_options {nullptr, nullptr, nullptr} + }; + cgltf_data* data = makeData(model, /* embed_textures = */ true); + if (data == nullptr) { + throw ModelSaveError("Failed to create data for model"); + } + cgltf_result result = cgltf_write_file(&options, output_path.string().c_str(), data); + if (result != cgltf_result_success) + { + throw ModelSaveError("Failed to write model to file: " + std::to_string(static_cast(result))); + } +} From d6b355408522f859431f928020f705c04c9d71ec Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Sun, 19 Oct 2025 18:52:17 +0900 Subject: [PATCH 28/30] implement fog --- CMakeLists.txt | 1 + include/limitless/renderer/fog_pass.hpp | 29 +++++++++++ include/limitless/renderer/renderer.hpp | 1 + shaders/pipeline/fog.frag | 55 +++++++++++++++++++++ shaders/pipeline/fog.vert | 13 +++++ src/limitless/renderer/bloom_pass.cpp | 8 ++- src/limitless/renderer/composite_pass.cpp | 10 +++- src/limitless/renderer/fog_pass.cpp | 60 +++++++++++++++++++++++ src/limitless/renderer/renderer.cpp | 7 +++ src/limitless/shader_storage.cpp | 1 + 10 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 include/limitless/renderer/fog_pass.hpp create mode 100644 shaders/pipeline/fog.frag create mode 100644 shaders/pipeline/fog.vert create mode 100644 src/limitless/renderer/fog_pass.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ee0ee7b2..d4a76955 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -178,6 +178,7 @@ set(ENGINE_RENDERER src/limitless/renderer/deferred_lighting_pass.cpp src/limitless/renderer/depth_pass.cpp src/limitless/renderer/translucent_pass.cpp + src/limitless/renderer/fog_pass.cpp src/limitless/renderer/bloom_pass.cpp src/limitless/renderer/composite_pass.cpp src/limitless/renderer/ssao_pass.cpp diff --git a/include/limitless/renderer/fog_pass.hpp b/include/limitless/renderer/fog_pass.hpp new file mode 100644 index 00000000..fc2064e0 --- /dev/null +++ b/include/limitless/renderer/fog_pass.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +namespace Limitless { + class FogPass : public RendererPass { + private: + Framebuffer framebuffer; + public: + // Fog parameters + glm::vec3 fog_color = glm::vec3(0.75f, 0.75f, 0.75f); + float fog_density = 0.01f; + float fog_start = 10.0f; + float fog_end = 20.0f; + float fog_height_start = 5.0f; // altitude where attenuation begins + float fog_height_end = 9.0f; // altitude where fog fully attenuates + bool fog_enabled = true; + + explicit FogPass(Renderer& renderer); + + std::shared_ptr getResult(); + + void render(InstanceRenderer &renderer, Scene &scene, Context &ctx, const Assets &assets, const Camera &camera, UniformSetter &setter) override; + + void onFramebufferChange(glm::uvec2 size) override; + }; +} diff --git a/include/limitless/renderer/renderer.hpp b/include/limitless/renderer/renderer.hpp index d146846b..a1c1e904 100644 --- a/include/limitless/renderer/renderer.hpp +++ b/include/limitless/renderer/renderer.hpp @@ -123,6 +123,7 @@ namespace Limitless { Builder& addDeferredLightingPass(); Builder& addTranslucentPass(); Builder& addBloomPass(); + Builder& addFogPass(); Builder& addOutlinePass(); Builder& addCompositeWithBloomPass(); Builder& addCompositePass(); diff --git a/shaders/pipeline/fog.frag b/shaders/pipeline/fog.frag new file mode 100644 index 00000000..a97f20b0 --- /dev/null +++ b/shaders/pipeline/fog.frag @@ -0,0 +1,55 @@ +ENGINE::COMMON + +in vec2 uv; + +#include "./scene.glsl" +#include "../functions/linearize_depth.glsl" +#include "../functions/reconstruct_position.glsl" + +layout (location = 0) out vec3 color; + +uniform sampler2D lightened; +uniform sampler2D depth_texture; + +uniform vec3 fog_color; +uniform float fog_density; // for exponential fog +uniform float fog_start; // for linear fog start distance +uniform float fog_end; // for linear fog end distance +uniform float fog_height_start; // altitude where attenuation begins +uniform float fog_height_end; // altitude where fog becomes 0 +uniform int fog_enabled; + +float computeLinearFogFactor(float linearDepth) { + float f = clamp((linearDepth - fog_start) / max(0.0001, (fog_end - fog_start)), 0.0, 1.0); + return f; +} + +float computeExponentialFogFactor(float linearDepth) { + float f = 1.0 - exp(-linearDepth * fog_density); + return clamp(f, 0.0, 1.0); +} + +void main() { + vec3 scene_color = texture(lightened, uv).rgb; + if (fog_enabled == 0) { + color = scene_color; + return; + } + + float depth = texture(depth_texture, uv).r; + float linearDepth = linearize_depth(depth, getCameraNearPlane(), getCameraFarPlane()); + + // Reconstruct world position + vec3 worldPos = reconstructPosition(uv, depth); + float altitude = worldPos.y; // assuming Y-up + + // Height attenuation: at fog_height_start => 1.0, at fog_height_end => 0.0 + float heightAtten = 1.0 - clamp((altitude - fog_height_start) / max(0.0001, (fog_height_end - fog_height_start)), 0.0, 1.0); + + // Mix linear and exponential for flexibility; here use linear by default + float fogFactor = computeLinearFogFactor(linearDepth) * heightAtten; + + color = mix(scene_color, fog_color, fogFactor); +} + + diff --git a/shaders/pipeline/fog.vert b/shaders/pipeline/fog.vert new file mode 100644 index 00000000..8d349fd2 --- /dev/null +++ b/shaders/pipeline/fog.vert @@ -0,0 +1,13 @@ +ENGINE::COMMON + +layout (location = 0) in vec3 vertex_position; +layout (location = 1) in vec2 vertex_uv; + +out vec2 uv; + +void main() { + uv = vertex_uv; + gl_Position = vec4(vertex_position, 1.0); +} + + diff --git a/src/limitless/renderer/bloom_pass.cpp b/src/limitless/renderer/bloom_pass.cpp index 097b56c6..ba5f5ebe 100644 --- a/src/limitless/renderer/bloom_pass.cpp +++ b/src/limitless/renderer/bloom_pass.cpp @@ -2,6 +2,7 @@ #include #include #include +#include using namespace Limitless; @@ -17,7 +18,12 @@ void BloomPass::render( const Assets &assets, [[maybe_unused]] const Camera &camera, [[maybe_unused]] UniformSetter &setter) { - bloom.process(ctx, assets, renderer.getPass().getResult()); + // Prefer fogged image if present, else translucent result + if (renderer.isPresent()) { + bloom.process(ctx, assets, renderer.getPass().getResult()); + } else { + bloom.process(ctx, assets, renderer.getPass().getResult()); + } } void BloomPass::onFramebufferChange(glm::uvec2 size) { diff --git a/src/limitless/renderer/composite_pass.cpp b/src/limitless/renderer/composite_pass.cpp index 4ff2b265..99f10e44 100644 --- a/src/limitless/renderer/composite_pass.cpp +++ b/src/limitless/renderer/composite_pass.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include using namespace Limitless; @@ -38,7 +39,10 @@ void CompositePass::render( auto& shader = assets.shaders.get("composite"); - shader.setUniform("lightened", renderer.getPass().getResult()); + // Prefer fogged image if present, else translucent result + shader.setUniform("lightened", renderer.isPresent() + ? renderer.getPass().getResult() + : renderer.getPass().getResult()); { shader.setUniform("outline", renderer.getPass().getResult()) @@ -77,7 +81,9 @@ void CompositeWithBloomPass::render( auto& shader = assets.shaders.get("composite_with_bloom"); - shader.setUniform("lightened", renderer.getPass().getResult()); + shader.setUniform("lightened", renderer.isPresent() + ? renderer.getPass().getResult() + : renderer.getPass().getResult()); { auto& bloom_pass = renderer.getPass(); diff --git a/src/limitless/renderer/fog_pass.cpp b/src/limitless/renderer/fog_pass.cpp new file mode 100644 index 00000000..48f7b548 --- /dev/null +++ b/src/limitless/renderer/fog_pass.cpp @@ -0,0 +1,60 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Limitless; + +FogPass::FogPass(Renderer& renderer) + : RendererPass {renderer} + , framebuffer {Framebuffer::asRGB8LinearClampToEdge(renderer.getResolution())} { +} + +std::shared_ptr FogPass::getResult() { + return framebuffer.get(FramebufferAttachment::Color0).texture; +} + +void FogPass::render( + [[maybe_unused]] InstanceRenderer& instance_renderer, + [[maybe_unused]] Scene &scene, + Context &ctx, + const Assets &assets, + [[maybe_unused]] const Camera &camera, + [[maybe_unused]] UniformSetter &setter) { + + CpuProfileScope scope(global_profiler, "FogPass::render"); + + ctx.disable(Capabilities::DepthTest); + ctx.disable(Capabilities::Blending); + + ctx.setViewPort(getResult()->getSize()); + framebuffer.clear(); + + auto& shader = assets.shaders.get("fog"); + + shader + .setUniform("lightened", renderer.getPass().getResult()) + .setUniform("depth_texture", renderer.getPass().getDepth()) + .setUniform("fog_color", fog_color) + .setUniform("fog_start", fog_start) + .setUniform("fog_end", fog_end) + .setUniform("fog_height_start", fog_height_start) + .setUniform("fog_height_end", fog_height_end) + .setUniform("fog_density", fog_density) + .setUniform("fog_enabled", fog_enabled ? 1 : 0) + .use(); + + assets.meshes.at("quad")->draw(); +} + +void FogPass::onFramebufferChange(glm::uvec2 size) { + framebuffer.onFramebufferChange(size); +} + + diff --git a/src/limitless/renderer/renderer.cpp b/src/limitless/renderer/renderer.cpp index 0975436b..a00543a5 100644 --- a/src/limitless/renderer/renderer.cpp +++ b/src/limitless/renderer/renderer.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -138,6 +139,11 @@ Renderer::Builder &Renderer::Builder::addBloomPass() { return *this; } +Renderer::Builder &Renderer::Builder::addFogPass() { + renderer->passes.emplace_back(std::make_unique(*renderer)); + return *this; +} + Renderer::Builder &Renderer::Builder::addOutlinePass() { renderer->passes.emplace_back(std::make_unique(*renderer)); return *this; @@ -187,6 +193,7 @@ Renderer::Builder &Renderer::Builder::deferred() { } addDeferredLightingPass(); addTranslucentPass(); + addFogPass(); if (renderer->settings.bloom) { addBloomPass(); } diff --git a/src/limitless/shader_storage.cpp b/src/limitless/shader_storage.cpp index 098072e1..277677de 100644 --- a/src/limitless/shader_storage.cpp +++ b/src/limitless/shader_storage.cpp @@ -118,6 +118,7 @@ void ShaderStorage::initialize(Context& ctx, const RendererSettings& settings, c add("composite", compiler.compile(shader_dir / "pipeline/composite")); } add("outline", compiler.compile(shader_dir / "pipeline/outline")); + add("fog", compiler.compile(shader_dir / "pipeline/fog")); if (settings.screen_space_ambient_occlusion) { From add9b9422b6ca3d8bec92ad9936e7a31f74ff567 Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Mon, 20 Oct 2025 20:29:03 +0900 Subject: [PATCH 29/30] try ACES mapping --- shaders/functions/tone_mapping.glsl | 28 ++++++++++++++++++++++ shaders/pipeline/composite.frag | 2 +- shaders/pipeline/composite_with_bloom.frag | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/shaders/functions/tone_mapping.glsl b/shaders/functions/tone_mapping.glsl index 0f9b08c9..b6f97973 100644 --- a/shaders/functions/tone_mapping.glsl +++ b/shaders/functions/tone_mapping.glsl @@ -1,3 +1,31 @@ +mat3 ACESInputMat = mat3( + 0.59719, 0.07600, 0.02840, + 0.35458, 0.90834, 0.13383, + 0.04823, 0.01566, 0.83777); + +mat3 ACESOutputMat = mat3( + 1.60475, -0.10208, -0.00327, + -0.53108, 1.10813, -0.07276, + -0.07367, -0.00605, 1.07602); + +vec3 RRTAndODTFit(vec3 v) { + vec3 a = v * (v + 0.0245786) - 0.000090537; + vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081; + return a / b; +} + +vec3 acesFitted(vec3 color) { + color = ACESInputMat * color; + color = RRTAndODTFit(color); + color = ACESOutputMat * color; + return clamp(color, 0.0, 1.0); +} + +vec3 acesMapping(vec3 hdrColor, float exposure) { + vec3 mapped = acesFitted(hdrColor * exposure); + return pow(mapped, vec3(1.0 / 2.2)); +} + vec3 toneMapping(vec3 color, float exposure) { return vec3(1.0) - exp(-color * exposure); } diff --git a/shaders/pipeline/composite.frag b/shaders/pipeline/composite.frag index 6a9fcb90..4c1a8be8 100644 --- a/shaders/pipeline/composite.frag +++ b/shaders/pipeline/composite.frag @@ -16,7 +16,7 @@ void main() { color = texture(lightened, uv).rgb; // apply tone mapping function to HDR - color = toneMapping(color, tone_mapping_exposure); + color = acesMapping(color, tone_mapping_exposure); // apply gamma correction color = pow(color, vec3(1.0 / gamma)); diff --git a/shaders/pipeline/composite_with_bloom.frag b/shaders/pipeline/composite_with_bloom.frag index 3c63fbcf..1af8a478 100644 --- a/shaders/pipeline/composite_with_bloom.frag +++ b/shaders/pipeline/composite_with_bloom.frag @@ -19,7 +19,7 @@ void main() { color = texture(lightened, uv).rgb + bloom_color; // apply tone mapping function to HDR - color = toneMapping(color, tone_mapping_exposure); + color = acesMapping(color, tone_mapping_exposure); // apply gamma correction color = pow(color, vec3(1.0 / gamma)); From 371c19e2bbed124544c9588d7094d395d6b202fc Mon Sep 17 00:00:00 2001 From: Tehsapper Date: Tue, 25 Nov 2025 10:01:33 +0900 Subject: [PATCH 30/30] fix failure on 0x0 framebuffer --- src/limitless/camera.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/limitless/camera.cpp b/src/limitless/camera.cpp index d3e99938..73572ac7 100644 --- a/src/limitless/camera.cpp +++ b/src/limitless/camera.cpp @@ -54,6 +54,11 @@ void Camera::updateView() noexcept { } void Camera::updateProjection(glm::uvec2 screen_size) noexcept { + if (screen_size.x == 0 || screen_size.y == 0) { + // window probably got minimized, skip update due to meaningless aspect ratio. + return; + } + projection = glm::perspective( glm::radians(fov), static_cast(screen_size.x) / static_cast(screen_size.y),