病例快查网

 找回密码
 立即注册
查看: 109|回复: 3

SSS皮肤材质-魔改HDRP Standard Lit(进行中)

[复制链接]

3

主题

6

帖子

12

积分

新手上路

Rank: 1

积分
12
发表于 2022-12-26 21:42:18 | 显示全部楼层 |阅读模式
最近看到一位国际友人在做独立游戏,但一眼兰伯特的人物有点看不下去,便拿着之前写的build-in SSS 开源demo,想给他借鉴一下。结果他把开发环境给我一看,好家伙,他这画质,竟然是HDRP。然后我有点后悔了,实际摸起来发现,HDRP比URP难多了。好在最近还有一点点时间,本着提升自我的想法,还是对直接的demo做一下hdrp兼容吧,这篇便是踩坑和方案记录。
由于本人实力不佳,无法很完美的解决问题,存在部分暴力改写的情况,有所影响性能。但好在是电脑游戏,先把效果实现了,性能后续有空了再慢慢优化。(咕咕咕)
注:Standard lit也有SSS, 但针对皮肤的效果很差,毕竟只是通用shader。
一、总结

这里写的是sss的大致思路和必要流程
1.1 SSS效果要点

具体可以参考我之前的一篇帖子Vrchat中实现次表明散射(SSS)材质 - 知乎 (zhihu.com)。大致思路是预烘焙一张LUT图,对其采样来替代NdotL的光照衰减。
因此,SSS的重点在于custom NdotL,其实这就和卡通渲染二值化NdotL的流程很像了。
1.2 扩展standard lit心得


  • HDRP中只有半透明走forward rendering,其最外层在ShaderPassForward.hlsl。
  • 新增字段先加shader中,后在LitProperties.hlsl中添加。之后在其他脚本中均可获取。
  • 不建议在LightLoop.hlsl中使用N、L数据,建议在SurfaceShading.hlsl和Lit.hlsl中扩展。
  • LightLoop.hlsl调用Lit.hlsl再调用SurfaceShading.hlsl完成所有光源的Irradiance计算,并叠加在一起。
  • 顺序是先非平行光、再平行光、然后面光GI。
  • ShaderPassForward.hlsl调用LightLoop处理完Irradiance,之后会处理自动曝光、默认SSS、大气散射、动态模糊。
  • 法线读取在LitDataIndividualLayer.hlsl和LitData.hlsl中

    • LitData.hlsl引用LitDataIndividualLayer.hlsl
    • LitDataIndividualLayer.hlsl负责接受fragement参数,计算原始法线(切线空间),可以在此处更改读取的uv层。
    • LitData.hlsl负责把基础法线转换为高级法线,比如normalWS,BentNormal。输出SurfaceData,builtinData。

  • BSDF运算在Lit.hlsl中,阴影叠加运算在SurfaceShading.hlsl中。
二、效果展示

2.1 强主光照下,diffuse表现



这个应该还是很明显的,原生SSS只是让阴影变红了,但本SSS在明暗过渡带上表现更突出,而真是这点细微的变化,让皮肤看上去比较油腻。
2.2 夜晚点光聚光表现



主要展示了三种情况:

  • 主要区别是因为我使用了曲率图,而官方的没有用,所以他的看起来更通透,我的看得出厚度。删掉曲率图继续对比。


主要区别在于,官方使用的是厚度图,而我用的是曲率图。厚度图无法处理角度和散射的关系,因此看上去颜色没有渐变。这也是定制扩展的意义。
2.3 软阴影SSS



官方不具备软阴影sss特性,也是扩展的一大意义。
但是调节normal bios后会让阴影非线性,部分区域阴影断裂,这个问题没有很好的解决。
2.4 法线分通道模糊



对rgb通道使用不同模糊度的法线,考虑了法线贴图的色散,让右边指纹处阴影没那么黑了,说实话这个效果不是很明显,得仔细看才看得出。而且太近反而效果变差:



肉眼可见的红绿偏移

需要采样3倍次数的贴图,而且调起来难以符合直觉,建议写死在代码里,别给美术调,或者不加。(其实还是看贴图的,如果原来的高模非常精细,那效果才会不错)
三、HDRP实现难点和解决方案

3.1 HDRP的难点

HDRP管线最影响实现的几个特点如下的:(对hdrp的描述并不完整,但这些都是会影响的点)

  • opaque走deferred render,transparent走forward。
  • 光照系统重写,光源严格基于物理量Irradiance(受照面单位面积上的辐射通量),使用自动曝光。
  • 融合了SSR、GI、PBR、PCSS等各种高级渲染技术,技术栈很深,总之不太可能从0写一个shader。
