锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

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

时间:2022-09-19 11:30:00 流体连接器组件

文章目录

  • 前言
  • 一、VFX实现分析
  • 二、开始实现
    • 1.生成平面
    • 2.曲面细分初始顶点
    • 3.几何着色器处理顶点
    • 4.片元着色器着色
    • 5.总代码
  • 总结


前言

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

VFX显示模拟效果


一、VFX实现分析

(因为我不是专门做特效的,请轻轻喷一些分析错误)
只模拟了本文的模拟VFX部分实现,用于完成项目需求,模拟本文实现的雾效果。

VFX的雾效初始化

图片可以找到VFX首先,颗粒将从开始声明的粒子容器中创建,然后输出到颗粒初始化部分进行初始化设置,因此需要时间、方向和颜色属性。这部分作者使用曲面细分着色器来控制粒子的数量,并使用几何着色器来设置速度。
(按照VFX就设计而言,应该可以GPU固定生成属性,但由于没有,只能在几何着色器中设置速度)

不懂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;
元器件数据手册IC替代型号,打造电子元器件IC百科大全!
          

相关文章