Unity 5.x Shaders and Effects Cookbook中文版翻译(第二版)

​ 我打算试着翻译这本技术书,目的又两个,1.希望自己能帮助英文不太好的朋友,2.希望自己也学到这些知识,顺便帮助自己提升英语水平。我英语水平不是很好,接下来如果有什么错误的地方,有看到的朋友还请帮忙纠正。我不会web前端技术,我想试着学学markdown语法,尽量让页面好看些但是最重要的还是内容。 Unity 5.x Shaders and Effects Cookbook中文版(第二版) 目录表 鸣谢 关于作者 www.PacktPub.com 电子书, 优惠, 还有其他 为什么需要订阅? 前言 这本书包含哪些内容 学习的过程中你需要准备的 本书的适合人群 内容结构 始前准备 操作步骤 原理介绍 额外内容 相关补充 本书的一些文体说明 读者反馈 客户支持 示例代码下载 本书一些彩图的下载 勘误表 盗版声明 本书有问题请联系 1.创建你的第一个着色器 介绍 创建一个基本的标准着色器 始前准备 操作步骤 原理介绍 相关补充 如何把Unity 4的旧着色器迁移至Unity 5 始前准备 操作步骤 着色器版本的自动升级 使用标准着色器 迁移用户自定义的着色器 原理介绍 相关补充 给着色器添加属性 始前准备 操作步骤 原理介绍 相关补充 使用表面着色器的属性 操作步骤 原理介绍 额外内容 相关补充 2.表面着色器和纹理贴图 介绍 漫反射的着色处理 始前准备 操作步骤 原理介绍 使用包组 操作步骤 压缩矩阵 相关补充 向着色器添加纹理 ...

June 26, 2023 · 2 min · 282 words · Link

用数组来实现热度图

