我正在开发一个使用SharpGL的c#(.NET Framework(程序。该库具有使用Win32中的wglUseFontBitmaps调用将文本绘制到OpenGL上下文的方法;但是,该方法使用的显示列表在v3.0中被弃用。因此,我想找到一种使用VBO和VAO绘制文本的方法。然而,wglUseFontBitmaps方法即使在字体大小为10或12(我需要(的情况下也能产生相当清晰的文本。
我已经尝试使用几种方法来匹配这个结果,包括.NET的GlyphTypeface.GetGlyphOutlines和SharpFont(封装FreeType(。使用这两种方法,我尝试渲染更大尺寸的字体(没有抗锯齿(,并让OpenGL将它们缩放到更小的尺寸。我仍然无法获得与wglUseFontBitmaps相匹配的可靠且好看的结果。
因此,我目前的尝试是使用Win32 GDI API来编写文本,假设它可能会产生与wglUseFontBitmaps类似的结果;然而,我无法开始工作的第一步——只是将一个字符写入位图。
下面我发布了一个完整的c#程序文件。它可以作为.NET Framework控制台应用程序进行编译,但必须添加对System.Drawing的引用,并且必须打开";允许不安全代码";在"构建"选项卡下的项目首选项中。
目前,它会创建非常奇怪的位图文件(顺便说一句,它会将名为"TMP.BMP"的测试文件写入您的桌面文件夹(。
这是代码——有点长,但包括运行测试所需的所有内容:
using System;
using System.Drawing;
using System.Runtime.InteropServices;
namespace CharToBitmapConsoleTest
{
class Program
{
static void Main(string[] args)
{
var tester = new CharToBitmapTester();
tester.RunTests();
}
// Calls the TestWithCharacter method a few times for testing
public class CharToBitmapTester
{
public void RunTests()
{
var fontFamilyName = "Calibri";
var fontHeight = 14;
TestWithCharacter((int)'%', fontFamilyName, fontHeight);
TestWithCharacter((int)'#', fontFamilyName, fontHeight);
TestWithCharacter((int)'X', fontFamilyName, fontHeight);
TestWithCharacter((int)'H', fontFamilyName, fontHeight);
}
/// <summary>
/// Attempts to do every step needed to write a characte (corersponding to the given
/// unicode index) into a bitmap using the given font family name and font height.
/// The test returns true if any bits were written to memory as a result of the
/// attempt. The test also writes a bitmap file (TMP.BMP) to the Users's desktop.
/// </summary>
/// <param name="unicodeIndex"></param>
/// <param name="fontFamilyName"></param>
/// <param name="fontHeight"></param>
/// <returns></returns>
public bool TestWithCharacter(int unicodeIndex, string fontFamilyName, int fontHeight)
{
//var hDC = gl.RenderContextProvider.DeviceContextHandle;
// Get the desktop DC.
IntPtr desktopDC = WinGdi32.GetDC(IntPtr.Zero);
// Create our DC as a compatible DC for the desktop.
var hDC = WinGdi32.CreateCompatibleDC(desktopDC);
// Create the font handle (IntPtr) for the WinGDI font object
var hFont = WinGdi32.CreateFont(fontHeight, 0, 0, 0, WinGdi32.FW_DONTCARE, 0, 0, 0, WinGdi32.DEFAULT_CHARSET,
WinGdi32.OUT_OUTLINE_PRECIS, WinGdi32.CLIP_DEFAULT_PRECIS, WinGdi32.CLEARTYPE_QUALITY, WinGdi32.VARIABLE_PITCH, fontFamilyName);
// Select the font object into the Device Context
// GDI actions will use hFont as the current font object
WinGdi32.SelectObject(hDC, hFont);
// Get the true widths for the glyph placement of all the characters
var charWidthInfoArray = new WinGdi32.ABCFLOAT[256];
WinGdi32.GetCharABCWidthsFloat(hDC, 0, 255, charWidthInfoArray);
char character = (char)unicodeIndex;
string characterAsString = character.ToString();
var characterWidthInfo = charWidthInfoArray[unicodeIndex];
var characterFullWidth = characterWidthInfo.abcfA + characterWidthInfo.abcfB + characterWidthInfo.abcfC;
var glyphUnitWidth = (int)Math.Ceiling(characterWidthInfo.abcfB);
var glyphUnitHeight = (int)fontHeight;
//*************************************************************************************
// Create a DIBSection
//
// Start with the BITMAPINFO
var bitCount = 24;// 32;
var info = new WinGdi32.BITMAPINFO();
// Set the data.
info.biSize = Marshal.SizeOf(info);
info.biBitCount = (short)bitCount;
info.biPlanes = 1;
info.biWidth = glyphUnitWidth;
info.biHeight = glyphUnitHeight;
IntPtr bits;
// Create the bitmap.
var hBitmap = WinGdi32.CreateDIBSection(hDC, ref info, WinGdi32.DIB_RGB_COLORS, out bits, IntPtr.Zero, 0);
WinGdi32.SelectObject(hDC, hBitmap);
// Set the pixel format.
var pixelFormat = new WinGdi32.PIXELFORMATDESCRIPTOR();
pixelFormat.Init();
pixelFormat.nVersion = 1;
pixelFormat.dwFlags = (WinGdi32.PFD_DRAW_TO_BITMAP | WinGdi32.PFD_SUPPORT_OPENGL | WinGdi32.PFD_SUPPORT_GDI);
pixelFormat.iPixelType = WinGdi32.PFD_TYPE_RGBA;
pixelFormat.cColorBits = (byte)bitCount;
pixelFormat.cDepthBits = (byte)bitCount;
pixelFormat.iLayerType = WinGdi32.PFD_MAIN_PLANE;
// Try to match a pixel format and note failure if we get an error
int iPixelformat;
if ((iPixelformat = WinGdi32.ChoosePixelFormat(hDC, pixelFormat)) == 0)
return false;
// Sets pixel format and test for errors
if (WinGdi32.SetPixelFormat(hDC, iPixelformat, pixelFormat) == 0)
{
// Falure -- clear error and retur nfalse
int _ = Marshal.GetLastWin32Error();
return false;
}
// Done Creating a DIBSection
// If I understand correctly, the hDC now has the DIBSction as the current object and
// calls related to drawing should go to it (and, I belive, fill our "bits" buffer)
//*************************************************************************************
// Set a location to output the text -- not really sure what to use here but going with 0, 0
int x = 0;
int y = 9;
// Could play around with foreground and background colors...
//var prevFgColorRef = WinGdi32.SetTextColor(hDC, ColorTranslator.ToWin32(System.Drawing.Color.White));
//var prevBkColorRef = WinGdi32.SetBkColor(hDC, ColorTranslator.ToWin32(System.Drawing.Color.Black));
// NOTE: we've already set hFont as the current font and hBitmap as the current bitmap...
// Output the text -- this should go to the current bitmap and fill the bits buffer, right?
var textOutWorked = WinGdi32.TextOut(hDC, x, y, characterAsString.ToString(), 1);
if (textOutWorked)
{
System.Diagnostics.Debug.WriteLine("TextOut finished without complaint");
}
else
{
System.Diagnostics.Debug.WriteLine("TextOut says it did NOT work");
return false;
}
var dibSectionSize = glyphUnitWidth * glyphUnitHeight * bitCount;
var testArray = new byte[dibSectionSize];
Marshal.Copy(bits, testArray, 0, dibSectionSize);
var bitsWithData = 0;
foreach (var b in testArray)
{
if (b != 0)
{
bitsWithData++;
}
}
System.Diagnostics.Debug.WriteLine(bitsWithData > 0 ?
$"Test Wrote something to the bits! Font {fontFamilyName}; Character: {characterAsString}!" :
$"Test did NOT write to the bits! Font {fontFamilyName}; Character: {characterAsString}!");
var stride = bitCount * glyphUnitWidth;
using (Bitmap bitmap = new Bitmap(glyphUnitWidth, glyphUnitHeight, stride, System.Drawing.Imaging.PixelFormat.Format24bppRgb, bits))
{
bitmap.Save(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "TMP.BMP"));
}
return bitsWithData > 0;
}
}
public static class WinGdi32
{
public const string Gdi32 = "gdi32.dll";
public const string User32 = "user32.dll";
/// <summary>
/// The TextOut function writes a character string at the specified location, using the currently selected font, background color, and text color
/// </summary>
/// <param name="hDC">A handle to the device context.</param>
/// <param name="x">The x-coordinate, in logical coordinates, of the reference point that the system uses to align the string.</param>
/// <param name="y">The y-coordinate, in logical coordinates, of the reference point that the system uses to align the string.</param>
/// <param name="str">The string to be drawn. The string does not need to be zero-terminated, because cchString specifies the length of the string.</param>
/// <param name="c">The length of the string in characters.</param>
/// <returns></returns>
[DllImport(Gdi32, SetLastError = true)]
public static extern bool TextOut(IntPtr hDC, int x, int y, [MarshalAs(UnmanagedType.LPStr)] string str, int c);
/// <summary>
/// The GetCharABCWidthsFloat function retrieves the widths, in logical units, of consecutive characters in a specified range from the current font.
/// </summary>
/// <param name="hDC">Handle to the device context.</param>
/// <param name="iFirstChar">Specifies the code point of the first character in the group of consecutive characters where the ABC widths are seeked.</param>
/// <param name="iLastChar">Specifies the code point of the last character in the group of consecutive characters where the ABC widths are seeked. This range is inclusive. An error is returned if the specified last character precedes the specified first character</param>
/// <param name="ABCF">An array of ABCFLOAT structures that receives the character widths, in logical units</param>
/// <returns></returns>
[DllImport(Gdi32, SetLastError = true)]
public static extern bool GetCharABCWidthsFloat(IntPtr hDC, uint iFirstChar, uint iLastChar, [Out, MarshalAs(UnmanagedType.LPArray)] ABCFLOAT[] ABCF);
[DllImport(Gdi32, SetLastError = true)]
public static extern IntPtr SetTextColor(IntPtr hDC, int crColor);
[DllImport(Gdi32, SetLastError = true)]
public static extern IntPtr SetBkColor(IntPtr hDC, int crColor);
[DllImport(Gdi32, SetLastError = true)]
public unsafe static extern int ChoosePixelFormat(IntPtr hDC, [In, MarshalAs(UnmanagedType.LPStruct)] PIXELFORMATDESCRIPTOR ppfd);
[DllImport(Gdi32, SetLastError = true)]
public static extern IntPtr CreateDIBSection(IntPtr hdc, [In] ref BITMAPINFO pbmi, uint pila, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);
[DllImport(Gdi32, SetLastError = true)]
public unsafe static extern int SetPixelFormat(IntPtr hDC, int iPixelFormat, [In, MarshalAs(UnmanagedType.LPStruct)] PIXELFORMATDESCRIPTOR ppfd);
[DllImport(User32, SetLastError = true)]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport(Gdi32, SetLastError = true)]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport(Gdi32, SetLastError = true)]
public static extern IntPtr CreateFont(int nHeight, int nWidth, int nEscapement,
int nOrientation, uint fnWeight, uint fdwItalic, uint fdwUnderline, uint fdwStrikeOut,
uint fdwCharSet, uint fdwOutputPrecision, uint fdwClipPrecision, uint fdwQuality,
uint fdwPitchAndFamily, string lpszFace);
[DllImport(Gdi32, SetLastError = true)]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
/// <summary>
/// The SIZE structure specifies the width and height of a rectangle.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public class SIZE
{
/// <summary>
/// Specifies the rectangle's width. The units depend on which function uses this.
/// </summary>
public long cx;
/// <summary>
/// Specifies the rectangle's height. The units depend on which function uses this.
/// </summary>
public long cy;
}
/// <summary>
/// The ABCFLOAT structure contains the A, B, and C widths of a font character.
/// </summary>
public struct ABCFLOAT
{
/// <summary>
/// The A spacing of the character. The A spacing is the distance to add to the current position before drawing the character glyph.
/// </summary>
public float abcfA;
/// <summary>
/// The B spacing of the character. The B spacing is the width of the drawn portion of the character glyph.
/// </summary>
public float abcfB;
/// <summary>
/// The C spacing of the character. The C spacing is the distance to add to the current position to provide white space to the right of the character glyph.
/// </summary>
public float abcfC;
}
[StructLayout(LayoutKind.Sequential)]
public struct BITMAPINFO
{
public Int32 biSize;
public Int32 biWidth;
public Int32 biHeight;
public Int16 biPlanes;
public Int16 biBitCount;
public Int32 biCompression;
public Int32 biSizeImage;
public Int32 biXPelsPerMeter;
public Int32 biYPelsPerMeter;
public Int32 biClrUsed;
public Int32 biClrImportant;
public void Init()
{
biSize = Marshal.SizeOf(this);
}
}
[StructLayout(LayoutKind.Explicit)]
public class PIXELFORMATDESCRIPTOR
{
[FieldOffset(0)]
public UInt16 nSize;
[FieldOffset(2)]
public UInt16 nVersion;
[FieldOffset(4)]
public UInt32 dwFlags;
[FieldOffset(8)]
public Byte iPixelType;
[FieldOffset(9)]
public Byte cColorBits;
[FieldOffset(10)]
public Byte cRedBits;
[FieldOffset(11)]
public Byte cRedShift;
[FieldOffset(12)]
public Byte cGreenBits;
[FieldOffset(13)]
public Byte cGreenShift;
[FieldOffset(14)]
public Byte cBlueBits;
[FieldOffset(15)]
public Byte cBlueShift;
[FieldOffset(16)]
public Byte cAlphaBits;
[FieldOffset(17)]
public Byte cAlphaShift;
[FieldOffset(18)]
public Byte cAccumBits;
[FieldOffset(19)]
public Byte cAccumRedBits;
[FieldOffset(20)]
public Byte cAccumGreenBits;
[FieldOffset(21)]
public Byte cAccumBlueBits;
[FieldOffset(22)]
public Byte cAccumAlphaBits;
[FieldOffset(23)]
public Byte cDepthBits;
[FieldOffset(24)]
public Byte cStencilBits;
[FieldOffset(25)]
public Byte cAuxBuffers;
[FieldOffset(26)]
public SByte iLayerType;
[FieldOffset(27)]
public Byte bReserved;
[FieldOffset(28)]
public UInt32 dwLayerMask;
[FieldOffset(32)]
public UInt32 dwVisibleMask;
[FieldOffset(36)]
public UInt32 dwDamageMask;
public void Init()
{
nSize = (ushort)Marshal.SizeOf(this);
}
}
public const uint FW_DONTCARE = 0;
public const uint ANSI_CHARSET = 0;
public const uint DEFAULT_CHARSET = 1;
public const uint SYMBOL_CHARSET = 2;
public const uint OUT_OUTLINE_PRECIS = 8;
public const uint CLIP_DEFAULT_PRECIS = 0;
public const uint CLEARTYPE_QUALITY = 5;
public const uint FIXED_PITCH = 1;
public const uint VARIABLE_PITCH = 2;
public const uint DIB_RGB_COLORS = 0;
public const uint PFD_DRAW_TO_BITMAP = 8;
public const uint PFD_SUPPORT_GDI = 16;
public const uint PFD_SUPPORT_OPENGL = 32;
public const byte PFD_TYPE_RGBA = 0;
public const sbyte PFD_MAIN_PLANE = 0;
}
}
}
如果有人能告诉我如何获得将单个字符实际写入位图的代码,我应该能够从那里获得它,并在我的OpenGL项目中使用它。
谢谢!
为了显示,这将是一个使用GDI+的示例:
var text = "Hello world!";
var font = new Font("Calibri", 8);
var bitmap = new Bitmap(200, 100);
var targetRectangle = new RectangleF(0, 0, 200, 120);
var sf = new StringFormat(StringFormat.GenericDefault);
sf.SetMeasurableCharacterRanges(
Enumerable.Range(0, text.Length)
.Select(i => new CharacterRange(i, 1)).ToArray());
using (var gr = Graphics.FromImage(bitmap))
{
gr.Clear(Color.Black);
gr.DrawString(text, font, Brushes.White, targetRectangle, sf);
var ranges = gr.MeasureCharacterRanges(text, font, targetRectangle, sf);
gr.DrawRectangles(Pens.Red, ranges.Select(i => i.GetBounds(gr)).ToArray());
}
bitmap.Dump();
这产生了一个位图,该位图包含文本";你好,世界"以及每个字符的区域(尽管这些实际上不是字符的边界——注意重音(。这允许您创建一个字符图谱以便轻松渲染(当然,您希望字符之间有更多的间距来处理重叠(。
这会产生大约6 emsize的清晰文本(不过,8要好得多(,但这确实需要亚像素渲染,这意味着这只适用于不需要缩放字体的情况(例如,对于UI(。有很多方法可以调整这一点——默认值是渲染速度和质量之间的折衷。例如,使用gr.PixelOffsetMode = PixelOffsetMode.Half;
可以通过更好地对齐字符(以减少"像素完美"为代价(,为您提供更清晰、更好的文本(尤其是对于未针对小字体或最小混叠进行优化的字体中的小字体(。当你达到12左右的emsize时,即使有一点缩放,伪影也往往是不可见的(前提是你正确配置了ClearType(。当然,结果取决于显示器像素的几何形状——它们是不可移植的。GDI和GDI+主要用于表示,而不是生成与设备无关的图形。你可能会想使用专门的软件。
您也可以使用gr.TextRenderingHint = TextRenderingHint.SingleBitPerPixel;
删除所有提示。这将倾向于用现代字体产生糟糕的文本渲染;它确实需要一个优化的字体,以便在没有子像素的情况下进行渲染。主要的优点是,无论你的硬件如何,它看起来都是一样的,但看起来往往很糟糕。