简谈实时皮肤渲染之预积分SSS

前言

纵然前文提及的Disney BRDF可以用来描述(渲染)世间绝大多数不透明几何物体,但是一套BRDF也绝不是灵丹妙药,解决不了所有渲染上的需求。比如角色渲染中非常重要的皮肤处理,人们一般不会自缚手脚将通用的PBR渲染方程套用在这类“特殊”材质上(需求和判断标准与传统BRDF渲染结果有较大差异)。本文简述了一种主流的基于预积分次表面散射(SSS)的渲染方案,介绍中会提及SSS效果的成因,以及如何利用数学模型对这种物理现象进行描述,另外还结合《GPU Pro 2》中的“Pre-Integrated Skin Shading”一文简要介绍了对次表面散射的预积分模型;同时又结合了《GPU Gen 3》中相关内容,对一种皮肤高光处理的预积分模型也进行了梳理;最后我们在Unity URP管线下展示了基于预积分技术对面部皮肤进行实时渲染的效果。

什么是SSS

SSS既Subsurface Scattering,次表面散射,其实在前文介绍有关PBR的一些重要概念时已经概略的提及过,是一种在微观尺度上对物体表面漫反射光的解构。我们知道当一束光线照射到材质表面,一部分光(光能)被立即镜面反射回了空气中,另一部分进入物体内部,要么被吸收,要么再次发生折射或反射。对于不太透明的电介质来说,比如人的皮肤,这些折射光不太可能直接从另一侧透射出人体,更可能的是,在某一层介质中经过了多次光路改变,最后出射光以一个相对随机的角度再次回到入射光一侧的空气中,成为漫反射。这里有2处需要额外说明的地方:

  • 其一是随机性,由于材质内部介质通常是不连续的,如皮肤有油脂层,表皮层,真皮层,以及大量毛细血管和里面的血液构成,光路在内部的折射/反射往往是无规律可循的,导致最终出射时方向的随机化;
  • 其二是电介质的着色性,既漫反射光会被染上颜色,这是因为光线在物体内部传播时,一部分波长的光能被吸收所致(通常是金属成分中的自由电子),比较典型的是人体皮肤中血液的铁离子吸收了蓝绿波长的光谱,从而形成表皮翻红的染色效果。

借用一下网上的示例图:


sss_local

漫反射来自于这些重新回到空气中的折射光。当观察尺度足够小,我们便不能近似的认为这些漫反射光的出射点恰好位于入射光所在的入射点处,而是如上图所示,出射光被认为分布在入射光周围一定范围内(绿色圆圈)。在图像渲染领域,因为颜色的离散化(既光路以及颜色相关的值最终都会通过积分,以离散化数值的形式记录到像素点上),所以如果上图中绿色圆圈的区域恰好落在某个像素内,那么反映的宏观表现层面,我们看到的依然是普通漫反射效果。

那么在一般观察距离上,会不会有“绿圈”兜不住一个像素的情况呢?其实挺常见的,举个例子,我们知道人的皮肤一般可以分为表面油脂层,上皮层和下皮层(真皮层),当光线穿透油脂和上皮层,遇到人体皮肤中富含血管和组织液的真皮部分,光线很可能会沿着局部的均质(血液,组织液)做较长范围的折射迁移。

layer of skin

如果一速光线能够沿着路径在入射介质中传播足够远时,我们就正式进入了次表面散射的世界。

sss_nonlocal

如上图所示,这种情况下,我们必须考虑因长距离传播再出射而导致的着色差异。我们不再将计算局限于当前像素(图中绿圈)所在的范围,此时材质上(入射光点附近区域内)其他各个方向的影响分量都需要纳入考量,并反映到出射点光亮度的计算过程中。

关于BSSDF

在了解了SSS效果成因后,我们也许会好奇这么复杂的现象,如何用简练的数学语言描述清楚,进而指导我们构建渲染方程呢?类似于 BRDF,人们专门为SSS引入了新的方程,全称是 Bidirectional Surface Scattering Distribution Function(双向次表面散射分布方程),也可以简称为 BSSDF,该方程最早源自2001年由Jensen发布的论文,其核心概念可以简单总结为如下公式:

BSSRDF

等式左侧的积分输出是出射位置和出射角度下的光亮度Lo。为了得到Lo,我们先从最右侧积分看起:

在给定某一个面积微元A的前提下,对半球空域Ω所有入射光的光亮度积分,这种积分需要考虑光线对表面投影造成的衰减(i dot ni),当第一轮积分做完,我们得到的是单位面元(像素)的辉度值(irradiance)。

之后将积分所得辉度带入到外侧积分中理解:我们很容易可以看出,这部分是对面积A进行积分,被积函数是R与半球空域辉度值的乘积。仔细观察法线,函数R的入参是|xi - xo|,代表了从入射点到出射点之间距离。多提一嘴,积分函数R的本质是一个模糊函数(Blur Kernel),用于近似我们后文会提及的散射方程Scattering(xi, i; xo, o)。为啥?当然是因为完整的散射方程十分且非常且尤为复杂,它既要考虑入射和出射的位置,还要考虑入射和出射的角度,是个高纬方程。而我们的R函数之所以能够成立,是因为我们假设了物体表面是各向同性的(isotropic),如此一来,我们可以认为散射方程只受到光线在材质内传播的距离|xi - xo|影响,既 Scattering(|xi - xo|),而和具体入射和出射的方位无关。R的选择多种多样,可以是Gaussian kernelsmoothstepcubic function或者是Disney的normalized diffusion,但最好保证能量的守恒,既对整个A区域积分这些核的结构应该等于1,同时积分结果也要符合人眼对于目标材质的预期。

次表面散射中的预积分皮肤概念

如BSSDF方程所示,如果要考虑SSS效果,并得到某一个点处沿着某个方向发射的光强度,那么就得老老实实对整个区域的每个点积分,这显然非常的“昂贵”。因此自然有人开始尝试预积分技术,将BSSDF的部分或整体预先计算出来,并以一定方式保存进纹理。我们来参考下来自《GPU Pro 2》中由Eric Penner和George Borshukov提出的预积分技术的部分相关内容:

The obvious caveat of pre-intergation is that in order to pre-integrate a function, we need to know that it won't change in the future. Since the incident light on skin can conceivably be almost arbitrary, it seems as though precomputing this effect will prove difficult, especially for changing surfaces. However, by focusing only on skin rather than arbitrary materials, and choosing specifically where and what to pre-integrate, we found what we believe is a happy medium. In total, we pre-integrate the effect of scattering in three special steps:
* on the lighting model
* on small surface details
* on occluded light(shadows)
By applying all of these in tandem, we achieve similar results to texture-space diffusion approaches in a completely local pixel shader, with few additional constraints.
To understand the reasoning behind our approach, it first helps to picture a completely flat piece of skin under uniform directional light. In this particular case, no visible scattering will occur because the incident light is the same everywhere. The only three things that introduce visible scattering are
* changes in the surrounding mesh curvature,
* bumps in the normal map,
* and occluded light(shadow).
We deal with each of these phenomena seperately.

上述文章的片段概述了预积分技术在次表面散射方面的局限,为了确保预计算的东西不被模型和表面参数所束缚,就必须限定积分对象的材质(这里自然是指皮肤),并且仅预计算那些不变的组成部分。

至于SSS的形成条件及处理步骤,这边稍作整理用中文再输出一边重点:

对宏观上平坦的表面,由于平行光均匀分布于各个点,我们是无法察觉到次表面散射现象的,要达成这一现象必须满足3个要素,分别是有弧度变化的模型,有法线的扰动以及存在光线遮挡;而预积分也分为3个步骤,分别是处理光照,处理微表面以及处理遮蔽。对于3个步骤,我个人的理解是:

  • 处理光照 -> 确定Li 入射光函数
  • 处理微表面 -> 确定散射分布函数 S 或者 R
  • 处理遮蔽 -> 计算光照的投影 N dot L

文章片段还提及了形成SSS效果的3条重要因素:

  • 模型弧度变化 -> 统一了模型的尺寸和形状2个维度,对于人体而言能用一个标量很好的区分诸如脸颊、耳鼻等不同部位的SSS特征。
  • 法线扰动 -> 表面法线完全没有扰动的话,那么微表面区域完全镜面和平坦,SSS失去了形成细节特征的条件;如果表面法线扰动过于锐利,那么看起来效果会不好(像非散射的边缘效果)。
  • 光线的遮挡 -> 就是说必须包含阴影,从而形成局部由暗至明的转向,避免全方位的亮光造成SSS效果无法被察觉。

