[Shader/渲染] 【转载】Unity自定义粒子顶点流

查看:1377 |回复:2 | 2019-11-25 10:54:03

您需要 登录 才可以下载或查看,没有账号?注册

x
来自加拿大游戏特效大神Mirza Beig的粒子系统的系列教程,简单易懂,从而受到开发者欢迎,不少开发者按照教程动手实践出精美的特效场景。今天我们将分享新的教程-Unity自定义粒子顶点流。


20191125-105248.jpg



在本教程中,我们将学习如何在Unity粒子系统中使用自定义顶点流(Vertex Streams)。顶点流通过粒子系统的Renderer模块来设置,它可以将额外的单个粒子数据传递到着色器。

着色器可以使用该数据,为系统中的每个粒子创建各种独特的效果,所有粒子都会在GPU上以极快的速度处理。

最终的效果场景如下图所示,虽然本文实现的效果不是非常惊艳,但它为后续教程中学习创作精美特效奠定基础。



Part 1:基础部分

首先,我们使用Unity模板创建一个简单的无光粒子着色器。在项目窗口中单击右键,选择Create -> Shader -> Unlit Shader。



  我们将该文件命名为Simple Particle Unlit,代码如下图所示。


  1. Shader "Unlit/Simple Particle Unlit"
  2. {
  3.     Properties
  4.     {
  5.         _MainTex ("Texture", 2D) = "white" {}
  6.     }
  7.     SubShader
  8.     {
  9.         Tags { "RenderType"="Opaque" }
  10.         LOD 100

  11.         Pass
  12.         {
  13.             CGPROGRAM
  14.             #pragma vertex vert
  15.             #pragma fragment frag
  16.             // 实现模糊效果
  17.             #pragma multi_compile_fog

  18.             #include "UnityCG.cginc"

  19.             struct appdata
  20.             {
  21.                 float4 vertex : POSITION;
  22.                 float2 uv : TEXCOORD0;
  23.             };

  24.             struct v2f
  25.             {
  26.                 float2 uv : TEXCOORD0;
  27.                 UNITY_FOG_COORDS(1)
  28.                 float4 vertex : SV_POSITION;
  29.             };

  30.             sampler2D _MainTex;
  31.             float4 _MainTex_ST;

  32.             v2f vert (appdata v)
  33.             {
  34.                 v2f o;
  35.                 o.vertex = UnityObjectToClipPos(v.vertex);
  36.                 o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
  37.                 UNITY_TRANSFER_FOG(o,o.vertex);
  38.                 return o;
  39.             }

  40.             fixed4 frag (v2f i) : SV_Target
  41.             {
  42.                 // 采样纹理
  43.                 fixed4 col = tex2D(_MainTex, i.uv);
  44.                 // 应用模糊效果
  45.                 UNITY_APPLY_FOG(i.fogCoord, col);
  46.                 return col;
  47.             }
  48.             ENDCG
  49.         }
  50.     }
  51. }
点击此处复制文本


创建新材质,指定着色器,然后设置纹理属性为默认粒子纹理。



现在我们创建粒子系统,指定该材质到粒子系统Renderer模块的Materials字段。



我们会得到下图的效果。



图中的效果存在一些透明度问题,我们稍后会解决这些问题。

在相同粒子系统的Renderer模块中,勾选Custom Vertex Streams启用自定义顶点流,然后单击右下方的“+”按钮,添加Lifetime分类中的AgePercent流。

AgePercent是1D值,表示标准化范围[0.0, 1.0]内粒子的“生命周期”。0.0表示粒子生成时间,0.5表示粒子生命周期终点,1.0表示粒子消逝时间。



我们忽略顶点流与着色器输入不匹配红色警告信息,现在将顶点流传给着色器,我们需要接收并处理顶点流的数据。

在顶点流显示中,可以注意到数据已被紧凑地打包了。实际2D UV坐标位于TEXCOORD0.xy,AgePercent数据位于TEXCOORD0.z。要记住这些信息,以便我们知道在着色器中何处以及如何获取此数据。



每个texcoord都可以是4D向量,即CG/HLSL代码中,以[x, y, z, w]形式保存的float4变量。如果我们要添加额外的1D流,它将位于TEXCOORD0.w。如果数据比当前texcoord的可用空间大,它会作为余下部分移动到下一个texcoord,例如texcoord1或texcoord2等。

下图是相应的设置案例,里面没有添加这些顶点流。



我们可以看到,InverseStartLifetime(1D值)被添加到TEXCOORD0.w,Center(3D值)被添加到TEXCOORD1.xyz,Rotation3D(3D值)一部分被添加到TEXCOORD1 (w)。

另一部分被添加到TEXCOORD2 (xy)。(w|xy)表示xy属于下一个texcoord,即TEXCOORD2,尽管它显示TEXCOORD1.w|xy。因此Velocity从TEXCOORD2.zw开始,而Rotation3D有一部分存在TEXCOORD1.xy中,Velocity也有一部分存在TEXCOORD3 (x)中。

这可能有点难理解,因为Rotation3D的xyz值存在TEXCOORD1.w(Rotation3D的x)和TEXCOORD2.xy(Rotation3D的yz)中。它类似对Velocity中xyz的处理,Velocity的xyz存在TEXCOORD2.zw (xy)和TEXCOORD3.x (z)中。

现在关注AgePercent,回到我们的自定义着色器,开始进行处理。

该着色器只处理TEXCOORD0的x和y以获得实际纹理的UV坐标。AgePercent位于TEXCOORD0.z,因此我们需要在顶点输入和输出结构,分别为appdata和v2f将float2改为float3。

查看下面的代码,了解改动内容。


  1. struct appdata
  2. {
  3.     float4 vertex : POSITION;
  4.     float3 uv : TEXCOORD0;
  5. };

  6. struct v2f
  7. {
  8.     float3 uv : TEXCOORD0;
  9.     UNITY_FOG_COORDS(1)
  10.     float4 vertex : SV_POSITION;
  11. };
点击此处复制文本


接下来,我们需要在着色器的顶点部分初始化UV,使UV在被传递到片段部分前包含合适的数值,这些“部分”其实是一个.shader文件中的顶点着色器和片段/像素着色器。在栅格化前,先处理顶点操作。


  1. v2f vert (appdata v)
  2. {
  3.     v2f o;
  4.     o.vertex = UnityObjectToClipPos(v.vertex);
  5.     o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);               
  6.      
  7.     // 初始化当前uv.z(包含粒子的寿命百分比)
  8.     o.uv.z = v.uv.z;

  9.     UNITY_TRANSFER_FOG(o,o.vertex);
  10.     return o;
  11. }
点击此处复制文本


最后在片段部分,即给对象上色的像素着色器部分,我们可以利用该数据。下面代码中,我们根据粒子寿命,使用该数据向纹理(col)的粒子插补红色。


  1. fixed4 frag (v2f i) : SV_Target
  2. {
  3.     // 采样纹理
  4.     fixed4 col = tex2D(_MainTex, i.uv);

  5.     float particleAgePercent = i.uv.z;
  6.     float4 colourRed = float4(1, 0, 0, 1);

  7.     // 根据粒子寿命百分比,将纹理颜色插值为红色

  8.     col = lerp(col, colourRed * col.a, particleAgePercent);

  9.     //  应用模糊效果
  10.     UNITY_APPLY_FOG(i.fogCoord, col);
  11.     return col;
  12. }
点击此处复制文本


