Compute Shaders , flock with instanced meshes + frag/vert

Instead of getting data back from the GPU buffer, we're gonna use this code-

Graphics.DrawMeshInstancedIndirect(boidMesh, 0, boidMaterial, bounds, argsBuffer, 0);

- to draw instances of a mesh. Note the argsBuffer - this is a new type of buffer, containing arguments.

It is defined  in the C# script and we're only initialising the first 2 entries of the array that we fill the argsBuffer with. It can actually hold a lot more information, but for this example, we only provide it with the index of the mesh and the number of them we want to draw. Argument Buffers seem to be very specific to this type of instanced mesh drawing in Unity. It doesn't seem easy to find information about them...

 The compute shader and frag/vert shader are not directly concerned with the argument buffer, it seems that the code above is the only actual reference to the argsBuffer!

The compute shader only updates the position/direction of the boids. The frag/vert shader (which is actually a surface shader lol), reads the information from the buffer and applies rotation and position transforms to the vertices that then get rendered as our boid meshes.

 

 

 

 C# SCRIPT

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InstancedFlocking : MonoBehaviour
{
    public struct Boid
    {
        public Vector3 position;
        public Vector3 direction;
        public float noise_offset;

        public Boid(Vector3 pos, Vector3 dir, float offset)
        {
            position.x = pos.x;
            position.y = pos.y;
            position.z = pos.z;
            direction.x = dir.x;
            direction.y = dir.y;
            direction.z = dir.z;
            noise_offset = offset;
        }
    }
    const int SIZE_BOID = 7 * sizeof(float);
    
    public ComputeShader shader;

    public float rotationSpeed = 1f;
    public float boidSpeed = 1f;
    public float neighbourDistance = 1f;
    public float boidSpeedVariation = 1f;
    public Mesh boidMesh;
    public Material boidMaterial;
    public int boidsCount;
    public float spawnRadius;
    public Transform target;

    int kernelHandle;
    ComputeBuffer boidsBuffer;
    ComputeBuffer argsBuffer; //a buffer of arguments, used by the Graphics draw instances
    uint[] args = new uint[5] { 0, 0, 0, 0, 0 };//array of arguments
    Boid[] boidsArray;
    int groupSizeX;
    int numOfBoids;
    Bounds bounds;

    void Start()
    {
        kernelHandle = shader.FindKernel("CSMain");

        uint x;
        shader.GetKernelThreadGroupSizes(kernelHandle, out x, out _, out _);
        groupSizeX = Mathf.CeilToInt((float)boidsCount / (float)x);
        numOfBoids = groupSizeX * (int)x;

        bounds = new Bounds(Vector3.zero, Vector3.one * 1000);

        InitBoids();
        InitShader();
    }

    private void InitBoids()
    {
        boidsArray = new Boid[numOfBoids];

        for (int i = 0; i < numOfBoids; i++)
        {
            Vector3 pos = transform.position + Random.insideUnitSphere * spawnRadius;
            Quaternion rot = Quaternion.Slerp(transform.rotation, Random.rotation, 0.3f);
            float offset = Random.value * 1000.0f;
            boidsArray[i] = new Boid(pos, rot.eulerAngles, offset);
        }
    }

    void InitShader()
    {
        boidsBuffer = new ComputeBuffer(numOfBoids, SIZE_BOID);
        boidsBuffer.SetData(boidsArray);

        //Initialize args buffer
        argsBuffer = new ComputeBuffer(1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments); //this buffer doesn't do any calculation
        if(boidMesh!=null)//we only initialise the first 2 values of the argument array - the argument buffer can take many arguments but we're only passing in the index and number of boids
        {
            args[0] = (uint)boidMesh.GetIndexCount(0);
            args[1] = (uint)numOfBoids;
        }
        argsBuffer.SetData(args);//set argument buffer to the array

        shader.SetBuffer(this.kernelHandle, "boidsBuffer", boidsBuffer);
        shader.SetFloat("rotationSpeed", rotationSpeed);
        shader.SetFloat("boidSpeed", boidSpeed);
        shader.SetFloat("boidSpeedVariation", boidSpeedVariation);
        shader.SetVector("flockPosition", target.transform.position);
        shader.SetFloat("neighbourDistance", neighbourDistance);
        shader.SetInt("boidsCount", numOfBoids);

        boidMaterial.SetBuffer("boidsBuffer", boidsBuffer);
    }

    void Update()
    {
        shader.SetFloat("time", Time.time);
        shader.SetFloat("deltaTime", Time.deltaTime);

        shader.Dispatch(this.kernelHandle, groupSizeX, 1, 1);
        //main change of update method - no longer using draw procedural now
        //here we instantiate a mesh instead of just points or a quad
        Graphics.DrawMeshInstancedIndirect(boidMesh, 0, boidMaterial, bounds, argsBuffer, 0);//args are - the mesh to be rendered, the submesh in question (meshes can contain multiple, but only one can be rendered with this function),the material to use, the bounding limits of the mesh set in the start method,finally the buffer that contains data about the mesh
    }

    void OnDestroy()
    {
        if (boidsBuffer != null)
        {
            boidsBuffer.Dispose();
        }

        if (argsBuffer != null)
        {
            argsBuffer.Dispose();
        }
    }
}


THE COMPUTE SHADER

#pragma kernel CSMain
#define GROUP_SIZE 256

