fwalker 发表于 2022-10-6 10:46

Unity Spherical Gaussian Subsurface 小记

演示




与 standard 对比



与 standard 对比



手和腿这种大曲面看着还行其实


理论复读机(球面高斯)

常规的 BRDF用入射 radiance 与 lobes 做运算,进出处于同一个 pixel footprint 中。虽然可能 BRDF 也发生了一定程度的微弱 BSDF,但在 pixel footprint 级别上可以忽略。


而 BSSRDF 认为radiance 进入介质后,在内部发生了若干次散射,出来后的位置与原来已经有了一段距离。离线渲染中可能会定义这个半透明的介质厚度、折射率(或散射率)、吸收率等参数,供 volumetric path tracing 之类的方法计算。
path tracing through the medium 的实现比较费,大多数游戏使用了预积分 LUT 的方案,效果很好,实现也直接。



皮肤渲染常用的 LUT 图

19 年工程师 Matt 提出了使用球面高斯来近似预积分 LUT 的那张图,用额外 ALU 代价,节省了采样 LUT 的带宽
用图片来解释,如果是 PBR 的话,SG 方案将 PBR 原有的 Specular NoL Cosine Lobe 替换为 三个 SG Lobe,根据这三个 Lobe 各自对 radiance 的散射能力做计算,这个过程本质是在做预积分 LUT 的工作。


例如在皮肤渲染中,红光比绿光、蓝光散射得更远,三个 Lobe 的总和与出射角度呈 “白→黄→橙→红” 的变化



图源 s2018 unity,我涂了几笔

在数学模型层面上,构建出球面高斯替换掉 Cosine Lobe 即可
实践

1、搭建 PBR



2、搭建 高斯

一)构建 SG 结构体 FSphericalGaussian,包含轴向、锐度、幅度


    // === Spherical Gaussian ===
    // Below functions are ported from UE4
    struct FSphericalGaussian
    {
      float3        Axis;                // u, \mu
      float        Sharpness;        // L, \lambda
      float        Amplitude;        // a,
    };
二)将 SG 对准光源朝向,并标准化 SG.Amplitude 。此处可以在里面写一个 SG.Amplitude *= LightIntensity,也可以后期在算 DirectColor 时写一个乘以 mainLight.color,两种方法都行


// Normalize SG.
    FSphericalGaussian MakeNormalizedSG(float3 LightDir, half Sharpness)
    {
      FSphericalGaussian SG;

      // Align axis,multiply light Intensity.
      SG.Axis = LightDir;
      SG.Sharpness = Sharpness; // (1 / ScatterAmt.element)
      SG.Amplitude = SG.Sharpness / ((2 * PI) - (2 * PI) * exp(-2 * SG.Sharpness)); // Normalize
      
      // SG.Amplitude *= LightIntensity;
      return SG;
    }
三)用 DotCosineLobe 替换渲染方程的 Cosine Lobe。这里,DotCosineLobe 将三个不同颜色的 Kernel 与 法线做计算,类似于渲染方程的 L 与法线做计算。
// Inner product with cosine lobe
    // Assumes G is normalized
    float3 DotCosineLobe( FSphericalGaussian G, float3 N )
    {
      const float muDotN = dot( G.Axis, N );

      const float c0 = 0.36;
      const float c1 = 0.25 / c0;

      float eml= exp( -G.Sharpness );
      float em2l = eml * eml;
      float rl   = rcp( G.Sharpness );
      
      float scale = 1.0f + 2.0f * em2l - rl;
      float bias= (eml - em2l) * rl - em2l;

      float x = sqrt( 1.0 - scale );
      float x0 = c0 * muDotN;
      float x1 = c1 * x;

      float n = x0 + x1;
      float y = ( abs( x0 ) <= x1 ) ? n * n / x : saturate( muDotN );

      return scale * y + bias;
    }
四)计算 SGSGDiffuseLighting,也就是通过 DotCosineLobe 散射出来的三种光。注意此处需要做 DiffuseTonemapping。


// Compute the irradiance that would result from convolving a punctual light source
    // with the SG filtering kernels
    float3 SGSGDiffuseLighting (float3 N, float3 lightDir, float3 ScatterAmt)
    {
      FSphericalGaussian redKernel   = MakeNormalizedSG(lightDir, 1.0f / max(ScatterAmt.x, 0.0001f));
      FSphericalGaussian greenKernel = MakeNormalizedSG(lightDir, 1.0f / max(ScatterAmt.y, 0.0001f));
      FSphericalGaussian blueKernel= MakeNormalizedSG(lightDir, 1.0f / max(ScatterAmt.z, 0.0001f));

      float3 SGDiffuse = float3(DotCosineLobe(redKernel, N).x,
      DotCosineLobe(greenKernel, N).x,
      DotCosineLobe(blueKernel, N).x);

      // Below is Diffuse Tonemapping, without it, sss will be purple or grey.
      // Cuz too much red radiance scatter out from skin.
      // Tonemap_Filmic_UC2DefaultToGamma
      // Uncharted II fixed tonemapping formula.
      // The linear to sRGB conversion is baked in.
      half3 x = max(0, (SGDiffuse - 0.004));
      half3 DiffuseTonemapping = \
      lerp(SGDiffuse, (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06), 1);
      SGDiffuse = lerp(SGDiffuse, DiffuseTonemapping, 1); // 1 is complete toonmapping.

      return SGDiffuse;
    }
