CSharpGL(46)用Billboard绘制头顶文字
本文介绍CSharpGL用Billboard绘制头顶文字的方法。效果如下图所示。
下载
CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入( https://github.com/bitzhuwei/CSharpGL )
固定大小的Billboard
在OpenGL的渲染流水线上,描述 顶点位置 的坐标,依次要经过object space, world space, view/camera space, clip space, normalized device space, Screen/window space这几个状态。下表列出了各个状态的特点。
| Space |
Coordinate |
feature |
| object |
(x, y, z, 1) |
从模型中读取的原始位置(x,y,z),可在shader中编辑 |
| world |
(x, y, z, w) |
可在shader中编辑 |
| view/camera |
(x, y, z, w) |
可在shader中编辑 |
| clip |
(x, y, z, w) |
vertex shader中,赋给gl_Position的值 |
| normalized device |
(x, y, z, 1) |
上一步的(x, y, z, w)同时除以w。OpenGL自动完成。x, y, z的绝对值小于1时,此顶点在窗口可见范围内。即可见范围为[-1, -1, -1]到[1, 1, 1]。 |
| screen/window |
glViewport(x, y, width, height); glDepthRange(near, far) |
窗口左下角为(0, 0)。 上一步的顶点为(-1, -1, z)时,screen上的顶点为(x, y)。 上一步的顶点为(1, 1, z)时,screen上的顶点为(width, height)。 |
为了让Billboard保持他应有的 位置 和 深度值 ,object space, world space, view space这三步是必须照常进行的。
在normalized device space这个状态下,[-1,-1,-1]和[1,1,1]之间就是Billboard能 显示出来 的部分。例如,如果一个Billboard矩形的四个角落,恰好落在(-1,-1)和(1,1)上,那么这个Billboard就会恰好覆盖整个 画布 。所以,如果知道了 Billboard和画布的尺寸(像素值) ,就可以按比例计算出Billboard在此状态时应有的尺寸了。
这两段分析就是下面的vertex shader的精髓。Billboard的位置,由一个位于矩形中心的 点 表示。在object space里,这个点自然要位于(0, 0, 0, 1)。
1 #version 330 core
2
3 uniform mat4 projectionMatrix;
4 uniform mat4 viewMatrix;
5 uniform mat4 modelMatrix;
6 uniform vec2 screenSize; // screen size in pixels.
7
8 uniform float width; // Billboard’s width in pixels
9 uniform float height;// Billboard’s height in pixels.
10
11 in vec2 inPosition;// character's quad's position relative to left bottom(0, 0).
12 in vec3 inSTR;// character's quad's texture coordinate.
13
14 out vec3 passSTR;
15
16 void main(void) {
17 vec4 position = projectionMatrix * viewMatrix * modelMatrix * vec4(0, 0, 0, 1);
18 position = position / position.w;// 代替OpenGL pipeline除以w的步骤。
19 position.xy += (inPosition * height - vec2(width, height)) / screenSize;
20 gl_Position = position;
21
22 passSTR = inSTR;
23 }
绘制文字
首先,你要知道如何准备文字Texture(参考这里)。
然后,根据给定的字符串Text,找到各个char的位置,更新positionBuffer和uvBuffer,更新Billboard的Width和Height。为了减少客户端的计算量,在安排char的位置时,是从左下角(0,0)开始,到右上角(width, height)结束的。不然,就该把char的位置整体移动到以(0,0)为中心了。
下图中,把一个一个字符围起来的框框,说明了文字是如何排列的。
多个Billboard的重叠问题
在Billboard中,为了显示文字,启用了OpenGL的混合(blend)功能。
1 public static TextBillboardNode Create(int width, int height, int capacity, GlyphServer glyphServer = null)
2 {
3 var vs = new VertexShader(vertexCode);// this vertex shader has no vertex attributes.
4 var fs = new FragmentShader(fragmentCode);
5 var provider = new ShaderArray(vs, fs);
6 var map = new AttributeMap();
7 map.Add(inPosition, GlyphsModel.position);
8 map.Add(inSTR, GlyphsModel.STR);
9 // 启用混合功能
10 var blendState = new BlendState(BlendingSourceFactor.SourceAlpha, BlendingDestinationFactor.OneMinusSourceAlpha);
11 var builder = new RenderMethodBuilder(provider, map, blendState);
12 var node = new TextBillboardNode(width, height, new GlyphsModel(capacity), builder, glyphServer);
13 node.Initialize();
14
15 return node;
16 }
由于blend功能是与渲染顺序相关的(即渲染顺序不同,产生的结果就可能不同),所以在渲染多个Billboard时,就可能产生不好的效果:近处的Billboard可能遮挡住远处的。
为了解决这个问题,我想了一个办法:先按 深度 给各个Billboard排序,然后 依序 渲染各个Billboard。为此,需要新建一些东西。
排序动作BillboardSortAction
首先要将各个Billboard排序,并保存到数组。显然,在这里,使用 二分插入排序 是最快的排序方式。
1 public class BillboardSortAction : DependentActionBase
2 {
3 private List<float> depthList = new List<float>();
4 private List<TextBillboardNode> billboardList = new List<TextBillboardNode>();
5
6 /// <summary>
7 /// Sorted billboard list.
8 /// </summary>
9 public List<TextBillboardNode> BillboardList
10 {
11 get { return billboardList; }
12 }
13
14 /// <summary>
15 /// Sort billboards in depth order.
16 /// </summary>
17 /// <param name="scene"></param>
18 public BillboardSortAction(Scene scene) : base(scene) { }
19
20 public override void Act()
21 {
22 this.depthList.Clear();
23 this.billboardList.Clear();
24
25 mat4 viewMatrix = this.Scene.Camera.GetViewMatrix();
26 this.Sort(this.Scene.RootElement, viewMatrix);
27 }
28
29 private void Sort(SceneNodeBase sceneElement, mat4 viewMatrix)
30 {
31 if (sceneElement != null)
32 {
33 var billboard = sceneElement as TextBillboardNode;
34 if (billboard != null)
35 {
36 Insert(billboard, viewMatrix);
37 }
38
39 foreach (var item in sceneElement.Children)
40 {
41 this.Sort(item, viewMatrix);
42 }
43 }
44 }
45
46 /// <summary>
47 /// binary insertion sort.
48 /// </summary>
49 /// <param name="billboard"></param>
50 /// <param name="camera"></param>
51 /// <param name="list"></param>
52 private void Insert(TextBillboardNode billboard, mat4 viewMatrix)
53 {
54 // viewPosition.z is depth in view/camera space.
55 vec3 viewPosition = billboard.GetAbsoluteViewPosition(viewMatrix);
56 int left = 0, right = this.depthList.Count - 1;
57 while (left <= right)
58 {
59 int middle = (left + right) / 2;
60 float value = this.depthList[middle];
61 if (value < viewPosition.z)
62 {
63 left = middle + 1;
64 }
65 else if (value == viewPosition.z)
66 {
67 left = middle;
68 break;
69 }
70 else //(viewPosition.z < value)
71 {
72 right = middle - 1;
73 }
74 }
75
76 this.depthList.Insert(left, viewPosition.z);
77 this.billboardList.Insert(left, billboard);
78 }
79 }
BillboardSortAction
渲染动作BillboardRenderAction
虽然我们有专门的渲染动作RenderAction,但是RenderAction只会按结点的树结构顺次渲染。因此,我们要新建一个专门渲染 已经排序好了的Billboard数组 的动作。
1 /// <summary>
2 /// Render sorted billboards.
3 /// </summary>
4 public class BillboardRenderAction : DependentActionBase
5 {
6 private BillboardSortAction sortAction;
7 public BillboardRenderAction(Scene scene, BillboardSortAction sortAction)
8 : base(scene)
9 {
10 this.sortAction = sortAction;
11 }
12
13 public override void Act()
14 {
15 var arg = new RenderEventArgs(this.Scene, this.Scene.Camera);
16 foreach (var item in this.sortAction.BillboardList)
17 {
18 item.RenderBeforeChildren(arg);
19 }
20 }
21 }
22 }
当然,不要忘了取消Billboard在RenderAction里的渲染动作。
1 var billboard = TextBillboardNode.Create(200, 40, 100);
2 billboard.Text = string.Format("Hello TextBillboardNode[{0}]!", index);
3 // we don't render it in RenderAction. we render it in BillboardRenderAction.
4 billboard.EnableRendering = ThreeFlags.None;
总结
又一次,又一次,又一次,犯了很二的错误。
TextBillboardNode.cs是复制过来的,然后我就忘记了修改里面的AttributeMap的数据。原本2个小时就能完成的东西,花了2天才找到错误所在。
这个事情告诉我,即使很类似的代码,也不要复制过来。一点一点写才是最快的。
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。