Particles Color and Depth Textures
支持翻页书效果、近似淡入淡出、柔化及扭曲粒子。
确定正交与透视投影中的片段深度。
复制并采样颜色与深度缓冲区。
这是关于创建自定义可编程渲染管线的教程系列第15篇。我们将依托颜色和深度纹理,实现基于深度的粒子淡化和扭曲效果。
本教程基于Unity 2019.4.14f1版本创建,并升级至2022.3.5f1版本
Using particles to create a messy atmosphere
用粒子效果营造凌乱氛围
1. Unlit Particles 无光照粒子
粒子系统可以使用任意材质,因此我们的渲染管线已能渲染它们,但存在一些限制。在本教程中,我们将仅探讨无光照粒子。有光照粒子的实现方式相同,只是需要处理更多着色器属性和光照计算。
我基于现有测试场景创建了一个粒子专用场景,其中设置了若干垂直长立方体和一个明亮的黄色灯泡,作为粒子系统的背景环境
Scene without particles and without post FX
未添加粒子效果且未启用后期特效的场景
1.1 Particle System 粒子系统
通过菜单项 GameObject / Effects / Particle System 创建粒子系统,并将其放置在地平面下方。我们假定您已了解粒子系统的基本配置,因此不在此赘述具体细节。若需了解各模块功能及参数设置,请查阅Unity官方文档
那么可视化特效图表(Visual Effects Graph)呢?
VFX图表基于计算着色器,目前与URP和HDRP紧密耦合,无法直接用于自定义SRP。
需要注意的是,传统粒子系统并未被VFX图表取代。对于许多小型系统(每个系统最多约千个粒子)而言,传统粒子系统更适用;而VFX图表则更适用于大型粒子系统
默认系统会使粒子向上运动并填充锥形区域。若将无光照材质赋予粒子系统,粒子会显示为面向摄像机平面的纯白色方块。这些粒子会突然出现和消失,但由于起始位置低于地平面,它们会呈现出从地面升起的效果
Default particle system with unlit material, positioned below ground.
使用无光照材质的默认粒子系统,位于地平面下方
1.2 Unlit Particles Shader 无光照粒子着色器
我们可以使用现有的无光照着色器来渲染粒子,但建议为其创建专属着色器。新建的着色器以无光照着色器为模板,将其菜单路径修改为 Custom RP/Particles/Unlit。另外,由于粒子始终是动态对象,该着色器无需包含元通道(meta pass)
Shader "Custom RP/Particles/Unlit" {…SubShader {…//Pass {//Tags {//"LightMode" = "Meta"//}//…//}}CustomEditor "CustomShaderGUI"
}
使用此着色器为无光照粒子创建专属材质,并让粒子系统调用该材质。目前其效果与之前的无光照材质相同。您也可以将粒子系统设置为渲染网格模型,如果材质和粒子系统同时启用阴影功能,甚至还能投射阴影。但GPU实例化在此并不适用,因为粒子系统采用了程序化绘制方式(本教程不涉及此技术)。实际上,所有粒子网格都会像布告板粒子一样被合并为单个网格
Sphere mesh particles, with shadows
球体网格粒子(启用阴影效果)
从现在起,我们将专注于布告板粒子(无阴影效果)。下图是单个粒子的基础贴图,包含了一个边缘平滑渐变的白色圆盘
Base map for single particle, on black background.
黑色背景下的单粒子基础贴图
使用该纹理制作淡化粒子时,会产生类似白烟从地面升起的简易效果。为增强真实感,可将发射率提升至100左右
Textured billboard particles, emission rate set to 100
启用纹理的布告板粒子,发射率设置为100
1.3 Vertex Colors 顶点颜色
可以为每个粒子设置不同的颜色。最简单的演示方式是将初始颜色设置为在黑与白之间随机选取。但当前这样做并不会改变粒子的外观。要实现该功能,我们必须在着色器中添加对顶点颜色的支持。我们将在UnlitPass中添加此功能,而无需为粒子创建新的HLSL文件。
第一步是添加一个使用COLOR语义的float4顶点属性
struct Attributes {float3 positionOS : POSITION;float4 color : COLOR;float2 baseUV : TEXCOORD0;UNITY_VERTEX_INPUT_INSTANCE_ID
};
同时将其添加到Varyings结构体中,并通过UnlitPassVertex传递该属性,但仅当定义了_VERTEX_COLORS宏时生效。这样我们可以根据需要灵活启用或禁用顶点颜色支持
struct Varyings {float4 positionCS : SV_POSITION;#if defined(_VERTEX_COLORS)float4 color : VAR_COLOR;#endiffloat2 baseUV : VAR_BASE_UV;UNITY_VERTEX_INPUT_INSTANCE_ID
};Varyings UnlitPassVertex (Attributes input) {…#if defined(_VERTEX_COLORS)output.color = input.color;#endifoutput.baseUV = TransformBaseUV(input.baseUV);return output;
}
接下来,在UnlitInput的InputConfig中添加颜色属性,默认设置为不透明的白色,并将其纳入GetBase函数的输出结果中
struct InputConfig {float4 color;float2 baseUV;
};InputConfig GetInputConfig (float2 baseUV) {InputConfig c;c.color = 1.0;c.baseUV = baseUV;return c;
}…float4 GetBase (InputConfig c) {float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, c.baseUV);float4 baseColor = INPUT_PROP(_BaseColor);return baseMap * baseColor * c.color;
}
回到UnlitPass,在UnlitPassFragment中,如果存在插值后的顶点颜色,将其复制到config中。
InputConfig config = GetInputConfig(input.baseUV);#if defined(_VERTEX_COLORS)config.color = input.color;#endif
最后,为了给UnlitParticles添加顶点颜色支持,需要为其添加一个开关式着色器属性
[HDR] _BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)[Toggle(_VERTEX_COLORS)] _VertexColors ("Vertex Colors", Float) = 0
同时添加对应的着色器功能来定义该关键字。如果您希望常规无光照着色器也支持顶点颜色,同样可以为其添加此功能。
#pragma shader_feature _VERTEX_COLORS
Using vertex colors, without and with sorting by distance
使用顶点颜色时,未开启与开启按距离排序的对比效果
现在我们的粒子已能显示颜色。此时粒子排序就成了关键问题:当所有粒子颜色相同时,绘制顺序无关紧要;但若颜色各异,就必须按距离排序才能获得正确效果。需要注意的是,基于距离排序时,如同所有透明物体一样,随着视角位置变化,粒子的绘制顺序可能会突然发生交换
1.4 Flipbooks 翻页动画
布告板粒子可通过循环切换不同的基础贴图来实现动画效果,Unity将其称为翻页动画粒子。这是通过使用规则网格排列的纹理图集来实现的,例如这张包含4×4网格循环噪声图案的纹理
Base map for particle flipbook, on black background
黑色背景下的粒子翻页动画基础贴图
创建一个使用翻页贴图的新无光照粒子材质,然后复制现有的粒子系统并让其使用该翻页材质。停用单粒子版本的粒子系统,以便我们只观察翻页系统。由于每个粒子现在代表一小片云朵,将粒子尺寸增大至2左右。启用粒子系统的"纹理表格动画"模块,将其配置为4×4翻页动画,设置从随机帧开始播放,并在粒子生命周期内完成一个循环。
可通过以下方式增加多样性:以50%的概率沿X轴和Y轴随机翻转粒子,设置随机初始旋转角度,并让粒子以随机角速度持续旋转
Flipbook particle system
翻页动画粒子系统
1.5 Flipbook Blending 翻页动画混合
当系统运行时,可以明显看到粒子在循环播放少数几帧,这是因为翻页动画的帧率非常低。对于生命周期为五秒的粒子,帧率仅为每秒3.2帧。通过在连续帧之间进行混合过渡可以平滑这一效果。这需要向着色器传递第二组UV坐标和动画混合因子。具体操作是在渲染器模块中启用自定义顶点流,添加UV2和AnimBlend。您也可以移除法线流,因为我们并不需要它
Custom vertex streams
自定义顶点流
添加顶点流后,系统会显示错误提示,指出粒子系统与当前使用的着色器不匹配。待我们在着色器中调用这些数据流后,该错误便会消失。请在UnlitParticle着色器中添加一个开关属性,用于控制是否支持翻页动画混合功能
[Toggle(_VERTEX_COLORS)] _VertexColors ("Vertex Colors", Float) = 0[Toggle(_FLIPBOOK_BLENDING)] _FlipbookBlending ("Flipbook Blending", Float) = 0
同时添加配套的着色器功能
#pragma shader_feature _FLIPBOOK_BLENDING
若启用翻页动画混合功能,两套UV坐标将通过TEXCOORD0以float4形式传递(而非float2)。混合因子则通过TEXCOORD1以单精度浮点数形式传递
struct Attributes {float3 positionOS : POSITION;float4 color : COLOR;#if defined(_FLIPBOOK_BLENDING)float4 baseUV : TEXCOORD0;float flipbookBlend : TEXCOORD1;#elsefloat2 baseUV : TEXCOORD0;#endifUNITY_VERTEX_INPUT_INSTANCE_ID
};
我们将在Varyings结构体中按需添加一个新的float3类型字段flipbookUVB来存储这些数据
struct Varyings {…float2 baseUV : VAR_BASE_UV;#if defined(_FLIPBOOK_BLENDING)float3 flipbookUVB : VAR_FLIPBOOK;#endifUNITY_VERTEX_INPUT_INSTANCE_ID
};
调整UnlitPassVertex函数,使其在适当时机将所有相关数据复制到flipbookUVB中
Varyings UnlitPassVertex (Attributes input) {…output.baseUV.xy = TransformBaseUV(input.baseUV.xy);#if defined(_FLIPBOOK_BLENDING)output.flipbookUVB.xy = TransformBaseUV(input.baseUV.zw);output.flipbookUVB.z = input.flipbookBlend;#endifreturn output;
}
同时将flipbookUVB添加到InputConfig中,并添加一个布尔值用于指示是否启用翻页动画混合功能(默认不启用)
struct InputConfig {float4 color;float2 baseUV;float3 flipbookUVB;bool flipbookBlending;
};InputConfig GetInputConfig (float2 baseUV) {…c.flipbookUVB = 0.0;c.flipbookBlending = false;return c;
}
若启用翻页动画混合功能,我们必须在GetBase函数中基于混合因子对基础贴图进行二次采样(使用翻页UV),并在两次采样结果之间进行插值计算
float4 GetBase (InputConfig c) {float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, c.baseUV);if (c.flipbookBlending) {baseMap = lerp(baseMap, SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, c.flipbookUVB.xy),c.flipbookUVB.z);}float4 baseColor = INPUT_PROP(_BaseColor);return baseMap * baseColor * c.color;
}
最后,在适当时机通过重写UnlitPassFragment中的默认配置来启用翻页动画混合功能
#if defined(_VERTEX_COLORS)config.color = input.color;#endif#if defined(_FLIPBOOK_BLENDING)config.flipbookUVB = input.flipbookUVB;config.flipbookBlending = true;#endif
( 视频 )
Flipbook blending
翻页动画混合
2. Fading Near Camera 摄像机近端淡化
当摄像机位于粒子系统内部时,粒子会非常靠近摄像机的近切面,甚至从一侧穿到另一侧。粒子系统的"渲染器/最大粒子尺寸"属性可以防止单个布告板粒子遮挡过多视窗。当粒子达到最大可见尺寸后,它们会呈现滑移效果,而非随着接近近切面持续放大。
另一种处理靠近近切面粒子的方法是根据片段深度进行淡出处理。当穿越代表大气效果的粒子系统时,这种方法能呈现更自然的视觉效果
2.1 Fragment Data 片段数据
我们的片段函数中已经可以获取片段深度值,这是通过具有SV_POSITION语义的float4类型变量提供的。之前我们已经使用其XY分量进行抖动处理,现在让我们正式确立为使用片段数据。
在顶点函数中,SV_POSITION表示顶点在裁剪空间中的齐次坐标位置(4D)。而在片段函数中,SV_POSITION则表示片段在屏幕空间(也称为窗口空间)的位置。这个空间转换是由GPU自动完成的。为了明确区分,我们将所有Varyings结构体中的postionCS字段重命名为positionCS_SS
float4 positionCS_SS : SV_POSITION;
请同步调整相关顶点函数中的命名
output.positionCS_SS = TransformWorldToHClip(positionWS);
接下来,我们将创建一个新的Fragment HLSL头文件,其中包含Fragment结构体和GetFragment函数。该函数接收屏幕空间的float4位置向量作为参数,返回对应的片段信息。目前片段仅包含二维位置信息,该信息来自屏幕空间位置的XY分量。这些是带有0.5偏移的纹素坐标——屏幕左下角纹素坐标为(0.5,0.5),其右侧纹素坐标为(1.5,0.5),依此类推
#ifndef FRAGMENT_INCLUDED
#define FRAGMENT_INCLUDEDstruct Fragment {float2 positionSS;
};Fragment GetFragment (float4 positionSS) {Fragment f;f.positionSS = positionSS.xy;return f;
}#endif
请在其他所有include语句之后,将该头文件引入Common文件中,然后调整ClipLOD函数,使其第一个参数类型从float4改为Fragment
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl"#include "Fragment.hlsl"…void ClipLOD (Fragment fragment, float fade) {#if defined(LOD_FADE_CROSSFADE)float dither = InterleavedGradientNoise(fragment.positionSS, 0);clip(fade + (fade < 0.0 ? dither : -dither));#endif
}
同时,我们此刻也应在Common文件中定义常用的线性与点采样器的钳制模式,因为后续会在多个地方用到它们。请在引入Fragment头文件之前完成此操作
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl"SAMPLER(sampler_linear_clamp);
SAMPLER(sampler_point_clamp);#include "Fragment.hlsl"
随后从PostFXStackPasses中移除通用采样器的定义,因为现在这会成为重复定义并导致编译错误
TEXTURE2D(_PostFXSource2);
//SAMPLER(sampler_linear_clamp);
接下来,在LitInput和UnlitInput的InputConfig结构体中添加fragment字段。然后在GetInputConfig函数的第一个参数中添加屏幕空间位置向量,这样它们就能通过该参数调用GetFragment函数
struct InputConfig {Fragment fragment;…
};InputConfig GetInputConfig (float4 positionSS, …) {InputConfig c;c.fragment = GetFragment(positionSS);…
}
在所有调用GetInputConfig的地方添加该参数
InputConfig config = GetInputConfig(input.positionCS_SS, …);
接着调整LitPassFragment,使其在获取config后调用ClipLOD,以便向该函数传递fragment参数。同时将fragment的位置传递给InterleavedGradientNoise函数,而非直接使用input.positionCS_SS
float4 LitPassFragment (Varyings input) : SV_TARGET {UNITY_SETUP_INSTANCE_ID(input);//ClipLOD(input.positionSS.xy, unity_LODFade.x);InputConfig config = GetInputConfig(input.positionCS_SS, input.baseUV);ClipLOD(config.fragment, unity_LODFade.x);…surface.dither = InterleavedGradientNoise(config.fragment.positionSS, 0);…
}
ShadowCasterPassFragment也需进行修改,使其在获取config后执行片段裁剪
void ShadowCasterPassFragment (Varyings input) {UNITY_SETUP_INSTANCE_ID(input);//ClipLOD(input.positionCS.xy, unity_LODFade.x);InputConfig config = GetInputConfig(input.positionCS_SS, input.baseUV);ClipLOD(config.fragment, unity_LODFade.x);float4 base = GetBase(config);#if defined(_SHADOWS_CLIP)clip(base.a - GetCutoff(config));#elif defined(_SHADOWS_DITHER)float dither = InterleavedGradientNoise(input.positionSS.xy, 0);clip(base.a - dither);#endif
}
2.2 Fragment Depth 片段深度
为了在摄像机近处实现粒子淡出效果,我们需要获取片段的深度信息。因此请在Fragment结构体中添加depth字段
struct Fragment {float2 positionSS;float depth;
};
片段深度存储在屏幕空间位置向量的最后一个分量中。该值是用于执行透视除法、将3D位置投影到屏幕上的数值。这是视图空间深度,因此它表示到摄像机XY平面的距离,而非到近裁剪面的距离
Fragment GetFragment (float4 positionCS_SS) {Fragment f;f.positionSS = positionSS.xy;f.depth = positionSS.w;return f;
}
视图空间是世界空间经过旋转和平移后形成的坐标系,
使得摄像机最终位于坐标原点且无旋转状态
我们可以通过在LitPassFragment和UnlitPassFragment中直接返回按比例缩放的片段深度值来验证其正确性,这样就能以灰度渐变形式观察深度信息
InputConfig config = GetInputConfig(input.positionCS_SS, input.baseUV);return float4(config.fragment.depth.xxx / 20.0, 1.0);
Fragment depth, divided by 20
片段深度除以20后的可视化效果
2.3 Orthographic Depth 正交深度
上述方法仅适用于透视摄像机。使用正交摄像机时不会进行透视除法,因此屏幕空间位置向量的最后一个分量始终为1。
我们可以通过向UnityInput添加unity_OrthoParams字段来判断是否正在使用正交摄像机,Unity通过该向量向GPU传递正交摄像机的参数信息
float4 unity_OrthoParams;
float4 _ProjectionParams;
对于正交摄像机,其最后一个分量将为1,否则为0。请在Common中添加IsOrthographicCamera函数(在引入Fragment之前定义,以便在其中使用)。该函数基于此特性进行判断。如果您从不使用正交摄像机,可以硬编码返回false,或通过着色器关键字控制此行为
bool IsOrthographicCamera () {return unity_OrthoParams.w;
}#include "Fragment.hlsl"
对于正交摄像机,我们最佳的处理方式是依赖屏幕空间位置向量的Z分量,该分量包含经过转换的片段裁剪空间深度。这是用于深度比较的原始值,如果启用了深度写入,它会被写入深度缓冲区。该值位于0-1范围内,对于正交投影是线性的。要将其转换为视图空间深度,我们需要将其乘以摄像机的近远平面范围,然后加上近平面距离。近远平面距离存储在_ProjectionParams的Y和Z分量中。如果使用了反转深度缓冲区,我们还需要反转原始深度值。请在引入Fragment之前,在Common中定义一个新的OrthographicDepthBufferToLinear函数来实现此功能
float OrthographicDepthBufferToLinear (float rawDepth) {#if UNITY_REVERSED_ZrawDepth = 1.0 - rawDepth;#endifreturn (_ProjectionParams.z - _ProjectionParams.y) * rawDepth + _ProjectionParams.y;
}#include "Fragment.hlsl"
现在GetFragment函数可以检测是否使用了正交摄像机,如果是,则依靠OrthographicDepthBufferToLinear函数来确定片段深度
f.depth = IsOrthographicCamera() ?OrthographicDepthBufferToLinear(positionSS.z) : positionSS.w;
Fragment depth for orthographic camera
正交摄像机的片段深度
在确认两种摄像机类型的片段深度都正确无误后,请移除LitPassFragment和UnlitPassFragment中的调试可视化代码
//return float4(config.fragment.depth.xxx / 20.0, 1.0);
2.4 Distance-Based Fading 基于距离的淡化效果
回到UnlitParticles着色器,添加一个Near Fade关键字开关属性,同时添加可配置的距离和范围属性。距离参数控制粒子在距离摄像机多近时完全消失(这里指的是摄像机平面,而非近裁剪面)。该值至少应设置为近裁剪面距离,默认值1较为合理。范围参数控制过渡区域的长度,在此区域内粒子会线性淡出。同样以1作为默认值较为合适,且必须设置为一个小的正数值
[Toggle(_NEAR_FADE)] _NearFade ("Near Fade", Float) = 0_NearFadeDistance ("Near Fade Distance", Range(0.0, 10.0)) = 1_NearFadeRange ("Near Fade Range", Range(0.01, 10.0)) = 1
添加着色器功能以启用近景淡化效果
#pragma shader_feature _NEAR_FADE
随后在UnlitInput的UnityPerMaterial缓冲区中包含距离和范围参数
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)UNITY_DEFINE_INSTANCED_PROP(float, _NearFadeDistance)UNITY_DEFINE_INSTANCED_PROP(float, _NearFadeRange)UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)UNITY_DEFINE_INSTANCED_PROP(float, _ZWrite)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
接下来,在InputConfig中添加一个布尔型nearFade字段,用于控制是否启用近景淡出效果(默认不启用
struct InputConfig {…bool nearFade;
};InputConfig GetInputConfig (float4 positionCC_SS, float2 baseUV) {…c.nearFade = false;return c;
}
摄像机近处淡出效果可通过简单降低片段的基准透明度来实现。衰减系数等于片段深度减去淡出距离,再除以淡出范围。由于计算结果可能为负值,在将其纳入基础贴图的透明度计算前,需使用saturate函数进行钳位处理。请在GetBase函数中的适当时机执行此操作
if (c.flipbookBlending) { … }if (c.nearFade) {float nearAttenuation = (c.fragment.depth - INPUT_PROP(_NearFadeDistance)) /INPUT_PROP(_NearFadeRange);baseMap.a *= saturate(nearAttenuation);}
最后,若定义了_NEAR_FADE关键字,请在UnlitPassFragment中将片段的nearFade字段设为true以启用该功能
#if defined(_FLIPBOOK_BLENDING)config.flipbookUVB = input.flipbookUVB;config.flipbookBlending = true;#endif#if defined(_NEAR_FADE)config.nearFade = true;#endif
(视频)
Adjusting near fade distance
调整近景淡出距离
3. Soft Particles 柔化粒子
当布告板粒子与几何体相交时,生硬的过渡既会造成视觉上的突兀感,也会暴露其平面属性。解决方案是使用柔化粒子,当粒子后方存在不透明几何体时会逐渐淡出。实现此效果需要将粒子的片段深度与摄像机缓冲区中同一位置先前绘制的深度进行比较。这意味着我们必须对深度缓冲区进行采样
3.1 Separate Depth Buffer 分离深度缓冲区
截至目前,我们一直使用单一帧缓冲区来存储摄像机的颜色和深度信息。这是典型的帧缓冲区配置,但颜色和深度数据实际上始终存储在独立的缓冲区中,称为帧缓冲区附件。要访问深度缓冲区,我们需要分别定义这些附件。
第一步是将CameraRenderer中的_CameraFrameBuffer标识符替换为两个新标识符,分别命名为_CameraColorAttachment和_CameraDepthAttachment
//static int frameBufferId = Shader.PropertyToID("_CameraFrameBuffer");static intcolorAttachmentId = Shader.PropertyToID("_CameraColorAttachment"),depthAttachmentId = Shader.PropertyToID("_CameraDepthAttachment");
在Render方法中,我们现在需要将颜色附件传递给PostFXStack.Render,这在功能上与之前的方式是等效的
if (postFXStack.IsActive) {postFXStack.Render(colorAttachmentId);}
在Setup方法中,我们现在需要获取两个缓冲区而非单个复合缓冲区。颜色缓冲区不包含深度信息,而深度缓冲区的格式为RenderTextureFormat.Depth,其筛选模式设为FilterMode.Point,因为混合深度数据没有意义。这两个附件可以通过单次SetRenderTarget调用进行设置,并为每个附件使用相同的加载和存储操作
if (postFXStack.IsActive) {if (flags > CameraClearFlags.Color) {flags = CameraClearFlags.Color;}buffer.GetTemporaryRT(colorAttachmentId, camera.pixelWidth, camera.pixelHeight,0, FilterMode.Bilinear, useHDR ?RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default);buffer.GetTemporaryRT(depthAttachmentId, camera.pixelWidth, camera.pixelHeight,32, FilterMode.Point, RenderTextureFormat.Depth);buffer.SetRenderTarget(colorAttachmentId,RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store,depthAttachmentId,RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);}
两个缓冲区也都需要释放。完成这些设置后,我们的渲染管线仍能像之前一样正常工作,但现在可以通过独立的帧缓冲附件分别访问颜色和深度数据
void Cleanup () {lighting.Cleanup();if (postFXStack.IsActive) {buffer.ReleaseTemporaryRT(colorAttachmentId);buffer.ReleaseTemporaryRT(depthAttachmentId);}}
3.2 Copying Depth 深度复制
我们无法在将深度缓冲区用于渲染的同时对其进行采样,必须创建其副本。因此需要引入_CameraDepthTexture标识符,并添加布尔字段来指示是否使用深度纹理。我们仅在需要时才进行深度复制,这一判断将在Render方法中获取相机设置后确定。但初始阶段我们可以简单地始终启用该功能
static intcolorAttachmentId = Shader.PropertyToID("_CameraColorAttachment"),depthAttachmentId = Shader.PropertyToID("_CameraDepthAttachment"),depthTextureId = Shader.PropertyToID("_CameraDepthTexture");…bool useDepthTexture;public void Render (…) {…CameraSettings cameraSettings =crpCamera ? crpCamera.Settings : defaultCameraSettings;useDepthTexture = true;…}
新建一个CopyAttachments方法,该方法在需要时获取临时深度纹理副本,并将深度附件数据复制到其中。可通过在命令缓冲区上调用CopyTexture方法,传入源纹理和目标纹理来实现。这比通过全屏绘制调用执行复制要高效得多。同时请确保在Cleanup方法中释放额外的深度纹理
void Cleanup () {…if (useDepthTexture) {buffer.ReleaseTemporaryRT(depthTextureId);}}…void CopyAttachments () {if (useDepthTexture) {buffer.GetTemporaryRT(depthTextureId, camera.pixelWidth, camera.pixelHeight,32, FilterMode.Point, RenderTextureFormat.Depth);buffer.CopyTexture(depthAttachmentId, depthTextureId);ExecuteBuffer();}}
我们将在所有不透明几何体绘制完成后(即在Render方法中天空盒渲染之后)统一复制附件。这意味着深度纹理仅在执行透明物体渲染时可用
context.DrawSkybox(camera);CopyAttachments();
3.3 Copying Depth Without Post FX 无后期特效的深度复制
深度复制的前提是存在可供复制的深度附件,而当前仅当启用后期特效时才会创建深度附件。为了在没有后期特效时也能实现此功能,当使用深度纹理时,我们还需要使用中间帧缓冲区。引入一个useIntermediateBuffer布尔字段来跟踪此状态,该字段应在Setup中获取附件之前初始化。现在,无论是使用深度纹理还是启用后期特效,都应执行此操作。Cleanup方法也需进行相应调整
bool useDepthTexture, useIntermediateBuffer;…void Setup () {context.SetupCameraProperties(camera);CameraClearFlags flags = camera.clearFlags;useIntermediateBuffer = useDepthTexture || postFXStack.IsActive;if (useIntermediateBuffer) {if (flags > CameraClearFlags.Color) {flags = CameraClearFlags.Color;}…}…}void Cleanup () {lighting.Cleanup();if (useIntermediateBuffer) {buffer.ReleaseTemporaryRT(colorAttachmentId);buffer.ReleaseTemporaryRT(depthAttachmentId);//}if (useDepthTexture) {buffer.ReleaseTemporaryRT(depthTextureId);}}}
但现在当后期特效未启用时,渲染会失败,因为我们只渲染到了中间缓冲区。我们必须执行最终复制操作,将结果复制到摄像机的目标缓冲区。遗憾的是,CopyTexture只能复制到渲染纹理,无法直接复制到最终帧缓冲区。虽然可以使用后期特效的复制通道来实现,但这一步骤是摄像机渲染器的特定功能,因此我们将为此创建专用的CameraRenderer着色器。该着色器初始结构与后期特效着色器相同,但仅包含复制通道,并且引用其专属的HLSL文件
Shader "Hidden/Custom RP/Camera Renderer" {SubShader {Cull OffZTest AlwaysZWrite OffHLSLINCLUDE#include "../ShaderLibrary/Common.hlsl"#include "CameraRendererPasses.hlsl"ENDHLSLPass {Name "Copy"HLSLPROGRAM#pragma target 3.5#pragma vertex DefaultPassVertex#pragma fragment CopyPassFragmentENDHLSL}}
}
新的CameraRendererPasses HLSL文件具有与PostFXStackPasses相同的Varyings结构体和DefaultPassVertex函数。它还包含_SourceTexture纹理和一个CopyPassFragment函数,该函数直接返回采样后的源纹理
#ifndef CUSTOM_CAMERA_RENDERER_PASSES_INCLUDED
#define CUSTOM_CAMERA_RENDERER_PASSES_INCLUDEDTEXTURE2D(_SourceTexture);struct Varyings { … };Varyings DefaultPassVertex (uint vertexID : SV_VertexID) { … }float4 CopyPassFragment (Varyings input) : SV_TARGET {return SAMPLE_TEXTURE2D_LOD(_SourceTexture, sampler_linear_clamp, input.screenUV, 0);
}#endif
接下来,在CameraRenderer中添加一个材质字段。为了初始化它,创建一个包含着色器参数的公共构造方法,并让它调用CoreUtils.CreateEngineMaterial方法(以该着色器作为参数)。该方法会创建一个新材质,将其设置为在编辑器中隐藏,确保它不会作为资源保存,这样我们就不需要手动处理这些操作。如果着色器缺失,该方法还会记录错误信息
Material material;public CameraRenderer (Shader shader) {material = CoreUtils.CreateEngineMaterial(shader);}
同时添加一个公共的Dispose方法,通过将材质传递给CoreUtils.Destroy来进行清理。该方法会根据Unity是否处于播放模式,选择常规销毁或立即销毁材质。之所以需要这样做,是因为每当渲染管线资产被修改时,都会创建新的RP实例及相应的渲染器,这可能导致在编辑器中创建大量材质
public void Dispose () {CoreUtils.Destroy(material);}
现在CustomRenderPipeline在构造其渲染器时必须提供着色器。因此我们将在其自身的构造函数中添加参数,用于传入摄像机渲染器着色器
CameraRenderer renderer; // = new CameraRenderer();…public CustomRenderPipeline (bool allowHDR,bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher,bool useLightsPerObject, ShadowSettings shadowSettings,PostFXSettings postFXSettings, int colorLUTResolution, Shader cameraRendererShader) {…renderer = new CameraRenderer(cameraRendererShader);}
从现在开始,当管线自身被释放时,它还必须调用渲染器的Dispose方法。我们已为其创建了一个Dispose方法,但仅适用于编辑器代码。请将该版本重命名为DisposeForEditor,并仅保留重置光照映射委托的功能
partial void DisposeForEditor ();#if UNITY_EDITOR…partial void DisposeForEditor () {//base.Dispose(disposing);Lightmapping.ResetDelegate();}
随后添加一个非编辑器专用的新Dispose方法,该方法会调用其基类实现、编辑器版本的方法,并最终释放渲染器
protected override void Dispose (bool disposing) {base.Dispose(disposing);DisposeForEditor();renderer.Dispose();}
在最顶层的CustomRenderPipelineAsset中,必须获取着色器配置属性并将其传递给管线构造函数。这样我们最终就能连接该着色器了
[SerializeField]Shader cameraRendererShader = default;protected override RenderPipeline CreatePipeline () {return new CustomRenderPipeline(allowHDR, useDynamicBatching, useGPUInstancing, useSRPBatcher,useLightsPerObject, shadows, postFXSettings, (int)colorLUTResolution,cameraRendererShader);}
Camera renderer shader assigned
摄像机渲染器着色器已分配
此时CameraRenderer已具备可用的材质。同时向其添加_SourceTexture标识符,并提供一个与PostFXStack中类似的Draw方法,不同之处在于不需要传递通道参数,因为目前我们仅使用单个通道
static intcolorAttachmentId = Shader.PropertyToID("_CameraColorAttachment"),depthAttachmentId = Shader.PropertyToID("_CameraDepthAttachment"),depthTextureId = Shader.PropertyToID("_CameraDepthTexture"),sourceTextureId = Shader.PropertyToID("_SourceTexture");…void Draw (RenderTargetIdentifier from, RenderTargetIdentifier to) {buffer.SetGlobalTexture(sourceTextureId, from);buffer.SetRenderTarget(to, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);buffer.DrawProcedural(Matrix4x4.identity, material, 0, MeshTopology.Triangles, 3);}
为了最终修复渲染器问题,请在Render方法中,当未启用后期特效但使用了中间缓冲区时,通过调用Draw方法将颜色附件复制到摄像机目标
if (postFXStack.IsActive) {postFXStack.Render(colorAttachmentId);}else if (useIntermediateBuffer) {Draw(colorAttachmentId, BuiltinRenderTextureType.CameraTarget);ExecuteBuffer();}
3.4 Reconstructing View-Space Depth 重建视图空间深度
要对深度纹理进行采样,我们需要片段的UV坐标(位于屏幕空间中)。可以通过将片段位置除以屏幕像素尺寸来获得这些坐标,Unity通过float4 _ScreenParams的XY分量提供了屏幕尺寸信息,因此请将其添加到UnityInput中
float4 unity_OrthoParams;
float4 _ProjectionParams;
float4 _ScreenParams;
随后我们可以将片段UV和缓冲区深度添加到Fragment结构体中。通过SAMPLE_DEPTH_TEXTURE_LOD宏使用点采样钳位采样器对摄像机深度纹理进行采样,以获取缓冲区深度。该宏的功能与SAMPLE_TEXTURE2D_LOD相同,但仅返回R通道的值
TEXTURE2D(_CameraDepthTexture);struct Fragment {float2 positionSS;float2 screenUV;float depth;float bufferDepth;
};Fragment GetFragment (float4 positionCS_SS) {Fragment f;f.positionSS = positionSS.xy;f.screenUV = f.positionSS / _ScreenParams.xy;f.depth = IsOrthographicCamera() ?OrthographicDepthBufferToLinear(positionSS.z) : positionSS.w;f.bufferDepth =SAMPLE_DEPTH_TEXTURE_LOD(_CameraDepthTexture, sampler_point_clamp, f.screenUV, 0);return f;
}
这样我们就得到了原始的深度缓冲区值。为了将其转换为视图空间深度,对于正交摄像机,我们可以再次调用OrthographicDepthBufferToLinear函数,就像处理当前片段的深度那样。透视深度也需要进行转换,为此我们可以使用LinearEyeDepth函数。该函数需要_ZBufferParams作为第二个参数
f.bufferDepth =SAMPLE_DEPTH_TEXTURE_LOD(_CameraDepthTexture, sampler_point_clamp, f.screenUV, 0);f.bufferDepth = IsOrthographicCamera() ?OrthographicDepthBufferToLinear(f.bufferDepth) :LinearEyeDepth(f.bufferDepth, _ZBufferParams);
_ZBufferParams是Unity提供的另一个float4类型变量,其中包含从原始深度到线性深度的转换系数。请将其添加到UnityInput中
float4 unity_OrthoParams;
float4 _ProjectionParams;
float4 _ScreenParams;
float4 _ZBufferParams;
为了验证缓冲区深度采样是否正确,请在UnlitPassFragment中返回缩放后的深度值,就像之前测试片段深度时那样
InputConfig config = GetInputConfig(input.positionCS_SS, input.baseUV);return float4(config.fragment.bufferDepth.xxx / 20.0, 1.0);
Buffer depth, perspective and orthographic projections
缓冲区深度,透视投影与正交投影
确认采样深度正确后,请移除调试可视化代码
//return float4(config.fragment.bufferDepth.xxx / 20.0, 1.0);
3.5 Optional Depth Texture 可选深度纹理
深度复制需要额外的工作,特别是在未使用后期特效时,因为这还需要中间缓冲区以及向摄像机目标执行额外复制。因此,我们将把渲染管线是否支持深度复制设为可配置选项。为此,我们将创建一个新的CameraBufferSettings结构体,放在单独的文件中,用于分组所有与摄像机缓冲区相关的设置。除了深度复制的开关外,还将包含HDR的启用开关。同时引入一个独立的开关来控制渲染反射时是否复制深度。这是有用的,因为反射渲染时不使用后期特效,且粒子系统也不会出现在反射中,因此为反射复制深度开销大且可能无用。但我们仍保留此选项,因为深度也可能用于其他效果,而这些效果可能在反射中可见。即便如此,请注意深度缓冲区在每个立方体贴图反射面上是独立的,因此立方体贴图边缘会出现深度接缝
[System.Serializable]
public struct CameraBufferSettings {public bool allowHDR;public bool copyDepth, copyDepthReflection;
}
将CustomRenderPipelineAsset中当前的HDR开关替换为这些摄像机缓冲区设置
//[SerializeField]//bool allowHDR = true;[SerializeField]CameraBufferSettings cameraBuffer = new CameraBufferSettings {allowHDR = true};protected override RenderPipeline CreatePipeline () {return new CustomRenderPipeline(cameraBuffer, useDynamicBatching, useGPUInstancing, useSRPBatcher,useLightsPerObject, shadows, postFXSettings, (int)colorLUTResolution,cameraRendererShader);}
请同步对CustomRenderPipeline应用此更改
//bool allowHDR;CameraBufferSettings cameraBufferSettings;…public CustomRenderPipeline (CameraBufferSettings cameraBufferSettings,bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher,bool useLightsPerObject, ShadowSettings shadowSettings,PostFXSettings postFXSettings, int colorLUTResolution, Shader cameraRendererShader) {this.colorLUTResolution = colorLUTResolution;//this.allowHDR = allowHDR;this.cameraBufferSettings = cameraBufferSettings;…}…protected override void Render (ScriptableRenderContext context, List<Camera> cameras) {for (int i = 0; i < cameras.Count; i++) {renderer.Render(context, cameras[i], cameraBufferSettings,useDynamicBatching, useGPUInstancing, useLightsPerObject,shadowSettings, postFXSettings, colorLUTResolution);}}
CameraRenderer.Render现在需要根据是否正在渲染反射来使用相应的设置
public void Render (ScriptableRenderContext context, Camera camera,CameraBufferSettings bufferSettings,bool useDynamicBatching, bool useGPUInstancing, bool useLightsPerObject,ShadowSettings shadowSettings, PostFXSettings postFXSettings,int colorLUTResolution) {…//useDepthTexture = true;if (camera.cameraType == CameraType.Reflection) {useDepthTexture = bufferSettings.copyDepthReflection;}else {useDepthTexture = bufferSettings.copyDepth;}…useHDR = bufferSettings.allowHDR && camera.allowHDR;…}
Camera buffer settings, with HDR and non-reflection copy depth enabled
摄像机缓冲区设置,已启用HDR和非反射的深度复制功能
除了整个渲染管线的设置外,我们还可以在CameraSettings中添加一个深度复制开关,默认设为启用
public bool copyDepth = true;
Camera copy depth toggle
摄像机深度复制开关
对于常规摄像机,深度纹理的使用需同时满足渲染管线和摄像机均启用该功能,这与HDR的控制方式类似
if (camera.cameraType == CameraType.Reflection) {useDepthTexture = bufferSettings.copyDepthReflection;}else {useDepthTexture = bufferSettings.copyDepth && cameraSettings.copyDepth;}
3.6 Missing Texture 纹理缺失
由于深度纹理是可选的,它可能不存在。当着色器仍尝试采样时,结果将是随机的——可能是空纹理或旧副本(甚至来自其他摄像机)。着色器也可能在渲染不透明物体阶段过早采样深度纹理。我们至少应确保无效采样能产生一致结果。为此,在CameraRender的构造函数中创建默认的缺失纹理。CoreUtils没有纹理相关方法,因此我们需手动将其hide flags设置为HideFlags.HideAndDontSave。将其命名为"Missing",这样通过帧调试器检查着色器属性时能明显识别出使用了错误纹理。将其创建为1×1尺寸、所有通道值为0.5的简单纹理。并在渲染器释放时正确销毁该纹理
Texture2D missingTexture;public CameraRenderer (Shader shader) {material = CoreUtils.CreateEngineMaterial(shader);missingTexture = new Texture2D(1, 1) {hideFlags = HideFlags.HideAndDontSave,name = "Missing"};missingTexture.SetPixel(0, 0, Color.white * 0.5f);missingTexture.Apply(true, true);}public void Dispose () {CoreUtils.Destroy(material);CoreUtils.Destroy(missingTexture);}
在Setup方法末尾将缺失纹理用作深度纹理
void Setup () {…buffer.BeginSample(SampleName);buffer.SetGlobalTexture(depthTextureId, missingTexture);ExecuteBuffer();}
3.7 Fading Particles Nearby Background 背景近处粒子淡化效果
现在我们已经拥有了可用的深度纹理,可以最终实现柔化粒子功能。第一步是在UnlitParticles着色器中添加柔化粒子的关键字开关属性、距离参数和范围参数,这与近景淡出属性类似。此处的距离参数是从粒子后方物体开始测量的,因此我们默认将其设置为零
[Toggle(_SOFT_PARTICLES)] _SoftParticles ("Soft Particles", Float) = 0_SoftParticlesDistance ("Soft Particles Distance", Range(0.0, 10.0)) = 0_SoftParticlesRange ("Soft Particles Range", Range(0.01, 10.0)) = 1
同时为其添加着色器功能。
#pragma shader_feature _SOFT_PARTICLES
与近景淡出类似,如果定义了关键字,请在UnlitPassFragment中将相应的配置字段设为true
#if defined(_NEAR_FADE)config.nearFade = true;#endif#if defined(_SOFT_PARTICLES)config.softParticles = true;#endif
在UnlitInput中,将新的着色器属性添加到UnityPerMaterial缓冲区,并将对应字段添加到InputConfig结构体
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)…UNITY_DEFINE_INSTANCED_PROP(float, _SoftParticlesDistance)UNITY_DEFINE_INSTANCED_PROP(float, _SoftParticlesRange)UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)UNITY_DEFINE_INSTANCED_PROP(float, _ZWrite)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)#define INPUT_PROP(name) UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, name)struct InputConfig {…bool softParticles;
};InputConfig GetInputConfig (float4 positionCC_SS, float2 baseUV) {…c.softParticles = false;return c;
}
随后在GetBase中应用另一种近端衰减,这次基于片段的缓冲区深度与其自身深度的差值来计算
if (c.nearFade) {float nearAttenuation = (c.fragment.depth - INPUT_PROP(_NearFadeDistance)) /INPUT_PROP(_NearFadeRange);baseMap.a *= saturate(nearAttenuation);}if (c.softParticles) {float depthDelta = c.fragment.bufferDepth - c.fragment.depth;float nearAttenuation = (depthDelta - INPUT_PROP(_SoftParticlesDistance)) /INPUT_PROP(_SoftParticlesRange);baseMap.a *= saturate(nearAttenuation);}
Soft particles, adjusting fade range
柔化粒子,调整淡出范围
3.8 No Copy Texture Support 不支持复制纹理功能
这一切都能正常运行,但前提是必须支持通过CopyTexture直接复制纹理(至少基础支持)。大多数平台都满足这个条件,但WebGL 2.0除外。因此,如果我们需要支持WebGL 2.0,就必须回退到通过着色器进行复制的方式——虽然效率较低,但至少能正常工作。
在CameraRenderer中使用静态布尔字段来跟踪是否支持CopyTexture。初始值设为false,这样即使我们的开发机器都支持该功能,也能测试回退方案
static bool copyTextureSupported = false;
在CopyAttachments中,如果支持CopyTexture则通过该方式复制深度,否则回退到使用我们的Draw方法
void CopyAttachments () {if (useDepthTexture) {buffer.GetTemporaryRT(depthTextureId, camera.pixelWidth, camera.pixelHeight,32, FilterMode.Point, RenderTextureFormat.Depth);if (copyTextureSupported) {buffer.CopyTexture(depthAttachmentId, depthTextureId);}else {Draw(depthAttachmentId, depthTextureId);}ExecuteBuffer();}}
初始阶段这会无法产生正确结果,因为Draw方法会更改渲染目标,导致后续绘制出错。我们必须在之后将渲染目标重新设置为摄像机缓冲区,再次加载我们的附件
if (copyTextureSupported) {buffer.CopyTexture(depthAttachmentId, depthTextureId);}else {Draw(depthAttachmentId, depthTextureId);buffer.SetRenderTarget(colorAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store,depthAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);}
第二个问题是深度数据完全未被复制,因为我们的复制通道仅写入默认着色器目标(用于颜色数据,而非深度数据)。要复制深度数据,我们需要在CameraRenderer着色器中添加第二个深度复制通道,该通道改为写入深度数据而非颜色数据。为此,需将其ColorMask设置为零并开启ZWrite。同时还需要一个特殊的片段函数,我们将其命名为CopyDepthPassFragment
Pass {Name "Copy Depth"ColorMask 0ZWrite OnHLSLPROGRAM#pragma target 3.5#pragma vertex DefaultPassVertex#pragma fragment CopyDepthPassFragmentENDHLSL}
新的片段函数必须对深度进行采样,并使用SV_DEPTH语义以单精度浮点数形式返回,而不是使用SV_TARGET语义返回四维浮点数。通过这种方式,我们采样原始深度缓冲区的值,并直接将其用作片段的新深度值
float4 CopyPassFragment (Varyings input) : SV_TARGET {return SAMPLE_TEXTURE2D_LOD(_SourceTexture, sampler_linear_clamp, input.screenUV, 0);
}float CopyDepthPassFragment (Varyings input) : SV_DEPTH {return SAMPLE_DEPTH_TEXTURE_LOD(_SourceTexture, sampler_point_clamp, input.screenUV, 0);
}
接下来,回到CameraRenderer,在Draw方法中添加一个布尔参数(默认为false)来指示是否进行深度数据的绘制。如果是深度绘制,则使用第二个通道而非第一个通道
public void Draw (RenderTargetIdentifier from, RenderTargetIdentifier to, bool isDepth = false) {…buffer.DrawProcedural(Matrix4x4.identity, material, isDepth ? 1 : 0, MeshTopology.Triangles, 3);}
随后在复制深度缓冲区时注明我们正在处理深度数据
Draw(depthAttachmentId, depthTextureId, true);
在验证该方法同样有效后,通过检查SystemInfo.copyTextureSupport来确定是否支持CopyTexture。只要支持级别高于"无支持"即满足要求
static bool copyTextureSupported =SystemInfo.copyTextureSupport > CopyTextureSupport.None;
3.9 Gizmos and Depth Gizmos与深度
现在我们有了绘制深度的方法,可以结合后期特效或使用深度纹理,让gizmos重新具备深度感知功能。在DrawGizmosBeforeFX中,在绘制第一个gizmo之前,如果使用了中间缓冲区,请将深度复制到摄像机目标
partial void DrawGizmosBeforeFX () {if (Handles.ShouldRenderGizmos()) {if (useIntermediateBuffer) {Draw(depthAttachmentId, BuiltinRenderTextureType.CameraTarget, true);ExecuteBuffer();}context.DrawGizmos(camera, GizmoSubset.PreImageEffects);}}
如果使用了后期特效,我们还需要再次执行此操作
partial void DrawGizmosAfterFX () {if (Handles.ShouldRenderGizmos()) {if (postFXStack.IsActive){Draw(depthAttachmentId, BuiltinRenderTextureType.CameraTarget, true);ExecuteBuffer();}context.DrawGizmos(camera, GizmoSubset.PostImageEffects);}}
Gizmos recognizing depth
Gizmos深度识别功能
4. Distortion 扭曲效果
我们将支持的Unity粒子系统的另一项功能是扭曲效果,该效果可用于创建诸如热浪引起的大气折射等现象。这需要对颜色缓冲区进行采样(就像我们对深度缓冲区采样一样),但需要额外添加UV偏移
4.1 Color Copy Texture 颜色复制纹理
我们首先在CameraBufferSettings中添加颜色复制的开关选项,同样需要分别为常规摄像机和反射摄像机设置独立的开关
public bool copyColor, copyColorReflection, copyDepth, copyDepthReflection;
Copying color and depth
复制颜色与深度数据
同时实现按摄像机配置颜色复制功能
public bool copyColor = true, copyDepth = true;
Also enabled for camera
同时为摄像机启用该功能
CameraRendering现在还需要记录颜色纹理的标识符以及是否使用了颜色纹理
colorTextureId = Shader.PropertyToID("_CameraColorTexture"),depthTextureId = Shader.PropertyToID("_CameraDepthTexture"),sourceTextureId = Shader.PropertyToID("_SourceTexture");…bool useColorTexture, useDepthTexture, useIntermediateBuffer;…public void Render (…) {…if (camera.cameraType == CameraType.Reflection) {useColorTexture = bufferSettings.copyColorReflection;useDepthTexture = bufferSettings.copyDepthReflection;}else {useColorTexture = bufferSettings.copyColor && cameraSettings.copyColor;useDepthTexture = bufferSettings.copyDepth && cameraSettings.copyDepth;}…}
是否使用中间缓冲区现在也取决于是否使用了颜色纹理。同时,我们初始时应将颜色纹理设置为缺失纹理,并在清理时一并释放它
void Setup () {…useIntermediateBuffer =useColorTexture || useDepthTexture || postFXStack.IsActive;…buffer.BeginSample(SampleName);buffer.SetGlobalTexture(colorTextureId, missingTexture);buffer.SetGlobalTexture(depthTextureId, missingTexture);ExecuteBuffer();}void Cleanup () {lighting.Cleanup();if (useIntermediateBuffer) {buffer.ReleaseTemporaryRT(colorAttachmentId);buffer.ReleaseTemporaryRT(depthAttachmentId);if (useColorTexture) {buffer.ReleaseTemporaryRT(colorTextureId);}if (useDepthTexture) {buffer.ReleaseTemporaryRT(depthTextureId);}}}
现在当使用颜色纹理、深度纹理或两者同时使用时,我们都需要复制摄像机附件。让CopyAttachments的调用取决于这个条件
context.DrawSkybox(camera);if (useColorTexture || useDepthTexture) {CopyAttachments();}
随后我们可以分别复制两个纹理,接着重置渲染目标并执行一次缓冲区命令
void CopyAttachments () {if (useColorTexture) {buffer.GetTemporaryRT(colorTextureId, camera.pixelWidth, camera.pixelHeight,0, FilterMode.Bilinear, useHDR ?RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default);if (copyTextureSupported) {buffer.CopyTexture(colorAttachmentId, colorTextureId);}else {Draw(colorAttachmentId, colorTextureId);}}if (useDepthTexture) {buffer.GetTemporaryRT(depthTextureId, camera.pixelWidth, camera.pixelHeight,32, FilterMode.Point, RenderTextureFormat.Depth);if (copyTextureSupported) {buffer.CopyTexture(depthAttachmentId, depthTextureId);}else {Draw(depthAttachmentId, depthTextureId, true);//buffer.SetRenderTarget(…);}//ExecuteBuffer();}if (!copyTextureSupported) {buffer.SetRenderTarget(colorAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store,depthAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);}ExecuteBuffer();}
4.2 Sampling the Buffer Color 缓冲区颜色采样
为了对摄像机颜色纹理进行采样,请将其添加到Fragment中。我们不会在Fragment中添加缓冲区颜色属性,因为我们并不关心其确切位置的颜色。取而代之的是,我们引入一个GetBufferColor函数,该函数接收片段和UV偏移作为参数,返回采样得到的颜色
TEXTURE2D(_CameraColorTexture);
TEXTURE2D(_CameraDepthTexture);struct Fragment { … };Fragment GetFragment (float4 positionCS_SS) { … }float4 GetBufferColor (Fragment fragment, float2 uvOffset = float2(0.0, 0.0)) {float2 uv = fragment.screenUV + uvOffset;return SAMPLE_TEXTURE2D_LOD(_CameraColorTexture, sampler_linear_clamp, uv, 0);
}
为了测试此功能,在UnlitPassFragment中返回带小偏移量(如两个维度均偏移5%)的缓冲区颜色
InputConfig config = GetInputConfig(input.positionCS_SS, input.baseUV);return GetBufferColor(config.fragment, 0.05);
Sampling camera color buffer with offset
采样带偏移的摄像机颜色缓冲区
需要注意的是,由于颜色是在不透明阶段之后复制的,因此透明物体不会包含在其中。因此,粒子会抹除在它们之前绘制的所有透明物体(包括其他粒子)。同时,深度在这种情况下不起作用,所以比片段本身更靠近摄像机平面的片段颜色也会被复制。确认功能正常后,请移除调试可视化代码
//return GetBufferColor(config.fragment, 0.05);
4.3 Distortion Vectors 扭曲矢量
要创建有效的扭曲效果,我们需要一张包含平滑过渡扭曲矢量的贴图。这里是一张用于单个圆形粒子的简单贴图。由于它是法线贴图,请按此类型导入
Particle distortion map
粒子扭曲贴图
在UnlitParticles着色器中添加关键字开关属性,同时添加扭曲贴图和强度属性。扭曲效果将作为屏幕空间UV偏移来应用,因此需要较小的数值。我们将强度范围设为0–0.2,默认值为0.1
[Toggle(_DISTORTION)] _Distortion ("Distortion", Float) = 0[NoScaleOffset] _DistortionMap("Distortion Vectors", 2D) = "bumb" {}_DistortionStrength("Distortion Strength", Range(0.0, 0.2)) = 0.1
Distortion enabled
扭曲效果已启用
添加必需的着色器功能
#pragma shader_feature _DISTORTION
随后将扭曲贴图和强度属性添加到UnlitInput中
TEXTURE2D(_BaseMap);
TEXTURE2D(_DistortionMap);
SAMPLER(sampler_BaseMap);UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)…UNITY_DEFINE_INSTANCED_PROP(float, _SoftParticlesRange)UNITY_DEFINE_INSTANCED_PROP(float, _DistortionStrength)…
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
创建一个新的GetDistortion函数,返回float2向量。该函数对扭曲贴图进行采样,并像基础贴图那样应用翻页动画混合,然后解码法线并乘以扭曲强度。我们只需要向量的XY分量,因此可舍弃Z分量
float2 GetDistortion (InputConfig c) {float4 rawMap = SAMPLE_TEXTURE2D(_DistortionMap, sampler_BaseMap, c.baseUV);if (c.flipbookBlending) {rawMap = lerp(rawMap, SAMPLE_TEXTURE2D(_DistortionMap, sampler_BaseMap, c.flipbookUVB.xy),c.flipbookUVB.z);}return DecodeNormal(rawMap, INPUT_PROP(_DistortionStrength)).xy;
}
在UnlitPassFragment中,若启用了扭曲效果,则获取扭曲值并将其作为偏移量来采样缓冲区颜色,覆盖基础颜色。此操作应在裁剪之后执行
float4 base = GetBase(config);#if defined(_CLIPPING)clip(base.a - GetCutoff(config));#endif#if defined(_DISTORTION)float2 distortion = GetDistortion(config);base = GetBufferColor(config.fragment, distortion);#endif
Distorted color buffer
扭曲的颜色缓冲区
结果是粒子会径向扭曲颜色纹理,但角落区域除外,因为该处的扭曲向量为零。不过扭曲效果应当取决于粒子的视觉强度,而这是由原始基础透明度控制的。因此,需要用基础透明度来调制扭曲偏移向量
float2 distortion = GetDistortion(config) * base.a;
Modulated distortion
调制后的扭曲效果
此时我们仍能看到硬边,这暴露了粒子完全相互重叠且呈矩形的问题。我们通过保留粒子的原始透明度来隐藏这个缺陷
base.rgb = GetBufferColor(config.fragment, distortion).rgb;
Faded distortion
淡化扭曲效果
现在扭曲后的颜色纹理采样也会逐渐淡化,这使得未扭曲的背景和其他粒子重新部分可见。最终效果是一种虽不符合物理规律但足以营造大气折射幻觉的平滑混沌。可以通过以下方式进一步改善:调整扭曲强度,配合在粒子生命周期内调整其颜色来实现平滑淡入淡出效果。此外,偏移矢量与屏幕对齐,不受粒子朝向影响。因此,如果粒子在生命周期内旋转,其各自的扭曲图案将会呈现扭转变换
(视频)
Distortion effect
扭曲效果
4.4 Distortion Blend 扭曲混合
目前启用扭曲效果时,我们会完全替换粒子的原始颜色,仅保留其透明度。粒子颜色可以通过多种方式与扭曲后的颜色缓冲区结合。我们将添加一个简单的扭曲混合着色器属性,采用与Unity粒子着色器相同的方法,在粒子自身颜色与其引发的扭曲效果之间进行插值
_DistortionStrength("Distortion Strength", Range(0.0, 0.2)) = 0.1_DistortionBlend("Distortion Blend", Range(0.0, 1.0)) = 1
Distortion blend slider
扭曲混合滑动条
将该属性添加到UnlitInput中,并创建一个函数来获取它
UNITY_DEFINE_INSTANCED_PROP(float, _DistortionStrength)UNITY_DEFINE_INSTANCED_PROP(float, _DistortionBlend)…float GetDistortionBlend (InputConfig c) {return INPUT_PROP(_DistortionBlend);
}
其原理是当混合滑块设为1时,我们仅看到扭曲效果。降低该值会使粒子颜色显现,但不会完全遮盖扭曲效果。我们根据粒子透明度减去混合滑块的值(经饱和处理)在扭曲效果与粒子颜色之间进行插值。因此,当启用扭曲时,除非粒子完全不透明,否则粒子自身颜色总会显得更淡且范围更小。请在UnlitPassFragment中执行此插值计算
#if defined(_DISTORTION)float2 distortion = GetDistortion(config) * base.a;base.rgb = lerp(GetBufferColor(config.fragment, distortion).rgb, base.rgb,saturate(base.a - GetDistortionBlend(config)));#endif
这对于更复杂的粒子(比如我们的翻页动画示例)看起来效果更好。因此,这里提供了一个适用于翻页动画的扭曲纹理
Distortion map for particle flipbook
粒子翻页动画的扭曲贴图
这可用于创建有趣的扭曲效果。真实的效果应该是微妙的,因为系统在运动时只需少量扭曲就足够了。但为了演示目的,我将效果调得很强烈,使其在截图中也能明显可见
(视频)
Distortion with flipbook and post FX
使用翻页动画和后期特效实现的扭曲效果
4.5 Fixing Nonstandard Cameras 修复非标准摄像机
我们当前的方法在仅使用单一摄像机时有效,但在渲染到中间纹理且未启用后期特效时会出现问题。这是因为我们执行的是常规复制到摄像机目标的操作,忽略了视口和最终混合模式。因此CameraRenderer还需要一个FinalPass方法。该方法是PostFXStack.FinalPass的副本,不同之处在于我们将使用常规复制通道,因此之后应将混合模式重置为one-zero以免影响其他复制操作。源纹理始终是颜色附件,最终混合模式则作为参数传入。
同样地,对于Unity 2022,如果我们不是渲染到完整视口,就需要关注缓冲区的加载问题
static Rect fullViewRect = new Rect(0f, 0f, 1f, 1f);…void DrawFinal (CameraSettings.FinalBlendMode finalBlendMode) {buffer.SetGlobalFloat(srcBlendId, (float)finalBlendMode.source);buffer.SetGlobalFloat(dstBlendId, (float)finalBlendMode.destination);buffer.SetGlobalTexture(sourceTextureId, colorAttachmentId);buffer.SetRenderTarget(BuiltinRenderTextureType.CameraTarget,finalBlendMode.destination == BlendMode.Zero && camera.rect == fullViewRect?RenderBufferLoadAction.DontCare : RenderBufferLoadAction.Load,RenderBufferStoreAction.Store);buffer.SetViewport(camera.pixelRect);buffer.DrawProcedural(Matrix4x4.identity, material, 0, MeshTopology.Triangles, 3);buffer.SetGlobalFloat(srcBlendId, 1f);buffer.SetGlobalFloat(dstBlendId, 0f);}
在这种情况下,我们将把混合模式着色器属性命名为 _CameraSrcBlend 和 _CameraDstBlend
sourceTextureId = Shader.PropertyToID("_SourceTexture"),srcBlendId = Shader.PropertyToID("_CameraSrcBlend"),dstBlendId = Shader.PropertyToID("_CameraDstBlend");
调整CameraRenderer的复制通道,使其依赖这些属性
Pass {Name "Copy"Blend [_CameraSrcBlend] [_CameraDstBlend]HLSLPROGRAM#pragma target 3.5#pragma vertex DefaultPassVertex#pragma fragment CopyPassFragmentENDHLSL}
最后,在Render方法中调用DrawFinal替代Draw
if (postFXStack.IsActive) {postFXStack.Render(colorAttachmentId);}else if (useIntermediateBuffer) {DrawFinal(cameraSettings.finalBlendMode);ExecuteBuffer();}
请注意,颜色和深度纹理仅包含当前摄像机渲染的内容。扭曲粒子及类似效果将无法从其他摄像机获取数据