OpenGLTANotes

🕹️OpenGL丨ShadowDepthMap实现SSS透射 - 皮肤渲染(3)

by ERIN.Z, 2022-11-15


ssss

真实感皮肤渲染

本文的SSSS主要参考Jorge Jimenez的实现,在开始之前,先来回顾一下实时皮肤渲染的研究历程—— 首先来向毛老师学习: 《GPU Gems 3》:真实感皮肤渲染技术总结 对于皮肤渲染,我们需要使用BSSDF(Bidirectional Surface Scattering Reflectance Distribution Function,双向表面散射反射分布函数)。实时渲染的皮肤渲染过程由镜面反射和次表面散射两部分组成,我们主要关注SSS的部分。

Diffusion Profile

扩散剖面(diffusion profile)提供了光在高度散射的半透明材质表面下散射方式的近似。每种色光的扩散剖面不同,红光比绿色和蓝色散射得更远,因此强光照射下皮肤会产生橙红色的。 diffusion profile 到目前为止,图形学的科学家们偶极子与多级子等各种方法拟合了Diffusion Profile,实践表明多个高斯分布在一起可以对扩散剖面提供极好的近似。高斯函数同时是可分离的和径向对称的,可以相互卷积来产生新的高斯函数。 中间的推导看不懂(...),我们就直接用科学家们的研究成果就好! ssss

预积分的皮肤着色(Pre-Integrated Skin Shading)

之前在wy的T星课上,实现过这个过程的极简版,即2010年的预积分的皮肤着色(Pre-Integrated Skin Shading),是把次表面散射的效果预计算成一张二维查找表,查找表的参数分别是dot(N,L)和曲率,因为这两者结合就能够反映出光照随着曲率的变化。 预积分的皮肤着色

            float3 R(float r)
            {
                return float3(0.233, 0.455, 0.649) * G(r, 0.0064) + 
                       float3(0.100, 0.336, 0.344) * G(r, 0.0484) + 
                       float3(0.118, 0.198, 0    ) * G(r, 0.187 ) + 
                       float3(0.113, 0.007, 0.007) * G(r, 0.567 ) + 
                       float3(0.358, 0.004, 0    ) * G(r, 1.99  ) + 
                       float3(0.078, 0    , 0    ) * G(r, 7.41  ); 
            }

注意上面贴出的这种图是tonemapping&gamma后的结果,使用时要注意转换。 预积分很适合移动端,因为实时的计算量非常小,直接采样即可~

Real-Time Realistic Skin Translucency

本文的SSSS即SSS透射主要参考Jorge Jimenez的实现。 Real-Time Realistic Skin Translucency论文原文 Jorge的方法在《GPU Gems 3》里半透明阴影贴图(Translucent Shadow Maps,TSMs)的透射计算方法(2007)的基础上进一步进行了提升(2010),但竟然也是12年前的算法了。

像耳朵、鼻孔等较薄的地方受强光照射时,会有明显偏红的透射现象,前面说的种种SSS方法有一定效果但不够理想。有些方法会预计算模型的厚度贴图,作为控制SSS效果的参数。但预计算时不能得知光线的方向,这样求得的厚度其实是不准确的。Jorge提供了一种使用ShadowMap来获取光线方向厚度的方法。

下图中的红线是Jorge的算法,蓝线为《GPU Gems 3》中较早的算法,由于只需要读取ShadowMap的深度值,减少了数据的存储量~ Snipaste_2022-11-14_18-03-56.jpg

先来复习一下灯光利用shadowMap的方式,这与我们厚度的算法其实是非常相似的~ ShadowMap或者DepthMap,是从光线空间对场景的预渲染,存储了光线视角下场景的最小深度Zin。我们将[WorldSpace→LightSpace]这个转换矩阵也一并传入着色器,就可以取得片元在光线空间的坐标,它到光源的距离即是在光线视角的深度Zout。如果Zout小于Zin,那么就说明这个片元与光源之间有遮挡,它应该处于阴影中。 而Zin和Zout的差,其实就是光线穿过物体的厚度!

获取shadow map

