前言

感觉好久没更新博客了,这段时间决定重新把写博客的习惯捡起来!前段时间学习研究了一下次表面散射相关的知识,这次我们就在Unity中简单实现一下该效果。如果哪里有错误的地方,希望大家能够指出,多多讨论。

次表面散射(Subsurface scattering)

次表面散射是光在传播时的一种现象,表现为光在穿过透明物体表面后,与材料之间发生交互作用而导致光被散射开来,光路也在其他的位置穿出物体。光一般会穿透物体的表面,在物体内部在不同的角度被反射若干次,最终穿出物体。次表面散射在三维计算机图形中十分重要,可用来渲染大理石、皮肤、树叶、蜡、牛奶等多种不同材料。

例如:



当然为了能在游戏中实时渲染,我们只能近似模拟次表面散射现象。本篇文章实现原理主要参考了这篇文章
Fast Subsurface Scattering

话不多说,下面我们一步一步的来实现伪次表面散射,在本篇文章中只贴出关键的Shader代码,基础的Shader代码就不再一一解释了。

实现

在自然界中,光线的传播一般包含三种情况,即:

  1. 反射: 入射光与反射光在表面的同一侧,且入射点与反射点相同

  2. 次表面散射:入射光与反射光在表面的同一侧,且入射点与反射点不同

  3. 透射:入射光与反射光在表面的不同侧,即光线投过了物体

为了模拟这种背面透光的效果,我们可以把法线向光源方向偏移一定程度后,然后取反,再去和视线方向做运算。

