移植UE4的Spline与SplineMesh组件到Unity5
一个月前,想开始看下UE4的源码,刚开始以为有Ogre1.9与Ogre2.1源码的基础 ,应该还容易理解,把源码下起后,发现我还是想的太简单了,UE4的代码量对比Ogre应该多了一个量级,毕竟Ogre只是一个渲染引擎,而UE4包含渲染,AI,网络,编辑器等等,所以要理解UE4的源码,应该带着目地去看,这样容易理解。
在看UE4提供的ContentExamples例子中,一个树生长的例子感觉不错,与之有关的Spline与SplineMesh组件代码比较独立也很容易理解,刚好拿来移植到Unity5中,熟悉UE4与Unity5这二种引擎,哈哈,如下是现在Unity5中的效果图,其中树苗与管子模型默认都是直的,UE4的效果就不截了,因为移植的还是差了好多,有兴趣的大家可以完善,因为时间和水平,暂时只做到这里了。
如下是改写UE4中的FInterpCurve的C#版InterpCurve,都是泛形版的,在这要说下由于C#泛型对比C++泛形缺少很多功能,如T+T这种,C++在编译时能正确指出是否实现+。而C#就算使用泛形约束,也不能指定实现重载+的类型,然后如局部泛形实例化的功能也没有。可以使用泛形加继承来实现,父类泛形T,子类继承泛形的实例化(A : T[Vector3])来完成类似功能。在这我们不使用这种,使用另外一种把相应具体类型有关的操作用委托包装起来,这样也可以一是用来摆脱具体操作不能使用的局限,二是用来实现C++中的局部泛形实例化。照说C#是运行时生成的泛形实例化代码,应该比C++限制更少,可能是因为C#要求安全型等啥原因吧,只能使用功能有限的泛形约束。
public class InterpCurve<T> { public List<InterpCurveNode<T>> Points = new List<InterpCurveNode<T>>(); public bool IsLooped; public float LoopKeyOffset; public InterpCurve(int capity = 0) { for (int i = 0; i < capity; ++i) { this.Points.Add(new InterpCurveNode<T>()); } } public InterpCurveNode<T> this[int index] { get { return this.Points[index]; } set { this.Points[index] = value; } } public void SetLoopKey(float loopKey) { float lastInKey = Points[Points.Count - 1].InVal; if (loopKey < lastInKey) { IsLooped = true; LoopKeyOffset = loopKey - lastInKey; } else { IsLooped = false; } } public void ClearLoopKey() { IsLooped = false; } /// <summary> /// 计算当线曲线的切线 /// </summary> /// <param name="tension"></param> /// <param name="bStationaryEndpoints"></param> /// <param name="computeFunc"></param> /// <param name="subtract"></param> public void AutoSetTangents(float tension, bool bStationaryEndpoints, ComputeCurveTangent<T> computeFunc, Func<T, T, T> subtract) { int numPoints = Points.Count; int lastPoint = numPoints - 1; for (int index = 0; index < numPoints; ++index) { int preIndex = (index == 0) ? (IsLooped ? lastPoint : 0) : (index - 1); int nextIndex = (index == lastPoint) ? (IsLooped ? 0 : lastPoint) : (index + 1); var current = Points[index]; var pre = Points[preIndex]; var next = Points[nextIndex]; if (current.InterpMode == InterpCurveMode.CurveAuto || current.InterpMode == InterpCurveMode.CurevAutoClamped) { if (bStationaryEndpoints && (index == 0 || (index == lastPoint && !IsLooped))) { current.ArriveTangent = default(T); current.LeaveTangent = default(T); } else if (pre.IsCurveKey()) { bool bWantClamping = (current.InterpMode == InterpCurveMode.CurevAutoClamped); float prevTime = (IsLooped && index == 0) ? (current.InVal - LoopKeyOffset) : pre.InVal; float nextTime = (IsLooped && index == lastPoint) ? (current.InVal + LoopKeyOffset) : next.InVal; T Tangent = computeFunc(prevTime, pre.OutVal, current.InVal, current.OutVal, nextTime, next.OutVal, tension, bWantClamping); current.ArriveTangent = Tangent; current.LeaveTangent = Tangent; } else { current.ArriveTangent = pre.ArriveTangent; current.LeaveTangent = pre.LeaveTangent; } } else if (current.InterpMode == InterpCurveMode.Linear) { T Tangent = subtract(next.OutVal, current.OutVal); current.ArriveTangent = Tangent; current.LeaveTangent = Tangent; } else if (current.InterpMode == InterpCurveMode.Constant) { current.ArriveTangent = default(T); current.LeaveTangent = default(T); } } } /// <summary> /// 根据当前inVale对应的Node与InterpCurveMode来得到在对应Node上的值 /// </summary> /// <param name="inVal"></param> /// <param name="defalutValue"></param> /// <param name="lerp"></param> /// <param name="cubicInterp"></param> /// <returns></returns> public T Eval(float inVal, T defalutValue, Func<T, T, float, T> lerp, CubicInterp<T> cubicInterp) { int numPoints = Points.Count; int lastPoint = numPoints - 1; if (numPoints == 0) return defalutValue; int index = GetPointIndexForInputValue(inVal); if (index < 0) return this[0].OutVal; // 如果当前索引是最后索引 if (index == lastPoint) { if (!IsLooped) { return Points[lastPoint].OutVal; } else if (inVal >= Points[lastPoint].InVal + LoopKeyOffset) { // Looped spline: last point is the same as the first point return Points[0].OutVal; } } //check(Index >= 0 && ((bIsLooped && Index < NumPoints) || (!bIsLooped && Index < LastPoint))); bool bLoopSegment = (IsLooped && index == lastPoint); int nextIndex = bLoopSegment ? 0 : (index + 1); var prevPoint = Points[index]; var nextPoint = Points[nextIndex]; //当前段的总长度 float diff = bLoopSegment ? LoopKeyOffset : (nextPoint.InVal - prevPoint.InVal); if (diff > 0.0f && prevPoint.InterpMode != InterpCurveMode.Constant) { float Alpha = (inVal - prevPoint.InVal) / diff; //check(Alpha >= 0.0f && Alpha <= 1.0f); if (prevPoint.InterpMode == InterpCurveMode.Linear) { return lerp(prevPoint.OutVal, nextPoint.OutVal, Alpha); } else { return cubicInterp(prevPoint.OutVal, prevPoint.LeaveTangent, nextPoint.OutVal, nextPoint.ArriveTangent, diff, Alpha); } } else { return Points[index].OutVal; } } /// <summary> /// 因为Points可以保证所有点让InVal从小到大排列,故使用二分查找 /// </summary> /// <param name="InValue"></param> /// <returns></returns> private int GetPointIndexForInputValue(float InValue) { int NumPoints = Points.Count; int LastPoint = NumPoints - 1; //check(NumPoints > 0); if (InValue < Points[0].InVal) { return -1; } if (InValue >= Points[LastPoint].InVal) { return LastPoint; } int MinIndex = 0; int MaxIndex = NumPoints; while (MaxIndex - MinIndex > 1) { int MidIndex = (MinIndex + MaxIndex) / 2; if (Points[MidIndex].InVal <= InValue) { MinIndex = MidIndex; } else { MaxIndex = MidIndex; } } return MinIndex; } public T EvalDerivative(float InVal, T Default, Func<T, T, float, T> subtract, CubicInterp<T> cubicInterp) { int NumPoints = Points.Count; int LastPoint = NumPoints - 1; // If no point in curve, return the Default value we passed in. if (NumPoints == 0) { return Default; } // Binary search to find index of lower bound of input value int Index = GetPointIndexForInputValue(InVal); // If before the first point, return its tangent value if (Index == -1) { return Points[0].LeaveTangent; } // If on or beyond the last point, return its tangent value. if (Index == LastPoint) { if (!IsLooped) { return Points[LastPoint].ArriveTangent; } else if (InVal >= Points[LastPoint].InVal + LoopKeyOffset) { // Looped spline: last point is the same as the first point return Points[0].ArriveTangent; } } // Somewhere within curve range - interpolate. //check(Index >= 0 && ((bIsLooped && Index < NumPoints) || (!bIsLooped && Index < LastPoint))); bool bLoopSegment = (IsLooped && Index == LastPoint); int NextIndex = bLoopSegment ? 0 : (Index + 1); var PrevPoint = Points[Index]; var NextPoint = Points[NextIndex]; float Diff = bLoopSegment ? LoopKeyOffset : (NextPoint.InVal - PrevPoint.InVal); if (Diff > 0.0f && PrevPoint.InterpMode != InterpCurveMode.Constant) { if (PrevPoint.InterpMode == InterpCurveMode.Linear) { //return (NextPoint.OutVal - PrevPoint.OutVal) / Diff; return subtract(NextPoint.OutVal, PrevPoint.OutVal, Diff); } else { float Alpha = (InVal - PrevPoint.InVal) / Diff; //check(Alpha >= 0.0f && Alpha <= 1.0f); //turn FMath::CubicInterpDerivative(PrevPoint.OutVal, PrevPoint.LeaveTangent * Diff, NextPoint.OutVal, NextPoint.ArriveTangent * Diff, Alpha) / Diff; return cubicInterp(PrevPoint.OutVal, PrevPoint.LeaveTangent, NextPoint.OutVal, NextPoint.ArriveTangent, Diff, Alpha); } } else { // Derivative of a constant is zero return default(T); } } }
实现就是拷的UE4里的逻辑,泛形主要是提供公共的一些实现,下面会放出相应附件,其中文件InterpHelp根据不同的T实现不同的逻辑,UESpline结合这二个文件来完成这个功能。
然后就是UE4里的SplineMesh这个组件,上面的Spline主要是CPU解析顶点和相应数据,而SplineMesh组件是改变模型,如果模型顶点很多,CPU不适合处理这种,故相应实现都在LocalVertexFactory.usf这个着色器代码文件中,开始以为这个很容易,后面花的时间比我预料的多了不少,我也发现我本身的一些问题,相应矩阵算法没搞清楚,列主序与行主序搞混等,先看如下一段代码。
//如下顶点位置偏移右上前1 float4x4 mx = float4x4(float4(1, 0, 0, 0), float4(0, 1, 0, 0), float4(0, 0, 1, 0), float4(1, 1, 1, 1)); //矩阵左,向量右,向量与矩阵为列向量。 v.vertex = mul(transpose(mx), v.vertex); //向量左,矩阵右,则向量与矩阵为行向量。 v.vertex = mul(v.vertex, mx); //向量左,矩阵右,([1*N])*([N*X]),向量与矩阵为行向量。 float4x3 mx4x3 = float4x3(float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1),float3(1,1,1)); v.vertex = float4(mul(v.vertex,mx4x3),v.vertex.w); //矩阵左与向量右,([X*N])*([N*1]) mx3x4 = transpose(mx4x3),表面看矩阵无意义,实际是mx4x3的列向量 float3x4 mx3x4 = float3x4(float4(1, 0, 0, 1), float4(0, 1, 0, 1), float4(0, 0, 1, 1)); v.vertex = float4(mx3x4, v.vertex), v.vertex.w); //这种错误,mx4x3是由行向量组成,必需放左边才有意义 v.vertex = mul(mx4x3, v.vertex.xyz);
其中,Unity本身用的是列矩阵形式,我们定义一个矩阵向x轴移动一个单位,然后打印出来看下结果就知道了,然后把相应着色器的代码转换到Unity5,这段着色器代码我并不需要改变很多,只需要在模型空间中顶点本身需要做点改变就行,那么我就直接使用Unity5中的SurfShader,提供一个vert函数改变模型空间的顶点位置,后面如MVP到屏幕,继续PBS渲染,阴影我都接着用,如下是针对LocalVertexFactory.usf的简单改版。
Shader "Custom/SplineMeshSurfShader" { Properties{ _Color("Color", Color) = (1,1,1,1) _MainTex("Albedo (RGB)", 2D) = "white" {} _Glossiness("Smoothness", Range(0,1)) = 0.5 _Metallic("Metallic", Range(0,1)) = 0.0 //_StartPos("StartPos",Vector) = (0, 0, 0, 1) //_StartTangent("StartTangent",Vector) = (0, 1, 0, 0) //_StartRoll("StartRoll",float) = 0.0 //_EndPos("EndPos",Vector) = (0, 0, 0, 1) //_EndTangent("EndTangent",Vector) = (0, 1, 0, 0) //_EndRoll("EndRoll",float) = 0.0 //_SplineUpDir("SplineUpDir",Vector) = (0, 1, 0, 0) //_SplineMeshMinZ("SplineMeshMinZ",float) = 0.0 //_SplineMeshScaleZ("SplineMeshScaleZ",float) = 0.0 //_SplineMeshDir("SplineMeshDir",Vector) = (0,0,1,0) //_SplineMeshX("SplineMeshX",Vector) = (1,0,0,0) //_SplineMeshY("SplineMeshY",Vector) = (0,1,0,0) } SubShader{ Tags { "RenderType" = "Opaque" } LOD 200 CGPROGRAM // Upgrade NOTE: excluded shader from OpenGL ES 2.0 because it uses non-square matrices #pragma exclude_renderers gles // Physically based Standard lighting model, and enable shadows on all light types #pragma surface surf Standard fullforwardshadows vertex:vert // Use shader model 3.0 target, to get nicer looking lighting #pragma target 3.0 sampler2D _MainTex; float3 _StartPos; float3 _StartTangent; float _StartRoll; float3 _EndPos; float3 _EndTangent; float _EndRoll; float3 _SplineUpDir; float _SplineMeshMinZ; float _SplineMeshScaleZ; float3 _SplineMeshDir; float3 _SplineMeshX; float3 _SplineMeshY; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; float3 SplineEvalPos(float3 StartPos, float3 StartTangent, float3 EndPos, float3 EndTangent, float A) { float A2 = A * A; float A3 = A2 * A; return (((2 * A3) - (3 * A2) + 1) * StartPos) + ((A3 - (2 * A2) + A) * StartTangent) + ((A3 - A2) * EndTangent) + (((-2 * A3) + (3 * A2)) * EndPos); } float3 SplineEvalDir(float3 StartPos, float3 StartTangent, float3 EndPos, float3 EndTangent, float A) { float3 C = (6 * StartPos) + (3 * StartTangent) + (3 * EndTangent) - (6 * EndPos); float3 D = (-6 * StartPos) - (4 * StartTangent) - (2 * EndTangent) + (6 * EndPos); float3 E = StartTangent; float A2 = A * A; return normalize((C * A2) + (D * A) + E); } float4x3 calcSliceTransform(float YPos) { float t = YPos * _SplineMeshScaleZ - _SplineMeshMinZ; float smoothT = smoothstep(0, 1, t); //实现基于frenet理论 //当前位置的顶点与方向根据起点与终点的设置插值 float3 SplinePos = SplineEvalPos(_StartPos, _StartTangent, _EndPos, _EndTangent, t); float3 SplineDir = SplineEvalDir(_StartPos, _StartTangent, _EndPos, _EndTangent, t); //根据SplineDir与当前_SplineUpDir 计算当前坐标系(过程类似视图坐标系的建立) float3 BaseXVec = normalize(cross(_SplineUpDir, SplineDir)); float3 BaseYVec = normalize(cross(SplineDir, BaseXVec)); // Apply roll to frame around spline float UseRoll = lerp(_StartRoll, _EndRoll, smoothT); float SinAng, CosAng; sincos(UseRoll, SinAng, CosAng); float3 XVec = (CosAng * BaseXVec) - (SinAng * BaseYVec); float3 YVec = (CosAng * BaseYVec) + (SinAng * BaseXVec); //mul(transpose(A),B), A为正交矩阵,A由三轴组成的行向量矩阵. //简单来看,_SplineMeshDir为x轴{1,0,0},则下面的不转换,x轴={0,0,0},y轴=XYec,z轴=YVec //_SplineMeshDir为y轴{0,1,0},则x轴=YVec,y轴={0,0,0},z轴=XYec //_SplineMeshDir为z轴{0,0,1},则x轴=XYec,y轴=YVec,z轴={0,0,0} float3x3 SliceTransform3 = mul(transpose(float3x3(_SplineMeshDir, _SplineMeshX, _SplineMeshY)), float3x3(float3(0, 0, 0), XVec, YVec)); //SliceTransform是一个行向量组成的矩阵 float4x3 SliceTransform = float4x3(SliceTransform3[0], SliceTransform3[1], SliceTransform3[2], SplinePos); return SliceTransform; } void vert(inout appdata_full v) { float t = dot(v.vertex.xyz, _SplineMeshDir); float4x3 SliceTransform = calcSliceTransform(t); v.vertex = float4(mul(v.vertex,SliceTransform),v.vertex.w); } void surf(Input IN, inout SurfaceOutputStandard o) { // Albedo comes from a texture tinted by color fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; // Metallic and smoothness come from slider variables o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
树的动画就简单了,对应UE4相应的蓝图实现,自己改写下。
public class VineShow : MonoBehaviour { public AnimationCurve curve = null; private UESpline spline = null; private UESplineMesh splineMesh = null; // Use this for initialization void Start() { spline = GetComponentInChildren<UESpline>(); splineMesh = GetComponentInChildren<UESplineMesh>(); spline.SceneUpdate(); if (curve == null || curve.length == 0) { curve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(6, 1)); } } // Update is called once per frame void Update() { float t = Time.time % curve.keys[curve.length - 1].time; var growth = curve.Evaluate(t); float length = spline.GetSplineLenght(); var start = 0.18f * growth; float scale = Mathf.Lerp(0.5f, 3.0f, growth); UpdateMeshParam(start * length, scale, ref splineMesh.param.StartPos, ref splineMesh.param.StartTangent); UpdateMeshParam(growth * length, scale, ref splineMesh.param.EndPos, ref splineMesh.param.EndTangent); splineMesh.SetShaderParam(); } public void UpdateMeshParam(float key, float scale, ref Vector3 position, ref Vector3 direction) { var pos = this.spline.GetPosition(key); var dir = this.spline.GetDirection(key); position = splineMesh.transform.worldToLocalMatrix * InterpHelp.Vector3To4(pos); direction = (splineMesh.transform.worldToLocalMatrix * dir) * scale; } }
本来还准备完善下才发出来,但是时间太紧,没有时间来完善这个,特此记录下实现本文遇到的相关点供以后查找。