这次要给每个粒子加上网格。
添加的网格只是一个简单的四边形,它需要分成两个三角形。并且三角形的顶点必须按照顺时针排列,同时每个顶点都应该有UV信息。
所以接下来就要添加顶点结构体以及相关的compute buffer
struct Vertex
{
public Vector3 position; // 顶点的3D位置
public Vector2 uv; // 纹理坐标
public float life; // 生命周期
}
ComputeBuffer vertexBuffer;
接下来就要创建顶点数组并且初始化。这里只需要设置顶点数组的uv就可以了。位置信息后面通过computebuffer实时更新。然后传递buffer到两个shader中并且设置相关变量
int numVertices = numParticles * 6;
Vertex[] vertexArray = new Vertex[numVertices];
Vector3 pos = new Vector3();
int index;
for (int i = 0; i < numParticles; i++)
{
pos.Set(Random.value * 2 - 1.0f, Random.value * 2 - 1.0f, Random.value * 2 - 1.0f);
pos.Normalize();
pos *= Random.value;
pos *= 0.5f;
particleArray[i].position.Set(pos.x, pos.y, pos.z + 3);
particleArray[i].velocity.Set(0, 0, 0);
// Initial life value
particleArray[i].life = Random.value * 5.0f + 1.0f;
index = i * 6;
// Triangle 1 - bottom-left, top-left, top-right
vertexArray[index].uv.Set(0, 0);
vertexArray[index + 1].uv.Set(0, 1);
vertexArray[index + 2].uv.Set(1, 1);
// Triangle 2 - bottom-left, top-right, bottom-right
vertexArray[index + 3].uv.Set(0, 0);
vertexArray[index + 4].uv.Set(1, 1);
vertexArray[index + 5].uv.Set(1, 0);
}
// create compute buffers
particleBuffer = new ComputeBuffer(numParticles, SIZE_PARTICLE);
particleBuffer.SetData(particleArray);
vertexBuffer = new ComputeBuffer(numVertices, SIZE_VERTEX);
vertexBuffer.SetData(vertexArray);
// bind the compute buffers to the shader and the compute shader
shader.SetBuffer(kernelID, "particleBuffer", particleBuffer);
shader.SetBuffer(kernelID, "vertexBuffer", vertexBuffer);
shader.SetFloat("halfSize", quadSize * 0.5f);
material.SetBuffer("vertexBuffer", vertexBuffer);
然后去computeshader中更新顶点位置信息,其余内容都和上次一样,这里要记住三角形顶点是顺时针排列的。
int index = id.x * 6;
// Triangle 1 - bottom-left, top-left, top-right
vertexBuffer[index].position.x = p.position.x - halfSize;
vertexBuffer[index].position.y = p.position.y - halfSize;
vertexBuffer[index].position.z = p.position.z;
vertexBuffer[index].life = p.life;
vertexBuffer[index + 1].position.x = p.position.x - halfSize;
vertexBuffer[index + 1].position.y = p.position.y + halfSize;
vertexBuffer[index + 1].position.z = p.position.z;
vertexBuffer[index + 1].life = p.life;
vertexBuffer[index + 2].position.x = p.position.x + halfSize;
vertexBuffer[index + 2].position.y = p.position.y + halfSize;
vertexBuffer[index + 2].position.z = p.position.z;
vertexBuffer[index + 2].life = p.life;
// Triangle 2 - bottom-left, top-right, bottom-right
vertexBuffer[index + 3].position.x = p.position.x - halfSize;
vertexBuffer[index + 3].position.y = p.position.y - halfSize;
vertexBuffer[index + 3].position.z = p.position.z;
vertexBuffer[index + 3].life = p.life;
vertexBuffer[index + 4].position.x = p.position.x + halfSize;
vertexBuffer[index + 4].position.y = p.position.y + halfSize;
vertexBuffer[index + 4].position.z = p.position.z;
vertexBuffer[index + 4].life = p.life;
vertexBuffer[index + 5].position.x = p.position.x + halfSize;
vertexBuffer[index + 5].position.y = p.position.y - halfSize;
vertexBuffer[index + 5].position.z = p.position.z;
vertexBuffer[index + 5].life = p.life;
接着我们去到顶点着色器就可以获取到修改后的顶点信息,和上次一样的设置颜色,然后转换位置到裁剪空间,这次还要加上uv,因为我们一会要对提额图采样。这次不一样的是每一个instance有六个顶点,这是我们在onrenderobject中设置的。
v2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)
{
v2f o = (v2f)0;
int index = instance_id * 6 + vertex_id;
float lerpVal = vertexBuffer[index].life * 0.25;
// 计算颜色值
o.color = fixed4(
1 - lerpVal + 0.1, // Red component
lerpVal + 0.1, // Green component
1, // Blue component
lerpVal // Alpha component
);
// 计算顶点的世界空间到裁剪空间的位置
o.position = UnityWorldToClipPos(float4(vertexBuffer[index].position, 1));
// 设置纹理坐标
o.uv = vertexBuffer[index].uv;
return o;
}
接下来是片段着色器,只需要加上贴图采样
float4 frag(v2f i) : COLOR
{
fixed4 color = tex2D(_MainTex,i.uv)*i.color;
return color;
}
目前效果:目前我们看不到五角星,尽管在shader中我们对五角星贴图进行了采样,这是因为我们没有设置混合模式。这样的话透明度就没有了作用,仍然会显示成正方形。
增加下面这些代码以后,就可以正常渲染了
LOD 200
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
最终效果:
完整代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#pragma warning disable 0649
public class QuadParticles : MonoBehaviour
{
private Vector2 cursorPos;
// struct
struct Particle
{
public Vector3 position;
public Vector3 velocity;
public float life;
}
struct Vertex
{
public Vector3 position; // 顶点的3D位置
public Vector2 uv; // 纹理坐标
public float life; // 生命周期
}
const int SIZE_PARTICLE = 7 * sizeof(float);
const int SIZE_VERTEX = 6 * sizeof(float);
public int particleCount = 10000;
public Material material;
public ComputeShader shader;
[Range(0.01f, 1.0f)]
public float quadSize = 0.1f;
int numParticles;
int numVerticesInMesh;
int kernelID;
ComputeBuffer particleBuffer;
ComputeBuffer vertexBuffer;
int groupSizeX;
// Use this for initialization
void Start()
{
Init();
}
void Init()
{
// find the id of the kernel
kernelID = shader.FindKernel("CSMain");
uint threadsX;
shader.GetKernelThreadGroupSizes(kernelID, out threadsX, out _, out _);
groupSizeX = Mathf.CeilToInt((float)particleCount / (float)threadsX);
numParticles = groupSizeX * (int)threadsX;
// initialize the particles
Particle[] particleArray = new Particle[numParticles];
int numVertices = numParticles * 6;
Vertex[] vertexArray = new Vertex[numVertices];
Vector3 pos = new Vector3();
int index;
for (int i = 0; i < numParticles; i++)
{
pos.Set(Random.value * 2 - 1.0f, Random.value * 2 - 1.0f, Random.value * 2 - 1.0f);
pos.Normalize();
pos *= Random.value;
pos *= 0.5f;
particleArray[i].position.Set(pos.x, pos.y, pos.z + 3);
particleArray[i].velocity.Set(0, 0, 0);
// Initial life value
particleArray[i].life = Random.value * 5.0f + 1.0f;
index = i * 6;
// Triangle 1 - bottom-left, top-left, top-right
vertexArray[index].uv.Set(0, 0);
vertexArray[index + 1].uv.Set(0, 1);
vertexArray[index + 2].uv.Set(1, 1);
// Triangle 2 - bottom-left, top-right, bottom-right
vertexArray[index + 3].uv.Set(0, 0);
vertexArray[index + 4].uv.Set(1, 1);
vertexArray[index + 5].uv.Set(1, 0);
}
// create compute buffers
particleBuffer = new ComputeBuffer(numParticles, SIZE_PARTICLE);
particleBuffer.SetData(particleArray);
vertexBuffer = new ComputeBuffer(numVertices, SIZE_VERTEX);
vertexBuffer.SetData(vertexArray);
// bind the compute buffers to the shader and the compute shader
shader.SetBuffer(kernelID, "particleBuffer", particleBuffer);
shader.SetBuffer(kernelID, "vertexBuffer", vertexBuffer);
shader.SetFloat("halfSize", quadSize * 0.5f);
material.SetBuffer("vertexBuffer", vertexBuffer);
}
void OnRenderObject()
{
material.SetPass(0);
Graphics.DrawProceduralNow(MeshTopology.Triangles, 6, numParticles);
}
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));
cursorPos.x = p.x;
cursorPos.y = p.y;
}
}
Shader "Custom/QuadParticle" {
Properties
{
_MainTex("Texture", 2D) = "white" {}
}
SubShader {
Pass {
Tags{ "Queue"="Transparent" "RenderType"="Transparent" "IgnoreProjector"="True" }
LOD 200
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 5.0
struct Vertex{
float3 position;
float2 uv;
float life;
};
struct v2f{
float4 position : SV_POSITION;
float4 color : COLOR;
float2 uv: TEXCOORD0;
float life : LIFE;
};
// particles' data
StructuredBuffer<Vertex> vertexBuffer;
sampler2D _MainTex;
v2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)
{
v2f o = (v2f)0;
int index = instance_id*6 + vertex_id;
float lerpVal = vertexBuffer[index].life * 0.25f;
o.color = fixed4(1.0f - lerpVal+0.1, lerpVal+0.1, 1.0f, lerpVal);
o.position = UnityWorldToClipPos(float4(vertexBuffer[index].position, 1.0f));
o.uv = vertexBuffer[index].uv;
return o;
}
float4 frag(v2f i) : COLOR
{
fixed4 color = tex2D( _MainTex, i.uv ) * i.color;
return color;
}
ENDCG
}
}
FallBack Off
}
#pragma kernel CSMain
// Particle's data
struct Particle
{
float3 position;
float3 velocity;
float life;
};
struct Vertex
{
float3 position;
float2 uv;
float life;
};
// Particle's data, shared with the shader
RWStructuredBuffer<Particle> particleBuffer;
RWStructuredBuffer<Vertex> vertexBuffer;
// Variables set from the CPU
float deltaTime;
float2 mousePosition;
float halfSize;
uint rng_state;
// http://www.reedbeta.com/blog/quick-and-easy-gpu-random-numbers-in-d3d11/
uint rand_xorshift()
{
// Xorshift algorithm from George Marsaglia's paper
rng_state ^= (rng_state << 13);
rng_state ^= (rng_state >> 17);
rng_state ^= (rng_state << 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 CSMain(uint3 id : SV_DispatchThreadID)
{
Particle p = particleBuffer[id.x];
// subtract the life based on deltaTime
p.life -= deltaTime;
float3 delta = float3(mousePosition.xy, 3) - p.position;
float3 dir = normalize(delta);
p.velocity += dir;
p.position += p.velocity * deltaTime;
particleBuffer[id.x] = p;
if (particleBuffer[id.x].life < 0){
respawn(id.x);
p = particleBuffer[id.x];
}
//Set the vertex buffer //
int index = id.x * 6;
//Triangle 1 - bottom-left, top-left, top-right
vertexBuffer[index].position.x = p.position.x-halfSize;
vertexBuffer[index].position.y = p.position.y-halfSize;
vertexBuffer[index].position.z = p.position.z;
vertexBuffer[index].life = p.life;
vertexBuffer[index+1].position.x = p.position.x-halfSize;
vertexBuffer[index+1].position.y = p.position.y+halfSize;
vertexBuffer[index+1].position.z = p.position.z;
vertexBuffer[index+1].life = p.life;
vertexBuffer[index+2].position.x = p.position.x+halfSize;
vertexBuffer[index+2].position.y = p.position.y+halfSize;
vertexBuffer[index+2].position.z = p.position.z;
vertexBuffer[index+2].life = p.life;
//Triangle 2 - bottom-left, top-right, bottom-right // //
vertexBuffer[index+3].position.x = p.position.x-halfSize;
vertexBuffer[index+3].position.y = p.position.y-halfSize;
vertexBuffer[index+3].position.z = p.position.z;
vertexBuffer[index+3].life = p.life;
vertexBuffer[index+4].position.x = p.position.x+halfSize;
vertexBuffer[index+4].position.y = p.position.y+halfSize;
vertexBuffer[index+4].position.z = p.position.z;
vertexBuffer[index+4].life = p.life;
vertexBuffer[index+5].position.x = p.position.x+halfSize;
vertexBuffer[index+5].position.y = p.position.y-halfSize;
vertexBuffer[index+5].position.z = p.position.z;
vertexBuffer[index+5].life = p.life;
}