3评论

移动端大规模草渲染的实现

/yx日音/hanx 2018-11-06 1.9k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏开发行业精英群711501594

先上最终效果:

在群里经常看到草的问题,也会和他们一起讨论。结果现在不得不自己也开始做了,就想把一些东西给汇总一下, 然后看做出来效果如何。

按照做拼接地表的经验,我一开始就打算使用程序生成网格,通过尽量多的顶点去做,尽量不适用alpha test.原因在于使用了alphatest之后,early z就 无效了,但对于草来说,early z应该是特别重要的可以提升性能的点。

不过为了看效果,一开始还是用最简单的方式,用程序生成了一个大网格,放上贴图,看上去是这样:


首先在手机上测试了一下,法线帧率堪忧,如果视角再平一点,估计就卡了,目前这样子大概有8w个面,还可以接受。因为后面本来就有优化策略,所以打算还是继续先做效果。草肯定不会这么均匀,我先打乱下草的分布。

感觉有点差强人意,真实的草不应该是这么杂乱,而且打乱之后变稀疏了,就更丑了。调整下疏密度,稍微舒服了点, 这里觉得颜色太单一,但又不想通过额外的参数来控制颜色变化,想了一下其实可以通过顶点色或者顶点索引。顶点索引需要在shader里多几步计算,考虑到性能,还是先往顶点色里塞,如果后续需要做其他东西,我觉得可以通过uv2来塞额外数据。



考虑到自然风,打算模仿一个dx 草下面的一个算法:

//用位置信息作为随机种子
#ifdef GRASS_OBJECT_MODE
	float3 randCalcPos = p[0].objectSpacePos;
#else
	float3 randCalcPos = oPos;
#endif
进一步获得xz的随机
fixed randX = rand(randCalcPos.xz + 1000) * _Disorder * 2 - _Disorder;
fixed randZ = rand(randCalcPos.xz - 1000) * _Disorder * 2 - _Disorder;
//Random value from 2D value between 0 and 1
inline float rand(float2 co){
	return frac(sin(dot(co.xy, float2(12.9898,78.233))) * 43758.5453);
}
//If grass is looked at from the top, it should still look like grass
#ifdef GRASS_TOP_VIEW_COMPENSATION
	fixed topViewCompensation = 1 + pow(max(0, dot(viewDir, up)), 20) * 0.8;
	width *= topViewCompensation;
	fixed2 windDir = wind(randCalcPos, fixed2(randX, randZ) * (topViewCompensation));
#else
	fixed2 windDir = wind(randCalcPos, fixed2(randX, randZ));
#endif
inline fixed2 wind(float3 pos, fixed2 offset)
{
	float3 realPos = float3(pos.x * cos(_WindRotation) - pos.z * sin(_WindRotation), pos.y, pos.x * sin(_WindRotation) + pos.z * cos(_WindRotation));
//这个是绕原点旋转之后的新坐标,具体推倒可以参考https://blog.csdn.net/u012138730/article/details/80320162
	fixed2 windWaveStrength = _WindParams.x * sin(0.7f*windStrength(realPos)) * cos(0.15f*windStrength(realPos));
	windWaveStrength += windRipple(realPos);
	fixed2 wind = fixed2(windWaveStrength.x + offset.x, windWaveStrength.y + offset.y);
	return fixed2(wind.x * cos(_WindRotation) - wind.y * sin(_WindRotation), wind.x * sin(_WindRotation) + wind.y * cos(_WindRotation));
}
//这个风力函数比较复杂,用四个正余弦函数弄出的一个波动效果
inline fixed windStrength(float3 pos)
{
	return pos.x + _Time.w*_WindParams.y + 5*cos(0.01f*pos.z + _Time.y*_WindParams.y * 0.2f) + 4*sin(0.05f*pos.z - _Time.y*_WindParams.y*0.15f) + 4*sin(0.2f*pos.z + _Time.y*_WindParams.y * 0.2f) + 2*cos(0.6f*pos.z - _Time.y*_WindParams.y*0.4f);
}
inline fixed windRippleStrength(float3 pos)
{
	return sin(100*pos.x + _Time.y*_WindParams.w*3 + pos.z)*cos(10*pos.x + _Time.y*_WindParams.w*2 + pos.z*0.5f);
}
inline fixed2 windRipple(float3 pos)
{
	return _WindParams.z * fixed2(windRippleStrength(pos), windRippleStrength(pos + float3(452, 0, 987)));
}


