← Back to blog list

Scaling Our Particle System: From Shuriken to VFX Graph

May 6, 2023 (1y ago)

Scaling Our Particle System: From Shuriken to VFX Graph

The Challenge: Rendering Millions of Particles

When we first set out to create our scanner effect, we knew we wanted something visually striking that could represent millions of data points in real-time. Our initial implementation used Unity's built-in Shuriken particle system, but we quickly ran into performance issues as we scaled up the number of particles.

The Shuriken Approach

Our LookScanner class was responsible for casting rays and generating particle data:

LookScanner.cs
 
public class LookScanner : MonoBehaviour, IScans
{
    // ... other code ...
 
    private void Update()
    {
        for (int i = 0; i < rayCount; i++)
        {
            RaycastHit hitInfo;
            if (Physics.Raycast(rays[i], out hitInfo, maxRayDistance, layerMask))
            {
                var scannable = hitInfo.collider.gameObject.GetComponent<IScannable>();
                if (scannable != null)
                {
                    EmitParticleFromHit(hitInfo, scannable);
                }
            }
        }
    }
 
    // ... other code ...
}

The VFX Graph Solution

Our breakthrough came when we realized we could leverage the power of VFX Graph and a novel approach to particle data storage. Instead of trying to manage millions of individual particle objects, we decided to bake the coordinates of each particle into a texture. Texture-Based Particle Storage

The key insight was that we could use the RGB channels of a texture to represent the XYZ coordinates of each particle. This allowed us to efficiently store and transfer large amounts of particle data to the GPU.

Here's how we implemented this in our ParticleCollector class:

ParticleCollector.cs
 
public class ParticleCollector : MonoBehaviour
{
    // ... other code ...
 
    public void CacheParticle(Vector3 position, Color color, float size)
    {
        if (currentParticleCount >= Texture2DMaxHeight)
        {
            CreateAndInitializeEffect();
        }
        pointData[currentParticleCount] = new Color(position.x, position.y, position.z);
        colorData[currentParticleCount] = new Color(color.r, color.g, color.b, size);
        currentParticleCount++;
    }
 
    // ... other code ...
}

This approach allowed us to store millions of particles in a compact format that could be easily processed by the GPU.

VFX Graph Integration

With our particle data now stored in textures, we could use VFX Graph to efficiently render these particles. We created a custom VFX Graph setup that reads from our position and color textures to generate the final visual effect. The Results

By transitioning from Shuriken to VFX Graph and implementing our texture-based particle storage system, we achieved real-time rendering of millions of particles. This not only enhanced the visual fidelity of our scanner effect but also opened up new possibilities for representing complex data in our game.

The performance gains were substantial, allowing us to create much more detailed and immersive environments without sacrificing frame rate.

Lessons Learned

  1. Think outside the box: Our texture-based approach wasn't immediately obvious, but it proved to be a game-changer.
  2. Leverage GPU power: By moving more work to the GPU through VFX Graph, we achieved much higher particle counts.
  3. Optimize data transfer: The texture-based storage allowed for efficient data transfer between CPU and GPU.

This project taught us the importance of being willing to rethink our approach when faced with technical limitations. By combining creative problem-solving with Unity's powerful tools, we were able to push the boundaries of what's possible in real-time particle rendering.