在具体实现上,首先我们需要一个从光线视角的Pass,只需写入深度。这里的VertexShader进行场景的坐标转换,而Fragment什么都不用做。

        // 1. shadow pass: depth mapping from light space for shading and thickness calculations
        // -----------------------------------------------------------------
        glViewport(0, 0, 1024, 1024);
        glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
        glClear( GL_DEPTH_BUFFER_BIT );
        shadowPass.use();
        shadowPass.setMat4("lightSpaceMatrix", lightSpaceMatrix);
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f)); // translate it down so it's at the center of the scene
        model = glm::scale(model, glm::vec3(0.05f, 0.05f, 0.05f));  // it's a bit too big for our scene, so scale it down
        shadowPass.setMat4("model", model);
        headModel.Draw();

        glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

这里需要注意一下,我将深度图渲染到了一张固定尺寸(1024*1024)的纹理上,这需要绘制前使用glViewport()命令设置视口。记得在之后的pass前再次使用glViewport()设置回视窗尺寸~ 第一个绘制得到的深度图如下: depth

Thickness计算

在第二个Pass中,我们正常绘制场景,计算光照,并添加透射效果。 厚度计算的代码主要参考了SeparableSSS.h中的SSSSTransmittance()方法。

在Fragment Shader中,我们先计算Light Space下的片元坐标:

    vec4 shrinkedPos = vec4(WorldPos - 0.005*Normal,1.0);
    vec4 LightSpacePos = lightSpaceMatrix * shrinkedPos;

与阴影计算相似地,由于分辨率的限制,可能会出现类似shadow acne的条纹状artifact,所以我们将坐标沿法向进行一个小小的偏移来消除掉误差。 shrinkedPos

至此我们获得的坐标范围是[-w,w],我们将他们归一化到[-1,1],以便和深度图中[0,1]的深度进行比较。

    LightSpacePos.xyz /= LightSpacePos.w;

因为纹理坐标是[0,1]范围内的,所以采样时还要处理一下xy坐标,d1采样后的结果如下:

    float d1 = texture(shadowMap,(LightSpacePos.xy+1)/2.0).r;

d1 深度图中深色代表距离光源更近,而人脸上“阴影”的位置即受到了遮挡,采样到的实际是光线方向上最先hit到的深度。 depthmapping.png

d2则是光线空间下片元的真实深度:

    float d2 = LightSpacePos.z;

d2

采样得到的d1范围是[0,1]但d2的范围是[0,lightFarPlane],我们还要给d1转换一下才能对d1,d2做差。 SSSSwidth是一个全局控制SSS效果的参数,SSSSwidth越大,效果越明显(卷积部分的步长也会增大,如果步长很大采样数又不够的话重影会有些明显!)单位与世界坐标相同; 而translucency是专门控制投射效果的参数。

    d1 *= lightFarPlane;

    float scale = 8.25 * (1.0 - translucency) / SSSSwidth;

    float d = scale*abs(d1 - d2);

这个d就是我们计算到的厚度啦!(虽然已经为了效果缩放了一下!)

Transmittance Profile

这里就就直接贴一下源码啦,用d即可使用高斯和求出拟合的投射颜色。具体数据的原理参见论文(太多数学了我实在看不懂)... 如果对性能要求严格,也可以预积分算好存储到纹理~

    /**
     * Armed with the thickness, we can now calculate the color by means of the
     * precalculated transmittance profile.
     * (It can be precomputed into a texture, for maximum performance):
     */
    float dd = -d * d;
    float3 profile = float3(0.233, 0.455, 0.649) * exp(dd / 0.0064) +
                     float3(0.1,   0.336, 0.344) * exp(dd / 0.0484) +
                     float3(0.118, 0.198, 0.0)   * exp(dd / 0.187)  +
                     float3(0.113, 0.007, 0.007) * exp(dd / 0.567)  +
                     float3(0.358, 0.004, 0.0)   * exp(dd / 1.99)   +
                     float3(0.078, 0.0,   0.0)   * exp(dd / 7.41);

    /** 
     * Using the profile, we finally approximate the transmitted lighting from
     * the back of the object:
     */
    return profile * SSSSSaturate(0.3 + dot(light, -worldNormal));

锵锵!这样我们就得到了很逼真的皮肤投射效果!对d缩放一下可以获得更夸张的投射效果,看起来脑袋像一块医用硅胶(?)! Snipaste_2022-11-15_15-54-54.jpg (右图是已经经过SSSS后处理的结果~) (计算结果在tonemapping和gamma后会更明显一些)

至此我们的SSS效果已经实现一半了,最后一篇真正实现SSSS的第一个S!

Reference:

by ERIN.Z

2024 © typecho & elise