在Unity中创建配色效果


这种效果的灵感来自飞天小女警 。 我想创建一个在黑白世界中传播颜色的效果,但要在世界空间的坐标中实现它 ,以观察颜色是如何绘制对象的 ,而不是像卡通一样在屏幕上平展地传播。

我在Unity引擎的新轻量级渲染管道中创建了效果,这是可脚本化渲染管道管道的内置示例。 所有概念都适用于其他管道,但是某些内置函数或矩阵可能具有不同的名称。 我也使用了新的后处理堆栈,但是在本教程中,我将省略其设置的详细说明,因为在其他手册中对此视频进行了很好的描述。



灰度后处理的效果


仅供参考,这是没有后处理效果的场景。


为此,我使用了新的Unity 2018 Post-Processing程序包,可以从程序包管理器中下载该程序包。 如果您不知道如何使用它,那么我推荐本教程

我通过扩展用C#编写的PostProcessingEffectSettings和PostProcessEffectRenderer类来编写自己的效果,可以在此处查看其源代码。 实际上,除了在Inspector中添加了一组常规属性外,我并没有对CPU端的这些效果进行任何特别有趣的操作(使用C#代码),因此在本教程中不会解释如何执行此操作。 我希望我的代码能说明一切。

让我们继续着色器代码,并从灰度效果开始。 在本教程中,我们将不会修改shaderlab文件,输入结构和顶点着色器,因此您可以在此处查看其源代码。 相反,我们将处理片段着色器。

为了将颜色转换为灰度,我们将每个像素的值减小为描述其亮度 的亮度值 。 这可以通过获取相机纹理的颜色值加权矢量的标量积来完成,该标量积描述了每个颜色通道对总体颜色亮度的贡献。

为什么使用标量积? 不要忘记标量积的计算如下:

dot(a, b) = a x * b x + a y * b y + a z * b z

在这种情况下,我们将颜色值的每个通道乘以weight 。 然后,我们将这些乘积相加,以将它们减小到一个标量值。 当RGB颜色在R,G和B通道中具有相同的值时,该颜色变为灰色。

着色器代码如下所示:

 float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); float3 weight = float3(0.299, 0.587, 0.114); float luminance = dot(fullColor.rgb, weight); float3 greyscale = luminance.xxx; return float4(greyscale, 1.0); 

如果基本着色器配置正确,则后处理效果应以灰度为整个屏幕着色。




在世界空间中渲染色彩效果


由于这是后处理效果,因此在顶点着色器中我们没有有关场景几何的任何信息 。 在后期处理阶段,我们仅有的信息是相机渲染的图像以及用于对其进行采样的截断坐标空间 。 但是,我们希望着色效果能够像在世界上一样发生在整个对象上,而不仅仅是在纯平屏幕上。

要在场景的几何图形中绘制此效果,我们需要每个像素的世界空间坐标 。 要从截断坐标空间的坐标移动世界空间坐标 ,我们需要对坐标空间进行转换

通常,要从一个坐标空间转到另一个坐标空间,需要一个矩阵来定义从坐标空间A到空间B的变换。要从A到B,我们将坐标空间A中的向量乘以此变换矩阵。 在我们的例子中,我们将执行以下转换: 截断坐标空间(剪辑空间) -> 视图空间(视图空间) -> 世界空间(世界空间) 。 也就是说,我们需要Unity提供的剪辑到视图空间矩阵和视图到世界空间矩阵。

但是, 截断坐标空间Unity坐标没有z值该z值确定像素的深度或到相机的距离。 我们需要此值从截断的坐标空间移动到物种空间。 让我们开始吧!

获取深度缓冲区值


如果启用了渲染管线,则它将在视口中绘制纹理,该纹理将z值存储称为depth buffer的结构中。 我们可以对该缓冲区进行采样,以获得截断坐标的坐标空间的丢失z值

首先,通过在检查器中单击相机的“添加其他数据”部分,并选中“需要深度纹理”复选框,确保深度缓冲区已真正渲染。 还要确保为相机启用了“允许MSAA”选项。 我不知道为什么需要检查这种效果,但是确实如此。 如果绘制了深度缓冲区,则在帧调试器中,您应该看到“深度预通过”阶段。

hlsl文件中创建_CameraDepthTexture采样器

 TEXTURE2D_SAMPLER2D(_CameraDepthTexture, sampler_CameraDepthTexture); 

现在让我们编写GetWorldFromViewPosition函数,现在我们将使用它来检查depth buffer 。 (稍后我们将对其进行扩展以在世界上占有一席之地。)

 float3 GetWorldFromViewPosition (VertexOutput i) { float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; return z.xxx; } 

在片段着色器中,绘制深度纹理样本的值。

 float3 depth = GetWorldFromViewPosition(i); return float4(depth, 1.0); 

当场景中只有一个丘陵平原时,这就是我的结果(我关闭了所有树木,以进一步简化对世界空间值的测试)。 您的结果应该看起来相似。 黑白值描述了从几何图形到相机的距离。


如果遇到问题,可以采取以下步骤:

  • 确保相机启用了深度渲染。
  • 确保相机已启用MSAA。
  • 尝试改变相机的近和远平面。
  • 确保您希望在深度缓冲区中看到的对象使用具有深度传递的着色器。 这样可以确保对象绘制到深度缓冲区。 LWRP中的所有标准着色器都可以执行此操作。

在世界空间获取价值


现在我们有了截断坐标空间所需的所有信息,让我们转换为物种空间 ,然后转换为世界空间

