diff options
Diffstat (limited to 'src/engine/client/text.cpp')
| -rw-r--r-- | src/engine/client/text.cpp | 718 |
1 files changed, 718 insertions, 0 deletions
diff --git a/src/engine/client/text.cpp b/src/engine/client/text.cpp new file mode 100644 index 00000000..b05d49f8 --- /dev/null +++ b/src/engine/client/text.cpp @@ -0,0 +1,718 @@ +#include <base/system.h> +#include <base/math.h> +#include <engine/graphics.h> +#include <engine/textrender.h> + +#ifdef CONF_FAMILY_WINDOWS + #include <windows.h> +#endif + +#ifdef CONF_PLATFORM_MACOSX + #include <OpenGL/gl.h> + #include <OpenGL/glu.h> +#else + #include <GL/gl.h> + #include <GL/glu.h> +#endif + +// ft2 texture +#include <ft2build.h> +#include FT_FREETYPE_H + +// TODO: Refactor: clean this up + + +enum +{ + MAX_CHARACTERS = 64, +}; + + +static int aFontSizes[] = {8,9,10,11,12,13,14,15,16,17,18,19,20,36}; +#define NUM_FONT_SIZES (sizeof(aFontSizes)/sizeof(int)) + +struct CFontChar +{ + int m_Id; + + // these values are scaled to the pFont size + // width * font_size == real_size + float m_Width; + float m_Height; + float m_OffsetX; + float m_OffsetY; + float m_AdvanceX; + + float m_aUvs[4]; + int64 m_TouchTime; +}; + +struct CFontSizeData +{ + int m_FontSize; + FT_Face *m_pFace; + + unsigned m_aTextures[2]; + int m_TextureWidth; + int m_TextureHeight; + + int m_NumXChars; + int m_NumYChars; + + int m_CharMaxWidth; + int m_CharMaxHeight; + + CFontChar m_aCharacters[MAX_CHARACTERS*MAX_CHARACTERS]; + + int m_CurrentCharacter; +}; + +struct CFont +{ + char m_aFilename[128]; + FT_Face m_FtFace; + CFontSizeData m_aSizes[NUM_FONT_SIZES]; +}; + + +class CTextRender : public IEngineTextRender +{ + IGraphics *m_pGraphics; + IGraphics *Graphics() { return m_pGraphics; } + + int WordLength(const char *pText) + { + int s = 1; + while(1) + { + if(*pText == 0) + return s-1; + if(*pText == '\n' || *pText == '\t' || *pText == ' ') + return s; + pText++; + s++; + } + } + + float m_TextR; + float m_TextG; + float m_TextB; + float m_TextA; + + int m_FontTextureFormat; + + struct CFont *m_pDefaultFont; + + FT_Library m_FTLibrary; + + int GetFontSizeIndex(int Pixelsize) + { + for(unsigned i = 0; i < NUM_FONT_SIZES; i++) + { + if(aFontSizes[i] >= Pixelsize) + return i; + } + + return NUM_FONT_SIZES-1; + } + + + + void Grow(unsigned char *pIn, unsigned char *pOut, int w, int h) + { + for(int y = 0; y < h; y++) + for(int x = 0; x < w; x++) + { + int c = pIn[y*w+x]; + + for(int sy = -1; sy <= 1; sy++) + for(int sx = -1; sx <= 1; sx++) + { + int GetX = x+sx; + int GetY = y+sy; + if (GetX >= 0 && GetY >= 0 && GetX < w && GetY < h) + { + int Index = GetY*w+GetX; + if(pIn[Index] > c) + c = pIn[Index]; + } + } + + pOut[y*w+x] = c; + } + } + + void InitTexture(CFontSizeData *pSizeData, int CharWidth, int CharHeight, int Xchars, int Ychars) + { + static int FontMemoryUsage = 0; + int Width = CharWidth*Xchars; + int Height = CharHeight*Ychars; + void *pMem = mem_alloc(Width*Height, 1); + mem_zero(pMem, Width*Height); + + if(pSizeData->m_aTextures[0] == 0) + glGenTextures(2, pSizeData->m_aTextures); + else + FontMemoryUsage -= pSizeData->m_TextureWidth*pSizeData->m_TextureHeight*2; + + pSizeData->m_NumXChars = Xchars; + pSizeData->m_NumYChars = Ychars; + pSizeData->m_TextureWidth = Width; + pSizeData->m_TextureHeight = Height; + pSizeData->m_CurrentCharacter = 0; + + for(int i = 0; i < 2; i++) + { + glBindTexture(GL_TEXTURE_2D, pSizeData->m_aTextures[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, m_FontTextureFormat, Width, Height, 0, m_FontTextureFormat, GL_UNSIGNED_BYTE, pMem); + FontMemoryUsage += Width*Height; + } + + dbg_msg("", "pFont memory usage: %d", FontMemoryUsage); + + mem_free(pMem); + } + + void IncreaseTextureSize(CFontSizeData *pSizeData) + { + if(pSizeData->m_TextureWidth < pSizeData->m_TextureHeight) + pSizeData->m_NumXChars <<= 1; + else + pSizeData->m_NumYChars <<= 1; + InitTexture(pSizeData, pSizeData->m_CharMaxWidth, pSizeData->m_CharMaxHeight, pSizeData->m_NumXChars, pSizeData->m_NumYChars); + } + + + // TODO: Refactor: move this into a pFont class + void InitIndex(CFont *pFont, int Index) + { + int OutlineThickness = 1; + CFontSizeData *pSizeData = &pFont->m_aSizes[Index]; + + pSizeData->m_FontSize = aFontSizes[Index]; + FT_Set_Pixel_Sizes(pFont->m_FtFace, 0, pSizeData->m_FontSize); + + if(pSizeData->m_FontSize >= 18) + OutlineThickness = 2; + + { + unsigned GlyphIndex; + int MaxH = 0; + int MaxW = 0; + + int Charcode = FT_Get_First_Char(pFont->m_FtFace, &GlyphIndex); + while(GlyphIndex != 0) + { + // do stuff + FT_Load_Glyph(pFont->m_FtFace, GlyphIndex, FT_LOAD_DEFAULT); + + if(pFont->m_FtFace->glyph->metrics.width > MaxW) MaxW = pFont->m_FtFace->glyph->metrics.width; // ignore_convention + if(pFont->m_FtFace->glyph->metrics.height > MaxH) MaxH = pFont->m_FtFace->glyph->metrics.height; // ignore_convention + Charcode = FT_Get_Next_Char(pFont->m_FtFace, Charcode, &GlyphIndex); + } + + MaxW = (MaxW>>6)+2+OutlineThickness*2; + MaxH = (MaxH>>6)+2+OutlineThickness*2; + + for(pSizeData->m_CharMaxWidth = 1; pSizeData->m_CharMaxWidth < MaxW; pSizeData->m_CharMaxWidth <<= 1); + for(pSizeData->m_CharMaxHeight = 1; pSizeData->m_CharMaxHeight < MaxH; pSizeData->m_CharMaxHeight <<= 1); + } + + //dbg_msg("pFont", "init size %d, texture size %d %d", pFont->sizes[index].font_size, w, h); + //FT_New_Face(m_FTLibrary, "data/fonts/vera.ttf", 0, &pFont->ft_face); + InitTexture(pSizeData, pSizeData->m_CharMaxWidth, pSizeData->m_CharMaxHeight, 8, 8); + } + + CFontSizeData *GetSize(CFont *pFont, int Pixelsize) + { + int Index = GetFontSizeIndex(Pixelsize); + if(pFont->m_aSizes[Index].m_FontSize != aFontSizes[Index]) + InitIndex(pFont, Index); + return &pFont->m_aSizes[Index]; + } + + + void UploadGlyph(CFontSizeData *pSizeData, int Texnum, int SlotId, int Chr, const void *pData) + { + int x = (SlotId%pSizeData->m_NumXChars) * (pSizeData->m_TextureWidth/pSizeData->m_NumXChars); + int y = (SlotId/pSizeData->m_NumXChars) * (pSizeData->m_TextureHeight/pSizeData->m_NumYChars); + + glBindTexture(GL_TEXTURE_2D, pSizeData->m_aTextures[Texnum]); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, + pSizeData->m_TextureWidth/pSizeData->m_NumXChars, + pSizeData->m_TextureHeight/pSizeData->m_NumYChars, + m_FontTextureFormat, GL_UNSIGNED_BYTE, pData); + } + + // 8k of data used for rendering glyphs + unsigned char ms_aGlyphData[(4096/64) * (4096/64)]; + unsigned char ms_aGlyphDataOutlined[(4096/64) * (4096/64)]; + + int GetSlot(CFontSizeData *pSizeData) + { + int CharCount = pSizeData->m_NumXChars*pSizeData->m_NumYChars; + if(pSizeData->m_CurrentCharacter < CharCount) + { + int i = pSizeData->m_CurrentCharacter; + pSizeData->m_CurrentCharacter++; + return i; + } + + // kick out the oldest + // TODO: remove this linear search + { + int Oldest = 0; + for(int i = 1; i < CharCount; i++) + { + if(pSizeData->m_aCharacters[i].m_TouchTime < pSizeData->m_aCharacters[Oldest].m_TouchTime) + Oldest = i; + } + + if(time_get()-pSizeData->m_aCharacters[Oldest].m_TouchTime < time_freq()) + { + IncreaseTextureSize(pSizeData); + return GetSlot(pSizeData); + } + + return Oldest; + } + } + + int RenderGlyph(CFont *pFont, CFontSizeData *pSizeData, int Chr) + { + FT_Bitmap *pBitmap; + int SlotId = 0; + int SlotW = pSizeData->m_TextureWidth / pSizeData->m_NumXChars; + int SlotH = pSizeData->m_TextureHeight / pSizeData->m_NumYChars; + int SlotSize = SlotW*SlotH; + int OutlineThickness = 1; + int x = 1; + int y = 1; + int px, py; + + FT_Set_Pixel_Sizes(pFont->m_FtFace, 0, pSizeData->m_FontSize); + + if(FT_Load_Char(pFont->m_FtFace, Chr, FT_LOAD_RENDER|FT_LOAD_NO_BITMAP)) + { + dbg_msg("pFont", "error loading glyph %d", Chr); + return -1; + } + + pBitmap = &pFont->m_FtFace->glyph->bitmap; // ignore_convention + + // fetch slot + SlotId = GetSlot(pSizeData); + if(SlotId < 0) + return -1; + + // adjust spacing + if(pSizeData->m_FontSize >= 18) + OutlineThickness = 2; + x += OutlineThickness; + y += OutlineThickness; + + // prepare glyph data + mem_zero(ms_aGlyphData, SlotSize); + + if(pBitmap->pixel_mode == FT_PIXEL_MODE_GRAY) // ignore_convention + { + for(py = 0; py < pBitmap->rows; py++) // ignore_convention + for(px = 0; px < pBitmap->width; px++) // ignore_convention + ms_aGlyphData[(py+y)*SlotW+px+x] = pBitmap->buffer[py*pBitmap->pitch+px]; // ignore_convention + } + else if(pBitmap->pixel_mode == FT_PIXEL_MODE_MONO) // ignore_convention + { + for(py = 0; py < pBitmap->rows; py++) // ignore_convention + for(px = 0; px < pBitmap->width; px++) // ignore_convention + { + if(pBitmap->buffer[py*pBitmap->pitch+px/8]&(1<<(7-(px%8)))) // ignore_convention + ms_aGlyphData[(py+y)*SlotW+px+x] = 255; + } + } + + if(0) for(py = 0; py < SlotW; py++) + for(px = 0; px < SlotH; px++) + ms_aGlyphData[py*SlotW+px] = 255; + + // upload the glyph + UploadGlyph(pSizeData, 0, SlotId, Chr, ms_aGlyphData); + + if(OutlineThickness == 1) + { + Grow(ms_aGlyphData, ms_aGlyphDataOutlined, SlotW, SlotH); + UploadGlyph(pSizeData, 1, SlotId, Chr, ms_aGlyphDataOutlined); + } + else + { + Grow(ms_aGlyphData, ms_aGlyphDataOutlined, SlotW, SlotH); + Grow(ms_aGlyphDataOutlined, ms_aGlyphData, SlotW, SlotH); + UploadGlyph(pSizeData, 1, SlotId, Chr, ms_aGlyphData); + } + + // set char info + { + CFontChar *pFontchr = &pSizeData->m_aCharacters[SlotId]; + float Scale = 1.0f/pSizeData->m_FontSize; + float Uscale = 1.0f/pSizeData->m_TextureWidth; + float Vscale = 1.0f/pSizeData->m_TextureHeight; + int Height = pBitmap->rows + OutlineThickness*2 + 2; // ignore_convention + int Width = pBitmap->width + OutlineThickness*2 + 2; // ignore_convention + + pFontchr->m_Id = Chr; + pFontchr->m_Height = Height * Scale; + pFontchr->m_Width = Width * Scale; + pFontchr->m_OffsetX = (pFont->m_FtFace->glyph->bitmap_left-1) * Scale; // ignore_convention + pFontchr->m_OffsetY = (pSizeData->m_FontSize - pFont->m_FtFace->glyph->bitmap_top) * Scale; // ignore_convention + pFontchr->m_AdvanceX = (pFont->m_FtFace->glyph->advance.x>>6) * Scale; // ignore_convention + + pFontchr->m_aUvs[0] = (SlotId%pSizeData->m_NumXChars) / (float)(pSizeData->m_NumXChars); + pFontchr->m_aUvs[1] = (SlotId/pSizeData->m_NumXChars) / (float)(pSizeData->m_NumYChars); + pFontchr->m_aUvs[2] = pFontchr->m_aUvs[0] + Width*Uscale; + pFontchr->m_aUvs[3] = pFontchr->m_aUvs[1] + Height*Vscale; + } + + return SlotId; + } + + CFontChar *GetChar(CFont *pFont, CFontSizeData *pSizeData, int Chr) + { + CFontChar *pFontchr = NULL; + + // search for the character + // TODO: remove this linear search + int i; + for(i = 0; i < pSizeData->m_CurrentCharacter; i++) + { + if(pSizeData->m_aCharacters[i].m_Id == Chr) + { + pFontchr = &pSizeData->m_aCharacters[i]; + break; + } + } + + // check if we need to render the character + if(!pFontchr) + { + int Index = RenderGlyph(pFont, pSizeData, Chr); + if(Index >= 0) + pFontchr = &pSizeData->m_aCharacters[Index]; + } + + // touch the character + // TODO: don't call time_get here + if(pFontchr) + pFontchr->m_TouchTime = time_get(); + + return pFontchr; + } + + // must only be called from the rendering function as the pFont must be set to the correct size + void RenderSetup(CFont *pFont, int size) + { + FT_Set_Pixel_Sizes(pFont->m_FtFace, 0, size); + } + + float Kerning(CFont *pFont, int Left, int Right) + { + FT_Vector Kerning = {0,0}; + FT_Get_Kerning(pFont->m_FtFace, Left, Right, FT_KERNING_DEFAULT, &Kerning); + return (Kerning.x>>6); + } + + +public: + CTextRender() + { + m_pGraphics = 0; + + m_TextR = 1; + m_TextG = 1; + m_TextB = 1; + m_TextA = 1; + + m_pDefaultFont = 0; + + // GL_LUMINANCE can be good for debugging + m_FontTextureFormat = GL_ALPHA; + } + + virtual void Init() + { + m_pGraphics = Kernel()->RequestInterface<IGraphics>(); + FT_Init_FreeType(&m_FTLibrary); + } + + + virtual CFont *LoadFont(const char *pFilename) + { + CFont *pFont = (CFont *)mem_alloc(sizeof(CFont), 1); + + mem_zero(pFont, sizeof(*pFont)); + str_copy(pFont->m_aFilename, pFilename, sizeof(pFont->m_aFilename)); + + if(FT_New_Face(m_FTLibrary, pFont->m_aFilename, 0, &pFont->m_FtFace)) + { + mem_free(pFont); + return NULL; + } + + for(unsigned i = 0; i < NUM_FONT_SIZES; i++) + pFont->m_aSizes[i].m_FontSize = -1; + + dbg_msg("textrender", "loaded pFont from '%s'", pFilename); + return pFont; + }; + + virtual void DestroyFont(CFont *pFont) + { + mem_free(pFont); + } + + virtual void SetDefaultFont(struct CFont *pFont) + { + dbg_msg("textrender", "default pFont set %p", pFont); + m_pDefaultFont = pFont; + } + + + virtual void SetCursor(CTextCursor *pCursor, float x, float y, float FontSize, int Flags) + { + mem_zero(pCursor, sizeof(*pCursor)); + pCursor->m_FontSize = FontSize; + pCursor->m_StartX = x; + pCursor->m_StartY = y; + pCursor->m_X = x; + pCursor->m_Y = y; + pCursor->m_LineCount = 1; + pCursor->m_LineWidth = -1; + pCursor->m_Flags = Flags; + pCursor->m_CharCount = 0; + } + + + virtual void Text(void *pFontSetV, float x, float y, float Size, const char *pText, int MaxWidth) + { + CTextCursor Cursor; + SetCursor(&Cursor, x, y, Size, TEXTFLAG_RENDER); + Cursor.m_LineWidth = MaxWidth; + TextEx(&Cursor, pText, -1); + } + + virtual float TextWidth(void *pFontSetV, float Size, const char *pText, int Length) + { + CTextCursor Cursor; + SetCursor(&Cursor, 0, 0, Size, 0); + TextEx(&Cursor, pText, Length); + return Cursor.m_X; + } + + virtual float TextLineCount(void *pFontSetV, float Size, const char *pText, int LineWidth) + { + CTextCursor Cursor; + SetCursor(&Cursor, 0, 0, Size, 0); + Cursor.m_LineWidth = LineWidth; + TextEx(&Cursor, pText, -1); + return Cursor.m_LineCount; + } + + virtual void TextColor(float r, float g, float b, float a) + { + m_TextR = r; + m_TextG = g; + m_TextB = b; + m_TextA = a; + } + + virtual void TextEx(CTextCursor *pCursor, const char *pText, int Length) + { + CFont *pFont = pCursor->m_pFont; + CFontSizeData *pSizeData = NULL; + + //dbg_msg("textrender", "rendering text '%s'", text); + + float ScreenX0, ScreenY0, ScreenX1, ScreenY1; + float FakeToScreenX, FakeToScreenY; + int ActualX, ActualY; + + int ActualSize; + int i; + int GotNewLine = 0; + float DrawX, DrawY; + float CursorX, CursorY; + const char *pEnd; + + float Size = pCursor->m_FontSize; + + // to correct coords, convert to screen coords, round, and convert back + Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); + + FakeToScreenX = (Graphics()->ScreenWidth()/(ScreenX1-ScreenX0)); + FakeToScreenY = (Graphics()->ScreenHeight()/(ScreenY1-ScreenY0)); + ActualX = pCursor->m_X * FakeToScreenX; + ActualY = pCursor->m_Y * FakeToScreenY; + + CursorX = ActualX / FakeToScreenX; + CursorY = ActualY / FakeToScreenY; + + // same with size + ActualSize = Size * FakeToScreenY; + Size = ActualSize / FakeToScreenY; + + // fetch pFont data + if(!pFont) + pFont = m_pDefaultFont; + + if(!pFont) + return; + + pSizeData = GetSize(pFont, ActualSize); + RenderSetup(pFont, ActualSize); + + // set length + if(Length < 0) + Length = str_length(pText); + + pEnd = pText + Length; + + // if we don't want to render, we can just skip the first outline pass + i = 1; + if(pCursor->m_Flags&TEXTFLAG_RENDER) + i = 0; + + for(;i < 2; i++) + { + const char *pCurrent = (char *)pText; + const char *pEnd = pCurrent+Length; + DrawX = CursorX; + DrawY = CursorY; + + if(pCursor->m_Flags&TEXTFLAG_RENDER) + { + // TODO: Make this better + glEnable(GL_TEXTURE_2D); + if (i == 0) + glBindTexture(GL_TEXTURE_2D, pSizeData->m_aTextures[1]); + else + glBindTexture(GL_TEXTURE_2D, pSizeData->m_aTextures[0]); + + Graphics()->QuadsBegin(); + if (i == 0) + Graphics()->SetColor(0.0f, 0.0f, 0.0f, 0.3f*m_TextA); + else + Graphics()->SetColor(m_TextR, m_TextG, m_TextB, m_TextA); + } + + while(pCurrent < pEnd) + { + int NewLine = 0; + const char *pBatchEnd = pEnd; + if(pCursor->m_LineWidth > 0 && !(pCursor->m_Flags&TEXTFLAG_STOP_AT_END)) + { + int Wlen = min(WordLength((char *)pCurrent), (int)(pEnd-pCurrent)); + CTextCursor Compare = *pCursor; + Compare.m_X = DrawX; + Compare.m_Y = DrawY; + Compare.m_Flags &= ~TEXTFLAG_RENDER; + Compare.m_LineWidth = -1; + TextEx(&Compare, pText, Wlen); + + if(Compare.m_X-DrawX > pCursor->m_LineWidth) + { + // word can't be fitted in one line, cut it + CTextCursor Cutter = *pCursor; + Cutter.m_CharCount = 0; + Cutter.m_X = DrawX; + Cutter.m_Y = DrawY; + Cutter.m_Flags &= ~TEXTFLAG_RENDER; + Cutter.m_Flags |= TEXTFLAG_STOP_AT_END; + + TextEx(&Cutter, (const char *)pCurrent, Wlen); + Wlen = Cutter.m_CharCount; + NewLine = 1; + + if(Wlen <= 3) // if we can't place 3 chars of the word on this line, take the next + Wlen = 0; + } + else if(Compare.m_X-pCursor->m_StartX > pCursor->m_LineWidth) + { + NewLine = 1; + Wlen = 0; + } + + pBatchEnd = pCurrent + Wlen; + } + + while(pCurrent < pBatchEnd) + { + const char *pTmp; + float Advance = 0; + int Character = 0; + int Nextcharacter = 0; + CFontChar *pChr; + + // TODO: UTF-8 decode + Character = str_utf8_decode(&pCurrent); + pTmp = pCurrent; + Nextcharacter = str_utf8_decode(&pTmp); + + if(Character == '\n') + { + DrawX = pCursor->m_StartX; + DrawY += Size; + DrawX = (int)(DrawX * FakeToScreenX) / FakeToScreenX; // realign + DrawY = (int)(DrawY * FakeToScreenY) / FakeToScreenY; + ++pCursor->m_LineCount; + continue; + } + + pChr = GetChar(pFont, pSizeData, Character); + + if(pChr) + { + if(pCursor->m_Flags&TEXTFLAG_RENDER) + { + Graphics()->QuadsSetSubset(pChr->m_aUvs[0], pChr->m_aUvs[1], pChr->m_aUvs[2], pChr->m_aUvs[3]); + IGraphics::CQuadItem QuadItem(DrawX+pChr->m_OffsetX*Size, DrawY+pChr->m_OffsetY*Size, pChr->m_Width*Size, pChr->m_Height*Size); + Graphics()->QuadsDrawTL(&QuadItem, 1); + } + + Advance = pChr->m_AdvanceX + Kerning(pFont, Character, Nextcharacter)/Size; + } + + if(pCursor->m_Flags&TEXTFLAG_STOP_AT_END && DrawX+Advance*Size-pCursor->m_StartX > pCursor->m_LineWidth) + { + // we hit the end of the line, no more to render or count + pCurrent = pEnd; + break; + } + + DrawX += Advance*Size; + pCursor->m_CharCount++; + } + + if(NewLine) + { + DrawX = pCursor->m_StartX; + DrawY += Size; + GotNewLine = 1; + DrawX = (int)(DrawX * FakeToScreenX) / FakeToScreenX; // realign + DrawY = (int)(DrawY * FakeToScreenY) / FakeToScreenY; + ++pCursor->m_LineCount; + } + } + + if(pCursor->m_Flags&TEXTFLAG_RENDER) + Graphics()->QuadsEnd(); + } + + pCursor->m_X = DrawX; + + if(GotNewLine) + pCursor->m_Y = DrawY; + } + +}; + +IEngineTextRender *CreateEngineTextRender() { return new CTextRender; } |