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