上一次博客中實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的PBR,既然提到了PBR笔横,又怎么能不提一下3S(Subsurface Scattering约计,次表面散射)。在Disney最初的論文里牙甫,3S只是PBR材質(zhì)中的一個(gè)變量掷酗,名叫subsurface,通過這個(gè)來控制次表面散射的程度窟哺。然而到了實(shí)時(shí)渲染領(lǐng)域泻轰,特別是游戲領(lǐng)域,這個(gè)東西被單獨(dú)提了出來且轨,相應(yīng)發(fā)展出了很多種技術(shù)來實(shí)現(xiàn)它浮声。理論這里我也不講了,《GPU Gems 3》:真實(shí)感皮膚渲染技術(shù)總結(jié)已經(jīng)講得非常棒了旋奢。
從實(shí)現(xiàn)上來說泳挥,有基于紋理空間的模糊,有基于屏幕空間的模糊至朗,有改進(jìn)改進(jìn)半透明陰影貼圖(Translucent Shadow Maps屉符,TSMs),有預(yù)積分的皮膚著色(Pre-Integrated Skin Shading)锹引,有結(jié)合延遲渲染技術(shù)(Deferred Single Scattering)的矗钟,還有最新的是路徑追蹤次表面散射(Path-Traced Subsurface Scattering),這種區(qū)別于傳統(tǒng)的光柵圖形學(xué)嫌变,用了光線追蹤技術(shù)吨艇,是基于Ray Marching的解決方案。
本文并非要實(shí)現(xiàn)以上技術(shù)腾啥,而是實(shí)現(xiàn)由動(dòng)視暴雪于2013年首先應(yīng)用的技術(shù)东涡,2年后他們把這種技術(shù)寫成論文,稱作Separable Subsurface Scattering倘待,可以叫它4S技術(shù)疮跑。它也是一種基于屏幕空間模糊的技術(shù),不過相比于之前的屏幕空間技術(shù)凸舵,它大大降低了消耗祸挪。原來的技術(shù)需要6次高斯模糊,而一次模糊需要x贞间,y方向都做一個(gè)pass贿条,6次就要12個(gè)pass來滿足需要”⒎拢現(xiàn)在4S技術(shù)只需要2個(gè)pass來做模糊,所以成為了現(xiàn)在游戲業(yè)界的主流技術(shù)整以,Unreal也對(duì)此進(jìn)行了集成胧辽。
另外,我參考(抄襲公黑?)了separable-sss邑商、Unity_ScreenSpaceTechStack、separable-sss-unity
以及Unity-Human-Skin-Shader-PC這四個(gè)項(xiàng)目凡蚜,才最終實(shí)現(xiàn)了4S人断,不過其中4S的核心技術(shù)我也不是很明白,屬于別人怎么做我也怎么做的階段朝蜘,如果以后搞懂了恶迈,可以再回來解釋。
首先谱醇,既然4S是一種基于屏幕空間的技術(shù)暇仲,那么到Unity里就是后處理效果了。本質(zhì)上這種技術(shù)就是屏幕空間模糊副渴,那么我們先創(chuàng)建一個(gè)后處理文件掛在Camera上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class SubsurfaceScatterPostProcessing : MonoBehaviour
{
[Range(2,50)]
public int nSamples = 25;
[Range(0,3)]
public float scaler = 0.1f;
public Color strength;
public Color falloff;
Camera mCam;
CommandBuffer buffer;
Material mMat;
private static int SceneID = Shader.PropertyToID("_SceneID");//用一個(gè)數(shù)代表現(xiàn)當(dāng)前RT,_SceneID沒有用在任何地方奈附,這樣返回的數(shù)不會(huì)和其他沖突
private static int SSSScaler = Shader.PropertyToID("_SSSScaler");
private static int SSSKernel = Shader.PropertyToID("_Kernel");
private static int SSSSamples = Shader.PropertyToID("_Samples");
private void OnEnable() {
mCam = GetComponent<Camera>();
mCam.depthTextureMode |= DepthTextureMode.Depth;
mMat = new Material(Shader.Find("Unlit/SSS"));
buffer = new CommandBuffer();
buffer.name = "Separable Subsurface Scatter";
mCam.clearStencilAfterLightingPass = true;
mCam.AddCommandBuffer(CameraEvent.AfterForwardOpaque,buffer);
}
private void OnPreRender() {
Vector3 normalizedStrength = Vector3.Normalize(new Vector3(strength.r,strength.g,strength.b));
Vector3 normalizedFallOff = Vector3.Normalize(new Vector3(falloff.r,falloff.g,falloff.b));
List<Vector4> kernel = KernelCalculator.CalculateKernel(nSamples,normalizedStrength,normalizedFallOff);
mMat.SetInt(SSSSamples,nSamples);
mMat.SetVectorArray(SSSKernel,kernel);
mMat.SetFloat(SSSScaler,scaler);
buffer.Clear();
buffer.GetTemporaryRT(SceneID,mCam.pixelWidth,mCam.pixelHeight,0,FilterMode.Trilinear,RenderTextureFormat.DefaultHDR);
buffer.BlitStencil(BuiltinRenderTextureType.CameraTarget,SceneID,BuiltinRenderTextureType.CameraTarget,mMat,0);
buffer.BlitSRT(SceneID,BuiltinRenderTextureType.CameraTarget,mMat,1);
}
private void OnDisable() {
buffer.ReleaseTemporaryRT(SceneID);
mCam.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque,buffer);
buffer.Release();
}
}
里面最重要的就是KernelCalculator.CalculateKernel
這個(gè)方法,決定了這個(gè)模糊到底該怎么模糊煮剧,其余都是些Command Buffer的應(yīng)用斥滤,不過有兩個(gè)方法BlitStencil
和BlitSRT
并不是Command Buffer里提供的,是用了C#的一個(gè)特性Extension Methods實(shí)現(xiàn)的勉盅,是這樣實(shí)現(xiàn)的
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
public static class GraphicsHelper
{
private static Mesh mMesh;
private static Mesh mesh{
get{
if (mMesh != null){
return mMesh;
}
mMesh = new Mesh();
mMesh.vertices = new Vector3[]{
new Vector3(-1,-1,0),
new Vector3(-1,1,0),
new Vector3(1,1,0),
new Vector3(1,-1,0)
};
mMesh.uv = new Vector2[]{
new Vector2(0,1),
new Vector2(0,0),
new Vector2(1,0),
new Vector2(1,1)
};
mMesh.SetIndices(new int[]{0,1,2,3},MeshTopology.Quads,0);
return mMesh;
}
}
public static void BlitSRT(this CommandBuffer buffer,RenderTargetIdentifier source, RenderTargetIdentifier destination,Material material, int pass){
buffer.SetGlobalTexture(ShaderID._MainTex,source);
buffer.SetRenderTarget(destination);
buffer.DrawMesh(mesh,Matrix4x4.identity,material,0,pass);
}
public static void BlitStencil(this CommandBuffer buffer,RenderTargetIdentifier colorSrc, RenderTargetIdentifier colorBuffer, RenderTargetIdentifier depthStencilBuffer,Material material,int pass){
buffer.SetGlobalTexture(ShaderID._MainTex,colorSrc);
buffer.SetRenderTarget(colorBuffer,depthStencilBuffer);
buffer.DrawMesh(mesh,Matrix4x4.identity,material,0,pass);
}
}
為什么要這樣寫呢佑颇?Unity-Human-Skin-Shader-PC項(xiàng)目里為了兼容延遲渲染寫了不少這樣的方法,我把這兩個(gè)對(duì)項(xiàng)目有用的方法拿了出來(我主要測(cè)試前向渲染菇篡,延遲渲染不怎么考慮)。
從代碼來看一喘,就是依靠KernelCalculator.CalculateKernel
這個(gè)方法算出一個(gè)Kernel Array傳給shader(我叫它Unlit/SSS驱还,用來做x,y方向的2次模糊),利用這個(gè)shader產(chǎn)生的材質(zhì)把屏幕原來的圖像給模糊一下凸克,從實(shí)現(xiàn)上來說和普通的模糊特效實(shí)現(xiàn)過程差不了多少。
然后來看KernelCalculator.CalculateKernel
這個(gè)方法萎战,實(shí)現(xiàn)如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class KernelCalculator
{
/**
* We use a falloff to modulate the shape of the profile. Big falloffs
* spreads the shape making it wider, while small falloffs make it
* narrower.
*/
private static Vector3 Gaussian(float variance, float r, Vector3 falloff){
Vector3 g = Vector3.zero;
for (int i=0;i<3;i++){
float rr = r / (0.001f + falloff[i]);
g[i] = Mathf.Exp((-(rr*rr)) / (2.0f * variance)) / (2.0f * Mathf.PI * variance);
}
return g;
}
/**
* We used the red channel of the original skin profile defined in
* [d'Eon07] for all three channels. We noticed it can be used for green
* and blue channels (scaled using the falloff parameter) without
* introducing noticeable differences and allowing for total control over
* the profile. For example, it allows to create blue SSS gradients, which
* could be useful in case of rendering blue creatures.
*/
private static Vector3 Profile(float r, Vector3 falloff){
return 0.100f * Gaussian(0.0484f, r, falloff) +
0.118f * Gaussian(0.187f, r, falloff) +
0.113f * Gaussian(0.567f, r, falloff) +
0.358f * Gaussian(1.99f, r, falloff) +
0.078f * Gaussian(7.41f, r, falloff);
}
public static List<Vector4> CalculateKernel(int nSamples, Vector3 strength, Vector3 falloff){
List<Vector4> kernel = new List<Vector4>();
float RANGE = nSamples > 20 ? 3.0f : 2.0f;
float EXPONENT = 2.0f;
//calculate the offsets
float step = 2.0f * RANGE / (nSamples - 1);
for (int i=0;i<nSamples;i++){
float o = -RANGE + i * step;
float sign = o < 0.0f ? -1.0f : 1.0f;
float w = RANGE * sign * Mathf.Abs(Mathf.Pow(o,EXPONENT)) / Mathf.Pow(RANGE, EXPONENT);
kernel.Add(new Vector4(0,0,0,w));
}
//calculate the weights
for (int i=0;i<nSamples;i++){
float w0 = i > 0 ? Mathf.Abs(kernel[i].w - kernel[i-1].w) : 0.0f;
float w1 = i < nSamples - 1 ? Mathf.Abs(kernel[i].w - kernel[i+1].w) : 0.0f;
float area = (w0 + w1) / 2.0f;
Vector3 temp = area * Profile(kernel[i].w,falloff);
kernel[i] = new Vector4(temp.x,temp.y,temp.z,kernel[i].w);
}
//We want the offset 0.0 come first
Vector4 t = kernel[nSamples / 2];
for (int i=nSamples/2;i>0;i--){
kernel[i] = kernel[i-1];
}
kernel[0] = t;
//calculate the sum of the weights, we will need to normalize them below
Vector4 sum = Vector4.zero;
for (int i=0;i<nSamples;i++){
sum += kernel[i];
}
//normalize the weight
for(int i=0;i<nSamples;i++){
Vector4 v = kernel[i];
v.x /= sum.x;
v.y /= sum.y;
v.z /= sum.z;
kernel[i] = v;
}
// Tweak them using the desired strength. The first one is:
// lerp(1.0, kernel[0].rgb, strength)
Vector4 v0 = kernel[0];
v0.x = (1.0f - strength.x) * 1.0f + strength.x * v0.x;
v0.y = (1.0f - strength.y) * 1.0f + strength.y * v0.y;
v0.z = (1.0f - strength.z) * 1.0f + strength.z * v0.z;
kernel[0] = v0;
// The others:
// lerp(0.0, kernel[0].rgb, strength)
for (int i=1;i<nSamples;i++){
Vector4 v = kernel[i];
v.x *= strength.x;
v.y *= strength.y;
v.z *= strength.z;
kernel[i] = v;
}
return kernel;
}
}
這個(gè)就是把separable-sss項(xiàng)目的C++代碼翻譯成了C#代碼蚂维,并且我把原項(xiàng)目里的注釋也copy過來了戳粒,如果哪天看懂了4S的核心算法,這個(gè)我也能懂了>_<蔚约。
最后來看用來模糊的那個(gè)shader(Unlit/SSS),這個(gè)shader分成兩部分苹祟,一部分公用的叫SSSCommon.cginc砸抛,另一部分就是SSS了。
#include "UnityCG.cginc"
#define DistanceToProjectionWindow 5.671281819617709 // 1.0 / tan(0.5 * radians(20))
#define DPTimes300 1701.384545885313 //DistanceToProjectionWindow * 300
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _CameraDepthTexture;
float4 _CameraDepthTexture_TexelSize;
sampler2D _MainTex;
float4 _MainTex_ST;
float _SSSScaler;
float4 _Kernel[100];
int _Samples;
v2f vert (appdata v)
{
v2f o;
o.vertex = v.vertex;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
float4 SSS(float4 sceneColor, float2 uv, float2 sssIntensity){
float sceneDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv));
float blurLength = DistanceToProjectionWindow / sceneDepth;
float2 uvOffset = sssIntensity * blurLength;
float4 blurSceneColor = sceneColor;
blurSceneColor.rgb *= _Kernel[0].rgb;
[loop]
for(int i=1;i<_Samples;i++){
float2 sssUV = uv + _Kernel[i].a * uvOffset;
float4 sssSceneColor = tex2D(_MainTex, sssUV);
float sssDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sssUV)).r;
float sssScale = saturate(DPTimes300 * sssIntensity * abs(sceneDepth - sssDepth));
sssSceneColor.rgb = lerp(sssSceneColor.rgb, sceneColor.rgb,sssScale);
blurSceneColor.rgb += _Kernel[i].rgb * sssSceneColor.rgb;
}
return blurSceneColor;
}
SSSCommon.cginc在通過深度以及傳進(jìn)來的Kernel Array做一些模糊的計(jì)算树枫,SSS就是具體兩個(gè)方向的模糊了砂轻。不過這里我并不明白DistanceToProjectionWindow
和DPTimes300
的意義奔誓,有沒有知道的同學(xué)能解釋一下丝里?
Shader "Unlit/SSS"
{
CGINCLUDE
#include "SSSCommon.cginc"
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
ZTest Always
ZWrite Off
Cull Off
Stencil{
Ref 5
Comp Equal
Pass Keep
}
Pass
{
Name "XBlur"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
float4 col = tex2D(_MainTex, i.uv);
float sssIntensity = _SSSScaler * _CameraDepthTexture_TexelSize.x;
float3 xBlur = SSS(col, i.uv, float2(sssIntensity,0)).rgb;
return float4(xBlur,col.a);
}
ENDCG
}
Pass
{
Name "YBlur"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
float4 col = tex2D(_MainTex, i.uv);
float sssIntensity = _SSSScaler * _CameraDepthTexture_TexelSize.y;
float3 yBlur = SSS(col, i.uv, float2(0,sssIntensity)).rgb;
return float4(yBlur,col.a);
}
ENDCG
}
}
}
注意這里我用了stencil test体谒,所以在需要被4S技術(shù)所模糊的那個(gè)(或幾個(gè))對(duì)象的shader(我用了自己上次寫的簡(jiǎn)陋版PBR著色器)里需要加入這樣一段來啟用模糊
Stencil{
Ref 5
Comp Always
Pass Replace
}
并且,我用的Unity2019.3版本里加了上一面那段stencil依然不能開啟模糊幌绍,必須要在shader最后加上Fallback
才能起效故响,難道stencil的使用方法改變了?
最后放上效果圖
感覺這個(gè)已經(jīng)很不錯(cuò)了樟蠕,貼圖模型做的非常好啊。下面是開了4S效果的圖
模糊了一下看起來暗了點(diǎn)吓懈,我們?cè)賮戆涯:{(diào)大點(diǎn)看看
感覺從一個(gè)中年大叔變年輕了靡狞,滿臉的膠原蛋白。。腮恩。
這里有個(gè)坑扒磁。。缸榛。如果我把相機(jī)的MSAA屬性設(shè)為Use Graphics Settings兰伤,那么渲染結(jié)果將是全黑(至少Windows平臺(tái)是這樣的),我還沒整明白為什么=_=!
參考
Unity_SeparableSubsurface
【02】實(shí)時(shí)高逼真皮膚渲染02 次表面散射技術(shù)發(fā)展歷史及技術(shù)詳細(xì)解釋 2
Post-Processing Full-Screen Effects