參考:(1)https://zhuanlan.zhihu.com/p/72161323
(2)https://zhuanlan.zhihu.com/p/56052015
(3)https://www.slideshare.net/leegoonz/penner-preintegrated-skin-rendering-siggraph-2011-advances-in-realtime-rendering-course
(4)https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch14.html
(5)https://www.w3cschool.cn/ngyasc/q9gm7ozt.html
(6)http://filmicworlds.com/blog/filmic-tonemapping-operators/

主要講上面這張圖是怎么生成的。
在Pre-integrated Skin Shading(這篇文章收錄在《GPU Pro2》和《GPU Gem3》中,還有一個PPT里面說了這張圖的來源。
次表面散射
一些說法是,從光源發出的光進入物體內部,經過多次反射、折射、散射及吸收后返回物體表面的光,指的是Diffuse reflection,但在《Real Time Rendering》中把Diffuse Reflection稱為Local Subsurface Scattering。
次表面散射,大家都知道是光進入物體內部一通操作后又飛出來,local SSS指的是飛出去的點與最初進入的點之間的距離非常小,可以忽略不計,當作原地飛出。

有了local就有Global,Global SSS指的是兩點之間的距離無法忽略,比着色的最小單位要大(最小單位可以認為是光柵化后的一個像素)。

如上圖,小圈內的是local SSS,大圈內的是Global SSS。
積分

這張圖來自這個ppt:https://www.slideshare.net/leegoonz/penner-preintegrated-skin-rendering-siggraph-2011-advances-in-realtime-rendering-course
圖上這個方程是

gpu pro2的方程是

可以看出(2)比(1)多一個r,那么哪個是正確的呢?方程a可能是方程b的r取1時的結果,那么a就先不算錯了。
正確方程是

下面來推導,

假設p是屏幕上一點,由於皮膚具有次表面散射的特性,P受到P周圍一定范圍內的其他點的散射影響,假設是半圓APB的影響。(參考文檔2中,認為法線反方向的半球相當於皮膚背面,是不受光照的)。
假設L方向光的輻射度總和位1,取半圓上任意一點Q,則Q點的直接光照亮度為:

法線與OQ之間的夾角為x,法線與L方向的夾角為theta。假設Q點對P點的散射率為q(x),則Q點散射到P點后的亮度為:
半球上有無限個這樣的Q點,我們把這些Q點對P點的貢獻做個積分就是P點最后的亮度:

上式(1)中,只有Q點對P點的散射率q(x)是未知的,我們來求q(x)。
根據次表面散射的特征(后面會講,擴散剖面是與距離有關的),離發射光的點越遠的點,受到發射光的點的散射的影響應該越小,所以Q點對P點的散射率應該是一個與距離有關的函數。設d是QP的弦長,則

散射是對稱的(后面會講,擴散剖面計算是用的高斯函數,是對稱的),也可能是散射是跟距離有關的,所以,Q對P的散射率和P對Q的散射率相等。假設P點受到半圓上其他各點的影響,P點也會影響他周圍的半圓上的點。
根據能量守恆定律,P點在整個半圓上的散射率總和應該等於1,所以,實際是一個概率密度函數,滿足:

我們的目的是找到q(x),他既滿足次表面特征函數(下面會講,擴散曲面的函數R(d),https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch14.html 可參考這篇文章),也要滿足能量守恆公式(2).
但我們不能之間讓q(x)=R(d),因為可能不滿足公式(2),即積分不為1。
我們可以令q(x)=kR(d),帶入(2),得到:

上式代入(1),有

到這兒,就得推導出了開始的公式。
總結:1.得到能量守恆公式(2)的物理解釋是關鍵 ;2. diffusion profile是基於距離的,不能用弧長代替弦長;3.對x的積分區域為(-pi/2, pi/2);4.實踐中,應該使cos(theta+x)大於0,因為光亮度不應該為負。
擴散剖面
上面提到了diffusion profile,gpu gem3這篇文章說出,高斯函數的和公式被用來近似擴散剖面。對每個擴散剖面R(r),我們用k個不同權重w,不同方差的高斯和表示:

變量v的高斯定義為

參考鏈接5中說,

正態分布的密度函數叫做"高斯函數"(Gaussian function)。它的一維形式是:

其中,μ是x的均值,σ是x的方差。因為計算平均值的時候,中心點就是原點,所以μ等於0。

根據一維高斯函數,可以推導得到二維高斯函數:

參考文檔4中說,6個高斯的和能精確的模擬皮膚的三層模型。Gaussian參數如下,

6個高斯的方差是相同的,權重不同。r,g,b的權重不同。
看參考文檔1中的代碼
def G2(Neg_r_2, v):
v2 = 2.0 * v
return 1.0/(v2 * math.pi) * math.exp(Neg_r_2/v2)
# Sum( w * G(v, r) )
def Cal(distance,G):
Neg_r_2 = -distance*distance
rgb = array([0.233,0.455,0.649]) * G(Neg_r_2 , 0.0064)+\
array([0.100,0.336,0.344]) * G(Neg_r_2 , 0.0484)+\
array([0.118,0.198,0.000]) * G(Neg_r_2 , 0.1870)+\
array([0.113,0.007,0.007]) * G(Neg_r_2 , 0.5670)+\
array([0.358,0.004,0.000]) * G(Neg_r_2 , 1.9900)+\
array([0.078,0.000,0.000]) * G(Neg_r_2 , 7.4100)
return rgb
G2函數對應高斯公式,v是方差,Cal函數里的每個array代表r、g、b通道的不同權重。
這里求出來的顏色,正是上面D(theta)函數里的R(x),也就是某一點的散射率權重。
filmic-tonemapping
積分求出來后,需要進行filmic-tonemapping,參考這篇文章http://filmicworlds.com/blog/filmic-tonemapping-operators/,
將上面求出來的積分結果,進行如下計算:
float A = 0.15;
float B = 0.50;
float C = 0.10;
float D = 0.20;
float E = 0.02;
float F = 0.30;
float W = 11.2;
float3 Uncharted2Tonemap(float3 x)
{
return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}
float ExposureBias = 2.0f;
float3 curr = Uncharted2Tonemap(ExposureBias*Color);
float3 whiteScale = 1.0f/Uncharted2Tonemap(W);
float3 color = curr*whiteScale;
float3 retColor = pow(color,1/2.2);
return float4(retColor,1);
乘以255.
上面的結果乘以255,返回:
rgb = multiply(rgb,array([255,255,255]))
return (int(rgb[0]),int(rgb[1]),int(rgb[2]))
運行python生成lut貼圖
參考文檔1中的python文件運行:安裝python,安裝pip,安裝PIL庫,安裝numpy庫(可參考這篇文章https://www.cnblogs.com/Shaojunping/p/11641778.html),運行,生成lut。它的python代碼里用的還是-pi到pi的積分(如果安裝參考鏈接2所說,此處應該改成-pi/2, pi/2).
將生成的lut在ue4中使用:
參考uod2019的文章《常見材質效果在移動端實現的思路分享》,使用上面生成的lut貼圖,加到材質的自發光上:

這張貼圖的tiling 模式:



