Заметки / Сглаживание шрифта на основе смешивания

C++, OpenGL
 Для создания сглаженного шрифта можно воспользоваться полноэкранным сглаживанием (anti-aliasing'ом / multisampling'ом) и обычным системным шрифтом. Но сглаживание в OpenGL не поддерживается на достаточно хорошем уровне всеми видеокартами. К примеру, вот эта программа, несмотря на включённый multisampling...

...выглядит на моёи ПК так:

...хотя, может выглядеть и так:

Выходом может стать создание собственного шрифта на основе текстур с эффектом blend'а (смешивания), который имеет намного лучшую совместимость с "железом". Сначала надо напечатать отдельные буквы алфавита в jpeg'и (целая картинка со шрифтом, кажется, менее удобна и эффективна в плане производительности). Для удобства возьмём моноширинный (с одинаковой для всех букв шириной) шрифт, скажем "Courier New". Добавим к символам серый градиентный контур (для blend'а). Получится такая программа:
void paint ()
{
 int x=33, y=7;// смещение символа по x и y
 int y2, x2;
 WCHAR wFile [32];
 char file [32], txt [2];

 hDC=BeginPaint (hwnd, &PaintStruct);

 // Шрифты; шрифт бордюра без антиалиасинга, символа - со сглаживанием
 hFont=CreateFont (150, 0, 0, 0, FW_BOLD, false, false,
                   false, RUSSIAN_CHARSET, false, false, 5, false, "Courier New");
 hFontB=CreateFont (150, 0, 0, 0, FW_BOLD, false, false,
                    false, RUSSIAN_CHARSET, false, false, 3, false, "Courier New");

 // Проходим по всем символам
 for (int i=32; i<256; i++)
 {
  if (!(i>126 && i<161))
  {
   hCompatibleDC=CreateCompatibleDC (hDC);
   GetClientRect (hwnd, &Rect);
   HBITMAP hbm=CreateCompatibleBitmap (hDC, Rect.right, Rect.bottom);
   HBITMAP holdBM=(HBITMAP)SelectObject (hCompatibleDC, hbm);

   // Фон
   HBRUSH hLinePen=CreateSolidBrush (RGB (0, 0, 255));
   (HBRUSH)SelectObject (hCompatibleDC, hLinePen);
   Rectangle (hCompatibleDC, -1, -1, Rect.right+2, Rect.bottom+2);

   SetBkMode (hCompatibleDC, 0);// прозрачность фона буквы

   // Рисуем окаймление
   txt [0]=i;
   for (int ib=7; ib>=0; ib--)
   {
    if (ib>5)
     SelectObject (hCompatibleDC, hFontB);// шрифт внешнего контура
    else
     SelectObject (hCompatibleDC, hFont);// шрифт символа

    SetTextColor (hCompatibleDC, RGB (255-ib*32, 255-ib*32, 255-ib*32));
    for (y2=y-ib; y2<=y+ib; y2++)
    {
     for (x2=x-ib; x2<=x+ib; x2++)
     {
      GetClientRect (hwnd, &Rect);
      SetRect (&Rect, Rect.left+x2, Rect.top+y2, Rect.right, Rect.bottom);
      DrawText (hCompatibleDC, txt, 1, &Rect, DT_SINGLELINE | DT_LEFT | DT_TOP | DT_NOPREFIX);
     }
    }
   }

   // ...букву
   SelectObject (hCompatibleDC, hFont);// шрифт символа
   SetTextColor (hCompatibleDC, RGB (255, 255, 255));// цвет
   GetClientRect (hwnd, &Rect);
   SetRect (&Rect, Rect.left+x, Rect.top+y, Rect.right, Rect.bottom);
   DrawText (hCompatibleDC, txt, 1, &Rect, DT_SINGLELINE | DT_LEFT | DT_TOP | DT_NOPREFIX);
   GetClientRect (hwnd, &Rect);
   BitBlt (hDC, Rect.left, Rect.top, Rect.right, Rect.bottom, hCompatibleDC, 0, 0, SRCCOPY);

   // Сохраняем в gif
   Gdiplus::Graphics g(hDC);
   g.SetPageUnit(Gdiplus::UnitPixel);
   Gdiplus::RectF bounds(0, 0, float(Rect.right), float(Rect.bottom));
   Gdiplus::Bitmap bg(hbm,0);

   CLSID imgClsid;
   UINT num, size, j;
   using namespace Gdiplus;
   GetImageEncodersSize(&num, &size);
   ImageCodecInfo* pArray = (ImageCodecInfo*)(malloc(size));
   GetImageEncoders(num, size, pArray);
   for(j=0; j< num; ++j)
   {
    if (pArray[j].FormatID==ImageFormatJPEG)
     imgClsid=pArray[j].Clsid;
   }
   sprintf (file, "litera\\%d.jpg", i);
   MultiByteToWideChar (CP_ACP, 0, file, -1, wFile, 32);
   bg.Save (wFile, &imgClsid, NULL);

   DeleteObject (hbm);
   DeleteDC (hCompatibleDC);
  }
 }

 EndPaint (hwnd, &PaintStruct);

 PostQuitMessage (0);       /* send a WM_QUIT to the message queue */
}


Далее загрузим полученный шрифт в тестовую программу, убрав фон символов и изменив градиент: выставим соответствующую цвету "альфу" (прозрачность) - чем темнее, тем прозрачнее. Примерно так:
void LoadGLTextures ()
{
 HDC hdcTemp;                 // DC для растра
 HBITMAP hbmpTemp;            // иногда храним в ней растр
 IPicture *pPicture;          // интерфейс IPicture
 OLECHAR wszPath [MAX_PATH+1];// полный путь до картинки (WCHAR)
 long lWidth;                 // ширина в логических единицах
 long lHeight;                // высота в логических единицах
 long lWidthPixels;           // ширина в пикселях
 long lHeightPixels;          // высота в пикселях
 GLint glMaxTexDim ;          // максимальный размер текстуры

 char texs [5] [32]={"blank.gif", "sky.jpg"};
 char texd [16]={"data\\textures"};
 char txt [_MAX_PATH];
 int i2, xi, yi, cl, ri;

 for (i2=0; i2<256; i2++)
 {
  if (!(i2>126 && i2<161) && !(i2>1 && i2<32))
  {
   if (i2<2)
    sprintf (txt, "%s%s\\%s", AppDir, texd, texs [i2]);
   else
    sprintf (txt, "%sdata\\litera\\%d.jpg", AppDir, i2);

   MultiByteToWideChar (CP_ACP, 0, txt, -1, wszPath, MAX_PATH);// преобразуем к юникоду
   HRESULT hr=OleLoadPicturePath (wszPath, 0, 0, 0, IID_IPicture, (void**)&pPicture);

   if (!FAILED (hr)) // Если загрузка удачна
   {
    hdcTemp=CreateCompatibleDC (GetDC (0));// создать совместимый с устройством Windows контекст
    if (!hdcTemp)         // ну что, создали?
      pPicture->Release();// не-а… :( уменьшение счетчика ссылок на IPicture
    else
    {
     // получить максимально возможное разрешение изображения
     glGetIntegerv (GL_MAX_TEXTURE_SIZE, &glMaxTexDim);

     pPicture->get_Width (&lWidth);  // получить ширину изображения
     lWidthPixels=MulDiv (lWidth, GetDeviceCaps (hdcTemp, LOGPIXELSX), 2540);
     pPicture->get_Height (&lHeight);// получить высоту изображения
     lHeightPixels=MulDiv (lHeight, GetDeviceCaps (hdcTemp, LOGPIXELSY), 2540);

     // преобразовать изображение к ближайшей степени двойки
     if (lWidthPixels<=glMaxTexDim)
      // если ширина изображения меньше либо равна максимально-допустимому пределу карточки
      lWidthPixels=1<<(int)floor((log ((double)lWidthPixels)/log (2.0f))+0.5f);
     else
      // иначе установить размер равный максимальной степени двойки,
      // которую поддерживает карточка
      lWidthPixels=glMaxTexDim;
     // то же самое повторяется для высоты
     if (lHeightPixels<=glMaxTexDim)
      lHeightPixels=1<<(int)floor ((log ((double)lHeightPixels)/log (2.0f))+0.5f);
     else
      lHeightPixels=glMaxTexDim;

     // создать временный растр
     BITMAPINFO  bi={0}; // нужный нам тип растра
     DWORD   *pBits=0;   // указатель на биты растра

     bi.bmiHeader.biSize=sizeof (BITMAPINFOHEADER);// размер структуры
     bi.bmiHeader.biBitCount=32;                   // 32 бита
     bi.bmiHeader.biWidth=lWidthPixels;            // ширина кратная степени двойки
     // Сделаем изображение расположенным вверх (положительное направление оси Y)
     bi.bmiHeader.biHeight=lHeightPixels;
     bi.bmiHeader.biCompression=BI_RGB;            // RGB формат
     bi.bmiHeader.biPlanes=1;                      // 1 битовая плоскость 

     // создавая растр, таким образом, мы можем установить глубину цвета,
     // а также получить прямой доступ к битам.
     hbmpTemp=CreateDIBSection (hdcTemp, &bi, DIB_RGB_COLORS, (void**)&pBits, 0, 0);

     if (!hbmpTemp)        // создали?
     {                     // сам вижу что нет
      DeleteDC (hdcTemp);  // убить контекст устройства
      pPicture->Release ();// уменьшить счетчик количества интерфейсов IPicture
     }
     else
     {
      // есть растр!
      SelectObject (hdcTemp, hbmpTemp);// загрузить описатель временного растра
                                       // в описатель временного контекста устройства 
      // отрисовка IPicture в растр
      pPicture->Render (hdcTemp, 0, 0, lWidthPixels, lHeightPixels, 0, lHeight, lWidth, -lHeight, 0);

      long i, n=lWidthPixels*lHeightPixels;
      // преобразовать из BGR в RGB формат и устанавливаем значение Alpha

if (i2>=32)// обработка символов { for (i=0; i<n; i++) // Цикл по всем пикселям { BYTE* pPixel=(BYTE*)(&pBits[i]);// берем текущий пиксель BYTE temp=pPixel [0]; // сохраняем первый цвет в переменной // Temp (Синий) pPixel [0]=pPixel [2]; // ставим Красный на место (в первую позицию) pPixel [2]=temp; // ставим значение Temp в третий параметр (3rd) if (pPixel [0]<25 && pPixel [1]<25 && pPixel [2]>55)// убираем фон { pPixel [0]=0; pPixel [1]=0; pPixel [2]=0; pPixel [3]=0; } else { // Меняем цвет на серый, убирая случайные цветные пиксели pPixel [0]=(pPixel [0]+pPixel [1]+pPixel [2])/3; pPixel [1]=(pPixel [0]+pPixel [1]+pPixel [2])/3; pPixel [2]=(pPixel [0]+pPixel [1]+pPixel [2])/3; // Добавляем к градиенту прозрачность, на основе цвета pPixel [3]=(pPixel [0]+pPixel [1]+pPixel [2])/3; } } }
else { for (i=0; i<n; i++)// Цикл по всем пикселям { BYTE* pPixel=(BYTE*)(&pBits[i]);// берем текущий пиксель BYTE temp=pPixel [0]; // сохраняем первый цвет в переменной // Temp (Синий) pPixel [0]=pPixel [2]; // ставим Красный на место (в первую позицию) pPixel [2]=temp; // ставим значение Temp в третий параметр (3rd) pPixel [3]=255; // установить значение alpha =255 } } glGenTextures (1, &texture [i2]);// создаем текстуру texid // типичная генерация текстуры, используя данные из растра glBindTexture (GL_TEXTURE_2D, texture [i2]);// делаем привязку к texid // (измените для нужного вам типа фильтрации) glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//GL_NEAREST // (измените, если хотите использовать мипмап-фильтрацию) glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // (мипмап - множественное отображение (последовательность текстур одного // и того же изображения с уменьшающимся разрешением по мере удаления отображаемого // объекта от наблюдателя)) glTexImage2D (GL_TEXTURE_2D, 0, 4, lWidthPixels, lHeightPixels, 0, GL_RGBA, GL_UNSIGNED_BYTE, pBits); DeleteObject (hbmpTemp);// удаляем объект DeleteDC (hdcTemp); // удаляем контекст устройства pPicture->Release(); //уменьшает счетчик IPicture } } } } } }

Для сравнения выведем обычный и только что созданный шрифты:
void glPrintB (const char *fmt, ...)// Заказная функция «Печати» GL
{
 float length=0; // Переменная для нахождения физической длины текста
 char text [256];// Здесь наша строка
 va_list ap;     // Указатель на переменный список аргументов
 if (fmt==NULL)  // Если нет текста
  return;        // Ничего не делать

 va_start (ap, fmt);         // Анализ строки на переменные
 vsprintf (text, fmt, ap);   // И конвертация символов в реальные коды
 va_end (ap);                // Результат сохраняется в text

 for (int i=0; i<strlen (text); i++)// %
 {
  if (text [i]==17)
   text [i]=37;
 }

 glPushAttrib (GL_LIST_BIT);    // Сохраняет в стеке значения битов списка отображения
 glListBase (base);             // Устанавливает базовый символ в 0

 // Создает списки отображения текста
 glCallLists (strlen (text), GL_UNSIGNED_BYTE, text);
 glPopAttrib ();// Восстанавливает значение Display List Bits
}

// Функция для печати текстурного шрифта; // нет параметров, вместо - глобальные переменные float literaScale=0.25f, literaX=0.0f, literaY=0.0f, literaZ=-2.4325f; char literaText [256]=""; void glPrintLitera () { glEnable (GL_BLEND); for (int i=0; i<strlen (literaText); i++) { int t=literaText [i]; if (t<0) t+=256; if (!((t>126 && t<161) || (t>-1 && t<=32))) { glBindTexture (GL_TEXTURE_2D, texture [t]); glBegin (GL_QUADS); glTexCoord2f (0.0f, 0.0f); glVertex3f (-literaScale/2.0f+literaX+i*literaScale/1.85f, -literaScale/2.0f+literaY, literaZ);// Низ лево glTexCoord2f (1.0f, 0.0f); glVertex3f ( literaScale/2.0f+literaX+i*literaScale/1.85f, -literaScale/2.0f+literaY, literaZ);// Низ право glTexCoord2f (1.0f, 1.0f); glVertex3f ( literaScale/2.0f+literaX+i*literaScale/1.85f, literaScale/2.0f+literaY, literaZ);// Верх право glTexCoord2f (0.0f, 1.0f); glVertex3f (-literaScale/2.0f+literaX+i*literaScale/1.85f, literaScale/2.0f+literaY, literaZ);// Верх лево glEnd (); } } glDisable (GL_BLEND); }
... void paint () { if (pC<10 && !(pLoad>0.0f)) pC++; // OpenGL animation code goes here glPushMatrix (); glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glDisable (GL_DEPTH_TEST);// Выключаем тест глубины // glEnable (GL_MULTISAMPLE_ARB);// если поддерживается видеокартой, дефолтный шрифт будет лучше // Фон glLoadIdentity (); // Сброс просмотра glTranslatef (0.0f, 0.0f, -2.4f); glRotatef (-bgAngle, 0.0f, 0.0f, 1.0f); bgAngle+=0.05f*fpsTweak (); if (bgAngle>180.0f) bgAngle-=360.0f; glColor3f (1.0f, 1.0f, 1.0f); glCallList (bgList); // Вывод текста char txt [256]; sprintf (txt, "Lorem ipsum dolor sit amet, consectetur adipiscing elit"); strcat (txt, ", sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."); // ...сначала системным шрифтом glBindTexture (GL_TEXTURE_2D, texture [0]); glLoadIdentity ();// Сброс просмотра glTranslatef (-1.0f, 0.85f, literaZ); glScalef (0.25f, 0.25f, 1.0f); glColor3f (1.0f, 1.0f, 1.0f); glPrintB ("%s", txt);// Печать текста GL на экран glLoadIdentity ();// Сброс просмотра glTranslatef (-1.0f, 0.65f, literaZ); glScalef (0.18f, 0.18f, 1.0f); glColor3f (1.0f, 0.0f, 0.5f); glPrintB ("%s", txt);// Печать текста GL на экран glLoadIdentity ();// Сброс просмотра glTranslatef (-1.0f, 0.45f, literaZ); glScalef (0.12f, 0.12f, 1.0f); glColor3f (0.5f, 1.0f, 0.0f); glPrintB ("%s", txt);// Печать текста GL на экран glLoadIdentity ();// Сброс просмотра glTranslatef (-1.0f, 0.25f, literaZ); glScalef (0.07f, 0.07f, 1.0f); glColor3f (1.0f, 1.0f, 1.0f); glPrintB ("%s", txt);// Печать текста GL на экран glLoadIdentity ();// Сброс просмотра glTranslatef (-1.0f, 0.15f, literaZ); glScalef (0.07f, 0.07f, 1.0f); glColor3f (1.0f, 1.0f, 0.0f); glPrintB ("%s", txt);// Печать текста GL на экран glDisable (GL_MULTISAMPLE_ARB); glLoadIdentity ();// Сброс просмотра glColor3f (0.5f, 1.0f, 0.0f); glBindTexture (GL_TEXTURE_2D, texture [0]); glBegin (GL_QUADS); glTexCoord2f (0.0f, 0.0f); glVertex3f (-1.25f, -0.01f, literaZ);// Низ лево glTexCoord2f (1.0f, 0.0f); glVertex3f (2.5f, -0.01f, literaZ);// Низ право glTexCoord2f (1.0f, 1.0f); glVertex3f (2.5f, 0.01f, literaZ);// Верх право glTexCoord2f (0.0f, 1.0f); glVertex3f (-1.25f, 0.01f, literaZ);// Верх лево glEnd ();
// Печать тесктурным шрифтом glLoadIdentity ();// Сброс просмотра glColor3f (1.0f, 1.0f, 1.0f); literaScale=0.25f; literaX=-1.0f; literaY=-0.15f, literaZ=-2.4325f;//0.0f; sprintf (literaText, txt); glPrintLitera ();// Печать текста GL на экран glColor3f (1.0f, 0.0f, 0.5f); literaScale=0.18f; literaY=-0.35f; glPrintLitera ();// Печать текста GL на экран glColor3f (0.5f, 1.0f, 0.0f); literaScale=0.12f; literaY=-0.55f; glPrintLitera ();// Печать текста GL на экран glColor3f (1.0f, 1.0f, 1.0f); literaScale=0.07f; literaY=-0.75f; glPrintLitera ();// Печать текста GL на экран glColor3f (1.0f, 1.0f, 0.0f); literaY=-0.85f; glPrintLitera ();// Печать текста GL на экран
// fpsC ();// показывает FPS statusBar (); glEnable (GL_DEPTH_TEST);// Выключаем тест глубины glPopMatrix (); SwapBuffers (hDC); }


Как видно, шрифт получился более плавным и засчёт чуть затемнённых краёв - контрастным. Наибольший эффект достигнут при большем соотношении оригинального шрифта к напечатанному. Кроме того, полученный шрифт будет отображаться независимо от того, есть ли на компьютере кириллица. Также данный шрифт потребляет почти столько же, что и обычный, ресурсов, давая в приведённом выше примере чуть больше FPS.

Скачать полные версии программ.

Скачать игру, ставшую причиной написания данной заметки, с пропатченным шрифтом.

P.S. Оказалось, что хранить шрифт в одной картинке лучше: программа быстрее распаковывается, копируется и загружается. Шрифт можно грузить не целиком (со смещениями) в разные текстуры, а распределять по небольшим текстурам из исходного изображения. Так сделано в играх: Repaint, CornersGL и Checkers.

07.09.2018