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 }
渲染動作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天才找到錯誤所在。
這個事情告訴我,即使很類似的代碼,也不要復制過來。一點一點寫才是最快的。
文章列表