如何理解3要素呢?我相信很多人可能和我一样,对“模型需要有弧度的变化”印象最为深刻,毕竟它将微观上的SSS现象同物体的宏观形状关联了起来。这乍听起来有点不可思议,然而深究其原因,连同另外2个要素一起思考很容易发现,它们表示的都是一个意思,既:在物体表面要有明显的光强度变化,要能够形成明和暗部。

sss effect

一种预积分SSS技术

讲解了半天各种前因后果,也许你会好奇到底SSS效果是如何通过预积分方式在实时渲染过程中体现的呢?别急,这节我就和大家一起来梳理下《GPU Pro 2》提出的预积分技术原理和实现。首先大家需要注意所谓预积分积分的是皮肤表面漫反射分量,是基于BSSDF进行的,这点和BRDF着重于Specular是不同的(皮肤高光我们后面会谈到)。此外不得不感谢网络上的幽玄大神对公式的细心推理,本节大部分数学推导是基于他的工作进行的。

我们先从原文作者给出的对SSS这一物理现象的数学模型示意图开始吧:

SSS Distribution

我先简要归纳下图中要点。

  • 左图表示BSSDF中的散射函数R,入参x应当是一个角度标量,可以通过关联半径r转换成从入射点到出射点的距离(Distance),而返回值(既纵坐标)是对应距离的贡献强度。
  • 右图中的圆形代表抽象的几何模型,你可以把这圆想象成角色头模。
  • 右图中由法线N与入射光线L所构成的夹角定义为θ
  • 右图中圆形的半径为r
  • 红绿蓝色的包络线代表不同R函数(但是能量守恒),是以当前点(法线N与圆的交点)为中心,对周围各点计算光亮度贡献所得的曲线。
  • 右图中N+θ表示从法线N开始逆时针旋转θ角对于的位置。

下面是作者Eric给出的积分公式,我这边采用幽玄大神重解读后的改版,与原本相比,积分区间缩小到了半球空域,同时显式得引入了模型半径r。注意原始公式是为了描述关于距离的一维函数(上图左侧Profile函数),所以r被默认设置成了1,我们最终的积分函数是需要包含rdistance(或者角度x)这2个控制维度的。

SSS Distribution Formula

因为式子中已经提出了作为常量的入射光强 Li,为了便于理解,我们姑且认为其值为1。如下图所示,对于圆形上任一点Q,假设圆心OQ构成的向量QO与法线N的夹角为xx可以在-π/2+π/2之间取值)。那么该点的入射光强度可以简单得表示为:Lq = Li* (OQ dot OL) = 1 * cos(x + θ)

SSS Distribution 2

设法线N与圆的交点为P,这是我们的积分目标点,所有围绕在点P周围半球空域内的表面积都需要被积分公式舔一遍,加总它们对点P的贡献度。Q点我们抽象出来的无数被积点之一,假设QP的散射率是q(x),前面说过,xOQ与法线N的夹角,它随Q的移动而变化。采用微分思想,我们视点Q所在的表面弧长为△x,那么这段小区间所切走的概率密度蛋糕应当是 q(x)△x,然后乘以Q点接受到的光亮度,我们可以得到该处△x区域提供给P点的散射分量(既有多少光线从Q点所在的△x区域折射进物体内部,然后传播到了P点并出射),公式如下:

scattering contribution

我们对每一个符合要求的点Q做同样的事情:

(1)

前面我们引入了概率密度函数q(x),它本质上就是图(SSS Distribution)中的散射分布函数R,只不过这里的入参使用的是角度x。现在我们希望使用切线距离(distance)作为函数的入参,从xd的转换公式如下,应用了一点三角函数和中学几何知识,这里不再赘述。

x to distance

我们还知道散射分布函数 R需要保证能量守恒,也就是如下积分结果:

(2)

理解所谓的能量守恒需要一些技巧,我们并不是说来自所有半球面积上的微元分量△x加总的贡献度等于100%的当前点P的入射能量,这是不合理也不可能的,上式的物理意义在于说明,对于所有符合条件的当前点P,计算他们周边区域的散射贡献度总和应当是一个确定的常数,比如此处的常数1。更深一层的含义是:只要给定某种材质,那么光能在材质内沿着特定方向传播的概率和损耗与观察位置无关,只与传播的距离有关。

其实q(x)长什么样并不重要,重要的是

  1. 分布方程需要让最终结果看起来还不错。
  2. 分布方程需要满足“能量守恒”。

