Unity Shader - “快速“ 次散射 (Fast SSS : Fast Subsurface Scattering)


环境

unity : 2018.2.11f1, 2020.3.18f1
pipeline : BRP, URP


目的

备份,备忘


思路

一般次散射效果都是使用 光线追踪 来渲染的话,还原度会很高(也不是100精准,都是模拟,毕竟物质内的如何让光子偏移方向散射出来,这不是现代计算机,现代渲染架构能支撑得起的)

所以这个 假 次散射,相对 实时渲染效果的价值还是有的

具体思路:

  • 导出模型的厚度图
  • 将片段(像素)中的 观察位置方向 和 光源入射方向 做 dot
  • 然后控制 dot 后的 pow, scale 来控制边缘,和 整体亮度
  • 叠加到之前的颜色着色上即可

在这里插入图片描述
在这里插入图片描述


次表面散射原理

次表面散射我就 简称为:次散射

  • 次散射:SubSurface Scattering,简写:SSS
  • 次散射不是透明(两者没有什么关系,次散射是透光的,只不过吸收,直射,物质内部漫反射比较特殊)
  • 次散射不是透射(准确的说是,还是透射的,没透射光线,你怎么看到光照效果?只不过先经过了介质内部的 漫反射,吸收,折射后,光波 波长,波频都可能发生了变化)
  • 次散射可以使用光线追踪实现,但是比较计算量比较大
  • 实时游戏中更多的是对次散射“快速模拟”

在这里插入图片描述

快速的模拟当中,可以简单概括:SSS 包含:背光、扰动、扩散


背光

根据我们的 lambert 可以得到正面光照的 diffuse 效果 lambert = dot(L, N);,这是基于 表面(N)受正面光照的强度

那么要相对于我们的眼睛,透过物体的某个片段是,判断该片段是否在视角(V)、光源是背光的 back4L = dot(-L, V) 即可


背光强度扰动

在这里插入图片描述

