最近十一因为怕疫情问题,哪里也不去,就在家里窝着搞塞尔达(荒野之息和织梦岛)。
荒野之息这游戏有个特点就是野外漫山遍野的青草,这对于图形性能是个严格的考验,当然这漫山的草可不是我们通常用地形刷刷上去的那种,因为这个密密麻麻的数量级,不论是静态批处理还是动态批处理亦或者GPUInstancing全都不顶用,因为这三种操作必须经过渲染流程的应用阶段,而这个mesh数量级毫无疑问可以爆掉CPU和总线BUS了。
不过我们可以通过geometry shader生成mesh,绕过应用阶段。
我们以前就学过geometry shader,而且用geometry shader做过效果的,大家还记得吧(不记得或者没看过建议返回去看,有个概念)。我们通过geometry shader可以将网格的每个顶点都扩展成另外的几何体(之前我们扩展的立方体),那么我们我们将顶点扩展成为一个草体也是一样的原理。
核心:将mesh的一个vertex扩展成一个grass mesh
我们先创建一个mesh:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class RectangleMesh : MonoBehaviour
{
public bool updated = false;
public int CellCount = 50;
public float CellLength = 10f;
void Start()
{
updated = true;
}
void Update()
{
if (updated)
{
CreateMesh();
updated = false;
}
}
private void CreateMesh()
{
Mesh mesh = new Mesh();
int cellCountAddOne = CellCount + 1;
Vector3[] vertices = new Vector3[cellCountAddOne * cellCountAddOne];
Vector3[] normals = new Vector3[cellCountAddOne * cellCountAddOne];
int[] triangles = new int[CellCount * CellCount * 2 * 3];
Vector2[] uvs = new Vector2[cellCountAddOne * cellCountAddOne];
int triangleindex = 0;
for (int x = 0; x < cellCountAddOne; x++)
{
for (int y = 0; y < cellCountAddOne; y++)
{
int index = x + cellCountAddOne * y;
vertices[index] = new Vector3(x * CellLength, 0, y * CellLength);
normals[index] = new Vector3(0, 1, 0);
uvs[index] = new Vector2((float) x / (float) CellCount, (float) y / (float) CellCount);
if (x < CellCount && y < CellCount)
{
triangles[triangleindex] = x + y * cellCountAddOne;
triangles[triangleindex + 1] = x + (y + 1) * cellCountAddOne;
triangles[triangleindex + 2] = x + y * cellCountAddOne + 1;
triangles[triangleindex + 3] = x + y * cellCountAddOne + 1;
triangles[triangleindex + 4] = x + (y + 1) * cellCountAddOne;
triangles[triangleindex + 5] = x + (y + 1) * cellCountAddOne + 1;
triangleindex += 6;
}
}
}
mesh.vertices = vertices;
mesh.normals = normals;
mesh.triangles = triangles;
mesh.uv = uvs;
GetComponent<MeshFilter>().sharedMesh = mesh;
}
}
创建一个正方形的mesh(创建mesh的方法以前聊过,不理解的返回以前学习怎么创建拓扑网格),帖上拓扑结构:
效果如图:
接下来使用geometry shader将正方形网格的每一个顶点扩展出一个草型网格,草型网格拓扑结构图:
所以我们根据草型网格在geom函数中生成一片草地:
Shader "GeomGrass/GeometryGrassShader"
{
Properties
{
_MainColor ("Color", Color) = (1,1,1,1)
_GrassWidth("Grass Bottom Width",Range(0,5)) = 1
_GrassHeight("Grass Single Height",Range(0,10)) = 3
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Cull Off
CGPROGRAM
#pragma target 4.0
#pragma vertex vert
#pragma fragment frag
#pragma geometry geom
#include "UnityCG.cginc"
struct app2vert
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct vert2geom
{
float4 vertex : POSITION;
float3 normal : TEXCOORD0;
float3 tangent : TEXCOORD1;
};
struct geom2frag
{
float4 vertex : SV_POSITION;
};
float4 _MainColor;
float _GrassWidth;
float _GrassHeight;
vert2geom vert (app2vert v)
{
vert2geom o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent.xyz;
return o;
}
geom2frag topoVert(float4 vertex)
{
geom2frag g2f;
g2f.vertex = vertex;
return g2f;
}
void topoTri(float4 v0,float4 v1,float4 v2,inout TriangleStream<geom2frag> tss)
{
tss.Append(topoVert(v0));
tss.Append(topoVert(v1));
tss.Append(topoVert(v2));
}
//拓扑构建grass的网格
void topoGrass(float4 vertex,float3 norm,float3 tan,inout TriangleStream<geom2frag> tss)
{
float4 vertex0 = UnityObjectToClipPos(vertex-float4(tan*_GrassWidth*0.5,0));
float4 vertex1 = UnityObjectToClipPos(vertex+float4(tan*_GrassWidth*0.5,0));
float4 vertex2 = UnityObjectToClipPos(vertex-float4(tan*_GrassWidth*0.4,0)+float4(norm*_GrassHeight,0));
float4 vertex3 = UnityObjectToClipPos(vertex+float4(tan*_GrassWidth*0.4,0)+float4(norm*_GrassHeight,0));
float4 vertex4 = UnityObjectToClipPos(vertex-float4(tan*_GrassWidth*0.25,0)+float4(norm*_GrassHeight*2,0));
float4 vertex5 = UnityObjectToClipPos(vertex+float4(tan*_GrassWidth*0.25,0)+float4(norm*_GrassHeight*2,0));
float4 vertex6 = UnityObjectToClipPos(vertex+float4(norm*_GrassHeight*3,0));
topoTri(vertex0,vertex1,vertex3,tss);
topoTri(vertex0,vertex3,vertex2,tss);
topoTri(vertex2,vertex3,vertex5,tss);
topoTri(vertex2,vertex5,vertex4,tss);
topoTri(vertex4,vertex5,vertex6,tss);
tss.RestartStrip();
}
[maxvertexcount(36)]
void geom(triangle vert2geom vg[3],inout TriangleStream<geom2frag> tss)
{
for(int i=0;i<3;i++)
{
float4 localvertex = vg[i].vertex;
float3 localnorm = normalize(vg[i].normal);
float3 localtan = normalize(vg[i].tangent);
topoGrass(localvertex,localnorm,localtan,tss);
}
}
fixed4 frag (geom2frag i) : SV_Target
{
fixed4 col = _MainColor;
return col;
}
ENDCG
}
}
}
效果如下:
干巴巴的效果,我们继续增加一点点“生气”,比如纹理、运动、随机等。
首先用ps制作一个草的纹理图:
然后给草型网格映射uv:
同时现实中我们观察草地,草的生长大部分是随机的,所以我们可以绕normal轴随机旋转一下草的角度即可。
同时草地也有自己的随风运动、使用顶点运动即可。
Shader "GeomGrass/LifeGeomGrassShader"
{
Properties
{
_GrassTex("Grass Gradient Texture",2D) = "white" {}
_GrassWidth("Grass Bottom Width",Range(0,5)) = 1
_GrassHeight("Grass Single Height",Range(0,10)) = 3
_GrassSliceHWid0("Grass Buttom0 Half Width Ratio",Range(0,0.5)) = 0.5
_GrassSliceHWid1("Grass Buttom1 Half Width Ratio",Range(0,0.5)) = 0.4
_GrassSliceHWid2("Grass Buttom2 Half Width Ratio",Range(0,0.5)) = 0.25
_GrassWaveSpeed("Grass Wave Speed",Range(0,10)) = 1
_GrassWavePower("Grass Wave Power",Range(1,5)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Cull Off
CGPROGRAM
#pragma target 4.0
#pragma vertex vert
#pragma fragment frag
#pragma geometry geom
#define PI 3.1415926
#include "UnityCG.cginc"
struct app2vert
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct vert2geom
{
float4 vertex : POSITION;
float3 normal : TEXCOORD0;
float3 tangent : TEXCOORD1;
};
struct geom2frag
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _GrassTex;
float _GrassWidth;
float _GrassHeight;
float _GrassSliceHWid0;
float _GrassSliceHWid1;
float _GrassSliceHWid2;
float _GrassWaveSpeed;
float _GrassWavePower;
//wave顶点动画
//根据vertical权重(越根部权重越小,不会摆动,越顶部权重越大,摆动越大)
float4 waveVertex(float4 vertex,float vertical)
{
float4 wavepos = float4(abs(sin(_Time.y*_GrassWaveSpeed))*_GrassWavePower*vertical,0,abs(cos(_Time.y*_GrassWaveSpeed))*_GrassWavePower*vertical,0);
vertex+=wavepos;
return vertex;
}
//随机数
//随便写的,怎么改都无所谓
float getRandom(float4 vertex)
{
float a = 125;
float b = 557;
float c = 9831;
float d = 5412;
float val = a*vertex.x+b*vertex.y+c*vertex.z+d*vertex.w;
return val%PI;
}
//以O原点为基准绕轴旋转
//先根据cvertex移动到O原点“附近”
//再绕local法向量旋转
//再平移到cvertex“附近”
float4 rotateAroundAxis(float4 cvertex,float4 vertex,float3 axis,float rad)
{
float n1 = axis.x;
float n2 = axis.y;
float n3 = axis.z;
float cosAngle = cos(rad);
float sinAngle = sin(rad);
float4x4 mataxis = float4x4(n1*n1 * (1 - cosAngle) + cosAngle, n1*n2*(1 - cosAngle) - n3*sinAngle, n1*n3*(1 - cosAngle) + n2*sinAngle, 0,
n1*n2*(1 - cosAngle) + n3*sinAngle, n2*n2 * (1 - cosAngle) + cosAngle, n2*n3*(1 - cosAngle) - n1*sinAngle, 0,
n1*n3*(1 - cosAngle) - n2*sinAngle, n2*n3*(1 - cosAngle) + n1*sinAngle, n3*n3 * (1 - cosAngle) + cosAngle, 0,
0, 0, 0, 1);
float4x4 mattoO = float4x4(1,0,0,-cvertex.x,
0,1,0,-cvertex.y,
0,0,1,-cvertex.z,
0,0,0,1);
float4x4 mattoC = float4x4(1,0,0,cvertex.x,
0,1,0,cvertex.y,
0,0,1,cvertex.z,
0,0,0,1);
vertex = mul(mattoC,mul(mataxis,mul(mattoO,vertex)));
return vertex;
}
vert2geom vert (app2vert v)
{
vert2geom o;
o.vertex = v.vertex;
o.normal = v.normal;
o.tangent = v.tangent.xyz;
return o;
}
geom2frag topoVert(float4 vertex,float2 uv)
{
geom2frag g2f;
g2f.vertex = vertex;
g2f.uv = uv;
return g2f;
}
void topoTri(float4 v0,float2 uv0,float4 v1,float2 uv1,float4 v2,float2 uv2,inout TriangleStream<geom2frag> tss)
{
tss.Append(topoVert(v0,uv0));
tss.Append(topoVert(v1,uv1));
tss.Append(topoVert(v2,uv2));
}
//拓扑构建grass的网格
void topoGrass(float4 vertex,float3 norm,float3 tan,inout TriangleStream<geom2frag> tss)
{
float3 localaxis = norm;
float4 localcenter = vertex;
float4 localvertex0 = vertex-float4(tan*_GrassWidth*_GrassSliceHWid0,0);
float4 localvertex1 = vertex+float4(tan*_GrassWidth*_GrassSliceHWid0,0);
float4 localvertex2 = vertex-float4(tan*_GrassWidth*_GrassSliceHWid1,0)+float4(norm*_GrassHeight,0);
float4 localvertex3 = vertex+float4(tan*_GrassWidth*_GrassSliceHWid1,0)+float4(norm*_GrassHeight,0);
float4 localvertex4 = vertex-float4(tan*_GrassWidth*_GrassSliceHWid2,0)+float4(norm*_GrassHeight*2,0);
float4 localvertex5 = vertex+float4(tan*_GrassWidth*_GrassSliceHWid2,0)+float4(norm*_GrassHeight*2,0);
float4 localvertex6 = vertex+float4(norm*_GrassHeight*3,0);
float4 vertex0 = waveVertex(UnityObjectToClipPos(rotateAroundAxis(localcenter,localvertex0,localaxis,getRandom(localvertex0))),0);
float4 vertex1 = waveVertex(UnityObjectToClipPos(rotateAroundAxis(localcenter,localvertex1,localaxis,getRandom(localvertex0))),0);
float4 vertex2 = waveVertex(UnityObjectToClipPos(rotateAroundAxis(localcenter,localvertex2,localaxis,getRandom(localvertex0))),0.33);
float4 vertex3 = waveVertex(UnityObjectToClipPos(rotateAroundAxis(localcenter,localvertex3,localaxis,getRandom(localvertex0))),0.33);
float4 vertex4 = waveVertex(UnityObjectToClipPos(rotateAroundAxis(localcenter,localvertex4,localaxis,getRandom(localvertex0))),0.67);
float4 vertex5 = waveVertex(UnityObjectToClipPos(rotateAroundAxis(localcenter,localvertex5,localaxis,getRandom(localvertex0))),0.67);
float4 vertex6 = waveVertex(UnityObjectToClipPos(rotateAroundAxis(localcenter,localvertex6,localaxis,getRandom(localvertex0))),1);
float uvlen = 3*_GrassHeight;
float uvhflen = 1.5*_GrassHeight;
float2 uv0 = float2((uvhflen-_GrassSliceHWid0*_GrassWidth)/uvlen,0);
float2 uv1 = float2((uvhflen+_GrassSliceHWid0*_GrassWidth)/uvlen,0);
float2 uv2 = float2((uvhflen-_GrassSliceHWid1*_GrassWidth)/uvlen,0.33);
float2 uv3 = float2((uvhflen+_GrassSliceHWid1*_GrassWidth)/uvlen,0.33);
float2 uv4 = float2((uvhflen-_GrassSliceHWid2*_GrassWidth)/uvlen,0.67);
float2 uv5 = float2((uvhflen+_GrassSliceHWid2*_GrassWidth)/uvlen,0.67);
float2 uv6 = float2(0.5,1);
topoTri(vertex0,uv0,vertex1,uv1,vertex3,uv3,tss);
topoTri(vertex0,uv0,vertex3,uv3,vertex2,uv2,tss);
topoTri(vertex2,uv2,vertex3,uv3,vertex5,uv5,tss);
topoTri(vertex2,uv2,vertex5,uv5,vertex4,uv4,tss);
topoTri(vertex4,uv4,vertex5,uv5,vertex6,uv6,tss);
tss.RestartStrip();
}
[maxvertexcount(36)]
void geom(triangle vert2geom vg[3],inout TriangleStream<geom2frag> tss)
{
for(int i=0;i<3;i++)
{
float4 localvertex = vg[i].vertex;
float3 localnorm = normalize(vg[i].normal);
float3 localtan = normalize(vg[i].tangent);
topoGrass(localvertex,localnorm,localtan,tss);
}
}
fixed4 frag (geom2frag i) : SV_Target
{
fixed4 col = tex2D(_GrassTex,i.uv);
return col;
}
ENDCG
}
}
}
效果如下:
这样就添加了随机生成、摆动、uv贴图效果,稍微生动一些。
ps:上面的shader建议不要直接使用,因为效率不高,只是写博客说明原理的示例。如果小伙伴有草地渲染的需求,建议修改,修改方向如下:
1.使用noise纹理代替getRandom产生随机radian
2.使用c#传入uniform vector[] uvs代替uv0-uv6的计算,节省GPU的tflpos,因为uvs是固定的
3.使用c#传入uniform vector[] worldvertexoffset,然后根据vert中worldvertex和worldvertexoffset计算(简单的加运算)得到worldvertex0-worldvertex6,这样就避免了TopoGrass时候大量的矩阵浮点运算