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

效果图


Author: superzhan
Blog:http://www.superzhan.cn/
GitHub:https://github.com/superzhan
转载时,请注明出处。

7 Replies to “Unity GPU Instance 无尽草地渲染”

  1. Does your website have a contact page? I’m having trouble locating it but, I’d like to send
    you an email. I’ve got some recommendations for your blog you
    might be interested in hearing. Either way, great
    website and I look forward to seeing it develop over time.

  2. Long time reader, first time commenter — so, thought
    I’d drop a comment.. — and at the same time ask for a favor.

    Your wordpress site is very simplistic – hope
    you don’t mind me asking what theme you’re using? (and don’t mind if I steal it?
    :P)

    I just launched my small businesses site –also built in wordpress
    like yours– but the theme slows (!) the site down quite a bit.

    In case you have a minute, you can find it by searching for “royal cbd” on Google (would appreciate any
    feedback)

    Keep up the good work– and take care of yourself during the
    coronavirus scare!

    ~Justin

发表评论

电子邮件地址不会被公开。 必填项已用*标注