CSharpGL(15)用GLSL渲染2种类型的文字
下载
这个示例是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 }
总结
本文还没有彻底解决血条型文字"在屏幕上大小不变"的问题。