屏幕效果中的覆盖混合模式

屏幕效果中的覆盖混合模式 对于我们最后要讲的知识点,我们将会去了解另一种混合模式,覆盖混合模式。这种模式实际上是利用了一些条件声明,这些条件声明决定了每个通道上的每个像素的最终颜色。所以,在使用这种混合模式的过程中需要编写的代码会更多一些。接下来我们看看该如何实现它。 始前准备 对于最后这个屏幕特效,我们需要像前面两个知识点中那样设置两个脚本(一个C#, 一个shader)。对于这个知识点,我们将使用之前使用的场景,所以我们不必创建新的场景了: 1.分别创建一个名为 Overlay_ImageEffect 的C#脚本和一个名为 Overlay_Effect 的着色器脚本。 2.把上一个知识点中用的C#脚本代码复制到这个新的C#脚本中来。 3.将上一个知识点中使用的着色器代码复制到这个新的着色器代码中来。 4.将 Overlay_ImageEffect C#脚本挂载到主摄像机上(注意把之前的C#脚本先移除),然后在 检查器面板(Inspector) 中将 Overlay_Effect 着色器拖拽到C#脚本组件的着色器变量上。 5.然后分别双击C#脚本和着色器在代码编辑器上打开它们。 操作步骤 开始处理我们的覆盖屏幕效果,我们将需要完成着色器代码而且要运行起来没有错误。接下来我们就可以修改C#脚本用来给着色器发送正确的数据。 1.首先要做的是在着色器的 属性块(Properties block) 中添加需要的属性。我们将使用这一章前面几个知识点中一样的一些属性: Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _BlendTex ("Blend Texture", 2D) = "white" {} _Opacity ("Blend Opacity", Range(0, 1)) = 1 } 2.接下来我们需要在 CGPROGRAM 代码块之内添加与属性对应的变量: CGPROGRAM #pragma vertex vert_img #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" uniform sampler2D _MainTex; uniform sampler2D _BlendTex; fixed _Opacity; 3.为了让 覆盖混合效果(Overlay Blend effect) 能起作用,我们必须要分别处理每通道中的每一个像素。为了实现这一操作,我们要编写一个自定义函数,这个函数接收一个单独通道,比如传递一个红色通道给它,并且进行覆盖操作。在着色器的变量声明下面输入下面的代码: fixed OverlayBlendMode(fixed basePixel, fixed blendPixel) { if(basePixel < 0.5) { return (2.0 * basePixel * blendPixel); } else { return (1.0 - 2.0 * (1.0 - basePixel) * (1.0 - blendPixel)); } } 4.最后,我们要去修改 frag() 函数来处理纹理的每一个通道从而进行混合操作: fixed4 frag(v2f_img i) : COLOR { //获得渲染纹理的颜色并且获取 v2f_img的uv fixed4 renderTex = tex2D(_MainTex, i.uv); fixed4 blendTex = tex2D(_BlendTex, i.uv); fixed4 blendedImage = renderTex; blendedImage.r = OverlayBlendMode(renderTex.r, blendTex.r); blendedImage.g = OverlayBlendMode(renderTex.g, blendTex.g); blendedImage.b = OverlayBlendMode(renderTex.b, blendTex.b); //对混合模式程度进行线性插值 renderTex = lerp(renderTex, blendedImage, _Opacity); return renderTex; } 5.当我们的着色器代码编写好后,我们的期待的效果应该起作用了。保存着色器代码并且返回到Unity编辑器让着色器编译。我们的C#脚本完全不用改并且已经设置好了。当着色器编译完成之后,你将会看到跟下图相似的一个结果: 原理介绍 我们的覆盖混合模式的确涉及到了很多更深的内容,但是如果你真的仔细剖析这些函数的话,你就会发现它是一个简单的 multiply 混合模式和一个简单的 屏幕混合模式(screen blend mode) (就是说可以拆为这两个,通过条件语句)。真的就是那样,在这个例子中,我们通过条件检测对一个像素执行不同的混合模式。 通过这个特定的屏幕效果,当 覆盖函数(Overlay function) 接收到一个像素后,会检测它是否小于0.5.如果是,我们就对它执行一个修改过的 multiply 混合模式;如果不是,则对它执行一个修改过的 屏幕混合模式(screen blend mode)。对于每个通道上的每一个像素我们都执行上述操作,最终得到我们屏幕效果的RGB像素值。 正如你所看到的,对于屏幕效果来说可以做很多的事情。这真的就取决于是什么平台和可以为屏幕效果分配多少内存。通常,这是由游戏项目的整个过程决定的,所以,玩的开心并且在屏幕效果上尽情发挥你的想象力吧。

April 18, 2023 · 1 min · 176 words · Link

在屏幕效果中使用基础的类Photoshop混合模式

在屏幕效果中使用基础的类Photoshop混合模式 屏幕效果不仅仅只限于调整游戏中渲染纹理的颜色。我们还可以使用它将渲染纹理和其他的图像结合在一起。这个技术跟Photoshop中创建一个新的图层没有什么不同然后选择一种混合模式将两张图片混合在一起,当然在我们这里就是将一张纹理跟渲染纹理混合。这是一个非常强的技术,因为它提供给了艺术家一个在游戏中模拟混合模式的生产环境,而不不仅仅只是在Photoshop中。 对于这个特定的知识点,我们将会了解一些更加常用的混合模式,比如说 Multiply,Add 和 Overlay。你将会看到在游戏中拥有一个Photoshop中的混合模式的功能是多么的简单。 始前准备 开始前,我们需要准备资源。所以请跟着下面的步骤为我们新的 混合模式屏幕效果( Blend mode screen effect) 设置好我们的屏幕效果系统并且让它顺利运行起来: 1.创建一个新的C#脚本,并且为其命名为 BlendMode_ImageEffect 2.创建一个新的着色器,命名为 BlendMode_Effect 3.我们简单的将我们本章节第一个知识点中的C#脚本中的代码复制到我们这个新的C#脚本中来。这样我们就可以将精力放在混合模式效果实现的数学原理上。 4.同样,将本章节第一个知识点中的着色器代码赋值到我们这个新的着色器代码中来。 5.最后,我们需要一张额外的纹理来表现我们的混合模式效果。在这个知识点,我们将使用一张 粗旧类型纹理(grunge type texture)。它能让测试效果看起来非常明显。 下图是这个效果要使用的一张粗旧型贴图。使用一张有足够细节和有良好灰度值范围的纹理有助于我们测试新的效果: 操作步骤 我们第一个要实现的混合模式是Photoshop中的一样的 Multiply 混合模式。让我们先修改我们着色器中的代码。 1.在Unity的项目窗口中双击着色器,在代码编辑器中打开我们的着色器代码。 2.我们需要在属性块中添加一些新的属性好让我们可以有纹理可以混合并且要有一个 不透明度滑动条(a slider for an opacity value) 。在你的着色器中输入下面的代码: Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _BlendTex ("Blend Texture", 2D) = "white" {} _Opacity ("Blend Opacity", Range(0, 1)) = 1 } 3.在 CGPROGRAM 代码块中添加与属性对应的变量好让我们可以访问 属性块(Properties block) 中的数据: CGPROGRAM #pragma vertex vert_img #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" uniform sampler2D _MainTex; uniform sampler2D _BlendTex; fixed _Opacity; 4.最后我们修改 frag() 函数从而来表现我们两张纹理的 multiply 操作: fixed4 frag(v2f_img i) : COLOR { //获得渲染纹理的颜色并且获取 v2f_img的uv fixed4 renderTex = tex2D(_MainTex, i.uv); fixed4 blendTex = tex2D(_BlendTex, i.uv); //执行 multiplay 混合模式 fixed4 blendedMultiply = renderTex * blendTex; //对混合模式程度进行线性插值 renderTex = lerp(renderTex, blendedMultiply, _Opacity); return renderTex; } 5.保存着色器代码并且返回Unity编辑器,让着色器编译并且看看有没有错误。如果没有遇到错误,我们就双击C#脚本在代码编辑器中打开。 6.在C#脚本中我们同样需要创建一些相应的变量。我们需要一个纹理变量用来赋值纹理并且将它发送给着色器,我们还需要一个滑动条用来调整我们想要的混合模式的最终程度: #region Variables public Shader curShader; public Material curMaterial; public Texture2D blendTexture; public float blendOpacity = 1.0f; #endregion 7.接下来,我们需要通过 OnRenderImage() 方法给着色器传递我们的变量数据: private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (curShader != null) { material.SetTexture("_BlendTex", blendTexture); material.SetFloat("_Opacity", blendOpacity); Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } } 8.为了完成我们的脚本,我们还要修改 Update() 方法,这样我们就能把 blendOpacity 变量的值控制在 [0, 1] 范围内。 private void Update() { blendOpacity = Mathf.Clamp(blendOpacity, 0.0f, 1.0f); } 当这些都完成之后,我们将屏幕效果脚本挂到我们的主摄像机上并且将我们的着色器拖拽赋值到我们的C#脚本中,这样让脚本可以有一个着色器来进行逐像素操作。最终,为了让效果能完全发挥功能,脚本和着色器都需要一张纹理。你可以在Unity的 检查器面板(Inspector tab) 给屏幕效果脚本的纹理变量添加任何一张纹理。一旦你添加了一张纹理,你就能看到游戏的渲染纹理之上还有我们刚刚添加的纹理,并且已经进行了 multiplying 混合操作。下图演示了这个屏幕效果: ...

April 16, 2023 · 2 min · 342 words · Link

在屏幕效果中使用亮度, 饱和度和对比度

在屏幕效果中使用亮度, 饱和度和对比度 现在我们有了自己的屏幕效果系统并且能正常运行,我们就可以去探索在当今的游戏当中更多涉及到像素操作的一些更常用的屏幕效果。 首先,使用屏幕效果来调节游戏整体的最终颜色效果,这肯定可以给艺术家对于游戏最终的样子,有一个全局的控制。比如可以用一些颜色滑动条用来调节游戏最终渲染结果的 R,G,B 颜色强度。又或者是给整个屏幕填充大量的某个颜色这样看起来就像是一种深褐色的胶片效果。 对于这个特殊的知识点,我们将会涵盖一些可以在图像上进行的更加核心的颜色修改操作。它们是 亮度(brightness), 饱和度(saturation) 和 对比度(contrast)。学习如何对这些颜色调整过程进行编码,将给我们学习屏幕的艺术效果一个很好的基础。 始前准备 这里我们需要创建一些新的资源。我们可以利用同样的场景作为我们的测试场景,但是我们需要一个新的脚本和着色器: 1.创建一个新的名为 BSC_ImageEffect 的脚本。 2.创建一个名为 BSC_Effect 的新着色器。 3.现在我们需要简单将前一个知识点中的脚本代码复制到现在这个新的脚本中来。这样的话可以让我们把重点放在亮度,饱和度和对比度的数学原理上。 4.把上一个知识点中的着色器代码复制到我们这个新的着色器中。 5.在场景中创建几个新的游戏对象,然后添加几个不同颜色的漫反射材质球,然后把这些材质球随机的添加给场景中这几个新的游戏对象。这将会给我们一个很好的颜色范围来测试我们的新屏幕效果。 当这些完成之后,你将会有一个类似于下图的游戏场景: 操作步骤 现在我们设置好了我们的场景并且创建好了我们的新脚本和着色器,我们可以开始填写必要的代码从而获得亮度,饱和度和对比度屏幕特效了。我们将着重于像素操作和为我们的脚本和着色器设置好变量。就跟我们在 设置屏幕效果脚本系统 这个知识点中描述的一样,准备好我们的屏幕效果系统并且让它跑起来: 1.用我们的代码编辑器打开我们的脚本和着色器。我们只需简单在 项目窗口(project view) 双击就可以进行前面的两个操作。 2.先编辑着色器,这样可以让我们更加清楚我们的C#脚本需要哪些变量。我们先在属性块中给亮度,饱和度和对比度效果添加对应的属性。注意,我们要保留属性块中的 _MainTex 属性,因为这是我们创建屏幕效果时 渲染纹理 目标需要的属性: Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _BrightnessAmount ("Brightness Amount", Range(0.0, 1)) = 1.0 _satAmount ("Saturation Amount", Range(0.0, 1)) = 1.0 _conAmount ("Contrast Amount", Range(0.0, 1)) = 1.0 } 3.如往常一样,为了能在 CGPROGRAM 代码块中访问来自属性的数据,我们需要在 CGPROGRAM 代码块中创建与之对应的变量: CGPROGRAM #pragma vertex vert_img #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" uniform sampler2D _MainTex; fixed _BrightnessAmount; fixed _satAmount; fixed _conAmount; 4.现在我们需要添加一些操作好用来表现我们的亮度,饱和度和对比度效果。在我们的着色器中添加下面的新函数,在 frag() 函数上面添加即可。不要担心它现在虽然还没啥用;我们将在下一个知识点中解释所有的代码: float3 ContrastSaturationBrightness(float3 color, float brt, float sat, float con) { float AvgLumR = 0.5; float AvgLumG = 0.5; float AvgLumB = 0.5; float LuminanceCoeff = float3(0.2125, 0.7154, 0.0721); float3 AvgLumin = float3(AvgLumR, AvgLumG, AvgLumB); float3 brtColor = color * brt; float intensityf = dot(brtColor, LuminanceCoeff); float3 intensity = float3(intensityf, intensityf, intensityf); float3 satColor = lerp(intensity, brtColor, sat); float3 conColor = lerp(AvgLumin, satColor, con); return conColor; } 5.最后,我们需要更新 frag() 函数去使用 ContrastSaturationBrightness() 函数。这将会处理我们渲染纹理的所有像素并且传回给我们的C#脚本: fixed4 frag(v2f_img i) : COLOR { fixed4 renderTex = tex2D(_MainTex, i.uv); renderTex.rgb = ContrastSaturationBrightness(renderTex.rgb, _BrightnessAmount, _satAmount, _conAmount); return renderTex; } 着色器代码写好之后,返回Unity编辑器让它编译着色器。如果没有报错,我们返回代码编辑器来编辑C#脚本了。开始时先创建几行新的代码用来发送合适数据给着色器: ...

April 15, 2023 · 2 min · 323 words · Link

通过Unity渲染纹理实现屏幕效果

第八章 通过Unity渲染纹理实现屏幕效果 我们在这一章会学习下面的这些知识点: 设置屏幕效果脚本系统 在屏幕效果中使用亮度, 饱和度和对比度 在屏幕效果中使用基础的类Photoshop混合模式 屏幕效果中的覆盖混合模式 介绍 学习编写着色器最让人印象深刻的是创建你自己的屏幕效果的过程,也就是常说的后处理。有了这些屏幕效果,我们就可以用 Bloom ,Motion Blur 和 HDR 效果等技术创建出一些惊奇的实时图像。当今游戏市场推出的大部分游戏在 景深(depth of field) 效果,辉光(bloom) 效果甚至 颜色修正(color correction) 效果上都大量使用了屏幕效果。 通过这个章节,你将会学习如何构建一个如何去控制这些屏幕效果的脚本系统。我们将会涵盖 渲染纹理(Render Texture),深度缓冲(depth buffer),以及如何创建一个类Photoshop感的效果,从而去控制游戏的最终渲染图片的效果。通过为你的游戏使用屏幕效果,你不经可以完成你的着色器编写知识,同时你还将会拥有在Unity中创自己那些不可思议的渲染器的能力。 设置屏幕效果脚本系统 创建屏幕效果是这么一个过程,我们先抓取一张全屏的图像(或者纹理),然后用一个着色器在GPU中去处理它的像素,之后再把它发回给Unity的渲染器并且应用到游戏的整个渲染好的图像上去。这样的话就允许我们实时的在游戏的渲染图片上进行逐像素操作了,从而使我们对艺术效果有更广的控制。 试想一下如果你不得不仔细检查并且调节你游戏中每一个游戏对象上的每个材质,而这么做仅仅是想调节游戏最终视效的对比度。尽管可行,但还是有点费事。通过利用屏幕效果,我们就可以整个的调整屏幕的最终效果,因此对于有些最终呈现我们能获得更强的 类Photoshop(Photoshop-like) 的控制。 为了建立起一个屏幕效果系统并且使它成功运行,我们需要准备一个脚本来跟游戏当前的渲染图片进行通信,这个 渲染图片(rendered image) 就是Unity中的 渲染纹理(Render Texture) 。利用这个脚本将渲染纹理传给着色器,我们能创建一个灵活的系统来创建屏幕效果。对于我们的第一个屏幕效果,我们将创建一个非常简单的灰度效果,这个效果可以让我们的游戏看起来是黑白的效果。让我们拭目以待。 始前准备 为了构建好我们的屏幕效果系统并且顺利运行,我们需要为当前的Unity工程创建一些资产。为了获得这些,跟着下面的步骤走就对了: 1.在当前的项目中,我们创建一个名为 TestRenderImage.cs 的脚本。 2.创建一个新的着色器,命名为 ImageEffect.shader 。 3.在场景中新建一个球体并且为其添加一个新的材质球,新材质可以任意,比如我们创建了一个简单的红色的,带有高光的材质球。 4.最后,创建一个新的方向光,然后保存场景。 当我们把所有的资源准备好后,就等于是简单的设置好了场景,看起来就跟下图一样: 操作步骤 为了让我们灰度屏幕效果能运行,我们需要一个脚本和着色器。我们将会在这里完成这新的两项并且给它们添加合适的代码从而生成我们的第一个屏幕效果。我们的首个任务就是完成C#脚本。这样可以让我们的整个系统跑起。在这之后,我们将会完成着色器的编写并且看到我们的屏幕效果。让我们通过下面的步骤完成我们的脚本和着色器: 1.打开 TestRenderImage.cs 这个脚本并且添加一些变量好让我们能保存导入的游戏对象和数据。在 TestRenderImage 这个类的最上面添加下面的代码: public class TestRenderImage : MonoBehaviour { #region Variables public Shader curShader; public float grayScaleAmount = 1.0f; public Material curMaterial; #endregion #region Properties } 2.当Unity编辑器没有运行的时候,为了让我们能够实时的编辑屏幕效果,我们需要在 TestRenderImage 类的声明上面添加下面这行代码: 3.因为我们的屏幕效果是使用一个着色器在一个屏幕图像上进行逐像素操作,所以我们必须要创建一个材质来运行这个着色器。没有这个材质,我们就没有办法访问着色器的属性。因此,我们将创建一个C#的属性来检测材质,如果没有找到这个材质就创建一个。在第一个步骤的变量声明的下面输入下面的代码: #region Properties Material material { get { if (curMaterial == null) { curMaterial = new Material(CurShader); curMaterial.hideFlags = HideFlags.HideAndDontSave; } return curMaterial; } } #endregion 4.现在我们想在脚本中设置一些检测来看看当前我们build的Unity游戏在平台上是否支持图像效果。如果在脚本开始运行的时候发现不支持,这个脚本将会被禁用掉: private void Start() { if (!SystemInfo.supportsImageEffects) { enabled = false; return; } if (curShader && !curShader.isSupported) { enabled = false; } } 事实上 SystemInfo.supportsImageEffects 在较新的Unity版本中,是一直返回 true 的,这个属性已经被废弃掉了【译者述】 5.为了能够从Unity的渲染器中抓取 渲染图像(Rendered Image) ,我们需要利用下面这个Unity内建的 OnRenderImage() 方法。请输入下面的代码以便于我们能访问当前的 渲染纹理(Render Texture) : private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (curShader != null) { material.SetFloat("_LuminosityAmount", grayScaleAmount); Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } } 6.我们的屏幕效果有一个叫 grayScaleAmount 的变量,它可以控制我们想要的灰度屏幕效果的程度。所以,在这里我们需要控制它的取值范围是[ 0 - 1 ],0表示没有灰度效果,1表示满程度的灰度效果。我们将会在 Update() 方法中进行这个操作,这意味着当脚本运行的时候会在游戏的每一帧去设置它们: private void Update() { grayScaleAmount = Mathf.Clamp(grayScaleAmount, 0.0f, 1.0f); } 7.最后,当脚本运行的时候,对于我们创建的这些对象我们需要对它们进行一些清理,这样这个脚本就完成了: private void OnDisable() { if (curMaterial) { DestroyImmediate(curMaterial); } } 这个时候,如果编译通过后,我们可以将脚本挂在到我们的摄像机中去了。让我们把 TestRenderImage.cs 这个脚本挂载到我们场景中的主摄像机上。你可以在编辑器上看到 grayScaleAmount 这个值和一个着色器的域,但这个脚本会在控制台窗口抛出一个错误。说它丢失了一个对象实例并且有可能会运行不正常。如果你回顾第四个步骤的话,可以看到我们做了一些检测来确定我们是否有着色器和当前的平台是否支持该着色器。我们还没有给这个屏幕效果脚本一个着色器让它能正常的工作,所以 curShader 变量是空的,所以抛出了这个错误。所以让我们继续完成着色器来完善我们的屏幕效果系统吧。 8.开始着手编写我们的着色器了,我们将会修改着色器的属性块,添加一些属性,好让我们能给这个着色器发送一些数据: Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _LuminosityAmount ("GrayScale Amount", Range(0.0, 1)) = 1.0 } 9.现在我们的着色器将使用纯CG着色器代码编写,而不是使用Unity内建的 表面着色器(Surface Shader) 代码。因为这样我们的屏幕效果可以得到更好的优化,因为这里我们仅仅是需要处理 渲染纹理(Render Texture) 的像素而已。所以我们将在着色器中创建一个新的 通道块(Pass block) 并且添加一些我们之前没有看过的 #pragma 声明: Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" 10.为了能够访问从Unity编辑器发送到着色器的数据,我们需要在 CGPROGRAM 中创建对应的变量: uniform sampler2D _MainTex; fixed _LuminosityAmount; 11.最后,就剩下去设置我们的 像素函数(pixel function) 了,在这个例子中就是 frag() 函数。这也是这个屏幕效果的关键代码。这个函数将会处理 渲染纹理(Render Texture) 的每一个像素并且给我们的 TestRenderImage.cs 脚本中返回一张新的图像: fixed4 frag(v2f_img i) : COLOR { fixed4 renderTex = tex2D(_MainTex, i.uv); float luminosity = 0.299 * renderTex.r + 0.587 * renderTex.g + 0.114 * renderTex.b; fixed4 finalColor = lerp(renderTex, luminosity, _LuminosityAmount); return finalColor; } 【为了防止大家跟我一样,看的一头雾水,我在这里贴一下完整的着色器代码】: Shader "Custom/ImageEffect" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _LuminosityAmount ("GrayScale Amount", Range(0.0, 1)) = 1.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" uniform sampler2D _MainTex; fixed _LuminosityAmount; fixed4 frag(v2f_img i) : COLOR { fixed4 renderTex = tex2D(_MainTex, i.uv); float luminosity = 0.299 * renderTex.r + 0.587 * renderTex.g + 0.114 * renderTex.b; fixed4 finalColor = lerp(renderTex, luminosity, _LuminosityAmount); return finalColor; } ENDCG } } FallBack "Diffuse" } 当完成我们的着色器之后,返回到Unity编辑器让它编译着色器并且看看有没有遇到任何的错误。如果没有,就将这个新的着色器拖拽并且赋值给 TestRenderImage.cs 脚本,并且修改脚本上面的灰度变量的值。你应该可以在游戏窗口中看到游戏从有颜色变为灰色。下面的图片演示了这个屏幕效果: 当完成了这些之后,我们就有了一个非常方便的途径去测试新的屏幕效果着色器,这样就不用反复的去编写 屏幕效果系统(Screen Effect system) 的代码了。接下来让我们更深入的去了解一下在 渲染纹理(Render Texture) 中都发生了什么,并且在整个过程中是如何处理它的。 原理介绍 为了完成我们的屏幕效果并且在Unity中能运行起来,我们需要创建一个脚本和一个着色器。这个脚本在Unity的编辑器上会实时刷新,它也负责从从主摄像机中捕获 渲染纹理(Render Texture) 并且把它传到着色器中。一旦这个渲染纹理到达着色器,我们就可以使用这个着色器进行逐像素操作。 ...

April 2, 2023 · 3 min · 576 words · Link

针对移动设备修改着色器

针对移动设备修改着色器 我们在优化着色器这方面已经了解了较多的技术了,现在让我们来了解如何为移动设备编写高效,高质量的着色器代码。对于我们已经写好的着色器代码,通过一些小的修改让他们能在移动设备上高速运行好事比较简单的。这里包含了使用 approxview 或者 halfasview 光照函数变量等知识内容。我们可以减少所需的纹理数量并且对所用的纹理使用更好的压缩方式。这个知识点的最后,对于移动游戏,我们将会有一个优化很好的法线贴图,高光着色器。 始前准备 在开始前,我们先创建一个新的场景并且创建一些游戏对象用来使用我们的着色器: 创建一个新的场景并且添加一个默认球体和一个方向光。 创建一个新的材质球和着色器,并且把着色器应用到材质。 最后把材质应用到场景中的球体上。 当完成上面的步骤后,你的场景看起来大概更下图差不多: 操作步骤 在这个知识点中,我们会反复斟酌着色器中各种元素从而编写一个对移动平台友好着色器: 1.首先根据所需的纹理修改着色器的 属性块(Properties block) 。在这个例子中,我们会使用一个alpha通道带有光滑纹理漫反射纹理,一张法线贴图,一个控制高光强度的滑动条。 Properties { _Diffuse ("Base (RGB) Specular Amount (A)", 2D) = "white" {} _SpecIntensity ("Specular Width", Range(0.01, 1)) = 0.5 _NormalMap ("Normal Map", 2D) = "bump" {} } 2.下一个任务是设置 #pragma 申明。这些声明会打开或者关闭 表面着色器(Surface Shader) 的一些具体特性,并且最终影响着色器的性能消耗,是高成本还是低成本。 CGPROGRAM #pragma surface surf MobileBlinnPhong exclude_path:prepass nolightmap noforwardadd halfasview 3.接着我们在 CGPROGRAM 中定义与 属性块(Properties block) 中对应的变量。这次对于高光强度这个滑动条,我们将使用 fixed 类型的变量,从而减少着色器的内存使用: sampler2D _Diffuse; sampler2D _NormalMap; fixed _SpecIntensity; 4.为了能将我们的纹理映射到游戏对象的表面,我们需要获取相应的UV。这个例子里,为了让着色器数据保持最小,我们将仅使用一个UV设置: struct Input { half2 uv_Diffuse; }; 5.这一步是要完成我们的光照函数,由于在 #pragma 声明中有了一些新的变量,所以这里我们就可以使用它们: inline fixed4 LightingMobileBlinnPhong(SurfaceOutput s, fixed3 lightDir, fixed3 halfDir, fixed atten) { fixed diff = max(0, dot(s.Normal, lightDir)); fixed nh = max(0, dot(s.Normal, halfDir)); fixed spec = pow(nh, s.Specular*128) * s.Gloss; fixed4 c; c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten * 2); c.a = 0.0; return c; } 6.最后,我们需要创建 surf() 函数并且处理表面的最终颜色: void surf (Input IN, inout SurfaceOutput o) { // Albedo comes from a texture tinted by color fixed4 diffuseTex = tex2D(_Diffuse, IN.uv_Diffuse); o.Albedo = diffuseTex.rgb; o.Gloss = diffuseTex.a; o.Alpha = 0.0; o.Specular = _SpecIntensity; o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_Diffuse)); } 当完成上面的步骤后,我们完成了这个知识点的部分代码,保存你的着色器代码并且返回Unity编辑等待着色器编译完。如果没有遇到什么错误,你会获得一个类似下图的结果: 原理介绍 所以,让我们开始介绍这个着色器吧,看看它做了什么和没做什么。首先,它排除了后光线通道。也就是说如果你创建了一个连接 后渲染前通道(deferred renderer’s prepass) 的光线函数,那么它将不会使用那个特定的光线函数并且会去寻找默认的光线函数,比如我们这本书目前为止创建的那些默认的光线函数一样。 这个特定的着色器并不受Unity内部的光照贴图系统的光照映射支持。这样的话对于使用了这个着色器的游戏对象来说,可以防止着色器试图去寻找法线贴图,从而可以让着色器有更好的性能表现,因为它不用再执行光线映射检测了。 我们添加了 noforwardadd 声明,这样的话通过单个的方向光我们只要处理逐像素纹理即可。所有其他类型的光将会强制变成逐顶点光并且不会被在 surf() 函数中的任何逐像素操作所涉及。 最后,我们使用 halfasview 声明告诉Unity,我们将不会使用普通光线函数中的 viewDir 参数。取而代之的是,我们将使用 半向量(half vector) 作为视野方向并且处理我们的高光。这样的话着色器的处理将会快很多,因为它是基于逐顶点操作来完成的。当用这个着色器来模拟真实世界中的高光时,其实它并不够精确,但是对于移动设备中的视效来说,它看起来已经很不错了并且着色器也优化的更好。 这些技术可以让着色器更加高效和简洁【codewise 我不知道怎么翻译】。按照游戏的要求,根据你的游戏硬件和视觉质量要求来衡量你到底需要那些数据,最好确保只使用你需要的数据。最后,这些技术最终组成了游戏使用的那些着色器。

March 26, 2023 · 1 min · 202 words · Link

着色器的性能分析

着色器的性能分析 我们现在知道该如何减少色器可能出现的内存消耗,让我们来了解一下在场景中,如何在大量同时运行的游戏对象,着色器和脚本等包含的大量着色器中,找出有问题的着色器。要在整个游戏中去找到某一个单独的游戏对象或者着色器可能会让人有点望而却步,但是Unity给我们提供了它内建的性能分析工具。它可以让我们知道在游戏中每一帧都发生了什么,CPU和GPU资源使用情况。 通过使用性能分析工具,我们可以使用其界面创建分析作业块,来单独分析诸如着色器、几何图新和一些一般渲染项。我们可以筛选出我们要寻找的影响性能的单个游戏对象。这让我们能够在运行时观察对象执行其功能时对 CPU和GPU的影响。 让我们来看看性能分析工具的不同部分和并且学习如何调试我们的场景,当然更重要的是如何调试我们的着色器。 始前准备 为了使用性能分析器,我们要准备好一些资源并且打开我们的性能分析窗口: 1.我们就使用介绍上一个知识点时的场景,然后通过菜单 Window | Profiler 或 Ctrl + 7 打开性能分析窗口。 2.我们多复制几个球体,看看这样会对渲染有什么影响。 你会看到跟下图类似的一些东西: 操作步骤 使用性能分析工具的时候,你会在该窗口看到一些UI元素。在我们点击运行按钮前,让我们了解一下该如何从性能分析器中获取我们想要的信息: 1.首先,点击 Profiler 窗口中的 GPU Usage ,CPU Usage 和 Rendering 这几栏。你可以在窗口的左上角找到这几栏: 使用这几栏,我们可以看到跟游戏主要功能相关的不同的数据。CPU Usage 给我们展示的是我们大部分脚本在干什么,当然还有物理运算和总体渲染。GPU Usage 这一栏给我们的是光照,阴影和渲染队列等的详情信息。最后,Rendering 这一栏有每一帧中 drawcall 和游戏场景中集合体的数量这些信息。 点击其中的一栏,我们就可以把在 性能会话(profiling session) 中看到的数据类型单独分离出来分析。 2.现在,我们可以点击性能分析栏中的小颜色块然后点击编辑器的运行按钮或者用 Ctrl + P 快捷键运行场景。 这样选择性的查看,可以让我们更加深入的分析性能会话,因为这样我们可以选择我们想分析的内容。当场景运行的时候,再 GPU使用(GPU Usage) 栏中取消其他所有的颜色小块的勾选,然后留下 Opaque 这一勾选。请注意这样我们就可以知道 Opaque 渲染队列中的游戏对象再渲染中花了多长时间了: 3.性能分析窗口中另一个非常好用的功能是可以在图形窗口中进行的拖拽操作。这个操作会自动暂停你的游戏,这样你就可以更细致的分析图形中某一个具体的波峰从而找出引起性能问题的具体项了。你可以在图形区域内点击或者拖拽移动来暂停游戏,从而了解这一功能的具体效果: 4.现在让我们把目光聚焦到性能分析窗口的下半部分,你会发现当我们选中GPU那一栏的时候这里会有一个下拉选择框。我们可以把它展开从而获得更多当前激活的性能会话中的详细信息,这种情况下我们可以知道有关于当前摄像机渲染情况和花费时间的更多信息: 它能让我们全面了解Unity在某一帧中内部工作都在处理什么。在这个例子中,我们能看到场景中的球体和我们优化过的着色器在绘制到屏幕上花了大概0.14毫秒,花了7个drawcall,并且这个处理每帧花费了大概3.1%的GPU时间。通过这些类型的信息我们可以去诊断和解决跟着色器性能相关的问题。让我们准备一个测试,看看如果我们给着色器添加一个额外的纹理并且用 lerp 函数把两张漫反射纹理混合到一块会造成什么样的影响。你将会在在性能分析器中看到清晰的影响。 5.修改着色器的 属性块(Properties block) ,然后添加下面的代码,这样就可以为我们的着色器添加另一个纹理了: Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} NormalMap ("Normal Map", 2D) = "bump" {} _BlendTex ("Blend Texture", 2d) = "white" {} } 6.然后在 CGPROGRAM 中添加一个变量用来使用这个纹理: CGPROGRAM #pragma surface surf SimpleLambert exclude_path:prepass noforwardadd sampler2D _MainTex; sampler2D _NormalMap; sampler2D _BlendTex; 7.相应的我们也要去修改一下我们的 surf() 函数以便我们能将纹理和漫反射纹理混合到一块: void surf (Input IN, inout SurfaceOutput o) { // Albedo comes from a texture tinted by color fixed4 c = tex2D (_MainTex, IN.uv_MainTex); fixed4 blendTex = tex2D(_BlendTex, IN.uv_MainTex); c = lerp(c, blendTex, blendTex.r); o.Albedo = c.rgb; o.Alpha = c.a; o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex)); } 当你保存了你着色器的修改并且回到Unity编辑器后,你就可以运行我们的游戏并且看看我们的着色器增加的时间消耗。回到Unity后点击运行按钮并且在性能分析器窗口查看结果: 你可以看到场景中我们着色器渲染 Opaque 队列的时间消耗数量从 0.140 毫秒增加到了 0.179 毫秒。从添加了另一张额外的纹理后和使用了 lerp() 函数后,我们的球体的渲染时间增加了。当然这个变化非常的小,但是想象一下如果有20个着色器用不同的工作方式在不同的游戏对象上,那消耗就多了。 ...

March 23, 2023 · 1 min · 170 words · Link

在2D游戏中实现水效果的着色器

在2D游戏中实现水效果的着色器 上一个知识点介绍的玻璃着色器它的效果是静态的;它的扭曲效果永远都不会改变。只要对着色器稍加修改,就可以将它转换成一个有动画的材质,它非常的适合2D游戏中的水体特效。在这个知识点将会使用 第五章,对表面着色器中的顶点使用动画 中类似的技术: 始前准备 这个知识点基于 使用抓取通道 知识点中描述的顶点和片元着色器,因为它很依赖抓取通道。 1.创建一个新的抓取通道着色器;你可以自己写一个新的着色器或者使用 使用抓取通道 这个知识点中用到的着色器作为开始。 2.为你的着色器创建一个对应的材质球。 3.将材质球应用到一个平面几何图形中,它将用来表示2D中的水。为了让这个效果起作用,您应该在其后渲染一些东西,以便可以看到类似水的扰动效果。 4.这个知识点需要一张噪音纹理,用来获得伪随机的值。选择一个无缝的噪音纹理很重要,比如由可以铺砌的2D的 Perlin 噪音生成的噪音纹理,如下图所示的那样。这是为了确保材质应用到一个很大的游戏对象中时,不会看到有任何不连续的割裂感。为了让效果起作用,纹理需要以 Repeat 模式导入。如果你想要让你的水体效果看起来平滑和连续,那么在 导入器(Inspector) 那里要设置成 Bilinear 。这样设置能确保纹理能从着色器中正确的被采样: 操作步骤 你可以修改着色器中的代码来创建动画效果。请跟着下面的步骤走: 1.将下面的代码添加到着色器的属性块中: _NoiseTex("Noise text", 2D) = "white" {} _Colour ("Colour", Color) = (1,1,1,1) _Period ("Period", Range(0,50)) = 1 _Magnitude ("Magnitude", Range(0,0.5)) = 0.05 _Scale ("Scale", Range(0,10)) = 1 2.并且在次通道中添加与属性对应的变量 sampler2D _NoiseTex; fixed4 _Colour; float _Period; float _Magnitude; float _Scale; 3.为顶点函数定义下面 输出结构体(output structure) : struct vertOutput { float4 vertex : POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; float4 worldPos : TEXCOORD1; float4 uvgrab : TEXCOORD2; }; 4.这个着色器需要知道每个片元在空间上的精确位置。为了实现这一过程,将代码 o.worldPos = mul(unity_ObjectToWorld, i.vertex); 添加到顶点函数中去: vertOutput vert(vertInput i) { vertOutput o; o.vertex = UnityObjectToClipPos(i.vertex); o.color = i.color; o.texcoord = i.texcoord; o.worldPos = mul(unity_ObjectToWorld, i.vertex); o.uvgrab = ComputeGrabScreenPos(o.vertex); return o; } 5.使用下面的片元函数: fixed4 frag(vertOutput o): COLOR { float sinT = sin(_Time.w / _Period); float2 distortion = float2( tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(sinT, 0) ).r - 0.5,tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(0, sinT) ).r - 0.5); o.uvgrab.xy += distortion * _Magnitude; fixed4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(o.uvgrab)); return col * _Colour; } 6.完整代码: Shader "Custom/IWS" { Properties { _NoiseTex("Noise text", 2D) = "white" {} _Colour ("Colour", Color) = (1,1,1,1) _Period ("Period", Range(0,50)) = 1 _Magnitude ("Magnitude", Range(0,0.5)) = 0.05 _Scale ("Scale", Range(0,10)) = 1 } SubShader { Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Opaque" } GrabPass { "_GrabTexture" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _GrabTexture; sampler2D _NoiseTex; fixed4 _Colour; float _Period; float _Magnitude; float _Scale; struct vertOutput { float4 vertex : POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; float4 worldPos : TEXCOORD1; float4 uvgrab : TEXCOORD2; }; struct vertInput { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; }; vertOutput vert(vertInput i) { vertOutput o; o.vertex = UnityObjectToClipPos(i.vertex); o.color = i.color; o.texcoord = i.texcoord; o.worldPos = mul(unity_ObjectToWorld, i.vertex); o.uvgrab = ComputeGrabScreenPos(o.vertex); return o; } fixed4 frag(vertOutput o): COLOR { float sinT = sin(_Time.w / _Period); float2 distortion = float2 ( tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(sinT, 0) ).r - 0.5, tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(0, sinT) ).r - 0.5 ); o.uvgrab.xy += distortion * _Magnitude; fixed4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(o.uvgrab)); return col * _Colour; } ENDCG } } } 原理介绍 这个着色器跟 实现一个玻璃效果的着色器 知识点中介绍的着色器很像。主要的区别就是这个着色器它时一个有动画的材质;它的扰动效果不是从法线贴图中生成的,而是通过当前的时间来计算出的一个持续的动画。用来扰动抓取纹理的UV数据的代码似乎有点复杂;让我们来理解它的效果时如何生成的。思路是用一个正弦函数来让水晃动。这个效果需要随着时间变化。为了获得这个效果,着色器产生的扭曲效果依赖于当前的时间,而这个时间可以通过内建的 _Time 变量获得。变量 _Period 决定了正弦函数的周期,意味着水波出现的有多快: float2 distortion = float2(tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(sinT, 0) ).r - 0.5,tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(0, sinT) ).r - 0.5); 这里的代码有一个问题,就是在 X 轴和 Y 轴它们的扰动是一样的;结果就是整个抓取纹理以圆形运动旋转,这看起来一点都不像水了。很显然我们需要为此添加一些随机性。 给着色器添加随即行为的最常用的方式就是添加一张噪音纹理。现在的问题就变成了找到一种方法来对纹理进行一个看似随机的采样。为了避免效果看起来有明显的正弦模式,最好的方法就是在噪声纹理的UV数据中使用正弦波作为偏移量: float sinT = sin(_Time.w / _Period); float2 distortion = float2( tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(sinT, 0) ).r - 0.5,tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(0, sinT) ).r - 0.5); 变量 _Scale 用来决定波的大小。这个方案已经很接近最终版本了,但还是有一些问题——如果水体移动,UV数据也会跟着它动然后你就会看到水波跟着材质动而不是锚定在背景上。为了解决这个问题,我们需要使用当前片元的世界坐标作为UV数据的初始位置: float sinT = sin(_Time.w / _Period); float2 distortion = float2( tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(sinT, 0) ).r - 0.5,tex2D(_NoiseTex, o.worldPos.xy / _Scale + float2(0, sinT) ).r - 0.5); o.uvgrab.xy += distortion * _Magnitude; 这种没有任何明显的移动方向的无缝扭曲效果确实让人看起来心情愉悦。 注意 正如所有的这些特效一样,没有完美的方案。这个知识点向你展示了创建类似水的扭曲的技术,但是我们鼓励你多对它进行试验,直到你找到一个符合你游戏美术风格的效果。

February 26, 2023 · 3 min · 473 words · Link

移动设备着色器适配

第七章 移动设备着色器适配 在接下来的两章,我们将着手于让我们写的着色器对不同的平台都有较好的性能表现。我们不会讨论任何一个特殊的平台,我们将会分解着色器内的元素,这样的话我们就可以对它们进行调整,从而让它们对于移动平台有更好的优化并且通常来说对其他任何平台来说也更高效。这些技术涵盖了从 了解Unity提供的一些可以减少着色器内存溢出方面的内建变量 到 学习可以让我们的着色器代码更加高效的方法 。这一章将会包含下面的这些知识点: 什么是低成本着色器 着色器的性能分析 针对移动设备修改着色器 介绍 学习如何优化着色器的艺术将会出现在你参与的任何游戏项目中。在任何产品中总有需要优化着色器的时候,或者需要用更少的纹理来产生相同的效果。作为一个技术美术或者着色器编程人员,你必须要理解这些核心的基本原理来优化你的着色器代码从而让你的游戏在提升性能表现的同时又能达到相同的视觉表现。有了这些知识也可以为你自己开始写着色器代码进行铺垫。比如,你知道使用你着色器的游戏将会运行在移动设备中,我们可以自动的设置所有的光照函数使用 half vector 作为视野方向,或者把所有的 浮点型变量类型(float variable types) 都设置成 fixed 类型 或 half 类型。前面提到的这些技术或者很多的其他技术,都可以让你的着色器在目标硬件上更加高效的运行。开始我们的着色器优化学习之旅吧。 什么是低成本着色器 我们首先问一个问题,什么是低成本的着色器,它回答起来可能有点困难因为有太多的元素可以可以让一个着色器变得更加高效了。它可以是你的变量使用的内存的大小。可以是你的着色器使用的纹理的大小。也可是一个工作良好的着色器,但是我们却只使用了相较之前一半的代码或者数据就获得了相同的视觉效果。我们将会在这个知识点中探索一些这样的技术并且会展示如何将这些技术结合起来从而让你的着色器更快更高效,并且不管是在移动设备还是在PC上都生成当今游戏中每个人都期望的高质量的视觉效果。 始前准备 在开始这个知识点之前,我们需要准备一些资源并且把它们放一块。所以让我们按照下面的几个任务来: 1.创建一个新的场景,并且在场景中添加一个球体和一个方向光。 2.创建一个新的着色器和材质球,并且把着色器应用到材质上。 3.然后把材质球应用到我们刚刚创建的球体。 4.最后,我们修改我们之前创建的着色器让它能使用漫反射纹理和法线贴图,并且创建一个自定义的光线函数。下面的代码展示的是修改后的着色器代码: Shader "Custom/MSA" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _NormalMap ("Normal Map", 2D) = "bump" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM sampler2D _MainTex; sampler2D _NormalMap; #pragma surface surf SimpleLambert struct Input { float2 uv_MainTex; float2 uv_NormalMap; }; inline float4 LightingSimpleLambert(SurfaceOutput s, float3 lightDir, float atten) { float diff = max(0, dot(s.Normal, lightDir)); float4 c; c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2); c.a = s.Alpha; return c; } void surf (Input IN, inout SurfaceOutput o) { // Albedo comes from a texture tinted by color fixed4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)); } ENDCG } FallBack "Diffuse" } 现在你应该有如下图所示的一个设置。下面的这个设置将让我们初步了解一些在Unity中使用表面着色器进行优化的基本概念: ...

February 26, 2023 · 2 min · 363 words · Link

实现一个玻璃效果的着色器

实现一个玻璃效果的着色器 玻璃是一个非常复杂的材质;没必要对它刚到惊讶,在第四章,向PBR中添加透明度 这个知识点中,通过行为驱动开发创建测试用例和编写场景(Creating Test Cases and Writing Scenarios for Behavior Driven Development in Symfony) 我们已经创建了一个这样的着色器来模拟它了。然而,透明度没有办法复现玻璃的扭曲效果。大部分的玻璃自身是不完美的,所以当我们再看玻璃的时候会有扭曲效果。这个知识点我们将教你如何实现这样的效果。这个效果背后的思路是使用顶点和片元着色器以及抓取通道,然后对抓取纹理做一些修改并应用到它的UV数据中,从而实现扭曲效果。你可以从下面的图中看到效果,使用的是Unity标准资源库 (Unity Standard Assets) 中的玻璃染色纹理: 始前准备 这个知识点的步骤跟前一章中的有点像: 创建一个新的顶点和片元着色器。你可以复制前一个知识点 抓取通道 的着色器作为基础。 创建一个材质,用来承载着色器。 将材质球赋值给一个 quad,也可以是其他的扁平的几何图形,用来模拟玻璃。 然后再这个模拟的玻璃后面放一些其他的游戏物体,好观察扭曲效果。 操作步骤【原书有错,下面是纠正后的步骤和代码】 我们开始编辑顶点和片元着色器: 向着色器的属性快 ( Properties block) 中添加4个属性: Properties { _MainTex("Base (RGB) Trans (A)", 2D) = "white" {} _Colour("Colour", Color) = (1,1,1,1) _BumpMap("Noise text", 2D) = "bump" {} _Magnitude("Magnitude", Range(0,1)) = 0.05 } 在Pass通道中添加下面的这些变量 sampler2D _MainTex; sampler2D _BumpMap; float _Magnitude; sampler2D _GrabTexture; fixed4 _Colour; 将下面的纹理信息添加到输入和输出结构体中: struct vertInput { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; }; struct vertOutput { float4 vertex : SV_POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; float4 uvgrab : TEXCOORD1; }; 将UV数据从输入结构体赋值到输出结构体中: vertOutput vert(vertInput input) { vertOutput o; o.vertex = UnityObjectToClipPos(input.vertex); o.color = input.color; o.texcoord = input.texcoord; o.uvgrab = ComputeGrabScreenPos(o.vertex); return o; } 使用下面的片元函数: half4 frag(vertOutput i) : COLOR { half4 mainColour = tex2D(_MainTex, i.texcoord); half4 bump = tex2D(_BumpMap, i.texcoord); half2 distortion = UnpackNormal(bump).rg; i.uvgrab.xy += distortion * _Magnitude; fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab)); return col * mainColour * _Colour; } 因为这个材质是透明的,所以我们还需要在它的 SubShader 块中改变它的 标签(tags) Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Opaque" } 接下的工作就是为玻璃设置纹理和法线贴图从而替换掉抓取纹理。 原理介绍 该着色器的核心作用是使用抓取通道来获取已经被渲染在屏幕上的东西。我们在片元函数中实现了扭曲效果的。在这里法线贴图被解析并且用来计算抓取纹理的UV数据偏移: half4 bump = tex2D(_BumpMap, i.texcoord); half2 distortion = UnpackNormal(bump).rg; i.uvgrab.xy += distortion * _Magnitude; _Magnitude 这个滑动条用来控制效果的强弱。 额外内容 这个效果非常的通用;它可以基于法线贴图,通过抓取屏幕来创建扭曲效果。如果想模拟一些更有趣的效果没理由不使用它。有很多游戏会在爆炸中或者一些科幻设备上使用扭曲效果。这个材质也可以应用到球体中,如果使用不同的法线贴图,它还可以很好的模拟爆炸中的冲击波。

February 10, 2023 · 1 min · 204 words · Link

使用抓取通道

使用抓取通道 在第四章,向PBR中添加透明度 这个知识点中,通过行为驱动开发创建测试用例和编写场景(Creating Test Cases and Writing Scenarios for Behavior Driven Development in Symfony) ,我们了解了材质是如何实现透明的。尽管一个透明材质可以在一个场景之上进行绘制,但是它不能改变在场景之下已经绘制的东西。这也意味着那些 透明着色器(Transparent Shaders) 不能创建像从玻璃或者水里看到的那些常见的扭曲效果。为了模拟它们,我们需要介绍另一种叫做 抓取通道(grab pass) 的技术。这个技术可以让我们获取到目前为止,已经绘制在屏幕上的信息,从而让我们的着色器没有限制的去使用(或者修改)它们。为了学习如何使用抓取通道,我们会创建一个材质球,来抓取它背后的渲染信息并且在屏幕上再次绘制它们。这让人感觉有点荒谬,这个材质用了一系列的操作,显示效果还是跟原来一样【作者的意思可能是在这个例子中,使用了抓取通道和没有使用的着色器,它们的显示效果是一样的】。 始前准备 这个知识点需要下面的一系列操作: 创建一个着色器,之后我们会对它进行初始化。 创建一个材质球,用来使用我们的着色器。 将材质球应用到一块扁平的几何图形上,比如Unity中的quad。然后将它放在某个物体的前面,能挡住你看后面的物体。当我们的着色器完成之后,这个quad将会变得透明。 操作步骤 为了能使用抓取通道,请你按照下面的步骤操作: 删除着色器的 属性快(Properties section) ;这个着色器将不会使用里面的任何东西。 在 SubShader 中,添加抓取通道: GrabPass{ } 在添加完抓取通道后,我们将需要添加下面这个额外的通道: Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _GrabTexture; struct vertInput { float4 vertex : POSITION; }; struct vertOutput { float4 vertex : POSITION; float4 uvgrab : TEXCOORD1; }; // Vertex function vertOutput vert(vertInput v) { vertOutput o; o.vertex = UnityObjectToClipPos(v.vertex); o.uvgrab = ComputeGrabScreenPos(o.vertex); return o; } // Fragment function half4 frag(vertOutput i) : COLOR { fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab)); return col + half4(0.5,0,0,0); } ENDCG } 原理介绍 这个知识点不仅仅介绍抓取通道同时也会介绍顶点着色器和片元着色器;因此,我们必须要分析着色器的各种细节。 到目前为止,所有的着色器代码都是直接放在 SubShader 中的。这是因为我们前面的着色器只需要一个单独的通道。但我们这次需要两个。第一个就是我们的抓取通道,我们简单的通过 GrabPass{} 定义了它。剩余的代码我们放在了第二个通道中,包含在我们的 Pass 块中。 着色器中第二个通道在结构上跟我们这一章的第一个知识点中所展示没有什么不同;我们使用顶点函数 vert 来获取顶点的位置,之后我们在片元函数 frag 中给它赋予颜色。不同的地方在于方法 vert 计算了另一个重要的细节:抓取通道的UV数据。下面的代码展示了抓取通道自动创建的一个与之相关的纹理: sampler2D _GrabTexture; 为了对纹理进行采样,我们需要它的UV数据。ComputeGrabScreenPos 函数可以的返回之后要用到的数据,这样我们就能对抓取的纹理进行正确的采样。我们可以在片元着色器中用下面这行代码来完成这个操作: fixed4 col = tex2Dproj(_GrabTexture, UNITY_PROJ_COORD(i.uvgrab)); 这是对纹理进行抓取并且把它应用到屏幕正确的位置的一种标准做法。如果每一步都操作正确,这个着色器会简单的把几何图形后面已经渲染的东西简单的克隆。我们将在接下的知识点中了解到如何使用这个技术来创建水或者玻璃这样的材质。 额外内容 当你每一次使用带有 GrabPass {} 的材质球时,Unity都会把屏幕渲染到一张纹理中。这是一个非常消耗性能的操作并且限制了你在游戏中能使用的抓取通道的数量。Cg语言提供了一个稍微不同的方式: GrabPass {"TextureName"} 这行代码不仅可以让你对纹理进行取名,并且它能让所有的抓取通道叫做 TextureName 的材质球共享同一个纹理。这意味着如果你有10个材质,Unity将仅使用一个抓取通道并且让它们共享一个纹理。这个技术的主要问题是它不允许效果的叠加。如果你使用这个技术来创建玻璃,你做不到在玻璃后面再有一块玻璃的效果。

January 2, 2023 · 1 min · 146 words · Link