撕夜
用心爱你你不懂
十年
时间都去哪儿了
其实你不懂我的心
关于你们之间的故事
假如爱有天意
Unity SRP Misc
渲染指定物体
在SRP里面一个非常重要的函数是ScriptableRenderContext.DrawRenderers
,通过它可以帮我们完成整个场景的渲染。但是这个函数用起来很方便,因为它封装了很多细节。但是在游戏UI中经常会用RenderTexture
来混合UI和模型,就需要将个别物体渲染到RenderTexture
。如果我们只想渲染某一两个物体时,操作起来就比较麻烦,因为首先我们需要将需要渲染的物体ScriptableRenderContext.Cull
出来,这个剔除操作是针对整个场景的,必然有额外的消耗。特别是我们已经知道要渲染哪个物体的情况下,我们可能希望跳过剔除这一步。但是跳过剔除,ScriptableRenderContext.DrawRenderers
函数就用不了了。
这时候我们可以使用CommandBuffer.DrawRanderer
函数,针对单个物体进行渲染。具体操作就是获取所有物体的Render
组件、材质,使用CommandBuffer
渲染。示例代码如下:
1 | public class RenderData{ |
这里有几个点需要注意:
- 首先是
Camera.SetupCurrent(camera)
,如果没有这行代码的话,粒子系统是不会渲染的参考。 ScriptableRenderContext.DrawRenderers
支持SRPBatch
,但是其他绘制函数不一定支持,比如这里的CommandBuffer.DrawRanderer
,这是当前方法的一个弊端。但是Unity2022版本增加了BatchRendererGroup据说是可以实现SRPBatch
,参考这里,但是WebGL目前还不支持。BatchRendererGroup
的原理是在SRPBatch
上扩展的自定义合批方法。- 这里也有对剔除的探讨,但是好像并没有什么有价值的东西,暂且留个入口。
- 另外,
Animator
组件和ParticleSystem
组件上都带有CullingMode
属性,一般来说当物体不可见时,我们希望动画和粒子系统都停止,从而减少性能消耗。CullingMode
就是用来控制物体不可见时的行为。当使用ScriptableRenderContext.DrawRenderers
绘制时,会自动标记物体是否可见的状态,所以通常设置为Automatic
,也就是说不可见时停止,可见时照常运行。但是使用CommandBuffer.DrawRanderer
绘制时,并不会标记这些可见状态,所以需要强制设置为Always Simulate
,当然有些粒子系统设置为Automatic
也能正常运行,这个应该是Unity的bug。 - 当然,我非要用
ScriptableRenderContext.DrawRenderers
渲染某一个物体也不是不行,就是要把剔除流程走一遍。如果我们需要将多个物体分别渲染到多个纹理上呢?是不是就的把整个流程走多遍?有一种做法,可以只走一边剔除流程,就可以渲染多个模型到多个贴图上。FilteringSettings.renderingLayerMask
这个参数就是可以用来控制本次渲染渲染指定渲染层的模型,我们可以为不同的模型设置不同的层,渲染多张纹理的时候把FilteringSettings.renderingLayerMask
设置到对应的层。渲染层总共有32层,也就是最多可分32个独立渲染的物体。我之前想过渲染一次设置一次模型的渲染层,这样看起来就可以渲染无数多个独立渲染的物体,但是实际上这是行不通的,因为整个指令是CommandBuffer
缓存执行,也就是说动态修改物体的渲染层是无效的,CommandBuffer
指令只知道最后一次设置的渲染层。
Shader Tools
源码
1 | // ShaderOpenTools.cs |
参考
VSCode Command line
VSCode settings
VSCode GlobPattern:可以用于设置VSCode不显示.meta
文件,在Settings
界面,搜索file:exclude
,然后增加**/*.meta
。
使用配置文件
上面是通过路径打开VSCode文件,更实用的一种方法是直接通过配置文件打开。通过配置文件打开有几个好处,就是可以在同一个VSCode里面打开多个文件路径,并且保存设置。配置文件格式如下,文件名为shaderworkspace.code-workspace
:
1 | { |
然后前面打开VSCode工程的命令可以改成:
1 | if(line > 0) |
当然,上面命令有效的前提是shaderworkspace.code-workspace
文件在当前工作目录下。
资源网站
Unity AssetBundles
接着上一篇Unity资源逆向解析,这里再来谈一谈AssetBundles。
AssetBundles 使用
使用AssetBundles
总体上分成两步,第一步是创建AssetBundles
资源,第二部是使用AssetBundles
资源。
AssetBundles 创建
Unity提供内置的BuildPipeline管线和可编程的Scriptable Build Pipeline,这里仅以内置的BuildPipeline
管线为例进行讲解。
首先内置的BuildPipeline
管线提供了BuildPipeline.BuildAssetBundles()
函数来帮助创建资源,有两种调用方式。
方式一:
public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
方式二:
public static AssetBundleManifest BuildAssetBundles(string outputPath, AssetBundleBuild[] builds, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);
方式一是针对项目中所有被标记为AssetBundles
的资源,根据其标记的AssetBundles
名称对这些资源进行打包的。
方式二是忽略AssetBundles
的资源标记,而是通过纯代码的形式,向BuildAssetBundles
传入所需要构建的资源,以及AssetBundles
资源名称。
如果想实现自动化构建资源的话,偏向于使用方式二,可以完全通过脚本进行控制。
但是这里为了方便,采用方式一进行构建资源。
因此资源构建代码如下:
1 | using UnityEngine; |
然后要手动编辑哪些资源需要被打包成AssetBundles
,根据官方文档,可以对Assets目录下的文件、以及文件夹进行标记,如果标记的是文件夹,那么相当于标记了文件夹下所有子文件。当然文件的标记优先级高于文件夹的优先级。
这里我的目录结构如下:
1 | 资源 AssetBundleName 资源依赖 |
上面第一列是资源目录结构,第二列是针对资源的AssetBundles
资源名称,第三列表示的是该资源所依赖的其他资源。
一切准备就绪后,在Assets菜单下点击Build AssetBundles
开始构建资源。
在assetBundleDirectory
目录下得到如下资源:
1 | [assetBundleDirectory] |
其中以manifest为后缀的文件是给人读的,展示了一些基本信息。加载AssetBundles
时只需要读取无后缀的文件就行。而assetBundleDirectory
这个资源文件记录了其他所有资源的manifest文件信息。
上面打包出来的资源文件可以使用Unity资源逆向解析中提到的解包工具打开。
AssetBundles 加载
本文加载的基本思路是读取assetBundleDirectory
资源文件中的信息,然后去构建一个资源目录表,然后在使用资源时,首先访问资源目录表,看看该资源是否已经加载,如果没有加载的话就对该资源进行加载,加载时也会判断资源依赖,待所有依赖资源以及该资源都加载成功后,触发资源加载成功回调函数。
代码如下:
1 | using System.Collections.Generic; |
AssetBundles 内存分析
这里以实例化mogucolor.prefab
为例。总共有以下几个步骤:
- 调用Init函数初始化
AssetBundleManifest
- LoadAsset异步加载
mogucolor
,因为其直接以及间接依赖的资源有texture
、mogu
、mogumaterial
、mogushader
,所以一共会有5个AssetBundle会被加载 - 然后调用
AssetBundle.LoadAsset<GameObject>("mogucolor)
,提取其中的mogucolor.prefab
资源供Unity后面使用。 - 最后使用
GameObject.Inistatiate()
函数将mogucolor.prefab
实例化出来。
在Profiler监控窗口中,选中Momory
,然后在下方选择Detailed
。执行实例化加载流程,然后每一步点击Task Sample Editor
进行性能数据采样,你会发现下方有以下5大类:
- Other
- Assets
- Not Saved
- Builtin Resources
- Scene Momory
通过记录发现,每一步各个类型数据下都各有变化,以下是变化情况:
1 | 第一步:调用Init函数初始化`AssetBundleManifest` |
可以发现,第一和第二步,主要是增加了NotSaved里面的AssetBundle。第三步增加的是Assets里面的资源数据。第四步增加的是场景里面的数据,至于NotSaved和Builtin Resources数据也被动增加,主要是因为新入场景的物体可能会导致渲染流程发生改变,所以内置的一些材质或者RenderTexture会有所增加。
至于Builtin Resources里面的数据,从分析上来看主要是GUI相关的的一些内置数据,以及异常Shader等。
AssetBundles 与 Shader
Shader也是一种资源,和模型、贴图资源一样,可以打包进AssetBundle。但是Shader又是一种程序,只不过是运行在GPU上的,因此为了节约性能,需要对Shader进行裁剪,将不需要的变体剔除掉。而我们自己使用AssetBundles对Shader进行打包时,也会进行剔除。以下是官方文档中的原文,介绍了如何根据需要将需要的变体打包。
1 | Interactions between Shaders and AssetBundles |
从文档总结就是,假设我们有一个Shader定义了多个关键字,如果我们只想打包某几个关键字相关的变体时,我们可以采用以下几种方式:
- 创建与变体相对应的材质,然后将这些材质一起打包到这个Shader的AssetBundle中
- 创建变体集,如ShaderVar.shadervariants,以shadervariants为后缀的资源,在这个变体集中定义变体,然后将变体集一起打包到这个Shader的AssetBundle中
- 使用Graphics Settings,(暂时不知道怎么使用)。
有一个很重要的点是,一个Shader可以打包进多个AssetBundle,各个AssetBundle中所生成的变体是相互独立的,最终运行时也是相互独立看待。
Shader变体分析
既然谈到Shader变体,这里就再多说几句,在Unity中Shader变体主要是受Shader关键字控制,当然根据官网描述,一个shader里面拥有多个Pass,也是形成变体的因素。
这里主要是谈一下关键字,首选关键字的增加,对于变体的数量影响很大,最极端的情况是变体的数量是关键字数量的指数级。所以我们需要控制关键字的数量,尽量减少关键字的使用。
假设当前关键字的数量已经是最优的了,我们还可以控制关键字的搭配来控制变体的数量。上一节提到了变体与关键字的关系,就是通过材质和shadervariants等形式,来限定哪些变体生成。
但是目前Unity在关键字上还有一个限制,就是关键字数量不超过256,其中Unity内部已经占用了一部分,所以留给我们用来定义关键字的数量根本够不到256。所以在定义关键字时是需要精打细算的。
在Unity2018版本以及之前的版本,关键字只有global全局关键字,也就是说,所有Shader定义过的关键字数量之和不得超过256,这个限制就非常大,如果使用一些第三方资源,有可能会突破这个限制,最终导致项目崩溃等问题。
Unity2018之后的版本,关键字分为了local和global两种,其中global还是延续了全局关键字的逻辑,也就是所有shader共享的,但是local关键字是从属于当前定义的shader。关键字256的数量限制还在,只不过是local和global两种关键字的总和。怎么理解呢?
举个例子,现在只有两个Shader,Shader1和Shader2,Shader1中定义了100个global关键字,50个local关键字,Shader2中定义了90个global关键字,49个local关键字,这些关键字没有重复的。那么在Shader1看来,gloabl关键字有190个,local关键字有50个,两者之和没有超过256。那么在Shader2看来,gloabl关键字有190个,local关键字有49个,两者之和没有超过256。但是将所有关键字都加起来其实是超过256的。但是这两个Shader的变体组合是有效的。也就是说新的关键字规则将一部分全局关键字的份额分配给了局部关键字,具体来说就是192个全局关键字,和64个局部关键字。
关于关键字的上限,以及因为这个上限导致的问题在这里有很多讨论,其中网友发现问题最多的是使用HDRP时,很容易超出限制,以及使用AssetStore里面的资源也会很容易超出限制。所以网友希望能够直接提高上限,或者给个上限可选项,但是Unity客服表示不太愿意这样做。这个问题提出了好几年了,关键字分为了local和global两种也是基于这个问题给出的一个解决方案。
从程序优化的角度来讲,控制关键字的数量是非常有必要的,因此我们首先需要知道有哪些关键字被使用的了,Unity本身并没有提供这个功能(我目前没看到,并且从上面的讨论链接里面,Unity客服也表示这个很难实现)。但是广大的开发网友开发了一些工具可以用来辅助分析关键字。这里我知道的有两个:
- Shader Control :Shader Control 是一款功能强大的编辑器插件,让你可以完全控制着色器的编译和关键字的使用以及它们在游戏中的影响(广告是这么说的,我穷没用过)
- Shader Keywords Tool UNITY3D :免费开源的,用了一下下,可用。
另外这里以及这里详细的介绍了变体相关的东西,包括变体数量的计算,以及如何控制变体的数量。而且Unity还提供scriptable shader variants stripping
自定义变体剔除的方法,只要是实现IPreprocessShaders,这个回调接口在打包项目和打包Assetbundle资源时都会调用。
Create Parent For Transforms
扩展命令
1 | using UnityEngine; |
Unity Package Menu Item
Package扩展命令
1 | using System; |
Unity资源逆向解析
Asset Ripper
支持打开打包后的资源,对于图片等资源可以直接预览,但是Shader只能查看到名字属性等基本信息,里面的代码变体等信息都是假的。
下载链接
UABE(AssetsBundleExtractor)
支持解析打包后的资源,但是好像只能罗列出资源信息,不能像Asset Ripper
一样完美的将资源解包出来。但是支持修改打包后的资源,这是其他工具所不具备的。
下载链接
社区
AssetStudio
和Asset Ripper
一样,支持打开打包后的资源,但是Shader信息展示的更多一点,可以看到变体信息,但是代码块部分仍然看不了。
下载链接
关于Unity Resources
Unity项目运行时可以通过Resources和AssetsBundle方式读取资源。推荐是使用AssetsBundle,但是Resource资源的组织方式影响着包体的大小,所以即便项目中没有使用到Resources资源读取方式,也还是需要有所了解。
- 首先Resources资源读取方式,需要将资源放置在Assets目录及其子目录下的Resources目录。只有在这个文件夹下的资源才可以使用Resources的方式读取。
- 打包项目工程时,Resources下的资源会直接打到项目App,如Apk。
- Resources资源在App中,是存储在
resources.assets
、resources.assets.resS
、sharedassets0.assets
、sharedassets0.assets.resS
文件中。 - 关于
*.assets
和*.assets.resS
两个后缀文件,同名文件中的*.assets存储的是序列化文件。而*.assets.resS
里面存储的是二进制数据源,如图片、音效等。 *.assets
文件类似于Prefab
,存储着资源的所有序列化信息,以及对二进制数据源的引用,比如说图片,而*.assets.resS
里面便是*.assets
里面所引用的实际的图片、音效等数据。- Resources目录下的资源是打包到
resources*
中还是打包到sharedassets*
文件中,是有一定的规律。未被引用的资源是打包到resources*
中,比如新放入的图片还没有使用。而被引用的资源是被打包到sharedassets*
,比如被其他Prefab引用的图片。
除了我们自己创建的Resources资源,Unity在打包时还会拷贝一些默认资源,比如内置的着色器,按钮图集等等。这些资源是被统一打包到unity default resources
文件中(实际操作是将这个文件直接拷贝到包内,这个资源文件是平台预先就打包好的)。unity default resources
资源实际上是*.assets
和*.assets.resS
文件的合体,也就是包含序列化数据和二进制源数据。整个文件有好几兆,但是实际项目中我们可能不会使用这些内置资源,所以无疑会导致包体变大,资源空间浪费。而且在苹果IOS查重时,这个文件还可能被标记。所以可以适当的修改这个文件。
以上这些Resources资源文件,都是可以使用上面的解包工具打开,可以查看到这些文件中具体包含了哪些资源。
参考
Compiler Options
功能
在用Unity开发项目时,每次编写或者修改C#脚本,Unity都会自动检查脚本,并进行自动编译。频繁的自动编译,无疑是会降低开发效率,基于这个出发点,我希望能够关闭自动编译功能。在Unity下有两个设置是和自动编译相关的,就是Preferences界面下的General里面的Auto Refresh
和Script Changes While Playing
。前者是自动刷新,每次刷新的时候就会检查脚本是否需要编译,这时候我们可以把自动刷新关掉,然后使用手动的方式进行刷新。后者主要是控制运行时脚本自动编译的时机,是否在运行时进行自动编译。
上图中的设置项的位置可能会因Unity的版本不同而有所改变,这里我用的Unity版本是Unity 2020.3.33f1c2 Personal
这里写了一个编辑器脚本,将上面的设置暴露在菜单栏中。
1 | using UnityEditor; |
回到开头的问题,为了解决频繁自动编译的问题,这里我禁用用自动刷新的功能,但是这就导致了我每次都需要手动刷新,如果没有手动刷新,那么修改的代码就不会生效。
所以这里换个思路来解决问题,我们只在运行前和退出运行的时候,进行代码编译,而其他时候都不会进行代码编译。这样上面提到的设置都不用管,仍然可以启用自动刷新功能,但是自动刷新不会进行自动编译。刚好Unity提供是否启用自动编译的接口。所以在不需要任何设置的情况下,只需要将下面的编辑器脚本导入到Unity的Editor目录下,在运行前和运行结束的时候会自动检查代码进行自动编译。
1 | using UnityEditor; |
参考
GitLab and Jenkins and Unity
在这篇文章讲了如何在Docker上安装GitLab,Jenkins,并且最终Jenkins从GitLab上拉取到项目文件。
我最终的目的是希望能够对Unity项目进行打包部署,所以我开始关联Jenkins和Unity。因为我的Unity是装在物理机上,而Jenkins是装在Docker上,所以Jenkins没办法调取Unity进行打包操作。
所以我后面采用的是GitLab部署在Docker上,而Jenkins和Unity都是安装在物理机上,这样Jenkins通过GitLab的Url拉取项目,然后通过Unity Puglin插件调用Unity进行打包,当然也可以使用windows的批处理命令build.bat
的方式。采用Unity Plugin的方式的一个好处就是unity的log会直接显示到Jenkins的log界面上。
其中,虽然Jenkins和GitLab的安装位置不一样,但是他们之间的关联操作和之前的是一样的。这里我们讲一下Jenkins和Unity的关联步骤。
首先建一个Unity项目,然后在项目根目录下创建两个文件build_image_url_link.bat
和myqrcode.py
。build_image_url_link.bat
1 | set BASE_PATH=D:\ServicesData\Publish\%1\%2 |
myqrcode.py
1 | import qrcode |
然后在Editor下创建BuildTools.cs
脚本。
1 | using System.Collections; |
到这一步Unity项目就建好了,然后将它提交的GitLab上。
创建一个打包项目,关联到GitLab,操作步骤参考上面的链接。进入当前打包项目的配置界面。按下图配置。-quit -batchmode -nographics -executeMethod BuildTools.BuildApk --productName:$productName --version:$version --projectName:JenkinsUnityForAndroid --target:$target
build_image_url_link.bat JenkinsUnityForAndroid %target% %productName% %version%
DESC_INFO:(.*),(.*)
<img src="\1" height="200" width="200" /> <a href="\2">点击下载</a>
这样就基本配置好Jenkins和Unity项目了。但是上面配置中多了很多奇怪的操作,像什么Python脚本之类的。这是因为我想将打包好的包发布到一个本地服务,然后通过二维码下载。
所以这里我需要搭建一个本地服务,看网上有说用蒲公英什么的来当作发布平台,但是我就想本地局域网用,所以使用我比较熟悉的HFS
来冲动服务。
前面打包代码里面会将好的包放到这个服务路径下面,然后可以通过Url来访问,假设当前打好的包在这个路径下,那么手机输入对应的Url就可以下载了。不过每次都要输入Url很麻烦。所以这里考虑使用二维码。
这里讲了使用qrcode
和Image
两个python插件来生成二维码,所以首先我们要安装Python,这里Python 3.7.7下载,安装默认安装,并勾选写入环境变量。
然后打开命令行窗口,分别输入pip install qrcode
和pip install Image
两个命令,安装好两个插件。
然后回到Jenkins,设置Jenkins内部的环境变量。
这里因为我的python是安装在C:\Users\10524\AppData\Local\Programs\Python\Python37
目录下的。都准备好后,就可以开始构建了,下面是最终效果。
参考
docker+gitlab+jenkins从零搭建自动化部署
Jenkins生成APK链接的二维码
Jenkins构建Python项目提示:‘python‘ 不是内部或外部命令,也不是可运行的程序
Python 3.7.7下载Windows x86-64 executable installer,安装默认安装,并勾选写入环境变量。
HFS下载,搭建简易服务。