• 移植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

  • 相关阅读:
    关于根据索引 删除队
    Vs 2012 编译C 语言
    动态绑定 dgvlist 列
    自定义控件闪烁问题
    程序员都不读书,但你应该读
    对于委托的理解 委托和事件
    ​label 中 文字的位置
    vs2010 折叠C/c++中的if
    关于动态创建控件性能提高 以及 SuspendLayout & ResumeLayout 的使用
    c# WinForm开发 DataGridView控件的各种操作总结(单元格操作,属性设置)
  • 原文地址:https://www.cnblogs.com/zhouxin/p/5260831.html
Copyright © 2020-2023  润新知