BlendAdd and Per-Particle Offset in Unity
Learning VFX, Unity3D 5, Stylized
After seeing this GDC talk on the VFX in Diablo III, I wanted to learn how to do something similar.
The lava-effect is based on the winning entry of Riot’s Art Contest on polycount in the category VFX.
To create the splines for the lava-effect I followed this amazing tutorial on editor friendly splines for Unity and did a few modifications so that they work with animations.
BlendAdd
At first I searched for a way to implement the Blend Mode mentioned in the talk. As it turns out, it is very easy to do and should be one of the default blend modes available in every larger engine.
1 |
Blend One OneMinusSrcAlpha |
Per-Particle Offset
After having the blend mode done in a custom Unity shader I went on to search for a way to realize the per-particle UV offset that was also mentioned in the GDC talk. Even though Unity has no way of doing per-particle UV offsets with the default particle system, my goal was, to find a solution that works without 3rd party plugins and using solely the default particles.
This workaroung for this problem is absolutely not the best one and I do not recommend using it for anything else than experimentation!
The only way I came up with, without having access to the Unity engine source code and using only the default particle system, was a simple script and a custom shader.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
using UnityEngine; using System.Collections; public class PerParticleOffset : MonoBehaviour { ParticleSystem psys; ParticleSystem.Particle[] particles_S; // Use this for initialization void Start () { psys = gameObject.GetComponent<ParticleSystem>(); particles_S = new ParticleSystem.Particle[psys.maxParticles]; } // Update is called once per frame void LateUpdate () { int partNUM = psys.GetParticles(particles_S); for (int i = 0; i < partNUM; i++) { //Calculate the current lifetime of the particle. float pLifeTime = particles_S[i].startLifetime - particles_S[i].lifetime; float pRealLifeTime = 1 / particles_S[i].startLifetime * pLifeTime; Color32 newcolor = new Color32(255,255,255,255); //R is used for gradient over lifetime. newcolor.r = (byte)(pRealLifeTime * 255); //G maybe used for color by speed. newcolor.g = (byte)(255); //B is used for random uv offset in x. if (particles_S[i].startColor.b == (byte)255) { newcolor.b = (byte)(Random.Range(0, 254)); } else { newcolor.b = particles_S[i].startColor.b; } //A is used for random uv offset in y. if (particles_S[i].startColor.a == (byte)255) { newcolor.a = (byte)(Random.Range(0, 254)); } else { newcolor.a = particles_S[i].startColor.a; } particles_S[i].startColor = newcolor; } psys.SetParticles(particles_S, partNUM); } } |
It uses the Blue and Alpha channel of the initial color to offset along U and V for each particle individually. This on the other hand makes the particle Color over Lifetime and Color by Speed unusable as anything else than values of 1 would change the offsets. To compensate this I created the shader with two additional color parameters. One for a lifetime of 0 and one for a lifetime of 1 and used the Red channel of the initial color as lifetime input, to have at least a linear transition between a start and end color. The same could be done for Color by Speed which would use the Green channel of the initial color and another two color parameters in the shader.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
Shader "Particles/PPOffset" { Properties { _MainTex ("Maks1", 2D) = "white" {} _GradientA("GradientA", Color) = (0, 0, 0, 1) _GradientB("GradientB", Color) = (0, 0, 0, 1) [Enum(One,1,SrcAlpha,5)] _SrcBlend ("SrcBlend", Float) = 1 [Enum(OneMinusSrcAlpha,10,DstAlpha,7)] _DstBlend ("DstBlend", Float) = 7 _AmountX ("AmountX", Float) = 0 _AmountY ("AmountY", Float) = 0 _PanSpeed ("PanSpeed", Float) = 0 } SubShader { //Transparent Tags { "Queue" = "Transparent" "IgnoreProjectors" = "True" "RenderType" = "Transparent" } Lighting Off ZWrite Off Cull Off Blend [_SrcBlend] [_DstBlend] Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" //Init shader parameters //Textures sampler2D _MainTex; float4 _MainTex_ST; //Colors half4 _GradientA; half4 _GradientB; //Other float _AmountX; float _AmountY; float _PanSpeed; struct appdata { float4 vertex : POSITION; float2 uv0 : TEXCOORD0; float4 color : COLOR; }; struct v2f { float4 color : COLOR; float2 uv0 : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); //Get Random UV offset from vertex color. //This is set per particle in the script. _MainTex_ST.z += v.color.b; _MainTex_ST.w += v.color.a; //Pan UV coordinates based on _Amount and _PanSpeed. _MainTex_ST.z += (_Time.y * _AmountX) * _PanSpeed; _MainTex_ST.w += (_Time.y * _AmountY) * _PanSpeed; //Transform UV coordinates with the input of _MainTex (Tiling / Offset). o.uv0.xy = TRANSFORM_TEX(v.uv0, _MainTex); o.color = v.color; return o; } float4 frag (v2f i) : COLOR { // sample the texture fixed4 tex = tex2D(_MainTex, i.uv0); //Color over Lifetime lerp based on per particle vertex color. //Currently supports only 2 colors at positions 0 and 1. half4 lerpS = lerp(_GradientA, _GradientB, i.color.r); return half4(lerpS * tex); } ENDCG } } } |