模拟背光反射率的方程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GlRyTesr-1631352827920)(https://www.alanzucconi.com/wp-content/ql-cache/quicklatex.com-577e07757a85aaf42b85235abb657979_l3.svg)]

  • L 光源方向,
  • V 视图方向,
  • N 法线方向。

公式转换为Shader代码:

    //fragmentfloat3 H = L + N * distortion;float sss = pow(saturate(dot(V, -H)), power) * scale;return sss;

在平行光下的渲染效果如图:

环绕照明(Warp Lighting)

其实还有一种简单模拟次表面的技巧:环绕照明(Warp Lighting),正常情况下,当表面的法线对于光源方向垂直的时候,Lambert漫反射提供的照明度是0。而环绕光照修改漫反射函数,使得光照环绕在物体的周围,越过那些正常时会变黑变暗的点。这减少了漫反射光照明的对比度,从而减少了环境光和所要求的填充光的量。

下图和代码片段显示了如何将漫反射光照函数进行改造,使其包含环绕效果。

其中,wrap变量为环绕值,是一个范围为0到1之间的浮点数,用于控制光照环绕物体周围距离。

代码:

    float diffuse = max(0, dot(L, N));float wrap_diffuse = max(0, (dot(L, N) + _WrapValue) / (1 + _WrapValue));return wrap_diffuse;

渲染效果:

然后,我们把前两种合成看看效果

代码:

    float3 H = L + N * distortion;float sss = pow(saturate(dot(V, -H)), power) * scale;float diffuse = max(0, dot(L, N));float wrap_diffuse = max(0, (dot(L, N) + _WrapValue) / (1 + _WrapValue));return sss + wrap_diffuse;



现在看来是不是有点散射那味了

叠加上自定义的颜色再看看效果如何:

现在还只是考虑了平行光,下面我们把点光源也考虑进去看看效果如何

说到关于点光源相关的计算,那么一般都是在 ForwardAdd 的Pass 中去计算,但是这样会造成每多一盏灯,DrawCall就会翻一倍,所以这里我就直接在 ForwardBase 里面计算点光源了。

在Unity中 有一个内置函数用来计算点光源 Shade4PointLights,

使用如下:

float3 pointColor = Shade4PointLights (
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0, i.worldPos, -N);return fixed4(pointColor,1);

然后我去看看该方法的源码,分析一下大概意思, 源码可以在 UnityCG.cginc 文件中找到:

// Used in ForwardBase pass: Calculates diffuse lighting from 4 point lights, with data packed in a special way.
float3 Shade4PointLights (float4 lightPosX, float4 lightPosY, float4 lightPosZ,float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,float4 lightAttenSq,float3 pos, float3 normal)
{// to light vectorsfloat4 toLightX = lightPosX - pos.x;float4 toLightY = lightPosY - pos.y;float4 toLightZ = lightPosZ - pos.z;// squared lengthsfloat4 lengthSq = 0;lengthSq += toLightX * toLightX;lengthSq += toLightY * toLightY;lengthSq += toLightZ * toLightZ;// don't produce NaNs if some vertex position overlaps with the lightlengthSq = max(lengthSq, 0.000001);// NdotLfloat4 ndotl = 0;ndotl += toLightX * normal.x;ndotl += toLightY * normal.y;ndotl += toLightZ * normal.z;// correct NdotLfloat4 corr = rsqrt(lengthSq);ndotl = max (float4(0,0,0,0), ndotl * corr);// attenuationfloat4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);float4 diff = ndotl * atten;// final colorfloat3 col = 0;col += lightColor0 * diff.x;col += lightColor1 * diff.y;col += lightColor2 * diff.z;col += lightColor3 * diff.w;return col;
}

具体分析可以参考这篇文章:Unity3D ShaderLab 之 Shade4PointLights 解读

以上代码可以很明显的发现,引擎会把四盏点光源的x, y, z坐标,分别存储到lightPosX, lightPosY, lightPosZ中,
换句话说:
light0 的位置是 float3(lightPosX[0], lightPosY[0], lightPosZ[0])
light1 的位置是 float3(lightPosX[1], lightPosY[1], lightPosZ[1])
light2 的位置是 float3(lightPosX[2], lightPosY[2], lightPosZ[2])
light3 的位置是 float3(lightPosX[3], lightPosY[3], lightPosZ[3])

unity_LightColor数组就是点光源颜色。

有了以上信息就好办了,我们来魔改一下,改成我们需要的次表面散射。

代码如下:

 // 计算SSS
inline float SubsurfaceScattering (float3 V, float3 L, float3 N, float distortion,float power,float scale)
{// float3 H = normalize(L + N * distortion);float3 H = L + N * distortion;float I = pow(saturate(dot(V, -H)), power) * scale;return I;
}// 计算点光源SSS(参考UnityCG.cginc 中的Shade4PointLights)
float3 CalculatePointLightSSS (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,float3 pos,float3 N,float3 V)
{// to light vectorsfloat4 toLightX = lightPosX - pos.x;float4 toLightY = lightPosY - pos.y;float4 toLightZ = lightPosZ - pos.z;// squared lengthsfloat4 lengthSq = 0;lengthSq += toLightX * toLightX;lengthSq += toLightY * toLightY;lengthSq += toLightZ * toLightZ;// don't produce NaNs if some vertex position overlaps with the lightlengthSq = max(lengthSq, 0.000001);// NdotLfloat4 ndotl = 0;ndotl += toLightX * N.x;ndotl += toLightY * N.y;ndotl += toLightZ * N.z;// correct NdotLfloat4 corr = rsqrt(lengthSq);ndotl = max (float4(0,0,0,0), ndotl * corr);float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);// float4 diff = ndotl * atten;float3 pointLightDir0 = normalize(float3(toLightX[0],toLightY[0],toLightZ[0]));float pointSSS0 = SubsurfaceScattering(V,pointLightDir0,N,_DistortionBack,_PowerBack,_ScaleBack);float3 pointLightDir1 = normalize(float3(toLightX[1],toLightY[1],toLightZ[1]));float pointSSS1 = SubsurfaceScattering(V,pointLightDir1,N,_DistortionBack,_PowerBack,_ScaleBack);float3 pointLightDir2 = normalize(float3(toLightX[2],toLightY[2],toLightZ[2]));float pointSSS2 = SubsurfaceScattering(V,pointLightDir2,N,_DistortionBack,_PowerBack,_ScaleBack);float3 pointLightDir3 = normalize(float3(toLightX[3],toLightY[3],toLightZ[3]));float pointSSS3 = SubsurfaceScattering(V,pointLightDir3,N,_DistortionBack,_PowerBack,_ScaleBack);// final colorfloat3 col = 0;col += lightColor0 * atten.x * (pointSSS0+ndotl.x);col += lightColor1 * atten.y * (pointSSS1+ndotl.y);col += lightColor2 * atten.z * (pointSSS2+ndotl.z);col += lightColor3 * atten.w * (pointSSS3+ndotl.w);return col;
}// fargfloat3 pointColor = CalculatePointLightSSS(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,unity_4LightAtten0,i.worldPos,N,V);return fixed4(pointColor,1);

然后我们在场景中放3盏点光源看看效果如何:

叠加上之前计算的平行光:

最后我们把 Wrap-Diffuse、Specular-BlinnPhong 等效果叠加上去看看整体效果。
当然,散射颜色也可以定义一个变量来控制,方便美术调整效果。

在有平行光、无点光源的情况下的背面和正面:

在无平行光、有点光源的情况下:

在有平行光、有点光源的情况下(颜色太杂乱了…):

厚度图

吸收(Absorption)是模拟半透明材质的最重要特性之一。
光线在物质中传播得越远,它被散射和吸收得就越厉害。
为了模拟这种效果,我们需要测量光在物质中传播的距离,并相应地对其进行衰减。

可以在下图中看到具有相同入射角的三种不同光线,穿过物体的长度却截然不同。

这里我们就采用外部局部厚度图来模拟该现象,当然,该方法在物理上来说并不准确,但是可以比较简单快速的模拟出这种效果。

烘焙厚度图可以用Substance Painter
或者用Unity的插件:Mesh Materializer把厚度信息存储在顶点色里面。

厚度图输出来是这样(这里换了个简单的模型,之前那个模型厚度烘焙有点问题-.-):

最后用厚度值乘上次表面散射值,就能得到最终效果:

最后奉上完整代码:

/*
* @Descripttion: 次表面散射
* @Author: lichanglong
* @Date: 2021-08-20 18:21:10* @FilePath: \LearnUnityShader\Assets\Scenes\SubsurfaceScattering\FastSSSTutorial.shader
*/
Shader "lcl/SubsurfaceScattering/FastSSSTutorial" {Properties{_MainTex ("Texture", 2D) = "white" {}_BaseColor("Base Color",Color) = (1,1,1,1)_Specular("Specular Color",Color) = (1,1,1,1)[PowerSlider()]_Gloss("Gloss",Range(1,200)) = 10[Main(sss,_,3)] _group ("SubsurfaceScattering", float) = 1[Tex(sss)]_ThicknessTex ("Thickness Tex", 2D) = "white" {}[Sub(sss)]_ThicknessPower ("ThicknessPower", Range(0,10)) = 1[Sub(sss)][HDR]_ScatterColor ("Scatter Color", Color) = (1,1,1,1)[Sub(sss)]_WrapValue ("WrapValue", Range(0,1)) = 0.0[Title(sss, Back SSS Factor)][Sub(sss)]_DistortionBack ("Back Distortion", Range(0,1)) = 1.0[Sub(sss)]_PowerBack ("Back Power", Range(0,10)) = 1.0[Sub(sss)]_ScaleBack ("Back Scale", Range(0,1)) = 1.0// 是否开启计算点光源[SubToggle(sss, __)] _CALCULATE_POINTLIGHT ("Calculate Point Light", float) = 0}SubShader {Pass{Tags { "LightMode"="Forwardbase" }CGPROGRAM#pragma vertex vert#pragma fragment frag#pragma multi_compile_fwdbase// #pragma enable_d3d11_debug_symbols#pragma multi_compile _ _CALCULATE_POINTLIGHT_ON #include "UnityCG.cginc"#include "Lighting.cginc"#include "AutoLight.cginc"sampler2D _MainTex,_ThicknessTex;float4 _MainTex_ST;fixed4 _BaseColor;half _Gloss;float3 _Specular;float4 _ScatterColor;float _DistortionBack;float _PowerBack;float _ScaleBack;float _ThicknessPower;float _WrapValue;float _ScatterWidth;// float  _RimPower;// float _RimIntensity;struct a2v {float4 vertex : POSITION;float3 normal: NORMAL;float2 uv : TEXCOORD0;};struct v2f{float4 position:SV_POSITION;float2 uv : TEXCOORD0;float3 normalDir: TEXCOORD1;float3 worldPos: TEXCOORD2;float3 viewDir: TEXCOORD3;float3 lightDir: TEXCOORD4;};v2f vert(a2v v){v2f o;o.position = UnityObjectToClipPos(v.vertex);o.uv = TRANSFORM_TEX(v.uv, _MainTex);o.worldPos = mul (unity_ObjectToWorld, v.vertex);o.normalDir = UnityObjectToWorldNormal (v.normal);o.viewDir = UnityWorldSpaceViewDir(o.worldPos);o.lightDir = UnityWorldSpaceLightDir(o.worldPos);return o;};// 计算SSSinline float SubsurfaceScattering (float3 V, float3 L, float3 N, float distortion,float power,float scale){// float3 H = normalize(L + N * distortion);float3 H = L + N * distortion;float I = pow(saturate(dot(V, -H)), power) * scale;return I;}// 计算点光源SSS(参考UnityCG.cginc 中的Shade4PointLights)float3 CalculatePointLightSSS (float4 lightPosX, float4 lightPosY, float4 lightPosZ,float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,float4 lightAttenSq,float3 pos,float3 N,float3 V){// to light vectorsfloat4 toLightX = lightPosX - pos.x;float4 toLightY = lightPosY - pos.y;float4 toLightZ = lightPosZ - pos.z;// squared lengthsfloat4 lengthSq = 0;lengthSq += toLightX * toLightX;lengthSq += toLightY * toLightY;lengthSq += toLightZ * toLightZ;// don't produce NaNs if some vertex position overlaps with the lightlengthSq = max(lengthSq, 0.000001);// NdotLfloat4 ndotl = 0;ndotl += toLightX * N.x;ndotl += toLightY * N.y;ndotl += toLightZ * N.z;// correct NdotLfloat4 corr = rsqrt(lengthSq);ndotl = max (float4(0,0,0,0), ndotl * corr);float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);float4 diff = ndotl * atten;float3 pointLightDir0 = normalize(float3(toLightX[0],toLightY[0],toLightZ[0]));float pointSSS0 = SubsurfaceScattering(V,pointLightDir0,N,_DistortionBack,_PowerBack,_ScaleBack)*3;float3 pointLightDir1 = normalize(float3(toLightX[1],toLightY[1],toLightZ[1]));float pointSSS1 = SubsurfaceScattering(V,pointLightDir1,N,_DistortionBack,_PowerBack,_ScaleBack)*3;float3 pointLightDir2 = normalize(float3(toLightX[2],toLightY[2],toLightZ[2]));float pointSSS2 = SubsurfaceScattering(V,pointLightDir2,N,_DistortionBack,_PowerBack,_ScaleBack)*3;float3 pointLightDir3 = normalize(float3(toLightX[3],toLightY[3],toLightZ[3]));float pointSSS3 = SubsurfaceScattering(V,pointLightDir3,N,_DistortionBack,_PowerBack,_ScaleBack)*3;// final colorfloat3 col = 0;// col += lightColor0 * diff.x;// col += lightColor1 * diff.y;// col += lightColor2 * diff.z;// col += lightColor3 * diff.w;col += lightColor0 * atten.x * (pointSSS0+ndotl.x);col += lightColor1 * atten.y * (pointSSS1+ndotl.y);col += lightColor2 * atten.z * (pointSSS2+ndotl.z);col += lightColor3 * atten.w * (pointSSS3+ndotl.w);return col;}fixed4 frag(v2f i): SV_TARGET{fixed4 col = tex2D(_MainTex, i.uv) * _BaseColor;fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb;fixed3 lightColor = _LightColor0.rgb;float3 N = normalize(i.normalDir);float3 V = normalize(i.viewDir);float3 L = normalize(i.lightDir);float NdotL = dot(N, L);float3 H = normalize(L + V);float NdotH = dot(N, H);float NdotV = dot(N, V);float thickness = tex2D(_ThicknessTex, i.uv).r * _ThicknessPower;// -----------------------------SSS-----------------------------// 快速模拟次表面散射float3 sss = SubsurfaceScattering(V,L,N,_DistortionBack,_PowerBack,_ScaleBack) * lightColor * _ScatterColor * thickness;// -----------------------------Wrap Lighting-----------------------------// float wrap_diffuse = pow(dot(N,L)*_WrapValue+(1-_WrapValue),2) * col;float wrap_diffuse = max(0, (NdotL + _WrapValue) / (1 + _WrapValue));// float scatter = smoothstep(0.0, _ScatterWidth, wrap_diffuse) * smoothstep(_ScatterWidth * 2.0, _ScatterWidth,wrap_diffuse);// -----------------------------Diffuse-----------------------------// float diffuse = lightColor * (max(0, NdotL)*0.5+0.5) * col;// float diffuse = lightColor * (max(0, NdotL)) * col;float3 diffuse = lightColor * wrap_diffuse  * col;// --------------------------Specular-BlinnPhong-----------------------------fixed3 specular = lightColor * pow(max(0,NdotH),_Gloss) * _Specular;// -----------------------------Rim-----------------------------// float rim = 1.0 - max(0, NdotV);// return rim;// -----------------------------Point Light SSS-----------------------------fixed3 pointColor = fixed3(0,0,0);// 计算点光源#ifdef _CALCULATE_POINTLIGHT_ONpointColor = CalculatePointLightSSS(unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,unity_4LightAtten0,i.worldPos,N,V) * thickness;#endiffloat3 resCol = diffuse + sss + pointColor + specular;return fixed4(resCol,1);};ENDCG}}FallBack "Diffuse"
}

工程源码:https://github.com/csdjk/LearnUnityShader/tree/master/Assets/Scenes/SubsurfaceScattering

参考

fast-subsurface-scattering
https://www.patreon.com/posts/subsurface-write-20905461
https://zhuanlan.zhihu.com/p/42433792
https://zhuanlan.zhihu.com/p/21247702
https://zhuanlan.zhihu.com/p/36499291
http://walkingfat.com
https://zhuanlan.zhihu.com/p/27842876

Unity Shader - 伪次表面散射模拟相关推荐

  1. 【unity shader/风格化水表面渲染/基础笔记】urp代码版01-水面与水底的深度判断

    目录 1 场景搭建与实现思路 2 深度图获取与原理 获取方式 深度图计算原理 变换过程 3 重建世界坐标 采样深度图 重建方法1 重建方法2 4 结果 仅做学习,如有错误望指正 涉及的知识点:dept ...

  2. 【unity shader/风格化水表面渲染/基础笔记】urp代码版05-焦散模拟

    前言 前面4章完成了波浪动画.上色.岸边泡沫的生成.本章梳理水底的焦散模拟. 焦散模拟 我们可以使用类似的水纹的贴图作色散分离,或者直接使用分离的三通道贴图. 下面的代码设置了宏_DISPERSION ...

  3. 【unity shader/风格化水表面渲染/基础笔记】urp代码版03-水表面颜色

    前言 上一节生成了岸边的泡沫.本节主要梳理水表面的上色过程,这部分涉及半透明渲染和fresnel颜色 颜色 首先,岸边_ShallowCol和远离岸边_DeepCol的颜色应该有所区分.这里依旧利用上 ...

  4. 【unity shader/风格化水表面渲染/基础笔记】urp代码版02-岸边泡沫的生成

    前言 上一节,达成了水面与水底的深度判断,结果用RawDepth表示.这一节梳理利用深度关系生成岸边的泡沫. 设计函数_getFoam(RawDepth) float sinWave = _getFo ...

  5. Unity Shader 窗前雨滴效果衍生(表面水滴附着)

    Unity Shader 窗前雨滴效果衍生(表面水滴附着) 霓虹中国视频截图 现实中的水珠附着效果 实现思路 1.首先创建一个Cube来作为实现效果的物体 2.创建一个Shader开始着色器的编写 实 ...

  6. 【Unity Shader】使用Geometry Shader进行大片草地的实时渲染

    效果预览图 0. 前言 笔者最近阅读学习了知乎大神@陈嘉栋 所写的这篇文章:<利用GPU实现无尽草地的实时渲染>,这篇文章写得非常好,给出了实时生成一片草地的核心思路和基本流程,非常清晰- ...

  7. Unity Shader入门精要学习笔记 - 第6章 开始 Unity 中的基础光照

    转自冯乐乐的<Unity Shader入门精要> 通常来讲,我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象. 首先,光线从光源中被发射出来. 然后,光线和场景中的一些物体相交 ...

  8. 【Unity Shader】(六) ------ 复杂的光照(上)

    笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题.              [Unity Sha ...

  9. Unity Shader入门精要第七章 基础纹理之遮罩纹理

    Unity系列文章目录 文章目录 Unity系列文章目录 前言 一.实践 参考 前言 遮罩纹理(mask texture)是本章要介绍的最后一种纹理,它非常有用,在很多商业游戏中 都可以见到它的身影. ...

最新文章

  1. CentOS Linux解决 Device eth0 does not seem to be present
  2. 爸爸又给Spring MVC生了个弟弟叫Spring WebFlux
  3. instancesRespondToSelector与respondsToSelector的区别
  4. hadoop搭建在Ubuntu16.04上
  5. 简释iptables防火墙
  6. JAVA8之lambda表达式
  7. catia钣金根据线段折弯_折弯大神分析钣金折弯下刀顺序
  8. bootstrap-table 列属性_bootstrap中table如何隐藏列?
  9. 系统工程理论与实践投稿经验_钱学森的系统工程 | 如是读
  10. 从命令行接收多个数字,求和之后输出结果
  11. 无限滚动加载最佳实践
  12. 关于CentOS 7(Linux)下 软件|脚本 的自启动
  13. 修改了DNS服务器网速慢,网络速度缓慢怎么办?轻松一键修改DNS设置让网速提升五倍...
  14. 禅定是否一定要打坐,为什么?
  15. 小程序进入首页时弹出广告
  16. js 三大家族(offset/scroll/client)
  17. 一文搞懂Typescript
  18. python 字典列表,元组列表 列表嵌套字典 列表嵌套元组 字典嵌套列表
  19. 计算机培训教师自我介绍,面试教师时的自我介绍
  20. 人类无法通过时光机器回到过去

热门文章

  1. am5728通过DM框架配置GPIO管脚
  2. Coggle打卡——Linux使用基础
  3. 使用python爬取某药品网站药品说明
  4. 在linux前台和后台运行程序
  5. 平均获客成本_获客成本(线上,线下)如何计算?
  6. kettle使用 - 开启Carte服务
  7. ndk 的emac_德州仪器的EMAC外设开发包,配合NDK使用,应在CCS4.2版本以上使用
  8. 【弄nèng - Zookeeper】Zookeeper入门教程(三)—— 客户端Curator的基本API使用(Curator framework)
  9. 无理数存在性的几何证明
  10. Linux系统编程:串口编程