找回密码
 立即注册
查看: 454|回复: 0

[笔记] Unity自定义粒子顶点流 P1

[复制链接]
发表于 2021-12-6 10:53 | 显示全部楼层 |阅读模式
在本教程中,我们将学习如何在Unity粒子系统中使用自定义顶点流(Vertex Streams)。顶点流通过粒子系统的Renderer模块来设置,它可以将额外的单个粒子数据传递到着色器。着色器可以使用该数据,为系统中的每个粒子创建各种独特的效果,所有粒子都会在GPU上以极快的速度处理。最终的效果场景如下图所示,虽然本文实现的效果不是非常惊艳,但它为后续教程中学习创作精美特效奠定基础。
对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀~



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



我们将该文件命名为Simple Particle Unlit,代码如下图所示。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 实现模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 采样纹理
fixed4 col = tex2D(_MainTex, i.uv);
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
创建新材质,指定着色器,然后设置纹理属性为默认粒子纹理。



现在我们创建粒子系统,指定该材质到粒子系统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。
查看下面的代码,了解改动内容。
struct appdata
{
float4 vertex : POSITION;
float3 uv : TEXCOORD0;
};
struct v2f
{
float3 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
接下来,我们需要在着色器的顶点部分初始化UV,使UV在被传递到片段部分前包含合适的数值,这些“部分”其实是一个.shader文件中的顶点着色器和片段/像素着色器。在栅格化前,先处理顶点操作。
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
// 初始化当前uv.z(包含粒子的寿命百分比)
o.uv.z = v.uv.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
最后在片段部分,即给对象上色的像素着色器部分,我们可以利用该数据。下面代码中,我们根据粒子寿命,使用该数据向纹理(col)的粒子插补红色。
fixed4 frag (v2f i) : SV_Target
{
// 采样纹理
fixed4 col = tex2D(_MainTex, i.uv);
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根据粒子寿命百分比,将纹理颜色插值为红色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
经过修改后,以下是完整的着色器代码。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 实现模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 uv : TEXCOORD0;
};
struct v2f
{
float3 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
// 初始化当前uv.z(包含粒子的寿命百分比)
o.uv.z = v.uv.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 采样纹理
fixed4 col = tex2D(_MainTex, i.uv);
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根据粒子寿命百分比,从纹理颜色插值为红色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我们得到了下图的结果,或许它并不惊艳,但这仅只是开始。



Part 2:透明度,深度测试及渲染队列
继续下一步前,我们先解决之前出现的问题,从透明度开始。为了从输入纹理获取合适的Alpha值,只需添加混合模式即可。
我们可以选择常用命令,例如:加法(One One命令),Alpha混合(SrcAlpha OneMinusSrcAlpha)和Alpha混合预乘(One OneMinusSrcAlpha)。对黑色背景上纹理(例如默认粒子纹理)的最好选择是加法和预乘。
我们选择加法,因为它最直接,在黑暗场景中效果最好,并且和HDR 和 阈值泛光的结合效果很好,因为像素会通过加法互相叠加。
Tags { "RenderType"="Opaque" }
LOD 100
Blend One One // 加法混合
返回到Unity编辑器,我们增大了粒子大小,以突出目前存在的一个显示问题。虽然粒子通过加法混合清楚地渲染了出来,渲染效果就像液滴或熔岩灯,但它们没有半透明效果,而且公告牌四边形的轮廓很清楚。



为了解决该问题,我们需要禁用深度测试。
Tags { "RenderType"="Opaque" }
LOD 100
Blend One One // 加法混合
ZWrite Off //关闭深度测试
如下图所示,处理方法很有效。



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



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



我们可以添加Queue = Transparent标记从着色器自动执行此操作。
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
Part 3:顶点颜色和着色
下面我们来解决顶点流不匹配着色器输入的警告。



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



但是本文想说明,我们应该了解真正解决该错误,而不是简单的删除Color流。
熟悉Unity粒子系统的基础知识的开发者,应该知道我们可以定义所有粒子初始化时的起始颜色,生命周期颜色和随速度变化的颜色。在当前着色器中,这些设置没有任何效果,因为数据是通过COLOR顶点输入传递的。
这些警告实际在告诉我们,着色器中没有地方接收该数据,即使粒子系统已设置为发送数据。因此当我们移除它时,警告就消失了。如果在着色器中接收Color流,但并不发送该数据,我们也会得到相同的警告。
现在我们更新着色器,以便从粒子系统接收Color输入流。
struct appdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float3 uv : TEXCOORD0;
};
struct v2f
{
float3 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
然后我们将v2f struct和输入初始化到顶点部分。
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色。
o.color = v.color;
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
// 初始化uv.z(它保存了粒子寿命百分比)
o.uv.z = v.uv.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
返回到Unity编辑器的粒子系统设置,警告已经消失了。



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



可能你已经明白了,但是这样做不会改变什么。因为我们接收了数据,但还没在片段部分处理数据。
现在修改设置,使粒子系统组件的颜色对纹理颜色进行着色,然后再插补为红色。
fixed4 frag (v2f i) : SV_Target
{
// 采样纹理
fixed4 col = tex2D(_MainTex, i.uv);
//让纹理颜色和粒子系统的顶点颜色输入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
// 根据粒子寿命百分比从纹理颜色插值为红色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
现在Unity编辑器中,我们可以看见下图画面,和预期一样,粒子首先会被着色为蓝色。



我们已经成功编写好了粒子着色器,它可以处理自定义顶点流,下面是完整的着色器代码。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
LOD 100
Blend One One // 加法混合
ZWrite Off // 关闭深度测试
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 实现模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float3 uv : TEXCOORD0;
};
struct v2f
{
float3 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色
o.color = v.color;
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
// 初始化当前uv.z(包含粒子的寿命百分比)
o.uv.z = v.uv.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 采样纹理
fixed4 col = tex2D(_MainTex, i.uv);
// 让纹理颜色和粒子系统的顶点颜色输入相乘
col *= i.color;
float particleAgePercent = i.uv.z;
float4 colourRed = float4(1, 0, 0, 1);
//根据粒子寿命百分比,从纹理颜色插值为红色
col = lerp(col, colourRed * col.a, particleAgePercent);
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}

本帖子中包含更多资源

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

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-10 05:03 , Processed in 0.091123 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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