Devlog 6 / PA Particle Field 1.2

Links: PAPF Demos | Asset Store

Much of the last month for me has been the development of PA Particle Field 1.2 and with it finally released it’s time for an in depth look at what’s new. In this post I’ll briefly reiterate what PA Particle Field is about, detail the inner workings of the mesh particles and explain how the animated mesh particles demo works.

About PA Particle Field

PA Particle Field is an alternative particle system for Unity, specifically optimised for looping ambient effects. The key difference between it and other systems is its distribution of work between CPU and GPU. Particle emission is done on the CPU and movement is done on the GPU, the GPU also recycles particles so in practice the expensive CPU emission is usually only performed once, resulting in wicked fast GPU particles across all platforms.


So Mesh Particles

Version 1.2 introduced mesh particles, which brought with it some new problems. With billboard particles, once pivot position has been sorted normals and tangents can be calculated in the shader and the rest can be handled in 2D, all that disappears with Meshes.

The mesh data for a billboard particle looks like this; : pivot position
     Normal.xy : move speed, spin speed
     UV : texture coordinates
     UV1 : local vertex position
     Color : vertex color

I leave tangents untouched as I found data stored there may be modified and unreliable for anything other than tangents.

Things of interest there is the data in the normals is unrelated to the actual normals, and the vertex position is stored in UV1, because it only needs an X and Y position. 

Here’s how the data is arranged for mesh particles; = local vertex position = pivot position, vertex normal, axis of rotation
     UV = texture coordinates
     UV1 = move speed, spin speed
     Color = vertex color
     Tangents = vertex tangents

The thing to note here is the normal now contains 3 pieces of 3 channel information, this is done using a technique called float packing, where 1 float can be encoded with multiple values.

Originally I went off on one and implemented this using spherical spiral, the graph below shows a sphere created using a single variable t;

sin(x)*sin(256*x) vs sin(x)*cos(256*x)


Spherical Spiral Equation:
x = sin(t) * sin(256* t)
y = sin(t) * cos(256* t)
z = cos(t)


Using this I could store a single value for and unpack it to a normalized Vector3. This worked and I could store mesh normals using this with negligible loss of accuracy, but encoding and decoding were expensive and being limited to normalized values was too restrictive.

Turned out Unity’s own Aras had a solution to this back in 2009 so with a bit of tweaking I was able to compress much more useful, non-normalized data in a faster way. The code in the mesh generation for packing the vertex normals looked like this;

