diff --git a/docs/examples.rst b/docs/examples.rst index ddbaa94b7..c1071a3d4 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -710,3 +710,17 @@ Reference(s): Demonstrate running bgfx in headless mode. Initialize bgfx without window, render into frame buffer, and output result into an image. + +`51-gpufont `__ +---------------------------------------------------------------------------------- + +GPU font rendering using the Slug algorithm. + +Slug reference shaders by Eric Lengyel (MIT License). +https://github.com/EricLengyel/Slug?tab=readme-ov-file#slug + +For more information see: +https://sluglibrary.com/ + +.. figure:: https://github.com/bkaradzic/bgfx/raw/master/examples/51-gpufont/screenshot.png + :alt: example-51-gpufont diff --git a/examples/51-gpufont/fs_slug.sc b/examples/51-gpufont/fs_slug.sc new file mode 100644 index 000000000..12d9fcf81 --- /dev/null +++ b/examples/51-gpufont/fs_slug.sc @@ -0,0 +1,254 @@ +$input v_texcoord0, v_banding, v_glyph + +// =================================================== +// Reference pixel shader for the Slug algorithm. +// This code is made available under the MIT License. +// Copyright 2017, by Eric Lengyel. +// =================================================== + +#include "../common/common.sh" + +// The curve and band textures use a fixed width of 4096 texels. + +#define kLogBandTextureWidth 12 + +SAMPLER2D(s_curveData, 0); +SAMPLER2D(s_bandData, 1); + +uniform vec4 u_params; + +int calcRootCode(float _y1, float _y2, float _y3) +{ + // Calculate the root eligibility code for a sample-relative quadratic Bezier curve. + // Extract the signs of the y coordinates of the three control points. + + int i1 = (_y1 < 0.0) ? 1 : 0; + int i2 = (_y2 < 0.0) ? 2 : 0; + int i3 = (_y3 < 0.0) ? 4 : 0; + int shift = i1 + i2 + i3; + + // Eligibility is returned in bits 0 and 8. + + return (0x2E74 >> shift) & 0x0101; +} + +vec2 solveHorizPoly(vec4 _p12, vec2 _p3) +{ + // Solve for the values of t where the curve crosses y = 0. + // The quadratic polynomial in t is given by + // + // a t^2 - 2b t + c, + // + // where a = p1.y - 2 p2.y + p3.y, b = p1.y - p2.y, and c = p1.y. + // The discriminant b^2 - ac is clamped to zero, and imaginary + // roots are treated as a double root at the global minimum + // where t = b / a. + + vec2 a = _p12.xy - _p12.zw * 2.0 + _p3; + vec2 b = _p12.xy - _p12.zw; + float ra = 1.0 / a.y; + float rb = 0.5 / b.y; + + float d = sqrt(max(b.y * b.y - a.y * _p12.y, 0.0) ); + float t1 = (b.y - d) * ra; + float t2 = (b.y + d) * ra; + + // If the polynomial is nearly linear, then solve -2b t + c = 0. + + if (abs(a.y) < 1.0 / 65536.0) + { + t1 = t2 = _p12.y * rb; + } + + // Return the x coordinates where C(t) = 0. + + return vec2( (a.x * t1 - b.x * 2.0) * t1 + _p12.x, (a.x * t2 - b.x * 2.0) * t2 + _p12.x); +} + +vec2 solveVertPoly(vec4 _p12, vec2 _p3) +{ + // Solve for the values of t where the curve crosses x = 0. + + vec2 a = _p12.xy - _p12.zw * 2.0 + _p3; + vec2 b = _p12.xy - _p12.zw; + float ra = 1.0 / a.x; + float rb = 0.5 / b.x; + + float d = sqrt(max(b.x * b.x - a.x * _p12.x, 0.0) ); + float t1 = (b.x - d) * ra; + float t2 = (b.x + d) * ra; + + if (abs(a.x) < 1.0 / 65536.0) t1 = t2 = _p12.x * rb; + + return vec2( + (a.y * t1 - b.y * 2.0) * t1 + _p12.y + , (a.y * t2 - b.y * 2.0) * t2 + _p12.y + ); +} + +ivec2 calcBandLoc(ivec2 glyphLoc, int offset) +{ + // If the offset causes the x coordinate to exceed the texture width, then wrap to the next line. + + ivec2 bandLoc = ivec2(glyphLoc.x + offset, glyphLoc.y); + bandLoc.y += bandLoc.x >> kLogBandTextureWidth; + bandLoc.x &= (1 << kLogBandTextureWidth) - 1; + return bandLoc; +} + +float calcCoverage(float xcov, float ycov, float xwgt, float ywgt) +{ + // Combine coverages from the horizontal and vertical rays using their weights. + // Absolute values ensure that either winding direction convention works. + + float coverage = max(abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0 / 65536.0), min(abs(xcov), abs(ycov) )); + + // Using nonzero fill rule here. + + coverage = clamp(coverage, 0.0, 1.0); + return coverage; +} + +float slugRender(vec2 _renderCoord, vec4 _bandTransform, ivec2 _glyphLoc, ivec2 _bandMax) +{ + int curveIndex; + + // The effective pixel dimensions of the em square are computed + // independently for x and y directions with texcoord derivatives. + + vec2 emsPerPixel = fwidth(_renderCoord); + vec2 pixelsPerEm = 1.0 / emsPerPixel; + + // Determine what bands the current pixel lies in by applying a scale and offset + // to the render coordinates. The scales are given by _bandTransform.xy, and the + // offsets are given by _bandTransform.zw. Band indexes are clamped to [0, _bandMax.xy]. + + ivec2 bandIndex = clamp(ivec2(_renderCoord * _bandTransform.xy + _bandTransform.zw), ivec2(0, 0), _bandMax); + + float xcov = 0.0; + float xwgt = 0.0; + + // Fetch data for the horizontal band from the index texture. The number + // of curves intersecting the band is in the x component, and the offset + // to the list of locations for those curves is in the y component. + + vec4 hbandRaw = texelFetch(s_bandData, ivec2(_glyphLoc.x + bandIndex.y, _glyphLoc.y), 0); + int hbandCount = int(hbandRaw.x + 0.5); + int hbandOffset = int(hbandRaw.y + 0.5); + ivec2 hbandLoc = calcBandLoc(_glyphLoc, hbandOffset); + + // Loop over all curves in the horizontal band. + + for (curveIndex = 0; curveIndex < hbandCount; curveIndex++) + { + // Fetch the location of the current curve from the index texture. + + vec4 locRaw = texelFetch(s_bandData, ivec2(hbandLoc.x + curveIndex, hbandLoc.y), 0); + ivec2 curveLoc = ivec2(int(locRaw.x + 0.5), int(locRaw.y + 0.5) ); + + // Fetch the three 2D control points for the current curve from the curve texture. + // The first texel contains both p1 and p2 in the (x,y) and (z,w) components, respectively, + // and the the second texel contains p3 in the (x,y) components. Subtracting the render + // coordinates makes the curve relative to the sample position. The quadratic Bezier curve + // C(t) is given by + // + // C(t) = (1 - t)^2 p1 + 2t(1 - t) p2 + t^2 p3 + + vec4 p12 = texelFetch(s_curveData, curveLoc, 0) - vec4(_renderCoord, _renderCoord); + vec2 p3 = texelFetch(s_curveData, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - _renderCoord; + + // If the largest x coordinate among all three control points falls + // left of the current pixel, then there are no more curves in the + // horizontal band that can influence the result, so exit the loop. + // (The curves are sorted in descending order by max x coordinate.) + + if (max(max(p12.x, p12.z), p3.x) * pixelsPerEm.x < -0.5) break; + + int code = calcRootCode(p12.y, p12.w, p3.y); + if (code != 0) + { + vec2 r = solveHorizPoly(p12, p3) * pixelsPerEm.x; + + if ( (code & 1) != 0) + { + xcov += clamp(r.x + 0.5, 0.0, 1.0); + xwgt = max(xwgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0) ); + } + + if (code > 1) + { + xcov -= clamp(r.y + 0.5, 0.0, 1.0); + xwgt = max(xwgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0) ); + } + } + } + + float ycov = 0.0; + float ywgt = 0.0; + + // Fetch data for the vertical band from the index texture. This follows + // the data for all horizontal bands, so we have to add _bandMax.y + 1. + + vec4 vbandRaw = texelFetch(s_bandData, ivec2(_glyphLoc.x + _bandMax.y + 1 + bandIndex.x, _glyphLoc.y), 0); + int vbandCount = int(vbandRaw.x + 0.5); + int vbandOffset = int(vbandRaw.y + 0.5); + ivec2 vbandLoc = calcBandLoc(_glyphLoc, vbandOffset); + + // Loop over all curves in the vertical band. + + for (curveIndex = 0; curveIndex < vbandCount; curveIndex++) + { + vec4 locRaw = texelFetch(s_bandData, ivec2(vbandLoc.x + curveIndex, vbandLoc.y), 0); + ivec2 curveLoc = ivec2(int(locRaw.x + 0.5), int(locRaw.y + 0.5) ); + + vec4 p12 = texelFetch(s_curveData, curveLoc, 0) - vec4(_renderCoord, _renderCoord); + vec2 p3 = texelFetch(s_curveData, ivec2(curveLoc.x + 1, curveLoc.y), 0).xy - _renderCoord; + + if (max(max(p12.y, p12.w), p3.y) * pixelsPerEm.y < -0.5) break; + + int code = calcRootCode(p12.x, p12.z, p3.x); + if (code != 0) + { + vec2 r = solveVertPoly(p12, p3) * pixelsPerEm.y; + + if ( (code & 1) != 0) + { + ycov -= clamp(r.x + 0.5, 0.0, 1.0); + ywgt = max(ywgt, clamp(1.0 - abs(r.x) * 2.0, 0.0, 1.0) ); + } + + if (code > 1) + { + ycov += clamp(r.y + 0.5, 0.0, 1.0); + ywgt = max(ywgt, clamp(1.0 - abs(r.y) * 2.0, 0.0, 1.0) ); + } + } + } + + return calcCoverage(xcov, ycov, xwgt, ywgt); +} + +vec4 unpackRGBA8(float _packed) +{ + uint rgba = floatBitsToUint(_packed); + return vec4( + (rgba >> 24) & 0xffu + , (rgba >> 16) & 0xffu + , (rgba >> 8) & 0xffu + , (rgba ) & 0xffu + ) / 255.0 + ; +} + +void main() +{ + ivec2 glyphLoc = ivec2(v_glyph.xy); + ivec2 bandMax = ivec2(v_glyph.zw); + bandMax.y &= 0x00FF; + + float coverage = slugRender(v_texcoord0, v_banding, glyphLoc, bandMax); + vec4 fg = unpackRGBA8(u_params.x); + vec4 bg = unpackRGBA8(u_params.y); + + gl_FragColor = mix(bg, fg, coverage); +} diff --git a/examples/51-gpufont/gpufont.cpp b/examples/51-gpufont/gpufont.cpp new file mode 100644 index 000000000..52883f0f7 --- /dev/null +++ b/examples/51-gpufont/gpufont.cpp @@ -0,0 +1,1427 @@ +/* + * Copyright 2011-2026 Branimir Karadzic. All rights reserved. + * License: https://github.com/bkaradzic/bgfx/blob/master/LICENSE + */ + +// GPU font rendering using the Slug algorithm. +// +// Slug reference shaders by Eric Lengyel (MIT License). +// https://github.com/EricLengyel/Slug?tab=readme-ov-file#slug +// +// For more information see: +// https://sluglibrary.com/ + +#include "common.h" +#include "bgfx_utils.h" +#include "imgui/imgui.h" +#include "font/utf8.h" + +#include +#include +#include +#include + +#include +#include +namespace stl = tinystl; + +#include + +namespace +{ + +static constexpr uint16_t kTexWidth = 4096; +static constexpr uint8_t kMaxBands = 8; + +struct Curve +{ + float p1x, p1y; + float p2x, p2y; + float p3x, p3y; +}; + +struct Rect +{ + float minX; + float minY; + float maxX; + float maxY; +}; + +struct SlugGlyph +{ + int32_t glyphIndex; + int32_t advance; + int32_t bearingX; + int32_t bearingY; + int32_t width; + int32_t height; + + Rect rect; + + int32_t curveStart; + int32_t curveCount; + + uint16_t glyphLocX; + uint16_t glyphLocY; + uint8_t bandMaxX; + uint8_t bandMaxY; + + float bandScaleX; + float bandScaleY; + float bandOffsetX; + float bandOffsetY; +}; + +struct FontVertex +{ + float m_x; // pos.xy = object-space vertex coordinates. + float m_y; // + float m_nx; // pos.zw = object-space normal vector. + float m_ny; // + + float m_u; // tex.xy = em-space sample coordinates. + float m_v; // + float m_glyphLoc; // tex.z = location of glyph data in band texture / Bit-packed: (glyphLocY << 16) | glyphLocX, reinterpreted as float. + float m_bandMax; // tex.w = max band indexes and flags / Bit-packed: (bandMaxY << 16) | bandMaxX, reinterpreted as float. + + float m_bandScaleX; // banding + float m_bandScaleY; + float m_bandOffsetX; + float m_bandOffsetY; + + static void init() + { + ms_layout.begin() + .add(bgfx::Attrib::Position, 4, bgfx::AttribType::Float) // pos.xy = object-space vertex coordinates. + // pos.zw = object-space normal vector. + .add(bgfx::Attrib::TexCoord0, 4, bgfx::AttribType::Float) // tex.xy = em-space sample coordinates. + // tex.z = location of glyph data in band texture (interpreted as integer): + // tex.w = max band indexes and flags (interpreted as integer): + .add(bgfx::Attrib::TexCoord1, 4, bgfx::AttribType::Float) // banding + .end(); + } + + static bgfx::VertexLayout ms_layout; +}; + +bgfx::VertexLayout FontVertex::ms_layout; + +class SlugFont +{ +public: + float worldSize; + + SlugFont(const uint8_t* _fontData, float _worldSize) + : worldSize(_worldSize) + , m_curveTex(BGFX_INVALID_HANDLE) + , m_bandTex(BGFX_INVALID_HANDLE) + , m_vbh(BGFX_INVALID_HANDLE) + , m_ibh(BGFX_INVALID_HANDLE) + { + if (!stbtt_InitFont(&m_fontInfo, _fontData, stbtt_GetFontOffsetForIndex(_fontData, 0) ) ) + { + return; + } + + float scale1px = stbtt_ScaleForMappingEmToPixels(&m_fontInfo, 1.0f); + m_emSize = 1.0f / scale1px; + + buildGlyph(0, 0); // Undefined glyph at index 0. + + for (uint32_t codePoint = 32; codePoint < 128; ++codePoint) + { + const int32_t glyphIdx = stbtt_FindGlyphIndex(&m_fontInfo, codePoint); + + if (0 != glyphIdx) + { + buildGlyph(codePoint, glyphIdx); + } + + } + + uploadTextures(); + } + + ~SlugFont() + { + if (bgfx::isValid(m_curveTex) ) + { + bgfx::destroy(m_curveTex); + m_curveTex = BGFX_INVALID_HANDLE; + } + + if (bgfx::isValid(m_bandTex) ) + { + bgfx::destroy(m_bandTex); + m_bandTex = BGFX_INVALID_HANDLE; + } + + destroyMesh(); + } + + void destroyMesh() + { + if (bgfx::isValid(m_vbh) ) + { + bgfx::destroy(m_vbh); + m_vbh = BGFX_INVALID_HANDLE; + } + + if (bgfx::isValid(m_ibh) ) + { + bgfx::destroy(m_ibh); + m_ibh = BGFX_INVALID_HANDLE; + } + } + + void prepareGlyphsForText(const bx::StringView& _text) + { + bool changed = false; + + uint32_t state = 0; + + for (const char* textIt = _text.getPtr(); textIt != _text.getTerm(); ++textIt) + { + uint32_t codePoint; + if (0 != utf8_decode(&state, &codePoint, *textIt) ) + { + continue; + } + + if ('\r' == codePoint + || '\n' == codePoint) + { + continue; + } + + if (m_glyphs.find(codePoint) != m_glyphs.end() ) + { + continue; + } + + const int32_t glyphIdx = stbtt_FindGlyphIndex(&m_fontInfo, codePoint); + if (0 != glyphIdx) + { + buildGlyph(codePoint, glyphIdx); + changed = true; + } + } + + BX_ASSERT(state == UTF8_ACCEPT, "The string is not well-formed"); + + if (changed) + { + uploadTextures(); + } + } + + void buildMesh(float _x, float _y, const bx::StringView& _text) + { + destroyMesh(); + + float originalX = _x; + + stl::vector vertices; + stl::vector indices; + + uint32_t state = 0; + + int32_t previous = 0; + for (const char* textIt = _text.getPtr(); textIt != _text.getTerm(); ++textIt) + { + uint32_t codePoint; + if (0 != utf8_decode(&state, &codePoint, *textIt) ) + { + continue; + } + + if ('\r' == codePoint) + { + continue; + } + + if ('\n' == codePoint) + { + _x = originalX; + int32_t ascent, descent, lineGap; + stbtt_GetFontVMetrics(&m_fontInfo, &ascent, &descent, &lineGap); + float lineHeight = (float)(ascent - descent + lineGap) / m_emSize * worldSize; + _y -= lineHeight; + continue; + } + + auto glyphIt = m_glyphs.find(codePoint); + SlugGlyph& glyph = (glyphIt == m_glyphs.end() ) ? m_glyphs[0] : glyphIt->second; + + if (previous != 0 && glyph.glyphIndex != 0) + { + int32_t kern = stbtt_GetGlyphKernAdvance(&m_fontInfo, previous, glyph.glyphIndex); + _x += (float)kern / m_emSize * worldSize; + } + + if (glyph.curveCount > 0) + { + const float u0 = glyph.rect.minX; + const float v0 = glyph.rect.minY; + const float u1 = glyph.rect.maxX; + const float v1 = glyph.rect.maxY; + + const float x0 = _x + u0 * worldSize; + const float y0 = _y + v0 * worldSize; + const float x1 = _x + u1 * worldSize; + const float y1 = _y + v1 * worldSize; + + static constexpr float kN = 1.0f; + + uint32_t base = (uint32_t)vertices.size(); + + FontVertex vert; + vert.m_bandScaleX = glyph.bandScaleX; + vert.m_bandScaleY = glyph.bandScaleY; + vert.m_bandOffsetX = glyph.bandOffsetX; + vert.m_bandOffsetY = glyph.bandOffsetY; + { + uint32_t loc = (uint32_t(glyph.glyphLocY) << 16) | uint32_t(glyph.glyphLocX); + uint32_t bm = (uint32_t(glyph.bandMaxY) << 16) | uint32_t(glyph.bandMaxX); + bx::memCopy(&vert.m_glyphLoc, &loc, sizeof(float)); + bx::memCopy(&vert.m_bandMax, &bm, sizeof(float)); + } + + vert.m_x = x0; + vert.m_y = y0; + vert.m_u = u0; + vert.m_v = v0; + vert.m_nx = -kN; + vert.m_ny = -kN; + vertices.push_back(vert); + + vert.m_x = x1; + vert.m_y = y0; + vert.m_u = u1; + vert.m_v = v0; + vert.m_nx = +kN; + vert.m_ny = -kN; + vertices.push_back(vert); + + vert.m_x = x1; + vert.m_y = y1; + vert.m_u = u1; + vert.m_v = v1; + vert.m_nx = +kN; + vert.m_ny = +kN; + vertices.push_back(vert); + + vert.m_x = x0; + vert.m_y = y1; + vert.m_u = u0; + vert.m_v = v1; + vert.m_nx = -kN; + vert.m_ny = +kN; + vertices.push_back(vert); + + indices.push_back(base + 0); + indices.push_back(base + 1); + indices.push_back(base + 2); + indices.push_back(base + 2); + indices.push_back(base + 3); + indices.push_back(base + 0); + } + + _x += float(glyph.advance) / m_emSize * worldSize; + previous = glyph.glyphIndex; + } + + BX_ASSERT(state == UTF8_ACCEPT, "The string is not well-formed"); + + if (!indices.empty() ) + { + const bgfx::Memory* vmem = bgfx::alloc( (uint32_t)(vertices.size() * sizeof(FontVertex)) ); + bx::memCopy(vmem->data, vertices.data(), vmem->size); + + const bgfx::Memory* imem = bgfx::alloc( (uint32_t)(indices.size() * sizeof(uint32_t)) ); + bx::memCopy(imem->data, indices.data(), imem->size); + + m_vbh = bgfx::createVertexBuffer(vmem, FontVertex::ms_layout); + m_ibh = bgfx::createIndexBuffer(imem, BGFX_BUFFER_INDEX32); + } + } + + void submit( + bgfx::ViewId _view + , bgfx::ProgramHandle _program + , bgfx::UniformHandle _curveSampler + , bgfx::UniformHandle _bandSampler + , bgfx::UniformHandle _paramsUniform + ) + { + const float params[] = + { + bx::bitCast(0xffffffff), + bx::bitCast(0x303030ff), + worldSize, + 1.0f / worldSize, + }; + + bgfx::setVertexBuffer(0, m_vbh); + bgfx::setIndexBuffer(m_ibh); + + bgfx::setTexture(0, _curveSampler, m_curveTex); + bgfx::setTexture(1, _bandSampler, m_bandTex); + + bgfx::setUniform(_paramsUniform, params); + + bgfx::setState(0 + | BGFX_STATE_WRITE_RGB + | BGFX_STATE_WRITE_A + | BGFX_STATE_DEPTH_TEST_ALWAYS + | BGFX_STATE_BLEND_FUNC(BGFX_STATE_BLEND_SRC_ALPHA, BGFX_STATE_BLEND_INV_SRC_ALPHA) + ); + + bgfx::submit(_view, _program); + } + + Rect measure(float _x, float _y, const bx::StringView& _text) + { + Rect bb; + bb.minX = bx::max(); + bb.minY = bx::max(); + bb.maxX = bx::min(); + bb.maxY = bx::min(); + + float originalX = _x; + + uint32_t state = 0; + int32_t previous = 0; + for (const char* textIt = _text.getPtr(); textIt != _text.getTerm(); ++textIt) + { + uint32_t codePoint; + if (0 != utf8_decode(&state, &codePoint, *textIt) ) + { + continue; + } + + if (codePoint == '\r') + { + continue; + } + + if (codePoint == '\n') + { + _x = originalX; + int32_t ascent, descent, lineGap; + stbtt_GetFontVMetrics(&m_fontInfo, &ascent, &descent, &lineGap); + const float lineHeight = float(ascent - descent + lineGap) / m_emSize * worldSize; + _y -= lineHeight; + continue; + } + + auto glyphIt = m_glyphs.find(codePoint); + SlugGlyph& glyph = (glyphIt == m_glyphs.end() ) ? m_glyphs[0] : glyphIt->second; + + if (0 != previous + && 0 != glyph.glyphIndex) + { + const int32_t kern = stbtt_GetGlyphKernAdvance(&m_fontInfo, previous, glyph.glyphIndex); + _x += float(kern) / m_emSize * worldSize; + } + + const float x0 = _x + glyph.rect.minX * worldSize; + const float y0 = _y + glyph.rect.minY * worldSize; + const float x1 = _x + glyph.rect.maxX * worldSize; + const float y1 = _y + glyph.rect.maxY * worldSize; + + bb.minX = bx::min(bb.minX, x0); + bb.minY = bx::min(bb.minY, y0); + bb.maxX = bx::max(bb.maxX, x1); + bb.maxY = bx::max(bb.maxY, y1); + + _x += (float)glyph.advance / m_emSize * worldSize; + previous = glyph.glyphIndex; + } + + return bb; + } + +private: + void buildGlyph(uint32_t _charcode, int32_t _glyphIndex) + { + SlugGlyph glyph; + glyph.glyphIndex = _glyphIndex; + glyph.curveStart = (int32_t)m_curves.size(); + + stbtt_vertex* stbVerts = NULL; + int32_t numVerts = stbtt_GetGlyphShape(&m_fontInfo, _glyphIndex, &stbVerts); + + if (0 < numVerts) + { + convertOutline(stbVerts, numVerts); + stbtt_FreeShape(&m_fontInfo, stbVerts); + } + + glyph.curveCount = int32_t(m_curves.size() ) - glyph.curveStart; + + int32_t advanceWidth, leftSideBearing; + stbtt_GetGlyphHMetrics(&m_fontInfo, _glyphIndex, &advanceWidth, &leftSideBearing); + glyph.advance = advanceWidth; + + int32_t x0, y0, x1, y1; + if (!stbtt_GetGlyphBox(&m_fontInfo, _glyphIndex, &x0, &y0, &x1, &y1) ) + { + x0 = y0 = x1 = y1 = 0; + } + + glyph.bearingX = x0; + glyph.bearingY = y1; + glyph.width = x1 - x0; + glyph.height = y1 - y0; + + glyph.rect.minX = float(x0 / m_emSize); + glyph.rect.minY = float(y0 / m_emSize); + glyph.rect.maxX = float(x1 / m_emSize); + glyph.rect.maxY = float(y1 / m_emSize); + + glyph.glyphLocX = 0; + glyph.glyphLocY = 0; + glyph.bandMaxX = 0; + glyph.bandMaxY = 0; + glyph.bandScaleX = 0.0f; + glyph.bandScaleY = 0.0f; + glyph.bandOffsetX = 0.0f; + glyph.bandOffsetY = 0.0f; + + m_glyphs[_charcode] = glyph; + } + + void convertOutline(const stbtt_vertex* _vertices, int32_t _numVertices) + { + const float emSize = m_emSize; + + float cx = 0.0f, cy = 0.0f; + + for (int32_t ii = 0; ii < _numVertices; ++ii) + { + const stbtt_vertex& v = _vertices[ii]; + + switch (v.type) + { + case STBTT_vmove: + cx = float(v.x)/emSize; + cy = float(v.y)/emSize; + break; + + case STBTT_vline: + { + Curve c; + c.p1x = cx; + c.p1y = cy; + c.p3x = float(v.x)/emSize; + c.p3y = float(v.y)/emSize; + c.p2x = 0.5f * (c.p1x + c.p3x); + c.p2y = 0.5f * (c.p1y + c.p3y); + m_curves.push_back(c); + + cx = c.p3x; + cy = c.p3y; + } + break; + + case STBTT_vcurve: + { + Curve c; + c.p1x = cx; + c.p1y = cy; + c.p2x = float(v.cx)/emSize; + c.p2y = float(v.cy)/emSize; + c.p3x = float(v.x )/emSize; + c.p3y = float(v.y )/emSize; + m_curves.push_back(c); + + cx = c.p3x; + cy = c.p3y; + } + break; + + case STBTT_vcubic: + { + const float b1x = float(v.cx )/emSize; + const float b1y = float(v.cy )/emSize; + const float b2x = float(v.cx1)/emSize; + const float b2y = float(v.cy1)/emSize; + const float b3x = float(v.x )/emSize; + const float b3y = float(v.y )/emSize; + + const float c0x = cx + (b1x - cx ) * 0.75f; + const float c0y = cy + (b1y - cy ) * 0.75f; + const float c1x = b3x + (b2x - b3x) * 0.75f; + const float c1y = b3y + (b2y - b3y) * 0.75f; + const float dx = 0.5f * (c0x + c1x); + const float dy = 0.5f * (c0y + c1y); + + Curve q1; + q1.p1x = cx; q1.p1y = cy; + q1.p2x = c0x; q1.p2y = c0y; + q1.p3x = dx; q1.p3y = dy; + m_curves.push_back(q1); + + Curve q2; + q2.p1x = dx; q2.p1y = dy; + q2.p2x = c1x; q2.p2y = c1y; + q2.p3x = b3x; q2.p3y = b3y; + m_curves.push_back(q2); + + cx = b3x; + cy = b3y; + } + break; + } + } + } + + struct CurveRef + { + int32_t index; + float sortKey; + }; + + static int32_t compareCurveRefDescending(const void* _lhs, const void* _rhs) + { + const float lk = static_cast(_lhs)->sortKey; + const float rk = static_cast(_rhs)->sortKey; + return (lk > rk) ? -1 : (lk < rk) ? 1 : 0; + } + + static void collectHBand( + const stl::vector& _curves + , int32_t _start + , int32_t _count + , float _bandMinY + , float _bandMaxY + , stl::vector& _outResult + ) + { + stl::vector refs; + + for (int32_t ii = 0; ii < _count; ++ii) + { + const Curve& curve = _curves[_start + ii]; + const float cMinY = bx::min(curve.p1y, curve.p2y, curve.p3y); + const float cMaxY = bx::max(curve.p1y, curve.p2y, curve.p3y); + + if (cMaxY >= _bandMinY + && cMinY <= _bandMaxY) + { + CurveRef ref = { _start + ii, bx::max(curve.p1x, curve.p2x, curve.p3x) }; + refs.push_back(ref); + } + } + + if (!refs.empty() ) + { + bx::quickSort(refs.data(), (uint32_t)refs.size(), compareCurveRefDescending); + } + + for (const CurveRef& ref : refs) + { + _outResult.push_back(ref.index); + } + } + + static void collectVBand( + const stl::vector& _curves + , int32_t _start + , int32_t _count + , float _bandMinX + , float _bandMaxX + , stl::vector& _outResult + ) + { + stl::vector refs; + + for (int32_t ii = 0; ii < _count; ++ii) + { + const Curve& curve = _curves[_start + ii]; + const float cMinX = bx::min(curve.p1x, curve.p2x, curve.p3x); + const float cMaxX = bx::max(curve.p1x, curve.p2x, curve.p3x); + + if (cMaxX >= _bandMinX + && cMinX <= _bandMaxX) + { + CurveRef ref = { _start + ii, bx::max(curve.p1y, curve.p2y, curve.p3y) }; + refs.push_back(ref); + } + } + + if (!refs.empty() ) + { + bx::quickSort(refs.data(), (uint32_t)refs.size(), compareCurveRefDescending); + } + + for (const CurveRef& ref : refs) + { + _outResult.push_back(ref.index); + } + } + + void uploadTextures() + { + uint32_t numCurves = (uint32_t)m_curves.size(); + + stl::vector curveTexelIndex(numCurves); + uint32_t curveTexels = 0; + + for (uint32_t ii = 0; ii < numCurves; ++ii) + { + const uint32_t row0 = curveTexels / kTexWidth; + const uint32_t row1 = (curveTexels + 1) / kTexWidth; + + if (row0 != row1) + { + curveTexels = row1 * kTexWidth; + } + + curveTexelIndex[ii] = curveTexels; + curveTexels += 2; + } + + uint16_t curveTexH = (uint16_t)( (curveTexels + kTexWidth - 1) / kTexWidth); + curveTexH = bx::max(1, curveTexH); + + struct BandEntry + { + uint32_t count; + uint32_t offset; + }; + + struct GlyphBandData + { + BandEntry hbands[kMaxBands]; + BandEntry vbands[kMaxBands]; + uint8_t numHBands; + uint8_t numVBands; + stl::vector curveLocList; + }; + + stl::vector allGlyphBands; + allGlyphBands.reserve(m_glyphs.size() ); + + uint32_t bandTexRow = 0; + + for (auto& kv : m_glyphs) + { + SlugGlyph& glyph = kv.second; + + GlyphBandData gbd; + + if (glyph.curveCount == 0) + { + glyph.glyphLocX = 0; + glyph.glyphLocY = uint16_t(bandTexRow); + glyph.bandMaxX = 0; + glyph.bandMaxY = 0; + glyph.bandScaleX = 0.0f; + glyph.bandScaleY = 0.0f; + glyph.bandOffsetX = 0.0f; + glyph.bandOffsetY = 0.0f; + + gbd.hbands[0] = {0, 0}; + gbd.vbands[0] = {0, 0}; + gbd.numHBands = 1; + gbd.numVBands = 1; + allGlyphBands.push_back(gbd); + + bandTexRow++; + continue; + } + + float bbW = glyph.rect.maxX - glyph.rect.minX; + float bbH = glyph.rect.maxY - glyph.rect.minY; + + const uint8_t numHBands = bx::clamp(uint8_t(glyph.curveCount / 2), 1, kMaxBands); + const uint8_t numVBands = bx::clamp(uint8_t(glyph.curveCount / 2), 1, kMaxBands); + + glyph.bandMaxX = numVBands - 1; + glyph.bandMaxY = numHBands - 1; + + if (bbH > 0.0f) + { + glyph.bandScaleY = float(numHBands) / bbH; + glyph.bandOffsetY = -glyph.rect.minY * glyph.bandScaleY; + } + else + { + glyph.bandScaleY = 0.0f; + glyph.bandOffsetY = 0.0f; + } + + if (bbW > 0.0f) + { + glyph.bandScaleX = float(numVBands) / bbW; + glyph.bandOffsetX = -glyph.rect.minX * glyph.bandScaleX; + } + else + { + glyph.bandScaleX = 0.0f; + glyph.bandOffsetX = 0.0f; + } + + glyph.glyphLocX = 0; + glyph.glyphLocY = uint16_t(bandTexRow); + + gbd.numHBands = numHBands; + + const float bandH = bbH / (float)numHBands; + + for (int32_t bb = 0; bb < numHBands; ++bb) + { + const float bMinY = glyph.rect.minY + float(bb ) * bandH; + const float bMaxY = glyph.rect.minY + float(bb + 1) * bandH; + + stl::vector curvesInBand; + collectHBand(m_curves, glyph.curveStart, glyph.curveCount, bMinY, bMaxY, curvesInBand); + + gbd.hbands[bb].count = (uint32_t)curvesInBand.size(); + gbd.hbands[bb].offset = (uint32_t)gbd.curveLocList.size(); + + for (int32_t ci : curvesInBand) + { + uint32_t texIdx = curveTexelIndex[ci]; + gbd.curveLocList.push_back(texIdx % kTexWidth); + gbd.curveLocList.push_back(texIdx / kTexWidth); + } + } + + gbd.numVBands = numVBands; + + const float bandW = bbW / (float)numVBands; + + for (int32_t bb = 0; bb < numVBands; ++bb) + { + const float bMinX = glyph.rect.minX + float(bb ) * bandW; + const float bMaxX = glyph.rect.minX + float(bb + 1) * bandW; + + stl::vector curvesInBand; + collectVBand(m_curves, glyph.curveStart, glyph.curveCount, bMinX, bMaxX, curvesInBand); + + gbd.vbands[bb].count = (uint32_t)curvesInBand.size(); + gbd.vbands[bb].offset = (uint32_t)gbd.curveLocList.size(); + + for (int32_t ci : curvesInBand) + { + uint32_t texIdx = curveTexelIndex[ci]; + gbd.curveLocList.push_back(texIdx % kTexWidth); + gbd.curveLocList.push_back(texIdx / kTexWidth); + } + } + + allGlyphBands.push_back(gbd); + bandTexRow++; + } + + { + uint32_t glyphIdx = 0; + for (auto& kv : m_glyphs) + { + BX_UNUSED(kv); + GlyphBandData& gbd = allGlyphBands[glyphIdx]; + + const int32_t numHBands = gbd.numHBands; + const int32_t numVBands = gbd.numVBands; + const uint32_t headerSize = uint32_t(numHBands + numVBands); + + for (int32_t bb = 0; bb < numHBands; ++bb) + { + gbd.hbands[bb].offset = headerSize + gbd.hbands[bb].offset / 2; + } + + for (int32_t bb = 0; bb < numVBands; ++bb) + { + gbd.vbands[bb].offset = headerSize + gbd.vbands[bb].offset / 2; + } + + glyphIdx++; + } + } + + { + if (bgfx::isValid(m_curveTex) ) + { + bgfx::destroy(m_curveTex); + } + + const bgfx::Memory* mem = bgfx::alloc(kTexWidth * curveTexH * 4 * sizeof(float) ); + bx::memSet(mem->data, 0, mem->size); + float* dst = (float*)mem->data; + + for (uint32_t ii = 0; ii < numCurves; ++ii) + { + const Curve& curve = m_curves[ii]; + const uint32_t texIdx = curveTexelIndex[ii]; + + uint32_t off0 = texIdx * 4; + dst[off0 + 0] = curve.p1x; + dst[off0 + 1] = curve.p1y; + dst[off0 + 2] = curve.p2x; + dst[off0 + 3] = curve.p2y; + + uint32_t off1 = (texIdx + 1) * 4; + dst[off1 + 0] = curve.p3x; + dst[off1 + 1] = curve.p3y; + dst[off1 + 2] = 0.0f; + dst[off1 + 3] = 0.0f; + } + + m_curveTex = bgfx::createTexture2D( + kTexWidth + , curveTexH + , false + , 1 + , bgfx::TextureFormat::RGBA32F + , BGFX_SAMPLER_POINT | BGFX_SAMPLER_UVW_CLAMP + , mem + ); + } + + { + if (bgfx::isValid(m_bandTex) ) + { + bgfx::destroy(m_bandTex); + } + + uint16_t bandTexH = uint16_t(bandTexRow); + + const bgfx::Memory* mem = bgfx::alloc(kTexWidth * bandTexH * 4 * sizeof(float) ); + bx::memSet(mem->data, 0, mem->size); + float* dst = (float*)mem->data; + + uint32_t glyphIdx = 0; + for (auto& kv : m_glyphs) + { + SlugGlyph& glyph = kv.second; + GlyphBandData& gbd = allGlyphBands[glyphIdx]; + + uint32_t rowBase = (uint32_t)glyph.glyphLocY * kTexWidth; + + const int32_t numHBands = gbd.numHBands; + const int32_t numVBands = gbd.numVBands; + + for (int32_t bb = 0; bb < numHBands; ++bb) + { + uint32_t col = (uint32_t)(glyph.glyphLocX + bb); + uint32_t off = (rowBase + col) * 4; + dst[off + 0] = (float)gbd.hbands[bb].count; + dst[off + 1] = (float)gbd.hbands[bb].offset; + dst[off + 2] = 0.0f; + dst[off + 3] = 0.0f; + } + + for (int32_t bb = 0; bb < numVBands; ++bb) + { + uint32_t col = (uint32_t)(glyph.glyphLocX + numHBands + bb); + uint32_t off = (rowBase + col) * 4; + dst[off + 0] = (float)gbd.vbands[bb].count; + dst[off + 1] = (float)gbd.vbands[bb].offset; + dst[off + 2] = 0.0f; + dst[off + 3] = 0.0f; + } + + const uint32_t headerSize = (uint32_t)(numHBands + numVBands); + const uint32_t numLocPairs = (uint32_t)(gbd.curveLocList.size() / 2); + + for (uint32_t ll = 0; ll < numLocPairs; ++ll) + { + uint32_t col = glyph.glyphLocX + headerSize + ll; + uint32_t off = (rowBase + col) * 4; + dst[off + 0] = (float)gbd.curveLocList[ll * 2 + 0]; + dst[off + 1] = (float)gbd.curveLocList[ll * 2 + 1]; + dst[off + 2] = 0.0f; + dst[off + 3] = 0.0f; + } + + glyphIdx++; + } + + m_bandTex = bgfx::createTexture2D( + kTexWidth + , bandTexH + , false + , 1 + , bgfx::TextureFormat::RGBA32F + , BGFX_SAMPLER_POINT | BGFX_SAMPLER_UVW_CLAMP + , mem + ); + } + } + + stbtt_fontinfo m_fontInfo; + float m_emSize; + + stl::vector m_curves; + stl::unordered_map m_glyphs; + + bgfx::TextureHandle m_curveTex; + bgfx::TextureHandle m_bandTex; + + bgfx::VertexBufferHandle m_vbh; + bgfx::IndexBufferHandle m_ibh; +}; + +class ExampleGpuFont : public entry::AppI +{ +public: + ExampleGpuFont(const char* _name, const char* _description, const char* _url) + : entry::AppI(_name, _description, _url) + , m_fontData(NULL) + , m_mainFont(NULL) + , m_fontProgram(BGFX_INVALID_HANDLE) + , m_curveSampler(BGFX_INVALID_HANDLE) + , m_bandSampler(BGFX_INVALID_HANDLE) + , m_paramsUniform(BGFX_INVALID_HANDLE) + , m_animate(true) + , m_scrollSpeed(0.25f) + , m_scrollOffset(0.0f) + , m_scrollDirection(1.0f) + { + } + + void init(int32_t _argc, const char* const* _argv, uint32_t _width, uint32_t _height) override + { + Args args(_argc, _argv); + + m_width = _width; + m_height = _height; + m_debug = BGFX_DEBUG_NONE; + m_reset = BGFX_RESET_VSYNC; + + bgfx::Init init; + init.type = args.m_type; + init.vendorId = args.m_pciId; + init.platformData.nwh = entry::getNativeWindowHandle(entry::kDefaultWindowHandle); + init.platformData.ndt = entry::getNativeDisplayHandle(); + init.platformData.type = entry::getNativeWindowHandleType(); + init.resolution.width = m_width; + init.resolution.height = m_height; + init.resolution.reset = m_reset; + bgfx::init(init); + + bgfx::setDebug(m_debug); + + bgfx::setViewClear(0 + , BGFX_CLEAR_COLOR + , 0x303030ff + , 1.0f + , 0 + ); + + FontVertex::init(); + + m_curveSampler = bgfx::createUniform("s_curveData", bgfx::UniformType::Sampler); + m_bandSampler = bgfx::createUniform("s_bandData", bgfx::UniformType::Sampler); + m_paramsUniform = bgfx::createUniform("u_params", bgfx::UniformType::Vec4); + + m_fontProgram = loadProgram("vs_slug", "fs_slug"); + + imguiCreate(); + + { + uint32_t txtSize = 0; + void* txtData = load("text/sherlock_holmes_a_scandal_in_bohemia_arthur_conan_doyle.txt", &txtSize); + BX_ASSERT(NULL != txtData, "Text file not found!"); + + const bx::StringView text = bx::StringView( (const char*)txtData, txtSize); + + uint32_t fontDataSize = 0; + m_fontData = (uint8_t*)load("font/roboto-regular.ttf", &fontDataSize); + m_mainFont = new SlugFont(m_fontData, 0.05f); + m_mainFont->prepareGlyphsForText(text); + m_bb = m_mainFont->measure(0.0f, 0.0f, text); + + const float cx = 0.5f * (m_bb.minX + m_bb.maxX); + const float cy = m_bb.maxY; + m_mainFont->buildMesh(-cx, -cy, text); + + unload(txtData); + txtData = NULL; + } + + { + const float textW = m_bb.maxX - m_bb.minX; + const float aspect = float(m_width) / float(m_height); + const float halfFovRad = 30.0f * bx::kPi / 180.0f; // half of 60° vertical FOV + m_distance = (textW * 0.5f) / (bx::tan(halfFovRad) * aspect) * 1.1f; + } + + m_posX = 0.0f; + m_posY = 0.0f; + m_rotX = 0.0f; + m_rotY = 0.0f; + m_rotZ = 0.0f; + m_lastMouseX = 0; + m_lastMouseY = 0; + m_lastMz = 0; + m_dragging = false; + + m_frameTime.reset(); + m_animTimeOffset = bx::toSeconds(m_frameTime.getDurationTime() ); + m_animReset = true; + } + + virtual int32_t shutdown() override + { + delete m_mainFont; + m_mainFont = NULL; + + if (m_fontData != NULL) + { + unload(m_fontData); + m_fontData = NULL; + } + + imguiDestroy(); + + bgfx::destroy(m_curveSampler); + bgfx::destroy(m_bandSampler); + bgfx::destroy(m_paramsUniform); + bgfx::destroy(m_fontProgram); + + bgfx::shutdown(); + + return 0; + } + + bool update() override + { + if (!entry::processEvents(m_width, m_height, m_debug, m_reset, &m_mouseState) ) + { + m_frameTime.frame(); + + const float time = bx::toSeconds(m_frameTime.getDurationTime() ); + + imguiBeginFrame(m_mouseState.m_mx + , m_mouseState.m_my + , (m_mouseState.m_buttons[entry::MouseButton::Left ] ? IMGUI_MBUT_LEFT : 0) + | (m_mouseState.m_buttons[entry::MouseButton::Right ] ? IMGUI_MBUT_RIGHT : 0) + | (m_mouseState.m_buttons[entry::MouseButton::Middle] ? IMGUI_MBUT_MIDDLE : 0) + , m_mouseState.m_mz + , uint16_t(m_width) + , uint16_t(m_height) + ); + + showExampleDialog(this); + + ImGui::SetNextWindowPos( + ImVec2(m_width - m_width / 5.0f - 10.0f, 10.0f) + , ImGuiCond_FirstUseEver + ); + ImGui::SetNextWindowSize( + ImVec2(m_width / 5.0f, m_height / 3.0f) + , ImGuiCond_FirstUseEver + ); + ImGui::Begin("Settings", NULL, 0); + + ImGui::Checkbox("Animate", &m_animate); + ImGui::SliderFloat("Scroll Speed", &m_scrollSpeed, 0.0f, 2.0f); + + if (ImGui::Button("Reset View") ) + { + const float textW = m_bb.maxX - m_bb.minX; + const float aspect = float(m_width) / float(m_height); + const float halfFovRad = 30.0f * bx::kPi / 180.0f; + m_distance = (textW * 0.5f) / (bx::tan(halfFovRad) * aspect) * 1.1f; + m_posX = 0.0f; + m_posY = 0.0f; + m_rotX = 0.0f; + m_rotY = 0.0f; + m_scrollOffset = 0.0f; + m_scrollDirection = 1.0f; + } + + ImGui::End(); + + if (!ImGui::GetIO().WantCaptureMouse) + { + if (m_mouseState.m_buttons[entry::MouseButton::Right]) + { + if (!m_dragging) + { + m_dragging = true; + m_lastMouseX = m_mouseState.m_mx; + m_lastMouseY = m_mouseState.m_my; + } + + const float deltaX = (float)(m_mouseState.m_mx - m_lastMouseX); + const float deltaY = (float)(m_mouseState.m_my - m_lastMouseY); + m_lastMouseX = m_mouseState.m_mx; + m_lastMouseY = m_mouseState.m_my; + + m_posX += deltaX * m_distance * 0.001f; + m_posY -= deltaY * m_distance * 0.001f; + } + else if (m_mouseState.m_buttons[entry::MouseButton::Left]) + { + if (!m_dragging) + { + m_dragging = true; + m_lastMouseX = m_mouseState.m_mx; + m_lastMouseY = m_mouseState.m_my; + } + + const float deltaX = (float)(m_mouseState.m_mx - m_lastMouseX); + const float deltaY = (float)(m_mouseState.m_my - m_lastMouseY); + m_lastMouseX = m_mouseState.m_mx; + m_lastMouseY = m_mouseState.m_my; + + const float size = bx::min((float)m_width, (float)m_height); + m_rotY -= deltaX / size * bx::kPi; + m_rotX -= deltaY / size * bx::kPi; + } + else if (m_mouseState.m_buttons[entry::MouseButton::Middle]) + { + if (!m_dragging) + { + m_dragging = true; + m_lastMouseX = m_mouseState.m_mx; + m_lastMouseY = m_mouseState.m_my; + } + + const float deltaX = (float)(m_mouseState.m_mx - m_lastMouseX); + const float deltaY = (float)(m_mouseState.m_my - m_lastMouseY); + m_lastMouseX = m_mouseState.m_mx; + m_lastMouseY = m_mouseState.m_my; + + const float size = bx::min((float)m_width, (float)m_height); + m_rotZ -= (deltaX + deltaY) / size * bx::kPi; + } + else + { + m_dragging = false; + } + + bool resetAnimation = m_dragging; + + { + const int32_t deltaZ = m_mouseState.m_mz - m_lastMz; + m_lastMz = m_mouseState.m_mz; + if (deltaZ != 0) + { + float factor = bx::clamp(1.0f - (float)deltaZ / 40.0f, 0.1f, 1.9f); + m_distance = bx::clamp(m_distance * factor, 0.01f, 10.0f); + resetAnimation = true; + } + } + + if (resetAnimation) + { + m_animTimeOffset = time + 3.0f; + } + } + + if (m_animate) + { + const float dt = bx::toMilliseconds(m_frameTime.getDeltaTime() ); + m_scrollOffset += m_scrollDirection * m_scrollSpeed/1000.0f * dt; + + const float textHeight = m_bb.maxY - m_bb.minY; + + if (m_scrollOffset > textHeight) + { + m_scrollOffset = textHeight; + m_scrollDirection = -1.0f; + } + else if (m_scrollOffset < 0.0f) + { + m_scrollOffset = 0.0f; + m_scrollDirection = 1.0f; + } + } + + if (time > m_animTimeOffset) + { + struct Frame + { + float px, py, pz; + float rx, ry, rz; + }; + + struct AnimFrame + { + Frame frame; + float animTime; + float duration; + }; + + static const AnimFrame animFrame[] = + { + { { 0.0f, 0.0f, 2.00f, 0.0f, 0.0f, bx::kPiHalf }, 1.0f, 2.0f }, + { { 0.0f, 0.0f, 3.00f, 0.0f, 0.0f, bx::kPiHalf }, 3.0f, 4.0f }, + { { 0.0f, 0.0f, 10.00f, 0.0f, 0.0f, bx::kPiHalf }, 1.0f, 5.0f }, + { { 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f }, 6.0f, 8.0f }, + { { 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 1.0f }, 1.0f, 3.0f }, + { { 0.42f, 0.05f, 0.69f, 0.79f, -0.55f, 0.0f }, 1.0f, 5.0f }, + { { 0.07f, -0.31f, 0.59f, -0.50f, -0.00f, -0.3f }, 1.0f, 5.0f }, + { { 0.07f, -0.31f, 0.59f, -0.50f, -0.00f, 0.3f }, 1.0f, 5.0f }, + { { 0.00f, 0.00f, 0.85f, 1.35f, -0.01f, 0.0f }, 1.0f, 5.0f }, + }; + + static uint32_t currIdx = 0; + static float timeOffset = time; + static Frame prev; + + if (m_animReset) + { + currIdx = 0; + timeOffset = time; + + prev = + { + .px = m_posX, + .py = m_posY, + .pz = m_distance, + .rx = m_rotX, + .ry = m_rotY, + .rz = m_rotZ, + }; + + m_animReset = false; + } + + float animFrameTime = time - timeOffset; + if (animFrameTime >= animFrame[currIdx].duration) + { + timeOffset = time; + animFrameTime -= animFrame[currIdx].duration; + prev = animFrame[currIdx].frame; + currIdx = (currIdx + 1) % BX_COUNTOF(animFrame); + } + + const float animTime = bx::min(animFrameTime, animFrame[currIdx].animTime) / animFrame[currIdx].animTime; + const Frame& curr = animFrame[currIdx].frame; + const float tt = bx::easeInOutQuad(animTime); + + Frame frame = + { + .px = bx::lerp(prev.px, curr.px, tt), + .py = bx::lerp(prev.py, curr.py, tt), + .pz = bx::lerp(prev.pz, curr.pz, tt), + .rx = bx::lerp(prev.rx, curr.rx, tt), + .ry = bx::lerp(prev.ry, curr.ry, tt), + .rz = bx::lerp(prev.rz, curr.rz, tt), + }; + + m_posX = frame.px; + m_posY = frame.py; + m_distance = frame.pz; + m_rotX = frame.rx; + m_rotY = frame.ry; + m_rotZ = frame.rz; + } + else + { + m_animReset = true; + } + + imguiEndFrame(); + + bgfx::touch(0); + bgfx::setViewRect(0, 0, 0, uint16_t(m_width), uint16_t(m_height) ); + + const float aspect = float(m_width) / float(m_height); + + float view[16]; + { + float eye[16]; + bx::mtxLookAt(eye + , { 0.0f, 0.0f, m_distance } + , { 0.0f, 0.0f, 0.0f } + , { 0.0f, 1.0f, 0.0f } + , bx::Handedness::Right + ); + + float rotation[16]; + bx::mtxRotateXYZ(rotation, m_rotX, m_rotY, m_rotZ); + + float translation[16]; + bx::mtxTranslate(translation, m_posX, m_posY, 0.0f); + + float tmp[16]; + bx::mtxMul(tmp, rotation, translation); + bx::mtxMul(view, tmp, eye); + } + + float proj[16]; + bx::mtxProj(proj + , 60.0f + , aspect + , 0.002f + , 12.0f + , bgfx::getCaps()->homogeneousDepth + , bx::Handedness::Right + ); + + bgfx::setViewTransform(0, view, proj); + + float mtxScroll[16]; + bx::mtxTranslate(mtxScroll, 0.0f, m_scrollOffset, 0.0f); + bgfx::setTransform(mtxScroll); + + m_mainFont->submit(0 + , m_fontProgram + , m_curveSampler + , m_bandSampler + , m_paramsUniform + ); + + bgfx::frame(); + + return true; + } + + return false; + } + + entry::MouseState m_mouseState; + + uint32_t m_width; + uint32_t m_height; + uint32_t m_debug; + uint32_t m_reset; + + float m_animTimeOffset; + bool m_animReset = true; + + FrameTime m_frameTime; + + uint8_t* m_fontData; + SlugFont* m_mainFont; + Rect m_bb; + + bgfx::ProgramHandle m_fontProgram; + bgfx::UniformHandle m_curveSampler; + bgfx::UniformHandle m_bandSampler; + bgfx::UniformHandle m_paramsUniform; + + bool m_animate; + float m_scrollSpeed; + float m_scrollOffset; + float m_scrollDirection; + + float m_distance; + float m_posX; + float m_posY; + float m_rotX; + float m_rotY; + float m_rotZ; + int32_t m_lastMouseX; + int32_t m_lastMouseY; + int32_t m_lastMz; + bool m_dragging; +}; + +} // namespace + +ENTRY_IMPLEMENT_MAIN( + ExampleGpuFont + , "51-gpu-font" + , "GPU Font Rendering (Slug Algorithm)" + , "https://bkaradzic.github.io/bgfx/examples.html#gpu-font" + ); diff --git a/examples/51-gpufont/makefile b/examples/51-gpufont/makefile new file mode 100644 index 000000000..633edbfab --- /dev/null +++ b/examples/51-gpufont/makefile @@ -0,0 +1,10 @@ +# +# Copyright 2011-2026 Branimir Karadzic. All rights reserved. +# License: https://github.com/bkaradzic/bgfx/blob/master/LICENSE +# + +BGFX_DIR=../.. +RUNTIME_DIR=$(BGFX_DIR)/examples/runtime +BUILD_DIR=../../.build + +include $(BGFX_DIR)/scripts/shader.mk diff --git a/examples/51-gpufont/screenshot.png b/examples/51-gpufont/screenshot.png new file mode 100644 index 000000000..3345ff6b6 Binary files /dev/null and b/examples/51-gpufont/screenshot.png differ diff --git a/examples/51-gpufont/varying.def.sc b/examples/51-gpufont/varying.def.sc new file mode 100644 index 000000000..2acd24a8a --- /dev/null +++ b/examples/51-gpufont/varying.def.sc @@ -0,0 +1,9 @@ +vec2 v_texcoord0 : TEXCOORD0 = vec2(0.0, 0.0); +flat vec4 v_banding : TEXCOORD1 = vec4(0.0, 0.0, 0.0, 0.0); +flat vec4 v_glyph : TEXCOORD2 = vec4(0.0, 0.0, 0.0, 0.0); +vec2 v_position : TEXCOORD3 = vec2(0.0, 0.0); + +vec4 a_position : POSITION; +vec4 a_texcoord0 : TEXCOORD0; +vec4 a_texcoord1 : TEXCOORD1; +vec2 a_texcoord2 : TEXCOORD2; diff --git a/examples/51-gpufont/vs_slug.sc b/examples/51-gpufont/vs_slug.sc new file mode 100644 index 000000000..ce19dd415 --- /dev/null +++ b/examples/51-gpufont/vs_slug.sc @@ -0,0 +1,95 @@ +$input a_position, a_texcoord0, a_texcoord1 +$output v_texcoord0, v_banding, v_glyph + +// =================================================== +// Reference vertex shader for the Slug algorithm. +// This code is made available under the MIT License. +// Copyright 2017, by Eric Lengyel. +// =================================================== + +#include "../common/common.sh" + +uniform vec4 u_params; + +// The per-vertex input data consists of 5 attributes all having 4 floating-point components: +// +// 0 - pos +// 1 - tex +// 2 - jac +// 3 - bnd +// 4 - col + +// pos.xy = object-space vertex coordinates. +// pos.zw = object-space normal vector. + +// tex.xy = em-space sample coordinates. + +// tex.z = location of glyph data in band texture (interpreted as integer): + +// | 31 24 | 23 16 | 15 8 | 7 0 | +// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +// | y coordinate of glyph data in band texture | x coordinate of glyph data in band texture | +// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + +// tex.w = max band indexes and flags (interpreted as integer): + +// | 31 24 | 23 16 | 15 8 | 7 0 | +// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +// | 0 0 0 | E | 0 0 0 0 | band max y | 0 0 0 0 0 0 0 0 | band max x | +// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + +// jac = inverse Jacobian matrix entries (00, 01, 10, 11). +// bnd = (band scale x, band scale y, band offset x, band offset y). +// col = vertex color (red, green, blue, alpha). + +void slugUnpack(vec4 _bnd, vec2 _bits, out vec4 _outBanding, out vec4 _outGlyph) +{ + uvec2 g = floatBitsToUint(_bits); + _outGlyph = vec4(g.x & 0xffffu, g.x >> 16u, g.y & 0xffffu, g.y >> 16u); + _outBanding = _bnd; +} + +vec2 slugDilate(vec2 _pos, vec2 _normal, vec2 _tex, vec4 _jac, vec2 _dim, out vec2 _outPos) +{ + vec2 n = normalize(_normal); + + vec4 pClip = mul(u_modelViewProj, vec4(_pos, 0.0, 1.0) ); + vec4 nClip = mul(u_modelViewProj, vec4(n, 0.0, 0.0) ); + + float s = pClip.w; + float t = nClip.w; + + float u = (s * nClip.x - t * pClip.x) * _dim.x; + float v = (s * nClip.y - t * pClip.y) * _dim.y; + + float s2 = s * s; + float st = s * t; + float uv = u * u + v * v; + vec2 d = _normal * (s2 * (st + sqrt(uv) ) / (uv - st * st) ); + + _outPos = _pos + d; + return vec2(_tex.x + dot(d, _jac.xy), _tex.y + dot(d, _jac.zw) ); +} + +void main() +{ + vec2 p; + vec4 jacobian = vec4(u_params.w, 0.0, 0.0, u_params.w); + + // Apply dynamic dilation to vertex position. Returns new em-space sample position. + + vec2 pos = a_position.xy; + vec2 normal = a_position.zw; + vec2 uv = a_texcoord0.xy; + vec2 bits = a_texcoord0.zw; + + v_texcoord0 = slugDilate(pos, normal, uv, jacobian, u_viewRect.zw, p); + + // Apply MVP matrix to dilated vertex position. + + gl_Position = mul(u_modelViewProj, vec4(p, 0.0, 1.0) ); + + // Unpack or pass through remaining vertex data. + + slugUnpack(a_texcoord1, bits, v_banding, v_glyph); +} diff --git a/examples/common/example-glue.cpp b/examples/common/example-glue.cpp index 8ee89e825..759fd1018 100644 --- a/examples/common/example-glue.cpp +++ b/examples/common/example-glue.cpp @@ -471,8 +471,6 @@ void showExampleDialog(entry::AppI* _app, const char* _errorText) { if (ImGui::CollapsingHeader(ICON_FA_PUZZLE_PIECE " Resources") ) { - const bgfx::Caps* caps = bgfx::getCaps(); - const float itemHeight = ImGui::GetTextLineHeightWithSpacing(); const float maxWidth = 90.0f; diff --git a/examples/runtime/shaders/dxbc/fs_slug.bin b/examples/runtime/shaders/dxbc/fs_slug.bin new file mode 100644 index 000000000..fd9c5b711 Binary files /dev/null and b/examples/runtime/shaders/dxbc/fs_slug.bin differ diff --git a/examples/runtime/shaders/dxbc/vs_slug.bin b/examples/runtime/shaders/dxbc/vs_slug.bin new file mode 100644 index 000000000..529ef5c39 Binary files /dev/null and b/examples/runtime/shaders/dxbc/vs_slug.bin differ diff --git a/examples/runtime/shaders/dxil/fs_slug.bin b/examples/runtime/shaders/dxil/fs_slug.bin new file mode 100644 index 000000000..eddf2f5ff Binary files /dev/null and b/examples/runtime/shaders/dxil/fs_slug.bin differ diff --git a/examples/runtime/shaders/dxil/vs_slug.bin b/examples/runtime/shaders/dxil/vs_slug.bin new file mode 100644 index 000000000..2cc98b5fc Binary files /dev/null and b/examples/runtime/shaders/dxil/vs_slug.bin differ diff --git a/examples/runtime/shaders/essl/fs_slug.bin b/examples/runtime/shaders/essl/fs_slug.bin new file mode 100644 index 000000000..63be00bcf Binary files /dev/null and b/examples/runtime/shaders/essl/fs_slug.bin differ diff --git a/examples/runtime/shaders/essl/vs_slug.bin b/examples/runtime/shaders/essl/vs_slug.bin new file mode 100644 index 000000000..b059564f7 Binary files /dev/null and b/examples/runtime/shaders/essl/vs_slug.bin differ diff --git a/examples/runtime/shaders/glsl/fs_slug.bin b/examples/runtime/shaders/glsl/fs_slug.bin new file mode 100644 index 000000000..ca983f90b Binary files /dev/null and b/examples/runtime/shaders/glsl/fs_slug.bin differ diff --git a/examples/runtime/shaders/glsl/vs_slug.bin b/examples/runtime/shaders/glsl/vs_slug.bin new file mode 100644 index 000000000..3b72d4458 Binary files /dev/null and b/examples/runtime/shaders/glsl/vs_slug.bin differ diff --git a/examples/runtime/shaders/metal/fs_slug.bin b/examples/runtime/shaders/metal/fs_slug.bin new file mode 100644 index 000000000..8871449d5 Binary files /dev/null and b/examples/runtime/shaders/metal/fs_slug.bin differ diff --git a/examples/runtime/shaders/metal/vs_slug.bin b/examples/runtime/shaders/metal/vs_slug.bin new file mode 100644 index 000000000..d00f2a27b Binary files /dev/null and b/examples/runtime/shaders/metal/vs_slug.bin differ diff --git a/examples/runtime/shaders/spirv/fs_slug.bin b/examples/runtime/shaders/spirv/fs_slug.bin new file mode 100644 index 000000000..2fcff62f3 Binary files /dev/null and b/examples/runtime/shaders/spirv/fs_slug.bin differ diff --git a/examples/runtime/shaders/spirv/vs_slug.bin b/examples/runtime/shaders/spirv/vs_slug.bin new file mode 100644 index 000000000..154a9444b Binary files /dev/null and b/examples/runtime/shaders/spirv/vs_slug.bin differ diff --git a/examples/runtime/shaders/wgsl/fs_slug.bin b/examples/runtime/shaders/wgsl/fs_slug.bin new file mode 100644 index 000000000..93a02da64 Binary files /dev/null and b/examples/runtime/shaders/wgsl/fs_slug.bin differ diff --git a/examples/runtime/shaders/wgsl/vs_slug.bin b/examples/runtime/shaders/wgsl/vs_slug.bin new file mode 100644 index 000000000..17c95ace0 Binary files /dev/null and b/examples/runtime/shaders/wgsl/vs_slug.bin differ diff --git a/scripts/genie.lua b/scripts/genie.lua index 945a95fe2..6116b11fc 100644 --- a/scripts/genie.lua +++ b/scripts/genie.lua @@ -574,6 +574,7 @@ or _OPTIONS["with-combined-examples"] then , "47-pixelformats" , "48-drawindirect" , "49-hextile" + , "51-gpufont" )