//lod是这个草要分成几段,是曲面细分那边的内容
for(fixed i = 1; i <= lod; i++)
{
	fixed segment = i*invLod;
	fixed sqrSegment = segment*segment;
        //segment是草的长度的百分比,pos就是最终的实际高度
	float3 pos = float3(up*segment*realHeight);
        //xz要加上风所带来的影响,
	pos.xz += windDir.xy * sqrSegment * stiffnessFactor;
        //高度也要矫正
	pos.y  -= length(windDir) * sqrSegment * 0.5f * stiffnessFactor;
	fixed uvHeight = segment;
	viewDir = normalize(rendererPos - pos);
	fixed3 localUp = pos - lastPos;
	//Simple grass has no texture, so the mesh has to look like a blade of grass
	pIn.vertex =  float4((pos - width * groundRight * (1 - sqrSegment)).xyz, 1);
	getNormals(localUp, lightDir, groundRight, /*out*/ pIn.normal, /*out*/ pIn.reflectionNormal);
	triStream.Append(geomToFrag(pIn));
	//Simple grass has no texture, so the mesh has to look like a blade of grass
	pIn.vertex =  float4((pos + width * groundRight * (1 - sqrSegment)).xyz, 1);	
	getNormals(localUp, lightDir, groundRight, /*out*/ pIn.normal, /*out*/ pIn.reflectionNormal);
	triStream.Append(geomToFrag(pIn));
	lastPos = pos;
}


最终我只采纳了windStrength的部分,ripple部分在面数不够的情况下,草扭曲的样子不是很好看。



然后开始优化性能,首当其中的就是alpha test,因为为了风场动画,顶点本来就不少了,于是干脆直接用硬边。

性能从10帧跳到24帧,确实提高很多,硬边也还可以接受,然后发现另一个问题,就是手机上跑久了很抖,应该是数值过大引起的,需要将大数值取余。这样做了之后基本可以达到要求了。暂时先不考虑性能的进一步优化,因为我发现是我的手机太差了,换了台手机其实帧率可以到60帧。我更想处理的是和草的交互,因为图中这个球其实是压着草的,但因为没有交互,看上去很奇怪。

但在这之前还是需要处理一个问题,就是草是单面的,从其他方向看就会是一条线,一般是用billboard技术,但对我这种做法却不适用,因为billboard的重点是中心点,而我这么一大片网格,中心点和草是没啥关系的。于是我就换了一种做法,堆了两层草。用十字星的形式,这样的坏处是增加了面数,但好处也很明显,让整个草看上去更加自然。

接下来要处理交互了。

想了一下方案,首先草根据对应rt里的坐标读取像素,根据像素确定歪的方向,并且做歪曲。那么如何得到这张rt呢?

第一步是球的正面下压,我的想法是根据不同朝向,分别转成颜色值并显示出来,最终渲染到rt中。

shader代码如下:

v2f o;
float4 vertex = v.vertex;
o.vertex = UnityObjectToClipPos(v.vertex);
o.pos = mul(unity_ObjectToWorld, v.vertex);
float r = v.vertex.x + 0.5;
float b = v.vertex.z + 0.5;
float len = length(v.vertex.xz * 2);
o.color.r = r;
o.color.g = pow(1 - len, 1);
o.color.b = b;
o.color.a = 1;
return o;


然后是运动轨迹,原理一样,只不过需要通过运动动态创建网格,效果如下:

这样rt就是用摄像机从正上方去看它,一切都很完美了。

实际测试的时候发现一个大问题,就是如果草只有一部分顶点被压住,那么草会扭曲成很难看的样子,解决方案是把草的位置浓缩成一个点,草上面的所有顶点都用浓缩的这个点来判断,这样草的位置就一致了。

 v2f vert(appdata v, uint vid : SV_VertexID)
	            {
			v2f o;
			float index = floor(vid / ((_GrassSeg + 1) * 2));
			float grid = _GrassRange / _GrassNum;
			float row = floor(index / _GrassNum);
			float col = index % _GrassNum;
			float4 objectPos = float4(-_GrassRange / 2 + row * grid, 0, -_GrassRange / 2 + col * grid, 1);
			float4 worldPos1 = mul(unity_ObjectToWorld, objectPos);
			float4 vertex = v.vertex;
			float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
			float3 randCalcPos = worldPos;
			float2 windDir = wind(randCalcPos);
			float4 grassuv1 = mul(GrassMatrix, worldPos1);
			float2 grassuv2 = grassuv1.xy / grassuv1.w * 0.5 + 0.5;
#if UNITY_UV_STARTS_AT_TOP
			grassuv2.y = 1 - grassuv2.y;
#endif
		        float4 n = tex2Dlod(_GrassTex, float4(grassuv2, 0, 0));
			n.xz = (n.xz - 0.5) * 2;
			float2 off = (windDir.xy) * pow(v.vertex.y * 2, 2);// +n.xyz;
			worldPos.y -= 10 * dot(off, off);
			float2 newOff = off * (1 - n.g) + normalize(n.xz) * v.vertex.y * n.g * 0.6;
			worldPos.y *= (1 - n.g);
			//off = float2(-0.3, 0) * v.vertex.y;
			worldPos.xz += newOff;
			o.pos = mul(UNITY_MATRIX_VP, float4(worldPos, 1));
			//o.pos = UnityObjectToClipPos(vertex);
			o.uv = v.uv;
			half3 worldNormal = UnityObjectToWorldNormal(v.normal);
			half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
			o.diff = nl * _LightColor0;
			o.diff *= v.color;
			o.diff.rgb += +ShadeSH9(half4(worldNormal, 1));
			TRANSFER_SHADOW(o)
			return o;
		}


最后是插件地址:

https://www.assetstore.unity3d.com/en/?stay#!/content/132241