問題
項目中需要渲染一大片草地,最初始的實現是使用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