CSharpGL(26)在opengl中实现控件布局/渲染文字
效果图
如图所示,可以将文字、坐标轴固定在窗口的一角。
下载
CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
UI控件布局关键点
ILayout
类似Winform控件那样,控件的位置、大小由其Anchor等属性决定。窗口大小改变时,控件的位置、大小会随之改变。
所以模仿Control类,直接使用Anchor作为UIRenderer的接口。
/// <summary>
/// Supports layout UI element in OpenGL canvas.
/// 实现在OpenGL窗口中的UI布局
/// </summary>
public interface ILayout : ITreeNode<UIRenderer>
{
//event EventHandler afterLayout; /// <summary>
/// the edges of the <see cref="GLCanvas"/> to which a UI’s rect is bound and determines how it is resized with its parent.
/// <para>something like AnchorStyles.Left | AnchorStyles.Bottom.</para>
/// </summary>
System.Windows.Forms.AnchorStyles Anchor { get; set; } /// <summary>
/// Gets or sets the space between viewport and SimpleRect.
/// </summary>
System.Windows.Forms.Padding Margin { get; set; } /// <summary>
/// 相对于Parent左下角的位置(Left Down location)
/// </summary>
System.Drawing.Point Location { get; set; } /// <summary>
/// Stores width when <see cref="Anchor"/>.Left & <see cref="Anchor"/>.Right is <see cref="Anchor"/>.None.
/// <para> and height when <see cref="Anchor"/>.Top & <see cref="Anchor"/>.Bottom is <see cref="Anchor"/>.None.</para>
/// </summary>
System.Drawing.Size Size { get; set; } /// <summary>
///
/// </summary>
System.Drawing.Size ParentLastSize { get; set; } /// <summary>
///
/// </summary>
int zNear { get; set; } /// <summary>
///
/// </summary>
int zFar { get; set; } }
实现在OpenGL窗口中的UI布局
有了数据结构,就可以实现窗口中的UI布局了。当窗口大小改变时,调用下面的函数。
/// <summary>
/// layout controls in OpenGL canvas.
/// <para>This coordinate system is as below.</para>
/// <para> /\ y</para>
/// <para> |</para>
/// <para> |</para>
/// <para> |</para>
/// <para> |</para>
/// <para> |</para>
/// <para> |----------------->x</para>
/// <para>(0, 0)</para>
/// </summary>
/// <param name="uiRenderer"></param>
internal static void Layout(this ILayout uiRenderer)
{
ILayout parent = uiRenderer.Parent;
if (parent != null)
{
uiRenderer.Self.DoBeforeLayout();
NonRootNodeLayout(uiRenderer, parent);
uiRenderer.Self.DoAfterLayout();
} foreach (var item in uiRenderer.Children)
{
item.Layout();
} if (parent != null)
{
uiRenderer.ParentLastSize = parent.Size;
}
} /// <summary>
/// leftRightAnchor = (AnchorStyles.Left | AnchorStyles.Right);
/// </summary>
private const AnchorStyles leftRightAnchor = (AnchorStyles.Left | AnchorStyles.Right); /// <summary>
/// topBottomAnchor = (AnchorStyles.Top | AnchorStyles.Bottom);
/// </summary>
private const AnchorStyles topBottomAnchor = (AnchorStyles.Top | AnchorStyles.Bottom); /// <summary>
/// Gets <paramref name="currentNode"/>'s location and size according to its state and parent's information.
/// </summary>
/// <param name="currentNode"></param>
/// <param name="parent"></param>
private static void NonRootNodeLayout(ILayout currentNode, ILayout parent)
{
int x, y, width, height;
if ((currentNode.Anchor & leftRightAnchor) == leftRightAnchor)
{
width = parent.Size.Width - currentNode.Margin.Left - currentNode.Margin.Right;
//width = currentNode.Size.Width + (parent.Size.Width - currentNode.ParentLastSize.Width);
if (width < ) { width = ; }
}
else
{
width = currentNode.Size.Width;
} if ((currentNode.Anchor & topBottomAnchor) == topBottomAnchor)
{
height = parent.Size.Height - currentNode.Margin.Top - currentNode.Margin.Bottom;
//height = currentNode.Size.Height + (parent.Size.Height - currentNode.ParentLastSize.Height);
if (height < ) { height = ; }
}
else
{
height = currentNode.Size.Height;
} if ((currentNode.Anchor & leftRightAnchor) == AnchorStyles.None)
{
x = (int)(
(parent.Size.Width - width)
* ((double)currentNode.Margin.Left / (double)(currentNode.Margin.Left + currentNode.Margin.Right)));
}
else if ((currentNode.Anchor & leftRightAnchor) == AnchorStyles.Left)
{
x = parent.Location.X + currentNode.Margin.Left;
}
else if ((currentNode.Anchor & leftRightAnchor) == AnchorStyles.Right)
{
x = parent.Location.X + parent.Size.Width - currentNode.Margin.Right - width;
}
else if ((currentNode.Anchor & leftRightAnchor) == leftRightAnchor)
{
x = parent.Location.X + currentNode.Margin.Left;
}
else
{ throw new Exception("uiRenderer should not happen!"); } if ((currentNode.Anchor & topBottomAnchor) == AnchorStyles.None)
{
y = (int)(
(parent.Size.Height - height)
* ((double)currentNode.Margin.Bottom / (double)(currentNode.Margin.Bottom + currentNode.Margin.Top)));
}
else if ((currentNode.Anchor & topBottomAnchor) == AnchorStyles.Bottom)
{
//y = currentNode.Margin.Bottom;
y = parent.Location.Y + currentNode.Margin.Bottom;
}
else if ((currentNode.Anchor & topBottomAnchor) == AnchorStyles.Top)
{
//y = parent.Size.Height - height - currentNode.Margin.Top;
y = parent.Location.Y + parent.Size.Height - currentNode.Margin.Top - height;
}
else if ((currentNode.Anchor & topBottomAnchor) == topBottomAnchor)
{
//y = currentNode.Margin.Top + parent.Location.Y;
y = parent.Location.Y + currentNode.Margin.Bottom;
}
else
{ throw new Exception("This should not happen!"); } currentNode.Location = new System.Drawing.Point(x, y);
currentNode.Size = new Size(width, height);
}
public static void Layout(this ILayout uiRenderer)
glViewport/glScissor
这是避免复杂的矩阵操作,实现稳定的UI布局显示的关键。glViewport指定了GLRenderer在窗口的渲染位置,glScissor将GLRenderer范围之外的部分保护起来。
在渲染之前,根据UIRenderer的位置和大小更新viewport和scissor即可。不再需要为UI固定在窗口某处而煞费苦心地设计projection,view,model矩阵了。
/// <summary>
/// Renderer that supports UI layout.
/// 支持2D UI布局的渲染器
/// </summary>
public class UIRenderer : RendererBase, ILayout
{
private ViewportSwitch viewportSwitch;
private ScissorTestSwitch scissorTestSwitch;
private GLSwitchList switchList = new GLSwitchList(); /// <summary>
///
/// </summary>
public GLSwitchList SwitchList
{
get { return switchList; }
} /// <summary>
/// triggered before layout in <see cref="ILayout"/>.Layout().
/// </summary>
public event EventHandler BeforeLayout;
/// <summary>
/// triggered after layout in <see cref="ILayout"/>.Layout().
/// </summary>
public event EventHandler AfterLayout; internal void DoBeforeLayout()
{
EventHandler BeforeLayout = this.BeforeLayout;
if (BeforeLayout != null)
{
BeforeLayout(this, null);
}
} internal void DoAfterLayout()
{
EventHandler AfterLayout = this.AfterLayout;
if (AfterLayout != null)
{
AfterLayout(this, null);
}
} /// <summary>
///
/// </summary>
public RendererBase Renderer { get; protected set; }
/// <summary>
///
/// </summary>
/// <param name="anchor"></param>
/// <param name="margin"></param>
/// <param name="size"></param>
/// <param name="zNear"></param>
/// <param name="zFar"></param>
public UIRenderer(
System.Windows.Forms.AnchorStyles anchor, System.Windows.Forms.Padding margin,
System.Drawing.Size size, int zNear, int zFar)
{
this.Children = new ChildList<UIRenderer>(this);// new ILayoutList(this); this.Anchor = anchor; this.Margin = margin;
this.Size = size; this.zNear = zNear; this.zFar = zFar;
} /// <summary>
///
/// </summary>
public System.Windows.Forms.AnchorStyles Anchor { get; set; } /// <summary>
///
/// </summary>
public System.Windows.Forms.Padding Margin { get; set; } /// <summary>
///
/// </summary>
public System.Drawing.Point Location { get; set; } /// <summary>
///
/// </summary>
public System.Drawing.Size Size { get; set; }
/// <summary>
///
/// </summary>
public System.Drawing.Size ParentLastSize { get; set; } /// <summary>
///
/// </summary>
public int zNear { get; set; } /// <summary>
///
/// </summary>
public int zFar { get; set; } /// <summary>
///
/// </summary>
protected override void DoInitialize()
{
this.viewportSwitch = new ViewportSwitch();
this.scissorTestSwitch = new ScissorTestSwitch(); RendererBase renderer = this.Renderer;
if (renderer != null)
{
renderer.Initialize();
}
} /// <summary>
///
/// </summary>
/// <param name="arg"></param>
protected override void DoRender(RenderEventArg arg)
{
this.viewportSwitch.X = this.Location.X;
this.viewportSwitch.Y = this.Location.Y;
this.viewportSwitch.Width = this.Size.Width;
this.viewportSwitch.Height = this.Size.Height;
this.scissorTestSwitch.X = this.Location.X;
this.scissorTestSwitch.Y = this.Location.Y;
this.scissorTestSwitch.Width = this.Size.Width;
this.scissorTestSwitch.Height = this.Size.Height; this.viewportSwitch.On();
this.scissorTestSwitch.On();
int count = this.switchList.Count;
for (int i = ; i < count; i++) { this.switchList[i].On(); } // 把所有在此之前渲染的内容都推到最远。
// Push all rendered stuff to farest position.
OpenGL.Clear(OpenGL.GL_DEPTH_BUFFER_BIT); RendererBase renderer = this.Renderer;
if (renderer != null)
{
renderer.Render(arg);
} for (int i = count - ; i >= ; i--) { this.switchList[i].Off(); }
this.scissorTestSwitch.Off();
this.viewportSwitch.Off();
} /// <summary>
///
/// </summary>
protected override void DisposeUnmanagedResources()
{
RendererBase renderer = this.Renderer;
if (renderer != null)
{
renderer.Dispose();
}
} /// <summary>
///
/// </summary>
public UIRenderer Self { get { return this; } } /// <summary>
///
/// </summary>
public UIRenderer Parent { get; set; } //ChildList<UIRenderer> children; /// <summary>
///
/// </summary>
[Editor(typeof(IListEditor<UIRenderer>), typeof(UITypeEditor))]
public ChildList<UIRenderer> Children { get; private set; }
}
UIRenderer
叠加/覆盖
注意在UIRenderer.DoRender(RenderEventArgs arg)中,使用
// 把所有在此之前渲染的内容都推到最远。
// Push all rendered stuff to farest position.
OpenGL.Clear(OpenGL.GL_DEPTH_BUFFER_BIT);
把所有在此之前渲染的内容都推到最远。
从ILayout的定义中可以看到,控件与控件组成了一个树结构。其根结点是覆盖整个窗口的控件,在渲染UI时处于第一个渲染的位置,然后渲染它的各个子结点代表的控件。这就实现了子控件能够完全覆盖在父控件之上。
我突然想到了WPF。
渲染文字
从TTF文件获取字形
(https://github.com/MikePopoloski/SharpFont)是一个纯C#的解析TTF文件的库,能够代替C++的FreeType。我将其稍作修改,实现了从TTF文件获取任意uncode字形,进而获取字形纹理,实现渲染文字的功能。
例如下面这几个字形纹理。
使用FontResource
FontResource类型封装了使用字形贴图的功能。
使用方式也非常简单。首先创建一个字体资源对象。
FontResource fontResouce = FontResource.Load(ttfFilename, ' ', (char));
然后交给GLText。
var glText = new GLText(AnchorStyles.Left | AnchorStyles.Top,
new Padding(, , , ), new Size(, ), -, , fontResouce);
glText.Initialize();
glText.SetText("The quick brown fox jumps over the lazy dog!");
GLText在初始化时指定此字体对象包含的二维纹理。
protected override void DoInitialize()
{
base.DoInitialize(); this.Renderer.SetUniform("fontTexture", this.fontResource.GetSamplerValue());
}
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()方法里指定对象的缩放比例为:
mat4 model = glm.scale(mat4.identity(), new vec3(this.Size.Width, this.Size.Height, ));
这样的缩放比例就可以恰好使得SomeRenderer的模型填满UI的矩形范围。
总结
CSharpGL支持控件布局,支持渲染文字了。
欢迎对OpenGL有兴趣的同学关注(https://github.com/bitzhuwei/CSharpGL)