You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/CryEngine/CryFont/FFont.cpp

1549 lines
46 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.
// Description : Font class.
#include "CryFont_precompiled.h"
#if !defined(USE_NULLFONT_ALWAYS)
#include "FFont.h"
#include "CryFont.h"
#include "FontTexture.h"
#include "UnicodeIterator.h"
#include <AzCore/std/parallel/lock.h>
#include <AzFramework/Archive/Archive.h>
static ColorB ColorTable[10] =
{
ColorB(0x00, 0x00, 0x00), // black
ColorB(0xff, 0xff, 0xff), // white
ColorB(0x00, 0x00, 0xff), // blue
ColorB(0x00, 0xff, 0x00), // green
ColorB(0xff, 0x00, 0x00), // red
ColorB(0x00, 0xff, 0xff), // cyan
ColorB(0xff, 0xff, 0x00), // yellow
ColorB(0xff, 0x00, 0xff), // purple
ColorB(0xff, 0x80, 0x00), // orange
ColorB(0x8f, 0x8f, 0x8f), // grey
};
static const int TabCharCount = 4;
static const size_t MsgBufferSize = 1024;
static const size_t MaxDrawVBQuads = 128;
CFFont::CFFont(ISystem* pSystem, CCryFont* pCryFont, const char* pFontName)
: m_name(pFontName)
, m_curPath("")
, m_pFontTexture(0)
, m_fontBufferSize(0)
, m_pFontBuffer(0)
, m_texID(-1)
, m_textureVersion(0)
, m_pSystem(pSystem)
, m_pCryFont(pCryFont)
, m_fontTexDirty(false)
, m_effects()
, m_pDrawVB(0)
, m_nRefCount(0)
, m_monospacedFont(false)
{
assert(m_name.c_str());
assert(m_pSystem);
assert(m_pCryFont);
// create default effect
SEffect* pEffect = AddEffect("default");
pEffect->AddPass();
m_pDrawVB = new SVF_P3F_C4B_T2F[MaxDrawVBQuads * 6];
AddRef();
}
CFFont::~CFFont()
{
// The font should already be unregistered through a call to
// CFFont::Release() at this point.
CRY_ASSERT(m_pCryFont == nullptr);
Free();
SAFE_DELETE_ARRAY(m_pDrawVB);
}
int32 CFFont::AddRef()
{
int32 nRef = CryInterlockedIncrement(&m_nRefCount);
return nRef;
}
int32 CFFont::Release()
{
if (m_nRefCount > 0)
{
int32 nRef = CryInterlockedDecrement(&m_nRefCount);
if (nRef < 0)
{
CryFatalError("CBaseResource::Release() called more than once!");
}
if (nRef <= 0)
{
if (m_pCryFont)
{
AZStd::lock_guard<AZStd::recursive_mutex> locker(m_fontMutex);
// Unregister font so no one can increase it's ref count while it is queued for deletion
m_pCryFont->UnregisterFont(m_name);
m_pCryFont = nullptr;
}
gEnv->pRenderer->DeleteFont(this);
return 0;
}
return nRef;
}
return 0;
}
// Load a font from a TTF file
bool CFFont::Load(const char* pFontFilePath, unsigned int width, unsigned int height, unsigned int widthNumSlots, unsigned int heightNumSlots, unsigned int flags, float sizeRatio)
{
if (!pFontFilePath)
{
return false;
}
Free();
auto pPak = gEnv->pCryPak;
string fullFile;
if (pPak->IsAbsPath(pFontFilePath))
{
fullFile = pFontFilePath;
}
else
{
fullFile = m_curPath + pFontFilePath;
}
int iSmoothMethod = (flags & TTFFLAG_SMOOTH_MASK) >> TTFFLAG_SMOOTH_SHIFT;
int iSmoothAmount = (flags & TTFFLAG_SMOOTH_AMOUNT_MASK) >> TTFFLAG_SMOOTH_AMOUNT_SHIFT;
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* pBuffer = new unsigned char[fileSize];
if (!pPak->FReadRaw(pBuffer, fileSize, 1, fileHandle))
{
pPak->FClose(fileHandle);
delete [] pBuffer;
return false;
}
pPak->FClose(fileHandle);
if (!m_pFontTexture)
{
m_pFontTexture = new CFontTexture();
}
if (!m_pFontTexture || !m_pFontTexture->CreateFromMemory(pBuffer, (int)fileSize, width, height, iSmoothMethod, iSmoothAmount, widthNumSlots, heightNumSlots, sizeRatio))
{
delete [] pBuffer;
return false;
}
m_monospacedFont = m_pFontTexture->GetMonospaced();
m_pFontBuffer = pBuffer;
m_fontBufferSize = fileSize;
m_fontTexDirty = false;
m_sizeRatio = sizeRatio;
InitCache();
return true;
}
void CFFont::Free()
{
IRenderer* pRenderer = gEnv->pRenderer;
if (m_texID >= 0 && pRenderer)
{
pRenderer->RemoveTexture(m_texID);
m_texID = -1;
m_textureVersion = 0;
}
delete m_pFontTexture;
m_pFontTexture = 0;
delete [] m_pFontBuffer;
m_pFontBuffer = 0;
}
void CFFont::DrawString(float x, float y, const char* pStr, const bool asciiMultiLine, const STextDrawContext& ctx)
{
IF (!pStr, 0)
{
return;
}
DrawStringUInternal(x, y, 1.0f, pStr, asciiMultiLine, ctx);
}
void CFFont::DrawString(float x, float y, float z, const char* pStr, const bool asciiMultiLine, const STextDrawContext& ctx)
{
IF (!pStr, 0)
{
return;
}
DrawStringUInternal(x, y, z, pStr, asciiMultiLine, ctx);
}
void CFFont::DrawStringUInternal(float x, float y, float z, const char* pStr, const bool asciiMultiLine, const STextDrawContext& ctx)
{
IF (!pStr || !m_pFontTexture || ctx.m_fxIdx >= m_effects.size() || m_effects[ctx.m_fxIdx].m_passes.empty(), 0)
{
return;
}
AZStd::lock_guard<AZStd::recursive_mutex> locker(m_fontMutex);
IRenderer* pRenderer = gEnv->pRenderer;
AZ_Assert(pRenderer, "gEnv->pRenderer is NULL");
pRenderer->DrawStringU(this, x, y, z, pStr, asciiMultiLine, ctx);
}
ILINE uint32 COLCONV(uint32 clr)
{
return ((clr & 0xff00ff00) | ((clr & 0xff0000) >> 16) | ((clr & 0xff) << 16));
}
void CFFont::RenderCallback(float x, float y, float z, const char* pStr, const bool asciiMultiLine, const STextDrawContext& ctx)
{
AZStd::lock_guard<AZStd::recursive_mutex> locker(m_fontMutex);
const size_t fxSize = m_effects.size();
IF (fxSize && m_texID == -1 && !InitTexture(), 0)
{
return;
}
// if the font is about to be deleted then m_pCryFont can be nullptr
if (!m_pCryFont)
{
return;
}
SVF_P3F_C4B_T2F* pVertex = m_pDrawVB;
size_t vbOffset = 0;
bool isFontRenderStateSet = false;
TransformationMatrices backupSceneMatrices;
IRenderer* pRenderer = gEnv->pRenderer;
AZ_Assert(pRenderer, "gEnv->pRenderer is NULL");
int baseState = ctx.m_baseState;
bool overrideViewProjMatrices = ctx.m_overrideViewProjMatrices;
int texId = m_texID;
// Local function to share code needed when we render the vertex buffer so far
AZStd::function<void(void)> RenderVB = [&pVertex, &vbOffset, pRenderer]
{
gEnv->pRenderer->DrawDynVB(pVertex, 0, vbOffset, 0, prtTriangleList);
vbOffset = 0;
};
// Local function that is passed into CreateQuadsForText as the AddQuad function
AddFunction AddQuad = [&pVertex, &vbOffset, RenderVB]
(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 packedColor)
{
// define char quad
pVertex[vbOffset].xyz = v0;
pVertex[vbOffset].color.dcolor = packedColor;
pVertex[vbOffset].st = tc0;
pVertex[vbOffset + 1].xyz = v1;
pVertex[vbOffset + 1].color.dcolor = packedColor;
pVertex[vbOffset + 1].st = tc1;
pVertex[vbOffset + 2].xyz = v2;
pVertex[vbOffset + 2].color.dcolor = packedColor;
pVertex[vbOffset + 2].st = tc2;
pVertex[vbOffset + 3].xyz = v2;
pVertex[vbOffset + 3].color.dcolor = packedColor;
pVertex[vbOffset + 3].st = tc2;
pVertex[vbOffset + 4].xyz = v3;
pVertex[vbOffset + 4].color.dcolor = packedColor;
pVertex[vbOffset + 4].st = tc3;
pVertex[vbOffset + 5].xyz = v0;
pVertex[vbOffset + 5].color.dcolor = packedColor;
pVertex[vbOffset + 5].st = tc0;
vbOffset += 6;
if (vbOffset >= MaxDrawVBQuads * 6)
{
RenderVB();
}
};
// Local function that is passed into CreateQuadsForText as the BeginPass function
BeginPassFunction BeginPass = [&pVertex, &vbOffset, &isFontRenderStateSet, &backupSceneMatrices, texId, pRenderer, baseState, RenderVB, overrideViewProjMatrices]
(const SRenderingPass* pPass)
{
// We don't want to set this state before the call to CreateQuadsForText since that calls Prepare,
// which is needed before FontSetTexture
if (!isFontRenderStateSet)
{
pRenderer->FontSetTexture(texId, FILTER_TRILINEAR);
pRenderer->FontSetRenderingState(overrideViewProjMatrices, backupSceneMatrices);
gEnv->pRenderer->FontSetBlending(pPass->m_blendSrc, pPass->m_blendDest, baseState);
isFontRenderStateSet = true;
}
if (vbOffset > 0)
{
RenderVB();
}
gEnv->pRenderer->FontSetBlending(pPass->m_blendSrc, pPass->m_blendDest, baseState);
};
CreateQuadsForText(x, y, z, pStr, asciiMultiLine, ctx, AddQuad, BeginPass);
if (vbOffset > 0)
{
RenderVB();
}
// restore the old states
if (isFontRenderStateSet)
{
pRenderer->FontRestoreRenderingState(overrideViewProjMatrices, backupSceneMatrices);
}
}
Vec2 CFFont::GetTextSize(const char* pStr, const bool asciiMultiLine, const STextDrawContext& ctx)
{
IF (!pStr, 0)
{
return Vec2(0.0f, 0.0f);
}
return GetTextSizeUInternal(pStr, asciiMultiLine, ctx);
}
Vec2 CFFont::GetTextSizeUInternal(const char* pStr, const bool asciiMultiLine, const STextDrawContext& ctx)
{
const size_t fxSize = m_effects.size();
IF (!pStr || !m_pFontTexture || !fxSize, 0)
{
return Vec2(0, 0);
}
AZStd::lock_guard<AZStd::recursive_mutex> locker(m_fontMutex);
Prepare(pStr, false, ctx.m_requestSize);
IRenderer* pRenderer = gEnv->pRenderer;
AZ_Assert(pRenderer, "gEnv->pRenderer is NULL");
// 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)
{
pRenderer->ScaleCoord(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(ctx));
float maxW = 0;
float maxH = 0;
const size_t fxIdx = ctx.m_fxIdx < fxSize ? ctx.m_fxIdx : 0;
const SEffect& fx = m_effects[fxIdx];
for (size_t i = 0, numPasses = fx.m_passes.size(); i < numPasses; ++i)
{
const SRenderingPass* pPass = &fx.m_passes[numPasses - i - 1];
// gather pass data
Vec2 offset = pPass->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<const char*, false> pChar(pStr);
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;
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 * 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 Vec2i requestSize = rerenderGlyphs ? ctx.m_requestSize : CCryFont::defaultGlyphSize;
int horizontalAdvance = m_pFontTexture->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_pFontTexture->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 CFFont::GetNumQuadsForText(const char* pStr, const bool asciiMultiLine, const STextDrawContext& ctx)
{
uint32 numQuads = 0;
const size_t fxSize = m_effects.size();
const size_t fxIdx = ctx.m_fxIdx < fxSize ? ctx.m_fxIdx : 0;
const SEffect& 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<const char*, false> pChar(pStr);
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 CFFont::WriteTextQuadsToBuffers(SVF_P2F_C4B_T2F_F4B* verts, uint16* indices, uint32 maxQuads, float x, float y, float z, const char* pStr, const bool asciiMultiLine, const STextDrawContext& ctx)
{
AZStd::lock_guard<AZStd::recursive_mutex> locker(m_fontMutex);
uint32 numQuadsWritten = 0;
const size_t fxSize = m_effects.size();
IF (fxSize && m_texID == -1 && !InitTexture(), 0)
{
return numQuadsWritten;
}
// if the font is about to be deleted then m_pCryFont can be nullptr
if (!m_pCryFont)
{
return numQuadsWritten;
}
SVF_P2F_C4B_T2F_F4B* pVertex = verts;
uint16* pIndex = indices;
size_t vbOffset = 0;
size_t ibOffset = 0;
// Local function that is passed into CreateQuadsForText as the AddQuad function
AddFunction AddQuad = [&pVertex, &pIndex, &vbOffset, &ibOffset, 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 packedColor)
{
Vec2 xy0(v0);
Vec2 xy1(v1);
Vec2 xy2(v2);
Vec2 xy3(v3);
AZ_Assert(vbOffset + 3 < maxQuads * 4, "Vertex buffer overflow");
AZ_Assert(ibOffset + 5 < maxQuads * 6, "Index buffer overflow");
// 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
pVertex[vbOffset].xy = xy0;
pVertex[vbOffset].color.dcolor = packedColor;
pVertex[vbOffset].st = tc0;
pVertex[vbOffset].texIndex = 0;
pVertex[vbOffset].texHasColorChannel = 0;
pVertex[vbOffset].texIndex2 = 0;
pVertex[vbOffset].pad = 0;
pVertex[vbOffset + 1].xy = xy1;
pVertex[vbOffset + 1].color.dcolor = packedColor;
pVertex[vbOffset + 1].st = tc1;
pVertex[vbOffset + 1].texIndex = 0;
pVertex[vbOffset + 1].texHasColorChannel = 0;
pVertex[vbOffset + 1].texIndex2 = 0;
pVertex[vbOffset + 1].pad = 0;
pVertex[vbOffset + 2].xy = xy2;
pVertex[vbOffset + 2].color.dcolor = packedColor;
pVertex[vbOffset + 2].st = tc2;
pVertex[vbOffset + 2].texIndex = 0;
pVertex[vbOffset + 2].texHasColorChannel = 0;
pVertex[vbOffset + 2].texIndex2 = 0;
pVertex[vbOffset + 2].pad = 0;
pVertex[vbOffset + 3].xy = xy3;
pVertex[vbOffset + 3].color.dcolor = packedColor;
pVertex[vbOffset + 3].st = tc3;
pVertex[vbOffset + 3].texIndex = 0;
pVertex[vbOffset + 3].texHasColorChannel = 0;
pVertex[vbOffset + 3].texIndex2 = 0;
pVertex[vbOffset + 3].pad = 0;
pIndex[ibOffset] = vbOffset;
pIndex[ibOffset + 1] = vbOffset + 1;
pIndex[ibOffset + 2] = vbOffset + 2;
pIndex[ibOffset + 3] = vbOffset + 2;
pIndex[ibOffset + 4] = vbOffset + 3;
pIndex[ibOffset + 5] = vbOffset + 0;
vbOffset += 4;
ibOffset += 6;
++numQuadsWritten;
}
};
// Local function that is passed into CreateQuadsForText as the BeginPass function
BeginPassFunction BeginPass = []
(const SRenderingPass* /* pPass */)
{
};
IRenderer* pRenderer = gEnv->pRenderer;
CreateQuadsForText(x, y, z, pStr, asciiMultiLine, ctx, AddQuad, BeginPass);
return numQuadsWritten;
}
int CFFont::GetFontTextureId()
{
if (m_texID == -1 && !InitTexture())
{
return -1;
}
return m_texID;
}
uint32 CFFont::GetFontTextureVersion()
{
return m_textureVersion;
}
void CFFont::CreateQuadsForText(float x, float y, float z, const char* pStr, const bool asciiMultiLine, const STextDrawContext& ctx,
AddFunction AddQuad, BeginPassFunction BeginPass)
{
const size_t fxSize = m_effects.size();
Prepare(pStr, true, ctx.m_requestSize);
const size_t fxIdx = ctx.m_fxIdx < fxSize ? ctx.m_fxIdx : 0;
const SEffect& fx = m_effects[fxIdx];
bool isRGB = m_pCryFont->RndPropIsRGBA();
float halfTexelShift = m_pCryFont->RndPropHalfTexelOffset();
bool passZeroColorOverridden = ctx.IsColorOverridden();
uint32 alphaBlend = passZeroColorOverridden ? ctx.m_colorOverride.a : fx.m_passes[0].m_color.a;
if (alphaBlend > 128)
{
++alphaBlend; // 0..256 for proper blending
}
IRenderer* pRenderer = gEnv->pRenderer;
AZ_Assert(pRenderer, "gEnv->pRenderer is NULL");
// 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)
{
pRenderer->ScaleCoord(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(ctx));
Vec2 baseXY = Vec2(x, y); // in pixels
if (ctx.m_sizeIn800x600)
{
pRenderer->ScaleCoord(baseXY.x, baseXY.y);
}
// Apply overscan border
const int flags = ctx.GetFlags();
if ((flags & eDrawText_2D) && !(flags & eDrawText_IgnoreOverscan))
{
Vec2 overscanBorder = Vec2(0.0f, 0.0f);
pRenderer->EF_Query(EFQ_OverscanBorders, overscanBorder);
const float screenWidth = (float)pRenderer->GetOverlayWidth();
const float screenHeight = (float)pRenderer->GetOverlayHeight();
Vec2 overscanBorderOffset = ZERO;
if (!(flags & eDrawText_Center))
{
overscanBorderOffset.x = (overscanBorder.x * screenWidth);
}
if (!(flags & eDrawText_CenterV))
{
overscanBorderOffset.y = (overscanBorder.y * screenHeight);
}
if (flags & eDrawText_Right)
{
overscanBorderOffset.x = -overscanBorderOffset.x;
}
if (flags & eDrawText_Bottom)
{
overscanBorderOffset.y = -overscanBorderOffset.y;
}
baseXY += overscanBorderOffset;
}
// 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 SRenderingPass* pPass = &fx.m_passes[i];
if (!i)
{
alphaBlend = 256;
}
ColorB passColor = !i && passZeroColorOverridden ? ctx.m_colorOverride : fx.m_passes[i].m_color;
// gather pass data
Vec2 offset = pPass->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;
BeginPass(pPass);
if (drawFrame)
{
ColorB tempColor(255, 255, 255, 255);
uint32 frameColor = tempColor.pack_abgr8888();
if (!isRGB)
{
frameColor = COLCONV(frameColor);
}
Vec2 textSize = GetTextSizeUInternal(pStr, 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, 0)
{
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 && ((x1 - x0 <= 0.0f) || (y1 - y0 <= 0.0f)))
// culled = true;
}
IF (!culled, 1)
{
Vec3 v0(x0, y0, z);
Vec3 v2(x1, y1, z);
Vec3 v1(v2.x, v0.y, v0.z); // to avoid float->half conversion
Vec3 v3(v0.x, v2.y, v0.z); // to avoid float->half conversion
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);
AddQuad(v0, v1, v2, v3, uv, uv, uv, uv, frameColor);
}
}
// parse the string, ignoring control characters
uint32_t nextCh = 0;
Unicode::CIterator<const char*, false> pChar(pStr);
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;
continue;
}
break;
case '\r':
{
charX = baseXY.x + offset.x;
continue;
}
break;
case '\t':
{
if (ctx.m_proportional)
{
charX += TabCharCount * size.x * 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)
{
int colorIndex = (*pChar) - '0';
ColorB newColor = 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 Vec2i requestSize = rerenderGlyphs ? ctx.m_requestSize : CCryFont::defaultGlyphSize;
m_pFontTexture->GetTextureCoord(m_pFontTexture->GetCharSlot(ch, requestSize), texCoord, charSizeX, charSizeY, charOffsetX, charOffsetY, requestSize);
int horizontalAdvance = m_pFontTexture->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_pFontTexture->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;
}
}
newX += halfTexelShift;
newY += halfTexelShift;
newR += halfTexelShift;
newB += halfTexelShift;
//int offset = vbLen * 6;
Vec3 v0(newX, newY, z);
Vec3 v2(newR, newB, z);
Vec3 v1(v2.x, v0.y, v0.z); // to avoid float->half conversion
Vec3 v3(v0.x, v2.y, v0.z); // to avoid float->half conversion
Vec2 tc0(texCoord[0], texCoord[1]);
Vec2 tc2(texCoord[2], texCoord[3]);
Vec2 tc1(tc2.x, tc0.y); // to avoid float->half conversion
Vec2 tc3(tc0.x, tc2.y); // to avoid float->half conversion
uint32 packedColor = 0;
{
ColorB tempColor = color;
tempColor.a = ((uint32) tempColor.a * alphaBlend) >> 8;
packedColor = tempColor.pack_abgr8888();
if (!isRGB)
{
packedColor = COLCONV(packedColor);
}
}
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;
}
AddQuad(v0, v1, v2, v3, tc0, tc1, tc2, tc3, packedColor);
charX += advance + kerningOffset.x + trackingOffset;
}
}
}
CFFont::TextScaleInfoInternal CFFont::CalculateScaleInternal(const STextDrawContext& ctx) const
{
Vec2 size = GetRestoredFontSize(ctx); // in pixel
IRenderer* pRenderer = gEnv->pRenderer;
AZ_Assert(pRenderer, "gEnv->pRenderer is NULL");
if (ctx.m_sizeIn800x600)
{
pRenderer->ScaleCoord(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<float>(fontTextureCellWidth)) * size.x;
scale = Vec2(rcpCellWidth * ctx.m_widthScale, size.y / static_cast<float>(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 CFFont::GetTextLength(const char* pStr, const bool asciiMultiLine) const
{
size_t len = 0;
// parse the string, ignoring control characters
const char* pChar = pStr;
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 CFFont::WrapText(string& result, float maxWidth, const char* pStr, const STextDrawContext& ctx)
{
result = pStr;
if (ctx.m_sizeIn800x600)
{
maxWidth = gEnv->pRenderer->ScaleCoordX(maxWidth);
}
Vec2 strSize = GetTextSizeUInternal(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<const char*, false> 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 = GetTextSizeUInternal(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* pBuf = pChar.GetPosition();
size_t bytesProcessed = pBuf - result.c_str();
result.insert(bytesProcessed, '\n'); // Insert the newline, this invalidates the iterator
pBuf = result.c_str() + bytesProcessed; // In case reallocation occurs, we ensure we are inside the new buffer
assert(*pBuf == '\n');
pChar.SetPosition(pBuf); // 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 CFFont::GetMemoryUsage (ICrySizer* pSizer) const
{
pSizer->AddObject(m_name);
pSizer->AddObject(m_curPath);
pSizer->AddObject(m_pFontTexture);
pSizer->AddObject(m_pFontBuffer, m_fontBufferSize);
pSizer->AddObject(m_effects);
pSizer->AddObject(m_pDrawVB, sizeof(SVF_P3F_C4B_T2F) * MaxDrawVBQuads * 6);
}
void CFFont::GetGradientTextureCoord(float& minU, float& minV, float& maxU, float& maxV) const
{
const CTextureSlot* pSlot = m_pFontTexture->GetGradientSlot();
assert(pSlot);
float invWidth = 1.0f / (float) m_pFontTexture->GetWidth();
float invHeight = 1.0f / (float) m_pFontTexture->GetHeight();
// deflate by one pixel to avoid bilinear filtering on the borders
minU = pSlot->vTexCoord[0] + invWidth;
minV = pSlot->vTexCoord[1] + invHeight;
maxU = pSlot->vTexCoord[0] + (pSlot->iCharWidth - 1) * invWidth;
maxV = pSlot->vTexCoord[1] + (pSlot->iCharHeight - 1) * invHeight;
}
unsigned int CFFont::GetEffectId(const char* pEffectName) const
{
if (pEffectName)
{
for (size_t i = 0, numEffects = m_effects.size(); i < numEffects; ++i)
{
if (!strcmp(m_effects[i].m_name.c_str(), pEffectName))
{
return i;
}
}
}
return 0;
}
unsigned int CFFont::GetNumEffects() const
{
return m_effects.size();
}
const char* CFFont::GetEffectName(unsigned int effectId) const
{
return (effectId < m_effects.size()) ? m_effects[effectId].m_name.c_str() : nullptr;
}
Vec2 CFFont::GetMaxEffectOffset(unsigned int effectId) const
{
Vec2 maxOffset(0.0f, 0.0f);
if (effectId < m_effects.size())
{
const SEffect& fx = m_effects[effectId];
for (size_t i = 0, numPasses = fx.m_passes.size(); i < numPasses; ++i)
{
const SRenderingPass* pPass = &fx.m_passes[numPasses - i - 1];
// gather pass data
Vec2 offset = pPass->m_posOffset;
if (maxOffset.x < offset.x)
{
maxOffset.x = offset.x;
}
if (maxOffset.y < offset.y)
{
maxOffset.y = offset.y;
}
}
}
return maxOffset;
}
bool CFFont::DoesEffectHaveTransparency(unsigned int effectId) const
{
const size_t fxSize = m_effects.size();
const size_t fxIdx = effectId < fxSize ? effectId : 0;
const SEffect& 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 CFFont::AddCharsToFontTexture(const char* pChars, int glyphSizeX, int glyphSizeY)
{
AZStd::lock_guard<AZStd::recursive_mutex> locker(m_fontMutex);
Vec2i glyphSize(glyphSizeX, glyphSizeY);
Prepare(pChars, false, glyphSize);
}
Vec2 CFFont::GetKerning(uint32_t leftGlyph, uint32_t rightGlyph, const STextDrawContext& ctx) const
{
const TextScaleInfoInternal scaleInfo(CalculateScaleInternal(ctx));
return m_pFontTexture->GetKerning(leftGlyph, rightGlyph) * scaleInfo.scale.x;
}
float CFFont::GetAscender(const STextDrawContext& ctx) const
{
return (ctx.m_size.y * m_pFontTexture->GetAscenderToHeightRatio());
}
float CFFont::GetBaseline(const STextDrawContext& ctx) const
{
const TextScaleInfoInternal scaleInfo(CalculateScaleInternal(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_pFontTexture->GetCellHeight() * GetSizeRatio()) + 1.0f) * scaleInfo.scale.y;
}
bool CFFont::InitTexture()
{
m_texID = gEnv->pRenderer->FontCreateTexture(m_pFontTexture->GetWidth(), m_pFontTexture->GetHeight(), (uint8*)m_pFontTexture->GetBuffer(), eTF_A8, IRenderer::FontCreateTextureGenMipsDefaultValue, m_name.c_str());
m_textureVersion = 0;
return m_texID >= 0;
}
bool CFFont::InitCache()
{
m_pFontTexture->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;
}
CFFont::SEffect* CFFont::AddEffect(const char* pEffectName)
{
m_effects.push_back(SEffect(pEffectName));
return &m_effects[m_effects.size() - 1];
}
CFFont::SEffect* CFFont::GetDefaultEffect()
{
return &m_effects[0];
}
void CFFont::Prepare(const char* pStr, bool updateTexture, const Vec2i& glyphSize)
{
const bool rerenderGlyphs = m_sizeBehavior == SizeBehavior::Rerender;
const Vec2i usedGlyphSize = rerenderGlyphs ? glyphSize : CCryFont::defaultGlyphSize;
bool texUpdateNeeded = m_pFontTexture->PreCacheString(pStr, nullptr, m_sizeRatio, usedGlyphSize, m_fontHintParams) == 1 || m_fontTexDirty;
if (updateTexture && texUpdateNeeded && m_texID >= 0)
{
gEnv->pRenderer->FontUpdateTexture(m_texID, 0, 0, m_pFontTexture->GetWidth(), m_pFontTexture->GetHeight(), (unsigned char*)m_pFontTexture->GetBuffer());
m_fontTexDirty = false;
++m_textureVersion;
// Let any listeners know that the font texture has changed
EBUS_EVENT(FontNotificationBus, OnFontTextureUpdated, this);
}
else
{
m_fontTexDirty = texUpdateNeeded;
}
}
Vec2 CFFont::GetRestoredFontSize(const STextDrawContext& 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);
}
#endif