在Unity的Build-in管线中实现VFX的部分功能


前言

在开发项目时需要模拟真实雾效的效果,但是Build-in管线中立粒子系统实在太慢了,于是打算在Build-in管线中模拟VFX的实现,将计算放到GPU中,由一个4顶点的平面通过曲面细分着色器生成粒子进行模拟。

VFX模拟效果展示


一、VFX实现分析

(由于本人并非专门做特效的,一些分析错误请轻点喷)
本文的模拟只模拟了VFX的一部分实现,用来完成项目的需求,模拟的是该文章实现的雾效效果。

VFX的雾效初始化

由图片可以发现VFX首先会从一开始声明的粒子容器中创造粒子,创造完后输出给粒子初始化部分进行初始化设置,因此需要有时间以及方向属性还有颜色属性。这一部分笔者使用曲面细分着色器进行粒子数量控制,用几何着色器进行速度设置。
(按照VFX的设计来看,应该可以在GPU中固定生成属性的吧,但是由于没有所以只能在几何着色器进行速度设置了)
VFX雾效运行部分
不懂Update中实际上在干什么,因此打算将粒子移动方式设置为根据随机出来的速度乘以时间。
曲线生成可以使用Unity提供的组件:AnimationCurve来进行模拟,看向摄像机也是在几何着色器进行设置。

二、开始实现

1.生成平面

想要实现挂上一个代码就可以生成粒子的效果,自然需要使用Unity提供的顶点方式,由于粒子这个面只是用来创建粒子的,就只赋值了位置以及点的连接方式。
代码如下:

  meshFilter = gameObject.GetComponent<MeshFilter>();
  if (meshFilter == null)
      meshFilter = gameObject.AddComponent<MeshFilter>();
  else
  {
      //清除已经存在的Mesh,可能之前这个组件就有Mesh,
      //但是这个方法目标生成的Mesh与此时的Mesh不同,因此直接清除
      meshFilter.mesh.Clear();
      meshFilter.mesh = null;
  }
  renderer = gameObject.GetComponent<MeshRenderer>();
  if (renderer == null)
      renderer = gameObject.AddComponent<MeshRenderer>();
  if (material == null) return;
  renderer.material = material;

  Vector3[] poss = new Vector3[4];
  int[] tris = new int[6];
  //限制顶点边缘在0-1之间,同时不需要Y轴有数据
  poss[0] = new Vector3(0, 0, 0);
  poss[1] = new Vector3(0, 0, 1);
  poss[2] = new Vector3(1, 0, 1);
  poss[3] = new Vector3(1, 0, 0);
  tris[0] = 0;
  tris[1] = 1;
  tris[2] = 3;

  tris[3] = 3;
  tris[4] = 1;
  tris[5] = 2;

  Mesh mesh = new Mesh();
  mesh.vertices = poss;
  mesh.triangles = tris;
  meshFilter.mesh = mesh;

2.曲面细分初始化顶点

代码如下(示例):

//判定GPU是否支持曲面细分的宏
#ifdef UNITY_CAN_COMPILE_TESSELLATION
    //曲面细分中传入进行细分部分的结构体,也就是类似于顶点传几何,传递给细分着色器控制部分进行数据判断
    struct TessVertex{
        float4 vertex : INTERNALTESSPOS;
        float3 normal : NORMAL;
        float4 tangent : TANGENT;
        float2 uv : TEXCOORD0;
    };
    //细分着色器进行控制的根据值,细分程度由这个结构体定义,因此这个结构体不能变
    struct OutputPatchConstant{
        float edge[3]        : SV_TessFactor;
        float inside         : SV_InsideTessFactor;
        float3 vTangent[4]   : TANGENT;
        float2 vUV[4]        : TEXCOORD;
        float3 vTanUCorner[4]: TANUCORNER;
        float3 vTanVCorner[4]: TANVCORNER;
        float4 vCWts         : TANWEIGHTS;
    };
    //细分着色器的顶点着色器
    TessVertex tessvert (VertexInput v){
        TessVertex o;
        o.vertex = v.vertex;
        o.normal = v.normal;
        o.tangent = v.tangent;
        o.uv = v.uv;
        return o;
    }

    OutputPatchConstant hullconst(InputPatch<TessVertex, 3>v){
        OutputPatchConstant o = (OutputPatchConstant)0;
        //获得三个顶点的细分距离值
        float4 ts = float4(_ParticleSize, _ParticleSize, _ParticleSize, _ParticleSize);
        //本质上下面的赋值操作是对细分三角形的三条边以及里面细分程度的控制
        //这个值本质上是一个int值,0就是不细分,每多1细分多一层
        //控制边缘的细分程度,这个边缘程度的值不是我们用的,而是给Tessllation进行细分控制用的
        o.edge[0] = ts.x;
        o.edge[1] = ts.y;
        o.edge[2] = ts.z;
        //内部的细分程度
        o.inside = ts.w;
        return o;
    }

    [domain("tri")]    //输入图元的是一个三角形
    //确定分割方式
    [partitioning("fractional_odd")]
    //定义图元朝向,一般用这个即可,用切线为根据
    [outputtopology("triangle_cw")]
    //定义补丁的函数名,也就是我们上面的函数,hull函数的返回值会传到这个函数中,然后进行曲面细分
    [patchconstantfunc("hullconst")]
    //定义输出图元是一个三角形,和上面对应
    [outputcontrolpoints(3)]
    TessVertex hull (InputPatch<TessVertex, 3> v, uint id : SV_OutputControlPointID){
        return v[id];
    }

    //细分后对每一个图元的计算,这下面都是标准的获取新顶点数据的方式,为了方便,我们直接处理完数据后就扔到顶点着色器上了
    [domain("tri")]
    VertexInput domain (OutputPatchConstant tessFactors, const OutputPatch<TessVertex, 3> vi, float3 bary : SV_DomainLocation){
        VertexInput v = (VertexInput)0;
        v.vertex = vi[0].vertex * bary.x + vi[1].vertex*bary.y + vi[2].vertex * bary.z;
        v.normal = vi[0].normal * bary.x + vi[1].normal*bary.y + vi[2].normal * bary.z;
        v.tangent = vi[0].tangent * bary.x + vi[1].tangent*bary.y + vi[2].tangent * bary.z;
        v.uv = vi[0].uv * bary.x + vi[1].uv*bary.y + vi[2].uv * bary.z;
        return v;
    }
#endif

实际上就是简单的曲面细分,将数据传递给几何着色器。
在几何着色器中顶点生成一个朝向摄像机的面,但是在生成面之前先实现曲线的生成算法。
Unity的AnimationCurve曲线原理可以看这篇文章,由于AnimationCurve看到的是三次多项式插值生成的曲线,所以这里运用的就是三次多项式插值的处理方式。
直接上代码:

//时间控制函数,用来读取Curve中的值
float LoadCurveTime(float nowTime, int _Count, float4 _PointArray[10]){
    //有数据才循环
    for(int i=0; i<_Count; i++){
        //找到在范围中的
        if(_PointArray[i].x < nowTime && _PointArray[i+1].x > nowTime){
            //Unity的Curve的曲线本质上是一个三次多项式插值,公式为:y = ax^3 + bx^2 + cx +d
            float a = ( _PointArray[i].w + _PointArray[i+1].z ) * ( _PointArray[i + 1].x - _PointArray[i].x ) - 2 * ( _PointArray[i + 1].y - _PointArray[i].y );
            float b = ( -2 * _PointArray[i].w - _PointArray[i + 1].z ) * ( _PointArray[i + 1].x - _PointArray[i].x ) + 3 * ( _PointArray[i + 1].y - _PointArray[i].y );
            float c = _PointArray[i].w * ( _PointArray[i + 1].x - _PointArray[i].x );
            float d = _PointArray[i].y;

            float trueTime = (nowTime - _PointArray[i].x) / ( _PointArray[i+1].x  - _PointArray[i].x);
            return a * pow( trueTime, 3 ) + b * pow( trueTime, 2 ) + c * trueTime + d;
            
        }
    }
    //返回1,表示没有变化
    return 1;
}

函数中有3个参数,第一个是时间,也就是曲线中的X轴,第二个是帧数量,即曲线中的插入的帧数量,第三个是每一个帧的数据,这里每一个float4存储的数据是(帧的X值, 帧的Y值, 帧的左斜率, 帧的右斜率)。

3.几何着色器处理顶点

由于顶点不能时时刷新数据,因此对于单个顶点来说其数据是固定的,只有时间会改变,因此为了达到随机的效果,笔者打算让单个顶点跑向一个固定的方向,但是这个方向要在设置的速度范围值中随机。也就是说就是这个顶点跑向的方向永远都是一个位置,但是因为顶点的起始时间不一样,大量顶点在一起时看起来就像是随机的一样。

//加载一个顶点,将一个顶点输出为一个面
 void LoadOnePoint(VertexInput IN, inout TriangleStream<FragInput> tristream){
	//生成随机的范围在(0-1)的xyzw
     float4 ramdom = 0;
     ramdom.x = frac( abs( cos( (IN.vertex.x + IN.vertex.z)  * 100000 ) ) * 1000 );
     ramdom.y = frac( abs( sin( (IN.vertex.y - IN.vertex.z )  * 100000 ) ) * 1000 );
     ramdom.z = frac( abs( sin( (ramdom.x + ramdom.y )  * 100000 ) ) * 1000 );

     ramdom.w = frac( abs( sin( (ramdom.x + ramdom.y + ramdom.z) * UNITY_PI * 100000 ) ) * 1000 );
	
	//在设置的两个速度中进行随机,确定此时该顶点要随机前往的方向
     float3 dir0 = lerp(_VerticalStart, _VerticalEnd, ramdom.xyz);

     float addTime = _LifeTime * ramdom.w;

     //归一化
     float time = fmod( (_Time.y + addTime), _LifeTime) ;
     
     //控制移动方向
     dir0 *= time;

     time /= _LifeTime;

     dir0 = mul((float3x3)unity_ObjectToWorld, dir0);

     //确定要移动曲线控制移动
     #ifdef _CURVE_MOVE
         float moveVal = LoadCurveTime(time, _MovePointCount, _MovePointArray);
         //控制y轴移动
         #ifdef _MOVE_HIGHT
             dir0.y = dir0.y * moveVal;
         //控制水平移动
         #elif _MOVE_WIDTH
             dir0.xz = dir0.xz * moveVal;
         #endif
     #endif
     
     IN.vertex.xyz = dir0 + _BeginPos;

     outOnePoint(tristream, IN, time, time);
 }

朝向摄像机的方式就是根据矩阵:UNITY_MATRIX_V
矩阵含义:
UNITY_MATRIX_V[0].xyz == world space camera Right unit vector
UNITY_MATRIX_V[1].xyz == world space camera Up unit vector
UNITY_MATRIX_V[2].xyz == -1 * world space camera Forward unit vector
所以按照这个矩阵就可以直接生成一个面,这个面朝向摄像机

//封装点生成面,朝向摄像机的面,time是此时时间
void outOnePoint(inout TriangleStream<FragInput> tristream, VertexInput IN, float time){
    FragInput o[4] = (FragInput[4])0;

    float3 worldVer = IN.vertex;
    //粒子大小
    float paritcleLen = _ParticleLength;
    // 是否要开启大小跟随时间变化
    #ifdef _CURVE_SIZE
        paritcleLen *= LoadCurveTime(time, _SizePointCount, _SizePointArray);
    #endif
	//左下方
    float3 vertex = worldVer + UNITY_MATRIX_V[0].xyz * -paritcleLen 
        + UNITY_MATRIX_V[1].xyz * -paritcleLen;
    o[0].pos = mul(UNITY_MATRIX_VP, float4(vertex, 1));
    o[0].uv = float2(0.0,0.0);
    o[0].time = time;
	//左上方
    vertex = worldVer + UNITY_MATRIX_V[0].xyz * -paritcleLen 
        + UNITY_MATRIX_V[1].xyz * paritcleLen;
    o[1].pos = mul(UNITY_MATRIX_VP, float4(vertex, 1));
    o[1].uv = float2(1.0,0.0);
    o[1].time = time;
    
	//右下方
    vertex = worldVer + UNITY_MATRIX_V[0].xyz * paritcleLen 
        + UNITY_MATRIX_V[1].xyz * -paritcleLen;
    o[2].pos = mul(UNITY_MATRIX_VP, float4(vertex, 1));
    o[2].uv = float2(0.0,1.0);
    o[2].time = time;
	//右上方
    vertex = worldVer + UNITY_MATRIX_V[0].xyz * paritcleLen 
        + UNITY_MATRIX_V[1].xyz * paritcleLen;
    o[3].pos = mul(UNITY_MATRIX_VP, float4(vertex, 1));
    o[3].uv = float2(1.0,1.0);
    o[3].time = time;
	
	//将粒子平面加入流中
    tristream.Append(o[1]);
    tristream.Append(o[2]);
    tristream.Append(o[0]);
    tristream.RestartStrip();

    tristream.Append(o[1]);
    tristream.Append(o[3]);
    tristream.Append(o[2]);
    tristream.RestartStrip();
}


4.片元着色器进行着色


片元着色器只进行了简单的序列帧播放以及时间透明度处理

fixed4 SimpleFrag (FragInput i) : SV_Target
{
    //一个循环用的图片
    float time = floor( i.time.x * _RowCount * _ColumnCount );
    float row = floor ( time/_RowCount );
    float column = floor( time - row*_ColumnCount );

    float2 uv =  i.uv + float2(column, -row);
    uv.x /= _RowCount;
    uv.y /= _ColumnCount;


    fixed4 col = tex2D(_MainTex, uv) * _Color;
    //透明度处理
    #ifdef _CURVE_ALPHA
        col.a *= saturate( LoadCurveTime( i.time.x, _AlphaPointCount, _AlphaPointArray ) );
    #endif
    return col;
}

5.总代码

为了方便封装不一样的粒子移动方式,我将粒子的移动方式全部放到了Include文件中,只在Shader中进行移动计算。

#include "UnityCG.cginc"
#include "Tessellation.cginc"

struct VertexInput{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
};

struct FragInput{
    float2 uv : TEXCOORD0;
    float4 pos : SV_POSITION;
    //当前循环到的比例,开头为0,终点为1,数据设定[纹理时间,距离时间](texTime, DisTime)
    float time : TEXCOORD2;
};

sampler2D _MainTex;
float4 _MainTex_ST;
int _ParticleSize;
float _MaxDistance;
float _MoveSpeed;

float _ParticleLength;

int _RowCount;
int _ColumnCount;
fixed _IsLogToSize;

fixed4 _Color;

//移动控制
float _ShapeWidth;
float4 _MovePointArray[10];
int _MovePointCount;

//大小设置
float4 _SizePointArray[10];
int _SizePointCount;

//透明度设置
float4 _AlphaPointArray[10];
int _AlphaPointCount;

//顶点的起始位置
float4 _BeginPos;


void vert (inout VertexInput v)
{
    
}
//时间控制函数,用来读取Curve中的值
float LoadCurveTime(float nowTime, int _Count, float4 _PointArray[10]){
    //有数据才循环
    for(int i=0; i<_Count; i++){
        //找到在范围中的
        if(_PointArray[i].x < nowTime && _PointArray[i+1].x > nowTime){
            //Unity的Curve的曲线本质上是一个三次多项式插值,公式为:y = ax^3 + bx^2 + cx +d
            float a = ( _PointArray[i].w + _PointArray[i+1].z ) * ( _PointArray[i + 1].x - _PointArray[i].x ) - 2 * ( _PointArray[i + 1].y - _PointArray[i].y );
            float b = ( -2 * _PointArray[i].w - _PointArray[i + 1].z ) * ( _PointArray[i + 1].x - _PointArray[i].x ) + 3 * ( _PointArray[i + 1].y - _PointArray[i].y );
            float c = _PointArray[i].w * ( _PointArray[i + 1].x - _PointArray[i].x );
            float d = _PointArray[i].y;

            float trueTime = (nowTime - _PointArray[i].x) / ( _PointArray[i+1].x  - _PointArray[i].x);
            return a * pow( trueTime, 3 ) + b * pow( trueTime, 2 ) + c * trueTime + d;
            
        }
    }
    //返回1,表示没有变化
    return 1;
}