经过修改后,以下是完整的着色器代码。


  1. Shader "Unlit/Simple Particle Unlit"
  2. {
  3.     Properties
  4.     {
  5.         _MainTex ("Texture", 2D) = "white" {}
  6.     }
  7.     SubShader
  8.     {
  9.         Tags { "RenderType"="Opaque" }
  10.         LOD 100

  11.         Pass
  12.         {
  13.             CGPROGRAM
  14.             #pragma vertex vert
  15.             #pragma fragment frag
  16.             // 实现模糊效果
  17.             #pragma multi_compile_fog

  18.             #include "UnityCG.cginc"

  19.             struct appdata
  20.             {
  21.                 float4 vertex : POSITION;
  22.                 float3 uv : TEXCOORD0;
  23.             };

  24.             struct v2f
  25.             {
  26.                 float3 uv : TEXCOORD0;
  27.                 UNITY_FOG_COORDS(1)
  28.                 float4 vertex : SV_POSITION;
  29.             };

  30.             sampler2D _MainTex;
  31.             float4 _MainTex_ST;

  32.             v2f vert (appdata v)
  33.             {
  34.                 v2f o;
  35.                 o.vertex = UnityObjectToClipPos(v.vertex);
  36.                 o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);

  37.                 // 初始化当前uv.z(包含粒子的寿命百分比)

  38.                 o.uv.z = v.uv.z;

  39.                 UNITY_TRANSFER_FOG(o,o.vertex);
  40.                 return o;
  41.             }

  42.             fixed4 frag (v2f i) : SV_Target
  43.             {
  44.                 // 采样纹理
  45.                 fixed4 col = tex2D(_MainTex, i.uv);

  46.                 float particleAgePercent = i.uv.z;
  47.                 float4 colourRed = float4(1, 0, 0, 1);

  48.                 // 根据粒子寿命百分比,从纹理颜色插值为红色

  49.                 col = lerp(col, colourRed * col.a, particleAgePercent);

  50.                 // 应用模糊效果
  51.                 UNITY_APPLY_FOG(i.fogCoord, col);
  52.                 return col;
  53.             }
  54.             ENDCG
  55.         }
  56.     }
  57. }
点击此处复制文本


我们得到了下图的结果,或许它并不惊艳,但这仅只是开始。



Part 2:透明度,深度测试及渲染队列

继续下一步前,我们先解决之前出现的问题,从透明度开始。为了从输入纹理获取合适的Alpha值,只需添加混合模式即可。

我们可以选择常用命令,例如:加法(One One命令),Alpha混合(SrcAlpha OneMinusSrcAlpha)和Alpha混合预乘(One OneMinusSrcAlpha)。对黑色背景上纹理(例如默认粒子纹理)的最好选择是加法和预乘。

我们选择加法,因为它最直接,在黑暗场景中效果最好,并且和HDR 和 阈值泛光的结合效果很好,因为像素会通过加法互相叠加。


  1. Tags { "RenderType"="Opaque" }
  2. LOD 100

  3. Blend One One // 加法混合
点击此处复制文本


返回到Unity编辑器,我们增大了粒子大小,以突出目前存在的一个显示问题。虽然粒子通过加法混合清楚地渲染了出来,渲染效果就像液滴或熔岩灯,但它们没有半透明效果,而且公告牌四边形的轮廓很清楚。



为了解决该问题,我们需要禁用深度测试。


  1. Tags { "RenderType"="Opaque" }
  2. LOD 100

  3. Blend One One // 加法混合
  4. ZWrite Off //关闭深度测试
点击此处复制文本


如下图所示,处理方法很有效。



下图是没有修改粒子大小时,应该呈现的效果。



虽然可能效果不太明显,但我们的着色器仍然会在场景中对其它透明对象进行排序,例如精灵。解决这个问题很简单,只需将材质上的渲染队列更改为透明层即可。



我们可以添加Queue = Transparent标记从着色器自动执行此操作。


  1. Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
点击此处复制文本


Part 3:顶点颜色和着色

下面我们来解决顶点流不匹配着色器输入的警告。



最简单的方法是用过在编辑器选择Color流,单击“+”旁边的“-”按钮,移除Color流,这样问题就解决了。



但是本文想说明,我们应该了解真正解决该错误,而不是简单的删除Color流。

熟悉Unity粒子系统的基础知识的开发者,应该知道我们可以定义所有粒子初始化时的起始颜色,生命周期颜色和随速度变化的颜色。在当前着色器中,这些设置没有任何效果,因为数据是通过COLOR顶点输入传递的。

这些警告实际在告诉我们,着色器中没有地方接收该数据,即使粒子系统已设置为发送数据。因此当我们移除它时,警告就消失了。如果在着色器中接收Color流,但并不发送该数据,我们也会得到相同的警告。

