INtial text again
This commit is contained in:
450
src/render/runtime/RuntimeTextTextureCache.cpp
Normal file
450
src/render/runtime/RuntimeTextTextureCache.cpp
Normal file
@@ -0,0 +1,450 @@
|
||||
#include "RuntimeTextTextureCache.h"
|
||||
|
||||
#include "../../runtime/RuntimeJson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <wincodec.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr GLuint kFirstTextTextureUnit = 8;
|
||||
constexpr unsigned kTextTextureHeight = 128;
|
||||
constexpr unsigned kTextTexturePadding = 8;
|
||||
constexpr double kFontPixelsPerEm = 96.0;
|
||||
|
||||
std::string ReadTextFile(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
return std::string();
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
const JsonValue* FindObjectValue(const JsonValue& object, const std::string& key)
|
||||
{
|
||||
return object.isObject() ? object.find(key) : nullptr;
|
||||
}
|
||||
|
||||
double NumberMember(const JsonValue& object, const std::string& key, double fallback = 0.0)
|
||||
{
|
||||
const JsonValue* value = FindObjectValue(object, key);
|
||||
return value && value->isNumber() ? value->asNumber(fallback) : fallback;
|
||||
}
|
||||
|
||||
struct ComThreadGuard
|
||||
{
|
||||
~ComThreadGuard()
|
||||
{
|
||||
if (initialized)
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
bool Initialize()
|
||||
{
|
||||
const HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
initialized = SUCCEEDED(result);
|
||||
return SUCCEEDED(result) || result == RPC_E_CHANGED_MODE;
|
||||
}
|
||||
|
||||
bool initialized = false;
|
||||
};
|
||||
}
|
||||
|
||||
RuntimeTextTextureCache::~RuntimeTextTextureCache()
|
||||
{
|
||||
ShutdownGl();
|
||||
}
|
||||
|
||||
bool RuntimeTextTextureCache::Configure(const RuntimeShaderArtifact& artifact, std::string& error)
|
||||
{
|
||||
ShutdownGl();
|
||||
mArtifact = artifact;
|
||||
|
||||
for (const RenderCadenceCompositor::FontAtlasBuildOutput& output : artifact.fontAtlases)
|
||||
{
|
||||
Atlas atlas;
|
||||
if (!LoadAtlas(output, atlas, error))
|
||||
return false;
|
||||
mAtlases.push_back(std::move(atlas));
|
||||
}
|
||||
|
||||
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
|
||||
{
|
||||
if (definition.type != ShaderParameterType::Text)
|
||||
continue;
|
||||
|
||||
if (FindAtlas(definition.fontId) == nullptr)
|
||||
{
|
||||
error = "No prepared font atlas is available for text parameter '" + definition.id + "'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
TextTexture texture;
|
||||
texture.parameterId = definition.id;
|
||||
texture.fontId = definition.fontId;
|
||||
mTextTextures.push_back(std::move(texture));
|
||||
}
|
||||
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeTextTextureCache::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
|
||||
mArtifact.parameterValues = artifact.parameterValues;
|
||||
}
|
||||
|
||||
void RuntimeTextTextureCache::BindTextTextures(GLuint program)
|
||||
{
|
||||
for (std::size_t index = 0; index < mTextTextures.size(); ++index)
|
||||
{
|
||||
TextTexture& textTexture = mTextTextures[index];
|
||||
if (!EnsureTextTexture(textTexture))
|
||||
continue;
|
||||
|
||||
glActiveTexture(GL_TEXTURE0 + kFirstTextTextureUnit + static_cast<GLuint>(index));
|
||||
glBindTexture(GL_TEXTURE_2D, textTexture.texture);
|
||||
}
|
||||
|
||||
glUseProgram(program);
|
||||
AssignSamplerUniforms(program, mArtifact);
|
||||
glUseProgram(0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
void RuntimeTextTextureCache::ShutdownGl()
|
||||
{
|
||||
for (TextTexture& texture : mTextTextures)
|
||||
DestroyTexture(texture);
|
||||
mTextTextures.clear();
|
||||
mAtlases.clear();
|
||||
}
|
||||
|
||||
void RuntimeTextTextureCache::AssignSamplerUniforms(GLuint program, const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
glUseProgram(program);
|
||||
GLuint nextUnit = kFirstTextTextureUnit;
|
||||
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
|
||||
{
|
||||
if (definition.type != ShaderParameterType::Text)
|
||||
continue;
|
||||
|
||||
const std::string samplerName = definition.id + "Texture";
|
||||
const GLint location = glGetUniformLocation(program, samplerName.c_str());
|
||||
if (location >= 0)
|
||||
glUniform1i(location, static_cast<GLint>(nextUnit));
|
||||
const std::string samplerArrayName = samplerName + "_0";
|
||||
const GLint arrayLocation = glGetUniformLocation(program, samplerArrayName.c_str());
|
||||
if (arrayLocation >= 0)
|
||||
glUniform1i(arrayLocation, static_cast<GLint>(nextUnit));
|
||||
++nextUnit;
|
||||
}
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
bool RuntimeTextTextureCache::LoadAtlas(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const
|
||||
{
|
||||
atlas.fontId = output.fontId;
|
||||
if (!LoadAtlasJson(output, atlas, error))
|
||||
return false;
|
||||
if (!LoadAtlasImage(output, atlas, error))
|
||||
return false;
|
||||
if (atlas.width == 0 || atlas.height == 0 || atlas.rgbaPixels.empty())
|
||||
{
|
||||
error = "Font atlas image is empty for font '" + output.fontId + "'.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeTextTextureCache::LoadAtlasJson(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const
|
||||
{
|
||||
const std::string jsonText = ReadTextFile(output.jsonPath);
|
||||
if (jsonText.empty())
|
||||
{
|
||||
error = "Could not read font atlas json: " + output.jsonPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonValue root;
|
||||
if (!ParseJson(jsonText, root, error))
|
||||
return false;
|
||||
|
||||
const JsonValue* metrics = FindObjectValue(root, "metrics");
|
||||
if (metrics)
|
||||
{
|
||||
atlas.ascender = NumberMember(*metrics, "ascender", atlas.ascender);
|
||||
atlas.descender = NumberMember(*metrics, "descender", atlas.descender);
|
||||
atlas.lineHeight = NumberMember(*metrics, "lineHeight", atlas.lineHeight);
|
||||
}
|
||||
|
||||
const JsonValue* glyphs = FindObjectValue(root, "glyphs");
|
||||
if (!glyphs || !glyphs->isArray())
|
||||
{
|
||||
error = "Font atlas json has no glyph array: " + output.jsonPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const JsonValue& glyphJson : glyphs->asArray())
|
||||
{
|
||||
if (!glyphJson.isObject())
|
||||
continue;
|
||||
|
||||
const unsigned codepoint = static_cast<unsigned>(NumberMember(glyphJson, "unicode", 0.0));
|
||||
Glyph glyph;
|
||||
glyph.advance = NumberMember(glyphJson, "advance", 0.0);
|
||||
|
||||
const JsonValue* planeBounds = FindObjectValue(glyphJson, "planeBounds");
|
||||
const JsonValue* atlasBounds = FindObjectValue(glyphJson, "atlasBounds");
|
||||
if (planeBounds && atlasBounds)
|
||||
{
|
||||
glyph.planeLeft = NumberMember(*planeBounds, "left", 0.0);
|
||||
glyph.planeTop = NumberMember(*planeBounds, "top", 0.0);
|
||||
glyph.planeRight = NumberMember(*planeBounds, "right", 0.0);
|
||||
glyph.planeBottom = NumberMember(*planeBounds, "bottom", 0.0);
|
||||
glyph.atlasLeft = NumberMember(*atlasBounds, "left", 0.0);
|
||||
glyph.atlasTop = NumberMember(*atlasBounds, "top", 0.0);
|
||||
glyph.atlasRight = NumberMember(*atlasBounds, "right", 0.0);
|
||||
glyph.atlasBottom = NumberMember(*atlasBounds, "bottom", 0.0);
|
||||
glyph.hasBounds = true;
|
||||
}
|
||||
|
||||
atlas.glyphsByCodepoint[codepoint] = glyph;
|
||||
}
|
||||
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeTextTextureCache::LoadAtlasImage(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const
|
||||
{
|
||||
ComThreadGuard comGuard;
|
||||
if (!comGuard.Initialize())
|
||||
{
|
||||
error = "Could not initialize COM for font atlas PNG loading.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICImagingFactory> factory;
|
||||
HRESULT result = CoCreateInstance(
|
||||
CLSID_WICImagingFactory,
|
||||
nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(factory.GetAddressOf()));
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not create WIC imaging factory for font atlas PNG loading.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICBitmapDecoder> decoder;
|
||||
result = factory->CreateDecoderFromFilename(
|
||||
output.imagePath.wstring().c_str(),
|
||||
nullptr,
|
||||
GENERIC_READ,
|
||||
WICDecodeMetadataCacheOnLoad,
|
||||
decoder.GetAddressOf());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not decode font atlas PNG: " + output.imagePath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICBitmapFrameDecode> frame;
|
||||
result = decoder->GetFrame(0, frame.GetAddressOf());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not read font atlas PNG frame: " + output.imagePath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IWICFormatConverter> converter;
|
||||
result = factory->CreateFormatConverter(converter.GetAddressOf());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not create WIC format converter for font atlas PNG.";
|
||||
return false;
|
||||
}
|
||||
|
||||
result = converter->Initialize(
|
||||
frame.Get(),
|
||||
GUID_WICPixelFormat32bppRGBA,
|
||||
WICBitmapDitherTypeNone,
|
||||
nullptr,
|
||||
0.0,
|
||||
WICBitmapPaletteTypeCustom);
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not convert font atlas PNG to RGBA.";
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT width = 0;
|
||||
UINT height = 0;
|
||||
converter->GetSize(&width, &height);
|
||||
atlas.width = static_cast<unsigned>(width);
|
||||
atlas.height = static_cast<unsigned>(height);
|
||||
atlas.rgbaPixels.assign(static_cast<std::size_t>(atlas.width) * atlas.height * 4u, 0);
|
||||
|
||||
const UINT stride = width * 4u;
|
||||
result = converter->CopyPixels(nullptr, stride, static_cast<UINT>(atlas.rgbaPixels.size()), atlas.rgbaPixels.data());
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not copy font atlas PNG pixels.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture)
|
||||
{
|
||||
const ShaderParameterValue* value = FindParameterValue(mArtifact, texture.parameterId);
|
||||
const std::string text = value ? value->textValue : DefaultTextValue(mArtifact, texture.parameterId);
|
||||
if (texture.texture != 0 && texture.cachedText == text)
|
||||
return true;
|
||||
|
||||
const Atlas* atlas = FindAtlas(texture.fontId);
|
||||
if (!atlas)
|
||||
return false;
|
||||
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
std::vector<unsigned char> pixels = ComposeTextMask(*atlas, text, width, height);
|
||||
if (pixels.empty() || width == 0 || height == 0)
|
||||
return false;
|
||||
|
||||
if (texture.texture == 0)
|
||||
glGenTextures(1, &texture.texture);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, texture.texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_RED, GL_UNSIGNED_BYTE, pixels.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
texture.cachedText = text;
|
||||
texture.width = width;
|
||||
texture.height = height;
|
||||
return texture.texture != 0;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> RuntimeTextTextureCache::ComposeTextMask(const Atlas& atlas, const std::string& text, unsigned& width, unsigned& height) const
|
||||
{
|
||||
double advance = 0.0;
|
||||
for (unsigned char character : text)
|
||||
{
|
||||
const auto glyphIt = atlas.glyphsByCodepoint.find(character);
|
||||
if (glyphIt != atlas.glyphsByCodepoint.end())
|
||||
advance += glyphIt->second.advance;
|
||||
}
|
||||
|
||||
width = (std::max)(1u, static_cast<unsigned>(std::ceil(advance * kFontPixelsPerEm)) + kTextTexturePadding * 2u);
|
||||
height = kTextTextureHeight;
|
||||
std::vector<unsigned char> mask(static_cast<std::size_t>(width) * height, 0);
|
||||
|
||||
const double baseline = kTextTexturePadding + (-atlas.ascender * kFontPixelsPerEm);
|
||||
double penX = static_cast<double>(kTextTexturePadding);
|
||||
|
||||
for (unsigned char character : text)
|
||||
{
|
||||
const auto glyphIt = atlas.glyphsByCodepoint.find(character);
|
||||
if (glyphIt == atlas.glyphsByCodepoint.end())
|
||||
continue;
|
||||
|
||||
const Glyph& glyph = glyphIt->second;
|
||||
if (glyph.hasBounds)
|
||||
{
|
||||
const int destLeft = static_cast<int>(std::floor(penX + glyph.planeLeft * kFontPixelsPerEm));
|
||||
const int destTop = static_cast<int>(std::floor(baseline + glyph.planeTop * kFontPixelsPerEm));
|
||||
const int destRight = static_cast<int>(std::ceil(penX + glyph.planeRight * kFontPixelsPerEm));
|
||||
const int destBottom = static_cast<int>(std::ceil(baseline + glyph.planeBottom * kFontPixelsPerEm));
|
||||
const double destWidth = (std::max)(1.0, static_cast<double>(destRight - destLeft));
|
||||
const double destHeight = (std::max)(1.0, static_cast<double>(destBottom - destTop));
|
||||
|
||||
for (int y = destTop; y < destBottom; ++y)
|
||||
{
|
||||
if (y < 0 || y >= static_cast<int>(height))
|
||||
continue;
|
||||
const double v = (static_cast<double>(y) + 0.5 - destTop) / destHeight;
|
||||
for (int x = destLeft; x < destRight; ++x)
|
||||
{
|
||||
if (x < 0 || x >= static_cast<int>(width))
|
||||
continue;
|
||||
const double u = (static_cast<double>(x) + 0.5 - destLeft) / destWidth;
|
||||
const double atlasX = glyph.atlasLeft + u * (glyph.atlasRight - glyph.atlasLeft);
|
||||
const double atlasY = glyph.atlasTop + v * (glyph.atlasBottom - glyph.atlasTop);
|
||||
unsigned char& destination = mask[static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)];
|
||||
destination = (std::max)(destination, SampleAtlasAlpha(atlas, atlasX, atlasY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
penX += glyph.advance * kFontPixelsPerEm;
|
||||
}
|
||||
|
||||
for (unsigned y = 0; y < height / 2u; ++y)
|
||||
{
|
||||
unsigned char* topRow = mask.data() + static_cast<std::size_t>(y) * width;
|
||||
unsigned char* bottomRow = mask.data() + static_cast<std::size_t>(height - 1u - y) * width;
|
||||
for (unsigned x = 0; x < width; ++x)
|
||||
std::swap(topRow[x], bottomRow[x]);
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
const RuntimeTextTextureCache::Atlas* RuntimeTextTextureCache::FindAtlas(const std::string& fontId) const
|
||||
{
|
||||
for (const Atlas& atlas : mAtlases)
|
||||
{
|
||||
if (atlas.fontId == fontId)
|
||||
return &atlas;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ShaderParameterValue* RuntimeTextTextureCache::FindParameterValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId)
|
||||
{
|
||||
const auto valueIt = artifact.parameterValues.find(parameterId);
|
||||
return valueIt == artifact.parameterValues.end() ? nullptr : &valueIt->second;
|
||||
}
|
||||
|
||||
std::string RuntimeTextTextureCache::DefaultTextValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId)
|
||||
{
|
||||
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
|
||||
{
|
||||
if (definition.id == parameterId)
|
||||
return definition.defaultTextValue;
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
unsigned char RuntimeTextTextureCache::SampleAtlasAlpha(const Atlas& atlas, double x, double y)
|
||||
{
|
||||
const int ix = (std::max)(0, (std::min)(static_cast<int>(atlas.width) - 1, static_cast<int>(std::floor(x))));
|
||||
const int iy = (std::max)(0, (std::min)(static_cast<int>(atlas.height) - 1, static_cast<int>(std::floor(y))));
|
||||
const std::size_t pixelOffset = (static_cast<std::size_t>(iy) * atlas.width + static_cast<std::size_t>(ix)) * 4u;
|
||||
return atlas.rgbaPixels[pixelOffset + 3u];
|
||||
}
|
||||
|
||||
void RuntimeTextTextureCache::DestroyTexture(TextTexture& texture)
|
||||
{
|
||||
if (texture.texture != 0)
|
||||
glDeleteTextures(1, &texture.texture);
|
||||
texture.texture = 0;
|
||||
texture.width = 0;
|
||||
texture.height = 0;
|
||||
texture.cachedText.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user