float hash( float n )
{
    return frac(sin(n)*43758.5453);
}

// The noise function returns a value in the range 0 -> 1
float noise1( float3 x )
{
    float3 p = floor(x);
    float3 f = frac(x);

    f       = f*f*(3.0-2.0*f);
    float n = p.x + p.y*57.0 + 113.0*p.z;

    return lerp(lerp(lerp( hash(n+0.0), hash(n+1.0),f.x),
                    lerp( hash(n+57.0), hash(n+58.0),f.x),f.y),
                lerp(lerp( hash(n+113.0), hash(n+114.0),f.x),
                    lerp( hash(n+170.0), hash(n+171.0),f.x),f.y),f.z);
}

struct Boid
{
    float3 position;
    float3 direction;
    float noise_offset;
};

RWStructuredBuffer<Boid> boidsBuffer;

float time;
float deltaTime;
float rotationSpeed;
float boidSpeed;
float boidSpeedVariation;
float3 flockPosition;
float neighbourDistance;
int boidsCount;

[numthreads(GROUP_SIZE,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    uint instanceId = id.x;
    Boid boid = boidsBuffer[instanceId];

    float noise = clamp(noise1(time / 100.0 + boid.noise_offset), -1, 1)*2.0 - 1.0;
    float velocity = boidSpeed*(1.0+noise*boidSpeedVariation);

    float3 boid_pos = boid.position;
    float3 boid_dir = boid.direction;

    float3 separation = 0;
    float3 alignment = 0;
    float3 cohesion = flockPosition;

    uint nearbyCount = 1; // Add self that is ignored in loop

    for (uint i = 0; i < (uint)boidsCount; i++) {
        if (i == instanceId)
            continue;
       
        float3 tempBoid_position = boidsBuffer[i].position;

        float3 offset = boid.position - tempBoid_position;
        float dist = max(length(offset), 0.000001);//get distance between boid and scanned boid, making sure it is never 0, because of divisions
       
        if (dist < neighbourDistance)//when tempboid is within radius
        {
            separation += offset * (1.0/dist - 1.0/neighbourDistance);
            alignment += boidsBuffer[i].direction;
            cohesion += tempBoid_position;

            nearbyCount += 1;
        }
    }


    float avg = 1.0 / nearbyCount;
    alignment *= avg;
    cohesion *= avg;
    cohesion = normalize(cohesion - boid_pos);

    float3 direction = alignment + separation + cohesion;

    float prop = exp(-rotationSpeed * deltaTime);//This will be around 0.95
    boid.direction = lerp((direction), normalize(boid_dir), prop);

    boid.position += boid.direction * velocity * deltaTime;

    boidsBuffer[id.x] = boid;
}


THE FRAG VERT SHADER

Shader "Flocking/Instanced" {

   Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _BumpMap ("Bumpmap", 2D) = "bump" {}
        _MetallicGlossMap("Metallic", 2D) = "white" {}
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _Glossiness ("Smoothness", Range(0,1)) = 1.0
    }

   SubShader {
 
        CGPROGRAM

        sampler2D _MainTex;
        sampler2D _BumpMap;
        sampler2D _MetallicGlossMap;
        struct Input {
            float2 uv_MainTex;
            float2 uv_BumpMap;
            float3 worldPos;
        };
        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
 
        //surface shader, so no frag function
        #pragma surface surf Standard vertex:vert addshadow nolightmap
        #pragma instancing_options procedural:setup

        float4x4 _Matrix;
        float3 _BoidPosition;

         #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            struct Boid
            {
                float3 position;
                float3 direction;
                float noise_offset;
            };

            StructuredBuffer<Boid> boidsBuffer;
         #endif

        float4x4 create_matrix(float3 pos,float3 dir, float3 up) {
            float3 zaxis = normalize(dir);
            float3 xaxis = normalize(cross(up, zaxis));
            float3 yaxis = cross(zaxis, xaxis);
            return float4x4(//column 1 is rotation for  x, 2 is y, 3 is z, 4th is the translate
                xaxis.x, yaxis.x, zaxis.x, pos.x,
                xaxis.y, yaxis.y, zaxis.y, pos.y,
                xaxis.z, yaxis.z, zaxis.z, pos.z,
                0, 0, 0, 1
                );

        }
     
         void vert(inout appdata_full v, out Input data)
        {
            UNITY_INITIALIZE_OUTPUT(Input, data);

            #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            v.vertex = mul(_Matrix, v.vertex);//multiply vertex with our matrix
             //   v.vertex.xyz += _BoidPosition;//commented out because we're defining the position with the matrix
            #endif
        }

        void setup()//precedes the vertex shader
        {
            #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            _Matrix = create_matrix(boidsBuffer[unity_InstanceID].position ,boidsBuffer[unity_InstanceID].direction, float3(0, 1,0));//call the matrix creation function, providing an up vector of 0,1,0
                _BoidPosition = boidsBuffer[unity_InstanceID].position;
            #endif
        }
 
         void surf (Input IN, inout SurfaceOutputStandard o) {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            fixed4 m = tex2D (_MetallicGlossMap, IN.uv_MainTex);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
            o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
            o.Metallic = m.r;
            o.Smoothness = _Glossiness * m.a;
         }
 
         ENDCG
   }
}

Comments

Popular posts from this blog

Unity's "new" input system and Keijiro's Minis midi stuff

setting VFX graph properties using C#