案例学习——Unity基于体绘制的大气散射shader

本案例学习资料来源
Volumetric Atmospheric Scattering

0 效果展示

先看看实现的效果(米氏散射和瑞利散射结合的地球大气+云层+地球昼夜)
在这里插入图片描述
在这里插入图片描述

1 引入

抬头望天,我们会发现天空经常是蓝色
在这里插入图片描述
日出日落时天空经常会是红色
在这里插入图片描述
这样的光学现象主要是光进行了瑞利散射(Rayleigh scattering),本文依照AlanZucconi大神的教程,介绍如何对大气散射进行数学建模,来重现这些大气中的视觉效果。

1.0 介绍

对于传统的渲染技术,会假设物体是一个空的外壳,我们在外壳表面即可进行所有效果的计算。这种简化方式对于渲染物体表面来说是很高效的。

但是有些效果是由于光穿透了物体才能实现的,比如半透明物体正是如此。不过聪明的人们也研究了一些快速近似半透明物体的方法(GDC 2011 – Approximating Translucency for a Fast, Cheap and Convincing Subsurface Scattering Look)。

但是大气就很难这么做了,因为天空并不是一个“物体”,我们不能只是渲染表面的壳,我们还需要模拟光线在大气中到底经历了什么。

不光光渲染壳,同时也要渲染物体内部的光线传播,这个技术通常被称为体积渲染/体绘制(volumetric rendering),在本文的上一篇文章做出过一些小的探讨(比如光线步进和有向距离函数)。

不过仅是这样还不足以模拟大气散射,接下来会介绍一种更合适的方法,被称为体积单次散射(volumetric single scattering)

1.1 单次散射(Single Scattering)

在游戏引擎的基本光照模型实现中,都假设光线在真空中传播,所以物体的表面属性往往是影响光照的唯一因素。

然而在物理上,传播媒介对光的影响也是很重要的,对于大气来说,穿过的空气多少或者空气的成分就很能影响光照的效果。

在图形学进行大量的简化之后,空气影响我们感知光线的方式,通常会分为两种,分别是出射散射(Out-Scattering)入射散射(In-Scattering)

1.1.1 出射散射(Out-Scattering)

出射散射也就是光子打中空气分子后,原本要射向摄像机的光线进行了偏转,光线的方向进行了改变。
在这里插入图片描述
对于一个大光源来说,能够发射出很多很多光子,因而在空气中传播时经常会发生出射散射。
空气密度越大,光线传播的距离越长,出射散射发生的光线偏转越多,对光线的削弱效果越明显。
在这里插入图片描述

1.1.2 入射散射

与出射散射相反,本来不指向相机的光线也可能被偏转到相机的方向,这就是入射散射。
在这里插入图片描述
入射散射允许相机看到不在可视范围内的光源,比较明显的例子就是光晕,比如下图由于Particle的偏转,会让人觉得沿着视线和Particle的连线处有光线,看起来也就是光晕。
在这里插入图片描述

1.2 基于体绘制的单次散射模型(Volumetric Single Scattering)

光线在实际传播当中会偏转很多次,就像下图这样
在这里插入图片描述
进行仿真是光线追踪的内容,对于本文来说,我们只考虑单次散射——即光线从太阳发出过、只经过一次散射改变方向后射入我们的眼睛。

对于渲染一个行星的大气,我们会逐步构建一个模型,首先我们考虑相机
在这里插入图片描述

  • 摄像机的视线从A处进入大气,经过B处
  • 我们计算光线从B到A的过程中,每一个位置P造成的出射散射的衰减贡献&入射散射的叠加贡献

该模型中也存在光源的贡献,我们通常假设是太阳的光线经过入射散射的偏转来到我们眼前
在这里插入图片描述
太阳在照射到P的过程中也存在出射散射
在这里插入图片描述
总结这些部分,混合在一起,也就是如下的样子
在这里插入图片描述

  • 相机的视线从A进入,经过行星大气到B
  • 我们需要去计算AB段上每一点P的散射贡献
    • P点接受的光线都来自于太阳
    • P点接受的光线进入大气层时会受到出射散射的影响
    • P点接受的光线经过入射散射偏转进入摄像机,途中又会经过出射散射偏离

2 大气散射背后的理论

2.1 透射函数

为了计算传入相机的光线有多少,我们可以先考虑一段光线,如下图所示
在这里插入图片描述

  • 光线从太阳传到C,此时是真空,所以没有损失,我们用 I C I_C IC指代C点的光强
  • 在从C到P的过程中,经过出射散射,光强会衰减,我们用 I P I_P IP指代P点的光强
  • I P I_P IP I C I_C IC的值也就是透射率(transmittance)
  • 透射率 T T T表示在某段路径上的对光照的衰减程度。 T ( C P ‾ ) = I P I C T(\overline{CP})=\frac{I_P}{I_C} T(CP)=ICIP
  • 对于某一个值的透射率 T T T,P点的光强也就是 I P = I C ∗ T ( C P ‾ ) I_P=I_C*T(\overline{CP}) IP=ICT(CP)

2.2 散射函数

P点在接受光照后,会因为散射偏转一部分
于是我们需要散射函数 S S S来表示某一方向上光线的偏转情况。

