engine/ext/behaviors/gui/glyph/behavior.cpp

564 lines
19 KiB
C++

#include "behavior.h"
#include <uf/utils/hook/hook.h>
#include <uf/utils/image/atlas.h>
#include <uf/utils/time/time.h>
#include <uf/utils/serialize/serializer.h>
#include <uf/utils/userdata/userdata.h>
#include <uf/utils/window/window.h>
#include <uf/utils/camera/camera.h>
#include <uf/utils/mesh/mesh.h>
#include <uf/utils/graphic/graphic.h>
#include <uf/utils/string/ext.h>
#include <uf/utils/math/physics.h>
#include <uf/utils/text/glyph.h>
#include <uf/engine/asset/asset.h>
#include <uf/engine/scene/scene.h>
#include <uf/utils/memory/unordered_map.h>
#include <uf/utils/renderer/renderer.h>
#include <uf/ext/openvr/openvr.h>
#include <uf/utils/http/http.h>
#include <uf/utils/audio/audio.h>
#include <uf/utils/window/payloads.h>
#include "../payload.h"
#include "../behavior.h"
#include "../manager/behavior.h"
#define EXT_COLOR_FLOATS 1
namespace {
struct Mesh {
pod::Vector3f position;
pod::Vector2f uv;
#if EXT_COLOR_FLOATS
pod::Vector4f color;
#else
pod::ColorRgba color;
#endif
static uf::stl::vector<uf::renderer::AttributeDescriptor> descriptor;
};
}
UF_VERTEX_DESCRIPTOR(Mesh,
UF_VERTEX_DESCRIPTION(Mesh, R32G32B32_SFLOAT, position)
UF_VERTEX_DESCRIPTION(Mesh, R32G32_SFLOAT, uv)
#if EXT_COLOR_FLOATS
UF_VERTEX_DESCRIPTION(Mesh, R32G32B32A32_SFLOAT, color)
#else
UF_VERTEX_DESCRIPTION(Mesh, R8G8B8A8_UNORM, color)
#endif
)
#include <regex>
#include <uf/utils/string/hash.h>
namespace {
const uint64_t COLOR_CTRL = 0x7F;
struct GlyphBox {
struct {
float x, y, w, h;
} box;
pod::Vector3f color;
uint64_t code;
};
struct {
ext::freetype::Glyph glyph;
uf::stl::unordered_map<uf::stl::string, uf::stl::unordered_map<size_t, uf::Glyph>> cache;
} glyphs;
struct {
uf::Serializer settings;
pod::Vector2ui size = { 1920, 1080 };
} defaults;
}
UF_BEHAVIOR_REGISTER_CPP(ext::GuiGlyphBehavior)
UF_BEHAVIOR_TRAITS_CPP(ext::GuiGlyphBehavior, ticks = true, renders = false, multithread = false)
#define this (&self)
namespace {
size_t hashGlyphSettings( uint64_t c, const ext::GuiGlyphBehavior::Metadata& metadata ) {
size_t seed{};
uf::hash( seed, c, metadata.padding[0], metadata.padding[1], metadata.spread, metadata.size, metadata.font, metadata.sdf );
return seed;
}
size_t hashGlyphSettings( const uf::stl::string& c, const ext::GuiGlyphBehavior::Metadata& metadata ) {
size_t seed{};
uf::hash( seed, c, metadata.padding[0], metadata.padding[1], metadata.spread, metadata.size, metadata.font, metadata.sdf );
return seed;
}
uf::stl::vector<::GlyphBox> generateGlyphs( uf::Object& self, const uf::stl::string& _string ) {
auto& glyphs = ::glyphs;
auto& metadata = this->getComponent<ext::GuiGlyphBehavior::Metadata>();
auto& metadataGui = this->getComponent<ext::GuiBehavior::Metadata>();
auto& transform = this->getComponent<pod::Transform<>>();
uf::stl::vector<::GlyphBox> gs;
auto string = _string == "" ? metadata.string : _string;
auto font = uf::io::root+"/fonts/" + metadata.font;
auto color = metadataGui.color;
auto origin = uf::stl::string("top");
auto align = uf::stl::string("center");
auto direction = uf::stl::string("down");
if ( glyphs.cache[font].empty() ) ext::freetype::initialize( glyphs.glyph, font );
struct {
struct {
float x = 0;
float y = 0;
} origin;
struct {
float x = 0;
float y = 0;
} cursor;
struct {
float sum = 0;
float len = 0;
float proc = 0;
float tab = 4;
} average;
struct {
float x = 0;
float y = 0;
} biggest;
struct {
float w = 0;
float h = 0;
} box;
struct {
uf::stl::vector<pod::Vector3f> container;
size_t index = 0;
} colors;
} stat;
stat.colors.container.push_back(color);
// grab escaped color markers: ${#RRGGBB}
{
uf::stl::unordered_map<size_t, pod::Vector3f> colors;
uf::stl::string text = string;
std::regex regex("\\$\\{\\#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})\\}");
std::smatch match;
bool matched = false;
int maxTries = 128;
while ( (matched = std::regex_search( text, match, regex )) && --maxTries > 0 ) {
struct {
uf::stl::string str;
int dec;
} r, g, b;
r.str = match[1].str();
g.str = match[2].str();
b.str = match[3].str();
{ uf::stl::stringstream stream; stream << r.str; stream >> std::hex >> r.dec; }
{ uf::stl::stringstream stream; stream << g.str; stream >> std::hex >> g.dec; }
{ uf::stl::stringstream stream; stream << b.str; stream >> std::hex >> b.dec; }
pod::Vector3f color = { r.dec / 255.0f, g.dec / 255.0f, b.dec / 255.0f };
stat.colors.container.push_back(color);
text = uf::string::replace( text, "${" + r.str + g.str + b.str + "}", "\x7F" );
}
if ( maxTries == 0 ) text += "\n(error formatting)";
string = text;
}
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> convert;
std::wstring str = convert.from_bytes(string);
if ( str.size() == 0 ) return gs;
// Calculate statistics
{
// Find tallest glyph for new line
for ( auto it = str.begin(); it != str.end(); ++it ) {
uint64_t c = *it; if ( c == '\n' ) continue; if ( c == '\t' ) continue; if ( c == 0x01 ) continue;
auto key = hashGlyphSettings( c, metadata );
auto& glyph = glyphs.cache[font][key];
if ( !glyph.generated() ) {
glyph.setPadding( { metadata.padding[0], metadata.padding[1] } );
glyph.setSpread( metadata.spread );
#if UF_USE_VULKAN
if ( metadata.sdf ) glyph.useSdf(true);
#else
glyph.useSdf(false);
#endif
glyph.generate( glyphs.glyph, c, metadata.size );
}
stat.biggest.x = std::max( (float) stat.biggest.x, (float) glyph.getSize().x);
stat.biggest.y = std::max( (float) stat.biggest.y, (float) glyph.getSize().y);
stat.average.sum += glyph.getSize().x;
++stat.average.len;
}
stat.average.proc = stat.average.sum / stat.average.len;
stat.average.tab *= stat.average.proc;
// Calculate box: Second pass required because of tab
stat.cursor.x = 0;
stat.cursor.y = 0;
stat.origin.x = 0;
stat.origin.y = 0;
for ( auto it = str.begin(); it != str.end(); ++it ) {
uint64_t c = *it; if ( c == '\n' ) {
stat.cursor.y += stat.biggest.y;
stat.cursor.x = 0;
continue;
} else if ( c == '\t' ) {
// Fixed movement vs Real Tabbing
if ( false ) {
stat.cursor.x += stat.average.tab;
} else {
stat.cursor.x = ((stat.cursor.x / stat.average.tab) + 1) * stat.average.tab;
}
continue;
} else if ( c == COLOR_CTRL ) {
continue;
}
auto key = hashGlyphSettings( c, metadata );
auto& glyph = glyphs.cache[font][key];
::GlyphBox g;
g.box.w = glyph.getSize().x;
g.box.h = glyph.getSize().y;
g.box.x = stat.cursor.x + glyph.getBearing().x;
g.box.y = stat.cursor.y - glyph.getBearing().y; // - (glyph.getSize().y - glyph.getBearing().y);
stat.cursor.x += (glyph.getAdvance().x);
}
stat.origin.x = transform.position.x * ::defaults.size.x; // ( !metadataGui.world && transform.position.x != (int) transform.position.x ) ? transform.position.x * ::defaults.size.x : transform.position.x;
stat.origin.y = transform.position.y * ::defaults.size.y; // ( !metadataGui.world && transform.position.y != (int) transform.position.y ) ? transform.position.y * ::defaults.size.y : transform.position.y;
if ( origin == "top" ) stat.origin.y = ::defaults.size.y - stat.origin.y - stat.biggest.y;// else stat.origin.y = stat.origin.y;
if ( align == "right" ) stat.origin.x = ::defaults.size.x - stat.origin.x - stat.box.w;// else stat.origin.x = stat.origin.x;
else if ( align == "center" )
stat.origin.x -= stat.box.w * 0.5f;
}
// Render Glyphs
stat.cursor.x = 0;
stat.cursor.y = stat.biggest.y;
for ( auto it = str.begin(); it != str.end(); ++it ) {
uint64_t c = *it; if ( c == '\n' ) {
if ( direction == "down" ) stat.cursor.y += stat.biggest.y; else stat.cursor.y -= stat.biggest.y;
stat.cursor.x = 0;
continue;
} else if ( c == '\t' ) {
// Fixed movement vs Real Tabbing
if ( false ) {
stat.cursor.x += stat.average.tab;
} else {
stat.cursor.x = ((stat.cursor.x / stat.average.tab) + 1) * stat.average.tab;
}
continue;
} else if ( c == ' ' ) {
stat.cursor.x += stat.average.tab / 4.0f;
continue;
} else if ( c == COLOR_CTRL ) {
++stat.colors.index;
continue;
}
auto key = hashGlyphSettings( c, metadata );
auto& glyph = glyphs.cache[font][key];
::GlyphBox g;
g.code = c;
g.box.w = glyph.getSize().x;
g.box.h = glyph.getSize().y;
g.box.x = stat.cursor.x + (glyph.getBearing().x);
g.box.y = stat.cursor.y - glyph.getBearing().y; // - (glyph.getSize().y - glyph.getBearing().y);
stat.cursor.x += (glyph.getAdvance().x);
if ( stat.colors.index < stat.colors.container.size() ) {
g.color = stat.colors.container.at(stat.colors.index);
} else {
g.color = metadataGui.color;
}
g.box.x /= ::defaults.size.x;
g.box.w /= ::defaults.size.x;
g.box.y /= ::defaults.size.y;
g.box.h /= ::defaults.size.y;
gs.push_back(g);
}
return gs;
}
}
void ext::GuiGlyphBehavior::initialize( uf::Object& self ) {
auto& metadata = this->getComponent<ext::GuiGlyphBehavior::Metadata>();
auto& metadataGui = this->getComponent<ext::GuiBehavior::Metadata>();
auto& metadataJson = this->getComponent<uf::Serializer>();
/*
if ( ext::json::isNull( ::defaults.settings["metadata"] ) ) ::defaults.settings.readFromFile(uf::io::root+"/entities/gui/text/string.json");
// set defaults
if ( metadataJson["string"].is<uf::stl::string>() ) {
auto copyMetadataJson = metadataJson;
ext::json::forEach(::defaults.settings["metadata"], [&]( const uf::stl::string& key, ext::json::Value& value ){
if ( ext::json::isNull( copyMetadataJson[key] ) ) {
metadataJson[key] = value;
}
});
}
*/
// if ( metadataJson["scaling"] == "auto" ) metadataJson["scaling"] = "none";
this->addHook( "gui:UpdateText.%UID%", [&](ext::json::Value& payload){
auto string = payload["string"].as(metadata.string);
auto font = uf::io::root+"/fonts/" + payload["font"].as(metadata.font);
bool forced = payload["force"].as(false);
auto glyphs = ::generateGlyphs( self, string );
auto& scene = uf::scene::getCurrentScene();
auto& mesh = this->getComponent<uf::Mesh>();
auto& atlas = this->getComponent<uf::Atlas>();
auto& images = atlas.getImages();
auto& glyphs_cache = ::glyphs.cache;
uf::stl::unordered_map<size_t, uf::stl::string> glyph_atlas_map;
metadataGui.scaleMode = "none";
// generate texture
{
atlas.clear();
for ( auto& g : glyphs ) {
auto key = ::hashGlyphSettings( g.code, metadata );
auto& glyph = glyphs_cache[font][key];
const uint8_t* buffer = glyph.getBuffer();
uf::Image::container_t pixels;
size_t len = glyph.getSize().x * glyph.getSize().y;
if ( metadata.sdf ) {
glyph_atlas_map[key] = atlas.addImage( glyph.getBuffer(), glyph.getSize(), 8, 1, true );
} else {
#if 1 || UF_ENV_DREAMCAST
pixels.reserve( len * 4 );
for ( size_t i = 0; i < len; ++i ) {
pixels.emplace_back( buffer[i] );
pixels.emplace_back( buffer[i] );
pixels.emplace_back( buffer[i] );
pixels.emplace_back( buffer[i] );
// pixels.emplace_back( buffer[i] > 127 ? 255 : 0 );
}
glyph_atlas_map[key] = atlas.addImage( &pixels[0], glyph.getSize(), 8, 4, true );
#else
pixels.reserve( len * 2 );
for ( size_t i = 0; i < len; ++i ) {
pixels.emplace_back( buffer[i] );
pixels.emplace_back( buffer[i] );
// pixels.emplace_back( buffer[i] > 127 ? 255 : 0 );
}
glyph_atlas_map[key] = atlas.addImage( &pixels[0], glyph.getSize(), 8, 2, true );
#endif
}
}
atlas.generate();
atlas.clear(false);
}
// generate mesh
{
mesh.destroy();
mesh.bind<::Mesh, uint16_t>();
uf::stl::vector<::Mesh> vertices; vertices.reserve( glyphs.size() * 6 );
uf::stl::vector<uint16_t> indices; indices.reserve( glyphs.size() * 6 );
// pod::Vector2f min = { 0, 0 };
// pod::Vector2f max = { 0, 0 };
for ( auto& g : glyphs ) {
auto key = ::hashGlyphSettings( g.code, metadata );
auto& glyph = glyphs_cache[font][key];
auto hash = glyph_atlas_map[key];
#if EXT_COLOR_FLOATS
auto& color = g.color;
#else
pod::ColorRgba color = { g.color[0] * 255, g.color[1] * 255, g.color[2] * 255, g.color[3] * 255 };
#endif
// add vertices
vertices.emplace_back( ::Mesh{pod::Vector3f{ g.box.x, g.box.y + g.box.h, 0 }, atlas.mapUv( pod::Vector2f{ 0.0f, 0.0f }, hash ), color}); indices.emplace_back( indices.size() );
vertices.emplace_back( ::Mesh{pod::Vector3f{ g.box.x, g.box.y , 0 }, atlas.mapUv( pod::Vector2f{ 0.0f, 1.0f }, hash ), color}); indices.emplace_back( indices.size() );
vertices.emplace_back( ::Mesh{pod::Vector3f{ g.box.x + g.box.w, g.box.y , 0 }, atlas.mapUv( pod::Vector2f{ 1.0f, 1.0f }, hash ), color}); indices.emplace_back( indices.size() );
vertices.emplace_back( ::Mesh{pod::Vector3f{ g.box.x, g.box.y + g.box.h, 0 }, atlas.mapUv( pod::Vector2f{ 0.0f, 0.0f }, hash ), color}); indices.emplace_back( indices.size() );
vertices.emplace_back( ::Mesh{pod::Vector3f{ g.box.x + g.box.w, g.box.y , 0 }, atlas.mapUv( pod::Vector2f{ 1.0f, 1.0f }, hash ), color}); indices.emplace_back( indices.size() );
vertices.emplace_back( ::Mesh{pod::Vector3f{ g.box.x + g.box.w, g.box.y + g.box.h, 0 }, atlas.mapUv( pod::Vector2f{ 1.0f, 0.0f }, hash ), color}); indices.emplace_back( indices.size() );
}
/*
pod::Vector2f min = { std::numeric_limits<float>::max(), std::numeric_limits<float>::max() };
pod::Vector2f max = { -std::numeric_limits<float>::max(), -std::numeric_limits<float>::max() };
for ( auto& vertex : vertices ) {
min.x = std::min( min.x, vertex.position.x * ::defaults.size.x * transform.scale.x );
min.y = std::min( min.y, vertex.position.y * ::defaults.size.y * transform.scale.y );
max.x = std::max( max.x, vertex.position.x * ::defaults.size.x * transform.scale.x );
max.y = std::max( max.y, vertex.position.y * ::defaults.size.y * transform.scale.y );
#if UF_USE_OPENGL
vertex.position.y = -vertex.position.y;
#endif
}
metadataGui.size = {
max.x - min.x,
max.y - min.y,
};
*/
mesh.insertVertices( vertices );
mesh.insertIndices( indices );
// mesh.resizeVertices( metadata.reserve * 6 );
// mesh.resizeIndices( metadata.reserve * 6 );
}
if ( metadata.sdf ) {
metadataJson["shaders"]["vertex"] = uf::io::root+"/shaders/gui/text/vert.spv";
metadataJson["shaders"]["fragment"] = uf::io::root+"/shaders/gui/text/frag.spv";
}
// fire image update
{
ext::payloads::GuiInitializationPayload payload;
payload.image = &atlas.getAtlas();
payload.mesh = &mesh;
payload.free = false;
this->callHook( "gui:Update.%UID%", payload );
}
});
UF_BEHAVIOR_METADATA_BIND_SERIALIZER_HOOKS(metadata, metadataJson);
}
void ext::GuiGlyphBehavior::tick( uf::Object& self ) {
if ( !this->hasComponent<uf::Graphic>() ) return;
#if !UF_USE_OPENGL
auto& transform = this->getComponent<pod::Transform<>>();
auto& metadata = this->getComponent<ext::GuiGlyphBehavior::Metadata>();
auto& metadataJson = this->getComponent<uf::Serializer>();
auto& mesh = this->getComponent<uf::Mesh>();
auto& graphic = this->getComponent<uf::Graphic>();
auto model = uf::matrix::identity();
auto& scene = uf::scene::getCurrentScene();
auto& controller = scene.getController();
auto& camera = controller.getComponent<uf::Camera>();
// bind UBO
if ( graphic.material.hasShader("vertex") ) {
auto& shader = graphic.material.getShader("vertex");
if ( shader.hasUniform("UBO_Glyph") ) {
auto& uniformBuffer = shader.getUniformBuffer("UBO_Glyph");
struct Glyph {
/*alignas(16)*/ pod::Vector4f stroke;
/*alignas(8)*/ pod::Vector2i range;
/*alignas(4)*/ int32_t spread;
/*alignas(4)*/ float weight;
/*alignas(4)*/ float scale;
/*alignas(4)*/ float padding1;
/*alignas(4)*/ float padding2;
/*alignas(4)*/ float padding3;
} ubo = {
.stroke = metadata.shader.stroke,
.range = metadata.shader.range,
.spread = metadata.spread,
.weight = metadata.shader.weight,
.scale = metadata.shader.scale,
};
// if ( metadataGlyph.sdf ) uniforms.gui.mode &= 1 << 1;
// if ( metadataGlyph.shadowbox ) uniforms.gui.mode &= 1 << 2;
shader.updateBuffer( (const void*) &ubo, sizeof(ubo), uniformBuffer );
}
}
#endif
}
void ext::GuiGlyphBehavior::render( uf::Object& self ){}
void ext::GuiGlyphBehavior::destroy( uf::Object& self ){}
#undef this
void ext::GuiGlyphBehavior::Metadata::serialize( uf::Object& self, uf::Serializer& serializer ){
serializer["string"] = /*this->*/string;
serializer["font"] = /*this->*/font;
serializer["sdf"] = /*this->*/sdf;
serializer["spread"] = /*this->*/spread;
serializer["padding"] = uf::vector::encode( /*this->*/padding);
serializer["scale"] = /*this->*/shader.scale;
serializer["weight"] = /*this->*/shader.weight;
serializer["stroke"] = uf::vector::encode( /*this->*/shader.stroke);
serializer["range"] = uf::vector::encode( /*this->*/shader.range);
}
void ext::GuiGlyphBehavior::Metadata::deserialize( uf::Object& self, uf::Serializer& serializer ){
size_t oldHash = ::hashGlyphSettings( string, *this );
/*this->*/string = serializer["string"].as(/*this->*/string);
/*this->*/font = serializer["font"].as(/*this->*/font);
/*this->*/sdf = serializer["sdf"].as(/*this->*/sdf);
/*this->*/spread = serializer["spread"].as(/*this->*/spread);
/*this->*/padding = uf::vector::decode(serializer["padding"], /*this->*/padding);
/*this->*/shader.scale = serializer["scale"].as(/*this->*/shader.scale);
/*this->*/shader.weight = serializer["weight"].as(/*this->*/shader.weight);
/*this->*/shader.stroke = uf::vector::decode(serializer["stroke"], /*this->*/shader.stroke);
/*this->*/shader.range = uf::vector::decode(serializer["range"], /*this->*/shader.range);
size_t newHash = ::hashGlyphSettings( string, *this );
// fire text update
if ( oldHash != newHash ) {
ext::json::Value payload;
payload["string"] = /*this->*/string;
self.callHook("gui:UpdateText.%UID%", payload);
}
}