问题

项目中需要渲染一大片草地,最初始的实现是使用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;
    }
}

效果图

01-31 21:06