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

Unity的Addressables使用体验

之前大致的了解了Addressalbe这个插件的大致功能和原理,并没有深入的使用。刚好这段时间有时间,就按照具体的项目场景搭建了一个简单的框架来体验。我是用它跟XLua来搭建整个游戏的框架的。Addressables的版本是1.22.3。我的体验也是围绕这个版本来讲的。使用的过程中发现它的下面几个问题,让我觉得目前还是不要在项目中使用它。 1.性能问题 通过标签一次性加载大量粒度很细的文件,会非常的慢。 Adressables可以通过lable标签来加载同一类型的资源。比如在项目中,把所有类型的lua文件都打上 <Lua> 标签。你可以选择 Addressables.LoadResourceLocationsAsync 先获取所有打了该标签的文件地址,然后再用一个循环去加载每一个地址的lua文件,然后把文件按照Key-Value的方式存在一个字典中。也可使用 Addressables.LoadAssetsAsync<Type>("key"), (asset)=>{} 方式按照标签加载所有的资源,然后在它里面的回调中去处理每一个lua文件,并保存到字典。这个回调每加载一个该标签下的文件,都会回调一次。 很显然,这个加载循环无法避免。由于lua文件的数量巨大,导致这个循环完成非常的耗时,比如在这次测试的项目中,总共有接近400个lua文件,总耗时在webgl平台达到惊人的 5.89 秒,并且随着功能的增多,这个数量只会往上加。这是很难接受的。android和IOS我没有测试,不过由于多线程的加持,估计是会比较快的。我们的主要平台是webgl. 2.工具黑盒 这个插件并不是开源的,对于用户来说,资源的下载,打包过程都是不受用户控制的。也就是说,它对用户来说是一个黑盒。他在官网有用户手册和API说明,但也就仅仅是那些而已。我甚至觉得这个东西不是为游戏开发者准备的,更像是一个功能说明。它的加载API少的非常的可怜,估计也就3个左右吧,不知道是不是我看的不够仔细,我真的没有再发现别的加载API了。而且API设计的很模糊,太多的功能细节集中到一个API上了,然后例子又少。虽然GitHub上有官方的示例,但我真的还是第一次见到如此模糊的API设计,我真的无法通过参数去了解该API到底能做什么事情。 用户没有办法了解其中具体的细节,也没有办法去定制项目需要的功能,万一有一些东西满足不了项目的需求需要修改,就只能干着急,你做不了任何事情。它的封装级别很高,但是对于游戏开发者来说,这过头了。这个插件更像是面向产品用来设计快速原型的产物。就拿上面的第一个问题来说,所有的lua其实已经打在一个AB包里面了,那么AB包下载之后,再从AB包中去加载lua文件,应该是非常的快的(毫秒级别),但不知道为什么,400多个文件加载居然要5.89秒,它里面是不是每一个文件加载都去走了什么特殊流程导致非常的慢。然后勾选了缓存,第二次,第三次,第四次的时间也是一样的,居然没有任何的时间减少。用户知道的太少了。 3.各种的小问题 我这里例举一个下载进度的问题。 public IEnumerator LoadAssetAsyn(string key, Action<Object> completeCB, Action<float> pgrFunc = null) { AsyncOperationHandle<Object> opHandle; opHandle = Addressables.LoadAssetAsync<Object>(key); while (opHandle.Status == AsyncOperationStatus.None) { Log.Error("Percent=" + opHandle.GetDownloadStatus().Percent + " pp=" + opHandle.PercentComplete); if (pgrFunc != null) { pgrFunc(opHandle.GetDownloadStatus().Percent); } yield return null; } yield return opHandle; if (opHandle.Status == AsyncOperationStatus.Succeeded) { Object instObj = Object.Instantiate(opHandle.Result); completeCB(instObj); if (pgrFunc != null) { pgrFunc(1); } } yield return null; } 上面的这个方法去加载一个资源,然后通过回调告诉目前的下载进度。opHandle.GetDownloadStatus().Percent 和 opHandle.PercentComplete 都不准确。前者是通过大小计算百分比,后者是通过数量计算百分比。 下面是文档的原话: AsyncOperationHandle.PercentComplete: Reports the percentage of sub-operations that have finished. For example, if an operation uses six sub-operations to perform its task, the PercentComplete indicates the entire operation is 50% complete when three of those operations have finished (it doesn’t matter how much data each operation loads). AsyncOperationHandle.GetDownloadStatus: Returns a DownloadStatus struct that reports the percentage in terms of total download size. For example, if an operation has six sub-operations, but the first operation represented 50% of the total download size, then GetDownloadStatus indicates the operation is 50% complete when the first operation finishes. ...

November 7, 2024 · 1 min · 188 words · LINK

Unity的Addressables学习日志

1.构建Player的时候,项目的资产数据是如何放进Player中的 由下图可以看出来Unity中的资产分成了4个主要类别: 1.被场景引用的资源 2.放在Resources文件夹中的资源 3.标记为了Addressables Group的资源 4.放在了StreamingAssets文件夹中的资源 2.如果同一个资产被划分到了不止一个类别上,那么该资产在构建的时候就会产生重复,划分了多少个就会复制多少个 3.Addressables Groups中的共享资产 对于一个标记为非Adressables的资产,如果有不止一个Adressablees的资产引用了它,那么它会在每个引用了它的Addressables的bundle中都复制一份。如下图所示: 为了消除这种复制,那么可以将这个共有的资产也标记为Addressables。然后把它引入到一个已存在的bundle中就可以了。如果此时一个bundle要引用它,那么必须在该bundle实例化前,将引入了共有资产的bundle先加载才可以。 4.Sprite的依赖 图集的引用跟其他资产不一样。 情形一 三张纹理,分别在三个不同的group中,它们会分别进入三个不同的ab包,彼此之间没有依赖,每个ab中图片的大小是500KB 情形二 还是这三张图片,它们被放进了一个图集,这个图集没有被标记为Addressables。那么包含这个图集的ab包将有图集的1500KB的大小。而另外两个ab包则只包含Sprite的元数据(只有很小的几KB),并且将包含图集的ab包列为自己的依赖。无论是哪个ab包包含这个图集,在重新构建之后都是跟前面一样的结果,这个过程由Unity控制,用户决定不了。这是跟标准的处理依赖重复资源过程的最大不同。Sprites的加载依赖它的图集,而图集的加载只依赖包含它的ab包的加载。 情形三 在上面的情形二的基础上,图集被标记成了Addressables并且单独放在它自己的图集中。这时候就会创建4个ab包。如果使用的是Unity2020.x或者更新的版本,那么在构建之后,4个ab包之间的依赖关系会和期待的一样,即图集资源在图集的这个ab包,有1500KB。另外三个ab包把图集的ab包作为依赖,只有几KB的引用数据而已。如果使用的是Unity2019.x或者更旧的版本,那么纹理资源就可能存在于这4个ab包中的某一个。另外三个ab包依然把图集所在的ab包作为依赖。然而此时图集所在的ab包可能仅仅只包含了图集的一些元数据,而真正的纹理资源可能在其他的另外3个ab包中。 5.Addressable预制体跟Sprite的依赖 情形一 跟三个标记为Addressables的纹理不同的是,这里是三个标记为Addressables的Sprite的预制体。每个预制体包含它独自的Sprite。在构建之后,三个ab包的大小如期望的那样,都是500KB。 情形二 跟情形一类似,3个预制体分别引用不同的Sprite,只不过所有的3个Sprite添加到一个图集中,但是这个图集没有标记为Addressables。在这种情形下,图集的纹理机会产生重复。在构建之后,三个ab包都大概有1500KB。这个重复资源的规则类似于一般的Addressables重复资源规则,但是跟前面介绍的 Sprite的依赖 中的情形二又不一样。 情形三 基于上面的情形二的预制体,纹理和图集,不过这里把图集标记为Addressables。此时图集纹理就仅仅只存在在包含图集的ab包中。另外的三个预制体的ab包就把这个图集的ab包作为依赖。 6.ab包的分包规则 1.可以将一个group中所有Addressables的资源都打进一个ab包中 2.可以将一个group中的每个Addressables资源分别打进它独自的ab包中 3.可以将使用同一个label的所有的Addressables资源打包进一个ab包中 7.同一个group中的场景资源会跟其他的Addressables资源分开打包 也就是说一个group中如果含有场景资源和其他非场景资源,那么构建之后,至少会得到两个bundle,一个包含场景资源,一个是除了场景外的其他资源。

August 29, 2023 · 1 min · 35 words · LINK

Ubuntu22.04运行unity2020.3.xx报错unity_csc.sh_Aborted

在Ubuntu22.04中出现,而且出现这个错误的版本是低于Unity2022.x的版本。自己测试了Unity2020.x和Unity2021.x都会出现这个错误。 重现的步骤也很简单,创建一个空项目并进入就会报错,然后提示进入safety mode。 这个错误会在Unity的Editor.log中留下线索,不过在打开项目前,记得先把Editor.log先删除,这样在报错的时候会生成一个新的Editor.log文件。也可以不用这样做,这样做的主要目的是日志少检查起来比较简单。 打开Unity编辑器的日志:Editor.log。发现以下关键信息 -----CompilerOutput:-stderr---------- No usable version of the libssl was found /home/eyap/Unity/Hub/Editor/2020.3.33f1/Editor/Data/Tools/RoslynScripts/unity_csc.sh: line 3: 25959 Aborted (core dumped) "/home/eyap/Unity/Hub/Editor/2020.3.33f1/Editor/Data/Tools/RoslynScripts/../../Tools/Roslyn/csc" /shared /noconfig @temp/UnityTempFi -----EndCompilerOutput--------------- 从 No usable version of the libssl was found 可以看出是缺少了 libssl库导致的。用下面的命令安装一个就行了: echo "deb http://security.ubuntu.com/ubuntu focal-security main" | sudo tee /etc/apt/sources.list.d/focal-security.list sudo apt-get update sudo apt-get install libssl1.1 相关的讨论连接: https://forum.unity.com/threads/unity2020-3-doesnt-works-on-linux.1338869/

August 29, 2023 · 1 min · 55 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