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.表面着色器和纹理贴图 介绍 漫反射的着色处理 始前准备 操作步骤 原理介绍 使用包组 操作步骤 压缩矩阵 相关补充 向着色器添加纹理 ...
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. ...
修改代码换行符
Windows中换行符转换 1.使用到的工具dos2unix 工具的下载地址dos2unix 下载完后解压到一个目录中,并且把该目录设置到path中去。 2.新建一个bat文件,将下面的批处理代码复制进去 @echo off setlocal enabledelayedexpansion chcp 65001 set "CURRENT_DIR=%~dp0" set "CURRENT_DIR=%CURRENT_DIR:~0,-1%" echo %CURRENT_DIR% cd %CURRENT_DIR% echo 当前CMD默认目录:"%cd%" ::下面的地址换成自己对应的地址 cd ..\..\..\Assets for /R %%G in (*.cs) do dos2unix "%%G" echo 当前CMD默认目录:"%cd%" echo "转换完毕" PAUSE 双击运行bat文件,就会把Assets目录下所有的.cs文件转换成LF换行符,如果要包含其他类型的文件,则把for /R %%G in (*.cs) do dos2unix "%%G" 这行代码的 (*.cs) 改成想要的文件类型即可。比如 for /R %%G in (*.cs *.lua *.html) do dos2unix "%%G" 则会把目录下所有的.cs,.lua,.html都转换成LF。 如果是想把LF转换成CRLF,则把for /R %%G in (*.cs) do dos2unix "%%G" 中的 dos2unix 改成 unix2dos 即可: setlocal enabledelayedexpansion chcp 65001 set "CURRENT_DIR=%~dp0" set "CURRENT_DIR=%CURRENT_DIR:~0,-1%" echo %CURRENT_DIR% cd %CURRENT_DIR% echo 当前CMD默认目录:"%cd%" ::下面的地址换成自己对应的地址 cd ..\..\..\Assets for /R %%G in (*.cs) do unix2dos "%%G" echo 当前CMD默认目录:"%cd%" echo "转换完毕" PAUSE
Cpp模板学习
CPP模板学习 1.C++ 模板,模板的声明和定义都放在同一个.h文件中,不然会出现undefined reference to。因为模板类它是一个模板,只有应用了具体的类型之后,才会实例化。如果分开在.h和.cpp两个文件中,在编译器编译cpp文件的时候,因为也是以template <typename T> 开头,这不是具体的实例,不会有具体的代码生成。所以还是没有定义。 2.当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。 3.对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本 4.在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。 5.
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,一个包含场景资源,一个是除了场景外的其他资源。
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/
Xlua集成第三方库
XLua集成第三方库(Rapidjson举例) 参考教程:https://zhuanlan.zhihu.com/p/389424029 1.环境准备 (1)cmake的安装 https://cmake.org/download/ (2)安装Visual Studio最新版(2017,2019,2022)都行,去微软官网下载 (3)安卓的话,下载Android Studio,然后安装好Android SDK,NDK,并且配置好ANDROID_SDK和ANDROID_NDK 2.将XLua克隆到本地 xlua的地址 https://github.com/Tencent/xLua 进入到拉取下来的xlua文件夹, 在build文件夹中新建lua-rapidjson文件夹,并在该文件夹下新建include和source文件夹 4.拉取lua-rapidjson的项目到本地 这里需要说明一下,lua-rapidjson两个版本: 一个是原作者的最新版 https://github.com/xpol/lua-rapidjson 另一个是XLua的做过兼容的版本: https://github.com/chexiongsheng/build_xlua_with_libs 新版肯定是更新,但是再编译的过程中会遇到很多错误,非常的麻烦浪费时间,所以我们还是选择XLua做过兼容的版本,节省时间,并且也能满足要求。 将build_xlua_with_libs克隆到本地。 5,直接将build_xlua_with_libs中build文件夹下的lua-rapidjson文件夹复制到自己的xlua的build文件夹下面。 6.修改xlua/build/CMakeList.txt文件,在头部加入如下代码 #begin lua-rapidjson set (RAPIDJSON_SRC lua-rapidjson/source/rapidjson.cpp ) set_property( SOURCE ${RAPIDJSON_SRC} APPEND PROPERTY COMPILE_DEFINITIONS LUA_LIB ) list(APPEND THIRDPART_INC lua-rapidjson/include) set (THIRDPART_SRC ${THIRDPART_SRC} ${RAPIDJSON_SRC}) #end lua-rapidjson 并且把头部的VERSION 改成自己的cmake版本(在控制台输入cmake –version),我这里是cmake_minimum_required(VERSION 3.26.4) 6.编译windows的xlua.dll 这里我们选择lua54版本 所有编译脚本都是按这个方式命名:make_平台_lua版本.后缀。 比如windows 64位lua53版本是make_win64_lua53.bat,android的luajit版本是make_android_luajit.sh,要编译哪个版本就执行相应的脚本即可。 执行完编译脚本会自动拷贝到plugin_lua53或者plugin_luajit目录,前者是lua53版本放置路径,后者是luajit。 用代码编辑器打开这个bat文件,将Visual Studio版本改成自己安装的对应版本, 如果不知道就打开 Developer Command Prompt这个控制台,里面会有如图所示 ...
Unity编辑器为切换平台,工程加载完成事件添加自动处理
有时候我们在Unity项目中,需要在切换平台后,或者项目加载完后,做一些自动化的处理,比如项目加载完后检测一些配置,或者检测项目版本。或者在切换项目平台的时候,需要跟据不同的平台设置不同的字体移除不属于该平台的一些资源等等。下面记录一下如何添加对这两个事件的处理。 1.Unity切换平台事件的处理 要处理这个事件,我们需要将类实现 IActiveBuildTargetChanged 接口,并重写该接口的 OnActiveBuildTargetChanged方法。那么当切换平台的时候,就会执行这个类的 OnActiveBuildTargetChanged 方法。在这个方法中,我们就可以添加自定义的一些处理逻辑了。代码如下: /// <summary> /// 切换平台后,对一些资源的处理 /// 有些资源只有再特定的平台才会使用,再切换平添后,不要的资源要移除或者移动到别的地方 /// </summary> public class BuildTargetChangedTool : IActiveBuildTargetChanged { public int callbackOrder { get; } public void OnActiveBuildTargetChanged(BuildTarget previousTarget, BuildTarget newTarget) { //切换平台后的一些处理 PlatformResTool.ProcessFonts(newTarget); PlatformResTool.ProcessThirdSDK(newTarget); LinkXmlBuildProcessor.LinkXmlOp(newTarget); AssetDatabase.Refresh(); } } 2.Unity项目加载完事件的处理 项目加载完成的事件跟切换平台的事件的处理方式不一样,这里需要使用 InitializeOnLoad 这个特性来实现。我们需要给类添加这个特性,同时它需要一个静态构造方法,当项目加载完成后,Unity编辑器就会调用这个静态构造方法,然后在这个静态构造方法中我们就可以添加一些自己的处理逻辑了。代码如下: /// <summary> /// 工程加载完,要处理这些跟平台相关的资源 /// </summary> [InitializeOnLoad] public class StartUpTools { static StartUpTools() { if (!EditorApplication.isPlayingOrWillChangePlaymode) { BuildTarget buildTarget = Packager.GetBuildTarget(); PlatformResTool.ProcessFonts(buildTarget); PlatformResTool.ProcessThirdSDK(buildTarget); LinkXmlBuildProcessor.LinkXmlOp(buildTarget); AssetDatabase.Refresh(); } } }
水的智慧
水它蕴含很多智慧, 它的智慧在于总能适应。 若寒,则成冰。 若温,则成水。 若火,则成汽。 把水放在不同的容器里,水会变成不同的形状。 好比一桶水, 你静,它静。 你去搅拌,它也跟着流动起来。 你若拍打,它就溅起水花。 放入不同的颜料,则成不同色彩。 它总会让自己放低,总会找到一条路,流向大海。 它并不会消失,所见蒸发,流失,那不过在循环。 它能包容。 你会看见水有色彩,有泥沙,会浑浊,那不过是掺和进来的颜料,杂质。 水的本质并没有变,它还是水。 水流不息,生命不止
用数组来实现热度图
用数组来实现热度图 着色器难以掌握的一个典型的原因就是缺少合适的文档。许多开发者在学习着色器的时候被代码搞得一团糟,原因就是他们没有很深的知识来解释眼前到底发生了什么。事实上 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 保存了来自所有点的热度,它是根据半径和强度计算出来的。之后它就被用于查看到底该用渐变纹理上的哪些颜色。 着色器和数组其实是一个很好的组合,但是却很少有游戏能在使用时发挥出它们的全部潜力。然而在这里,它们却引来了一个很大的瓶颈,因为对于每一个像素来说,着色器都必须要去遍历完所有的点。