如下图,那些偏转了 θ θ θ角的光线才能被指向A点的摄像机
在这里插入图片描述
S ( λ , θ , h ) S(λ,θ,h) S(λ,θ,h)的值来代表发生了 θ θ θ度偏转的光线比重。

λ λ λ代表光线的波长, θ θ θ是偏转角度, h h h是P点的离地高度(高度会影响大气密度,密度会影响散射程度)

于是我们可以得到从P点出发到A点的光强了
在这里插入图片描述
带入之前求出的 I P = I C ∗ T ( C P ‾ ) I_P=I_C*T(\overline{CP}) IP=ICT(CP)
在这里插入图片描述
整个过程是这样

  • 光从太阳进入 C C C点,真空中无影响
  • C C C点进入大气传到 P P P点,受到出射散射影响只有占比 T ( C P ‾ ) T(\overline{CP}) T(CP)的光线到达了 P P P
  • P P
以下是一个基本的Unity URP次表面散射(SSS)着色器示例: ``` Shader "Custom/URP SSS" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _NormalMap ("Normal Map", 2D) = "bump" {} _SubsurfaceColor ("Subsurface Color", Color) = (1,1,1,1) _SubsurfaceRadius ("Subsurface Radius", Range(0.0,1.0)) = 0.25 _ThicknessMap ("Thickness Map", 2D) = "white" {} _SpecularColor ("Specular Color", Color) = (0.5,0.5,0.5,1) _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags {"RenderType"="Opaque"} LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; float4 tangent : TANGENT; }; struct v2f { float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; float3 worldTangent : TEXCOORD3; float3 worldBitangent : TEXCOORD4; UNITY_VERTEX_INPUT_INSTANCE_ID }; sampler2D _MainTex; sampler2D _NormalMap; sampler2D _ThicknessMap; float4 _MainTex_ST; float4 _ThicknessMap_ST; float _SubsurfaceRadius; float4 _SubsurfaceColor; float4 _SpecularColor; float _Glossiness; float _Metallic; float3 CalculateTangent(float3 n, float3 p, float2 uv, float3 dpdu) { float3 dpdv = cross(n, dpdu); float3 t = normalize(dpdv - uv.y * dpdu); return t; } float3 CalculateBitangent(float3 n, float3 p, float2 uv, float3 dpdu) { float3 dpdv = cross(n, dpdu); float3 b = normalize(cross(n, dpdv)); return b; } float3 CalculateSubsurface(float3 worldPos, float3 worldNormal, float3 worldTangent, float3 worldBitangent) { // Calculate tangent space vectors float3x3 TBN = float3x3(worldTangent, worldBitangent, worldNormal); // Calculate thickness float thickness = tex2D(_ThicknessMap, TRANSFORM_TEX(worldPos, _ThicknessMap)).r; // Calculate subsurface scattering float3 subsurface = _SubsurfaceColor.rgb * thickness * _SubsurfaceRadius; // Transform subsurface scattering to world space subsurface = mul(TBN, subsurface); return subsurface; } float4 LightingURP(SurfaceOutputStandard s, float3 lightDir, float3 worldPos, float atten) { // Calculate subsurface scattering float3 subsurface = CalculateSubsurface(worldPos, s.Normal, s.Tangent, s.Bitangent); // Calculate diffuse lighting float diff = max(0.0, dot(s.Normal, lightDir)); float3 diffuse = (s.Albedo * _LightColor0.rgb * diff); // Calculate specular lighting float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); float3 halfDir = normalize(lightDir + viewDir); float spec = pow(max(0.0, dot(s.Normal, halfDir)), s.Smoothness); float3 specular = _SpecularColor.rgb * _LightColor0.rgb * spec; // Combine lighting float3 finalColor = ((diffuse + specular) * atten) + subsurface; return float4(finalColor, s.Alpha); } v2f vert (appdata v) { v2f o; o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldTangent = UnityObjectToWorldDir(UnityObjectToWorldDir(v.tangent.xyz, UNITY_MATRIX_IT_MV), UNITY_MATRIX_IT_MV); o.worldBitangent = cross(o.worldNormal, o.worldTangent) * v.tangent.w; o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_SETUP_INSTANCE_ID(v); return o; } fixed4 frag (v2f i) : SV_Target { SurfaceOutputStandard s = SurfaceOutputStandard(); s.Albedo = tex2D(_MainTex, i.uv).rgb; s.Normal = UnpackNormal(tex2D(_NormalMap, i.uv)); s.Smoothness = _Glossiness; s.Metallic = _Metallic; float3 worldPos = i.worldPos; float3 worldNormal = normalize(i.worldNormal); float3 worldTangent = normalize(i.worldTangent); float3 worldBitangent = normalize(i.worldBitangent); float4 finalColor = LightingURP(s, normalize(_WorldSpaceLightPos0.xyz), worldPos, _LightColor0.a); return finalColor; } ENDCG } } FallBack "Diffuse" } ``` 这个着色器包含了一个基本的表面纹理、法线贴图、次表面散射颜色和半径、厚度贴图、高光颜色、光滑度和金属度选项。在顶点着色器中,我们还计算了Tangent和Bitangent向量,并传递给片段着色器以计算次表面散射。在片段着色器中,我们计算了次表面散射、漫反射和高光光照,并将它们合并以产生最终颜色。请注意,此着色器是基于URP版本的标准表面着色器,并使用UnityCG.cginc文件中定义的LightingURP函数来计算光照。
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值