背光强度 = dot(V, -normalize(L + a * N) = dot(V, -H),其中 0 <= a <= 1 可以控制强度

如果为了性能考虑,可以省去:normalize,反正都是快速模拟,不用太精准

所以我们可以将公式修改为:背光强度 = dot(V, -(L+a * N) = dot(V, -H)


背光扩散

在扰动公式基础上调整为:背光强度 = (saturate(dot(V, -(L + a * N))^p) * s

现将 背光强度 clamp[0~1] 之间,所以使用 saturate,然后再用 pow(x, p)p 来控制边缘大小

因此:其中 0 < p <= +R 是控制扩展边缘大小,0 <= s <= +R 是控制背光整体强度值


Translucent shader 伪代码示例

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include ....

...
uniform float _TIDistortion;
uniform float _TIPower;
uniform float _TIScale;
uniform float _TIIvAttenByDistance; // 1.0 / atten
...

fixed4 frag(...) : SV_Target
{
	// jave.lin : 原始光照颜色
	fixed4 albedo = tex2D(_MainTex, i.uv);
	fixed3 diffuse = lamert;
	fixed3 specular = blinn phong mode...;
	fixed3 ambient = ...;
	fixed4 finalColor;
	finalColor.rgb = lambert + blinn phong + ambient + ...;
	finalColor.a = 1;
	
	// jave.lin : Shadow map
	float shadowMapDepth = tex2D(_ShadowMap, i.shadowProjUV);
	float3 shadowMapDepth_positionShadowWS = ...;
	float3 shadowMapDepth_positionMainCamWS = ...;
	float distance_with_shadow = distance(i.positionWS.xyz - shadowMapDepth_positionMainCamWS);
	
	// jave.lin : 计算透光度
	float3 L = normalize(_WorldSpaceLightPos.xyz);
	float3 V = normalize(_WorldSpaceCameraPos.xyz - i.positionWS.xyz);
	float3 N = s.Normal;
	float3 TH = normalize(L + N * _TIDistortion);
	float translucentAtten = distance_with_shadow * _TIIvAttenByDistance;
	float translucentIntensity = pow(saturate(dot(V, -TH)), _TIPower) * _TIScale;
	translucentIntensity *= translucentAtten;
		
	// jave.lin : 叠加透光颜色
	finalColor.rgb += albedo.rgb * _LightColor.rgb * translucentIntensity;
	
	return finalColor;
}

添加采样厚度图 伪代码

在上面的代码基础上,添加一个图

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include ....

...
uniform float _TIDistortion;
uniform float _TIPower;
uniform float _TIScale;
...

fixed4 frag(...) : SV_Target
{
	// jave.lin : 原始光照颜色
	fixed4 albedo = tex2D(_MainTex, i.uv);
	fixed3 diffuse = lamert;
	fixed3 specular = blinn phong mode...;
	fixed3 ambient = ...;
	fixed4 finalColor;
	finalColor.rgb = lambert + blinn phong + ambient + ...;
	finalColor.a = 1;
	
	// jave.lin : 厚度采样
	float thickness = tex2D(_TITicknessMap, i.uv).r; // 0~1 => TI弱~TI强
	
	// jave.lin : 计算透光度
	float3 L = normalize(_WorldSpaceLightPos.xyz);
	float3 V = normalize(_WorldSpaceCameraPos.xyz - i.positionWS.xyz);
	float3 N = s.Normal;
	float3 TH = normalize(L + N * _TIDistortion);
	float translucentIntensity = pow(saturate(dot(V, -TH)), _TIPower) * _TIScale;
	translucentIntensity *= thickness;	
		
	// jave.lin : 叠加透光颜色
	finalColor.rgb += albedo.rgb * _LightColor.rgb * translucentIntensity;
	
	return finalColor;
}

考虑上 shadow map 距离的 伪代码

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include ....

...
uniform float _TIDistortion;
uniform float _TIPower;
uniform float _TIScale;
uniform float _TIIvAttenByDistance; // 1.0 / atten
...

fixed4 frag(...) : SV_Target
{
	// jave.lin : 原始光照颜色
	fixed4 albedo = tex2D(_MainTex, i.uv);
	fixed3 diffuse = lamert;
	fixed3 specular = blinn phong mode...;
	fixed3 ambient = ...;
	fixed4 finalColor;
	finalColor.rgb = lambert + blinn phong + ambient + ...;
	finalColor.a = 1;
	
	// jave.lin : Shadow map
	float shadowMapDepth = tex2D(_ShadowMap, i.shadowProjUV);
	float3 shadowMapDepth_positionShadowWS = ...;
	float3 shadowMapDepth_positionMainCamWS = ...;
	float distance_with_shadow = distance(i.positionWS.xyz - shadowMapDepth_positionMainCamWS);
	
	// jave.lin : 厚度采样
	float thickness = tex2D(_TITicknessMap, i.uv).r; // 0~1 => TI弱~TI强
	
	// jave.lin : 计算透光度
	float3 L = normalize(_WorldSpaceLightPos.xyz);
	float3 V = normalize(_WorldSpaceCameraPos.xyz - i.positionWS.xyz);
	float3 N = s.Normal;
	float3 TH = normalize(L + N * _TIDistortion);
	float translucentAtten = distance_with_shadow * _TIIvAttenByDistance;
	float translucentIntensity = pow(saturate(dot(V, -TH)), _TIPower) * _TIScale;
	translucentIntensity *= translucentAtten * thickness;	
		
	// jave.lin : 叠加透光颜色
	finalColor.rgb += albedo.rgb * _LightColor.rgb * translucentIntensity;
	
	return finalColor;
}

多光源下的 SSS

下面是以类似 URP 中的多光源遍历的方式 (BRP 中是区分 Pass 来绘制多光源的)

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include ....

...
uniform float _TIDistortion;
uniform float _TIPower;
uniform float _TIScale;
uniform float _TIIvAttenByDistance; // 1.0 / atten
uniform float _TIAmbientScale;
...

fixed4 frag(...) : SV_Target
{
	fixed4 finalColor = 0;
	float3 V = normalize(_WorldSpaceCameraPos.xyz - i.positionWS.xyz);
	float3 N = normalize(i.normalWS);
	fixed3 ambient = ...;
	
	fixed4 albedo = tex2D(_MainTex, i.uv);
	// jave.lin : 厚度采样
	float thickness = tex2D(_TITicknessMap, i.uv).r; // 0~1 => TI弱~TI强

	for (int i = 0; i < _AvailidatedLightCount; ++i)
	{
		Light light = Lights[i];

		// jave.lin : 光源方向
		float3 L = light.dir;
	
		// jave.lin : 原始光照颜色
		fixed3 diffuse = lamert with N and L;
		fixed3 specular = blinn phong mode with V, N, L;
		finalColor.rgb = lambert + blinn phong + ...;
		
		// jave.lin : Shadow map - 这部分多光源的话,不同类型的 shadow map 是不同的
		// 点光源可能是:一个 cube map,或是 6 张 shadow map
		// spot, directional light 一张
		// 这里不光是
		float3 shadowSampleLocation = GetShadowMapSampleLocation(light);
		float shadowMapDepth = tex2D(GetShadowMap(light), i.shadowProjUV);
		float3 shadowMapDepth_positionShadowWS = ...;
		float3 shadowMapDepth_positionMainCamWS = ...;
		float distance_with_shadow = distance(i.positionWS.xyz - shadowMapDepth_positionMainCamWS);
		
		// jave.lin : 计算透光度
		float3 TH = normalize(L + N * _TIDistortion);
		float translucentAtten = distance_with_shadow * _TIIvAttenByDistance;
		float translucentIntensity = pow(saturate(dot(V, -TH)), _TIPower) * _TIScale;
		translucentIntensity *= translucentAtten * thickness;	
			
		// jave.lin : 叠加透光颜色
		finalColor.rgb += albedo.rgb * light.color.rgb * translucentIntensity;
	}
	// jave.lin : 最后添加上 ambient 的计算
	finalColor.rgb += albedo.rgb * ambient * (_TIAmbientScale * thickness);

	finalColor.a = 1;
	
	return finalColor;
}

BRP 多光源效果

我们将 厚度值 放到 MRAT 贴图中的 a 通道
在这里插入图片描述

厚度值的显示
在这里插入图片描述

下面是 BRP 中的效果
请添加图片描述
请添加图片描述

请添加图片描述


URP 多光源效果

(URP下麻烦很,毕竟是个 package,需要修改其里面的 shader,具体可以参考另一篇: Unity - 如何修改一个 Package 或是如何将 Package Local化

没开效果
请添加图片描述
开了效果
请添加图片描述

关了
请添加图片描述

开了
请添加图片描述


SP 中贴图导出设置

在这里插入图片描述


(后续找个 皮肤的 素材来看效果)


用途

可以用于一些 散射透光强 的材质:

  • 玉石
  • 皮肤
  • 浓稠不透明的液体
  • 橡胶
  • 等等。。。

Project


其他方式 - RampTexture Or LUT

后面重写编写的:Unity Shader - SSS皮肤


References


版权声明:本文为linjf520原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。