Written in HLSL for Unity 2021.3.10f1
thumb.mp4
- Compute Shaders course by Nik Lever
- Low-Poly Simple Nature Pack
- Spatial Transformation Matrices
- Matrix multiplication
- Shadow Caster and Receive Shadow Shaders
- Create a Mesh procedurally, to render the grass blades.
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;
}
- 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;
}
- 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);
}
- 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
};
- 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);
- This will be in charge of drawing all the grasses.
Graphics.DrawMeshInstancedIndirect(
mesh: _mesh,
submeshIndex: 0,
material: Material,
bounds: _bounds,
bufferWithArgs: _argsBuffer
);
- 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;
}
- 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;
}
- 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);
}
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
}