【发布时间】:2021-12-31 05:46:02
【问题描述】:
我正在开发一个使用 SharpGL 的 c# (.NET Framework) 程序。该库具有使用来自 Win32 的 wglUseFontBitmaps 调用将文本绘制到 OpenGL 上下文的方法;但是,该方法使用了在 v3.0 中弃用的显示列表。因此,我想找到一种使用 VBO 和 VAO 来绘制文本的方法。但是,即使字体大小为 10 或 12(我需要),wglUseFontBitmaps 方法也会生成相当清晰的文本。
我尝试使用多种方法来匹配该结果,包括 .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+ 与官方
Bitmap和Graphics类一起使用? -
为什么要调整位图文本的大小?如果您需要多个尺寸,只需为每种尺寸制作一个图集,它不会给您带来任何真正的麻烦。然后你可以使用适当的抗锯齿文本(虽然如果你想在 3D 中渲染,进行完整的亚像素渲染 ala ClearType 真的很棘手)。就像 Charlieface 说的那样,当你有 GDI+ 可用而不处理手动本机互操作时,使用 GDI 没有什么意义。如果您能展示您遇到的那种麻烦(例如,用于比较您得到的与您想要的)的屏幕截图,那将非常有帮助。
-
@Luaan,感谢您的评论。我通常会为每个字体系列和大小生成不同的位图;但是,我发现使用
GetGlyphOutlines和 SharpFont,较小的尺寸质量较差,因此我尝试了将它们放大并让 OpenGL 调整它们大小的方法(仅在某些情况下会稍微好一些)。我使用 GDI 是因为我更熟悉它的调用(SharpGL 使用它们),但我会研究 GDI+。我会考虑发布一些图片,虽然它们可以突出我的整体问题,但我发布的代码会生成几乎空的位图,这是我当前的问题。 -
@FTLPhysicsGuy 你应该先尝试更大的位图;如果有一些您不期望的偏移量等,我不会感到惊讶(通常文本大小函数会考虑重音之类的东西,即使它们不存在 - 否则重音字符将不适合)。跨度>