Unity引擎提供了基础的terrain工具,可以制作地形,在上面刷树刷草。对于树,Unity是支持带LOD的Prefab,不同距离显示不同细节的模型,效果还不错。对于草,Unity支持两种方式来刷草,一种是Add Grass Texture,一种是Add Detail Mesh。第一种实际上就是放一张图片,可以支持billboard,目的是营造一片草的错觉,这个只能远看,但是离得近,就穿帮了,尤其是在俯视的情况下,如下图。第二种是Add Detail Mesh,类似刷树,但是不支持LOD。
- 面片:最传统的一种做法,用一片方形的网格贴一张草的贴图插在地上就行,原神里也可以看到用面片表现的小黄花。缺点很明显,俯视的时候穿帮很严重
- CPU生成网格:其实就是想办法用代码绘制点线面拼出一棵(或者一大片)草。相比于3d软件中建模再导入,用代码画的草在体现摇摆动画的时候可能会方便些,不像建模出来的草想摇摆的话可能还得k个形态键啥的。https://github.com/ECHibiki/WindSway-HDRP
- VIsualEffectGraph这方案也是在Github上偶然看到的,而且它好像还上架unity商店了,如果是用HDRP来做游戏的话可以直接用:https://github.com/SirMishMash/Unity-StylizedGrass
- VEG粒子
- 几何着色器
1.使用面片+贴图 性能好但效果不好
2.使用模型草,用GPUInstance来做渲染优化,并且使用ComputeShader做剔除,再结合Lod做分级。
草的着色
基于现实的观察,草的光照主要收以下光照模型影响:
1.漫反射
2.高光
3.透射
4.次表面散射
事实上,任何你不知道怎么去做光照模型的渲染,都可以尝试将这光照渲染四兄弟用上去。比如头发,水,树,云,雪,冰等等。接下来就是怎么具体去实现这四兄弟。
单个草有自己的光照表现,而多个草组成草堆,草堆也有自己的光照表现。在做草的光照时,需要考虑到,局部的“单草” 与 整体的“草丛”。对于单草部分,使用单个草的法线做光照计算;对于草丛部分可以使用地形的法线来近似草丛的法线。总体的思路是,高光用来表现草个体的差异性,漫反射 透射 用来表现草丛的整体性。不同的部位,不同的光照方向,可以营造出“体积感”,有局部也有整体的效果是符合美术感官的。
对于草的观察,我们可以做出以下假设:
越年轻的草,水越多,颜色更鲜艳,重量大,弯曲程度小,摇晃小,光照越丰富(漫反射+高光+透射+SSS)。
越老的草,水越少,颜色扁暗,重量轻,弯曲程度大,摇晃大,光照越简陋(越多的漫反射)。
因为草颜色偏绿色块 即使在VS里计算光照模型 与PS里计算出的效果也基本一致,所以可以做一个优化:直接在VS里计算光照模型。因为草的顶点数量比较少,这可以节省大量的计算资源。这是一个基于美术观察的优化Trick。
具体实现
漫反射
对于“草”这方面,使用PBR光照模型的草效果不好。其在于漫反射是Lambert,在暗部就是一片死黑,即使换成了DisneyDiffuse效果也没能更好。为了达到风格化的效果,我们可以借鉴“日式卡通渲染”中漫反射的Trick表现。
float NL = dot(N,L);
float v1 = NL+1;
float v2 = NL;
float3 D1 = lerp(DiffuseColorLow,DiffuseColorMid,v1);
float3 D2 = lerp(DiffuseColorMid,DiffuseColorHigh,v2);
float3 DiffuseGrass = lerp(D1,D2,NL>0);
高光
PBR高光GGX三兄弟DFG中的G项被我干掉,因为G项本质上就是将高光在暗部裁剪掉,但是由于草非常薄,光可以直接透过,所以对于G项来说,不能把草当成球直接将高光暗部裁剪!但是如果不做任何处理,又会导致满屏的高光,基于现实与美学上的考量,越靠近人眼处,高光越弱,越远的地方,高光可以越强。我们可以重构一个G项,叫做CameraFade,根据到相机的距离衰减高光。
float GGX_DistanceFade(float3 N,float3 V,float3 L,float Roughness,float DistanceFade)
{
float3 H = normalize(L+V);
float D = D_DistributionGGX(N,H,Roughness);
float F = F_FrenelSchlick(saturate( dot(N,V)),0.04);
float G = G_GeometrySmith(N,V,L,Roughness);
return D*F*DistanceFade;//Kill G for more natural looking
}
用单草模型的顶点法线做 初级高光表现局部,草的高光在草尖端比在底部更明显。
用草丛(地形法线)做次级高光表现整体。
草丛+单草高光(Width DistanceFade)
高光波瓣
一层高光看起来缺少细节层次感,那么就多加几层,Siggraph上的大佬管这种方式叫做高光波瓣,那么我们可以使用多层不同水分的高光相叠加,但要保证能量守恒。我们可以构建出如下高光波瓣叠加方式:
float3 g1 = GrassSpecular(GrassWater)
float3 g2 = GrassSpecular(GrassWater*0.5)
float3 g3 = GrassSpecular(GrassWater*0.5*0.5)
float3 grassSpecular = g1*0.5+g2*0.3+g3*0.2;//高光能量守恒
能量守恒
我们可以使用GrassWater来调合,这符合我们前面的所讨论的,草水份越多光照表现越丰富,否则就更偏向于漫反射。艺术家可以预设不同水分的草,从而使一片草原中,草的光照表现更丰富。
FinalColor = lerp(Diffuse,Specular+Transmission+SSS,GrassWater);
草的AO
通过周围草的数量来决定该草AO信息,而不是通过后处理深度与法线来计算AO!
为此可以构造出一个二维高斯采样算子(或其他算子),快速计算出AO信息。离线计算出AO信息后直接写入到草的Data中,读取的时候就可以不占用计算资源。
透射
单草很薄,但是很多草组合起来又很厚。因此透射对于这两种情况都要考虑到。AO信息也包含着一块区域草的“密度”,如果越密那么意味着,光越少会透过该区域。因此在计算透射的时候也要将AO信息考虑进去。
float SimpleTransmission(float3 N,float3 L,float3 V,float TransLerp,float TransExp,float TransIntensity,float ThicknessFade)
{
float3 fakeN = -normalize(lerp(N,L,TransLerp));
float trans = TransIntensity * pow( saturate( dot(fakeN,V)),TransExp);
return trans*ThicknessFade;
}
草的次表面散射
Siggraph上的大佬说,做次表面散射需要考虑三点:
- -大曲率处的SSS:
- -小曲率处的SSS
- -阴影处的SSS
1.大曲率处的SSS
草的曲率可以用地形的曲率来近似。那么用NL以及曲率采样Lut图就可以近似地模拟出草的SSS,但是草不需要像皮肤那么精细,我们在实现草的漫反射时,其实已经将这一步给近似模拟了,因为NL小于零的部分可以自定义出透过的颜色。
2.小曲率处的SSS
小曲率处的SSS在皮肤渲染中是直接用模糊法线贴图来解决。在草地渲染上我们也可以模糊地形的法线来近似模拟草丛的模糊法线。这一步可以离线在TerrainData里预处理好并且保存为草的数据。在使用时可以直接使用这个插值来模拟草SSS的程度
float3 GrassNoraml = lerp(TerrainNoraml,TerrainBlurNormal,sssIntensity);
3.阴影处的SSS
因为草地阴影处的SSS其实并没有皮肤那么明显,因此我们忽略这项处理。
草的弯曲
草的弯曲程度我们做出以下假设
1.取决于地形
草的弯曲程度,取决于地形的法线。
2.取决于周围草的高度与密度
如果一个草它周围有很高很密的草存在,那么意味着该草得到的阳光会更少,为了获取到更多的阳光,它会横向生长而不是纵向发展。横向生在的越长那么越容易摇晃。
3.取决于生长素
根据高中所学的生物知识,植物的生长素是由尖端产生,适量的生长素有助于生长,过量的生长素抑生长。被抑制部分比较矮。越靠近主杆部分,摇晃程度小,反之则摇晃程度大。在代码实现上,我们只需要根据顶点的Y值以及该顶点到坐标中心的XZ距离计算粗摇晃度即可。
可能还需要:
与地形表面进行颜色混合(支持平铺设置和网格地形)
风动画
透视校正,实现从上而下的最佳覆盖视觉效果