static float Vector3ToFloat( Vector3 c ) {
     c = (c + * 0.5f;
     return Vector3.Dot(new Vector3(Mathf.Round(c.x * 255),Mathf.Round(c.y * 255),Mathf.Round(c.z * 255)), new Vector3(65536, 256, 1));

And on the shader side the unpack code looks like this;

float3 UnpackVector( float v ) {
     float3 f = frac(v / float3(16777216, 65536, 256));
     f.x = (f.x*2)-1;
     f.y = (f.y*2)-1;
     f.z = (f.z*2)-1;
     return f;

With this code in hand I could then pack enough data into the PA Particle Field system for it to support Mesh particles, Billboard particles are still handled the same for performance.

The ‘Warrior’ Shader

Because the local vertex position of the model is unmodified, customized vertex movement can be performed in the vertex shader, to show this I created the animated mesh particles demo, featuring the “Warrior” model from our free SciFi Vehicles and Enemies Pack, with its legs animated in a custom vertex shader. The full warrior shader looks like this;

Shader "PA/Warrior" {
 Properties {
 _Color ("Color", Color) = (1,1,1,1)
 _MainTex ("Albedo (RGB)", 2D) = "white" {}
 _Glossiness ("Smoothness", Range(0,1)) = 0.5
 _Metallic ("Metallic", Range(0,1)) = 0.0
 _MoveSpeed ("Move Speed", Float) = 5
 SubShader {
 Tags { "RenderType"="Opaque" }
 LOD 200


 // Basic set of includes required for effect
 #pragma multi_compile DIRECTIONAL_ON
 #pragma multi_compile EDGE_SCALE_ON
 #pragma multi_compile EDGE_ALPHA_OFF
 #pragma multi_compile WORLDSPACE_OFF WORLDSPACE_ON
 #pragma multi_compile SHAPE_CUBE

 // Include PAParticleField cgincs
 #include "Assets/PopupAsylum/PAParticleField/Shaders/ParticleField.cginc"
 #include "Assets/PopupAsylum/PAParticleField/Shaders/ParticleMeshField.cginc"

 // PBS with all frills removed for copmile speed
 #pragma surface surf Standard noshadow nometa nolightmap nodynlightmap nodirlightmap exclude_path:deferred exclude_path:prepass vertex:vert

 // Use shader model 3.0 target, to get nicer looking lighting
 #pragma target 3.0

 sampler2D _MainTex;

 struct Input {
 float2 uv_MainTex;

 half _Glossiness;
 half _Metallic;
 half _MoveSpeed;
 fixed4 _Color;

 void vert(inout appdata_full v, out Input o){
 // Apply Z Sine Wave
 float sinTime = sin(_Time.w * _MoveSpeed + v.color.g * 4 + v.color.b * 4);
 v.vertex.z += sinTime * v.color.r * 0.01;
 // Apply Clamped Y Sine wave
 sinTime = clamp(sin(_Time.w * _MoveSpeed + 1 + v.color.g * 4 + v.color.b * 4), 0, 1);
 v.vertex.y += sinTime * v.color.r * 0.01;
 // Apply PA Particle Field and Position vertex in world space or local space
 v.vertex = PAPositionVertexSurf(v.vertex);

 void surf (Input IN, inout SurfaceOutputStandard o) {
 fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
 o.Albedo = c.rgb;
 o.Metallic = _Metallic;
 o.Smoothness = _Glossiness;
 o.Alpha = c.a;
 FallBack "Diffuse"

The surface shader’s as simple as they come, just the default shader produced by Unity 5, all the interesting stuff is done in the vertex shader which animates the model using 2 sine waves and a specially configured model.

To configure the model I started with the free ‘Warrior’ model available in the SciFi Enemies Pack and reduced the vertex count, PA Particle Field is still limited to 65536 vertices and at 2053 vertices the Warrior model could only have 31 particles per field. After better optimizing the UVs and removing a few edge loops the Warrior was down to 1255 vertices that could create 52 mesh particles.


After that I created an extra UV set with the right/front/left/back legs were isolated and orientated to fill the UVs horizontally to make it easier to apply vertex color. I created a gradient texture in photoshop with a horizontal gradient from 0 to 1 in the red channel and divided into two halves of 0 and 0.5 in the green channel, these will be used to modify the amplitude and time offset of the sine waves. I filled the blue channel with 1 to use later as a second time offset.


I then applied the gradient to the vertices using Maya’s Import Attributes function, giving me the final model.


The leg will be moved by 2 sine waves, one that moves the leg forward and backward, and another that moves if up and down, the combination of these would move the vertices in a circle but by clamping the y component between 0 and 1 it becomes a half circle. This is handled by these lines;

<pre>// Apply Z Sine Wave
float sinTime = sin(_Time.w * _MoveSpeed + v.color.g * 4 + v.color.b * 4);
 v.vertex.z += sinTime * v.color.r * 0.01;
 // Apply Clamped Y Sine wave
 sinTime = clamp(sin(_Time.w * _MoveSpeed + 1 + v.color.g * 4 + v.color.b * 4), 0, 1);
 v.vertex.y += sinTime * v.color.r * 0.01;</pre>


By putting a gradient of (1, 1, 0)  to (1,1,1) in the color variation of the PA Particle Field each particle gets a random ‘b’ vertex color time offset.

Then the PA Particle Field methods position the particles and transform them into local or world space. 

<pre>// Apply PA Particle Field and Position vertex in world space or local space
 v.vertex = PAPositionVertexSurf(v.vertex);</pre>

The final result looks like this, each particle has their own rotation and animation offset giving a vary varies scene using just 1 drawcall for all the Warriors.


Leave a comment

Your email address will not be published.