//判定GPU是否支持曲面细分的宏
#ifdef UNITY_CAN_COMPILE_TESSELLATION
    //曲面细分中传入进行细分部分的结构体,也就是类似于顶点传几何,传递给细分着色器控制部分进行数据判断
    struct TessVertex{
        float4 vertex : INTERNALTESSPOS;
        float3 normal : NORMAL;
        float4 tangent : TANGENT;
        float2 uv : TEXCOORD0;
    };
    //细分着色器进行控制的根据值,细分程度由这个结构体定义,因此这个结构体不能变
    struct OutputPatchConstant{
        float edge[3]        : SV_TessFactor;
        float inside         : SV_InsideTessFactor;
        float3 vTangent[4]   : TANGENT;
        float2 vUV[4]        : TEXCOORD;
        float3 vTanUCorner[4]: TANUCORNER;
        float3 vTanVCorner[4]: TANVCORNER;
        float4 vCWts         : TANWEIGHTS;
    };
    //细分着色器的顶点着色器
    TessVertex tessvert (VertexInput v){
        TessVertex o;
        o.vertex = v.vertex;
        o.normal = v.normal;
        o.tangent = v.tangent;
        o.uv = v.uv;
        return o;
    }

    OutputPatchConstant hullconst(InputPatch<TessVertex, 3>v){
        OutputPatchConstant o = (OutputPatchConstant)0;
        //获得三个顶点的细分距离值
        float4 ts = float4(_ParticleSize, _ParticleSize, _ParticleSize, _ParticleSize);
        //本质上下面的赋值操作是对细分三角形的三条边以及里面细分程度的控制
        //这个值本质上是一个int值,0就是不细分,每多1细分多一层
        //控制边缘的细分程度,这个边缘程度的值不是我们用的,而是给Tessllation进行细分控制用的
        o.edge[0] = ts.x;
        o.edge[1] = ts.y;
        o.edge[2] = ts.z;
        //内部的细分程度
        o.inside = ts.w;
        return o;
    }

    [domain("tri")]    //输入图元的是一个三角形
    //确定分割方式
    [partitioning("fractional_odd")]
    //定义图元朝向,一般用这个即可,用切线为根据
    [outputtopology("triangle_cw")]
    //定义补丁的函数名,也就是我们上面的函数,hull函数的返回值会传到这个函数中,然后进行曲面细分
    [patchconstantfunc("hullconst")]
    //定义输出图元是一个三角形,和上面对应
    [outputcontrolpoints(3)]
    TessVertex hull (InputPatch<TessVertex, 3> v, uint id : SV_OutputControlPointID){
        return v[id];
    }

    //细分后对每一个图元的计算,这下面都是标准的获取新顶点数据的方式,为了方便,我们直接处理完数据后就扔到顶点着色器上了
    [domain("tri")]
    VertexInput domain (OutputPatchConstant tessFactors, const OutputPatch<TessVertex, 3> vi, float3 bary : SV_DomainLocation){
        VertexInput v = (VertexInput)0;
        v.vertex = vi[0].vertex * bary.x + vi[1].vertex*bary.y + vi[2].vertex * bary.z;
        v.normal = vi[0].normal * bary.x + vi[1].normal*bary.y + vi[2].normal * bary.z;
        v.tangent = vi[0].tangent * bary.x + vi[1].tangent*bary.y + vi[2].tangent * bary.z;
        v.uv = vi[0].uv * bary.x + vi[1].uv*bary.y + vi[2].uv * bary.z;
        return v;
    }
#endif


//封装点生成面,朝向摄像机的面,time是此时时间
void outOnePoint(inout TriangleStream<FragInput> tristream, VertexInput IN, float time){
    FragInput o[4] = (FragInput[4])0;

    float3 worldVer = IN.vertex;
    float paritcleLen = _ParticleLength;
    // if(step( _IsCurveToSize, 0.5 ))
    #ifdef _CURVE_SIZE
        paritcleLen *= LoadCurveTime(time, _SizePointCount, _SizePointArray);
    #endif

    float3 vertex = worldVer + UNITY_MATRIX_V[0].xyz * -paritcleLen 
        + UNITY_MATRIX_V[1].xyz * -paritcleLen;
    o[0].pos = mul(UNITY_MATRIX_VP, float4(vertex, 1));
    o[0].uv = float2(0.0,0.0);
    o[0].time = time;

    vertex = worldVer + UNITY_MATRIX_V[0].xyz * -paritcleLen 
        + UNITY_MATRIX_V[1].xyz * paritcleLen;
    o[1].pos = mul(UNITY_MATRIX_VP, float4(vertex, 1));
    o[1].uv = float2(1.0,0.0);
    o[1].time = time;

    vertex = worldVer + UNITY_MATRIX_V[0].xyz * paritcleLen 
        + UNITY_MATRIX_V[1].xyz * -paritcleLen;
    o[2].pos = mul(UNITY_MATRIX_VP, float4(vertex, 1));
    o[2].uv = float2(0.0,1.0);
    o[2].time = time;

    vertex = worldVer + UNITY_MATRIX_V[0].xyz * paritcleLen 
        + UNITY_MATRIX_V[1].xyz * paritcleLen;
    o[3].pos = mul(UNITY_MATRIX_VP, float4(vertex, 1));
    o[3].uv = float2(1.0,1.0);
    o[3].time = time;

    tristream.Append(o[1]);
    tristream.Append(o[2]);
    tristream.Append(o[0]);
    tristream.RestartStrip();

    tristream.Append(o[1]);
    tristream.Append(o[3]);
    tristream.Append(o[2]);
    tristream.RestartStrip();
}

