移植UE4的Spline与SplineMesh组件到Unity5

摘要:
在UE4提供的ContentExamples示例中,树生长示例很好。相关的Spline和SplineMesh组件代码相对独立,易于理解。它们只是用来迁移到Unity5。我对UE4和Unity5的两个引擎很熟悉。哈哈,下面是Unity5中的效果图。默认情况下,树苗和管道模型是直的,所以UE4的效果不会被切断,因为移植的效果还差得多,如果你对它感兴趣,你可以改进它。因为时间和级别,这只是暂时的。

一个月前,想开始看下UE4的源码,刚开始以为有Ogre1.9与Ogre2.1源码的基础 ,应该还容易理解,把源码下起后,发现我还是想的太简单了,UE4的代码量对比Ogre应该多了一个量级,毕竟Ogre只是一个渲染引擎,而UE4包含渲染,AI,网络,编辑器等等,所以要理解UE4的源码,应该带着目地去看,这样容易理解。

在看UE4提供的ContentExamples例子中,一个树生长的例子感觉不错,与之有关的Spline与SplineMesh组件代码比较独立也很容易理解,刚好拿来移植到Unity5中,熟悉UE4与Unity5这二种引擎,哈哈,如下是现在Unity5中的效果图,其中树苗与管子模型默认都是直的,UE4的效果就不截了,因为移植的还是差了好多,有兴趣的大家可以完善,因为时间和水平,暂时只做到这里了。

移植UE4的Spline与SplineMesh组件到Unity5第1张

如下是改写UE4中的FInterpCurve的C#版InterpCurve,都是泛形版的,在这要说下由于C#泛型对比C++泛形缺少很多功能,如T+T这种,C++在编译时能正确指出是否实现+。而C#就算使用泛形约束,也不能指定实现重载+的类型,然后如局部泛形实例化的功能也没有。可以使用泛形加继承来实现,父类泛形T,子类继承泛形的实例化(A : T[Vector3])来完成类似功能。在这我们不使用这种,使用另外一种把相应具体类型有关的操作用委托包装起来,这样也可以一是用来摆脱具体操作不能使用的局限,二是用来实现C++中的局部泛形实例化。照说C#是运行时生成的泛形实例化代码,应该比C++限制更少,可能是因为C#要求安全型等啥原因吧,只能使用功能有限的泛形约束。  

