воскресенье, 22 января 2012 г.

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


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;
}


* This source code was highlighted with Source Code Highlighter.

Преобразуем битмап в текстуру

/// <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;
}


* This source code was highlighted with Source Code Highlighter.

И отображаем текстуру

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();
}


* This source code was highlighted with Source Code Highlighter.

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



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

Для корректной передачи альфа-канала требуется произвести дополнительные действия в методе 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;
}


* This source code was highlighted with Source Code Highlighter.

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


Исходный код.

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

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

Комментариев нет:

Отправить комментарий