Ever17's Studio.

快速入门大气渲染【实现篇(一)】

字数统计: 1.9k阅读时长: 9 min
2021/10/10 Share

前言

上一篇讲到了大气散射单次散射的理论,这篇将会将理论化作实现,一步步去实现这套单次散射。因为是在URP去做后处理散射实现,需要用到RendererFeature,这部分本文不会涉及,如果对RendererFeature不怎么了解可以自行找文章了解。

第一步:大体框架

首先是volume面板,这样就能在volume里添加大气散射的后处理了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using System;
namespace UnityEngine.Rendering.Universal
{
[Serializable,VolumeComponentMenu("Atmospheric Scattering")]
public class Atmosphere:VolumeComponent,IPostProcessComponent
{
public BoolParameter Enable = new BoolParameter(false);


public bool IsActive()
{
return Enable.value&&active;
}

public bool IsTileCompatible()
{
return false;
}


}
}

然后就是rendererfeature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class AtmosphereFeature : ScriptableRendererFeature
{
class CustomRenderPass : ScriptableRenderPass
{
const string tag = "Atmosphere";
Atmosphere atmosphere;
RenderTargetIdentifier source;
Material mtl;
RenderTextureDescriptor desc;
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
desc = renderingData.cameraData.cameraTargetDescriptor;
}


public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var stack = VolumeManager.instance.stack;
atmosphere = stack.GetComponent<Atmosphere>();
if(atmosphere.IsActive())
{
CommandBuffer cmd = CommandBufferPool.Get(tag);
int AtmosphereTempRTid = Shader.PropertyToID("_AtmosphereTempRT");

using (new ProfilingScope(cmd, new ProfilingSampler(tag)))
{
cmd.GetTemporaryRT(AtmosphereTempRTid, desc);
cmd.Blit(this.source, AtmosphereTempRTid,mtl);
cmd.Blit(AtmosphereTempRTid, this.source);
cmd.ReleaseTemporaryRT(AtmosphereTempRTid);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}

}


public override void OnCameraCleanup(CommandBuffer cmd)
{
}
public void setUp(RenderTargetIdentifier source,Material mtl)
{
this.source = source;
this.mtl = mtl;


}

}

CustomRenderPass m_ScriptablePass;
public Material AtmosphericScatteringMaterial;

/// <inheritdoc/>
public override void Create()
{
m_ScriptablePass = new CustomRenderPass();


m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
}


public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
m_ScriptablePass.setUp(renderer.cameraColorTarget, AtmosphericScatteringMaterial);
renderer.EnqueuePass(m_ScriptablePass);
}
}



第二步:后处理shader编写

10

我们需要计算的部分其实非常简单,相位函数P是和ViewDir/LightDir点积相关,这部分套公式非常好求出。β(λ)也是套公式或者我们完全可以先粗略用一个参数代替。后面的积分部分就复杂了很多,我们先聚焦一点P的强度,是和CP、PA的光学深度有关,这部分需要各对路径上做一次积分,积分这部分我们用光线步进代替,路径长度也非常简单,我们可以利用勾股定理三角形的性质或者干脆用最粗暴的联立方程组解交点然后distance拿到长度。拿到P点的强度后,对应方向上的强度需要对整个视线路径上的P点积分。也就是整体其实算是一个积分嵌套(其中能把三次积分的for循环放在两次里做,这里仅仅为了跟着自己的思路来不做这些优化)

第一步我们需要知道世界坐标,我们通过深度图可以很方便的拿到世界坐标,深度重建世界坐标方法非常多,这里直接用unity封装的方法解出

1
2
3
4
5
  
float depth = SAMPLE_TEXTURE2D(_CameraDepthTexture,sampler_CameraDepthTexture,i.uv);

float3 posWS=ComputeWorldSpacePosition( i.uv,depth, UNITY_MATRIX_I_VP);

接着就是上文提到的积分过程

首先是求解整个视线的长度【后面的PPos就是AP上的Pn位置】