请注意,这些操作所需的转换矩阵已经在SRP库中。 但是,它们包含在Unity引擎的C#库中,因此我将它们插入ColorSpreadRenderer脚本的Render函数中的着色器中:

 sheet.properties.SetMatrix("unity_ViewToWorldMatrix", context.camera.cameraToWorldMatrix); sheet.properties.SetMatrix("unity_InverseProjectionMatrix", projectionMatrix.inverse); 

现在,让我们扩展GetWorldFromViewPosition函数。

首先,我们需要通过将截断坐标空间中的位置乘以InverseProjectionMatrix来获得视口中的位置 。 我们还需要使用屏幕位置来做更多的巫毒术,这与Unity如何在截断坐标的空间中存储其位置有关。

最后,我们可以将视口中的位置乘以ViewToWorldMatrix以获得世界空间中的位置。

 float3 GetWorldFromViewPosition (VertexOutput i) { //    float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, i.screenPos).r; //      float4 result = mul(unity_InverseProjectionMatrix, float4(2*i.screenPos-1.0, z, 1.0)); float3 viewPos = result.xyz / result.w; //      float3 worldPos = mul(unity_ViewToWorldMatrix, float4(viewPos, 1.0)); return worldPos; } 

让我们进行检查以确保全局空间中的位置正确。 为此,我编写了一个着色器 ,该着色器仅返回对象在世界空间中的位置; 这是基于常规着色器的相当简单的计算,其正确性可以信赖。 关闭后处理效果,并为世界空间拍摄此测试着色器的屏幕快照。 在将着色器应用于场景中的地球表面后,我的样子如下所示:


(请注意,世界空间中的值远大于1.0,因此请不必担心这些颜色是否有意义;相反,只需确保“真”和“计算”答案的结果相同即可。)接下来,让我们返回测试该对象是普通材料(而不是世界空间的测试材料),然后再次启用后处理效果。 我的结果如下所示:


这与我编写的测试着色器完全相似,也就是说,世界空间的计算很可能是正确的!

在世界空间中画一个圆


现在我们在世界空间中有了位置 ,我们可以在场景中绘制一个颜色的圆圈! 我们需要设置效果将在其中绘制颜色的半径 。 在外部,效果将以灰度渲染图片。 要进行设置,您需要调整效果半径_MaxSize )和圆心(_Center)的值。 我在C# ColorSpread类中设置了这些值,以便它们在检查器中可见。 让我们通过强制其检查当前像素是否在圆半径内来扩展片段着色器:

 float4 Frag(VertexOutput i) : SV_Target { float3 worldPos = GetWorldFromViewPosition(i); // ,      .  //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= _MaxSize? 0 : 1; //   float4 fullColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.screenPos); //   float luminance = dot(fullColor.rgb, float3(0.2126729, 0.7151522, 0.0721750)); float3 greyscale = luminance.xxx; // ,       float3 color = (1-blend)*fullColor + blend*greyscale; return float4(color, 1.0); } 

最后,我们可以根据颜色是否在世界空间半径内绘制颜色。 这就是基本效果!




添加特殊效果


我将介绍几种用于使颜色分布在整个地面上的技术。 完整的效果还有很多,但是本教程已经变得太大了,因此我们将自己限制在最重要的位置。

圆扩大动画


我们希望这种影响扩散到整个世界,即好像在增长。 为此,您需要根据时间更改半径

_StartTime指示圆应开始增长的时间。 在我的项目中,我使用了一个额外的脚本,该脚本允许您单击屏幕上的任意位置以开始圈的增长。 在这种情况下,开始时间等于单击鼠标的时间。

_GrowthSpeed设置增加圆的速度。

 //           float timeElapsed = _Time.y - _StartTime; float effectRadius = min(timeElapsed * _GrowthSpeed, _MaxSize); //  ,      effectRadius = clamp(effectRadius, 0, _MaxSize); 

我们还需要更新距离检查,以将当前距离与效果增加的半径进行比较 ,而不是与_MaxSize进行比较。

 // ,         //   ,   ,  ,   float dist = distance(_Center, worldPos); float blend = dist <= effectRadius? 0 : 1; //     ... 

结果如下所示:


增加了噪声的半径


我希望效果更像是模糊的油漆,而不仅仅是一个不断扩大的圆圈。 为此,让我们在效果的半径中添加噪点,以使分布不均匀。

首先,我们需要对世界空间中的纹理进行采样。 i.screenPos的UV坐标位于屏幕空间中 ,如果我们基于它们进行采样,效果的形状将随相机一起移动; 因此,让我们使用世界空间中的坐标。 我添加了_NoiseTexScale参数来控制噪声纹理样本比例 ,因为世界空间中的坐标非常大。

 //          float2 worldUV = worldPos.xz; worldUV *= _NoiseTexScale; 

现在,让我们对噪声纹理进行采样,并将此值添加到效果的半径。 我使用_NoiseSize缩放比例来更好地控制噪声大小。

 //     float noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, worldUV).r; effectRadius -= noise * _NoiseSize; 

经过一些调整后,结果如下所示:




总结


您可以在我的Twitter上关注这些教程的更新,而在Twitch ,我会花很多时间编写代码流! (此外,我会不时播放游戏,因此,如果您看到我坐在睡衣上玩《王国之心3》,请不要感到惊讶。)

致谢:

Source: https://habr.com/ru/post/zh-CN436456/


All Articles