实现一个毛皮效果的着色器

实现一个毛皮效果的着色器 材质的外观取决于它的物理结构。着色器试图去模拟它们,但在那样做的过程中,它们都把光的行为方式过度的简化了。因为材质有肉眼可见的复杂结构所以渲染起来尤其的难。比如大多数的纺织布料和动物的毛皮。在这个知识点中将会展示去模拟皮毛和其他材质(比如草)的可能性,而不仅仅是限于那些平坦的的表面模拟。为了完成这些,同样的材质将会被一遍又一遍的进行多次绘制,每一次都会增加它的大小。因此创造了皮毛的假象。 这里着色器所呈现出的效果基于了 Jonathan Czeck 和 Aras Pranckevičius 的工作成果: 始前准备 为了让这个知识点能起效果,你需要准备两样东西。首先是一张皮毛的纹理因为它要呈现外观。其次是另外一张用来表示皮毛分布的纹理,并且它要跟前一张纹理高度匹配。下面的图片展示的是美洲豹的毛皮纹理(左)和它可能的控制遮罩纹理(右): 白像素的控制遮罩将从原始的材质挤压而来,模拟了一张毛皮。那些白像素分布稀疏程度非常的重要,因为它能给我们一种材料是由很多细小的毛发构成的假象。创建一张那样的纹理的简单方式如下: 1.给你的原始纹理设置一个阈值,好让原始纹理失去毛皮厚度的时候截取斑点纹理。 2.添加一个噪音过滤器让图像像素画。噪音的RGB通道一定去掉关联从而获得黑白的结果。 3.为了看起来真实一点,覆盖一个 Perlin 噪音过滤器来给毛皮添加一些变化。 4.最后,再应用一个阈值过滤器让纹理的像素更好的分离。 跟其他着色器开始前一样,你需要创建一个新的 标准着色器(standard shader) 和材质来容纳它们。 操作步骤 对于这个知识点,我们能开始修改我们的 标准着色器(standard shader) 了: 1.在着色器的 属性(Properties) 上添加下面的代码: Properties { _Color ("Main Color", Color) = (1,1,1,1) _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 _FurLength ("Fur Length", Range (.0002, 1)) = .25 _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5 // how "thick" _CutoffEnd ("Alpha cutoff end", Range(0,1)) = 0.5 // how thick they are at the end _EdgeFade ("Edge Fade", Range(0,1)) = 0.4 _Gravity ("Gravity direction", Vector) = (0,0,1,0) _GravityStrength ("G strenght", Range(0,1)) = 0.25 } 2.这个着色器要求你重复同一个通道多次。你将会使用在 使用CG包含让着色器模块化 部分介绍的技术在外部文件中的单个通道来对所有的代码进行必要的分组整理。让我们使用下面的代码开始创建一个名为 FurPass.cginc 的新CG包含文件: #pragma target 3.0 fixed4 _Color; sampler2D _MainTex; half _Glossiness; half _Metallic; uniform float _FurLength; uniform float _Cutoff; uniform float _CutoffEnd; uniform float _EdgeFade; uniform fixed3 _Gravity; uniform fixed _GravityStrength; void vert (inout appdata_full v) { fixed3 direction = lerp(v.normal, _Gravity * _GravityStrength + v.normal * (1-_GravityStrength), FUR_MULTIPLIER); v.vertex.xyz += direction * _FurLength * FUR_MULTIPLIER * v.color.a; //v.vertex.xyz += v.normal * _FurLength * FUR_MULTIPLIER * v.color.a; } struct Input { float2 uv_MainTex; float3 viewDir; }; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; //o.Alpha = step(_Cutoff, c.a); o.Alpha = step(lerp(_Cutoff,_CutoffEnd,FUR_MULTIPLIER), c.a); float alpha = 1 - (FUR_MULTIPLIER * FUR_MULTIPLIER); alpha += dot(IN.viewDir, o.Normal) - _EdgeFade; o.Alpha *= alpha; } 3.返回你开始的哪个着色器然后再 ENDCG 后面添加这个额外的通道: CGPROGRAM #pragma surface surf Standard fullforwardshadows alpha:blend vertex:vert #define FUR_MULTIPLIER 0.05 #include "FurPass.cginc" ENDCG 4.要添加更多的通道,需要逐渐增加 FUR_MULTIPLIER。你可以得到20个通道,从 0.05 到 0.95。 当着色器编译完成并且挂载到一个材质上时,你就可以从 检查器(Inspector) 上修改它的外观了。Fur Length 属性决定了毛皮外壳的厚度,也就是毛发的长度。越长的毛发可能需要更多的通道来让它看起来更真实。Alpha Cutoff 和 Alpha Cutoff End 被用于控制毛发的浓密程度和毛发的逐渐变疏程度。Edge Fade 决定了毛皮的最终透明度,让毛皮看起来有种毛茸茸的效果。软质的材料应该要有 high Edge Fade 这个属性。最后,Gravity Direction 和 Gravity Strength 可以让这层毛皮模拟重力的效果。 [原书作者这里的代码由有个严重问题,就是书上的代码跟实际能运行起来的代码有很大的差别,我在这后面贴上着色器的完整代码,译者注]。 ...

May 3, 2023 · 5 min · 907 words · Link

使用CG包含让着色器模块化

使用CG包含让着色器模块化 都说关于内建的CG包含文件很棒,但是如果我们想构建自己CG包含文件来保存我们自己的光照模型和帮助函数该怎么办呢?我们是能这样做的,事实上,在我们的着色器编写管线中如果要高效的使用自己包含文件,还需要多学一点代码的语法才能来创建自己的包含文件。不用纠结了,让我们来看看如何创建一个新的CG包含文件的过程吧。 始前准备 让我们概览一下在这个知识点中生成这些新的项的过程吧。 1.首先我们创建一个新的名为 MyCgInclude.txt 的文本文件。 2.其次将这个文件的扩展名改为 .cginc,Windows可能会给出一个该文件会不可用的警告消息,但它依然时可以正常工作。 3.将这个新的 .cginc 文件导入到Unity工程中并且让它编译。如果一切顺利,你将会看到Unity会把它编译成一个CG包含文件。 我们现在已经准备好创建我们自定义的CG包含代码了。然后简单的双击这个你创建的CG包含文件在代码编辑器中打开它。 操作步骤 打开我们的CG包含文件按之后,我们就能向其中添加代码了,好让它可以跟我们表面着色器一起工作。下面的代码可以准备好我们的CG包含文件让它可以跟表面着色器一起使用,并且随着我们开发越来越多着色器,还能对它持续的添加所需的代码: 1.开始创建我们的CG包含文件的时候我们来了解一下 预编译指令(preprocessor directive)。它们时类似于 #pragma 和 #include 这样的声明。在这个例子中,我们想定义一个新的代码设置,在这个代码设置中如果我们的着色器在它的预编译指令中包含了这个文件,那么这个代码设置将会执行。在你的CG包含文件的顶部输入下面的代码: #ifndef MY_CG_INCLUDE #define MY_CG_INCLUDE 2.我们总是需要确保对 #ifndef 或者 #ifdef 用 #endif 将前面的这些定义检测闭合[就是我们编程中常说的成对出现,译者注],就好像C#中的 if 声明一样,我们需要用两个括号将它闭合。在 #define 指令后面输入下面的的代码: #endif 3.此时,我们只需要给CG包含文件填充代码了。所以我们通过添加下面的代码来完成我们的CG包含文件: fixed4 _MyColor; inline fixed4 LightingHalfLamber (SurfaceOutput s, fixed3 lightDir, fixed atten) { fixed diff = max(0, dot(s.Normal, lightDir)); diff = (diff + 0.5)*0.5; fixed4 c; c.rgb = s.Albedo * _LightColor0.rgb * ((diff * _MyColor.rgb) * atten); c.a = s.Alpha; return c; } #endif 4.完成上面这一步之后,你现在有了自己的第一个CG包含文件了。就这点代码,我们就能大量的减少我们要编写的代码量,我们就可以开始将那些要经常使用的光照模型保存到文件中了,这样这些模型也不会丢失。你的CG包含文件看起来就跟下面的代码类似: #ifndef MY_CG_INCLUDE #define MY_CG_INCLUDE fixed4 _MyColor; inline fixed4 LightingHalfLamber (SurfaceOutput s, fixed3 lightDir, fixed atten) { fixed diff = max(0, dot(s.Normal, lightDir)); diff = (diff + 0.5)*0.5; fixed4 c; c.rgb = s.Albedo * _LightColor0.rgb * ((diff * _MyColor.rgb) * atten); c.a = s.Alpha; return c; } #endif 在我们能完全利用CG包含文件之前我们还有一些步骤需要完成。我们只需要告诉当前的着色器我们要使用这个CG包含文件以及它的代码。为了完成使用和创建CG包含文件的过程,让我们继续完成下面的步骤: 1.如果我们将注意力转移到我们的着色器上来,我们需要告诉我们的 CGPROGRAM代码块(CGPROGRAM block) 去把新的CG包含文件包含进来,好让我们可以去访问其中的代码。添加下面的代码,修改CGPROGRAM代码块的预编译指令: #include "MyCgInclude.cginc" #pragma surface surf Lambert 2.我们当前的着色器使用的是内建的 Lambert 光照模型,但是我们想让它使用我们自己CG包含文件中创建的 Half Lambert 光照模型。通过下面的代码,我们就可以引入来自我们自己的CG包含文件中的代码,就能使用其中的 Half Lambert 光照模型了: CGPROGRAM #include "MyCgInclude.cginc" #pragma surface surf HalfLamber 3.最终,我们也在CG包含文件中声明了一个自定义的变量,也就是意味着我们可以给我们使用的着色器设置默认的变量。为了了解这一操作,请在你的着色器的 属性块(Properties block) 中输入下面的代码: Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _DesatValue ("Desaturate", Range(0, 1)) = 0.5 _MyColor ("My Color", Color) = (1,1,1,1) } 4.当返回Unity编辑器,着色器和CG包含文件都将会被编译,如果你没有遇到什么错误,你就会注意到事实上我们是在使用我们自己的新 Half Lambert 光照模型并且一个新的颜色样本将会出现在材质的 检查器(Inspector) 中。下面的截屏展示了使用我们自己的CG包含文件的结果: 原理介绍 当我们使用着色器的时候,我们可以使用 #include 预编译指令将其他的代码设置包含进来。这其实是在告诉Unity我们想在着色器中让当前的着色器使用来自另一个包含进来的文件中的代码;这就是为什么这些文件被称作CG包含文件的原因。我们使用 #include 指令包含进了Cg代码的部分代码片段。 一旦我们声明了 #include 指令然后Unity就可以在项目中去搜寻这个文件了,Unity将会从已经定义了的代码中去寻找想要的代码片段。也正是在这里我们开始使用了 #ifndef 和 #endif 指令。当我们使用 #ifndef 指令的时候,我们其实在说,如果没有定义某某,那么就用这个名字定义它(if not defined, define something with a name)。在这个知识点的例子中,我们表达的是我们想 #define MY_CG_INCLUDE。所以如果Unity没有找到名为 MY_CG_INCLUDE 的定义,那么当这个CG包含文件进行编译的时候就会去创建它,因此才让我们在接下来的步骤中可以去访问那些代码。而 #endif 指令则是简单的告诉我们此乃定义的结束,因此没有必要再往下看啦。 由此你也可以看到这项技术可以变得多么的给力,因为现在我们可以将所有自己的光照模型和自定义变量都保存在这一个文件当中,并且还可以极大的减少我们的代码量。该技术的真正威力在于,在CG包含文件中为不同的功能去定义一些不同的状态,由此增加我们着色器的灵活性。

May 3, 2023 · 2 min · 218 words · Link

更高级的着色器技术

第十章 更高级的着色器技术 在本章节中,你将会学习下面的这些知识点: 使用Unity内建的 CG包含(CgInclude) 文件功能 使用CG包含让着色器模块化 实现一个毛皮效果的着色器 用数组来实现热度图 介绍 最后这一章涵盖了一些可用于游戏的高级的着色器技术。你需要记住的是很多你在游戏中看到的引人入胜的效果,都来自于对着色器技术反复打磨,力求完美的追求。这本书只是抛砖引玉罢了,教你一些修改和创建着色的知识。所以非常强烈的鼓励你竭尽所能的利用这些知识去实践和实验。制作一款好的游戏并不是一项追求超现实主义的任务;学习着色器不是为了完全模仿现实,因为这是办不到的。相反的,你应该试着把着色器当成一个工具来让你的游戏变得独一无二。有了最后这一章的知识后,你就能够创建自己想要的材质了。 使用Unity内建的CG包含文件功能 编写我们自己的 CG包含文件(CgInclude files) 的第一步就是先去了解Unity都给我们提供了哪些现成的着色器。编写 表面着色器(Surface Shaders) 的时候,其实这隐藏的背后发生了很多的事情,这让编写表面着色器的过程变得非常高效。我们是可以查看这些CG包含文件所包含的代码的,位置就在 Editor | Data | CGIncludes [编辑器的安装位置,译者注]。所有的文件都包含在这个目录下,发挥着跟我们自己的着色器一起将我们的游戏对象渲染到屏幕上的作用。这些文件中,有些负责阴影和光照,有些提供一些有用的功能,还有的负责管理跟平台相关的依赖等。如果没有它们,那么我们的着色器编写体验将会变得更加繁琐。 你可以通过下面的链接查找Unity给我们的提供的相关CG包含文件的信息列表: https://docs.unity3d.com/Manual/SL-BuiltinIncludes.html 让我们开始了解Unity内建的CG包含文件吧,从 UnityCG.cginc 这个文件中使用一些内建的帮助函数。 始前准备 在迫不及待开始编写着色器前,我们需要在场景中设置一些东西。我们需要创建下面的这些东西并且在代码编辑器打开着色器: 1.创建一个新的场景然后在场景中添加一个简单的球体。 2.创建一个新的材质和着色器。 3.将着色器挂载到材质上然后将材质应用到球体上。 4.之后,创建一个 平行光(directional light) 并且将它放到我们的球体的上面。 5.最后,我们将要去打开 UnityCG.cginc 这文件了,它在Unity的 包含文件夹(CgInclude folder) 下面,位置在Unity编辑器的安装位置。这可以让我们分析一些帮助函数的代码,好让我们能更好的理解当我们使用它们的过程中都发生了什么。 6.你应该有了一个简单的场景用来跑着色器了,就像下面的屏幕截图一样: 操作步骤 场景准备好后,我们现在就可以开始对一些包含在 UnityCG.cginc 文件中的内建帮助函数进行实验了。在Unity中双击我们之前为场景创建的着色器,这样在代码编辑器中打开它,然后在着色器中输入下面步骤提供的代码: 1.在着色器的 属性块(Properties block) 中添加下面的代码。我们需要给这个演示的着色器添加一张单独的纹理和一个滑动条: Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _DesatValue ("Desaturate", Range(0, 1)) = 0.5 } 2.之后我们需要给着色的 属性块(Properties block) 和 CGPROGRAM代码块(CGPROGRAM blocks) 建立数据链接,在 CGPROGRAM 声明和 #pragma指令(#pragma directives) 后面添加下面的代码: sampler2D _MainTex; fixed _DesatValue; 3.最后,我们要更新 surf() 函数,请使用下面的代码。我们会介绍一个我们还没有见过的新的函数,这个函数就是内建在Unity的 UnityCG.cginc 文件中的: void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex); c.rgb = lerp(c.rgb, Luminance(c.rgb), _DesatValue); o.Albedo = c.rgb; o.Alpha = c.a; } [译者:注意上面的函数中的第二行代码,如果你按照原书写的话,请把r.rgb改成c.rgb] 当代码修改好之后,那么你将会看见跟下面的截屏类似的结果。我们简单的使用了一个Unity的内建包含文件中的帮助函数,它让我们着色器的主纹理有一个 不饱和度的效果(effect of desaturating): 原理介绍 使用名为 Luminance() 的内建帮助函数,我们可以在着色器快速获得不饱和或者灰度效果。这些都是可能的,因为我们在使用表面着色器的时候 UnityCG.cginc 文件会被自动引入进我们的着色器当中。 当你在代码编辑器中打开这个 UnityCG.cginc 文件,然后搜索 Luminance 这个函数,你会在475行发现它[原文作者说在276行,新版的unity现在在475行,而且可能随时会变,所以请自己亲自打开这个文件搜索一下,译者注]。下面的代码片段就是从该文件中拿出来的: // Converts color to luminance (grayscale) inline half Luminance(half3 rgb) { return dot(rgb, unity_ColorSpaceLuminance.rgb); } 当这个函数被包含进着色器并且Unity自动编译这个着色器之后,我们同样也就可以在自己的代码中使用这个函数了,正因如此减少了非常多代码量,而这些代码我们如果不这样做可能会一遍又一遍的重复编写。 如果你有注意的话,Unity还给我们提供了一个名为 Lighting.cginc 的文件。这个文件包含了我们使用的所有的光照模型,比如我们在着色器中声明 #pragma Surface surf Lambert 类似的语句就在引用其中的一些东西。仔细筛查一遍这个文件显示,所有的光照模型都定义在这个文件内,它们可以重用和用于模块化。

May 3, 2023 · 1 min · 156 words · Link

创建一个夜视屏幕效果

创建一个夜视屏幕效果 我们的下一个屏幕效果绝对是一个更受欢迎的效果。在游戏市场中的 使命召唤现代战争(Call of Duty Modern Warfare),光环(Halo) 等第一人称游戏中都出现过。是使用一种独特的青柠色屏幕颜色,让整个图像增亮的效果。 为了获得这种夜视效果,我们需要使用Photoshop来剖析这个效果。它是这么一个简单的过程,在网上找一些相关的图片然后将这些图像组成一个层级,看看你将需要哪种混合模式,或者需要哪一种顺序来组合我们的这些层级。下图展示的是用Photoshop处理后的呈现效果: 让我们把由Photoshop组成的图像分解成不同的组成部分,好让我们可以更好的理解这些我们之后要组合的资源。在下一个知识点中,我们将涵盖这些的处理过程。 始前准备 开始制作我们这个屏幕效果,我们又要将这个效果分解成不同的组成层级。我们可以使用Photoshop创建一个效果图,这样对于创建我们的夜视效果来说,可以对如何构成这个效果提供一些灵感: 绿色着色: 我们屏幕效果的第一层是标志性的绿色,几乎在每一张夜视图像中都能找到。这将给我们的效果带来标志性的夜视的样子,就像下图所示那样: 扫描线: 为了给玩家带来一种新的显示效果,提升效果呈现,我们在着色层的上面添加扫描线。为了这个,我们将用Photoshop创建一个纹理,让用户可以对纹理进行平铺,从而可以让这些扫描线变大或者缩小。 噪音图: 我们的下一层级是一张简单的噪音纹理,我们将会把它平铺在平铺后的着色图像和扫描线的上面,然后将图像打散并且给我们的效果添加更多的细节。这一层简单的强调了 数字读出外观(digital read-out look) :[各位觉得听起来拗口,可以去查一下DRO的概念,我这里贴一个相关的百科] 渐晕纹理: 我们的夜视效果的最后一个层级是渐晕。如果看过使命召唤现代战争中的夜视效果,你就会注意到它使用了一个渐晕来仿造一个从单筒望眼镜看出来的效果。我们将在这个屏幕效果中也做同样的事情: 让我们聚集需要的这些纹理,开始创建我们的屏幕效果。请按照下面的步骤进行: 1.收集一张渐晕纹理,一张噪音纹理和一张扫描线纹理,就跟我们上面看到哪些那样的就行。 2.创建一个名为 NightVisionEffect.cs 的脚本和一个名为 NightVisionEffectShader.shader 的着色器。 3.创建好这些代码文件后,给这些文件添加必要的代码好让屏幕效果系统可以设置好并且正常运行。至于该如何做的操作步骤,可以参考 第八章, 通过Unity渲染纹理实现屏幕效果。 最终,随着我们的屏幕效果系统设置好并且顺利运行,以及收集好所需的纹理后,我们就可以开始创建夜视效果的过程了。 操作步骤 当我们把所有的资源收集好并且让屏幕效果系统流畅的跑起来后,让我们开始给脚本和着色器添加一些必要的代码。我们将首先给 NightVisionEffect.cs 脚本添加代码,双击脚本在代码编辑器中打开这个文件。 1.我们先要给脚本添加一些变量,这样可以让这个效果的用户在 检查器面板(Inspector) 中去调整这些变量。在 NightVisionEffect.cs 脚本中输入下面的代码: #region Variables public Shader nightVisionShader; public float constrast = 2.0f; public float brightness = 1.0f; public Color nightVisionColor = Color.white; public Texture2D vignetteTexture; public Texture2D scanLineTexture; public float scanLineTileAmount = 4.0f; public Texture2D nightVisionNoise; public float noiseXSpeed = 100.0f; public float noiseYSpeed = 100.0f; public float distortion = 0.2f; public float scale = 0.8f; private Material curMaterial; private float randomValue; #endregion 2.接下来,我们需要完成 OnRenderImage() 方法,让我们可以传递正确的数据给着色器,这样着色器才能正确的处理屏幕效果。通过下面的代码来完成 OnRenderImage() 方法调整: private void OnRenderImage(RenderTexture src, RenderTexture dest) { if(nightVisionShader != null) { material.SetFloat("_Contrast", constrast); material.SetFloat("_Brightness", brightness); material.SetColor("_NightVisionColor", nightVisionColor); material.SetFloat("_RandomValue", randomValue); material.SetFloat("_distortion", distortion); material.SetFloat("_scale",scale); if(vignetteTexture) { material.SetTexture("_VignetteTex", vignetteTexture); } if(scanLineTexture) { material.SetTexture("_ScratchesTex", scanLineTexture); material.SetFloat("_ScanLineTileAmount", scanLineTileAmount); } if(nightVisionNoise) { material.SetTexture("_NoiseTex", nightVisionNoise); material.SetFloat("_NoiseXSpeed", noiseXSpeed); material.SetFloat("_NoiseYSpeed", noiseYSpeed); } Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } } 3.为了完成 NightVisionEffect.cs 脚本,我们只需要确保这些具体的变量被限制在一个合理的范围。这个范围是随意的可以在之后修改。这些是可以工作的很好的一些值: private void Update() { constrast = Mathf.Clamp(constrast, 0f, 4f); brightness = Mathf.Clamp(brightness, 0f, 2f); randomValue = Random.Range(-1f, 1f); distortion = Mathf.Clamp(distortion, -1f, 1f); scale = Mathf.Clamp(scale, 0f, 3f); } 4.现在我们能把注意力转移到屏幕效果的着色器部分上来了。如果你还没有打开着色器代码,用代码编辑器打开着色器,然后再着色器的 属性块(Properties block) 中输入下面的代码: Properties { MainTex ("Base (RGB)", 2D) = "white" {} _VignetteTex ("Vignette Texture", 2D) = "white"{} _ScanLineTex ("Scan Line Texture", 2D) = "white"{} _NoiseTex ("Noise Texture", 2D) = "white"{} _NoiseXSpeed ("Noise X Speed", Float) = 100.0 _NoiseYSpeed ("Noise Y Speed", Float) = 100.0 _ScanLineTileAmount ("Scan Line Tile Amount", Float) = 4.0 _NightVisionColor ("Night Vision Color", Color) = (1,1,1,1) _Contrast ("Contrast", Range(0,4)) = 2 _Brightness ("Brightness", Range(0,2)) = 1 _RandomValue ("Random Value", Float) = 0 _distortion ("Distortion", Float) = 0.2 _scale ("Scale (Zoom)", Float) = 0.8 } 5.为了确保数据可以从 属性块(Properties block) 传递给 CGPROGRAM代码块(CGPROGRAM block),我们需要在CGPROGRAM代码块声明与属性块中相同名字的变量: CGPROGRAM #pragma vertex vert_img #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" uniform sampler2D _MainTex; uniform sampler2D _VignetteTex; uniform sampler2D _ScanLineTex; uniform sampler2D _NoiseTex; fixed4 _NightVisionColor; fixed _Contrast; fixed _ScanLineTileAmount; fixed _Brightness; fixed _RandomValue; fixed _NoiseXSpeed; fixed _NoiseYSpeed; fixed _distortion; fixed _scale; 6.我们的效果还包括一个透镜畸变,用来进一步的表达透过镜头来观察的效果,并且图像的边缘由于镜头的角度而发生了扭曲。在CGPROGRAM代码块中变量的声明后添加下面这个函数的代码: float2 barrelDistortion(float2 coord) { // 镜头畸变算法 // 详情 http://www.ssontech.com/content/lensalg.htm float2 h = coord.xy - float2(0.5, 0.5); float r2 = h.x * h.x + h.y * h.y; float f = 1.0 + r2 * (_distortion * sqrt(r2)); return f * _scale * h + 0.5; } 7.我们现在可以专心于 NightVisionEffect 着色器了。给着色器添加一些必要的代码,用于获取渲染纹理和渐晕纹理。在我们着色器中的 frag() 函数中添加下面的代码: fixed4 frag(v2f_img i) : COLOR { //获得渲染纹理的颜色并且获取 v2f_img的uv half2 distortedUV = barrelDistortion(i.uv); fixed4 renderTex = tex2D(_MainTex, distortedUV); fixed4 vignetteTex = tex2D(_VignetteTex, i.uv); 8.我们 frag() 函数下一步要处理是扫描线和噪音纹理,并且将一些有合理动画的UV应用到它们身上: //处理扫描线和噪音纹理 half2 scanLinesUV = half2(i.uv.x * _ScanLineTileAmount, i.uv.y * _ScanLineTileAmount); fixed4 scanLineTex = tex2D(_ScanLineTex, scanLinesUV); half2 noiseUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _NoiseXSpeed), i.uv.y + (_Time.x * _NoiseYSpeed)); fixed4 noiseTex = tex2D(_NoiseTex, noiseUV); 9.为了完成我们的屏幕效果中的所有层级,我们只需要简单的处理渲染纹理的亮度值,然后将夜视颜色应用到它身上从而获得典型的夜视效果外观: //利用YIQ值从渲染纹理中获得亮度值 fixed lum = dot(fixed3(0.299, 0.587, 0.114), renderTex.rgb); lum += _Brightness; fixed4 finalColor = (lum * 2) + _NightVisionColor; 10.最后,我们将所有的层级合到一块,然后返回我们夜视效果的最终颜色: //最后的颜色输出 finalColor = pow(finalColor, _Contrast); finalColor *= vignetteTex; finalColor *= scanLineTex * noiseTex; return finalColor; 当所有的代码都完成后,返回Unity编辑器让脚本和着色器编译。如果没有遇到错误,在编辑器中点击运行然后观察结果。你将会看到跟下图类似的结果: ...

May 2, 2023 · 3 min · 559 words · Link

游戏和屏幕效果

第九章 游戏和屏幕效果 当我们要创建可信和沉浸的游戏的时候,我们要考虑的不仅仅只有材料。屏幕效果也会改变游戏的整体感觉。这在电影里面非常常见,比如后期制作阶段中的调色。使用 第八章 通过Unity渲染纹理实现屏幕效果 中学到的知识,你也可以在游戏中实现这些技术。在这一章将会呈现两个有趣的效果;当然,你可以适当修改它们以适用于你的需求,也可以创建完全属于你自己屏幕效果。 在这一章,你将会学到下面的这些知识点: 创建一个老电影屏幕效果 创建一个夜视屏幕效果 介绍 如果你正在阅读这本书,你很可能玩过一两个游戏。即时游戏一方面会使玩家进入一个沉浸世界,让人觉得他们好像在现实世界玩游戏一样。现代的游戏利用的屏幕效果越多获得的沉浸感也越多。 通过屏幕效果,我们可以将在某个确切环境中的心境从平静转为惊恐,仅仅只要改变屏幕看起来的样子。想象一下走进了某个关卡中的房间,然后游戏突然接管并且将你带进一个电影时刻。很多现代游戏都会使用不同的屏幕效果来改变不同时刻的一个心境。理解如何创建在游戏中使用的效果是我们学习编写着色器的下一个旅程。 在这一章,我们将了解一些更加常用的游戏中的屏幕效果。你将会学习如何改变游戏的样子,把它从正常的样子改成一个老电影效果的样子,并且我们还会去了解大多数 FPS(first-person shooter第一人称射击) 游戏是如何使他们的夜视效果呈现在屏幕中的。通过这些知识点,我们将了解如何将这些效果跟游戏中的事件关联起来,好让游戏根据当前演出的需要去打开或者关闭这些特效。 创建一个老电影屏幕效果 很多游戏背景设定在不同的时期。有些发生在幻想世界或者科幻世界,更有甚者发生在旧西部,那个时候电影摄像机才刚刚发明并且人们看到的都是一些黑白电影或者棕褐色效果色调的电影。它们看起来格外不同,我们将在Unity中用屏幕效果来复制这种看起来的样子。 获得这种效果需要一些步骤,如果要将整个屏幕变成黑或白或灰,我们需要将这个效果分解成不同的组成部分。如果我们分析一些相关的老电影的镜头,我们就可以开始做这个了。让我们来看看下面这张图片并且分解其中的元素,看看是那些构成了这个老电影的样子: 我们用一些在网上找到的图片构建了这个图片。像这样尝试利用Photoshop来构建图片总是一个很好的主意,它能为你的新的屏幕特效打好一个计划。在这个过程中它不仅能将我们需要用代码编写的元素告诉我们,还提供了一个快捷的方式让我们了解我们的屏幕效果需要使用哪一种混合模式和我们将要构建那些层级。本书这个知识点中我们为Photoshop创建的这些文件的支持网站在http://www.packtpub.com/support[已经失效(译者注)]。它是一个名为 OldFilmEffect_Research_Layout.psd 的文件。 始前准备 我们现在目的明确,让我们看看最终效果的每一层都包含了什么然后为我们的着色器和C#脚本收集一些资源。 复古色调(Sepia tone): 这是一个相对容易获得的效果,我们只需把原始渲染纹理的所有像素颜色变为一个单一的颜色范围。使用原始图像的亮度然后加上一个常量颜色就可以很容易获得。我们的第一层将会看起来跟下面的图片一样: 渐晕效果(Vignette effect): 当一些古老的电影放映机放映老电影的时候,我们经常看到某种软边界[ 我个人觉得翻译的不够准确 ]围绕在老电影的四周。这是因为这种电影放映机使用的球形灯泡发出的光中间部位比周围要亮造成的。这种效果通常叫做渐晕效果并且是我们屏幕效果的第二层。我们可以通过在整个屏幕上覆盖一张纹理来获得这个效果。下面的图片演示了这一层看起来的样子,就是一张纹理: 灰尘和划痕(Dust and scratches): 第三层也是最后一层就是我们的老电影屏幕效果中的灰尘和划痕。这一层将使用两种不同的平铺纹理,一种作为划痕然后另一种作为灰尘。原因就是我们将根据时间以不同的速度对这两种纹理做动画。这将会产生一种效果,就是当电影在播放的时候同时在老电影的每一帧中都会有一些细小的划痕和灰尘。下图演示了它们的纹理看起来效果: 我们用前面的那些纹理来准备好我们的 屏幕效果系统(screen effect system)。按照下面的步骤来: 1.将 渐晕纹理(vignette texture) 和 灰尘划痕纹理(dust and scratches texture) 收集起来,就像我们前面看到的那几张。 2.创建一个名为 OldFilmEffect.cs 的新脚本和一个名为 OldFilmEffectShader.shader 的新着色器。 3.创建好这些新的文件后,给它们编写需要的代码来完成我们的屏幕效果系统并且顺利的跑起来。想知道具体如何做,可以参考 第八章 通过Unity渲染纹理实现屏幕效果 来了解。 最后,随着我们的屏幕效果系统完成并且顺利跑起来,以及收集好了我们的纹理,我们就可以开始复现老电影效果的制作过程了。 操作步骤 老电影屏幕效果的那些独特层级都很简单,但是将它们组合之后我们就能获得令人震惊的视觉效果。让我们缕一缕该怎么构建我们的脚本和着色器,之后我们就能逐行解析并且学习为什么可以那样写。此时,你应该有个设置好的屏幕效果系统而且能顺利运行,因为我们这个知识点不会涵盖如何设置这个系统。 1.我们将添加脚本代码。我们要输入的第一个代码块将定义我们的变量,这些变量会在 检查器 (Inspector) 上显示,好让这个效果的使用者可以可以用想填的数据修改它们。如我们还想在检查器上显示我们效果用到的那些我们处理好的Photoshop文件,也可以在此添加它们的引用。在脚本中添加下面的代码: #region Variables public Shader oldFilmShader; public float OldFilmEffectAmount = 1.0f; public float contrast = 3.0f; public float distortion = 0.2f; public float cubicDistortion = 0.6f; public float scale = 0.8f; public Color sepiaColor = Color.white; public Texture2D vignetteTexture; public float vignetteAmount = 1.0f; public Texture2D scratchesTexture; public float scratchesYSpeed = 10.0f; public float scratchesXSpeed = 10.0f; public Texture2D dustTexture; public float dustYSpeed = 10.0f; public float dustXSpeed = 10.0f; private Material curMaterial; private float randomValue; #endregion 2.接下来,我们需要修改 OnRenderImage() 方法中的内容了。在这里,我们将把脚本中变量的数据传给着着色器好让着色器在处理渲染纹理的时候可以使用这些数据: private void OnRenderImage(RenderTexture src, RenderTexture dest) { if(oldFilmShader != null) { material.SetColor("_SepiaColor", sepiaColor); material.SetFloat("_VignetteAmount", vignetteAmount); material.SetFloat("_EffectAmount", OldFilmEffectAmount); material.SetFloat("_Contrast", contrast); material.SetFloat("_cubicDistortion", cubicDistortion); material.SetFloat("_distortion", distortion); material.SetFloat("_scale",scale); if(vignetteTexture) { material.SetTexture("_VignetteTex", vignetteTexture); } if(scratchesTexture) { material.SetTexture("_ScratchesTex", scratchesTexture); material.SetFloat("_ScratchesYSpeed", scratchesYSpeed); material.SetFloat("_ScratchesXSpeed", scratchesXSpeed); } if(dustTexture) { material.SetTexture("_DustTex", dustTexture); material.SetFloat("_dustYSpeed", dustYSpeed); material.SetFloat("_dustXSpeed", dustXSpeed); material.SetFloat("_RandomValue", randomValue); } Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } } 3.为了完成特效的脚本部分,接下来只要确保把变量值控制在合理的范围而不是任意值就可以了。 private void Update() { vignetteAmount = Mathf.Clamp01(vignetteAmount); OldFilmEffectAmount = Mathf.Clamp(OldFilmEffectAmount, 0f, 1.5f); randomValue = Random.Range(-1f,1f); contrast = Mathf.Clamp(contrast, 0f, 4f); distortion = Mathf.Clamp(distortion, -1f,1f); cubicDistortion = Mathf.Clamp(cubicDistortion, -1f, 1f); scale = Mathf.Clamp(scale, 0f, 1f); } 注意 作者这里只贴了关键代码,下面是完整的脚本代码[译者注]: using UnityEngine; [ExecuteInEditMode] public class OldFilmEffect : MonoBehaviour { #region Variables public Shader oldFilmShader; public float OldFilmEffectAmount = 1.0f; public float contrast = 3.0f; public float distortion = 0.2f; public float cubicDistortion = 0.6f; public float scale = 0.8f; public Color sepiaColor = Color.white; public Texture2D vignetteTexture; public float vignetteAmount = 1.0f; public Texture2D scratchesTexture; public float scratchesYSpeed = 10.0f; public float scratchesXSpeed = 10.0f; public Texture2D dustTexture; public float dustYSpeed = 10.0f; public float dustXSpeed = 10.0f; private Material curMaterial; private float randomValue; #endregion #region Properties Material material { get { if(curMaterial == null) { curMaterial = new Material(oldFilmShader); curMaterial.hideFlags = HideFlags.HideAndDontSave; } return curMaterial; } } #endregion private void Start() { if (!SystemInfo.supportsImageEffects) { enabled = false; return; } if (oldFilmShader && !oldFilmShader.isSupported) { enabled = false; } } private void OnRenderImage(RenderTexture src, RenderTexture dest) { if(oldFilmShader != null) { material.SetColor("_SepiaColor", sepiaColor); material.SetFloat("_VignetteAmount", vignetteAmount); material.SetFloat("_EffectAmount", OldFilmEffectAmount); material.SetFloat("_Contrast", contrast); material.SetFloat("_cubicDistortion", cubicDistortion); material.SetFloat("_distortion", distortion); material.SetFloat("_scale",scale); if(vignetteTexture) { material.SetTexture("_VignetteTex", vignetteTexture); } if(scratchesTexture) { material.SetTexture("_ScratchesTex", scratchesTexture); material.SetFloat("_ScratchesYSpeed", scratchesYSpeed); material.SetFloat("_ScratchesXSpeed", scratchesXSpeed); } if(dustTexture) { material.SetTexture("_DustTex", dustTexture); material.SetFloat("_dustYSpeed", dustYSpeed); material.SetFloat("_dustXSpeed", dustXSpeed); material.SetFloat("_RandomValue", randomValue); } Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } } private void Update() { vignetteAmount = Mathf.Clamp01(vignetteAmount); OldFilmEffectAmount = Mathf.Clamp(OldFilmEffectAmount, 0f, 1.5f); randomValue = Random.Range(-1f,1f); contrast = Mathf.Clamp(contrast, 0f, 4f); distortion = Mathf.Clamp(distortion, -1f,1f); cubicDistortion = Mathf.Clamp(cubicDistortion, -1f, 1f); scale = Mathf.Clamp(scale, 0f, 1f); } private void OnDisable() { if (curMaterial) { DestroyImmediate(curMaterial); } } } 4.当我们的脚本完成之后,注意力转移到着色器上来。我们要在着色器上也创建跟脚本变量对应的着色器变量。这样的话可以让脚本跟着色器相互通信。在着色器的 属性块(Properties block) 中添加下面的代码: Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _VignetteTex ("Vignette Texture", 2D) = "white"{} _ScratchesTex ("Scartches Texture", 2D) = "white"{} _DustTex ("Dust Texture", 2D) = "white"{} _SepiaColor ("Sepia Color", Color) = (1,1,1,1) _EffectAmount ("Old Film Effect Amount", Range(0,1)) = 1.0 _VignetteAmount ("Vignette Opacity", Range(0,1)) = 1.0 _ScratchesYSpeed ("Scratches Y Speed", Float) = 10.0 _ScratchesXSpeed ("Scratches X Speed", Float) = 10.0 _dustXSpeed ("Dust X Speed", Float) = 10.0 _dustYSpeed ("Dust Y Speed", Float) = 10.0 _RandomValue ("Random Value", Float) = 1.0 _Contrast ("Contrast", Float) = 3.0 _distortion ("Distortion", Float) = 0.2 _cubicDistortion ("Cubic Distortion", Float) = 0.6 _scale ("Scale (Zoom)", Float) = 0.8 } 5.接下来,跟往常一样,接下来在 CGPROGRAM 块中添加与属性块对应的变量好让属性块可以跟 CGPROGRAM 块通信: uniform sampler2D _MainTex; uniform sampler2D _VignetteTex; uniform sampler2D _ScratchesTex; uniform sampler2D _DustTex; fixed4 _SepiaColor; fixed _VignetteAmount; fixed _ScratchesYSpeed; fixed _ScratchesXSpeed; fixed _dustXSpeed; fixed _dustYSpeed; fixed _EffectAmount; fixed _RandomValue; fixed _Contrast; float _distortion; float _cubicDistortion; float _scale; 6.现在,我们简单的将 frag() 函数内容修改一下好让我们能处理屏幕效果上的像素。让我们获取来自脚本的 渲染纹理(render texture) 和 渐晕纹理(vignette texture): fixed4 frag(v2f_img i) : COLOR { //获得渲染纹理的颜色并且获取 v2f_img的uv half2 distortedUV = barrelDistortion(i.uv); distortedUV = half2(i.uv.x, i.uv.y + (_RandomValue * _SinTime.z * 0.005)); fixed4 renderTex = tex2D(_MainTex, i.uv); //获取渐晕纹理的像素 fixed4 vignetteTex = tex2D(_VignetteTex, i.uv); 7.我们接下来添加处理划痕和灰尘的代码: //处理划痕的UV和像素 half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed), i.uv.y + (_Time.x * _ScratchesYSpeed)); fixed4 scratchesTex = tex2D(_ScratchesTex, scratchesUV); //处理灰尘的UV和像素 half2 dustUV = half2(i.uv.x + (_RandomValue * (_SinTime.z * _dustXSpeed)), i.uv.y + (_RandomValue * (_SinTime.z * _dustYSpeed))); fixed4 dustTex = tex2D(_DustTex, dustUV); 8.接下来处理深褐色色调: //使用YIQ值从渲染纹理中获取亮度值。 fixed lum = dot(fixed3(0.299, 0.587, 0.114), renderTex.rgb); //给亮度值加上一个常量颜色 fixed4 finalColor = lum + lerp(_SepiaColor, _SepiaColor + fixed4(0.1f, 0.1f, 0.1f, 1.0f), _RandomValue); finalColor = pow(finalColor, _Contrast); 9.最后,我们将所有的层和颜色都叠加到一块并将最终的屏幕效果纹理返回: //创建一个白色的常量颜色,这样我们可以调节效果的不透明度。 fixed3 constantWhite = fixed3(1,1,1); //将不同的层混合到一起来创建最终的屏幕效果 finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount); finalColor.rgb *= lerp(scratchesTex, constantWhite, (_RandomValue)); finalColor.rgb *= lerp(dustTex.rgb, constantWhite, (_RandomValue * _SinTime.z)); finalColor = lerp(renderTex, finalColor, _EffectAmount); return finalColor; } 10.当我们把所有代码都完成并且没有遇到错误,你应该有个跟下图很相似的结果。在编辑器中点击运行按钮,就可以看到灰尘效果和划痕效果,而且可以看到屏幕效果上的轻微的图片位移: 注意 作者少说了一个函数,将下面的 barrelDistortion 函数添加到 frag() 函数上面[译者注]: float2 barrelDistortion(float2 coord) { // Inspired by SynthEyes lens distortion algorithm // See http://www.ssontech.com/content/lensalg.htm float2 h = coord.xy - float2(0.5, 0.5); float r2 = h.x * h.x + h.y * h.y; float f = 1.0 + r2 * (_distortion + _cubicDistortion * sqrt(r2)); return f * _scale * h + 0.5; } 原理介绍 现在让我们来逐步分析这个屏幕效果的每一层,剖析每一行代码的工作原理,以及对于这个屏幕效果我们该如何丰富它这方面获得更多的简洁。 ...

April 20, 2023 · 5 min · 1033 words · Link

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

屏幕效果中的覆盖混合模式 对于我们最后要讲的知识点,我们将会去了解另一种混合模式,覆盖混合模式。这种模式实际上是利用了一些条件声明,这些条件声明决定了每个通道上的每个像素的最终颜色。所以,在使用这种混合模式的过程中需要编写的代码会更多一些。接下来我们看看该如何实现它。 始前准备 对于最后这个屏幕特效,我们需要像前面两个知识点中那样设置两个脚本(一个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

linux下自动连接openvpn的bash

Linux下的openvpn自动连接bash,自动尝试指定目录下的所有.ovpn文件 脚本如下所示: #!/usr/bin/env bash # 存放所有ovpn文件的目录 ovpn_dir="/media/link/D/config/vpn/config/" # 存放用户认证信息的文件 auth_file="/media/link/D/config/vpn/auth.txt" # 存放临时日志的文件 log_file="/media/link/D/config/vpn/temp.log" process_name=openvpn # 遍历ovpn目录下的所有ovpn文件 for file in ${ovpn_dir}/*.ovpn; do echo "Trying ${file}..." # 删除旧的日志文件并创建新的 rm -f ${log_file} && touch ${log_file} # 连接VPN并将所有日志重定向到临时日志文件 sudo openvpn --config ${file} --auth-user-pass ${auth_file} --connect-timeout 10 --connect-retry-max 3 > ${log_file} & while true ; do for line in $(cat ${log_file});do if echo "${line}" | grep -q "Initialization Sequence Completed"; then echo "connect success!" while true; do read -p "Input 'stop' to exit" input if [ "${input}" == "stop" ]; then echo "Exiting..." exit 0 fi done fi done sleep 0.2 process_id=$(pidof openvpn) if [[ -z $process_id ]]; then echo "openvpn exited! try next config!" break fi done # 如果连VPN接失败,则提示connect fail! echo "connect fail!" done 这个教程使用的openvpn这个客户端,如果没有这个命令,请先安装openvpn,openvpn的安装方法,请去网上查找。 ...

March 28, 2023 · 1 min · 121 words · Link