1
float PALen=min(getLen(PPos,viewDir,_AtmosphereHeight+_EarthRadius,centerWS),LinearEyeDepth(depth,_ZBufferParams)); 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
float getLen(float3 pos, float3 dir, float R,float3 centerPos)
{
float3 PO = centerPos - pos;
float lPO = length(PO);
float B = 2.0*dot(PO, dir);

float C = B*B - 4*(lPO*lPO-R*R);
float det = sqrt( max(0, C) );

//elsed: .5*(B+det)
float d = 0.5*(B-det);
float3 intersect = pos+d*dir;

return distance(intersect,pos);
}

然后就是喜闻乐见的步进积分过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
for(float k=0.5;k<maxStepCount;++k)
{
PPos=_WorldSpaceCameraPos+stepRay*k; //P步进


//==========DCP

float CPLen=getLen(PPos,lightDir,_AtmosphereHeight+_EarthRadius,centerWS);

//float3 CPos=PPos+lightDir*CPLen;


float3 stepCP=(lightDir*CPLen)/maxStepCount;
float stepCPSize=length(stepCP);
float opticalDepthCP=0;


for(float j=0.5;j<maxStepCount;++j)
{
float3 pos=PPos+stepCP*j;
float h=abs(length(pos-centerWS)-_EarthRadius);

opticalDepthCP+=exp(-(h/_H) )* stepCPSize ;


}


//============DCPEnd
//PA路径上
float PALen=distance(_WorldSpaceCameraPos,PPos);
float3 stepPA=PALen*viewDir/maxStepCount;
float stepPASize=length(stepPA);
float3 pSamplePos=PPos;
for(float g=0;g<maxStepCount;++g) //P
{

opticalDepthPA+=exp(-abs(length(PPos-centerWS)-_EarthRadius)/_H)*stepPASize;
pSamplePos+=stepPA;
}
T+= exp(-_betaRL*(opticalDepthCP+opticalDepthPA))*exp(-abs(length(PPos-centerWS)-_EarthRadius)/_H)*stepRaySize; //T项


}

到此为止,最麻烦的部分结束了,只需要吧其他的项乘上去就OK了,我们拿瑞利散射为例,最后的shader为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
Shader "Hidden/AtmosphericScattering"
{
Properties
{
_EarthRadius("EarthRadius",float)=1600
_AtmosphereHeight("AtmosphereHeight",float)=1000
_H("H0(大气平均密度所在的高度)",float)=2000
//_betaMie("β(λ)Mie",float)=0.9
_betaRL("β(λ)RL",float)=0.9
_SunColor("SunColor",Color)=(1,1,1,1)
}
SubShader
{
Tags { "LightMode" = "UniversalForward"}
LOD 100

Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag


#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;

float4 vertex : SV_POSITION;
//float4 rayVS:TEXCOORD1;
};
CBUFFER_START(UnityPerMaterial)
float _EarthRadius;
float _AtmosphereHeight;
float _H;
//float _betaMie;
float _betaRL;
half4 _SunColor;
CBUFFER_END

TEXTURE2D(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture);
// float getLen2(float3 pos,float3 dir,float R,float3 centerPos)
// {

// float3 ACenter=centerPos-pos;
// float lenACenter=length(ACenter);
// float cosTheta=dot(normalize(ACenter),dir);

// //
// float lenAC=lenACenter*cosTheta;
// float lenO2C= sqrt(lenACenter*lenACenter-lenAC*lenAC);
// float lenA2C=sqrt(R*R-lenO2C*lenO2C);
// float lenAA2=lenA2C-lenAC;
// float lenBC=lenA2C;
// float AB=cosTheta>0?lenBC+lenAC:lenBC-lenAC;
// return AB;
// }
float getLen(float3 pos, float3 dir, float R,float3 centerPos)
{
float3 PO = centerPos - pos;
float lPO = length(PO);
float B = 2.0*dot(PO, dir);

float C = B*B - 4*(lPO*lPO-R*R);
float det = sqrt( max(0, C) );

//elsed: .5*(B+det)
float d = 0.5*(B-det);
float3 intersect = pos+d*dir;

return distance(intersect,pos);
}


v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex);
o.uv = v.uv;
#if UNITY_UV_STARTS_TOP
o.uv.y=1-o.uv.y;
#endif



