問題背景
上次寫了動態繪制立方體,這最近又來了新功能,繪制圓柱(風筒),要求是給了很多節點,根據節點去動態繪制風筒,風筒就是圓柱連接而成的,可以理解為管道,還有就是拐角處注意倒角,圓潤過度過來。
實現原理
動態繪制圓柱mesh,注意,圓柱的mesh繪制遠比立方體復雜得多,上節闡述過基本mesh創建立方體,有興趣可以去看看https://www.cnblogs.com/answer-yj/p/11231247.html,頂點以及倒角需要你自己去插值出來,其中倒角是使用貝塞爾曲線插值過度出來的。
實現步驟
1.頂點坐標
2.uv坐標
3.構建索引
4.構建實體
具體過程
1.准備頂點數據
構建圓柱應當理解為這是多個圓插值出來的,首先設置圓的頂點坐標,首先說第一種方式,我先前考慮,在世界坐標空間中,創建一個Gamobject通過它的坐標作為圓上頂點坐標,不從本地創建坐標了(因為涉及坐標轉換,矩陣變換),所以我用了這種方式
第一種方式:
具體代碼:
/// <summary> /// 設置起中間點斷面頂點數據 /// </summary> /// <param name="currPos">當前點坐標</param> /// <param name="curDirection">朝向</param> /// <param name="radius">半徑</param> private void SetSectionVertexData(Vector3 currPos, Vector3 curDirection, float radius,float u) { // 首先計算導線點相對場景中心點的偏移坐標 Vector3 position = currPos - center; // 計算direction、right、up向量(注:Unity3d中使用左手坐標系) Vector3 direction = curDirection.normalized; Vector3 right = Vector3.Cross(Vector3.up, direction).normalized; Vector3 up = Vector3.Cross(direction, right).normalized; angleStep = 360.0f / (float)devide; GameObject gameObject = new GameObject(); gameObject.transform.position = position + radius * right;for (int i = 0; i < 360.0f; i += (int)angleStep) { gameObject.transform.RotateAround(currPos, direction, angleStep); vertices.Add(gameObject.transform.position); } Destroy(gameObject); }
代碼中direction是方向向量(上一點指向下一點坐標),構建頂點也需要順序,我這是在右側90度逆時針創建的,gameObject.transform.position = position + radius * right;這是起點頂點,一共十個頂點。
第二種方式:
我已經用第一種方式功能都做完了,創建好了,完事領導說我們風筒可能要在子線程創建,哇,一瞬間透心涼,GameObject就意味着淘汰了,矩陣變換是躲不開了。從本地創建圓,這句話的意思是指模型本地坐標,即左手坐標系(0,0,0)點 找一點為(0,0,0)點去創建。
具體代碼:
1 /// <summary> 2 /// 設置起點圓頂點數據 3 /// </summary> 4 /// <param name="startPos">起點坐標</param> 5 /// <param name="nextPos">下一點坐標</param> 6 private void SetStartVertexData(Vector3 startPos, Vector3 nextPos) 7 { 8 // 首先計算導線點相對場景中心點的偏移坐標 9 Vector3 position = startPos - center; 10 Vector3 nextPosition = nextPos - center; 11 12 // 計算direction、right、up向量(注:Unity3d中使用左手坐標系) 13 Vector3 direction = (nextPosition - position).normalized;//z軸 14 Vector3 right = Vector3.Cross(Vector3.up, direction).normalized;//x軸,右 15 Vector3 up = Vector3.Cross(direction, right).normalized;//x和z得到up 16 // 設起點圓定點數據 17 SetSectionVertexData(position, direction, up, right, uvPosU); 18 } 19 20 21 /// <summary> 22 /// 設置斷面頂點數據 23 /// </summary> 24 /// <param name="currentPos">當前點坐標</param> 25 /// <param name="direction">朝向</param> 26 private void SetSectionVertexData(Vector3 currentPos, Vector3 direction, Vector3 up, Vector3 right, float u) 27 { 28 // 構建旋轉矩陣 29 Matrix4x4 m = Utils.MakeBasis(right, up, direction); 30 for (float i = 0f; i < 360.0f; i += 36.0f) 31 { 32 // 計算頂點 33 float rad = Mathf.Deg2Rad * i; 34 Vector3 targetPos = currentPos + m.MultiplyPoint3x4(new Vector3(Mathf.Cos(rad) * radius, Mathf.Sin(rad) * radius, 0.0f)); 35 // 計算V坐標 36 float v = ((baseValue * 36.0f) / 360.0f); 37 // 保存頂點坐標&紋理坐標 38 vertices.Add(targetPos); 39 } 40 }
這里解釋下,過程是這樣的,在本地坐標構建圓,通過旋轉矩陣變換到世界坐標。Vector3 direction = (nextPosition - position).normalized;,一般將指向方向向量為坐標系Z軸,假設y軸(0,1,0)為Up方向,那么Vector3 right = Vector3.Cross(Vector3.up, direction).normalized;Z軸與假設Y軸的叉乘得到的是X軸的方向向量(即right方向),垂直與zy平面,但是接下來求方向向量(Z軸)與X軸的叉乘,指定是我們要的Y方向,因為確定的Z和X(垂直XZ平面)。
Utils.MakeBasis具體邏輯如下:
1 /// <summary> 2 /// 通過坐標軸構建矩陣 3 /// </summary> 4 /// <param name="xAxis">x軸</param> 5 /// <param name="yAxis">y軸</param> 6 /// <param name="zAxis">z軸</param> 7 /// <returns>4x4矩陣</returns> 8 public static Matrix4x4 MakeBasis(Vector3 xAxis, Vector3 yAxis, Vector3 zAxis) 9 { 10 Matrix4x4 mat = Matrix4x4.identity; 11 12 mat.m00 = xAxis.x; 13 mat.m10 = xAxis.y; 14 mat.m20 = xAxis.z; 15 16 mat.m01 = yAxis.x; 17 mat.m11 = yAxis.y; 18 mat.m21 = yAxis.z; 19 20 mat.m02 = zAxis.x; 21 mat.m12 = zAxis.y; 22 mat.m22 = zAxis.z; 23 24 return mat; 25 }
矩陣就不解釋了,自己看吧。Matrix4x4 m = Utils.MakeBasis(right, up, direction);這是在得到所構建節點的坐標系即坐標走向。因為要把構建的圓放到該點坐標下。m.MultiplyPoint3x4是將構建圓的頂點轉換到當前點坐標系下,new Vector3(Mathf.Cos(rad) * radius, Mathf.Sin(rad) * radius, 0.0f)圓上點坐標XY平面構建元,Z為0,到這里圓心始終是(0,0,0),那么將這個圓放到目標點下需要再加上目標點在當下坐標系的偏移量(即坐標值)。所以加currentPos。
到這就將兩種設置頂點坐標方式說完了。還有就是拐角處圓的構建,需要用貝塞爾曲線差值出來。
代碼:
1 /// <summary> 2 /// 設置中間點頂點數據 3 /// </summary> 4 /// <param name="currentPos"></param> 5 /// <param name="prevPos"></param> 6 /// <param name="nextPos"></param> 7 private void SetPrepareVertexData(Vector3 currentPos, Vector3 prevPos, Vector3 nextPos) 8 { 9 // 首先計算導線點相對場景中心點的偏移坐標 10 Vector3 prevPosition = prevPos - center; 11 Vector3 position = currentPos - center; 12 Vector3 anitherposition = nextPos - center; 13 14 // 計算前一段巷道的direction、right、up向量(注:Unity3d中使用左手坐標系) 15 Vector3 prevDirection = (position - prevPosition).normalized; 16 Vector3 prevRight = Vector3.Cross(Vector3.up, prevDirection).normalized; 17 Vector3 prevUp = Vector3.Cross(prevDirection, prevRight).normalized; 18 19 // 計算后一段巷道的direction、right、up向量(注:Unity3d中使用左手坐標系) 20 Vector3 anitherDirection = (anitherposition - position).normalized; 21 Vector3 anitherlRight = Vector3.Cross(Vector3.up, anitherDirection).normalized; 22 Vector3 anitherUp = Vector3.Cross(anitherDirection, anitherlRight).normalized; 23 24 float angle = Vector3.Angle(-prevDirection, anitherDirection); 25 26 if (angle >= 179.0) 27 { 28 //生成斷面數據不倒角處理 29 uvPosU += (GetPointDistance(position, prevPosition) / textureSizeL); 30 SetSectionVertexData(position, prevDirection, prevUp, prevRight, uvPosU); 31 return; 32 } 33 //倒角處理 34 35 //前后兩段風筒長度 36 float PrevLength = Vector3.Distance(position, prevPosition); 37 float anithorLength = Vector3.Distance(position, anitherposition); 38 39 indentationValue = PrevLength > anithorLength ? (anithorLength * 0.25f) : (PrevLength * 0.25f);//縮進為短風筒的1/4 40 41 // 計算縮進后的位置 42 Vector3 prevEnd = position - prevDirection * indentationValue;//左 43 Vector3 behindStart = position + anitherDirection * indentationValue;// 右 44 45 uvPosU += (GetPointDistance(prevPosition, prevEnd) / textureSizeL); 46 // 生成前段結束斷面頂點數據 47 SetSectionVertexData(prevEnd, prevDirection, prevUp, prevRight, uvPosU); 48 49 // 生成中間倒角斷面頂點數據 50 //插值0-1 51 float timer = 0.1f; 52 Vector3 prevpos = prevEnd; 53 for (float i = 1.0f; i <= 9.0f; i++) 54 { 55 Vector3 pos = Utils.CalculateCubicBezierPoint(timer, prevEnd, position, behindStart); 56 // 計算斷面方向 57 Vector3 direction = (pos - prevpos).normalized; 58 Vector3 right = Vector3.Cross(Vector3.up, direction).normalized; 59 Vector3 up = Vector3.Cross(direction, right).normalized; 60 uvPosU += (GetPointDistance(pos, prevpos) / textureSizeL); 61 // 生成斷面頂點數據 62 SetSectionVertexData(pos, direction, up, right, uvPosU); 63 //遞增插值時間 64 timer += 0.1f; 65 //更新前一個點坐標 66 prevpos = pos; 67 } 68 // 生成后段起始斷面頂點數據 69 SetSectionVertexData(behindStart, anitherDirection, anitherUp, anitherlRight, ++uvPosU); 70 }
1到9插10個圓。需要找拐角處前后兩個點分別為開始和結束插值點,Utils.CalculateCubicBezierPoint(timer, prevEnd, position, behindStart);timer為每次插值(可理解為時刻)。此函數(三階貝塞爾曲線)具體邏輯:
1 /// <summary> 2 /// 計算三階貝塞爾曲線點坐標 3 /// </summary> 4 /// <param name="t">時刻(0.0~1.0)</param> 5 /// <param name="p0">起點坐標</param> 6 /// <param name="p1">中間點坐標</param> 7 /// <param name="p2">終點坐標</param> 8 /// <returns>坐標點</returns> 9 public static Vector3 CalculateCubicBezierPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2) 10 { 11 float u = 1.0f - t; 12 float tt = t * t; 13 float uu = u * u; 14 15 return (p0 * uu + p1 * 2 * u * t + p2 * tt); 16 }
這樣准備頂點坐標就結束了。
2.uv坐標
UV坐標是與頂點一一對應的,所以在創建頂點時最好一起計算出來,我這里的要求是,上下面貼想同紋理,而且是重復帖,所以V向基本就貼1次上下兩面貼,就要從圓上10個點分成兩半,或者按角度角度分,一般從0-1后一半從1-0這樣就可以實現,重點是U方向坐標值的計算,每個橫向重復帖很多次紋理,需要根據貼圖比例以及長度去設置,但是U的值應該是累加的。一次從起點到終點貼。知道這一點就好辦了。
代碼:
//橫向代碼,U是累加的,根據圓柱長度去設置計算。 uvPosU += (GetPointDistance(pos, prevpos) / textureSizeL); //V方向按上述規律貼 /// <summary> /// 設置斷面頂點數據 /// </summary> /// <param name="currentPos">當前點坐標</param> /// <param name="direction">朝向</param> private void SetSectionVertexData(Vector3 currentPos, Vector3 direction, Vector3 up, Vector3 right, float u) { // 構建旋轉矩陣 Matrix4x4 m = Utils.MakeBasis(right, up, direction); int baseValue = 2; for (float i = 0f; i < 360.0f; i += 36.0f) { // 計算頂點 float rad = Mathf.Deg2Rad * i; Vector3 targetPos = currentPos + m.MultiplyPoint3x4(new Vector3(Mathf.Cos(rad) * radius, Mathf.Sin(rad) * radius, 0.0f)); // 計算V坐標 float v = ((baseValue * 36.0f) / 360.0f); // 保存頂點坐標&紋理坐標 vertices.Add(targetPos); uvs.Add(new Vector2(u, v)); if (i > 108) { baseValue -= 2; } else { baseValue += 2; } } }
這float v = ((baseValue * 36.0f) / 360.0f);算法需要按要求設置。
3.構建索引
構建索引就要按照定點順序去構建,連接圓與圓之間的曲面,索引值也是累加的比如這里十個點,第一個圓是0-9/第二個10-19/以此類推。
1 /// <summary> 2 /// 設置索引數據 3 /// </summary> 4 private void SetIndexData() 5 { 6 int faceCount = (vertices.Count / devide) - 1; 7 int baseValue; 8 for (int i = 0; i < faceCount; i++) 9 { 10 baseValue = 0; 11 for (int j = 1; j <= 10; j++) 12 { 13 if (j < 10) 14 { 15 triangles.Add(i * 10 + baseValue); 16 triangles.Add(i * 10 + 11 + baseValue); 17 triangles.Add(i * 10 + baseValue + 10); 18 19 triangles.Add(i * 10 + baseValue); 20 triangles.Add(i * 10 + baseValue + 1); 21 triangles.Add(i * 10 + 11 + baseValue); 22 } 23 else 24 { 25 triangles.Add(i * 10 + baseValue); 26 triangles.Add(i * 10 + 10); 27 triangles.Add(i * 10 + baseValue + 10); 28 29 triangles.Add(i * 10 + baseValue); 30 triangles.Add(i * 10); 31 triangles.Add(i * 10 + 10); 32 } 33 baseValue++; 34 } 35 } 36 }
注意構建順序不然順逆時針容易反了。
4.構建實體
這里解釋最后一步了,也是最簡單的一步了,不解釋上代碼:
1 /// <summary> 2 /// 繪制風筒實體 3 /// </summary> 4 /// <param name="posList">風筒節點數據集合</param> 5 /// <param name="radius">風筒半徑</param> 6 public void DrawAirDuct(List<Vector3> posList, float radius) 7 { 8 this.radius = radius; 9 PrepareVertexData(posList); 10 SetIndexData(); 11 Mesh mesh = new Mesh 12 { 13 vertices = vertices.ToArray(), 14 triangles = triangles.ToArray(), 15 uv = uvs.ToArray(), 16 }; 17 mesh.RecalculateNormals(); 18 GameObject airDuctObj = new GameObject(name); 19 airDuctObj.AddComponent<MeshFilter>().mesh = mesh; 20 airDuctObj.AddComponent<MeshRenderer>().material = new Material(Resources.Load<Material>("Materials/Mine/LanewayColor")); 21 22 // 添加碰撞器MeshCollider 23 airDuctObj.AddComponent<MeshCollider>(); 24 }
切記別忘了重新計算法線mesh.RecalculateNormals();這才出預期效果。
完整代碼:

1 using System; 2 using System.Collections.Generic; 3 using UnityEngine; 4 5 namespace Tx3d.Framework 6 { 7 /// <summary> 8 /// 繪制風筒實體 9 /// </summary> 10 public class AirDuct : Entity 11 { 12 #region Fields 13 14 //計算圓角的縮進值 15 private float indentationValue; 16 17 //圓划分為多少等份 18 private int devide = 10; 19 20 //中心點坐標 21 private Vector3 center = Vector3.zero; 22 23 //頂點坐標集合 24 private List<Vector3> vertices = new List<Vector3>(); 25 26 //uv坐標集合 27 private List<Vector2> uvs = new List<Vector2>(); 28 29 //索引集合 30 private List<int> triangles = new List<int>(); 31 32 //半徑 33 private float radius; 34 35 //貼圖長度縮放尺寸 36 private float textureSizeL = 10.24f; 37 38 //uv坐標的u坐標值 39 private float uvPosU = 0; 40 41 #endregion 42 43 #region Public Methods 44 45 /// <summary> 46 /// 風筒實體 47 /// </summary> 48 /// <param name="name">實體名</param> 49 public AirDuct(string name) : base(name) 50 { 51 52 } 53 54 /// <summary> 55 /// 繪制風筒實體 56 /// </summary> 57 /// <param name="posList">風筒節點數據集合</param> 58 /// <param name="radius">風筒半徑</param> 59 public void DrawAirDuct(List<Vector3> posList, float radius) 60 { 61 this.radius = radius; 62 PrepareVertexData(posList); 63 SetIndexData(); 64 Mesh mesh = new Mesh 65 { 66 vertices = vertices.ToArray(), 67 triangles = triangles.ToArray(), 68 uv = uvs.ToArray(), 69 }; 70 mesh.RecalculateNormals(); 71 GameObject airDuctObj = new GameObject(name); 72 airDuctObj.AddComponent<MeshFilter>().mesh = mesh; 73 airDuctObj.AddComponent<MeshRenderer>().material = new Material(Resources.Load<Material>("Materials/Mine/LanewayColor")); 74 75 // 添加碰撞器MeshCollider 76 airDuctObj.AddComponent<MeshCollider>(); 77 } 78 79 /// <summary> 80 /// 釋放風筒實體 81 /// </summary> 82 public override void Dispose() 83 { 84 85 } 86 87 /// <summary> 88 /// 射線查詢 89 /// </summary> 90 /// <param name="ray">射線</param> 91 /// <param name="hit">拾取結果</param> 92 /// <param name="maxDistance">最大拾取距離</param> 93 /// <returns>拾取成功返回true,否則返回false</returns> 94 public override bool Raycast(Ray ray, out RaycastHit hit, float maxDistance) 95 { 96 var collider = gameObject.GetComponent<MeshCollider>(); 97 return collider.Raycast(ray, out hit, maxDistance); 98 } 99 100 /// <summary> 101 /// 體積查詢 <see cref="Entity.VolumeQuery(PlaneBoundedVolume)"/> 102 /// </summary> 103 /// <param name="volume">查詢體</param> 104 /// <returns>查詢成功返回true,否則返回false</returns> 105 public override bool VolumeQuery(PlaneBoundedVolume volume) 106 { 107 throw new NotImplementedException(); 108 } 109 110 #endregion 111 112 #region Private Methods 113 114 /// <summary> 115 /// 准備頂點數據 116 /// </summary> 117 private void PrepareVertexData(List<Vector3> posList) 118 { 119 for (int i = 0; i < posList.Count; i++) 120 { 121 //起點圓面 122 if (i == 0) 123 { 124 SetStartVertexData(posList[i], posList[i + 1]); 125 } 126 else if (i != posList.Count - 1) 127 { 128 ////中間點(求縮進點設置圓角) 129 SetPrepareVertexData(posList[i], posList[i - 1], posList[i + 1]); 130 } 131 else 132 { 133 //終點圓面 134 SetEndVertexData(posList[i], posList[i - 1]); 135 } 136 } 137 } 138 139 /// <summary> 140 /// 設置起點圓頂點數據 141 /// </summary> 142 /// <param name="startPos">起點坐標</param> 143 /// <param name="nextPos">下一點坐標</param> 144 private void SetStartVertexData(Vector3 startPos, Vector3 nextPos) 145 { 146 // 首先計算導線點相對場景中心點的偏移坐標 147 Vector3 position = startPos - center; 148 Vector3 nextPosition = nextPos - center; 149 150 // 計算direction、right、up向量(注:Unity3d中使用左手坐標系) 151 Vector3 direction = (nextPosition - position).normalized;//z軸 152 Vector3 right = Vector3.Cross(Vector3.up, direction).normalized;//x軸,右 153 Vector3 up = Vector3.Cross(direction, right).normalized;//x和z得到up 154 // 設起點圓定點數據 155 SetSectionVertexData(position, direction, up, right, uvPosU); 156 } 157 158 /// <summary> 159 /// 設置終點圓頂點數據 160 /// </summary> 161 /// <param name="endPos">終點坐標</param> 162 /// <param name="previousPos">上一點坐標</param> 163 private void SetEndVertexData(Vector3 endPos, Vector3 previousPos) 164 { 165 // 首先計算導線點相對場景中心點的偏移坐標 166 Vector3 position = endPos - center; 167 Vector3 PreviousPosition = previousPos - center; 168 169 // 計算direction、right、up向量(注:Unity3d中使用左手坐標系) 170 Vector3 direction = (position - PreviousPosition).normalized;//指向下一點(結束)方向向量 171 Vector3 right = Vector3.Cross(Vector3.up, direction).normalized; 172 Vector3 up = Vector3.Cross(direction, right).normalized; 173 //計算U值 174 uvPosU += (GetPointDistance(position, PreviousPosition) / textureSizeL); 175 SetSectionVertexData(position, direction, up, right, uvPosU); 176 } 177 178 /// <summary> 179 /// 設置斷面頂點數據 180 /// </summary> 181 /// <param name="currentPos">當前點坐標</param> 182 /// <param name="direction">朝向</param> 183 private void SetSectionVertexData(Vector3 currentPos, Vector3 direction, Vector3 up, Vector3 right, float u) 184 { 185 // 構建旋轉矩陣 186 Matrix4x4 m = Utils.MakeBasis(right, up, direction); 187 int baseValue = 2; 188 for (float i = 0f; i < 360.0f; i += 36.0f) 189 { 190 // 計算頂點 191 float rad = Mathf.Deg2Rad * i; 192 Vector3 targetPos = currentPos + m.MultiplyPoint3x4(new Vector3(Mathf.Cos(rad) * radius, Mathf.Sin(rad) * radius, 0.0f)); 193 // 計算V坐標 194 float v = ((baseValue * 36.0f) / 360.0f); 195 // 保存頂點坐標&紋理坐標 196 vertices.Add(targetPos); 197 uvs.Add(new Vector2(u, v)); 198 if (i > 108) 199 { 200 baseValue -= 2; 201 } 202 else 203 { 204 baseValue += 2; 205 } 206 } 207 } 208 209 /// <summary> 210 /// 設置中間點頂點數據 211 /// </summary> 212 /// <param name="currentPos"></param> 213 /// <param name="prevPos"></param> 214 /// <param name="nextPos"></param> 215 private void SetPrepareVertexData(Vector3 currentPos, Vector3 prevPos, Vector3 nextPos) 216 { 217 // 首先計算導線點相對場景中心點的偏移坐標 218 Vector3 prevPosition = prevPos - center; 219 Vector3 position = currentPos - center; 220 Vector3 anitherposition = nextPos - center; 221 222 // 計算前一段巷道的direction、right、up向量(注:Unity3d中使用左手坐標系) 223 Vector3 prevDirection = (position - prevPosition).normalized; 224 Vector3 prevRight = Vector3.Cross(Vector3.up, prevDirection).normalized; 225 Vector3 prevUp = Vector3.Cross(prevDirection, prevRight).normalized; 226 227 // 計算后一段巷道的direction、right、up向量(注:Unity3d中使用左手坐標系) 228 Vector3 anitherDirection = (anitherposition - position).normalized; 229 Vector3 anitherlRight = Vector3.Cross(Vector3.up, anitherDirection).normalized; 230 Vector3 anitherUp = Vector3.Cross(anitherDirection, anitherlRight).normalized; 231 232 float angle = Vector3.Angle(-prevDirection, anitherDirection); 233 234 if (angle >= 179.0) 235 { 236 //生成斷面數據不倒角處理 237 uvPosU += (GetPointDistance(position, prevPosition) / textureSizeL); 238 SetSectionVertexData(position, prevDirection, prevUp, prevRight, uvPosU); 239 return; 240 } 241 //倒角處理 242 243 //前后兩段風筒長度 244 float PrevLength = Vector3.Distance(position, prevPosition); 245 float anithorLength = Vector3.Distance(position, anitherposition); 246 247 indentationValue = PrevLength > anithorLength ? (anithorLength * 0.25f) : (PrevLength * 0.25f);//縮進為短風筒的1/4 248 249 // 計算縮進后的位置 250 Vector3 prevEnd = position - prevDirection * indentationValue;//左 251 Vector3 behindStart = position + anitherDirection * indentationValue;// 右 252 253 uvPosU += (GetPointDistance(prevPosition, prevEnd) / textureSizeL); 254 // 生成前段結束斷面頂點數據 255 SetSectionVertexData(prevEnd, prevDirection, prevUp, prevRight, uvPosU); 256 257 // 生成中間倒角斷面頂點數據 258 //插值0-1 259 float timer = 0.1f; 260 Vector3 prevpos = prevEnd; 261 for (float i = 1.0f; i <= 9.0f; i++) 262 { 263 Vector3 pos = Utils.CalculateCubicBezierPoint(timer, prevEnd, position, behindStart); 264 // 計算斷面方向 265 Vector3 direction = (pos - prevpos).normalized; 266 Vector3 right = Vector3.Cross(Vector3.up, direction).normalized; 267 Vector3 up = Vector3.Cross(direction, right).normalized; 268 uvPosU += (GetPointDistance(pos, prevpos) / textureSizeL); 269 // 生成斷面頂點數據 270 SetSectionVertexData(pos, direction, up, right, uvPosU); 271 //遞增插值時間 272 timer += 0.1f; 273 //更新前一個點坐標 274 prevpos = pos; 275 } 276 // 生成后段起始斷面頂點數據 277 SetSectionVertexData(behindStart, anitherDirection, anitherUp, anitherlRight, ++uvPosU); 278 } 279 280 /// <summary> 281 /// 設置索引數據 282 /// </summary> 283 private void SetIndexData() 284 { 285 int faceCount = (vertices.Count / devide) - 1; 286 int baseValue; 287 for (int i = 0; i < faceCount; i++) 288 { 289 baseValue = 0; 290 for (int j = 1; j <= 10; j++) 291 { 292 if (j < 10) 293 { 294 triangles.Add(i * 10 + baseValue); 295 triangles.Add(i * 10 + 11 + baseValue); 296 triangles.Add(i * 10 + baseValue + 10); 297 298 triangles.Add(i * 10 + baseValue); 299 triangles.Add(i * 10 + baseValue + 1); 300 triangles.Add(i * 10 + 11 + baseValue); 301 } 302 else 303 { 304 triangles.Add(i * 10 + baseValue); 305 triangles.Add(i * 10 + 10); 306 triangles.Add(i * 10 + baseValue + 10); 307 308 triangles.Add(i * 10 + baseValue); 309 triangles.Add(i * 10); 310 triangles.Add(i * 10 + 10); 311 } 312 baseValue++; 313 } 314 } 315 } 316 317 /// <summary> 318 /// 求兩點距離 319 /// </summary> 320 /// <param name="prevPos">前一點坐標</param> 321 /// <param name="nextPos">后一點坐標</param> 322 /// <returns></returns> 323 private float GetPointDistance(Vector3 prevPos, Vector3 nextPos) 324 { 325 return Vector3.Distance(prevPos, nextPos); 326 } 327 328 #endregion 329 } 330 }
這里定了五個測試點,
效果如下:
到這基本結束了,但是這期間計算思考過程還是還是有點東西的,這玩意也是會的不難難的不會,做過一次了就好辦了,這里給沒搞過的小朋友啟發一下。
渴望指正交流。
最新:
最近在扭曲問題上羈絆住了,記錄下,原因是在創建本地正交基時有特殊情況沒考慮進去,就是方向與Up方向平行時導致計算的Right無意義,所以改了下:
完整代碼:
using System.Collections.Generic; using LitJson; using UnityEngine; namespace Tx3d.Framework { /// <summary> /// 所需要的巷道節點順序(由交叉口決定) /// </summary> public enum BuildOrder { None, Positive,//s-e Reverse,//e-s } /// <summary> /// 繪制風筒實體 /// </summary> public class AirDuct : Entity { #region Fields /// <summary> /// 獲取或設置風筒起點顏色 /// </summary> public Color StartColor { get { return startColor; } set { startColor = value; if (gameObject != null) { gameObject.GetComponent<MeshRenderer>().material.SetColor("_StartColor", value); } } } /// <summary> /// 獲取或設置風筒終點顏色 /// </summary> public Color EndColor { get { return endColor; } set { endColor = value; if (gameObject != null) { gameObject.GetComponent<MeshRenderer>().material.SetColor("_EndColor", value); } } } /// <summary> /// 獲取或設置風筒是否顯示流動箭頭 /// </summary> public bool IsShowFlowArrow { get { return isShowFlowArrow; } set { isShowFlowArrow = value; if (gameObject != null) { gameObject.GetComponent<MeshRenderer>().material.SetFloat("_IsShow", value ? 1 : 0); } } } /// <summary> /// 獲取或設置風筒的流動箭頭流動速度 /// </summary> public float FlowArrowSpeed { get { return flowArrowSpeed; } set { flowArrowSpeed = value; if (gameObject != null) { gameObject.GetComponent<MeshRenderer>().material.SetFloat("_Speed", value); } } } /// <summary> /// 獲取或設置流動箭頭方向 /// </summary> public float FlowArrowDirection { get { return direction; } set { direction = value; if (gameObject != null) { gameObject.GetComponent<MeshRenderer>().material.SetFloat("_Direction", direction); } } } /// <summary> /// 獲取風筒外包盒 /// </summary> public override Bounds Bounds { get { if (gameObject != null) { return gameObject.GetComponent<MeshCollider>().bounds; } return new Bounds(); } } /// <summary> /// 獲取&設置風筒是否高亮 /// </summary> public override bool Highlight { get { return highlight; } set { highlight = value; if (gameObject != null) { gameObject.GetComponent<MeshRenderer>().material.SetFloat("_FillAmount", highlight ? 0.25f : 0.0f); } } } /// <summary> /// 獲取風筒總長度 /// </summary> public float Length { get; private set; } /// <summary> /// 獲取風筒半徑 /// </summary> public float Radius { get; private set; } //豎直偏移量 public float VerticalOffset { get; private set; } // 風筒最終點坐標 public List<Vector3> airductPosList; //startColor private Color startColor = new Color(0.7490f, 1.0f, 0.0f, 1.0f); //endColor private Color endColor = new Color(0.7490f, 1.0f, 0.0f, 1.0f); //是否顯示流向箭頭紋理 private bool isShowFlowArrow = true; //流向箭頭流動速度 private float flowArrowSpeed = 1.0f; //是否反向紋理流向箭頭 private float direction = 1.0f; //計算圓角的縮進值 private float indentationValue; //圓划分為多少等份 //private readonly int devide = 10; //中心點坐標 private Vector3 center = Vector3.zero; //頂點坐標集合 private List<Vector3> vertices = new List<Vector3>(); //uv坐標集合 private List<Vector2> uvs = new List<Vector2>(); //索引集合 private List<int> triangles = new List<int>(); //貼圖長度縮放尺寸 private float textureSizeL = 5.12f; //uv坐標的u坐標值 private float uvPosU = 0; //風筒Mesh過濾器 private MeshFilter airDuctMeshFilter; //風筒Mesh渲染器 private MeshRenderer airDuctMeshRenderer; //風筒MeshCollider private MeshCollider airDuctMeshCollider; //本地緩存巷道信息 private List<string> lanewaysList; //本地緩存巷道序列信息 private List<BuildOrder> buildOrdersList; //基本縱向偏移量(底面到頂板) private float baseHeight = 0; //貼圖漸變集合數據 private List<Vector2> mapUVList = new List<Vector2>(); //保存比例值 private List<float> saveRatioList = new List<float>(); //風筒數據JsonData private List<JsonData> lanewayJsonDataList; //查詢風筒坐標(實際處理數據)列表 private List<Vector3> askPosList; #endregion #region Public Methods /// <summary> /// 通過名稱風筒實體 /// </summary> /// <param name="name">實體名</param> public AirDuct(string name) : base(name) { type = "AirDuct"; queryMask = QueryMask.AirDuct; } /// <summary> /// 通過名稱以及GUID風筒實體 /// </summary> /// <param name="name">實體名</param> /// <param name="guid">GUID</param> public AirDuct(string name, string guid) : base(name, guid) { type = "AirDuct"; queryMask = QueryMask.AirDuct; } /// <summary> /// 通過名稱以及數據列表風筒實體 /// </summary> /// <param name="name">實體名</param> /// <param name="guidList">巷道坐標信息列表</param> /// <param name="buildOrdersList">巷道坐標序列(順逆)</param> /// <param name="radius">風筒半徑</param> /// <param name="verticalOffset">風筒相對巷道的豎直偏移量</param> public AirDuct(string name, List<string> guidList, List<BuildOrder> buildOrdersList, float radius, float verticalOffset, float isReverse = 1.0f) : base(name) { type = "AirDuct"; queryMask = QueryMask.AirDuct; this.Radius = radius; this.VerticalOffset = verticalOffset; this.direction = isReverse; InitCopyLanewayData(guidList, buildOrdersList); } /// <summary> /// 通過名稱以及GUID和數據列表風筒實體 /// </summary> /// <param name="name">實體名</param> /// <param name="posList">巷道坐標信息列表</param> /// <param name="buildOrdersList">巷道坐標序列(順逆)</param> /// <param name="radius">風筒半徑</param> /// <param name="verticalOffset">風筒相對巷道的豎直偏移量</param> public AirDuct(string name, string guid, List<string> posList, List<BuildOrder> buildOrdersList, float radius, float verticalOffset, float isReverse = 1.0f) : base(name, guid) { type = "AirDuct"; this.guid = guid; queryMask = QueryMask.AirDuct; this.Radius = radius; this.VerticalOffset = verticalOffset; this.direction = isReverse; InitCopyLanewayData(posList, buildOrdersList); } /// <summary> /// 釋放風筒實體 /// </summary> public override void Dispose() { UnityEngine.Object.Destroy(airDuctMeshCollider); UnityEngine.Object.Destroy(airDuctMeshRenderer); UnityEngine.Object.Destroy(airDuctMeshFilter); UnityEngine.Object.Destroy(gameObject); } /// <summary> /// 生成風筒Json數據 <see cref="Entity.ToJson"/> /// </summary> /// <returns>風筒Json數據</returns> public override JsonData ToJson() { var data = new JsonData { ["guid"] = guid, ["name"] = name, ["radius"] = Radius, ["verticalOffset"] = VerticalOffset, ["startColor"] = startColor.ToJson(), ["endColor"] = endColor.ToJson(), ["direction"] = direction, ["AirDuctPos"] = new JsonData() }; foreach (var node in lanewayJsonDataList) { data["AirDuctPos"].Add(node); } return data; } /// <summary> /// 射線查詢 /// </summary> /// <param name="ray">射線</param> /// <param name="hit">拾取結果</param> /// <param name="maxDistance">最大拾取距離</param> /// <returns>拾取成功返回true,否則返回false</returns> public override bool Raycast(Ray ray, out RaycastHit hit, float maxDistance) { if (gameObject != null && gameObject.activeSelf) { var collider = gameObject.GetComponent<MeshCollider>(); return collider.Raycast(ray, out hit, maxDistance); } else { hit = new RaycastHit(); return false; } } /// <summary> /// <see cref="Entity.RectangleQuery(float, float, float, float)"/> /// </summary> /// <param name="screenLeft">框選框左側屏幕坐標</param> /// <param name="screenTop">框選框頂部屏幕坐標</param> /// <param name="screenRight">框選框右側屏幕坐標</param> /// <param name="screenBottom">框選框底部屏幕坐標</param> /// <returns>拾取成功返回true,否則返回false</returns> public override bool RectangleQuery(float screenLeft, float screenTop, float screenRight, float screenBottom) { // 判斷風筒是否已經渲染 if (gameObject == null || !gameObject.activeSelf) { return false; } // 判斷是否相交 for (var i = 0; i < airductPosList.Count - 1; i++) { var start = Utils.WorldToScreenPointProjected(CameraControllerManager.Instance.MainCamera, airductPosList[i]); var end = Utils.WorldToScreenPointProjected(CameraControllerManager.Instance.MainCamera, airductPosList[i + 1]); // 判斷起點或終點是否落在矩形框范圍內 if (start.x >= screenLeft && start.x <= screenRight && start.y >= screenBottom && start.y <= screenTop) { return true; } if (end.x >= screenLeft && end.x <= screenRight && end.y >= screenBottom && end.y <= screenTop) { return true; } // 判斷起點到終點的線段是否與矩形框相交 var s = new Vector2(start.x, start.y); var e = new Vector2(end.x, end.y); var leftTop = new Vector2(screenLeft, screenTop); var rightTop = new Vector2(screenRight, screenTop); var rightBottom = new Vector2(screenRight, screenBottom); var leftBottom = new Vector2(screenLeft, screenBottom); // 判斷是否與頂邊相交 if (Utils.IsTwoSegmentsIntersect(s, e, leftTop, rightTop)) { return true; } // 判斷是否與右邊相交 if (Utils.IsTwoSegmentsIntersect(s, e, rightTop, rightBottom)) { return true; } // 判斷是否與底邊相交 if (Utils.IsTwoSegmentsIntersect(s, e, rightBottom, leftBottom)) { return true; } // 判斷是否與左邊相交 if (Utils.IsTwoSegmentsIntersect(s, e, leftBottom, leftTop)) { return true; } } // 返回不相交 return false; } /// <summary> /// 判斷實體是否與外包盒相交<see cref="Entity.IntersectBounds(Bounds)"/> /// </summary> /// <param name="bounds">外包盒</param> /// <returns>相交返回true,否則返回false</returns> public override bool IntersectBounds(Bounds bounds) { if (gameObject.activeSelf) { return bounds.Intersects(Bounds); } return false; } /// <summary> /// 更新風筒實體 /// </summary> /// <param name="posList">更新的節點數據</param> public void UpdateAirDuct() { Dispose(); PrepareRenderData(); RenderAirDuct(); } /// <summary> /// 反轉箭頭流向 /// </summary> public void ReverseArrow() { direction = -direction; if (gameObject != null) { gameObject.GetComponent<MeshRenderer>().material.SetFloat("_Direction", direction); } } /// <summary> /// 判斷某點是否可以投影到風筒的中線上 /// </summary> /// <param name="point">檢測點坐標</param> /// <param name="projectionPoint">投影點坐標</param> /// <param name="startIndex">投影點位於的風筒段起點索引</param> /// <param name="endIndex">投影點位於的風筒段終點索引</param> /// <returns>true可以投影到風筒上,false不能投影到風筒上</returns> public bool IsPointProjectedToAirDuct(Vector3 point, out Vector3 projectionPoint, out int startIndex, out int endIndex) { // 初始化輸出參數 projectionPoint = Vector3.zero; startIndex = -1; endIndex = -1; // 標識是否投影到巷道上 bool isProjectedTo = false; // 最小投影距離 var minProjectionDistance = float.PositiveInfinity; for (int i = 0; i < askPosList.Count - 1; i++) { var startPosition = askPosList[i]; var endPosition = askPosList[i + 1]; // 計算風筒長度 var length = Vector3.Distance(endPosition, startPosition); // 風筒段是否與Y軸平行 var dir = (endPosition - startPosition).normalized; var dot = Mathf.Abs(Vector3.Dot(dir, Vector3.up)); // 計算向量&向量模長 var vector0 = point - startPosition; // 風筒段起點指向檢測點 var vector1 = endPosition - startPosition; // 風筒段起點指向終點 var length0 = vector0.magnitude; var length1 = vector1.magnitude; // 計算向量v0在向量v1的點積 dot = Vector3.Dot(vector0, vector1); // 判斷檢測點是否可以投影到巷道段上 var ratio = dot / (length1 * length1); if (ratio >= 0.0f && ratio <= 1.0f) { // 計算向量v0在向量v1上的投影長度 var projectionLength = dot / length1; // 計算向量v0到向量v1的投影距離 var projectionDistance = Mathf.Sqrt(length0 * length0 - projectionLength * projectionLength); // 判斷是否是最短距離 if (projectionDistance < minProjectionDistance) { // 標識投影到巷道上 isProjectedTo = true; // 更新最短投影距離 minProjectionDistance = projectionDistance; // 計算投影點坐標 projectionPoint = startPosition + (dir * length * ratio); // 記錄前后導線點索引 startIndex = i; endIndex = i + 1; } } } return isProjectedTo; } /// <summary> /// (最終)獲取風筒坐標節點數目 /// </summary> /// <returns>坐標節點數目</returns> public int GetAirDuctPosCount() { return askPosList.Count; } /// <summary> /// (最終)通過索引獲取風筒坐標點 /// </summary> /// <param name="index"></param> /// <returns></returns> public Vector3 GetAirDuctPos(int index) { if (index < 0 || index >= askPosList.Count) { return Vector3.zero; } return askPosList[index]; } /// <summary> /// (初始)獲取風筒原始數據(巷道數據)結點數目 /// </summary> /// <returns>結點數目</returns> public int GetAirDuctNodeCount() { return lanewaysList.Count; } /// <summary> /// (初始)獲取風筒原始數據(巷道數據)對應的巷道GUID /// </summary> /// <param name="index">索引</param> /// <returns>巷道GUID</returns> public string GetAirDuctLanwayGUID(int index) { if (index < 0 || index >= lanewaysList.Count) { return string.Empty; } return lanewaysList[index]; } /// <summary> /// (初始)獲取風筒對應的巷道走向 /// </summary> /// <param name="index">索引</param> /// <returns>構建順序</returns> public BuildOrder GetAirDuctBuildOrder(int index) { if (index < 0 || index >= buildOrdersList.Count) { return BuildOrder.None; } return buildOrdersList[index]; } #endregion #region internal Methods /// <summary> /// 准備風筒渲染數據 /// </summary> /// <param name="posList">風筒節點數據集合</param> /// <param name="radius">風筒半徑</param> internal void PrepareRenderData() { airductPosList = GetPosListData(); askPosList = new List<Vector3>(); for (int i = 0; i < airductPosList.Count; i++) { if (i != 0 || i != airductPosList.Count - 1) { askPosList.Add(airductPosList[i]); } } PrepareVertexData(airductPosList); SetIndexData(); } /// <summary> /// 渲染風筒實體 /// </summary> internal void RenderAirDuct() { gameObject = new GameObject(name); gameObject.transform.position = center; //渲染風筒 Mesh mesh = new Mesh(); mesh.SetVertices(vertices); mesh.SetTriangles(triangles.ToArray(), 0); mesh.SetUVs(0, uvs); mesh.SetUVs(1, mapUVList); mesh.RecalculateNormals(); mesh.RecalculateBounds(); airDuctMeshFilter = gameObject.AddComponent<MeshFilter>(); airDuctMeshFilter.mesh = mesh; airDuctMeshRenderer = gameObject.AddComponent<MeshRenderer>(); airDuctMeshRenderer.material = new Material(Resources.Load<Material>("Materials/Mine/LanewayColor")); airDuctMeshRenderer.material.SetColor("_StartColor", startColor); airDuctMeshRenderer.material.SetColor("_EndColor", endColor); airDuctMeshRenderer.material.SetFloat("_IsShow", isShowFlowArrow ? 1 : 0); airDuctMeshRenderer.material.SetFloat("_Speed", flowArrowSpeed); airDuctMeshRenderer.material.SetFloat("_Direction", direction); // 添加碰撞器MeshCollider airDuctMeshCollider = gameObject.AddComponent<MeshCollider>(); } #endregion #region Private Methods /// <summary> /// 准備頂點數據 /// </summary> private void PrepareVertexData(List<Vector3> posList) { // 更新風筒總長度 UpdateAirDuctLength(posList); for (int i = 0; i < posList.Count; i++) { //起點圓面 if (i == 0) { SetStartVertexData(posList[i], posList[i + 1], posList[i + 2], GetCurrPosRatioValue(i)); } else if (i != posList.Count - 1) { //中間點(求縮進點設置圓角) if (i == 1 || i == posList.Count - 2) { SetPrepareVertexData(posList[i], posList[i - 1], posList[i + 1], true, GetCurrPosRatioValue(i)); } else { SetPrepareVertexData(posList[i], posList[i - 1], posList[i + 1], false, GetCurrPosRatioValue(i)); } } else { //終點圓面 SetEndVertexData(posList[i], posList[i - 1], posList[i - 2], GetCurrPosRatioValue(i)); } } } /// <summary> /// 更新風筒總長度 /// </summary> /// <param name="airductPosList">風筒節點坐標</param> private void UpdateAirDuctLength(List<Vector3> airductPosList) { saveRatioList.Clear(); float length = 0; saveRatioList.Add(0); for (int i = 0; i < airductPosList.Count; i++) { if (i != airductPosList.Count - 1) { length += GetPointDistance(airductPosList[i], airductPosList[i + 1]); saveRatioList.Add(length); } } Length = length; } /// <summary> /// 獲取當前點在整體風筒總長度中比例分子 /// </summary> /// <param name="index">第幾個點(0開始)</param> /// <returns></returns> private float GetCurrPosRatioValue(int index) { if (index < saveRatioList.Count) { return (saveRatioList[index]); } return -1.0f; } /// <summary> /// 設置起點圓頂點數據 /// </summary> /// <param name="startPos">起點坐標</param> /// <param name="nextPos">下一點坐標</param> private void SetStartVertexData(Vector3 startPos, Vector3 nextPos, Vector3 thirdPos, float currMapValue) { // 首先計算導線點相對場景中心點的偏移坐標 Vector3 position = startPos - center; Vector3 nextPosition = nextPos - center; // 計算direction、right、up向量(注:Unity3d中使用左手坐標系) Vector3 direction = (nextPosition - position).normalized;//z軸 // Vector3 right = Vector3.Cross(Vector3.up, direction).normalized;//x軸,右 Vector3 right = Vector3.Cross((thirdPos-startPos).normalized, direction).normalized;//x軸,右 Vector3 up = Vector3.Cross(direction, -right).normalized;//x和z得到up //if (Vector3.Angle(Vector3.up, direction) <= 1.0f || Vector3.Angle(Vector3.up, direction) >= 179.0f) //{ // up = Vector3.Cross(direction, Vector3.right).normalized; // right = Vector3.Cross(up, direction).normalized; //} GetPointDistance(startPos, nextPos); // 設起點圓定點數據 SetSectionVertexData(position, direction, up, -right, uvPosU, currMapValue); } /// <summary> /// 設置終點圓頂點數據 /// </summary> /// <param name="endPos">終點坐標</param> /// <param name="previousPos">上一點坐標</param> private void SetEndVertexData(Vector3 endPos, Vector3 previousPos, Vector3 thirdPos, float currRatio) { // 首先計算導線點相對場景中心點的偏移坐標 Vector3 position = endPos - center; Vector3 PreviousPosition = previousPos - center; // 計算direction、right、up向量(注:Unity3d中使用左手坐標系) Vector3 direction = (position - PreviousPosition).normalized;//指向下一點(結束)方向向量 Vector3 right = Vector3.Cross((endPos-thirdPos).normalized, direction).normalized; Vector3 up = Vector3.Cross(direction, right).normalized; //if (Vector3.Angle(Vector3.up, direction) <= 1.0f || Vector3.Angle(Vector3.up, direction) >= 179.0f) //{ // up = Vector3.Cross(direction, Vector3.right).normalized; // right = Vector3.Cross(up, direction).normalized; //} //計算U值 uvPosU += (GetPointDistance(position, PreviousPosition) / textureSizeL); SetSectionVertexData(position, direction, up, right, uvPosU, currRatio); } /// <summary> /// 設置斷面頂點數據 /// </summary> /// <param name="currentPos">當前點坐標</param> /// <param name="direction">朝向</param> /// <param name="up">向上方向</param> /// <param name="right">向右向</param> /// <param name="u">UV坐標U值</param> private void SetSectionVertexData(Vector3 currentPos, Vector3 direction, Vector3 up, Vector3 right, float u, float currRatio) { // 構建旋轉矩陣 Matrix4x4 m = Utils.MakeBasis(right, up, direction); int baseValue = 0; for (float i = 0f; i < 360.0f; i += 36.0f) { //計算頂點 float rad = Mathf.Deg2Rad * i; Vector3 targetPos = currentPos + m.MultiplyPoint3x4(new Vector3(Mathf.Cos(rad) * Radius, Mathf.Sin(rad) * Radius, 0.0f)); //計算V坐標 float v = ((baseValue * 36.0f) / 360.0f); //保存頂點坐標 & 紋理坐標 vertices.Add(targetPos); uvs.Add(new Vector2(u, v)); mapUVList.Add(new Vector2(currRatio, Length)); if (i >= 180) { baseValue -= 2; } else { baseValue += 2; } } } /// <summary> /// 設置中間點頂點數據 /// </summary> /// <param name="currentPos">當前點坐標</param> /// <param name="prevPos">上一點坐標</param> /// <param name="nextPos">下一點坐標</param> /// <param name="isPoint">中間點是否是起始兩端水平端點</param> private void SetPrepareVertexData(Vector3 currentPos, Vector3 prevPos, Vector3 nextPos, bool isPoint, float currRatio) { // 首先計算導線點相對場景中心點的偏移坐標 Vector3 prevPosition = prevPos - center; Vector3 position = currentPos - center; Vector3 anitherposition = nextPos - center; // 計算前一段巷道的direction、right、up向量(注:Unity3d中使用左手坐標系) Vector3 prevDirection = (position - prevPosition).normalized; Vector3 prevRight = Vector3.Cross(Vector3.up, prevDirection).normalized; Vector3 prevUp = Vector3.Cross(prevDirection, prevRight).normalized; // 計算后一段巷道的direction、right、up向量(注:Unity3d中使用左手坐標系) Vector3 anitherDirection = (anitherposition - position).normalized; Vector3 anitherlRight = Vector3.Cross(Vector3.up, anitherDirection).normalized; Vector3 anitherUp = Vector3.Cross(anitherDirection, anitherlRight).normalized; float angle = Vector3.Angle(-prevDirection, anitherDirection); // 如果前后兩端風筒的夾角大於等於179度則不進行倒角處理 if (angle >= 179.0 || angle <= 1) { //生成斷面數據不倒角處理 uvPosU += (GetPointDistance(position, prevPosition) / textureSizeL); SetSectionVertexData(position, prevDirection, prevUp, prevRight, uvPosU, currRatio); return; } // 計算前后段巷道長度 float prevLength = Vector3.Distance(position, prevPosition); float follLength = Vector3.Distance(position, anitherposition); indentationValue = 2 * Radius; //平分向量 Vector3 centerRight = (-prevDirection + anitherDirection).normalized; //圓角所在圓弧半徑 var centerRadius = Mathf.Tan(Mathf.Deg2Rad * angle * 0.5f) * indentationValue; //圓角所在圓圓心點 var centerPoint = position + centerRight * (centerRadius / (Mathf.Sin(Mathf.Deg2Rad * angle * 0.5f))); //縮進前后坐標 Vector3 prevPointEnd = position - prevDirection * indentationValue;//左 Vector3 behindPointStart = position + anitherDirection * indentationValue;// 右 Vector3 lastPoint = prevPosition; Vector3 v0 = prevPointEnd - centerPoint; Vector3 v1 = behindPointStart - centerPoint; float t1 = 0.1f; for (float t = 0.0f; t <= 1.0f; t+=0.1f) { Vector3 point = Vector3.Slerp(v0, v1, t) + centerPoint; Vector3 point1 = Vector3.Slerp(v0, v1, t1) + centerPoint; // 計算斷面方向 Vector3 direction = (point - lastPoint).normalized; Vector3 right = Vector3.zero; Vector3 up = Vector3.zero; if (isPoint) { if (t == 1.0f) { point1 = anitherposition; } right = Vector3.Cross((point-point1).normalized, direction).normalized; up = Vector3.Cross(direction, right).normalized; } else { right = Vector3.Cross(Vector3.up, direction).normalized; up = Vector3.Cross(direction, right).normalized; if (Vector3.Angle(Vector3.up, direction) <= 1.0f || Vector3.Angle(Vector3.up, direction) >= 179.0f) { up = Vector3.Cross(direction, Vector3.right).normalized; right = Vector3.Cross(up, direction).normalized; } } uvPosU += (GetPointDistance(point, lastPoint) / textureSizeL); // 生成斷面頂點數據 SetSectionVertexData(point, direction, up, right, uvPosU, currRatio); t1 += 0.1f; //更新前一個點坐標 lastPoint = point; } } /// <summary> /// 設置索引數據 /// </summary> private void SetIndexData() { var length = (vertices.Count / 10) - 1; for (int i = 0; i < length; i++) { for (int j = 0; j < 10; j++) { if (j < 9) { triangles.Add(i * 10 + (j + 1)); triangles.Add((i + 1) * 10 + j); triangles.Add(i * 10 + j); triangles.Add((i + 1) * 10 + (j + 1)); triangles.Add((i + 1) * 10 + j); triangles.Add(i * 10 + (j + 1)); } else { triangles.Add(i * 10); triangles.Add((i + 1) * 10 + j); triangles.Add(i * 10 + j); triangles.Add((i + 1) * 10); triangles.Add((i + 1) * 10 + j); triangles.Add(i * 10); } } } } /// <summary> /// 求兩點距離 /// </summary> /// <param name="prevPos">前一點坐標</param> /// <param name="nextPos">后一點坐標</param> /// <returns></returns> private float GetPointDistance(Vector3 prevPos, Vector3 nextPos) { return Vector3.Distance(prevPos, nextPos); } #region ReadyPosInfoWork /// <summary> /// 通過巷道信息獲取風筒節點坐標 /// </summary> private List<Vector3> GetPosListData() { List<Laneway.LeadNode> tempNodeList = new List<Laneway.LeadNode>(); List<Vector3> posList = new List<Vector3>(); //排重(順不要尾逆不要頭(最后一條巷道保留)) for (int i = 0; i < lanewaysList.Count; i++) { int length = ((Laneway)EntityManager.Instance.GetEntity(lanewaysList[i])).GetLeadNodeCount(); switch (buildOrdersList[i]) { case BuildOrder.Positive: for (int j = 0; j < length; j++) { if (i != lanewaysList.Count - 1) { if (j != length - 1) { tempNodeList.Add(GetLanewayNodePos(lanewaysList[i], j)); } } else { tempNodeList.Add(GetLanewayNodePos(lanewaysList[i], j)); } } break; case BuildOrder.Reverse: for (int j = length - 1; j >= 0; j--) { if (i != lanewaysList.Count - 1) { if (j != 0) { tempNodeList.Add(GetLanewayNodePos(lanewaysList[i], j)); } } else { tempNodeList.Add(GetLanewayNodePos(lanewaysList[i], j)); } } break; default: break; } } //設置風筒具體坐標 for (int i = 0; i < tempNodeList.Count; i++) { if (i == 0)//起點 { posList.Add(CalculateStartEndPos(tempNodeList[i], tempNodeList[i + 1], true)); } else if (i == (tempNodeList.Count - 1))//終點 { posList.Add(CalculateStartEndPos(tempNodeList[i], tempNodeList[i - 1], false)); } else//中間點 { posList.Add(CalculatePos(tempNodeList[i], tempNodeList[i - 1])); } } //插入開始結束兩端的水平端點 Vector3 direction = (tempNodeList[1].RelativePosition - tempNodeList[0].RelativePosition).normalized; Vector3 right = Vector3.Cross(Vector3.up, direction).normalized; Vector3 up = Vector3.Cross(direction, right).normalized; posList.Insert(1, tempNodeList[0].RelativePosition + up * (baseHeight + VerticalOffset)); Vector3 direction1 = (tempNodeList[tempNodeList.Count - 1].RelativePosition - tempNodeList[tempNodeList.Count - 2].RelativePosition).normalized; Vector3 right1 = Vector3.Cross(Vector3.up, direction1).normalized; Vector3 up1 = Vector3.Cross(direction1, right1).normalized; posList.Insert(posList.Count - 1, tempNodeList[tempNodeList.Count - 1].RelativePosition + up1 * (baseHeight + VerticalOffset)); return posList; } /// <summary> /// 計算風筒中間點坐標 /// </summary> /// <param name="node">當前巷道導線點數據</param> /// <param name="prepNode">上一巷道導線點數據</param> /// <returns></returns> private Vector3 CalculatePos(Laneway.LeadNode node, Laneway.LeadNode prepNode) { Vector3 direction = (node.RelativePosition - prepNode.RelativePosition).normalized; Vector3 right = Vector3.Cross(Vector3.up, direction).normalized; Vector3 up = Vector3.Cross(direction, right).normalized; Vector3 vector3 = node.RelativePosition; vector3 += up * (baseHeight + VerticalOffset); return vector3; } /// <summary> /// 計算風筒兩端點坐標 /// </summary> /// <param name="node">當前巷道導線點數據</param> /// <param name="anotherNode">下一巷道導線點數據</param> /// <param name="isStart">是否是開始端點</param> /// <returns></returns> private Vector3 CalculateStartEndPos(Laneway.LeadNode node, Laneway.LeadNode anotherNode, bool isStart) { float width; if (!node.UseSecondWidth)//第一套 { width = (node.LeftWidth + node.RightWidth); } else//第二套 { width = (node.LeftWidth2 + node.RightWidth2); } float radius = Mathf.Sqrt(Mathf.Pow((0.50f * width), 2) + Mathf.Pow((0.50f * width), 2)); float targetHight = (radius - (0.50f * width)) + node.Height; baseHeight = targetHight; Vector3 direction = Vector3.zero; if (isStart) { direction = (anotherNode.RelativePosition - node.RelativePosition).normalized; } else { direction = (node.RelativePosition - anotherNode.RelativePosition).normalized; } Vector3 right = Vector3.Cross(Vector3.up, direction).normalized; Vector3 up = Vector3.Cross(direction, right).normalized; Vector3 CurPos = node.RelativePosition; CurPos += up * targetHight; return CurPos; } /// <summary> /// 初始化Copy巷道數據 /// </summary> /// <param name="lanewaysList">巷道信息集合</param> /// <param name="buildOrdersList">巷道坐標序列(順逆)</param> private void InitCopyLanewayData(List<string> lanewaysList, List<BuildOrder> buildOrdersList) { lanewayJsonDataList = new List<JsonData>(); this.lanewaysList = new List<string>(); this.buildOrdersList = new List<BuildOrder>(); for (int i = 0; i < lanewaysList.Count; i++) { if (EntityManager.Instance.GetEntity(lanewaysList[i]) != null) { this.lanewaysList.Add(lanewaysList[i]); this.buildOrdersList.Add(buildOrdersList[i]); lanewayJsonDataList.Add(GetAirDuctJsonData(lanewaysList[i], buildOrdersList[i])); } } } /// <summary> /// 根據guid和節點索引獲取節點坐標 /// </summary> /// <param name="guid"></param> /// <param name="index"></param> /// <returns></returns> private Laneway.LeadNode GetLanewayNodePos(string guid, int index) { if (EntityManager.Instance.GetEntity(guid) != null) { return ((Laneway)EntityManager.Instance.GetEntity(guid)).GetLeadNode(index); } return null; } /// <summary> /// 生成風筒數據jsonData /// </summary> /// <param name="guid">GUID</param> /// <param name="buildOrder">順序</param> /// <returns></returns> private JsonData GetAirDuctJsonData(string guid, BuildOrder buildOrder) { return new JsonData { ["guid"] = guid, ["buildOrder"] = (int)buildOrder, }; } /// <summary> /// 校驗風筒基本數據坐標 /// </summary> /// <returns>減去中心坐標</returns> private List<Vector3> CorrectPos(List<Vector3> list) { List<Vector3> posList = new List<Vector3>(); Vector3 targetPos = Vector3.zero; for (int i = 0; i < list.Count; i++) { targetPos += list[i]; } Vector3 centerPos = (targetPos / (list.Count)); for (int i = 0; i < list.Count; i++) { posList.Add((list[i] - centerPos)); } return posList; } #endregion #endregion } }