I've been working on a little voxel-based mobile game, a game with plenty of explosions... and I thought it would be cool if stuff could explode into their constituent cubes.

Exploding stuff

PicaVoxel has scripts for exploding voxel meshes, but I just couldn't squeeze the performance out of it that I needed for mobile.... so I decided some fakery was in order instead, and passed on the package.

The problem

My game has a lot of "explodable objects" in different sizes and with different colour makeup. All of the objects have both a diffuse and emission texture. I wanted emissive parts of the object to explode into emissive cubes. I also wanted all of the cubes generated in the explosion to be emissive at first, and then fade to their default state, to simulate a burst of heat.

The solution

The solution consisted of:

  • Use object pooling, for the obvious performance reasons, but also because we want to precalculate some stuff for the explosions on awake.
  • Explode stuff using a particle system, from a volume that roughly matches the shape of the object, into cubes, with colours that match the object, and with the aforementioned emissive properties.
  • Make a shader based on vertex colours to support the above

Object Pooling

Pooling is simply keeping the gameobjects in the world and using "SetActive" instead of Instantiate/Destroy. It saves precious mobile CPU cycles, prevents garbage collection, and since we want to do some expensive precalculation on each object, it's basically mandatory. All the precalculation is done on awake, before we render the first frame in the scene.

Setting up the particle system (and collider too)

We'll attach a script to our explodable gameobject. Each explodable object has a rigidbody and box collider, a child object with a mesh + renderer for the non-exploded version, and a child object with a particle system for the exploded version.

The particle system has:

  • a zero particle emission rate
  • a single burst configured with 1 cycle only (the explosion)
  • A shape of "Box"
  • Renderer set to Mesh: Cube

Setup code below. We're simply setting the size of our particle system, and the number of particles it produces, based on the size of our game object.

    // explosion-related private variables
    GameObject _gameObject;
    Color[] _explosionColors;
    GameObject _explosion;
    ParticleSystem _explosionParticles;
    ParticleSystemRenderer _explosionRenderer;
    bool _explosionColorsSet;
    float _explosionStartTime;

    // collider
    protected BoxCollider _collider;

    void Awake()
    {
        _gameObject = GetComponentInChildren<MeshFilter>();
        var bounds = _gameObject.GetComponent<MeshFilter>().mesh.bounds;
        
        length = bounds.size.z;

        var particleSystem = GetComponentInChildren<ParticleSystem>();

        _explosion = particleSystem.gameObject;
        _explosionParticles = particleSystem;
        _explosionRenderer = _explosionParticles.GetComponent<ParticleSystemRenderer>();

        var main = _explosionParticles.main;
        main.maxParticles = Mathf.RoundToInt(length * 40);

        var emission = _explosionParticles.emission;
        var bursts = new ParticleSystem.Burst[1];
        emission.GetBursts(bursts);

        bursts[0].minCount = bursts[0].maxCount = (short)main.maxParticles;
        emission.SetBursts(bursts);

        _explosion.SetActive(false);
        
        GetExplosionColors();

        _collider = GetComponent<BoxCollider>();

        _collider.size = new Vector3(bounds.size.x - 0.2f, bounds.size.y - 0.2f, bounds.size.z - 0.2f); // slightly smaller than bounds to be a little forgiving on close calls

        var explosionShape = _explosionParticles.shape;
        explosionShape.scale = _collider.size;
    }

Mapping texture mapped colours to particles

This assumes the shader you are using has maps called _MainTex and _EmissionTex.

Here we load up the material used by the mesh we want to load, traverse the UV coordinates, go find the corresponding pixel in the texture map for each UV, and store the colour found along with how many occurences of that colour were found.

Once we have the weights, we extrapolate that into a lookup table for our particle system.

Basically we are converting UV coloured meshes into vertex colour data. It's not perfect - we should probably pixel grab from the midpoint of each triangle instead of from the vertices, but it's good enough.

void GetExplosionColors()
    {
        var meshFilter = _gameObject.GetComponent<MeshFilter>();
        var mesh = meshFilter.sharedMesh;
        var meshRenderer = _gameObject.GetComponent<MeshRenderer>();

        var albedo = meshRenderer.sharedMaterial.GetTexture("_MainTex") as Texture2D;
        var emission = meshRenderer.sharedMaterial.GetTexture("_EmissionTex") as Texture2D;

        var colorWeights = new Dictionary<Color, int>();

        foreach (var uv in mesh.uv)
        {
            // emissive colors are set to alpha 0.1 (render them with vertex color shader)
            var emissiveColor = emission.GetPixelBilinear(uv.x, uv.y);
            if (emissiveColor.r > 0 || emissiveColor.g > 0 || emissiveColor.b > 0)
            {
                emissiveColor.a = 0.1f;

                if (colorWeights.ContainsKey(emissiveColor))
                    colorWeights[emissiveColor] += 1;
                else
                    colorWeights.Add(emissiveColor, 1);

                continue;
            }

            var albedoColor = albedo.GetPixelBilinear(uv.x, uv.y);
            albedoColor.a = 1f;

            if (colorWeights.ContainsKey(albedoColor))
                colorWeights[albedoColor] += 1;
            else
                colorWeights.Add(albedoColor, 1);
        }

        // now that we have color weights, turn it into a simple lookup for each individual particle
        _explosionColors = new Color[_explosionParticles.main.maxParticles];

        // normalized sum for easy lookupification
        var colors = new Color[colorWeights.Count];
        var weightSums = new float[colorWeights.Count];

        for (var i = 0; i < colors.Length; i++)
        {
            colors[i] = colorWeights.Keys.ElementAt(i);
            var normalizedWeight = (float)colorWeights.Values.ElementAt(i) / mesh.uv.Length * _explosionColors.Length;
            weightSums[i] = i == 0 ? normalizedWeight : weightSums[i - 1] + normalizedWeight;
        }

        var j = 0;
        // load weighted colors into lookup table
        for (int i = 0; i < _explosionColors.Length; i++)
        {
            if (Mathf.RoundToInt(weightSums[j]) <= i)
                j++;
            if (j > weightSums.Length - 1)
                j--;

            _explosionColors[i] = colors[j];
        }

    }

