最近看到一位国际友人在做独立游戏,但一眼兰伯特的人物有点看不下去,便拿着之前写的build-in SSS 开源demo,想给他借鉴一下。结果他把开发环境给我一看,好家伙,他这画质,竟然是HDRP。然后我有点后悔了,实际摸起来发现,HDRP比URP难多了。好在最近还有一点点时间,本着提升自我的想法,还是对直接的demo做一下hdrp兼容吧,这篇便是踩坑和方案记录。
由于本人实力不佳,无法很完美的解决问题,存在部分暴力改写的情况,有所影响性能。但好在是电脑游戏,先把效果实现了,性能后续有空了再慢慢优化。(咕咕咕)注:Standard lit也有SSS, 但针对皮肤的效果很差,毕竟只是通用shader。
具体可以参考我之前的一篇帖子Vrchat中实现次表明散射(SSS)材质 - 知乎 (zhihu.com)。大致思路是预烘焙一张LUT图,对其采样来替代NdotL的光照衰减。
因此,SSS的重点在于custom NdotL,其实这就和卡通渲染二值化NdotL的流程很像了。
这个应该还是很明显的,原生SSS只是让阴影变红了,但本SSS在明暗过渡带上表现更突出,而真是这点细微的变化,让皮肤看上去比较油腻。
主要展示了三种情况:
主要区别是因为我使用了曲率图,而官方的没有用,所以他的看起来更通透,我的看得出厚度。删掉曲率图继续对比。主要区别在于,官方使用的是厚度图,而我用的是曲率图。厚度图无法处理角度和散射的关系,因此看上去颜色没有渐变。这也是定制扩展的意义。
官方不具备软阴影sss特性,也是扩展的一大意义。
但是调节normal bios后会让阴影非线性,部分区域阴影断裂,这个问题没有很好的解决。
对rgb通道使用不同模糊度的法线,考虑了法线贴图的色散,让右边指纹处阴影没那么黑了,说实话这个效果不是很明显,得仔细看才看得出。而且太近反而效果变差:
肉眼可见的红绿偏移需要采样3倍次数的贴图,而且调起来难以符合直觉,建议写死在代码里,别给美术调,或者不加。(其实还是看贴图的,如果原来的高模非常精细,那效果才会不错)
HDRP管线最影响实现的几个特点如下的:(对hdrp的描述并不完整,但这些都是会影响的点)
opaque走deferred render,transparent走forward。光照系统重写,光源严格基于物理量Irradiance(受照面单位面积上的辐射通量),使用自动曝光。融合了SSR、GI、PBR、PCSS等各种高级渲染技术,技术栈很深,总之不太可能从0写一个shader。shader graph应该是大家最先想到的。但是还记得SSS的重点在于custom NdotL吗?shader graph lit的输出是Base Color,不含NdotL,NdotL会在输出后乘上base color。NdotL是在shader graph里是改不到的,如果强行用连连看连线,最终结果也只会变成
diffuse=BaseColor∗CustomNdotL∗NdotLdiffuse=BaseColor*CustomNdotL *NdotL
这样NdotL其实乘了2次,颜色变暗,无法在lit下实现所需的SSS效果。
至于Unlit,如果你愿意徒手把SSR、GI、PBR自动曝光都实现一边,倒也不是不行。但关键是,我也不知道一共用了多少技术才达到standard lit效果,造轮子又费时又容易漏掉feature。
把standard lit shader和hlsl库复制到项目resource目录里,改shader中的引用路径,就能魔改hdrp standard lit了。
这个思路大体上ok,但opaque走的是deferred rendering,他只有4张rt可以设置:
outGBuffer0为基础颜色级高光遮蔽,
outGBuffer1是法线以及a通道是粗糙度
outGBuffer2是高光遮蔽及厚度,或者是各向异性及金属度,或者是菲涅尔,根据用的材质决定。
outGBuffer3是自发光或者是环境光。
然后我们要改的ndotl衰减,并不能通过这4张rt实现。(outGBuffer0只是纯色)
通过frame debug发现,4张纹理在GBuffer Pass绘制完成,然后到Deferred lighting pass渲染着色。
关键是这个Deferred lighting的源码我找不到,查到过build-in替换deferred rendering的方案如何在Unity中实现非真实感渲染-腾讯游戏学堂,但hdrp中无法替换。
问题是就算找到了也没结束,直接改brdf会导致所以物体都变成SSS管线,并不能用。
tansparent是走前向渲染的,前向渲染跟着/Runtime/Lighting/LightLoop/LightLoop.hlsl找下去还是很容易定位到代码的。看到搜狐大佬实现过hdrp的部分opaque前向渲染,我的话只会全部设置成前向,这样失去了点光源随便加的优势,所以还是用tansparent吧。
这个方案比较迁就,但好在能快速实现。
这几个stuct我直接供起来!多谢大佬梳理!
对于其他找不到的函数,善用vs即可找到。
值定义在LitProperties中,改动涉及以下脚本:
Runtime\Material\Lit\LitProperties.hlsl引用关系为:
Shader->LitProperties.hlsl
LitProperties.hlsl改动内容新增浮点和向量直接加
贴图两步处理
BRDF在Lit.hlsl中,改动涉及以下脚本:
Runtime\Material\Lit\Lit.hlslRuntime\Lighting\SurfaceShading.hlsl引用关系为:
Shader->ShaderPassForward.hlsl->LightLoop.hlsl->Lit.hlsl->SurfaceShading.hlsl->Lit.hlsl
SurfaceShading.hlsl改动内容平行光的优化部分需要删除
if((light.lightDimmer >0)&&IsNonZeroBSDF(V, L, preLightData, bsdfData))是优化,ndotl小于0不计算,sss仍然需要计算。删除IsNonZeroBSDF即可。
Lit.hlsl改动内容把EvaluateBSDF中的cbsdf.diffR = diffTerm * clampedNdotL,和cbsdf.diffT = diffTerm * flippedNdotL;中的ndotl项删除。
效果如下:已经没有阴影衰减了,可以实现sss的预计算的阴影衰减。法线uv读取在LitDataIndividualLayer.hlsl中,改动涉及以下脚本:
Runtime\Material\Lit\LitDataIndividualLayer.hlslRuntime\Material\Lit\LitData.hlsl引用关系为:
Shader->LitData.hlsl->LitDataIndividualLayer.hlsl
LitData改动内容结构体扩展用于储存自定义uv层
LitDataIndividualLayer.hlsl中改动内容修改ComputeLayerTexCoord函数,uvMappingMask由交互面板上选择uv层确定,默认为1,0,0,0,选择uv0。此处把uv0的数据存入上面扩展出的baseNormal字段,并在之后GetSurfaceData采样主贴图的时候传入uv0。这样保证主贴图一定采样uv0,法线等其他贴图根据所选uv层采样。
这里可以选择还有一个小的问题,uvMappingMask为1,0,0,0时,uv1不会传入,则读不到内容,需要注意。
因为我的处理方法是默认读可选uv,颜色固定读uv0,uv0一定会传入,所以没有问题。
平行光的实现在SurfaceShading.hlsl的ShadeSurface_Directional函数中。改动需涉及以下脚本:
Runtime\Material\Lit\Lit.hlslRuntime\Lighting\SurfaceShading.hlslRuntime\Lighting\LightLoop\LightLoop.hlslRuntime\Material\Lit\LitData.hlslRuntime\RenderPipeline\ShaderPass\ShaderPassForward.hlsl引用路径
Shader->ShaderPassForward.hlsl->LightLoop.hlsl->Lit.hlsl->SurfaceShading.hlsl->Lit.hlsl
LitData.hlsl可以很容易获得uv信息,在GetSurfaceAndBuiltinData里采样曲率贴图这里改的比较暴力,临时存入了官方SSS的property(因为我不会用他了),ShaderPassForward中调用GetSurfaceAndBuiltinData得到曲率值,传入扩展参数后的lightLoop.hlsl
LightLoop.hlsl的改动这里只负责把曲率值继续传到SurfaceShading,不负责任何计算。
先传给Lit.hlsl的EvaluateBSDF_Directional,再传到SurfaceShading.hlsl的ShadeSurface_Directional函数,专门处理平行光的函数。
直接在ShadeSurface_Directional里改,不继续传下去到brdf,是因为阴影的计算也在这个函数里,在这里计算出的SSS比较方便和阴影做混合运算。
我们在Lit.hlsl中定义两个函数,一个计算模糊法线、一个计算customNdotL。这样只需要在SurfaceShading.hlsl直接调用CalaSSSColor就能取得SSS的NdotL。此外,这里采样贴图要用SAMPLE_TEXTURE2D_LOD,不然在点光loop时会报错迭代次数太多。(普通采样还有texture streaming的计算,很复杂)
完成,不考虑阴影的时候,SSS效果比较明显。
SurfaceShading.hlsl的ShadeSurface_Punctual函数中。改动需涉及以下脚本:
Runtime\Material\Lit\Lit.hlslRuntime\Lighting\SurfaceShading.hlslRuntime\Lighting\LightLoop\LightLoop.hlslRuntime\Material\Lit\LitData.hlslRuntime\RenderPipeline\ShaderPass\ShaderPassForward.hlsl引用路径
Shader->ShaderPassForward.hlsl->LightLoop.hlsl->Lit.hlsl->SurfaceShading.hlsl->Lit.hlsl
函数嵌套关系几乎和平行光一样,点光和聚光同一由ShadeSurface_Punctual处理。
因为其他一样,这里只讲ShadeSurface_Punctual的改动了,在SurfaceShading.hlsl里非平行光计算L的步骤变复杂了,还多了一个distance,但distance我们不用管,距离的影响已经作用于lightColor。
除去已经被我们改掉的IsNonZeroBSDF,还有一个if分支:上面处理的是官方SSS厚物体,下面则是处理薄SSS物体和常规物体,我们计算写在else内。调用之前写在Lit.hlsl但函数计算SSS衰减,依旧只需要传入L、曲率和bsdfData就行,和平行光没什么区别。
这是效果,右图看出明显sss。
右边是加了SSS效果的由于影子是shadowmap采样出的,没什么调整空间,所以我这里想出两个方法:
阴影和NdotL的非线性结合着色公式入下
可以看到整个式子是线性的,那么shadow是突变的,lighting.diffuse就一定也是突变的。(默认NdotL不突变是因为有一个max(0,NdotL),打破了线性)
那么我们也突发奇想(恶疾),让他非线性就好了。
lighting.diffuse=bsdf.diffuse*min(shadow,SSSNdotL);这就是我们的公式,只要shadow半影区比较大且比较淡,过渡显示的颜色就是SSSNdotL的颜色,那么就不会突变。看一下效果:
乍一看好像完美解决问题了,但以下情况缺变的很糟:
特殊情况这个问题是normal bios设置过大引起的,NdotL会盖掉突变位置,但SSS下不会,因此没有什么好办法。
反而没有normal bios时,min的效果其实还是不错的。
normal bios=0时本质是锯齿和粉刺阴影的问题,优化手段不适用于SSS,寻求兼容不如改个阴影系统。
说归这么说,还是试了很多tricky的方法,最终选择加一个_ShadowOffest去对冲normal bios的割裂感,缺点是影子面积会变小,但断裂感不明显了。
SSS半影区GPU Pro 2中讲到,阴影也可以做SSS。由于项目环境中半影区很小,像上文所做的钳制意义不大,反而为了对冲normal bios我反过来把后半段变平了,总之就是对阴影进行一次采样。
这里有两个问题,1是hdrp阴影可以设置颜色,通过ComputeShadowColor函数,但如果先读sss再转换阴影颜色,阴影变淡会变白,看起来不好看。
所以我们先转换为自定义阴影颜色,再用颜色的亮度(hsv_v)对SSS采样,并叠加回去。
第二个则是为了让影子半影区更明显,我在采样的时,uv.v乘了10(float2(hsv_v,curve*_SSSStrength*10)).
最终效果:
cube的影子有明显的SSS效果了自定义Inspector得2021.1.9f1(HDRP 11.0.0)以后版本的unity才能比较完美的支持(HDRP custom Material Inspectors)
不然要么把整个hdrp代码搬过来,处理一堆报错(一般项目组会做这个);要么自己重写一套兼容Lit的自定义面板,比较费时,但不写也能将就着用,我就摸了。
如果以后他游戏用的版本升上去了我就写。
标准SSS还有屏幕空间SSS和Transmittance两个feature,我这里并没有实现,同时性能优化也是一个重要的点。后期可以继续往这几个方向提升。
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:704559159@qq.com