Skip to content

Instantly share code, notes, and snippets.

@johans2
Created August 20, 2018 11:14
Show Gist options
  • Save johans2/4d9f224469a66f58444c1f8519b934ec to your computer and use it in GitHub Desktop.
Save johans2/4d9f224469a66f58444c1f8519b934ec to your computer and use it in GitHub Desktop.
#pragma kernel UpdateParticles
// Noise functions from https://github.com/keijiro/NoiseShader/blob/master/Assets/HLSL/SimplexNoise3D.hlsl
#include "SimplexNoise3D.hlsl"
struct Particle
{
float3 startPos;
float3 position;
float3 velocity;
float3 convergenceTarget;
float life;
float colorLookup;
float STRIDE_FILLER1;
float STRIDE_FILLER2;
};
struct MeshTriangle
{
float3 vert1;
float3 vert2;
float3 vert3;
};
StructuredBuffer<MeshTriangle> meshBuffer;
RWStructuredBuffer<Particle> particleBuffer;
// Variables set from the CPU
float deltaTime;
float curlE;
float curlMultiplier;
float particleMinLife;
float particleMaxLife;
float3 emitterPos;
float3 emitterScale;
float3 emitterRot;
float3 convergencePoint;
float convergenceStrength;
float totalSmokeDistance;
float updraft;
float randSeed;
int numVertices;
const float uintMax = 4294967296.0;
// A super fast way of generating random numbers.
// http://www.reedbeta.com/blog/quick-and-easy-gpu-random-numbers-in-d3d11/
uint rng_state;
uint rand_xorshift()
{
rng_state ^= (rng_state << 13);
rng_state ^= (rng_state >> 17);
rng_state ^= (rng_state << 5);
return rng_state;
}
uint wang_hash(uint seed)
{
seed = (seed ^ 61) ^ (seed >> 16);
seed *= 9;
seed = seed ^ (seed >> 4);
seed *= 0x27d4eb2d;
seed = seed ^ (seed >> 15);
return seed;
}
// https://github.com/cabbibo/glsl-curl-noise/blob/master/curl.glsl
float3 snoiseVec3(float3 x)
{
float s = snoise(x);
float s1 = snoise(float3(x.y - 19.1, x.z + 33.4, x.x + 47.2));
float s2 = snoise(float3(x.z + 74.2, x.x - 124.5, x.y + 99.4));
float3 c = float3(s, s1, s2);
return c;
}
float3 curlNoise(float3 p, float E)
{
float e = E;
float3 dx = float3(e, 0.0, 0.0);
float3 dy = float3(0.0, e, 0.0);
float3 dz = float3(0.0, 0.0, e);
float3 p_x0 = snoiseVec3(p - dx);
float3 p_x1 = snoiseVec3(p + dx);
float3 p_y0 = snoiseVec3(p - dy);
float3 p_y1 = snoiseVec3(p + dy);
float3 p_z0 = snoiseVec3(p - dz);
float3 p_z1 = snoiseVec3(p + dz);
float x = p_y1.z - p_y0.z - p_z1.y + p_z0.y;
float y = p_z1.x - p_z0.x - p_x1.z + p_x0.z;
float z = p_x1.y - p_x0.y - p_y1.x + p_y0.x;
const float divisor = 1.0 / (2.0 * e);
return normalize(float3(x, y, z) * divisor);
}
float PI = 3.14159265359;
//https://stackoverflow.com/questions/14607640/rotating-a-vector-in-3d-space
float3 RotateAroundX(float3 p, float angle) {
angle = (3.14159265359 / 180.0) * -angle;
float3x3 m = float3x3( 1, 0, 0,
0, cos(angle), -sin(angle),
0, sin(angle), cos(angle)
);
return mul(p, m);
}
float3 RotateAroundY(float3 p, float angle) {
angle = (3.14159265359 / 180.0) * -angle;
float3x3 m = float3x3(
cos(angle), 0, sin(angle),
0, 1, 0,
-sin(angle), 0, cos(angle)
);
return mul(p, m);
}
float3 RotateAroundZ(float3 p, float angle) {
angle = (3.14159265359 / 180.0) * -angle;
float3x3 m = float3x3(
cos(angle), -sin(angle), 0,
sin(angle), cos(angle), 0,
0, 0, 1
);
return mul(p, m);
}
float3 GetRandomPointOnTriangle(MeshTriangle tri, uint rngState)
{
rng_state = rngState;
// Get a random float position on one side of the triangle
float rng01 = float(wang_hash(rng_state) * (1.0 / 4294967296.0));
float3 sideA = lerp(tri.vert1, tri.vert3, rng01);
// Update the random state
rng_state += randSeed;
// Get a random float position on the other side of the triangle
rng01 = float(wang_hash(rng_state) * (1.0 / 4294967296.0));
float3 sideB = lerp(tri.vert2, tri.vert3, rng01);
// Update the random state
rng_state += randSeed;
// Get the final random position between the two above calculated ones.
rng01 = float(wang_hash(rng_state) * (1.0 / 4294967296.0));
float3 finalPoint = lerp(sideA, sideB, rng01);
return finalPoint;
}
[numthreads(256, 1, 1)]
void UpdateParticles(uint3 id : SV_DispatchThreadID)
{
// subtract the life based on deltaTime
particleBuffer[id.x].life -= deltaTime;
// Update the position by curling it
particleBuffer[id.x].position += curlNoise(particleBuffer[id.x].position, curlE) * deltaTime * curlMultiplier + float3(0, updraft, 0);
// Update the position by the convergence.
float2 convergence = normalize( particleBuffer[id.x].convergenceTarget.xz - particleBuffer[id.x].position.xz);
convergence *= convergenceStrength;
convergence *= clamp( (particleBuffer[id.x].position.y - emitterPos.y) / ( convergencePoint.y - emitterPos.y ), 0,1);
particleBuffer[id.x].position.xz += convergence;
// Update the color lookup value. Also make it from the emitter pos.
float distance = length(particleBuffer[id.x].position - particleBuffer[id.x].startPos);
float colorLookup = saturate(distance / totalSmokeDistance);
particleBuffer[id.x].colorLookup = colorLookup;
if (particleBuffer[id.x].life <= 0)
{
rng_state = id.x + randSeed;
uint randomIndex = float(wang_hash(rng_state)) * (1.0 / 4294967296.0) * float(numVertices);
MeshTriangle tri = meshBuffer[randomIndex % numVertices];
float3 newPos = GetRandomPointOnTriangle(tri, rng_state);
// Mirror the transform of the emitter
newPos = RotateAroundZ(newPos, emitterRot.z);
newPos = RotateAroundY(newPos, emitterRot.y);
newPos = RotateAroundX(newPos, emitterRot.x);
newPos *= emitterScale;
newPos += emitterPos;
// Set the new position
particleBuffer[id.x].position = newPos;
// Set the starting position
particleBuffer[id.x].startPos = newPos;
// Reset color lookup
particleBuffer[id.x].colorLookup = 0.01;
// Set the convergence point
particleBuffer[id.x].convergenceTarget = emitterPos + convergencePoint;
// reset the life of this particle
particleBuffer[id.x].life = lerp(particleMinLife, particleMaxLife, float(wang_hash(id.x) * (1.0 / 4294967296.0)));
}
}
Shader "Custom/FireParticlesComputed" {
Properties{
_ParticleSize("ParticleSize", Float) = 0.01
_MainTex("Particle texture", 2D) = "white" {}
_ColorRampTex("Color ramp texture", 2D) = "white" {}
_TotalSmokeDistance("Particle total smoke distance", Float) = 5.0
_SizeByLifeMin("Size by life min", Float) = 0.015
_SizeByLifeMax("Size by life min", Float) = 0.03
}
SubShader {
Pass {
Tags{ "Queue" = "Opaque" "RenderType" = "Opaque" }
LOD 200
Cull front
ZWrite off
Blend One OneMinusSrcAlpha
//BlendOp Add
//Blend SrcAlpha One
CGPROGRAM
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#include "UnityCG.cginc"
// Use shader model 5.0 target, to be able to use buffers
#pragma target 5.0
struct Particle
{
float3 startPos;
float3 position;
float3 velocity;
float3 convergenceTarget;
float life;
float colorLookup;
float STRIDE_FILLER1;
float STRIDE_FILLER2;
};
struct v2g {
float4 position : SV_POSITION;
float4 color : COLOR;
float distance : DISTANCE;
};
struct g2f {
float4 pos : SV_POSITION;
float3 norm : NORMAL;
float2 uv : TEXCOORD0;
float4 color: COLOR;
};
half _ParticleSize;
half _TotalSmokeDistance;
half _SizeByLifeMin;
half _SizeByLifeMax;
sampler2D _MainTex;
sampler2D _ColorRampTex;
// particles' data
StructuredBuffer<Particle> particleBuffer;
v2g vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)
{
v2g output = (v2g)0;
// Need to calculate the distance from the start position in order to alter the size from that value in the geom function.
float3 pos = particleBuffer[instance_id].position;
float3 startPos = particleBuffer[instance_id].startPos;
float distance = length(pos - startPos);
// Color
float4 color = tex2Dlod(_ColorRampTex, float4(particleBuffer[instance_id].colorLookup - 0.001, 0, 0, 0));
color.a = 1.1 - particleBuffer[instance_id].colorLookup;
output.color = color;
output.distance = distance;
// Position in worldspace
output.position = float4(particleBuffer[instance_id].position, 1.0f);
return output;
}
[maxvertexcount(3)]
void geom(point v2g IN[1], inout TriangleStream<g2f> triStream) {
g2f OUT;
float pSize = lerp(_SizeByLifeMin, _SizeByLifeMax, saturate(IN[0].distance / _TotalSmokeDistance));//0.015;
float3 v0 = IN[0].position - float4(0, pSize, 0,0);
float3 v1 = IN[0].position + float4(0, pSize, 0,0);
float3 camToPos = IN[0].position - _WorldSpaceCameraPos;
float3 perpVector = normalize(cross(camToPos, float3(0, 1, 0))) * pSize;
// 1
OUT.pos = UnityObjectToClipPos(v1);
OUT.norm = float3(1,0,0);
OUT.uv = float2(0.5,1);
OUT.color = IN[0].color;
triStream.Append(OUT);
// 2
OUT.pos = UnityObjectToClipPos(v0 + perpVector);
OUT.norm = float3(1, 0, 0);
OUT.uv = float2(0, 0);
triStream.Append(OUT);
// 3
OUT.pos = UnityObjectToClipPos(v0 - perpVector);
OUT.norm = float3(1,0, 0);;
OUT.uv = float2(1, 0);
triStream.Append(OUT);
}
float4 frag(g2f IN) : COLOR
{
float4 c = tex2D(_MainTex, IN.uv) * IN.color;
//clip(c.a - 0.1);
c.rgb *= c.a;
return c;
}
ENDCG
}
}
FallBack off
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FireParticleSimulation : MonoBehaviour
{
struct Particle
{
public Vector3 startPos;
public Vector3 position;
public Vector3 velocity;
public Vector3 convergenceTarget;
public float life;
public float colorLookup;
public float STRIDE_FILLER1;
public float STRIDE_FILLER2;
}
struct MeshTriangle
{
public Vector3 vert1;
public Vector3 vert2;
public Vector3 vert3;
}
public MeshFilter meshFilter;
public Transform emitterTrans;
[Range(0.001f,1f)]
public float curlE = 0.1f;
[Range(1f, 100f)]
public float curlMultiplier = 0.05f;
[Range(0.0f, 10f)]
public float particleMaxLife = 6.0f;
[Range(0.0f, 10f)]
public float particleMinLife = 2.0f;
[Range(0.001f, 1f)]
public float curlEmin = 0.1f;
[Range(0.001f, 1f)]
public float curlEmax = 0.3f;
[Range(0.0f, 20f)]
public float curlESpeed = 1f;
[Range(0.001f, 0.1f)]
public float sizeByLifeMin = 0.015f;
[Range(0.001f, 0.1f)]
public float sizeByLifeMax = 0.03f;
public Vector3 convergencePoint = new Vector3(0, 2, 0);
[Range(0.0f, 0.3f)]
public float convergenceStrength = 0.01f;
[Range(-0.3f, 0.3f)]
public float updraft = 0.025f;
[Range(0f, 20f)]
public float totalSmokeDistance = 5f;
/// <summary>
/// Material used to draw the Particle on screen.
/// </summary>
public Material material;
/// <summary>
/// Compute shader used to update the Particles.
/// </summary>
public ComputeShader computeShader;
public Transform emitterTransform;
/// <summary>
/// Size in octet of the Particle struct.
/// Vector3 position = 12 bytes
/// Vector3 velocity = 12 bytes
/// float life = 4 bytes
/// float startPos = 12 bytes
/// float colorLookup = 4 bytes
/// Vector3 convergenceTarget = 12 bytes
/// float STRIDE_FILLER1 = 4 bytes
/// float STRIDE_FILLER2 = 4 bytes
/// ----------
/// TOTAL = 64 bytes
/// </summary>
private const int SIZE_PARTICLE = 64;
/// <summary>
/// Number of Particle created in the system.
/// </summary>
private int particleCount = 1000000;
/// <summary>
/// Id of the kernel used.
/// </summary>
private int mComputeShaderKernelID;
/// <summary>
/// Buffer holding the Particles.
/// </summary>
ComputeBuffer particleBuffer;
/// <summary>
/// Number of particle per warp.
/// </summary>
private const int WARP_SIZE = 256; // TODO?
/// <summary>
/// Number of warp needed.
/// </summary>
private int numThreadGroups; // TODO?
/// <summary>
/// temporary list for storing mesh verts.
/// </summary>
private List<Vector3> verts = new List<Vector3>();
/// <summary>
/// Compute buffer used for storing mesh triangles.
/// </summary>
private ComputeBuffer meshBuffer;
private const float away = 99999999.0f;
// Use this for initialization
void Start()
{
InitComputeShader();
}
void UpdateCurlE() {
curlE = Mathf.Lerp(curlEmin, curlEmax, Mathf.Sin(Time.timeSinceLevelLoad * curlESpeed) / 2.0f + 0.5f);
}
void InitComputeShader()
{
numThreadGroups = Mathf.CeilToInt((float)particleCount / WARP_SIZE);
// initialize the particles
Particle[] particleArray = new Particle[particleCount];
for (int i = 0; i < particleCount; i++)
{
particleArray[i].position.x = away;
particleArray[i].position.y = away;
particleArray[i].position.z = away;
particleArray[i].velocity.x = 0;
particleArray[i].velocity.y = 0;
particleArray[i].velocity.z = 0;
// Initial life value
particleArray[i].life = Random.value * 5.0f + 1.0f;
}
// find the id of the kernel
mComputeShaderKernelID = computeShader.FindKernel("UpdateParticles");
// Get the mesh buffer into the compute shader
MeshTriangle[] meshVerts = GetMeshTriangles();
meshBuffer = new ComputeBuffer(meshVerts.Length, 36);
meshBuffer.SetData(meshVerts);
computeShader.SetBuffer(mComputeShaderKernelID, "meshBuffer", meshBuffer);
computeShader.SetInt("numVertices", meshVerts.Length);
computeShader.SetFloat("particleMinLife", particleMinLife);
computeShader.SetFloat("particleMaxLife", particleMaxLife);
computeShader.SetFloats("convergencePoint", new float[] { convergencePoint.x, convergencePoint.y, convergencePoint.z } );
computeShader.SetFloat("convergenceStrength", convergenceStrength);
computeShader.SetFloat("updraft", updraft);
computeShader.SetFloat("totalSmokeDistance", totalSmokeDistance);
// create compute buffer
particleBuffer = new ComputeBuffer(particleCount, SIZE_PARTICLE);
particleBuffer.SetData(particleArray);
// bind the compute buffer to the shader and the compute shader
computeShader.SetBuffer(mComputeShaderKernelID, "particleBuffer", particleBuffer);
material.SetBuffer("particleBuffer", particleBuffer);
material.SetFloat("_SizeByLifeMin", sizeByLifeMin);
material.SetFloat("_SizeByLifeMax", sizeByLifeMax);
}
void OnRenderObject()
{
material.SetPass(0);
Graphics.DrawProcedural(MeshTopology.Points, 1, particleCount);
}
void OnDestroy()
{
if (particleBuffer != null)
particleBuffer.Release();
if (meshBuffer != null)
meshBuffer.Release();
}
void Update()
{
UpdateCurlE();
float[] emitterPosition = { emitterTransform.position.x, emitterTransform.position.y, emitterTransform.position.z };
float[] emitterScale = { emitterTransform.localScale.x, emitterTransform.localScale.y, emitterTransform.localScale.z };
float[] emitterRot = { emitterTransform.rotation.eulerAngles.x, emitterTransform.rotation.eulerAngles.y, emitterTransform.rotation.eulerAngles.z };
// Send datas to the compute shader
computeShader.SetFloat("deltaTime", Time.deltaTime);
computeShader.SetFloat("curlE", curlE);
computeShader.SetFloat("curlMultiplier", curlMultiplier);
computeShader.SetFloat("particleMinLife", particleMinLife);
computeShader.SetFloat("particleMaxLife", particleMaxLife);
computeShader.SetFloats("emitterPos", emitterPosition);
computeShader.SetFloats("emitterScale", emitterScale);
computeShader.SetFloats("emitterRot", emitterRot);
computeShader.SetFloat("randSeed", Random.Range(0.0f, verts.Count));
computeShader.SetFloats("convergencePoint", new float[] { convergencePoint.x, convergencePoint.y, convergencePoint.z });
computeShader.SetFloat("convergenceStrength", convergenceStrength);
computeShader.SetFloat("updraft", updraft);
computeShader.SetFloat("totalSmokeDistance", totalSmokeDistance);
material.SetFloat("_SizeByLifeMin", sizeByLifeMin);
material.SetFloat("_SizeByLifeMax", sizeByLifeMax);
// Update the Particles
computeShader.Dispatch(mComputeShaderKernelID, numThreadGroups, 1, 1);
}
private MeshTriangle[] GetMeshTriangles() {
List<MeshTriangle> triangles = new List<MeshTriangle>();
meshFilter.mesh.GetVertices(verts);
int[] triangelIndices = meshFilter.mesh.GetTriangles(0);
Debug.Log("tris: " + triangelIndices.Length);
Debug.Log("verts: " + verts.Count);
for (int i = 0; i < triangelIndices.Length; i += 3)
{
MeshTriangle triangle = new MeshTriangle
{
vert1 = verts[triangelIndices[i]],
vert2 = verts[triangelIndices[i + 1]],
vert3 = verts[triangelIndices[i + 2]]
};
triangles.Add(triangle);
}
Debug.Log(triangles.Count + " triangels added.");
return triangles.ToArray();
}
void OnDrawGizmosSelected() {
Gizmos.DrawSphere(emitterTrans.position + convergencePoint, .1f);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment