實現目標
因為需求,想找一個在Ogre中好用的文本顯示,經過查找和一些比對.有三種方案
一利用Overlay的2D顯示來達到效果.
http://www.ogre3d.org/tikiwiki/tiki-index.php?page=MovableTextOverlay
二重寫Renderable與MovableObject,利用對應字體查找到每個字符元素紋理坐標.
http://www.ogre3d.org/tikiwiki/tiki-index.php?page=MovableText
三利用BillboardSet在3D空間顯示(公告板技術),這個有點意思的是對於字體的處理是自己用GUI畫成紋理.
http://www.ogre3d.org/tikiwiki/tiki-index.php?page=MOGRE+MovableText+by+Billboards
我的要求應該是這樣的,文本會比較多,每個文本通常關聯3D空間里一個點,在動畫效果時點會移動要求文本也能移動,方便添加,刪除,修改文本.
其中第一與第二沒有用到視圖變換,透視變換,直接根據3D位置轉化成對應屏幕xy中的0到1之間,丟失第三維深度信息,這里倒是沒有多大關系.但是這三種都有一個問題,單個顯示沒什么,如果有100個點,就一百個文本,就有一百個Renderable,一百次渲染,在這個需求里,這是完全存在的.
所以自己動手,豐衣足食(好吧,是因為找到的都不滿足需求),先細清下要達到的目標及思路.
首先.文本是否保持3維,如果3維顯示,那么需要一直保證文本面對攝像機.如果2維顯示,會簡單一些,只需要去掉對應視圖,透視變換,然后把對應點的位置直接轉化成對應二維的-1到1之間就OK.
其次,如何把所有文本一次渲染,先說明在Ogre里如何渲染的,如果是一個字母A,那么我們畫一個四邊形,然后對應圖片(這個圖片畫着一個A)放在這個四邊形上就OK了.那么如果有4個點,分別顯示A,B,C,D.我們可以這樣考慮,先把ABCD這四個字母畫到一張圖片上,然后取每個字母在這個圖片上的紋理坐標,這樣我們就能根據一張紋理一下顯示4個文本.
最后,因為有動畫效果以及添加,刪除,修改文本,考慮每楨更新顯示,本來想着是用Mesh-Entity,但是感覺是在用高射炮打蚊子,花費高效果不好.如果能最簡單方便的達到這個目的,我們需要先看下最基本的渲染元素.
Ogre 渲染基類
Renderable則負責渲染,主要包含獲取材質,材質技術,得到渲染體RenderOperation,這個類里面比較簡單,就是頂點數據,索引數據這些.
主要字段:
mUseIdentityProjection:是否啟用投影矩陣.前面說過,投影矩陣(正交,透視)是把視圖矩陣里的空間轉化成各邊為-1到1的立方體.(ogre中,caram里mFoVy,mFarDist,mNearDist,mAspect可生成二種類型投影矩陣).
mUseIdentityView:是否啟用視圖變換,就是啟用不視圖變換,如果不啟用,那么渲染物體和抽象機的位置無關.(Ogre中,caram里mPosition,mOrientation生成以視圖矩陣,其實就是一個以mPosition為原點,以lookat方向為Z值的三維坐標軸).
這二個屬性一般為false,除非在Overlay(多用於UI)中,都設置false,因為這種UI位置與攝像機位置無關,所以mUseIdentityView,至於把mUseIdentityProjection設為false,則是把對應值(0,1)轉化成(-1,1),不然用戶需要先把在頂點在視圖中的位置除viewport的長寬再轉化成(-1,1),會很麻煩. mCustomParameters:當用戶渲染需要用到着色器,並且需要設置參數時,提供的一種簡單方法.mPolygonModeOverrideable:簡單來說,就是是否受到攝像機針對顯示模式的修改而影響.(顯示點,線,面).
主要方法:
getMaterial(純虛函數):當前要渲染的紋理.
getRenderOperation(純虛函數):渲染中的頂點緩沖區,頂點索引緩沖區,渲染類型(點,線,三角形這些).
preRender:getRenderOperation后,渲染前,可以得到當前場景管理與渲染系統的對象.
postRender:和preRender,只是發生在渲染后.
MovableObject負責與場景管理中的SceneNode交互,是否可見,包圍體,包圍圈.查詢標識,更新對應渲染體Renderable到渲染列表中.
主要字段:
mParentNode: 附加上的節點.
mVisible:是否可見.
mRenderQueneID:一般設置大於0,少於100,值大的會覆蓋值少的,越大越顯示在上面.
mQueryFlags: 場景查詢相交測試標示相與,是則查詢.
mVisibilityFlags: 與對應viewport對應標示相與,是則顯示.
mWorldAABB:包圍圈
mCastShadows:是否啟用陰影
主要方法:
_updateRenderQueue(純虛函數):把自己要渲染的內容(Renderable)更新到渲染列表.
_notifyCurrentCamera:在_updateRenderQueue之前,獲取當前攝像機的信息,用於后期要和攝像機有關的渲染處理.
Renderable與MovableObject組合渲染物體
Renderable可以直接用於渲染,但是Renderable需要依靠MovableObject置入場景管理,更新渲染列表中.所以大部分需要渲染的元素都是Renderable和MovableObject的派生類.在渲染中,一般是根據MovableObject所附加的SceneNode位置來決定對應Renderable是否添加對應到渲染列表(_updateRenderQueue),然后渲染 渲染列表中的物體時,根據Renderable的getMaterial來設置渲染環境(如果有着色器代碼,則啟用),然后getRenderOperation設置要渲染頂點與索引,開始渲染.
Ogre中大致有三種組合方式來渲染.下面列舉其中一些比較常見的:
第一種:只從Renderable繼承,不需要附加到SceneNode上.一般用於OgreUI系統.
OverlayElement: Overlay組件里所有元素的基類.如panel,textArea,borderPanel.從基類中得到可以渲染的能力.
BorderRenderable:borderPanel比panel多出來要渲染的部分,這個元素會分二次渲染.
第二種:從Renderable與MovableObject繼承.
SimpleRenderable: Ogre中幫我們定義的一個簡單實現,Ogre內部有一些此類的派生實現,大家可以簡單看下.
BillboardSet:公告板技術的一種實現,繪制多個始終面對攝像機的方形框.用於一些特效如草地啥的,還有Ogre中的Particle粒子效果也是交給BillboardSet處理的.
BillboardChain:實現線條特效,其子類RibbonTrail實現軌跡特效,如刀光,流星等.
Frustum:畫對應投影可視體,如正交是一個長方體,透視則是一個立方錐.
第三種:類A從MovableObject繼承,然后類A中包含類B的列表,而類B從Renderable繼承.
Entity-SubEntity:Entity繼承MovableObject,可以附加到SceneNode上,而SubEntity繼承Renderable,才是真正用於渲染.比如一個人是一個整體,但是我們需要分開渲染,先渲染頭,手,身體就是這個道理.同時也是內置模型Mesh的包裝類,其中,封裝了如姿態,頂點,骨骼動畫.
ManualObject-ManualObjectSection:同Entity-SubEntity一樣,讓用戶能方便實現一個物體包含多個組件的模型,並且參考了opengl即時模式的API,不同之后,最后還是以緩沖區模式提交數據.
實現代碼
回到我們需求,我們采用最合適的方式當是第二種,從Renderable與MovableObject繼承,如果第一種,我們能控制的比較少,第三種我們又只需要一次渲染,用不着.先確定下,我們采用公告板技術,啟用視圖,投影矩陣.
下面是Axiom代碼,要轉成Ogre,MOgre相應代碼都非常方便.
public class ALabel { public string Label { get; set; } public Vector3 Position { get; set; } } public class ALabelSet : MovableObject, IRenderable { protected AxisAlignedBox aab = new AxisAlignedBox(); public ALabelList labelList = new ALabelList(); protected RenderOperation renderOperation = new RenderOperation(); private Font _font; private string _fontName; protected Material material; //字體名,設置后加載對應字體的紋理,上面有各個字符在紋理中的坐標以及大小 public string FontName { get { return this._fontName; } set { if (this._fontName != value || material == null || this._font == null) { this._fontName = value; this._font = (Font)FontManager.Instance[this._fontName]; if (this._font == null) { throw new Exception(String.Format("Could not find font '{0}'.", this._fontName)); } this._font.Load(); if (material != null) { if (material.Name != "BaseWhite") { MaterialManager.Instance.Unload(material); } material = null; } material = this._font.Material.Clone(name + "Material", false, this._font.Material.Group); if (material.IsLoaded == true) { material.Load(); } material.DepthCheck = false; //material.CullingMode = CullingMode.None; //material.ManualCullingMode = ManualCullingMode.None; material.Lighting = false; } } } private int _spaceWidth; private int _characterHeight; public int CharacterHeight { get { return this._characterHeight; } set { this._characterHeight = value; } } public int SpaceWidth { get { return this._spaceWidth; } set { this._spaceWidth = value; } } public bool buffersCreated; protected VertexData vertexData = null; protected HardwareVertexBuffer mainBuffer = null; private BufferBase lockPtr = null; private int ptrOffset = 0; //多攝像機,在每個viewport渲染時,需要記錄對應攝像機,在渲染時要計算對應位置 protected Camera currentCamera; private ColorEx _color = ColorEx.White; private int iColor = 0; public ColorEx Color { get { return this._color; } set { this._color = value; } } public ALabelSet(string fontName, int charHeight) { this.FontName = fontName; this._characterHeight = charHeight; castShadows = false; } protected override void dispose(bool disposeManagedResources) { if (!IsDisposed) { if (disposeManagedResources) { if (this.renderOperation != null) { if (!this.renderOperation.IsDisposed) { this.renderOperation.Dispose(); } this.renderOperation = null; } } } base.dispose(disposeManagedResources); } //先檢查是否需要重新申請緩沖區,然后獲取當前緩沖區句柄 private void BeginRender() { if (this.labelList.Count == 0) return; if (this.buffersCreated) { int count = this.labelList.TextAll.Length; if (count * 6 != this.vertexData.vertexCount) { this.DestroyBuffers(); } } if (!this.buffersCreated) { this.CreateBuffer(); } this.iColor = Root.Instance.ConvertColor(this._color); this.lockPtr = this.mainBuffer.Lock(BufferLocking.Discard); this.ptrOffset = 0; } //根據當前攝像機,得到每個字符的位置.根據字體紋理,得到每個字符紋理坐標. private void Rendering(ALabel label) { if (!this.currentCamera.IsObjectVisible(label.Position)) { return; } var charlen = label.Label.Length; var camera = this.currentCamera; var camPos = this.parentNode.FullTransform * camera.DerivedPosition; //camera to pos var camTo = camPos - label.Position; camTo.Normalize(); //var xAxis = camTo.Cross(Vector3.UnitY); //var yAxis = camTo.Cross(xAxis); //xAxis = camTo.Cross(yAxis); //var camQ = Quaternion.FromAxes(xAxis, yAxis, camTo); var labelTo = Vector3.UnitZ; if (camTo.z < 0) labelTo = Vector3.NegativeUnitZ; var dotAngle = camTo.Dot(labelTo); var angle = Math.Acos(dotAngle); var axis = labelTo.Cross(camTo); var camQ = Quaternion.FromAngleAxis(angle, axis); //if (camera.Name == "PerspectiveViewportCamera") // System.Diagnostics.Debug.WriteLine("{0}<->{1}<->{2}", dotAngle, angle, camTo.z); var left = 0.0f; for (var i = 0; i != charlen; i++) { char cr = label.Label[i]; var clyph = this._font.Glyphs[cr]; var width = clyph.aspectRatio;// this._font.GetGlyphAspectRatio(cr); //Real u1, u2, v1, v2;this._font.GetGlyphTexCoords(cr, out u1, out v1, out u2, out v2); Real u1 = clyph.uvRect.Top; Real u2 = clyph.uvRect.Bottom; Real v1 = clyph.uvRect.Left; Real v2 = clyph.uvRect.Right; var xLeft = camQ * Vector3.UnitX * left * 2.0f * labelTo.z; var xRigth = camQ * Vector3.UnitX * (left + width) * 2.0f * labelTo.z; var y = camQ * (Vector3.UnitY * this._characterHeight * 2.0f); var tl = label.Position + xLeft; var bl = label.Position + xLeft - y; var tr = label.Position + xRigth; var br = label.Position - y + xRigth; left += width; unsafe { var posPtr = this.lockPtr.ToFloatPointer(); var colPtr = this.lockPtr.ToIntPointer(); var texPtr = posPtr; //first tri //top left posPtr[ptrOffset++] = tl.x; posPtr[ptrOffset++] = tl.y; posPtr[ptrOffset++] = tl.z;// ml.Position.z; colPtr[ptrOffset++] = this.iColor; texPtr[ptrOffset++] = u1; texPtr[ptrOffset++] = v1; //2 bottom left posPtr[ptrOffset++] = bl.x; posPtr[ptrOffset++] = bl.y; posPtr[ptrOffset++] = bl.z;// ml.Position.z; colPtr[ptrOffset++] = this.iColor; texPtr[ptrOffset++] = u1; texPtr[ptrOffset++] = v2; //3 top right posPtr[ptrOffset++] = tr.x; posPtr[ptrOffset++] = tr.y; posPtr[ptrOffset++] = tr.z;// ml.Position.z; colPtr[ptrOffset++] = this.iColor; texPtr[ptrOffset++] = u2; texPtr[ptrOffset++] = v1; //second tri //1 top right posPtr[ptrOffset++] = tr.x; posPtr[ptrOffset++] = tr.y; posPtr[ptrOffset++] = tr.z;// ml.Position.z; colPtr[ptrOffset++] = this.iColor; texPtr[ptrOffset++] = u2; texPtr[ptrOffset++] = v1; //2 bottom left posPtr[ptrOffset++] = bl.x; posPtr[ptrOffset++] = bl.y; posPtr[ptrOffset++] = bl.z;// ml.Position.z; colPtr[ptrOffset++] = this.iColor; texPtr[ptrOffset++] = u1; texPtr[ptrOffset++] = v2; //3 bottom right posPtr[ptrOffset++] = br.x; posPtr[ptrOffset++] = br.y; posPtr[ptrOffset++] = br.z;// ml.Position.z; colPtr[ptrOffset++] = this.iColor; texPtr[ptrOffset++] = u2; texPtr[ptrOffset++] = v2; } } } //提交修改后的緩沖區數據 private void EndRender() { this.mainBuffer.Unlock(); this.lockPtr = null; } private void CreateBuffer() { this.vertexData = new VertexData(); this.vertexData.vertexStart = 0; this.vertexData.vertexCount = this.labelList.TextAll.Length * 6; var decl = this.vertexData.vertexDeclaration; var binding = this.vertexData.vertexBufferBinding; var offset = 0; decl.AddElement(0, offset, VertexElementType.Float3, VertexElementSemantic.Position); offset += VertexElement.GetTypeSize(VertexElementType.Float3); decl.AddElement(0, offset, VertexElementType.Color, VertexElementSemantic.Diffuse); offset += VertexElement.GetTypeSize(VertexElementType.Color); decl.AddElement(0, offset, VertexElementType.Float2, VertexElementSemantic.TexCoords, 0); this.mainBuffer = HardwareBufferManager.Instance.CreateVertexBuffer(decl.Clone(0), this.vertexData.vertexCount, BufferUsage.DynamicWriteOnlyDiscardable); binding.SetBinding(0, this.mainBuffer); this.buffersCreated = true; } private void DestroyBuffers() { this.vertexData = null; this.mainBuffer = null; this.buffersCreated = false; } #region MovableObject //不需要場景查詢. public override AxisAlignedBox BoundingBox { get { return (AxisAlignedBox)this.aab.Clone(); } } //同上 public override Real BoundingRadius { get { return 1.0f; } } //檢查物體是否渲染時,更新當前Renderable到渲染列表中 public override void UpdateRenderQueue(RenderQueue queue) { //if (bCameraMove) { BeginRender(); foreach (var label in this.labelList) { Rendering(label); } EndRender(); } queue.AddRenderable(this);//, RenderQueue.DEFAULT_PRIORITY, renderQueueID); } private bool bCameraMove = false; private Vector3 prePosition = Vector3.Zero; //我們有多個攝像機,而每次渲染需要根據攝像機位置更新緩沖區 public override void NotifyCurrentCamera(Camera camera) { var currPos = Root.Instance.SceneManager.GetCamera("PerspectiveViewportCamera").Position; bCameraMove = currentCamera == null || currPos != prePosition; if (camera.Name == "PerspectiveViewportCamera") prePosition = currPos; this.currentCamera = camera; } #endregion #region IRenderable //不需要陰影 public bool CastsShadows { get { return false; } } //沒有啟用着色器.不需要 public Vector4 GetCustomParameter(int index) { return Vector4.Zero; } public Real GetSquaredViewDepth(Camera camera) { return parentNode.GetSquaredViewDepth(camera); } //當前模型矩陣 public void GetWorldTransforms(Matrix4[] matrices) { matrices[0] = parentNode.FullTransform; } public Axiom.Core.Collections.LightList Lights { get { return QueryLights(); } } //字體紋理 public Material Material { get { return material; } } //格式化法線 public bool NormalizeNormals { get { return false; } } //一個 public ushort NumWorldTransforms { get { return 1; } } //是否和攝像機同PolygonMode public bool PolygonModeOverrideable { get { return true; } } public RenderOperation RenderOperation { get { if (bCameraMove) { this.renderOperation.vertexData = this.vertexData; this.renderOperation.vertexData.vertexStart = 0; this.renderOperation.operationType = OperationType.TriangleList; this.renderOperation.useIndices = false; this.renderOperation.indexData = null; this.renderOperation.vertexData.vertexCount = this.vertexData.vertexCount; } return this.renderOperation; } } //着色器 public void SetCustomParameter(int index, Vector4 val) { } public Technique Technique { get { return this.material.GetBestTechnique(); } } //着色器 public void UpdateCustomGpuParameter(GpuProgramParameters.AutoConstantEntry constant, GpuProgramParameters parameters) { } //是否啟用投影變換 public bool UseIdentityProjection { get { return false; } } //是否啟用視圖變換 public bool UseIdentityView { get { return false; } } //當前模型在世界空間方位 public Quaternion WorldOrientation { get { return parentNode.DerivedOrientation; } } //當前模型在世界空間頂點 public Vector3 WorldPosition { get { return parentNode.DerivedPosition; } } #endregion } public class ALabelList : List<ALabel> { public ALabelList() { } public void Add(string label, Vector3 position) { ALabel ab = new ALabel(); ab.Label = label ?? this.Count.ToString(); ab.Position = position; this.Add(ab); } public string TextAll { get { string text = string.Empty; foreach (var txt in this) { text += txt.Label; } return text; } } }
代碼其實還沒有完成,不過大部分已經實現,對照前面Renderable與MovableObject的實現,說下代碼中要注意的是.
代碼中,所有方法都加入了注釋.主要重載Renderable與MovableObject相關屬性,實現其方法.包含是否啟用投影變換,視圖變換,陰影,紋理等.
注意二個地方,一是當我們設置字體時,從字體中我們能得到一張紋理,里面包含所有字符,通過提供的方法,對得到對應字符在紋理中的坐標.二是當我們渲染時,label要一直面對攝像機方向,而我們提供的是XY方向的位置,如何保證XY變換成垂直於攝像機到Label的方向的平面,
我們得到字符的長寬,在水平面的長度,也就是XY方向,水平面的法線是Z軸,如果當我們攝像機與Label方向垂直XY正平面時,那時我們的字符位置就不需要轉化.我們已知字符轉化前法向量是z軸,而方向矢量(攝像機-label位置)是變化后的法向量.如果我們知道如何從Z軸轉化成label位置到攝像機的方向矢量,那么我們把頂點也進行這個轉化就OK了.(這里法向量旋轉等於頂點的旋轉,如果元素發生大小的變化,這個邏輯不再正確,大家具體可以找相關旋轉變化的書).
前面有說過,二個向量的點乘可以得到二個向量的角度A,二個向量的叉乘可以得到垂直於這個向量的方向向量V,簡單來說,在方向向量V上,旋轉角度A就能得到達到前面目錄.我們知道,四元數的定義就是由着方向軸旋轉角度,由此,得到前面的V與A,我們能很容易就得到對應四元數,不過需要注意的,二個向量的角度是計算的0-180度,而我們這明顯是360度,比如旋轉了270度和90度都是計算為90度的,所以在這里,我們需要做點改變,當攝像機跑到label后面去時,我們把對應的Z軸變為負Z軸.
這里我們還可以換個角度來想,Z軸變換后成為(攝像機-label位置)矢量,這個矢量也是新坐標軸的Z軸,知道Z軸,如何求XY軸,想起視圖矩陣是如何得到的沒?很簡單,Z corss Y(0,1,0)得到X,然后Z cross X得到Y,新的三軸我們得到了,或者后變換后的坐標軸我們得到了,根據3軸得到對應矩陣也是一樣的效果.
下面是效果圖,可以看出,有個視圖還是有問題,大家可以猜下是那導致的.
中間,我想做個嘗試,現在是每楨都更新,這是沒必要的,只有啟用動畫這里才需要如此,我開始想的是,如果數據沒變化,就不更新,可惜的是,我這里是多視圖,多攝像機,然后頂點位置計算又和攝像機有關,而當前緩沖區數據只有一個,這樣必需每楨更新,除非每個攝像機聲明一個緩沖區才行,這樣一想感覺又不划算.
最后,如上面問題,我們可以關閉視圖變換與投影變換,如Overlay一樣,那么我們如何修改了,記的前面說過,關閉視圖變換與投影變換后,我們設定的x,y值要在-1到1之間,否則是看不到的.所以如果大家記的把Node上的點變換成對應的-1到1就完成了,比上面公告板技術要簡單一點,順便可以參考Overlay里的計算.