Particle Shader

In order to render our particles in the appropriate albedo colours, and with the appropriate emission, we'll need to write our own shader based on vertex colours.

This is a standard lit surface shader. It should be assigned to a material and assigned to the explosion particle system's renderer.

Note the _ElapsedTime, _FadeTime and _InitialEmission properties, which are for the fading emission. _ElapsedTime will need to be passed in from Script at runtime.

A note on shaders: I highly recommend diving in and trying to write your own. It's daunting at first but once you get the hang of it, it's a powerful tool in your toolbelt, and you'll never look back.

Shader "Custom/Vertex Color Fading Emission" {
	Properties {
		_Glossiness ("Smoothness", Range(0,1)) = 0.5
		_Metallic ("Metallic", Range(0,1)) = 0.0
		_EmissionStrength ("Emission Strength", Range(0,3)) = 1.0
		_ElapsedTime("Elapsed Time", Range(0,10000)) = 0
		_FadeTime ("Fade Time", Range(0,10)) = 5
		_InitialEmission ("Initial Emission Strength", Range(0,3)) = 1.0
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200
		
		CGPROGRAM

		#pragma surface surf Standard fullforwardshadows
		#pragma target 3.0

		struct Input {
			float4 vertexColor : COLOR;
		};

		half _Glossiness;
		half _Metallic;
		half _EmissionStrength;

		half _ElapsedTime; // elapsed time must be passed in by unity script on Update
		half _FadeTime;
		half _InitialEmission;
		half _FinalEmission;

		UNITY_INSTANCING_CBUFFER_START(Props)

		UNITY_INSTANCING_CBUFFER_END

		void surf (Input IN, inout SurfaceOutputStandard o) {

			fixed4 c = IN.vertexColor;
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;

			half emission = 0;

			if (c.a < 0.2)
				emission = _EmissionStrength;

			if (_ElapsedTime <= _FadeTime)
				emission = max(emission, (_FadeTime - _ElapsedTime) * _InitialEmission / _FadeTime);

			o.Emission = c.rgb * emission;
		}
		ENDCG
	}
	FallBack "Diffuse"
}

Colourize the particles

We're colourizing in late update. If the explosion has been triggered, then the particles will be available for us to modify here, and need to be modified just once.

Setting the particle colour literally just sets the vertex colours on the particle, and won't be visible without an appropriate vertex colour shader like the one we just made.

Here we also pass in the _ElapsedTime value for our fading emission.

You'll note below that I'm doing a small size increment on each particle. This is a dirty hack to prevent "z-fighting".

void LateUpdate()
    {
        if (_explosionParticles.isPlaying)
            _explosionRenderer.material.SetFloat("_ElapsedTime", Mathf.Clamp(Time.time - _explosionStartTime, 0, 5));

        if (_explosionColorsSet)
            return;

        var particles = new ParticleSystem.Particle[_explosionParticles.main.maxParticles];
        _explosionParticles.GetParticles(particles);

        for (var i = 0; i < particles.Length; i++)
        {
            particles[i].startColor = _explosionColors[Random.Range(0, _explosionColors.Length)];
            particles[i].startSize = particles[i].startSize + i * 0.001f;
        }

        _explosionParticles.SetParticles(particles, particles.Length);

        _explosionColorsSet = true;
    }

Blow Stuff Up

The trigger, and the reset for repeated explosion fun times.

    public virtual void Explode()
    {
        if (_explosion.activeSelf)
            return;

        _explosionStartTime = Time.time;
        
        _gameObject.SetActive(false);
        _collider.enabled = false;

        if (_animator != null)
            _animator.enabled = false;

        _explosion.SetActive(true);
        _explosionColorsSet = false;
        _explosionParticles.Play();
    }
    
    public void Initialize()
    {
        _explosionParticles.Stop();
        explosion.SetActive(false);
        _gameObject.SetActive(true);

        gameObject.SetActive(true);

        _explosionColorsSet = false;
    }

That's it!

Whilst I am an experienced (mostly non-game) developer, I have only been developing in Unity for a short while. I'm currently working on my first commercial game in 17 years.

I'm able to generate multiple explosions at once on my 4 year old test device, in-game, and maintain a locked 30fps. There is still headroom for more optimisation. Using an unlit shader should see big performance gains.

If you learned something, I'd be very grateful if you could email it to a friend, or share it on Twitter or Facebook. Thankyou!