GPU Instancing mit der Unity Engine

Seite wird geladen

In diesem Blog erkläre ich Euch, was GPU Instancing überhaupt ist, wofür wir es brauchen und wie die Implementierung letztendlich aussieht.

Ich benutzte hier Unity 2020 und die Standard-Render-Pipeline, aber dieses Feature gibt es bereits in älteren Versionen sowie in allen Render-Pipelines.

Mit meinem Blog richte ich mich an Fortgeschrittene, die schon etwas Erfahrung mit Unity und oder mit Programmierung im Allgemeinen haben. Deshalb gehe ich auch nicht auf alle Fachbegriffe oder auf die Programmiersprachen ein, da diese eigenständige Themen sind.

Am Ende des Blogs steht der komplette C#- und Shader-Code zum Kopieren bereit.

Für die, die es ganz eilig haben: Hier mein Repository mit Code 

Was ist GPU Instancing?

GPU Instancing bezeichnet eine Methode, mit der wir die Menge Draw Calls minimieren können.
Bei einem Draw Call sagt die CPU (Prozessor) der GPU (Grafikkarte), was und wie sie etwas rendern bzw. darstellen soll.
Ein Draw Call enthält bspw. die Geometry (Mesh-Daten), Texturen und Shader.

Wofür brauchen wir GPU Instancing?

 Ab einer großen Menge Draw Calls leidet die Performance des Spiels, da unsere CPU der GPU in jedem Frame mitteilen muss, was gerendert werden soll. Das Kopieren der Daten von der CPU auf die GPU ist einfach nicht schnell genug für viele Objekte, weil die CPU die Daten nur nacheinander abarbeiten kann und die GPU erst warten muss, bis alle Daten übertragen worden sind.

Wie funktioniert GPU Instancing?

Der Trick: Anstatt in jedem Frame die Daten von der CPU auf die GPU zu kopieren, speichern wir die Daten direkt auf der GPU und sparen uns so den Kopiervorgang von CPU zu GPU.

Vorteile und Nachteile

Vorteile:

Mit GPU Instancing können wir mehrere Hunderttausend/Millionen Objekte (je nach Komplexität) gleichzeitig mit Lichtberechnungen (Schatten) rendern, ohne dass die Performance zu stark beeinflusst wird.

Wir können jedes 3D-Objekt für GPU Instancing benutzen.

Nachteile:

Dadurch, dass die Daten nur auf der GPU liegen, können wir nicht mehr damit interagieren. Beispielsweise lassen sich Kollisionen nicht mehr abfragen und die Position lässt sich nicht mehr einfach per C#-Skript verändern. Position, Form etc. können wir aber immer noch mit einem Vertex-Shader beeinflussen.

Frustum Culling müssen wir nun selber implementieren.
So nennt sich der Vorgang, bei dem Objekte "deaktiviert" werden, sobald sie nicht mehr für die Kamera sichtbar sind.

Da dies ein komplexes, eigenständiges Thema ist, gehe ich in diesem Beitrag nicht weiter darauf ein.

Aber ich erkläre später noch, wie wir eine sehr vereinfachte Version davon erstellen können.

Warum also GPU Instancing benutzen?

Wenn wir bspw. das Gras auf einer großen Wiese rendern möchten, ist GPU Instancing extrem gut dafür geeignet. Grashalme müssen nicht vom Spieler beeinflusst werden und ihre Position nicht verändern. Also warum soll nicht einfach die GPU die Daten direkt verarbeiten lassen?

Eine Bewegung der Grashalme durch Wind lässt sich auch direkt durch einen Shader simulieren. Zusätzlich könnten wir das Gras mit so einem Shader auch noch durch die Spieler-Bewegung verändern. Dieses Thema eignet sich jedoch ebenfalls für einen separaten Blog-Beitrag.  

Voraussetzungen

GPU Instancing funktioniert mit allen Render-Pipelines, aber nicht mit dem SRP Batcher.
Mehr Infos findet Ihr in der Unity Dokumentation.

GPU Instancing mit Unity auf zwei verschiedene Arten

Unity bietet uns mehrere GPU Instancing-Möglichkeiten. Im Folgenden gehe ich jedoch nur auf diese beiden hier ein:

Graphics.DrawMeshInstanced:
Mit dieser Methode definieren wir eine Batches-Liste, die wir in jedem Frame an die GPU schicken.

Ein Batch beinhaltet hier wiederum eine Liste von Transformations-Matrizen, die dann von der GPU verarbeitet werden.

Allerdings kann ein Batch die maximale Länge 1023 (von 0 bis 1023) haben.
Mit diesem Ansatz könnten wir diese Batches auf bestimmte Bereiche/Objekte aufteilen. Ab einer gewissen Distanz zur Kamera können wir diese auch überspringen, um erneut Performance zu gewinnen.

Transformations-Matrizen (4 x 4 Matrizen) enthalten Informationen über die Position, Rotation und Größe eines Objekts.

 

Graphics.DrawMeshInstancedIndirect:
Diese Methode ermöglicht es uns, einen ComputeBuffer direkt auf der GPU zu erstellen und so das Limit von 1023 zu überschreiten.

Dieser ComputeBuffer speichert dann erneut Transformations-Matrizen. Allerdings ist die Größe des Buffers hier nur an den Grafikkartenspeicher gebunden (VRAM).

Zwar bietet diese Methode mehr Performance, aber wir können dann nicht mehr so leicht entscheiden, ob und welche Objekte nicht mehr gerendert werden sollen.

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
         

Batches initialisieren

Damit wir der GPU keine leere Batches-Liste geben, müssen wir diese zunächst befüllen.

Zuerst iterieren wir über die gewünschte Anzahl _instances und erstellen für diese unsere Batches-Liste.

Dabei testen wir mit einem RayCast innerhalb unserer _range, ob wir unseren _groundLayer getroffen haben.
Der Punkt, den unser RayCast getroffen hat, wird später die (mögliche) Position eines Objekts für das GPU Instancing sein. Nach ein paar weiteren Checks erstellen wir dann eine Transformations-Matrix für diese Position.

Hilfsfunktionen erklärt:

Die Funktion GetRandomRayPosition() liefert uns eine zufällige Position innerhalb unserer _range, von der aus wir einen RayCast starten können.

Die Funktion IsToSteep() prüft, ob die Steigung an dem Punkt zu groß/klein ist.

Die Funktion GetRandomScale() liefert uns eine zufällige Größe zurück, die mit _scaleMin und _scaleMax definiert worden ist.

Die Funktion GetRotation(Vector3 normal) liefert uns die "Rotation" an dem Punkt zurück, den der Raycast getroffen hat.
Zusätzlich werden hier noch _maxYRotation und _rotateToGroundNormal mit einbezogen.

private void InitializeBatches() {
        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);

                //we did not hit our ground layer
                if (!Physics.Raycast(ray, out hit, _groundLayer)) continue;

                if (IsToSteep(hit.normal, ray.direction)) continue;

                Quaternion rotation = GetRotation(hit.normal);
                Vector3 scale = GetRandomScale();
                Vector3 targetPos = hit.point;

                targetPos.y += scale.y / 2f; //keep or remove, depends on your mesh

                _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);
        eulerIdentiy.x += 90; //can be removed or changed, depends on your mesh

        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
         

Debugging

Damit wir sehen können, in welchem Bereich die Objekte spawnen, können wir uns hiermit grob den Bereich unserer _range anzeigen lassen.

private void OnDrawGizmos() {
        if (!_drawGizmos) return;
        Gizmos.color = Color.red;
        Gizmos.DrawWireCube(transform.position, new Vector3(_range.x * 2, 5, _range.y * 2));
}
            Code kopieren
         

MeshLODs auswerten

Bevor wir mit dem Rendern beginnen, müssen wir auswerten, welchen Mesh wir dafür nutzen sollten. Je weiter die Kamera weg ist, desto weniger komplex sollte unser Mesh sein oder sogar ganz verschwinden.

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;
         }
     }
}
            Code kopieren
         

Batches Rendern

Da wir jetzt unseren Mesh ausgewertet haben, können wir beginnen, die Batches zu rendern. Dafür iterieren wir über unsere Batches-Liste und schicken diese an die GPU.

Den Vorgang mit dem Auswählen des Meshes und Rendern der Batches sollten wir in jedem Frame in folgender Reihenfolge durchführen.

private void Update() {
     GetMeshFromCameraDistance();
     RenderBatches();
}
            Code kopieren
         
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
          );
      }
}
            Code kopieren
         

Material und Shader

Damit wir überhaupt etwas sehen, müssen wir für das Material GPU Instancing aktivieren.

Shader

Wir können einen Standard-Shader benutzen, einen Shader selber schreiben oder mit Shadergraph einen Shader bauen.

Wenn wir unseren Shader selbst schreiben, brauchen wir in unserm Shader Pass noch folgende Anweisung (damit taucht die Option vom Screenshot oben auf).

#pragma multi_compile_Instancing
            Code kopieren
         

Hier ist ein Shader-Beispiel aus der Unity-Dokumentation, das sich super zum Testen eignet (Standard Render-Pipeline):

Shader "Custom/SimplestInstancedShader"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_Instancing
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID // use this to access instanced properties in the fragment shader.
            };

            UNITY_Instancing_BUFFER_START(Props)
                UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
            UNITY_Instancing_BUFFER_END(Props)

            v2f vert(appdata v)
            {
                v2f o;

                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);
                return UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
            }
            ENDCG
        }
    }
}
            Code kopieren
         

Zwischenergebnis

Mit dem Skript und unserem Shader können wir nun schon mal probeweise rendern.

Auf dem Bild sehen wir 10.000 Grashalme, die sogar Schatten werfen!

Ansatz erweitern

Am besten eignen sich dafür mehrere kleine Volumes.
Als Volume bezeichne ich hier die Objekte, die mit unserem Skript die Grashalme instantiieren und einen gewissen Bereich damit füllen (s. der rote Rahmen auf dem Screenshot).

Wir können bereits coole Ergebnisse erzielen, wenn wir mehrere Volumes nebeneinander platzieren und diese etwas überlappen lassen.

Dummy Frustum Culling

Hier passt eher "Dummy", da wir den folgenden Content nicht wirklich "Frustum Culling" nennen können. Aber mit diesem Trick können wir es nachahmen.
Dafür brauchen wir auf jedem Volume einen SpriteRenderer ohne Sprite, damit wir innerhalb des Instancer-Scrips Folgendes nutzen können:

private void OnBecameVisible() {
     _visible = true;
}

private void OnBecameInvisible() {
     _visible = false;
}
            Code kopieren
         

Und das verwenden wir dann so:

private void GetMeshFromCameraDistance() {
    if(!_visible) {
        _mesh = null;
        return;
    }
   //...
}
            Code kopieren
         

Somit überspringen Volumes, die nicht mehr sichtbar sind, das Rendering, womit wir einen kleinen Performance-Boost erzielen.

Ansatz DrawMeshInstancedIndirect

Dieser Ansatz ist sehr ähnlich zu DrawMeshInstanced.
Allerdings brauchen wir folgende neuen Variablen für das Skript:

_trsList ist eine Liste mit Transformations-Matrizen, die dann mittels _trsBuffer auf die Grafikkarte kopiert wird.

_argsBuffer wird in jedem Frame an die Grafikkarte übergeben.
Dieser enthält Informationen über den Mesh und die Anzahl der Instanzen, die gerendert werden sollen.

_trueInstanceCount wird für den _argsBuffer benötigt. Da wir beim Initialisieren von Transformationen eventuell welche überspringen, weil bspw. die Steigung an dem Punkt zu steil ist, brauchen wir hier eine Variable, die die tatsächliche Anzahl mitzählt. 

private ComputeBuffer _argsBuffer;
private ComputeBuffer _trsBuffer;
private List<Matrix4x4> _trsList = new List<Matrix4x4>();
private int _trueInstanceCount;
            Code kopieren
         

Ansatz DrawMeshInstancedIndirect

Die Funktion InitializeBatches() können wir etwas abgeändert nutzen. Wir erweitern sie, indem wir den _argsBuffer und den _trsBuffer mit der _trsList füllen.
Am Ende können wir den Speicher der _trsList leeren, da wir sie auf der CPU-Seite nicht mehr brauchen.

private void InitializeTransformBuffer() {
     _camera = Camera.main.transform;
     RaycastHit hit;

     for (int i = 0; i < _instances; i++) {
          Vector3 rayTestPosition = GetRandomRayPosition();
          Ray ray = new Ray(rayTestPosition, Vector3.down);

          if (!Physics.Raycast(ray, out hit, _groundLayer)) continue;
          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();

}
            Code kopieren
         

Aufräumen nicht vergessen!

Zusätzlich müssen wir - sobald das Script zerstört wird - den Speicher auf der Grafikkarte freigeben.

private void OnDestroy() {
     if (_argsBuffer != null) {
         _argsBuffer.Release();
     }
     if (_trsBuffer != null) {
         _trsBuffer.Release();
     }
}
            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
         

UNITY_MATRIX_VP ist eine globale Shader-Variable von Unity.

Dahinter verbirgt sich eine 4x4-Matrix für unsere View und Projection.

Die View-Matrix ist das Inverse der Model-Matrix der Kamera und transformiert Vertices vom WorldSpace in CameraSpace (auch View- oder ClipSpace genannt).

Die Projection-Matrix definiert - grob gesagt - das Sichtfeld der Kamera.
Hier ein Beispiel mit perspektivischer (links) und orthographischer (rechts) Ansicht:

Zwischenergebnis

Das ist schon alles, was wir brauchen, um unsere Objekte an der richtigen Position und mit der richtigen Größe und Rotation zu rendern.

Hier unsere kleine Wiese mit von vorhin aber mit 600.000 Grashalmen und immer noch über 300 FPS:

Stresstest

Ich habe getestet, wie viele Grashalme mein PC rendern kann.
Die Grafikkarte, die ich benutze, ist die GTX 970 4GB und als FPS-Limit habe ich 60 FPS bei einer 1920px x 1080px-Auflösung gewählt, da diese heutzutage die Mindestanforderung für Spiele darstellt.

Erreicht habe ich das Limit auf einem 300m x 300m-Bereich mit 6.000.000 Grashalmen, die zusätzlich im Vertex-Shader animiert werden.

Interessant: Da pro Grashalm eine 4 x 4-Matrix gespeichert wird, haben wir 16 * 4 Bytes = 64 Bytes pro Grashalm. Somit werden 384.000.000 Bytes bzw. 375 MiB nur für die Transformationen verbraucht. 

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

* Diese Felder sind erforderlich

Kommentare

Keine Kommentare