重写引擎的草渲染的原因
1. 效果图
2. 为什么决定重写引擎的草渲染
我的项目立项较早引擎版本为Unity 5.6,这里探讨Terrain自带草的问题,以及我一一对应的解决方案。以下的问题不是每个都必须重写渲染机制能力解决,但次要的几个需要就决定重写了。
- 法线问题,不带Mesh法线,用的是地表法线作为渲染法线(因为Mesh或Billboard都会用插片来做,不传法线贴图,面数限度下不可能正确表白草叶子的实在法线方向,这种模式能够根本保障光照不发黑,地表根本处于能够被阳光找到的半球)。
- 动静Mesh问题,个别选Collect Detail Patches运行时CPU动态创建一堆Mesh,即有顶点数下限,又会引起卡顿(引擎源码批改成异步能够缩小卡顿但不缩小计算量会发热,PC发热则让一些机器CPU降频)。
- Hi-Z低效问题,动态创建一堆Mesh。无奈最高水平剔除每个不渲染的草,不能最大施展Hi-Z剔除性能。
- PBR材质问题,因为没有Smooth贴图无奈实现PBR渲染,导致材质成果十分差(这里独自传图也是能够实现)。
- 淡入淡出问题,因为不反对淡入淡出,所以120米外草的隐没与呈现无奈做到PUBG那样的抖动半透明过渡。
- LOD问题,自带的草不反对LOD,改引擎也不好实现,因为合并Mesh渲染的,所以无奈逐棵切换LOD。
- Shadowmask问题,提早渲染下,草会获取地形的Shadowmask进行计算到G-buffer的RT4,这里能够省略优化。
- Shadowmap问题,草无奈投递暗影。
- AO问题,SSAO诚然能够减少一点草的AO,但那个精度体现的成果不显著且成果不好。
- 草的成长方向问题,植被尽管是应该反重力成长,但游戏中的草一片表白的是一小堆草,当这些草很矮时,心愿草的成长方向按地表方向成长,相似苔藓。这样做能够无效防止底部与地形切出一条直线的问题,看起来更贴合高空。
- Mipmap问题,草在远处,惯例的Mipmap计算会导致剔除过多而提前隐没。应该应用间隔场贴图。
- PreZ效率问题,PreZ阶段只须要透明度信息,采样因Albedo精度须要而创立贴图性能不是最省,须要独立的Alpha图。
解决方案
1. 法线计划
下图中上半局部是Unity法线策略,导致画面较平,下半局部是半球Mesh法线。树和草常常会用半球法线(原理不再赘述),间接用Mesh法线须要较粗疏的建模或法线贴图并实现双面渲染的带简略透射的光照逻辑(即法线取反再算一次亮度)。因为我是采样GPU Instance绘制,所以顶点自带了法线,如果要沿用Terrain的渲染草形式,能够把草顶点组成Buffer传Shader,而后依据VertexID来取。
近处须要法线是Mesh法线,远处须要用地形法线作为草的法线,这样能够让草与底部在远处更融为一体,所以须要对Normal做过渡。
2. Mesh与Hi-Z计划
Mesh采纳ComputeShader+GPU Instance绘制,每帧给ComputeShader传入四周7x7虚构网格草的数据(一个格子40米,这样能够做到120米都有草),只传发生变化的格子,一帧传一个格子。ComputeShader每帧对这些小范畴的草进行间隔裁剪、LOD判断、视锥裁剪与Hi-Z裁剪。实现最大水平剔除。这里具体做法有之前文章介绍。
《Compute Shader 进阶利用:联合Hi-Z 剔除海量草渲染》
3. PBR材质计划
因为草金属度都为0,所以只有传Smooth贴图,把它放在Albedo的a通道,因为它总是和 Albedo同一个Pass被拜访。而且只有 Alpha>0像素的Smooth数据才有意义,而草个别用Alphatest,所以须要的透明度只有不是0即可。所以能够压缩在同一个a通道,前面提到a会独自存储。传了Smooth因为提早渲染是对立的前期Lighting,所以这里不须要写光照,只须要在dot(normal,lightDir)<0的中央加一点自发光模仿背光面亮度防止发黑。
下图中上半局部是Unity的草对立Smooth,下半局部是Smooth贴图在a通道,成果比照不是特地显著。这里能够去看结尾美术调的PBR效果图。
4. 淡入淡出计划
仔细观察其余游戏成果,发现用抖动半透明(Dithering Alphatest) 能够很好地实现草的呈现与隐没成果,而Unity自带的Alphatest也会因为间隔越远裁剪面积比就越大。扣除边缘比例减少来做的隐没,就相似缩放,成果必定不如挖一堆小孔(Dithering )好,Dithering的变动更平均,然而在性能上小小取胜,倡议草的可视间隔在100米内的采纳Dithering ,反之太远了看不清可用Unity这套。但有一种成果与性能均衡做法能够对Clip参数做个依据间隔变动的函数。比方:
clip(a - lerp(_cutout, 1, smoothstep(hzbGrassDistance - 5, hzbGrassDistance, disCmr))); 代https://blog.uwa4d.com/admin/write-post.php#wmd-preview替简略clip(a-_cutout).
比照成果:
这是引擎间接Clip成果
会呈现一条显著切线
这是模拟PUBG罗马之子等抖动半透明成果
进行时候可看到纱窗效应
这是性能更好利用间隔动静计算Clip值的后果
但通明区域先隐没,经常是顶部先隐没,所以成果不如抖动半透明
抖动半透明淡入淡出算法
5. Shadowmask计划
这个版本的GPU Instance是不带Shadowmask的,草也是不参加烘焙的,因为它的Lightmap、Shadowmask和贴着的地表十分统一,间接取对应地表的这2个图采样即可。然而这一步能够简化算法,不做任何采样。提早渲染的Shadowmask逻辑是这样的,烘焙的对象会采样Shadowmask图,并记录屏幕空间的Shadowmask到G-buffer的RT4,示意本人是否在暗影里,动静对象如果没Lightprobe,会绘制红色示意本人不在任何暗影里(因为没通过烘焙提前计算好,零碎不晓得其在不在暗影里,所以一律疏忽暗影),但如果存在Lightprobe,那么会依据左近的Probe是否在暗影里来插值计算。同样写到RT4。如果在我的项目中用到Bakey烘焙,那么对于是否让Probe烘焙遮挡信息是可选的,若不勾上就会失落Mask数据。
接下来用案例解释:
下面这张是简略烘焙的场景,带了Lightmap、Shadowmask和Lightprobe。它们是怎么表白本人在暗影里的呢?
1.渲染完底板、墙壁和小方块这3个动态物体,它们会读取烘焙的Shadowmask来写到RT4 如下图,咱们只有一个带Shadowmask烘焙的光所以只看红色通道。这里表白很分明,彩色为暗影内区域。
2.渲染小球,把它想成一堆草贴地摆放(下图),能够看到是青色(这里只探讨红色通道),截图可知是0,这套就是Unity对草的Shadowmask计算逻辑。
3.兴许你会发现一个问题:草可不可以不去写RT4呢?让地表彩色的区域仍然放弃彩色就好,最初屏幕空间计算光照时,反正都是要用平行光计算光照前获取这个像素的Mask值当作Atten用,当参加Shadowmask烘焙的只有一个平行光的时候,青色和彩色就齐全是一个后果。或者说,不须要晓得草绘制哪些区域,如果绘制了,取草背地(绝大部分都是地表)的Mask作为本人是否在暗影里的判断。这样对于海量的草即便少了这个采样仍然有肉眼可见的晋升的。
4.那么这样省略有没有问题?当然有。Unity不会白白多计算一次的,它须要思考齐全正确性,所以性能不敢省,咱们本人的我的项目能够依据理论状况取舍。这里有1个小问题,当咱们从侧面看上坡上草的时候,草的背景是天空或远方就有问题了。这时候它的前面间接用的Mask不再是地表了,会让明明在暗影里的草显得和阳光下一样亮。如下图红框内的球体局部,如果取前面的红色,示意不在暗影里,如果本人采样计算并写入RT4,就是青色(红色为0)示意在暗影里。然而Shadowmask产生在实时暗影外,少说也有70到120米,所以那时候你了解为侧面的草的透光也曾经十分不显著了,须要看我的项目接受程度。
5.如果你承受了这点瑕疵,防止了Shadowmask的采样开销,那么要如何不写RT4呢?frag/vert 模式的Shader很简略,把out float4 RT4的写入间接正文掉即可。如果是surface shader则更简略,#pragmasurface surfaceFunction lightModel noshadowmask 即可。
6.开展聊一聊Shadowmask,Unity的Shadowmask有个很乏味的特点,它是一个超级宅男,能够轻易地让3年、5年、7年、10年Unity程序、主程序、甚至TA都认不全它。先说什么叫Mask,个别渲染里可能间接查图就晓得本人是不是某个属性,或是多少这种属性的叫Mask,所以Shadowmask表白是不是在暗影里。而Shadowmap不是记录是否在暗影里,而是记录相机空间深度,失去深度不是表白是否在暗影里所以并不算Mask。
对于Unity 5.6,我所晓得的至多有4种Shadowmask,有学习脱漏的同学能够依据这个查问具体内容,补上常识空缺。
- 烘焙出动态物体Shadowmask贴图
- 烘焙出给动静物体用的,利用Lightprobe记录Shadowmask插值信息
- 平行光的Shadowmap独自转换的Screenspaceshadowmask
- G-buffer中RT4 全屏动静动态都参加计算的Shadowmask
以下是反对Shadowmask前后比照:
没有Shadowmask反对的草在实时暗影间隔外成果
实现Shadowmask反对的草在实时暗影间隔外的成果
6. Shadowmap与Contact Shadow计划
因为用了GPU Instance来绘制草,这里比较简单,能够让Graphics.DrawMeshInstancedIndirect 分3次调用:
- 30米内的Instance,选CastShadow为true的参数,提交MeshLOD0。
- 30米外LOD Distance内的Instance,选CastShadow为false的参数,提交MeshLOD0。
- LOD Distance外的Instance,选CastShadow为false的参数,提交MeshLOD1。
以下是开启暗影与否的比照图,个别做个暗影浓度越远越衰减为好,衰减形式能够是ShadowCast pass里Clip值的变动。
Unity引擎不反对投递暗影的成果
本人实现Shadowmap后成果
Shadowmap是用来实现对地表的投影,但因为原理与精度的限度,用来体现细小叶片的自暗影成果并不好,这方面可采纳ContactShadow补救。而对于ContactShadwo可参考:
《超过育碧品质的接触暗影(contact shadow)》
Shadowmap+ContactShadow成果
7. AO计划
草的AO计划很少被提及,后面提到SSAO/HBAO+不能精密表白好草和四周地表的AO及其它色彩变动。长草的土与不长草的土,每天受到的日照并不同,所以它们的色彩或轻微植被状况也不同。而这不适宜做在地表贴图上,因为同一种地表有局部长草、有局部不长草。所以为了这个辨别,同时为了模仿光线进入草堆后各种衰减的模仿,单个阳光对植被自身的衰减能够用植被的半球法线简略模仿,但这份衰减也应该让地表变暗。这个计算就往往被抛弃了。想了一种简略的做法,就是离线统计草的密度,依据不同密度生成一张地表AO图。相似另一张Shadowmask,然而不同于Shadowmask,它在实时暗影范畴内也有成果。为了显著点,把参数调夸大后成果如下图:
依据草密度高度换算成光线衰减水平的图并在PS里加个高斯含糊
https://www.youku.com/video/X...
8. 草的成长方向计划
草的成长方向后面提到须要依据植被来独自设计,是偏差重力反方向;还是偏差地表方向。个别瘦高的(少数单子叶动物)适宜反重力方向生成,低矮的或根茎面积大的(多为双子叶动物)适宜贴地表成长。用叉乘来计算:
重力权重为1,植被up方向与引擎默认雷同
重力权重为0,采纳地表方向为植被up方向
9. Mipmap与PreZ计划
Unity的Alphatest的Mipmap可能为了降噪默认有个问题越远剔除越多,导致SpeedTree官网给的资源都不得不对每个LOD设置不同材质球,就为了独自设置不同间隔的Cutout的数值。这里有我晚期提到的一些解决办法《解决AlphaTest 远处隐没问题》。
然而对于草,这样却有个意外播种,即会有简略版的淡入淡出,然而为了更好是变动成果,最好还是本人管制应该的LOD或本人计算动静的Clip值。淡入淡出里,提到后者的做法这里不反复了。
PreZ是什么能够简略了解为强制版显式的Early Z。在引擎革新过程中,咱们会强制渲染一遍深度,而后在绘制Opaque的对象时,采纳ZTest Equal来绘制 G-buffer,所以Clip的过程只产生在PreZ的Pass。这个Pass只须要透明度不关注色彩。所以咱们把透明度独自存一张图,为什么独自进去能进步性能呢?因为咱们能够让这张图尺寸比Albedo小。为什么放大却不会重大失真呢?因为我会用半透明的有向间隔场SDF数据代替半透明数据来寄存,如果理解间隔场会晓得它是更准确的表白,不是它原始数据准确而是对插值更准确,所以 Mipmaps采样时成果更好,且容许用更小的图。
额定小技巧
根部的透明度,人为成心擦除一些,好像断了一些。这样视觉错觉会让玩家脑部草的前后关系,否则都在一个面上很僵硬。
这是美术制作给到我的草,根部齐平很难看
这是我手动擦除了些根部产生的立体感
当贴近朝上看的时候须要缓缓降落一点,防止浮空地位穿帮
以上是整个我的项目的开发过程中积攒到的对于草的总结,心愿对其他人有局部启发。
这是侑虎科技第1229篇文章,感激作者偶然不帅供稿。欢送转发分享,未经作者受权请勿转载。如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/...
再次感激偶然不帅的分享,如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ群:793972859)