unity有自己的粒子系统,但是这次我们要尝试创建一个我们自己的粒子系统,而且使用计算着色器有下面这些好处。总而言之,计算着色器适合处理大规模的数据集。例如,能够高效地处理数万个甚至数百万个粒子的计算。这对于粒子系统这样的效果特别重要,因为粒子数量通常很大。
首先创建一个粒子结构体,然后给上要用到的属性,以及一些相关的变量
struct Particle
{
public Vector3 position;
public Vector3 velocity;
public float life;
}
const int SIZE_PARTICLE = 7 * sizeof(float);
public int particleCount = 1000000;
然后通过init方法初始化粒子数据,分别随机位置和生命周期,然后重置速度为0.很明显位置会随机分布在-0.5-0.5之间。然后填充粒子数据到computebuffer中,分别传递buffer到computeshader和shader中,这里很关键的一部分就是我们在computershader中修改粒子数据,可以在shader中的buffer访问到修改后的数据
void Init()
{
// initialize the particles
Particle[] particleArray = new Particle[particleCount];
for (int i = 0; i < particleCount; i++)
{
// Initialize particle
Vector3 v = new Vector3();
v.x = Random.value * 2 - 1.0f;
v.y = Random.value * 2 - 1.0f;
v.z = Random.value * 2 - 1.0f;
v.Normalize();
v *= Random.value * 0.5f;
// Assign particle properties
particleArray[i].position.x = v.x;
particleArray[i].position.y = v.y;
particleArray[i].position.z = v.z + 3;//远离摄像机
particleArray[i].velocity.x = 0;
particleArray[i].velocity.y = 0;
particleArray[i].velocity.z = 0;
particleArray[i].life = Random.value * 5.0f + 1.0f;//1-6
}
// create compute buffer
particleBuffer = new ComputeBuffer(particleCount, SIZE_PARTICLE);
particleBuffer.SetData(particleArray);
// find the id of the kernel
kernelID = shader.FindKernel("CSParticle");
uint threadsX;
shader.GetKernelThreadGroupSizes(kernelID, out threadsX, out _, out _);
groupSizeX = Mathf.CeilToInt((float)particleCount / (float)threadsX);
// bind the compute buffer to the shader and the compute shader
shader.SetBuffer(kernelID, "particleBuffer", particleBuffer);
material.SetBuffer("particleBuffer", particleBuffer);
material.SetInt("_PointSize", pointSize);
}
然后是OnRenderObject函数,每当 Unity 需要渲染一个对象时,这个函数就会被调用,确保在渲染期间可以执行自定义的渲染操作,下面图片是对DrawProceduralNow函数的解释
void OnRenderObject()//相机的每个渲染过程自动调用
{
material.SetPass(0);//使用第一个Pass
Graphics.DrawProceduralNow(MeshTopology.Points, 1, particleCount);//程序化绘制顶点
}
然后看一下我们的顶点着色器,首先是两个参数,第二实例ID就是逐渐增加的,从0增加到particleCount,第一个参数是每个实例的顶点索引,因为这次粒子都是点,所以永远是0,如果是三角形就会是0,1,2.
v2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)
{
v2f o = (v2f)0;
// Color
o.color = fixed4(1,0,0,1)
// Position
o.position = UnityObjectToClipPos(float4(particleBuffer[instance_id].position,1));
o.size = 1;
return o;
}
好了,现在运行会得到一个在屏幕中央半径为0.5左右的红色小球。就像下面这样,这是因为我们并没有对粒子进行任何处理,只是设置了位置和颜色。
接下来就是让这些粒子动起来,我们让粒子跟着鼠标的位置移动,首先找到鼠标的位置。然后设置粒子的速度并且更改粒子的位置。然后如果生命变成0,就调用函数重新生成粒子。(重新生成函数代码与获取鼠标位置代码在后面完整代码里)
下面这个链接是将鼠标位置转换为世界空间坐标
gamedevbeginner.com/
how-to-convert-the-mouse-position-to-world-space-in-unity-2d-3d/
[numthreads(256, 1, 1)]
void CSParticle(uint3 id : SV_DispatchThreadID)
{
Particle particle = particleBuffer[id.x];
// 减少粒子的生命值
particle.life -= deltaTime;
// 计算粒子位置与鼠标位置的差值
float3 delta = float3(mousePosition.xy, 3) - particle.position;
// 计算粒子运动的方向
float3 dir = normalize(delta);
// 更新粒子的速度
particle.velocity += dir;
// 根据速度更新粒子的位置
particle.position += particle.velocity * deltaTime;
// 将更新后的粒子数据存储回缓冲区
particleBuffer[id.x] = particle;
// 如果粒子的生命值小于 0,则重新生成粒子
if (particle.life < 0)
{
respawn(id.x);
}
}
现在因为只有红色,有点单调,所以我们要丰富一下颜色。
随着粒子的生命周期减少,这是每个通道的颜色变化
v2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)
{
v2f o = (v2f)0;
// Color
float life = particleBuffer[instance_id].life;
float lerpVal = life * 0.25;
// 计算颜色值
o.color = fixed4(
1 - lerpVal + 0.1, // Red component
lerpVal + 0.1, // Green component
1, // Blue component
lerpVal // Alpha component
);
// Position
o.position = UnityObjectToClipPos(float4(particleBuffer[instance_id].position,1));
o.size = _PointSize;
return o;
}
最后就来看一下最终效果把
完整代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#pragma warning disable 0649
public class ParticleFun : MonoBehaviour
{
private Vector2 cursorPos;
// struct
struct Particle
{
public Vector3 position;
public Vector3 velocity;
public float life;
}
const int SIZE_PARTICLE = 7 * sizeof(float);
public int particleCount = 1000000;
public Material material;
public ComputeShader shader;
[Range(1, 10)]
public int pointSize = 2;
int kernelID;
ComputeBuffer particleBuffer;
int groupSizeX;
// Use this for initialization
void Start()
{
Init();
}
void Init()
{
// initialize the particles
Particle[] particleArray = new Particle[particleCount];
for (int i = 0; i < particleCount; i++)
{
// Initialize particle
Vector3 v = new Vector3();
v.x = Random.value * 2 - 1.0f;
v.y = Random.value * 2 - 1.0f;
v.z = Random.value * 2 - 1.0f;
v.Normalize();
v *= Random.value * 0.5f;
// Assign particle properties
particleArray[i].position.x = v.x;
particleArray[i].position.y = v.y;
particleArray[i].position.z = v.z + 3;//远离摄像机
particleArray[i].velocity.x = 0;
particleArray[i].velocity.y = 0;
particleArray[i].velocity.z = 0;
particleArray[i].life = Random.value * 5.0f + 1.0f;//1-6
}
// create compute buffer
particleBuffer = new ComputeBuffer(particleCount, SIZE_PARTICLE);
particleBuffer.SetData(particleArray);
// find the id of the kernel
kernelID = shader.FindKernel("CSParticle");
uint threadsX;
shader.GetKernelThreadGroupSizes(kernelID, out threadsX, out _, out _);
groupSizeX = Mathf.CeilToInt((float)particleCount / (float)threadsX);
// bind the compute buffer to the shader and the compute shader
shader.SetBuffer(kernelID, "particleBuffer", particleBuffer);
material.SetBuffer("particleBuffer", particleBuffer);
material.SetInt("_PointSize", pointSize);
}
void OnRenderObject()//相机的每个渲染过程自动调用
{
material.SetPass(0);//使用第一个Pass
Graphics.DrawProceduralNow(MeshTopology.Points, 1, particleCount);//程序化绘制顶点
}
void OnDestroy()
{
if (particleBuffer != null)
particleBuffer.Release();
}
// Update is called once per frame
void Update()
{
float[] mousePosition2D = { cursorPos.x, cursorPos.y };
// Send datas to the compute shader
shader.SetFloat("deltaTime", Time.deltaTime);
shader.SetFloats("mousePosition", mousePosition2D);
// Update the Particles
shader.Dispatch(kernelID, groupSizeX, 1, 1);
}
void OnGUI()
{
Vector3 p = new Vector3();
Camera c = Camera.main;
Event e = Event.current;
Vector2 mousePos = new Vector2();
// Get the mouse position from Event.
// Note that the y position from Event is inverted.
mousePos.x = e.mousePosition.x;
mousePos.y = c.pixelHeight - e.mousePosition.y;
p = c.ScreenToWorldPoint(new Vector3(mousePos.x, mousePos.y, c.nearClipPlane + 14));// z = 3.
cursorPos.x = p.x;
cursorPos.y = p.y;
}
}
Shader "Custom/Particle" {
Properties
{
_PointSize("Point size", Float) = 5.0
}
SubShader {
Pass {
Tags{ "RenderType" = "Opaque" }
LOD 200
Blend SrcAlpha one
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma vertex vert
#pragma fragment frag
uniform float _PointSize;
#include "UnityCG.cginc"
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 5.0
struct v2f{
float4 position : SV_POSITION;
float4 color : COLOR;
float life : LIFE;
float size: PSIZE;
};
// 定义粒子结构体
struct Particle
{
float3 position; // 粒子位置
float3 velocity; // 粒子速度
float life; // 粒子的生命值
};
// 声明结构化缓冲区
StructuredBuffer<Particle> particleBuffer;
v2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)
{
v2f o = (v2f)0;
// Color
float life = particleBuffer[instance_id].life;
float lerpVal = life * 0.25;
// 计算颜色值
o.color = fixed4(
1 - lerpVal + 0.1, // Red component
lerpVal + 0.1, // Green component
1, // Blue component
lerpVal // Alpha component
);
// Position
o.position = UnityObjectToClipPos(float4(particleBuffer[instance_id].position,1));
o.size = _PointSize;
return o;
}
float4 frag(v2f i) : COLOR
{
return i.color;
}
ENDCG
}
}
FallBack Off
}
#pragma kernel CSParticle
// Variables set from the CPU
float deltaTime;
float2 mousePosition;
uint rng_state;
// 定义粒子结构体
struct Particle
{
float3 position; // 粒子位置
float3 velocity; // 粒子速度
float life; // 粒子的生命值
};
// 声明结构化缓冲区
RWStructuredBuffer<Particle> particleBuffer;
uint rand_xorshift()//随机数范围0-4,294,967,295
{
// Xorshift 算法,来自 George Marsaglia 的论文
rng_state ^= (rng_state << 13); // 将状态左移13位,并与原状态进行异或
rng_state ^= (rng_state >> 17); // 将状态右移17位,并与原状态进行异或
rng_state ^= (rng_state << 5); // 将状态左移5位,并与原状态进行异或
return rng_state; // 返回新的状态,作为随机数
}
void respawn(uint id)
{
rng_state = id;
float tmp = (1.0 / 4294967296.0);
float f0 = float(rand_xorshift()) * tmp - 0.5;
float f1 = float(rand_xorshift()) * tmp - 0.5;
float f2 = float(rand_xorshift()) * tmp - 0.5;
float3 normalF3 = normalize(float3(f0, f1, f2)) * 0.8f;
normalF3 *= float(rand_xorshift()) * tmp;
particleBuffer[id].position = float3(normalF3.x + mousePosition.x, normalF3.y + mousePosition.y, normalF3.z + 3.0);
// reset the life of this particle
particleBuffer[id].life = 4;
particleBuffer[id].velocity = float3(0,0,0);
}
[numthreads(256, 1, 1)]
void CSParticle(uint3 id : SV_DispatchThreadID)
{
Particle particle = particleBuffer[id.x];
// 减少粒子的生命值
particle.life -= deltaTime;
// 计算粒子位置与鼠标位置的差值
float3 delta = float3(mousePosition.xy, 3) - particle.position;
// 计算粒子运动的方向
float3 dir = normalize(delta);
// 更新粒子的速度
particle.velocity += dir;
// 根据速度更新粒子的位置
particle.position += particle.velocity * deltaTime;
// 将更新后的粒子数据存储回缓冲区
particleBuffer[id.x] = particle;
// 如果粒子的生命值小于 0,则重新生成粒子
if (particle.life < 0)
{
respawn(id.x);
}
}