diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 78acc8d4d4..8e79406bee 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -30,3 +30,4 @@ endfunction() add_subdirectory(core) add_subdirectory(gl) +add_subdirectory(ren) diff --git a/demo/gl/tz_mesh_demo/main.cpp b/demo/gl/tz_mesh_demo/main.cpp index 096a113df9..d438326202 100644 --- a/demo/gl/tz_mesh_demo/main.cpp +++ b/demo/gl/tz_mesh_demo/main.cpp @@ -6,7 +6,6 @@ #include "tz/core/matrix_transform.hpp" #include "tz/ren/mesh.hpp" -#include "tz/ren/mesh2.hpp" struct dbgui_data_t { bool mesh_renderer_enabled = false; @@ -22,8 +21,8 @@ int main() { dbgui_init(); - tz::ren::mesh_renderer2 mr; - tz::ren::mesh_renderer2::mesh m; + tz::ren::mesh_renderer mr; + tz::ren::mesh_renderer::mesh m; m.indices = {0u, 1u, 2u}; m.vertices = { diff --git a/demo/ren/CMakeLists.txt b/demo/ren/CMakeLists.txt new file mode 100644 index 0000000000..7cc6a00ad3 --- /dev/null +++ b/demo/ren/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(tz_text_rendering_demo) \ No newline at end of file diff --git a/demo/ren/tz_text_rendering_demo/CMakeLists.txt b/demo/ren/tz_text_rendering_demo/CMakeLists.txt new file mode 100644 index 0000000000..cb4565156d --- /dev/null +++ b/demo/ren/tz_text_rendering_demo/CMakeLists.txt @@ -0,0 +1,13 @@ +add_demo( + TARGET tz_text_rendering_demo + SOURCE_FILES + main.cpp +) + +add_text( + TARGET tz_text_rendering_demo + INPUT_DIR ${PROJECT_SOURCE_DIR} + OUTPUT_DIR ${PROJECT_BINARY_DIR} + TEXT_FILES + demo/ren/tz_text_rendering_demo/res/ProggyClean.ttf +) diff --git a/demo/ren/tz_text_rendering_demo/main.cpp b/demo/ren/tz_text_rendering_demo/main.cpp new file mode 100644 index 0000000000..428f7e9663 --- /dev/null +++ b/demo/ren/tz_text_rendering_demo/main.cpp @@ -0,0 +1,98 @@ +#include "tz/tz.hpp" +#include "tz/gl/device.hpp" +#include "tz/io/ttf.hpp" + +#include "tz/core/imported_text.hpp" +#include ImportedTextHeader(ProggyClean, ttf) + + +// debug rendering +#include "tz/ren/mesh.hpp" + +struct dbgui_data_t +{ + bool text_renderer_enabled = false; +} dbgui_data; +void dbgui_init(); + +static tz::io::ttf::rasterise_info rast +{ + .dimensions = {32u, 32u}, + .angle_threshold = 3.0f, + .range = 0.1f, + .scale = 64.0f, + .translate = tz::vec2::zero() +}; + +int main() +{ + tz::initialise({.name = "tz_text_rendering_demo"}); + { + dbgui_init(); + tz::io::ttf ttf = tz::io::ttf::from_memory(ImportedTextData(ProggyClean, ttf)); + tz::io::image img_c = ttf.rasterise_msdf('A', rast); + + tz::ren::mesh_renderer mr; + mr.append_to_render_graph(); + + tz::ren::mesh_renderer::mesh quad; + quad.vertices = + { + {.position = {0.5f, 0.5f, 0.0f}, .texcoord = {1.0f, 1.0f}}, + {.position = {0.5f, -0.5f, 0.0f}, .texcoord = {1.0f, 0.0f}}, + {.position = {-0.5f, -0.5f, 0.0f}, .texcoord = {0.0f, 0.0f}}, + {.position = {-0.5f, 0.5f, 0.0f}, .texcoord = {0.0f, 1.0f}} + }; + quad.indices = {0u, 1u, 3u, 1u, 2u, 3u}; + auto mesh = mr.add_mesh(quad); + auto tex = mr.add_texture(img_c); + mr.camera_perspective({.aspect_ratio = 1920.0f/1080.0f, .fov = 1.5708f}); + mr.set_camera_transform({.translate = {0.0f, 0.0f, 1.0f}}); + + auto obj = mr.add_object + ({ + .mesh = mesh, + .bound_textures = {{.texture = tex}} + }); + + while(!tz::window().is_close_requested()) + { + tz::begin_frame(); + tz::gl::get_device().render(); + tz::dbgui::run([&mr, &ttf, obj]() + { + if(dbgui_data.text_renderer_enabled) + { + if(ImGui::Begin("Text Renderer", &dbgui_data.text_renderer_enabled)) + { + bool changed = false; + changed |= ImGui::SliderInt2("Dimensions", reinterpret_cast(rast.dimensions.data().data()), 1u, 512u, "%u"); + changed |= ImGui::SliderFloat("Angle Threshold", &rast.angle_threshold, 0.0f, 5.0f); + changed |= ImGui::SliderFloat("Range", &rast.range, 0.01f, 2.0f); + changed |= ImGui::SliderFloat("Scale", &rast.scale, 0.01f, 64.0f); + changed |= ImGui::SliderFloat2("Translate", rast.translate.data().data(), -1.0f, 1.0f); + + if(changed) + { + auto new_tex = mr.add_texture(ttf.rasterise_msdf('c', rast)); + mr.get_object(obj).bound_textures[0].texture = new_tex; + } + + ImGui::End(); + } + } + }); + tz::end_frame(); + } + } + tz::terminate(); +} + +// invoked when dbgui needs to set up initial state. +void dbgui_init() +{ + tz::dbgui::game_menu().add_callback([]() + { + ImGui::MenuItem("Text Renderer", nullptr, &dbgui_data.text_renderer_enabled); + }); +} \ No newline at end of file diff --git a/src/tz/io/ttf.cpp b/src/tz/io/ttf.cpp index 234d6df85a..9739c56653 100644 --- a/src/tz/io/ttf.cpp +++ b/src/tz/io/ttf.cpp @@ -38,7 +38,7 @@ namespace tz::io return ttf::from_memory(buffer); } - tz::io::image ttf::rasterise_msdf(char c) const + tz::io::image ttf::rasterise_msdf(char c, rasterise_info i) const { tz::io::image ret; @@ -69,9 +69,11 @@ namespace tz::io // } // generate bitmap - msdfgen::edgeColoringSimple(shape, 3.0f); - msdfgen::Bitmap msdf(128u, 128u); - msdfgen::generateMSDF(msdf, shape, 128.0f, 1.0f, msdfgen::Vector2(100.0f, -100.0f)); + shape.normalize(); + msdfgen::edgeColoringSimple(shape, i.angle_threshold); + msdfgen::Bitmap msdf(i.dimensions[0], i.dimensions[1]); + + msdfgen::generateMSDF(msdf, shape, i.range, i.scale, msdfgen::Vector2(i.translate[0], i.translate[1])); ret.width = msdf.width(); ret.height = msdf.height(); ret.data.resize(1 * 4u * ret.width * ret.height, std::byte{0}); @@ -82,9 +84,10 @@ namespace tz::io { for(std::size_t i = 0; i < 3; i++) { - imgdata(x, y)[i] = static_cast(msdf(x, y)[i] * 255); - imgdata(x, y)[3] = std::byte{255}; + float val = msdf(x, y)[i]; + imgdata(x, y)[i] = static_cast(255 - std::clamp(val * 256.0f, 0.0f, 255.0f)); } + imgdata(x, y)[3] = std::byte{255}; } } @@ -327,7 +330,7 @@ namespace tz::io if(this->head.index_to_loc_format == 0) { // 16 bit - for(std::size_t i = 0; std::cmp_less(i, this->maxp.num_glyphs); i++) + for(std::size_t i = 0; std::cmp_less(i, this->maxp.num_glyphs + 1); i++) { this->loca.locations16.push_back(ttf_read_value(ptr)); } @@ -335,7 +338,7 @@ namespace tz::io else { // 32 bit - for(std::size_t i = 0; std::cmp_less(i, this->maxp.num_glyphs); i++) + for(std::size_t i = 0; std::cmp_less(i, this->maxp.num_glyphs + 1); i++) { this->loca.locations32.push_back(ttf_read_value(ptr)); } @@ -360,7 +363,7 @@ namespace tz::io { auto& g = this->glyf.glyfs.emplace_back(); g.number_of_contours = ttf_read_value(ptr); - tz::assert(g.number_of_contours > 0, "Only support simple glyphs. Number of contours: %d", (int)g.number_of_contours); + tz::assert(g.number_of_contours >= 0, "Only support simple glyphs. Number of contours: %d", (int)g.number_of_contours); g.xmin = ttf_read_value(ptr); g.ymin = ttf_read_value(ptr); g.xmax = ttf_read_value(ptr); @@ -374,13 +377,17 @@ namespace tz::io std::memcpy(g.instructions.data(), ptr, g.instructions.size()); std::advance(ptr, g.instructions.size()); + if(g.number_of_contours == 0) + { + return; + } int last_index = g.end_pts_of_contours.back(); g.flags.resize(last_index + 1); for(int i = 0; i < (last_index + 1); i++) { g.flags[i] = static_cast(*ptr); ptr++; - if(static_cast(g.flags[i]) & 0x00001000) // repeat bit + if(1 == ((static_cast(g.flags[i]) >> 3) & 1)) { std::uint8_t repeat_count = *ptr; while(repeat_count-- > 0) @@ -409,10 +416,10 @@ namespace tz::io case 1: cur_coord = 0; break; - case 2: + case 3: cur_coord = -ttf_read_value(ptr); break; - case 3: + case 2: cur_coord = ttf_read_value(ptr); break; default: @@ -420,6 +427,8 @@ namespace tz::io break; } g.x_coords[i] = cur_coord + prev_coord; + tz::assert(g.xmin <= g.x_coords[i]); + tz::assert(g.xmax >= g.x_coords[i]); prev_coord = g.x_coords[i]; } g.y_coords.resize((last_index + 1)); @@ -439,10 +448,10 @@ namespace tz::io case 1: cur_coord = 0; break; - case 2: + case 3: cur_coord = -ttf_read_value(ptr); break; - case 3: + case 2: cur_coord = ttf_read_value(ptr); break; default: @@ -450,6 +459,8 @@ namespace tz::io break; } g.y_coords[i] = cur_coord + prev_coord; + tz::assert(g.ymin <= g.y_coords[i]); + tz::assert(g.ymax >= g.y_coords[i]); prev_coord = g.y_coords[i]; } }; @@ -461,7 +472,8 @@ namespace tz::io for(std::uint16_t loc16 : this->loca.locations16) { auto loca_offset = loc16 * multiplier; - read_glyph(ptrcpy + loca_offset); + ptrcpy = ptr + loca_offset; + read_glyph(ptrcpy); } } else @@ -471,8 +483,8 @@ namespace tz::io for(std::uint32_t loc32 : this->loca.locations32) { auto loca_offset = loc32 * multiplier; - ptr = ptrcpy + loca_offset; - read_glyph(ptrcpy + loca_offset); + ptrcpy = ptr + loca_offset; + read_glyph(ptrcpy); } } this->glyf.canary = true; @@ -618,23 +630,55 @@ namespace tz::io char c = ttf_alphabet[i]; auto index = this->cmap.glyph_index_map[c | 0] | 0; auto glyfd = this->glyf.glyfs[index]; - auto hmtxd = this->hmtx.hmetrics[index]; + // its possible index > hmetrics.length + // spec:The table uses a longHorMetric record to give the advance width and left side bearing of a glyph. Records are indexed by glyph ID. As an optimization, the number of records can be less than the number of glyphs, in which case the advance width value of the last record applies to all remaining glyph IDs + // If numberOfHMetrics is less than the total number of glyphs, then the hMetrics array is followed by an array for the left side bearing values of the remaining glyphs. + std::size_t advance_width, left_side_bearing; + if(std::cmp_greater_equal(index, this->hmtx.hmetrics.size())) + { + advance_width = hmtx.hmetrics.back().advance_width; + left_side_bearing = hmtx.left_side_bearings[index - hmtx.hmetrics.size()]; + } + else + { + auto hmtxd = this->hmtx.hmetrics[index]; + advance_width = hmtxd.advance_width; + left_side_bearing = hmtxd.left_side_bearing; + } ttf_glyph_shape_info shape; tz::assert(glyfd.x_coords.size() == glyfd.y_coords.size()); std::size_t contour_cursor = 0; shape.contours.resize(glyfd.number_of_contours); - for(std::size_t i = 1; i < glyfd.x_coords.size(); i++) + std::optional cache = std::nullopt; + for(std::size_t i = 0; i < glyfd.x_coords.size(); i++) { - auto beg = static_cast(tz::vec2i{glyfd.x_coords[i - 1], glyfd.y_coords[i - 1]}); - beg /= this->head.units_per_em; - auto end = static_cast(tz::vec2i{glyfd.x_coords[i], glyfd.y_coords[i]}); - end /= this->head.units_per_em; - shape.contours[contour_cursor].edges.push_back({beg, end}); + if(!cache.has_value()) + { + cache = static_cast(tz::vec2i{glyfd.x_coords[i], glyfd.y_coords[i]}); + } + else + { + shape.contours[contour_cursor].edges.push_back({ + cache.value(), + static_cast(tz::vec2i{glyfd.x_coords[i], glyfd.y_coords[i]}) + }); + cache = std::nullopt; + } if(i == glyfd.end_pts_of_contours[contour_cursor]) { // contour ends here. contour_cursor++; + cache = std::nullopt; + } + } + + for(auto& contour : shape.contours) + { + for (auto& [a, b] : contour.edges) + { + a /= this->head.units_per_em; + b /= this->head.units_per_em; } } @@ -644,8 +688,8 @@ namespace tz::io { .position = static_cast(tz::vector{glyfd.xmin, glyfd.ymin}), .dimensions = static_cast(tz::vector{glyfd.xmax - glyfd.xmin, glyfd.ymax - glyfd.ymin}), - .left_side_bearing = static_cast(hmtxd.left_side_bearing), - .right_side_bearing = static_cast(hmtxd.advance_width - hmtxd.left_side_bearing - (glyfd.xmax - glyfd.xmin)) + .left_side_bearing = static_cast(left_side_bearing), + .right_side_bearing = static_cast(advance_width - left_side_bearing - (glyfd.xmax - glyfd.xmin)) }, .shape = shape }; diff --git a/src/tz/io/ttf.hpp b/src/tz/io/ttf.hpp index 70526d5c40..a4fdedcf93 100644 --- a/src/tz/io/ttf.hpp +++ b/src/tz/io/ttf.hpp @@ -183,7 +183,16 @@ namespace tz::io static ttf from_memory(std::string_view sv); static ttf from_file(const char* path); - tz::io::image rasterise_msdf(char c) const; + struct rasterise_info + { + tz::vec2ui dimensions; + float angle_threshold; + float range; + float scale; + tz::vec2 translate; + }; + + tz::io::image rasterise_msdf(char c, rasterise_info i) const; ttf(std::string_view ttf_data); private: std::string_view parse_header(std::string_view str); diff --git a/src/tz/ren/shaders/mesh.vertex.tzsl b/src/tz/ren/shaders/mesh.vertex.tzsl index 4f9b72916b..d0b1b075e6 100644 --- a/src/tz/ren/shaders/mesh.vertex.tzsl +++ b/src/tz/ren/shaders/mesh.vertex.tzsl @@ -48,7 +48,7 @@ resource(id = 2) const buffer camera_buffer output(id = 0) vec2 out::texcoord; output(id = 1) vec3 out::normal; output(id = 2) vec3 out::colour; -output(id = 3) uvec2 out::impl_unused; +output(id = 3) vec3 out::impl_unused; output(id = 4) texture_locator out::textures[MAX_TEX_COUNT]; void main()