移植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);
        }
    }
}
InterpCurve

  实现就是拷的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"
}
SplineMesh

  树的动画就简单了,对应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;
    }
}
VineShow

  本来还准备完善下才发出来,但是时间太紧,没有时间来完善这个,特此记录下实现本文遇到的相关点供以后查找。

  附件:SplineMeshUE4.zip

posted @ 2016-03-10 14:54  天天不在  阅读(3500)  评论(2编辑  收藏  举报