Ansatz DrawMeshInstanced
Hierfür erstellen wir zuerst ein neues C#-Script. Dort speichern wir folgende Variablen:
_instances ist die gewünschte Objekt-Anzahl
_mesh ist das 3D-Objekt, das letztendlich instantiieren wird
_camera benötigen wir später, um die Entfernung zur Kamera zu erhalten
_batches ist die oben erklärte Batches-Liste, die wiederum eine Liste von Transformations-Matrizen enthält
_batchSize ist die Größe der Batches, die maximal 1023 sein kann
_range ist der Bereich, in dem die Objekte gespawnt werden sollen
_scaleMin, _scaleMax wird die Größe der Objekte beeinflussen
_rotateToGroundNormal dreht unsere Objekte so, dass sie passend auf dem Boden liegen/stehen
_randomYAxisRotation sorgt dafür, dass sich unsere Objekte auf der y-Achse beliebig drehen in einem Winkel von _maxYRotation
_groundLayer ist eine LayerMaske, die wir in Unity einem Objekt zuweisen können - alles mit dieser Maske innerhalb unserer _range wird als mögliche Position für die gespawnten Objekte benutzt
_steepness gibt die maximale Steigung an, auf der unsere Objekte spawnen dürfen
_material ist das Materialit,mit dem wir noch Farbe und Vertex-Animation auf die Objekte packen
MeshLOD ist ein Struct, das einen Mesh speichert und je nach Distanz zur Kamera mittels dem float lod (level of detail) auswertet, welcher Mesh benutzt wird.
Zusätzlich können wir mit shadows für einzelne LOD-Stufen einstellen, ob Schatten gerendert werden sollen oder nicht.
private bool _visible;
private bool _castShadows;
private Mesh _mesh;
private Transform _camera;
private List<List<Matrix4x4>> _batches = new List<List<Matrix4x4>>();
[SerializeField] private bool _drawGizmos;
[SerializeField][Range(1, 1023)] private int _batchSize = 1000;
[SerializeField] private int _instances;
[SerializeField] private Vector2 _range;
[SerializeField] private Vector3 _scaleMin = Vector3.one;
[SerializeField] private Vector3 _scaleMax = Vector3.one;
[SerializeField][Range(0f, 1f)] private float _steepness;
[SerializeField] private bool _rotateToGroundNormal = false;
[SerializeField] private bool _randomYAxisRotation = false;
[SerializeField] private float _maxYRotation = 90;
[SerializeField] private bool _recieveShadows;
[SerializeField] private LayerMask _groundLayer;
[SerializeField] private Material _material;
[SerializeField] private MeshLOD[] _meshes;
Code kopieren
[System.Serializable]
public struct MeshLOD {
public Mesh mesh;
public float lod;
public bool shadows;
}
Code kopieren
Shader
Mit diesem Ansatz müssen wir unseren Shader selber schreiben.
Dafür erweitere ich in diesem Blog den Standard-Unlit Shader.
Zuerst erweitern wir diesen erneut mit folgender Anweisung, um GPU Instancing zu aktivieren.
#pragma multi_compile_Instancing
Code kopieren
Dann brauchen wir eine Variable, die unsere Transformations-Matrizen von der C#-Seite (_trsList) speichert.
StructuredBuffer<float4x4> trsBuffer;
Code kopieren
Damit wir auf die Bufferdaten zugreifen können, benötigen wir die InstanceID.
Die InstanceID ist einfach eine Zahl, die jeder Instanz - also jedem zu rendernden Objekt - eine ID/Index zuweist.
Damit können wir auf den trsBuffer wie ein Array zugreifen.
Um die InstanceID zu erhalten, erweitern wir die Vertex-Shader-Parameter um Folgendes:
v2f vert (appdata v, uint instanceID : SV_InstanceID)
Code kopieren
Jetzt können wir den trsBuffer und die instanceID benutzen.
Da jedes Element im trsBuffer eine Position, Rotation und Größe als 4 x 4-Matrix enthält, können wir damit die bereits existierenden Vertex-Daten (ein Vektor mit drei Komponenten) multiplizieren (Matrix * Vektor), um die gewünschte Transformation im WorldSpace zu erhalten.
v2f vert (appdata v, uint instanceID : SV_InstanceID) {
v2f o;
//applying transformation matrix
float3 positionWorldSpace = mul(trsBuffer[instanceID], float4(v.vertex.xyz, 1));
//..
}
Code kopieren
Jetzt müssen wir die neue Transformation nur noch auf den Vertex übertragen und am Ende zurückgeben.
o.vertex = mul(UNITY_MATRIX_VP, float4(positionWorldSpace, 1));
//...
return o;
Code kopieren
Finaler C#- und Shader Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public struct MeshLOD {
public Mesh mesh;
public float lod;
public bool shadows;
}
public class Instancer : MonoBehaviour {
private bool _visible;
private bool _castShadows;
private Mesh _mesh;
private Transform _camera;
private List<List<Matrix4x4>> _batches = new List<List<Matrix4x4>>();
[SerializeField] private bool _drawGizmos;
[SerializeField][Range(1, 1023)] private int _batchSize = 1000;
[SerializeField] private int _instances;
[SerializeField] private Vector2 _range;
[SerializeField] private Vector3 _scaleMin = Vector3.one;
[SerializeField] private Vector3 _scaleMax = Vector3.one;
[SerializeField][Range(0f, 1f)] private float _steepness;
[SerializeField] private bool _rotateToGroundNormal = false;
[SerializeField] private bool _randomYAxisRotation = false;
[SerializeField] private float _maxYRotation = 90;
[SerializeField] private bool _recieveShadows;
[SerializeField] private LayerMask _groundLayer;
[SerializeField] private Material _material;
[SerializeField] private MeshLOD[] _meshes;
private void Start() {
_camera = Camera.main.transform;
Initialize();
transform.position = new Vector3(transform.position.x, _camera.position.y, transform.position.z);
}
private void Update() {
GetMeshFromCameraDistance();
RenderBatches();
}
private void OnBecameVisible() {
_visible = true;
}
private void OnBecameInvisible() {
_visible = false;
}
private void GetMeshFromCameraDistance() {
float dist = Vector3.Distance(_camera.position, transform.position);
float ratio = dist > 1f ? 1f / Mathf.Clamp(dist, 0.1f, Mathf.Infinity) : dist;
for (int i = _meshes.Length - 1; i >= 0; i--) {
if (ratio <= _meshes[i].lod) {
_mesh = _meshes[i].mesh;
_castShadows = _meshes[i].shadows;
break;
}
}
}
private void RenderBatches() {
if (_mesh == null) return;
for (int i = 0; i < _batches.Count; i++) {
Graphics.DrawMeshInstanced(
_mesh,
0,
_material,
_batches[i],
null,
_castShadows ? UnityEngine.Rendering.ShadowCastingMode.On : UnityEngine.Rendering.ShadowCastingMode.Off,
_recieveShadows
);
}
}
private void OnDrawGizmos() {
if (!_drawGizmos) return;
Gizmos.color = Color.red;
Gizmos.DrawWireCube(transform.position, new Vector3(_range.x * 2, 5, _range.y * 2));
}
private void Initialize() {
int addedMatricies = 0;
_batches.Clear();
_batches.Add(new List<Matrix4x4>());
RaycastHit hit;
for (int i = 0; i < _instances; i++) {
if (addedMatricies < _batchSize && _batches.Count != 0) {
Vector3 rayTestPosition = GetRandomRayPosition();
Ray ray = new Ray(rayTestPosition, Vector3.down);
if (!Physics.Raycast(ray, out hit)) continue;
if (IsToSteep(hit.normal, ray.direction)) continue;
Quaternion rotation = GetRotation(hit.normal);
Vector3 scale = GetRandomScale();
Vector3 targetPos = hit.point;
_batches[_batches.Count - 1].Add(Matrix4x4.TRS(targetPos, rotation, scale));
addedMatricies++;
continue;
}
_batches.Add(new List<Matrix4x4>());
addedMatricies = 0;
}
}
private Vector3 GetRandomRayPosition() {
return new Vector3(transform.position.x + Random.Range(-_range.x, _range.x), transform.position.y + 100, transform.position.z + Random.Range(-_range.y, _range.y));
}
private bool IsToSteep(Vector3 normal, Vector3 direction) {
float dot = Mathf.Abs(Vector3.Dot(normal, direction));
return dot < _steepness;
}
private Vector3 GetRandomScale() {
return new Vector3(Random.Range(_scaleMin.x, _scaleMax.x), Random.Range(_scaleMin.y, _scaleMax.y), Random.Range(_scaleMin.z, _scaleMax.z));
}
private Quaternion GetRotation(Vector3 normal) {
Vector3 eulerIdentiy = Quaternion.ToEulerAngles(Quaternion.identity);
if (_randomYAxisRotation) eulerIdentiy.y += Random.Range(-_maxYRotation, _maxYRotation);
if (_rotateToGroundNormal) {
return Quaternion.FromToRotation(Vector3.up, normal) * Quaternion.Euler(eulerIdentiy);
}
return Quaternion.Euler(eulerIdentiy);
}
}
Code kopieren
Shader "Custom/InstancedColorSurfaceShader"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Uses the physically based standard lighting model with shadows enabled for all light types.
#pragma surface surf Standard fullforwardshadows
// Use Shader model 3.0 target
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
UNITY_Instancing_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_Instancing_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
Code kopieren
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InstancerIndirect : MonoBehaviour {
[Header("Debugging")]
[SerializeField] private bool _drawGizmos;
[Header("Instances")]
[SerializeField] private int _instances;
[SerializeField] private int _trueInstanceCount;
[Header("Grass settings")]
[SerializeField] private Vector2 _range;
[SerializeField] private Vector3 _scaleMin = Vector3.one;
[SerializeField] private Vector3 _scaleMax = Vector3.one;
[SerializeField][Range(0f, 1f)] private float _steepness;
[SerializeField] private bool _rotateToGroundNormal = false;
[SerializeField] private bool _randomYAxisRotation = false;
[SerializeField] private float _maxYRotation = 90;
[SerializeField] private LayerMask _groundLayer;
[Header("Rendering")]
[SerializeField] private Material _material;
[SerializeField] private bool _recieveShadows;
[SerializeField] private Transform _mainLight;
[SerializeField] private Mesh _mesh;
private Transform _camera;
private float distToCamera;
private bool _castShadows;
private ComputeBuffer _argsBuffer;
private ComputeBuffer _trsBuffer;
private List<Matrix4x4> _trsList = new List<Matrix4x4>();
private void Start() {
Initialize();
Invoke("UpdateLight", 1f);
}
private void Update() {
GetDistToCamera();
UpdateLight();
RenderInstances();
}
private void OnDestroy() {
if (_argsBuffer != null) {
_argsBuffer.Release();
}
if (_trsBuffer != null) {
_trsBuffer.Release();
}
}
private void OnDrawGizmos() {
if (!_drawGizmos) return;
if (_camera == null) _camera = Camera.main.transform;
Gizmos.color = Color.red;
Gizmos.DrawWireCube(transform.position, new Vector3(_range.x * 2, 5, _range.y * 2));
}
private void GetDistToCamera() {
distToCamera = Vector3.Distance(_camera.position, transform.position); ;
}
private void UpdateLight() {
Vector3 lightDir = -_mainLight.forward;
_material.SetVector("_LightDir", new Vector4(lightDir.x, lightDir.y, lightDir.z, 1));
}
private void RenderInstances() {
if (_mesh == null) return;
Graphics.DrawMeshInstancedIndirect(
_mesh,
0,
_material,
new Bounds(transform.position, Vector3.one * _range.x),
_argsBuffer,
0,
null,
_castShadows ? UnityEngine.Rendering.ShadowCastingMode.On : UnityEngine.Rendering.ShadowCastingMode.Off
);
}
private void Initialize() {
_camera = Camera.main.transform;
RaycastHit hit;
for (int i = 0; i < _instances; i++) {
Vector3 rayTestPosition = GetRandomRayPosition();
Ray ray = new Ray(rayTestPosition, Vector3.down);
if (!HitSomething(ray, out hit)) continue;
if (hit.transform.tag.Equals("IgnoreRaycast")) continue; //can be replaced with whatever you want
if (IsToSteep(hit.normal, ray.direction)) continue;
Quaternion rotation = GetRotation(hit.normal);
Vector3 scale = GetRandomScale();
Vector3 targetPos = hit.point;
_trsList.Add(Matrix4x4.TRS(targetPos, rotation, scale));
_trueInstanceCount++;
}
Mesh mesh = _meshes[0].mesh;
uint[] args = new uint[5];
args[0] = (uint)mesh.GetIndexCount(0);
args[1] = (uint)_trueInstanceCount;
args[2] = (uint)mesh.GetIndexStart(0);
args[3] = (uint)mesh.GetBaseVertex(0);
args[4] = 0;
_argsBuffer = new ComputeBuffer(1, 5 * sizeof(uint), ComputeBufferType.IndirectArguments);
_argsBuffer.SetData(args);
_trsBuffer = new ComputeBuffer(_trueInstanceCount, 4 * 4 * sizeof(float));
_trsBuffer.SetData(_trsList.ToArray());
_material.SetBuffer("trsBuffer", _trsBuffer);
_trsList.Clear();
}
private bool HitSomething(Ray ray, out RaycastHit hit) {
return Physics.Raycast(ray, out hit, _groundLayer);
}
private Vector3 GetRandomRayPosition() {
return new Vector3(transform.position.x + Random.Range(-_range.x, _range.x), transform.position.y + 100, transform.position.z + Random.Range(-_range.y, _range.y));
}
private bool IsToSteep(Vector3 normal, Vector3 direction) {
float dot = Mathf.Abs(Vector3.Dot(normal, direction));
return dot < _steepness;
}
private Vector3 GetRandomScale() {
return new Vector3(Random.Range(_scaleMin.x, _scaleMax.x), Random.Range(_scaleMin.y, _scaleMax.y), Random.Range(_scaleMin.z, _scaleMax.z));
}
private Quaternion GetRotation(Vector3 normal) {
Vector3 eulerIdentiy = Quaternion.ToEulerAngles(Quaternion.identity);
if (_randomYAxisRotation) eulerIdentiy.y += Random.Range(-_maxYRotation, _maxYRotation);
if (_rotateToGroundNormal) {
return Quaternion.FromToRotation(Vector3.up, normal) * Quaternion.Euler(eulerIdentiy);
}
return Quaternion.Euler(eulerIdentiy);
}
}
Code kopieren
Shader "Unlit/GrassBladeIndirect"
{
Properties
{
_MainTex ("Main Tex", 2D) = "white" {}
_PrimaryCol ("Primary Color", Color) = (1, 1, 1)
_SecondaryCol ("Secondary Color", Color) = (1, 0, 1)
_AOColor ("AO Color", Color) = (1, 0, 1)
_TipColor ("Tip Color", Color) = (0, 0, 1)
_Scale ("Scale", Range(0.0, 2.0)) = 0.0
_MeshDeformationLimit ("Mesh Deformation Limit", Range(0.0, 5.0)) = 0.0
_WindNoiseScale ("Wind Noise Scale", float) = 0.0
_WindSpeed ("Wind Speed", Vector) = (0, 0, 0, 0)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Cull Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#pragma target 4.5
#pragma multi_compile_Instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
//generated by shadergraph
inline float Unity_SimpleNoise_RandomValue_float (float2 uv) {
return frac(sin(dot(uv, float2(12.9898, 78.233)))*43758.5453);
}
//generated by shadergraph
inline float Unity_SimpleNnoise_Interpolate_float (float a, float b, float t) {
return (1.0-t)*a + (t*b);
}
//generated by shadergraph
inline float Unity_SimpleNoise_ValueNoise_float (float2 uv) {
float2 i = floor(uv);
float2 f = frac(uv);
f = f * f * (3.0 - 2.0 * f);
uv = abs(frac(uv) - 0.5);
float2 c0 = i + float2(0.0, 0.0);
float2 c1 = i + float2(1.0, 0.0);
float2 c2 = i + float2(0.0, 1.0);
float2 c3 = i + float2(1.0, 1.0);
float r0 = Unity_SimpleNoise_RandomValue_float(c0);
float r1 = Unity_SimpleNoise_RandomValue_float(c1);
float r2 = Unity_SimpleNoise_RandomValue_float(c2);
float r3 = Unity_SimpleNoise_RandomValue_float(c3);
float bottomOfGrid = Unity_SimpleNnoise_Interpolate_float(r0, r1, f.x);
float topOfGrid = Unity_SimpleNnoise_Interpolate_float(r2, r3, f.x);
float t = Unity_SimpleNnoise_Interpolate_float(bottomOfGrid, topOfGrid, f.y);
return t;
}
//generated by shadergraph
void Unity_SimpleNoise_float(float2 UV, float Scale, out float Out) {
float t = 0.0;
float freq = pow(2.0, float(0));
float amp = pow(0.5, float(3-0));
t += Unity_SimpleNoise_ValueNoise_float(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;
freq = pow(2.0, float(1));
amp = pow(0.5, float(3-1));
t += Unity_SimpleNoise_ValueNoise_float(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;
freq = pow(2.0, float(2));
amp = pow(0.5, float(3-2));
t += Unity_SimpleNoise_ValueNoise_float(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;
Out = t;
}
StructuredBuffer<float4x4> trsBuffer;
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _PrimaryCol, _SecondaryCol, _AOColor, _TipColor;
float _Scale;
float4 _LightDir;
float _MeshDeformationLimit;
float4 _WindSpeed;
float _WindNoiseScale;
v2f vert (appdata v, uint instanceID : SV_InstanceID)
{
v2f o;
//applying transformation matrix
float3 positionWorldSpace = mul(trsBuffer[instanceID], float4(v.vertex.xyz, 1));
o.vertex = mul(UNITY_MATRIX_VP, float4(positionWorldSpace, 1));
//move world UVs by time
float4 worldPos = float4(positionWorldSpace, 1);
float2 worldUV = worldPos.xz + _WindSpeed * _Time.y;
//creating noise from world UVs
float noise = 0;
Unity_SimpleNoise_float(worldUV, _WindNoiseScale, noise);
noise = pow(noise, 2);
//to keep bottom part of mesh at its position
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
float smoothDeformation = smoothstep(0, _MeshDeformationLimit, o.uv.y);
float distortion = smoothDeformation * noise;
//apply distortion
o.vertex.x += distortion;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float4 col = lerp(_PrimaryCol, _SecondaryCol, i.uv.y);
//from https://github.com/GarrettGunnell/Grass/blob/main/Assets/Shaders/ModelGrass.shader
float light = clamp(dot(_LightDir, normalize(float3(0, 1, 0))), 0 , 1);
float4 ao = lerp(_AOColor, 1.0f, i.uv.y);
float4 tip = lerp(0.0f, _TipColor, i.uv.y * i.uv.y * (1.0f + _Scale));
float4 grassColor = (col + tip) * light * ao;
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return grassColor;
}
ENDCG
}
}
}
Code kopieren
Kommentar schreiben
Kommentare
Keine Kommentare