Quick Background:
I’m a self-taught hobbyist (tinkering since Unity 3.5) who is working on my first long term project. I typically can overcome any problem or bug by taking a stroll through google results for a few days, maybe even a week or two. Tons of online resources. One particular issue, though, has become an outlier this month. The heart of the issue lies in a subject way over my head. Multithreading. Before I dive head first into research for several weeks, if not months, I’d like to throw the problem your way and see if anyone has any quick and dirty solutions. I’d rather not halt progress.
The Bug:
My goal is to generate height maps using the ‘CoherentNoise’ library on several threads to speed up world generation process. The height maps are for individual terrains which are tiled together to emulate a massive finite world. Everything works perfectly when done on a single background ‘ThreadNinja’ thread (by Ciela Spike). When I dispatch several threads to handle a portion of the total terrains, the results aren’t consistent. The failure appears to caused by the ‘Function’ generator since that’s the only generator acting up. Or perhaps I’m using ThreadNinja poorly.
Note: I’m generating textures which are applied to tiled quads to make investigation easier.
Single Background Thread Code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using CoherentNoise;
using CoherentNoise.Generation;
using CoherentNoise.Generation.Displacement;
using CoherentNoise.Generation.Fractal;
using CoherentNoise.Generation.Modification;
using CoherentNoise.Generation.Patterns;
using CoherentNoise.Generation.Voronoi;
using CoherentNoise.Texturing;
using CielaSpike;
using System.Threading;
public class FuncTestSingleThread : MonoBehaviour {
[Header("World Settings")]
[Space(10)]
public GameObject prefab;
public int seed = 1;
public int size = 64;
public int count = 5;
[Header("Mask Settings")]
[Space(10)]
public float freq = 3f;
public float power = 0.1f;
public int octaves = 3;
public float iscale = 1.1f;
int xindex = 0;
int yindex = 0;
public AnimationCurve curve;
[Header("Mountainous Terrain Settings")]
[Space(10)]
public float ridgeExp = 0.4f;
public float ridgeOffset = .75f;
public float ridgeGain = 13.0f;
public float ridgeFreq = 0.5f;
public int ridgeOctaves = 8;
// other stuff
Vector3 mypos;
Quaternion myq;
void Start(){
mypos = transform.position;
myq = Quaternion.identity;
StartCoroutine(StartAsync());
}
IEnumerator StartAsync(){
Debug.Log("StartAsync()");
Task task;
yield return this.StartCoroutineAsync(FuncToThread(), out task);
Debug.Log ("DONE!");
}
IEnumerator FuncToThread(){
LogAsync ("FuncToThread Started");
//
Function opt = new Function((x,y,z) => { return curve.Evaluate(Vector2.Distance (new Vector2(x,y), new Vector2(count*0.5f,count*0.5f))/(count/2 * iscale));});
Turbulence trb = new Turbulence (opt, seed);
trb.Power = power;
trb.Frequency = freq;
trb.OctaveCount = octaves;
RidgeNoise mts = new RidgeNoise (seed);
mts.Exponent = ridgeExp;
mts.Offset = ridgeOffset;
mts.Gain = ridgeGain;
mts.Frequency = ridgeFreq;
mts.OctaveCount = ridgeOctaves;
Generator hmap = (mts * 0.5f + 0.25f) * trb;
for (int tileX = 0; tileX < count; tileX++) {
for (int tileY = 0; tileY < count; tileY++) {
int i = 0;
//float highest = -10f;
//float lowest = 10f;
yield return Ninja.JumpToUnity;
Texture2D img = new Texture2D(size,size);
Color[] pixels = new Color[size*size];
GameObject tile = Instantiate(prefab, mypos + new Vector3(tileX, tileY, 0), myq, transform);
yield return Ninja.JumpBack;
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
float sx = (float)x / (size - 1f) + (float)tileY;
float sy = (float)y / (size - 1f) + (float)tileX;
float num = hmap.GetValue (sx, sy, 0f);
pixels[i] = new Color (num, num, num, 1);
i++;
}
}
yield return Ninja.JumpToUnity;
img.SetPixels (pixels);
img.Apply (true);
tile.GetComponent<Renderer> ().material.SetTexture("_EmissionMap", (Texture)img);
yield return Ninja.JumpBack;
}
}
yield return Ninja.JumpToUnity;
}
private void LogAsync(string msg)
{
Debug.Log("[LogAsync] " + msg);
}
}
Single Background Results:
Multiple background Threads Code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using CoherentNoise;
using CoherentNoise.Generation;
using CoherentNoise.Generation.Displacement;
using CoherentNoise.Generation.Fractal;
using CoherentNoise.Generation.Modification;
using CoherentNoise.Generation.Patterns;
using CoherentNoise.Generation.Voronoi;
using CoherentNoise.Texturing;
using CielaSpike;
using System.Threading;
public class FuncTestMultiThread : MonoBehaviour {
[Header("World Settings")]
[Space(10)]
public GameObject prefab;
public int seed = 1;
public int size = 64;
public int chunkGridRoot = 5;
[Header("Mask Settings")]
[Space(10)]
public float freq = 3f;
public float power = 0.1f;
public int octaves = 3;
public float iscale = 1.1f;
int xindex = 0;
int yindex = 0;
public AnimationCurve curve;
[Header("Mountainous Terrain Settings")]
[Space(10)]
public float ridgeExp = 0.4f;
public float ridgeOffset = .75f;
public float ridgeGain = 13.0f;
public float ridgeFreq = 0.5f;
public int ridgeOctaves = 8;
// other stuff
Vector3 mypos;
Quaternion myq;
int coreCount;
int chunkCount;
void Start(){
mypos = transform.position;
myq = Quaternion.identity;
coreCount = SystemInfo.processorCount;
chunkCount = chunkGridRoot * chunkGridRoot;
StartCoroutine(StartAsync());
}
IEnumerator StartAsync(){
int cc = coreCount > 1 ? coreCount - 1 : 1; // reserve one core, hopefully this helps the main thread run quicker
Task[] tasks; // array of tasks -- ThreadNinja
int num = Mathf.CeilToInt(chunkCount / cc); // max chunks for all threads except the last
int sc = 0; // start chunk
int ec = 0; // end chunk
Debug.Log ("START");
if (cc >= chunkCount) { // if there less chunks (or same number of chunks) than targeted threads
tasks = new Task[chunkCount];
for (int i = 0; i < chunkCount; i++) {
// there is no 'yeild return' here because it prevents the threads from running at the same time
this.StartCoroutineAsync (FuncToThread (i, i, i), out tasks [i]);
}
} else { // There are more chunks than targeted threads.
tasks = new Task[cc];
for (int c = 0; c < cc; c++) { // For each targeted thread...
if (c == cc - 1) { // if this is the last thread...
sc = ec;
ec = chunkCount;
}else if(c==0){ // if this is the first thread...
sc = 0;
ec = num;
} else { // if this is not the first or last thread...
sc += num;
ec += num;
}
Debug.Log ("Thread " + c+ " start chunk: " + sc + " end chunk: " +ec);
// there is no 'yeild return' here because it prevents the threads from running at the same time
this.StartCoroutineAsync (FuncToThread (c, sc, ec), out tasks [c]); // start thread
}
}
Debug.Log ("GenChunk() FINISHED.");
yield return null;
}
IEnumerator FuncToThread(int threadnumber, int startchunk, int endchunk){
LogAsync ("FuncToThread Started");
// opt might be the source of the error
Function opt = new Function((x,y,z) => { return curve.Evaluate(Vector2.Distance (new Vector2(x,y), new Vector2(chunkGridRoot*0.5f,chunkGridRoot*0.5f))/(chunkGridRoot/2 * iscale));});
Turbulence trb = new Turbulence (opt, seed);
trb.Power = power;
trb.Frequency = freq;
trb.OctaveCount = octaves;
RidgeNoise mts = new RidgeNoise (seed);
mts.Exponent = ridgeExp;
mts.Offset = ridgeOffset;
mts.Gain = ridgeGain;
mts.Frequency = ridgeFreq;
mts.OctaveCount = ridgeOctaves;
Generator hmap = (mts * 0.5f + 0.25f) * trb;
for (int chunk = startchunk; chunk < endchunk; chunk++) {
int i = 0; // used to track 1d index during a 2d loop
float tileX = chunk % chunkGridRoot; // breaking a 1d into a 2d array, x index
float tileY = Mathf.Floor(chunk / chunkGridRoot); // breaking a 1d into a 2d array, y index
yield return Ninja.JumpToUnity; // ThreadNinja starts working on the main thread
Texture2D img = new Texture2D(size,size); // Our chunk image ('tile' == 'chunk')
Color[] pixels = new Color[size*size]; // img pixels. Our 'heightmap', which is texture2D at this time for demonstration
GameObject tile = Instantiate(prefab, mypos + new Vector3(tileX, tileY, 0), myq, transform); // Our quad (prefab set in inspector)
tile.name = "Chunk("+chunk+")_X"+tileX+"_Y"+tileY;
yield return Ninja.JumpBack; // ThreadNinja returns to this background thread
for (int x = 0; x < size; x++) { // 2d loop to fill pixels
for (int y = 0; y < size; y++) { // ^
float sx = (float)x / (size - 1) + (float)tileY; // 'CoherentNoise'operates on 0..1, increments of 1 shift the generator by a full tile
float sy = (float)y / (size - 1) + (float)tileX; // ^
float num = hmap.GetValue(sx,sy, 0); // change 'hmap' to 'trb' or 'opt' to view precursors
pixels[i] = new Color (num, num, num, 1); // grayscale image, so rgb are all the same value. alpha is 1.
i++; // increment pixel indexor
}
}
// generate terrain and chunk component data
yield return Ninja.JumpToUnity; // ThreadNinja starts working on the main thread again.
// apply component data
img.SetPixels (pixels);
img.Apply (true);
tile.GetComponent<Renderer> ().material.SetTexture("_EmissionMap", (Texture)img); // emission, not, albedo, so lighting doesn't screw with our inspection
yield return Ninja.JumpBack; // ThreadNinja returns to this background thread
}
yield return Ninja.JumpToUnity; // chunks loop is finished. // The loop is finished. ThreadNinja goes back to the main thread again.
}
private void LogAsync(string msg) // used as a debugger while outside Unity's main loop
{
Debug.Log("[LogAsync] " + msg);
}
}
Multiple background Threads Result:
