Unity GPU Instance 無盡草地渲染


問題

項目中需要渲染一大片草地,最初始的實現是使用Unity自帶的地形插件,直接在地形中繪制大量的草。這種方法會導致場景中的模型頂點數爆炸,一棵草的頂點數雖然不多,但是大量的草疊加到場景中時,場景中的模型頂點數過度,會造成卡頓。

方案

shader geometry

有一個解決方案是,通過shader來繪制草,在GPU中繪制草的頂點,模擬風等動畫。但是對於某些GPU,並不支持shader的頂點繪制。

GPU Instance

另外的一個方案是使用Unity 提供的 GPU Instance 方式,使用 Graphics.DrawMeshInstanced 接口傳入模型,材質,位置等信息,然后由GPU批量渲染。對於手機游戲,有一定的限制,例如,單次的渲染的數量不能超過1024。具體可以參考https://docs.unity3d.com/2019.1/Documentation/Manual/GPUInstancing.html

shader code

shader中需要在 pass 中聲明 #pragma multi_compile_instancing,在輸入輸出的結構體中聲明宏 UNITY_VERTEX_INPUT_INSTANCE_ID

需要在shader 中模擬風吹動的效果,調用GetWinWave計算風影響的頂點位移, 然后根據頂點的高度計算位移的大小。frag函數中根據高度,處理輸出的顏色。

Shader "Grass/Grass"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _WindTex ("風貼圖", 2D) = "white" {}
        [HDR]_Color ("顏色", Color) = (0,1,0,1)
        _Height("高度",Float)=1
        _WindSpeed("風速",Float)=2
        _WindSize("風尺寸",Float)=10

        _LowColor("草根部顏色",Color)= (1,1,1,1)
        _TopColor("草頂部顏色",Color) = (1,1,1,1)
        _MaxHight("草的最大高度",Float) = 3
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Cull off

        Pass
        {
            Tags { "LightMode"="ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            #pragma multi_compile_instancing

            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)

                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _WindTex;

            float _Height;
            float _WindSpeed;
            float _WindSize;
            float4 _Color;

            float4 _LowColor;
            float4 _TopColor;
            float _MaxHight;

            float GetWindWave(float2 position,float height){
                //以物體坐標點采樣風的強度,
                //風按照時間*風速移動,以高度不同獲得略微有差異的數據
                //移動值以高度不同進行減免,越低移動的越少.
                //根據y值獲得不同的
                float4 p=tex2Dlod(_WindTex,float4(position/_WindSize+float2(_Time.x*_WindSpeed+height*.01,0),0.0,0.0)); 
                return height * saturate(p.r-.2);
            }

            v2f vert (appdata v , uint instanceID : SV_InstanceID)
            {
                v2f o;
                
                //GPU Instance 宏
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);

                //設置風的影響
                float4 worldPos = mul(unity_ObjectToWorld,v.vertex);
                float win = GetWindWave(worldPos.xz,v.vertex.y);
                v.vertex.x += win;
                v.vertex.y +=_Height+ win * 0.2;
                
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = TRANSFORM_TEX(v.uv.xy, _MainTex);
                o.uv.z = saturate( v.vertex.y / _MaxHight);

                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                 UNITY_SETUP_INSTANCE_ID(i);
        
                fixed4 col = tex2D(_MainTex, i.uv.xy)*_Color;
                clip(col.a -0.6); //透明度剔除

                fixed hightColFac = i.uv.z;
                fixed3 higthCol = lerp(_LowColor,_TopColor,hightColFac);
                col = fixed4(col.rgb*higthCol , col.a); 

                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

c# Code

在C#代碼中,需要在每個update 中調用 Graphics.DrawMeshInstanced(grassMesh, 0, grassMaterial, grassMaterix4X4,grassMaterix4X4.Length); ,每幀渲染一次草地。

在調用這個接口前需要准備好相關的數據,模型,材質,和矩陣數組。矩陣中包括每棵草的位置旋轉縮放信息,參考SetupGrassBuffers函數。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DrawGrass : MonoBehaviour
{
    public int grassCount = 100;
    public int flowerCount = 100;


    private Mesh grassMesh;
    private Material grassMaterial;

    private Mesh flowerMesh;
    private Material flowerMaterial;

    public Transform grassContainer;
    public Transform flowerContainer;

    private GameObject grassGO=null;
    private GameObject flowerGo = null;

    public float xRange= 100f;
    public float zRange= 100f;

    public float minHightScale = 0.8f;
    public float maxHightScale = 1.5f;

    public float drawGrassHeight = 20f;

    public float limitHeight = 29f;

    public Bounds grassBounds;

    Matrix4x4[] grassMaterix4X4;
    Matrix4x4[] flowerMaterix4X4;
    Vector4[] positions;

    Vector3 selfPosition;
    private float maxHeight=0f;

    //private int curGrassCount=0, curFlowerCount=0;

    void Start(){
          Draw();
    }
    public void Draw(){
        
        if(grassGO == null){
            int childCount = grassContainer.childCount;
            int randomIndex = Random.Range(0,childCount);
            grassGO = grassContainer.GetChild(randomIndex).gameObject;
        }
        if(flowerGo == null){
            int childCount = flowerContainer.childCount;
            int randomIndex =Random.Range(0,childCount);
            flowerGo = flowerContainer.GetChild(randomIndex).gameObject;
        }

         grassMesh = grassGO.GetComponent<MeshFilter>().mesh;  
         grassMaterial = grassGO.GetComponent<MeshRenderer>().sharedMaterial;
         if(grassMesh == null || grassMaterial == null){
             Debug.LogError("mesh or material is null");
             return;
         }

        flowerMesh = flowerGo.GetComponent<MeshFilter>().mesh;  
        flowerMaterial = flowerGo.GetComponent<MeshRenderer>().sharedMaterial;
       

        selfPosition = transform.position;
        maxHeight =0;
        SetupGrassBuffers();
        SetupFlowerBuffers();
        maxHeight+=1.5f;
        grassBounds = new Bounds(new Vector3(selfPosition.x,maxHeight/2,selfPosition.z),new Vector3(xRange*2,maxHeight/2,zRange*2));
    }
    void Update()
    {
        Graphics.DrawMeshInstanced(grassMesh, 0, grassMaterial, grassMaterix4X4,grassMaterix4X4.Length);

        if(flowerCount>0){
           Graphics.DrawMeshInstanced(flowerMesh, 0, flowerMaterial, flowerMaterix4X4,flowerMaterix4X4.Length);
        }

    }
    // void OnDrawGizmos(){
    //     Gizmos.DrawCube(grassBounds.center,grassBounds.size);
    // }

    void SetupGrassBuffers()
    {
        if (grassCount < 1) grassCount = 1;
        List<Matrix4x4> matrixList = new List<Matrix4x4>();
       
        for (int i = 0; i < grassCount; i++)
        {

            float x = Random.Range(-xRange,xRange) + selfPosition.x;
            float z = Random.Range(-zRange,zRange) + selfPosition.z;
            float y = drawGrassHeight;//selfPosition.y;

            Vector3 randomPos=new Vector4(x, y, z, 1f);
            if(GetGround(ref randomPos)){
                float rotateY = Random.Range(0,360);
                float heightScale = Random.Range(minHightScale,maxHightScale);
                if(randomPos.y > maxHeight){
                    maxHeight = randomPos.y;
                }
                matrixList.Add( Matrix4x4.TRS(randomPos, Quaternion.Euler(0F, rotateY, 0F), new Vector3(1,heightScale,1)));
            }
        }
        grassMaterix4X4 = matrixList.ToArray();
    }

    void SetupFlowerBuffers()
    {
        if (flowerCount < 1) {
           return;
        }

        List<Matrix4x4> matrixList = new List<Matrix4x4>();

        for (int i = 0; i < flowerCount; i++)
        {
            float x = Random.Range(-xRange,xRange) + selfPosition.x;
            float z = Random.Range(-zRange,zRange) + selfPosition.z;
            float y = drawGrassHeight;//selfPosition.y;

            Vector3 randomPos=new Vector4(x, y, z, 1f);
            if(GetGround(ref randomPos)){
                float rotateY = Random.Range(0,360);
                if(randomPos.y > maxHeight){
                    maxHeight = randomPos.y;
                }
               matrixList.Add( Matrix4x4.TRS(randomPos, Quaternion.Euler(0F, rotateY, 0F), Vector3.one) );
            }
        }
        flowerMaterix4X4 = matrixList.ToArray();
    }

	RaycastHit[] hitArr = new RaycastHit[3];
    bool GetGround(ref Vector3 p)
	{
		Ray ray = new Ray(p, Vector3.down);
        int hitCount = Physics.RaycastNonAlloc(ray, hitArr, drawGrassHeight);
		if (hitCount>0)
		{
            hitCount = Mathf.Min(hitCount,hitArr.Length);
            float maxHight = float.MinValue;
            int index=-1;
            for(int i=0;i<hitCount;++i){
                RaycastHit hit = hitArr[i];

                if(hit.point.y > maxHight){
                    maxHight = hit.point.y;
                    index = i;
                }
            }
            if(index >=0){
                RaycastHit closeHit = hitArr[index];
                
                if (closeHit.collider.CompareTag("Terrain") || closeHit.collider.CompareTag("SkyGround"))
                {
                    //如果命中地面,則使用命中后的位置.
                    p = closeHit.point;
                    if(p.y >= limitHeight){
                        return false;
                    }
                    return true;
                }
            }
        
		}
		return false;
	}
}

效果圖


About

Author:superzhan
Blog: http://www.superzhan.cn
Github: https://github.com/superzhan


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM