mylovelyqq 发表于 2024-7-15 18:48

【UnityShader】Volumetric Light 体积光(8)

前言:

其实我想说一句,基于后措置光线步进的体衬着实际上巨简单。
体积光 = 重构世界坐标 + 光线步进 + 暗影采样 + 散射模型(可选)
体积云/雾 = 重构世界坐标 + 光线步进 + 密度采样 + 散射模型 + 光照采样
而实际在使用时,又有许多相对性能友好的trick方案来模拟体衬着——比如万能的BillBoard。不外这都是后话,我们先来看看传统的体积光是怎么实现吧。
以下运行环境基于Unity URP。
一、体积光的物理意义

当光线穿过胶体,由于胶体微粒对光线的散射感化,发生丁达尔现象。在实时衬着中这样的效果常称为体积光 (Volumetric Light),有时也称作上帝光 (god light),或叫做LightShaft、GodRay

上图就是体积光应用的例子——“优秀的体积光特效在衬托游戏氛围,提高画面质感方面阐扬了很大的感化。”
那么“丁达尔效应”究竟是如何影响光照信息的呢?


如图所示,我们的摄像机的视线,本来应该是打在墙面长进行着色。但是由于左侧环境有“缺口”,光源的光路与视线存在交点。此时我们认为空气中有悬浮颗粒,能造成光线的散射,那么意味着有一部门光线会沿着与我们视线平行的标的目的进入摄像机,那么就会导致原本的着色点更“亮”。
这种造成“着色点更亮”的成果,就是我们视觉上认为的“体积光”。
而“光线步进”实际上就是在我们的视线标的目的上不竭采样,看看采样点是否受光,统计起来就是这一点的总计“光强”。


此中,0、1号采样点会贡献光强,2、3号采样点由于光路被墙壁否决,光强为0。
那么,光强信息是怎么得到的呢?很明显,这是一种“暗影信息”,我们需要的是类似于:
Intensity = GetLightIntensity(float 3 worldPosition); 的接口,最直接的方式就是ShadowMap。
当然,如果我们使用的引擎的暗影解决方案不是ShadowMap,那么自然有此外的配套的暗影信息计算,那就是另一码事了。
一般的,有了暗影信息之后,我们就能知道采样点的光照信息了。但是实际上光线在空间传布时,会有本身的衰减模型。
另一方面,光子在触碰到悬浮颗粒时,它也有本身的散射模型。这些城市影响到实际的光强。
光线散射
光在介质中传布,会受以下四种因素而发生改变:
Absorption,光线被物质吸收并由光能转化为其他能量
Out-scattering,光线被介质中的微粒向外散射
Emission,介质由于黑体辐射等因素发生自发光
In-scattering,从其他处所散射到当前光路上的光线

只不外本章不搞这么复杂,直接认为【采样点的光强=光源光强 * 强度因子】
至此,基于光线步进的体积光算法我们就构建出来了,下面我们来看看实现吧。
二、代码实现

1. 重构世界坐标

我们是基于后措置的体积光,自然需要从深度图重构世界坐标来进行步进计算。
虽然在这之前我也介绍过重构方式:
但在这里,我想使用Unity URP自带的函数来重构看看。
首先声明深度图,直接引用一个库:
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl”然后进行坐标计算,代码来自Unity文档
float3 GetWorldPosition(float3 positionHCS)
{
    /* get world space position from clip position */

    float2 UV = positionHCS.xy / _ScaledScreenParams.xy;
    #if UNITY_REVERSED_Z
    real depth = SampleSceneDepth(UV);
    #else
    real depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(UV));
    #endif
    return ComputeWorldSpacePosition(UV, depth, UNITY_MATRIX_I_VP);
}然后在frag中显示世界坐标看当作果:
half4 frag(Output i) : SV_TARGET
{
    float3 worldPos = GetWorldPosition(i.positionCS);
    return half4(worldPos,1);
}定义好URP下的RenderFeature与RenderPass,启用后措置看看效果:



左: 正常视图衬着 右:世界坐标衬着

看起来没什么问题,就这样用吧!
2. 光线步进

定义好一系列参数。
起点:摄像机的世界坐标。终点:像素的世界坐标。步进次数: _StepTime等。然后一个for循环搞定。
half4 frag(Output i) : SV_TARGET
{
    float3 worldPos = GetWorldPosition(i.positionCS); //像素的世界坐标
    float3 startPos = _WorldSpaceCameraPos; //摄像机上的世界坐标
    float3 dir = normalize(worldPos - startPos); //视线标的目的
    float rayLength = length(worldPos - startPos); //视线长度
    rayLength = min(rayLength, MAX_RAY_LENGTH); //限制最大步进长度,MAX_RAY_LENGTH这里设置为20

    float3 final = startPos + dir * rayLength; //定义步进结束点

    half3 intensity = 0; //累计光强
    float2 step = 1.0 / _StepTime; //定义单次插值大小,_StepTime为步进次数
    for(float i = 0; i < 1; i += step) //光线步进
    {
      float3 currentPosition = lerp(startPos, final, i); //当前世界坐标
      float atten = GetLightAttenuation(currentPosition) * _Intensity; //暗影采样,_Intensity为强度因子
      float3 light = atten;
      intensity += light;
    }
    intensity /= _StepTime;
    return half4(intensity,1); //查当作果
}这里的采样点计算方式是“插值计算”,是为了后面优化改削便利。
3. 暗影采样

在上面代码中GetLightAttenuation(float3 worldPos)用于计算暗影。实际上,它的实现是:
float GetLightAttenuation(float3 position)
{
    float4 shadowPos = TransformWorldToShadowCoord(position); //把采样点的世界坐标转到暗影空间
    float intensity = MainLightRealtimeShadow(shadowPos); //进行shadow map采样
    return intensity; //返回暗影值
}这是URP实现下的ShadowMap采样方式。值得注意的是,如果启用了“级联暗影”,我们必需在文件头上定义两个宏:MAIN_LIGHT_CALCULATE_SHADOWS 与 _MAIN_LIGHT_SHADOWS_CASCADE
...
#define MAIN_LIGHT_CALCULATE_SHADOWS//定义暗影采样
#define _MAIN_LIGHT_SHADOWS_CASCADE //启用级联暗影

#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
#include ”Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl”
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl”//暗影计算库
#include ”Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl”
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl”
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl”
...这两个宏的定义非常重要,原因后文再解释。
当我们弄好暗影采样后,输出的图像应该类似于这样:



左:原场景 右:步进+暗影采样衬着成果

cool,看起来有内味了。
当然,值得注意的是,步进次数(_StepTime)直接影响衬着质量:



_StepTime = 8,16,32,64

4. 图像混合

现阶段,图像混合很简单,直接把上面的衬着图和原本的场景颜色相加即可:



左:场景原图 中:体积光采样图 右:混合图步进次数:64,强度: 0.556

棒极了!至此,我们就实现了最基本的体积光啦!
三、 细节,优化与trick

虽然看起来我们已经实现了体积光,但是在步进次数64的情况下实际消耗是怎样的呢?



恐怖如斯,体积光衬着直接需要近20ms

减少步进次数是一个好方式,改成8尝尝看?



耗时骤降到3.5ms摆布

但是8的采样次数,效果惨不忍睹。



虽然8的采样次数只要3.5ms,但是效果完全不能看

能不能给力一点啊sir,还有什么其他的黑科技能兼顾性能与表示吗?
虽然这世界上没有“Silver Bullet”(银弹),但是“Trick”总是有的。
1. 降低采样次数

一般的,采样次数是必定要降低的,这里,我们粗暴地定义为8。
2. 降低采样分辩率

值得注意的是,我们是基于后措置的采样,是“逐像素”的计算。
一张“1920x1080”的RenderTexture,在8次采样下,总计会计算:1920x1080x8 =16,588,800次。
想一想,体积光的衬着成果,有必要这么“精细”吗?完全没有必要,因此,我们完全可以拿一张1/2分辩率的图去进行光线步进。直接提升75%的效率。



960 x 540的分辩率 8次步进

棒极了,在1/2分辩率与8次步进下,只用了0.84ms。
但是!这成果怎么不合错误!



1/2分辩率下,光照错位

直觉告诉我这是“世界坐标”计算错误而导致的。上文我们使用的时Unity文档的计算世界坐标方式,似乎无法应对分歧分辩率下的情况,我们还是老诚恳实本身写一套重构世界坐标的代码吧。
float3 GetWorldPosition(float2 uv, float3 viewVec)
{
    float depth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture,sampler_CameraDepthTexture,uv).r;//采样深度图
    depth = Linear01Depth(depth, _ZBufferParams); //转换为线性深度
    float3 viewPos = viewVec * depth; //获取实际的不雅察看空间坐标(插值后)
    float3 worldPos = mul(unity_CameraToWorld, float4(viewPos,1)).xyz; //不雅察看空间-->世界空间坐标
    return worldPos;
}
...在VertexShader中...   
    float3 ndcPos = float3(v.uv.xy * 2.0 - 1.0, 1); //直接把uv映射到ndc坐标
    float far = _ProjectionParams.z; //获取投影信息的z值,代表远平面距离
    float3 clipVec = float3(ndcPos.x, ndcPos.y, ndcPos.z * -1) * far; //裁切空间下的视锥顶点坐标
    o.viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz; //不雅察看空间下的视锥向量
..在FragmentShader中...
    float3 worldPos = GetWorldPosition(i.uv, i.viewVec); //像素的世界坐标嗯,这下对了。



改削世界坐标计算方式后的成果

3. 发抖

好,此刻开销在1ms内是下来了,但是衬着效果太差了,怎么办呢?
之前就说了,想对RayMarching系做表示优化,问就是发抖(Jitter)。这里,我们选择发抖采样点。


为什么发抖采样点?如图:如果我们等距采样,在某些极端情况下,所有采样点全部被物体遮挡,得到的光强为0。但实际上实现与光路是有交点的,这样就发生了绝对的误差。
如果我们随机采样(发抖),原本被遮挡的采样点,更有“概率”跑到光强不为0的点,这样,即使是低次数采样,成果“更有可能”接近实际的光强信息,误差相对而言变小了。
如果我们定义一个概率【p=视线上光强不为0长度/总视线长度】,甚至我们可以从数学上证明:随着尝试次数增大,随机采样得到的概率p趋近于真实概率p。(当然我没有证明,盲猜是对的)
总而言之,我们在光线步进处,插手发抖:
#define random(seed) sin(seed * 641.5467987313875 + 1.943856175)
...
half4 frag(Output i) : SV_TARGET
{
    float3 worldPos = GetWorldPosition(i.uv, i.viewVec); //像素的世界坐标
    float3 startPos = _WorldSpaceCameraPos; //摄像机上的世界坐标
    float3 dir = normalize(worldPos - startPos); //视线标的目的
    float rayLength = length(worldPos - startPos); //视线长度
    rayLength = min(rayLength, MAX_RAY_LENGTH); //限制最大步进长度,MAX_RAY_LENGTH这里设置为20

    float3 final = startPos + dir * rayLength; //定义步进结束点
    float2 step = 1.0 / _StepTime;
    step.y *= 0.4;
    float seed = random((_ScreenParams.y * i.uv.y + i.uv.x) * _ScreenParams.x + _RandomNumber);
    half3 intensity = 0; //累计光强
    for(float i = step.x; i < 1; i += step.x)
    {      
      seed = random(seed);
      float3 currentPosition = lerp(startPos, final, i + seed * step.y);
      float atten = GetLightAttenuation(currentPosition) * _Intensity; //暗影采样,_Intensity为强度因子
      float3 light = atten;
      intensity += light;
    }
    intensity /= _StepTime;
    return half4(intensity,1); //查当作果
}step.y则看起来有些不易理解,其实他的感化是确定随机采样的范围,我们将其手动设置为0.4,意思是上一个采样点在-0.5的位置,下一个采样点在1.5的位置,而当前采样点的抱负采样点应该是0.5处,这时我们的随机范围则应该逗留在0.1到0.9之间,这样设计的目的是每一个采样点都有较大的随机范围,同时总采样距离相对一致且采样点之间不会反复采样或交叉采样,影响衬着正确性,而这个随机算法也很有意思,图中调用的random函数定义如下:
#define random(seed) sin(seed * 641.5467987313875 + 1.943856175)
我们看到,这其实是一个非常简单的操作sin函数进行一个魔数偏移的伪随机,由于sin函数的分布相对均匀且我们这里每次偏移数值较大,可以保证必然范围内采样的随机性和均匀性,而这一句
float seed = random((_ScreenParams.y * screenPos.y + screenPos.x) * _ScreenParams.x + _RandomNumber);
才是我们这里随机计算的点睛之笔,_RandomNumber是一个依靠脚本传输的随机数,每帧都是纷歧样的,而上边的这一句其实是将整张图想象成一个二维数组,而UV则是数组的索引值,因此通过对这个“数组”的降维措置,获得每个像素点并世无双的随机种子,接下来的代码就非常好理解了,将每个随机数生成的种子反复操作,并生成一个区间在[-1,1]的随机数,使其配合上述的raymarch流程工作。这段算法与代码来自MaxwellGeng老师的Unity3D实时体积光(二)咱们吠影吠声搬过来当黑盒用用。
插手发抖后,再看当作果是怎样的。



8次采样,插手发抖

看,发抖就是这么的神奇,“体积感”瞬间就呈现了。
当然,值得注意的是,如果我们的发抖算法与噪声算法定的不合适,效果定然会分歧(甚至更糟),这里再次感激一下麦老师。
4. 模糊

此刻,我们使用模糊措置满屏的噪点。一般的,“双边滤波”能在保留边缘信息的同时消除噪点的锋利程度。只不外当前笔者还没有学习过双边滤波,暂时使用Kawase模糊作为替代。
half4 fragment(v2f i):SV_TARGET //Kawase模糊的核心思想就是动态大小的卷积核而已
{            
    half4 tex=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord); //中心像素
    //四角像素
    //注意这个【_BlurRange】,这就是扩大卷积核范围的参数
    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord+float2(-1,-1)*_MainTex_TexelSize.xy*_BlurRange);
    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord+float2(1,-1)*_MainTex_TexelSize.xy*_BlurRange);
    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord+float2(-1,1)*_MainTex_TexelSize.xy*_BlurRange);
    tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord+float2(1,1)*_MainTex_TexelSize.xy*_BlurRange);
    return tex/5.0;
}同时,既然是模糊,那我们还可以再降一次分辩率,再使用1/2的体积光衬着的分辩率进行模糊(此刻的模糊RT已经是原分辩率的1/4了)



左:模糊范围0.1 右:模糊范围0.56



GPU : Radeon RX550, 0.9ms摆布的开销

实际上,一旦涉及到“随机采样”“发抖”,那么TAA(Temporal Anti-Aliasing)也自然而然会被提及,如果再加上TAA啥的,画面效果可能会更好。
至此,我们用0.9ms的开销,逼近了采样64次(20ms)的画面,太棒了。
四、踩坑记录

我不得不吐槽的是,之前看别人的体积光进行暗影采样的时候,似乎都是本身手动算的级联暗影信息。那么在URP环境下在如何采样级联暗影呢?搜索了半天,才知道所需要的函数名称与库:
#define MAIN_LIGHT_CALCULATE_SHADOWS//定义暗影采样
#define _MAIN_LIGHT_SHADOWS_CASCADE //启用级联暗影
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl” 第一,宏定义必需在库引用之前!(之前我写在了后面,暗影贴图死活不采样,弄了半天才大白)。
第二,若项目中使用了级联暗影,_MAIN_LIGHT_SHADOWS_CASCADE 这个宏必需要有。



左:定义了级联宏 右:不决义

如果不定义,你就会发此刻特定情况下,如右图一般的严重漏光现象。
此现象发生的本质原因是,右侧像素已经超过一级级联的采样范围(这里是10m),如果不定义宏,那么永远返回第一级级联,然后采样成果错误。
//URP的shadow.hlsl
float4 TransformWorldToShadowCoord(float3 positionWS)
{
#ifdef _MAIN_LIGHT_SHADOWS_CASCADE
    half cascadeIndex = ComputeCascadeIndex(positionWS); //按照世界坐标进行级联计算
#else
    half cascadeIndex = 0; //万恶之源,永远范围第一级级联
#endif

    float4 shadowCoord = mul(_MainLightWorldToShadow, float4(positionWS, 1.0));

    return float4(shadowCoord.xyz, cascadeIndex);
}这个问题弄了我半个月,搞了半天才反映过来这是级联计算的问题,以此为鉴。
五、完整代码

VolumetricLightTutorial.hlsl

#ifndef VOLUME_LIGHT
#define VOLUME_LIGHT

#define MAIN_LIGHT_CALCULATE_SHADOWS//定义暗影采样
#define _MAIN_LIGHT_SHADOWS_CASCADE //启用级联暗影

#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
#include ”Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl”
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl”
#include ”Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl”
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl”
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl”


#define random(seed) sin(seed * 641.5467987313875 + 1.943856175)
#define MAX_RAY_LENGTH 20

struct Varings
{
    float3 positionOS : POSITION;
    float2 uv : TEXCOORD0;
};
struct Output
{
    float4 positionCS : SV_POSITION;
    float2 uv : TEXCOORD0;
    float3 viewVec : TEXCOORD1;
};

float _RandomNumber;
float _Intensity;
float _StepTime;

TEXTURE2D(_MainTex);
TEXTURE2D(_LightTex);

SAMPLER(sampler_MainTex);
SAMPLER(sampler_LightTex);

Output vert(Varings v)
{
    Output o;
    o.positionCS = TransformObjectToHClip(v.positionOS);
    o.uv = v.uv;

    float3 ndcPos = float3(v.uv.xy * 2.0 - 1.0, 1); //直接把uv映射到ndc坐标
    float far = _ProjectionParams.z; //获取投影信息的z值,代表远平面距离
    float3 clipVec = float3(ndcPos.x, ndcPos.y, ndcPos.z * -1) * far; //裁切空间下的视锥顶点坐标
    o.viewVec = mul(unity_CameraInvProjection, clipVec.xyzz).xyz; //不雅察看空间下的视锥向量

    return o;
}
float3 GetWorldPosition(float2 uv, float3 viewVec, out float depth, out float linearDepth)
{
    depth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture,sampler_CameraDepthTexture,uv).r;//采样深度图
    depth = Linear01Depth(depth, _ZBufferParams); //转换为线性深度
    linearDepth = LinearEyeDepth(depth,_ZBufferParams);
    float3 viewPos = viewVec * depth; //获取实际的不雅察看空间坐标(插值后)
    float3 worldPos = mul(unity_CameraToWorld, float4(viewPos,1)).xyz; //不雅察看空间-->世界空间坐标
    return worldPos;
}
float GetLightAttenuation(float3 position)
{
    float4 shadowPos = TransformWorldToShadowCoord(position); //把采样点的世界坐标转到暗影空间
    float intensity = MainLightRealtimeShadow(shadowPos); //进行shadow map采样
    return intensity; //返回暗影值
}
half4 blendFrag(Output i): SV_TARGET
{
    half4 sceneColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
    half4 lightColor = SAMPLE_TEXTURE2D(_LightTex, sampler_LightTex, i.uv);
    return lightColor + sceneColor;
}
half4 frag(Output i) : SV_TARGET
{
    float depth = 0;
    float linearDepth = 0;
    float3 worldPos = GetWorldPosition(i.uv, i.viewVec, depth, linearDepth); //像素的世界坐标
    float3 startPos = _WorldSpaceCameraPos; //摄像机上的世界坐标
    float3 dir = normalize(worldPos - startPos); //视线标的目的
    float rayLength = length(worldPos - startPos); //视线长度

    rayLength = min(rayLength, linearDepth); //裁剪被遮挡片元
    rayLength = min(rayLength, MAX_RAY_LENGTH); //限制最大步进长度,MAX_RAY_LENGTH这里设置为20

    float3 final = startPos + dir * rayLength; //定义步进结束点
    float2 step = 1.0 / _StepTime;
    step.y *= 0.4;
    float seed = random((_ScreenParams.y * i.uv.y + i.uv.x) * _ScreenParams.x + _RandomNumber);
    half3 intensity = 0; //累计光强
    for(float i = step.x; i < 1; i += step.x)
    {      
      seed = random(seed);
      float3 currentPosition = lerp(startPos, final, i + seed * step.y);
      float atten = GetLightAttenuation(currentPosition) * _Intensity; //暗影采样,_Intensity为强度因子
      float3 light = atten;
      intensity += light;
    }
    intensity /= _StepTime;

    Light mainLight = GetMainLight(); //引入场景灯光数据
    if(depth > 0.999) //这里做一个远视强度限制。
    {
      intensity = 0;
    }
    return half4(mainLight.color * intensity,1);
}
#endifVolumetricLightTutorial.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering;

public class VolumetricLightTutorial : ScriptableRendererFeature
{
   
    public class Setting
    {
      public RenderPassEvent passEvent = RenderPassEvent.BeforeRenderingPostProcessing;
      public Shader shader;

      
      public float intensity = 0.7f;
      
      public float stepTimes = 16;
      
      public float blurRange = 1;
    }

    class VolumetricLightTutorialPass : ScriptableRenderPass
    {
      private Material material;
      private RenderTextureDescriptor dsp;
      public RenderTargetIdentifier cameraTarget;
      private RenderTargetHandle temp;

