CSharpGL(26)在opengl中實現控件布局/渲染文字
效果圖
如圖所示,可以將文字、坐標軸固定在窗口的一角。
下載
CSharpGL已在GitHub開源,歡迎對OpenGL有興趣的同學加入(https://github.com/bitzhuwei/CSharpGL)
UI控件布局關鍵點
ILayout
類似Winform控件那樣,控件的位置、大小由其Anchor等屬性決定。窗口大小改變時,控件的位置、大小會隨之改變。
所以模仿Control類,直接使用Anchor作為UIRenderer的接口。
1 /// <summary> 2 /// Supports layout UI element in OpenGL canvas. 3 /// 實現在OpenGL窗口中的UI布局 4 /// </summary> 5 public interface ILayout : ITreeNode<UIRenderer> 6 { 7 //event EventHandler afterLayout; 8 9 /// <summary> 10 /// the edges of the <see cref="GLCanvas"/> to which a UI’s rect is bound and determines how it is resized with its parent. 11 /// <para>something like AnchorStyles.Left | AnchorStyles.Bottom.</para> 12 /// </summary> 13 System.Windows.Forms.AnchorStyles Anchor { get; set; } 14 15 /// <summary> 16 /// Gets or sets the space between viewport and SimpleRect. 17 /// </summary> 18 System.Windows.Forms.Padding Margin { get; set; } 19 20 /// <summary> 21 /// 相對于Parent左下角的位置(Left Down location) 22 /// </summary> 23 System.Drawing.Point Location { get; set; } 24 25 /// <summary> 26 /// Stores width when <see cref="Anchor"/>.Left & <see cref="Anchor"/>.Right is <see cref="Anchor"/>.None. 27 /// <para> and height when <see cref="Anchor"/>.Top & <see cref="Anchor"/>.Bottom is <see cref="Anchor"/>.None.</para> 28 /// </summary> 29 System.Drawing.Size Size { get; set; } 30 31 /// <summary> 32 /// 33 /// </summary> 34 System.Drawing.Size ParentLastSize { get; set; } 35 36 /// <summary> 37 /// 38 /// </summary> 39 int zNear { get; set; } 40 41 /// <summary> 42 /// 43 /// </summary> 44 int zFar { get; set; } 45 46 }
實現在OpenGL窗口中的UI布局
有了數據結構,就可以實現窗口中的UI布局了。當窗口大小改變時,調用下面的函數。

1 /// <summary> 2 /// layout controls in OpenGL canvas. 3 /// <para>This coordinate system is as below.</para> 4 /// <para> /\ y</para> 5 /// <para> |</para> 6 /// <para> |</para> 7 /// <para> |</para> 8 /// <para> |</para> 9 /// <para> |</para> 10 /// <para> |----------------->x</para> 11 /// <para>(0, 0)</para> 12 /// </summary> 13 /// <param name="uiRenderer"></param> 14 internal static void Layout(this ILayout uiRenderer) 15 { 16 ILayout parent = uiRenderer.Parent; 17 if (parent != null) 18 { 19 uiRenderer.Self.DoBeforeLayout(); 20 NonRootNodeLayout(uiRenderer, parent); 21 uiRenderer.Self.DoAfterLayout(); 22 } 23 24 foreach (var item in uiRenderer.Children) 25 { 26 item.Layout(); 27 } 28 29 if (parent != null) 30 { 31 uiRenderer.ParentLastSize = parent.Size; 32 } 33 } 34 35 /// <summary> 36 /// leftRightAnchor = (AnchorStyles.Left | AnchorStyles.Right); 37 /// </summary> 38 private const AnchorStyles leftRightAnchor = (AnchorStyles.Left | AnchorStyles.Right); 39 40 /// <summary> 41 /// topBottomAnchor = (AnchorStyles.Top | AnchorStyles.Bottom); 42 /// </summary> 43 private const AnchorStyles topBottomAnchor = (AnchorStyles.Top | AnchorStyles.Bottom); 44 45 /// <summary> 46 /// Gets <paramref name="currentNode"/>'s location and size according to its state and parent's information. 47 /// </summary> 48 /// <param name="currentNode"></param> 49 /// <param name="parent"></param> 50 private static void NonRootNodeLayout(ILayout currentNode, ILayout parent) 51 { 52 int x, y, width, height; 53 if ((currentNode.Anchor & leftRightAnchor) == leftRightAnchor) 54 { 55 width = parent.Size.Width - currentNode.Margin.Left - currentNode.Margin.Right; 56 //width = currentNode.Size.Width + (parent.Size.Width - currentNode.ParentLastSize.Width); 57 if (width < 0) { width = 0; } 58 } 59 else 60 { 61 width = currentNode.Size.Width; 62 } 63 64 if ((currentNode.Anchor & topBottomAnchor) == topBottomAnchor) 65 { 66 height = parent.Size.Height - currentNode.Margin.Top - currentNode.Margin.Bottom; 67 //height = currentNode.Size.Height + (parent.Size.Height - currentNode.ParentLastSize.Height); 68 if (height < 0) { height = 0; } 69 } 70 else 71 { 72 height = currentNode.Size.Height; 73 } 74 75 if ((currentNode.Anchor & leftRightAnchor) == AnchorStyles.None) 76 { 77 x = (int)( 78 (parent.Size.Width - width) 79 * ((double)currentNode.Margin.Left / (double)(currentNode.Margin.Left + currentNode.Margin.Right))); 80 } 81 else if ((currentNode.Anchor & leftRightAnchor) == AnchorStyles.Left) 82 { 83 x = parent.Location.X + currentNode.Margin.Left; 84 } 85 else if ((currentNode.Anchor & leftRightAnchor) == AnchorStyles.Right) 86 { 87 x = parent.Location.X + parent.Size.Width - currentNode.Margin.Right - width; 88 } 89 else if ((currentNode.Anchor & leftRightAnchor) == leftRightAnchor) 90 { 91 x = parent.Location.X + currentNode.Margin.Left; 92 } 93 else 94 { throw new Exception("uiRenderer should not happen!"); } 95 96 if ((currentNode.Anchor & topBottomAnchor) == AnchorStyles.None) 97 { 98 y = (int)( 99 (parent.Size.Height - height) 100 * ((double)currentNode.Margin.Bottom / (double)(currentNode.Margin.Bottom + currentNode.Margin.Top))); 101 } 102 else if ((currentNode.Anchor & topBottomAnchor) == AnchorStyles.Bottom) 103 { 104 //y = currentNode.Margin.Bottom; 105 y = parent.Location.Y + currentNode.Margin.Bottom; 106 } 107 else if ((currentNode.Anchor & topBottomAnchor) == AnchorStyles.Top) 108 { 109 //y = parent.Size.Height - height - currentNode.Margin.Top; 110 y = parent.Location.Y + parent.Size.Height - currentNode.Margin.Top - height; 111 } 112 else if ((currentNode.Anchor & topBottomAnchor) == topBottomAnchor) 113 { 114 //y = currentNode.Margin.Top + parent.Location.Y; 115 y = parent.Location.Y + currentNode.Margin.Bottom; 116 } 117 else 118 { throw new Exception("This should not happen!"); } 119 120 currentNode.Location = new System.Drawing.Point(x, y); 121 currentNode.Size = new Size(width, height); 122 }
glViewport/glScissor
這是避免復雜的矩陣操作,實現穩定的UI布局顯示的關鍵。glViewport指定了GLRenderer在窗口的渲染位置,glScissor將GLRenderer范圍之外的部分保護起來。
在渲染之前,根據UIRenderer的位置和大小更新viewport和scissor即可。不再需要為UI固定在窗口某處而煞費苦心地設計projection,view,model矩陣了。

1 /// <summary> 2 /// Renderer that supports UI layout. 3 /// 支持2D UI布局的渲染器 4 /// </summary> 5 public class UIRenderer : RendererBase, ILayout 6 { 7 private ViewportSwitch viewportSwitch; 8 private ScissorTestSwitch scissorTestSwitch; 9 private GLSwitchList switchList = new GLSwitchList(); 10 11 /// <summary> 12 /// 13 /// </summary> 14 public GLSwitchList SwitchList 15 { 16 get { return switchList; } 17 } 18 19 /// <summary> 20 /// triggered before layout in <see cref="ILayout"/>.Layout(). 21 /// </summary> 22 public event EventHandler BeforeLayout; 23 /// <summary> 24 /// triggered after layout in <see cref="ILayout"/>.Layout(). 25 /// </summary> 26 public event EventHandler AfterLayout; 27 28 internal void DoBeforeLayout() 29 { 30 EventHandler BeforeLayout = this.BeforeLayout; 31 if (BeforeLayout != null) 32 { 33 BeforeLayout(this, null); 34 } 35 } 36 37 internal void DoAfterLayout() 38 { 39 EventHandler AfterLayout = this.AfterLayout; 40 if (AfterLayout != null) 41 { 42 AfterLayout(this, null); 43 } 44 } 45 46 /// <summary> 47 /// 48 /// </summary> 49 public RendererBase Renderer { get; protected set; } 50 /// <summary> 51 /// 52 /// </summary> 53 /// <param name="anchor"></param> 54 /// <param name="margin"></param> 55 /// <param name="size"></param> 56 /// <param name="zNear"></param> 57 /// <param name="zFar"></param> 58 public UIRenderer( 59 System.Windows.Forms.AnchorStyles anchor, System.Windows.Forms.Padding margin, 60 System.Drawing.Size size, int zNear, int zFar) 61 { 62 this.Children = new ChildList<UIRenderer>(this);// new ILayoutList(this); 63 64 this.Anchor = anchor; this.Margin = margin; 65 this.Size = size; this.zNear = zNear; this.zFar = zFar; 66 } 67 68 /// <summary> 69 /// 70 /// </summary> 71 public System.Windows.Forms.AnchorStyles Anchor { get; set; } 72 73 /// <summary> 74 /// 75 /// </summary> 76 public System.Windows.Forms.Padding Margin { get; set; } 77 78 /// <summary> 79 /// 80 /// </summary> 81 public System.Drawing.Point Location { get; set; } 82 83 /// <summary> 84 /// 85 /// </summary> 86 public System.Drawing.Size Size { get; set; } 87 /// <summary> 88 /// 89 /// </summary> 90 public System.Drawing.Size ParentLastSize { get; set; } 91 92 /// <summary> 93 /// 94 /// </summary> 95 public int zNear { get; set; } 96 97 /// <summary> 98 /// 99 /// </summary> 100 public int zFar { get; set; } 101 102 /// <summary> 103 /// 104 /// </summary> 105 protected override void DoInitialize() 106 { 107 this.viewportSwitch = new ViewportSwitch(); 108 this.scissorTestSwitch = new ScissorTestSwitch(); 109 110 RendererBase renderer = this.Renderer; 111 if (renderer != null) 112 { 113 renderer.Initialize(); 114 } 115 } 116 117 /// <summary> 118 /// 119 /// </summary> 120 /// <param name="arg"></param> 121 protected override void DoRender(RenderEventArg arg) 122 { 123 this.viewportSwitch.X = this.Location.X; 124 this.viewportSwitch.Y = this.Location.Y; 125 this.viewportSwitch.Width = this.Size.Width; 126 this.viewportSwitch.Height = this.Size.Height; 127 this.scissorTestSwitch.X = this.Location.X; 128 this.scissorTestSwitch.Y = this.Location.Y; 129 this.scissorTestSwitch.Width = this.Size.Width; 130 this.scissorTestSwitch.Height = this.Size.Height; 131 132 this.viewportSwitch.On(); 133 this.scissorTestSwitch.On(); 134 int count = this.switchList.Count; 135 for (int i = 0; i < count; i++) { this.switchList[i].On(); } 136 137 // 把所有在此之前渲染的內容都推到最遠。 138 // Push all rendered stuff to farest position. 139 OpenGL.Clear(OpenGL.GL_DEPTH_BUFFER_BIT); 140 141 RendererBase renderer = this.Renderer; 142 if (renderer != null) 143 { 144 renderer.Render(arg); 145 } 146 147 for (int i = count - 1; i >= 0; i--) { this.switchList[i].Off(); } 148 this.scissorTestSwitch.Off(); 149 this.viewportSwitch.Off(); 150 } 151 152 /// <summary> 153 /// 154 /// </summary> 155 protected override void DisposeUnmanagedResources() 156 { 157 RendererBase renderer = this.Renderer; 158 if (renderer != null) 159 { 160 renderer.Dispose(); 161 } 162 } 163 164 /// <summary> 165 /// 166 /// </summary> 167 public UIRenderer Self { get { return this; } } 168 169 /// <summary> 170 /// 171 /// </summary> 172 public UIRenderer Parent { get; set; } 173 174 //ChildList<UIRenderer> children; 175 176 /// <summary> 177 /// 178 /// </summary> 179 [Editor(typeof(IListEditor<UIRenderer>), typeof(UITypeEditor))] 180 public ChildList<UIRenderer> Children { get; private set; } 181 }
疊加/覆蓋
注意在UIRenderer.DoRender(RenderEventArgs arg)中,使用
1 // 把所有在此之前渲染的內容都推到最遠。 2 // Push all rendered stuff to farest position. 3 OpenGL.Clear(OpenGL.GL_DEPTH_BUFFER_BIT);
把所有在此之前渲染的內容都推到最遠。
從ILayout的定義中可以看到,控件與控件組成了一個樹結構。其根結點是覆蓋整個窗口的控件,在渲染UI時處于第一個渲染的位置,然后渲染它的各個子結點代表的控件。這就實現了子控件能夠完全覆蓋在父控件之上。
我突然想到了WPF。
渲染文字
從TTF文件獲取字形
(https://github.com/MikePopoloski/SharpFont)是一個純C#的解析TTF文件的庫,能夠代替C++的FreeType。我將其稍作修改,實現了從TTF文件獲取任意uncode字形,進而獲取字形紋理,實現渲染文字的功能。
例如下面這幾個字形紋理。
使用FontResource
FontResource類型封裝了使用字形貼圖的功能。
使用方式也非常簡單。首先創建一個字體資源對象。
1 FontResource fontResouce = FontResource.Load(ttfFilename, ' ', (char)126);
然后交給GLText。
1 var glText = new GLText(AnchorStyles.Left | AnchorStyles.Top, 2 new Padding(3, 3, 3, 3), new Size(850, 50), -100, 100, fontResouce); 3 glText.Initialize(); 4 glText.SetText("The quick brown fox jumps over the lazy dog!");
GLText在初始化時指定此字體對象包含的二維紋理。
1 protected override void DoInitialize() 2 { 3 base.DoInitialize(); 4 5 this.Renderer.SetUniform("fontTexture", this.fontResource.GetSamplerValue()); 6 }
2016-07-30
現在我已經廢棄了FontResource,改用更簡單的實現方式(FontTexture)。
FontResource需要通過復雜的SharpFont來自行解析TTF文件。我至今沒有詳細看過SharpFont的代碼,因為SharpFont實在太大了。而FontTexture直接借助System.Drawing.Font類型的Font.MeasureString()方法來獲取字形的大小,并且可以通過Graphics.DrawString()把字形貼到 Bitmap 對象上。這就解決了獲取文字貼圖及其UV字典的問題。
不得不說.net framework自帶類庫的功能之豐富,簡直富可敵國。
2016-8-3
如何創建一個對象,然后用UI的方式渲染?
創建一個對象SomeRenderer時,像普通對象一樣,用IBufferable+Renderer的方式創建模型和渲染器(或者用RendererBase,這可以使用Legacy OpenGL)。注意,模型的邊界應該是(-0.5, -0.5, -0.5)到(0.5, 0.5, 0.5),即邊長為(1, 1, 1)且中心在原點的立方體。如此一來,就可以在SomeRenderer的DoRender()方法里指定對象的縮放比例為:
1 mat4 model = glm.scale(mat4.identity(), new vec3(this.Size.Width, this.Size.Height, 1));
這樣的縮放比例就可以恰好使得SomeRenderer的模型填滿UI的矩形范圍。
總結
CSharpGL支持控件布局,支持渲染文字了。
歡迎對OpenGL有興趣的同學關注(https://github.com/bitzhuwei/CSharpGL)
文章列表