现在我们更新着色器,以便从粒子系统接收Color输入流。


  1. struct appdata
  2. {
  3.     float4 vertex : POSITION;
  4.     fixed4 color : COLOR;
  5.     float3 uv : TEXCOORD0;
  6. };

  7. struct v2f
  8. {
  9.     float3 uv : TEXCOORD0;
  10.     UNITY_FOG_COORDS(1)
  11.     float4 vertex : SV_POSITION;
  12.     fixed4 color : COLOR;
  13. };
点击此处复制文本


然后我们将v2f struct和输入初始化到顶点部分。


  1. v2f vert (appdata v)
  2. {
  3.     v2f o;
  4.     o.vertex = UnityObjectToClipPos(v.vertex);

  5.     // 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色。
  6.     o.color = v.color;
  7.     o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
  8.      
  9.     // 初始化uv.z(它保存了粒子寿命百分比)
  10.     o.uv.z = v.uv.z;

  11.     UNITY_TRANSFER_FOG(o,o.vertex);
  12.     return o;
  13. }
点击此处复制文本


返回到Unity编辑器的粒子系统设置,警告已经消失了。  



现在将Main模块的Start Color设为蓝色。



可能你已经明白了,但是这样做不会改变什么。因为我们接收了数据,但还没在片段部分处理数据。

现在修改设置,使粒子系统组件的颜色对纹理颜色进行着色,然后再插补为红色。


  1. fixed4 frag (v2f i) : SV_Target
  2. {
  3.     // 采样纹理
  4.     fixed4 col = tex2D(_MainTex, i.uv);

  5.     //让纹理颜色和粒子系统的顶点颜色输入相乘
  6.     col *= i.color;

  7.     float particleAgePercent = i.uv.z;
  8.     float4 colourRed = float4(1, 0, 0, 1);

  9.     // 根据粒子寿命百分比从纹理颜色插值为红色
  10.     col = lerp(col, colourRed * col.a, particleAgePercent);

  11.     // 应用模糊效果
  12.     UNITY_APPLY_FOG(i.fogCoord, col);
  13.     return col;
  14. }
点击此处复制文本


现在Unity编辑器中,我们可以看见下图画面,和预期一样,粒子首先会被着色为蓝色。



我们已经成功编写好了粒子着色器,它可以处理自定义顶点流,下面是完整的着色器代码。


  1. Shader "Unlit/Simple Particle Unlit"
  2. {
  3.     Properties
  4.     {
  5.         _MainTex ("Texture", 2D) = "white" {}
  6.     }
  7.     SubShader
  8.     {
  9.         Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
  10.         LOD 100

  11.         Blend One One // 加法混合
  12.         ZWrite Off // 关闭深度测试

  13.         Pass
  14.         {
  15.             CGPROGRAM
  16.             #pragma vertex vert
  17.             #pragma fragment frag
  18.             // 实现模糊效果
  19.             #pragma multi_compile_fog

  20.             #include "UnityCG.cginc"

  21.             struct appdata
  22.             {
  23.                 float4 vertex : POSITION;
  24.                 fixed4 color : COLOR;
  25.                 float3 uv : TEXCOORD0;
  26.             };

  27.             struct v2f
  28.             {
  29.                 float3 uv : TEXCOORD0;
  30.                 UNITY_FOG_COORDS(1)
  31.                 float4 vertex : SV_POSITION;
  32.                 fixed4 color : COLOR;
  33.             };

  34.             sampler2D _MainTex;
  35.             float4 _MainTex_ST;

  36.             v2f vert (appdata v)
  37.             {
  38.                 v2f o;
  39.                 o.vertex = UnityObjectToClipPos(v.vertex);

  40.                 // 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色
  41.                 o.color = v.color;
  42.                 o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
  43.                  
  44.                 // 初始化当前uv.z(包含粒子的寿命百分比)
  45.                 o.uv.z = v.uv.z;

  46.                 UNITY_TRANSFER_FOG(o,o.vertex);
  47.                 return o;
  48.             }

  49.             fixed4 frag (v2f i) : SV_Target
  50.             {
  51.                 // 采样纹理
  52.                 fixed4 col = tex2D(_MainTex, i.uv);

  53.                 // 让纹理颜色和粒子系统的顶点颜色输入相乘
  54.                 col *= i.color;

  55.                 float particleAgePercent = i.uv.z;
  56.                 float4 colourRed = float4(1, 0, 0, 1);

  57.                 //根据粒子寿命百分比,从纹理颜色插值为红色
  58.                 col = lerp(col, colourRed * col.a, particleAgePercent);

  59.                 // 应用模糊效果
  60.                 UNITY_APPLY_FOG(i.fogCoord, col);
  61.                 return col;
  62.             }
  63.             ENDCG
  64.         }
  65.     }
  66. }
