CSharpGL(26)在opengl中实现控件布局/渲染文字
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)
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |