259 lines
9.3 KiB
C++
259 lines
9.3 KiB
C++
/*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#include "TextDrawable.hpp"
|
|
#include "Scene/Geometry.hpp"
|
|
#include "Shader/Shader.hpp"
|
|
#include "Text/IFontAtlasGenerator.hpp"
|
|
#include "Base/Logger.hpp"
|
|
#include "DataFormat.hpp"
|
|
#include <utf8.h>
|
|
|
|
namespace OpenVulkano::Scene
|
|
{
|
|
namespace
|
|
{
|
|
constexpr uint32_t MISSING_GLYPH_SYMBOL = '?';
|
|
|
|
Shader DEFAULT_SHADER_BITMAP = TextDrawable::MakeDefaultShader(FontAtlasType::BITMAP);
|
|
Shader DEFAULT_SHADER_BITMAP_SUBPIXEL = TextDrawable::MakeDefaultShader(FontAtlasType::BITMAP_SUBPIXEL);
|
|
Shader DEFAULT_SHADER_SDF = TextDrawable::MakeDefaultShader(FontAtlasType::SDF);
|
|
Shader DEFAULT_SHADER_MSDF = TextDrawable::MakeDefaultShader(FontAtlasType::MSDF);
|
|
|
|
void HandleSpecialCharacter(const std::map<uint32_t, GlyphInfo>& symbols, uint32_t c,
|
|
const float heightBetweenLines, const float initialPosX,
|
|
float& cursorX, float& cursorY, float& prevGlyphXBound)
|
|
{
|
|
if (c == '\n')
|
|
{
|
|
cursorY -= heightBetweenLines;
|
|
prevGlyphXBound = -INFINITY;
|
|
cursorX = initialPosX;
|
|
}
|
|
else if (c == '\t')
|
|
{
|
|
if (!symbols.contains(' '))
|
|
{
|
|
Logger::RENDER->warn("Tab special character can't be handled since no space glyph is available", c);
|
|
return;
|
|
}
|
|
const GlyphInfo& info = symbols.at(' ');
|
|
cursorX += info.advance * 4;
|
|
prevGlyphXBound = cursorX;
|
|
}
|
|
else if (c == '\v')
|
|
{
|
|
cursorY -= heightBetweenLines;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
Shader TextDrawable::MakeDefaultShader(const FontAtlasType type)
|
|
{
|
|
Shader shader;
|
|
shader.AddShaderProgram(ShaderProgramType::VERTEX, "Shader/text");
|
|
shader.AddShaderProgram(ShaderProgramType::FRAGMENT, std::string(type.GetDefaultFragmentShader()));
|
|
VertexInputDescription inputDesc(0, sizeof(TextGlyphVertex));
|
|
inputDesc.AddInputParameter(DataFormat::R32G32_SFLOAT, offsetof(TextGlyphVertex, position));
|
|
inputDesc.AddInputParameter(DataFormat::R32G32_SFLOAT, offsetof(TextGlyphVertex, position)+8);
|
|
inputDesc.AddInputParameter(DataFormat::R32G32_SFLOAT, offsetof(TextGlyphVertex, position)+16);
|
|
inputDesc.AddInputParameter(DataFormat::R32G32_SFLOAT, offsetof(TextGlyphVertex, position)+24);
|
|
inputDesc.AddInputParameter(DataFormat::R32G32_SFLOAT, offsetof(TextGlyphVertex, uv));
|
|
inputDesc.AddInputParameter(DataFormat::R32G32_SFLOAT, offsetof(TextGlyphVertex, uv)+8);
|
|
inputDesc.AddInputParameter(DataFormat::R32G32_SFLOAT, offsetof(TextGlyphVertex, uv)+16);
|
|
inputDesc.AddInputParameter(DataFormat::R32G32_SFLOAT, offsetof(TextGlyphVertex, uv)+24);
|
|
inputDesc.AddInputParameter(DataFormat::R8G8B8A8_UNORM, offsetof(TextGlyphVertex, color));
|
|
inputDesc.AddInputParameter(DataFormat::R8G8B8A8_UNORM, offsetof(TextGlyphVertex, background));
|
|
inputDesc.stepMode = VertexStepMode::INSTANCE;
|
|
shader.AddVertexInputDescription(inputDesc);
|
|
shader.AddDescriptorSetLayoutBinding(Texture::DESCRIPTOR_SET_LAYOUT_BINDING);
|
|
shader.alphaBlend = true;
|
|
shader.cullMode = CullMode::NONE;
|
|
shader.topology = Topology::TRIANGLE_STRIP;
|
|
return shader;
|
|
}
|
|
|
|
TextDrawable::TextDrawable(const TextConfig& config)
|
|
: Drawable(DrawEncoder::GetDrawEncoder<TextDrawable>()), m_cfg(config)
|
|
{}
|
|
|
|
TextDrawable::TextDrawable(const std::shared_ptr<FontAtlas>& atlasData, const TextConfig& config)
|
|
: Drawable(DrawEncoder::GetDrawEncoder<TextDrawable>()), m_atlasData(atlasData), m_cfg(config)
|
|
{
|
|
if (!atlasData || !*atlasData) throw std::runtime_error("Cannot initialize text drawable with empty atlas data");
|
|
}
|
|
|
|
uint32_t TextDrawable::GetFallbackGlyph() const
|
|
{ //TODO move into FontAtlas
|
|
if (m_atlasData->GetGlyphs().contains(MISSING_GLYPH_SYMBOL))
|
|
{
|
|
return MISSING_GLYPH_SYMBOL;
|
|
}
|
|
Logger::RENDER->warn("Could not find glyph for character ? to use as fallback. Using first glyph instead");
|
|
return m_atlasData->GetGlyphs().begin()->first;
|
|
}
|
|
|
|
float TextDrawable::GetHeightBetweenLines(const std::string& text) const
|
|
{
|
|
if (!m_cfg.minimalSpacingBetweenMultipleLines)
|
|
{
|
|
return m_atlasData->GetLineHeight();
|
|
}
|
|
const std::map<uint32_t, GlyphInfo>& symbols = m_atlasData->GetGlyphs();
|
|
const uint32_t fallbackGlyph = GetFallbackGlyph();
|
|
std::vector<float> heightBetweenLines;
|
|
float currentLineHeightAboveBaseline = 0;
|
|
float currentLineHeightBelowBaseline = 0;
|
|
float prevLineHeightBelowBaseline = -INFINITY;
|
|
float extraOffset = m_atlasData->GetAtlasType().IsBitmap() ? 0.1 : 0.05;
|
|
bool isMultiline = false;
|
|
|
|
for (auto begin = text.begin(), end = text.end(); begin != end;)
|
|
{
|
|
uint32_t c = utf8::next(begin, end);
|
|
if (c == '\n' || c == '\v')
|
|
{
|
|
isMultiline = true;
|
|
if (prevLineHeightBelowBaseline != -INFINITY)
|
|
{
|
|
heightBetweenLines.push_back(std::abs(prevLineHeightBelowBaseline)
|
|
+ std::abs(currentLineHeightAboveBaseline) + extraOffset);
|
|
prevLineHeightBelowBaseline = currentLineHeightBelowBaseline;
|
|
}
|
|
prevLineHeightBelowBaseline = currentLineHeightBelowBaseline;
|
|
currentLineHeightAboveBaseline = currentLineHeightBelowBaseline = 0;
|
|
continue;
|
|
}
|
|
|
|
if (!symbols.contains(c))
|
|
{
|
|
c = fallbackGlyph;
|
|
}
|
|
const GlyphInfo& info = symbols.at(c);
|
|
currentLineHeightAboveBaseline = std::max(currentLineHeightAboveBaseline, info.pos[2].y);
|
|
currentLineHeightBelowBaseline = std::min(currentLineHeightBelowBaseline, -info.pos[0].y);
|
|
}
|
|
|
|
if (isMultiline && text.back() != '\n')
|
|
{
|
|
heightBetweenLines.push_back(std::abs(prevLineHeightBelowBaseline)
|
|
+ std::abs(currentLineHeightAboveBaseline) + extraOffset);
|
|
}
|
|
|
|
if (heightBetweenLines.empty())
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
float maxH = *std::max_element(heightBetweenLines.begin(), heightBetweenLines.end());
|
|
return std::min<float>(m_atlasData->GetLineHeight(), maxH);
|
|
}
|
|
|
|
bool TextDrawable::IsSpecialCharacter(uint32_t c) const
|
|
{
|
|
return c == '\t' || c == '\n' || c == '\v';
|
|
}
|
|
|
|
void TextDrawable::GenerateText(const std::string& text, const Math::Vector2f& pos, float scale)
|
|
{
|
|
if (text.empty()) return;
|
|
if (m_vertexBuffer.data) throw std::runtime_error("Text has already been initialized");
|
|
const uint32_t fallbackGlyph = GetFallbackGlyph();
|
|
|
|
m_text = text;
|
|
m_symbolCount = 0;
|
|
const size_t len = utf8::distance(text.begin(), text.end());
|
|
m_vertexBuffer.Close();
|
|
TextGlyphVertex* vertices = m_vertexBuffer.Init<TextGlyphVertex>(len);
|
|
const std::map<uint32_t, GlyphInfo>& symbols = m_atlasData->GetGlyphs();
|
|
|
|
Math::Vector2f cursor = pos / scale;
|
|
Math::Vector2f bmin(pos), bmax(pos);
|
|
float prevGlyphXBound = -INFINITY;
|
|
const float heightBetweenLines = GetHeightBetweenLines(text);
|
|
|
|
for (auto begin = text.begin(), end = text.end(); begin != end;)
|
|
{
|
|
uint32_t c = utf8::next(begin, end);
|
|
if (IsSpecialCharacter(c))
|
|
{
|
|
HandleSpecialCharacter(symbols, c, heightBetweenLines, pos.x, cursor.x, cursor.y, prevGlyphXBound);
|
|
continue;
|
|
}
|
|
|
|
if (!symbols.contains(c))
|
|
{
|
|
Logger::RENDER->warn("Could not find glyph for character {}, using fallback", c);
|
|
c = fallbackGlyph;
|
|
}
|
|
|
|
const GlyphInfo& info = symbols.at(c);
|
|
|
|
float offset = 0;
|
|
const bool isZeroLenGlyph = info.pos[1].x == 0;
|
|
const bool isBitmap = m_atlasData->GetAtlasType().IsBitmap();
|
|
if (prevGlyphXBound != -INFINITY && !isZeroLenGlyph && !isBitmap)
|
|
{
|
|
offset = prevGlyphXBound - (info.pos[0].x + cursor.x);
|
|
}
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
vertices->position[i].x = info.pos[i].x + cursor.x + offset;
|
|
vertices->uv[i] = info.uv[i];
|
|
if (i < 2)
|
|
{
|
|
vertices->position[i].y = cursor.y - info.pos[i].y;
|
|
}
|
|
else
|
|
{
|
|
vertices->position[i].y = cursor.y + info.pos[i].y;
|
|
}
|
|
vertices->color = m_cfg.textColor;
|
|
vertices->background = m_cfg.backgroundColor;
|
|
vertices->position[i] *= scale;
|
|
}
|
|
std::swap(vertices->position[3], vertices->position[3]);
|
|
std::swap(vertices->uv[3], vertices->uv[3]);
|
|
|
|
// somehow it's possible that cursorX can be less than prevGlyphXBound for sdf atlases
|
|
// e.g. string `A, _` where space is not noticeable in the resulting output
|
|
cursor.x = std::max(cursor.x, prevGlyphXBound);
|
|
// slight offset for bitmap atlas since it's tightly packed without any extra padding, while sdf has additional padding
|
|
cursor.x += info.advance + (isBitmap ? 0.05 : 0);
|
|
prevGlyphXBound = isZeroLenGlyph ? cursor.x : vertices->position[2].x / scale;
|
|
|
|
if (!m_symbolCount) bmin.x = vertices->position[0].x;
|
|
bmax.x = std::max(bmax.x, vertices->position[1].x);
|
|
bmax.y = std::max(bmax.y, vertices->position[2].y);
|
|
bmin.y = std::min(bmin.y, vertices->position[1].y);
|
|
vertices++;
|
|
m_symbolCount++;
|
|
}
|
|
m_bbox.Init(bmin, bmax);
|
|
|
|
if (!GetShader()) SetShader(GetDefaultShader(m_atlasData->GetAtlasType()));
|
|
}
|
|
|
|
void TextDrawable::SetAtlasData(const std::shared_ptr<FontAtlas>& atlasData)
|
|
{
|
|
if (!atlasData || !*atlasData) throw std::runtime_error("Cannot initialize text drawable with empty atlas data");
|
|
m_atlasData = atlasData;
|
|
}
|
|
|
|
Shader* TextDrawable::GetDefaultShader(const FontAtlasType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case FontAtlasType::SDF: return &DEFAULT_SHADER_SDF;
|
|
case FontAtlasType::MSDF: return &DEFAULT_SHADER_MSDF;
|
|
default: Logger::RENDER->warn("No default shader for atlas type: {}", type.GetName());
|
|
case FontAtlasType::BITMAP: return &DEFAULT_SHADER_BITMAP;
|
|
case FontAtlasType::BITMAP_SUBPIXEL: return &DEFAULT_SHADER_BITMAP_SUBPIXEL;
|
|
}
|
|
}
|
|
} |