outDrag;
FWidget::GetAbsoluteTranslationDelta
移动的算法差不多就是这样,其中如何生成移动模型就不拉出来,后面会给出源代码,大家自己去找。至于如何找到移动模型对应的X,Y,Z轴,或是全部移动,算法以前写过,求得二射线相隔最近的二点,然后根据二点的长度判断是否认为相交,在代码文件上的GetAxisType,具体大家去看。
旋转时,我们根据摄像机到模型的向量分别计算对应的XYZ轴上正负向量,再分别生成如X轴上对应YZ平面的90度弧形,顺便我们得到每个对应平面在对应屏幕上的方向,这样我们在屏幕上移动就能正确的对应模型应该的旋转方向,列出其中相关代码,更详细的解释请看函数对应的注释。
#region 渲染旋转
public void Render_Rotate()
if (currentAxis == AxisType.None)
Render_RotateArc();
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调用。