假设我们找到了一个满足条件公式(1)的分布函数R(d),为了使之也满足条件公式(2),我们不妨令q(x) = k*R(d),其中k是归一化因子,确保分布R在半球积分后能量守恒。
于是我们可以很容易带入积分公式,求取k的表达式,以及qR直接的关系:

R to q

q(x)以R形式的表达式导入到公式(1)式中,这样同时考虑到了各点的光亮度:

Distruibution of theta

上式最后就是《GPU Pro 2》相关文章中提出的公式原型,注意这个公式中,只有θr是变量。

将上述公式转化为代码,可以求取不同曲率和角度下的散射分布,并将结果记录到一张完整的纹理上:

private void PreIntegrateSSSLUT(Texture texture)
{
    for (int j = 0; j < texture.height; ++j)
    {
        for (int i = 0; i < texture.width; ++i)
        {
            float NDotL = Mathf.Lerp(-1f, 1f, i / (float)texture.width); 
            float oneOverR = 2.0f * 1f / ((j + 1) / (float)texture.height);  
            //Integrate Diffuse Scattering
            Vector3 diff = Integrate(NDotL, oneOverR);
            texture.SetPixel(i, j, new Color(diff.x, diff.y, diff.z, 1));
        }
    }   
}

private Vector3 Integrate(float cosTheta, float skinRadius)
{
    float theta = Mathf.Acos(cosTheta);  // theta -> the angle from lighting direction
    Vector3 totalWeights = Vector3.zero;
    Vector3 totalLight = Vector3.zero;

    float a = -(Mathf.PI / 2.0f);

    const float inc = 0.05f;

    while (a <= (Mathf.PI / 2.0f))
    {
        float sampleAngle = theta + a;
        float diffuse = Mathf.Clamp01(Mathf.Cos(sampleAngle));

        // calc distance
        float sampleDist = Mathf.Abs(2.0f * skinRadius * Mathf.Sin(a * 0.5f));

        // estimated by Gaussian pdf
        Vector3 weights = Gaussian(sampleDist);

        totalWeights += weights;
        totalLight += diffuse * weights;
        a += inc;
    }

    Vector3 result = new Vector3(totalLight.x / totalWeights.x, totalLight.y / totalWeights.y, totalLight.z / totalWeights.z);
    return result;
}

如何使用预积分LUT

我们先看看积分结果被保存到2D纹理后的样子,如下图:

pre-integrated lut

横坐标是NdotL,对应上节最终公式里的cosθ;纵坐标是1/r,对应公式中的模型半径r的倒数,也可以理解为物体当前点附近的曲率。我们知道uv坐标的定义域是[0,1],而NdotL1/rr>=1)恰好是符合[0,1]区间的定义,这使得将预积分数据压缩在一张2D纹理中成为可能。

那么如何在运行时采样取值呢?需要处理好如下2个采样维度:

  • N dot L
    这部分相对简单,对于主平行光,L应当能够直接从引擎中获取到;法线N建议使用法线贴图获取,一方面是为了更好的精度和细节表现,另一方面是我们可以视情况对法线贴图进行预处理(滤波器模糊或者使用mipmap技术),之所以要模糊法线贴图,理由原作者的解释是“避免看起来过于僵硬的边界出现”,我想为了更好的艺术效果,大家还是尽量让法线的分布尽量均匀连续,避免过于陡峭的“边界”出现。

  • 1/r
    其本质是半径为r的圆上任意一点的曲率,我们已知1/r定义在[0,1]区间,这就意味着r的取值范围被规定在[1, ∞]区间上,当r递减至最小单位1时,1/r趋向于1,从pre-integrated LUT图来看,散射能量在各个光照角度都相对提高了,且在可以类比下人脸上的鼻子和耳廓等处在光线照射下阴影附近透红的效果;另一方面当r非常大时,1/r趋向0,LUT采样的结果更加接近一般光照下的明暗交替,无明显SSS效果。

那么我们如何在运行时获取到1/r呢?考虑到采样LUT图时的运行上下文环境,可以得出一个很简单的方法:尝试找到局部曲率,然后把曲率映射成为球体半径r,最后用1/r采样LUT。于是重点就变成了如何寻找这个局部曲率,特别是如何在像素着色器中找到它:

scattering param 1/r

Eric 给出了一个很巧妙的方法:先求取当前像素点对应法线的局部4领域差值△N(相当于求导数),然后比上模型顶点坐标相对于其周围4领域像素的差值△p(也相当于求导数),根据如上图左侧所示的等比三角形法则,所得结果就是 1/r 的近似。
计算代码如下:

float cuv = saturate(_CurveFactor * (length(fwidth(worldNormal)) / length(fwidth(worldPos))));

其中

fwidth(v) = abs(ddx(v))+ abs(ddy(v))

特别得:

ddx(v) = 该像素点右边的v值 - 该像素点的v值
ddy(v) = 该像素点下面的v值 - 该像素点的v值
fwidth(Position) -> 该像素与相邻两个像素的坐标位置的差值
fwidth(Normal)  ->  该像素与相邻两个像素的法线的差值

而_CurveFactor用于调整比值的强度,用于影响最终效果。

实时渲染的片元着色器代码

float curve = saturate(_CurveFactor * (length(fwidth(worldNormal)) / length(fwidth(worldPos))));
fixed NDotL = dot(blurNormalDirection, lightDirection);
fixed4 sssColor = tex2D(_SSSLUTTex, float2(NDotL * 0.5 + 0.5, curve)) * _LightColor0;

当然我们也可以走捷径,预先将模型表面各点的曲率烘焙到uv贴图上,然后运行时直接采样获得1/r的计算结果。

计算皮肤的高光

人们经过测量发现只有很少一部分的光在接触到皮肤表面后会被镜面反射,这种反射光具有若干性质,可以归纳如下:

  • 反射光不带有任何颜色信息,这是电介质作为反射层的固有特性;
  • 反射光中的大部分来自于皮肤最表面那很薄的一层油脂层(Thin Oily Layer)
  • 反射光遵循菲尼尔效应,掠射角处反射量增大

我们还知道人类皮肤的反射光不可能构成干净又存粹的镜面像(吴克的头?),这是由于皮肤的微表面其实并不平整,所有反射光线会分布到一个范围内,很显然要描述好皮肤的Specular必须要引入BRDF(bidirectional reflectance distribution function双向反射分布函数),那么如何选择合适的BRDF呢?本文的后续内容将依据《GPU Gem 3》第14.3节内容,简单梳理一下基于Kelemen/Szirmay-Kalos BRDF模型的皮肤高光渲染方案。

首先我们回顾下PBR高光模型的一般实现形式,它由直接入射光强度(可能被遮蔽影响),BRDF项,还有一个余弦函数( 控制入射光能的衰减强度)组成,具体形式参考如下代码:

specularLight += lightColor[i] * lightShadow[i] * rho_s * specBRDF( N, V, L[i], eta, m) * saturate( dot( N, L[i] ) );

上述代码中的rho_s是一个引入的“非物理”因子用于控制整体强度,而specBRDF一般是由菲尼尔项F,几何遮蔽项G,还有法线分布项D构成的,需要用到的入参有:法线方向N, 视方向V,光方向L,菲尼尔系数F0,以及反应粗糙度的m项。

首先是菲尼尔项F,和Disney BRDF一样《GPU Gem 3》也采用了Schlick菲尼尔近似,它有如下代码形式,简单理解就是当视方向V越接近掠射角(与微表面法线H垂直)所形成的反射光照强度越高。

float fresnelReflectance( float3 H, float3 V, float F0 ) 
{   
    float base = 1.0 - dot( V, H );   
    float exponential = pow( base, 5.0 );   
    return exponential + F0 * ( 1.0 - exponential ); 
}

另外代码中的F0表示当皮肤遇到垂直入射光照时的反射率,《GPU Gem 3》中使用的是测量常数0.028。

下面是G项和D项,结合论文 《A Microface Based Coupled Specular-Matte BRDF Model with Importance Sampling》所提出的一种简化版 Cook-Torrance模型BRDF:

Cook-Torrance BRDF

公式中的 P 项代表微表面法线分布的概率密度,也就是传统意义上的D项。而右侧分子上的F就是菲尼尔项,余下的部分是简化后的G项,没错,整个Cook-Torrance模型中的G项连同 1/(4(NdotL)(NdotV))因子被合并简化成了 1/(hdoth),而h代表的是(V+L),既半角向量未被归一化前的样子。

