在Unity中只使用shader实现的粒子动画
前言最近想实现一个降雪效果,考虑到particles带来的cpu负担,决定只用shader来实现类似效果,渲染阶段cpu端不参与渲染之外的事情,shader中管理粒子的生命周期和运动轨迹。
最终效果:
https://www.zhihu.com/video/1565759442431221760
模型准备
要想达到目标需求,需要特制模型数据,这里在C#端生成所需的模型数据。这里虽然称之为“模型”但实际储存的是生成模型的参数(BillBoard),并不能直接用于一般的渲染,需要与对应shader配合才能渲染内容。
Mesh fallMesh
{
get
{
Mesh mesh = new Mesh();
mesh.name = "WeatherFall";
Vector3 size = new Vector3(300, 200, 300); // 范围,作为算法验证这里直接用固定数值了,以下同
int count = 10; // 粒子在棱方向的数量
int total_count = count * count * count;
Vector3[] vertices = new Vector3;
Vector2[] uvs = new Vector2;
int[] indices = new int;
float halfsize = (count - 1) * 0.5f;
Vector3 center = new Vector3(halfsize, halfsize, halfsize);
for (int z = 0; z < count; z++)
{
for (int y = 0; y < count; y++)
{
for (int x = 0; x < count; x++)
{
int index = z * count * count + y * count + x;
Vector3 point = new Vector3(x - center.x, y - center.y, z - center.z);
vertices = vertices =
vertices = vertices = point;
uvs.Set(0, 0);
uvs.Set(1, 0);
uvs.Set(0, 1);
uvs.Set(1, 1);
indices = index * 4 + 0;
indices = index * 4 + 1;
indices = index * 4 + 2;
indices = index * 4 + 2;
indices = index * 4 + 1;
indices = index * 4 + 3;
}
}
}
mesh.vertices = vertices;
mesh.uv = uvs;
mesh.SetIndices(indices, MeshTopology.Triangles, 0, false);
mesh.bounds = new Bounds(Vector3.zero, size);
mesh.UploadMeshData(true);
return mesh;
}
}
这个模型替换MeshFilter中的sharedMesh,剩下的工作就是写shader了。
Shader实现
生成的模型参数适用于BillBoard方式,四个顶点在同一个位置,通过UV坐标变化在View空间扩展成四边形,这样可以保证旋转时始终面向屏幕。这里先测试以下生成的数据:
v2f vert (appdata v)
{
v2f o;
o.color = _Color;
float3 vertex = v.vertex;
// Vector3 _Gap是每个点之间的距离,是C#代码中size/count得到的值
// 这里worldPos的计算方法是为了保证不受transform的缩放和旋转影响,可以根据需要自行修改
float3 worldPos = UNITY_MATRIX_M._m03_m13_m23 + vertex.xyz * _Gap;
float4 positionVS = mul(UNITY_MATRIX_V, float4(worldPos, 1));
float2 patch = v.uv * 2 - 1;
positionVS.xy += patch * _Size; // 在View空间展开片面,float _Size是片面的大小
o.vertex = mul(UNITY_MATRIX_P, positionVS);
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return 1; // 白色
}渲染结果如下,这时是一个均匀分布的10x10x10的长方体点阵列:
增加噪声
能够渲染片面后,需要增加一些噪声来实现降雪错乱的效果。这里可以使用随机数生成一张RGBA的图片,或者使用Photoshop中增加噪声并提高对比度方法生成一张噪声图导入到Unity中:
噪声图
红圈是导入建议的设置
修改代码,在worldPos计算ViewSpace位置前叠加噪声:
float4 SampleNoise(float3 v3)
{
// 在2D noise实现3D noise纹理拾取,_Repeat取范围是(0,1)
// 因为不考虑噪声连续性,这个实现比较随意,也可以使用Texture3D实现
float2 uv3D = float2(v3.xy * _Repeat + frac(v3.z * _Repeat) * 0.11 + v3.z * _Repeat);
return tex2Dlod(_NoiseTex, float4(uv3D, 0, 0)) * 2.0 - 1; // 顶点纹理拾取必须用tex2Dlod
}
v2f vert (appdata v)
{
v2f o;
o.color = _Color;
float3 vertex = v.vertex;
float3 uvw = v.vertex;
float3 worldPos = UNITY_MATRIX_M._m03_m13_m23 + vertex.xyz * _Gap;
float4 noise = SampleNoise(uvw);
worldPos.xyz += noise.xyz * (_Gap.xyz + _Size); // 扰动worldPos
float4 positionVS = mul(UNITY_MATRIX_V, float4(worldPos, 1));
float2 patch = v.uv * 2 - 1;
positionVS.xy += patch * _Size;
o.vertex = mul(UNITY_MATRIX_P, positionVS);
o.uv = v.uv;
return o;
}这里在顶点中读取纹理信息,使用了顶点纹理拾取(Vertex Texture Fetch),与纹理采样不同的是访问不考虑连续性,所以是“拾取”而不是“采样”。最后是这样:
粒子下落
最简单的粒子下落公式就是:
位置=初始位置+时间*速度*方向
在CPU实现的粒子系统中,一般是检查粒子的生命周期,当生命周期大于设定时间后销毁粒子(或转入到池中),然后在利用这个粒子的空间创建一个新的粒子。在shader中只能从只读数据中获得数据,无法控制粒子的销毁与创建,这里需要做的就是让粒子在y轴某个范围内不断滚动即可。
float Round(float y, float range)
{
float r = range * 2;
return frac((y + range) / r) * r - range;
}
v2f vert (appdata v)
{
// ...
float3 vertex = v.vertex;
// ...
vertex.y -= _Time.y * _FallSpeed; // float _FallSpeed 是下落速度
vertex.y = Round(vertex.y, _Count * 0.5); // float _Count 是粒子棱方向的数量,粒子间距为1,
// 所以在模型空间的运动范围是(-5, 5]
// ...
}现实中,如果雪花很轻或者风很大,受到空气扰动影响下落方向不是很稳定,所以我们在横向上增加一个摆动效果。
float s, c;
sincos(noise.w * UNITY_PI * 2.0 + _Time.y, s, c);
float2 swing = float2(c, s) * _Swing * _Gap.xz; // float _Swing 是摆动幅度
worldPos.xz -= (swing + _Direction.xy) * vertex.y; // float4 _Direction.xy 是风力方向效果图:
风力+扰动效果
可以看出,因为风力方向的影响,粒子超出了设定的范围框,同时在范围内也有一部分空间没有粒子,解决办法是对worldPos.xz做出限定:
// _Range 是模型空间的范围,所以滚动xz时要转到模型空间
float3 _Range = _Gap.xyz * _Count * 0.5;
worldPos.xz = Round(worldPos.xz - UNITY_MATRIX_M._m03_m23, _Range.xz) + UNITY_MATRIX_M._m03_m23;
限定xz范围
结尾
最后,在fragment中加一个简单的纹理采样即可。如果想得到更丰富的粒子形态,可以再叠加旋转,大小和颜色随机。稍微修改一下也可以做成全屏下雪效果。
完整代码:
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 color : COLOR0;
};
sampler2D _MainTex;
sampler2D _NoiseTex;
float4 _Color;
float4 _MainTex_ST;
float _Repeat;
float4 _Gap;
float _Size;
float _FallSpeed;
float4 _Direction;
float _Swing;
float4 SampleNoise(float3 v3)
{
float2 uv3D = float2(v3.xy * _Repeat + frac(v3.z * _Repeat) * 0.11 + v3.z * _Repeat);
return tex2Dlod(_NoiseTex, float4(uv3D, 0, 0)) * 2.0 - 1;
}
float Round(float y, float range)
{
float r = range * 2;
return frac((y + range) / r) * r - range;
}
float2 Round(float2 y, float2 range)
{
float2 r = range * 2;
return frac((y + range) / r) * r - range;
}
v2f vert (appdata v)
{
v2f o;
o.color = _Color;
float3 vertex = v.vertex;
float3 uvw = v.vertex;
float _Count = _Gap.w;
float3 _Range = _Gap.xyz * _Count * 0.5;
vertex.y -= _Time.y * _FallSpeed;
vertex.y = Round(vertex.y, _Count * 0.5);
float3 worldPos = UNITY_MATRIX_M._m03_m13_m23 + vertex.xyz * _Gap;
float4 noise = SampleNoise(uvw);
float s, c;
sincos(noise.w * UNITY_PI * 2.0 + _Time.y, s, c);
float2 swing = float2(c, s) * _Swing * _Gap.xz;
worldPos.xz -= (swing + _Direction.xy) * vertex.y;
worldPos.xz = Round(worldPos.xz - UNITY_MATRIX_M._m03_m23, _Range.xz) + UNITY_MATRIX_M._m03_m23;
worldPos.xyz += noise.xyz * (_Gap.xyz + _Size);
float4 positionVS = mul(UNITY_MATRIX_V, float4(worldPos, 1));
float2 patch = v.uv * 2 - 1;
positionVS.xy += patch * _Size;
o.vertex = mul(UNITY_MATRIX_P, positionVS);
o.uv = v.uv;
return o;
}
half4 frag(v2f i) : SV_Target
{
half4 col = tex2D(_MainTex, i.uv) * i.color;
return col;
}
页:
[1]