通过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

片元着色器和抓取通道

第六章 片元着色器和抓取通道 到目前为止,我们都在折腾 表面着色器(Surface Shaders) 。它的设计初衷是简化我们的着色器编码工作,为艺术家提供一个有意义的工具。但是如果想让我们的着色器知识更上一层楼,我们就要前往 顶点(Vertex) 和 片元(Fragment) 着色器的知识岛屿冒险啦。 在这一章节,我们将会学习下面的一些知识点: 理解顶点和片元着色器 使用抓取通道 实现一个玻璃效果的着色器 在2D游戏中实现水效果的着色器 介绍 跟 表面着色器(Surface Shaders) 相比,顶点 和 片元 着色器少了一些诸如,光是如何在物体表面反射的物理属性信息。所谓有失必有得,这样的话顶点和片元着色器就没有了物理规则的限制并且特别适合实现非真实的效果。这个章节将集中讲抓取通道的技术,这些技术可以让着色器来模拟形变效果。 理解顶点和片元着色器 理解顶点和片元着色器最好的方法就是你自己亲自创建一个。在这个知识点我们将展示如何编写一个这样的着色器,该着色器简单的将一张纹理应用到一个模型上并且通过给定的颜色进行乘积运算,效果就如同下图一样: 这里展示的着色器非常的简单,只是作为学习其他顶点和片元着色器基础。 始前准备 对于这个知识点,我们将需要一个新的着色器。我们按照下面的步骤来: 1.创建一个新的着色器。 2.创建一个新的材质并且把着色器应用于该材质。 操作步骤 在前面的所有章节中,我们总是能在 表面着色器(Surface Shaders) 的基础上进行修改。但在这里就不能再那样做了,因为表面着色器和片元着色器在结构上是不一样的。我们需要做如下的修改: 删除着色器上的所有属性,然后用下面的属性替换: Color ("Color", Color) = (1,0,0,1) // Red _MainTex ("Base texture", 2D) = "white" {} 删除 SubShader 块中的所有代码,然后用下面的代码替换: Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag half4 _Color; sampler2D _MainTex; struct vertInput { float4 pos : POSITION; float2 texcoord : TEXCOORD0; }; struct vertOutput { float4 pos : SV_POSITION; float2 texcoord : TEXCOORD0; }; vertOutput vert(vertInput input) { vertOutput o; o.pos = UnityObjectToClipPos(input.pos); o.texcoord = input.texcoord; return o; } half4 frag(vertOutput output) : COLOR { half4 mainColour = tex2D(_MainTex, output.texcoord); return mainColour * _Color; } ENDCG } 后面所有的顶点和片元着色器都会以此为基础。 ...

December 3, 2022 · 2 min · 300 words · Link

实现范围体爆炸

实现范围体爆炸 对于实现游戏中的艺术效果,有时候需要在画质和运行效率上进行巧妙的权衡。在实现爆炸效果上尤其如此;因为它是很多游戏的核心效果,但是在它之后的一些物理计算通常都会超过现代计算机的算力。爆炸本质上就是一团温度非常高的火红气体;所以正确模拟它的唯一方式就是在游戏中用流体模拟来模拟它。正如你所想的一样,这在运行时是不可行的,在很多的游戏中都是通过粒子来模拟。当一个物体爆炸的时候,通常会同时产生很多的火花,烟雾和一些散落的碎片,这样可以获得一个比较像的爆炸。不幸的是,这种模拟方法很容易被看破而且可能不是很真实。这里我们会了解一种折中的技术来实现爆炸效果,并且画质更好:范围体爆炸(volumetric explosions) 。这个知识点背后的思考是我们不再把爆炸当作是一系列粒子的模拟;它们现在进阶到3D物体了,而不仅仅是扁平的2D贴图。 始前准备 我们通过下面的几个步骤来讲解这个知识点: 为这个效果创建一个新的着色器 创建一个新的材质,并且关联该着色器 把这个材质关联到一个球体模型上。你可以在编辑器上直接创建一个球体模型,通过菜单 GameObject | 3D Object | Sphere 。 注意 使用标准的Unity球体就可以很好的演示这个知识点,但是如果你想要更大范围的爆炸,那么你可能需要面数更多的球体。事实上,顶点函数只能修改网格的顶点。所有其他的点都可以通过修改相邻顶点的位置的方式来修改它们。顶点数越少,那么爆炸效果的精细度也就越低。 这个知识点中,你需要一个 渐变纹理(ramp texture) ,这个纹理需要有你爆炸的所有颜色梯度。你可以用GIMP或者PhotoShop工具创建一个跟下面类似的纹理: 当你有了这个图片后,把它导入到你的Unity中。然后在 检查器面板(Inspector) 中,确保 Filter Mode 设置为 Bilinear ,然后 Wrap Mode 设置为 Clamp 。这两个设置是为了确保对渐变纹理平滑采样。 最后,你还需要一张 噪音纹理(noisy texture) 。你可以在网上搜索免费的噪音纹理。一般我们都使用 Perlin noise 。【这里我自己找到一个网站http://kitfox.com/projects/perlinNoiseMaker/】 操作步骤 这个效果我们分两步来实现:通过顶点函数改变几何形状,通过表面函数给与正确的颜色。这两个步骤如下: 添加下面的属性到着色器中: _RampTex("Color Ramp", 2D) = "white" {} _RampOffset("Ramp offset", Range(-0.5,0.5))= 0 _NoiseTex("Noise tex", 2D) = "gray" {} _Period("Period", Range(0,1)) = 0.5 _Amount("_Amount", Range(0, 1.0)) = 0.1 _ClipRange("ClipRange", Range(0,1)) = 1 添加相应变量,让着色器的Cg代码可以访问到它们: sampler2D _RampTex; half _RampOffset; sampler2D _NoiseTex; float _Period; half _Amount; half _ClipRange; 修改 输入结构体(Input structure) ,这样可以让它接收渐变纹理的UV数据: struct Input { float2 uv_NoiseTex; }; 添加下面的顶点函数: void vert(inout appdata_full v) { float3 disp = tex2Dlod(_NoiseTex, float4(v.texcoord.xy,0,0)); float time = sin(_Time[3] *_Period + disp.r*10); v.vertex.xyz += v.normal * disp.r * _Amount * time; } 添加下面的的表面函数: void surf(Input IN, inout SurfaceOutput o) { float3 noise = tex2D(_NoiseTex, IN.uv_NoiseTex); float n = saturate(noise.r + _RampOffset); clip(_ClipRange - n); half4 c = tex2D(_RampTex, float2(n,0.5)); o.Albedo = c.rgb; o.Emission = c.rgb*c.a; } 我们直接通过 #pragma 来指定我们要使用的顶点函数,通过添加 nolightmap 参数来阻止Unity添加真实光照到我们的爆炸效果中: #pragma surface surf Lambert vertex:vert nolightmap 最后一步,给我们球体模型选择我们刚刚创建的材质,然后在 检查器面板(Inspector) 中,为我们的材质添加噪音纹理和渐变纹理。这是一个动画材质,也就是说会随着时间变化。你可以观察材质的变化,只要在编辑器的 场景窗口(Scene Window) 中点击 Animated Materials : 原理介绍 在学习这个知识点的时候,如果你了解 表面着色器(Surface Shaders) 和 顶点修饰(vertex modifiers) 的工作原理。这个效果背后的主要思路是以一种混乱的方式修改这个圆球几何图形的表面,然后使它看起来像真正的爆炸。下图所示是在Unity编辑器内这种爆炸看起来的样子。可以看到这个网格已经发生了明显的畸变: ...

October 23, 2022 · 2 min · 287 words · Link

实现下雪效果着色器

实现下雪效果着色器 在游戏中模拟下雪效果一直都是一件有挑战的事情。大部分的游戏都会简单的直接在模型的纹理上包含雪,让这些模型看起来雪白。然而要是其中某个模型开始旋转了呢?雪并不是敷衍了事的表面工作;它应该被当做是一些材料的合理的堆积【意思是物体表面 的雪是雪花一点一点堆积起来的,而不是简简单单的给它一张白色的贴图】。在这个知识点中将会向你展示如何用一个着色器让你的模型看起来有种下雪的样子。 要完成这个效果有两个步骤。首先,对于朝向天空的三角面我们给它白色。其次,通过挤压顶点来模拟雪的堆积效果。你可以从下图看到最终的效果: 注意 本知识点并无意去创建那种超真实的下雪效果。它只是抛砖引玉,但在你的游戏当中,最终的效果定位,还是取决于你们的艺术家们,通过他们设置正确的纹理和参数来达到你的要求。 始前准备 这个效果完全基于着色器来实现,所以请按照下面的步骤操作: 1.为雪的效果创建一个新的着色器。 2.为这个着色器创建一个新的材质。 3.把这个材质添加到你想表现雪的效果的模型上去。 操作步骤 为了创建下雪的效果,请打开你的着色器,然后做以下的修改: 1.把下面的属性块替换掉你原来的着色器属性块: _MainColor("Main Color", Color) = (1.0,1.0,1.0,1.0) _MainTex("Base (RGB)", 2D) = "white" {} _Bump("Bump", 2D) = "bump" {} _Snow("Level of snow", Range(1, -1)) = 1 _SnowColor("Color of snow", Color) = (1.0,1.0,1.0,1.0) _SnowDirection("Direction of snow", Vector) = (0,1,0) _SnowDepth("Depth of snow", Range(0,1)) = 0 2.添加与属性块对应的变量: sampler2D _MainTex; sampler2D _Bump; float _Snow; float4 _SnowColor; float4 _MainColor; float4 _SnowDirection; float _SnowDepth; 3.用下面的代码替换掉原来的 输入结构体(Input structure) struct Input { float2 uv_MainTex; float2 uv_Bump; float3 worldNormal; INTERNAL_DATA }; 4.用下面的表面函数替换掉原有的表面函数。它会让模型着雪的部分变成白色: void surf(Input IN, inout SurfaceOutputStandard o) { half4 c = tex2D(_MainTex, IN.uv_MainTex); o.Normal = UnpackNormal(tex2D(_Bump, IN.uv_Bump)); if (dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) >= _Snow) o.Albedo = _SnowColor.rgb; else o.Albedo = c.rgb * _MainColor; o.Alpha = 1; } 5.配置 #pragma 预编译指令,让我们可以使用顶点修饰: #pragma surface surf Standard vertex:vert 6.添加下面的顶点修饰,这样就可以对被雪覆盖部分的顶点进行挤压: void vert(inout appdata_full v) { float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection); if (dot(v.normal, sn.xyz) >= _Snow) v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow; } 现在你可以去你的模型材质 检查器面板(Inspector tab) 查看,然后通过调节上面的参数,你可以调节雪的覆盖面积和厚度。 ...

July 17, 2022 · 2 min · 233 words · Link