CSharpGL(2)設計和使用場景元素及常用接口
2016-08-13
由于CSharpGL一直在更新,現在這個教程已經不適用最新的代碼了。CSharpGL源碼中包含10多個獨立的Demo,更適合入門參考。
為了盡可能提升渲染效率,CSharpGL是面向Shader的,因此稍有難度。
主要內容
描述在OpenGL中繪制場景的思路。
設計場景元素的抽象基類SceneELementBase。
以PyramidElement為例演示SceneELementBase的用法。
下載
您可以在(https://github.com/bitzhuwei/CSharpGL)找到最新的源碼。歡迎感興趣的同學fork之。
在OpenGL中繪制場景的思路
規范用詞
首先我們明確2個關鍵的用詞。
場景
場景是指用OpenGL渲染出來的所有東西,包括每個模型和背景。
元素
元素是指場景中的一個模型。
所有元素的基類
在上一篇里我們已經體驗了用legacy OpenGL和modern OpenGL渲染3D場景的過程。這一過程對任意簡單的或復雜的場景都適用。我們將任意場景的渲染過程抽象出共性來,就是"初始化"和"渲染"這兩點。也就是說,一個在場景中進行渲染的元素,必須具有"初始化"和"渲染"這兩個功能。這就找到了場景元素的基類。
1 /// <summary>
2 /// 用OPENGL初始化和渲染一個元素。
3 /// </summary>
4 public abstract class SceneElementBase
5 {
6 protected bool initialized = false;
7
8 /// <summary>
9 /// 初始化此Element
10 /// </summary>
11 public void Initialize()
12 {
13 if (!initialized)
14 {
15 DoInitialize();
16
17 initialized = true;
18 }
19 }
20
21 /// <summary>
22 /// 初始化此Element
23 /// </summary>
24 protected abstract void DoInitialize();
25
26 /// <summary>
27 /// 渲染
28 /// </summary>
29 /// <param name="renderMode"></param>
30 public abstract void Render(RenderEventArgs e);
31 }
這個抽象基類告訴我們,任何一個場景中的元素,必須實現"初始化"和"渲染"這兩個方法,即必須知道如何初始化自己的數據,如何渲染自己。
常用接口
有了上面的SceneElementBase,整個渲染的藍圖就定型了。下一個問題是,modern OpenGL需要加載很多東西,不同的元素要實現的功能的多少、種類也各不相同,這如何實現?方法是:為每項功能設計相應的接口,讓具有此功能的元素繼承此接口。下面是幾個常用的功能接口的例子。
IMVP
定義
這是最常用的接口。對于用modern OpenGL方式渲染的元素,這是一個必選項。(當然,不實現此接口也可以,但是本質上仍然是實現了此接口的功能)
1 /// <summary>
2 /// 通過此接口設置元素的MVP矩陣
3 /// </summary>
4 public interface IMVP
5 {
6 /// <summary>
7 /// 更新此元素的MVP值。
8 /// </summary>
9 /// <param name="mvp">三個矩陣的乘積(Projection * View * Model)</param>
10 void SetShaderProgram(mat4 mvp);
11
12 /// <summary>
13 /// 解綁當前shader program。
14 /// </summary>
15 void ResetShaderProgram();
16
17 /// <summary>
18 ///
19 /// </summary>
20 /// <returns></returns>
21 Shaders.ShaderProgram GetShaderProgram();
22
23 }
MVP是投影矩陣(Projection) * 視圖矩陣(View) * 模型矩陣(Model)的簡寫。由于在很多GLSL的shader里都有"uniform mat4 mvp;"這樣的命名方式,所以我將此接口命名為IMVP。
IMVP的用處,是在渲染前設置mvp矩陣。任何一個元素都應該有位置(Position)這個屬性(否則就沒有可畫的東西了),而輸入的位置VBO里存儲的是模型自身的位置,要想變換到窗口合適的位置上,就必然用到mvp矩陣。所以說這個IMVP接口是任何一個用modern OpenGL方式渲染的元素必須實現的。
如何使用
要調用此接口,就必須與SceneElementBase.Render()配合,在SceneElementBase.Render()渲染之前調用IMVP.SetShaderProgram()。
1 public override void Render(RenderEventArgs e)
2 {
3 // 綁定shader,設置MVP
4 mat4 projectionMatrix = e.Camera.GetProjectionMat4();
5 mat4 viewMatrix = e.Camera.GetViewMat4();
6 mat4 modelMatrix = mat4.identity();
7 mat4 mvp = projectionMatrix * viewMatrix * modelMatrix;
8 IMVP element = this as IMVP;
9 element.SetShaderProgram(mvp);
10
11 // 此時進行渲染
12 // ...
13
14 // 解綁shader
15 element.ResetShaderProgram();
16 }
你注意到,此時RenderEventArgs參數里需要有一個Camera字段,Camera需要實現獲取投影矩陣和視圖矩陣的方法GetProjectionMat4()和GetViewMat4()。關于Camera的實現我們以后再詳述。
改進
再思考一下這個Render()方法,它有2個問題:
A:設置mvp矩陣的代碼寫死到元素的Render方法里,靈活性不夠。如果以后我希望用別的方式指定mvp值,就必須修改Camera。在此處對Camera的改動就牽涉過多了。
B:如果場景中的元素很多,那么每個元素內部的Render方法都要計算一遍mvp值,這顯然是重復計算。更好的做法是:提前計算出mvp值,然后依次喂給每個元素的SetShaderProgram(mvp);方法。
為解決這2個問題,我們對SceneElementBase進行改造,使得元素外部代碼可以動態改變指定mvp的方式。
1 /// <summary>
2 /// 用OPENGL初始化和渲染一個元素。
3 /// </summary>
4 public abstract class SceneElementBase : IRenderable
5 {
6 protected bool initialized = false;
7
8 /// <summary>
9 /// 初始化此Element
10 /// </summary>
11 public void Initialize()
12 {
13 if (!initialized)
14 {
15 DoInitialize();
16
17 initialized = true;
18 }
19 }
20
21 /// <summary>
22 /// 初始化此Element
23 /// </summary>
24 protected abstract void DoInitialize();
25
26 /// <summary>
27 /// 渲染
28 /// </summary>
29 /// <param name="renderMode"></param>
30 public void Render(RenderEventArgs e)
31 {
32 if (!initialized) { Initialize(); }
33
34 EventHandler<RenderEventArgs> beforeRendering = this.BeforeRendering;
35 if (beforeRendering != null)
36 {
37 beforeRendering(this, e);
38 }
39
40 DoRender(e);
41
42 EventHandler<RenderEventArgs> afterRendering = this.AfterRendering;
43 if (afterRendering != null)
44 {
45 afterRendering(this, e);
46 }
47 }
48
49 /// <summary>
50 /// 執行渲染操作
51 /// </summary>
52 /// <param name="renderMode"></param>
53 protected abstract void DoRender(RenderEventArgs e);
54
55 /// <summary>
56 /// 在渲染前進行某些準備(更新camera矩陣信息等)
57 /// </summary>
58 public event EventHandler<RenderEventArgs> BeforeRendering;
59
60 /// <summary>
61 /// 在渲染后進行某些善后(恢復OpenGL狀態等)
62 /// </summary>
63 public event EventHandler<RenderEventArgs> AfterRendering;
64
65 }
改進后的使用
現在,我們可以在元素外部通過為BeforeRendering和AfterRendering添加自定義事件函數的方式自由指定mvp。
1 PyramidElement[] elements = new PyramidElement[10];
2 mat4 mvp; //每次渲染場景前被更新
3
4 public InitElements()
5 {
6 for (int i = 0; i < 10; i++)
7 {
8 var element = new PyramidElement();
9 element.Initialize();
10 element.BeforeRendering += element_BeforeRendering;
11 element.AfterRendering += element_AfterRendering;
12
13 this.elements[i] = element;
14 }
15 }
16
17 void element_AfterRendering(object sender, Objects.RenderEventArgs e)
18 {
19 IMVP element = sender as IMVP;
20 element.ResetShaderProgram();
21 }
22
23 void element_BeforeRendering(object sender, Objects.RenderEventArgs e)
24 {
25 IMVP element = sender as IMVP;
26 element.SetShaderProgram(mvp);
27 }
28 void glCanvas1_OpenGLDraw(object sender, PaintEventArgs e)
29 {
30 mat4 modelMatrix = glm.identity();
31 mat4 viewMatrix = this.camera.GetViewMat4();
32 mat4 projectionMatrix = this.camera.GetProjectionMat4();
33 mvp = projectionMatrix * viewMatrix * modelMatrix;
34
35 GL.Clear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
36
37 var arg = new RenderEventArgs(RenderModes.Render, this.camera);
38 for (int i = 0; i < 10; i++)
39 {
40 this.elements[i].Render(arg);
41 }
42 }
這樣一來,上面2個問題都解決了。
用Helper實現最常用的IMVP
說了這么多,還沒有說明如何實現IMVP。
1 class PyramidElement : SceneElementBase, IMVP
2 {
3 // other stuff
4
5 ShaderProgram shaderProgram;
6
7 void IMVP.SetShaderProgram(mat4 mvp)
8 {
9 IMVPHelper.DoUpdateMVP(this, mvp);
10 }
11
12 void IMVP.ResetShaderProgram()
13 {
14 IMVPHelper.DoUnbindShaderProgram(this);
15 }
16
17 ShaderProgram IMVP.GetShaderProgram()
18 {
19 return this.shaderProgram;
20 }
21 }
22
23 public static class IMVPHelper
24 {
25 /// <summary>
26 /// public static string strMVP = "MVP";
27 /// <para>使用此<see cref="IMVPHelper"/>的<see cref="SceneElement"/>所使用的Vertex Shader必須含有<code>uniform mat4 MVP;</code>并使其作為變換矩陣。</para>
28 /// </summary>
29 public static string strMVP = "MVP";
30
31 /// <summary>
32 /// 請確保此元素的GLSL中含有uniform mat4 MVP;并作為位置轉換矩陣。
33 /// </summary>
34 /// <param name="element"></param>
35 /// <param name="mvp"></param>
36 public static void DoUpdateMVP(this IMVP element, mat4 mvp)
37 {
38 ShaderProgram shaderProgram = element.GetShaderProgram();
39 shaderProgram.Bind();
40 shaderProgram.SetUniformMatrix4(strMVP, mvp.to_array());
41 }
42
43 /// <summary>
44 /// 請確保此元素的GLSL中含有uniform mat4 MVP;并作為位置轉換矩陣。
45 /// </summary>
46 /// <param name="element"></param>
47 public static void DoUnbindShaderProgram(this IMVP element)
48 {
49 ShaderProgram shaderProgram = element.GetShaderProgram();
50 shaderProgram.Unbind();
51 }
52 }
IColorCodedPicking
這是為實現在VBO中拾取一個圖元而設計的接口。繼承此接口的SceneElementBase的子類能夠告訴你你用鼠標拾取了哪個圖元。
具體使用方法請參考(http://www.cnblogs.com/bitzhuwei/p/modern-opengl-picking-primitive-in-VBO.html)。不再重述。
IUILayout
定義
實現IUILayout接口的元素能夠在窗口固定位置顯示,類似Winform里的控件那樣,設置其長度、寬度,指定其Anchor(綁定到上下左右)。
1 /// <summary>
2 /// 實現在OpenGL窗口中的UI布局
3 /// </summary>
4 public interface IUILayout
5 {
6 IUILayoutParam Param { get; set; }
7
8 }
9 public struct IUILayoutParam
10 {
11 /// <summary>
12 /// the edges of the <see cref="GLCanvas"/> to which a UI’s rect is bound and determines how it is resized with its parent.
13 /// <para>something like AnchorStyles.Left | AnchorStyles.Bottom.</para>
14 /// </summary>
15 public System.Windows.Forms.AnchorStyles Anchor;
16
17 /// <summary>
18 /// Gets or sets the space between viewport and SimpleRect.
19 /// </summary>
20 public System.Windows.Forms.Padding Margin;
21
22 /// <summary>
23 /// Stores width when <see cref="OpenGLUIRect.Anchor"/>.Left & <see cref="OpenGLUIRect.Anchor"/>.Right is <see cref="OpenGLUIRect.Anchor"/>.None.
24 /// <para> and height when <see cref="OpenGLUIRect.Anchor"/>.Top & <see cref="OpenGLUIRect.Anchor"/>.Bottom is <see cref="OpenGLUIRect.Anchor"/>.None.</para>
25 /// </summary>
26 public System.Drawing.Size Size;
27
28 public int zNear;
29
30 public int zFar;
31
32 public IUILayoutParam(AnchorStyles anchorStyle, Padding padding, System.Drawing.Size size,
33 int zNear = -1000, int zFar = 1000)
34 {
35 this.Anchor = anchorStyle;
36 this.Margin = padding;
37 this.Size = size;
38 this.zNear = zNear;
39 this.zFar = zFar;
40 }
41 }
當然, 僅僅一個接口是不能"實現"這個功能的。還需要一些輔助類型。
如何實現UI布局
最核心的是下面這個能夠讓元素像UI一樣布局的Helper類型。
這個Helper類型會根據IUILayout接口提供的此UI元素的布局參數,計算出它應該使用的透視矩陣、投影矩陣和模型矩陣。所以,本質上,UI元素也是場景中的一種元素,只不過由于其mvp值比較特殊,使其看起來像Winform里的控件而已。

1 public static class IUILayoutHelper
2 {
3 /// <summary>
4 /// 獲取此UI元素的投影矩陣、視圖矩陣和模型矩陣
5 /// </summary>
6 /// <param name="uiElement"></param>
7 /// <param name="projectionMatrix"></param>
8 /// <param name="viewMatrix"></param>
9 /// <param name="modelMatrix"></param>
10 /// <param name="camera">如果為null,會以glm.lookAt(new vec3(0, 0, 1), new vec3(0, 0, 0), new vec3(0, 1, 0))計算默認值。</param>
11 /// <param name="maxDepth">UI元素的外接球半徑的倍數。</param>
12 public static void GetMatrix(this IUILayout uiElement,
13 out mat4 projectionMatrix, out mat4 viewMatrix, out mat4 modelMatrix,
14 IViewCamera camera = null, float maxDepth = 2.0f)
15 {
16 IUILayoutArgs args = uiElement.GetArgs();
17 float max = (float)Math.Max(args.UIWidth, args.UIHeight);
18
19 {
20 projectionMatrix = glm.ortho((float)args.left / 2, (float)args.right / 2, (float)args.bottom / 2, (float)args.top / 2,
21 uiElement.Param.zNear, uiElement.Param.zFar);
22 projectionMatrix = glm.translate(projectionMatrix, new vec3(0, 0, uiElement.Param.zFar - max / 2 * maxDepth));
23 }
24 {
25 // UI元素不在三維場景中,所以其Camera可以是null。
26 if (camera == null)
27 {
28 //viewMatrix = glm.lookAt(new vec3(0, 0, 1), new vec3(0, 0, 0), new vec3(0, 1, 0));
29 viewMatrix = glm.lookAt(
30 ScientificCamera.defaultPosition,
31 ScientificCamera.defaultTarget,
32 ScientificCamera.defaultUpVector);
33 }
34 else
35 {
36 vec3 position = camera.Position - camera.Target;
37 position.Normalize();
38 viewMatrix = glm.lookAt(position, new vec3(0, 0, 0), camera.UpVector);
39 }
40 }
41 {
42 modelMatrix = glm.scale(mat4.identity(), new vec3(args.UIWidth / 2, args.UIHeight / 2, max / 2));
43 }
44 }
45
46 const AnchorStyles leftRightAnchor = (AnchorStyles.Left | AnchorStyles.Right);
47 const AnchorStyles topBottomAnchor = (AnchorStyles.Top | AnchorStyles.Bottom);
48
49 /// <summary>
50 /// 獲取為UI元素布局所需的參數對象。
51 /// </summary>
52 /// <param name="uiElement"></param>
53 /// <returns></returns>
54 public static IUILayoutArgs GetArgs(this IUILayout uiElement)
55 {
56 var args = new IUILayoutArgs();
57
58 CalculateViewport(args);
59
60 CalculateCoords(uiElement, args.viewportWidth, args.viewportHeight, args);
61
62 return args;
63 }
64
65 /// <summary>
66 /// 計算opengl畫布的大小。
67 /// </summary>
68 /// <param name="args"></param>
69 static void CalculateViewport(IUILayoutArgs args)
70 {
71 int[] viewport = new int[4];
72 GL.GetInteger(GetTarget.Viewport, viewport);
73 args.viewportWidth = viewport[2];
74 args.viewportHeight = viewport[3];
75 }
76
77 /// <summary>
78 /// 根據UI元素的布局設定,計算其應有的寬高及其在ortho()中應有的參數。
79 /// </summary>
80 /// <param name="uiElement"></param>
81 /// <param name="viewportWidth"></param>
82 /// <param name="viewportHeight"></param>
83 /// <param name="args"></param>
84 static void CalculateCoords(IUILayout uiElement, int viewportWidth, int viewportHeight, IUILayoutArgs args)
85 {
86 IUILayoutParam param = uiElement.Param;
87
88 if ((param.Anchor & leftRightAnchor) == leftRightAnchor)
89 {
90 args.UIWidth = viewportWidth - param.Margin.Left - param.Margin.Right;
91 if (args.UIWidth < 0) { args.UIWidth = 0; }
92 }
93 else
94 {
95 args.UIWidth = param.Size.Width;
96 }
97
98 if ((param.Anchor & topBottomAnchor) == topBottomAnchor)
99 {
100 args.UIHeight = viewportHeight - param.Margin.Top - param.Margin.Bottom;
101 if (args.UIHeight < 0) { args.UIHeight = 0; }
102 }
103 else
104 {
105 args.UIHeight = param.Size.Height;
106 }
107
108 if ((param.Anchor & leftRightAnchor) == AnchorStyles.None)
109 {
110 args.left = -(args.UIWidth / 2
111 + (viewportWidth - args.UIWidth)
112 * ((double)param.Margin.Left / (double)(param.Margin.Left + param.Margin.Right)));
113 }
114 else if ((param.Anchor & leftRightAnchor) == AnchorStyles.Left)
115 {
116 args.left = -(args.UIWidth / 2 + param.Margin.Left);
117 }
118 else if ((param.Anchor & leftRightAnchor) == AnchorStyles.Right)
119 {
120 args.left = -(viewportWidth - args.UIWidth / 2 - param.Margin.Right);
121 }
122 else // if ((Anchor & leftRightAnchor) == leftRightAnchor)
123 {
124 args.left = -(args.UIWidth / 2 + param.Margin.Left);
125 }
126
127 if ((param.Anchor & topBottomAnchor) == AnchorStyles.None)
128 {
129 args.bottom = -viewportHeight / 2;
130 args.bottom = -(args.UIHeight / 2
131 + (viewportHeight - args.UIHeight)
132 * ((double)param.Margin.Bottom / (double)(param.Margin.Bottom + param.Margin.Top)));
133 }
134 else if ((param.Anchor & topBottomAnchor) == AnchorStyles.Bottom)
135 {
136 args.bottom = -(args.UIHeight / 2 + param.Margin.Bottom);
137 }
138 else if ((param.Anchor & topBottomAnchor) == AnchorStyles.Top)
139 {
140 args.bottom = -(viewportHeight - args.UIHeight / 2 - param.Margin.Top);
141 }
142 else // if ((Anchor & topBottomAnchor) == topBottomAnchor)
143 {
144 args.bottom = -(args.UIHeight / 2 + param.Margin.Bottom);
145 }
146 }
147 }
如何使用
我們以畫一個簡單的邊框為例說明如何使用IUILayout。這個邊框畫出了IUILayout元素本身的范圍,在調試期間也是很有用的。

1 /// <summary>
2 /// Draw a rectangle on OpenGL control like a <see cref="Windows.Forms.Control"/> drawn on a <see cref="windows.Forms.Form"/>.
3 /// Set its properties(Anchor, Margin, Size, etc) to adjust its behaviour.
4 /// </summary>
5 public class SimpleUIRect : SceneElementBase, IUILayout, IMVP//, IRenderable, IHasObjectSpace
6 {
7 /// <summary>
8 /// shader program
9 /// </summary>
10 public ShaderProgram shaderProgram;
11 const string strin_Position = "in_Position";
12 const string strin_Color = "in_Color";
13
14 /// <summary>
15 /// VAO
16 /// </summary>
17 protected uint[] vao;
18
19 /// <summary>
20 /// 圖元類型
21 /// </summary>
22 protected DrawMode axisPrimitiveMode;
23
24 /// <summary>
25 /// 頂點數
26 /// </summary>
27 protected int axisVertexCount;
28
29 vec3 rectColor;
30
31 /// <summary>
32 ///
33 /// </summary>
34 /// <param name="anchor">the edges of the viewport to which a SimpleUIRect is bound and determines how it is resized with its parent.
35 /// <para>something like AnchorStyles.Left | AnchorStyles.Bottom.</para></param>
36 /// <param name="margin">the space between viewport and SimpleRect.</param>
37 /// <param name="size">Stores width when <see cref="OpenGLUIRect.Anchor"/>.Left & <see cref="OpenGLUIRect.Anchor"/>.Right is <see cref="OpenGLUIRect.Anchor"/>.None.
38 /// <para> and height when <see cref="OpenGLUIRect.Anchor"/>.Top & <see cref="OpenGLUIRect.Anchor"/>.Bottom is <see cref="OpenGLUIRect.Anchor"/>.None.</para></param>
39 /// <param name="zNear"></param>
40 /// <param name="zFar"></param>
41 /// <param name="rectColor">default color is red.</param>
42 public SimpleUIRect(IUILayoutParam param, GLColor rectColor = null)
43 {
44 IUILayout layout = this;
45 layout.Param = param;
46
47 if (rectColor == null)
48 { this.rectColor = new vec3(0, 0, 1); }
49 else
50 { this.rectColor = new vec3(rectColor.R, rectColor.G, rectColor.B); }
51 }
52
53 protected override void DoInitialize()
54 {
55 this.shaderProgram = InitializeShader();
56
57 InitVAO();
58
59 base.BeforeRendering += this.GetSimpleUI_BeforeRendering();
60 base.AfterRendering += this.GetSimpleUI_AfterRendering();
61 }
62
63 private void InitVAO()
64 {
65 this.axisPrimitiveMode = DrawMode.LineLoop;
66 this.axisVertexCount = 4;
67 this.vao = new uint[1];
68
69 GL.GenVertexArrays(1, vao);
70
71 GL.BindVertexArray(vao[0]);
72
73 // Create a vertex buffer for the vertex data.
74 {
75 UnmanagedArray<vec3> positionArray = new UnmanagedArray<vec3>(4);
76 positionArray[0] = new vec3(-0.5f, -0.5f, 0);
77 positionArray[1] = new vec3(0.5f, -0.5f, 0);
78 positionArray[2] = new vec3(0.5f, 0.5f, 0);
79 positionArray[3] = new vec3(-0.5f, 0.5f, 0);
80
81 uint positionLocation = shaderProgram.GetAttributeLocation(strin_Position);
82
83 uint[] ids = new uint[1];
84 GL.GenBuffers(1, ids);
85 GL.BindBuffer(BufferTarget.ArrayBuffer, ids[0]);
86 GL.BufferData(BufferTarget.ArrayBuffer, positionArray, BufferUsage.StaticDraw);
87 GL.VertexAttribPointer(positionLocation, 3, GL.GL_FLOAT, false, 0, IntPtr.Zero);
88 GL.EnableVertexAttribArray(positionLocation);
89
90 positionArray.Dispose();
91 }
92
93 // Now do the same for the colour data.
94 {
95 UnmanagedArray<vec3> colorArray = new UnmanagedArray<vec3>(4);
96 vec3 color = this.rectColor;
97 for (int i = 0; i < colorArray.Length; i++)
98 {
99 colorArray[i] = color;
100 }
101
102 uint colorLocation = shaderProgram.GetAttributeLocation(strin_Color);
103
104 uint[] ids = new uint[1];
105 GL.GenBuffers(1, ids);
106 GL.BindBuffer(BufferTarget.ArrayBuffer, ids[0]);
107 GL.BufferData(BufferTarget.ArrayBuffer, colorArray, BufferUsage.StaticDraw);
108 GL.VertexAttribPointer(colorLocation, 3, GL.GL_FLOAT, false, 0, IntPtr.Zero);
109 GL.EnableVertexAttribArray(colorLocation);
110
111 colorArray.Dispose();
112 }
113
114 // Unbind the vertex array, we've finished specifying data for it.
115 GL.BindVertexArray(0);
116 }
117
118 protected ShaderProgram InitializeShader()
119 {
120 var vertexShaderSource = ManifestResourceLoader.LoadTextFile(@"UIs.SimpleUIRect.vert");
121 var fragmentShaderSource = ManifestResourceLoader.LoadTextFile(@"UIs.SimpleUIRect.frag");
122
123 shaderProgram = new ShaderProgram();
124 shaderProgram.Create(vertexShaderSource, fragmentShaderSource, null);
125
126 shaderProgram.AssertValid();
127
128 return shaderProgram;
129 }
130
131 protected override void DoRender(RenderEventArgs e)
132 {
133 GL.BindVertexArray(vao[0]);
134
135 GL.DrawArrays(this.axisPrimitiveMode, 0, this.axisVertexCount);
136
137 GL.BindVertexArray(0);
138 }
139
140 public IUILayoutParam Param { get; set; }
141
142
143 void IMVP.SetShaderProgram(mat4 mvp)
144 {
145 IMVPHelper.DoUpdateMVP(this, mvp);
146 }
147
148
149 void IMVP.ResetShaderProgram()
150 {
151 IMVPHelper.DoUnbindShaderProgram(this);
152 }
153
154 ShaderProgram IMVP.GetShaderProgram()
155 {
156 return this.shaderProgram;
157 }
158 }
這段代碼關注如下幾點:
A:實現IUILayout只需一句" public IUILayoutParam Param { get; set; }"。
B:實現IUILayout的元素也必須實現IMVP。實際上任何用modern OpenGL方式進行渲染的元素都應該實現IMVP。
C:其他方面與普通元素無異。
D:此元素借助了2個擴展方法:

1 public static class IUILayoutRenderingHelper
2 {
3 private static readonly object synObj = new object();
4 private static EventHandler<RenderEventArgs> simpleUIAxis_BeforeRendering = null;
5 private static EventHandler<RenderEventArgs> simpleUIAxis_AfterRendering = null;
6
7 /// <summary>
8 /// 對Xxx : SceneElementBase, IUILayout, IMVP有效的After事件。
9 /// <para>此處用泛型方法是為了讓編譯器檢測where約束條件,這樣就沒有“坑”了。</para>
10 /// </summary>
11 /// <typeparam name="T"></typeparam>
12 /// <param name="element"></param>
13 /// <returns></returns>
14 public static EventHandler<RenderEventArgs> GetSimpleUI_AfterRendering<T>(this T element)
15 where T : SceneElementBase, IUILayout, IMVP
16 {
17 if (simpleUIAxis_AfterRendering == null)
18 {
19 lock (synObj)
20 {
21 if (simpleUIAxis_AfterRendering == null)
22 {
23 simpleUIAxis_AfterRendering = new EventHandler<RenderEventArgs>(SimpleUI_AfterRendering);
24 }
25 }
26 }
27
28 return simpleUIAxis_AfterRendering;
29 }
30
31 /// <summary>
32 /// 對Xxx : SceneElementBase, IUILayout, IMVP有效的Before事件。
33 /// <para>此處用泛型方法是為了讓編譯器檢測where約束條件,這樣就沒有“坑”了。</para>
34 /// </summary>
35 /// <typeparam name="T"></typeparam>
36 /// <param name="element"></param>
37 /// <returns></returns>
38 public static EventHandler<RenderEventArgs> GetSimpleUI_BeforeRendering<T>(this T element)
39 where T : SceneElementBase, IUILayout, IMVP
40 {
41 if (simpleUIAxis_BeforeRendering == null)
42 {
43 lock (synObj)
44 {
45 if (simpleUIAxis_BeforeRendering == null)
46 {
47 simpleUIAxis_BeforeRendering = new EventHandler<RenderEventArgs>(SimpleUI_BeforeRendering);
48 }
49 }
50 }
51
52 return simpleUIAxis_BeforeRendering;
53 }
54
55 static void SimpleUI_AfterRendering(object sender, RenderEventArgs e)
56 {
57 IMVP element = sender as IMVP;
58 element.ResetShaderProgram();
59 }
60
61 static void SimpleUI_BeforeRendering(object sender, RenderEventArgs e)
62 {
63 mat4 projectionMatrix, viewMatrix, modelMatrix;
64 {
65 IUILayout element = sender as IUILayout;
66 element.GetMatrix(out projectionMatrix, out viewMatrix, out modelMatrix, e.Camera);
67 }
68
69 {
70 IMVP element = sender as IMVP;
71 element.SetShaderProgram(projectionMatrix * viewMatrix * modelMatrix);
72 }
73 }
74 }
借助擴展方法、類型約束等等機制,編寫OpenGL程序效率高了很多。
下面是效果圖。下圖中,在窗口的四個角落各安排了1個SimpUIRect。無論Camera如何改變,窗口大小如何改變,這四個藍色矩形框的大小、邊距都不會改變。
總結
本篇是寫起來最有難度的一篇。本篇所實現的類型、接口,都是在上一篇的基礎上設計的。上一篇里講的渲染過程,隱含著本篇的設計方案的前提條件。
本篇里的類型、接口都有各自的一套輔助類型構成一套實現某種功能的機制。但愿這不太復雜難用。我已經用Demo詳細演示了各個功能是如何實現的。
文章列表