发布WebGL的过程

Posted tiancaiKG

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了发布WebGL的过程相关的知识,希望对你有一定的参考价值。

  今天测试了一下发布 WebGL 的过程, 通过 Unity3D 创建, 相当麻烦, 它不仅对API有限制, 对测试Debug有限制, 也对服务器有要求, 并且现在的浏览器都很注重安全策略, 这些都增加了复杂度...

  流程大概如下:

  1. 做个简单场景, 放到 BuildSettings 里面去

  2. 如果有代码, 检查是不是有不能使用的API或是引用不能用的命名空间, 比如 System.Threading 这些, 即使引用了打包也不报错, 然后发布之后运行抛异常, 它就不动了.

  3. Build 出来的工程不能直接拖到浏览器运行, 360 / Firefox / Google Chrome 试过了都不让运行, 安全策略的问题

  4. 打开IIS服务, 创建本地服务器, 把生成的WebGL的工程拖进去, 绑定端口

  5. 添加Web.config文件, 添加各种文件流支持, 要不然浏览器会报Unexpected Token错误

  6. 使用各种浏览器直接 localhost:端口 打开都没有问题

 

  最简单的工程坑还是挺多, 按顺序看下来:

  2.1 WebGL多线程不能用, 所以Threading有关都不能用. 

  2.2 部署在服务器上, 所以文件读写都不能用, StreamingAssets的地址在本地变成了 [ http:/localhost:61281/StreamingAssets ] , 所以只能老实用WebRequest来进行下载了

  2.3 Resources文件夹下的东西还是能正常读取, 它的资源应该是会在加载时就全部下载了, 所以很大的话基本没有用户体验了, 不过小工程还是能用

  

  5.1 没有Web.config的话似乎任何传输都不正确, 就是资源 跨域/传输 之类的问题了

 

  其实还有很多问题, 中文输入法跟随啊, Shader啊.......

 

  先从搭建IIS开始:

  必须用服务器, 先打开本地的IIS服务, win10比win7快了至少10倍:

  启动完成后继续打开管理工具, 可以设置IIS了:

  在设置中设置本地硬盘映射, 直接设置到WebGL的输出目录:

  添加一个绑定端口, 免得多个地址冲突:

  设置好了之后, 需要在根目录添加 Web.config 文件支持资源类型文件传输:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <staticContent>
      <remove fileExtension=".mem" />
      <remove fileExtension=".data" />
      <remove fileExtension=".unity3d" />
      <remove fileExtension=".jsbr" />
      <remove fileExtension=".membr" />
      <remove fileExtension=".databr" />
      <remove fileExtension=".unity3dbr" />
      <remove fileExtension=".jsgz" />
      <remove fileExtension=".memgz" />
      <remove fileExtension=".datagz" />
      <remove fileExtension=".unity3dgz" />
      <remove fileExtension=".json" />
      <remove fileExtension=".unityweb" />
      <remove fileExtension=".obj" />
      <remove fileExtension=".mjs" />
      <remove fileExtension="." />
      <remove fileExtension=".assetbundle" />

      <mimeMap fileExtension=".mem" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".data" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3d" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".jsbr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".membr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".databr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3dbr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".jsgz" mimeType="application/x-javascript; charset=UTF-8" />
      <mimeMap fileExtension=".memgz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".datagz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3dgz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".json" mimeType="application/json; charset=UTF-8" />
      <mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".obj" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".mjs" mimeType="text/javascript; charset=UTF-8" />
      <mimeMap fileExtension="." mimeType="application/octet-stream" />
      <mimeMap fileExtension=".assetbundle" mimeType="application/octet-stream" />
    </staticContent>
  </system.webServer>
</configuration>

  我猜测在创建 Stream 的时候服务器会指定类型, 没有指定的就不知道怎么传了, 一般默认肯定是二进制传吧, 怎么这么无聊...

  这里有个特殊的就是无后缀的文件, 直接 "." 代表即可. "application/octet-stream" 就是二进制了吧...

  

  这样就能在本地浏览器打开了, 放在 Resources 中的 Txt 资源文件可以正常读取:

  text2.text = Resources.Load<TextAsset>("Test").text;    // Test Resources

  放在 StreamingAssets 下的 Txt 文件不能通过IO读取, 使用 UnityWebRequest 进行获取, 读取地址经过 System.Uri 加工:

    private void Start()
    {
        StartCoroutine(GetData(Application.streamingAssetsPath + "/Test.txt", (_handle) =>
        {
            text1.text = _handle.text;    // Test streamingAssetsPath
        }));
    }

    IEnumerator GetData(string loadPath, System.Action<DownloadHandler> succ)
    {
        var uri = new System.Uri(loadPath);
        UnityWebRequest www = UnityWebRequest.Get(uri.ToString());
        yield return www.SendWebRequest();

        if(www.isNetworkError || www.isHttpError)
        {
            Debug.Log(www.error);
        }
        else
        {
            Debug.Log(www.downloadHandler.text);
            succ.Invoke(www.downloadHandler);
        }
    }

  

  不过如果在Build的时候选择 [ Build & Run ] 的话, 它会临时起一个服务器来跑打包出来的工程, 不需要IIS也是可以的......

 