return o;
}

half4 frag (v2f i) : SV_Target
{

float depth = SAMPLE_TEXTURE2D(_CameraDepthTexture,sampler_CameraDepthTexture,i.uv);

float3 posWS=ComputeWorldSpacePosition( i.uv,depth, UNITY_MATRIX_I_VP);

//return half4(posWS,1);

//
float3 viewDir=_WorldSpaceCameraPos-posWS;
float viewLen=length(viewDir);
viewDir=-normalize(viewDir);
Light light=GetMainLight();
float3 lightDir=light.direction;

float LdotV=dot(lightDir,-viewDir);

float3 centerWS=float3(0,-_EarthRadius,0); //世界空间原点(0,0,0)作为表面起始位置,地心则在正下方R处

float maxStepCount=20;



float3 PPos=_WorldSpaceCameraPos;

//float PALen=getLen(PPos,viewDir,_AtmosphereHeight+_EarthRadius,centerWS);
float PALen=min(getLen(PPos,viewDir,_AtmosphereHeight+_EarthRadius,centerWS),LinearEyeDepth(depth,_ZBufferParams));

float3 stepRay=(viewDir*PALen)/maxStepCount;
float stepRaySize=length(stepRay);
float opticalDepthPA=0;
float T=0;



for(float k=0.5;k<maxStepCount;++k)
{
PPos=_WorldSpaceCameraPos+stepRay*k; //P步进


//==========DCP

float CPLen=getLen(PPos,lightDir,_AtmosphereHeight+_EarthRadius,centerWS);

//float3 CPos=PPos+lightDir*CPLen;


float3 stepCP=(lightDir*CPLen)/maxStepCount;
float stepCPSize=length(stepCP);
float opticalDepthCP=0;


for(float j=0.5;j<maxStepCount;++j)
{
float3 pos=PPos+stepCP*j;
float h=abs(length(pos-centerWS)-_EarthRadius);

opticalDepthCP+=exp(-(h/_H) )* stepCPSize ;


}


//============DCPEnd
//PA路径上
float PALen=distance(_WorldSpaceCameraPos,PPos);
float3 stepPA=PALen*viewDir/maxStepCount;
float stepPASize=length(stepPA);
float3 pSamplePos=PPos;
for(float g=0;g<maxStepCount;++g) //P
{

opticalDepthPA+=exp(-abs(length(PPos-centerWS)-_EarthRadius)/_H)*stepPASize;
pSamplePos+=stepPA;
}
T+= exp(-_betaRL*(opticalDepthCP+opticalDepthPA))*exp(-abs(length(PPos-centerWS)-_EarthRadius)/_H)*stepRaySize; //T项


}



//相位函数
float phase=(3.0 / (16.0 * 3.1415926)) * (1.0+ (LdotV * LdotV));

float3 I_A=_SunColor.rgb*T*_betaRL*phase;

// return T;
return half4(I_A.rgb,1);
}

ENDHLSL
}
}
}

结果:

RL

RL2

先解释一下为什么近景是黑的,这就涉及到大气散射的大气透视了,他和天空盒渲染不一样的地方在于,地表上的物体本身也会贡献反射出来的光到眼睛,为什么我们能看到物体,因为光打到物体反射到人眼内,而这部分的光也会受到大气散射的影响,也就是需要本身颜色乘上衰减方程T,而画面里近处景色我们还没处理,所以本身积分的量就相比非常远的天空盒就非常微弱基本为0。

结束

本文暂时到这里就告一段落了,本篇算是按照推导比较完整的实现了瑞利散射模型。因为瑞利散射和米氏散射本身做法没区别只是在相位函数以及H的取值有区别,本文出于时间问题先结一个尾。后续有时间应该会再发布一篇米氏散射+瑞利散射+大气透视的更加完整的版本。

CATALOG
  1. 1. 前言
  2. 2. 第一步:大体框架
  3. 3. 第二步:后处理shader编写
  4. 4. 结束