Pull to refresh

XNA: Вывод текста системными шрифтами

Reading time3 min
Views6K
XNA предполагает вывод текста только заранее подготовленными растровыми шрифтами. И это правильно. Быстро, не зависимо от ОС, предсказуемые размеры текста.
В моём случае требовалось совершенно противоположное. Произвольный выбор гарнитуры и размера шрифта, и низкие требования к производительности. Задача оказалась довольно трудной. Информации в интернете оказалось мало и она была крайне разрознена.


Условия: 2D приложение, пользователь в любое время должен иметь возможность изменить гарнитуру, стиль и размер шрифта.

Способ решения довольно прост. Создаём битмап с нужным текстом, создаём из него текстуру и отображаем её.

Битмап создаём средствами GDI.
/// <summary> Draw text on bitmap </summary>
/// <returns>Bitmap with text</returns>
private System.Drawing.Bitmap Layout() {

  // Get font
  var font = new System.Drawing.Font( fontName, fontSize, fontStyle);

  // Get text size
  var bitmap = new System.Drawing.Bitmap( 1, 1 );
  var graphics = System.Drawing.Graphics.FromImage( bitmap );
  var textSize = graphics.MeasureString( text, font );

  // Draw text on bitmap
  bitmap = new System.Drawing.Bitmap( (int) textSize.Width, (int) textSize.Height );
  graphics = System.Drawing.Graphics.FromImage( bitmap );
  graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
  graphics.DrawString( text, font, new System.Drawing.SolidBrush( this.color ), 0f, 0f );

  return bitmap;
}


Преобразуем битмап в текстуру
/// <summary> Create texture from bitmap </summary>
/// <param name="gdev">Graphic device</param>
/// <param name="bmp">Bitmap</param>
/// <returns>Texture</returns>
private static Texture2D TextureFromBitmap(GraphicsDevice gdev, System.Drawing.Bitmap bmp) {

  Stream fs = new MemoryStream();
  bmp.Save(fs, System.Drawing.Imaging.ImageFormat.Png);
  var tex = Texture2D.FromStream(gdev, fs);
  fs.Close();
  fs.Dispose();

  return tex;
}


И отображаем текстуру
public void Draw() {

  var spriteBatch = new SpriteBatch( graphicsDevice );

  spriteBatch.Begin();

  spriteBatch.Draw(
   texture,
   coordinate,
   new Rectangle(0, 0, texture.Width, texture.Height),
   Color.White,
   0f, new Vector2(0, 0),
   1.0f / textureDownsizeRatio,
   SpriteEffects.None, 0);
  spriteBatch.End();
}


В результате получаем текст с рваными краями, полное отсутствие сглаживания.


Конвертация битмапа в текстуру оказалась нетривиальной процедурой.
Метод найден здесь

Для корректной передачи альфа-канала требуется произвести дополнительные действия в методе TextureFromBitmap.
Сначала отрисовываем в текстуру данные о цветах битмапа, потом информацию об альфа-канале.
/// <summary> Create texture from bitmap </summary>
/// <param name="gdev">Graphic device</param>
/// <param name="bmp">Bitmap</param>
/// <returns>Texture</returns>
private static Texture2D TextureFromBitmap(GraphicsDevice gdev, System.Drawing.Bitmap bmp) {

  Stream fs = new MemoryStream();
  bmp.Save(fs, System.Drawing.Imaging.ImageFormat.Png);
  var tex = Texture2D.FromStream(gdev, fs);
  fs.Close();
  fs.Dispose();

  // Setup a render target to hold our final texture which will have premulitplied alpha values
  var res = new RenderTarget2D(gdev, tex.Width, tex.Height);

  gdev.SetRenderTarget(res);
  gdev.Clear(Color.Black);

  // Multiply each color by the source alpha, and write in just the color values into the final texture
  var blendColor = new BlendState {
    ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue,
    AlphaDestinationBlend = Blend.Zero,
    ColorDestinationBlend = Blend.Zero,
    AlphaSourceBlend = Blend.SourceAlpha,
    ColorSourceBlend = Blend.SourceAlpha
  };

  var spriteBatch = new SpriteBatch(gdev);
  spriteBatch.Begin(SpriteSortMode.Immediate, blendColor);
  spriteBatch.Draw(tex, tex.Bounds, Color.White);
  spriteBatch.End();

  // Now copy over the alpha values from the PNG source texture to the final one, without multiplying them
  var blendAlpha = new BlendState {
    ColorWriteChannels = ColorWriteChannels.Alpha,
    AlphaDestinationBlend = Blend.Zero,
    ColorDestinationBlend = Blend.Zero,
    AlphaSourceBlend = Blend.One,
    ColorSourceBlend = Blend.One
  };

  spriteBatch.Begin(SpriteSortMode.Immediate, blendAlpha);
  spriteBatch.Draw(tex, tex.Bounds, Color.White);
  spriteBatch.End();

  // Release the GPU back to drawing to the screen
  gdev.SetRenderTarget(null);

  return res;
}


Теперь получаем удовлетворительный результат.


Исходный код

Проблемы:
1. Медленно. Ни в коем случае не надо использовать этот метод в играх и тем более на мобильных устройствах.
2. Не надежно. Нужных шрифтов может не оказаться в системе.
3. Желательно отслеживать какая часть текста будет отображаться на экране и обрезать невидимый текст. Максимальный размер текстуры 2048x2048. Если размер битмапа будет больше, то текстура будет создана максимального размера и потом растянута средствами видеокарты до нужного размера. Текст будет размытым.

Можно избавиться от переконвертирования в PNG в TextureFromBitmap используя неуправляемый код. Пример можно посмотреть здесь.
Tags:
Hubs:
+9
Comments17

Articles