Multi-threaded circle texture code, can it be faster?

Hey all,

Just wanted to share one of a few multi-threaded pieces of coding I did that can be used within Unity. It creates a circle texture that will fade from an inner color to an outer color which can come in handy for several processes :slight_smile:

I pretty sure it’s the first multi-threaded algorithm I’ve written and wanted to get some other eyes on it and see if anyone would give me some pointers on how to increase the performance of it. The bigger the image, the more performance increase you’ll see since some of the functions like Texture2D.Apply(), understandably, cannot be multi-threaded. I’ve been using it for several months now, so it should be error free. Sorry for the lack of commenting also.

Thanks,
Nathan

using UnityEngine;
using System.Collections;
using System.Threading;


public class Texture2DHelper {
	private const int sleepTime = 1;

   private static Texture2D MakeCircleTexture(int pixelDiameter, float innerRadius, Color innerColor, Color outerColor, TextureFormat format, bool mipmap, int threadCount) {
      Texture2D circleTexture = new Texture2D(pixelDiameter, pixelDiameter, format, mipmap);
      Color[] colors = new Color[pixelDiameter * pixelDiameter];
      float outerRadius = pixelDiameter/2.0f;
      MakeCirclePixelProcessor processor;
      Thread[] threads = null;

      outerRadius -= 1;
      if ( innerRadius > outerRadius )
         innerRadius = outerRadius-1;

      processor = new MakeCirclePixelProcessor(colors, pixelDiameter, innerRadius, innerColor, outerColor, threadCount);

      if ( threadCount > 1 ) {
         threads = new Thread[threadCount-1];

         for ( int i = 0; i < threads.Length; i++ ) {
            processor.currentThread = i+1;
            threads[i] = new Thread(new ThreadStart(processor.Process));
            threads[i].Start();

            while ( !processor.threadStarted[i+1] )
               Thread.Sleep(sleepTime);
         }
      }

      processor.currentThread = 0;
      processor.Process();

      while ( !processor.IsDone() )
         Thread.Sleep(sleepTime);

      circleTexture.SetPixels(processor.colors);
      circleTexture.Apply(true);

      if ( threads != null )
         for ( int i = 0; i < threads.Length; i++ )
            threads[i] = null;

      circleTexture.name = "CircleTexture";

      return circleTexture;
   }


   private class MakeCirclePixelProcessor {
      public Color[] colors;
      int size;
      float innerRadius;
      Color innerColor;
      Color outerColor;
      int threadCount;
      public bool[] threadStarted;
      public bool[] threadDone;
      public int currentThread = 0;

      
      public MakeCirclePixelProcessor(Color[] colorPixels, int pixelSize, float innerRadius, Color innerColor, Color outerColor, int threadCount) {
         this.colors = colorPixels;
         this.size = pixelSize;
         this.innerRadius = innerRadius;
         this.innerColor = innerColor;
         this.outerColor = outerColor;
         this.threadCount = threadCount;

         threadStarted = new bool[threadCount];
         threadDone = new bool[threadCount];

         for ( int i = 0; i < threadStarted.Length; i++ ) {
            threadStarted[i] = false;
            threadDone[i] = false;
         }
      }


      public void Process() {
         float outerRadius = size/2.0f;
         Vector2 centerPoint = new Vector2(outerRadius, outerRadius);
         int currentColorIndex = 0;
         int thisThread = currentThread;
         Color newColor = new Color(0,0,0,0);
         int x = 0;
         int y = 0;

         threadStarted[thisThread] = true;

         for ( y = thisThread; y < size; y += threadCount ) {
            currentColorIndex = y * size;

            for ( x = 0; x < size; x++ ) {
               Vector2 currentPoint = new Vector2(x, y);
               float distance = Vector2.Distance(centerPoint, currentPoint);
               float percentOuter = GetRangePercentage(innerRadius, outerRadius, distance);
               float percentInner = 1.0f - percentOuter;

               newColor.r = (innerColor.r * percentInner) + (outerColor.r * percentOuter);
               newColor.g = (innerColor.g * percentInner) + (outerColor.g * percentOuter);
               newColor.b = (innerColor.b * percentInner) + (outerColor.b * percentOuter);
               newColor.a = (innerColor.a * percentInner) + (outerColor.a * percentOuter);

               colors[currentColorIndex] = newColor;
               currentColorIndex++;
            }
         }

         threadDone[thisThread] = true;
      }


      public bool IsDone() {
         bool isDone = true;

         foreach ( bool done in threadDone )
            if ( !done )
               isDone = false;

         return isDone;
      }
   }


   // This will return a percentage of a value (curVal) between 2 values (lowVal and highVal)
	public static float GetRangePercentage( float lowVal, float highVal, float curVal ) {
		if ( curVal > highVal )
			return 1.0f;

		if ( curVal < lowVal )
			return 0.0f;

		float percentage = 1.0f - (( highVal - curVal ) / ( highVal - lowVal ));

		return percentage;
	}
}

I am needing to generate a circle in code so this is a useful reference. Looking at your code it looks pretty good. Having multiple threads is pretty straightfoward, you just have it work on generating the new arrays in parallel and when they’re done you you do a single SetPixels. I would use a single array and an offset/range assigned to each thread.

How this script could be implemented?

Thanks

Oops, I had several overloads so the main function was private… here’s an update:

using UnityEngine;
using System.Collections;
using System.Threading;