点击此处复制文本


Part 4:顶点动画

效果依旧不是很特别,因为现在可以接收并处理粒子系统的输入颜色,所以我们可以通过将Start Color恢复为纯白色,注释掉将col插补为红色的代码,并使用Color over Lifetime模块在标准化粒子生命周期将红色过渡为蓝色,从而获得完全相同的结果。



如果我们只是为了制作一个自定义着色器,它应该能实现更多功能。现在我们要实现无法通过修改组件设置或在CPU编程来轻松实现的效果。

我们已经学习如何使用自定义顶点流处理渲染粒子的像素。接下来处理它的顶点。在顶点部分中,在修改位置的代码前,即代码o.vertex = UnityObjectToClipPos(v.vertex),添加这部分代码。


  1. // 奇怪的顶点动画
  2. float sineFrequency = 5.0;
  3. float sineAmplitude = 4.0;

  4. float sineOffset = sin(_Time.y * sineFrequency) * sineAmplitude;
  5. float agePercent = v.uv.z;

  6. float3 vertexOffset = float3(0, sineOffset * agePercent, 0);
  7. v.vertex.xyz += vertexOffset;
点击此处复制文本


注意:_Time是Unity着色器内置的4D变量,_Time的Y值没有更改。我们也可以使用_SinTime.w,它是未更改的时间正弦。

我们使用正弦波创建了动画,其中Y偏移被调整为正弦偏移和粒子寿命的乘积。粒子寿命越大,偏移越大,于是得到了下图效果。



通过将基础粒子系统改为粒子弹簧,我们可以让效果没那么奇怪。在得到正常运行的顶点动画后,为粒子系统实现粒子弹簧效果非常简单。

Part 5:粒子弹簧

首先,新粒子系统的默认Rotation X值为-90,请确保将其重置为0。



在Main模块,勾选Prewarm,将Start Lifetime设为4,Start Speed设为0。



完全禁用Shape模块。因为我们想要完美的点发射器。



最后启用Velocity over Lifetime模块,并按下图进行设置。Linear Y设为2,Orbital Y设为8,Offset X设为1。这样一来,粒子会上升,绕着本地Y轴,将Space设为Local旋转,由于旋转中心在X轴偏移1个单位,它会实现螺旋图案。

通过修改以上设置,我们可以控制弹簧的外形。



下图是得到的效果,弹簧会上下移动。



如果想要多个弹簧粒子,应该怎样处理呢?因为该着色器使用和噪声相同的输入,无论材质如何,每个弹簧都有相同的动画效果。我们需要一种方法来根据材质指定偏移。

现在返回到着色器代码,在主纹理下添加偏移时间的新属性。我们在此创建了一个数值滑块,调整范围在0.0和100.0之间,默认值为0.0,即无偏移。


  1. _MainTex("Texture", 2D) = "white" {}
  2. _TimeOffset("Noise Offset", Range(0, 100)) = 0.0
点击此处复制文本




然后,添加时间偏移变量。


  1. sampler2D _MainTex;
  2. float4 _MainTex_ST;

  3. uniform float _TimeOffset;
点击此处复制文本


最后将偏移值与_Time.y相加,将得到的结果存为新变量,以使代码更清楚,接下来将该变量传到正弦函数中,使它在函数中与频率相乘。


  1. float time = _Time.y + _TimeOffset;
  2. float sineOffset = sin(time * sineFrequency) * sineAmplitude;
点击此处复制文本


现在,我们只需要复制粒子系统和材质,然后修改刚添加的偏移属性即可。创建当前粒子系统的副本,然后确保每个副本有独特的材质和不同的偏移值,并使用相同的着色器。

我们完成的效果如下图所示。



着色器代码

完整的着色器代码如下。


  1. Shader "Unlit/Simple Particle Unlit"
  2. {
  3.     Properties
  4.     {
  5.         _MainTex("Texture", 2D) = "white" {}
  6.         _TimeOffset("Noise Offset", Range(0, 100)) = 0.0
  7.     }
  8.     SubShader
  9.     {
  10.         Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
  11.         LOD 100

  12.         Blend One One // 加法混合
  13.         ZWrite Off // 关闭深度测试

  14.         Pass
  15.         {
  16.             CGPROGRAM
  17.             #pragma vertex vert
  18.             #pragma fragment frag
  19.             // 实现模糊效果
  20.             #pragma multi_compile_fog

  21.             #include "UnityCG.cginc"

  22.             struct appdata
  23.             {
  24.                 float4 vertex : POSITION;
  25.                 fixed4 color : COLOR;
  26.                 float3 uv : TEXCOORD0;
  27.             };

  28.             struct v2f
  29.             {
  30.                 float3 uv : TEXCOORD0;
  31.                 UNITY_FOG_COORDS(1)
  32.                 float4 vertex : SV_POSITION;
  33.                 fixed4 color : COLOR;
  34.             };

  35.             sampler2D _MainTex;
  36.             float4 _MainTex_ST;

  37.             float _TimeOffset;

  38.             v2f vert(appdata v)
  39.             {
  40.                 v2f o;

  41.                 // 奇怪的定点动画
  42.                 float sineFrequency = 5.0;
  43.                 float sineAmplitude = 4.0;

  44.                 float time = _Time.y + _TimeOffset;
  45.                 float sineOffset = sin(time * sineFrequency) * sineAmplitude;

  46.                 float agePercent = v.uv.z;

  47.                 float3 vertexOffset = float3(0, sineOffset * agePercent, 0);

  48.                 v.vertex.xyz += vertexOffset;
  49.                 o.vertex = UnityObjectToClipPos(v.vertex);

  50.                 // 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色
  51.                 o.color = v.color;

  52.                 o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);

  53.                 // 初始化tex coord变量
  54.                 o.uv.z = v.uv.z;

  55.                 UNITY_TRANSFER_FOG(o,o.vertex);
  56.                 return o;
  57.             }

  58.             fixed4 frag(v2f i) : SV_Target
  59.             {
  60.                 //采样纹理
  61.                 fixed4 col = tex2D(_MainTex, i.uv);

  62.                 // 让纹理颜色和粒子系统的顶点颜色输入相乘
  63.                 col *= i.color;

  64.                 float particleAgePercent = i.uv.z;
  65.                 float4 colourRed = float4(1, 0, 0, 1);

  66.                 // 根据粒子寿命百分比,从纹理颜色插值为红色
  67.                 col = lerp(col, colourRed * col.a, particleAgePercent);

  68.                 // 应用模糊效果
  69.                 UNITY_APPLY_FOG(i.fogCoord, col);
  70.                 return col;
  71.             }
  72.             ENDCG
  73.         }
  74.     }
  75. }
点击此处复制文本


小结

本文帮助你了解如何在Unity使用自定义粒子顶点流





评分

参与人数 1元素币 +10 活跃度 +12 展开 理由
源支始 + 10 + 12

查看全部评分

2019-11-25 10:54:03  
 赞 赞 0

使用道具 登录

2个回答,把该问题分享到群,邀请大神一起回答。
2#
好帖~顶!!!!!!!!!
回复 收起回复
2019-11-25 11:30:54   回复
 赞 赞 0

使用道具 登录

3#
顶顶顶
回复 收起回复
2019-11-25 13:31:22   回复
 赞 赞 0

使用道具 登录

CG 游戏行业专业问题

手机游戏引擎手游引擎Unity3D技术
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表