GPU Instancing,即GPU实例化,实现方式是将使用同一个材质及网格的物体(需要大批量渲染)的数据一次性打包发给GPU(最常见的运用无非是草的渲染了…),以达到减少大量DrawCall的目的。其它的我就不啰嗦了,一些细节有必要我会在后面再说。
这次我也只是实现(抄了)了一个比较简单的草地Demo,参考项目是Unity利用GPUinstancing实现大面积草地。下面看一下我自己实现的效果图:
送你一片青青草原!
动图来一发,看着很卡还很糊还很难看…但是实际上一点都不卡(电脑上)
以下是代码
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public class MeshGrass : MonoBehaviour
{
public Material grassmat;
public Mesh quad;
public Texture2D heightMap;
public float terrainHeight = 20;
public int terrainSize = 80;
public Material terrainMat;
public int grasscount = 300;
[Range(0.0f, 1f)]
private float grassdensity;
private List<Matrix4x4[]> matrixlist = new List<Matrix4x4[]>();
private Camera mainCamera;
private CommandBuffer myBuffer;
void Start()
{
CreateTerrian();
MeshComputing();
}
private void Update()
{
RenderGrass();
}
void CreateTerrian()
{
List<Vector3> vertices = new List<Vector3>();
List<int> triangles = new List<int>();
for (int i = 0; i < terrainSize; i++)
{
for (int j = 0; j < terrainSize; j++)
{
vertices.Add(new Vector3(i, heightMap.GetPixel(i, j).grayscale * terrainHeight, j));
if (i == 0 || j == 0) continue;
triangles.Add(terrainSize * i + j);
triangles.Add(terrainSize * i + j - 1);
triangles.Add(terrainSize * (i - 1) + j - 1);
triangles.Add(terrainSize * (i - 1) + j - 1);
triangles.Add(terrainSize * (i - 1) + j);
triangles.Add(terrainSize * i + j);
}
}
Vector2[] uvs = new Vector2[vertices.Count];
for (var i = 0; i < uvs.Length; i++)
{
uvs[i] = new Vector2(vertices[i].x, vertices[i].z);
}
GameObject terrain = this.gameObject;
terrain.AddComponent<MeshFilter>();
MeshRenderer renderer = terrain.AddComponent<MeshRenderer>();
renderer.sharedMaterial = terrainMat;
renderer.shadowCastingMode = ShadowCastingMode.On;
renderer.receiveShadows = true;
Mesh groundMesh = new Mesh();
groundMesh.vertices = vertices.ToArray();
groundMesh.uv = uvs;
groundMesh.triangles = triangles.ToArray();
groundMesh.RecalculateNormals();//生成法线
terrain.GetComponent<MeshFilter>().mesh = groundMesh;
terrain.AddComponent<MeshCollider>();
vertices.Clear();
}
void MeshComputing()
{
grassdensity = (float)terrainSize / grasscount;
Matrix4x4[] matrices = new Matrix4x4[1023];
Quaternion rotation = Quaternion.Euler(0, 0, 0);
Vector3 scale = Vector3.one;
int mm = 0;
for (int i = 0; i < grasscount; i++)
{
for (int j = 0; j < grasscount; j++)
{
float ran = Random.Range(-0.19f, 0.2f);
float ii = i * grassdensity;
float jj = j * grassdensity;
float x = ii + ran;
float y = jj + ran;
float h = heightMap.GetPixel(Mathf.FloorToInt(ii), Mathf.FloorToInt(jj)).grayscale * terrainHeight + 0.5f;
Vector3 position = new Vector3(x, h, y)+transform.position;
matrices[mm] = Matrix4x4.TRS(position, rotation, scale);
mm++;
if (mm % 1022 == 0)
{
matrixlist.Add(new Matrix4x4[1023]);
matrixlist[matrixlist.Count - 1] = matrices;
matrices = new Matrix4x4[1023];
mm = 0;
}
}
}
matrixlist.Add(matrices);
}
void RenderGrass()
{
MaterialPropertyBlock block = new MaterialPropertyBlock();
foreach (Matrix4x4[] mat in matrixlist)
{
Graphics.DrawMeshInstanced(quad, 0, grassmat, mat, 1023, block, ShadowCastingMode.Off, false);
}
}
}
这个代码也很好理解,花点时间看一看就基本能看懂了
void CreateTerrian() 根据噪音图生成地形网格
void MeshComputing() 生成草的Model矩阵并保存
void RenderGrass() 渲染草
这里需要重点说明一下Graphics.DrawMeshInstanced()
,该函数需要每帧调用。我们这里只用到了前8个参数,分别是mesh
网格,submeshIndex
要绘制网格的哪个子集(这仅适用于由多种材料组成的网格。),material
材质,matrices
模型矩阵数组,count
数量(最大数量是1023,这就是为什么草的Model矩阵分1023存做一批),properties
要应用的其他材料属性(这个可以跳过),castShadows
是否投射阴影(我开了好像没有用,不知道为什么)。
需要注意的是GetPixel()函数需要将贴图的属性设置为可读,不然会报错
随便找个空物体把脚本一挂,参数一设,启动就一切OK了
以下是Shader的代码
Shader "MyShader/GPUInstancing"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_WindTex ("Wind Texture",2D)="while"{}
_Color("Texture Color",Color)=(1,1,1,1)
_VerticalBillboarding("Vertical Restraints",Range(0,1))=1
_WindVector("Wind Vector",Vector)=(1,0,0,0)
_TimeScale("Time Scale",float)=1
_CutOff("Cut Off",Range(0,0.8))=0.2
}
SubShader
{
Tags {"IgnoreProjector" = "True" "RenderType" = "TransparentCutout" "DisableBatching" = "True"}
LOD 100
Cull Off
Pass
{
Tags{ "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
//第一步,必须要使用这个编译指示符宣告使用GPU多例化技术
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
//第二步,如果要访问片元着色器中的多例化属性变量,需要使用此宏
UNITY_VERTEX_INPUT_INSTANCE_ID
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _WindTex;
fixed4 _Color;
float _VerticalBillboarding;
float4 _MapMessage;
float4 _WindVector;
float _TimeScale;
float _CutOff;
//第三步,宣告多例化属性变量语句
//UNITY_INSTANCING_CBUFFER_START(Props)
//UNITY_DEFINE_INSTANCED_PROP(float4,_Color)
//UNITY_INSTANCING_CBUFFER_END(Props)
//我这里用不上,就是给每个着色器一个不同的属性(例如颜色),
//可以代码赋值也可以Inspectors窗口赋值
v2f vert (appdata v)
{
v2f o;
//第四步,如果要访问片元着色器中的多例化属性变量,需要使用此宏
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v,o);
float3 center=float3(0,0,0);
float4 worldPos=mul(unity_ObjectToWorld,float4(center,1));
float3 viewDir=mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1));
float3 normalDir=viewDir-center;
normalDir.y=normalDir.y*_VerticalBillboarding;
normalDir=normalize(normalDir);
float3 upDir=abs(normalDir.y)>0.999?float3(0,0,1):float3(0,1,0);
float3 rightDir=normalize(cross(normalDir,upDir));
float3 centerOffs=v.vertex.xyz-center;
float3 localPos=center+rightDir*centerOffs.x+upDir*centerOffs.y+normalDir*centerOffs.z;
float2 map=_MapMessage.zw-_MapMessage.xy;
float wind=1-tex2Dlod(_WindTex,float4(worldPos.x/map.x+_Time.x,worldPos.z/map.y,0,0)).b;
float time=_Time.y*_TimeScale;
float4 localWindVector=normalize(mul(unity_WorldToObject,_WindVector));
localPos+=sin(time+wind*10)*cos(time*2/3+1+wind*10)*localWindVector.xyz*
clamp(v.uv.y-0.5,0,1);
o.vertex = UnityObjectToClipPos(float4(localPos,1));
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//第五步,如果要访问片元着色器中的多例化属性变量,需要使用此宏
UNITY_SETUP_INSTANCE_ID(i);
fixed4 col = tex2D(_MainTex, i.uv);
clip(col.a-_CutOff);
fixed4 finalColor=col*_Color;
return finalColor;
}
ENDCG
}
}
}
着色器的重点就在vert部分以及如何使用宏实现GPUInstancing
广告牌部分
float3 center=float3(0,0,0);
float4 worldPos=mul(unity_ObjectToWorld,float4(center,1));
float3 viewDir=mul(unity_WorldToObject,float4(_WorldSpaceCameraPos,1));
float3 normalDir=viewDir-center;
normalDir.y=normalDir.y*_VerticalBillboarding;
normalDir=normalize(normalDir);
float3 upDir=abs(normalDir.y)>0.999?float3(0,0,1):float3(0,1,0);
float3 rightDir=normalize(cross(normalDir,upDir));
float3 centerOffs=v.vertex.xyz-center;
float3 localPos=center+rightDir*centerOffs.x+upDir*centerOffs.y+normalDir*centerOffs.z;
实现风的扰动效果
float wind=1-tex2Dlod(_WindTex,float4(worldPos.x/map.x+_Time.x,worldPos.z/map.y,0,0)).b;
float time=_Time.y*_TimeScale;
float4 localWindVector=normalize(mul(unity_WorldToObject,_WindVector));
localPos+=sin(time+wind*10)*cos(time*2/3+1+wind*10)*localWindVector.xyz*
clamp(v.uv.y-0.5,0,1);
至于实例化部分我也不是很明白,我也只是Copy一下书中的解释贴在代码里,那些宏我也没办法展开讲,如果想要深入了解推荐购买《Unity内建着色器源码剖析》,那里面第五章就有对Unity实例化的宏有详细的解析,只是仅仅想用这个技术只需要Copy就行了。
如果当材质使用就把Enable GPU Instancing 勾上,Unity会自动使用实例化,如上面用代码就不用在意。
风的图片资源可以在我参考的博主的GitHub链接上找。
这一篇压了大概两周,996的生活开始展现它枯燥的本质了,每天下班就想干些愉悦的事儿,一直拖拖拖…