用数组来实现热度图 着色器难以掌握的一个典型的原因就是缺少合适的文档。许多开发者在学习着色器的时候被代码搞得一团糟,原因就是他们没有很深的知识来解释眼前到底发生了什么。事实上 Cg/HLSL 中有着大量的臆断,这些臆断又没有被正确的证明过,这就让问题变得更加的突出。Unity3d允许C#脚本使用诸如 SetFloat ,SetInt ,SetVector 等之类的方法来跟着色器通信。遗憾的是,Unity3D没有类似 SetArray 的方法,也正因如此导致很多开发者们以为 Cg/HLSL 不支持 数组(arrays)。但事实并非如此,这篇文章将会给你展示向着色器传递数组的可能性。需要注意的是GPU为并行计算进行了高度的优化,所以如果在着色器中使用 循环结构(loops) 将会极大的降低它的性能。 在这个知识点中,我们将会实现一个热度图,看起来大概跟下图一样: 始前准备 这个知识点中介绍的效果是从一些设置好的点中创建一个热度图。这个热度图将会覆盖在另外一张图片的上面,就像一张前置图片。下面是必要的步骤: 1.在Unity中使用一张纹理创建一个 quad,这张纹理就是用来创建热度图的那张纹理。在我们的例子中,使用了一张伦敦地图的纹理。 2.创建另外一个 quad,并且把它放在前面创建的那个quad的上面。我们的热度图将会在这个quad上产生。 3.创建一个对应的材质,并且把材质和着色器都应用到第二个quad上。 操作步骤 这个着色器跟我们之前创建的着色器还是有很大不同,当然它也相对较短。因此,下面的步骤中直接提供了着色器完整代码: 1.将下面的的着色器代码整个复制到新创建的着色器中: // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' shader "Custom/Heatmap" { Properties { _HeatTex ("Texture", 2D) = "white" {} } Subshader { Tags {"Queue"="Transparent"} Blend SrcAlpha OneMinusSrcAlpha // Alpha blend Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag struct vertInput { float4 pos : POSITION; }; struct vertOutput { float4 pos : POSITION; fixed3 worldPos : TEXCOORD1; }; vertOutput vert(vertInput input) { vertOutput o; o.pos = UnityObjectToClipPos(input.pos); o.worldPos = mul(unity_ObjectToWorld, input.pos).xyz; return o; } uniform int _Points_Length = 0; uniform float3 _Points [20]; // (x, y, z) = position uniform float2 _Properties [20]; // x = radius, y = intensity sampler2D _HeatTex; half4 frag(vertOutput output) : COLOR { // Loops over all the points half h = 0; for (int i = 0; i < _Points_Length; i ++) { // Calculates the contribution of each point half di = distance(output.worldPos, _Points[i].xyz); half ri = _Properties[i].x; half hi = 1 - saturate(di / ri); h += hi * _Properties[i].y; } // Converts (0-1) according to the heat texture h = saturate(h); half4 color = tex2D(_HeatTex, fixed2(h, 0.5)); return color; } ENDCG } } Fallback "Diffuse" } 2.当你把这个着色器挂载到对应的材质球上的时候,你还需要为热度图提供一张渐变纹理。重要的是记得把这张纹理的 Wrap Mode 设置为 Clamp。下图中的纹理就是我们这个例子中使用的渐变纹理: 注意 如果你的热度图要作为一个覆盖层,那么还要确保这张渐变纹理有一个alpha通道并且导入图片设置那里记得勾选 Alpha is Transparency 。 3.创建一个名为 Heatmaps 的C#脚本,并输入下面的代码: using System.Collections; using UnityEngine; public class Heatmaps : MonoBehaviour { public Vector3[] positions; public float[] radiuses; public float[] intensities; public Material material; void Start() { material.SetInt("_Points_Length", positions.Length); for (int i = 0; i < positions.Length; i++) { material.SetVector("_Points"+ i, positions[i]); Vector2 properties = new Vector2(radiuses[i], intensities[i]); material.SetVector("_Properties" + i, properties); } } void Update() { } } 4.将这个脚本挂载到场景中的一个游戏对象上,最好是你之前创建的quad。然后将为这个效果创建的材质球拖拽到脚本中的 材质变量(material slot) 上。做完这些后,C#脚本就能访问材质球了并且会对它进行初始化。 5.最后,在C#脚本上,展开 positions,radiuses 和 intensities 这些数组变量,给它们填入热度图需要的参数值。positions 指的是热度图的位置点信息(世界坐标),radiuses 指的是热度图的半径大小,intensities 指的是热度图周围的强烈程度: 原理介绍 这个着色器的实现依赖了我们本书之前从来没有介绍的一些东西;首先第一个就是数组。在Cg语言中可以通过下面的语法创建数组: uniform float3 _Points [20]; Cg不支持未知大小的数组:你必须要预先知道你要分配的空间大小。前面那行代码创建了一个20个元素的数组。 Unity并没有直接暴露出任何能初始化那些数组的方法。然而,里面的单个元素却可以用数组的名称 (_Points) 加元素位置的方式去访问,比如 _Points0 或者 _Points10 这样去访问。但是这种方式只对确切类型的数组有效,比如 float3 类型和 float2 类型。我们把这个C#脚本挂载在了一个quad上,然后对着色器中的数组元素逐个进行了初始化。 在着色器的片元函数中,每个循环结构中相似的事情是,对材质上的每个像素,求所有的点对热度图的影响程度: // Loops over all the points half h = 0; for (int i = 0; i < _Points_Length; i ++) { // Calculates the contribution of each point half di = distance(output.worldPos, _Points[i].xyz); half ri = _Properties[i].x; half hi = 1 - saturate(di / ri); h += hi * _Properties[i].y; } 变量 h 保存了来自所有点的热度,它是根据半径和强度计算出来的。之后它就被用于查看到底该用渐变纹理上的哪些颜色。 着色器和数组其实是一个很好的组合,但是却很少有游戏能在使用时发挥出它们的全部潜力。然而在这里,它们却引来了一个很大的瓶颈,因为对于每一个像素来说,着色器都必须要去遍历完所有的点。

May 4, 2023 · 2 min · 375 words · Link

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

实现一个毛皮效果的着色器 材质的外观取决于它的物理结构。着色器试图去模拟它们,但在那样做的过程中,它们都把光的行为方式过度的简化了。因为材质有肉眼可见的复杂结构所以渲染起来尤其的难。比如大多数的纺织布料和动物的毛皮。在这个知识点中将会展示去模拟皮毛和其他材质(比如草)的可能性,而不仅仅是限于那些平坦的的表面模拟。为了完成这些,同样的材质将会被一遍又一遍的进行多次绘制,每一次都会增加它的大小。因此创造了皮毛的假象。 这里着色器所呈现出的效果基于了 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