Skip to content

Instantly share code, notes, and snippets.

@julhe
Last active October 11, 2019 20:03
Show Gist options
  • Save julhe/1223091d27782e7f0ec38ceb065ab7ac to your computer and use it in GitHub Desktop.
Save julhe/1223091d27782e7f0ec38ceb065ab7ac to your computer and use it in GitHub Desktop.
A simple Voronoi Generator and visualizer for Unity with Burst.
/*
* 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