移植UE4的模型操作到Unity中


  最近在Unity上要寫一個東東,功能差不多就是在Unity編輯器上的旋轉,移動這些,在手機上也能比較容易操作最好,原來用Axiom3D寫過一個類似的,有許多位置並不好用,剛好在研究UE4的源碼,在模型操作上,很多位置都解決了,其實大家可以對比下,在UE4與Unity中,UE4的如移動和旋轉都要正確和好用。

  如下是根據UE4中簡單移植過來的效果圖,差不多已經夠用,UE4相關源碼主要在EditorViewportClient與UnrealWidget。

  

  介紹一下這個組件主要功能。

  1. 模型本地空間與世界空間二種模式。

  2. 根據情況動態生成操作模型,如在移動模型時,選擇的軸變色,旋轉時,視角與模型的方向產生不同模型。

  3. 移動根據鼠標平面映射到對應移動平面,點保存上軸上距離不變(作為對比,可以看到Unity上長距離移動,鼠標位置在軸上的位置位移會不斷拉大)。

  4. 不管模型與攝像機的距離,旋轉與移動操作都是合適的大小。

  5. 旋轉方向的確定,簡單說,就是在旋轉時,如果用鼠標移動來確定旋轉的方向,這個問題看似簡單,我以前就沒搞出來。

  6. 在移動本台,我們需要更方便的操作,所以在移動平台會有些操作,如更容易選中,生成的模型會更大等。

  最后,有一些,如箭頭模型,選擇旋轉與移動軸的算法以前考慮過,就沒用UE4本身的,如果感覺有問題,自己去移植UE4的。其中旋轉因為移動平台易用性,就設定了一個值,如在我這設定的是10,就是每次只旋轉10度。

  簡單分析一下,UE4里相關思路。

  其中移動的算法思路非常贊,比如我們要移動X軸,那么我們對應在法線為Y或是法線為Z軸上的平面都可以,通過攝像機的方向與這二個平面的夾角,在這如果攝像機的方向與法線Y平面的角大於與法線Z平面的角,那么我們選擇法線Y平面的面做映射面,而Z向量作偏向軸方向,什么意思了,我們鼠標是在二維面上移動的,但是對應的只在X軸上移動,那么我們在法線Y平面上的映射向量需要去掉在Z向量上偏向量的影響。如下是移動的主要代碼,每步我加了注釋,其中一些比較常用如投影,向量減向量在某向量上的投影的意義要記清,當初我也是看到這,就一下想通這個算法的思路了。  

    /// <summary>
/// FWidget::GetAbsoluteTranslationDelta
/// 算法思想,如果移動X軸,選取以Y軸或是Z軸為法線並過模型上的面,鼠標移動映射在這個面上。
/// 其中,如果選擇Y軸面,要去掉Z軸上運動值,參看NormalToRemove
/// </summary>
/// <param name="inParams"></param>
/// <returns></returns>
public Vector3 GetAbsoluteTranslationDelta(AbsoluteMovementParams inParams)
{
//鼠標移動的位置 對應的面,請看GetAxisPlaneNormalAndMask方法
Plane movementPlane = new Plane(inParams.PlaneNormal, inParams.Position);
//估算鼠標點擊在模型上的位置(點擊射線方向)
Vector3 eyeVector = inParams.EyePos + inParams.PixelDir * (inParams.Position - inParams.EyePos).magnitude;
//模型的世界位置
Vector3 requestedPositon = inParams.Position;

//點擊方向與面的夾角
float dotPlaneNormal = Vector3.Dot(inParams.PixelDir, inParams.PlaneNormal);
//攝像機方向與面的夾角不為90度
if (Mathf.Abs(dotPlaneNormal) > 0.00001)
{
//攝像機到點擊位置 與 面的交點 ,把requestedPositon映射到面上位置
requestedPositon = LinePlaneIntersection(inParams.EyePos, eyeVector, movementPlane);
}
//拖動的增量(都在movementPlane上,二點相差)
var deltaPosition = requestedPositon - inParams.Position;
//保存最開始點擊下去得到的偏移
Vector3 offset = GetAbsoluteTranslationInitialOffset(requestedPositon, inParams.Position);
//去掉最開始本身的偏移
deltaPosition -= initialOffset;
//.Log("delta:" + deltaPosition);
//去掉deltaPosition到NormalToRemove上投影 outDrag與NormalToRemove 互相垂直,outDrag+NormalToRemove = deltaPosition
float movementAxis = Vector3.Dot(deltaPosition, inParams.NormalToRemove);
Vector3 outDrag
= deltaPosition - inParams.NormalToRemove * movementAxis;
//Debug.Log("outDrag:" + outDrag);
//get the distance from the original position to the new proposed position
//Vector3 deltaFromStart = inParams.Position + outDrag - initialPosition;
//模型到攝像機方向
Vector3 eyeToNewPosition = inParams.Position + outDrag - inParams.EyePos;
//模型到攝像機方向與攝像機方向 夾角大於90度
float behindDot = Vector3.Dot(eyeToNewPosition, inParams.CameraDir);
if (behindDot <= 0)
{
outDrag
= Vector3.zero;
}
return outDrag;
}
FWidget::GetAbsoluteTranslationDelta

  移動的算法差不多就是這樣,其中如何生成移動模型就不拉出來,后面會給出源代碼,大家自己去找。至於如何找到移動模型對應的X,Y,Z軸,或是全部移動,算法以前寫過,求得二射線相隔最近的二點,然后根據二點的長度判斷是否認為相交,在代碼文件上的GetAxisType,具體大家去看。

  旋轉時,我們根據攝像機到模型的向量分別計算對應的XYZ軸上正負向量,再分別生成如X軸上對應YZ平面的90度弧形,順便我們得到每個對應平面在對應屏幕上的方向,這樣我們在屏幕上移動就能正確的對應模型應該的旋轉方向,列出其中相關代碼,更詳細的解釋請看函數對應的注釋。  

    #region 渲染旋轉
public void Render_Rotate()
{
if (currentAxis == AxisType.None)
{
Render_RotateArc();
}
else
{
Render_RotateAll();
}
}

//旋轉模式下,生成三個面的旋轉模型
public void Render_RotateArc()
{
Vector3 toWidget
= ((this.transform.position - Camera.main.transform.position)).normalized;
Vector3 XAxis
= coordSystem * Vector3.right;
Vector3 YAxis
= coordSystem * Vector3.up;
Vector3 ZAxis
= coordSystem * Vector3.forward;

//畫對應的旋轉的90度面
var redMesh = DrawRotationArc(AxisType.X, this.transform.position, ZAxis, YAxis, 0, Mathf.PI / 2.0f, toWidget, Color.red, ref xAxisDir);
var greenMesh = DrawRotationArc(AxisType.Y, this.transform.position, XAxis, ZAxis, 0, Mathf.PI / 2.0f, toWidget, Color.green, ref yAxisDir);
var blueMesh = DrawRotationArc(AxisType.Z, this.transform.position, XAxis, YAxis, 0, Mathf.PI / 2.0f, toWidget, Color.blue, ref zAxisDir);
//分別合並面與線,合成一個SubMesh時,要求MeshTopology與材質一樣
var faceMesh = CombineMesh(true, redMesh.FaceMesh, greenMesh.FaceMesh, blueMesh.FaceMesh);

//var lineMesh = CombineMesh(true, redMesh.LineMesh, greenMesh.LineMesh, blueMesh.LineMesh);
float x = Mathf.Sign(Vector3.Dot(toWidget, bLocation ? axisTransform.right : Vector3.right));
float y = Mathf.Sign(Vector3.Dot(toWidget, bLocation ? axisTransform.up : Vector3.up));
float z = Mathf.Sign(Vector3.Dot(toWidget, bLocation ? axisTransform.forward : Vector3.forward));
var redLineMesh = CreateLine(Vector3.zero, -XAxis * innerRadius * x, Color.red);
var greenLineMesh = CreateLine(Vector3.zero, -YAxis * innerRadius * y, Color.green);
var blueLineMesh = CreateLine(Vector3.zero, -ZAxis * innerRadius * z, Color.blue);
var lineMesh = CombineMesh(true, redLineMesh, greenLineMesh, blueLineMesh);

//合並面與線,分別對應一個SubMesh,可以用不同MeshTopology與材質
meshFilter.mesh = CombineMesh(false, faceMesh, lineMesh);
//給每個SubMesh對應材質
meshRender.sharedMaterials = new Material[2] { faceMat, lineMat };
}

/// <summary>
/// FWidget::DrawRotationArc 渲染選擇某個軸后的對應模型,360度的面
/// </summary>
public void Render_RotateAll()
{
Vector3 toWidget
= (this.transform.position - Camera.main.transform.position).normalized;
Vector3 XAxis
= coordSystem * Vector3.right; // Quaternion.Inverse(coordSystem) *
Vector3 YAxis = coordSystem * Vector3.up; //
Vector3 ZAxis = coordSystem * Vector3.forward; //

float adjustDeltaRotation = bLocation ? -totalDeltaRotation : totalDeltaRotation;
float absRotation = Mathf.Abs(totalDeltaRotation) % 360.0f;
float angleRadians = absRotation * Mathf.Deg2Rad;

float startAngle = adjustDeltaRotation < 0.0f ? -angleRadians : 0.0f;
float filledAngle = angleRadians;

LineFaceMesh meshRotation
= null;
LineFaceMesh meshAll
= null;
//畫對應的旋轉的90度面
if (currentAxis == AxisType.X)
{
meshRotation
= DrawRotationArc(AxisType.X, this.transform.position, ZAxis, YAxis, startAngle, startAngle + filledAngle, toWidget, Color.red);
meshAll
= DrawRotationArc(AxisType.X, this.transform.position, ZAxis, YAxis, startAngle + filledAngle, startAngle + 2.0f * Mathf.PI, toWidget, Color.yellow);
}
else if (currentAxis == AxisType.Y)
{
meshRotation
= DrawRotationArc(AxisType.Y, this.transform.position, XAxis, ZAxis, startAngle, startAngle + filledAngle, toWidget, Color.green);
meshAll
= DrawRotationArc(AxisType.Y, this.transform.position, XAxis, ZAxis, startAngle + filledAngle, startAngle + 2.0f * Mathf.PI, toWidget, Color.yellow);
}
else if (currentAxis == AxisType.Z)
{
meshRotation
= DrawRotationArc(AxisType.Z, this.transform.position, XAxis, YAxis, startAngle, startAngle + filledAngle, toWidget, Color.blue);
meshAll
= DrawRotationArc(AxisType.Z, this.transform.position, XAxis, YAxis, startAngle + filledAngle, startAngle + 2.0f * Mathf.PI, toWidget, Color.yellow);
}

meshFilter.mesh
= CombineMesh(false, meshRotation.FaceMesh, meshAll.FaceMesh);
//給每個SubMesh對應材質
meshRender.sharedMaterials = new Material[2] { lineMat, faceMat };
}

public LineFaceMesh DrawRotationArc(AxisType type, Vector3 inLocation, Vector3 axis0, Vector3 axis1, float inStartAngle, float inEndAngle, Vector3 toWidget, Color32 color)
{
Vector2 outAxis
= new Vector2();
return DrawRotationArc(type, inLocation, axis0, axis1, inStartAngle, inEndAngle, toWidget, color, ref outAxis);
}
///X軸上,我們渲染YZ平面,先確定在攝像機->模型在Y軸與Z軸上的方向,再確定這個平面對應在屏幕上的方向
public LineFaceMesh DrawRotationArc(AxisType type, Vector3 inLocation, Vector3 axis0, Vector3 axis1, float inStartAngle, float inEndAngle, Vector3 toWidget, Color32 color, ref Vector2 outAxisDir)
{
//確定采用軸的正向還是反向
bool bMirrorAxis0 = Vector3.Dot(axis0, toWidget) <= 0.0f;
bool bMirrorAxis1 = Vector3.Dot(axis1, toWidget) <= 0.0f;

Vector3 renderAxis0
= bMirrorAxis0 ? axis0 : -axis0;
Vector3 renderAxis1
= bMirrorAxis1 ? axis1 : -axis1;
//畫90度弧形
var mesh = DrawThickArc(renderAxis0, renderAxis1, inStartAngle, inEndAngle, toWidget, color);

//確定屏幕上對應方向
float direction = (bMirrorAxis0 ^ bMirrorAxis1) ? -1.0f : 1.0f;
var axisSceen0 = ScreenToPixel(this.transform.position + renderAxis0 * 64);
var axisSceen1 = ScreenToPixel(this.transform.position + renderAxis1 * 64);

outAxisDir
= ((axisSceen1 - axisSceen0) * direction).normalized;
return mesh;
}

//世界點轉成屏幕對應的像素位置
public Vector2 ScreenToPixel(Vector3 pos)
{
Vector4 loc
= pos;
loc.w
= 1;
//MVP 后的位置,其值在 DX/OpenGL 范圍各不相同
Vector4 mvpLoc = Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix * loc;
//四維數據轉到三維,簡單來說,X,Y,Z限定范圍到DX/OpenGL所定義的包圍圈中
float InvW = 1.0f / mvpLoc.w;
//這里是在DX下的范圍,由[-1,1]映射到[0,1]中
var x = (0.5f + mvpLoc.x * 0.5f * InvW) * Camera.main.pixelWidth;
var y = (0.5f - mvpLoc.y * 0.5f * InvW) * Camera.main.pixelHeight;

return new Vector2(x, y);
}

