文章出處

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天才找到錯誤所在。

這個事情告訴我,即使很類似的代碼,也不要復制過來。一點一點寫才是最快的。

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()