Unity Shader - 伪次表面散射模拟
前言
感觉好久没更新博客了,这段时间决定重新把写博客的习惯捡起来!前段时间学习研究了一下次表面散射相关的知识,这次我们就在Unity中简单实现一下该效果。如果哪里有错误的地方,希望大家能够指出,多多讨论。
次表面散射(Subsurface scattering)
次表面散射是光在传播时的一种现象,表现为光在穿过透明物体表面后,与材料之间发生交互作用而导致光被散射开来,光路也在其他的位置穿出物体。光一般会穿透物体的表面,在物体内部在不同的角度被反射若干次,最终穿出物体。次表面散射在三维计算机图形中十分重要,可用来渲染大理石、皮肤、树叶、蜡、牛奶等多种不同材料。
例如:
当然为了能在游戏中实时渲染,我们只能近似模拟次表面散射现象。本篇文章实现原理主要参考了这篇文章
Fast Subsurface Scattering
话不多说,下面我们一步一步的来实现伪次表面散射,在本篇文章中只贴出关键的Shader代码,基础的Shader代码就不再一一解释了。
实现
在自然界中,光线的传播一般包含三种情况,即:
反射: 入射光与反射光在表面的同一侧,且入射点与反射点相同
次表面散射:入射光与反射光在表面的同一侧,且入射点与反射点不同
透射:入射光与反射光在表面的不同侧,即光线投过了物体
为了模拟这种背面透光的效果,我们可以把法线向光源方向偏移一定程度后,然后取反,再去和视线方向做运算。
模拟背光反射率的方程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 - 伪次表面散射模拟相关推荐
- 【unity shader/风格化水表面渲染/基础笔记】urp代码版01-水面与水底的深度判断
目录 1 场景搭建与实现思路 2 深度图获取与原理 获取方式 深度图计算原理 变换过程 3 重建世界坐标 采样深度图 重建方法1 重建方法2 4 结果 仅做学习,如有错误望指正 涉及的知识点:dept ...
- 【unity shader/风格化水表面渲染/基础笔记】urp代码版05-焦散模拟
前言 前面4章完成了波浪动画.上色.岸边泡沫的生成.本章梳理水底的焦散模拟. 焦散模拟 我们可以使用类似的水纹的贴图作色散分离,或者直接使用分离的三通道贴图. 下面的代码设置了宏_DISPERSION ...
- 【unity shader/风格化水表面渲染/基础笔记】urp代码版03-水表面颜色
前言 上一节生成了岸边的泡沫.本节主要梳理水表面的上色过程,这部分涉及半透明渲染和fresnel颜色 颜色 首先,岸边_ShallowCol和远离岸边_DeepCol的颜色应该有所区分.这里依旧利用上 ...
- 【unity shader/风格化水表面渲染/基础笔记】urp代码版02-岸边泡沫的生成
前言 上一节,达成了水面与水底的深度判断,结果用RawDepth表示.这一节梳理利用深度关系生成岸边的泡沫. 设计函数_getFoam(RawDepth) float sinWave = _getFo ...
- Unity Shader 窗前雨滴效果衍生(表面水滴附着)
Unity Shader 窗前雨滴效果衍生(表面水滴附着) 霓虹中国视频截图 现实中的水珠附着效果 实现思路 1.首先创建一个Cube来作为实现效果的物体 2.创建一个Shader开始着色器的编写 实 ...
- 【Unity Shader】使用Geometry Shader进行大片草地的实时渲染
效果预览图 0. 前言 笔者最近阅读学习了知乎大神@陈嘉栋 所写的这篇文章:<利用GPU实现无尽草地的实时渲染>,这篇文章写得非常好,给出了实时生成一片草地的核心思路和基本流程,非常清晰- ...
- Unity Shader入门精要学习笔记 - 第6章 开始 Unity 中的基础光照
转自冯乐乐的<Unity Shader入门精要> 通常来讲,我们要模拟真实的光照环境来生成一张图像,需要考虑3种物理现象. 首先,光线从光源中被发射出来. 然后,光线和场景中的一些物体相交 ...
- 【Unity Shader】(六) ------ 复杂的光照(上)
笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题. [Unity Sha ...
- Unity Shader入门精要第七章 基础纹理之遮罩纹理
Unity系列文章目录 文章目录 Unity系列文章目录 前言 一.实践 参考 前言 遮罩纹理(mask texture)是本章要介绍的最后一种纹理,它非常有用,在很多商业游戏中 都可以见到它的身影. ...
最新文章
- CentOS Linux解决 Device eth0 does not seem to be present
- 爸爸又给Spring MVC生了个弟弟叫Spring WebFlux
- instancesRespondToSelector与respondsToSelector的区别
- hadoop搭建在Ubuntu16.04上
- 简释iptables防火墙
- JAVA8之lambda表达式
- catia钣金根据线段折弯_折弯大神分析钣金折弯下刀顺序
- bootstrap-table 列属性_bootstrap中table如何隐藏列?
- 系统工程理论与实践投稿经验_钱学森的系统工程 | 如是读
- 从命令行接收多个数字,求和之后输出结果
- 无限滚动加载最佳实践
- 关于CentOS 7(Linux)下 软件|脚本 的自启动
- 修改了DNS服务器网速慢,网络速度缓慢怎么办?轻松一键修改DNS设置让网速提升五倍...
- 禅定是否一定要打坐,为什么?
- 小程序进入首页时弹出广告
- js 三大家族(offset/scroll/client)
- 一文搞懂Typescript
- python 字典列表,元组列表 列表嵌套字典 列表嵌套元组 字典嵌套列表
- 计算机培训教师自我介绍,面试教师时的自我介绍
- 人类无法通过时光机器回到过去