I’ve tweaked the occlusion culling setting of the main camera, but that doesn’t work.
The C# code I used:
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Rendering;
public class GrassRenderer : MonoSingleton<GrassRenderer>
{
public Material grassMaterial;
public ComputeShader grassComputeShader;
private float boundSize = 30;
private uint maxInstanceCount = 1000;
private GraphicsBuffer argsBuffer;
private ComputeBuffer outputGrassBuffer;
private ComputeBuffer inputGrassBuffer;
private static readonly int InstanceBuffer = Shader.PropertyToID("instance_buffer");
private uint threadGroupSizeX;
private Bounds bounds;
struct GrassPrecomputeData
{
Vector3 position;
public static int SizeOf => sizeof(float) * 3;
}
struct GrassInstanceData
{
Vector3 position;
float height;
float width;
float darkness;
float angle_dir;
float bend;
}
protected override void Awake()
{
base.Awake();
bounds = new Bounds(Vector3.zero, Vector3.one * boundSize);
}
private void Start()
{
InitBuffer();
}
private void OnDestroy()
{
DisposeBuffer();
}
private void InitBuffer()
{
argsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 5, sizeof(int));
inputGrassBuffer = new ComputeBuffer((int)maxInstanceCount, GrassPrecomputeData.SizeOf,ComputeBufferType.Structured);
outputGrassBuffer = new ComputeBuffer((int)maxInstanceCount, Marshal.SizeOf(typeof(GrassInstanceData)),ComputeBufferType.Structured);
grassComputeShader.SetBuffer(0, "input_grass_buffer", inputGrassBuffer);
grassComputeShader.SetBuffer(0, "output_grass_buffer", outputGrassBuffer);
grassMaterial.SetBuffer(InstanceBuffer, outputGrassBuffer);
grassComputeShader.GetKernelThreadGroupSizes(0, out uint x, out _, out _);
threadGroupSizeX = x;
}
private void DisposeBuffer()
{
argsBuffer.Dispose();
argsBuffer = null;
outputGrassBuffer.Dispose();
outputGrassBuffer = null;
inputGrassBuffer.Dispose();
inputGrassBuffer = null;
}
public uint InstanceCount
{
get => maxInstanceCount;
set => maxInstanceCount = value;
}
public void DrawGrass(Vector3[] positions)
{
int currentInstanceCount;
if (positions.Length > maxInstanceCount)
{
positions = new ArraySegment<Vector3>(positions, 0, (int)maxInstanceCount).ToArray();
currentInstanceCount = (int)maxInstanceCount;
}
else
{
currentInstanceCount = positions.Length;
}
if(currentInstanceCount<=0)
{
return;
}
inputGrassBuffer.SetData(positions);
grassComputeShader.SetInt("grass_count", currentInstanceCount);
grassComputeShader.Dispatch(0, Mathf.CeilToInt(currentInstanceCount / (float)threadGroupSizeX), 1, 1);
argsBuffer.SetData(new uint[] { GrassMeshData.GrassMesh.GetIndexCount(0), (uint)currentInstanceCount, 0, 0, 0 });
Graphics.DrawMeshInstancedIndirect(GrassMeshData.GrassMesh,
0,
grassMaterial,
bounds,
argsBuffer);
}
}
The urp shader:
Shader "Custom/Grass"
{
Properties
{
_BaseColor ("Example Colour", Color) = (0, 0.66, 0.73, 1)
}
SubShader
{
Tags
{
"RenderPipeline"="UniversalPipeline"
"RenderType"="Opaque"
"Queue"="Geometry"
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
CBUFFER_END
struct InstanceData
{
float3 position;
float height;
float width;
float darkness;
float angle_dir;
float bend;
};
UNITY_INSTANCING_BUFFER_START(instance_buffer)
StructuredBuffer<InstanceData> instance_buffer;
UNITY_INSTANCING_BUFFER_END(instance_buffer)
ENDHLSL
Pass
{
Name "ForwardLit"
Tags { "LightMode"="UniversalForward" }
Cull Off
HLSLPROGRAM
#pragma vertex UnlitPassVertex
#pragma fragment UnlitPassFragment
#pragma instancing_options procedural:setup
// Structs
struct a2v
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};
struct v2f
{
float4 positionCS : SV_POSITION;
};
// Vertex Shader
v2f UnlitPassVertex(a2v IN, uint id : SV_InstanceID)
{
v2f OUT;
const VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
const float3 position = instance_buffer[id].position + positionInputs.positionWS;
OUT.positionCS = TransformWorldToHClip(position);
return OUT;
}
// Fragment Shader
half4 UnlitPassFragment(v2f IN) : SV_Target
{
return _BaseColor;
}
ENDHLSL
}
}
}
The compute shader which does nothing:
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain
#include "../Noise/FastSnoise.hlsl"
#include "../Includes/QuaternionEssential.hlsl"
struct GrassInput
{
float3 position;
};
struct GrassOutput
{
float3 position;
float height;
float width;
float darkness;
float angle_dir;
float bend;
};
int grass_count;
// float time;
// float wind_direction_angle;
// float wind_strength;
// float swing_speed;
// float swing_strength;
StructuredBuffer<GrassInput> input_grass_buffer;
RWStructuredBuffer<GrassOutput> output_grass_buffer;
[numthreads(64,1,1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
if (id.x >= grass_count)
{
return;
}
const float3 position = input_grass_buffer[id.x].position;
// // Random swing
// const float3 swing = swing_strength * float3(
// 0.5 * (f_snoise(swing_speed * (position * 0.1 + time)) - 0.5),
// 0.5 * (f_snoise(swing_speed * (position * 0.1 + time + 100)) - 0.5),
// 0.5 * (f_snoise(swing_speed * (position * 0.1 + time + 200)) - 0.5)
// );
//
//
// const float3 wind_direction = float3(cos(wind_direction_angle), 0, sin(wind_direction_angle));
// const float3 wind = wind_direction * wind_strength;
GrassOutput output;
output.position = position;
output.height = 1.0;
output.width = 1;
output.darkness = 0.5;
output.angle_dir = 0;
output.bend = 0;
output_grass_buffer[id.x] = output;
}