/// <summary>
/// 動態生成以axis0和axis1組成的平面,以axis0為0度,畫從inStartAngle到inEndAngle弧形
/// </summary>
public LineFaceMesh DrawThickArc(Vector3 axis0, Vector3 axis1, float inStartAngle, float inEndAngle, Vector3 toWidget, Color32 color)
{
LineFaceMesh lineFace
= new LineFaceMesh();
Mesh mesh
= lineFace.FaceMesh;
//Mesh lineMesh = lineFace.LineMesh;

int numPoints = (int)(circleSide * (inEndAngle - inStartAngle) / (Mathf.PI / 2.0f)) + 1;
Vector3 zAxis
= Vector3.Cross(axis0, axis1);

Vector3[] posArray
= new Vector3[2 * numPoints + 2];
Color32[] colorArray
= new Color32[2 * numPoints + 2];
Vector2[] uvArray
= new Vector2[2 * numPoints + 2];
//Vector3[] linePosArray = new Vector3[4 * numPoints + 4];

int index = 0;
Vector3 lastVertex
= Vector3.zero;
for (int radiusIndex = 0; radiusIndex < 2; ++radiusIndex)
{
float radius = (radiusIndex == 0) ? outerRadius : innerRadius;
float tcRadius = radius / (float)innerRadius;

for (int vectexIndex = 0; vectexIndex <= numPoints; vectexIndex++)
{
float percent = vectexIndex / (float)numPoints;
float angle = Mathf.Lerp(inStartAngle, inEndAngle, percent);
float angleDeg = angle * Mathf.Rad2Deg;

Vector3 vertexDir
= Quaternion.AngleAxis(angleDeg, zAxis) * axis0;
vertexDir.Normalize();

float tcAngle = percent * Mathf.PI / 2;

Vector2 tc
= new Vector2(tcRadius * Mathf.Cos(angle), tcRadius * Mathf.Sin(angle));
Vector3 vertexPos
= vertexDir * radius;

posArray[index]
= vertexPos;
uvArray[index]
= tc;
colorArray[index]
= color;

++index;
lastVertex
= vertexPos;
}
}
mesh.vertices
= posArray;
mesh.uv
= uvArray;
mesh.colors32
= colorArray;

int innerStart = numPoints + 1;
int[] triArray = new int[3 * 2 * numPoints];
index
= 0;
for (int vertexIndex = 0; vertexIndex < numPoints; vertexIndex++)
{
triArray[index
++] = vertexIndex;
triArray[index
++] = vertexIndex + 1;
triArray[index
++] = vertexIndex + innerStart;
triArray[index
++] = vertexIndex + 1;
triArray[index
++] = vertexIndex + innerStart + 1;
triArray[index
++] = vertexIndex + innerStart;
}
mesh.triangles
= triArray;
lineFace.LineMesh
= CreateLine(Vector3.zero, zAxis * innerRadius, color);
return lineFace;
}

//創建一個線段
public Mesh CreateLine(Vector3 start, Vector3 end, Color32 color)
{
Mesh mesh
= new Mesh();
mesh.vertices
= new Vector3[2] { start, end };
mesh.uv
= new Vector2[2] { Vector2.zero, Vector2.zero };
mesh.colors32
= new Color32[2] { color, color };
mesh.SetIndices(
new int[] { 0, 1 }, MeshTopology.Lines, 0);
return mesh;
}