(2020.07.22)

  最近想要测试一下资源远程加载的方案, 于是把加载逻辑添加了通过 UnityWebRequest 方式获取 AssetBundle 的逻辑, 然而这又是一个坑, 在编辑器下, 可以从网站上获取到 AssetBundle, 可是如果发布到服务器上, 并且资源在其它服务器, 就产生了一个跨域问题, 然后资源是无法传输的, 虽然理解这是 http 服务器设计的问题, 可是我网上查了半天也没解决, 真是神奇了...

  首先把打出的包放到服务器上 : 

  然后使用服务器的路径来下载包 : 

  看到 Sprite 的 AssetBundle 可以正确通过网址下载来, 并且正确加载出来了, 可是发布到服务器后 (资源服务器 localhost:12354, 运行服务器 localhost:44599), 因为跨域问题, 无法获取了 : 

XMLHttpRequest cannot load http://localhost:12354/unityassets/AssetBundleManifest. No \'Access-Control-Allow-Origin\' header is present on the requested resource. Origin \'http://localhost:44599\' is therefore not allowed access.

  然后找跨域的相关描述, 是需要服务器添加一个 Access-Control-Allow-Origin 相关的返回? 

  在360浏览器里面可以查看到各个 http 请求的信息, F12 -> NetWork -> XHR -> xxxxx

  找到很多论坛都说添加一个 customheader 就行了, 下面这样 (https://enable-cors.org/server_iis7.html) :

<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <system.webServer>
   <httpProtocol>
     <customHeaders>
       <add name="Access-Control-Allow-Origin" value="*" />
     </customHeaders>
   </httpProtocol>
 </system.webServer>
</configuration>

  那么加到原有的 Web.config 文件里 : 

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
      </customHeaders>
    </httpProtocol>
    <staticContent>
      <remove fileExtension=".mem" />
      <remove fileExtension=".data" />
      <remove fileExtension=".unity3d" />
      <remove fileExtension=".jsbr" />
      <remove fileExtension=".membr" />
      <remove fileExtension=".databr" />
      <remove fileExtension=".unity3dbr" />
      <remove fileExtension=".jsgz" />
      <remove fileExtension=".memgz" />
      <remove fileExtension=".datagz" />
      <remove fileExtension=".unity3dgz" />
      <remove fileExtension=".json" />
      <remove fileExtension=".unityweb" />
      <remove fileExtension=".obj" />
      <remove fileExtension=".mjs" />
      <remove fileExtension="." />
      <remove fileExtension=".assetbundle" />

      <mimeMap fileExtension=".mem" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".data" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3d" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".jsbr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".membr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".databr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3dbr" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".jsgz" mimeType="application/x-javascript; charset=UTF-8" />
      <mimeMap fileExtension=".memgz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".datagz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".unity3dgz" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".json" mimeType="application/json; charset=UTF-8" />
      <mimeMap fileExtension=".unityweb" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".obj" mimeType="application/octet-stream" />
      <mimeMap fileExtension=".mjs" mimeType="text/javascript; charset=UTF-8" />
      <mimeMap fileExtension="." mimeType="application/octet-stream" />
      <mimeMap fileExtension=".assetbundle" mimeType="application/octet-stream" />
    </staticContent>    
  </system.webServer>
</configuration>

  还是报错, 我就纳闷了, 然后有些人在 Unity 请求代码里面添加了一些头, 我也添加之后测试仍然报错, 无用 : 

    var unityWebRequest = UnityWebRequest.GetAssetBundle(url, 0);

    unityWebRequest.SetRequestHeader("Access-Control-Allow-Credentials", "true");
    unityWebRequest.SetRequestHeader("Access-Control-Allow-Headers", "Accept, X-Access-Token, X-Application-Name, X-Request-Sent-Time");
    unityWebRequest.SetRequestHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
    unityWebRequest.SetRequestHeader("Access-Control-Allow-Origin", "*");

    var request = unityWebRequest.SendWebRequest();
    // ...

  换了个错误再来一遍, 变成了认证错误之类的....

  继续再找, 看到一个说 IIS 跨域还要安装一个 CORS Module 的东西的 : 

I had a similar issue recently. Most tutorial/documentation only suggests adding custom headers in the configuration. But this does not tell IIS to handle the CORS Pre-flight request by itself.

To do so, you must install the CORS Module in IIS and add some configuration in the web.config file, as explained here: IIS CORS module Configuration Reference

  好吧, 进入微软找 CORS Module (https://www.iis.net/downloads/microsoft/iis-cors-module), 下载安装之后, 再打包一次, Unity 代码也使用最简单的看看 : 

            public void SendWebRequest()
            {
                if(request == null)
                {
                    var unityWebRequest = this.hash.HasValue ? UnityWebRequest.GetAssetBundle(url, hash.Value, 0) : UnityWebRequest.GetAssetBundle(url, 0);
                    request = unityWebRequest.SendWebRequest();
                    request.completed += OnLoaded;
                }
            }

  结果居然可以读取了, 反正不知道是不是安装了 CORS Module, 能用就行了 : 

  没想到部署个 WebGL 测试也这么多幺蛾子, 这些服务器就不能给个省心的逻辑吗, 要啥功能给个界面式的功能列表也好啊, 如果明天用阿帕奇服务器, 又是查资料查半天, 心累...

 

   然后看一下各个默认文件夹在运行时的位置 : 

    Debug.Log("Application.dataPath : " + Application.dataPath);    // http://localhost:44599
    Debug.Log("Application.persistentDataPath : " + Application.persistentDataPath);    // /idbfs/9ed0d20bc957a25b21e872bbf1c2f671
    Debug.Log("Application.streamingAssetsPath : " + Application.streamingAssetsPath);    // http://localhost:44599/StreamingAssets
    Debug.Log("Application.temporaryCachePath : " + Application.temporaryCachePath);    // /tmp
    Debug.Log("Caching.currentCacheForWriting.path : " + Caching.currentCacheForWriting.path);    // /idbfs/9ed0d20bc957a25b21e872bbf1c2f671/UnityCache/Shared
    Debug.Log("System.Environment.CurrentDirectory : " + System.Environment.CurrentDirectory);    // /

  Application.dataPath 和 Application.streamingAssetsPath 是服务器的相对路径, 所以服务器资源可以正常读取, 而其它的应该都是本地路径, 至于这个路径在哪, 可能是浏览器的缓存路径, 我用360浏览器, 直接到下面去找看看 : 

  我看论坛他们是说用的 IndexedDB 存储临时文件的, 不知道用什么开来看...

  发现浏览器自带了查看器的样子, F12 -> Appliction ->  IndexedDB -> xxxx

   不知道这些是不是本地缓存, 我在初始界面就有一个加载代码 : 

    void Start()
    {
        // 屏幕右上角 
        AssetBundleMaster.Core.ABM_ResourceLoader.Instance.LoadAsync<Sprite>("Sprites/Pic002PNG", (_pic) =>
        {
            image.sprite = _pic;
            Debug.Log("Loaded Sprite " + _pic + " : " + Time.frameCount);
        });
        // 屏幕左下角
        rawImage.texture = AssetBundleMaster.Core.ABM_ResourceLoader.Instance.Load<Texture2D>("Textures/Pic001");
        Debug.Log("Loaded Texture2D " + rawImage.texture + " : " + Time.frameCount);
    }

  看看删除 IndexedDB 相关文件夹后第一次运行的情况, 因为屏幕左下角的读取使用的是同步读取 (UnityWebRequest 发送请求后马上返回) : 

  这样结果左下角是肯定没有图片的, 因为 UnityWebRequest 是远程请求, 必定不能马上得到结果...

  这时候发起的资源请求有3个 (使用Hash128作为参数的请求) : 

assetBundleCreateRequest = UnityWebRequest.GetAssetBundle(loadPath, assetBundleManifest.GetAssetBundleHash(this.assetName), 0).SendWebRequest();
    [RuntimeInitializeOnLoadMethod]
    private static void StartUpRun()
    {
        AssetBundleMaster.Core.ABM_AssetLoadManager.Instance.OnModuleLoaded(() =>
        {
            Debug.Log("OnModuleLoaded At : " + Time.frameCount);

            AssetBundleMaster.Core.ABM_SceneLoader.Instance.LoadScene("Scenes/S1");
        });

    }

  1. 场景 : s1.assetbundle

  2. 右上角图片 : common.assetbundle

  3. 左下角图片 : pic001.assetbundle -- 因为是异步请求, 同步回调没有获得图片, 可是也进行了下载

  这时看到 IndexedDB 中显示的也是这三个文件 : 

   然后我关闭浏览器, 从新再打开网址 : 

  再打开一次的话, 异步加载的左下角图片, 居然在同步回调里面就能获取图片了, 这难道就是本地缓存的威力吗? ( 2020.07.28 -- 经测试并不一定能在同步回调中返回, 应该跟IO速度有关, 并不是进行了同步加载 ). 我再点击一下按钮加载一张新的图片覆盖左下角, 看看 IndexedDB中是不是有了新的图片了 : 

  左下角的图片改变了, 看看本地缓存 : 

  资源变成4个了, 关闭浏览器从新加载网页看看, 如果不是本地缓存的话, 不操作应该还是只会加载3个资源. 

  重新加载后还是4个资源, 说明 FILE_DATA 这个数据库这就是本地缓存无疑了. 当我们的 AssetBundleManifest 里面获取的 Hash128 跟本地不一样的时候, 就会去下载最新包了. 当然如果是 WebGL 的话 AssetBundleManifest 直接放服务器的 StreamingAssets 文件夹下就行了, 而资源一般会放到CDN服务器上, 所以前面搞了半天跨域的问题. 当然远程资源+缓存的模式, 也能作为PC, android, ios之类的平台加载逻辑也是可行的, 并且有 Cache.expirationDelay 这些自动删除逻辑在, 用不到的缓存资源能自动删除, 省了更新删除逻辑了...

  测试一下看看, 给另一个按钮添加清除缓存的功能 :

    clearBtn.onClick.AddListener(() =>
    {
        if(Caching.ClearCache())
        {
            Debug.Log("ClearCache Succ");
        }
        else
        {
            Debug.Log("ClearCache FAILED");
        }
    });

  ClearCache FAILED ......

  清除缓存失败, 这又是什么神操作, 找了下资料, 说要 Unload 掉所有已经加载的 AssetBundle 之后才能行, 修改代码来硬核一点的试试 : 

    void Start()
    {
        clearBtn.onClick.AddListener(() =>
        {
            StartCoroutine(ClearCache());
        });
    }
        
    IEnumerator ClearCache()
    {
        AssetBundle.UnloadAllAssetBundles(true);
        yield return new WaitForSeconds(1.0f);
        System.GC.Collect(0);
        yield return new WaitForSeconds(1.0f);
        yield return Resources.UnloadUnusedAssets();
        yield return new WaitForSeconds(1.0f);
        if(Caching.ClearCache())
        {
            Debug.Log("ClearCache Succ");
        }
        else
        {
            Debug.Log("ClearCache FAILED");
        }
    }

  居然还是不行!!! 震惊!! 删除了 AssetBundle 之后, UI 都变黑了 : 

  这就无语了, 虽然不是很大问题......

(2020.11.19)

  补充一下在Windows下的缓存测试, 缓存路径在 C:\\Users\\XXXX\\AppData\\LocalLow\\Unity\\DefaultCompany_工程名称     文件夹下, 设定了 expirationDelay 之后, 需要程序下载对象, 然后超过过期时间程序还在运行, 它才能帮你删除对应的缓存(在程序退出的时候), 也就是一个运行期间的功能, 不是 Cookie 那样通过系统维护的东西. 也就是说运行时间需要超过设定时间, 才会删除缓存, 非常搞笑......

 

 (2020.07.28)

  今天才发现 IndexedDB 其实就是键值对, 里面存储的数据能够跟 AssetBundle 对应得上 : 

  今天新打的包在浏览器的缓存中查看, 它的 __data 大小跟服务器上的原始文件 s1.assetbundle 大小是一样的 : 

  这样就知道它在浏览器中的缓存是以 [/idbfs/用户ID/UnityCache/Shared/文件名/文件哈希值/__data] 的key来做存储的, 还不知道它的用户ID是怎样来的, 不过默认路径根目录可以通过 Caching.currentCacheForWriting.path 来获得, 得到的是 : 

/idbfs/bc9cc5aa7eef26ca01726eef3f44ae31/UnityCache/Shared

  这个路径, 如果可以调用 IndexedDB 的API的话, 也是可以自己查询的...

 

  

 

以上是关于发布WebGL的过程的主要内容,如果未能解决你的问题,请参考以下文章

Phong 和 Gouraud 着色 WebGL

发布WebGL的过程

如何为每个 WebGL 三角形设置单独的颜色?

为啥我需要在 webgl 着色器中定义一个精度值?

如何在伪造查看器中将自定义材质添加到片段

WebGL 创建和初始化着色器过程