|
前言
随着轻量化3D需求的增长,WebGL越来越受到关注,诸如三维可视化,数字孪生、元宇宙区块链Web游戏等。相比传统的客户端应用,WebGL采用BS架构,即浏览器就可以直接体验3D效果。举个不是很恰当的例子:假如你是做客户端应用的,你和另一个做WebGL的竞争对手同时去竞标一个项目,你演示项目的时候,还要让甲方掏出手机,打开应用商城,搜索app,下载,安装,然后再打开,最后演示。而你的竞争对手,直接转发了一个链接过去,立即就开始演示了,这便捷程度,差不是一点半点了。
局限性
当然,也不是说WebGL就没缺点了(丑话说前头 >_< )。主流的WebGL开发方案包括:原生方案、渲染库(如ThreeJs)、基于跨平台的引擎发布(如本文重点要说的Unity),他们都各有优缺点。使用Unity的优势是开发效率高,一次开发,可多平台部署,当然缺点也明显,引擎内核重,不优化的话,执行效率上并没有优势,当然这也是本文要探讨的重点。
Unity 中的 WebGL 平台旨在替代以前的 UnityPlayer 平台,随着目前多数浏览器都已经能够很好的支持 Html5,因此 WebGL 也更加成熟,为了将项目发布为 WebGL 平台,在打包的时候,Unity 利用 Emscripten 工具链,将引擎的 C/C++ 代码转换为 WebAssembly(一种浏览器可以执行的格式,更加高效),而 C#代码则需要先通过 IL2CPP转换为 C/C++代码,在转换为 WebAssembly。
虽然目前 Unity WebGL 程序能够在大部分主流浏览器上运行,但是也存在一定差异,另外,在手机浏览器上是无法正常运行 WebGL 程序的(2022.3更新:当然不绝对,修改模板适配,其实是能跑的)。
手机浏览器运行Unity WebGL方法:将UnityLoader.js中的UnityLoader.SystemInfo.mobile和[&#34;Edge&#34;, &#34;Firefox&#34;, &#34;Chrome&#34;, &#34;Safari&#34;].indexOf(UnityLoader.SystemInfo.browser) == -1替换成false。 另外由于平台限制,有些功能在 WebGL 上是不支持的:
- 不支持多线程,因为 JavaScript 不支持多线程,所以 System.Threading 命名空间下的类不要使用;
- 不能在 VS 中进行断点调试,后面会介绍如何进行调试;
- 不能直接使用 Socket,包括 System.Net下的任何类型,以及 System.Net.Sockets 下的部分类型,以及 UnityEngine.Network,如果需要在 WebGL 平台使用网络功能,可以使用 WWW或者 UnityWebRequest这些都是基于 Http协议的实现,如要需要高实时性,可以选择 WebSockets或者 WebRTC;
- WebGL 1.0是基于 OpenGL ES 2.0,WebGL 2.0基于 OpenGL ES 3.0,所以存在相应的限制;
- WebGL 音频是基于自定义的后台,只具备基本的音频功能;
- WebGL 是 AOT(ahead of time,即静态编译平台,因此不能使用 System.Reflection.Emit 下的类型进行代码生成,IL2CPP和 iOS 也是如此。
浏览器支持
Unity WebGL 虽然在大部分浏览器上都支持,但是支持程度以及性能表现不一样,另外,在移动设备上是不支持的,虽然在一些高端设备上有可能运行,但是绝大部分设备是没有那么大内存来支持 Unity WebGL 的。
下表列出了Firefox、Chrome、Safri、Edge各个浏览器的支持情况
说明:
- 浏览器厂商为了稳定性,通常会采用黑名单/白名单的方式,针对不同的显卡驱动进行功能限制,在 chrome 中可以通过输入 chrome://gpu 来查看当前 gpu 的状态,或者可以在这里查询;
- Large-Allocation 响应头是高速浏览器需要分配大量内存,目前只有 Firefox 支持。
编译运行 WebGl 项目
WebGL 项目打包之后,会生成如下文件结构:
- index.html
- TemplateData - 包括logo、加载进度条,只有模板选择 Default 时才有)
- StreamingAssets - 项目中的 StreamingAssets 文件夹(WebGL平台下 Application.streamingAssetsPath 的值为 http://youer_host_url:port/StreamingAssets)
- Build
- UnityLoader.js - 加载 unity 内容的脚本
- myproject.json - PlayerSetting 中的部分设置以及其他资源的 Url,给 UnityLoader 用的
- myproject.wasm.framework.unityweb - JavaScript 运行时和插件
- myproject.wasm.code.unityweb - 编译完成的 WebAssembly
- myproject.wasm.memory.unityweb -
- myproject.data.unityweb - 项目中的资源和场景
Build 文件夹下载 *.unityweb 文件,可能是压缩文件,由 PlayerSetting/publishing setting/Compression Format 设置。
生成完毕之后,可以用浏览器打开 index.html 文件,由于 Chrome 不允许运行本地文件,因此需要将生成文件放到 Web 服务器,或者通过 unity 的 Build And Run,unity 会在编译完成之后启动一个本机 web 服务器。
调试模式
可以获得什么:
- 内置 Console,方便查看错误;
- 可读性良好的 JavaScript,Release 版的会压缩代码,不具备可读性;
- 查看调用堆栈;
- 可以连接 Unity Profiler 进行性能调试;
缺点:
WebGL 平台不要在 VS 中进行调试,而要使用 Chrome 的开发者工具(F12)进行调试。
常见错误:
Problem: The build runs out of memory
这通常在 32 位浏览器上出现,原因是内容占用内存过大,浏览器分配内存失败。
Problem: Files saved to Application.persistentDataPath do not persist
持久化文件没有保存完成,Unity WebGL 将所有持久化数据(PlayerPrefs、缓存)都保存到浏览器的 IndexedDB 中,在开发者工具中,可以通过 Application 标签页查看,这一API 是异步的,所以不知道什么时候能够完成。
Error message: Incorrect header check
通常是服务器配置不正确。
Error message: Decompressing this format (1) is not supported on this platform
通常是加载 LZMA 压缩格式的 AssetBundle 时发生,WebGL 平台不支持 LZMA 格式,改用 LZ4 格式压缩。
重要发布设置
Disable HW Statistics
默认不勾选,表示将会在加载内容时,向 Unity 发送你的硬件信息、用户信息等,建议勾选上,因为国内网络访问 Unity 服务器比较慢,会降低内容的加载速度。
如果勾选的话,在 Chrome 中可以看到它会向 https://config.uca.cloud.unity3d.com/ 发送请求,可以看到他的发送内容。
代码裁剪
Strip Engine Code
默认勾选,表示对于项目中没有用到类型,将会把那一部分代码剔除掉,从而减少编译大小,提高运行时的性能。Unity 会扫描项目中所有从 UnityObject 继承的类型,包括检查它的内部引用以及序列化字段,将会移除没有任何引用的类型,从而减少发布大小,生成的代码也更少、更快、内存占用更少。
代码裁剪可能引起的问题:
可能会裁剪掉实际用到的类型,比如预制体包含一个类型 A,而将该预制体打包到 AssetBundle 之中,Unity 就可能将 A 类型裁剪掉,运行时将在控制到看到错误 Could not produce class with ID XXX,可以按照下面两步来解决该问题:
- 根据提示 ID,查找裁剪的类型,查找地址:这里;
- 将该类型在脚本中引用一下,或者在创建 Assets 目录下 link.xml,格式如下。
<linker>
<assembly fullname=&#34;UnityEngine&#34;>
<type fullname=&#34;UnityEngine.Collider&#34; preserve=&#34;all&#34;/>
</assembly>
</liner>如果想要查看裁剪之后 Unity 包含了内些类型,可以在打包之后,找到文件 Temp/StagingArea/Data/il2cppOutput/UnityClassRegistration.cpp 进行查看,除此之外没有其他更加方便的方法。
另一一点需要知道的是 Strip Engine Code选项只是针对 Unity 的本机代码,对于托管代码来说,总是会进行代码裁剪,IL2CPP 通过托管 DLL,以及代码脚本中的静态引用,来进行代码裁剪,如果代码中使用反射来获取类型,那么同样的,该类型有可能被裁剪掉,因此也需要添加到 linker.xml中。
Enable Exceptions
该设置主要用来设置如何处理程序中的异常:
- None 不需要异常支持,性能最好,发布包最小,但是任何错误都会引起系统停止;
- Explicitly Thrown Exceptions Only : 默认设置,能够捕获由代码中 throw 抛出的异常,并且能够正确执行 finally 语句块,这将会引起生成大 JavaScript 代码更长、更慢,但这通常只在代码引起瓶颈时才要考虑;
- Full Without Stacktrace:能够捕捉如下异常:
- throw 抛出的异常
- 空引用异常,Null Reference
- 数组越界
- Full With Stacktrace:和上面多了一个调用栈信息,通常应该在调试阶段使用,并且在 64 位浏览器中测试。
Data Caching
默认启用,会使用资源缓存到浏览器的 IndexedDB 数据库,在后面运行的时候不用再次从服务器下载资源,不同浏览器的缓存策略也不尽相同。
如果只是想进行空引用检查和数组越界检查,而不想完全支持异常,可以通过如下代码来支持:
using UnityEditor;
public class WebGLEditorScript
{
[MenuItem(&#34;WebGL/EnableNullChecks&#34;)]
public static void EnableNullChecks()
{
PlayerSettings.SetPropertyString(&#34;additionalIl2CppArgs&#34;, &#34;--emit-null-checks --enable-array-bounds-check&#34;,UnityEditor.BuildTargetGroup.WebGL);
}
}降低发布尺寸
WebGL 平台的内容是需要用户通过浏览器,从 Web 服务器上进行下载,所以控制好发布大小,减少用户的加载时间是非常有必要的,可以通过以下几种方式帮助实现:
- 贴图采用有损压缩格式,即 Crunch Compression;
- 不要使用开发版本发布;
- 可以酌情考虑将 Enable Exception 设置为 None;
- 启用 Strip Engine Code
- 特别关注第三方托管库,因为它可能依赖很多系统库,造成 Strip Engine Code 不能有效减少尺寸。
- 启用 Compression Format
AssetBundles
因为 WebGL 在启动之前,需要将所有的资源预加载完成,因此减少启动时间的一个有效方式就是减少系统资源,或者说减少发布资源,可以利用 AssetBundle 将资源从主包中分离出来,这样,只需要加载一个非常小的加载场景即可,另外 AssetBundle 还能帮助资源管理。
在 WebGL 平台使用 AssetBundle 有以下几点需要注意:
- AssetBundle 中包括主包中没有的类型时,会引起资源加载失败,最好是在 AssetBundle 中不要打包新类型;
- WebGL 不支持多线程,而AssetBundle 数据在 Http 下载完成后才可用,因此,AssetBundle 就需要在主线程进行解压缩,这会引起主线阻塞,LZMA 在 WebGL 平台是不可用的,因为它是整包压缩,可以采用 LZ4 压缩,它是单个资源独立压缩,即加载单个资源时,不需要解压整个资源包,如果需要更小的资源包格式,可以采用 gzip 或者 brotli 进行二次压缩,不过这需要 web 服务器进行相应的配置;
- 支持 WWW.LoadFromCacheOrDownload 方法,它采用浏览器的 IndexedDB 来实现缓存;
改变 Build 文件夹位置
当把发布完成的内容放到服务器时,可能需要将 Build 文件夹放到其他的位置,这里需要修改两个地方:
- index.html 中修改 json 文件的 url;
- 修改 json 文件中各个 url,其中的相对位置,会认为是相对 json 文件的位置。
增量编译
IL2CPP 采用增量式编译,代码的编译结果会缓存到 Library/il2cpp_cache,如果代码没有变化,将不会进行再次编译,如果需要进行重新编译,只需要删除该文件夹即可。
压缩打包
前面提过在 PlayerSetting 中可以设置发布内容的压缩格式,分别是:
- gzip : 默认格式,压缩率不如 brotli,但是打包速度要快一些,并且 http 和 https都支持;
- brotli : 压缩率最好,但是打包速度要慢,但是只有 https 协议中支持;
- disabled : 不压缩;
目前主流的浏览器都已经支持了压缩格式,能够在 http(s)传输过程中,将压缩的数据进行解压,这在效率上要比 js 进行解压要快很多。
另外,unity 在发布时也提供了 js 实现的解压器,如果浏览器解压失败的话,将会采用 js 解压的方式,当然这会带来一些性能损失,增长内容加载时间。
为了让浏览器在 http(s)传输过程中进行解压,需要Web 服务器通过 http(s)头来告诉浏览器,传输内容的压缩格式,就需要对 Web 服务器进行相应的设置。
Apache 服务器设置
设置 Build 文件夹中的 .htaccess 文件,如果没有进行创建:
gzip 压缩格式设置
<IfModule mod_mime.c>
AddEncoding gzip .unityweb
</IfModule>grotli 压缩设置
<IfModule mod_mime.c>
AddEncoding gzip .unityweb
</IfModule>IIS 服务器设置
IIS 默认是无法访问未知的 MIME 类型,所以需要先给 .unityweb 指定 MIME 类型,这里有两种方式:
- 通过 IIS Manager 界面,选择网站的 MIME Types 设置,选择添加,然后关联 .unityweb到 application/octet-stream;
- 通过网站配置文件.config文件,该文件是影响网站所有子目录的,所以只需要修改根目录即可,修改内容如下:
<?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=&#34;.unityweb&#34; />
<mimeMap fileExtension=&#34;.unityweb&#34; mimeType=&#34;application/octet-stream&#34; />
</staticContent>
</system.webServer>
</configuration>上面设置完成之后,需要设置压缩格式支持,同样在 Build目录创建 web.config文件,内容如下:
gzip 压缩
<?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=&#34;.unityweb&#34; />
<mimeMap fileExtension=&#34;.unityweb&#34; mimeType=&#34;application/octet-stream&#34; />
</staticContent>
<rewrite>
<outboundRules>
<rule name=&#34;Append gzip Content-Encoding header&#34;>
<match serverVariable=&#34;RESPONSE_Content-Encoding&#34; pattern=&#34;.*&#34; />
<conditions>
<add input=&#34;{REQUEST_FILENAME}&#34; pattern=&#34;\.unityweb$&#34; />
</conditions>
<action type=&#34;Rewrite&#34; value=&#34;gzip&#34; />
</rule>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>brotli 压缩
<?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=&#34;.unityweb&#34; />
<mimeMap fileExtension=&#34;.unityweb&#34; mimeType=&#34;application/octet-stream&#34; />
</staticContent>
<rewrite>
<outboundRules>
<rule name=&#34;Append br Content-Encoding header&#34;>
<match serverVariable=&#34;RESPONSE_Content-Encoding&#34; pattern=&#34;.*&#34; />
<conditions>
<add input=&#34;{REQUEST_FILENAME}&#34; pattern=&#34;\.unityweb$&#34; />
</conditions>
<action type=&#34;Rewrite&#34; value=&#34;br&#34; />
</rule>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>
流式 WebAssembly
从 2019.2开始,增加了 WebAssembly streaming 选项,它能够提高内容的启动速度,它是通过浏览器在下载 WebAssembly 的同时,进行 WebAssembly 的转换和编译。
Web 服务器需要进行相关配置,和配置压缩格式类似。
Apache 服务器配置
修改 .htaccess 文件
如果未压缩:
<IfModule mod_mime.c>
AddType application/wasm .wasm
</IfModule>gzip 压缩
<IfModule mod_mime.c>
AddEncoding gzip .unityweb
AddEncoding gzip .wasm
AddType application/wasm .wasm
</IfModule>brotli 压缩
<IfModule mod_mime.c>
AddEncoding br .unityweb
AddEncoding br .wasm
AddType application/wasm .wasm
</IfModule>IIS 设置
修改 Build 文件夹的 web.config 文件。
未压缩设置
<?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=&#34;.unityweb&#34; />
<mimeMap fileExtension=&#34;.unityweb&#34; mimeType=&#34;application/octet-stream&#34; />
<remove fileExtension=&#34;.wasm&#34; />
<mimeMap fileExtension=&#34;.wasm&#34; mimeType=&#34;application/wasm&#34; />
</staticContent>
</system.webServer>
</configuration>gzip 设置
<?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=&#34;.unityweb&#34; />
<mimeMap fileExtension=&#34;.unityweb&#34; mimeType=&#34;application/octet-stream&#34; />
<remove fileExtension=&#34;.wasm&#34; />
<mimeMap fileExtension=&#34;.wasm&#34; mimeType=&#34;application/wasm&#34; />
</staticContent>
<rewrite>
<outboundRules>
<rule name=&#34;Append gzip Content-Encoding header&#34;>
<match serverVariable=&#34;RESPONSE_Content-Encoding&#34; pattern=&#34;.*&#34; />
<conditions>
<add input=&#34;{REQUEST_FILENAME}&#34; pattern=&#34;\.(unityweb|wasm)$&#34; />
</conditions>
<action type=&#34;Rewrite&#34; value=&#34;gzip&#34; />
</rule>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>brotli 设置
<?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=&#34;.unityweb&#34; />
<mimeMap fileExtension=&#34;.unityweb&#34; mimeType=&#34;application/octet-stream&#34; />
<remove fileExtension=&#34;.wasm&#34; />
<mimeMap fileExtension=&#34;.wasm&#34; mimeType=&#34;application/wasm&#34; />
</staticContent>
<rewrite>
<outboundRules>
<rule name=&#34;Append br Content-Encoding header&#34;>
<match serverVariable=&#34;RESPONSE_Content-Encoding&#34; pattern=&#34;.*&#34; />
<conditions>
<add input=&#34;{REQUEST_FILENAME}&#34; pattern=&#34;\.(unityweb|wasm)$&#34; />
</conditions>
<action type=&#34;Rewrite&#34; value=&#34;br&#34; />
</rule>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>
<hr/>Part2. 在前文中,主要写了一些关于 WebGL 发布、调试、以及开发限制等问题,本章主要是关于 WebGL平台中的图形渲染、网络、音频相关的东西,WebGL 是在 OpenGL ES 发展来的,WebGL 1.0 基于 OpenGL ES 2.0,WebGL 2.0 基于 OpenGL ES 3.0,目前主流火狐和 Chrome 已经支持 WebGL 2.0,而 Safari 和 Edge 还不支持 2.0。
渲染相关
Camera Clear
WebGL 在每帧最后都会清除frame buffer,即使 Camera.clearFlags 设置了 DontClear 也不起作用,如果需要改变这种默认行为,可以通过修改发布模板中的 index.html 文件。
UnityLoader.instantiate(&#34;unityContainer&#34;, &#34;%UNITY_WEBGL_BUILD_URL%&#34;, {
Module: {
&#34;webglContextAttributes&#34;: {&#34;preserveDrawingBuffer&#34;: true},
}
});延迟渲染
Unity WebGL 平台只在支持 WebGL 2.0 下支持延迟渲染,WebGL 1.0 将会采用前向渲染。
全局光照
只支持 baked GI,实时 GI 不支持,此外,只支持 Non-Directional光照贴图。
MovieTexture
WebGL 不支持通过 MovieTexture 播放视频,一种替代的方法是,使用 Html5 的视频组件。
字体渲染
WebGL 支持动态字体,但是由于它不能访问本机文件系统,所以使用到的字体文件必须放到项目当中。(包括 Fallback fonts,以及粗体和斜体字体),另外需要注意的是,默认的Arial字体在浏览器中不支持中文,需要自行替换字体以支持。
为了提高性能,建议在 WebGL 中使用 TMP 来替代默认的 UGUI 文本组件,使用静态字体来替代动态字体。 抗锯齿
绝大部分的浏览器和 GPU 都支持抗锯齿,只需要在 Quality 设置中启用抗锯齿即可,但是在 WebGL 1.0 中存在一些限制:
- 不能在运行时启用/禁用,必须在系统发布时就确定;
- 多重采样(multi sampleing 2x、4x…)没有作用,只有开启、关闭两个状态;
- 如果 Camera 上又任何Post-Processing-Effect,都会引起抗锯齿失效;
- HDR 和抗锯齿是不兼容的,如果开启抗锯齿,就需要关闭 Camera上的 Allow HDR选项。
WebGL2.0 不存在以上限制。
网络相关
处于安全考虑,JavaScript 没有直接访问 IP 套接字的权限,所以 System.Net 命名空间下的全部类型,System.Net.Sockets 命名空间下的部分类型,在 WebGL 平台是不支持的,同样的还有 UnityEngine.Network* 前缀的类型。
如果需要访问网络,有以下几种选择:
- 使用WWW;
- 使用UnityWebRequest;
- 使用新的 Unity Networking 模块;
- 在 JavaScript 中 WebSocket 或 WebRTC。
WWW
在 WebGL 平台,WWW和UnityWebRequest都是用 JavaScript 的XMLHttpRequest来实现,通过浏览器来处理 WWW 请求。
假设我们把 WebGL 内容部署在服务器 A,而在运行时,我们需要用 WWW 从另一个服务器 B 下载资源,这就需要 B 服务器通过 CORS 进行授权,如果服务器 B 没有进行 CORS 设置,那么将会在控制台看到如下错误:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://myserver.com/. This can be fixed by moving the resource to the same domain or enabling CORS.更多 CORS 信息可以参考这里。
不要使用阻塞代码
不要是阻塞代码来等待 WWW 或 WebRequest 完成,比如下面代码:
while(!www.isDone){}因为 WebGL 是单线程,而 XMLHttpRequest 又是异步方法,整个一直执行 while 循环,而不能更新 XMLHttpRequest 响应信息,可以使用协程和 yield 来等待下载完成。
WebSocket
在新版 Unity 网络模块中,可以设置 Networking.NetworkManager.useWebSockets 来启用 WebSocket 协议。
目前浏览器基本 都支持 WebSocket 和 WebRTC(Safari 不支持),但是Unity 没有公开这两个 API,如果要使用的话,可以使用 JavaScript 插件。
音频相关
与其他平台采用 FMOD 音频引擎不同, WebGL 由于不支持多线程,因此采用基于 WebApi 的音频控制模块,让浏览器来控制音频的混合和播放,因此 WebGL 音频使用存在很多限制,下面列举 WebGL 平台支持的音频功能,如果没有列出,则说明 WebGL 平台不支持该功能。
AudioSource
支持属性:
- clip
- dopplerLevel
- ignoreListenerPause
- ignoreListenerVolume
- isPlaying
- loop
- maxDistance
- minDistance
- mute
- pitch (只支持正值)
- playOnAwake
- rolloffMode
- time
- timeSamples
- velocityUpdateMode
- volume
支持函数:
- Pause
- Play
- PlayDelayed
- PlayOneShot
- PlayScheduled
- SetScheduledEndTime
- SetScheduledStartTime
- Stop
- UnPause
- PlayClipAtPoint
AudioListener
所有的 API 都支持
AudioClip
WebGL 总是将音频剪辑压缩格式设置为 ACC,因为该格式在浏览器支持比较广泛。
支持的属性:
支持的方法:
- AudioClip.Create :部分支持,传递给该方法的 stream 参数必须为 false,并且调用时整个音频已经加载完成;
- AudioClip.SetData :部分支持,只能进行整体设置,忽略传递的 offsetSamples 参数。
SystemInfo.supportsAudio
WebGL 平台没有实现该参数,总是返回 true。
WWW.audioClip
只有浏览器原生支持的音频格式,才能够通过通过 WWW.audioClip 进行加载,详细的支持格式可以查询这里。
如果只是选择音频格式,可以看下面的表格。
视频格式选择
Microphone
不支持
<hr/>Part3.最后我们再来探讨下性能问题、内存、与浏览器的交互。
性能问题
WebGL 应用在 GPU 上的性能应该与原生应用接近,因为 WebGL 也会使用 gpu 进行硬件加速,并且从 WebGL API 转换到操作系统的图形 API 性能消耗非常小。
CPU 方面,WebGL 平台将代码转换为 WebAssembly,它的性能取决于浏览器,各个浏览器之间的对比可以参考这里。
还有一些其他因素会影响性能,这里主要是 JavaScript 不支持多线程,Unity做的很多多线程优化在 WebGL 平台不起作用。
为了达到最优的性能,建议最终发布时,将 Exception support 设置为 none。
当设置了 runInBackground之后,即使画布或者浏览器失去焦点之后,仍然会在后台运行,然后有些浏览器会对后台运行的标签进行限制,如果 WebGL 内容所在标签不可见之后,将会每秒更新一次,这将会造成 Time.deltaTime 比实际要慢,这是因为 Unity 有个 Time.maximumDeltaTime 设置,这个值默认为 0.1,Time.deltaTime 不会小于此值,就是说实际已经过了 1秒,但是系统只步进了 0.1 秒。
如果想以低帧率运行,可以通过 Application.targetFrameRate 进行设置,如果不需要限制,将该参数设置为 -1,而非一个非常高的值。
内嵌资源
有些 .net 程序集中会内嵌一些资源,默认情况下,WebGL 在打包时是不打包这些资源的,目的是为了减少内容的大小,如果有些 API 确实需要这些资源,可以通过以下代码进行设置:
using UnityEditor;
public class WebGLEditorScript
{
[MenuItem(&#34;WebGL/Enable Embedded Resources&#34;)]
public static void EnableErrorMessageTesting()
{
PlayerSettings.SetPropertyBool(&#34;useEmbeddedResources&#34;, true, BuildTargetGroup.WebGL);
}
}
当启用设置之后,项目中 .net 程序集的内嵌资源都会打包到 WebGL 发布内容中。
WebGL 内存
内存是制约 WebGL 内容复杂度的一个重要因素,WebGL 内容运行在浏览器中,可用的内存大小取决于浏览器的类型和设备类型,主要由以下几点决定:
- 浏览器是 32 位还是 64 位;
- 浏览器是否给每个标签分配单独进程;
- 浏览器的 JavaScript 引擎所需内存。
下图是 Unity WebGL 在浏览器运行时的内存情况
可以看出,除了 Unity Heap 之外,还需要浏览器分配额外的内存,理解这一点有助于帮助后续的内存优化。
DOM、Unity Heap、Asset Data、Code 这几块内存在加载之后将会常驻内存,其他的像 AssetBundle、WebAudio 等都是由代码进行控制加载和卸载。
Unity Heap
UnityHeap 包括几部分组成:
- 静态对象内存 : 和 Unity 版本和项目代码有关;
- 栈内存 : 通常在 5mb 左右;
- 动态内存 :可以增长的部分,主要用于 GC 缓冲区,运行时生成的对象都在该内存段中;
- 未分配内存;
UnityHeap 的大小可以通过 PlayerSetting->Memory Size 来设置,默认为 256Mb,在 JavaScript 中,UnityHeap 是通过 TypedArray 来表示的,即一旦分配之后就不能增大或减小,并且这部分内存是不返还给浏览器的。
通常一个空的项目只需要 16Mb 即可运行,堆内存大小可以根据项目复杂度进行调整,但是需要记住,Unity Heap越大,所能使用的用户就越少。因此在实际项目中为了尽可能的减小 Unity Heap 的大小,我们通常需要将我们的项目整个跑一遍,然后通过 Profiler 来查看最终用了多少内存,然后调整一个稍大的值(16整数倍)即可满足需要。
资源数据(Asset Data)
在编译 WebGL 项目时,Unity 会创建一个a.data文件,它包含了项目的所有场景和资源,因为 WebGL 不能访问系统的文件系统,因此只能在开始之前进行下载,然后解压到浏览器分配的一块连续内存,并且会一直常驻在内存中,所以,如果想要提高加载速度,应该尽可能的降低资源的大小。
另一种有推荐的方法是采用 AssetBundle,它具备以下几点优势:
- 加载时间完全受控,不需要再开始前进行全部加载;
- 可以进行卸载,不需要常驻内存;
- 分配在堆内存中,不需要浏览器其他额外分配内存;
- 可以使用浏览器缓存。
内存相关问题
在调试过程中如果遇到内存相关的问题,首先要区分是浏览器分配失败,还是 Unity 从预分配块中分配失败。如果时浏览器分配失败,这说明 WebGL 内容内存占用过大,应该降低资源大小;如果时 Unity 从块中分配内存失败,这说明在 PlayerSetting 中设置的内存太小,应该适当增加。
那么如何判断这两种情况?不同的浏览器可能显示的结果不同,但是一般的,如果在控制台看到类似 Out of memory的错误,通常是浏览器分配失败;如果在加载内容时发生崩溃,并且没有显示什么错误,造成这种问题的原因很多,但通常都是 Unity 内存分配失败。
Large-Allocation
服务器在相应资源请求是,可以在 Http 头中标记 Large-Allocation,它告诉浏览器(目前只有火狐支持)我要使用大量的内存,这可以解决 32 位中堆内存分配问题。
垃圾回收
在堆内存中分配的对象,当没有任何引用指向它们时,将会被当做垃圾进行回收,在 WebGL 平台也是如此。和其他平台不同的是,GC 调用的时机不同,在其他平台,为了执行GC,通常会暂停所有线程,然后检查各个线程栈进行垃圾回收,但是在 WebGL 这是行不通的,因为 WebGL 是单线程,因此只在已知栈为空时WebGL 才进行 GC 调用(目前是每帧结束调用一次)。
这通常不会有什么问题,因为每帧的产生的垃圾内存很少,但是下面一段代码时不可行的,因为 WebGL 是单线程,在循环过程中,根本没有机会执行 GC 操作,所以造成的结果就是垃圾越来越多,最后内存分配失败。
string hugeString = &#34;&#34;;
for(int i=0;i<10000;i++)
{
hugeString += &#34;foo&#34;;
}
关于内存的更多信息,可以参考Understanding Memory in Unity WebGL,和Unity WebGL Memory: The Unity Heap。
与浏览器交互
在构建 WebGL 项目中,可能会需要与 Html 中的元素进行交互,或者通过 WebAPI 实现某些功能,这些都需要和浏览器的 JavaScript 进行交互,下面介绍一下应该怎么做。
从 C# 调用 JavaScript 代码
涂启标:Unity(WebGL)与JS通讯2022最新姿势
为了调用 JavaScript 代码,首先需要将 JavaScript 文件后缀名修改为 .jslib,并且放到 Assets/Plugins/WebGL 目录,JavaScript 文件需要按照下面的格式。
mergeInto(LibraryManager.library, {
Hello: function () {
window.alert(&#34;Hello, world!&#34;);
},
HelloString: function (str) {
window.alert(Pointer_stringify(str));
},
PrintFloatArray: function (array, size) {
for(var i = 0; i < size; i++)
console.log(HEAPF32[(array >> 2) + i]);
},
AddNumbers: function (x, y) {
return x + y;
},
StringReturnValueFunction: function () {
var returnStr = &#34;bla&#34;;
var bufferSize = lengthBytesUTF8(returnStr) + 1;
var buffer = _malloc(bufferSize);
stringToUTF8(returnStr, buffer, bufferSize);
return buffer;
},
BindWebGLTexture: function (texture) {
GLctx.bindTexture(GLctx.TEXTURE_2D, GL.textures[texture]);
},
});
在 C# 端,按照下面格式进行导入。
using UnityEngine;
using System.Runtime.InteropServices;
public class NewBehaviourScript : MonoBehaviour {
[DllImport(&#34;__Internal&#34;)]
private static extern void Hello();
[DllImport(&#34;__Internal&#34;)]
private static extern void HelloString(string str);
[DllImport(&#34;__Internal&#34;)]
private static extern void PrintFloatArray(float[] array, int size);
[DllImport(&#34;__Internal&#34;)]
private static extern int AddNumbers(int x, int y);
[DllImport(&#34;__Internal&#34;)]
private static extern string StringReturnValueFunction();
[DllImport(&#34;__Internal&#34;)]
private static extern void BindWebGLTexture(int texture);
void Start() {
Hello();
HelloString(&#34;This is a string.&#34;);
float[] myArray = new float[10];
PrintFloatArray(myArray, myArray.Length);
int result = AddNumbers(5, 7);
Debug.Log(result);
Debug.Log(StringReturnValueFunction());
var texture = new Texture2D(0, 0, TextureFormat.ARGB32, false);
BindWebGLTexture(texture.GetNativeTextureID());
}
}
在传递参数和返回值时,需要注意以下几点:
- 数值类型不需要进行转换即可传输,其他类型需要转换为指针进行传输;
- 向 JavaScript 传 string 类型,需要调用 Pointer_strigify(str) 转化为 JavaScript 字符串;
- 从 JavaScript 返回 string 类型,需要使用 _malloc和stringToUTF8,具体参考上面StringReturnValueFunction方法;
- 数组类型可以通过 HEAP8等类型进行访问,具体参考上面PrintFloatArray方法;
- 要从 JavaScript 访问贴图,可以通过 GL.textures 字典,键值为 texture 的指针值。
JavaScript 调用 C# 代码
涂启标:前端JS脚本调用Unity内的函数2022
从 JavaScript 调用 C#方法,需要获取 unityInstance 对象的 SendMessage 函数,具体格式如下:
unityInstance.SendMessage(objectName,methodName,value);
- objectName : 场景中物体的名称;
- methodName : 要调用的方法的名称;
- value :传递的参数,只能值数值类型、字符串、或为空。
调用 C 方法
在其他平台如果想要调用 C 函数,需要将 C 代码编译为动态链接库,而在 WebGL 平台则是通过emscripten 工具将 C 的源码转换为 JavaScript 代码,所以需要将 C 源码文件,放到 Assets/Plugins/WebGL 目录中。C 代码的编写和其他平台类似,如果时 C++ 代码,需要导出 C 声明接口。
#include <stdio.h>
extern &#34;C&#34; void Hello ()
{
printf(&#34;Hello, world!\n&#34;);
}
extern &#34;C&#34; int AddNumbers (int x, int y)
{
return x + y;
}
WebGL 模板
Unity 在打包 WebGL时,会将 player 嵌套进一个 Html 文件中,Unity 有两个内置的 hmtl 模板可以选择,但是在正式生产中,我们可能需要根据自己的需要,进行定制,可以按照下面的方法进行模板定制。
首先必须将自定义模板放到 Assets/WebGLTemplates/模板名称 文件夹内,每个模板必须包含一个 index.html文件,依赖的资源也需要放置到该文件夹内,当模板创建完成之后,可以在PlayerSetting中选择创建的模板,可以通过 thumbnail.png(128*128 像素)文件给刚创建的模板,添加一个预览图标,方便进行选择。
index.html 文件至少需要包含三个元素:
- WebGL Loader 的脚本标签:<script src=&#34;%UNITY_WEBGL_LOADER_URL%&#34;></script>;
- 实例化脚本标签:<script> var unityInstance = UnityLoader.instantiate(&#34;unityContainer&#34;, &#34;%UNITY_WEBGL_BUILD_URL%&#34;);</script>;
- 一个 div 标签,包含一个 id 属性,其值为实例化中的第一个参数:’\\‘。
UnityLoader.instantiate(container, url, override)
container,用于展示内容的容器,通常传入 div 的 id;
url,打包时 json 文件地址,通常指定一个宏:%UNITY_WEBGL_BUILD_URL%;
override,可选项,用于重写某些预制行为。
模板中可以使用的宏
可以通过宏来获取打包内容的信息,宏的格式为 %宏名称%,在打包时会将这些宏进行展开,可用的宏列表如下:
- UNITY_WEBGL_BUILD_URL : PlayerSetting 中的 ProductName;
- UNITY_WEBGL_LOADER_URL : UnityLoader.js 的 url;
- UNITY_WEBGL_BUILD_URL : 生成的 json 文件的 url;
- UNITY_WIDTH,UNITY_WIDTH : Player 的宽度和高度(像素值);
- UNITY_CUSTOM_SOME_TAG : 如果使用了 UNITY_CUSTOM_XXX这种格式的宏,当选中这个模板时,Unity 将会在属性面板中显示一个输入框,可以设置标签的值。
添加进度条
Unity WebGL 会提供一个默认的加载进度条,当然可以进行自定义。通过重写 UnityLoader.instantiate 中的 override 的 onProgress 行为。
var unityInstance = UnityLoader.instantiate(&#34;unityContainer&#34;, &#34;%UNITY_WEBGL_BUILD_URL%&#34;, {onProgress: UnityProgress});
UnityProgress 方法包含两个参数,第一个参数表示 unityInstance 对象,第二个参数表示加载进度。
function UnityProgress(unityInstance, progress) {
if (!unityInstance.Module)
return;
if (!unityInstance.logo) {
unityInstance.logo = document.createElement(&#34;div&#34;);
unityInstance.logo.className = &#34;logo &#34; + unityInstance.Module.splashScreenStyle;
unityInstance.container.appendChild(unityInstance.logo);
}
if (!unityInstance.progress) {
unityInstance.progress = document.createElement(&#34;div&#34;);
unityInstance.progress.className = &#34;progress &#34; + unityInstance.Module.splashScreenStyle;
unityInstance.progress.empty = document.createElement(&#34;div&#34;);
unityInstance.progress.empty.className = &#34;empty&#34;;
unityInstance.progress.appendChild(unityInstance.progress.empty);
unityInstance.progress.full = document.createElement(&#34;div&#34;);
unityInstance.progress.full.className = &#34;full&#34;;
unityInstance.progress.appendChild(unityInstance.progress.full);
unityInstance.container.appendChild(unityInstance.progress);
}
unityInstance.progress.full.style.width = (100 * progress) + &#34;%&#34;;
unityInstance.progress.empty.style.width = (100 * (1 - progress)) + &#34;%&#34;;
if (progress == 1)
unityInstance.logo.style.display = unityInstance.progress.style.display = &#34;none&#34;;
}<hr/>【引用&参考】
Understanding Memory in Unity WebGL | Unity Blog
Unity WebGL Memory: The Unity Heap | Unity Blog
WebAssembly Load Times and Performance | Unity Blog
Unity(WebGL)与JS通讯2022最新姿势
前端JS脚本调用Unity内的函数2022
Unity开发优化方案:WebGL篇 | U3DC.COM |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|