移植UE4的Spline与SplineMesh组件到Unity5第2张移植UE4的Spline与SplineMesh组件到Unity5第3张
public class InterpCurve<T>
{
    public List<InterpCurveNode<T>> Points = new List<InterpCurveNode<T>>();
    public boolIsLooped;
    public floatLoopKeyOffset;
    public InterpCurve(int capity = 0)
    {
        for (int i = 0; i < capity; ++i)
        {
            this.Points.Add(new InterpCurveNode<T>());
        }
    }
    public InterpCurveNode<T> this[intindex]
    {
        get
        {
            return this.Points[index];
        }
        set
        {
            this.Points[index] =value;
        }
    }
    public void SetLoopKey(floatloopKey)
    {
        float lastInKey = Points[Points.Count - 1].InVal;
        if (loopKey <lastInKey)
        {
            IsLooped = true;
            LoopKeyOffset = loopKey -lastInKey;
        }
        else
        {
            IsLooped = false;
        }
    }
    public voidClearLoopKey()
    {
        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)
            returndefalutValue;
        int index =GetPointIndexForInputValue(inVal);
        if (index < 0)
            return this[0].OutVal;
        //如果当前索引是最后索引
        if (index ==lastPoint)
        {
            if (!IsLooped)
            {
                returnPoints[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)
            {
                returnlerp(prevPoint.OutVal, nextPoint.OutVal, Alpha);
            }
            else
            {
                returncubicInterp(prevPoint.OutVal, prevPoint.LeaveTangent, nextPoint.OutVal, nextPoint.ArriveTangent, diff, Alpha);
            }
        }
        else
        {
            returnPoints[index].OutVal;
        }
    }
    /// <summary>
    ///因为Points可以保证所有点让InVal从小到大排列,故使用二分查找
    /// </summary>
    /// <param name="InValue"></param>
    /// <returns></returns>
    private int GetPointIndexForInputValue(floatInValue)
    {
        int NumPoints =Points.Count;
        int LastPoint = NumPoints - 1;
        //check(NumPoints > 0);
        if (InValue < Points[0].InVal)
        {
            return -1;
        }
        if (InValue >=Points[LastPoint].InVal)
        {
            returnLastPoint;
        }
        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;
            }
        }
        returnMinIndex;
    }
    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)
        {
            returnDefault;
        }
        //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)
            {
                returnPoints[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;
                returnsubtract(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;
                returncubicInterp(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这个着色器代码文件中,开始以为这个很容易,后面花的时间比我预料的多了不少,我也发现我本身的一些问题,相应矩阵算法没搞清楚,列主序与行主序搞混等,先看如下一段代码。

移植UE4的Spline与SplineMesh组件到Unity5第2张移植UE4的Spline与SplineMesh组件到Unity5第5张
//如下顶点位置偏移右上前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的简单改版。

移植UE4的Spline与SplineMesh组件到Unity5第2张移植UE4的Spline与SplineMesh组件到Unity5第7张
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;
            structInput {
                float2 uv_MainTex;
            };
            half _Glossiness;
            half _Metallic;
            fixed4 _Color;
            float3 SplineEvalPos(float3 StartPos, float3 StartTangent, float3 EndPos, float3 EndTangent, floatA)
            {
                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, floatA)
            {
                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(floatYPos)
            {
                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);
                floatSinAng, 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);
                returnSliceTransform;
            }
            voidvert(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);
            }
            voidsurf(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相应的蓝图实现,自己改写下。

移植UE4的Spline与SplineMesh组件到Unity5第2张移植UE4的Spline与SplineMesh组件到Unity5第9张
public classVineShow : MonoBehaviour
{
    public AnimationCurve curve = null;
    private UESpline spline = null;
    private UESplineMesh splineMesh = null;
    //Use this for initialization
    voidStart()
    {
        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
    voidUpdate()
    {
        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, refsplineMesh.param.StartTangent);
        UpdateMeshParam(growth * length, scale, ref splineMesh.param.EndPos, refsplineMesh.param.EndTangent);
        splineMesh.SetShaderParam();
    }
    public void UpdateMeshParam(float key, float scale, ref Vector3 position, refVector3 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

免责声明:文章转载自《移植UE4的Spline与SplineMesh组件到Unity5》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇AngularJS中$timeout和$interval的用法详解TeamCity安装Agent(Windows和Linux系统下)下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

零基础入门深度学习(5)

无论即将到来的是大数据时代还是人工智能时代,亦或是传统行业使用人工智能在云上处理大数据的时代,作为一个有理想有追求的程序员,不懂深度学习(Deep Learning)这个超热的技术,会不会感觉马上就out了?现在救命稻草来了,《零基础入门深度学习》系列文章旨在讲帮助爱编程的你从零基础达到入门级水平。零基础意味着你不需要太多的数学知识,只要会写程序就行了,...

MATLAB

总原则:能用向量矩阵解决的就不用for循环。 1.匿名函数  @定义一个函数或变量,用括号里的字母作为变量名字。 标准格式是: fhandle=@(arglist)express (1)express是一个matlab变量表达式,比如:x+x.^2,sin(x)等(2)argilst是参数列表;(3)符号@是matlab创建函数句柄的操作符,表示创建由参数...

「雕爷学编程」Arduino动手做(26)——4X4矩阵键盘模块

37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里准备逐一动手试试做实验,不管成功与否,都会记录下来---小小的进步或是搞不定的问题,希望能够抛砖引玉。 【Arduino】108种传感器模块系列实验(资料+代...

61 相机投影原理、相机模型中的坐标系统以及标定方法

0 引言   世界坐标系下的点如何投影到CCD镜头上,通过成像的方式得到点在二维图像上的像素坐标值,这是摄影测量中的一个基础而核心的问题。这个问题中核心的东西有两个:1、坐标系的定义及其空间转换矩阵  2、成像中的误差 下面将从这两个角度对这个问题进行详细探讨。文章参考了 # 陈建平: 《相机成像原理》PPThttps://blog.csdn.net/...

matlab范德蒙矩阵生成学习

范德蒙矩阵的形式 1、范德蒙德行列式概述(定义及其特点) 2、范德蒙德行列式的计算公式。 3、对上述计算公式的一些解释和例子。 4、利用数学归纳法证明范德蒙德行列式的计算公式(验证n=2的情形) 5、证明的详细步骤(将行列式按第一列展开)。 6、由“递推公式”得到“通项公式”(完成证明) >> >> syms x1...

二维码(QR code)基本知识

1.二维码定义:   二维码(2-Dimensional Bar Code),是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的。它是指在一维条码的基础上扩展出另一维具有可读性的条码,使用黑白矩形图案表示二进制数据,被设备扫描后可获取其中所包含的信息。一维条码的宽度记载着数据,而其长度没有记载数据。二维条码的长度、...