/grass-compute-shader-unity

Procedural Grass System with Compute Shaders in Unity

Primary LanguageC#

Procedural Grass System with Compute Shaders in Unity

Written in HLSL for Unity 2021.3.10f1

Screenshots

thumb.mp4

References

Sections

Implementation

C# Code

Procedural Mesh

  • Create a Mesh procedurally, to render the grass blades.

Mesh

public static Mesh GetGrassBladeMesh()
{
    var mesh = new Mesh();

    // define the vertices
    mesh.vertices = new Vector3[] {
        ...
    };

    // define the normals
    Vector3[] normalsArray = new Vector3[mesh.vertices.Length];
    System.Array.Fill(normalsArray, new Vector3(0, 0, -1));
    mesh.normals = normalsArray;

    mesh.uv = new Vector2[] {
        ...
    };

    mesh.SetIndices(
        // counter clock wise so the normals make sense
        indices: new int[]{
            ...
        },
        topology: MeshTopology.Triangles,
        submesh: 0
    );

    return mesh;
}

Initialize Grass Blades Array

  • Given the Bounds and the Density, create randomly located grass blades.
  • Using Raycast, position the grass on top of the terrain.
for (var i = 0; i < grassBlades.Length; i++)
{
    var grassBlade = grassBlades[i];

    var localPos = new Vector3(
        x: Random.Range(-bounds.extents.x, bounds.extents.x),
        y: 0,
        z: Random.Range(-bounds.extents.z, bounds.extents.z)
    );

    RaycastHit hit;
    var didHit = Physics.Raycast(
        origin: transform.TransformPoint(localPos) + (transform.up * 20),
        direction: -transform.up,
        hitInfo: out hit
    );

    if (didHit)
    {
        localPos.y = hit.point.y;
    }

    grassBlade.position = transform.TransformPoint(localPos);

    grassBlade.rotationY = Random.Range((float)-System.Math.PI, (float)System.Math.PI);

    grassBlades[i] = grassBlade;
}

Initialize Compute Buffers

  • Set the buffer to both the Compute Shader and the Material, so the Vertex/Fragment Shader can access the data using the SV_InstanceID from the GPU Instancing.
private void InitializeGrassBladesBuffer()
{
    var grassBladeMemorySize = (3 + 1 + 1 + 1) * sizeof(float);

    _grassBladesBuffer = new ComputeBuffer(
        count: _grassBlades.Length,
        stride: _grassBlades.Length * grassBladeMemorySize
    );

    _grassBladesBuffer.SetData(_grassBlades);

    _kernelIndex = ComputeShader.FindKernel("SimulateGrass");

    // this will let compute shader access the buffers
    ComputeShader.SetBuffer(_kernelIndex, "GrassBladesBuffer", _grassBladesBuffer);

    // this will let the surface shader access the buffer
    Material.SetBuffer("GrassBladesBuffer", _grassBladesBuffer);
}

Initialize IndirectArguments ComputeBuffer

  • The buffer with the arguments for DrawMeshInstancedIndirect will indicate how many meshes instances we need to draw.
// for Graphics.DrawMeshInstancedIndirect
// this will be used by the vertex/fragment shader
// to get the instance_id and vertex_id
var args = new int[_argsCount] {
    (int)_mesh.GetIndexCount(submesh: 0),       // indices of the mesh
    _grassBladesCount,                          // number of objects to render
    0,0,0                                       // unused args
};

Dispatch Compute Shader

  • The Compute Shader needs several variable set before it can compute the wind and age colors for the grasses.
  • The thread groups count will be the amount of individual grasses we need to render.
ComputeShader.SetFloat("Time", Time.time);
ComputeShader.SetInt("AgeNoiseColumns", AgeNoiseColumns);
ComputeShader.SetInt("AgeNoiseRows", AgeNoiseRows);
ComputeShader.SetInt("WindNoiseColumns", WindNoiseColumns);
ComputeShader.SetInt("WindNoiseRows", WindNoiseRows);
ComputeShader.SetFloat("WindVelocity", WindVelocity);

ComputeShader.Dispatch(_kernelIndex, (int)_threadGroupsCountX, 1, 1);

Material.SetVector("WindDirection", WindDirection);
Material.SetFloat("WindForce", WindForce);
Material.SetColor("YoungGrassColor", YoungGrassColor);
Material.SetColor("OldGrassColor", OldGrassColor);