public class Texture2DHelper {
   private const int sleepTime = 1;

   public static Texture2D MakeCircleTexture(int pixelDiameter, float innerRadius, Color innerColor, Color outerColor, TextureFormat format, bool mipmap) {
		return MakeCircleTexture(pixelDiameter, innerRadius, innerColor, outerColor, format, mipmap, GetCPUCount());
   }


   public static Texture2D MakeCircleTexture(int pixelDiameter, float innerRadius, Color innerColor, Color outerColor, TextureFormat format, bool mipmap, int threadCount) {
      Texture2D circleTexture = new Texture2D(pixelDiameter, pixelDiameter, format, mipmap);
      Color[] colors = new Color[pixelDiameter * pixelDiameter];
      float outerRadius = pixelDiameter/2.0f;
      MakeCirclePixelProcessor processor;
      Thread[] threads = null;

      outerRadius -= 1;
      if ( innerRadius > outerRadius )
         innerRadius = outerRadius-1;

      processor = new MakeCirclePixelProcessor(colors, pixelDiameter, innerRadius, innerColor, outerColor, threadCount);

      if ( threadCount > 1 ) {
         threads = new Thread[threadCount-1];

         for ( int i = 0; i < threads.Length; i++ ) {
            processor.currentThread = i+1;
            threads[i] = new Thread(new ThreadStart(processor.Process));
            threads[i].Start();

            while ( !processor.threadStarted[i+1] )
               Thread.Sleep(sleepTime);
         }
      }

      processor.currentThread = 0;
      processor.Process();

      while ( !processor.IsDone() )
         Thread.Sleep(sleepTime);

      circleTexture.SetPixels(processor.colors);
      circleTexture.Apply(true);

      if ( threads != null )
         for ( int i = 0; i < threads.Length; i++ )
            threads[i] = null;

      circleTexture.name = "CircleTexture";

      return circleTexture;
   }


   private class MakeCirclePixelProcessor {
      public Color[] colors;
      int size;
      float innerRadius;
      Color innerColor;
      Color outerColor;
      int threadCount;
      public bool[] threadStarted;
      public bool[] threadDone;
      public int currentThread = 0;

     
      public MakeCirclePixelProcessor(Color[] colorPixels, int pixelSize, float innerRadius, Color innerColor, Color outerColor, int threadCount) {
         this.colors = colorPixels;
         this.size = pixelSize;
         this.innerRadius = innerRadius;
         this.innerColor = innerColor;
         this.outerColor = outerColor;
         this.threadCount = threadCount;

         threadStarted = new bool[threadCount];
         threadDone = new bool[threadCount];

         for ( int i = 0; i < threadStarted.Length; i++ ) {
            threadStarted[i] = false;
            threadDone[i] = false;
         }
      }


      public void Process() {
         float outerRadius = size/2.0f;
         Vector2 centerPoint = new Vector2(outerRadius, outerRadius);
         int currentColorIndex = 0;
         int thisThread = currentThread;
         Color newColor = new Color(0,0,0,0);
         int x = 0;
         int y = 0;

         threadStarted[thisThread] = true;

         for ( y = thisThread; y < size; y += threadCount ) {
            currentColorIndex = y * size;

            for ( x = 0; x < size; x++ ) {
               Vector2 currentPoint = new Vector2(x, y);
               float distance = Vector2.Distance(centerPoint, currentPoint);
               float percentOuter = GetRangePercentage(innerRadius, outerRadius, distance);
               float percentInner = 1.0f - percentOuter;

               newColor.r = (innerColor.r * percentInner) + (outerColor.r * percentOuter);
               newColor.g = (innerColor.g * percentInner) + (outerColor.g * percentOuter);
               newColor.b = (innerColor.b * percentInner) + (outerColor.b * percentOuter);
               newColor.a = (innerColor.a * percentInner) + (outerColor.a * percentOuter);

               colors[currentColorIndex] = newColor;
               currentColorIndex++;
            }
         }

         threadDone[thisThread] = true;
      }


      public bool IsDone() {
         bool isDone = true;

         foreach ( bool done in threadDone )
            if ( !done )
               isDone = false;

         return isDone;
      }
   }


   // This will return a percentage of a value (curVal) between 2 values (lowVal and highVal)
   public static float GetRangePercentage( float lowVal, float highVal, float curVal ) {
      if ( curVal > highVal )
         return 1.0f;

      if ( curVal < lowVal )
         return 0.0f;

      float percentage = 1.0f - (( highVal - curVal ) / ( highVal - lowVal ));

      return percentage;
   }


   public static int GetCPUCount() {
	   return SystemInfo.processorCount;
   }
}

Here’s an example script you can attach to a GameObject with a call to the function:

using UnityEngine;
using System.Collections;

public class CircleTextureMaker : MonoBehaviour {
	public Texture2D tex1;
	public Texture2D tex2;

	// Use this for initialization
	void Start () {
		tex1 = Texture2DHelper.MakeCircleTexture(128, 0, Color.red, Color.clear, TextureFormat.ARGB32, true);
		tex2 = Texture2DHelper.MakeCircleTexture(128, 50, Color.red, Color.clear, TextureFormat.ARGB32, true);
	}


	void OnGUI() {
		GUILayout.Label(tex1);
		GUILayout.Label(tex2);
	}
}

Enjoy,
Nathan