上帝曾经说过:“要有光!"。
鲁迅曾经说过::"我草!"。
我就在想当“光”遇到了“草”会发生什么?于是我翻开谷歌一查,这资料都大体相同,每个网页上都写着“草的优化” 四个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着五个字是“自己造轮子”!于是第二天早上,我早早地到了工位,并在桌子上刻了一个“草”。
工作几年,我总结出了渲染中的三定律,这对于解决问题非常有帮助
1.看起来是对的,就是对的。
2.由于性能原因这个效果做不了。
3.我后面会优化的。
1.使用面片+贴图 性能好但效果不好
2.使用模型草,用GPUInstance来做渲染优化,并且使用ComputeShader做剔除,再结合Lod做分级。
Unity地形中默认的草,没有使用GPUInstance优化,因此有必要做一个自定义的草渲染管理,否则当草数量多时就只能跑几帧。
在Unity地形中刷草,使用TerrainData获取草的位置信息,再DrawMeshInstancedIndirect手动画草的模型。有了草地之后就可以开始愉快地做草的渲染了。
C4D中做的单个草模型,草需要朝向各个方向,有不同的弯曲度,高度,以及大小 形态,如此能增加生动性。
基于现实的观察,草的光照主要受以下光照模型影响:
1.漫反射
2.高光
3.透射
4.次表面散射
事实上,任何你不知道怎么去做光照模型的渲染,都可以尝试将这光照渲染四兄弟用上去。比如头发,水,树,云,雪,冰等等。接下来就是怎么具体去实现这四兄弟。
单个草有自己的光照表现,而多个草组成草堆,草堆也有自己的光照表现。在做草的光照时,需要考虑到,局部的“单草” 与 整体的“草丛”。对于单草部分,使用单个草的法线做光照计算;对于草丛部分可以使用地形的法线来近似草丛的法线。总体的思路是,高光用来表现草个体的差异性,漫反射 透射 用来表现草丛的整体性。不同的部位,不同的光照方向,可以营造出“体积感”,有局部也有整体的效果是符合美术感官的。
以下是草的光照模型组合表:
根据光照渲染第一定律,我们需要让光照看起对,那么就得从人类已知的经验出发,对草从感性的认知转为具体的实现目标。
不同种类的草,在不同生命周期中,它们的“水份”是不同的,因此可以根据这个“水份”来做光照模型的区分。
对于草的观察,我们可以做出以下假设:
越年轻的草,水越多,颜色更鲜艳,重量大,弯曲程度小,摇晃小,光照越丰富(漫反射+高光+透射+SSS)。
越老的草,水越少,颜色扁暗,重量轻,弯曲程度大,摇晃大,光照越简陋(越多的漫反射)。
因为草颜色偏绿色块 即使在VS里计算光照模型 与PS里计算出的效果也基本一致,所以可以做一个优化:直接在VS里计算光照模型。因为草的顶点数量比较少,这可以节省大量的计算资源。这是一个基于美术观察的优化Trick。
漫反射
使用PBR光照模型的草,其结果看起来非常阳痿!因为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
}
用单草模型的顶点法线做 初级高光表现局部,草的高光在草尖端比在底部更明显。
用草丛(地形法线)做次级高光表现整体。
单草高光 (SceneView)
草丛的高光
单草的高光
即使在暗部,也能看到光透过草的高光,而不是一片死黑。
草丛+单草高光(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;//高光能量守恒
能量守恒
注意我们加了很多光照计算进来,如果不考虑能量能量守恒,那么计算出的光照结果就会过爆,爆了之后又用参数去压暗,会导至参数数量过多,然后艺术家就会抱怨,你写的东西“太复杂”,难用!在PBR中漫反射与高光的调合方式是使用菲尼尔 即:
FianlColor = lerp(Diffuse,Specular,Fresnel);
但由于我们做草时破坏了GGX中的G项,菲尼尔强行用Fresnel调和效果不太好,但我们可以使用GrassWater来调合,这符合我们前面的所讨论的,草水份越多光照表现越丰富,否则就更偏向于漫反射。艺术家可以预设不同水分的草,从而使一片草原中,草的光照表现更丰富。
FinalColor = lerp(Diffuse,Specular+Transmission+SSS,GrassWater);
草的AO
通过周围草的数量来决定该草AO信息,而不是通过后处理深度与法线来计算AO!
为此可以构造出一个二维高斯采样算子(或其他算子),快速计算出AO信息。离线计算出AO信息后直接写入到草的Data中,读取的时候就可以不占用计算资源。
简单的例子:这草的 AO=7/9=0.78
草的AO显示 SceneView,可以看出越稀的地方越黑(1-AO显示)
草的AO显示GameView
透射
单草很薄,但是很多草组合起来又很厚。因此透射对于这两种情况都要考虑到。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距离计算粗摇晃度即可。
目前草的顶点摆动我只是使用了一张4kb的噪音贴图,根据光照渲染第三定律,这个东西后面还会有更好的办法。
最后放几张渲染图
(中间放一个球是为了方便预览光照对比)
草的效果还有很大的优化空间,比如结合gpu instance,程序化草,gpu culling 才能算是完整的草方案,目前的聚焦于试验性效果