Created
August 20, 2018 11:14
-
-
Save johans2/4d9f224469a66f58444c1f8519b934ec to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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))); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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