五)最后,用 SG_Mid_Result 替换 NoL,其中可以加入任意的调整参数(例如 add、clamp、multi)。SG_Curvature 可以烘焙,也可以 ddx ddy 计算。_SkinScatterAmount 是散射的颜色。


float SG_Curvature = SAMPLE_TEXTURE2D(_SkinBaseMap, sampler_SkinBaseMap, i.texcoord.xy).w;

float SG_Clamp = clamp((SG_Curvature * _SkinScatterAmountMulti + _SkinScatterAmountAdd), _SkinScatterClampMin, _SkinScatterClampMax);
                SG_Clamp = pow (SG_Clamp, 5) ;
               
                float3 SG_Mid_Result = SGSGDiffuseLighting
                (
                fin_normal,
                L,
                _SkinScatterAmount * SG_Clamp
                );

;=========================================

// #ifdef _SSS_ON
                DirectColor = (diffuse + specular) * SG_Mid_Result * PI * mainLight.color * Shadow;
                // #else
                //   DirectColor = (diffuse + specular) * NoL * PI * mainLight.color;
                // #endif
DirectColor 是直接光的 SG 散射结果,间接光散射没有写
3、细节

总得来说,再复读一次,球面高斯便是把渲染方程的 NoL 项给替换掉了,如同上面理论部分理解的那样。
通常情况,实现直接光的 SSS 后,效果已经挺不错了,如果再加上环境光的 SSS,我估计效果提升不会很明显。
在难度上来讲,大概理解 SG 的原理后,只会比写 PBR 要稍微复杂一点点。
值得注意的地方:一个是 toon mapping、另一个是自定义参数的换算,以及 Curvature 的烘焙。
一)由于红光散射的量比蓝光绿光强很多,在没有 toon mapping 情况下,调整 SSS 后会发生色偏。效果会逐渐偏灰偏紫,极端情况后偏蓝,最后偏黑。(此方法不能完美解决偏色,还是会有一点肉眼可见的偏)



没有 toon,或没有 clamp 散射量,会变蓝变黑



看着毫无血色的皮肤…

二)自定义参数的设置,基本按照 a * b + c 那一套来大概就 okay,再加一个 clamp min or max,这已经很自由啦。



对散射范围、散射颜色之类的参数进行随意设置。

三)实时用 ddx ddy 计算的曲率很不理想。


一般用烘焙的做法,烘焙 Curvature的方式多种多样,常见的 subs painter 可以直接输出,也可以用 houdini 的 labs curvature measure,甚至是自己往上面手绘涂几笔。总之有一张对散射程度进行区分的图就行了。



有些游戏截帧后,真的能看见右下角的这种图片

总结

1、移动端近来不怎么缺 ALU,经过优化后也能够使用这套方案,调参挺直观的(?)
2、其实已经有可视化的 LUT 方案了,能够实时生成并调 param,比如 HDRP 的 Diffusion Profile
3、在制作过程中,Alex 大佬为我细心解答了数个问题,在此表示由衷的感谢(我与大佬居然在一个群里,大震撼)
4、遗留很多问题还没做,阴影(带颜色的 penumbra)、背光下的漏光(发生可能性微存),延迟渲染下怎么存数据之类的…不管了,水专栏要紧


参考资料

https://blog.selfshadow.com/publications/s2020-shading-course/patry/slides(对马岛这个曲率图怎么得到的?迷)
Free Bird:【UE4.25】:修改渲染管线,实现新的皮肤渲染管线(妙不可言)
剖析Unreal Engine超真实人类的渲染技术Part 1 - 概述和皮肤渲染(全网最细博客)
分拇:预积分皮肤次表面散射LUT研究实践与遇到的问题(生成 LUT)
UE4中用球面高斯函数实现移动端SSS效果(A 大 yyds!)
CGBull:Unreal Subsurface(同样借鉴了一部分)
https://advances.realtimerendering.com/s2018/Efficient%20screen%20space%20subsurface%20scattering%20Siggraph%202018.pdf (HDRP 的 SSS 怎么来的)
Jeffrey Zhuang:Pre-Integrated Skin Shading 的常见问题解答(深入剖析偏色问题)
Infinite, 3D Head Scan. Released! (这里居然有 LPS Head 的 ZB 文件,太棒了)
另外网络上还有更多关于 BSSRDF 数学模型积分公式的推导方面的优质文章,现代化的 NVIDIA RTX digital human 值得一看,虽然也想放进参考资料中,但我的数学水平毕竟有限,引用也是故作玄虚 & 滥引,故不再枚举…
最后,感谢 Nexus 社区提供 4k 蒂法模型供测试,今年年初看见这个绅士模型公布出来以后,老早就想拿来做 shader 了!

XGundam05 发表于 2022-10-6 10:54

模型让我想起了黄油,这该不会是从哪部黄油拿出来的吧[飙泪笑][飙泪笑]

量子计算9 发表于 2022-10-6 10:54

就今年年初网上热传的那个 4k 蒂法模型 呀 = =

Doris232 发表于 2022-10-6 10:59

[害羞][害羞]蒂法就是黄油最多的,奇怪的冷知识,模型的肚子上的纹身印象中是淫纹[发呆]

super1 发表于 2022-10-6 11:07

我超,银纹[害羞]
页: [1]
查看完整版本: Unity Spherical Gaussian Subsurface 小记