3.2 最终实现方案

否决的方案1:shader graph
shader graph应该是大家最先想到的。但是还记得SSS的重点在于custom NdotL吗?shader graph lit的输出是Base Color,不含NdotL,NdotL会在输出后乘上base color。NdotL是在shader graph里是改不到的,如果强行用连连看连线,最终结果也只会变成
diffuse=BaseColor*CustomNdotL *NdotL
这样NdotL其实乘了2次,颜色变暗,无法在lit下实现所需的SSS效果。
至于Unlit,如果你愿意徒手把SSR、GI、PBR自动曝光都实现一边,倒也不是不行。但关键是,我也不知道一共用了多少技术才达到standard lit效果,造轮子又费时又容易漏掉feature。


否决的方案2:魔改Standard Lit Opaque 把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画Opaquetansparent是走前向渲染的,前向渲染跟着/Runtime/Lighting/LightLoop/LightLoop.hlsl找下去还是很容易定位到代码的。看到搜狐大佬实现过hdrp的部分opaque前向渲染,我的话只会全部设置成前向,这样失去了点光源随便加的优势,所以还是用tansparent吧。
这个方案比较迁就,但好在能快速实现。
3.3 任务拆解


  • 定位NdotL部分,删除NdotL的衰减。
  • 添加SSS自定义NdotL的衰减,乘在diffuse。
  • 定位法线贴图读取部分,改读uv1。
  • 试试自定义高光brdf是否优于pbr。
  • 扩展shader property,并把值用合(bao)理(li)的方法传入hlsl库。(如果我能顾及合批)
  • 重写shaderGUI。(2020.3f1似乎很难实现,添加c#代码一堆域的报错)
  • 适配原先shader。
  • 编写出色的使用文档,梳理制作流程。
四、 源码分析

首先感谢这位大佬做出的贡献:Unity2018的HDRPStandard材质分析笔记(一)_Calette的博客-CSDN博客
这几个stuct我直接供起来!多谢大佬梳理!
struct DirectLighting
{
    float3 diffuse;
    float3 specular;
};

struct BSDFData
{
    uint materialFeatures;
    float3 diffuseColor;
    float3 fresnel0;
    float ambientOcclusion;
    float specularOcclusion;
    float3 normalWS;
    float perceptualRoughness;
    float coatMask;
    uint diffusionProfile;
    float subsurfaceMask;
    float thickness;
    bool useThickObjectMode;
    float3 transmittance;
    float3 tangentWS;
    float3 bitangentWS;
    float roughnessT;
    float roughnessB;
    float anisotropy;
    float iridescenceThickness;
    float iridescenceMask;
    float coatRoughness;
    float3 geomNormalWS;
    float ior;
    float3 absorptionCoefficient;
    float transmittanceMask;
};
struct PreLightData
{
    float NdotV;                     // Could be negative due to normal mapping, use ClampNdotV()

    // GGX
    float partLambdaV;
    float energyCompensation;

    // IBL
    float3 iblR;                     // Reflected specular direction, used for IBL in EvaluateBSDF_Env()
    float  iblPerceptualRoughness;

    float3 specularFGD;              // Store preintegrated BSDF for both specular and diffuse
    float  diffuseFGD;

    // Area lights (17 VGPRs)
    // TODO: 'orthoBasisViewNormal' is just a rotation around the normal and should thus be just 1x VGPR.
    float3x3 orthoBasisViewNormal;   // Right-handed view-dependent orthogonal basis around the normal (6x VGPRs)
    float3x3 ltcTransformDiffuse;    // Inverse transformation for Lambertian or Disney Diffuse        (4x VGPRs)
    float3x3 ltcTransformSpecular;   // Inverse transformation for GGX                                 (4x VGPRs)

    // Clear coat
    float    coatPartLambdaV;
    float3   coatIblR;
    float    coatIblF;               // Fresnel term for view vector
    float3x3 ltcTransformCoat;       // Inverse transformation for GGX                                 (4x VGPRs)

#if HAS_REFRACTION
    // Refraction
    float3 transparentRefractV;      // refracted view vector after exiting the shape
    float3 transparentPositionWS;    // start of the refracted ray after exiting the shape
    float3 transparentTransmittance; // transmittance due to absorption
    float transparentSSMipLevel;     // mip level of the screen space gaussian pyramid for rough refraction
#endif
};

对于其他找不到的函数,善用vs即可找到。
4.1 传值到hlsl

值定义在LitProperties中,改动涉及以下脚本:

  • Runtime\Material\Lit\LitProperties.hlsl
引用关系为:
Shader->LitProperties.hlsl
<hr/>
LitProperties.hlsl改动内容
新增浮点和向量直接加


贴图两步处理


4.2 修改BRDF(去除ndotl)

主要是为了去除BRDF的ndotl衰减
BRDF在Lit.hlsl中,改动涉及以下脚本:

  • Runtime\Material\Lit\Lit.hlsl
  • Runtime\Lighting\SurfaceShading.hlsl
引用关系为:
Shader->ShaderPassForward.hlsl->LightLoop.hlsl->Lit.hlsl->SurfaceShading.hlsl->Lit.hlsl
<hr/>
SurfaceShading.hlsl改动内容
平行光的优化部分需要删除
DirectLighting ShadeSurface_Directional(LightLoopContext lightLoopContext,
                                        PositionInputs posInput, BuiltinData builtinData,
                                        PreLightData preLightData, DirectionalLightData light,
                                        BSDFData bsdfData, float3 V)
{
    DirectLighting lighting;
    ZERO_INITIALIZE(DirectLighting, lighting);

    float3 L = -light.forward;

    // Is it worth evaluating the light?
    if ((light.lightDimmer > 0) && IsNonZeroBSDF(V, L, preLightData, bsdfData))
    {
        float4 lightColor = EvaluateLight_Directional(lightLoopContext, posInput, light);
        lightColor.rgb *= lightColor.a; // Composite

#ifdef MATERIAL_INCLUDE_TRANSMISSION
        if (ShouldEvaluateThickObjectTransmission(V, L, preLightData, bsdfData, light.shadowIndex))
        {
            // Transmission through thick objects does not support shadowing
            // from directional lights. It will use the 'baked' transmittance value.
            lightColor *= _DirectionalTransmissionMultiplier;
        }
        else
#endif
        {
            SHADOW_TYPE shadow = EvaluateShadow_Directional(lightLoopContext, posInput, light, builtinData, GetNormalForShadowBias(bsdfData));
            float NdotL  = dot(bsdfData.normalWS, L); // No microshadowing when facing away from light (use for thin transmission as well)
            shadow *= NdotL >= 0.0 ? ComputeMicroShadowing(GetAmbientOcclusionForMicroShadowing(bsdfData), NdotL, _MicroShadowOpacity) : 1.0;
            lightColor.rgb *= ComputeShadowColor(shadow, light.shadowTint, light.penumbraTint);
        }

        // Simulate a sphere/disk light with this hack.
        // Note that it is not correct with our precomputation of PartLambdaV
        // (means if we disable the optimization it will not have the
        // same result) but we don't care as it is a hack anyway.
        ClampRoughness(preLightData, bsdfData, light.minRoughness);
        
        lighting = ShadeSurface_Infinitesimal(preLightData, bsdfData, V, L, lightColor.rgb,
                                              light.diffuseDimmer, light.specularDimmer);
                                             
    }
    return lighting;
}
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的预计算的阴影衰减。


4.3 法线处理(和SSS无关,项目适配)  

目标模型为了高质量法线,法线全在uv1中重新展开了,而albedo在uv0,因此也需要定制化读取。
法线uv读取在LitDataIndividualLayer.hlsl中,改动涉及以下脚本:

  • Runtime\Material\Lit\LitDataIndividualLayer.hlsl
  • Runtime\Material\Lit\LitData.hlsl
引用关系为:
Shader->LitData.hlsl->LitDataIndividualLayer.hlsl
<hr/>
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一定会传入,所以没有问题。
4.4 SSS平行光

平行光的实现在SurfaceShading.hlsl的ShadeSurface_Directional函数中。改动需涉及以下脚本:

  • Runtime\Material\Lit\Lit.hlsl
  • Runtime\Lighting\SurfaceShading.hlsl
  • Runtime\Lighting\LightLoop\LightLoop.hlsl
  • Runtime\Material\Lit\LitData.hlsl
  • Runtime\RenderPipeline\ShaderPass\ShaderPassForward.hlsl
引用路径
Shader->ShaderPassForward.hlsl->LightLoop.hlsl->Lit.hlsl->SurfaceShading.hlsl->Lit.hlsl
<hr/>
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比较方便和阴影做混合运算。
float3 CalcSSSNdotL(float3 L, BSDFData bsdfData, float3 SSSscater)
{
    float3 rN=lerp(bsdfData.normalWS,bsdfData.geomNormalWS,SSSscater.x);
    float3 gN=lerp(bsdfData.normalWS,bsdfData.geomNormalWS,SSSscater.y);
    float3 bN=lerp(bsdfData.normalWS,bsdfData.geomNormalWS,SSSscater.z);
    float3 sss_NdotL=float3(dot(rN,L),dot(gN,L),dot(bN,L));
    return sss_NdotL;         
}

float3 CalaSSSColor(float3 L, BSDFData bsdfData,float curveRate){
   
    float3 sss_NdotL=CalcSSSNdotL(L, bsdfData,_ScatterWidth)*0.5+0.5;
    float r =SAMPLE_TEXTURE2D_LOD(_SSSTex,sampler_SSSTex, float2(sss_NdotL.x,curveRate*_SSSStrength),0).r;
    float g =SAMPLE_TEXTURE2D_LOD(_SSSTex,sampler_SSSTex, float2(sss_NdotL.y,curveRate*_SSSStrength),0).g;
    float b =SAMPLE_TEXTURE2D_LOD(_SSSTex,sampler_SSSTex, float2(sss_NdotL.z,curveRate*_SSSStrength),0).b;
    return float3(r,g,b);
}
我们在Lit.hlsl中定义两个函数,一个计算模糊法线、一个计算customNdotL。这样只需要在SurfaceShading.hlsl直接调用CalaSSSColor就能取得SSS的NdotL。此外,这里采样贴图要用SAMPLE_TEXTURE2D_LOD,不然在点光loop时会报错迭代次数太多。(普通采样还有texture streaming的计算,很复杂)


完成,不考虑阴影的时候,SSS效果比较明显。


4.5 SSS点光聚光

点光源、聚光依葫芦画瓢
SurfaceShading.hlsl的ShadeSurface_Punctual函数中。改动需涉及以下脚本:

  • Runtime\Material\Lit\Lit.hlsl
  • Runtime\Lighting\SurfaceShading.hlsl
  • Runtime\Lighting\LightLoop\LightLoop.hlsl
  • Runtime\Material\Lit\LitData.hlsl
  • Runtime\RenderPipeline\ShaderPass\ShaderPassForward.hlsl
引用路径
Shader->ShaderPassForward.hlsl->LightLoop.hlsl->Lit.hlsl->SurfaceShading.hlsl->Lit.hlsl
<hr/>函数嵌套关系几乎和平行光一样,点光和聚光同一由ShadeSurface_Punctual处理。
因为其他一样,这里只讲ShadeSurface_Punctual的改动了,在SurfaceShading.hlsl里


非平行光计算L的步骤变复杂了,还多了一个distance,但distance我们不用管,距离的影响已经作用于lightColor。
除去已经被我们改掉的IsNonZeroBSDF,还有一个if分支:上面处理的是官方SSS厚物体,下面则是处理薄SSS物体和常规物体,我们计算写在else内。调用之前写在Lit.hlsl但函数计算SSS衰减,依旧只需要传入L、曲率和bsdfData就行,和平行光没什么区别。
这是效果,右图看出明显sss。



右边是加了SSS效果的

4.6 半影区

SSS的影子是个大问题,会覆盖sss效果,让结果变得难看,如下


由于影子是shadowmap采样出的,没什么调整空间,所以我这里想出两个方法:
<hr/>
阴影和NdotL的非线性结合
着色公式入下
lighting.diffuse=bsdf.diffuse*shadow*SSSNdotL;可以看到整个式子是线性的,那么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效果了

4.7 自定义Inspector

自定义Inspector得2021.1.9f1(HDRP 11.0.0)以后版本的unity才能比较完美的支持(HDRP custom Material Inspectors)
不然要么把整个hdrp代码搬过来,处理一堆报错(一般项目组会做这个);要么自己重写一套兼容Lit的自定义面板,比较费时,但不写也能将就着用,我就摸了。
如果以后他游戏用的版本升上去了我就写。
五 其他提升项

标准SSS还有屏幕空间SSS和Transmittance两个feature,我这里并没有实现,同时性能优化也是一个重要的点。后期可以继续往这几个方向提升。
回复

使用道具 举报

1

主题

3

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2022-12-26 21:42:29 | 显示全部楼层
blur normal 的效果看着不太明显
回复

使用道具 举报

1

主题

5

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2022-12-26 21:42:46 | 显示全部楼层
后面两张截图的时候没有采样uv1的法线,所以看起来特别平。不过确实调颜色衰竭的效果非常不明显,调节只有g通道变换明显,b通道几乎看不出变换,不确定是不是预积分图的问题。准备之后把多重uv写完,再看看指纹处的效果会不会明显一点。
回复

使用道具 举报

0

主题

2

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2022-12-26 21:43:13 | 显示全部楼层
大佬[好奇]
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|病例快查网

GMT+8, 2024-12-29 16:51 , Processed in 0.105901 second(s), 19 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc. Template By 【未来科技】【 www.wekei.cn 】

快速回复 返回顶部 返回列表