Draw Mesh Instanced Indirect

  • This will be in charge of drawing all the grasses.
Graphics.DrawMeshInstancedIndirect(
    mesh: _mesh,
    submeshIndex: 0,
    material: Material,
    bounds: _bounds,
    bufferWithArgs: _argsBuffer
);

Compute Shader

  • A simple program that computes the wind and color noises using the Perlin Noise algorithm.
  • The position of the grass relative to the parent terrain will determine the quadrant and coordinates used to calculate the noise.
  • The Time is used to animate the wind noise.
RWStructuredBuffer<GrassBlade> GrassBladesBuffer;
float Time;
int AgeNoiseColumns;
int AgeNoiseRows;
float WindVelocity;
int WindNoiseColumns;
int WindNoiseRows;
float3 GrassOrigin;
float3 GrassSize;

[numthreads(1,1,1)]
void SimulateGrass (uint3 id : SV_DispatchThreadID)
{
    GrassBlade grassBlade = GrassBladesBuffer[id.x];

    float3 grassLocalPosition = grassBlade.position - GrassOrigin;
    float2 uv = float2(
        (grassLocalPosition.x + (GrassSize.x / 2)) / GrassSize.x,
        (grassLocalPosition.z + (GrassSize.z / 2)) / GrassSize.z
    );

    grassBlade.ageNoise = (perlin(uv, AgeNoiseColumns, AgeNoiseRows, 1) + 1) / 2;
    grassBlade.windNoise = perlin(uv, WindNoiseColumns, WindNoiseRows, Time * WindVelocity);

    GrassBladesBuffer[id.x] = grassBlade;
}

Vertex Fragment Shader

Position Vertices in Clip Space

  • First get a Translation Matrix to move the vertices in world space, to the desired position.
  • Then get a Rotation Matrix to apply the random rotaion along the Y axis, to give a more natural look.
  • Multiply these matrices together to create a Transformation Matrix.
  • Use unity_ObjectToWorld to obtain the world space position of the vertex, then transform it using the transformation matrix.
float4 positionVertexInWorld(GrassBlade grassBlade, float4 positionOS) {
    // generate a translation matrix to move the vertex
    float4x4 translationMatrix = getTranslation_Matrix(grassBlade.position);
    float4x4 rotationMatrix = getRotationY_Matrix(grassBlade.rotationY);
    float4x4 transformationMatrix = mul(translationMatrix, rotationMatrix);

    // translate the object pos to world pos
    float4 worldPosition = mul(unity_ObjectToWorld, positionOS);
    // then use the matrix to translate and rotate it
    worldPosition = mul(transformationMatrix, worldPosition);

    return worldPosition;
}

Apply Wind

  • Move the vertices along the wind direction vector.
  • Multiply by the wind noise that was computed by the compute shader.
  • Multiply by the wind force, to have control over the animation.
  • Lerp between the displaced vertex and the original vertex, using the Y UV coordinate, so that the base of the grass is stationary, and the top moves.
float4 applyWind(GrassBlade grassBlade, float2 uv, float4 worldPosition, float3 windDirection, float windForce) {
    float3 displaced = worldPosition.xyz + (normalize(windDirection) * windForce * grassBlade.windNoise);
    float4 displacedByWind = float4(displaced, 1);

    // base of the grass needs to be static on the floor
    return lerp(worldPosition, displacedByWind, uv.y);
}

Shadows

Pass
{
    Tags {"LightMode"="ForwardBase"}

    Cull Off

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_instancing
    #pragma multi_compile_fwdbase

    struct Varyings
    {
        ...
        SHADOW_COORDS(1) // put shadows data into TEXCOORD1
    };

    Varyings vert (Attributes IN, uint vertex_id: SV_VERTEXID, uint instance_id: SV_INSTANCEID)
    {
        ...
        TRANSFER_SHADOW(OUT)
        ...
    }

    half4 frag (Varyings IN) : SV_Target
    {
        fixed shadow = SHADOW_ATTENUATION(IN);
        ...
    }
    ENDCG
}

Pass
{
    Tags {"LightMode"="ShadowCaster"}

    Cull Off

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_instancing
    #pragma multi_compile_shadowcaster

    ...

    ENDCG
}

Progress

1.mp4
2.mp4
3.mp4
4.mp4
5.mp4
6.mp4
7a.mp4
7b.mp4
8.mp4
9.mp4
10.mp4
11.mp4
12a.mp4
12b.mp4