Last active
October 11, 2019 20:03
-
-
Save julhe/1223091d27782e7f0ec38ceb065ab7ac to your computer and use it in GitHub Desktop.
A simple Voronoi Generator and visualizer for Unity with Burst.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* A simple Voronoi Generator and visualizer for Unity with Burst. | |
* USAGE: Place on any empty Game Object. | |
* @schneckerstein | |
*/ | |
using Unity.Burst; | |
using Unity.Collections; | |
using Unity.Jobs; | |
using Unity.Mathematics; | |
using UnityEngine; | |
using UnityEngine.Profiling; | |
using Random = UnityEngine.Random; | |
public class SimpleVoronoiWithBurst : MonoBehaviour | |
{ | |
public Vector2Int Resolution = new Vector2Int(64, 64); // the resolution we will render in | |
public Bounds VoronoiBounds = new Bounds(Vector3.zero, Vector3.one * 20f); // the bounds of the area we will render | |
public int InnerLoopBatchCount = 64; // the amount of pixels per thread. play around with it and notice how it affects performance | |
public float SiteExpandLimit = 10f; // the maximal growth of a site | |
private int[] voronoiRenderedId = new int[0]; // stores the index of the closest site | |
private Color[] voronoiRenderedColor = new Color[0];// stores the color of the closest site | |
private MeshRenderer visualizationPlane; // the MeshRenderer of the gameobject we will use to display the texture | |
private Material material; // the material for the visualizationPlane | |
private Texture2D voronoiAsTexture; // the voronoi rendered and colored | |
void Start() | |
{ | |
voronoiAsTexture = new Texture2D(Resolution.x, Resolution.y, TextureFormat.ARGB32, false){filterMode = FilterMode.Point}; | |
material = new Material(Shader.Find("Unlit/Texture")){ | |
mainTextureScale = new Vector2(-1f, 1f), //flip the texture horizontal, so it match the site positions when view from above | |
mainTexture = voronoiAsTexture | |
}; | |
// create the plane on which we will display the voronoi as a texture | |
GameObject visPlane = GameObject.CreatePrimitive(PrimitiveType.Plane); | |
visualizationPlane = visPlane.GetComponent<MeshRenderer>(); | |
//add random sites | |
for (int i = 0; i < 12; i++){ | |
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube); | |
go.transform.localScale = Vector3.one * 0.1f; | |
go.transform.SetParent(transform); | |
go.transform.localPosition = new Vector3( | |
Random.Range(VoronoiBounds.min.x, VoronoiBounds.max.x), | |
Random.Range(VoronoiBounds.min.y, VoronoiBounds.max.y)); | |
} | |
} | |
void Update(){ | |
// first we obtain the sites | |
// for the sake of simplicity, we use the children of our current GameObject | |
int siteCount = transform.childCount; | |
// We can't use a "normal" array for a Burst Job, so we will use a native array. | |
var sitesNative = new NativeArray<float3>( | |
siteCount, // the size of the array | |
Allocator.TempJob, //the allocator. this tells unity how we are going to use the array. TempJob is fine, because we won't use it for more than one frame | |
NativeArrayOptions.UninitializedMemory); // also tell unity not to initialize the array since we are doing this on our own. | |
// copy children position to site positionsD$ons | |
for (int i = 0; i < transform.childCount; i++){ | |
sitesNative[i] = transform.GetChild(i).position; | |
} | |
// create a array than stores the index of the closest site. | |
var voronoRenderedIdNative = new NativeArray<int>(voronoiRenderedId.Length, Allocator.TempJob); | |
// initialize the Job that computes our Voronoi | |
var renderVoronoi = new RenderVoronoi(){ | |
resolution = new int2(Resolution.x, Resolution.y), | |
cellsMinWs = (Vector2) VoronoiBounds.min, | |
cellsMaxWs = (Vector2) VoronoiBounds.max, | |
sites = sitesNative, | |
ExpandLimit = SiteExpandLimit, | |
VoronoiMap = voronoRenderedIdNative, | |
}; | |
Profiler.BeginSample("Compute Voronoi"); // tells the profiler to begin a sample, this is great to figure out how fast our job is actually | |
renderVoronoi.Schedule( // schedule our job | |
voronoiRenderedId.Length, // create as many instances as our Voronoi grid has pixels | |
InnerLoopBatchCount) // inner job count describes the amount of instances that are computed in one-thread. it should not be too small, so that every thread has work | |
.Complete(); // immediately request the job to be done | |
Profiler.EndSample(); | |
Profiler.BeginSample("Paint Voronoi Map"); | |
EnsureArraySize(ref voronoiRenderedId, Resolution.x * Resolution.y); //size of the array s | |
voronoRenderedIdNative.CopyTo(voronoiRenderedId); //copy the contents of the voronoiField to a managed array, | |
//because it is slightly faster to iterate over a native array outside a job | |
EnsureArraySize(ref voronoiRenderedColor, Resolution.x * Resolution.y); | |
// give each pixel the color of it's site id | |
// note that for optimal performance, this should be donne inside the RenderVoronoi Job | |
for (int i = 0; i < voronoiRenderedId.Length; i++){ | |
int id = voronoiRenderedId[i]; | |
Color c; | |
if (id == -1){ | |
c = Color.clear; | |
} else { | |
float idNormalized = id / (float) siteCount; | |
c = Color.HSVToRGB(idNormalized, 0.5f, 1f); | |
} | |
voronoiRenderedColor[i] = c; | |
} | |
Profiler.EndSample(); | |
Profiler.BeginSample("Upload Texture"); | |
EnsureTextureSize(ref voronoiAsTexture, Resolution); | |
voronoiAsTexture.SetPixels(voronoiRenderedColor); // copy the pixels to the visualization texture | |
voronoiAsTexture.Apply(); | |
Profiler.EndSample(); | |
// fit the plane into the bounds of the voronoi map | |
visualizationPlane.transform.position = VoronoiBounds.center; | |
visualizationPlane.transform.rotation = Quaternion.LookRotation(Vector3.down); | |
Vector3 planeScale = (VoronoiBounds.extents) / 5f; // the unity plane already has the extends of 5f; | |
visualizationPlane.transform.localScale = new Vector3(planeScale.x, planeScale.z, planeScale.y); //swizzle the scale dimensions, since we rotated the plane | |
visualizationPlane.GetComponent<MeshRenderer>().sharedMaterial = material; | |
// tell unity we don't need the native arrays anymore. | |
voronoRenderedIdNative.Dispose(); | |
sitesNative.Dispose(); | |
} | |
[BurstCompile] | |
struct RenderVoronoi : IJobParallelFor{ | |
// the data that we are going to use | |
[ReadOnly] public int2 resolution; // the resolution in cells/pixels | |
[ReadOnly] public float2 cellsMinWs, cellsMaxWs; // world space boundaries | |
[ReadOnly] public NativeArray<float3> sites; // list of the position of all sites | |
[ReadOnly] public float ExpandLimit; //limits the expansion of sites, good for visualizations | |
// the data that we are going to put out | |
[WriteOnly] public NativeArray<int> VoronoiMap; // output position map | |
// how we are going to compute the data | |
public void Execute(int pixelIndex){ | |
// figure out the location of the cell in world space | |
float2 pixelPositionWs; | |
// first, we turn the pixelIndex into a 2D space coordinate... | |
pixelPositionWs.x = pixelIndex % resolution.x; | |
pixelPositionWs.y = pixelIndex / resolution.x; | |
//note: it would be actually faster to stay in pixel coordinates from here on and convert the sites into pixel coordinates | |
// ...now normalized position into a range [0,1], so that 0 = cellsMinWs and 1 = cellsMaxWs... | |
pixelPositionWs.x /= resolution.x; | |
pixelPositionWs.y /= resolution.y; | |
// ...and transform into world space | |
pixelPositionWs = math.lerp(cellsMinWs, cellsMaxWs, pixelPositionWs); | |
// keep track of the shortest distance | |
float closestSiteDistance = ExpandLimit; // if you don't want to limit, use float.PositiveInfinty | |
int closestSiteId = -1; | |
for (int siteId = 0; siteId < sites.Length; siteId++){ | |
float2 sitePosition = sites[siteId].xy; | |
float siteDistance = math.distance(sitePosition, pixelPositionWs); // compute the distance between cell and site | |
if (siteDistance < closestSiteDistance) { | |
// if the curret site is closer than the previous one, remember it | |
closestSiteDistance = siteDistance; | |
closestSiteId = siteId; | |
} | |
} | |
VoronoiMap[pixelIndex] = closestSiteId; // write the result back | |
} | |
} | |
// utility functions | |
static void EnsureArraySize<T>(ref T[] array, int length){ | |
if (array.Length != length){ | |
array = new T[length]; | |
} | |
} | |
static void EnsureTextureSize(ref Texture2D t, Vector2Int resolution){ | |
if (t.width != resolution.x || t.height != resolution.y){ | |
t.Resize(resolution.x, resolution.y); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment