Hello everyone,
I am new to unity and unity Job system and wanted to multi thread a falling sand simulation that uses cellular automata like rules.
The idea is to make 16 chunks with 4 running at the same time on different threads to not overlap (Picture bellow). This patern allows to have some space around updated chunks in case a particle has to fly far away (1/4 of the width/height of the screen).

Backup Image link
I don’t know if this works, just in case :
Imgur: The magic of the Internet
Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Jobs;
using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;
using Unity.Collections.LowLevel.Unsafe;
public class SandSimulationWithJob : MonoBehaviour
{
public int brushSize = 0;
public int width = 512;
public int height = 512;
[Range(0, 100)]
public int cellDensity = 50;
//public Texture texture;
public Camera cam;
public ComputeShader computeShader;
ComputeBuffer computeBuffer;
RenderTexture renderTexture;
public Material material;
private int kernel;
private int numOfThreads = 32;
NativeArray<Cell> nativeCells;
public struct Cell
{
public int cellType;
public Color cellColor;
public int updated; //has been updated this frame
}
private void Start()
{
InitializeCells();
computeBuffer = new ComputeBuffer(width * height, 24);
renderTexture = new RenderTexture(width, height, 24);
renderTexture.enableRandomWrite = true;
renderTexture.filterMode = FilterMode.Point;
renderTexture.useMipMap = false;
renderTexture.Create();
kernel = computeShader.FindKernel("CSMain");
}
void InitializeCells()
{
nativeCells = new NativeArray<Cell>(width * height, Allocator.Persistent);
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
nativeCells[x + (width * y)] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };
}
}
}
private void FixedUpdate()
{
for (int x = 0; x < width; x++) // each frame all particle has their updated reset. I have to change how it's done.
{
for (int y = 0; y < height; y++)
{
Cell tmp_cell = nativeCells[x + (width * y)];
nativeCells[x + (width * y)] = new Cell { cellType = tmp_cell.cellType, cellColor = tmp_cell.cellColor, updated = 0 };
}
}
SimpleJobHandle();
Mouse();
UpdateComputeShader();
}
void SimpleJobHandle()
{
int k;
for (int i = 0; i < width / 2; i += width / 4)
{
for (int j = 0; j < height / 2; j += height / 4)
{
k = 0;
NativeArray<JobHandle> jobHandleArray = new NativeArray<JobHandle>(4, Allocator.Temp); //Doing 4 passes of 4 chunks
for (int x_ = i; x_ < width; x_ += width / 2)
{
for (int y_ = j; y_ < height; y_ += height / 2)
{
JobHandle jobHandle = new Cells_ { nativeCells = nativeCells, height = height, width = width, x_ = x_, y_ = y_ }.Schedule();
jobHandleArray[k] = jobHandle;
k++;
}
}
JobHandle.CompleteAll(jobHandleArray);
jobHandleArray.Dispose();
}
}
}
[BurstCompile]
public struct Cells_ : IJob
{
[NativeDisableContainerSafetyRestriction]
public NativeArray<Cell> nativeCells;
public int x_;
public int y_;
public int width;
public int height;
public void Execute()
{
for (int x = x_; x < width / 4 + x_; x++)
{
for (int y = y_; y < height / 4 + y_; y++)
{
if (nativeCells[x + (width * y)].cellType == 1 && (y + 1) < height && nativeCells[x + (width * y)].updated == 0) //Sand Simulation
{
if (nativeCells[x + (width * (y + 1))].cellType == 0)
{
nativeCells[x + (width * (y + 1))] = new Cell { cellType = 1, cellColor = Color.white, updated = 1 };
nativeCells[x + (width * y)] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };
}
else if ((x + 1) < width && nativeCells[(x + 1) + (width * (y + 1))].cellType == 0)
{
nativeCells[(x + 1) + (width * (y + 1))] = new Cell { cellType = 1, cellColor = Color.white, updated = 1 };
nativeCells[x + (width * y)] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };
}
else if ((x - 1) >= 0 && nativeCells[(x - 1) + (width * (y + 1))].cellType == 0)
{
nativeCells[(x - 1) + (width * (y + 1))] = new Cell { cellType = 1, cellColor = Color.white, updated = 1 };
nativeCells[x + (width * y)] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };
}
else if (nativeCells[x + (width * (y + 1))].cellType == 2) //Swap with water
{
nativeCells[x + (width * (y + 1))] = new Cell { cellType = 1, cellColor = Color.white, updated = 1 };
nativeCells[x + (width * y)] = new Cell { cellType = 2, cellColor = Color.blue, updated = 0 };
}
}
}
}
}
}
void UpdateComputeShader()
{
computeBuffer.SetData(nativeCells);
computeShader.SetInt("width", width);
computeShader.SetInt("height", height);
computeShader.SetBuffer(kernel, "Grid", computeBuffer);
computeShader.SetTexture(kernel, "Result", renderTexture);
computeShader.Dispatch(kernel, width / numOfThreads, height / numOfThreads, 1);
material.mainTexture = renderTexture;
}
void Mouse()
{
RaycastHit hit;
Ray ray = cam.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
Vector3 pos = ray.GetPoint(hit.distance);
Vector2 pixelPos;
pixelPos.x = Mathf.Lerp(width, 0, (pos.x + 5f) / 10f);
pixelPos.y = Mathf.Lerp(height, 0, (pos.z + 5f) / 10f);
if (Input.GetMouseButton(0))
{
for (int x = -brushSize; x < brushSize + 1; x++)
{
for (int y = -brushSize; y < brushSize + 1; y++)
{
if ((int)pixelPos.y + y >= 0 && (int)pixelPos.y + y < height && (int)pixelPos.x + x >= 0 && (int)pixelPos.x + x < width)
{
if (x * x + y * y <= brushSize * brushSize + 1)
{
if (Input.GetKey(KeyCode.S))
{
nativeCells[((int)pixelPos.x + x) + (width * ((int)pixelPos.y + y))] = new Cell { cellType = 1, cellColor = Color.white, updated = 0 };
}
else if (Input.GetKey(KeyCode.W))
{
nativeCells[((int)pixelPos.x + x) + (width * ((int)pixelPos.y + y))] = new Cell { cellType = 2, cellColor = Color.blue, updated = 0 };
}
else if (Input.GetKey(KeyCode.X))
{
nativeCells[((int)pixelPos.x + x) + (width * ((int)pixelPos.y + y))] = new Cell { cellType = 3, cellColor = Color.yellow, updated = 0 };
}
}
}
}
}
}
if (Input.GetMouseButton(1))
{
for (int x = -brushSize; x < brushSize + 1; x++)
{
for (int y = -brushSize; y < brushSize + 1; y++)
{
if ((int)pixelPos.y + y >= 0 && (int)pixelPos.y + y < height && (int)pixelPos.x + x >= 0 && (int)pixelPos.x + x < width)
{
if (x * x + y * y <= brushSize * brushSize + 1)
{
nativeCells[((int)pixelPos.x + x) + (width * ((int)pixelPos.y + y))] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };
}
}
}
}
}
}
}
void OnDestroy()
{
renderTexture.Release();
computeBuffer.Release();
nativeCells.Dispose();
}
}
NB : The compute shader is only for the display here for now.
Compute Shader
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain
struct Cell {
uint cellType;
float4 cellColor;
uint updated;
};
uint width;
uint height;
RWTexture2D<float4> Result;
RWStructuredBuffer<Cell> Grid;
[numthreads(32,32,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
Cell cell = Grid[id.x + (width * id.y)];
Result[id.xy] = cell.cellColor;
}
The fact is : It’s slower than the single threaded version, is it because of all the for loops i have for the chunk structure ? Or am I missing something ?
Thank you if you read through the code !
If you have any suggestion feel free to answer, like I said I’m new to unity and may be doing some weird things !