» VR開発メモトップへ

Unity WebGLビルドメモ

最終更新日:2022年05月11日

UnityでWebGLビルドを使用するときのTipsをメモしていく場所です。

更新履歴

(2022年5月11日)「使用できるレンダリングパイプラインは?」を追加
(2022年5月10日)「表示をウィンドウ全体に拡げるには」を追加
(2022年5月9日)「マルチタッチでOnPointerUp/OnEndDragが送られてこない」を追加
(2022年5月8日)「出力するウェブページについて」を追加
(2022年5月7日)「ブラウザでのパフォーマンス計測方法」にDevelopment Buildについて追記


UnityのWebGLビルドについて

Unityのバージョンについて

まず、Unity 2021.2以降にするのがおすすめです。Unity 2021.2でEmscriptenがバージョン2に更新され、ビルドが速くなり、出力サイズが小さくなっています。また、モバイルブラウザで圧縮テクスチャや圧縮オーディオがサポートされています。

さらにUnity 2022.1ではiPhone/Androidで実行するときの警告表示がなくなりました。スマートフォン向けのプロジェクトでも徐々に使っていけそうです。

Unityが出力するファイルについて

UnityのWebGLビルドは、WebAssemblyの実行ファイル(.wasm)とアセットをパッケージしたデータファイル(.data)、それらをロードするためのHTMLやJavaScriptファイルを出力します。これらのファイル一式を任意のウェブサーバーでホストすることで配信ができます。

WebAssemblyは、C#からIL2CPP(Unity製)で変換されたC++コードをEmscriptenでコンパイルし、UnityのランタイムモジュールのDLLとリンクして生成しているようです(このあたりもう少し追いかけたいです)。

WebAssemblyについて

WebAssemblyはW3Cで仕様の策定が進められているCPUアーキテクチャに依存しない実行バイナリ形式で、ブラウザのJavaScriptエンジンに搭載されたWebAssembly専用のパスを使用してネイティブコードにコンパイル・実行されます。

ブラウザ JavaScriptエンジン コンパイラの解説
Chrome V8 WebAssembly compilation pipeline
Safari JavaScriptCore JavaScriptCore’s new WebAssembly interpreter
Firefox SpiderMonkey Firefox’s new streaming and tiering compiler

どのブラウザのエンジンも何段かのコンパイラを持ち、.wasmをロードしつつ速やかに(低速で)実行を開始する一方、バックグラウンドでより最適化されたネイティブコードを生成して徐々に入れ替えていくという挙動のようです。このため、ユーザーが.wasmファイルをロードしてコンパイルがある程度進行するまで、フレームレートの低下やヒッチが発生する場合があります。

使用できるレンダリングパイプラインは?

ビルトインまたはURPが使用できます。HDRPはWebGLビルドでは動作しません。URPを使用するとビルトインよりも出力サイズが若干大きくなります。

出力するウェブページについて

Player SettingsのResolution and Presentationで、出力するHTMLのテンプレートを選択できます。Unity公式ではWebGL templatesに説明があります。

Unityの下記フォルダにデフォルトのテンプレート群が含まれていて、テンプレートのフォルダをプロジェクトのAssets/WebGLTemplatesフォルダにコピーして改造することで、アプリケーションにあわせてページをカスタマイズできます。

C:\Program Files\Unity\Hub\Editor\{Unityのバージョン}\Editor\Data\PlaybackEngines\WebGLSupport\BuildTools\WebGLTemplates

表示をウィンドウ全体に拡げるには

Unity 2021.2以降のテンプレートを、Unityの画面がPCとスマートフォンでブラウザのウィンドウ全体に表示されるように改造してみます。上記フォルダからDefaultフォルダをWebGLTemplatesフォルダにコピーして、TemplateData/style.cssに以下を追記します。

/* Unityの画面をウィンドウ全体に拡げる */
#unity-container, #unity-canvas {
    width: 100%;
    height: 100%;
}

/* フッターのロゴ等を隠す */
#unity-footer {
    display: none;
}

/* 縦スクロールバーを隠す */
body {
    overflow: hidden;
}

さらにindex.htmlにiPhone/Android対応のための箇所があるのですが、ウィンドウの拡大に支障があるのと、ページがちらついたりと不都合なので消してしまいます。

if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
  ...
}

の条件式をelse節を含めてすべて消して、headタグの中に

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

を追加してみてください。

.wasm/.dataファイルの圧縮

Unityが出力する.wasmファイルや.dataファイルはそこそこ巨大になります。ウェブサーバーから配信する際に、GzipやBrotliで圧縮するとネットワーク転送量およびダウンロードにかかる時間が小さくなります。UnityではProject Settings > Playerの設定によって出力ファイルが以下のように変わります。

Decompression Fallbackがオンの場合

*.data.unityweb
*.framework.js.unityweb
*.wasm.unityweb

Decompression FallbackがオフでCompression FormatがGzipの場合

*.data.gz
*.framework.js.gz
*.wasm.gz

Decompression FallbackがオフでCompression FormatがBrotliの場合

*.data.br
*.framework.js.br
*.wasm.br

Compression FormatをGzipにすると、Uncompressedのときに比べて出力サイズが4分の1くらいに小さくなります。Brotliにするとさらに4分の3くらいのサイズになりますが、圧縮にとても時間がかかるため開発中は使用しないほうがいいかもしれません(ビルドの最終段でシングルスレッドのbrotli.exeで長時間止まります)。

注意点として、Gzip・Brotli圧縮したファイルをウェブサーバーで配信するときにはContent-Encodingヘッダを付与する必要があります。Decompression FallbackをオンにするとContent-Encodingヘッダなしでも読み込めるようになりますが、ブラウザで表示するときに展開に時間がかかります。

Content-Encodingヘッダの設定方法は配信手段によって異なるので調べてください。下記はAWS S3に.brファイルのみContent-Encoding: brを付与してアップロードするコマンドの例です。

aws s3 sync . s3://example.com/ --exclude "*.br"
aws s3 sync . s3://example.com/ --exclude "*" --include "*.br" --content-encoding "br"

設定をスクリプトで変更する

エディタ拡張のメニューやCIでビルドする際に、スクリプトから圧縮フォーマットを変更するには以下のようにします。

PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Brotli;
PlayerSettings.WebGL.decompressionFallback = false;

ビルドを速くするには

Project SettingsのIL2CPP Code GenerationをFaster (smaller) buildsにするとIL2CPPのコンパイル速度が倍くらい速くなります。

また、上の項目の通り、Compression FormatをBrotliにすると圧縮に非常に長い時間がかかるので、開発中はGzipかDecompressedにしておくのがおすすめです。

ビルドサイズを小さくするには

Unityのビルドサイズを小さくするには」を参照してください。

URP、TextMesh Pro、Input System、UI Toolkitあたりは依存モジュールや付随するアセットが多く、使用するとサイズが大きくなりがちなので避けたほうがいいかもしれません。

テクスチャフォーマットについて

Unityのドキュメントの下記ページに、プラットフォームごとの対応テクスチャフォーマットの表(「Supported texture formats, by platform」)があります。

ページのUnity 2021.2の版から、WebGLの列が「WebGL (Desktop Browsers)」と「WebGL (iOS and Android browser)」に分かれています。

困ったことに、PCブラウザとモバイルブラウザで対応しているテクスチャフォーマットが異なります。ブラウザが対応していないフォーマットのテクスチャを読み込むと、「format is not supported, decompressing texture」の警告ログが出てテクスチャがソフトウェアで展開され、アプリの起動に時間がかかり、メモリも大きく消費しますので、どのフォーマットを使用するか検討する必要があります。

Unity 2021.2.13でリニアカラースペースのときに各フォーマットのテクスチャをビルドして読み込むと以下のようになりました。

RGB(A) Compressed ASTC

プラットフォーム 結果
iPhone 12 Pro 警告が出ない
Android (Pixel 4a) 警告が出ない
Windows Chrome 警告が出る
macOS Safari 警告が出ない

RGBA Compressed ETC2 / RGBA Crunched ETC2

プラットフォーム 結果
iPhone 12 Pro 警告が出る
Android (Pixel 4a) 警告が出ない
Windows Chrome 警告が出る
macOS Safari 警告が出る

(ドキュメントの表ではiPhoneのETC2圧縮はyesになっているのですが……)

RGBA Compressed DXT / RGBA Crunched DXT

