一年前曾嘗試過這個課題,研究的比較淺,最終效果也一般。最近重新搬出這個課題研究,以期獲得更令人滿意的結果。
本文會在Unity的PBR基礎上,依據真實的物理原理嘗試對絲襪這種物品給出一種仿真的渲染方法。原理和計算方面還是比較簡單的,但是也需要一些SurfaceShader基礎。
相關工程文件下載在文末
物理層面的分析在開始編寫Shader前,首先我們需要對絲襪這種物品在現實中的物理性質進行一個具體的分析。
絲襪有著眾多的款式,形狀、功能、材料各有不同,依據我在百度百科和各類淘寶爆款中豐富的調研,最終我選擇了其中最具有代表性,也是現今最常見的尼龍絲襪進行分析。
主要材質為 約90%聚醯胺纖維(錦綸/尼龍)+ 約10%聚氨酯纖維(氨綸)
基本上可以推測這種材質的金屬性(Metallic)為0,而光滑度(Smoothness)則根據種類不同會有較大區別。
各種絲襪的厚薄也有區別,「D」或「Denier」是指纖維的纖度單位。5D、10D幾乎透明,100D以上則幾乎不透,很好理解,請看下圖:
所以Shader中我們就以Denier值表示絲襪的厚薄程度。
機核網的相關文獻:11月2日褲襪之日,是時候研究神秘的褲襪了
我們可以發現一種現象——靠近腿部中央的位置絲襪顏色較淡,並且如果絲襪夠薄甚至會顯出底下皮膚的顏色,而越靠近腿兩側絲襪顏色則會越深。絲襪由無數細小交錯的纖維組成,所以正對視線的纖維是最稀疏的,而越靠近腿部輪廓的纖維則對於觀察者來說越顯得密集。
能否通過渲染出每一根纖維實現絲襪的效果?
幾乎不可行,就像遊戲的毛髮渲染也沒有一根根渲染的一樣,實際纖維的密集程度遠超越現階段電腦遊戲的精度,雖然可以做出大概的意思,但是這些交錯的線條會產生大量摩爾紋而影響觀感。
簡單嘗試一下,效果可以說相當噁心了。不過擴大間距後似乎就接近了漁網襪的效果,當然這不是本文的目標…
摩爾紋
空間頻率相近的兩組圖案相互幹涉,會有更低頻率(更寬間距)的圖案顯示出來。
使用相機拍攝屏幕的照片和掃描圖上常常可以看見,遊戲中因為像素點有限,在光柵化過程中也會出現。
既然基於PBR,新建一個Surface Shader文件,然後依據需求稍微刪改一下:
Shader "Custom/NewSurfaceShader" { Properties { _NormalTex("Normal", 2D) = "bump"{} _Smoothness("Smoothness", Range(0,1)) = 0.5 } SubShader { Tags { "RenderType"="Opaque" } CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _NormalTex; struct Input { float2 uv_NormalTex; }; half _Smoothness; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { o.Albedo = 1; o.Normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex)); o.Metallic = 0; o.Smoothness = _Smoothness; o.Alpha = 1; } ENDCG } FallBack "Diffuse"}
刪除了金屬度等一些不需要的屬性,加入了法線。
我們就以此為基礎,開始我們的絲襪探索之旅吧。
對絲襪纖維的密集程度定義一個屬性 疏密度(Density) ,取值範圍0(幾乎沒有纖維)到 1(完全被纖維覆蓋)。依據疏密度提供顏色給Albedo通道,疏密度越高顯現更多絲襪顏色,越低則顯現皮膚顏色。
計算疏密度需要三部步驟:
丹尼爾值對整體疏密度的影響。
受絲襪良好彈性影響,需要一張灰度貼圖來控制不同區域的拉伸程度,拉伸越大疏密度越低。
受觀察者視線的影響,絲襪平面與觀察者視線角度越小時,疏密度越大。稱其為邊緣度影響。
對於這幾點,我們定義幾個屬性:
Properties{ _Denier("Denier", Range(5,120)) = 25.0 _DenierTex("Density Texture", 2D) = "black"{} [Enum(Strong,6,Normal,12,Weak,20)] _RimPower("Rim Power", float) = 12 _SkinTint("Skin Color Tint", Color) = (1,0.9,0.8,1) _SkinTex("Skin Color", 2D) = "white" {} _StockingTint("Stocking Color Tint", Color) = (1,1,1,1) _StockingTex("Stocking Color", 2D) = "white"{}}
(1),(2) 丹尼爾值與拉伸程度貼圖float denier = (_Denier - 5) / 115;float density = denier * (1 - tex2D(_DenierTex, IN.uv_DenierTex));
將_Denier的數值進行歸一化(Normalize)。_DenierTex是一張灰度貼圖,越淡的地方表示絲襪被拉伸的程度越高,其纖維越稀疏相應會顯示更多皮膚顏色。兩者相乘。
如示例,一般情況下認為膝蓋部分和大腿越靠根部的地方會拉扯的更加厲害。
_Denier 一般只在沒有_DenierTex 貼圖的情況下做調整用,使用_DenierTex 時請保證_Denier 值為最大(120)以正確還原貼圖的效果。
有一種很常見的被稱為 Rim Lighting 的效果,簡單來說就是模型邊緣的內發光效果,簡單來說就是模型邊緣的內發光效果,我們可以參考其中計算邊緣程度的代碼。相關代碼在Unity官方文檔的Surface Shader examples中就可以找到。
根據自己的需求改寫:
float rim = pow(1 - dot(normalize(IN.viewDir), o.Normal), _RimPower / 10);
通過點乘(dot)計算視線方向和法線方向的數量積。視線方向與法線方向完全相反時,即絲襪最中心的部分,取得值為1;而最側邊視線方向與法線方向垂直時,取得值為0。因為我們定義的是邊緣度,所以應該是越靠邊緣值越大,用1與其相減獲得正確的數值。最後對數值進行了(_RimPower / 10) 次方,實際應用時,依據效果調整_RimPower,使邊緣度更符合現實的情況。
最終混合顏色首先需要計算出皮膚顏色(SkinColor) 和 絲襪顏色(StockingColor),很簡單不多解釋。
float4 skinColor = tex2D(_SkinTex, IN.uv_SkinTex) * _SkinTint;float4 stockingColor = tex2D(_StockingTex, IN.uv_StockingTex) * _StockingTint;
依據疏密度對這兩個顏色進行簡單的混合,疏密度越高顯現更多絲襪顏色,越低則顯現皮膚顏色,很簡單的lerp即可。
lerp(skinColor, stockingColor, density);
完整ShaderShader "Custom/Stocking" { Properties{ _Denier("Denier", Range(5,120)) = 25.0 _DenierTex("Density Texture", 2D) = "black"{} [Enum(Strong,6,Normal,12,Weak,20)] _RimPower("Rim Power", float) = 12 _SkinTint("Skin Color Tint", Color) = (1,0.9,0.8,1) _SkinTex("Skin Color", 2D) = "white" {} _StockingTint("Stocking Color Tint", Color) = (1,1,1,1) _StockingTex("Stocking Color", 2D) = "white"{} _NormalTex("Normal", 2D) = "bump"{} _Smoothness("Smoothness", Range(0,1)) = 0.1 } SubShader{ Tags{ "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 struct Input { float2 uv_SkinTex; float2 uv_StockingTex; float2 uv_DenierTex; float2 uv_NormalTex; float3 viewDir; }; float _RimPower; float _Denier; float _Smoothness; float4 _SkinTint; float4 _StockingTint; sampler2D _DenierTex; sampler2D _SkinTex; sampler2D _StockingTex; sampler2D _NormalTex; void surf(Input IN, inout SurfaceOutputStandard o) { o.Normal = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex)); float4 skinColor = tex2D(_SkinTex, IN.uv_SkinTex) * _SkinTint; float4 stockingColor = tex2D(_StockingTex, IN.uv_StockingTex) * _StockingTint; float rim = pow(1 - dot(normalize(IN.viewDir), o.Normal), _RimPower / 10); float denier = (_Denier - 5) / 115; float density = max(rim, (denier * (1 - tex2D(_DenierTex, IN.uv_DenierTex)))); o.Albedo = lerp(skinColor, stockingColor, density); o.Metallic = 0; o.Smoothness = _Smoothness; } ENDCG } FallBack "Diffuse"}
後處理屏幕空間次表面散射 (SSSSS)SSSSS你可能不知道,但是SSS你應該聽過,就是常說的3S材質。這個是在Github上找到的,利用屏幕空間實現的SSS效果。
最後使用Unity官方的Post Processing Stack對畫面進行調整也必不可少的。
連結:
Custom Phase Screen-Space Subsurface Scattering
Post Processing Stack
項目文件
在我的Coding上:
https://coding.net/u/gypsum/p/Unity-UltraStocking/git
裡面還有一個2B夏裝的模型,本來是自己準備調一下效果的,後來覺得麻煩算了...
作者:石膏 來源:簡書