3D Compute shader directional flow field created on Unity's newest Universal Rendering Pipeline.
Flow fields are a great way to simulate forces on any given object in 2D or 3D space. The concept is simple. You create a grid of points that each represent a position with a direction in space. As your agents/particles travel through this grid space, they will read the nearest point within that flow field to determine it's current direction. For this example, I am showing how you can achieve these result with a compute shader operation in Unity's Universal Rendering Pipeline.
- We first generate our buffers for our compute shader on the C# side. We basically initialize our 3D grid
protected void GenerateFlowFieldBuffer()
{
flowFieldBuffer = new ComputeBuffer(FLOWFIELD_POINTS_NUM, Marshal.SizeOf(typeof(FlowFieldPointData)));
var flowPoints = new FlowFieldPointData[FLOWFIELD_POINTS_NUM];
int iterations = 0;
for (int x = 0; x < xPointCount; x++)
{
for (int y = 0; y < yPointCount; y++)
{
for (int z = 0; z < zPointCount; z++)
{
var index = (x * yPointCount + y) * zPointCount + z;
Vector3 position = new Vector3(
simulationSpace.x / xPointCount * x + cellSize/2,
simulationSpace.y / yPointCount * y + cellSize/2,
simulationSpace.z / zPointCount * z + cellSize/2
);
position += this.transform.position - simulationSpace/2;
flowPoints[index] = CreateFlowFieldPoint(position);
iterations++;
}
}
}
flowFieldBuffer.SetData(flowPoints);
}
- Now that we have our buffer, we set it in the compute shader and dispatch our shader operation (basically calling it).
protected void DispatchFlowField()
{
flowFieldCS.SetBuffer(flowFieldKernelIndex, "_FlowFieldPointBuffer", flowFieldBuffer);
flowFieldCS.Dispatch(flowFieldKernelIndex, (Mathf.CeilToInt(FLOWFIELD_POINTS_NUM / (int)TCOUNT_X) + 1), 1, 1);
}
- Using DrawProceduralNow to draw points to screen:
protected void RenderFlowField()
{
flowFieldMat.SetPass(0);
flowFieldMat.SetBuffer("_FlowFieldBuffer", flowFieldBuffer);
Graphics.DrawProceduralNow(MeshTopology.Points, flowFieldBuffer.count);
}
- Now that our flow field is set up, we just grab the nearest flow field point based on the current particle position with this function (it's the same function from our c# script, just reversed and optimized for shaders):
uint GrabGridIndex(float3 position)
{
float x = floor((position.x - (_BoundsPosition.x - _BoundsDimensions.x/2)) / ((_BoundsDimensions.x / _XCellCount)));
float y = floor((position.y - (_BoundsPosition.y - _BoundsDimensions.y/2)) / ((_BoundsDimensions.y / _YCellCount)));
float z = floor((position.z - (_BoundsPosition.z - _BoundsDimensions.z/2)) / ((_BoundsDimensions.z / _ZCellCount)));
uint flowFieldIndex = (x * _YCellCount + y) * _ZCellCount + z;
return flowFieldIndex;
}
- Now that you have the flow field point index, just access it directly from the buffer and get it's direction. Like so:
uint flowIndex = GrabGridIndex(particle.position);
float3 flowDirection = _FlowFieldPointBuffer[flowIndex].direction;
Open-source! Use for whatever you want.
If you enjoy these open-source projects, the most direct way to support the development of future releases is through patreon.