//定义位移方法
void LoadOnePoint(VertexInput IN, inout TriangleStream<FragInput> tristream);

[maxvertexcount(100)]
void geom(triangle VertexInput IN[3], inout TriangleStream<FragInput> tristream)
{
    LoadOnePoint(IN[0], tristream);
    LoadOnePoint(IN[1], tristream);
    LoadOnePoint(IN[2], tristream);
}

fixed4 SimpleFrag (FragInput i) : SV_Target
{
    //一个循环用的图片
    float time = floor( i.time.x * _RowCount * _ColumnCount );
    float row = floor ( time/_RowCount );
    float column = floor( time - row*_ColumnCount );

    float2 uv =  i.uv + float2(column, -row);
    uv.x /= _RowCount;
    uv.y /= _ColumnCount;


    fixed4 col = tex2D(_MainTex, uv) * _Color;
    
    #ifdef _CURVE_ALPHA
        col.a *= saturate( LoadCurveTime( i.time.x, _AlphaPointCount, _AlphaPointArray ) );
    #endif
    return col;
}

Shader代码

//模仿VFX版本的粒子系统
Shader "Unlit/ParticleSimulationVFX"
{
    Properties
    {
        [Header(Base Setting)]
        [Space(10)]
        _MainTex ("Texture", 2D) = "white" {}
        _ParticleSize("Particle Count", Range(0, 100)) = 10
        [HDR] _Color("Color", Color) = (1,1,1,1)
        
        _ParticleLength("Particle Length", Range(0, 3)) = 0.5
        //由于unity默认开启视锥剔除,因此需要舍弃实际世界坐标,用这里存储
        _BeginPos("Begin world Pos", Vector) = (0,0,0,0)
        _RowCount ("Row count", INT) = 1
        _ColumnCount ("Column count", INT) = 1

        [Header(Other Setting)]
        [Space(10)]
        //VFX的移动根据是用两个方向存储xyz的波动范围,然后随机选值,这里面的值即表示方向,也表示速度
        _VerticalStart("Ramdom Velocity Begin", VECTOR) = (0,0,0)
        _VerticalEnd("Ramdom Velocity End", VECTOR) = (0,0,0)

        _LifeTime("Particle Life Time", FLOAT) = 1
        
    }
    SubShader
    {
        Tags { "Queue"="Transparent" "RenderType" = "Opaque" }

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off
            Cull Off
            CGPROGRAM
            #pragma target 4.6
            #pragma vertex tessvert
            #pragma fragment SimpleFrag
            #pragma geometry geom
            //细分控制
            #pragma hull hull
            //细分计算
            #pragma domain domain
            #pragma multi_compile_fog
            #pragma shader_feature  _CURVE_MOVE
            #pragma shader_feature  _MOVE_HIGHT
            #pragma shader_feature  _MOVE_WIDTH
            #pragma shader_feature  _CURVE_SIZE
            #pragma shader_feature  _CURVE_ALPHA


            //GPU实例化
            #pragma multi_compile_instancing
            #include "ParticleInclude.cginc"

            float3 _VerticalStart;
            float3 _VerticalEnd;
            float _LifeTime;

            
            void LoadOnePoint(VertexInput IN, inout TriangleStream<FragInput> tristream){
                //随机xy值
                float4 ramdom = 0;
                ramdom.x = frac( abs( cos( (IN.vertex.x + IN.vertex.z)  * 100000 ) ) * 1000 );
                ramdom.y = frac( abs( sin( (IN.vertex.y - IN.vertex.z )  * 100000 ) ) * 1000 );
                ramdom.z = frac( abs( sin( (ramdom.x + ramdom.y )  * 100000 ) ) * 1000 );

                ramdom.w = frac( abs( sin( (ramdom.x + ramdom.y + ramdom.z) * UNITY_PI * 100000 ) ) * 1000 );

                float3 dir0 = lerp(_VerticalStart, _VerticalEnd, ramdom.xyz);

                float addTime = _LifeTime * ramdom.w;

                //归一化
                float time = fmod( (_Time.y + addTime), _LifeTime) ;
                
                //控制移动方向
                dir0 *= time;

                time /= _LifeTime;

                dir0 = mul((float3x3)unity_ObjectToWorld, dir0);

                //确定要移动曲线控制移动
                #ifdef _CURVE_MOVE
                    float moveVal = LoadCurveTime(time, _MovePointCount, _MovePointArray);
                    //控制y轴移动
                    #ifdef _MOVE_HIGHT
                        dir0.y = dir0.y * moveVal;
                    //控制水平移动
                    #elif _MOVE_WIDTH
                        dir0.xz = dir0.xz * moveVal;
                    #endif
                #endif
                
                IN.vertex.xyz = dir0 + _BeginPos;

                outOnePoint(tristream, IN, time);
            }

            ENDCG
        }
    }
}