プラットフォーム 結果
iPhone 12 Pro 警告が出る
Android (Pixel 4a) 警告が出る
Windows Chrome 警告が出ない
macOS Safari 警告が出ない

上記の結果から、以下のようにするのがよさそう(?)です。

  • iPhone・Androidに対応するならBuild SettingsのTexture CompressionをASTCにする。また、個別のインポート設定はRGB(A) Compressed ASTCを使用する。圧縮率最優先ならRGBA Crunched ETC2を使ってもよさそう(?)
  • Windows・Macのみに対応するならBuild SettingsのTexture CompressionをDXTにする。また、個別のインポート設定はRGBA Compressed DXTまたはRGBA Crunched DXTを使用する
  • 非圧縮でなければならないテクスチャはRGBA16bit等を使う(PC、モバイルどちらも対応している)。
  • PCとモバイル両方に最適化するなら実行ファイルを分けてそれぞれテクスチャフォーマットを変えてビルドする

参考として、比較的簡単なプロジェクトでBuild SettingsのTexture CompressionをDXTからASTCに変更した場合に、テクスチャ読み込み時間がiPhone 12 Proで6秒から1秒に、PCで1秒から2秒に変わりました。

なお、Unity公式のロードマップに「Basis Universal Texture Support」があります。Basis Universalはさまざまなテクスチャフォーマットに変換するための中間フォーマットで、将来的にはこれを利用してすべてのブラウザ環境に対応するものと思われます。

デバッグ方法

ブラウザのデバッグコンソール

ブラウザの開発者用コンソールにエラーやDebug.Logの出力が表示されます。Chromeでは右上のメニューからその他のツール > デベロッパーツールを開いてConsoleタブを開きます。

iPhoneのSafariは、Macに接続すれば開発者コンソールを開けます。iPhoneの 設定 > Safari > 詳細 > Webインスペクタ(Settings > Safari > Advanced > Web Inspector)をオンにして、macOSのSafariのDevelopメニューから端末名 > Automatically Show Web Inspector for JSContexts、Connect via Network等をオンにしてiPhoneでページを開くとWeb Inspectorが開きます。

AndroidのChromeも同じくリモート接続ができます(詳細略)。

謎のエラーダイアログで止まる

ブラウザでの実行時にエラーダイアログが出る場合、Build Settings > Development BuildおよびProject Settings > Debug Symbolsを有効にしてビルドすると、どこでクラッシュしているか分かるようになります。

SRDebugger

実行時にコンソールを開けるSRDebuggerというアセットがありますが、WebGLビルドでも使用できてスマートフォン等でも使えるので便利です(画面の左上隅を3回タップするとコンソールが開けます)。なお、インポートするとビルド後の出力サイズが1MBほど増えます。

パフォーマンス最適化

Unityのプロファイラを使用する

UnityのプロファイラはWebGLターゲットでも使用できます。Build SettingsでDevelopment BuildとAutoconnect ProfilerをオンにしてBuild And Runで実行するとプロファイラが開きます。

  • Timeline表示にすると1フレームのCPUの処理内容が確認できます(0.1ms単位でレポートされるようです)。
  • GPUプロファイリングには対応していません。
  • Renderingの表示列で、ドローコール・SetPassコール、頂点数、テクスチャ容量等を確認できます。

なお、Unity 2022.1では、以下のメッセージがコンソールに大量に出力されてプロファイラが使用できない不具合があります。

Connection [number] is no longer valid. Calling auto disconnect.

ブラウザのプロファイラを使用する

ブラウザに搭載されている開発者向け機能を使用して、Unityが出力するWebAssemblyのパフォーマンス計測ができます。特に、UnityでBuild SettingsのDevelopment BuildをオンにしてビルドするとC#のメソッド名が見えるようになります。

また、プロファイルを開始してからページを読み込むと起動の待ち時間の詳細を追うことができます。

Chrome

PCでは、デベロッパーツール(右上のメニューボタン > その他のツール > デベロッパーツール)を開いてPerformanceタブを開き、左上の赤い丸をクリックして数秒ほど待って停止すると結果が表示されます。

Safari

macOSのSafariでは、Deveop > Show Web InspectorでTimelinesタブを開いて同じく左上の赤い丸をクリックして止めるとプロファイリングができます。JavaScript & Eventsタブで、各フレームのCPU実行時間を確認できます(「Animation Frame [num] Fired」からUnityが出力したWebAssemblyが呼ばれています)。

iPhone/iPadでもUSB接続したmacOSのSafariのWeb Inspectorで実行時間の詳細を確認できます。

フレームレートを下げるには

QualitySettings.vSyncCountを2以上にします。Application.targetFrameRateは効かない?ようです。

描画解像度を下げるには

Assets/WebGLTemplatesフォルダに入っているテンプレートのindex.htmlで、configを作成しているところにdevicePixelRatioを設定します(各デバイスのデフォルト値を把握していないので要調査)。

var config = {
  ...
  productName: "{{{ PRODUCT_NAME }}}",
  productVersion: "{{{ PRODUCT_VERSION }}}",
  devicePixelRatio: 1,
};

動画の再生について

iOSでは動画の再生に諸々の制限があり注意です(詳細はそのうち)。

ブラウザのURLが知りたい

Application.absoluteURLで取得できます。ブラウザで実行しているかどうかの判定にも使用できます。

不具合・トラブルのメモ

iPhoneでクラッシュする

iOS 15.4にて、UnityのWebGLビルドで下記のようなエラーダイアログが表示されて動作が停止する不具合が発生しています。

RuntimeError: call_indirect to a null table entry (evaluating 'dynCall_iiii(index,a1,a2,a3)')...

RuntimeError: Out of bounds memory access (evaluating 'asm[name].apply(null, arguments)')<?>.wasm-function[il2cpp::vm::GlobalMetaData::GetContainerDeclaringType...

RuntimeError: Out of bounds memory access (evaluating 'dynCall_ii(index,a1)')<?>.wasm-function[728]@[wasm code]...

iOS Safari側の問題らしく、WebKitのWebAssemblyのロードに関連しているようです。Unityのエンジニアより、IL2CPPの特定の関数についてClangの最適化を無効にする回避策が発見されています。手元ではこの方法で動作するのを確認しました。

Windowsでは、以下のファイルを開き、

C:\Program Files\Unity\Hub\Editor\{Unityのバージョン}\Editor\Data\il2cpp\libil2cpp\metadata\GenericMetadata.cpp

「const Il2CppType* GenericMetadata::InflateIfNeeded」関数を検索して、前後を「#pragma clang optimize off」と「#pragma clang optimize on」で囲って管理者権限で保存します。修正後、確実に再コンパイルするにはプロジェクトのLibrary/Beeフォルダを削除します。

iPhoneでApplication.OpenURLが効かない

JavaScriptでページ移動をすればオーケーです。「UnityからJavaScriptの関数を呼ぶには」を参照してください。

モバイルでソフトウェアキーボードが表示されない

Unity 2022.1からInputFieldでソフトウェアキーボードが表示されるようになっています。

Standaloneビルドと比べて描画品質が低い

WebGLビルドのときだけ影がギザギザになったりしている場合は、Project Settings > Qualityをチェックしてみてください。WebGLビルドはデフォルトで低めの描画品質に設定されています(Unityに慣れていないと盲点だと思います)。

WebGLビルドでアンチエイリアスが効かない

Unity 2021.2でMSAAが有効にならない不具合がありました。Unity 2021.2.15で修正されたようです。

iPhoneでフレームがきちんとクリアされない

MSAAを有効にしていると発生するようです(Unity 2021.2、2022.1で確認)。解像度が十分に高いので、iPhoneではMSAAはオフにしておくのがよさそうです。

Textコンポーネントで日本語フォントが表示されない

Textコンポーネントで、デフォルトのArialでは日本語が表示されません。プロジェクトに(権利的に使用可能な)日本語フォントファイルをインポートして指定してください。

なおWebGLビルドでは、Font.GetOSInstalledFontNamesはPCでもモバイルでも使用できない(空配列が返ってくる)ようです。

マルチタッチでOnPointerUp/OnEndDragが送られてこない

Unity 2021.3で、マルチタッチ操作をしているときに指を離してもIPointerUpHandlerとIEndDragHandlerのイベントが発生せずボタン類が押しっぱなしになる現象が起きました。Unity 2022.1だと大丈夫のようです。

オーディオが停止しない

AudioClipのLoad TypeをDecompress On Load以外にしているとAudioSource.Stopで止まらない現象が起きました。

Standaloneビルドのときとライトの当たり方が違う

カメラが移動したときにライティングがパカパカ変わることがありました。Directional Lightが複数あったのが原因でした。

AssetBundleについて

AssetBundleはWebGLビルドでも使用できます。大きいゲームやアプリケーションでは、最初のシーンを軽量にして、AssetBundleで逐次追加のシーンを読み込むようにすると起動が速くなります。

AssetBundleのロード時に固まる

Unity 2021.3時点ではWebGLビルドはマルチスレッドに対応しておらず、AssetBundleのダウンロード完了時にデータを一気に展開しメインスレッドをブロックします。このためリアルタイムゲーム等のインゲーム中にバックグラウンドでAssetBundleの読み込みを行うのは難しいです。

WebGL does not support threading, but http downloads only become available when they have finished downloading. Because of this, Unity WebGL builds need to decompress AssetBundle data on the main thread when the download is done, blocking the main thread.

なお、PlayerSettings.WebGL.threadsSupportというexperimentalなAPIがありますが、これを有効にしてしまうと以下のエラーでビルドが通らなくなります

Internal build system error. BuildProgram exited with code -2147024809.

修復するには、ProjectSettings.assetのwebGLThreadsSupportを0に戻します。

マテリアルが正常にロードされない

エディタがWebGLプラットフォームのときには、WebGLプラットフォームではなくStandaloneプラットフォームでビルドしたAssetBundleをロードする必要があります。

Addressable Asset Systemは使用できる?

AddressablesはWebGLプラットフォームでも使用できるようです。wasmのサイズがいくらか大きくなります(TODO:ある程度数字を出す)。

前述の問題で、WebGLのバンドルをリモートから読み込むとマテリアルが剥がれます。Use Asset Databaseを使うか、Standloneプラットフォームでバンドルをビルドして再生します。

異なるドメインからAssetBundleをロードするには

ブラウザのセキュリティにより、異なるドメインのウェブサーバーからAssetBundleをロードする場合、ウェブサーバーでCORS(Cross-Origin Resource Sharing)の設定をしてアクセスを許可する必要があります(配信方法によって異なるのでぐぐってください。たとえばAmazon S3だとbucketのPermissionsの下にCORSの設定があります)。

CORS有効のローカルウェブサーバーを立てたい場合、Node.jsのhttp-serverを使うのが簡単です。

$ npm install -g http-server
$ http-server -p 8080 --cors

AssetBundleで使用しているコンポーネントが読み込まれない

AssetBundleをロードした際にブラウザのコンソールに以下のようなエラーが出る場合は、AssetBundleで使用しているコンポーネントのコードがProject SettingsのStrip Engine Codeで削除されてしまっています。

Could not produce class with ID ??.

Build Report InspectorのStrippingタブで使用したいコンポーネントが含まれているか確認します。

link.xmlという名前のファイルをAssetsフォルダ内に配置してコンポーネントを指定すると保持されるようになります。Unityの公式ドキュメントではこちらに説明があります。

<linker>
    <assembly fullname="UnityEngine">
        <type fullname="UnityEngine.MeshCollider" preserve="all"/>
    </assembly>
</linker>

JavaScriptプラグイン(.jslib)の書き方

JavaScriptのプラグインを作ることができます。UnityのWebGLビルドではできない処理や、手間のかかる処理をブラウザ側で実行し、相互のやり取りができます。

Unity公式では下記ページに最低限の説明があります。

公式の情報が少ないのですが、jslibはEmscriptenの仕組みをほぼそのまま用いているというのが重要ポイントです。

gtk2kさんの記事が非常に詳しくおすすめです。

以下は個人的なメモです。Unity 2021.2以降(Emscripten 2.0.19)を想定しています。

UnityからJavaScriptの関数を呼ぶには

JavaScriptを呼ぶ最小限のプラグインです。下記ファイルをPluginsフォルダに配置してBrowser.OpenURLでブラウザのページが遷移します。

(Browser.cs)

using System.Runtime.InteropServices;

public class Browser
{
    [DllImport("__Internal")]
    public static extern void OpenURL(string url);
}

(Browser.jslib)

mergeInto(LibraryManager.library, {
  OpenURL: function(url) {
    window.open(UTF8ToString(url), '_self');
  },
}

JavaScript側での文字列の受け取りにUTF8ToString関数を使用しています。

プラグインを書くときに参考になるコード

Unity本体に、UnityのAPIで使用されているjslibや、EmscriptenのJavaScriptコードが含まれていて、jslibを自作するときの参考になります。以下の場所にあります。

C:\Program Files\Unity\Hub\Editor\{version}\Editor\Data\PlaybackEngines\WebGLSupport\BuildTools\lib
C:\Program Files\Unity\Hub\Editor\{version}\Editor\Data\PlaybackEngines\WebGLSupport\BuildTools\Emscripten\emscripten\src\library_*.js

JavaScriptの関数間で変数を共用したい

以下の要領でプライベートアクセスの変数を使用できます。

var LibraryWebGL = {
  $local: {
    name: null,
  }
    
  Initialize: function () {
    local.name = 'korinVR';
  },
};

autoAddDeps(LibraryWebGL, '$local');
mergeInto(LibraryManager.library, LibraryWebGL);

.jslibからページのJavaScriptを呼ぶには

WebGLTemplatesフォルダのテンプレートに含まれているindex.htmlを書き換えて、scriptタグからたとえば以下のようなスクリプトを読み込むと、.jslibからLoadSpeed.getElapsesdTime()でページを開いてからの経過時間を取得できます。

var startTime = Date.now();

LoadSpeed = {
  getElapsedTime: function () {
    return Date.now() - startTime;
  }
};

またこれの延長で、.jslibはUnityとのやり取りに留めておいて、実際の処理はページのJavaScriptを呼び出すようにすると、ビルドしなくてもJavaScriptを書き換えて動作を変えられるのでイテレーションが速くなって便利です。

JavaScriptからUnityの関数を呼ぶには

簡単な方法

jslibからSendMessageを呼び出せます。

unityInstance.SendMessage(objectName, methodName, value)

手軽なのですが、特定の名前のゲームオブジェクトをシーンに配置する必要があり、また名前をうっかり変更すると動かなくなるデメリットがあります。

難しい方法

Unity公式のドキュメントでは説明されていませんが、{{{ makeDynCall(‘sig’, ‘ptr’) }}} (arg1, arg2);のような形式でC#のMonoPInvokeCallback属性のメソッドを呼び出せます。呼び出されるメソッドはあらかじめC#からjslibに教えておきます。

TODO:サンプルを追加

「sig」は以下のような形式の文字列です。

'v': void type
'i': 32-bit integer type
'j': 64-bit integer type (currently does not exist in JavaScript)
'f': 32-bit float type
'd': 64-bit float type

JavaScriptからUnityに配列を渡すには

JavaScriptでEmscriptenのヒープを_mallocで確保してデータをストアし、C#のメソッドを呼んで_freeで解放します。C#側ではMonoPInvokeCallback属性のメソッドでMarshal.Copyして受け取ります。ポインタだけでは配列のサイズが分からないので教える必要があります。

TODO:サンプルを追加

TODO:Emscriptenのヒープ操作について

ビルドしたコードにJavaScriptを埋め込むには

拡張子.jspreのJavaScriptファイルをPluginsフォルダに置いておくと、ビルド後のJavaScriptコードに含まれます(Unity 2021.2で動かなくなってるという話も?)。

Pointer_stringifyで警告が出る

Unity 2021.2以降ではPointer_stringifyを使用するとブラウザコンソールに警告が出ます。UTF8ToStringに変更する必要があります。

Visual Studio Codeでの.jslib/.jspreの扱いについて

Visual Studio Codeで.jslib/.jspreをJavaScriptに関連付けておくと便利です。右下のファイル種別をクリックして、Cofigure Flie Association for ‘.jslib’…でJavaScriptを選択します。

UnityのWebGLビルド関連コンポーネント

Unityの下記フォルダにWebGLビルドに関連するコンポーネントが含まれており、目を通しておくと面白いかもしれません。

C:\Program Files\Unity\Hub\Editor\{Unityのバージョン}\Editor\Data\PlaybackEngines\WebGLSupport
書いた人:こりん(@korinVR
» VR開発メモトップへ