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.
1814 lines
57 KiB
C++
1814 lines
57 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 <AtomLyIntegration/AtomFont/AtomFont_precompiled.h>
|
|
|
|
#if !defined(USE_NULLFONT_ALWAYS)
|
|
|
|
#include <AzCore/Math/Color.h>
|
|
#include <AzCore/Math/Matrix4x4.h>
|
|
#include <AzCore/Math/MatrixUtils.h>
|
|
#include <AzCore/Casting/numeric_cast.h>
|
|
|
|
#include <AzFramework/Viewport/ViewportScreen.h>
|
|
#include <AzFramework/Viewport/ScreenGeometry.h>
|
|
|
|
#include <AzFramework/Archive/Archive.h>
|
|
|
|
#include <AtomLyIntegration/AtomFont/FFont.h>
|
|
#include <AtomLyIntegration/AtomFont/AtomFont.h>
|
|
#include <AtomLyIntegration/AtomFont/FontTexture.h>
|
|
#include <CryCommon/UnicodeIterator.h>
|
|
#include <CryCommon/MathConversion.h>
|
|
|
|
#include <AzCore/std/parallel/lock.h>
|
|
|
|
#include <Atom/RPI.Public/RPISystemInterface.h>
|
|
#include <Atom/RHI/RHISystemInterface.h>
|
|
#include <Atom/RPI.Public/Shader/Shader.h>
|
|
#include <Atom/RPI.Public/Image/StreamingImage.h>
|
|
#include <Atom/RPI.Public/RPIUtils.h>
|
|
#include <Atom/RPI.Public/ViewportContext.h>
|
|
#include <Atom/RPI.Public/View.h>
|
|
#include <Atom/RPI.Public/Image/ImageSystemInterface.h>
|
|
#include <Atom/RPI.Public/Image/StreamingImagePool.h>
|
|
#include <Atom/RPI.Public/ViewportContextManager.h>
|
|
#include <AzCore/Interface/Interface.h>
|
|
|
|
#include <Atom/RHI/Factory.h>
|
|
#include <Atom/RHI/DrawPacket.h>
|
|
#include <Atom/RHI/ImagePool.h>
|
|
|
|
#include <Atom/RHI.Reflect/InputStreamLayoutBuilder.h>
|
|
|
|
static const AZ::Vector2 UiDraw_TextSizeFactor = AZ::Vector2(12.0f, 12.0f);
|
|
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";
|
|
|
|
namespace ShaderInputs
|
|
{
|
|
static const char TextureIndexName[] = "m_texture";
|
|
static const char WorldToProjIndexName[] = "m_worldToProj";
|
|
static const char SamplerIndexName[] = "m_sampler";
|
|
}
|
|
|
|
AZ::FFont::FFont(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();
|
|
|
|
AddRef();
|
|
|
|
AZ::Render::Bootstrap::NotificationBus::Handler::BusConnect();
|
|
}
|
|
|
|
AZ::RPI::ViewportContextPtr AZ::FFont::GetDefaultViewportContext() const
|
|
{
|
|
auto viewContextManager = AZ::Interface<AZ::RPI::ViewportContextRequestsInterface>::Get();
|
|
return viewContextManager->GetDefaultViewportContext();
|
|
}
|
|
|
|
AZ::RPI::WindowContextSharedPtr AZ::FFont::GetDefaultWindowContext() const
|
|
{
|
|
if (auto defaultViewportContext = GetDefaultViewportContext())
|
|
{
|
|
return defaultViewportContext->GetWindowContext();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
bool AZ::FFont::InitFont(AZ::RPI::Scene* renderScene)
|
|
{
|
|
if (!renderScene)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto initializationState = InitializationState::Uninitialized;
|
|
// Do an atomic transition to Initializing if we're in the Uninitialized state.
|
|
// Otherwise, check the current state.
|
|
// If we're Initialized, there's no more work to be done, return true to indicate we're good to go.
|
|
// If we're Initializing (on another thread), return false to let the consumer know it's not safe for us to be used yet.
|
|
if (!m_fontInitializationState.compare_exchange_strong(initializationState, InitializationState::Initializing))
|
|
{
|
|
return initializationState == InitializationState::Initialized;
|
|
}
|
|
|
|
// Create and initialize DynamicDrawContext for font draw
|
|
AZ::RPI::Ptr<AZ::RPI::DynamicDrawContext> dynamicDraw = m_atomFont->GetOrCreateDynamicDrawForScene(renderScene);
|
|
|
|
// Save draw srg input indices for later use
|
|
Data::Instance<RPI::ShaderResourceGroup> drawSrg = dynamicDraw->NewDrawSrg();
|
|
const RHI::ShaderResourceGroupLayout* layout = drawSrg->GetAsset()->GetLayout();
|
|
|
|
m_fontShaderData.m_imageInputIndex = layout->FindShaderInputImageIndex(AZ::Name(ShaderInputs::TextureIndexName));
|
|
AZ_Error("AtomFont::FFont", m_fontShaderData.m_imageInputIndex.IsValid(), "Failed to find shader input constant %s.",
|
|
ShaderInputs::TextureIndexName);
|
|
|
|
m_fontShaderData.m_viewProjInputIndex = layout->FindShaderInputConstantIndex(AZ::Name(ShaderInputs::WorldToProjIndexName));
|
|
AZ_Error("AtomFont::FFont", m_fontShaderData.m_viewProjInputIndex.IsValid(), "Failed to find shader input constant %s.",
|
|
ShaderInputs::WorldToProjIndexName);
|
|
|
|
// 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;
|
|
|
|
m_fontInitializationState = InitializationState::Initialized;
|
|
return true;
|
|
}
|
|
|
|
AZ::FFont::~FFont()
|
|
{
|
|
AZ_Assert(m_atomFont == nullptr, "The font should already be unregistered through a call to AZ::FFont::Release()");
|
|
|
|
AZ::Render::Bootstrap::NotificationBus::Handler::BusDisconnect();
|
|
|
|
delete[] m_vertexBuffer;
|
|
delete[] m_indexBuffer;
|
|
|
|
Free();
|
|
}
|
|
|
|
int32_t AZ::FFont::AddRef()
|
|
{
|
|
ref_count::add_ref();
|
|
return aznumeric_cast<int32_t>(ref_count::use_count());
|
|
}
|
|
|
|
int32_t AZ::FFont::Release()
|
|
{
|
|
int32_t useCount = aznumeric_cast<int32_t>(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.
|
|
if (!viewportContext || !InitFont(viewportContext->GetRenderScene().get()))
|
|
{
|
|
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;
|
|
}
|
|
|
|
// if the font is about to be deleted then m_atomFont can be nullptr
|
|
if (!m_atomFont)
|
|
{
|
|
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 = 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<AZStd::mutex> lock(m_vertexDataMutex);
|
|
numQuads = CreateQuadsForText(viewport, x, y, z, str, asciiMultiLine, ctx, AddQuad);
|
|
}
|
|
|
|
if (numQuads)
|
|
{
|
|
auto dynamicDraw = m_atomFont->GetOrCreateDynamicDrawForScene(viewportContext->GetRenderScene().get());
|
|
//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<const char*, false> 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<const char*, false> 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;
|
|
}
|
|
|
|
// if the font is about to be deleted then m_atomFont can be nullptr
|
|
if (!m_atomFont)
|
|
{
|
|
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] = vertexOffset + 0;
|
|
indexData[indexOffset + 1] = vertexOffset + 1;
|
|
indexData[indexOffset + 2] = vertexOffset + 2;
|
|
indexData[indexOffset + 3] = vertexOffset + 2;
|
|
indexData[indexOffset + 4] = vertexOffset + 3;
|
|
indexData[indexOffset + 5] = 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_abgr8888(); //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<const char*, false> 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_abgr8888(); //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<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 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<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 = 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 i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
unsigned int AZ::FFont::GetNumEffects() const
|
|
{
|
|
return 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<RPI::StreamingImagePool> 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_fontInitializationState != InitializationState::Initialized || !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 (m_fontInitializationState == InitializationState::Initialized && 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;
|
|
}
|
|
|
|
|
|
void AZ::FFont::OnBootstrapSceneReady([[maybe_unused]] AZ::RPI::Scene* bootstrapScene)
|
|
{
|
|
InitFont(bootstrapScene);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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<AZ::RPI::ViewportContextRequestsInterface>::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.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(UiDraw_TextSizeFactor * params.m_scale));
|
|
internalParams.m_ctx.SetLineSpacing(params.m_lineSpacing);
|
|
if (params.m_monospace || !params.m_scaleWithWindow)
|
|
{
|
|
ScaleCoord(viewport, posX, posY);
|
|
}
|
|
|
|
if (params.m_hAlign != AzFramework::TextHorizontalAlignment::Left ||
|
|
params.m_vAlign != AzFramework::TextVerticalAlignment::Top ||
|
|
forceCalculateSize)
|
|
{
|
|
Vec2 textSize = GetTextSizeUInternal(viewport, text.data(), params.m_multiline, internalParams.m_ctx);
|
|
// 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};
|
|
}
|
|
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()
|
|
);
|
|
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
|
|
|