代码的曲线传递方式:
这里需要说明,由于Unity默认只带视锥体剔除,这个我不知道怎么设置哪些物体不进行剔除,因此我是将这个模型一直移动到摄像机的前方,同时在shader中设置起始的世界坐标,让起始点保持不变。

  private void SetMatValue()
  {
      Material ma = particleBase.material;
      if (ma == null) return;
      Vector4[] vector4;

      //设置移动
      vector4 = new Vector4[widthCurve.length];
      for (int i = 0; i < widthCurve.length; i++)
      {
          vector4[i] = new Vector4(widthCurve.keys[i].time, widthCurve.keys[i].value,
               widthCurve.keys[i].inTangent, widthCurve.keys[i].outTangent);
      }


      if (CurveMove) ma.EnableKeyword("_CURVE_MOVE");
      else ma.DisableKeyword("_CURVE_MOVE");

      if (CurveHight) ma.EnableKeyword("_MOVE_HIGHT");
      else ma.DisableKeyword("_MOVE_HIGHT");

      if (CurveWidth) ma.EnableKeyword("_MOVE_WIDTH");
      else ma.DisableKeyword("_MOVE_WIDTH");

      ma.SetInt("_MovePointCount", widthCurve.length);
      ma.SetVectorArray("_MovePointArray", vector4);


      //设置大小
      vector4 = new Vector4[sizeCurve.length];
      for (int i = 0; i < sizeCurve.length; i++)
      {
          vector4[i] = new Vector4(sizeCurve.keys[i].time, sizeCurve.keys[i].value,
              sizeCurve.keys[i].inTangent, sizeCurve.keys[i].outTangent);
      }


      if (CurveSize)
          ma.EnableKeyword("_CURVE_SIZE");
      else ma.DisableKeyword("_CURVE_SIZE");
      ma.SetInt("_SizePointCount", sizeCurve.length);
      ma.SetVectorArray("_SizePointArray", vector4);



      //设置透明度
      vector4 = new Vector4[alphaCurve.length];
      for (int i = 0; i < alphaCurve.length; i++)
      {
          vector4[i] = new Vector4(alphaCurve.keys[i].time, alphaCurve.keys[i].value,
              alphaCurve.keys[i].inTangent, alphaCurve.keys[i].outTangent);
      }

      if (CurveAlpha) ma.EnableKeyword("_CURVE_ALPHA");
      else ma.DisableKeyword("_CURVE_ALPHA");
      ma.SetInt("_AlphaPointCount", alphaCurve.length);
      ma.SetVectorArray("_AlphaPointArray", vector4);

      //设置位置
      ma.SetVector("_BeginPos", transform.position);

  }

总结

这里实际上就是提供了一种模拟粒子系统的方式,效果还是不错的,能够基本满足VFX的部分效果,速度也很可观,至少比Build-in的粒子系统快,就是这种方式是关闭了深度的,很容易出现深度问题,这是一个需要修改的问题。
同时使用GPU实例化应该也可以对速度有一定的优化,但是这里没加,因为不清楚细分着色器如何给实例化的定义进行赋值,是不需要赋值吗,之前试了一下,好像没什么变化,就先这样吧。
而且可能是我GPU比较好,这种计算方式对GPU的损耗依旧不大,但是由于在片元着色器输出给屏幕时会涉及CPU的运算,在片元反复输出粒子像素颜色的情况下对CPU的损耗会很恐怖,所以这部分也需要优化,感觉这个是一个大坑啊。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值