我们现在已经明确G项和F项的公式,这部分计算可以放在PS阶段处理,一来是因为计算消耗不大,而来也是因为涉及多项入参,特别是光照方向L和物体表面的菲尼尔系数F0,这些量变化范围广,很难压缩到速查表里。为啥要强调这些呢?那是因为我们希望尽可能预计算公式中的复杂部分,以供实时快速查询和取用,比如余下我们还没说得P项(D项),因为只涉及到微表面法线H一个控制变量,非常有利于我们创建速查表。举个例子(实际处理并非如此):我们有一张2D的纹理,让uv各自代表球面坐标系中的天顶角和方位角,就能模拟出H的朝向,然后以uv组合为索引,采样纹理获得预计算好的概率密度值。

再来看Kelemen/Szirmay-Kalos在论文中引用的的P项表述,其本质是Beckmann distribution:

Beckmann distribution

式中α表示宏观法线N与微表面法线H的夹角。符号m代表当前面元的粗糙度,可以通过对各个方向梯度值求取均方根(root mean square)来得到,但在实际工程中一般是由导入的粗糙度贴图采样获得。所以上式是一个由夹角α以及粗糙度m一共2个维度变量控制的函数。

We employ a similar approach to efficiently compute the Kelemen/Szirmay-Kalos specular BRDF, but instead we precompute a single texture (the Beckmann distribution function) and use the Schlick Fresnel approximation for a fairly efficient specular reflectance calculation that allows m, the roughness parameter, to vary over the object.

上述片段来自于《GPU Gem 3》,表述了作者对Beckmann分布函数预处理以便生成速查纹理的设想,如下代码同样摘录自书中,是对编码纹理的实现:

float PHBeckmann( float ndoth, float m ) 
{   
    float alpha = acos( ndoth );   
    float ta = tan( alpha );   
    float val = 1.0/(m*m*pow(ndoth,4.0))*exp(-(ta*ta)/(m*m));   
    return val; 
} 

// Render a screen-aligned quad to precompute a 512x512 texture.    
float KSTextureCompute(float2 tex : TEXCOORD0) 
{   
    // Scale the value to fit within [0,1] – invert upon lookup.    
    return 0.5 * pow( PHBeckmann( tex.x, tex.y ), 0.1 ); 
}

很简单,PHBeckmann方法负责计算Beckmann分布,注意其入参为ndothm,其中m不必多说,ndoth是宏观法线N与微表面法线H的夹角的余弦值,具体计算时需要使用反余弦函数acos处理出真实α夹角。方法KSTextureCompute是对纹理的编码,主要利用了uv轴[0,1]区间的特性,将y=cosα以及y=m在这个区间上展开,带入PHBeckmann方法进行计算。有2点需要注意,其一是粗糙度m需要确保提前转化到[0,1]区间;其二是我们需要将计算结果同样压缩到[0,1]区间中,具体方法参考代码。

实时取用的方法参考如下代码,同样很简单,共分为3个步骤:

  • 首先是准备诸如hH以及ndoth等中间变量;
  • 其次是采样预计算纹理,注意采样uvfloat2(ndoth,m),同时注意要反压缩采样获得的数据;
  • 最后是参考Kelemen/Szirmay-Kalos提出的简化版BRDF,装配上GF项和P项。
float KS_Skin_Specular( 
    float3 N,     // Bumped surface normal    
    float3 L,     // Points to light    
    float3 V,     // Points to eye    
    float m,      // Roughness    
    float rho_s,  // Specular brightness    
    uniform texobj2D beckmannTex ) 
{   
    float result = 0.0;   
    float ndotl = dot( N, L ); 
    if( ndotl > 0.0 ) 
    {    
        float3 h = L + V; // Unnormalized half-way vector    
        float3 H = normalize( h );    
        float ndoth = dot( N, H );    
        float PH = pow( 2.0*f1tex2D(beckmannTex,float2(ndoth,m)), 10.0 );    
        float F = fresnelReflectance( H, V, 0.028 );    
        float frSpec = max( PH * F / dot( h, h ), 0 );    
        result = ndotl * rho_s * frSpec; // BRDF * dot(N,L) * rho_s  
    }  
    return result; 
}

下图展示了预计算速查纹理(右),以及对人像采样该图后直接输出的效果(左)

precomputed p term

DEMO

TODO...[稍后放出]

Reference

[1] Advanced Techniques for Realistic Real-Time Skin Rendering
[2] Pre-Integrated Skin Shading
[3] Pre-Integrated Skin Shading 数学模型理解
[4] Pre-Integrated Skin Shading实现笔记
[5] GPU Gems 3 全书提炼总结

你可能感兴趣的:(简谈实时皮肤渲染之预积分SSS)