Skip to content

Commit

Permalink
Wrapped up font atlas generation
Browse files Browse the repository at this point in the history
  • Loading branch information
WillisMedwell committed Feb 18, 2024
1 parent d691136 commit 598860b
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 40 deletions.
9 changes: 2 additions & 7 deletions code/Engine/include/Media/Font.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "Media/Image.hpp"
#include <array>
#include <glm/vec2.hpp>
#include <limits>
#include <ranges>

Expand All @@ -22,16 +23,10 @@ namespace Media {

consteval static auto gen_is_char_drawable_table() -> std::array<bool, 256> {
auto table = std::array<bool, 256> { false };
#if 0
for (auto [i, e] : table | std::views::enumerate) {
e = std::ranges::contains(DRAWABLE_CHARS, static_cast<char>(i));
}
#else
std::ptrdiff_t i = 0;
for (auto iter = table.begin(); iter != table.end(); ++iter, ++i) {
*iter = std::ranges::find(DRAWABLE_CHARS, static_cast<char>(i)) != DRAWABLE_CHARS.end();
}
#endif
return table;
}
constexpr static auto IS_CHAR_DRAWABLE = gen_is_char_drawable_table();
Expand All @@ -45,7 +40,7 @@ namespace Media {
Font(Font&& other);

[[nodiscard]] auto init(std::vector<uint8_t>& encoded_ttf) noexcept -> Utily::Result<void, Utily::Error>;
[[nodiscard]] auto gen_image_atlas(uint32_t char_height_px) -> Utily::Result<Media::Image, Utily::Error>;
[[nodiscard]] auto gen_image_atlas(uint32_t char_height_px) -> Utily::Result<std::tuple<Media::Image, int, int>, Utily::Error>;

void stop() noexcept;

Expand Down
74 changes: 45 additions & 29 deletions code/Engine/src/Media/Font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ namespace Media {
}
};

auto Font::gen_image_atlas(uint32_t char_height_px) -> Utily::Result<Media::Image, Utily::Error> {
auto Font::gen_image_atlas(uint32_t char_height_px) -> Utily::Result<std::tuple<Media::Image, int, int>, Utily::Error> {
constexpr auto& drawable_chars = FontAtlasConstants::DRAWABLE_CHARS;

// Validate and Scale FreeType Face.
Expand All @@ -98,10 +98,10 @@ namespace Media {
ftg = FreeTypeGlyph(ff->glyph);
return GlyphInfo(ff->glyph);
};
auto cached_ft_bitmaps = std::array<FreeTypeGlyph, drawable_chars.size()> {};

auto cached_ft_bitmaps = std::array<FreeTypeGlyph, drawable_chars.size()> {};
// 1. Transform -> Generates and caches the freetype bitmaps & returns its glyph layout
// 2. Reduce -> Calculate a general glyph layout for the atlas.
// 2. Reduce -> Calculate the maximum bounding glyph layout, so all characters have enough space in atlas.
auto atlas_info = std::transform_reduce(
drawable_chars.begin(),
drawable_chars.end(),
Expand All @@ -110,42 +110,58 @@ namespace Media {
&GlyphInfo::take_max_values,
generate_and_cache_ft_glyph);

const size_t atlas_buffer_size = atlas_info.width * atlas_info.height * drawable_chars.size();
std::vector<uint8_t> atlas_buffer(atlas_buffer_size);
std::ranges::fill(atlas_buffer, (uint8_t)0);

#if 1
for (std::ptrdiff_t i = 0; i < static_cast<std::ptrdiff_t>(drawable_chars.size()); ++i) {
const auto& bitmap_ft = cached_ft_bitmaps.at(i);
uint8_t* bitmap_atlas = atlas_buffer.data() + (atlas_info.width * atlas_info.height * i);
for (std::ptrdiff_t y = 0; y < bitmap_ft.height; ++y) {
for (std::ptrdiff_t x = 0; x < bitmap_ft.width; ++x) {
const std::ptrdiff_t atlas_y = y + atlas_info.spanline - bitmap_ft.spanline;
const std::ptrdiff_t atlas_x = x;
const std::ptrdiff_t atlas_offset = atlas_info.width * atlas_y + atlas_x;
const std::ptrdiff_t ft_offset = y * bitmap_ft.width + x;
bitmap_atlas[atlas_offset] = bitmap_ft.buffer[ft_offset];
const auto [atlas_glyphs_per_row, atlas_num_rows] = [](float glyph_width, float glyph_height, float num_glyphs_in_atlas) {
#if 1 // generate most square shape by brute force.
auto [min_diff, min_x, min_y] = std::tuple{ std::numeric_limits<float>::max(), 0, num_glyphs_in_atlas };
for (float i = 1; i < num_glyphs_in_atlas; ++i) {
auto x = i;
auto y = num_glyphs_in_atlas / i;
x = std::floor(x);
y = std::ceil(y);
auto diff = std::abs((x * glyph_width) - (y * glyph_height));
if (diff < min_diff && num_glyphs_in_atlas <= x * y) {
min_diff = diff;
min_x = x;
min_y = y;
}
}
}
#else
// safer version
auto atlas_bitmaps = atlas_buffer | std::views::chunk(atlas_info.width * atlas_info.height);
for (auto [bitmap_ft, bitmap_atlas] : std::views::zip(cached_ft_bitmaps, atlas_bitmaps)) {
float expected_dimensions = std::sqrtf(num_glyphs_in_atlas);
float ratio = glyph_height / glyph_width;
float min_x = std::floor(expected_dimensions * ratio);
float min_y = std::ceil(expected_dimensions / ratio);
assert(num_glyphs_in_atlas < min_x * min_y);
#endif
return std::tuple<int, int> { min_x, min_y };
}(atlas_info.width, atlas_info.height, drawable_chars.size());

const int atlas_img_height = atlas_info.height * atlas_num_rows;
const int atlas_img_width = atlas_info.width * atlas_glyphs_per_row;

std::vector<uint8_t> atlas_buffer(atlas_img_height * atlas_img_width);
std::ranges::fill(atlas_buffer, (uint8_t)0);

for (std::ptrdiff_t i = 0; i < static_cast<std::ptrdiff_t>(drawable_chars.size()); ++i) {
const auto& bitmap_ft = cached_ft_bitmaps[i];
const auto atlas_coords = glm::ivec2 { i % atlas_glyphs_per_row, i / atlas_glyphs_per_row };
const auto adjusted_offset = glm::ivec2 { atlas_coords.x * atlas_info.width, atlas_coords.y * atlas_info.height };

auto get_atlas_buffer_dest = [&](int x, int y) -> uint8_t& {
const auto px_coords = glm::ivec2(adjusted_offset.x + x, adjusted_offset.y + y);
return atlas_buffer[px_coords.y * atlas_img_width + px_coords.x];
};
for (std::ptrdiff_t y = 0; y < bitmap_ft.height; ++y) {
for (std::ptrdiff_t x = 0; x < bitmap_ft.width; ++x) {
const std::ptrdiff_t atlas_y = y + atlas_info.spanline - bitmap_ft.spanline;
const std::ptrdiff_t atlas_x = x;
const std::ptrdiff_t atlas_offset = atlas_info.width * atlas_y + atlas_x;
// align to relative bitmap
const std::ptrdiff_t relative_y = (y + atlas_info.spanline - bitmap_ft.spanline);
const std::ptrdiff_t ft_offset = y * bitmap_ft.width + x;
bitmap_atlas[atlas_offset] = bitmap_ft.buffer[ft_offset];
get_atlas_buffer_dest(x, relative_y) = bitmap_ft.buffer[ft_offset];
}
}
}
#endif
Media::Image image;
image.init_raw(std::move(atlas_buffer), atlas_info.width, atlas_info.height * drawable_chars.size(), Media::ColourFormat::greyscale);
return std::move(image);
image.init_raw(std::move(atlas_buffer), atlas_img_width, atlas_img_height, ColourFormat::greyscale);
return std::tuple{ std::move(image), atlas_num_rows, atlas_glyphs_per_row };
}

void Font::stop() noexcept {
Expand Down
4 changes: 2 additions & 2 deletions code/Engine/src/Media/Image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ namespace Media {
lodepng::State state;
state.info_raw.colortype = LodePNGColorType::LCT_GREY;
if (auto error = lodepng::encode(encoded, _data, _width, _height, state); error) {
return Utily::Error { lodepng_error_text(error) };
return Utily::Error { std::string("Image.save_to_disk() failed to be converted to png: ") + lodepng_error_text(error) };
}
if (auto error = lodepng::save_file(encoded, path.string()); error) {
return Utily::Error { lodepng_error_text(error) };
return Utily::Error { std::string("Image.save_to_disk() failed to save: ") + lodepng_error_text(error) };
}
return {};
}
Expand Down
4 changes: 3 additions & 1 deletion code/Test/include/Integration/BasicApps.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,13 @@ struct FontLogic {

auto e2 = data.font.init(ttf_raw.value());
auto e3 = data.font.gen_image_atlas(100);
auto e4 = std::get<0>(e3.value()).save_to_disk("Roboto.png");

EXPECT_FALSE(ttf_raw.has_error());
EXPECT_FALSE(e2.has_error());
EXPECT_FALSE(e3.has_error());
EXPECT_FALSE(e3.value().save_to_disk("Roboto.png").has_error());
EXPECT_FALSE(e4.has_error());
e4.on_error(Utily::ErrorHandler::print_then_quit);
}
void update(float dt, AppInput& input, AppState& state, entt::registry& ecs, FontData& data) {
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - data.start_time);
Expand Down
2 changes: 1 addition & 1 deletion code/build-native.bat
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ if not exist "build-native\" (

cd build-native

call cmake .. -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=%VCPKG_PATH%/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=%BUILD_TYPE%
@REM call cmake .. -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=%VCPKG_PATH%/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=%BUILD_TYPE%
call cmake --build . --config %BUILD_TYPE%

cd Test
Expand Down

0 comments on commit 598860b

Please sign in to comment.