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