Now let’s take a look at how to achieve the same thing without writing to uniforms (unity_ObjectToWorld and unity_WorldToObject matrices).
To do this, we’re going to write our own ProceduralInstancing HLSL include.
At its core, here’s what’s required:
#ifndef SHADERGRAPH_PARTICLESINSTANCING_INCLUDED
#define SHADERGRAPH_PARTICLESINSTANCING_INCLUDED
#if defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) && !defined(SHADER_TARGET_SURFACE_ANALYSIS)
#define UNITY_PARTICLE_INSTANCING_ENABLED
#endif
#if defined(UNITY_PARTICLE_INSTANCING_ENABLED)
#ifndef UNITY_PARTICLE_INSTANCE_DATA
#define UNITY_PARTICLE_INSTANCE_DATA DefaultParticleInstanceData
#endif
struct DefaultParticleInstanceData
{
float3x4 transform;
uint color;
float animFrame;
};
StructuredBuffer<UNITY_PARTICLE_INSTANCE_DATA> unity_ParticleInstanceData;
float4 unity_ParticleUVShiftData;
half unity_ParticleUseMeshColors;
#endif // UNITY_PARTICLE_INSTANCING_ENABLED
void ParticleInstancingSetup() {}
#endif //SHADERGRAPH_PARTICLESINSTANCING_INCLUDED
Simply put, this
- Only enables particle instancing if it is supported and enabled.
- Defines a default particle data struct.
- Adds a StructuredBuffer for the particles data and a couple of other uniforms needed to store Particle Systems data.
This also contains a ParticleInstancingSetup function, required for the #pragma instancing_options procedural:ParticleInstancingSetup, which is needed for Shuriken Particle Systems to enable Particle Instancing, but left empty as we’re going to set things up in Shader Graph.
Particle Transform
As detailed in this other post, Shader Graph targets expect Vertex Data (Position, Normal and Tangent) in Object Space.
By default, the matrices unity_ObjectToWorld and unity_WorldToObject of Particles Systems are set to identity, in other words, they don’t apply any transformation.
When the Particle System is set to Billboard, or Mesh but with GPU Instancing disabled, the mesh contains all particles. All vertices are already transformed and in World Space.
When GPU Instancing is enabled, the Mesh vertices come in Object Space, and have to be transformed per-particle (per-instance), from Object (Mesh) Space to World Space.
The ParticleInstancingSetup function in URP’s ParticlesInstancing.hlsl does that by overwriting the unity_ObjectToWorld and unity_WorldToObject matrices with the particle’s transform, which are ultimately used to transform every vertex.
Instead of overwriting those matrices, we’re going to transform the vertices’ position, normal and tangent, from Object Space to World Space and feed the results in Shader Graph’s Vertex Stage inputs.
First, we want to be able to get the particle’s transformation matrix.
To do that, we add a function tapping in the StructuredBuffer using the unity_InstanceID.
void ParticleTransform_float(out float4x4 transform)
{
#if defined(UNITY_PARTICLE_INSTANCING_ENABLED)
UNITY_PARTICLE_INSTANCE_DATA data = unity_ParticleInstanceData[unity_InstanceID];
transform = float4x4(data.transform, 0, 0, 0, 1);
#else
transform = float4x4(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1);
#endif
}
Since Shuriken stores it as a float3x4, and Shader Graph only supports float3x3 or float4x4, we convert the float3x4 to float4x4.
We need to initialize the transform matrix for when particle instancing is not enabled, although this will never be used as we’re going to put this behind a Keyword.
With this we can create a shader subgraph to handle vertex position, normal and tangent.
Using the Keyword PROCEDURAL_INSTANCING_ON as Predefined, allows us to switch between Object Space Inputs and Transformed Inputs.
For Normal and Tangent, we can use a Custom Function Node in String Mode to simply cast the float4x4 to a float3x3, thus dropping the translation.
We can wrap all this in a subgraph, including the pragma injection custom function node.
All there is to do then is to use this subgraph node in our main graph to handle GPU instancing transformations.
The inputs use Custom Bindings to make them default to Object Space, but allows applying pre-transformations.
Particle UVs
To get the particles’ texture coordinates properly, we can copy/paste the GetParticleTexcoords function from earlier in the same HLSL file, and use the same Custom Function Nodes and Subgraphs as before.
Custom Particle Data Struct
To customize the Particle Data, we just need to create another HLSL file, just like we did before, with an include for this new instancing setup.
#ifndef UNITY_PARTICLE_INSTANCE_DATA
#define UNITY_PARTICLE_INSTANCE_DATA CustomParticleInstanceData
struct CustomParticleInstanceData
{
float3x4 transform;
uint color;
float animFrame;
#ifdef _FLIPBOOKBLENDING_ON
float animBlend;
#endif
};
#endif
#include “ShurikenDefault.hlsl”
To make sure it gets included first, we reference it with a passthrough like this.
And everything else will work the same.


