Windows (DirectX 12) |
Linux (Vulkan) |
MacOS (Metal) |
iOS (Metal) |
---|---|---|---|
This tutorial demonstrates animated text rendering with dynamic font atlas updates using Methane UI. Three colored text blocks are animated with continuous character typing. Each text block is rendered as a single mesh displaying character glyphs from the font atlas texture to screen rectangles, which are generated on the CPU using the Freetype 2.0 library.
The font atlas texture can be updated dynamically by adding new character glyphs on demand, as the user types text, including any non-ASCII character sets. Text character layout and mesh generation are done on the CPU using Methane implementation, without using third-party libraries, and support horizontal and vertical text alignment in rectangular areas with wrapping by characters and words. Right-to-left and Arabic language character layout is not supported yet.
Keyboard actions are enabled with TypographyAppController class derived from Platform::Input::Keyboard::ActionControllerBase:
Typography App Action | Keyboard Shortcut |
---|---|
Switch Text Wrap Mode (None, Anywhere, Word) | W |
Switch Text Horizontal Alignment (Left, Right, Center, Justify) | H |
Switch Text Vertical Alignment (Top, Bottom, Center) | V |
Switch Incremental Text Update | U |
Switch Typing Direction (Forward, Backward) | D |
Speedup Typing | + |
Slowdown Typing | - |
Common keyboard controls are enabled by the Platform
, Graphics
and UserInterface
application controllers:
- Methane::Platform::AppController
- Methane::Graphics::AppController, AppContextController
- Methane::UserInterface::AppController
TypographyApp
class is declared in header file TypographyApp.h
, and is derived from
UserInterface::App base class, same as in previous tutorial.
Base application class UserInterface::App<TypographyFrame>
uses frame structure TypographyFrame
, which defines only
the render command list and execution command list set that wraps this command list.
TypographyApp
class defines a settings structure TypographyApp::Settings
, a getter for the current settings
TypographyApp::GetSettings
, and individual setters for each application setting:
TypographyApp::SetTextLayout
- allows changing word wrapping mode, horizontal and vertical text layouts for all text blocks;TypographyApp::SetForwardTypingDirection
- changes text typing animation: appending new characters iftrue
, or backspace deleting iffalse
;TypographyApp::SetTextUpdateInterval
- changes text typing time interval in milliseconds;TypographyApp::SetIncrementalTextUpdate
- enables or disables incremental updating of text block mesh buffers.
Application setting getters can be changed by the user at runtime with keyboard shortcuts, handled in TypographyAppController.h.
TypographyApp
class contains the following private members:
gui::Font
objects, each for a unique font, size, and color;gui::TextItem
objects, each for one text block;gui::Badge
objects for rendering font atlas textures on screen;std::vector<size_t>
displayed lengths of text in each text block, incremented with animation;Timer::TimeDuration
holds the duration of the last text block update to be displayed on screen.
#pragma once
#include <Methane/Kit.h>
#include <Methane/Data/Receiver.hpp>
namespace Methane::Tutorials
{
namespace gfx = Methane::Graphics;
namespace rhi = Methane::Graphics::Rhi;
namespace gui = Methane::UserInterface;
struct TypographyFrame final : gfx::AppFrame
{
rhi::RenderCommandList render_cmd_list;
rhi::CommandListSet execute_cmd_list_set;
using gfx::AppFrame::AppFrame;
};
using UserInterfaceApp = UserInterface::App<TypographyFrame>;
class TypographyApp final
: public UserInterfaceApp
, private Data::Receiver<gui::IFontLibraryCallback>
, private Data::Receiver<gui::IFontCallback>
{
public:
struct Settings
{
gui::Text::Layout text_layout { gui::Text::Wrap::Word, gui::Text::HorizontalAlignment::Center, gui::Text::VerticalAlignment::Top };
bool is_incremental_text_update = true;
bool is_forward_typing_direction = true;
double typing_update_interval_sec = 0.03;
};
TypographyApp();
~TypographyApp() override;
// GraphicsApp overrides
...
// UserInterface::App overrides
std::string GetParametersString() override;
// Settings accessors
const Settings& GetSettings() const noexcept { return m_settings; }
void SetTextLayout(const gui::Text::Layout& text_layout);
void SetForwardTypingDirection(bool is_forward_typing_direction);
void SetTextUpdateInterval(double text_update_interval_sec);
void SetIncrementalTextUpdate(bool is_incremental_text_update);
private:
...
Settings m_settings;
gui::FontContext m_font_context;
std::vector<gui::Font> m_fonts;
Ptrs<gui::TextItem> m_texts;
Ptrs<gui::Badge> m_font_atlas_badges;
std::vector<size_t> m_displayed_text_lengths;
double m_text_update_elapsed_sec = 0.0;
Timer::TimeDuration m_text_update_duration;
};
} // namespace Methane::Tutorials
Fonts and text blocks are initialized in the TypographyApp::Init()
method in a for
loop for each block index. Text blocks
are positioned one below another using the vertical_text_pos_in_dots
variable, which holds the vertical position in
DPI-independent Dot units.
const gfx::FrameSize frame_size_in_dots = GetFrameSizeInDots();
const uint32_t frame_width_without_margins = frame_size_in_dots.GetWidth() - 2 * g_margin_size_in_dots;
int32_t vertical_text_pos_in_dots = g_top_text_pos_in_dots;
for(size_t block_index = 0; block_index < g_text_blocks_count; ++block_index)
{
const FontSettings& font_settings = g_font_settings[block_index];
const size_t displayed_text_length = m_displayed_text_lengths[block_index];
const std::u32string displayed_text_block = g_text_blocks[block_index].substr(0, displayed_text_length);
// Add font to library
m_fonts.push_back( ... );
// Add text element
m_texts.push_back( ... );
vertical_text_pos_in_dots = m_texts.back()->GetRectInDots().GetBottom() + g_margin_size_in_dots;
}
Font objects are used to create and render text blocks on screen. Font objects are loaded from the Data::FontProvider
singleton with specified font settings and are added to the fonts library. Data::FontProvider
loads TTF
fonts from
application resources. Font settings include:
- Font description:
- Unique name for registration in the fonts library
- File path loaded with the data provider
- Size in points
- Font resolution DPI
- Initial alphabet to be rendered and added to the font atlas (it is also dynamically extended on demand)
UserInterface::FontContext
is a helper class that holds references to FontLibrary
and Data::IProvider
to facilitate
font creation and queries via the FontContext::GetFont
method.
// Add font to library
m_fonts.push_back(
m_font_context.GetFont(
gui::Font::Settings
{
font_settings.desc,
GetUIContext().GetFontResolutionDpi(),
gui::Font::GetAlphabetFromText(displayed_text_block)
}
)
);
Each text block object is created using the UserInterface::Context
object acquired with the UserInterface::App::GetUIContext()
method, the font object which was created previously, and text settings:
- Font name registered in the fonts library
- Text string in UTF8 or UTF32 format
- Rectangle area in Dots or Pixels on the screen to fit the text in
- Layout describing how the text will fit into the rectangular area
- Wrapping mode (No wrapping, Wrap anywhere, Wrap on word boundaries)
- Horizontal alignment (Left, Right, Center)
- Vertical alignment (Top, Bottom, Center)
- Color of the text block
- Incremental text mesh update flag
// Add text element
m_texts.push_back(
std::make_shared<gui::TextItem>(GetUIContext(), m_fonts.back(),
gui::Text::SettingsUtf32
{
font_settings.desc.name,
displayed_text_block,
gui::UnitRect
{
gui::Units::Dots,
gfx::Point2I { g_margin_size_in_dots, vertical_text_pos_in_dots },
gfx::FrameSize { frame_width_without_margins, 0U /* calculated height */ }
},
m_settings.text_layout,
gfx::Color4F(font_settings.color, 1.F),
m_settings.is_incremental_text_update
}
)
);
Font objects hold font atlas textures, which are displayed on screen in this tutorial using UserInterface::Badge
objects.
These badges are created in the TypographyApp::UpdateFontAtlasBadges()
method:
oid TypographyApp::UpdateFontAtlasBadges()
{
const std::vector<gui::Font> fonts = m_font_context.GetFontLibrary().GetFonts();
const rhi::RenderContext& context = GetRenderContext();
// Remove obsolete font atlas badges
...
// Add new font atlas badges
for(const gui::Font& font : fonts)
{
const rhi::Texture& font_atlas_texture = font.GetAtlasTexture(context);
if (!font_atlas_texture.IsInitialized() ||
std::any_of(m_font_atlas_badges.begin(), m_font_atlas_badges.end(),
[&font_atlas_texture](const Ptr<gui::Badge>& font_atlas_badge_ptr)
{
return font_atlas_badge_ptr->GetTexture() == font_atlas_texture;
}))
continue;
m_font_atlas_badges.emplace_back(CreateFontAtlasBadge(font, font_atlas_texture));
}
LayoutFontAtlasBadges(GetRenderContext().GetSettings().frame_size);
}
Font atlas badge bound to atlas texture is created with helper method TypographyApp::CreateFontAtlasBadge
:
Ptr<gui::Badge> TypographyApp::CreateFontAtlasBadge(const gui::Font& font, const rhi::Texture& atlas_texture)
{
const auto font_color_by_name_it = g_font_color_by_name.find(font.GetSettings().description.name);
const gui::Color3F& font_color = font_color_by_name_it != g_font_color_by_name.end()
? font_color_by_name_it->second : g_misc_font_color;
return std::make_shared<gui::Badge>(
GetUIContext(), atlas_texture,
gui::Badge::Settings
{
font.GetSettings().description.name + " Font Atlas",
gui::Badge::FrameCorner::BottomLeft,
gui::UnitSize(gui::Units::Pixels, static_cast<const gfx::FrameSize&>(atlas_texture.GetSettings().dimensions)),
gui::UnitSize(gui::Units::Dots, 16U, 16U),
gui::Color4F(font_color, 0.5F),
gui::Badge::TextureMode::RFloatToAlpha,
}
);
}
TypographyApp::LayoutFontAtlasBadges
positions all created atlas badges in the bottom-left corner of the screen,
arranging them one after another, starting with the largest textures and ending with the smallest.
Animation function bound to time-animation in the constructor of the TypographyApp
class is called automatically as a part
of every render cycle, just before the App::Update
function call. The TypographyApp::Animate
function updates the text
displayed in text blocks by adding or deleting characters and updates text block positions at equal time intervals.
TypographyApp::TypographyApp()
{
...
GetAnimations().emplace_back(std::make_shared<Data::TimeAnimation>(std::bind(&ShadowCubeApp::Animate, this, std::placeholders::_1, std::placeholders::_2)));
}
bool TypographyApp::Animate(double elapsed_seconds, double)
{
if (elapsed_seconds - m_text_update_elapsed_sec < m_settings.typing_update_interval_sec)
return true;
m_text_update_elapsed_sec = elapsed_seconds;
int32_t vertical_text_pos_in_dots = g_top_text_pos_in_dots;
for(size_t block_index = 0; block_index < g_text_blocks_count; ++block_index)
{
AnimateTextBlock(block_index, vertical_text_pos_in_dots);
}
UpdateParametersText();
return true;
}
Each text rendering object manages its own set of GPU resources for each frame in the swap-chain:
- Vertex and index buffers
- Shader uniforms buffer
- Font atlas texture
- Program bindings
To update text resources on the GPU and make them ready for frame rendering, the Text::Update(...)
method is called for each
text object in TypographyApp::Update()
.
bool TypographyApp::Update()
{
if (!UserInterfaceApp::Update())
return false;
// Update text block resources
for(const Ptr<gui::TextItem>& text_ptr : m_texts)
{
text_ptr->Update(GetFrameSize());
}
return true;
}
After updating text resources on the GPU, rendering is straightforward: UserInterface::Text::Draw(...)
is called for each text
object with a per-frame render command list and debug group. Font atlas badges are rendered similarly using
UserInterface::Badge::Draw(...)
.
bool TypographyApp::Render()
{
if (!UserInterfaceApp::Render())
return false;
const TypographyFrame& frame = GetCurrentFrame();
// Draw text blocks
META_DEBUG_GROUP_VAR(s_text_debug_group, "Text Blocks Rendering");
for(const Ptr<gui::TextItem>& text_ptr : m_texts)
{
text_ptr->Draw(frame.render_cmd_list, &s_text_debug_group);
}
// Draw font atlas badges
META_DEBUG_GROUP_VAR(s_atlas_debug_group, "Font Atlases Rendering");
for(const Ptr<gui::Badge>& badge_atlas_ptr : m_font_atlas_badges)
{
badge_atlas_ptr->Draw(frame.render_cmd_list, &s_atlas_debug_group);
}
RenderOverlay(frame.render_cmd_list);
frame.render_cmd_list.Commit();
// Execute command list on render queue and present frame to screen
GetRenderContext().GetRenderCommandKit().GetQueue().Execute(frame.execute_cmd_list_set);
GetRenderContext().Present();
return true;
}
TTF
fonts are added to the application embedded resources with add_methane_embedded_fonts
cmake-function.
include(MethaneApplications)
add_methane_application(
TARGET MethaneTypography
NAME "Methane Typography"
DESCRIPTION "Tutorial demonstrating dynamic text rendering and font atlases management with Methane Kit."
INSTALL_DIR "Apps"
SOURCES
TypographyApp.h
TypographyApp.cpp
TypographyAppController.h
TypographyAppController.cpp
)
set(FONTS
${RESOURCES_DIR}/Fonts/Roboto/Roboto-Regular.ttf
${RESOURCES_DIR}/Fonts/Playball/Playball-Regular.ttf
${RESOURCES_DIR}/Fonts/SawarabiMincho/SawarabiMincho-Regular.ttf
)
add_methane_embedded_fonts(MethaneTypography "${RESOURCES_DIR}" "${FONTS}")
target_link_libraries(MethaneTypography
PRIVATE
MethaneAppsCommon
)
Continue learning Methane Graphics programming in the next tutorial CubeMapArray, which is demonstrating cube-map array texturing and sky-box rendering.