CSharpGL(15)用GLSL渲染2种类型的文字
2016-08-13
由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了。CSharpGL源码中包含10多个独立的Demo,更适合入门参考。
为了尽可能提升渲染效率,CSharpGL是面向Shader的,因此稍有难度。
下载
这个示例是CSharpGL的一部分,CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
血条
玩家头顶的血条、名字随着玩家在3D世界移动,但始终朝向摄像机,且在屏幕上的大小不变。
始终朝向camera
如何使模型始终朝向camera?
对模型进行坐标变换,使模型的顶点坐标从物体坐标系变换到世界坐标系再到屏幕坐标系。这过程需要三个矩阵。
1 gl_Position = projectionMatrix * viewMatrix * modelMatrix * position;
其中经过viewMatrix后,模型就变换到了世界坐标空间里。那么,只需调整viewMatrix(去掉旋转变换),就不会改变模型的朝向了。
在4x4矩阵中,旋转变换由左上角的3x3矩阵实现。所以,只要把viewMatrix的3x3矩阵变为单位矩阵即可。
1 mat4 translateView = mat4(1.0f);//单位矩阵
2
3 for (int t = 0; t < 3; t++)
4 { translateView[t].w = viewMatrix[t].w; }
5
6 translateView[3] = viewMatrix[3];
7
8 gl_Position = projectionMatrix * translateView * (modelMatrix * position);
当然,也可以在C#里直接计算translateView。
1 private mat4 AlwaysFaceCamera(mat4 viewMatrix)
2 {
3 mat4 result = mat4.identity();
4 for (int i = 0; i < 3; i++)
5 {
6 vec4 v = result[i];
7 v.w = viewMatrix[i].w;
8 result[i] = v;
9 }
10 result[3] = viewMatrix[3];
11
12 return result;
13 }
在屏幕上的大小不变
上面解决了始终朝向camera的问题。但是此时的血条仍会在远离camera时和其他模型一样缩小。我希望血条大小保持不变。
虽然还不能完全说明原理,不过还是做出来了。只需将血条的modelMatrix设置为下面值即可。
1 /// <summary>
2 ///
3 /// </summary>
4 /// <param name="length">camera的Position和Target之间的距离</param>
5 /// <param name="height">血条高度</param>
6 /// <returns></returns>
7 private mat4 AlwaysSameSize(float length, float height)
8 {
9 mat4 result = glm.translate(glm.scale(mat4.identity(),
10 new vec3(length, length, 1)),
11 new vec3(0, height / length, 0));
12
13 return result;
14 }
大体原理是:血条模型本身必须是中心对称的;摄像机远离其Target时,应该放大血条(glm.scale());之后向上移动一段距离,这段距离与摄像机到Target的距离是减函数关系(我就认为是成反比,具体原因我说不清)。这样才可能保持其大小不变。经过试验,上述AlwaysSameSize()方法是正确的。
demo
您可以在下图的例子中观察血条类型的模型是如何实现的。此demo顺便加上了后面要介绍的血条型文字(teapot)和UI文字(Hello Label!)。
字符串模型
渲染文字的基本思想很简单:字符都是贴图,贴到Quad上就行了。
贴图
所以要准备好贴图。这一步我已经在(http://www.cnblogs.com/bitzhuwei/p/generate-bitmap-from-ttf.html)详细叙述过了。
模型
文字模型是在同一平面内的若干个Quad。所以定义其模型如下。
1 /// <summary>
2 /// 用于渲染一段文字
3 /// </summary>
4 public class StringModel : IModel
5 {
6 public sampler2D glyphTexture { get; set; }
7 public GlyphPosition[] positions { get; set; }
8 public GlyphColor[] colors { get; set; }
9 public GlyphTexCoord[] texCoords { get; set; }
10
11 public Objects.VertexBuffers.BufferRenderer GetPositionBufferRenderer(string varNameInShader)
12 {
13 using (var buffer = new PositionBuffer(varNameInShader))
14 {
15 buffer.Alloc(positions.Length);
16 unsafe
17 {
18 var array = (GlyphPosition*)buffer.FirstElement();
19 for (int i = 0; i < positions.Length; i++)
20 {
21 array[i] = positions[i];
22 }
23 }
24
25 return buffer.GetRenderer();
26 }
27 }
28
29 public Objects.VertexBuffers.BufferRenderer GetColorBufferRenderer(string varNameInShader)
30 {
31 using (var buffer = new ColorBuffer(varNameInShader))
32 {
33 buffer.Alloc(colors.Length);
34 unsafe
35 {
36 var array = (GlyphColor*)buffer.FirstElement();
37 for (int i = 0; i < colors.Length; i++)
38 {
39 array[i] = colors[i];
40 }
41 }
42
43 return buffer.GetRenderer();
44 }
45 }
46
47 public Objects.VertexBuffers.BufferRenderer GetTexCoordBufferRenderer(string varNameInShader)
48 {
49 using (var buffer = new TexCoordBuffer(varNameInShader))
50 {
51 buffer.Alloc(texCoords.Length);
52 unsafe
53 {
54 var array = (GlyphTexCoord*)buffer.FirstElement();
55 for (int i = 0; i < texCoords.Length; i++)
56 {
57 array[i] = texCoords[i];
58 }
59 }
60
61 return buffer.GetRenderer();
62 }
63 }
64
65 public Objects.VertexBuffers.BufferRenderer GetNormalBufferRenderer(string varNameInShader)
66 {
67 return null;
68 }
69
70 public Objects.VertexBuffers.BufferRenderer GetIndexes()
71 {
72 using (var buffer = new ZeroIndexBuffer(DrawMode.Quads, 0, this.positions.Length * 4))
73 {
74 return buffer.GetRenderer();
75 }
76 }
77
78 public struct GlyphPosition
79 {
80 public vec2 leftUp;
81 public vec2 leftDown;
82 public vec2 rightUp;
83 public vec2 rightDown;
84
85 public GlyphPosition(
86 vec2 leftUp,
87 vec2 leftDown,
88 vec2 rightUp,
89 vec2 rightDown)
90 {
91 this.leftUp = leftUp;
92 this.leftDown = leftDown;
93 this.rightUp = rightUp;
94 this.rightDown = rightDown;
95 }
96 }
97
98 public struct GlyphColor
99 {
100 public vec4 leftUp;
101 public vec4 leftDown;
102 public vec4 rightUp;
103 public vec4 rightDown;
104
105 public GlyphColor(
106 vec4 leftUp,
107 vec4 leftDown,
108 vec4 rightUp,
109 vec4 rightDown)
110 {
111 this.leftUp = leftUp;
112 this.leftDown = leftDown;
113 this.rightUp = rightUp;
114 this.rightDown = rightDown;
115 }
116 }
117
118 public struct GlyphTexCoord
119 {
120 public vec2 leftUp;
121 public vec2 leftDown;
122 public vec2 rightUp;
123 public vec2 rightDown;
124
125 public GlyphTexCoord(
126 vec2 leftUp,
127 vec2 leftDown,
128 vec2 rightUp,
129 vec2 rightDown)
130 {
131 this.leftUp = leftUp;
132 this.leftDown = leftDown;
133 this.rightUp = rightUp;
134 this.rightDown = rightDown;
135 }
136 }
137
138 class PositionBuffer : PropertyBuffer<GlyphPosition>
139 {
140 public PositionBuffer(string varNameInShader)
141 : base(varNameInShader, 2, GL.GL_FLOAT, BufferUsage.StaticDraw)
142 { }
143 }
144 class ColorBuffer : PropertyBuffer<GlyphColor>
145 {
146 public ColorBuffer(string varNameInShader)
147 : base(varNameInShader, 4, GL.GL_FLOAT, BufferUsage.StaticDraw)
148 { }
149 }
150
151 class TexCoordBuffer : PropertyBuffer<GlyphTexCoord>
152 {
153 public TexCoordBuffer(string varNameInShader)
154 : base(varNameInShader, 2, GL.GL_FLOAT, BufferUsage.StaticDraw)
155 { }
156 }
157 }
StringModel
Shader
vertex shader如下。
1 #version 150 core
2
3 in vec2 position;
4 in vec4 color;
5 out vec4 passColor;
6 in vec2 texCoord;
7 out vec2 passTexCoord;
8 uniform mat4 mvp;
9
10 void main(void)
11 {
12 gl_Position = mvp * vec4(position, 0.0f, 1.0f);
13 passColor = color;
14 passTexCoord = texCoord;
15 }
fragment shader如下。
1 #version 150 core
2
3 in vec4 passColor;
4 in vec2 passTexCoord;
5 uniform sampler2D glyphTexture;
6 out vec4 outputColor;
7
8 void main(void)
9 {
10 float transparency = texture(glyphTexture, passTexCoord).r;
11 if (transparency == 0.0f)
12 {
13 discard;
14 }
15 else
16 {
17 outputColor = vec4(1, 1, 1, transparency) * passColor;
18 }
19 }
根据字符串创建模型
给定一个字符串,我们可以计算出相应的模型。
1 public static class DummyStringModelFactory
2 {
3 /// <summary>
4 /// 简单地生成一行文字。
5 /// </summary>
6 /// <param name="content"></param>
7 /// <returns></returns>
8 public static StringModel GetModel(this string content)
9 {
10 StringModel model = new StringModel();
11
12 var glyphPositions = new StringModel.GlyphPosition[content.Length];
13 FontResource fontResource = CSharpGL.GlyphTextures.FontResource.Default;
14 var glyphTexCoords = new StringModel.GlyphTexCoord[content.Length];
15 //fontResource.GenerateBitmapForString(content, 10, 10000);
16 int currentWidth = 0; int currentHeight = 0;
17 /*
18 * 0 3 4 6 8 11 12 15
19 * ------- ------- ------- -------
20 * | | | | | | | |
21 * | | | | | | | |
22 * | | | | | | | |
23 * ------- ------- ------- -------
24 * 1 2 5 6 9 10 13 14
25 */
26 for (int i = 0; i < content.Length; i++)
27 {
28 char ch = content[i];
29 CharacterInfo info = fontResource.CharInfoDict[ch];
30 glyphPositions[i] = new StringModel.GlyphPosition(
31 new GLM.vec2(currentWidth, currentHeight + fontResource.FontHeight),
32 new GLM.vec2(currentWidth, currentHeight),
33 new GLM.vec2(currentWidth + info.width, currentHeight),
34 new GLM.vec2(currentWidth + info.width, currentHeight + fontResource.FontHeight));
35 const int shrimp = 2;
36 glyphTexCoords[i] = new StringModel.GlyphTexCoord(
37 new GLM.vec2((float)(info.xoffset + shrimp) / (float)fontResource.FontBitmap.Width, (float)(currentHeight) / (float)fontResource.FontBitmap.Height),
38 new GLM.vec2((float)(info.xoffset + shrimp) / (float)fontResource.FontBitmap.Width, (float)(currentHeight + fontResource.FontHeight) / (float)fontResource.FontBitmap.Height),
39 new GLM.vec2((float)(info.xoffset - shrimp + info.width) / (float)fontResource.FontBitmap.Width, (float)(currentHeight + fontResource.FontHeight) / (float)fontResource.FontBitmap.Height),
40 new GLM.vec2((float)(info.xoffset - shrimp + info.width) / (float)fontResource.FontBitmap.Width, (float)(currentHeight) / (float)fontResource.FontBitmap.Height)
41 );
42 currentWidth += info.width + 10;
43 }
44 // move to center
45 for (int i = 0; i < content.Length; i++)
46 {
47 StringModel.GlyphPosition position = glyphPositions[i];
48
49 position.leftUp.x -= currentWidth / 2;
50 position.leftDown.x -= currentWidth / 2;
51 position.rightUp.x -= currentWidth / 2;
52 position.rightDown.x -= currentWidth / 2;
53 position.leftUp.y -= (currentHeight + fontResource.FontHeight) / 2;
54 position.leftDown.y -= (currentHeight + fontResource.FontHeight) / 2;
55 position.rightUp.y -= (currentHeight + fontResource.FontHeight) / 2;
56 position.rightDown.y -= (currentHeight + fontResource.FontHeight) / 2;
57
58 position.leftUp.x /= (currentHeight + fontResource.FontHeight);
59 position.leftDown.x /= (currentHeight + fontResource.FontHeight);
60 position.rightUp.x /= (currentHeight + fontResource.FontHeight);
61 position.rightDown.x /= (currentHeight + fontResource.FontHeight);
62 position.leftUp.y /= (currentHeight + fontResource.FontHeight);
63 position.leftDown.y /= (currentHeight + fontResource.FontHeight);
64 position.rightUp.y /= (currentHeight + fontResource.FontHeight);
65 position.rightDown.y /= (currentHeight + fontResource.FontHeight);
66 glyphPositions[i] = position;
67 }
68
69 var glyphColors = new StringModel.GlyphColor[content.Length];
70 for (int i = 0; i < glyphColors.Length; i++)
71 {
72 glyphColors[i] = new StringModel.GlyphColor(
73 new GLM.vec4(0, 0, 0, 1),
74 new GLM.vec4(0, 0, 0, 1),
75 new GLM.vec4(0, 0, 0, 1),
76 new GLM.vec4(0, 0, 0, 1)
77 );
78 }
79
80 model.positions = glyphPositions;
81 model.texCoords = glyphTexCoords;
82 model.colors = glyphColors;
83 model.glyphTexture = FontTextureManager.Instance.GetTexture2D(fontResource.FontBitmap);
84
85 return model;
86 }
87 }
public static StringModel GetModel(this string content);
标签(Label)
在OpenGL场景中,像Winform里的标签(Label)一样的控件如何实现?如何在UI空间内渲染大量文字?
我之前实现过的"标签"是用point sprite做的,其大小范围有限,最多到256x256个像素。当时不用GLSL+VBO,是因为那会还不知道如何使模型始终朝向camera。
现在在StringModel的基础上,只需借助IUILayout机制即可实现opengl里的标签控件。
1 public class DummyLabel : RendererBase, IUILayout
2 {
3 public StringRenderer renderer;
4
5 /// <summary>
6 ///
7 /// </summary>
8 /// <param name="param">the edges of the viewport to which a SimpleUIRect is bound and determines how it is resized with its parent.
9 /// <para>something like AnchorStyles.Left | AnchorStyles.Bottom.</para></param>
10 /// <param name="content"></param>
11 public DummyLabel(IUILayoutParam param, string content)
12 {
13 this.renderer = new StringRenderer(content.GetModel());
14
15 IUILayout layout = this;
16 layout.Param = param;
17 }
18
19 protected override void DisposeUnmanagedResources()
20 {
21 this.renderer.Dispose();
22 }
23
24 #region IUILayout
25
26 public IUILayoutParam Param { get; set; }
27
28 #endregion IUILayout
29
30 protected override void DoInitialize()
31 {
32 this.renderer.Initialize();
33 }
34
35 protected override void DoRender(RenderEventArgs e)
36 {
37 mat4 projectionMatrix, viewMatrix, modelMatrix;
38 {
39 IUILayout element = this as IUILayout;
40 element.GetMatrix(out projectionMatrix, out viewMatrix, out modelMatrix, null);
41 }
42 this.renderer.mvp = projectionMatrix * viewMatrix * modelMatrix;
43
44 this.renderer.Render(e);
45
46 }
47 }
总结
本文还没有彻底解决血条型文字"在屏幕上大小不变"的问题。