前言
說起顯示文字,估計大家都覺得很簡單。Opengl作為一個專業的繪圖技術,竟然沒有顯示文字的接口。后來經過詳細研究,發現顯示文字是一個非常高深的問題。Opengl作為一個底層API已經不適合提供對應的接口。
環境搭建
在開始之前,我們需要搭建開發環境。OpenGL是C++的接口,C#需要對其進行封裝才可以調用。目前有不少對OpenGL的封裝,我們選用了SharpGL作為我們的類庫。具體步驟如下:
創建一個窗口程序。
NUGet 里面安裝SharpGL和 SharpGL.WinForms.
NUGet 里面安裝SharpGL和 SharpGL.WinForms.
3. 安裝SharpFont.Dependencies和SharpFont.
這是FreeType的類庫,注意在安裝的時候,依賴項要選擇忽略依賴項。
引入FreeType.dll, 下載,這是編譯好的FreeFont.zip.
在Program中引入FreeFont.dll:
在Program中引入FreeFont.dll:
3. 窗口中添加SharpGL. OpenGLControl,由於改控件不能直接從工具箱拖入,所以需要手動修改.designer.cs
partial class Form1 { /// <summary> /// 必需的設計器變量。 /// </summary> private System.ComponentModel.IContainer components = null; /// <summary> /// 清理所有正在使用的資源。 /// </summary> /// <param name="disposing">如果應釋放托管資源,為 true;否則為 false。</param> protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Windows 窗體設計器生成的代碼 /// <summary> /// 設計器支持所需的方法 - 不要修改 /// 使用代碼編輯器修改此方法的內容。 /// </summary> private void InitializeComponent() { this.openGLControl1 = new SharpGL.OpenGLControl(); ((System.ComponentModel.ISupportInitialize)(this.openGLControl1)).BeginInit(); this.SuspendLayout(); // // openGLControl1 // this.openGLControl1.Dock = System.Windows.Forms.DockStyle.Fill; this.openGLControl1.DrawFPS = true; this.openGLControl1.FrameRate = 50; this.openGLControl1.Location = new System.Drawing.Point(0, 0); this.openGLControl1.Name = "openGLControl1"; this.openGLControl1.OpenGLVersion = SharpGL.Version.OpenGLVersion.OpenGL2_1; this.openGLControl1.RenderContextType = SharpGL.RenderContextType.NativeWindow; this.openGLControl1.RenderTrigger = SharpGL.RenderTrigger.Manual; this.openGLControl1.Size = new System.Drawing.Size(563, 473); this.openGLControl1.TabIndex = 0; this.openGLControl1.OpenGLInitialized += new System.EventHandler(this.openGLControl1_OpenGLInitialized); this.openGLControl1.OpenGLDraw += new SharpGL.RenderEventHandler(this.openGLControl1_OpenGLDraw); this.openGLControl1.Resized += new System.EventHandler(this.openGLControl1_Resized); this.openGLControl1.MouseDown += new System.Windows.Forms.MouseEventHandler(this.openGLControl1_MouseDown); this.openGLControl1.MouseMove += new System.Windows.Forms.MouseEventHandler(this.openGLControl1_MouseMove); this.openGLControl1.MouseUp += new System.Windows.Forms.MouseEventHandler(this.openGLControl1_MouseUp); // // Form1 // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(563, 473); this.Controls.Add(this.openGLControl1); this.Name = "Form1"; this.Text = "Form1"; ((System.ComponentModel.ISupportInitialize)(this.openGLControl1)).EndInit(); this.ResumeLayout(false); } #endregion private SharpGL.OpenGLControl openGLControl1; }
功能實現
可行性探討
列表顯示
OpenGL的列表顯示是一個常見的方案,他可以將生成的字形保存在一個列表中,在需要的時候直接調用這個列表,而且效果很清晰。
上圖的文字就是通過列表顯示的。下面是具體的代碼
private void drawCNString(string str, float x, float y, int fontSize, OpenGL gl) { int i; // Create the font based on the face name. var hFont = Win32.CreateFont(fontSize, 0, 0, 0, Win32.FW_DONTCARE, 0, 0, 0, Win32.DEFAULT_CHARSET, Win32.OUT_OUTLINE_PRECIS, Win32.CLIP_DEFAULT_PRECIS, Win32.CLEARTYPE_QUALITY, Win32.CLEARTYPE_NATURAL_QUALITY, "新宋體"); // Select the font handle. var hOldObject = Win32.SelectObject(gl.RenderContextProvider.DeviceContextHandle, hFont); // Create the list base. var list = gl.GenLists(1); gl.RasterPos(x, y); // 逐個輸出字符 for (i = 0; i < str.Length; ++i) { bool result = Win32.wglUseFontBitmapsW(gl.RenderContextProvider.DeviceContextHandle, str[i], 1, list); gl.CallList(list); } // 回收所有臨時資源 //free(wstring); gl.DeleteLists(list, 1); // Reselect the old font. Win32.SelectObject(gl.RenderContextProvider.DeviceContextHandle, hOldObject); // Free the font. Win32.DeleteObject(hFont); //glDeleteLists(list, 1); }
由於OpenGL存儲的顯示列表最大個數不超過256個,而操作系統提供的APIwglUseFontBitmap每次調用都會生成一個列表。顯示英文字母沒有關系,可以一次性生成所有的顯示列表,但是中文就沒法提前將所有的文字顯示列表生成,替代方案是每生成一個漢字的列表,就馬上顯示他,這樣的效率會大大降低。
另一個無法逾越的障礙是,這個顯示列表是生成的結果,看起來還是像素級別的位圖,而不是矢量圖。導致他不能放大或者縮小。
總結:這種方法適合做游戲菜單之類的問題,例如顯示血量什么的。但是不適合顯示圖紙上的文字。
位圖字體還是矢量字體
在早期Opengl程序中,往往是通過位圖來顯示字體,這些年隨着TrueType字體的發展,更多使用TrueType。然而,即使用的是TrueType字體,這些字體本身並不能直接顯示在Opengl中,往往是先生成一個紋理,然后再通過紋理的方式顯示到屏幕上。而紋理中還是位圖,總而言之,顯示字體還是離不開位圖。
系統默認字體還是FreeType
生成文字位圖,可以用系統默認的System.Texting.Font 也可以用FreeType類庫。然而,前者需要本機已經安裝過對應的字體才可以使用。后者可以直接加載字體文件,所以建議選擇FreeType。
圖形字體
另外,AutoCad還支持字形字體(.SHX文件),這種文件描述在一個標准尺寸中,一個文字從哪里下筆,然后往哪個方向划多長然后抬筆,不停重復,知道一個字寫完,跟我們寫字基本上一樣。
這樣的方式是真正的矢量字體,無論怎么放大縮小文字,都會很清晰。同樣,一個圖紙解析完畢后,永遠不需要重新渲染這些字體。
然而他也有他的缺點,他的一個字往往需要寫幾十筆(一筆對應2個點),如果一個圖紙有1萬個字要寫,光寫字就需要占用上百萬定點坐標。有顯存有一定的壓力。
第二個缺點是,由於他是通過線條來顯示文字,無限放大圖紙的時候,看見的就是一根一根的線條。
即便如此,圖形字體也是一個比較好的備選方案。
紋理
而紋理則是一個比較靠譜的方案,紋理的意思就是將一個一個文字生成一個圖片,然后將這些圖片顯示到對應的位置去。
通過測試1000個漢字,按照18像素生成位圖,保存出來的png文件不到1M,而加載到顯卡口,大概占用了十幾M的顯存。
遇到的問題
性能
考慮到一個圖紙可能有上萬條文本,估計幾萬文字,需要避免每次都重新生成問題,而是充分使用緩存,比較好的方式就是使用紋理、頂點緩沖、VBO VAO等技術來完成圖紙渲染。
內存
雖然一次紋理用了十幾M的顯存,但是我們不能無限生成紋理,例如8像素的10 12 16 18 24 36 都生成一遍紋理。
縮放
文字顯示的另外一個問題是縮放,放大的時候有毛刺,縮小就模模糊糊了。
閃爍
如果在縮放后重新生成紋理,不可避免的會造成卡頓或者閃爍。
最終的方案
FreeType
使用FreeType生成文字紋理,在c#可以使用SharpFont封裝庫來調用FreeType.
使用之前首先要創建Face對象
public void SetFont(string filename) { FontFace = new Face(lib, filename); SetSize(this.Size);} public static byte[] RenderString(Library library, Face face, string text,ref int w, ref int h) { var chars = new List<CharInfo>(); var poses = new List<CharTextture>(); float stringWidth = 0; // the measured width of the string float stringHeight = 0; // the measured height of the string // Bottom and top are both positive for simplicity. // Drawing in .Net has 0,0 at the top left corner, with positive X to the right // and positive Y downward. // Glyph metrics have an origin typically on the left side and at baseline // of the visual data, but can draw parts of the glyph in any quadrant, and // even move the origin (via kerning). float top = 0, bottom = 0; float x = 0, y = 0; int rowStart = 0; // Measure the size of the string before rendering it. We need to do this so // we can create the proper size of bitmap (canvas) to draw the characters on. for (int i = 0; i < text.Length; i++) { #region Load character char c = text[i]; // Look up the glyph index for this character. uint glyphIndex = face.GetCharIndex(c); // Load the glyph into the font's glyph slot. There is usually only one slot in the font. face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); var lineHeight = face.Glyph.LinearVerticalAdvance.Value; // Refer to the diagram entitled "Glyph Metrics" at http://www.freetype.org/freetype2/docs/tutorial/step2.html. // There is also a glyph diagram included in this example (glyph-dims.svg). // The metrics below are for the glyph loaded in the slot. float gAdvanceX = (float)face.Glyph.Advance.X; // same as the advance in metrics float gBearingX = (float)face.Glyph.Metrics.HorizontalBearingX; float gWidth = face.Glyph.Metrics.Width.ToSingle(); float gHeight = face.Glyph.Metrics.Height.ToSingle(); var ci = new CharInfo { Char = c, X = x, Y = y, Width = gWidth, Left = gBearingX, Right = gBearingX + gWidth, AdvanceX = gAdvanceX, Top = (float)face.Glyph.Metrics.HorizontalBearingY, Bottom = (float)(gHeight - face.Glyph.Metrics.HorizontalBearingY) }; #endregion #region Top/Bottom // If this character goes higher or lower than any previous character, adjust // the overall height of the bitmap. float glyphTop = (float)face.Glyph.Metrics.HorizontalBearingY; float glyphBottom = (float)(face.Glyph.Metrics.Height - face.Glyph.Metrics.HorizontalBearingY); if (glyphTop > top) top = glyphTop; if (glyphBottom > bottom) bottom = glyphBottom; #endregion if (ci.X + ci.AdvanceX > MaxWidth) { for (var j = rowStart; j < i; j++) { chars[j].Y += top; chars[j].LineTop = top; chars[j].LineBottom = bottom; } y += top + bottom; x = 0; ci.X = x; ci.Y = y; stringHeight += top + bottom ; rowStart = i; } x += ci.AdvanceX; chars.Add(ci); stringWidth = Math.Max(stringWidth, x ); // Accumulate the distance between the origin of each character (simple width). } for (var j = rowStart; j < chars.Count; j++) { chars[j].Y += top; chars[j].LineTop = top; chars[j].LineBottom = bottom; } stringHeight += top + bottom ; // If any dimension is 0, we can't create a bitmap if (stringWidth == 0 || stringHeight == 0) return null; // Create a new bitmap that fits the string. w = (int)Math.Ceiling(stringWidth); int l = 2; while (l < w) { l *= 2; } w = l; h = (int)Math.Ceiling(stringHeight); l = 2; while (l < h) { l *= 2; } h = l; // w = 512; // h = 512; byte[] buff = new byte[w * h * 2]; Pen borderPen = Pens.Blue; Pen shapePen = Pens.Red; // Draw the string into the bitmap. // A lot of this is a repeat of the measuring steps, but this time we have // an actual bitmap to work with (both canvas and bitmaps in the glyph slot). for (int i = 0; i < chars.Count; i++) { var ci = chars[i]; #region Load character char c = ci.Char; ; // Same as when we were measuring, except RenderGlyph() causes the glyph data // to be converted to a bitmap. uint glyphIndex = face.GetCharIndex(c); face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Mono); face.Glyph.RenderGlyph(RenderMode.Normal); FTBitmap ftbmp = face.Glyph.Bitmap; #endregion #region Draw glyph // Whitespace characters sometimes have a bitmap of zero size, but a non-zero advance. // We can't draw a 0-size bitmap, but the pen position will still get advanced (below). if ((ftbmp.Width > 0 && ftbmp.Rows > 0)) { var count = ftbmp.Width * ftbmp.Rows ; if (ftbmp.PixelMode == PixelMode.Mono) { // StringBuilder sb = new StringBuilder(); int cw = ftbmp.Width / 8; if (cw * 8 < ftbmp.Width) { cw++; } for (var r = 0; r < ftbmp.Rows; r++) { for(var col = 0; col < ftbmp.Width; col++) { var bit = col % 8; var bite = (col - bit) / 8; byte pix = ftbmp.BufferData[r * cw + bite]; var toCheck = pix >> (7 - bit); bool hasClr = (toCheck & 1) == 1; var p = (int)Math.Ceiling((ci.Y - ci.Top + r) * w + ci.X + ci.Left + col); if (hasClr) { buff[p * 2] = 255; buff[p * 2+1] = 255; } } } // var shape = sb.ToString(); } else if( ftbmp.PixelMode== PixelMode.Gray) { for (var j = 0; j < ftbmp.Rows; j++) { for (var k = 0; k < ftbmp.Width; k++) { var pos = j * ftbmp.Width + k; var p = (int)Math.Ceiling((ci.Y - ci.Top + j) * w + ci.X + ci.Left + k); var g = (int)ftbmp.BufferData[pos]; if (g > 0) { if (g < 100) { g = (int)Math.Ceiling(g * 2f); } else { g = (int)Math.Ceiling(g * 1.5f); } if (g > 255) { g = 255; } buff[p * 2] = 255; buff[p * 2 + 1] = (byte)g; } } } } ftbmp.Dispose(); } poses.Add(new CharTextture { Char = c, Left = (float)ci.X /w, Top = (float)(ci.Y - ci.LineTop) / h, Right = (float)(ci.X + ci.AdvanceX) / w, Bottom = (float)(ci.Y + ci.LineBottom) / h, WidthScale = ci.AdvanceX / (ci.LineTop + ci.LineBottom) }); #endregion } LastPoses = poses; return buff; }
然后,通過LoadGlyph方法加載字形相關信息:
uint glyphIndex = face.GetCharIndex(c);
face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal);
var lineHeight = face.Glyph.LinearVerticalAdvance.Value;
face.Glyph.RenderGlyph(RenderMode.Normal);
FTBitmap ftbmp = face.Glyph.Bitmap;
通過ftBmp的bufferData可以直接獲取到對應的字形信息,不過要注意的是,如果渲染模式為Mono的話,字形是按位存儲的,其他的是按照字節存儲的,讀取字形信息的時候需要區別對待
if (ftbmp.PixelMode == PixelMode.Mono) { // StringBuilder sb = new StringBuilder(); int cw = ftbmp.Width / 8; if (cw * 8 < ftbmp.Width) { cw++; } for (var r = 0; r < ftbmp.Rows; r++) { for(var col = 0; col < ftbmp.Width; col++) { var bit = col % 8; var bite = (col - bit) / 8; byte pix = ftbmp.BufferData[r * cw + bite]; var toCheck = pix >> (7 - bit); bool hasClr = (toCheck & 1) == 1; var p = (int)Math.Ceiling((ci.Y - ci.Top + r) * w + ci.X + ci.Left + col); if (hasClr) { buff[p * 2] = 255; buff[p * 2+1] = 255; } } } // var shape = sb.ToString(); } else if( ftbmp.PixelMode== PixelMode.Gray) { for (var j = 0; j < ftbmp.Rows; j++) { for (var k = 0; k < ftbmp.Width; k++) { var pos = j * ftbmp.Width + k; var p = (int)Math.Ceiling((ci.Y - ci.Top + j) * w + ci.X + ci.Left + k); var g = (int)ftbmp.BufferData[pos]; if (g > 0) { if (g < 100) { g = (int)Math.Ceiling(g * 2f); } else { g = (int)Math.Ceiling(g * 1.5f); } if (g > 255) { g = 255; } buff[p * 2] = 255; buff[p * 2 + 1] = (byte)g; } } } }
另外,讀取Alpha通道的時候,可以適當調高Alpha通道的值。
TrueType可以參照下面鏈接。
http://www.freetype.org/freetype2/docs/tutorial/step2.html.
紋理+MipMap
MipMap是紋理的一種取樣方式,OpenGL 在加載紋理后,可以生成一系列較小的紋理,在渲染的時候,如果圖紙縮小,則直接使用接近的小紋理,而不是直接使用原始紋理。
private void GetMipTextureViaBuff(OpenGL gl) { fontSizes = new int[] { 12 }; fontService.SetSize(12f); textures = new uint[fontSizes.Length]; int w = 0, h = 0; gl.GenTextures(fontSizes.Length, textures); var data = fontService.GetTextBuff(Chars, ref w, ref h); Coorses.Add(FontService.LastPoses); // Bind the texture. gl.BindTexture(OpenGL.GL_TEXTURE_2D, textures[0]); gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_WRAP_S, OpenGL.GL_CLAMP_TO_BORDER); gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_WRAP_T, OpenGL.GL_CLAMP_TO_BORDER); gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_MIN_FILTER, OpenGL.GL_LINEAR_MIPMAP_LINEAR); gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_MAG_FILTER, OpenGL.GL_LINEAR_MIPMAP_NEAREST); gl.TexEnv(OpenGL.GL_TEXTURE_ENV, OpenGL.GL_TEXTURE_ENV_MODE, OpenGL.GL_REPLACE); gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_BASE_LEVEL, 0); gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_MAX_LEVEL, 2); float[] max_antis = new float[1]; gl.GetFloat(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, max_antis); //多重采樣 gl.TexParameter(OpenGL.GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, max_antis[0] < 4 ? max_antis[0] : 4); //創建MipMap紋理 gluBuild2DMipmaps(OpenGL.GL_TEXTURE_2D, 2, w, h, OpenGL.GL_LUMINANCE_ALPHA, OpenGL.GL_UNSIGNED_BYTE, data); //int size = data .Length; //IntPtr buffer = Marshal.AllocHGlobal(size); //try //{ // Marshal.Copy(data, 0, buffer, size); // gl.Build2DMipmaps(OpenGL.GL_TEXTURE_2D, 2, w, h, OpenGL.GL_LUMINANCE_ALPHA, OpenGL.GL_UNSIGNED_BYTE, buffer); //} //finally //{ // Marshal.FreeHGlobal(buffer); //} }
使用Mipmap紋理可能會多占用大約1/3的顯存,但是一次生成后,不用再次生成。縮放圖紙也能較為順暢的顯示,效果如下:
着色器
使用MipMap紋理,我們需要處理一個問題,那就是顏色。默認着色器會將紋理本身的顏色顯示在屏幕,而我們的需求是根據不同的要求顯示不同的顏色且不用為每個顏色生成紋理。
通過着色器可以解決這個問題:
#region shader VertexShader vertexShader = new VertexShader(); vertexShader.CreateInContext(gl); vertexShader.SetSource(@"#version 130 varying vec2 texcoord;varying vec4 pass_color;void main(void) { gl_Position =gl_ProjectionMatrix * gl_ModelViewMatrix * vec4( gl_Vertex.xy,0,1);//ftransform();// texcoord =gl_Vertex.zw; pass_color=gl_Color; }"); // Create a fragment shader. FragmentShader fragmentShader = new FragmentShader(); fragmentShader.CreateInContext(gl); fragmentShader.SetSource(@"#version 130varying vec2 texcoord;varying vec4 pass_color;uniform sampler2D tex;uniform vec4 color;void main(void) { vec4 clr=texture2D(tex,texcoord );//texcoord gl_FragColor =vec4(1,1,1,clr.a)*pass_color; }"); // Compile them both. vertexShader.Compile(); string msg = GetShaderError(vertexShader.ShaderObject, gl); if (!string.IsNullOrEmpty(msg)) { MessageBox.Show(msg); } fragmentShader.Compile(); msg= GetShaderError(fragmentShader.ShaderObject,gl); if (!string.IsNullOrEmpty(msg)) { MessageBox.Show(msg); } // Build a program. program.CreateInContext(gl); // Attach the shaders. program.AttachShader(vertexShader); program.AttachShader(fragmentShader); program.Link(); int[] parm = new int[1]; gl.GetProgram(program.ProgramObject, OpenGL.GL_LINK_STATUS, parm); if (parm[0] == 0) { StringBuilder smsg = new StringBuilder(1024); IntPtr ptr = IntPtr.Zero; gl.GetProgramInfoLog(program.ProgramObject, 1024, ptr, smsg); msg = smsg.ToString(); if (!string.IsNullOrEmpty(msg)) { MessageBox.Show(msg); } } gl.ValidateProgram(program.ProgramObject); gl.GetProgram (program.ProgramObject, OpenGL.GL_VALIDATE_STATUS, parm); if (parm[0] == 0) { StringBuilder smsg = new StringBuilder(1024); IntPtr ptr = IntPtr.Zero; gl.GetProgramInfoLog(program.ProgramObject, 1024, ptr, smsg); msg = smsg.ToString(); if (!string.IsNullOrEmpty(msg)) { MessageBox.Show(msg); } } #endregion
最后顯示文字的代碼如下:
private void DrawText(string text, float x, float y, float h, float wscale, OpenGL gl,bool bUseShader) { float screenH =(float) Math.Round(h * scale); var x0 = x; var idx = fontSizes.Length - 1; for(var i = 0; i < fontSizes.Length-1; i++) { if (h * scale <= fontSizeTrigers[i]) { idx = i; break; } } this.Text = "fontsize:" + fontSizes[idx]+">"+h*scale; Coors = Coorses[idx]; int simpler2d =(int) textures[idx]; gl.ActiveTexture(textures[idx]); gl.BindTexture(OpenGL.GL_TEXTURE_2D, textures[idx]); gl.Enable(OpenGL.GL_TEXTURE_2D); gl.Begin(OpenGL.GL_QUADS); foreach (var ch in text) { var ci = Coors.Find(c => c.Char ==ch); var w = h * ci.WidthScale * wscale; if (bUseShader) { // gl.TexCoord(ci.Left, ci.Bottom); gl.Vertex4f(x, y, ci.Left, ci.Bottom); // Bottom Left Of The Texture and Quad // gl.TexCoord(ci.Right, ci.Bottom); gl.Vertex4f(x + w, y, ci.Right, ci.Bottom); // Bottom Right Of The Texture and Quad // gl.TexCoord(ci.Right, ci.Top); gl.Vertex4f(x + w, y + h, ci.Right, ci.Top); // Top Right Of The Texture and Quad // gl.TexCoord(ci.Left, ci.Top); gl.Vertex4f(x, y + h, ci.Left, ci.Top); // Top Left Of The Texture and Quad } else { gl.TexCoord(ci.Left, ci.Bottom); gl.Vertex(x, y, 0); // Bottom Left Of The Texture and Quad gl.TexCoord(ci.Right, ci.Bottom); gl.Vertex(x + w, y, 0); // Bottom Right Of The Texture and Quad gl.TexCoord(ci.Right, ci.Top); gl.Vertex(x + w, y + h, 0); // Top Right Of The Texture and Quad gl.TexCoord(ci.Left, ci.Top); gl.Vertex(x, y + h, 0); // Top Left Of The Texture and Quad } x += w; } gl.End(); gl.Disable(OpenGL.GL_TEXTURE_2D); }
進一步優化
VBO+紋理
本文只是解釋了以立即繪制的方式來顯示文字,而真正使用的時候,應該將紋理和VBO結合使用,以達到更高的性能。
最后 ,代碼下載地址為 下載 (正在審批中,估計過2天就可以下載了)。
————————————————
版權聲明:本文為CSDN博主「李世垚」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/snakegod/article/details/81126568
————————————————
版權聲明:本文為CSDN博主「李世垚」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/snakegod/article/details/81126568