      private Setting setting;
      private RenderTargetHandle buffer01, buffer02;
      public VolumetricLightTutorialPass(Setting setting)
      {
            this.renderPassEvent = setting.passEvent;
            this.setting = setting;

            if (setting.shader != null)
            {
                material = CoreUtils.CreateEngineMaterial(setting.shader);
            }
      }
      public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
      {
            base.Configure(cmd, cameraTextureDescriptor);
            dsp = cameraTextureDescriptor;
            temp.Init(”Temp”);
            buffer01.Init(”b1”);
            buffer02.Init(”b2”);
      }
      public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
      {
            if (material == null || !renderingData.cameraData.postProcessEnabled) return;

            var cmd = CommandBufferPool.Get(”VolumetricLightTutorial”);

            var dsp = this.dsp;
            dsp.depthBufferBits = 0;
            dsp.width /= 2;
            dsp.height /= 2;

            material.SetFloat(”_RandomNumber”, Random.Range(0.0f, 1.0f));
            material.SetFloat(”_Intensity”, this.setting.intensity);
            material.SetFloat(”_StepTime”, this.setting.stepTimes);

            cmd.GetTemporaryRT(temp.id, dsp);

            //体积光光线步进
            cmd.Blit(cameraTarget, temp.Identifier(), material, 0);

            //Kawase模糊
            var width = dsp.width / 2;
            var height = dsp.height / 2;
            var blurRange = this.setting.blurRange;
            cmd.GetTemporaryRT(buffer01.id, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.ARGB32);
            cmd.GetTemporaryRT(buffer02.id, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.ARGB32);

            material.SetFloat(”_BlurRange”, 0);

            cmd.Blit(temp.Identifier(), buffer01.Identifier(), material, 1);

            for (int i = 0; i < 4; i++)
            {
                material.SetFloat(”_BlurRange”, (i + 1) * blurRange);
                cmd.Blit(buffer01.Identifier(), buffer02.Identifier(), material, 1);

                var temRT = buffer01;
                buffer01 = buffer02;
                buffer02 = temRT;
            }
            cmd.SetGlobalTexture(”_LightTex”, buffer01.Identifier());

            ////blit 混合
            cmd.Blit(cameraTarget, temp.Identifier(), material, 2);
            cmd.Blit(temp.Identifier(), cameraTarget);

            context.ExecuteCommandBuffer(cmd);
            cmd.ReleaseTemporaryRT(temp.id);
            cmd.ReleaseTemporaryRT(buffer01.id);
            cmd.ReleaseTemporaryRT(buffer02.id);
            CommandBufferPool.Release(cmd);
      }
    }
    public Setting setting = new Setting();
    private VolumetricLightTutorialPass volumeLightPass;

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
      volumeLightPass.cameraTarget = renderer.cameraColorTarget;
      renderer.EnqueuePass(volumeLightPass);
    }
    public override void Create()
    {
      volumeLightPass = new VolumetricLightTutorialPass(setting);
    }
}VolumetricLightTutorialShader.shader

Shader ”Unlit/VolumetricLightTutorial”
{
    Properties
    {
      _MainTex (”Texture”, 2D) = ”white” {}
    }
    SubShader
    {
      Pass
      {
            //光线步进采样 计算体积光
            HLSLPROGRAM
            #include ”VolumetricLightTutorial.hlsl”
            #pragma vertex vert
            #pragma fragment frag
            ENDHLSL
      }      
      Pass
      {
            //Kawase 模糊
            HLSLPROGRAM
            #include ”KawaseBlur.hlsl”
            #pragma vertex vertex
            #pragma fragment fragment
            ENDHLSL
      }
      Pass
      {
            HLSLPROGRAM
            #include ”VolumetricLightTutorial.hlsl”
            #pragma vertex vert
            #pragma fragment blendFrag
            ENDHLSL
      }
    }
}小结:

其实要优化表示的话,还有什么“基于深度的模糊”“TAA”,把Kawase模糊改成双边模糊,插手散射模型啥的,笔者摸了。
当然,既然我们会了体积光,后面的体积云/雾自然也是探囊取物啦。
参考


[*]^体积光图例 https://zhuanlan.zhihu.com/p/21425792
[*]^光的散射模型 https://zhuanlan.zhihu.com/p/124297905
[*]^Unity世界坐标重构文档 https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@11.0/manual/writing-shaders-urp-reconstruct-world-position.html
[*]^发抖噪声与算法 https://zhuanlan.zhihu.com/p/39754801
[*]^URP下的光源与暗影 https://www.bilibili.com/read/cv15976514/
页: [1]
查看完整版本: 【UnityShader】Volumetric Light 体积光(8)