//Mesh.CombineMeshes 需要已經正確的subMesh indices,而這里的mesh的indices都是從0開始,自己寫個
public Mesh CombineMesh(bool mergeSubMeshes, params Mesh[] meshs)
{
List
<Vector3> vectors = new List<Vector3>();
List
<Vector2> uvs = new List<Vector2>();
List
<Color32> colors = new List<Color32>();
List
<int> startIndexs = new List<int>();
int start = 0;
int indexCount = 0;
bool bUV = true;
bool bColor = true;
foreach (var mesh in meshs)
{
vectors.AddRange(mesh.vertices);
uvs.AddRange(mesh.uv);
if (mesh.uv.Length == 0)
bUV
= false;
colors.AddRange(mesh.colors32);
if (mesh.colors32.Length == 0)
bColor
= false;
startIndexs.Add(start);
start
+= mesh.vertexCount;
indexCount
+= mesh.GetIndices(0).Length;
}

var combineMesh = new Mesh();
combineMesh.SetVertices(vectors);
if (bUV)
combineMesh.SetUVs(
0, uvs);
if (bColor)
combineMesh.SetColors(colors);

combineMesh.subMeshCount
= mergeSubMeshes ? 1 : meshs.Length;
int[] allIndices = new int[indexCount];
int autoIndex = 0;
for (int i = 0; i < meshs.Length; i++)
{
var indices = meshs[i].GetIndices(0);
int count = indices.Length;
int[] tris = new int[count];
for (int j = 0; j < count; j++)
{
allIndices[autoIndex
++] = indices[j] + startIndexs[i];
tris[j]
= indices[j] + startIndexs[i];
}
if (!mergeSubMeshes)
combineMesh.SetIndices(tris, meshs[i].GetTopology(
0), i);
}
if (mergeSubMeshes)
combineMesh.SetIndices(allIndices, meshs[
0].GetTopology(0), 0);
return combineMesh;
}
#endregion
渲染旋轉

  因為UE4中有RHI,所以只管放入相應Rendering Command,下面會自動合並,優化,而Unity因為高度集成,相反在寫這些代碼時比較麻煩,如上,我本意在場景里定義一個空的模型,加上我這個腳本后就能實現相應旋轉,移動的功能,不引入別的任何內容,也不生成子GameObject,所以動態生成對應的MeshFilter與MeshRenderer要考慮如下需求。

  1. 只有一個MeshFilter與MeshRender,這樣我們可能要自己組裝多個SubMesh.

  2. 每個軸用不同的顏色表示,並且每軸需要二種繪制方式,三角面,線條。

  3. 我們要優化渲染,需要最少的Material能完成就用最少的Material,以及最少的SubMesh.

  4. 渲染需要,深度測試通過,但是不要寫入深度緩存中,不受燈光影響。

  5. 層次顯示需要,面要透明,而線不需要透明。

  一般來說,每個面用不同顏色表示,在Unity中就需要不同的Material,或運行時設置Material的變量,這樣每個面就不能合並顯示,我們需要能利用模型本身顏色的Shader,並且要滿足上面第四點,通過Unity官方提供的Unity5Shader這個項目,我們找到GUI/Text Shader,滿足上面的條件,這樣,生成三個軸對應的面模型時,使用顏色數據,就能合並成一個SubMesh,使用一個Material渲染,我們知道,同一個SubMesh,不可能出現一個畫三角面,一個線,這樣我們最少有二個SubMesh。大家對照下Render_RotateArc這個方法,結合ComBineMesh這個方法,可能有的同學會問,Unity不是本身就提供了Mesh.CombineMeshs,使用這個合並不就OK了,Mesh.CombineMeshs這個方法需要本身的SubMesh對應的Indices里索引已經是全局數據的索引才可以用的,什么意思了,我們這邊生成的三個Mesh,其indices里的數據都是針對本身的vectices的索引,用CombineMeshs合並后,后面的Mesh對應的索引就錯了。

  上面的這部分UE4與Unity代碼幾乎完全不同,需要大家自己修改成自己所需要的。

  選擇旋轉軸的算法沒用UE4的,用的一種非常簡單的方法,大致思路,找到射線與圓的二個交點,把交點轉到模型空間中,查看交點的x,y,z的值,那個值接近0,就是那個軸,想具體理解可見我前文 一個簡單的旋轉控制器與固定屏幕位置 ,里面也有求得移動軸的算法。 

  最后,說一個簡單的東東,原來我一直沒搞出來,不管模型與攝像機的距離,旋轉與移動操作都是合適的大小,我原來求出來的值,要么就是在距離少時,顯示不對,要么就是在距離遠時,顯示不對,而UE4給出一個簡單的式子,如下面代碼。

Vector4 aposition = axisTransform.position;
aposition.w
= 1;
float w = (Camera.main.projectionMatrix * Camera.main.worldToCameraMatrix * aposition).w;
widgetScale
= w * (4.0f / Camera.main.pixelWidth / Camera.main.projectionMatrix[0, 0]);
widgetScale

  代碼完整鏈接 UWidget.zip,就一個文件,在Unity場景中,根節點下建立一個GameObject,把這個腳本放上面去就行,對應UI如設置 世界/本地,旋轉,移動都有相應API調用。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com