/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ // Description : Font class. #if !defined(USE_NULLFONT_ALWAYS) #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const int TabCharCount = 4; // set buffer sizes to hold max characters that can be drawn in 1 DrawString call static const size_t MaxVerts = 8 * 1024; // 2048 quads static const size_t MaxIndices = (MaxVerts * 6) / 4; // 6 indices per quad, 6/4 * MaxVerts static const char DrawList2DPassName[] = "2dpass"; AZ::FFont::FFont(AZ::AtomFont* atomFont, const char* fontName) : m_name(fontName) , m_atomFont(atomFont) { assert(m_name.c_str()); assert(m_atomFont); // create default effect FontEffect* effect = AddEffect("default"); effect->AddPass(); // Create cpu memory to cache the font draw data before submit m_vertexBuffer = new SVF_P3F_C4B_T2F[MaxVerts]; m_indexBuffer = new u16[MaxIndices]; m_vertexCount = 0; m_indexCount = 0; AddRef(); } AZ::RPI::ViewportContextPtr AZ::FFont::GetDefaultViewportContext() const { auto viewContextManager = AZ::Interface::Get(); return viewContextManager->GetDefaultViewportContext(); } AZ::RPI::WindowContextSharedPtr AZ::FFont::GetDefaultWindowContext() const { if (auto defaultViewportContext = GetDefaultViewportContext()) { return defaultViewportContext->GetWindowContext(); } return {}; } AZ::FFont::~FFont() { AZ_Assert(m_atomFont == nullptr, "The font should already be unregistered through a call to AZ::FFont::Release()"); delete[] m_vertexBuffer; delete[] m_indexBuffer; Free(); } int32_t AZ::FFont::AddRef() { ref_count::add_ref(); return aznumeric_cast(ref_count::use_count()); } int32_t AZ::FFont::Release() { int32_t useCount = aznumeric_cast(ref_count::use_count()) - 1; ref_count::release(); return useCount; } // Load a font from a TTF file bool AZ::FFont::Load(const char* fontFilePath, unsigned int width, unsigned int height, unsigned int widthNumSlots, unsigned int heightNumSlots, unsigned int flags, float sizeRatio) { if (!fontFilePath) { return false; } Free(); auto pPak = gEnv->pCryPak; string fullFile; if (pPak->IsAbsPath(fontFilePath)) { fullFile = fontFilePath; } else { fullFile = m_curPath + fontFilePath; } int smoothMethodFlag = (flags & TTFFLAG_SMOOTH_MASK) >> TTFFLAG_SMOOTH_SHIFT; AZ::FontSmoothMethod smoothMethod = AZ::FontSmoothMethod::None; switch (smoothMethodFlag) { case TTFFLAG_SMOOTH_BLUR: smoothMethod = AZ::FontSmoothMethod::Blur; break; case TTFFLAG_SMOOTH_SUPERSAMPLE: smoothMethod = AZ::FontSmoothMethod::SuperSample; break; } int smoothAmountFlag = (flags & TTFFLAG_SMOOTH_AMOUNT_MASK); AZ::FontSmoothAmount smoothAmount = AZ::FontSmoothAmount::None; switch (smoothAmountFlag) { case TTFLAG_SMOOTH_AMOUNT_2X: smoothAmount = AZ::FontSmoothAmount::x2; break; case TTFLAG_SMOOTH_AMOUNT_4X: smoothAmount = AZ::FontSmoothAmount::x4; break; } AZ::IO::HandleType fileHandle = pPak->FOpen(fullFile.c_str(), "rb"); if (fileHandle == AZ::IO::InvalidHandle) { return false; } size_t fileSize = pPak->FGetSize(fileHandle); if (!fileSize) { pPak->FClose(fileHandle); return false; } unsigned char* buffer = new unsigned char[fileSize]; if (!pPak->FReadRaw(buffer, fileSize, 1, fileHandle)) { pPak->FClose(fileHandle); delete [] buffer; return false; } pPak->FClose(fileHandle); if (!m_fontTexture) { m_fontTexture = new FontTexture(); } if (!m_fontTexture || !m_fontTexture->CreateFromMemory(buffer, (int)fileSize, width, height, smoothMethod, smoothAmount, widthNumSlots, heightNumSlots, sizeRatio)) { delete [] buffer; return false; } m_monospacedFont = m_fontTexture->GetMonospaced(); m_fontBuffer = buffer; m_fontBufferSize = fileSize; m_fontTexDirty = false; m_sizeRatio = sizeRatio; InitCache(); return true; } void AZ::FFont::Free() { m_fontImage = nullptr; m_fontImageVersion = 0; delete m_fontTexture; m_fontTexture = 0; delete[] m_fontBuffer; m_fontBuffer = 0; m_fontBufferSize = 0; } void AZ::FFont::DrawString(float x, float y, const char* str, const bool asciiMultiLine, const TextDrawContext& ctx) { if (!str) { return; } DrawStringUInternal(GetDefaultWindowContext()->GetViewport(), GetDefaultViewportContext(), x, y, 1.0f, str, asciiMultiLine, ctx); } void AZ::FFont::DrawString(float x, float y, float z, const char* str, const bool asciiMultiLine, const TextDrawContext& ctx) { if (!str) { return; } DrawStringUInternal(GetDefaultWindowContext()->GetViewport(), GetDefaultViewportContext(), x, y, z, str, asciiMultiLine, ctx); } void AZ::FFont::DrawStringUInternal( const RHI::Viewport& viewport, RPI::ViewportContextPtr viewportContext, float x, float y, float z, const char* str, const bool asciiMultiLine, const TextDrawContext& ctx) { // Lazily ensure we're initialized before attempting to render. // Validate that there is a render scene before attempting to init. if (!viewportContext || !viewportContext->GetRenderScene()) { return; } if (!str || !m_vertexBuffer // vertex buffer isn't created until BootstrapScene is ready, Editor tries to render text before that. || !m_fontTexture || ctx.m_fxIdx >= m_effects.size() || m_effects[ctx.m_fxIdx].m_passes.empty()) { return; } const size_t fxSize = m_effects.size(); if (fxSize && !m_fontImage && !InitTexture()) { return; } const bool orthoMode = ctx.m_overrideViewProjMatrices; const float viewX = viewport.m_minX; const float viewY = viewport.m_minY; const float viewWidth = viewport.m_maxX - viewport.m_minX; const float viewHeight = viewport.m_maxY - viewport.m_minY; const float zf = viewport.m_minZ; const float zn = viewport.m_maxZ; Matrix4x4 modelViewProjMat; if (!orthoMode) { AZ::RPI::ViewPtr view = viewportContext->GetDefaultView(); modelViewProjMat = view->GetWorldToClipMatrix(); } else { if (viewWidth == 0 || viewHeight == 0) { return; } AZ::MakeOrthographicMatrixRH(modelViewProjMat, viewX, viewX + viewWidth, viewY + viewHeight, viewY, zn, zf); } size_t startingVertexCount = m_vertexCount; // Local function that is passed into CreateQuadsForText as the AddQuad function AZ::FFont::AddFunction AddQuad = [this, startingVertexCount] (const Vec3& v0, const Vec3& v1, const Vec3& v2, const Vec3& v3, const Vec2& tc0, const Vec2& tc1, const Vec2& tc2, const Vec2& tc3, uint32_t packedColor) { const bool vertexSpaceLeft = m_vertexCount + 4 < MaxVerts; const bool indexSpaceLeft = m_indexCount + 6 < MaxIndices; if (!vertexSpaceLeft || !indexSpaceLeft) { return false; } size_t vertexOffset = m_vertexCount; m_vertexCount += 4; size_t indexOffset = m_indexCount; m_indexCount += 6; // define char quad m_vertexBuffer[vertexOffset + 0].xyz = v0; m_vertexBuffer[vertexOffset + 0].color.dcolor = packedColor; m_vertexBuffer[vertexOffset + 0].st = tc0; m_vertexBuffer[vertexOffset + 1].xyz = v1; m_vertexBuffer[vertexOffset + 1].color.dcolor = packedColor; m_vertexBuffer[vertexOffset + 1].st = tc1; m_vertexBuffer[vertexOffset + 2].xyz = v2; m_vertexBuffer[vertexOffset + 2].color.dcolor = packedColor; m_vertexBuffer[vertexOffset + 2].st = tc2; m_vertexBuffer[vertexOffset + 3].xyz = v3; m_vertexBuffer[vertexOffset + 3].color.dcolor = packedColor; m_vertexBuffer[vertexOffset + 3].st = tc3; uint16_t startingIndex = static_cast(vertexOffset - startingVertexCount); m_indexBuffer[indexOffset + 0] = startingIndex + 0; m_indexBuffer[indexOffset + 1] = startingIndex + 1; m_indexBuffer[indexOffset + 2] = startingIndex + 2; m_indexBuffer[indexOffset + 3] = startingIndex + 2; m_indexBuffer[indexOffset + 4] = startingIndex + 3; m_indexBuffer[indexOffset + 5] = startingIndex + 0; return true; }; int numQuads = 0; { AZStd::lock_guard lock(m_vertexDataMutex); numQuads = CreateQuadsForText(viewport, x, y, z, str, asciiMultiLine, ctx, AddQuad); } if (numQuads) { AZ::RPI::Ptr dynamicDraw = AZ::AtomBridge::PerViewportDynamicDraw::Get()->GetDynamicDrawContextForViewport(m_dynamicDrawContextName, viewportContext->GetId()); if (dynamicDraw) { //setup per draw srg auto drawSrg = dynamicDraw->NewDrawSrg(); drawSrg->SetConstant(m_fontShaderData.m_viewProjInputIndex, modelViewProjMat); drawSrg->SetImageView(m_fontShaderData.m_imageInputIndex, m_fontStreamingImage->GetImageView()); drawSrg->Compile(); dynamicDraw->DrawIndexed(m_vertexBuffer, m_vertexCount, m_indexBuffer, m_indexCount, RHI::IndexFormat::Uint16, drawSrg); } m_indexCount = 0; m_vertexCount = 0; } } Vec2 AZ::FFont::GetTextSize(const char* str, const bool asciiMultiLine, const TextDrawContext& ctx) { if (!str) { return Vec2(0.0f, 0.0f); } return GetTextSizeUInternal(GetDefaultWindowContext()->GetViewport(), str, asciiMultiLine, ctx); } Vec2 AZ::FFont::GetTextSizeUInternal( const RHI::Viewport& viewport, const char* str, const bool asciiMultiLine, const TextDrawContext& ctx) { const size_t fxSize = m_effects.size(); if (!str || !m_fontTexture || !fxSize) { return Vec2(0, 0); } Prepare(str, false, ctx.m_requestSize); // This is the "logical" size of the font (in pixels). The actual size of // the glyphs in the font texture may have additional scaling applied or // could have been re-rendered at a different size. Vec2 size = ctx.m_size; if (ctx.m_sizeIn800x600) { ScaleCoord(viewport, size.x, size.y); } // This scaling takes into account the logical size of the font relative // to any additional scaling applied (such as from "size ratio"). const TextScaleInfoInternal scaleInfo(CalculateScaleInternal(viewport, ctx)); float maxW = 0; float maxH = 0; const size_t fxIdx = ctx.m_fxIdx < fxSize ? ctx.m_fxIdx : 0; const FontEffect& fx = m_effects[fxIdx]; for (size_t i = 0, numPasses = fx.m_passes.size(); i < numPasses; ++i) { const FontRenderingPass* pass = &fx.m_passes[numPasses - i - 1]; // gather pass data Vec2 offset = pass->m_posOffset; float charX = offset.x; float charY = offset.y + size.y; if (charY > maxH) { maxH = charY; } // parse the string, ignoring control characters uint32_t nextCh = 0; Unicode::CIterator pChar(str); while (uint32_t ch = *pChar) { ++pChar; nextCh = *pChar; switch (ch) { case '\\': { if (*pChar != 'n' || !asciiMultiLine) { break; } ++pChar; } case '\n': { if (charX > maxW) { maxW = charX; } charX = offset.x; charY += size.y * (1.f + ctx.GetLineSpacing()); if (charY > maxH) { maxH = charY; } continue; } break; case '\r': { if (charX > maxW) { maxW = charX; } charX = offset.x; continue; } break; case '\t': { if (ctx.m_proportional) { charX += TabCharCount * size.x * AZ_FONT_SPACE_SIZE; } else { charX += TabCharCount * size.x * ctx.m_widthScale; } continue; } break; case '$': { if (ctx.m_processSpecialChars) { if (*pChar == '$') { ++pChar; } else if (isdigit(*pChar)) { ++pChar; continue; } else if (*pChar == 'O' || *pChar == 'o') { ++pChar; continue; } } } break; default: break; } const bool rerenderGlyphs = m_sizeBehavior == SizeBehavior::Rerender; const AtomFont::GlyphSize requestSize = rerenderGlyphs ? ctx.m_requestSize : AtomFont::defaultGlyphSize; int horizontalAdvance = m_fontTexture->GetHorizontalAdvance(ch, requestSize); float advance; if (ctx.m_proportional) { advance = horizontalAdvance * scaleInfo.scale.x; } else { advance = size.x * ctx.m_widthScale; } // Adjust "advance" here for kerning purposes Vec2 kerningOffset(Vec2_Zero); if (ctx.m_kerningEnabled && nextCh) { kerningOffset = m_fontTexture->GetKerning(ch, nextCh) * scaleInfo.scale.x; } // Adjust char width with tracking only if there is a next character if (nextCh) { charX += ctx.m_tracking; } charX += advance + kerningOffset.x; } if (charX > maxW) { maxW = charX; } } return Vec2(maxW, maxH); } uint32_t AZ::FFont::GetNumQuadsForText(const char* str, const bool asciiMultiLine, const TextDrawContext& ctx) { uint32_t numQuads = 0; const size_t fxSize = m_effects.size(); const size_t fxIdx = ctx.m_fxIdx < fxSize ? ctx.m_fxIdx : 0; const FontEffect& fx = m_effects[fxIdx]; for (size_t j = 0, numPasses = fx.m_passes.size(); j < numPasses; ++j) { size_t i = numPasses - j - 1; bool drawFrame = ctx.m_framed && i == numPasses - 1; if (drawFrame) { ++numQuads; } uint32_t nextCh = 0; Unicode::CIterator pChar(str); while (uint32_t ch = *pChar) { ++pChar; nextCh = *pChar; switch (ch) { case '\\': { if (*pChar != 'n' || !asciiMultiLine) { break; } ++pChar; } case '\n': { continue; } break; case '\r': { continue; } break; case '\t': { continue; } break; case '$': { if (ctx.m_processSpecialChars) { if (*pChar == '$') { ++pChar; } else if (isdigit(*pChar)) { ++pChar; continue; } else if (*pChar == 'O' || *pChar == 'o') { ++pChar; continue; } } } break; default: break; } ++numQuads; } } return numQuads; } uint32_t AZ::FFont::WriteTextQuadsToBuffers(SVF_P2F_C4B_T2F_F4B* verts, uint16_t* indices, uint32_t maxQuads, float x, float y, float z, const char* str, const bool asciiMultiLine, const TextDrawContext& ctx) { uint32_t numQuadsWritten = 0; const size_t fxSize = m_effects.size(); if (fxSize && !m_fontImage && !InitTexture()) { return numQuadsWritten; } SVF_P2F_C4B_T2F_F4B* vertexData = verts; uint16_t* indexData = indices; size_t vertexOffset = 0; size_t indexOffset = 0; // Local function that is passed into CreateQuadsForText as the AddQuad function AddFunction AddQuad = [&vertexData, &indexData, &vertexOffset, &indexOffset, maxQuads, &numQuadsWritten] (const Vec3& v0, const Vec3& v1, const Vec3& v2, const Vec3& v3, const Vec2& tc0, const Vec2& tc1, const Vec2& tc2, const Vec2& tc3, uint32_t packedColor) { Vec2 xy0(v0); Vec2 xy1(v1); Vec2 xy2(v2); Vec2 xy3(v3); const bool vertexSpaceLeft = vertexOffset + 3 < maxQuads * 4; const bool indexSpaceLeft = indexOffset + 5 < maxQuads * 6; if (!vertexSpaceLeft || !indexSpaceLeft) { return false; } // This should never happen but for safety make sure we never write off end of buffers (should hit asserts above if this is the case) if (numQuadsWritten < maxQuads) { // define char quad vertexData[vertexOffset].xy = xy0; vertexData[vertexOffset].color.dcolor = packedColor; vertexData[vertexOffset].st = tc0; vertexData[vertexOffset].texIndex = 0; vertexData[vertexOffset].texHasColorChannel = 0; vertexData[vertexOffset].texIndex2 = 0; vertexData[vertexOffset].pad = 0; vertexData[vertexOffset + 1].xy = xy1; vertexData[vertexOffset + 1].color.dcolor = packedColor; vertexData[vertexOffset + 1].st = tc1; vertexData[vertexOffset + 1].texIndex = 0; vertexData[vertexOffset + 1].texHasColorChannel = 0; vertexData[vertexOffset + 1].texIndex2 = 0; vertexData[vertexOffset + 1].pad = 0; vertexData[vertexOffset + 2].xy = xy2; vertexData[vertexOffset + 2].color.dcolor = packedColor; vertexData[vertexOffset + 2].st = tc2; vertexData[vertexOffset + 2].texIndex = 0; vertexData[vertexOffset + 2].texHasColorChannel = 0; vertexData[vertexOffset + 2].texIndex2 = 0; vertexData[vertexOffset + 2].pad = 0; vertexData[vertexOffset + 3].xy = xy3; vertexData[vertexOffset + 3].color.dcolor = packedColor; vertexData[vertexOffset + 3].st = tc3; vertexData[vertexOffset + 3].texIndex = 0; vertexData[vertexOffset + 3].texHasColorChannel = 0; vertexData[vertexOffset + 3].texIndex2 = 0; vertexData[vertexOffset + 3].pad = 0; indexData[indexOffset + 0] = static_cast(vertexOffset + 0); indexData[indexOffset + 1] = static_cast(vertexOffset + 1); indexData[indexOffset + 2] = static_cast(vertexOffset + 2); indexData[indexOffset + 3] = static_cast(vertexOffset + 2); indexData[indexOffset + 4] = static_cast(vertexOffset + 3); indexData[indexOffset + 5] = static_cast(vertexOffset + 0); vertexOffset += 4; indexOffset += 6; ++numQuadsWritten; } return true; }; CreateQuadsForText(GetDefaultWindowContext()->GetViewport(), x, y, z, str, asciiMultiLine, ctx, AddQuad); return numQuadsWritten; } uint32_t AZ::FFont::GetFontTextureVersion() { return m_fontImageVersion; } int AZ::FFont::CreateQuadsForText(const RHI::Viewport& viewport, float x, float y, float z, const char* str, const bool asciiMultiLine, const TextDrawContext& ctx, AddFunction AddQuad) { int numQuads = 0; const size_t fxSize = m_effects.size(); Prepare(str, true, ctx.m_requestSize); const size_t fxIdx = ctx.m_fxIdx < fxSize ? ctx.m_fxIdx : 0; const FontEffect& fx = m_effects[fxIdx]; bool passZeroColorOverridden = ctx.IsColorOverridden(); uint32_t alphaBlend = passZeroColorOverridden ? ctx.m_colorOverride.a : fx.m_passes[0].m_color.a; if (alphaBlend > 128) { ++alphaBlend; // 0..256 for proper blending } // This is the "logical" size of the font (in pixels). The actual size of // the glyphs in the font texture may have additional scaling applied or // could have been re-rendered at a different size. Vec2 size = ctx.m_size; if (ctx.m_sizeIn800x600) { ScaleCoord(viewport, size.x, size.y); } // This scaling takes into account the logical size of the font relative // to any additional scaling applied (such as from "size ratio"). const TextScaleInfoInternal scaleInfo(CalculateScaleInternal(viewport, ctx)); Vec2 baseXY = Vec2(x, y); // in pixels if (ctx.m_sizeIn800x600) { ScaleCoord(viewport, baseXY.x, baseXY.y); } // snap for pixel perfect rendering (better quality for text) if (ctx.m_pixelAligned) { baseXY.x = floor(baseXY.x); baseXY.y = floor(baseXY.y); // for smaller fonts (half res or less) it's better to average multiple pixels (we don't miss lines) if (scaleInfo.scale.x < 0.9f) { baseXY.x += 0.5f; // try to average two columns (for exact half res) } if (scaleInfo.scale.y < 0.9f) { baseXY.y += 0.25f; // hand tweaked value to get a good result with tiny font (640x480 underscore in console) } } for (size_t j = 0, numPasses = fx.m_passes.size(); j < numPasses; ++j) { size_t i = numPasses - j - 1; const FontRenderingPass* pass = &fx.m_passes[i]; if (!i) { alphaBlend = 256; } const ColorB& passColor = !i && passZeroColorOverridden ? ctx.m_colorOverride : fx.m_passes[i].m_color; // gather pass data Vec2 offset = pass->m_posOffset; // in pixels float charX = baseXY.x + offset.x; // in pixels float charY = baseXY.y + offset.y; // in pixels ColorB color = passColor; bool drawFrame = ctx.m_framed && i == numPasses - 1; if (drawFrame) { ColorB tempColor(255, 255, 255, 255); uint32_t frameColor = tempColor.pack_argb8888(); //note: this ends up in r,g,b,a order on little-endian machines Vec2 textSize = GetTextSizeUInternal(viewport, str, asciiMultiLine, ctx); float x0 = baseXY.x - 12; float y0 = baseXY.y - 6; float x1 = baseXY.x + textSize.x + 12; float y1 = baseXY.y + textSize.y + 6; bool culled = false; if (ctx.m_clippingEnabled) { float clipX = ctx.m_clipX; float clipY = ctx.m_clipY; float clipR = ctx.m_clipX + ctx.m_clipWidth; float clipB = ctx.m_clipY + ctx.m_clipHeight; if ((x0 >= clipR) || (y0 >= clipB) || (x1 < clipX) || (y1 < clipY)) { culled = true; } x0 = max(clipX, x0); y0 = max(clipY, y0); x1 = min(clipR, x1); y1 = min(clipB, y1); } if (!culled) { Vec3 v0(x0, y0, z); Vec3 v2(x1, y1, z); Vec3 v1(v2.x, v0.y, v0.z); Vec3 v3(v0.x, v2.y, v0.z); if (ctx.m_drawTextFlags & eDrawText_UseTransform) { v0 = ctx.m_transform * v0; v2 = ctx.m_transform * v2; v1 = ctx.m_transform * v1; v3 = ctx.m_transform * v3; } Vec2 gradientUvMin, gradientUvMax; GetGradientTextureCoord(gradientUvMin.x, gradientUvMin.y, gradientUvMax.x, gradientUvMax.y); // define the frame quad Vec2 uv(gradientUvMin.x, gradientUvMax.y); if (AddQuad(v0, v1, v2, v3, uv, uv, uv, uv, frameColor)) { ++numQuads; } else { return numQuads; } } } // parse the string, ignoring control characters uint32_t nextCh = 0; Unicode::CIterator pChar(str); while (uint32_t ch = *pChar) { ++pChar; nextCh = *pChar; switch (ch) { case '\\': { if (*pChar != 'n' || !asciiMultiLine) { break; } ++pChar; } case '\n': { charX = baseXY.x + offset.x; charY += size.y * (1.f + ctx.GetLineSpacing()); continue; } break; case '\r': { charX = baseXY.x + offset.x; continue; } break; case '\t': { if (ctx.m_proportional) { charX += TabCharCount * size.x * AZ_FONT_SPACE_SIZE; } else { charX += TabCharCount * size.x * ctx.m_widthScale; } continue; } break; case '$': { if (ctx.m_processSpecialChars) { if (*pChar == '$') { ++pChar; } else if (isdigit(*pChar)) { if (!i) { static const AZ::Color ColorTable[10] = { AZ::Colors::Black, AZ::Colors::White, AZ::Colors::Blue, AZ::Colors::Lime, AZ::Colors::Red, AZ::Colors::Cyan, AZ::Colors::Yellow, AZ::Colors::Fuchsia, AZ::Colors::Orange, AZ::Colors::Grey, }; int colorIndex = (*pChar) - '0'; ColorB newColor = AZColorToLYColorB(ColorTable[colorIndex]); color.r = newColor.r; color.g = newColor.g; color.b = newColor.b; // Leave alpha at original value! } ++pChar; continue; } else if (*pChar == 'O' || *pChar == 'o') { if (!i) { color = passColor; } ++pChar; continue; } } } break; default: break; } // get texture coordinates float texCoord[4]; int charOffsetX, charOffsetY; // in font texels int charSizeX, charSizeY; // in font texels const bool rerenderGlyphs = m_sizeBehavior == SizeBehavior::Rerender; const AtomFont::GlyphSize requestSize = rerenderGlyphs ? ctx.m_requestSize : AtomFont::defaultGlyphSize; m_fontTexture->GetTextureCoord(m_fontTexture->GetCharSlot(ch, requestSize), texCoord, charSizeX, charSizeY, charOffsetX, charOffsetY, requestSize); int horizontalAdvance = m_fontTexture->GetHorizontalAdvance(ch, requestSize); float advance; if (ctx.m_proportional) { advance = horizontalAdvance * scaleInfo.scale.x; } else { advance = size.x * ctx.m_widthScale; } Vec2 kerningOffset(Vec2_Zero); if (ctx.m_kerningEnabled && nextCh) { kerningOffset = m_fontTexture->GetKerning(ch, nextCh) * scaleInfo.scale.x; } float trackingOffset = 0.0f; if (nextCh) { trackingOffset = ctx.m_tracking; } float px = charX + charOffsetX * scaleInfo.scale.x; // in pixels float py = charY + charOffsetY * scaleInfo.scale.y; // in pixels float pr = px + charSizeX * scaleInfo.scale.x; float pb = py + charSizeY * scaleInfo.scale.y; // compute clipping float newX = px; // in pixels float newY = py; // in pixels float newR = pr; // in pixels float newB = pb; // in pixels if (ctx.m_clippingEnabled) { float clipX = ctx.m_clipX; float clipY = ctx.m_clipY; float clipR = ctx.m_clipX + ctx.m_clipWidth; float clipB = ctx.m_clipY + ctx.m_clipHeight; // clip non visible if ((px >= clipR) || (py >= clipB) || (pr < clipX) || (pb < clipY)) { charX += advance + kerningOffset.x + trackingOffset; continue; } // clip partially visible else { float width = horizontalAdvance * scaleInfo.rcpCellWidth; if ((width <= 0.0f) || (size.y <= 0.0f)) { charX += advance + kerningOffset.x + trackingOffset; continue; } // clip the image to the scissor rect newX = max(clipX, px); newY = max(clipY, py); newR = min(clipR, pr); newB = min(clipB, pb); float rcpWidth = 1.0f / width; float rcpHeight = 1.0f / size.y; float texW = texCoord[2] - texCoord[0]; float texH = texCoord[3] - texCoord[1]; // clip horizontal texCoord[0] = texCoord[0] + texW * (newX - px) * rcpWidth; texCoord[2] = texCoord[2] + texW * (newR - pr) * rcpWidth; // clip vertical texCoord[1] = texCoord[1] + texH * (newY - py) * rcpHeight; texCoord[3] = texCoord[3] + texH * (newB - pb) * rcpHeight; } } Vec3 v0(newX, newY, z); Vec3 v2(newR, newB, z); Vec3 v1(v2.x, v0.y, v0.z); Vec3 v3(v0.x, v2.y, v0.z); Vec2 tc0(texCoord[0], texCoord[1]); Vec2 tc2(texCoord[2], texCoord[3]); Vec2 tc1(tc2.x, tc0.y); Vec2 tc3(tc0.x, tc2.y); uint32_t packedColor = 0xffffffff; { ColorB tempColor = color; tempColor.a = ((uint32_t) tempColor.a * alphaBlend) >> 8; packedColor = tempColor.pack_argb8888(); //note: this ends up in r,g,b,a order on little-endian machines } if (ctx.m_drawTextFlags & eDrawText_UseTransform) { v0 = ctx.m_transform * v0; v2 = ctx.m_transform * v2; v1 = ctx.m_transform * v1; v3 = ctx.m_transform * v3; } if (AddQuad(v0, v1, v2, v3, tc0, tc1, tc2, tc3, packedColor)) { ++numQuads; } else { return numQuads; } charX += advance + kerningOffset.x + trackingOffset; } } return numQuads; } AZ::FFont::TextScaleInfoInternal AZ::FFont::CalculateScaleInternal(const RHI::Viewport& viewport, const TextDrawContext& ctx) const { Vec2 size = GetRestoredFontSize(ctx); // in pixel if (ctx.m_sizeIn800x600) { ScaleCoord(viewport, size.x, size.y); } float rcpCellWidth; Vec2 scale; int fontTextureCellWidth = GetFontTexture()->GetCellWidth(); int fontTextureCellHeight = GetFontTexture()->GetCellHeight(); if (ctx.m_proportional) { rcpCellWidth = (1.0f / static_cast(fontTextureCellWidth)) * size.x; scale = Vec2(rcpCellWidth * ctx.m_widthScale, size.y / static_cast(fontTextureCellHeight)); } else { rcpCellWidth = size.x / 16.0f; scale = Vec2(rcpCellWidth * ctx.m_widthScale, size.y * ctx.m_widthScale / 16.0f); } return TextScaleInfoInternal(scale, rcpCellWidth); } size_t AZ::FFont::GetTextLength(const char* str, const bool asciiMultiLine) const { size_t len = 0; // parse the string, ignoring control characters const char* pChar = str; while (char ch = *pChar++) { if ((ch & 0xC0) == 0x80) { continue; // Skip UTF-8 continuation bytes, we count only the first byte of a code-point } switch (ch) { case '\\': { if (*pChar != 'n' || !asciiMultiLine) { break; } ++pChar; } case '\n': case '\r': case '\t': { continue; } break; case '$': { if (*pChar == '$') { ++pChar; } else if (*pChar) { ++pChar; continue; } } break; default: break; } ++len; } return len; } void AZ::FFont::WrapText(string& result, float maxWidth, const char* str, const TextDrawContext& ctx) { result = str; if (ctx.m_sizeIn800x600) { // ToDo: Update to work with Atom? LYN-3676 // maxWidth = ???->ScaleCoordX(maxWidth); } Vec2 strSize = GetTextSize(result.c_str(), true, ctx); if (strSize.x <= maxWidth) { return; } // Assume a given string has multiple lines of text if it's height is // greater than the height of its font. const bool multiLine = strSize.y > GetRestoredFontSize(ctx).y; int lastSpace = -1; const char* pLastSpace = NULL; float lastSpaceWidth = 0.0f; float curCharWidth = 0.0f; float curLineWidth = 0.0f; float biggestLineWidth = 0.0f; float widthSum = 0.0f; int curChar = 0; Unicode::CIterator pChar(result.c_str()); while (uint32_t ch = *pChar) { // Dollar sign escape codes. The following scenarios can happen with dollar signs embedded in a string. // The following character is... // 1. ... a digit, 'O' or 'o' which indicates a color code. Both characters a skipped in the width calculation. // 2. ... another dollar sign. Only 1 dollar sign is skipped in the width calculation. // 3. ... anything else. The dollar sign is processed in the width calculation. if (ctx.m_processSpecialChars && ch == '$') { ++pChar; char nextChar = *pChar; if (isdigit(nextChar) || nextChar == 'O' || nextChar == 'o') { ++pChar; continue; } else if (nextChar != '$') { --pChar; } } // get char width and sum it to the line width // Note: This is not unicode compatible, since char-width depends on surrounding context (ie, combining diacritics etc) char codepoint[5]; Unicode::Convert(codepoint, ch); curCharWidth = GetTextSize(codepoint, true, ctx).x; // keep track of spaces // they are good for splitting the string if (ch == ' ') { lastSpace = curChar; lastSpaceWidth = curLineWidth + curCharWidth; pLastSpace = pChar.GetPosition(); assert(*pLastSpace == ' '); } bool prevCharWasNewline = false; const bool notFirstChar = pChar.GetPosition() != result.c_str(); if (*pChar && notFirstChar) { const char* pPrevCharStr = pChar.GetPosition() - 1; prevCharWasNewline = pPrevCharStr[0] == '\n'; } // if line exceed allowed width, split it if (prevCharWasNewline || (curLineWidth + curCharWidth >= maxWidth && (*pChar))) { if (prevCharWasNewline) { // Reset the current line width to account for newline curLineWidth = curCharWidth; widthSum += curLineWidth; } else if ((lastSpace > 0) && ((curChar - lastSpace) < 16) && (curChar - lastSpace >= 0)) // 16 is the default threshold { *(char*)pLastSpace = '\n'; // This is safe inside UTF-8 because space is single-byte codepoint if (lastSpaceWidth > biggestLineWidth) { biggestLineWidth = lastSpaceWidth; } curLineWidth = curLineWidth - lastSpaceWidth + curCharWidth; widthSum += curLineWidth; } else { const char* buf = pChar.GetPosition(); size_t bytesProcessed = buf - result.c_str(); result.insert(bytesProcessed, '\n'); // Insert the newline, this invalidates the iterator buf = result.c_str() + bytesProcessed; // In case reallocation occurs, we ensure we are inside the new buffer assert(*buf == '\n'); pChar.SetPosition(buf); // pChar once again points inside the target string, at the current character assert(*pChar == ch); ++pChar; ++curChar; if (curLineWidth > biggestLineWidth) { biggestLineWidth = curLineWidth; } widthSum += curLineWidth; curLineWidth = curCharWidth; } // if we don't need any more line breaks, then just stop, but for // multiple lines we can't assume that there aren't any more // strings to wrap, so continue if (strSize.x - widthSum <= maxWidth && !multiLine) { break; } lastSpaceWidth = 0; lastSpace = 0; } else { curLineWidth += curCharWidth; } ++curChar; ++pChar; } } void AZ::FFont::GetGradientTextureCoord(float& minU, float& minV, float& maxU, float& maxV) const { const TextureSlot* slot = m_fontTexture->GetGradientSlot(); assert(slot); float invWidth = 1.0f / (float) m_fontTexture->GetWidth(); float invHeight = 1.0f / (float) m_fontTexture->GetHeight(); // deflate by one pixel to avoid bilinear filtering on the borders minU = slot->m_texCoords[0] + invWidth; minV = slot->m_texCoords[1] + invHeight; maxU = slot->m_texCoords[0] + (slot->m_characterWidth - 1) * invWidth; maxV = slot->m_texCoords[1] + (slot->m_characterHeight - 1) * invHeight; } unsigned int AZ::FFont::GetEffectId(const char* effectName) const { if (effectName) { for (size_t i = 0, numEffects = m_effects.size(); i < numEffects; ++i) { if (!strcmp(m_effects[i].m_name.c_str(), effectName)) { return static_cast(i); } } } return 0; } unsigned int AZ::FFont::GetNumEffects() const { return static_cast(m_effects.size()); } const char* AZ::FFont::GetEffectName(unsigned int effectId) const { return (effectId < m_effects.size()) ? m_effects[effectId].m_name.c_str() : nullptr; } Vec2 AZ::FFont::GetMaxEffectOffset(unsigned int effectId) const { Vec2 maxOffset(0.0f, 0.0f); if (effectId < m_effects.size()) { const FontEffect& fx = m_effects[effectId]; for (size_t i = 0, numPasses = fx.m_passes.size(); i < numPasses; ++i) { const FontRenderingPass* pass = &fx.m_passes[numPasses - i - 1]; // gather pass data Vec2 offset = pass->m_posOffset; if (maxOffset.x < offset.x) { maxOffset.x = offset.x; } if (maxOffset.y < offset.y) { maxOffset.y = offset.y; } } } return maxOffset; } bool AZ::FFont::DoesEffectHaveTransparency(unsigned int effectId) const { const size_t fxSize = m_effects.size(); const size_t fxIdx = effectId < fxSize ? effectId : 0; const FontEffect& fx = m_effects[fxIdx]; for (auto& pass : fx.m_passes) { // if the alpha is not 255 then there is transparency if (pass.m_color.a != 255) { return true; } } return false; } void AZ::FFont::AddCharsToFontTexture(const char* chars, int glyphSizeX, int glyphSizeY) { AtomFont::GlyphSize glyphSize(glyphSizeX, glyphSizeY); Prepare(chars, false, glyphSize); } Vec2 AZ::FFont::GetKerning(uint32_t leftGlyph, uint32_t rightGlyph, const TextDrawContext& ctx) const { return GetKerningInternal(GetDefaultWindowContext()->GetViewport(), leftGlyph, rightGlyph, ctx); } Vec2 AZ::FFont::GetKerningInternal(const RHI::Viewport& viewport, uint32_t leftGlyph, uint32_t rightGlyph, const TextDrawContext& ctx) const { const TextScaleInfoInternal scaleInfo(CalculateScaleInternal(viewport, ctx)); return m_fontTexture->GetKerning(leftGlyph, rightGlyph) * scaleInfo.scale.x; } float AZ::FFont::GetAscender(const TextDrawContext& ctx) const { return (ctx.m_size.y * m_fontTexture->GetAscenderToHeightRatio()); } float AZ::FFont::GetBaseline(const TextDrawContext& ctx) const { return GetBaselineInternal(GetDefaultWindowContext()->GetViewport(), ctx); } float AZ::FFont::GetBaselineInternal(const RHI::Viewport& viewport, const TextDrawContext& ctx) const { const TextScaleInfoInternal scaleInfo(CalculateScaleInternal(viewport, ctx)); // Calculate baseline the same way as the font renderer which uses the glyph height * size ratio. // Adding 1 because FontTexture always adds 1 to the char height in GetTextureCoord return (round(m_fontTexture->GetCellHeight() * GetSizeRatio()) + 1.0f) * scaleInfo.scale.y; } bool AZ::FFont::InitTexture() { using namespace AZ; RHI::Format rhiImageFormat = RHI::Format::R8_UNORM; int width = m_fontTexture->GetWidth(); int height = m_fontTexture->GetHeight(); uint8_t* fontImageData = m_fontTexture->GetBuffer(); uint32_t fontImageDataSize = RHI::GetFormatSize(rhiImageFormat) * width * height; Data::Instance streamingImagePool = RPI::ImageSystemInterface::Get()->GetSystemStreamingPool(); m_fontStreamingImage = RPI::StreamingImage::CreateFromCpuData( *streamingImagePool.get(), RHI::ImageDimension::Image2D, RHI::Size(width, height, 1), rhiImageFormat, fontImageData, fontImageDataSize); m_fontImage = m_fontStreamingImage->GetRHIImage(); m_fontImage->SetName(Name(m_name.c_str())); m_fontImageVersion = 0; return true; } bool AZ::FFont::UpdateTexture() { using namespace AZ; if (!m_fontImage) { return false; } if (m_fontTexture->GetWidth() != m_fontImage->GetDescriptor().m_size.m_width || m_fontTexture->GetHeight() != m_fontImage->GetDescriptor().m_size.m_height) { AZ_Assert(false, "AtomFont::FFont:::UpdateTexture size mismatch between texture and image!"); return false; } RHI::ImageSubresourceRange range; range.m_mipSliceMin = 0; range.m_mipSliceMax = 0; range.m_arraySliceMin = 0; range.m_arraySliceMax = 0; RHI::ImageSubresourceLayoutPlaced layout; m_fontImage->GetSubresourceLayouts(range, &layout, nullptr); RHI::ImageUpdateRequest imageUpdateReq; imageUpdateReq.m_image = m_fontImage.get(); imageUpdateReq.m_imageSubresource = RHI::ImageSubresource{ 0, 0 }; imageUpdateReq.m_sourceData = m_fontTexture->GetBuffer(); imageUpdateReq.m_sourceSubresourceLayout = layout; m_fontStreamingImage->UpdateImageContents(imageUpdateReq); return true; } bool AZ::FFont::InitCache() { m_fontTexture->CreateGradientSlot(); // precache (not required but for faster printout later) const char first = ' '; const char last = '~'; char buf[last - first + 2]; char* p = buf; // precache all [normal] printable characters to the string (missing ones are updated on demand) for (int i = first; i <= last; ++i) { *p++ = i; } *p = 0; Prepare(buf, false); return true; } AZ::FFont::FontEffect* AZ::FFont::AddEffect(const char* effectName) { m_effects.push_back(FontEffect(effectName)); return &m_effects[m_effects.size() - 1]; } AZ::FFont::FontEffect* AZ::FFont::GetDefaultEffect() { return &m_effects[0]; } void AZ::FFont::Prepare(const char* str, bool updateTexture, const AtomFont::GlyphSize& glyphSize) { const bool rerenderGlyphs = m_sizeBehavior == SizeBehavior::Rerender; const AtomFont::GlyphSize usedGlyphSize = rerenderGlyphs ? glyphSize : AtomFont::defaultGlyphSize; bool texUpdateNeeded = m_fontTexture->PreCacheString(str, nullptr, m_sizeRatio, usedGlyphSize, m_fontHintParams) == 1 || m_fontTexDirty; if (updateTexture && texUpdateNeeded && m_fontImage) { UpdateTexture(); m_fontTexDirty = false; ++m_fontImageVersion; // Let any listeners know that the font texture has changed // TODO Update to an AZ::Event when Cry use of this bus is cleaned out. EBUS_EVENT(FontNotificationBus, OnFontTextureUpdated, this); } else { m_fontTexDirty = texUpdateNeeded; } } Vec2 AZ::FFont::GetRestoredFontSize(const TextDrawContext& ctx) const { // Calculate the scale that we need to apply to the text size to ensure // it's on-screen size is the same regardless of the slot scaling needed // to fit the glyphs of the font within the font texture slots. float restoringScale = IFFontConstants::defaultSizeRatio / m_sizeRatio; return Vec2(ctx.m_size.x * restoringScale, ctx.m_size.y * restoringScale); } void AZ::FFont::ScaleCoord(const RHI::Viewport& viewport, float& x, float& y) const { float width = viewport.m_maxX - viewport.m_minX; float height = viewport.m_maxY - viewport.m_minY; x *= width / WindowScaleWidth; y *= height / WindowScaleHeight; } static void SetCommonContextFlags(AZ::TextDrawContext& ctx, const AzFramework::TextDrawParameters& params) { if (params.m_hAlign == AzFramework::TextHorizontalAlignment::Center) { ctx.m_drawTextFlags |= eDrawText_Center; } if (params.m_hAlign == AzFramework::TextHorizontalAlignment::Right) { ctx.m_drawTextFlags |= eDrawText_Right; } if (params.m_vAlign == AzFramework::TextVerticalAlignment::Center) { ctx.m_drawTextFlags |= eDrawText_CenterV; } if (params.m_vAlign == AzFramework::TextVerticalAlignment::Bottom) { ctx.m_drawTextFlags |= eDrawText_Bottom; } if (params.m_monospace) { ctx.m_drawTextFlags |= eDrawText_Monospace; } if (params.m_depthTest) { ctx.m_drawTextFlags |= eDrawText_DepthTest; } if (params.m_virtual800x600ScreenSize) { ctx.m_drawTextFlags |= eDrawText_800x600; } if (!params.m_scaleWithWindow) { ctx.m_drawTextFlags |= eDrawText_FixedSize; } if (params.m_useTransform) { ctx.m_drawTextFlags |= eDrawText_UseTransform; ctx.SetTransform(AZMatrix3x4ToLYMatrix3x4(params.m_transform)); } } AZ::FFont::DrawParameters AZ::FFont::ExtractDrawParameters(const AzFramework::TextDrawParameters& params, AZStd::string_view text, bool forceCalculateSize) { DrawParameters internalParams; if (params.m_drawViewportId == AzFramework::InvalidViewportId || text.empty()) { return internalParams; } float posX = params.m_position.GetX(); float posY = params.m_position.GetY(); internalParams.m_viewportContext = AZ::Interface::Get()->GetViewportContextById(params.m_drawViewportId); const AZ::RHI::Viewport& viewport = internalParams.m_viewportContext->GetWindowContext()->GetViewport(); internalParams.m_viewport = &viewport; if (params.m_virtual800x600ScreenSize) { posX *= WindowScaleWidth / (viewport.m_maxX - viewport.m_minX); posY *= WindowScaleHeight / (viewport.m_maxY - viewport.m_minY); } internalParams.m_ctx.SetBaseState(GS_NODEPTHTEST); internalParams.m_ctx.SetColor(AZColorToLYColorF(params.m_color)); internalParams.m_ctx.SetEffect(params.m_effectIndex); internalParams.m_ctx.SetCharWidthScale((params.m_monospace || params.m_scaleWithWindow) ? 0.5f : 1.0f); internalParams.m_ctx.EnableFrame(false); internalParams.m_ctx.SetProportional(!params.m_monospace && params.m_scaleWithWindow); internalParams.m_ctx.SetSizeIn800x600(params.m_scaleWithWindow && params.m_virtual800x600ScreenSize); internalParams.m_ctx.SetSize(AZVec2ToLYVec2(AZ::Vector2(params.m_textSizeFactor, params.m_textSizeFactor) * params.m_scale * internalParams.m_viewportContext->GetDpiScalingFactor())); internalParams.m_ctx.SetLineSpacing(params.m_lineSpacing); if (params.m_hAlign != AzFramework::TextHorizontalAlignment::Left || params.m_vAlign != AzFramework::TextVerticalAlignment::Top || forceCalculateSize) { // We align based on the size of the default font effect because we do not want the // text to move when the font effect is changed unsigned int effectIndex = internalParams.m_ctx.m_fxIdx; internalParams.m_ctx.SetEffect(0); Vec2 textSize = GetTextSizeUInternal(viewport, text.data(), params.m_multiline, internalParams.m_ctx); internalParams.m_ctx.SetEffect(effectIndex); // If we're using virtual 800x600 coordinates, convert the text size from // pixels to that before using it as an offset. if (internalParams.m_ctx.m_sizeIn800x600) { float width = 1.0f; float height = 1.0f; ScaleCoord(viewport, width, height); textSize.x /= width; textSize.y /= height; } if (params.m_hAlign == AzFramework::TextHorizontalAlignment::Center) { posX -= textSize.x * 0.5f; } else if (params.m_hAlign == AzFramework::TextHorizontalAlignment::Right) { posX -= textSize.x; } if (params.m_vAlign == AzFramework::TextVerticalAlignment::Center) { posY -= textSize.y * 0.5f; } else if (params.m_vAlign == AzFramework::TextVerticalAlignment::Bottom) { posY -= textSize.y; } internalParams.m_size = AZ::Vector2{textSize.x, textSize.y} * internalParams.m_viewportContext->GetDpiScalingFactor(); } SetCommonContextFlags(internalParams.m_ctx, params); internalParams.m_ctx.m_drawTextFlags |= eDrawText_2D; internalParams.m_position = AZ::Vector2{posX, posY}; return internalParams; } void AZ::FFont::DrawScreenAlignedText2d( const AzFramework::TextDrawParameters& params, AZStd::string_view text) { DrawParameters internalParams = ExtractDrawParameters(params, text, false); if (!internalParams.m_viewportContext) { return; } DrawStringUInternal( *internalParams.m_viewport, internalParams.m_viewportContext, internalParams.m_position.GetX(), internalParams.m_position.GetY(), params.m_position.GetZ(), // Z text.data(), params.m_multiline, internalParams.m_ctx ); } void AZ::FFont::DrawScreenAlignedText3d( const AzFramework::TextDrawParameters& params, AZStd::string_view text) { DrawParameters internalParams = ExtractDrawParameters(params, text, false); if (!internalParams.m_viewportContext) { return; } AZ::RPI::ViewPtr currentView = internalParams.m_viewportContext->GetDefaultView(); if (!currentView) { return; } AZ::Vector3 positionNDC = AzFramework::WorldToScreenNDC( params.m_position, currentView->GetWorldToViewMatrix(), currentView->GetViewToClipMatrix() ); // Text behind the camera shouldn't get rendered. WorldToScreenNDC returns values in the range 0 - 1, so Z < 0.5 is behind the screen // and >= 0.5 is in front of the screen. if (positionNDC.GetZ() < 0.5f) { return; } internalParams.m_ctx.m_sizeIn800x600 = false; DrawStringUInternal( *internalParams.m_viewport, internalParams.m_viewportContext, positionNDC.GetX() * internalParams.m_viewport->GetWidth(), (1.0f - positionNDC.GetY()) * internalParams.m_viewport->GetHeight(), positionNDC.GetZ(), // Z text.data(), params.m_multiline, internalParams.m_ctx ); } AZ::Vector2 AZ::FFont::GetTextSize(const AzFramework::TextDrawParameters& params, AZStd::string_view text) { DrawParameters sizeParams = ExtractDrawParameters(params, text, true); return sizeParams.m_size; } #endif //USE_NULLFONT_ALWAYS