Files
WickedEngine/WickedEngine/wiFont.cpp
T

698 lines
18 KiB
C++

#include "wiFont.h"
#include "wiRenderer.h"
#include "wiResourceManager.h"
#include "wiHelper.h"
#include "ResourceMapping.h"
#include "ShaderInterop_Font.h"
#include "wiBackLog.h"
#include "wiTextureHelper.h"
#include "wiRectPacker.h"
#include "wiSpinLock.h"
#include "wiPlatform.h"
#include "wiEvent.h"
#include "Utility/stb_truetype.h"
#include <fstream>
#include <sstream>
#include <atomic>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <string>
using namespace std;
using namespace wiGraphics;
using namespace wiRectPacker;
#define WHITESPACE_SIZE ((float(params.size) + params.spacingX) * params.scaling * 0.25f)
#define TAB_SIZE (WHITESPACE_SIZE * 4)
#define LINEBREAK_SIZE ((float(params.size) + params.spacingY) * params.scaling)
namespace wiFont_Internal
{
string FONTPATH = wiHelper::GetOriginalWorkingDirectory() + "../WickedEngine/fonts/";
GPUBuffer constantBuffer;
BlendState blendState;
RasterizerState rasterizerState;
DepthStencilState depthStencilState;
Sampler sampler;
InputLayout inputLayout;
Shader vertexShader;
Shader pixelShader;
PipelineState PSO;
atomic_bool initialized { false };
Texture texture;
struct Glyph
{
float x;
float y;
float width;
float height;
uint16_t tc_left;
uint16_t tc_right;
uint16_t tc_top;
uint16_t tc_bottom;
};
unordered_map<int32_t, Glyph> glyph_lookup;
unordered_map<int32_t, rect_xywh> rect_lookup;
// pack glyph identifiers to a 32-bit hash:
// height: 10 bits (height supported: 0 - 1023)
// style: 6 bits (number of font styles supported: 0 - 63)
// code: 16 bits (character code range supported: 0 - 65535)
constexpr int32_t glyphhash(int code, int style, int height) { return ((code & 0xFFFF) << 16) | ((style & 0x3F) << 10) | (height & 0x3FF); }
constexpr int codefromhash(int64_t hash) { return int((hash >> 16) & 0xFFFF); }
constexpr int stylefromhash(int64_t hash) { return int((hash >> 10) & 0x3F); }
constexpr int heightfromhash(int64_t hash) { return int((hash >> 0) & 0x3FF); }
unordered_set<int32_t> pendingGlyphs;
wiSpinLock glyphLock;
struct wiFontStyle
{
string name;
vector<uint8_t> fontBuffer;
stbtt_fontinfo fontInfo;
int ascent, descent, lineGap;
void Create(const string& newName)
{
name = newName;
wiHelper::FileRead(newName, fontBuffer);
int offset = stbtt_GetFontOffsetForIndex(fontBuffer.data(), 0);
if (!stbtt_InitFont(&fontInfo, fontBuffer.data(), offset))
{
stringstream ss("");
ss << "Failed to load font: " << name;
wiHelper::messageBox(ss.str());
}
stbtt_GetFontVMetrics(&fontInfo, &ascent, &descent, &lineGap);
}
};
std::vector<wiFontStyle> fontStyles;
struct FontVertex
{
XMFLOAT2 Pos;
XMHALF2 Tex;
};
template<typename T>
uint32_t WriteVertices(volatile FontVertex* vertexList, const T* text, wiFontParams params)
{
const wiFontStyle& fontStyle = fontStyles[params.style];
const float fontScale = stbtt_ScaleForPixelHeight(&fontStyle.fontInfo, (float)params.size);
uint32_t quadCount = 0;
float line = 0;
float pos = 0;
float pos_last_letter = 0;
size_t last_word_begin = 0;
bool start_new_word = false;
auto word_wrap = [&] {
start_new_word = true;
if (last_word_begin > 0 && params.h_wrap >= 0 && pos >= params.h_wrap - 1)
{
// Word ended and wrap detected, push down last word by one line:
float word_offset = vertexList[last_word_begin].Pos.x;
for (size_t i = last_word_begin; i < quadCount * 4; ++i)
{
vertexList[i].Pos.x -= word_offset;
vertexList[i].Pos.y += LINEBREAK_SIZE;
}
line += LINEBREAK_SIZE;
pos -= word_offset;
}
};
int code_prev = 0;
size_t i = 0;
while(text[i] != 0)
{
T character = text[i++];
int code = (int)character;
const int32_t hash = glyphhash(code, params.style, params.size);
if (glyph_lookup.count(hash) == 0)
{
// glyph not packed yet, so add to pending list:
glyphLock.lock();
pendingGlyphs.insert(hash);
glyphLock.unlock();
continue;
}
if (code == '\n')
{
word_wrap();
line += LINEBREAK_SIZE;
pos = 0;
code_prev = 0;
}
else if (code == ' ')
{
word_wrap();
pos += WHITESPACE_SIZE;
start_new_word = true;
code_prev = 0;
}
else if (code == '\t')
{
word_wrap();
pos += TAB_SIZE;
start_new_word = true;
code_prev = 0;
}
else
{
const Glyph& glyph = glyph_lookup.at(hash);
const float glyphWidth = glyph.width * params.scaling;
const float glyphHeight = glyph.height * params.scaling;
const float glyphOffsetX = glyph.x * params.scaling;
const float glyphOffsetY = glyph.y * params.scaling;
const size_t vertexID = size_t(quadCount) * 4;
if (start_new_word)
{
last_word_begin = vertexID;
}
start_new_word = false;
if (code_prev != 0)
{
int kern = stbtt_GetCodepointKernAdvance(&fontStyle.fontInfo, code_prev, code);
pos += kern * fontScale;
}
code_prev = code;
const float left = pos + glyphOffsetX;
const float right = left + glyphWidth;
const float top = line + glyphOffsetY;
const float bottom = top + glyphHeight;
vertexList[vertexID + 0].Pos.x = left;
vertexList[vertexID + 0].Pos.y = top;
vertexList[vertexID + 1].Pos.x = right;
vertexList[vertexID + 1].Pos.y = top;
vertexList[vertexID + 2].Pos.x = left;
vertexList[vertexID + 2].Pos.y = bottom;
vertexList[vertexID + 3].Pos.x = right;
vertexList[vertexID + 3].Pos.y = bottom;
vertexList[vertexID + 0].Tex.x = glyph.tc_left;
vertexList[vertexID + 0].Tex.y = glyph.tc_top;
vertexList[vertexID + 1].Tex.x = glyph.tc_right;
vertexList[vertexID + 1].Tex.y = glyph.tc_top;
vertexList[vertexID + 2].Tex.x = glyph.tc_left;
vertexList[vertexID + 2].Tex.y = glyph.tc_bottom;
vertexList[vertexID + 3].Tex.x = glyph.tc_right;
vertexList[vertexID + 3].Tex.y = glyph.tc_bottom;
pos += glyph.width * params.scaling + params.spacingX;
pos_last_letter = pos;
quadCount++;
}
}
word_wrap();
return quadCount;
}
}
using namespace wiFont_Internal;
namespace wiFont
{
void LoadShaders()
{
std::string path = wiRenderer::GetShaderPath();
wiRenderer::LoadShader(VS, vertexShader, "fontVS.cso");
wiRenderer::LoadShader(PS, pixelShader, "fontPS.cso");
PipelineStateDesc desc;
desc.vs = &vertexShader;
desc.ps = &pixelShader;
desc.bs = &blendState;
desc.dss = &depthStencilState;
desc.rs = &rasterizerState;
desc.pt = TRIANGLESTRIP;
wiRenderer::GetDevice()->CreatePipelineState(&desc, &PSO);
}
void Initialize()
{
if (initialized)
{
return;
}
// add default font if there is none yet:
if (fontStyles.empty())
{
AddFontStyle((FONTPATH + "arial.ttf").c_str());
}
GraphicsDevice* device = wiRenderer::GetDevice();
{
GPUBufferDesc bd;
bd.Usage = USAGE_DYNAMIC;
bd.ByteWidth = sizeof(FontCB);
bd.BindFlags = BIND_CONSTANT_BUFFER;
bd.CPUAccessFlags = CPU_ACCESS_WRITE;
device->CreateBuffer(&bd, nullptr, &constantBuffer);
}
RasterizerStateDesc rs;
rs.FillMode = FILL_SOLID;
rs.CullMode = CULL_FRONT;
rs.FrontCounterClockwise = true;
rs.DepthBias = 0;
rs.DepthBiasClamp = 0;
rs.SlopeScaledDepthBias = 0;
rs.DepthClipEnable = false;
rs.MultisampleEnable = false;
rs.AntialiasedLineEnable = false;
device->CreateRasterizerState(&rs, &rasterizerState);
BlendStateDesc bd;
bd.RenderTarget[0].BlendEnable = true;
bd.RenderTarget[0].SrcBlend = BLEND_SRC_ALPHA;
bd.RenderTarget[0].DestBlend = BLEND_INV_SRC_ALPHA;
bd.RenderTarget[0].BlendOp = BLEND_OP_ADD;
bd.RenderTarget[0].SrcBlendAlpha = BLEND_ONE;
bd.RenderTarget[0].DestBlendAlpha = BLEND_ONE;
bd.RenderTarget[0].BlendOpAlpha = BLEND_OP_ADD;
bd.RenderTarget[0].RenderTargetWriteMask = COLOR_WRITE_ENABLE_ALL;
bd.IndependentBlendEnable = false;
device->CreateBlendState(&bd, &blendState);
DepthStencilStateDesc dsd;
dsd.DepthEnable = false;
dsd.StencilEnable = false;
device->CreateDepthStencilState(&dsd, &depthStencilState);
SamplerDesc samplerDesc;
samplerDesc.Filter = FILTER_MIN_MAG_LINEAR_MIP_POINT;
samplerDesc.AddressU = TEXTURE_ADDRESS_BORDER;
samplerDesc.AddressV = TEXTURE_ADDRESS_BORDER;
samplerDesc.AddressW = TEXTURE_ADDRESS_BORDER;
samplerDesc.MipLODBias = 0.0f;
samplerDesc.MaxAnisotropy = 0;
samplerDesc.ComparisonFunc = COMPARISON_NEVER;
samplerDesc.BorderColor[0] = 0;
samplerDesc.BorderColor[1] = 0;
samplerDesc.BorderColor[2] = 0;
samplerDesc.BorderColor[3] = 0;
samplerDesc.MinLOD = 0;
samplerDesc.MaxLOD = FLT_MAX;
device->CreateSampler(&samplerDesc, &sampler);
static wiEvent::Handle handle1 = wiEvent::Subscribe(SYSTEM_EVENT_RELOAD_SHADERS, [](uint64_t userdata) { LoadShaders(); });
LoadShaders();
static wiEvent::Handle handle2 = wiEvent::Subscribe(SYSTEM_EVENT_CHANGE_DPI, [](uint64_t userdata) {
glyphLock.lock();
for (auto& x : glyph_lookup)
{
pendingGlyphs.insert(x.first);
}
glyph_lookup.clear();
rect_lookup.clear();
glyphLock.unlock();
});
wiBackLog::post("wiFont Initialized");
initialized.store(true);
}
void UpdatePendingGlyphs()
{
glyphLock.lock();
// If there are pending glyphs, render them and repack the atlas:
if (!pendingGlyphs.empty())
{
// Pad the glyph rects in the atlas to avoid bleeding from nearby texels:
const int borderPadding = 1;
// Font resolution is upscaled to make it sharper:
const float upscaling = std::max(2.0f, wiPlatform::GetDPIScaling());
for (int32_t hash : pendingGlyphs)
{
const int code = codefromhash(hash);
const int style = stylefromhash(hash);
const float height = (float)heightfromhash(hash) * upscaling;
wiFontStyle& fontStyle = fontStyles[style];
float fontScaling = stbtt_ScaleForPixelHeight(&fontStyle.fontInfo, height);
// get bounding box for character (may be offset to account for chars that dip above or below the line
int left, top, right, bottom;
stbtt_GetCodepointBitmapBox(&fontStyle.fontInfo, code, fontScaling, fontScaling, &left, &top, &right, &bottom);
// Glyph dimensions are calculated without padding:
Glyph& glyph = glyph_lookup[hash];
glyph.x = float(left);
glyph.y = float(top) + float(fontStyle.ascent) * fontScaling;
glyph.width = float(right - left);
glyph.height = float(bottom - top);
// Remove dpi upscaling:
glyph.x = glyph.x / upscaling;
glyph.y = glyph.y / upscaling;
glyph.width = glyph.width / upscaling;
glyph.height = glyph.height / upscaling;
// Add padding to the rectangle that will be packed in the atlas:
right += borderPadding * 2;
bottom += borderPadding * 2;
rect_lookup[hash] = rect_ltrb(left, top, right, bottom);
}
pendingGlyphs.clear();
// This reference array will be used for packing:
vector<rect_xywh*> out_rects;
out_rects.reserve(rect_lookup.size());
for (auto& it : rect_lookup)
{
out_rects.push_back(&it.second);
}
// Perform packing and process the result if successful:
std::vector<bin> bins;
if (pack(out_rects.data(), (int)out_rects.size(), 4096, bins))
{
assert(bins.size() == 1 && "The regions won't fit into one texture!");
// Retrieve texture atlas dimensions:
const int bitmapWidth = bins[0].size.w;
const int bitmapHeight = bins[0].size.h;
const float inv_width = 1.0f / bitmapWidth;
const float inv_height = 1.0f / bitmapHeight;
// Create the CPU-side texture atlas and fill with transparency (0):
vector<uint8_t> bitmap(size_t(bitmapWidth) * size_t(bitmapHeight));
std::fill(bitmap.begin(), bitmap.end(), 0);
// Iterate all packed glyph rectangles:
for (auto it : rect_lookup)
{
const int32_t hash = it.first;
const wchar_t code = codefromhash(hash);
const int style = stylefromhash(hash);
const float height = (float)heightfromhash(hash) * upscaling;
const wiFontStyle& fontStyle = fontStyles[style];
rect_xywh& rect = it.second;
Glyph& glyph = glyph_lookup[hash];
// Remove border padding from the packed rectangle (we don't want to touch the border, it should stay transparent):
rect.x += borderPadding;
rect.y += borderPadding;
rect.w -= borderPadding * 2;
rect.h -= borderPadding * 2;
float fontScaling = stbtt_ScaleForPixelHeight(&fontStyle.fontInfo, height);
// Render the glyph inside the CPU-side atlas:
int byteOffset = rect.x + (rect.y * bitmapWidth);
stbtt_MakeCodepointBitmap(&fontStyle.fontInfo, bitmap.data() + byteOffset, rect.w, rect.h, bitmapWidth, fontScaling, fontScaling, code);
// Compute texture coordinates for the glyph:
float tc_left = float(rect.x);
float tc_right = tc_left + float(rect.w);
float tc_top = float(rect.y);
float tc_bottom = tc_top + float(rect.h);
tc_left *= inv_width;
tc_right *= inv_width;
tc_top *= inv_height;
tc_bottom *= inv_height;
glyph.tc_left = XMConvertFloatToHalf(tc_left);
glyph.tc_right = XMConvertFloatToHalf(tc_right);
glyph.tc_top = XMConvertFloatToHalf(tc_top);
glyph.tc_bottom = XMConvertFloatToHalf(tc_bottom);
}
// Upload the CPU-side texture atlas bitmap to the GPU:
wiTextureHelper::CreateTexture(texture, bitmap.data(), bitmapWidth, bitmapHeight, FORMAT_R8_UNORM);
}
}
glyphLock.unlock();
}
const Texture* GetAtlas()
{
return &texture;
}
const std::string& GetFontPath()
{
return FONTPATH;
}
void SetFontPath(const std::string& path)
{
FONTPATH = path;
}
int AddFontStyle(const std::string& fontName)
{
for (size_t i = 0; i < fontStyles.size(); i++)
{
const wiFontStyle& fontStyle = fontStyles[i];
if (!fontStyle.name.compare(fontName))
{
return int(i);
}
}
fontStyles.emplace_back();
fontStyles.back().Create(fontName);
return int(fontStyles.size() - 1);
}
template<typename T>
float textWidth_internal(const T* text, const wiFontParams& params)
{
if (params.style >= (int)fontStyles.size())
{
return 0;
}
float maxWidth = 0;
float currentLineWidth = 0;
size_t i = 0;
while (text[i] != 0)
{
int code = (int)text[i++];
const int32_t hash = glyphhash(code, params.style, params.size);
if (glyph_lookup.count(hash) == 0)
{
// glyph not packed yet, we just continue (it will be added if it is actually rendered)
continue;
}
if (code == '\n')
{
currentLineWidth = 0;
}
else if (code == ' ')
{
currentLineWidth += WHITESPACE_SIZE;
}
else if (code == '\t')
{
currentLineWidth += TAB_SIZE;
}
else
{
const Glyph& glyph = glyph_lookup.at(hash);
currentLineWidth += glyph.width + float(params.spacingX) * params.scaling;
}
maxWidth = std::max(maxWidth, currentLineWidth);
}
return maxWidth;
}
template<typename T>
float textHeight_internal(const T* text, const wiFontParams& params)
{
if (params.style >= (int)fontStyles.size())
{
return 0;
}
float height = LINEBREAK_SIZE;
size_t i = 0;
while (text[i] != 0)
{
int code = (int)text[i++];
if (code == '\n')
{
height += LINEBREAK_SIZE;
}
}
return height;
}
template<typename T>
void Draw_internal(const T* text, size_t text_length, const wiFontParams& params, CommandList cmd)
{
if (text_length <= 0 || !initialized.load())
{
return;
}
wiFontParams newProps = params;
if (params.h_align == WIFALIGN_CENTER)
newProps.posX -= textWidth_internal(text, newProps) / 2;
else if (params.h_align == WIFALIGN_RIGHT)
newProps.posX -= textWidth_internal(text, newProps);
if (params.v_align == WIFALIGN_CENTER)
newProps.posY -= textHeight_internal(text, newProps) / 2;
else if (params.v_align == WIFALIGN_BOTTOM)
newProps.posY -= textHeight_internal(text, newProps);
GraphicsDevice* device = wiRenderer::GetDevice();
GraphicsDevice::GPUAllocation mem = device->AllocateGPU(sizeof(FontVertex) * text_length * 4, cmd);
if (!mem.IsValid())
{
return;
}
volatile FontVertex* textBuffer = (volatile FontVertex*)mem.data;
const uint32_t quadCount = WriteVertices(textBuffer, text, newProps);
if (quadCount > 0)
{
device->EventBegin("Font", cmd);
device->BindPipelineState(&PSO, cmd);
device->BindConstantBuffer(VS, &constantBuffer, CB_GETBINDSLOT(FontCB), cmd);
device->BindConstantBuffer(PS, &constantBuffer, CB_GETBINDSLOT(FontCB), cmd);
device->BindResource(PS, &texture, TEXSLOT_FONTATLAS, cmd);
device->BindSampler(PS, &sampler, SSLOT_ONDEMAND1, cmd);
device->BindResource(VS, mem.buffer, 0, cmd);
FontCB cb;
cb.g_xFont_BufferOffset = mem.offset;
XMMATRIX Projection = device->GetScreenProjection();
if (newProps.shadowColor.getA() > 0)
{
// font shadow render:
XMStoreFloat4x4(&cb.g_xFont_Transform,
XMMatrixTranslation((float)newProps.posX + 1, (float)newProps.posY + 1, 0)
* Projection
);
cb.g_xFont_Color = newProps.shadowColor.toFloat4();
device->UpdateBuffer(&constantBuffer, &cb, cmd);
device->DrawInstanced(4, quadCount, 0, 0, cmd);
}
// font base render:
XMStoreFloat4x4(&cb.g_xFont_Transform,
XMMatrixTranslation((float)newProps.posX, (float)newProps.posY, 0)
* Projection
);
cb.g_xFont_Color = newProps.color.toFloat4();
device->UpdateBuffer(&constantBuffer, &cb, cmd);
device->DrawInstanced(4, quadCount, 0, 0, cmd);
device->EventEnd(cmd);
}
UpdatePendingGlyphs();
}
void Draw(const char* text, const wiFontParams& params, CommandList cmd)
{
size_t text_length = strlen(text);
if (text_length == 0)
{
return;
}
Draw_internal(text, text_length, params, cmd);
}
void Draw(const wchar_t* text, const wiFontParams& params, CommandList cmd)
{
size_t text_length = wcslen(text);
if (text_length == 0)
{
return;
}
Draw_internal(text, text_length, params, cmd);
}
void Draw(const string& text, const wiFontParams& params, CommandList cmd)
{
Draw_internal(text.c_str(), text.length(), params, cmd);
}
void Draw(const wstring& text, const wiFontParams& params, CommandList cmd)
{
Draw_internal(text.c_str(), text.length(), params, cmd);
}
float textWidth(const char* text, const wiFontParams& params)
{
return textWidth_internal(text, params);
}
float textWidth(const wchar_t* text, const wiFontParams& params)
{
return textWidth_internal(text, params);
}
float textWidth(const string& text, const wiFontParams& params)
{
return textWidth_internal(text.c_str(), params);
}
float textWidth(const wstring& text, const wiFontParams& params)
{
return textWidth_internal(text.c_str(), params);
}
float textHeight(const char* text, const wiFontParams& params)
{
return textHeight_internal(text, params);
}
float textHeight(const wchar_t* text, const wiFontParams& params)
{
return textHeight_internal(text, params);
}
float textHeight(const string& text, const wiFontParams& params)
{
return textHeight_internal(text.c_str(), params);
}
float textHeight(const wstring& text, const wiFontParams& params)
{
return textHeight_internal(text.c_str(), params);
}
}