凹みTips C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。 2024-02-18T22:10:03+09:00 hecomi Hatena::Blog hatenablog://blog/11696248318752683032 uLipSync x WebGL における再生方法の制限について hatenablog://entry/6801883189084124335 2024-02-18T22:10:03+09:00 2024-02-18T22:10:03+09:00 はじめに 先日、uLipSync の WebGL ビルドでランタイム生成した際、音声は再生されるもののリップシンクしない、という報告を受けて調査をしてみました。 tips.hecomi.com 原因解説 原因としては AudioSource.PlayOneShot() の利用でした。こちらを詳しく解説していきます。 非 WebGL 向けビルドでは、MonoBehaviour.OnAudioFilterRead() や AudioSource.GetOutputData() といった API を通じて、そのタイミングで再生している音のバッファを取得することが出来ます。しかしながら、これらは一つ前… <h2 id="はじめに">はじめに</h2> <p>先日、uLipSync の <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ビルドでランタイム生成した際、音声は再生されるものの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>しない、という報告を受けて調査をしてみました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2023%2F12%2F30%2F003934" title="uLipSync の WebGL 対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2023/12/30/003934">tips.hecomi.com</a></cite></p> <h2 id="原因解説">原因解説</h2> <p>原因としては <code>AudioSource.PlayOneShot()</code> の利用でした。こちらを詳しく解説していきます。</p> <p>非 <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 向けビルドでは、<code>MonoBehaviour.OnAudioFilterRead()</code> や <code>AudioSource.GetOutputData()</code> といった <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> を通じて、そのタイミングで再生している音のバッファを取得することが出来ます。しかしながら、これらは<a href="https://tips.hecomi.com/entry/2024/01/28/010109">&#x4E00;&#x3064;&#x524D;&#x306E;&#x8A18;&#x4E8B;</a>で触れましたが Unity の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A6%A5%F3%A5%C9">サウンド</a>周りの仕組み及び <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> の制約により使用することが出来ません。代わりに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ビルドでは、データから直接バッファ取得可能な <code>AudioClip.GetData()</code> を通じて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>を実現しています。この中で、uLipSync では目的の <code>AudioSource</code> に対して現在セットされている <code>AudioClip</code> を取得し、それに対して <code>GetData()</code> を行っています。</p> <p>さて、<code>PlayOneShot()</code> の話題に戻ります。この <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> は現在セットされている <code>AudioClip</code> を再生する <code>AudioSource.Play()</code> と異なり、引数で指定された <code>AudioClip</code> を再生します。そして連続で再生した場合は、前に再生されているものはそのままに重複して音を鳴らすことが出来ます。一方で、これらの音はキャンセルしたりポーズしたりすることは出来ません。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FScriptReference%2FAudioSource.html" title="AudioSource - Unity スクリプトリファレンス" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/ScriptReference/AudioSource.html">docs.unity3d.com</a></cite></p> <p>つまり、<code>PlayOneShot</code> では呼び出し時には <code>AudioSource.clip</code> プロパティを更新せずに内部的に <code>AudioClip</code> をリストとして保持し、そこで直接管理を行うような処理となります。この結果 <code>AudioSource.clip</code> 経由での <code>GetData()</code> ではデータが降ってこず、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>が出来ていなかった、という状態になっていました。</p> <h2 id="修正方法">修正方法</h2> <p><code>PlayOneShot()</code> を次のように <code>Play()</code> に変えることで動作するようになります。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">var</span> source <span class="synStatement">=</span> GetComponent&lt;AudioSource&gt;(); <span class="synStatement">if</span> (oneshot) { <span class="synComment">// こちらでは動作せず</span> source.PlayOneShot(clip); } <span class="synStatement">else</span> { <span class="synComment">// clip を指定して Play で動作</span> source.clip <span class="synStatement">=</span> clip; source.loop <span class="synStatement">=</span> <span class="synConstant">false</span>; source.Play(); } </pre> <p><code>PlayOneShot()</code> では重複で音が鳴りえますので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>、というコンテキストの上では <code>Play()</code> の方が良いかもしれません。</p> <h2 id="調査メモ">調査メモ</h2> <p>備忘録のために調査メモです。<a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> 側は以下のようなコードを書きました。URL を <code>InputField</code> から取ってきて、それを <code>Play</code> または <code>PlayOneShot</code> する、というものです。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.UI; <span class="synStatement">using</span> UnityEngine.Networking; <span class="synStatement">using</span> System.Collections; <span class="synType">namespace</span> uLipSync.Samples { [RequireComponent(<span class="synStatement">typeof</span><span class="synType">(AudioSource)</span>)] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">RuntimeAudioClipPlayer </span><span class="synStatement">:</span> MonoBehaviour { <span class="synType">public</span> InputField inputField; <span class="synType">public</span> <span class="synType">void</span> Play() { StartCoroutine(DownloadAndPlay(<span class="synConstant">false</span>)); } <span class="synType">public</span> <span class="synType">void</span> PlayOneShot() { StartCoroutine(DownloadAndPlay(<span class="synConstant">true</span>)); } IEnumerator DownloadAndPlay(<span class="synType">bool</span> oneshot) { <span class="synType">var</span> source <span class="synStatement">=</span> GetComponent&lt;AudioSource&gt;(); <span class="synStatement">if</span> (<span class="synStatement">!</span>source) <span class="synStatement">yield</span> <span class="synStatement">return</span> <span class="synConstant">null</span>; <span class="synType">var</span> url <span class="synStatement">=</span> inputField.text; <span class="synType">var</span> type <span class="synStatement">=</span> AudioType.WAV; <span class="synStatement">using</span> <span class="synType">var</span> www <span class="synStatement">=</span> UnityWebRequestMultimedia.GetAudioClip(url, type); <span class="synStatement">yield</span> <span class="synStatement">return</span> www.SendWebRequest(); <span class="synType">var</span> clip <span class="synStatement">=</span> DownloadHandlerAudioClip.GetContent(www); clip.name <span class="synStatement">=</span> inputField.text; <span class="synStatement">if</span> (oneshot) { source.PlayOneShot(clip); } <span class="synStatement">else</span> { source.loop <span class="synStatement">=</span> <span class="synConstant">false</span>; source.clip <span class="synStatement">=</span> clip; source.Play(); } } } } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20240218214344" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20240218/20240218214344.png" width="1200" height="791" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>テスト用に WAV ファイルを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DB%A5%B9%A5%C6%A5%A3%A5%F3%A5%B0">ホスティング</a>するためのサーバも用意しておきます。以下は Node.js で起動引数で指定した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リをホストするコードです。<a href="https://developer.mozilla.org/ja/docs/Web/HTTP/CORS">CORS</a> に対応するためにいくつかヘッダを追加する必要がある点に注意です。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> http = require(<span class="synConstant">'http'</span>); <span class="synStatement">const</span> fs = require(<span class="synConstant">'fs'</span>); <span class="synStatement">const</span> path = require(<span class="synConstant">'path'</span>); <span class="synStatement">const</span> basePath = process.argv<span class="synIdentifier">[</span>2<span class="synIdentifier">]</span> ? path.resolve(process.argv<span class="synIdentifier">[</span>2<span class="synIdentifier">]</span>) : __dirname; <span class="synStatement">const</span> server = http.createServer((req, res) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">const</span> filePath = path.join(basePath, req.url); <span class="synStatement">const</span> fileExtension = path.extname(filePath); res.setHeader(<span class="synConstant">'Access-Control-Allow-Origin'</span>, <span class="synConstant">'*'</span>); res.setHeader(<span class="synConstant">'Access-Control-Allow-Methods'</span>, <span class="synConstant">'GET'</span>); res.setHeader(<span class="synConstant">'Access-Control-Allow-Headers'</span>, <span class="synConstant">'Content-Type, Accept'</span>); <span class="synStatement">if</span> (fileExtension !== <span class="synConstant">'.wav'</span>) <span class="synIdentifier">{</span> res.writeHead(404); res.end(<span class="synConstant">'404 Not Found'</span>); <span class="synStatement">return</span>; <span class="synIdentifier">}</span> fs.stat(filePath, (err, stats) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">if</span> (err || !stats.isFile()) <span class="synIdentifier">{</span> res.writeHead(404); res.end(<span class="synConstant">'404 Not Found'</span>); <span class="synStatement">return</span>; <span class="synIdentifier">}</span> res.writeHead(200, <span class="synIdentifier">{</span><span class="synConstant">'Content-Type'</span>: <span class="synConstant">'audio/wav'</span><span class="synIdentifier">}</span>); fs.createReadStream(filePath).pipe(res); <span class="synIdentifier">}</span>); <span class="synIdentifier">}</span>); <span class="synStatement">const</span> PORT = 3000; server.listen(PORT, () =&gt; <span class="synIdentifier">{</span> console.log(<span class="synConstant">`Server is running on http://localhost:</span><span class="synSpecial">${PORT}</span><span class="synConstant">`</span>); console.log(<span class="synConstant">`Serving files from: </span><span class="synSpecial">${basePath}</span><span class="synConstant">`</span>); <span class="synIdentifier">}</span>); </pre> <p>これで <code>PlayOneShot</code> 側では口パクせず、<code>Play</code> 側では動作するのが確認できました。</p> <h2 id="おわりに">おわりに</h2> <p>これで uLipSync の <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 対応に関しての大体の問題点は修正できたかもしれません?</p> <p>理想的には音の取得部分の AudioWorklet 化を行い、Web Worker 側で解析処理を行う、というコードにまで出来れば最高ですが…、自分自身の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>がないのでひとまず動作するようになったここまでで留めておきます。</p> hecomi Unity の WebGL ビルドでマイク入力を扱えるライブラリを作ってみた hatenablog://entry/6801883189072498368 2024-01-28T01:01:09+09:00 2024-01-29T11:24:38+09:00 はじめに Unity には Microphone というクラスがあり、これを通じてマイクの情報や入力を取得できます。しかしながら Microphone は WebGL では利用することが出来ません。 docs.unity3d.com Unity では FMOD をオーディオ周りとして利用しているようで、基本的にスレッド上で動くためスレッドが(部分的にしか)利用できない WebGL とは相性がよくなく、結果的に Web Audio API をベースに自前で実装する、という選択をしたようです。その上で多くのオーディオ系 API は再実装されたものの、幾つか Web Audio API との相性が良… <h2 id="はじめに">はじめに</h2> <p>Unity には <code>Microphone</code> というクラスがあり、これを通じてマイクの情報や入力を取得できます。しかしながら <code>Microphone</code> は <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> では利用することが出来ません。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fwebgl-audio.html" title="Audio in WebGL - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/webgl-audio.html">docs.unity3d.com</a></cite></p> <p>Unity では <a href="https://www.fmod.com/">FMOD</a> をオーディオ周りとして利用しているようで、基本的にスレッド上で動くためスレッドが(部分的にしか)利用できない <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> とは相性がよくなく、結果的に Web Audio <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> をベースに自前で実装する、という選択をしたようです。その上で多くのオーディオ系 <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> は再実装されたものの、幾つか Web Audio <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> との相性が良くないものは利用不可、となっているようです。そして <code>Microphone</code> はごっそり「不対応」となったようですね。</p> <p>私は <a href="https://github.com/hecomi/uLipSync">uLipSync</a> という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>のためのライブラリを作っていまして、この中の機能の一つとしてマイク入力による<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>のサポートを行っているのですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 上でのマイク入力がサポートできていなかったのでなんとかしたいと思った次第です。直接 uLipSync 側の機能として作るより、再利用性も高いので <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 上でマイクを取り扱うためのモジュールとして切り出して設計することにしました。</p> <p>本記事では、このあたりの調査と、実際に <code>Microphone</code> クラスの代替となるようなものを設計・作成してみましたので、その内容および使い方をご紹介します。</p> <h2 id="ダウンロード--ユースケース">ダウンロード / <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a></h2> <h3 id="GitHub"><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a></h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuMicrophoneWebGL" title="GitHub - hecomi/uMicrophoneWebGL: Enable microphone input buffer access in Unity WebGL builds" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uMicrophoneWebGL">github.com</a></cite></p> <h3 id="録音">録音</h3> <p>デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>を選択して録音・再生が出来ます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20240127151816" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20240127/20240127151816.gif" width="720" height="480" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="リアルタイムアクセス">リアルタイムアクセス</h3> <p>コールバックを通じてマイク入力へリアルタイムにアクセスが出来ます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20240127151828" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20240127/20240127151828.gif" width="720" height="480" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>詳しい使い方については、後述の「使い方」の項をご参照ください。</p> <h2 id="事前調査">事前調査</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> でマイクを使えるようにする、という範囲の機能なら、すでに世の中にありそうな気がしたので調べてみましたら、実際に幾つか見つかりました。</p> <h3 id="Microphone-Pro">Microphone Pro</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fassetstore.unity.com%2Fpackages%2Ftools%2Finput-management%2Fmicrophone-pro-webgl-mobiles-desktop-79989%3Flocale%3Dja-JP" title="Microphone Pro - WebGL, Mobiles, Desktop | 入出力管理 | Unity Asset Store" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://assetstore.unity.com/packages/tools/input-management/microphone-pro-webgl-mobiles-desktop-79989?locale=ja-JP">assetstore.unity.com</a></cite></p> <p>こちらは Unity 互換の <code>Microphone</code> を実現するアセットです。評判もよくデモも用意されております。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Funitydemos.frostweepgames.com%2Fassetstore%2Fmicrophone-webgl%2F" title="Unity WebGL Player | Microphone Pro" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://unitydemos.frostweepgames.com/assetstore/microphone-webgl/">unitydemos.frostweepgames.com</a></cite></p> <p>Frostweep Games さんは、その他にも Goolgle の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%BB%C0%BC%C7%A7%BC%B1">音声認識</a>周りの <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> とのつなぎなど Unity x <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 用途でのアセットをリリースしているようです。</p> <h3 id="UnityWebGLMicrophone">UnityWebGLMicrophone</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftgraupmann%2FUnityWebGLMicrophone" title="GitHub - tgraupmann/UnityWebGLMicrophone: WebGL Microphone module for Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tgraupmann/UnityWebGLMicrophone">github.com</a></cite></p> <p>こちらは AudioClip 生成機能やバッファの取得はありませんが、デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>情報を取ってこれるようです。面白いのは <em>[Root]/Assets/Plugins/</em> 下に <code>UnityEngine.Microphone</code> クラスを置くことで、そのままマイクを使えるようにしている点ですね。これによって通常のマイクアクセス系の <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> のコード変更無しに使えるようです。</p> <h2 id="方針">方針</h2> <p><code>Microphone</code> クラス互換を作るのは面白そうですが、挙動を合わせるための様々な制限との戦いや、拡張性のしにくさ、メンテナンス性などを考慮して、従来の <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> はいったん無視して <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 専用のマイククラスという形にしてみます。また、最終目標はいい感じに uLipSync へ統合することなので、その方針を忘れずにシンプルな設計にしてみようと思います。最低限、次のような機能は取り揃えようと思います:</p> <ul> <li>マイクの選択</li> <li>マイクの情報取得</li> <li>開始と終了</li> <li>バッファへのリアルタイムアクセス(今回作るものの大事なところ)</li> <li>エディタ実行</li> </ul> <p>最後の「エディタ実行」ですが、これはリアルタイムアクセスの特性上、開発<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">イテレーション</a>がとても大事なので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ビルド時だけではなく普通にエディタ上でも結果が確認できるように Unity の <code>Microphone</code> クラスをラップして、同様に扱えるようにするインターフェースも用意する、というものです。</p> <h2 id="設計と実装">設計と実装</h2> <p>様々なメディア入力機器へのアクセスを可能にする <a href="https://developer.mozilla.org/ja/docs/Web/API/MediaDevices">MediaDevices</a> の <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> である <strong><code>getUserMedia</code></strong> を使うと、マイクへのアクセスが出来ます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fja%2Fdocs%2FWeb%2FAPI%2FMediaDevices%2FgetUserMedia" title="MediaDevices: getUserMedia() メソッド - Web API | MDN" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia">developer.mozilla.org</a></cite></p> <p>基本的には、この <code>getUserMedia</code> の初期化・終了の指示、およびこれによって開かれたストリームからのマイク入力データへのアクセスを Unity <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> 側へ jslib を使ってつなぎ込みます。Unity <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> から jslib 側への情報の通知は <code>DllImport</code> を通じた従来の方法で可能です。jslib 側から Unity <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> への情報の通知は、前回の記事でも書きましたが、<code>Module.dynCall_v(...)</code> のような <a class="keyword" href="https://d.hatena.ne.jp/keyword/Emscripten">Emscripten</a> の機能を使って行います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2023%2F12%2F30%2F003934" title="uLipSync の WebGL 対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2023/12/30/003934">tips.hecomi.com</a></cite></p> <p>前回の経験から、jslib と <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> 側での取り扱いはなんとなく固まってきました。今回は以下のようにしてみました。</p> <h3 id="jslib">jslib</h3> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> uMicrophoneWebGLPlugin = <span class="synIdentifier">{</span> <span class="synComment">// 複数の場所から参照する必要のある</span> <span class="synComment">// グローバルへと展開してほしくない変数や関数をまとめておく</span> $uMicrophoneWebGL: <span class="synIdentifier">{</span> startCallback: <span class="synStatement">null</span>, dataCallback: <span class="synStatement">null</span>, devices: <span class="synIdentifier">[]</span>, ... <span class="synComment">// 初期化でコールバックを保持しておく</span> initialize: async <span class="synIdentifier">function</span>(startCallback, dataCallback, ...) <span class="synIdentifier">{</span> <span class="synIdentifier">this</span>.startCallback = startCallback; <span class="synIdentifier">this</span>.dataCallback = dataCallback; ... await <span class="synIdentifier">this</span>.updateDeviceList(); <span class="synIdentifier">}</span>, updateDeviceList: async <span class="synIdentifier">function</span>() <span class="synIdentifier">{</span> devices = ... <span class="synIdentifier">}</span>, ... <span class="synComment">// 基本的には async で非同期実行となる</span> <span class="synComment">// 結果は登録しておいたコールバックを経由して返す</span> start: async <span class="synIdentifier">function</span>() <span class="synIdentifier">{</span> ... <span class="synStatement">const</span> stream = await navigator.mediaDevices.getUserMedia(<span class="synIdentifier">{</span> audio: <span class="synIdentifier">{</span> device: device.deviceId, sampleRate: <span class="synIdentifier">{</span> ideal: 44100 <span class="synIdentifier">}</span>, <span class="synIdentifier">}</span>, video: <span class="synConstant">false</span>, <span class="synIdentifier">}</span>); <span class="synIdentifier">this</span>.audioContext = <span class="synStatement">new</span> AudioContext(); <span class="synIdentifier">this</span>.source = <span class="synIdentifier">this</span>.audioContext.createMediaStreamSource(stream); <span class="synIdentifier">this</span>.scriptProcessor = <span class="synIdentifier">this</span>.audioContext.createScriptProcessor(<span class="synIdentifier">this</span>.samples, 1, 1); <span class="synIdentifier">this</span>.scriptProcessor.onaudioprocess = <span class="synIdentifier">this</span>.onAudioProcess.bind(<span class="synIdentifier">this</span>); <span class="synIdentifier">this</span>.source.connect(<span class="synIdentifier">this</span>.scriptProcessor); <span class="synIdentifier">this</span>.scriptProcessor.connect(<span class="synIdentifier">this</span>.audioContext.destination); Module.dynCall_v(<span class="synIdentifier">this</span>.startCallback); ... <span class="synIdentifier">}</span>, <span class="synComment">// JS -&gt; C# への float[] の受け渡しは HEAPF32 を通じて行う</span> onAudioProcess: <span class="synIdentifier">function</span>(<span class="synStatement">event</span>) <span class="synIdentifier">{</span> <span class="synStatement">const</span> data = <span class="synStatement">event</span>.inputBuffer.getChannelData(0); <span class="synStatement">const</span> len = data.length; <span class="synStatement">const</span> ptr = _malloc(len * 4); HEAPF32.set(data, ptr &gt;&gt; 2); Module.dynCall_vii(<span class="synIdentifier">this</span>.dataCallback, ptr, len); <span class="synIdentifier">}</span>, <span class="synComment">// その他諸々の API を用意</span> stop: <span class="synIdentifier">function</span>() <span class="synIdentifier">{</span> ... <span class="synIdentifier">}</span>, isRecording: <span class="synIdentifier">function</span>() <span class="synIdentifier">{</span> ... <span class="synIdentifier">}</span>, <span class="synIdentifier">}</span>, <span class="synComment">// Unity 向けに DllImport して使える API としての関数は</span> <span class="synComment">// 広いスコープへと展開されてしまうため、他のライブラリと名前被りしないように</span> <span class="synComment">// ライブラリ名を付与した関数として定義しておく</span> uMicrophoneWebGL_Initialize: async <span class="synIdentifier">function</span>(startCallback, stopCallback, ...) <span class="synIdentifier">{</span> await uMicrophoneWebGL.initialize(...); <span class="synIdentifier">}</span>, <span class="synComment">// ↑で用意しておいた API を叩く</span> uMicrophoneWebGL_GetDeviceCount: <span class="synIdentifier">function</span>() <span class="synIdentifier">{</span> <span class="synStatement">return</span> uMicrophoneWebGL.getDeviceCount(); <span class="synIdentifier">}</span>, uMicrophoneWebGL_GetLabel: <span class="synIdentifier">function</span>(index) <span class="synIdentifier">{</span> <span class="synStatement">const</span> device = uMicrophoneWebGL.getDevice(index); <span class="synStatement">return</span> device ? device.labelBuffer : <span class="synConstant">&quot;&quot;</span>; <span class="synIdentifier">}</span>, ... <span class="synIdentifier">}</span>; autoAddDeps(uMicrophoneWebGLPlugin, <span class="synConstant">'$uMicrophoneWebGL'</span>); mergeInto(LibraryManager.library, uMicrophoneWebGLPlugin); </pre> <p>どのように展開されてしまうかについては前回の記事で詳しく書いてあります。</p> <h3 id="Unity-C">Unity C#</h3> <pre class="code lang-cs" data-lang="cs" data-unlink>[Serializable] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">TimingEvent </span><span class="synStatement">:</span> UnityEvent { } [Serializable] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">DataEvent </span><span class="synStatement">:</span> UnityEvent&lt;<span class="synType">float</span>[]&gt; { } <span class="synType">public</span> <span class="synType">static</span> <span class="synType">class</span> <span class="synType">Lib</span> { ... <span class="synComment">// 非同期で返ってくるのでイベントにしておく</span> <span class="synType">public</span> <span class="synType">static</span> TimingEvent startEvent { <span class="synStatement">get</span>; } <span class="synStatement">=</span> <span class="synStatement">new</span>(); <span class="synType">public</span> <span class="synType">static</span> DataEvent dataEvent { <span class="synStatement">get</span>; } <span class="synStatement">=</span> <span class="synStatement">new</span>(); ... <span class="synComment">// DllImport では EntryPoint を指定して冗長になった名前を短くする</span> <span class="synComment">// エディタ上でも同じインターフェースで動くように処理を書いておく</span> <span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> [DllImport(<span class="synConstant">&quot;__Internal&quot;</span>, EntryPoint <span class="synStatement">=</span> <span class="synConstant">&quot;uMicrophoneWebGL_Initialize&quot;</span>)] <span class="synType">private</span> <span class="synType">static</span> <span class="synType">extern</span> <span class="synType">void</span> Initialize( Action startCallback, Action&lt;IntPtr, <span class="synType">int</span>&gt; dataCallback, ...); <span class="synPreProc">#else</span> <span class="synType">private</span> <span class="synType">static</span> <span class="synType">void</span> Initialize( Action startCallback, Action&lt;IntPtr, <span class="synType">int</span>&gt; dataCallback, ...) { ... } <span class="synPreProc">#endif</span> <span class="synComment">// これで JavaScript 側へ関数登録ができる</span> <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> Initialize() { ... Initialize(OnStarted, OnDataReceived, ...); } <span class="synComment">// JavaScript 側から呼び出されるコールバックには PInvokeCallback 属性を付けておく</span> [AOT.MonoPInvokeCallback(<span class="synStatement">typeof</span><span class="synType">(Action)</span>)] <span class="synType">static</span> <span class="synType">void</span> OnStarted() { startEvent.Invoke(); } <span class="synComment">// HEAPF32 で渡された配列は取り出して使う</span> [AOT.MonoPInvokeCallback(<span class="synStatement">typeof</span><span class="synType">(Action&lt;IntPtr, int&gt;)</span>)] <span class="synType">static</span> <span class="synType">void</span> OnDataReceived(IntPtr ptr, <span class="synType">int</span> length) { ... _dataBuffer <span class="synStatement">=</span> <span class="synStatement">new</span> float[length]; Marshal.Copy(ptr, _dataBuffer, <span class="synConstant">0</span>, length); dataEvent.Invoke(_dataBuffer); } <span class="synComment">// エディタでも動くように書くのは 2 重実装になるのでまぁまぁ大変</span> <span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> [DllImport(<span class="synConstant">&quot;__Internal&quot;</span>, EntryPoint <span class="synStatement">=</span> <span class="synConstant">&quot;uMicrophoneWebGL_Start&quot;</span>)] <span class="synType">public</span> <span class="synType">static</span> <span class="synType">extern</span> <span class="synType">void</span> Start(); <span class="synPreProc">#else</span> ... <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> Start() { _recordGameObj <span class="synStatement">=</span> <span class="synStatement">new</span> GameObject(<span class="synConstant">&quot;Microphone&quot;</span>); <span class="synComment">// こういった別途等価に扱うためのコンポーネントを用意する必要がある</span> _micInputRetriever <span class="synStatement">=</span> _recordGameObj.AddComponent&lt;EditorMicrophoneDataRetriever&gt;(); _micInputRetriever.dataEvent.AddListener(x <span class="synStatement">=&gt;</span> dataEvent.Invoke(x)); <span class="synType">var</span> deviceId <span class="synStatement">=</span> GetDeviceId(_deviceIndex); <span class="synType">var</span> freq <span class="synStatement">=</span> GetSampleRate(_deviceIndex); _micInputRetriever.Begin(deviceId, freq); startEvent.Invoke(); } <span class="synPreProc">#endif</span> <span class="synComment">// 同じように色々な API を用意していく</span> <span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> [DllImport(<span class="synConstant">&quot;__Internal&quot;</span>, EntryPoint <span class="synStatement">=</span> <span class="synConstant">&quot;uMicrophoneWebGL_GetDeviceCount&quot;</span>)] <span class="synType">public</span> <span class="synType">static</span> <span class="synType">extern</span> <span class="synType">int</span> GetDeviceCount(); <span class="synPreProc">#else</span> <span class="synType">public</span> <span class="synType">static</span> <span class="synType">int</span> GetDeviceCount() <span class="synStatement">=&gt;</span> Microphone.devices.Length; <span class="synPreProc">#endif</span> <span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> [DllImport(<span class="synConstant">&quot;__Internal&quot;</span>, EntryPoint <span class="synStatement">=</span> <span class="synConstant">&quot;uMicrophoneWebGL_GetDeviceId&quot;</span>)] <span class="synType">public</span> <span class="synType">static</span> <span class="synType">extern</span> <span class="synType">string</span> GetDeviceId(<span class="synType">int</span> index); <span class="synPreProc">#else</span> <span class="synType">public</span> <span class="synType">static</span> <span class="synType">string</span> GetDeviceId(<span class="synType">int</span> index) <span class="synStatement">=&gt;</span> index <span class="synStatement">&gt;=</span> <span class="synConstant">0</span> <span class="synStatement">&amp;&amp;</span> index <span class="synStatement">&lt;</span> Microphone.devices.Length <span class="synStatement">?</span> Microphone.devices[index] <span class="synStatement">:</span> <span class="synConstant">&quot;&quot;</span>; <span class="synPreProc">#endif</span> <span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> [DllImport(<span class="synConstant">&quot;__Internal&quot;</span>, EntryPoint <span class="synStatement">=</span> <span class="synConstant">&quot;uMicrophoneWebGL_GetLabel&quot;</span>)] <span class="synType">public</span> <span class="synType">static</span> <span class="synType">extern</span> <span class="synType">string</span> GetLabel(<span class="synType">int</span> index); <span class="synPreProc">#else</span> <span class="synType">public</span> <span class="synType">static</span> <span class="synType">string</span> GetLabel(<span class="synType">int</span> index) <span class="synStatement">=&gt;</span> GetDeviceId(index); <span class="synPreProc">#endif</span> ... } } </pre> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> と <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> 側での配列のやり取りについては、以下のフォーラムで <a href="https://twitter.com/gtk2k">gtk2k</a> さんが素晴らしい返答をしてくださっているので参考にさせていただきました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fforum.unity.com%2Fthreads%2Freturn-a-float-double-array-from-jslib-to-unity.1115686%2F" title="Question - Return a float/double array from jslib to Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://forum.unity.com/threads/return-a-float-double-array-from-jslib-to-unity.1115686/">forum.unity.com</a></cite></p> <p>3 種類の方法が紹介されていますが、今回は継続的にデータを受け取るためのコールバックを登録しているので 2 番目の方式を使っています。面白いので少し詳しく見てみましょう。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> data = <span class="synStatement">event</span>.inputBuffer.getChannelData(0); <span class="synStatement">const</span> len = data.length; <span class="synStatement">const</span> ptr = _malloc(len * 4); HEAPF32.set(data, ptr &gt;&gt; 2); <span class="synComment">// float の番地をセット</span> Module.dynCall_vii(<span class="synIdentifier">this</span>.dataCallback, ptr, len); </pre> <p><code>_malloc</code> でメモリを必要なメモリ領域を確保していますが、float は 4 byte なので、その個数分の領域を要求しています。<code>HEAPF32</code> は <a class="keyword" href="https://d.hatena.ne.jp/keyword/Emscripten">Emscripten</a> の提供してくれる型付き配列ビューで、この確保した領域に float 配列をセットしています。<code>ptr &gt;&gt; 2</code> は <code>ptr / 4</code> と等価で 4 byte を考慮して float としてのインデックスを返している感じですね。Wasm ではメモリはリニアなので、このように単純に割り算でインデックスが求められているのだと理解してます(たぶん)。</p> <h2 id="使い方">使い方</h2> <p>さて、こうして作成したものを <strong>uMicrophoneWebGL</strong> というパッケージにまとめました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuMicrophoneWebGL" title="GitHub - hecomi/uMicrophoneWebGL: Enable microphone input buffer access in Unity WebGL builds" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uMicrophoneWebGL">github.com</a></cite></p> <p>UPM にてパッケージ追加あるいは <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> にあげてある .unitypackage をダウンロード・インポートしてください。そのうえで <code>MicrophoneWebGL</code> という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>をアタッチすることで使えるようになります。UI は以下のような感じになります:</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20240127193057" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20240127/20240127193057.png" width="1014" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="UI-項目">UI 項目</h3> <ul> <li><p><strong>Is Auto Start</strong></p> <ul> <li>チェックされている場合、起動時に自動的に録音開始になります(後述の <code>Begin()</code> が呼ばれます)</li> </ul> </li> <li><p><strong>Devices</strong></p> <ul> <li>内部的にはインデックス(int)で管理されています。</li> <li>どのユーザー環境基本的には 0 番を選択しておいてください(デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>選択は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>目的の使用のみ)</li> </ul> </li> <li><p><strong>Events</strong></p> <ul> <li>様々なイベントのコールバックを設定できます。</li> <li><strong>Ready Event</strong> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> 側で初期化が完了し、準備ができた後に 1 回呼び出されます。</li> </ul> </li> <li><strong>Device List Event</strong> <ul> <li>デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>リストが構築されたときに呼び出されます。</li> <li>デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>情報を含むリストが引数として渡されます。</li> </ul> </li> <li><strong>Start Event</strong> <ul> <li>マイクが開始されたときに呼び出されます。</li> <li>内部的には <code>navigator.mediaDevices.getUserMedia()</code>の完了に対応しています。</li> </ul> </li> <li><strong>End Event</strong> <ul> <li>マイクが停止したときに呼び出されます。</li> </ul> </li> <li><strong>Data Event</strong> <ul> <li>マイク入力データが取得されたときに呼び出されます。</li> <li>float 配列の波形データの配列が引数として渡されます。</li> </ul> </li> </ul> </li> </ul> <h2 id="API-リファレンス"><a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> リファレンス</h2> <p><code>MicrophoneWebGL</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>は以下のプロパティとメソッドを提供します:</p> <h5 id="変数--プロパティ">変数 / プロパティ</h5> <ul> <li><p><code>bool isAutoStart</code></p> <ul> <li>UI の Is Auto Start オプションに対応します。true の場合、起動時にマイクが自動的に開始されます。</li> </ul> </li> <li><p><code>int micIndex</code></p> <ul> <li>選択されたマイクデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>のインデックス。</li> </ul> </li> <li><p><code>TimingEvent readyEvent</code></p> <ul> <li>初期化後、マイクが準備完了したときにトリガーされるイベント。</li> </ul> </li> <li><p><code>TimingEvent startEvent</code></p> <ul> <li>マイクが録音を開始するときにトリガーされるイベント。</li> </ul> </li> <li><p><code>TimingEvent stopEvent</code></p> <ul> <li>マイクが録音を停止するときにトリガーされるイベント。</li> </ul> </li> <li><p><code>DeviceListEvent deviceListEvent</code></p> <ul> <li>使用可能なデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>のリストが更新されたときにトリガーされるイベント。</li> </ul> </li> <li><p><code>DataEvent dataEvent</code></p> <ul> <li>マイクデータが取得されたときにトリガーされるイベント。</li> </ul> </li> <li><p><code>bool isValid { get; }</code></p> <ul> <li>マイクが使用可能な状態であるかどうかを示す。</li> </ul> </li> <li><p><code>List&lt;Device&gt; devices { get; }</code></p> <ul> <li>利用可能なマイクデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>のリスト。</li> <li><code>Device</code> 構造体は次のような情報を含む <ul> <li><code>deviceId</code> はマイクを識別する ID</li> <li><code>label</code> はデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>の名前</li> <li><code>sampleRate</code> はマイクのサンプルレート</li> </ul> </li> </ul> </li> <li><p><code>Device selectedDevice { get; }</code></p> <ul> <li>現在選択されているマイクに関する情報。</li> </ul> </li> <li><p><code>bool isRecording { get; }</code></p> <ul> <li>マイクロフォンが現在録音中であるかどうかを示します。</li> </ul> </li> </ul> <h5 id="メソッド">メソッド</h5> <ul> <li><p><code>void Begin()</code></p> <ul> <li>マイクを開始。録音中の場合は何もしない。</li> </ul> </li> <li><p><code>void End()</code></p> <ul> <li>マイクを停止。録音中でない場合は何もしない。</li> </ul> </li> <li><p><code>void RefreshDeviceList()</code></p> <ul> <li>マイクのリストの更新要求。</li> <li>初期化時には自動で呼ばれる。</li> <li>更新の終了時に再度 <code>deviceListEvent</code> がトリガーされる。</li> </ul> </li> </ul> <h2 id="サンプル解説">サンプル解説</h2> <p>サンプルでは録音を行う <em>01. Recorder</em> と、波形を表示する <em>02. Buffer</em> が含まれています。冒頭の GIF がこれらに対応しています。用意したイベントに登録する関数を用意し、適切なタイミングで <code>Begin()</code>、<code>End()</code> を呼んで後は各コールバックでイベントのハンドルを行っています。</p> <p>より色々なイベントを扱う Recorder の方のコードの重要な部分だけ抜き出してコメントを書いてみました。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">class</span> <span class="synType">MicrophoneRecorder </span><span class="synStatement">:</span> MonoBehaviour { <span class="synType">public</span> <span class="synType">float</span> maxDuration <span class="synStatement">=</span> <span class="synConstant">10f</span>; <span class="synType">private</span> <span class="synType">float</span>[] _buffer <span class="synStatement">=</span> <span class="synConstant">null</span>; <span class="synType">private</span> <span class="synType">int</span> _bufferSize <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synType">private</span> AudioClip _clip; ... <span class="synType">public</span> <span class="synType">void</span> ToggleRecord() { <span class="synComment">// この関数を UI のボタン押下に紐づけておく</span> ... <span class="synStatement">if</span> (<span class="synStatement">!</span>isRecording) { Begin(); } <span class="synStatement">else</span> { End(); } ... } <span class="synType">private</span> <span class="synType">void</span> Begin() { <span class="synComment">// マイクを選択して録音開始</span> microphoneWebGL.micIndex <span class="synStatement">=</span> dropdown.<span class="synStatement">value</span>; microphoneWebGL.Begin(); } <span class="synType">private</span> <span class="synType">void</span> End() { <span class="synComment">// 録音終了</span> microphoneWebGL.End(); } <span class="synType">public</span> <span class="synType">void</span> OnBegin() { <span class="synComment">// 録音開始イベント</span> <span class="synComment">// UI からこの関数を startEvent に登録しておく</span> <span class="synComment">// 必要なバッファのサイズを計算して領域確保</span> <span class="synType">int</span> freq <span class="synStatement">=</span> microphoneWebGL.selectedDevice.sampleRate; <span class="synType">int</span> n <span class="synStatement">=</span> (<span class="synType">int</span>)(freq <span class="synStatement">*</span> maxDuration); _buffer <span class="synStatement">=</span> <span class="synStatement">new</span> float[n]; _bufferSize <span class="synStatement">=</span> <span class="synConstant">0</span>; } <span class="synType">public</span> <span class="synType">void</span> OnData(<span class="synType">float</span>[] input) { <span class="synComment">// データ取得イベント</span> <span class="synComment">// UI からこの関数を dataEvent に登録しておく</span> <span class="synComment">// 先程確保した _buffer に来たデータをつめておく</span> <span class="synType">int</span> n <span class="synStatement">=</span> input.Length; <span class="synStatement">if</span> (_bufferSize <span class="synStatement">+</span> n <span class="synStatement">&gt;=</span> _buffer.Length) <span class="synStatement">return</span>; System.Array.Copy(input, <span class="synConstant">0</span>, _buffer, _bufferSize, n); _bufferSize <span class="synStatement">+=</span> n; } <span class="synType">public</span> <span class="synType">void</span> OnEnd() { <span class="synComment">// 録音終了イベント</span> <span class="synComment">// UI からこの関数を stopEvent に登録しておく</span> <span class="synComment">// AudioClip を作成し</span> <span class="synType">var</span> freq <span class="synStatement">=</span> microphoneWebGL.selectedDevice.sampleRate; _clip <span class="synStatement">=</span> AudioClip.Create(<span class="synConstant">&quot;Recorded&quot;</span>, _bufferSize, <span class="synConstant">1</span>, freq, <span class="synConstant">false</span>); <span class="synType">var</span> data <span class="synStatement">=</span> <span class="synStatement">new</span> float[_bufferSize]; System.Array.Copy(_buffer, data, _bufferSize); _clip.SetData(data, <span class="synConstant">0</span>); } <span class="synType">public</span> <span class="synType">void</span> TogglePlay() { <span class="synComment">// Play ボタンを押したら先程の AudioClip をセットして再生</span> ... <span class="synStatement">if</span> (audioSource.isPlaying) { audioSource.Stop(); } <span class="synStatement">else</span> { audioSource.clip <span class="synStatement">=</span> _clip; audioSource.Play(); } } } </pre> <h2 id="関連">関連</h2> <p>以前、Unity 側のバッファを Web Audio <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> 側で再生する方法の記事は書きましたので、よければこちらもご参照ください(本記事中は float 配列を <a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> 側から <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> へ、という流れでしたが、こちらの記事は逆)。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F09%2F30%2F014115" title="Unity の WebGL で動的にサウンドを生成・再生する方法を調べてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/09/30/014115">tips.hecomi.com</a></cite></p> <h2 id="おわりに">おわりに</h2> <p>当初は AudioClip に直接流し込みながらリアルタイムで音声再生、とした上でそのバッファを uLipSync に渡す、という設計をしていました(これだと AudioClip に対応して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>する仕組みに何も手を入れなくて良い)。しかしながら <code>SetData()</code> を毎フレーム行うのが <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> と相性が悪いのか余りうまく行かず…、結局今回の形にしました。ただこれによって生じる遅延が最小化出来たので結果的には良かったかなと思っています。次回は uLipSync とのつなぎ込みを行います。</p> hecomi uLipSync の WebGL 対応をしてみた hatenablog://entry/6801883189065375952 2023-12-30T00:39:34+09:00 2023-12-30T00:41:34+09:00 はじめに 前回調査記事を書きました。 tips.hecomi.com 今回は続きで、調査を元に実際に WebGL のサポートを行いました(マイクサポートのみまた後日)。実装しながら気づいたことや、問題に対してどのように対応したかなどについてまとめましたのでご紹介します。 ダウンロード github.com 実験と問題解決 前回は WebGL では解析の入口となっていたオーディオバッファを取得できる OnAudioFilterRead() が動作しないことから、代替手段として Web Audio API からバッファをもらってくる方式と AudioClip.GetData() によって取ってくる… <h2 id="はじめに">はじめに</h2> <p>前回調査記事を書きました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2023%2F11%2F30%2F231911" title="uLipSync の WebGL 対応を調査してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2023/11/30/231911">tips.hecomi.com</a></cite></p> <p>今回は続きで、調査を元に実際に <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> のサポートを行いました(マイクサポートのみまた後日)。実装しながら気づいたことや、問題に対してどのように対応したかなどについてまとめましたのでご紹介します。</p> <h2 id="ダウンロード">ダウンロード</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Freleases%2Ftag%2Fv3.1.0" title="Release v3.1.0 has been released! · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/releases/tag/v3.1.0">github.com</a></cite></p> <h2 id="実験と問題解決">実験と問題解決</h2> <p>前回は <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> では解析の入口となっていたオーディオバッファを取得できる <code>OnAudioFilterRead()</code> が動作しないことから、代替手段として Web Audio <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> からバッファをもらってくる方式と <code>AudioClip.GetData()</code> によって取ってくる方式のメリット・デメリットを比較しました。前回の記事の結論としては、汎用性とコード変更性の少なさから <code>AudioClip.GetData()</code> を選択することにした、というものでした。</p> <p>それではまずはシンプルにコードを書いて試してみます。<code>AudioClip.GetData()</code> が動作するので、あとは <code>AudioSource</code> から現在の再生位置を取ってくる形にします。オーディオバッファの取得部分はメインスレッドでの実行になってしまいますが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> 自体は動作します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> <span class="synType">float</span>[] _audioBuffer <span class="synStatement">=</span> <span class="synConstant">null</span>; <span class="synPreProc">#endif</span> ... <span class="synType">void</span> Update() { ... <span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> UpdateWebGL(); <span class="synPreProc">#endif</span> ... } ... <span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> <span class="synType">void</span> UpdateWebGL() { <span class="synStatement">if</span> (<span class="synStatement">!</span>_audioSource) <span class="synStatement">return</span>; <span class="synType">var</span> clip <span class="synStatement">=</span> _audioSource.clip; <span class="synStatement">if</span> (<span class="synStatement">!</span>clip <span class="synStatement">||</span> clip.loadState <span class="synStatement">!=</span> AudioDataLoadState.Loaded) <span class="synStatement">return</span>; <span class="synType">int</span> ch <span class="synStatement">=</span> clip.channels; <span class="synType">int</span> fps <span class="synStatement">=</span> <span class="synConstant">60</span>; <span class="synType">int</span> n <span class="synStatement">=</span> AudioSettings.outputSampleRate <span class="synStatement">/</span> fps <span class="synStatement">*</span> ch; <span class="synStatement">if</span> (_audioBuffer <span class="synStatement">==</span> <span class="synConstant">null</span> <span class="synStatement">||</span> _audioBuffer.Length <span class="synStatement">!=</span> n) { _audioBuffer <span class="synStatement">=</span> <span class="synStatement">new</span> float[n]; } <span class="synType">int</span> offset <span class="synStatement">=</span> _audioSource.timeSamples; offset <span class="synStatement">=</span> math.min(clip.samples <span class="synStatement">-</span> n <span class="synStatement">-</span> <span class="synConstant">1</span>, offset); clip.GetData(_audioBuffer, offset); OnDataReceived(_audioBuffer, ch); } <span class="synPreProc">#endif</span> ... <span class="synType">public</span> <span class="synType">void</span> OnDataReceived(<span class="synType">float</span>[] input, <span class="synType">int</span> channels) { <span class="synComment">// 解析</span> ... } ... <span class="synType">void</span> OnAudioFilterRead(<span class="synType">float</span>[] input, <span class="synType">int</span> channels) { ... OnDataReceived(input, channels); } ... </pre> <p>本当は前回の <code>timeSamples</code> を覚えておき、そこからの差分を取得するべきです。そうしないと前回与えた音声バッファとズレが起きてしまいます。ただ一方これだとパフォーマンスのブレでバッファ(<code>_audioBuffer</code>)の数が可変になってしまい、頻繁に <code>new float[]</code> する必要があるため、パフォーマンス観点から望ましくありません。そのためバッファの大きさが一定になるよう仮の <a class="keyword" href="https://d.hatena.ne.jp/keyword/FPS">FPS</a> を設定し、最新の再生部分から一定の量ずつバッファを取ってくる形としました。ただこれだとぶつ切りのバッファを解析のリングバッファに突っ込んでいる形になっています。この関係でバッファの接続部は不連続になっているのですが、基本的には母音の解析なのでそれぞれのチャンク程度の長さがあれば十分なのでこれでも実際は大きな問題なく動作すると思われます。</p> <h3 id="Load-Type-による制限">Load Type による制限</h3> <p>しかしながらこのままではビルドしても動作しません。原因を見ていきましょう。</p> <p><code>GetData()</code> の <a href="https://docs.unity3d.com/ScriptReference/AudioClip.GetData.html">&#x30EA;&#x30D5;&#x30A1;&#x30EC;&#x30F3;&#x30B9;</a> によると、現在の制限としては、AudioClip の <em>Load Type</em> が <em>Compressed In Memory</em> になっている場合、<code>AudioClip.GetData()</code> が使用できません(同様にストリーミングの設定の際も使用できません)。圧縮された音声に対して <code>GetData()</code> すると次のようにエラーとなります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231209171806" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231209/20231209171806.png" width="1200" height="749" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>確かに生バッファの特定位置にアクセスしたいわけですから、圧縮済みのものからはそのままでは持ってこれないわけですね。代わりに <em>Decompressed On Load</em> をすると、その名の通りロード時に圧縮解除してくれるため、バッファを取得することができるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231209172352" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231209/20231209172352.png" width="1052" height="898" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>この設定をすることで口パクしてくれるようになります。しかしながら次の問題が出てきます。</p> <h3 id="音の同期ズレ問題">音の同期ズレ問題</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Chrome">Chrome</a> などの Web ブラウザでは、ユーザの入力なしに勝手にメディアを再生することに対して制限を設けています。例えば <a class="keyword" href="https://d.hatena.ne.jp/keyword/Chrome">Chrome</a> では Autoplay Policy という名前で紹介されています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.chrome.com%2Fblog%2Fautoplay%3Fhl%3Dja" title="Chrome の自動再生ポリシー  |  Blog  |  Chrome for Developers" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://developer.chrome.com/blog/autoplay?hl=ja">developer.chrome.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fzprodev%2Fitems%2F7fcd8335d7e8e613a01f" title="Web Audio APIの闇 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/zprodev/items/7fcd8335d7e8e613a01f">qiita.com</a></cite></p> <p>これは Web の規格ではないようですが、これを許すと勝手にバックグラウンドで音やビデオがドコドコ鳴ってしまいますし、勝手に計算リソースも消費されてしまうので、エンドユーザ視点だとありがたい仕様です。ただ開発者側は少し面倒で、次のような手順で再生済みオーディオを再開する必要があります。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">// AudioContext を生成する</span> <span class="synStatement">window</span>.onload = <span class="synIdentifier">function</span>() <span class="synIdentifier">{</span> <span class="synIdentifier">var</span> context = <span class="synStatement">new</span> AudioContext(); ... <span class="synIdentifier">}</span> <span class="synComment">// なにかしらのユーザインタラクション(ここでは click イベント)が</span> <span class="synComment">// 発生したら AudioContext を resume する</span> <span class="synStatement">document</span>.querySelector(<span class="synConstant">'button'</span>).addEventListener(<span class="synConstant">'click'</span>, <span class="synIdentifier">function</span>() <span class="synIdentifier">{</span> context.resume().then(() =&gt; <span class="synIdentifier">{</span> console.log(<span class="synConstant">'ドコドコ'</span>); <span class="synIdentifier">}</span>); <span class="synIdentifier">}</span>); </pre> <p>さて、このレジューム処理により単に音を再開して鳴らすだけなら問題ないのですが、再生している音声と何かしら同期したいとなると、ズレが起きてしまうようです。どうやら <code>GetData()</code> では通常通り最初から再生されていると認識されバッファが降ってくるのですが、実際の再生されている音声はレジュームしたタイミング(ユーザ操作のタイミング)で頭から再生開始される、といった状態になるようです。Unity 側の実装で、このあたりうまく処理してほしいところですが...、出来ていないことに対する深い理由は調べましたがわかりませんでした(内部的には音声発音がされている形にしないと、確かに困る局面は出てきそうなので、どっちを取るか問題なのかもしれません)。一応、Unity での Autoplay Policy に対する実装を見てみると次のようにユーザ入力を見てコンテキストを <code>resume()</code> していました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231212220410" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231212/20231212220410.png" width="1200" height="636" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><code>mousedown</code> は主に PC 向け、<code>touchstart</code> はタッチ可能なデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>など)向けのようです。基本的には、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D7%A5%EA%C0%A9%BA%EE">アプリ制作</a>者向けのベストプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスとしては、何かしらのスタート画面を設け、クリックやタッチを促してからゲームやアプリを開始してもらい、その後に音を鳴らし始める、という感じのようです。ただ、ライブラリ制作者としては色々な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>を拾うために、ユーザ操作前から鳴っている音も対応したいです。</p> <h3 id="対策検討">対策検討</h3> <p>さて、同期ズレを直す方法はないかと調べていたところ少し主旨は違いますが Unity の <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> での知見の面白い記事がありました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fmetaformingpro%2Fn%2Fne6f34829a4d9" title="unity webGLの音声再生の謎に迫る!|MetaFormingPro" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://note.com/metaformingpro/n/ne6f34829a4d9">note.com</a></cite></p> <p>記事によると <code>AudioSource.timeSamples</code> はどうやら代入も効くようなので、次の謎コードを書いてみます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">void</span> UpdateWebGL() { ... _audioSource.timeSamples <span class="synStatement">=</span> _audioSource.timeSamples ... } </pre> <p>なんとこうすると再生位置がバッファの取得場所と一致して、タイミングが合う<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>になります(プロパティの get / set は中身は関数なので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> の場合は内部の実装が非対称になっていそうですね)。ただ、上のコードでは毎フレーム <code>Update()</code> タイミングで位置調整をしてしまっている関係で、オーディオスレッドとメインスレッドによる更新タイミングの誤差(オーディオ更新のスレッドの周期は Unity のメインスレッドの更新周期と異なる)で、音がガビガビします。</p> <p>どこかにフックしたり、コールバックなどで AudioContext のレジュームタイミングにこの処理を実行してあげれば良いかと思いましたが、Unity が吐き出すコード中の <code>_JS_Sound_Init</code> を覗いてみてもあまり良い仕組みは無さそうです。もちろん吐き出し後の .js を書き換えてあげれば色々できますが、毎度ビルド後に書き換えるのはライブラリ使用者側の手間もかかりますし避けたいところです。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> だけでなるべく完結したかったですが、ここは jslib <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を書いてユーザイベントにフックして上記コードを実行できるようにしてみます。</p> <h2 id="実装の流れ">実装の流れ</h2> <h3 id="設計">設計</h3> <p>方針としては <a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> 上で <code>window.addEventListner</code> したコールバックを、再生済みの複数の uLipSync <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が受け取れるようにしたいです。少し図にすると流れが複雑ですが次のような設計をしてみました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231222222645" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231222/20231222222645.png" width="1200" height="866" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <ol> <li>static なクラスを作成(ここでは <code>WebGL</code>)し、この中で <code>RuntimeInitializeOnLoadMethod</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%C8%A5%EA%A5%D3%A5%E5%A1%BC%A5%C8">アトリビュート</a>を利用して起動時に同クラス内のコールバックを(ここでは <code>OnAuidoContextInitiallyResumed</code> を jslib 側に登録</li> <li>同時に <code>window.addEventListener</code> で種々のイベントが呼ばれたらこのコールバックが呼ばれるよう登録</li> <li><code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の<code>Awake</code> で <code>WebGL</code> クラスに自身を登録</li> <li>ユーザーイベントが発火されると jslib 側から登録しておいた <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> 側の <code>OnAuidoContextInitiallyResumed</code> を発火</li> <li>登録しておいた <code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>に通知</li> </ol> <p>この流れです。</p> <h3 id="実装">実装</h3> <p>では具体的なコードを覗いてみましょう。</p> <h5 id="WebGLcs"><a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a>.cs</h5> <p>まずは <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> 側からです。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">class</span> <span class="synType">uLipSync </span><span class="synStatement">:</span> MonoBehaviour { ... <span class="synType">void</span> Awake() { ... <span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> InitializeWebGL(); <span class="synPreProc">#endif</span> } ... <span class="synPreProc">#if UNITY_WEBGL &amp;&amp; !UNITY_EDITOR</span> <span class="synType">public</span> <span class="synType">void</span> InitializeWebGL() { WebGL.Register(<span class="synStatement">this</span>); } <span class="synType">public</span> <span class="synType">void</span> OnAuidoContextInitiallyResumed() { _audioSource.timeSamples <span class="synStatement">=</span> _audioSource.timeSamples; ... } ... <span class="synPreProc">#endif</span> ... } <span class="synType">public</span> <span class="synType">static</span> <span class="synType">class</span> <span class="synType">WebGL</span> { <span class="synType">static</span> List&lt;uLipSync&gt; instances <span class="synStatement">=</span> <span class="synStatement">new</span> List&lt;uLipSync&gt;(); <span class="synType">static</span> <span class="synType">bool</span> isAudioContextResumed <span class="synStatement">=</span> <span class="synConstant">false</span>; <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> Register(uLipSync instance) { <span class="synStatement">if</span> (isAudioContextResumed) <span class="synStatement">return</span>; instances.Add(instance); } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] <span class="synType">static</span> <span class="synType">void</span> Init() { OnLoad(OnAuidoContextInitiallyResumed); } [DllImport(<span class="synConstant">&quot;__Internal&quot;</span>)] <span class="synType">public</span> <span class="synType">static</span> <span class="synType">extern</span> <span class="synType">void</span> OnLoad(System.Action callback); [AOT.MonoPInvokeCallback(<span class="synStatement">typeof</span><span class="synType">(System.Action)</span>)] <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> OnAuidoContextInitiallyResumed() { <span class="synStatement">foreach</span> (<span class="synType">var</span> instance <span class="synStatement">in</span> instances) { instance.OnAuidoContextInitiallyResumed(); } instances.Clear(); isAudioContextResumed <span class="synStatement">=</span> <span class="synConstant">true</span>; } } </pre> <p><code>DllImport</code> で宣言している <code>OnLoad</code> が後で見る jslib 側の関数になります(<a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> では <code>DllImport("__Internal")</code> をつけることで呼び出しが可能)。<code>MonoPInvokeCallback</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%C8%A5%EA%A5%D3%A5%E5%A1%BC%A5%C8">アトリビュート</a>をつけた <code>OnAuidoContextInitiallyResumed</code> をこの <code>OnLoad</code> に渡して初期化しています。後でこれを jslib 側から呼ぶイメージです(jslib 側から呼ぶ関数にはこの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%C8%A5%EA%A5%D3%A5%E5%A1%BC%A5%C8">アトリビュート</a>を付与する必要があります)。<code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>からは <code>Register</code> が呼ばれて各 <code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>がリストに登録され、後で jslib 側から <code>OnAuidoContextInitiallyResumed</code> をコールされた時にすべての<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>に通知が飛ぶ、というイメージです。</p> <h5 id="uLipSyncjslib">uLipSync.jslib</h5> <p>次は jslib 側です。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> uLipSyncPlugin = <span class="synIdentifier">{</span> $uLipSync: <span class="synIdentifier">{</span> unityCsharpCallback: <span class="synStatement">null</span>, resumeEventNames: <span class="synIdentifier">[</span><span class="synConstant">'keydown'</span>, <span class="synConstant">'mousedown'</span>, <span class="synConstant">'touchstart'</span><span class="synIdentifier">]</span>, userEventCallback: <span class="synIdentifier">function</span>() <span class="synIdentifier">{</span> Module.dynCall_v(uLipSync.unityCsharpCallback); <span class="synStatement">for</span> (<span class="synStatement">const</span> ev of uLipSync.resumeEventNames) <span class="synIdentifier">{</span> <span class="synStatement">window</span>.removeEventListener(ev, uLipSync.userEventCallback); <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span>, OnLoad: <span class="synIdentifier">function</span>(callback) <span class="synIdentifier">{</span> <span class="synStatement">if</span> (WEBAudio.audioContext.state !== <span class="synConstant">'suspended'</span>) <span class="synStatement">return</span>; uLipSync.unityCsharpCallback = callback; <span class="synStatement">for</span> (<span class="synStatement">const</span> ev of uLipSync.resumeEventNames) <span class="synIdentifier">{</span> <span class="synStatement">window</span>.addEventListener(ev, uLipSync.userEventCallback); <span class="synIdentifier">}</span> <span class="synIdentifier">}</span>, <span class="synIdentifier">}</span>; autoAddDeps(uLipSyncPlugin, <span class="synConstant">'$uLipSync'</span>); mergeInto(LibraryManager.library, uLipSyncPlugin); </pre> <p>jslib 側はちょっと癖がありますが…、とても詳しい解説が <a href="https://twitter.com/gtk2k">gtk2k</a> さんによって書かれているのでご一読ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fgtk2k%2Fitems%2F1c7aa7a202d5f96ebdbf" title="Unity(WebGL)でC#の関数からブラウザー側のJavaScript関数を呼び出すまたはその逆(JS⇒C#)に関する知見(プラグイン形式[.jslib]) - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/gtk2k/items/1c7aa7a202d5f96ebdbf">qiita.com</a></cite></p> <p>細かいところを見ていきます。<code>autoAddDeps</code> を行うと <a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> 側に $ を外したオブジェクトが(Unity のスコープ内に)生成されます。一方で通常通り <code>mergeInto</code> するとアンダースコア付きの関数として展開されます(その名の通り merge into として含まれるイメージ)。なので一時的に変数を保存したいときは <code>autoAddDeps</code> しています。具体的には以下のようなコードへと展開されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231222180102" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231222/20231222180102.png" width="1200" height="528" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><code>OnLoad</code> が呼ばれたタイミングではコンテキストが <code>suspended</code> のときだけ処理します(Autoplay Policy が働き、Unity 側からは再生しているように見えるけど音声は再生されていない状態)。与えられたコールバックを保存し、イベントリスナを登録しておきます。そして <code>userEventCallback</code> がユーザ操作によって呼ばれたとき、<code>Module.dynCall_v</code> でそのコールバックを呼び出し、イベントリスナを解除します。</p> <p>この <code>Module.dynCall_v</code> ですがドキュメントがあまりありません…。以下のサイトの解説がとてもわかりやすかったです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fhikipuro%2Fitems%2Fd7cbc4294dd6b58d0ffb" title="UnityのWebGLで気づいたことまとめ - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/hikipuro/items/d7cbc4294dd6b58d0ffb">qiita.com</a></cite></p> <p>Unity 公式では製品の一つである Untiy Forma のドキュメントに記載があります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.industrial.forma%404.3%2Fmanual%2Fforma-js-api-devGuide.html" title="JavaScript API Developer Guide | Unity Forma | 4.3.3" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.industrial.forma@4.3/manual/forma-js-api-devGuide.html">docs.unity3d.com</a></cite></p> <p><code>Module.dynCall_v(...)</code> の代わりに <code>dynCall('v', ...)</code> でも動作しますが、<code>dynCallLegacy</code> に接続されてしまっていたので、手元のコードでは <code>Module</code> を使用する方向でひとまず書いておくことにします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231222182432" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231222/20231222182432.png" width="1200" height="349" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="結果">結果</h3> <p>さて、これで <code>uLipSync</code> の <code>OnAuidoContextInitiallyResumed</code> がユーザ操作時によって呼ばれ、<code>AudioSource</code> の <code>timeSamples</code> に <code>timeSamples</code> を代入する、という謎コードを経て同期が取れるようになりました。これで <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> でのサポートが出来た形になります。</p> <h3 id="タイミング調整">タイミング調整</h3> <p>それでもちょっとタイミングがズレている(遅れている)ように見えました。今回は任意の場所のバッファを取ってくる形になっているので、この取ってくる位置にオフセットをかけることが可能で、これにより良い感じに口が合うように調整できます(逆にリアルタイムのバッファベースだと先読みが出来ません、なのでこの方式の唯一の利点だと思います)。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>[Range(<span class="synStatement">-</span><span class="synConstant">0.2f</span>, <span class="synConstant">0.2f</span>)] pulbic <span class="synType">float</span> audioSyncOffsetTime <span class="synStatement">=</span> <span class="synConstant">0.1f</span>; ... <span class="synType">void</span> UpdateWebGL() { ... <span class="synType">int</span> offset <span class="synStatement">=</span> _audioSource.timeSamples; offset <span class="synStatement">+=</span> (<span class="synType">int</span>)(audioSyncOffsetTime <span class="synStatement">*</span> AudioSettings.outputSampleRate <span class="synStatement">*</span> ch); ... clip.GetData(_audioBuffer, offset); OnDataReceived(_audioBuffer, ch); } </pre> <h2 id="パフォーマンス">パフォーマンス</h2> <p>最後に現状のパフォーマンスを見ていきましょう。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Chrome">Chrome</a> のプロファイラで見てみます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231222222450" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231222/20231222222450.png" width="1200" height="810" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>uLipSync では取得したバッファは JobSystem によって生成されたジョブがワーカスレッド 1 つを使い裏で計算が行われ、次フレームで解析結果が回収されます。ただ <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ではデフォルトでは上図のようにメインスレッド上でジョブが実行されます。</p> <p>Unity 上ではまだ <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> はメインスレッドのみの実行なので、現状のコードのままではパフォーマンス改善はしばらく待つことになるかもしれません。JobSystem 計算部分を切り出し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> 側の Web Worker 上で実行できるよう jslib を使ってつなぐことができればパフォーマンス改善の余地はあるかもしれません。これは今後の課題としたいと思います。</p> <p>上記画像を見ていただくとわかるように、私の <a class="keyword" href="https://d.hatena.ne.jp/keyword/MacBook%20Pro">MacBook Pro</a> (2021 / M1) だと 4 ms もかかってしまっています。。より細かく見ていくと、ダウンサンプルのための前処理として行っているローパスフィルタ処理に時間がかかっています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231229221223" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231229/20231229221223.png" width="1200" height="705" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>複数キャラの会話などの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>では、可能な際は事前ベイクを積極的に利用していったほうが良さそうです。</p> <h2 id="おわりに">おわりに</h2> <p>次回はマイクのサポートを行いたいと思います。マイク側は今回のように Unity 側の取り扱いを変えるだけでは実現できず、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> 側で <a href="https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia">getUserMedia()</a> して取得したバッファを Unity 側へ渡す必要があるので、既存の仕組みとの互換性を保つために少しかかる予定です。</p> hecomi uLipSync の WebGL 対応を調査してみた hatenablog://entry/6801883189061861349 2023-11-30T23:19:11+09:00 2023-11-30T23:21:51+09:00 はじめに 過去に何度かリクエストを頂いているのですが、まだ uLipSync で対応していないことの一つに WebGL 対応があります。WebGL ではいくつかの機能が制限されており、そのまま使えない機能が多々あります。例えば現在解析に用いている OnAudioFilterRead コールバックは WebGL では利用できませんし、他にもマイクも同様に使用できません。 tips.hecomi.com こういった制約の関係で uLipSync は WebGL では現在動作しない形になっています。ただ、うえぞうさんがオーディオ再生側に関しては動作するような仕組みを作成して下さっています(ちょうど … <h2 id="はじめに">はじめに</h2> <p>過去に何度かリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを頂いているのですが、まだ <a href="https://github.com/hecomi/uLipSync">uLipSync</a> で対応していないことの一つに <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 対応があります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ではいくつかの機能が制限されており、そのまま使えない機能が多々あります。例えば現在解析に用いている <a href="https://docs.unity3d.com/ja/2021.2/ScriptReference/MonoBehaviour.OnAudioFilterRead.html">OnAudioFilterRead</a> コールバックは <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> では利用できませんし、他にもマイクも同様に使用できません。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F09%2F30%2F014115" title="Unity の WebGL で動的にサウンドを生成・再生する方法を調べてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/09/30/014115">tips.hecomi.com</a></cite></p> <p>こういった制約の関係で uLipSync は <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> では現在動作しない形になっています。ただ、<a href="https://twitter.com/uezochan">&#x3046;&#x3048;&#x305E;&#x3046;&#x3055;&#x3093;</a>がオーディオ再生側に関しては動作するような仕組みを作成して下さっています(ちょうど Unity 2021 以降でも動作する v0.3 がリリースされました)。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fuezo%2FuLipSyncWebGL" title="GitHub - uezo/uLipSyncWebGL: An experimental plugin to use uLipSync on WebGL platform." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/uezo/uLipSyncWebGL">github.com</a></cite></p> <p>今回はマイク含め包括的にサポートを行うために、現状の問題点と使用できそうな技術調査を行います。</p> <h2 id="公式ドキュメント">公式ドキュメント</h2> <p>Unity の <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ビルドにおけるオーディオ周りの制約が以下の公式ドキュメントに記載されています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fwebgl-audio.html" title="Audio in WebGL - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/webgl-audio.html">docs.unity3d.com</a></cite></p> <h2 id="問題点調査">問題点調査</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> でビルドすると以下のようにエラーとなります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231126234459" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231126/20231126234459.png" width="1200" height="629" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <blockquote><p>error CS0103: The name 'Microphone' does not exist in the current context</p></blockquote> <p><code>Microphone</code> クラスは <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> プラットフォームでは使えない、というエラーですね。ひとまず他の問題調査のため、いったん <code>UNITY_WEBGL</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%EA%A5%D7%A5%ED%A5%BB%A5%C3%A5%B5">プリプロセッサ</a> ディレクティブを使って <code>Microphone</code> を使ってるコードを排除してみます。例えば、<code>uLipSync.MicUtil</code> では以下のように <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ではリストが空になるようにしてみます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">struct</span> MicDevice { ... } <span class="synType">public</span> <span class="synType">static</span> <span class="synType">class</span> <span class="synType">MicUtil</span> { <span class="synType">public</span> <span class="synType">static</span> List&lt;MicDevice&gt; GetDeviceList() { <span class="synType">var</span> list <span class="synStatement">=</span> <span class="synStatement">new</span> List&lt;MicDevice&gt;(); <span class="synPreProc">#if !UNITY_WEBGL</span> <span class="synStatement">for</span> (<span class="synType">int</span> i <span class="synStatement">=</span> <span class="synConstant">0</span>; i <span class="synStatement">&lt;</span> Microphone.devices.Length; <span class="synStatement">++</span>i) { ... } <span class="synPreProc">#endif</span> <span class="synStatement">return</span> list; } } } </pre> <p>さて、これで実行すると...</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231128004106" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231128/20231128004106.png" width="1200" height="808" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>実行はされるのですが、特に Console などにも情報はなく口パクがされていない形になります。<code>OnAudioFilterRead()</code> へ適当な <code>Debug.Log()</code> を仕込んでも何も出力されないことから、<a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ではこのコールバックが呼び出されない実装になっているようです。</p> <h2 id="マイク対応について">マイク対応について</h2> <p>Web 上でマイクの入力は <code>getUserMedia</code> を通じて取得することができます(ユーザの許可が必要な形です)。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fja%2Fdocs%2FWeb%2FAPI%2FMediaDevices%2FgetUserMedia" title="MediaDevices: getUserMedia() メソッド - Web API | MDN" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia">developer.mozilla.org</a></cite></p> <p>ここから得られるストリームを <code>createMediaStreamSource</code> と <code>createScriptProcessor</code> で接続することで生のバッファが取得できます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fja%2Fdocs%2FWeb%2FAPI%2FAudioContext%2FcreateMediaStreamSource" title="AudioContext: createMediaStreamSource() メソッド - Web API | MDN" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://developer.mozilla.org/ja/docs/Web/API/AudioContext/createMediaStreamSource">developer.mozilla.org</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fja%2Fdocs%2FWeb%2FAPI%2FBaseAudioContext%2FcreateScriptProcessor" title="BaseAudioContext.createScriptProcessor() - Web API | MDN" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://developer.mozilla.org/ja/docs/Web/API/BaseAudioContext/createScriptProcessor">developer.mozilla.org</a></cite></p> <p>こうして取得したバッファを uLipSync の解析の口につなげてあげれば、Web 上でもマイクを使った<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>が可能になります。<code>uLipSyncMicrophone</code> 相当にするには、マイクの選択など色々きれいに整えてあげる必要がありますが…、必要なものは揃っている印象です。</p> <p>なお、<code>createScriptProcessor</code> は deprecated なようなので、<code>AudioWorklets</code> を使うほうが望ましいようです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231130001132" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231130/20231130001132.png" width="1200" height="67" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>実装時はこちらで試してみます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmtg.github.io%2Fessentia.js%2Fdocs%2Fapi%2Ftutorial-2.%2520Real-time%2520analysis.html" title="2. Real-time analysis" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://mtg.github.io/essentia.js/docs/api/tutorial-2.%20Real-time%20analysis.html">mtg.github.io</a></cite></p> <p>真面目にやると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a> UI の用意や、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>データのローカルストレージへの保存など色々やれる余地はありそうです。ただここいらは自分の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>がまだないので...、しばらくまた寝かせるかもしれません。</p> <h2 id="オーディオ対応について">オーディオ対応について</h2> <h3 id="うえぞうさん方式">うえ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A4%BE%A4%A6%A4%B5%A4%F3">ぞうさん</a>方式</h3> <h5 id="概要">概要</h5> <p>うえ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A4%BE%A4%A6%A4%B5%A4%F3">ぞうさん</a>による <a href="https://github.com/uezo/uLipSyncWebGL">uLipSyncWebGL</a> では jslib を通じて JavaSciprt 上でオーディオのバッファを取得し、それを Unity 側の <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> の世界(正確には wasm と化した <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> の世界)に渡す方式です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fuezo%2FuLipSyncWebGL%2Fblob%2Fmain%2FPlugins%2FuLipSyncWebGL.jslib" title="uLipSyncWebGL/Plugins/uLipSyncWebGL.jslib at main · uezo/uLipSyncWebGL" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/uezo/uLipSyncWebGL/blob/main/Plugins/uLipSyncWebGL.jslib">github.com</a></cite></p> <p>uLipSyncWebGL を導入すると、同名の <code>uLipSyncWebGL</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を <code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>のついた GameObject にアタッチするだけで簡単に動きます。めちゃめちゃお手軽ですごいです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231130/20231130232114.gif" width="969" height="648" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>詳細な流れは次のとおりです。</p> <ul> <li>Unity のオーディオの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>情報を格納している <code>WEBAudio.audioInstances</code> を調べて再生されているオーディオ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を取得</li> <li>gain ノードを持つ最初の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を <code>createScriptProcessor</code> に接続</li> <li>onaudioprocess で得たバッファを文字列として<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%EA%A5%A2%A5%E9%A5%A4%A5%BA">シリアライズ</a>し <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> 側へ <code>SendMessage</code> で伝送</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> 側ではこれをデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%EA%A5%A2%A5%E9%A5%A4%A5%BA">シリアライズ</a>して uLipSync へ渡す</li> </ul> <p><code>setInterval</code> しないとならないのは謎だとのことでした。。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fuezo%2FuLipSyncWebGL%2Fissues%2F1" title="It doesn&#39;t work on WebGL built with Unity 2021 · Issue #1 · uezo/uLipSyncWebGL" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/uezo/uLipSyncWebGL/issues/1">github.com</a></cite></p> <h5 id="調査">調査</h5> <p><code>WEBAudio</code> はあまりネット上に情報がありませんでしたが、コードを読むと Unity が定義しているオブジェクトのようです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231130222655" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231130/20231130222655.png" width="1200" height="859" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><code>audioInstances</code> はそのプロパティとしてここで生成されています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231130223325" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231130/20231130223325.png" width="1200" height="128" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231130223349" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231130/20231130223349.png" width="1200" height="936" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>困ったらこのあたりのコードを直接読むと色々とわかりそうです。</p> <h5 id="該当するオーディオを見つける際の問題">該当するオーディオを見つける際の問題</h5> <p><code>audioInstances</code> から最初に見つかった(<code>gain</code> ノードを持つ)<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を connect しているため、例えば PlayOnAwake でない何かしらのトリガで再生開始されるような AudioSource などは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>対象ではなくなってしまいます(適当なキー押下で再生開始するように組んでみると適切な音声が割り当てられませんでした)。本来であれば、MonoBehaviour 同様、同じ GameObject についている AudioSource を対象に扱いたいところです。エンジン側から呼ばれる <code>_JS_Sound_Create_Channel</code> に渡されている <code>userData</code> がおそらく何らかの識別 ID なのではと考えているのですが、見てみても GUID でも無さそうですし使えるか不明です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231130224429" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231130/20231130224429.png" width="1200" height="620" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>こういう時にエンジンコードが読めれば…と思うのですが。ひとまず <code>loopEnd</code> で長さ一致を追加で見る、あたりが唯一使える方法かもしれません。</p> <h5 id="サンプリングレート合わせ">サンプリングレート合わせ</h5> <p>どこかでサンプリングレートを引っ張ってきて合っているか調べるコードが必要そうです。</p> <h3 id="GetData-方式">GetData() 方式</h3> <p>Issue で書いていただいた方式は、<code>OnAudioFilterRead</code> は使えないものの <code>AudioClip.GetData()</code> は一部使えるので、それを利用する方式です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Fissues%2F57%23issuecomment-1794570418" title="uLipSync 3.0.2 not working with WebGL in Unity 2022.3 · Issue #57 · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/issues/57#issuecomment-1794570418">github.com</a></cite></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 環境下での <code>GetData()</code> については以下の公式リファレンスに記載があります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FScriptReference%2FAudioClip.GetData.html" title="Unity - Scripting API: AudioClip.GetData" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ScriptReference/AudioClip.GetData.html">docs.unity3d.com</a></cite></p> <p>AudioSource(再生<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>)から現在の再生位置を取得し、AudioClip(データ)の適切な位置のバッファにアクセスする方式です。メリットとしては <a class="keyword" href="https://d.hatena.ne.jp/keyword/C%23">C#</a> の世界から出なくて良いのでメンテナンス性が比較的高い点で、デメリットはちょっと処理速度やメモリ的な不安が残る点でしょうか。Issue に書いてくださっている幾つかの点(1フレームディレイ、ステレオデータ)は、少し先読みしたり、適切にステレオデータを扱うことで修正可能です。また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 上ではユーザー操作がある(マウスクリックやキー操作)まで音声を再生しないという縛りがあるのですが、これも何かしらの手段で回避はできそうに思われます。</p> <h2 id="まとめ">まとめ</h2> <p>技術的な面白さとしてはうえ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A4%BE%A4%A6%A4%B5%A4%F3">ぞうさん</a>方式の Web Audio <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> を使う方式なのですが、ちょっと現状は汎用性面で解決できない問題が幾つかあるため、まずはシンプルに <code>GetData()</code> 方式で実装を試してみようかと思います。Web Audio <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> 的な面白さの部分は、ひとまずはマイクの実装つなぎのところで知識欲求は満たそうかな、と思います。</p> hecomi uLipSync の VRM モデル向けリップシンクアニメーションベイクについて hatenablog://entry/6801883189054745492 2023-10-30T23:10:36+09:00 2023-10-30T23:19:45+09:00 はじめに id:bibinbaleo さんのブログで uLipSync を試してみていただいており、その中で VRM モデルのアニメーション書き出しについての記述がありましたので補足解説させていただきます。 bibinbaleo.hatenablog.com uLipSync についてはこちら。 github.com アニメーション書き出しについて トマシープさんの記事では uLipSyncBlendShapeVRM を試されていらっしゃいましたが、現状、uLipSync のアニメーション書き出しは残念ながら uLipSyncBlendShape のみとなっています。VRM 0.X では VR… <h2 id="はじめに">はじめに</h2> <p><a href="http://blog.hatena.ne.jp/bibinbaleo/">id:bibinbaleo</a> さんのブログで uLipSync を試してみていただいており、その中で <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> モデルのアニメーション書き出しについての記述がありましたので補足解説させていただきます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbibinbaleo.hatenablog.com%2Fentry%2F2023%2F10%2F23%2F173210" title="uLipSyncで事前録音した音声からTimelineでVRMモデルを口パク - トマシープが学ぶ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://bibinbaleo.hatenablog.com/entry/2023/10/23/173210">bibinbaleo.hatenablog.com</a></cite></p> <p>uLipSync についてはこちら。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync" title="GitHub - hecomi/uLipSync: MFCC-based LipSync plug-in for Unity using Job System and Burst Compiler" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync">github.com</a></cite></p> <h2 id="アニメーション書き出しについて">アニメーション書き出しについて</h2> <p>トマシープさんの記事では <code>uLipSyncBlendShapeVRM</code> を試されていらっしゃいましたが、現状、uLipSync のアニメーション書き出しは残念ながら <code>uLipSyncBlendShape</code> のみとなっています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 0.X では <code>VRMBlendShapeProxy</code> という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を介して、<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 1.0 では Expression という仕組みを介して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプを扱っています。uLipSync ではこれらの仕組みを利用して <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> モデルに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>をさせていました。このように一層かんでしまっている関係で直接 Unity のアニメーションとしてベイクできなくなっています。</p> <p>一方で、一層かまない形で直接 BlendShape を駆動する <code>uLipSyncBlendShape</code> を利用することでアニメーションはベイクすることが出来ます。</p> <h3 id="1-不要なコンポーネントの削除">1. 不要な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の削除</h3> <p><code>uLipSyncBlendShapeVRM</code> や <code>uLipSyncExpressionVRM</code> は外します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231030224801" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231030/20231030224801.png" width="1020" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="2-uLipSyncBlendShape-の追加とセットアップ">2. uLipSyncBlendShape の追加とセットアップ</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> のモデルと言えど、内部的には <code>SkinnedMeshRenderer</code> を保持しており、その中で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプが定義されています。サンプルでは VRoid を利用して作ったモデルとなっており、ここでは <code>Face</code> というゲームオブジェクトに該当の <code>SkinnedMeshRenderer</code> がありました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231030225448" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231030/20231030225448.png" width="1200" height="681" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><code>Fcl_MTH_*</code>(* は AIUEO や Up, Down, Joy など)という名前で様々な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプが登録されています。</p> <p>では <code>uLipSyncBlendShape</code> をアタッチします。<strong>Skinned Mesh Renderer</strong> から顔を含むメッシュを選択して下さい。出てこない場合は <strong>Find From Children</strong> を外していただき、直接該当の <code>SkinnedMeshRenderer</code> をドラッグドロップします。これで音素を追加して該当の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプを指定してセットアップします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231030225229" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231030/20231030225229.png" width="770" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="3-uLipSync-への登録">3. uLipSync への登録</h3> <p>忘れずに uLipSync へイベントを登録しておきます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231030230010" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231030/20231030230010.png" width="1018" height="782" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="4-アニメーション書き出し">4. アニメーション書き出し</h3> <p>あとは以前の記事の通りアニメーション書き出しを行います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F02%2F19%2F202558" title="uLipSync にアニメーションベイク機能を追加してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/02/19/202558">tips.hecomi.com</a></cite></p> <p>おさらいとしては、<code>BakedData</code> という事前焼き込みアセットを制作し<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>結果をベイク、そしてそれを Window > uLipSync > Animation Clip Generator で指定してアニメーション変換、という感じです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20231030230616" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20231030/20231030230616.png" width="1200" height="851" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>これでアニメーションが生成されました。</p> <h2 id="おわりに">おわりに</h2> <p>エラーで止まってしまう問題は警告を出してプロセスが走らないよう修正しておきます。また、<code>uLipSyncBlendShapeVRM</code> や <code>uLipSyncExpressionVRM</code> 利用時でも自動的に該当の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプを探し出しアニメーションベイクまでたどり着けないかの調査もしようと思います。<a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 書き出しサポートもそろそろなんとかしたいですね…(年末のお休みなどで対応したいところです)。</p> hecomi uLipSync で解析結果を使ったカスタム挙動を作るためのチュートリアル hatenablog://entry/820878482971957521 2023-09-30T23:13:48+09:00 2023-09-30T23:15:43+09:00 はじめに uLipSync では AudioClip やマイクから得た音声を解析するモジュールと、その解析結果を受け取りリップシンクを動作させるためのコンポーネントが分かれています。 github.com この解析結果を受け取る部分を自作することで色々と遊ぶことができるので、本記事ではそのやり方をご紹介します。 導入 いくつか方法はありますが、Package Manager から次の URL を Add package from git URL... するのが簡単だと思います。 https://github.com/hecomi/uLipSync.git#upm 直接 .unitypackag… <h2 id="はじめに">はじめに</h2> <p>uLipSync では AudioClip やマイクから得た音声を解析するモジュールと、その解析結果を受け取り<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>を動作させるための<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が分かれています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync" title="GitHub - hecomi/uLipSync: MFCC-based LipSync plug-in for Unity using Job System and Burst Compiler" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync">github.com</a></cite></p> <p>この解析結果を受け取る部分を自作することで色々と遊ぶことができるので、本記事ではそのやり方をご紹介します。</p> <h2 id="導入">導入</h2> <p>いくつか方法はありますが、Package Manager から次の URL を <em>Add package from git URL...</em> するのが簡単だと思います。</p> <ul> <li><code>https://github.com/hecomi/uLipSync.git#upm</code></li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230930191626" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230930/20230930191626.png" width="922" height="470" loading="lazy" title="" class="hatena-fotolife" style="width:480px" itemprop="image"></a></span></p> <p>直接 .unitypackage を導入する場合は Package Manager から <code>Unity.Mathematics</code> や <code>Unity.Burst</code> の導入を忘れずにおこなって下さい。その他基本的な概念や機能説明は、必要に応じて以下の記事をご参照下さい。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F01%2F30%2F152519" title="uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/01/30/152519">tips.hecomi.com</a></cite></p> <h2 id="解析結果を受け取る手順">解析結果を受け取る手順</h2> <h3 id="uLipSync-コンポーネントを追加">uLipSync <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を追加</h3> <p>まず適当な GameObject を作成し、<code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>をアタッチします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230930194325" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230930/20230930194325.png" width="1200" height="723" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <em>Profile</em> に何かしらの <em>Profile</em> オブジェクトを指定します。<em>Profile</em> の作成の仕方は<a href="https://tips.hecomi.com/entry/2022/01/30/152519">&#x89E3;&#x8AAC;&#x8A18;&#x4E8B;</a>に載せていますが、ひとまずはパッケージに含まれているサンプル(<em>uLipSync-Profile-Sample</em> など)を指定すると動きます。</p> <h3 id="解析結果を受け取るコンポーネントの作成">解析結果を受け取る<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の作成</h3> <p>次のような<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を作ります。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synType">public</span> <span class="synType">class</span> <span class="synType">CustomLipSyncEventHandler </span><span class="synStatement">:</span> MonoBehaviour { <span class="synType">public</span> <span class="synType">void</span> OnLipSyncUpdate(uLipSync.LipSyncInfo info) { <span class="synStatement">if</span> (info.volume <span class="synStatement">&lt;</span> Mathf.Epsilon) <span class="synStatement">return</span>; Debug.LogFormat(<span class="synConstant">$&quot;PHENOME: </span><span class="synSpecial">{</span>info.phoneme<span class="synSpecial">}</span><span class="synConstant">, VOL: </span><span class="synSpecial">{</span>info.volume<span class="synSpecial">}</span><span class="synConstant"> &quot;</span>); } } </pre> <p>そして、これを任意の GameObject にアタッチします。次にこの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%D9%A5%F3%A5%C8%A5%CF%A5%F3%A5%C9%A5%E9">イベントハンドラ</a>を <code>uLipSync</code> に登録します。<em>On Lip Sync Update (LipSyncInfo)</em> で + ボタンを押し、イベントを追加、ここに先程の GameObject を指定してプルダウンリストから該当のイベントを選択します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230930195855" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230930/20230930195855.png" width="920" height="864" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>これで登録完了です。</p> <h3 id="AudioClip-またはマイクコンポーネントの追加">AudioClip またはマイク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の追加</h3> <p>マイクで喋る場合は、<code>uLipSyncMicrophone</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を <code>uLipSync</code> と同じ GameObject にアタッチしておきます。</p> <h3 id="実行">実行</h3> <p>実行してマイクに喋ると次のように認識された母音とボリュームが表示されます(AudioClip の場合はその音に応じたものが表示されます)。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230930213508" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230930/20230930213508.gif" width="768" height="341" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h2 id="渡ってくる情報">渡ってくる情報</h2> <p>イベントに渡ってくる <code>LipSyncInfo</code> の中身は次のようになっています。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">struct</span> LipSyncInfo { <span class="synComment">// 認識された音素</span> <span class="synType">public</span> <span class="synType">string</span> phoneme; <span class="synComment">// ボリューム(0 ~ 1)</span> <span class="synType">public</span> <span class="synType">float</span> volume; <span class="synComment">// ボリュームの生値</span> <span class="synType">public</span> <span class="synType">float</span> rawVolume; <span class="synComment">// 登録された音素と含まれる割合のテーブル(A が 80%、I が 20% など)</span> <span class="synType">public</span> Dictionary&lt;<span class="synType">string</span>, <span class="synType">float</span>&gt; phonemeRatios; } </pre> <p><code>rawVolume</code> は <a class="keyword" href="https://d.hatena.ne.jp/keyword/RMS">RMS</a>(Root Mean Square)ボリュームで、各サンプルの二乗和の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CA%BF%CA%FD%BA%AC">平方根</a>なので、比較的小さな値が出てきます。そのため、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%B7%A5%D9%A5%EB">デシベル</a>空間で適当な扱いやすい値に変更したものを <code>volume</code> として返しています。より拘りたい方は、<code>rawVolume</code> を使ってみて下さい。なお <code>uLipSyncBlendShape</code> などの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>では、その<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>内でこの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%B7%A5%D9%A5%EB">デシベル</a>空間での最小 / 最大値を調整できるようにし、<code>rawVolume</code> を使って計算しています。</p> <h2 id="試しになにか作ってみる">試しになにか作ってみる</h2> <p>それでは試しに <code>phonemeRatios</code> を活用して、認識した音を使ってオブジェクトのカラーを変える<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を作ってみます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> System.Collections.Generic; <span class="synType">public</span> <span class="synType">class</span> <span class="synType">CustomLipSyncEventHandler </span><span class="synStatement">:</span> MonoBehaviour { [System.Serializable] <span class="synType">public</span> <span class="synType">struct</span> PhonemeColor { <span class="synType">public</span> <span class="synType">string</span> phoneme; <span class="synType">public</span> Color color; } [SerializeField] List&lt;PhonemeColor&gt; phonemeColors; [SerializeField, Range(<span class="synConstant">0.01f</span>, <span class="synConstant">1f</span>)] <span class="synType">float</span> minVolume <span class="synStatement">=</span> <span class="synConstant">0.2f</span>; [SerializeField, Range(<span class="synConstant">0f</span>, <span class="synConstant">1f</span>)] <span class="synType">float</span> smooth <span class="synStatement">=</span> <span class="synConstant">0.7f</span>; [SerializeField] Renderer targetRenderer <span class="synStatement">=</span> <span class="synConstant">null</span>; Material _mat; Color _color; <span class="synType">void</span> Start() { <span class="synStatement">if</span> (<span class="synStatement">!</span>targetRenderer) <span class="synStatement">return</span>; _mat <span class="synStatement">=</span> targetRenderer.material; } <span class="synType">void</span> LateUpdate() { _mat.color <span class="synStatement">+=</span> (_color <span class="synStatement">-</span> _mat.color) <span class="synStatement">*</span> (<span class="synConstant">1f</span> <span class="synStatement">-</span> smooth); } <span class="synType">public</span> <span class="synType">void</span> OnLipSyncUpdate(uLipSync.LipSyncInfo info) { <span class="synStatement">if</span> (<span class="synStatement">!</span>_mat) <span class="synStatement">return</span>; <span class="synType">var</span> color <span class="synStatement">=</span> Color.clear; <span class="synStatement">foreach</span> (<span class="synType">var</span> pc <span class="synStatement">in</span> phonemeColors) { <span class="synType">float</span> ratio <span class="synStatement">=</span> info.phonemeRatios.GetValueOrDefault(pc.phoneme, <span class="synConstant">0f</span>); color <span class="synStatement">+=</span> ratio <span class="synStatement">*</span> pc.color; } <span class="synType">var</span> volume <span class="synStatement">=</span> Mathf.Min(info.volume <span class="synStatement">/</span> minVolume, <span class="synConstant">1f</span>); _color <span class="synStatement">=</span> color <span class="synStatement">*</span> volume <span class="synStatement">+</span> Color.white <span class="synStatement">*</span> (<span class="synConstant">1f</span> <span class="synStatement">-</span> volume); Debug.Log(<span class="synConstant">$&quot;&lt;color=</span><span class="synSpecial">\&quot;</span><span class="synConstant">#</span><span class="synSpecial">{</span>ColorUtility.ToHtmlStringRGB(_color)<span class="synSpecial">}\&quot;</span><span class="synConstant">&gt;■&lt;/color&gt; (</span><span class="synSpecial">{</span>volume<span class="synSpecial">}</span><span class="synConstant">)&quot;</span>); } } </pre> <p>予め登録した音素が、認識した音素にどれくらい入っているかを見て色を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>し、指定した <code>Renderer</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>のマテリアルのカラーを上書きしています。この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を適当なオブジェクトにアタッチしたあと、先程と同じように <code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>に登録し、次のようにセットアップします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230930230543" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230930/20230930230543.png" width="1200" height="1010" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>実行すると次のようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230930230711" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230930/20230930230711.gif" width="660" height="753" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>喋った音に応じてオブジェクトの色が変わりました。あとは色々とアイディア次第で面白いものが作れるかもしれません。</p> <h2 id="おわりに">おわりに</h2> <p>正確に音を取らないと行けないような<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>では、現状の解析法では外乱に弱いのでいまいちですが、間違っても大丈夫な賑やかしに使いたいようなケースでは面白い使い途が色々あるかもしれません。</p> <h2 id="余談">余談</h2> <p>先日、VTube Studio さんが uLipSync を音声を使った<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>向けの機能として採用して下さいました。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">【📢】 <a href="https://twitter.com/hashtag/VTubeStudio?src=hash&amp;ref_src=twsrc%5Etfw">#VTubeStudio</a> v1.27.5 is now released!! (<a class="keyword" href="https://d.hatena.ne.jp/keyword/Windows">Windows</a>/<a class="keyword" href="https://d.hatena.ne.jp/keyword/macOS">macOS</a>/<a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>/<a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a>)<br><br>Added advanced lipsync 🎤 ( uLipSync by <a href="https://twitter.com/hecomi?ref_src=twsrc%5Etfw">@hecomi</a> ), support for Live2D 5.0 and a cute Twitch chat scroll overlay thingy ✨<br><br>🧵👇 <a href="https://t.co/WInNWCBxBP">pic.twitter.com/WInNWCBxBP</a></p>&mdash; VTube Studio (@VTubeStudio) <a href="https://twitter.com/VTubeStudio/status/1705385957019857143?ref_src=twsrc%5Etfw">2023年9月23日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>v3 から追加した UI のための <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> も使ってくださっていて、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>の機構も搭載されています!カメラによるト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%C3%A5%AD%A5%F3%A5%B0">ラッキング</a>ではない場合に威力を発揮すると思います。VTube Studio をお使いの方はぜひお試し下さい。</p> hecomi Unity でボリュームレンダリングをしてみる - vol.6 ボリュームの内部 hatenablog://entry/820878482962156505 2023-08-27T18:53:03+09:00 2023-08-28T01:39:41+09:00 はじめに これまでいくつかボリュームレンダリングの記事を書いてきました。以下は前回の記事です。 tips.hecomi.com これまではキューブポリゴンを描画範囲として 3D テクスチャのボリュームレンダリングを行っていましたが、これだとポリゴンの内部にカメラが入り込んだときにカリングされてしまいます。 今回は内部にカメラが入っても継続して描画できるようにするには、どのように修正を加えればよいかの解説を行います。 リポジトリ github.com 10. Inside Volume が今回の内容になります。 原因と対策 先程も触れましたが、カメラがポリゴンの内部に入り込むと、ポリゴンは描画さ… <h2 id="はじめに">はじめに</h2> <p>これまでいくつかボリューム<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>の記事を書いてきました。以下は前回の記事です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2020%2F05%2F27%2F012539" title="Unity でボリュームレンダリングをしてみる - vol.5 遮蔽とループの最適化 - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2020/05/27/012539">tips.hecomi.com</a></cite></p> <p>これまではキューブポリゴンを描画範囲として 3D テクスチャのボリューム<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>を行っていましたが、これだとポリゴンの内部にカメラが入り込んだときにカリングされてしまいます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230827/20230827160909.png" width="1200" height="648" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>今回は内部にカメラが入っても継続して描画できるようにするには、どのように修正を加えればよいかの解説を行います。</p> <h2 id="リポジトリ"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a></h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FUnityVolumeRendering" title="GitHub - hecomi/UnityVolumeRendering: A simple example of Volume Rendering for Unity." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/UnityVolumeRendering">github.com</a></cite></p> <p><code>10. Inside Volume</code> が今回の内容になります。</p> <h2 id="原因と対策">原因と対策</h2> <p>先程も触れましたが、カメラがポリゴンの内部に入り込むと、ポリゴンは描画されません。正確には、カメラのニアクリップがオブジェクトの内部に入り込むと…、なので下図のようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230827163027" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230827/20230827163027.png" width="1200" height="746" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>図の赤い部分のポリゴンが表示されなくなる感じです。解決策としては、<code>Cull Off</code> にしてポリゴンの裏側も描画するようにし、レイの開始点をニアクリップ位置になるようにします(これまでのコードはポリゴン表面からレイをカメラ方向に打っていました)。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230827165630" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230827/20230827165630.png" width="1200" height="769" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h2 id="実装">実装</h2> <p>ではこのコードを実装してみましょう。まずは前回までのおさらい…、というかコードの前提だけ確認しておきます。これまでのボリューム<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>でやっていることは、レイを飛ばしてステップごとに 3D テクスチャや密度関数をサンプリングし、ライティングを考慮しながら該当<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>の色を決定している感じです。このレイの開始点はこれまではポリゴンの表面からでした。</p> <pre class="code lang-c" data-lang="c" data-unlink>Shader <span class="synConstant">&quot;CloudLitInsideVolume&quot;</span> { ... CGINCLUDE ... v2f <span class="synIdentifier">vert</span>(appdata v) { v2f o; o.vertex = <span class="synIdentifier">UnityObjectToClipPos</span>(v.vertex); o.worldPos = <span class="synIdentifier">mul</span>(unity_ObjectToWorld, v.vertex).xyz; o.projPos = <span class="synIdentifier">ComputeScreenPos</span>(o.vertex); <span class="synIdentifier">COMPUTE_EYEDEPTH</span>(o.projPos.z); <span class="synStatement">return</span> o; } float4 <span class="synIdentifier">frag</span>(v2f i) : SV_Target { float3 worldPos = i.worldPos; float3 worldDir = <span class="synIdentifier">normalize</span>(worldPos - _WorldSpaceCameraPos); ... <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i &lt; loop; ++i) { ... (レイを飛ばして 3D テクスチャや密度関数をサンプルして色を決定) } ... <span class="synStatement">return</span> color; } ENDCG SubShader { Tags { <span class="synConstant">&quot;Queue&quot;</span> = <span class="synConstant">&quot;Transparent&quot;</span> <span class="synConstant">&quot;RenderType&quot;</span> = <span class="synConstant">&quot;Transparent&quot;</span> } Pass { Cull Back ZWrite Off Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM <span class="synPreProc">#pragma vertex vert</span> <span class="synPreProc">#pragma fragment frag</span> ENDCG } } } </pre> <p>この <code>i.worldPos</code> に頂点シェーダから渡ってきたその<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>でのポリゴンの座標が入ってきています。さて、先程図で見たように、<code>Cull Back</code> から <code>Cull Off</code> にした場合は、この座標がポリゴン裏側、つまり向かいの面の裏側の座標となってしまいます。これをどうにか判定して、最終的に内部にあるときにはニアクリップ面からレイを飛ばすようにする必要があります。</p> <p>そこで裏側かどうかではなく、現在のニアクリップ面がキューブの内部に入っているかどうかを判定することにします。まず、とあるワールド座標が現在<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>中のキューブ(辺の長さが 1 m の立方体)に含まれているかの判定は以下の <code>isInnerCube</code> で行えます。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synType">inline</span> float3 <span class="synIdentifier">toLocal</span>(float3 pos) { <span class="synStatement">return</span> <span class="synIdentifier">mul</span>(unity_WorldToObject, <span class="synIdentifier">float4</span>(pos, <span class="synConstant">1.0</span>)).xyz; } <span class="synType">inline</span> float3 <span class="synIdentifier">getScale</span>() { <span class="synStatement">return</span> <span class="synIdentifier">float3</span>( <span class="synIdentifier">length</span>(<span class="synIdentifier">float3</span>(unity_ObjectToWorld[<span class="synConstant">0</span>].x, unity_ObjectToWorld[<span class="synConstant">1</span>].x, unity_ObjectToWorld[<span class="synConstant">2</span>].x)), <span class="synIdentifier">length</span>(<span class="synIdentifier">float3</span>(unity_ObjectToWorld[<span class="synConstant">0</span>].y, unity_ObjectToWorld[<span class="synConstant">1</span>].y, unity_ObjectToWorld[<span class="synConstant">2</span>].y)), <span class="synIdentifier">length</span>(<span class="synIdentifier">float3</span>(unity_ObjectToWorld[<span class="synConstant">0</span>].z, unity_ObjectToWorld[<span class="synConstant">1</span>].z, unity_ObjectToWorld[<span class="synConstant">2</span>].z))); } <span class="synType">inline</span> <span class="synType">bool</span> <span class="synIdentifier">isInnerCube</span>(float3 pos) { pos = <span class="synIdentifier">toLocal</span>(pos); float3 scale = <span class="synIdentifier">getScale</span>(); <span class="synStatement">return</span> <span class="synIdentifier">all</span>(<span class="synIdentifier">max</span>(scale * <span class="synConstant">0.5</span> - <span class="synIdentifier">abs</span>(pos), <span class="synConstant">0.0</span>)); } </pre> <p>そしてニアクリップ面までの距離を以下の <code>getDistanceFromCameraToNearClipPlane</code> で行います。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synType">inline</span> float3 <span class="synIdentifier">getCameraPosition</span>() { <span class="synStatement">return</span> UNITY_MATRIX_I_V._m03_m13_m23; } <span class="synType">inline</span> <span class="synType">float</span> <span class="synIdentifier">getCameraFocalLength</span>() { <span class="synStatement">return</span> <span class="synIdentifier">abs</span>(UNITY_MATRIX_P[<span class="synConstant">1</span>][<span class="synConstant">1</span>]); } <span class="synType">inline</span> <span class="synType">float</span> <span class="synIdentifier">getCameraNearClip</span>() { <span class="synStatement">return</span> _ProjectionParams.y; } <span class="synType">inline</span> <span class="synType">float</span> <span class="synIdentifier">getDistanceFromCameraToNearClipPlane</span>(float4 projPos) { projPos.xy /= projPos.w; projPos.xy = (projPos.xy - <span class="synConstant">0.5</span>) * <span class="synConstant">2.0</span>; projPos.x *= _ScreenParams.x / _ScreenParams.y; float3 norm = <span class="synIdentifier">normalize</span>(<span class="synIdentifier">float3</span>(projPos.xy, <span class="synIdentifier">getCameraFocalLength</span>())); <span class="synStatement">return</span> <span class="synIdentifier">getCameraNearClip</span>() / norm.z; } </pre> <p>これらを組み合わせて、ニアクリップ面の座標を求め、それがキューブ内に入っていたら例の開始点を上書きするように修正します。</p> <pre class="code lang-c" data-lang="c" data-unlink>float4 <span class="synIdentifier">frag</span>(v2f i) : SV_Target { float3 worldPos = i.worldPos; ... float3 cameraPos = <span class="synIdentifier">getCameraPosition</span>(); <span class="synType">float</span> distToNearClipPlane = <span class="synIdentifier">getDistanceFromCameraToNearClipPlane</span>(i.projPos); float3 nearClipPos = cameraPos + distToNearClipPlane * worldDir; <span class="synStatement">if</span> (<span class="synIdentifier">isInnerCube</span>(nearClipPos)) { worldPos = nearClipPos; } ... } } </pre> <p>これで内部に入れるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230827/20230827180211.gif" width="720" height="405" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>なお、2 パスにして、最初は表、次は裏(このときの開始点はニアクリップ面)とやっても良いかもしれません。</p> <h2 id="デプスのパスの修正">デプスのパスの修正</h2> <p>前回、デプス比較することによる通常のポリゴンとの干渉について書きましたが、上記コードだけでは次のように内部に入ったタイミングでパタパタしてしまいます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230827/20230827180956.gif" width="720" height="405" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これは <code>ZTest On</code> をしていることで、そもそもこのキューブの裏側ポリゴンの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>シェーダが駆動されなくなっています。通常、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>では手前のものから順番に描画し、後ろ側の見えないオブジェクトの不要な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>シェーダが駆動されないようにしています(これは頂点シェーダで出力した <code>SV_POSITION</code> セマンティクスのついた座標の情報を使って自動的に行われています)。少しパフォーマンス的にお行儀が悪いですが <code>ZTest Off</code> にしてしまうことで、このデプス比較を切ってしまい、常に描画されるようにしてみます。すると次のようにカメラが近づいても正しく表示されるようになりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230827/20230827184348.gif" width="720" height="405" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="デモ">デモ</h2> <p>DICOM の記事で見たテクスチャの中に入るのもやってみました。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">3D テクスチャの中に入るのもやってみた<a href="https://t.co/AtFyjokfu8">https://t.co/AtFyjokfu8</a> <a href="https://t.co/PLTcDay6C3">pic.twitter.com/PLTcDay6C3</a></p>&mdash; 凹 (@hecomi) <a href="https://twitter.com/hecomi/status/1695837838766678068?ref_src=twsrc%5Etfw">2023年8月27日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2021%2F09%2F27%2F002049" title="Unity で DICOM 画像をレンダリングしてみる - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2021/09/27/002049">tips.hecomi.com</a></cite></p> <h2 id="おわりに">おわりに</h2> <p>ちなみに、これらの計算は uRaymarching でも行っています。興味がある方はこちらもご参考いただけると幸いです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuRaymarching" title="GitHub - hecomi/uRaymarching: Raymarching Shader Generator in Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uRaymarching">github.com</a></cite></p> <h2 id="追記">追記</h2> <p>書き終わったあとに、<a href="https://twitter.com/karasusan">@karasusan</a> さんが同じ内容書かれているのに気づきました…。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fkarasusan%2Fitems%2Fce8891f5c4b63d7d06ba" title="Unity でリアルな雲を表現するためのシェーダを作成する - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/karasusan/items/ce8891f5c4b63d7d06ba">qiita.com</a></cite></p> hecomi Unity の Version Defines と.unitypackage インポートについて hatenablog://entry/820878482953989063 2023-07-30T19:11:52+09:00 2023-07-30T19:11:52+09:00 はじめに 先日、uLipSync v3.0.2 をリリースし、asmdef の Version Defines を利用した VRM 0.X / 1.0 のパッケージのインポート状況に応じた VRM 向けコンポーネントのエラー抑制を行いました。 tips.hecomi.com ただこちら、.unitypackage 経由でインポートした際にうまく動かない、という報告をいただき調査してみました。 原因 Version Defines はパッケージのインポート状況に応じてシンボルを定義できる仕組みです。パッケージはバージョン指定も可能で、またパッケージだけではなく Unity のバージョン指定も出来… <h2 id="はじめに">はじめに</h2> <p>先日、<strong>uLipSync</strong> v3.0.2 をリリースし、asmdef の <em>Version Defines</em> を利用した <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 0.X / 1.0 のパッケージのインポート状況に応じた <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 向け<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>のエラー抑制を行いました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2023%2F05%2F30%2F215345" title="uLipSync の不具合修正(VRM / .NET Standard 2.0 関連)をしました - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2023/05/30/215345">tips.hecomi.com</a></cite></p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530010403" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530010403.png" width="1160" height="410" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>ただこちら、.unitypackage 経由でインポートした際にうまく動かない、という報告をいただき調査してみました。</p> <h2 id="原因">原因</h2> <p><em>Version Defines</em> はパッケージのインポート状況に応じてシンボルを定義できる仕組みです。パッケージはバージョン指定も可能で、またパッケージだけではなく Unity のバージョン指定も出来ます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FManual%2FScriptCompilationAssemblyDefinitionFiles.html%23define-symbols" title="Unity - Manual: Assembly definitions" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html#define-symbols">docs.unity3d.com</a></cite></p> <p>ただ、.unitypackage 経由でインストールした際、<em>Version Defines</em> で指定する <em>Resource</em> がインポートされた状態として扱われず、残念ながら <em>Version Defines</em> 経由のシンボル定義が行われません。たしかに、<code>com.vrmc.vrm</code> という名前は package.<a class="keyword" href="https://d.hatena.ne.jp/keyword/json">json</a> に記述されている名前であり、単純なファイル展開を行う .unitypackage 経由インポートでは検出しようがなさそうです。パッケージ名ではなく、<em>Assembly Definition References</em>(こちらは内部的には GUID)と組み合わせたシンボル定義の仕組みが出来てくれたら救いがあるかもしれません。また、別ライブラリ側が何かしらのシンボルを用意してくれていれば、<em>Define Constraints</em> でそもそも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>されないようにする、なども有効かもしれません。</p> <h2 id="対策">対策</h2> <p>現状は、インストールしてもらったユーザーサイドで <em>Project Settings > Player > Script Compilation > Scripting Define Symbols </em>から必要なシンボルをマニュアルで定義してもらう必要がありそうです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230730182238" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230730/20230730182238.png" width="1200" height="649" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h2 id="おわりに">おわりに</h2> <p>依存ライブラリがインポートされていないときでも良い感じに動くように対応しようとしていますが、今回のように UPM 外の問題の対応は難しいので、コア機能を絞り、特殊なサンプルは別ライブラリに分離するといった検討も有効かもしれません。</p> hecomi Unity でエディタ拡張向けの重い画像生成の改善をしてみた hatenablog://entry/820878482945499548 2023-06-29T23:47:49+09:00 2023-06-29T23:47:49+09:00 はじめに 大分ニッチな話だと思うのですが、現在メンテしているプロジェクトで音声の解析結果を示すために画像生成を行い、それをエディタ拡張で表示して見せています。 これは Burst と Job を使って生成しているのですが、エディタ拡張の UI の体系は同期処理となるため、現状では Job を即時実行・回収することで、同期処理系となってしまっています。 tips.hecomi.com 今回はここを何とかしようと試みたお話を共有します。内容はテクスチャ生成ですが、それ以外にも重い処理を行うようなエディタ拡張のヒントになるかと思います。 コードと問題点 具体的に以下のようなコードを想定してみましょう… <h2 id="はじめに">はじめに</h2> <p>大分ニッチな話だと思うのですが、現在メンテしているプロジェクトで音声の解析結果を示すために画像生成を行い、それをエディタ拡張で表示して見せています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230629004550" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230629/20230629004550.png" width="1200" height="488" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>これは Burst と Job を使って生成しているのですが、エディタ拡張の UI の体系は同期処理となるため、現状では Job を即時実行・回収することで、同期処理系となってしまっています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F06%2F30%2F005251" title="uLipSync の Timeline エディタのパフォーマンス改善を行ってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/06/30/005251">tips.hecomi.com</a></cite></p> <p>今回はここを何とかしようと試みたお話を共有します。内容はテクスチャ生成ですが、それ以外にも重い処理を行うようなエディタ拡張のヒントになるかと思います。</p> <h2 id="コードと問題点">コードと問題点</h2> <p>具体的に以下のようなコードを想定してみましょう。</p> <h5 id="AsyncTextureCreatorcs">AsyncTextureCreator.cs</h5> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synType">public</span> <span class="synType">class</span> <span class="synType">AsyncTextureCreator </span><span class="synStatement">:</span> MonoBehaviour { <span class="synType">public</span> <span class="synType">int</span> width <span class="synStatement">=</span> <span class="synConstant">128</span>; <span class="synType">public</span> <span class="synType">int</span> height <span class="synStatement">=</span> <span class="synConstant">32</span>; } </pre> <h5 id="AsyncTextureCreatorEditorcs">AsyncTextureCreatorEditor.cs</h5> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEditor; <span class="synStatement">using</span> Unity.Burst; <span class="synStatement">using</span> Unity.Jobs; <span class="synStatement">using</span> Unity.Collections; <span class="synStatement">using</span> Unity.Mathematics; <span class="synStatement">using</span> UnityEngine.Profiling; <span class="synComment">// カラーバッファに適当な float バッファを元に色を書き込むジョブ</span> [BurstCompile] <span class="synType">internal</span> <span class="synType">struct</span> CreateTextureJob <span class="synStatement">:</span> IJob { [WriteOnly] <span class="synType">public</span> NativeArray&lt;Color32&gt; texColors; [ReadOnly] [DeallocateOnJobCompletion] <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; array; Color ToRGB(<span class="synType">float</span> hue) { hue <span class="synStatement">=</span> (<span class="synConstant">1f</span> <span class="synStatement">-</span> math.cos(math.PI <span class="synStatement">*</span> hue)) <span class="synStatement">*</span> <span class="synConstant">0.5f</span>; hue <span class="synStatement">=</span> <span class="synConstant">1f</span> <span class="synStatement">-</span> hue; hue <span class="synStatement">*=</span> <span class="synConstant">5f</span>; <span class="synType">var</span> x <span class="synStatement">=</span> <span class="synConstant">1</span> <span class="synStatement">-</span> math.abs(hue % <span class="synConstant">2f</span> <span class="synStatement">-</span> <span class="synConstant">1f</span>); <span class="synStatement">return</span> hue <span class="synStatement">&lt;</span> <span class="synConstant">1f</span> <span class="synStatement">?</span> <span class="synStatement">new</span> Color(<span class="synConstant">1f</span>, x, <span class="synConstant">0f</span>) <span class="synStatement">:</span> hue <span class="synStatement">&lt;</span> <span class="synConstant">2f</span> <span class="synStatement">?</span> <span class="synStatement">new</span> Color(x, <span class="synConstant">1f</span>, <span class="synConstant">0f</span>) <span class="synStatement">:</span> hue <span class="synStatement">&lt;</span> <span class="synConstant">3f</span> <span class="synStatement">?</span> <span class="synStatement">new</span> Color(<span class="synConstant">0f</span>, <span class="synConstant">1f</span>, x) <span class="synStatement">:</span> hue <span class="synStatement">&lt;</span> <span class="synConstant">4f</span> <span class="synStatement">?</span> <span class="synStatement">new</span> Color(<span class="synConstant">0f</span>, x, <span class="synConstant">1f</span>) <span class="synStatement">:</span> <span class="synStatement">new</span> Color(x <span class="synStatement">*</span> <span class="synConstant">0.5f</span>, <span class="synConstant">0f</span>, <span class="synConstant">0.5f</span>); } <span class="synType">public</span> <span class="synType">void</span> Execute() { <span class="synStatement">for</span> (<span class="synType">int</span> i <span class="synStatement">=</span> <span class="synConstant">0</span>; i <span class="synStatement">&lt;</span> array.Length; <span class="synStatement">++</span>i) { texColors[i] <span class="synStatement">=</span> ToRGB(array[i]); } } } [CustomEditor(<span class="synStatement">typeof</span><span class="synType">(AsyncTextureCreator)</span>)] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">AsyncTextureCreatorEditor </span><span class="synStatement">:</span> Editor { AsyncTextureCreator creator <span class="synStatement">=&gt;</span> target <span class="synStatement">as</span> <span class="synType">AsyncTextureCreator</span>; Texture2D _texture; CustomSampler _sampler <span class="synStatement">=</span> CustomSampler.Create(<span class="synConstant">&quot;AsyncTextureCreatorEditor.UpdateTexture&quot;</span>); <span class="synType">void</span> UpdateTexture() { _sampler.Begin(); <span class="synComment">// テクスチャサイズが変更されたら Texture2D を作成</span> <span class="synType">int</span> width <span class="synStatement">=</span> creator.width; <span class="synType">int</span> height <span class="synStatement">=</span> creator.height; <span class="synStatement">if</span> (<span class="synStatement">!</span>_texture <span class="synStatement">||</span> _texture.width <span class="synStatement">!=</span> width <span class="synStatement">||</span> _texture.height <span class="synStatement">!=</span> height) { _texture <span class="synStatement">=</span> <span class="synStatement">new</span> Texture2D(width, height); } <span class="synComment">// Texture2D のカラーバッファのポインタと、ジョブで色塗りする用のバッファを生成</span> <span class="synType">var</span> texColors <span class="synStatement">=</span> _texture.GetPixelData&lt;Color32&gt;(<span class="synConstant">0</span>); <span class="synType">var</span> array <span class="synStatement">=</span> <span class="synStatement">new</span> NativeArray&lt;<span class="synType">float</span>&gt;(width <span class="synStatement">*</span> height, Allocator.TempJob); <span class="synComment">// バッファには適当な数値を詰めておく</span> <span class="synStatement">for</span> (<span class="synType">int</span> y <span class="synStatement">=</span> <span class="synConstant">0</span>; y <span class="synStatement">&lt;</span> height; <span class="synStatement">++</span>y) { <span class="synStatement">for</span> (<span class="synType">int</span> x <span class="synStatement">=</span> <span class="synConstant">0</span>; x <span class="synStatement">&lt;</span> width; <span class="synStatement">++</span>x) { <span class="synType">int</span> i <span class="synStatement">=</span> y <span class="synStatement">*</span> width <span class="synStatement">+</span> x; array[i] <span class="synStatement">=</span> i <span class="synStatement">/</span> <span class="synConstant">256f</span>; } } <span class="synComment">// ジョブを作成</span> <span class="synType">var</span> job <span class="synStatement">=</span> <span class="synStatement">new</span> CreateTextureJob() { texColors <span class="synStatement">=</span> texColors, array <span class="synStatement">=</span> array, }; <span class="synComment">// ジョブを実行・終了待ち</span> job.Schedule().Complete(); <span class="synComment">// GPU 上のテクスチャへと反映</span> _texture.Apply(); _sampler.End(); } <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnInspectorGUI() { DrawDefaultInspector(); EditorGUILayout.Space(); UpdateTexture(); <span class="synType">var</span> area <span class="synStatement">=</span> EditorGUILayout.GetControlRect(<span class="synConstant">false</span>, creator.height); GUI.DrawTexture(area, _texture, ScaleMode.StretchToFill); } } </pre> <p>これを実行すると以下のような UI が表示されます。</p> <h5 id="UI">UI</h5> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230629005723" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230629/20230629005723.png" width="1114" height="1088" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>こちらのパフォーマンスは以下のような感じです。</p> <h5 id="Profiler">Profiler</h5> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230629010436" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230629/20230629010436.png" width="1200" height="442" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>同期ジョブを実行しているのでジョブが重いときにかなり長い時間<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%ED%A5%C3%A5%AD%A5%F3%A5%B0">ブロッキング</a>してしまうことが分かります。そして何故か一つの <code>InspectorWindow.Repaint</code> から <code>OnGUI</code> が 3 回呼ばれており謎です。。</p> <h2 id="改善案">改善案</h2> <p>一つは同期ジョブを何とか非同期ジョブにできないかの検討、そしてもう一つは単一描画リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト中に複数回呼ばれてしまい、無駄な画像生成が走っているのを止める点です。</p> <h3 id="複数回呼び出しの抑制">複数回呼び出しの抑制</h3> <p>まずは明らかに無駄である複数回呼び出しを何とかします。残念ながらなぜ単一の <a class="keyword" href="https://d.hatena.ne.jp/keyword/GUI">GUI</a> 描画リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トから複数回描画が走ってしまうのかは分からなかったですが、単一フレーム内に限らず、基本的には変更があったときのみ新しいテクスチャを生成・更新して、そうでないときはキャッシュしたものを再利用する形のほうが望ましいです。</p> <p>ここでは横幅や縦幅が変更されたときに更新するようにしてみます。実際の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>では、何かしらのフラグを通じて更新要求を監視するのが良いと思われます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> <span class="synType">AsyncTextureCreatorEditor </span><span class="synStatement">:</span> Editor { ... <span class="synType">bool</span> isTextureUpdateRequired <span class="synStatement">=&gt;</span> <span class="synStatement">!</span>_texture <span class="synStatement">||</span> _texture.width <span class="synStatement">!=</span> creator.width <span class="synStatement">||</span> _texture.height <span class="synStatement">!=</span> creator.height; <span class="synType">void</span> UpdateTexture() { <span class="synStatement">if</span> (<span class="synStatement">!</span>isTextureUpdateRequired) <span class="synStatement">return</span>; <span class="synType">int</span> width <span class="synStatement">=</span> creator.width; <span class="synType">int</span> height <span class="synStatement">=</span> creator.height; _texture <span class="synStatement">=</span> <span class="synStatement">new</span> Texture2D(width, height); ... } } </pre> <h3 id="非同期ジョブ検討">非同期ジョブ検討</h3> <p>当該フレームに得た情報を描画するのは、先にも述べたように同期 UI の体系なので基本的には難しいと思います。ただし 1 フレーム遅らせることが許容できるケースであれば比較的簡単そうです。戦略としては、更新要求があったら Job をスケジュールし、次の <code>OnInspectorGUI</code> の初めで回収する、というものです。ただ、UI に変化がない大半の時間は <code>OnInspectorGUI</code> は基本的には呼ばれません。そこで、無理やりもう一度 <code>OnInspectorGUI</code> が呼ばれるようにテクスチャを更新した際は、エディタに対して <code>Repaint</code> を要求します。これで次のフレームに再度 <code>OnInspectorGUI</code> が回ってくるのでそこで更新処理を行う、という形になります。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> <span class="synType">AsyncTextureCreatorEditor </span><span class="synStatement">:</span> Editor { ... JobHandle _jobHandle; <span class="synType">bool</span> _isTextureUpdateRequested <span class="synStatement">=</span> <span class="synConstant">false</span>; <span class="synType">bool</span> isTextureUpdateRequired <span class="synStatement">=&gt;</span> ...; <span class="synType">void</span> UpdateTexture() { <span class="synStatement">if</span> (_isTextureUpdateRequested) { _jobHandle.Complete(); _texture.Apply(); _isTextureUpdateRequested <span class="synStatement">=</span> <span class="synConstant">false</span>; } <span class="synStatement">if</span> (<span class="synStatement">!</span>isTextureUpdateRequired) <span class="synStatement">return</span>; ... <span class="synType">var</span> job <span class="synStatement">=</span> <span class="synStatement">new</span> CreateTextureJob() { ... }; _jobHandle <span class="synStatement">=</span> job.Schedule(); _isTextureUpdateRequested <span class="synStatement">=</span> <span class="synConstant">true</span>; Repaint(); } ... } </pre> <p>これでジョブが非同期実行されるようになり、エディタへのパフォーマンスの影響を最小限にすることができました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230629/20230629233446.png" width="1200" height="649" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>まだ <code>Texture2D</code> の生成をメインスレッドで行っている関係で、ここにパフォーマンス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%EB%A5%CD%A5%C3%A5%AF">ボトルネック</a>がありますが、テクスチャサイズを固定できるようなケースでは問題とならないと思います。</p> <h2 id="コード全文">コード全文</h2> <ul> <li><a href="https://gist.github.com/hecomi/62513a24bd3b07dd67ab17d3c63dde5b">AsyncTextureCreator.cs &middot; GitHub</a></li> </ul> <h2 id="おわりに">おわりに</h2> <p>この他にも、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>側から更新要求を行えるようなケースであれば、そのタイミングでジョブを実行し、運が良ければ(ジョブが <code>IsCompleted</code> になっていたら)当該フレームで描画、そうでなければ次フレーム、みたいな感じのも出来るかもしれません。</p> <p>エディタ拡張が重くなってしまい、そもそものゲームやアプリの方へのパフォーマンスの影響を与えてしまうのは大変よろしくないと思いますので、今後もパフォーマンスには気を遣っていきたいと思います。元の目的である uLipSync への組み込みは後日行います。</p> hecomi uLipSync の不具合修正(VRM / .NET Standard 2.0 関連)をしました hatenablog://entry/820878482936067832 2023-05-30T21:53:45+09:00 2023-05-30T21:54:47+09:00 はじめに 先日 v3.0.0 をリリースした uLipSync ですが、いくつか不具合修正が来ていたため修正しました。 tips.hecomi.com VRM パッケージを読み込んでいない際にエラーが出る VRM パッケージを両方(VRM 0.X / VRM 1.0)読み込まないとならない Unity 2019 でエラーが出る 古い Timeline アセットを読み込んでいる際にエラーが出る 本エントリでは修正についてのメモと、ついでなので改めて VRM でのセットアップ方法について解説を行いたいと思います。 最新リリース github.com VRM のセットアップ方法 uLipSync の… <h2 id="はじめに">はじめに</h2> <p>先日 v3.0.0 をリリースした uLipSync ですが、いくつか不具合修正が来ていたため修正しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2023%2F04%2F28%2F230437" title="uLipSync v3.0.0 をリリースしました - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2023/04/28/230437">tips.hecomi.com</a></cite></p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> パッケージを読み込んでいない際にエラーが出る</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> パッケージを両方(<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 0.X / <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 1.0)読み込まないとならない</li> <li>Unity 2019 でエラーが出る</li> <li>古い Timeline アセットを読み込んでいる際にエラーが出る</li> </ul> <p>本エントリでは修正についてのメモと、ついでなので改めて <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> でのセットアップ方法について解説を行いたいと思います。</p> <h2 id="最新リリース">最新リリース</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Freleases%2Ftag%2Fv3.0.2" title="Release v3.0.2 has been released · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/releases/tag/v3.0.2">github.com</a></cite></p> <h2 id="VRM-のセットアップ方法"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> のセットアップ方法</h2> <h3 id="uLipSync-のインポート">uLipSync のインポート</h3> <p>uLipSync のインポートの方法は 3 種類ありますが、お手軽なのは git URL かな?と思います(個人的なオススメは Scoped Registry 登録ですが)。<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 関連のパッケージも同じ仕組みで入れるので、ひとまずこちらで解説します。</p> <p>まず、<em>Window</em> > <em>Package Manager</em> から Package Manager ウィンドウを開き、左上の+ボタンから <em>Add package from git URL...</em> を選択します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230527170744" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230527/20230527170744.png" width="1200" height="369" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>ここで <code>https://github.com/hecomi/uLipSync.git#upm</code> を入力します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230527170940" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230527/20230527170940.png" width="1200" height="304" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>Add ボタンを押せば導入完了です。次にサンプルをインポートします。サンプルは、<em>Package Manager</em> で uLipSync を選択、右側にある <em>Samples</em> を展開すると見えます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230527172611" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230527/20230527172611.png" width="1200" height="651" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>以下のサンプルをインポートしてください。</p> <ul> <li><em>00. Common</em> <ul> <li>サンプルで使うユニティちゃん関連アセットや <em>Profile</em> などが含まれています</li> </ul> </li> <li><em>04. <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a></em> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>やサンプルシーンが含まれています</li> </ul> </li> </ul> <h3 id="VRM-関連パッケージのインポート"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 関連パッケージのインポート</h3> <p>次に UniVRM のパッケージをインポートします。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fvrm-c%2FUniVRM%2Freleases" title="Releases · vrm-c/UniVRM" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/vrm-c/UniVRM/releases">github.com</a></cite></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 1.0 の場合は以下のパッケージを git URL にて追加します。</p> <ul> <li><code>com.vrmc.vrmshaders</code></li> <li><code>com.vrmc.gltf</code></li> <li><code>com.vrmc.vrm</code></li> </ul> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 0.X の場合は以下になります。</p> <ul> <li><code>com.vrmc.vrmshaders</code></li> <li><code>com.vrmc.gltf</code></li> <li><code>com.vrmc.univrm</code></li> </ul> <p>両バージョン使う場合は両方インポートします。</p> <h3 id="VRM-のインポート"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> のインポート</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> は UniVRM のおかげでファイルを Unity に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E9%A5%C3%A5%B0%26amp%3B%A5%C9%A5%ED%A5%C3%A5%D7">ドラッグ&amp;ドロップ</a>すればインポートできるようになっています。テスト用には <em>04. <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a></em> に VRoid を使って作製した <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> データも含んでいますのでそちらをご使用ください。</p> <h3 id="セットアップ">セットアップ</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 1.0 の場合を解説します。サンプルシーンは <em>04-2. <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 1.0</em> になります。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> に次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>をアタッチします。</p> <ul> <li><strong><code>AudioSource</code></strong> <ul> <li>口パクのもととなる音声をセットします</li> </ul> </li> <li><strong><code>uLipSync</code></strong> <ul> <li>口パクの解析を行う<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>です</li> </ul> </li> <li><strong><code>uLipSyncExpressionVRM</code></strong> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> の Expression を使って uLipSync の解析結果を口パクに反映します</li> </ul> </li> </ul> <p>ここでは <em>00. Common</em> に含まれたユニティちゃんの音声を利用してみます。これにあった口パクを行えるよう、uLipSync <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <em>Profile</em> タブの中の <em>Profile</em> に、<em>uLipSync-Profile-UnityChan</em> を指定してください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530001912" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530001912.png" width="1200" height="940" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>そして、口パク解析結果が実際に Expression で動作するように、<em>Parameters</em> タブの <em>On Lip Sync Update (LipSyncInfo)</em> でイベントの登録を行います。+ ボタンを押下してスロットを追加し、自分自身のゲームをオブジェクトをドラッグドロップ、プルダウンリストから <em>uLipSyncExpressionVRM</em> > <em>OnLipSyncUpdate</em> を選択します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530002414" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530002414.png" width="1200" height="766" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>これで、シーンを再生すると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>が動作します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530002550" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530002550.png" width="1200" height="751" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="マイクを使ったセットアップ">マイクを使ったセットアップ</h3> <p>マイクを使って<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>させたい場合は、<strong><code>uLipSyncMicrophone</code></strong> という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を更にアタッチしてください。こちらは、指定されたマイク入力を <code>AudioSource</code> から再生するものになります。マイクはプルダウンリストから適切なものを選択しておいてください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530003550" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530003550.png" width="1200" height="489" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>また、同時に <code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <em>Profile</em> タブの <em>Profile</em> で、別の <code>Profile</code> を指定します。ここでは <em>uLipSync-Profile-Sample</em> を指定してみましょう。<em>Packages > uLipSync > Assets > Profiles</em> に格納されています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530003911" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530003911.png" width="1200" height="624" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>uLipSync では <code>Profile</code> という予め「あいうえお」といった音素の情報を登録したデータを使い、入力された音とその登録済みの音の情報を比較して、どの音に最も近いかの解析を行っています、そのため、入力される音に応じた <code>Profile</code> の指定が必要です。ここで指定したものはとある男性・女性の音声を登録したデータになっています(ご自身で作成することもできます、こちらは<a href="https://tips.hecomi.com/entry/2022/01/30/152519">&#x904E;&#x53BB;&#x306E;&#x89E3;&#x8AAC;&#x8A18;&#x4E8B;</a>または <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> をご参照ください)。</p> <p>これで実行すればマイク入力に応じた口パクを行ってくれるようになります。</p> <h2 id="タイムラインの利用方法">タイムラインの利用方法</h2> <p>タイムラインを利用する場合は、予め音声を解析した <code>BakedData</code> というデータを作製しておく必要があります。詳しい解説は以下の記事に書いてあります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F01%2F30%2F152519" title="uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/01/30/152519">tips.hecomi.com</a></cite></p> <p>ここでは簡単に手順だけ解説します。</p> <h3 id="BakedData-の作成">BakedData の作成</h3> <p>まず、<code>BakedData</code> を右クリックの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%C6%A5%AD%A5%B9%A5%C8%A5%E1%A5%CB%A5%E5%A1%BC">コンテキストメニュー</a>から生成します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530004452" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530004452.png" width="1195" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><code>Profile</code> と変換したい <code>AudioClip</code> を選択して <em>Bake</em> ボタンを押します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220125232304" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220125/20220125232304.gif" width="569" height="517" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>これで準備完了です。</p> <h3 id="コンポーネントの追加変更"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の追加変更</h3> <p>事前解析済みなので、<code>uLipSync</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>は必要ありません。この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>は削除しておきます。代わりに、<strong><code>uLipSyncTimelineEvent</code></strong> という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>をアタッチします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530005413" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530005413.png" width="1200" height="294" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>ここでは先程同様 <em>On Lip Sync Update (LipSyncInfo)</em> でイベントの登録を行います。</p> <h3 id="TImeline-の設定">TImeline の設定</h3> <p>Timeline では<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>をさせるための専用のトラック(<em>uLipSync.Timeline > uLipSync Track</em>)があります。これを追加します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530005117" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530005117.png" width="1200" height="376" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>追加したトラックにはどのモデルを動かすかの設定を行うため、トラックのスロットに先程の <code>uLipSyncTimelineEvent</code> を追加した GameObject を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E9%A5%C3%A5%B0%26amp%3B%A5%C9%A5%ED%A5%C3%A5%D7">ドラッグ&amp;ドロップ</a>します。そして、トラックには先程の <code>BakedData</code> を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E9%A5%C3%A5%B0%26amp%3B%A5%C9%A5%ED%A5%C3%A5%D7">ドラッグ&amp;ドロップ</a>します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230530005651" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530005651.png" width="1200" height="406" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>これで動作するようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530010032.gif" width="822" height="692" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="アニメーションのベイク">アニメーションのベイク</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 専用のアニメーションのベイクは現在サポートできていません。</p> <p>もし <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> データの口の動きが BlendShape を使ってセットアップされている場合は、<code>uLipSyncExpressionVRM</code> や <code>uLipSyncBlendShapeVRM</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を外し、代わりに <code>uLipSyncBlendShape</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>をセットアップしてください。<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> でも(BlendShape で作られたモデルなら)直接 BlendShape を使って操作が可能で、通常の仕組みを使って<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>させることができます。この上で、以下のページに書かれた手順に従ってデータを作成してください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F02%2F19%2F202558" title="uLipSync にアニメーションベイク機能を追加してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/02/19/202558">tips.hecomi.com</a></cite></p> <p>作成したアニメーションを使えば、<em>Animator</em> を使って動かしたり、通常通り Timeline にアニメーションを指定して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>させることができます。</p> <h2 id="修正についてのメモ">修正についてのメモ</h2> <p>ここからは開発のメモなので読み飛ばしていただいて大丈夫です。</p> <h3 id="VRM-パッケージ非読込み時の対応"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> パッケージ非読込み時の対応</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> パッケージが読み込まれているかどうかは <em>Version Defines</em> で判別するようにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230530/20230530010403.png" width="1160" height="410" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2FScriptCompilationAssemblyDefinitionFiles.html%23define-symbols" title="アセンブリの定義 - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/ScriptCompilationAssemblyDefinitionFiles.html#define-symbols">docs.unity3d.com</a></cite></p> <p>読み込まれていない際は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>は次のようにワーニング表示を行うようにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230527163017" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230527/20230527163017.png" width="952" height="134" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="PathGetRelativePath">Path.GetRelativePath</h3> <p>Unity 2019.X にて、<code>System.Path.GetRelativePath()</code> がないとエラーが出るとの報告がありました。実際は Unity 2021.2 から<a class="keyword" href="https://d.hatena.ne.jp/keyword/%20.NET"> .NET</a> Standard 2.1 が導入されたのですが、それ以前は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%20.NET"> .NET</a> Standard 2.0 で、ここには <code>Path.GetRelativePath</code> がないためエラーとなっていました。</p> <p>そこで、これを行うサブセットを追加して対応しました。以下、参考にした stackoverflow のスレッドです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F275689%2Fhow-to-get-relative-path-from-absolute-path" title="How to get relative path from absolute path" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path">stackoverflow.com</a></cite></p> <h2 id="おわりに">おわりに</h2> <p>長らく着手できていない <a class="keyword" href="https://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 周りのサポートもそろそろ何とかしたいですね。。</p> hecomi uLipSync v3.0.0 をリリースしました hatenablog://entry/4207112889985365125 2023-04-28T23:04:37+09:00 2023-04-28T23:05:18+09:00 はじめに Unity でキャラクタのリップシンクを行うことのできる uLipSync v3.0.0 をリリースしました。 github.com 本エントリではアップデートの内容について解説していきます。 MFCC の計算方法修正 一番大きなアップデートは MFCC の計算方法の改善です。これに関しては前回のブログ記事で解説しました。 tips.hecomi.com 端的に言うと、これまでの MFCC は計算方法が間違っていたため、正しくなるよう世の中一般のライブラリによる値と比較しながら修正を行った、という形です。ただし精度が劇的に上がった、というわけではありませんでした。。以前もそれっぽい特… <h2 id="はじめに">はじめに</h2> <p>Unity でキャ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>を行うことのできる <strong>uLipSync v3.0.0</strong> をリリースしました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Freleases%2Ftag%2Fv3.0.0" title="Release v3.0.0 has been released · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/releases/tag/v3.0.0">github.com</a></cite></p> <p>本エントリではアップデートの内容について解説していきます。</p> <h2 id="MFCC-の計算方法修正">MFCC の計算方法修正</h2> <p>一番大きなアップデートは MFCC の計算方法の改善です。これに関しては前回のブログ記事で解説しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2023%2F03%2F31%2F022324" title="uLipSync のアルゴリズム改善を行ってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2023/03/31/022324">tips.hecomi.com</a></cite></p> <p>端的に言うと、これまでの MFCC は計算方法が間違っていたため、正しくなるよう世の中一般のライブラリによる値と比較しながら修正を行った、という形です。ただし精度が劇的に上がった、というわけではありませんでした。。以前もそれっぽい特徴量を得られており判別は出来ていたようです。</p> <p>また、これまでは 12 個ある MFCC の比較は各係数の差分の絶対値を足した L1 距離(マンハッタン距離)のみでしたが、新たに <code>Profile</code> で指定できるオプションとして L2 距離(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%AF%A5%EA%A5%C3%A5%C9">ユークリッド</a>距離)とコサイン類似度を追加しました。またこれらの比較の際、各特徴量を標準化するオプション(<em>Use <a class="keyword" href="https://d.hatena.ne.jp/keyword/Standardization">Standardization</a></em>)も追加しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230428200447" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230428/20230428200447.png" width="1196" height="794" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>おすすめはどれか、と聞かれるとなかなか難しく...、比較関数もひとまずものすごい大きな差を生むような感じではありませんし、標準化を行うと大きい差分を生む特徴量に引っ張られないようになりますが、一方でノイズを生むような係数が合った際にそれにも引っ張られてしまいます。これ以上精度を上げるにはもっと単純な計算での比較ではなくて何かしらの分類をしないとですね。いずれ勉強のためにチャレンジしてみます。</p> <p>併せてデフォルトのパラメタも調整を行い、付属のサンプルの <code>Profile</code> もそれに合わせて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>しておきました。</p> <h2 id="VRM-10-のサンプル追加"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> 1.0 のサンプル追加</h2> <p>VRoid が <a href="https://vroid.pixiv.help/hc/ja/articles/15836003936665--v1-20-0-VRM1-0%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%97%E3%81%9FVRM%E3%82%92%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%E6%A9%9F%E8%83%BD%E3%82%92%E8%BF%BD%E5%8A%A0-CLCT-for-WONDERLAND%E3%81%AA%E3%81%A9-2023%E5%B9%B43%E6%9C%882%E6%97%A5-">v1.20.0</a> から VRM1.0 のエクスポートに対応したので、サンプルを VRM1.0 にアップグレードしてみました(従来の VRM0.X 向けの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>も従来どおり付属しています)。<a href="https://github.com/mkc1370">mkc1370 &#x3055;&#x3093;</a>に頂いた<a href="https://github.com/hecomi/uLipSync/pull/27">&#x30D7;&#x30EB;&#x30EA;&#x30AF;&#x30A8;&#x30B9;&#x30C8;</a>で VRM1.0 の Expression 対応をしているため、簡単なセットアップで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>させることが出来ます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230428220959" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230428/20230428220959.gif" width="640" height="360" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h2 id="ランタイムの-UI-の追加">ランタイムの UI の追加</h2> <p>ランタイムでプロファイルを調整できる UI の仕組みを整えてサンプルを追加しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230428222030" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230428/20230428222030.gif" width="640" height="360" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>あくまでサンプルなので見た目はアレですが、シンプルな実装にしましたので、サンプルを参考に独自の UI も作成できると思います。もともと <a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a> へのプロファイルのセーブおよびロードは出来ていましたが、このサンプルのように行えば配布ソフトなどでもユーザに適宜プロファイルの調整をしてもらえるのではないかなと思います。</p> <h2 id="エディタのパフォーマンス改善">エディタのパフォーマンス改善</h2> <p>前項のランタイムの UI の追加のために、MFCC のテクスチャ生成機構が必要になりました。以前はエディタ拡張をゴリゴリして色塗り矩形を組み合わせながらエディタ上の UI を組み立てていたのですが、この仕組みを利用してエディタ側の更新も行いました。内部的には、<code>uLipSync.TextureCreator</code> というクラスを作成し、ここに対して <code>CreateMfccTexture(Texture2D tex, MfccData mfcc, float min, float max)</code> という関数を呼ぶとテクスチャが生成されるというものです。中では Burst の効いたテクスチャ生成ジョブが走る形になってますが、<code>Texture2D</code> を即時出力したいのでジョブはスケジュールするのではなく即時実行・回収を行う形になってます(なのでもう少し改善の余地はあります)。これを使うようにした結果、エディタのパフォーマンスがそこそこ向上しました。これまでは Runtime Information という現在の認識結果を表示するタブを開いている場合、かなり <a class="keyword" href="https://d.hatena.ne.jp/keyword/FPS">FPS</a> 低下が見られましたが、現状はそこそこ大丈夫くらいになりました。とはいえ仕組み的に毎フレーム <a class="keyword" href="https://d.hatena.ne.jp/keyword/GUI">GUI</a> の更新要求を出すような仕組みなので、依然として重くはあります。</p> <h2 id="マイクの遅延改善">マイクの遅延改善</h2> <p>ポーズから復帰したときや、なにか重い処理が挟まったタイミングで <code>uLipSyncMicrophone</code> によるマイク音声が遅れてしまう、という現象が起こっていました。これに対処するために以下のプルリクを頂きました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Fpull%2F30" title="Fix microphone desynchronization issues by BX80646G3258 · Pull Request #30 · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/pull/30">github.com</a></cite></p> <p>頂いたものをもとに更に改善を行ったものが現在含まれています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230428225220" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230428/20230428225220.png" width="1200" height="580" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><em>Advanced</em> を展開すると <em>Latency Tolerance</em> と <em>Buffer Time</em> というものがあります。<em>Latency Tolerance</em> はこれ以上マイクが遅れた場合に再同期処理を行うものです。<em>Buffer Time</em> はマイクを出力するまでにバッファリングを行う時間で、小さすぎるとプチプチしてしまうため、ある程度の時間をセットする必要があります(デフォルトは 30 msec)。</p> <h2 id="uLipSyncAnimator">uLipSyncAnimator</h2> <p>以前追加した機能である <code>uLipSyncAnimator</code> に最初のパラメタが動作しない、というバグが有ったため修正しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Fissues%2F37" title="The first parameter set in uLipSyncAnimator does not work. · Issue #37 · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/issues/37">github.com</a></cite></p> <p>確かにリリース時のブログを見てみると最初の要素が反応していませんでした...</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F12%2F28%2F233803" title="uLipSync で Animator を使ったリップシンクができるように更新してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/12/28/233803">tips.hecomi.com</a></cite></p> <h2 id="おわりに">おわりに</h2> <p>今回は計算方法の修正およびそれに伴う <code>Profile</code> の修正という大きな更新をしたため、メジャーバージョンアップを行いました。ただ、まだ良い性能を出すためには道半ばな感じもあるので、今後も地道に改善していきます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B6%A5%D3%A5%EA%A5%C6%A5%A3">ユーザビリティ</a>方面でも要求がありましたら <a href="https://twitter.com/hecomi">Twitter</a> かなにかでご連絡ください。</p> hecomi uLipSync のアルゴリズム改善を行ってみた hatenablog://entry/4207112889976055121 2023-03-31T02:23:24+09:00 2023-03-31T02:23:24+09:00 はじめに ずっとやろうと思っていた Unity でリップシンクを実現するアセットである uLipSync のアルゴリズムの改善を行いました。 github.com uLipSyncでは、MFCC(Mel Frequency Cepstral Coefficients)と呼ばれる特徴量を計算しています。MFCC を利用することで音声信号から音素の違い(母音や子音など)や楽器の音色の違いを区別することができ、音声認識や音楽情報検索など、様々な音声・音楽処理タスクで広く利用されています。Python 環境では Librosa などのライブラリを使って MFCC は簡単に計算することが出来ます。 一方… <h2 id="はじめに">はじめに</h2> <p>ずっとやろうと思っていた Unity で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>を実現するアセットである <strong>uLipSync</strong> の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>の改善を行いました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync" title="GitHub - hecomi/uLipSync: MFCC-based LipSync plug-in for Unity using Job System and Burst Compiler" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync">github.com</a></cite></p> <p>uLipSyncでは、<strong>MFCC</strong>(Mel Frequency Cepstral Coefficients)と呼ばれる特徴量を計算しています。MFCC を利用することで音声信号から音素の違い(母音や子音など)や楽器の音色の違いを区別することができ、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B2%BB%C0%BC%C7%A7%BC%B1">音声認識</a>や音楽情報検索など、様々な音声・音楽処理タスクで広く利用されています。<a class="keyword" href="http://d.hatena.ne.jp/keyword/Python">Python</a> 環境では <a href="https://librosa.org/doc/latest/index.html">Librosa</a> などのライブラリを使って MFCC は簡単に計算することが出来ます。</p> <p>一方 uLipSync では、Unity 内でネイティブ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>などに頼らず JobSystem + Burst <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%E9">コンパイラ</a>の助けを借りて高速に処理したいコンセプトがある関係上、こういったライブラリに頼ることは出来ず、すべての計算をゼロから書かないとなりませんでした。計算した結果をエディタ拡張で図示して何となくそれっぽく値が出ており、かつ MFCC を特徴量として事前に取得しておいた音声の MFCC と比較することで音素判別がそれなりに出来ていたので、書いたコードは合ってるかなと思っていたのですが...、今回改めてこちらを検証しました。</p> <p>記事としては解説記事というか、備忘録になります。なので内容は多くの方には余り意味のある内容ではないかと思います。。現在 v3.0.0 のリリース準備中のため、来月リリースしその記事を書きます。</p> <h2 id="改善後の様子">改善後の様子</h2> <p>「あーいーうーえーおー、あいうえお、こんにちは」と喋っています。前より識別性能が良くなりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230331021309" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230331/20230331021309.gif" width="480" height="320" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h2 id="検証">検証</h2> <p>結論からいうと...、計算が....間違っていました...。最も駄目だったのは、メルフィルタバンクと呼ばれる人間の聴覚特性に基づいた周波数のまとまりを計算するためのところで、結構おバカな計算をしていました。しかしながら間違っていても同じ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>ですべての音素が計算されていた結果、MFCC のような何かが生成されており、結果、何となく音素の識別が出来ていたようです(より詳細には、周波数ビンの重み付けの計算にだけ影響が及ぼされていたようなので、比較的似たような何かではあったのかもしれません...)。パワーの計算なども Librosa のものと併せて、同じ値かどうか比較できるように修正しました。以下に流れをまとめておきます。</p> <h3 id="Jupyter-Notebook--Librosa--Matplotlib-で検証">Jupyter Notebook + Librosa + Matplotlib で検証</h3> <p>Jupyter Notebook を立て、Librosa や関連ライブラリをインストールします。まず <a class="keyword" href="http://d.hatena.ne.jp/keyword/Audacity">Audacity</a> で音声を録音、これを入力として与えてみます。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> librosa <span class="synPreProc">import</span> matplotlib.pyplot <span class="synStatement">as</span> plt <span class="synPreProc">import</span> numpy <span class="synStatement">as</span> np sample_rate = <span class="synConstant">16000</span> n_fft = frame_size n_mels = <span class="synConstant">40</span> n_mfcc = <span class="synConstant">13</span> y, sr = librosa.core.load(<span class="synConstant">'data/o.wav'</span>, sr=sample_rate) mfccs_librosa = librosa.feature.mfcc( y=y, sr=sample_rate, n_mels=n_mels, n_fft=n_fft, hop_length=n_fft, n_mfcc=n_mfcc, htk=<span class="synIdentifier">True</span>) mfccs_librosa = mfccs_librosa.T mfccs_librosa = mfccs_librosa[:,<span class="synConstant">1</span>:] cmin = -<span class="synConstant">40</span> cmax = <span class="synConstant">40</span> plt.clf() im = plt.imshow( mfccs_librosa, cmap=<span class="synConstant">'gnuplot'</span>, vmin=cmin, vmax=cmax, interpolation=<span class="synConstant">'bicubic'</span>, aspect=<span class="synConstant">0.2</span>) plt.yticks([]) plt.show() </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230330020040" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230330/20230330020040.png" width="1028" height="430" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>とても簡単に MFCC が得られますね。ではこれを分解して幾つかの部分を自前でコードを書いてみます。</p> <h3 id="MFCC-を-Librosa-を使わずに計算">MFCC を Librosa を使わずに計算</h3> <pre class="code lang-python" data-lang="python" data-unlink>base_freq = <span class="synConstant">1127.010480</span> <span class="synStatement">def</span> <span class="synIdentifier">hz_to_mel</span>(freq): <span class="synStatement">return</span> base_freq * np.log10(<span class="synConstant">1.0</span> + freq / <span class="synConstant">700.0</span>) <span class="synStatement">def</span> <span class="synIdentifier">mel_to_hz</span>(mel): <span class="synStatement">return</span> <span class="synConstant">700.0</span> * (<span class="synConstant">10.0</span>**(mel / base_freq) - <span class="synConstant">1.0</span>) <span class="synComment"># メルフィルタバンク</span> <span class="synComment"># 後で Burst 向け C# に変換しやすいよう numpy を使わず書いておく</span> <span class="synStatement">def</span> <span class="synIdentifier">create_mel_filter_bank</span>(num_filters, n_fft, sr): f_max = sr / <span class="synConstant">2</span> mel_max = hz_to_mel(f_max) n_max = n_fft // <span class="synConstant">2</span> df = <span class="synIdentifier">float</span>(sr) / n_fft d_mel = mel_max / (num_filters + <span class="synConstant">1</span>) filters = np.zeros((num_filters, n_max + <span class="synConstant">1</span>)) <span class="synStatement">for</span> n <span class="synStatement">in</span> <span class="synIdentifier">range</span>(num_filters): mel_begin = d_mel * n mel_center = d_mel * (n + <span class="synConstant">1</span>) mel_end = d_mel * (n + <span class="synConstant">2</span>) f_begin = mel_to_hz(mel_begin) f_center = mel_to_hz(mel_center) f_end = mel_to_hz(mel_end) i_begin = <span class="synIdentifier">int</span>(math.ceil(f_begin / df)) i_center = <span class="synIdentifier">int</span>(<span class="synIdentifier">round</span>(f_center / df)) i_end = <span class="synIdentifier">int</span>(math.floor(f_end / df)) <span class="synStatement">for</span> i <span class="synStatement">in</span> <span class="synIdentifier">range</span>(i_begin, i_end): a = <span class="synConstant">0.0</span> f = df * i <span class="synStatement">if</span> (i &lt; i_center): a = (f - f_begin) / (f_center - f_begin) <span class="synStatement">else</span>: a = (f_end - f) / (f_end - f_center) a /= (f_end - f_begin) * <span class="synConstant">0.5</span> filters[n, i] = a <span class="synStatement">return</span> filters <span class="synStatement">for</span> i <span class="synStatement">in</span> <span class="synIdentifier">range</span>(num_frames): <span class="synComment"># 1024 サンプルを取り出す</span> y_frame = y[i * frame_size : (i + <span class="synConstant">1</span>) * frame_size] <span class="synComment"># プリエンファシスフィルタ適用</span> y_preemphasized = np.append(y_frame[<span class="synConstant">0</span>], y_frame[<span class="synConstant">1</span>:] - pre_emphasis * y_frame[:-<span class="synConstant">1</span>]) <span class="synComment"># 正規化</span> max_abs_value = np.abs(y_preemphasized).max() y_normalized = y_preemphasized / max_abs_value <span class="synComment"># ハミング窓適用</span> window = np.hamming(frame_size) y_windowed = y_normalized * window <span class="synComment"># FFT</span> y_fft = scipy.fftpack.fft(y_windowed, n_fft) y_fft = np.abs(y_fft) <span class="synComment"># メルフィルタバンク適用</span> mel_filter_bank = create_mel_filter_bank(num_mel_filters, n_fft, new_sr) mel_spectrum = np.dot(mel_filter_bank, y_fft[: n_fft // <span class="synConstant">2</span> + <span class="synConstant">1</span>])**<span class="synConstant">2</span> <span class="synComment"># log10してパワーを計算してメルスペクトラムを得る</span> log_mel_spectrum = <span class="synConstant">10</span> * np.log10(np.abs(mel_spectrum)) <span class="synComment"># 離散コサイン変換をしてメルケプストラムを得る</span> mfcc = scipy.fftpack.dct(log_mel_spectrum, <span class="synIdentifier">type</span>=<span class="synConstant">2</span>, norm=<span class="synConstant">'ortho'</span>) mfcc = mfcc[<span class="synConstant">1</span>:<span class="synConstant">13</span>] mfcc_frames.append(mfcc) plt.clf() im = plt.imshow( mfcc_frames, cmap=<span class="synConstant">'gnuplot'</span>, vmin=cmin, vmax=cmax, interpolation=<span class="synConstant">'bicubic'</span>, aspect=<span class="synConstant">0.2</span>) plt.yticks([]) plt.show() </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230330015828" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230330/20230330015828.png" width="1058" height="424" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>詳細は違いますが概ねスケール感含め同じような MFCC が取得できました。サラッと書いてしまいましたが、この段階でもとの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>が間違っていることに気づき、ウンウン唸りながら同じような結果になるように色々と修正をしていました。そしてメルフィルタバンクのミスに気づいた、という形です。</p> <p>メルフィルタバンクも一応確認してみます。離散的なので細かくは違いますが概ね同じかと...</p> <pre class="code lang-python" data-lang="python" data-unlink>mel_filter_bank1 = librosa.filters.mel( sr=sample_rate, n_fft=n_fft, n_mels=n_mels, htk=<span class="synIdentifier">True</span>) mel_filter_bank2 = create_mel_filter_bank( num_mel_filters, n_fft, new_sr) n = <span class="synConstant">100</span> plt.clf() plt.plot(mel_filter_bank1.T[<span class="synConstant">0</span>:n]) plt.show() plt.clf() plt.plot(mel_filter_bank2.T[<span class="synConstant">0</span>:n]) plt.show() </pre> <h5 id="Librosa-のメルフィルタバンク">Librosa のメルフィルタバンク</h5> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230330020756" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230330/20230330020756.png" width="1126" height="826" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h5 id="自前のメルフィルタバンク">自前のメルフィルタバンク</h5> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230330020847" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230330/20230330020847.png" width="1130" height="830" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="Unity-C-側への移植">Unity <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a> 側への移植</h3> <p>これを <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a> に移植してみました。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> MelFilterBank( <span class="synStatement">in</span> NativeArray&lt;<span class="synType">float</span>&gt; spectrum, <span class="synStatement">out</span> NativeArray&lt;<span class="synType">float</span>&gt; melSpectrum, <span class="synType">float</span> sampleRate, <span class="synType">int</span> melDiv) { melSpectrum <span class="synStatement">=</span> <span class="synStatement">new</span> NativeArray&lt;<span class="synType">float</span>&gt;(melDiv, Allocator.Temp); MelFilterBank( (<span class="synType">float</span><span class="synStatement">*</span>)spectrum.GetUnsafeReadOnlyPtr(), (<span class="synType">float</span><span class="synStatement">*</span>)melSpectrum.GetUnsafePtr(), spectrum.Length, sampleRate, melDiv); } [BurstCompile] <span class="synType">static</span> <span class="synType">void</span> MelFilterBank( <span class="synType">float</span><span class="synStatement">*</span> spectrum, <span class="synType">float</span><span class="synStatement">*</span> melSpectrum, <span class="synType">int</span> len, <span class="synType">float</span> sampleRate, <span class="synType">int</span> melDiv) { <span class="synType">float</span> fMax <span class="synStatement">=</span> sampleRate <span class="synStatement">/</span> <span class="synConstant">2</span>; <span class="synType">float</span> melMax <span class="synStatement">=</span> ToMel(fMax); <span class="synType">int</span> nMax <span class="synStatement">=</span> len <span class="synStatement">/</span> <span class="synConstant">2</span>; <span class="synType">float</span> df <span class="synStatement">=</span> fMax <span class="synStatement">/</span> nMax; <span class="synType">float</span> dMel <span class="synStatement">=</span> melMax <span class="synStatement">/</span> (melDiv <span class="synStatement">+</span> <span class="synConstant">1</span>); <span class="synStatement">for</span> (<span class="synType">int</span> n <span class="synStatement">=</span> <span class="synConstant">0</span>; n <span class="synStatement">&lt;</span> melDiv; <span class="synStatement">++</span>n) { <span class="synType">float</span> melBegin <span class="synStatement">=</span> dMel <span class="synStatement">*</span> n; <span class="synType">float</span> melCenter <span class="synStatement">=</span> dMel <span class="synStatement">*</span> (n <span class="synStatement">+</span> <span class="synConstant">1</span>); <span class="synType">float</span> melEnd <span class="synStatement">=</span> dMel <span class="synStatement">*</span> (n <span class="synStatement">+</span> <span class="synConstant">2</span>); <span class="synType">float</span> fBegin <span class="synStatement">=</span> ToHz(melBegin); <span class="synType">float</span> fCenter <span class="synStatement">=</span> ToHz(melCenter); <span class="synType">float</span> fEnd <span class="synStatement">=</span> ToHz(melEnd); <span class="synType">int</span> iBegin <span class="synStatement">=</span> (<span class="synType">int</span>)math.ceil(fBegin <span class="synStatement">/</span> df); <span class="synType">int</span> iCenter <span class="synStatement">=</span> (<span class="synType">int</span>)math.round(fCenter <span class="synStatement">/</span> df); <span class="synType">int</span> iEnd <span class="synStatement">=</span> (<span class="synType">int</span>)math.floor(fEnd <span class="synStatement">/</span> df); <span class="synType">float</span> sum <span class="synStatement">=</span> <span class="synConstant">0f</span>; <span class="synStatement">for</span> (<span class="synType">int</span> i <span class="synStatement">=</span> iBegin <span class="synStatement">+</span> <span class="synConstant">1</span>; i <span class="synStatement">&lt;=</span> iEnd; <span class="synStatement">++</span>i) { <span class="synType">float</span> f <span class="synStatement">=</span> df <span class="synStatement">*</span> i; <span class="synType">float</span> a <span class="synStatement">=</span> (i <span class="synStatement">&lt;</span> iCenter) <span class="synStatement">?</span> (f <span class="synStatement">-</span> fBegin) <span class="synStatement">/</span> (fCenter <span class="synStatement">-</span> fBegin) <span class="synStatement">:</span> (fEnd <span class="synStatement">-</span> f) <span class="synStatement">/</span> (fEnd <span class="synStatement">-</span> fCenter); a <span class="synStatement">/=</span> (fEnd <span class="synStatement">-</span> fBegin) <span class="synStatement">*</span> <span class="synConstant">0.5f</span>; sum <span class="synStatement">+=</span> a <span class="synStatement">*</span> spectrum[i]; } melSpectrum[n] <span class="synStatement">=</span> sum; } } </pre> <p>これで正しい MFCC が得られているはずです。</p> <h3 id="Unity-での認識結果の出力">Unity での認識結果の出力</h3> <p>さて<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>は併せましたが本当に計算結果が一致しているかは気になるところです。そこで、Unity での計算結果を <a class="keyword" href="http://d.hatena.ne.jp/keyword/CSV">CSV</a> に書き出して Jupyter Notebook で描画してみて、Librosa の出力と一致しているかを見てみることにします。ただ <a class="keyword" href="http://d.hatena.ne.jp/keyword/CSV">CSV</a> の出力はユーザーは必要ないので、開発時のみこの機能が ON になるように、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>フラグを ON にしたときのみ幾つかのコードが追加され、少しのオーバーヘッドを犠牲に途中の計算結果などを出力できるようにしてみました。</p> <p>まず、Project Settings > Player から <strong>Scripting Define Symbols</strong> に <code>ULIPSYNC_DEBUG</code> というフラグを追加します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230330234824" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230330/20230330234824.png" width="1200" height="567" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>ちょっと面倒ですが、このフラグが立っているときにバッファを追加し、ジョブ側で計算の途中結果をコピーするようにしました。以下に関連している付近のコードのみ抜粋します。</p> <h5 id="uLipSynccs">uLipSync.cs</h5> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">class</span> <span class="synType">uLipSync </span><span class="synStatement">:</span> MonoBehaviour { ... <span class="synPreProc">#if ULIPSYNC_DEBUG</span> NativeArray&lt;<span class="synType">float</span>&gt; _debugData; NativeArray&lt;<span class="synType">float</span>&gt; _debugSpectrum; NativeArray&lt;<span class="synType">float</span>&gt; _debugMelSpectrum; NativeArray&lt;<span class="synType">float</span>&gt; _debugMelCepstrum; NativeArray&lt;<span class="synType">float</span>&gt; _debugDataForOther; NativeArray&lt;<span class="synType">float</span>&gt; _debugSpectrumForOther; NativeArray&lt;<span class="synType">float</span>&gt; _debugMelSpectrumForOther; NativeArray&lt;<span class="synType">float</span>&gt; _debugMelCepstrumForOther; <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; data <span class="synStatement">=&gt;</span> _debugDataForOther; <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; spectrum <span class="synStatement">=&gt;</span> _debugSpectrumForOther; <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; melSpectrum <span class="synStatement">=&gt;</span> _debugMelSpectrumForOther; <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; melCepstrum <span class="synStatement">=&gt;</span> _debugMelCepstrumForOther; <span class="synPreProc">#endif</span> ... ... <span class="synType">void</span> UpdateResult() { _jobHandle.Complete(); _mfccForOther.CopyFrom(_mfcc); <span class="synPreProc">#if ULIPSYNC_DEBUG</span> _debugDataForOther.CopyFrom(_debugData); _debugSpectrumForOther.CopyFrom(_debugSpectrum); _debugMelSpectrumForOther.CopyFrom(_debugMelSpectrum); _debugMelCepstrumForOther.CopyFrom(_debugMelCepstrum); <span class="synPreProc">#endif</span> ... } ... <span class="synType">void</span> ScheduleJob() { ... <span class="synType">var</span> lipSyncJob <span class="synStatement">=</span> <span class="synStatement">new</span> LipSyncJob() { ... <span class="synPreProc">#if ULIPSYNC_DEBUG</span> debugData <span class="synStatement">=</span> _debugData, debugSpectrum <span class="synStatement">=</span> _debugSpectrum, debugMelSpectrum <span class="synStatement">=</span> _debugMelSpectrum, debugMelCepstrum <span class="synStatement">=</span> _debugMelCepstrum, <span class="synPreProc">#endif</span> }; _jobHandle <span class="synStatement">=</span> lipSyncJob.Schedule(); } } </pre> <h5 id="LipSyncJobcs">LipSyncJob.cs</h5> <pre class="code lang-cs" data-lang="cs" data-unlink>[BurstCompile] <span class="synType">public</span> <span class="synType">struct</span> LipSyncJob <span class="synStatement">:</span> IJob { ... <span class="synPreProc">#if ULIPSYNC_DEBUG</span> <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; debugData; <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; debugSpectrum; <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; debugMelSpectrum; <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; debugMelCepstrum; <span class="synPreProc">#endif</span> <span class="synType">public</span> <span class="synType">void</span> Execute() { Algorithm.CopyRingBuffer(input, <span class="synStatement">out</span> <span class="synType">var</span> buffer, startIndex); Algorithm.LowPassFilter(<span class="synStatement">ref</span> buffer, outputSampleRate, cutoff, range); Algorithm.DownSample(buffer, <span class="synStatement">out</span> <span class="synType">var</span> data, outputSampleRate, targetSampleRate); Algorithm.PreEmphasis(<span class="synStatement">ref</span> data, <span class="synConstant">0.97f</span>); Algorithm.HammingWindow(<span class="synStatement">ref</span> data); Algorithm.Normalize(<span class="synStatement">ref</span> data, <span class="synConstant">1f</span>); Algorithm.ZeroPadding(<span class="synStatement">ref</span> data, <span class="synStatement">out</span> dataWithZeroPadding); Algorithm.FFT(dataWithZeroPadding, <span class="synStatement">out</span> <span class="synType">var</span> spectrum); Algorithm.MelFilterBank(spectrum, <span class="synStatement">out</span> <span class="synType">var</span> melSpectrum, targetSampleRate, melFilterBankChannels); <span class="synStatement">for</span> (<span class="synType">int</span> i <span class="synStatement">=</span> <span class="synConstant">0</span>; i <span class="synStatement">&lt;</span> melSpectrum.Length; <span class="synStatement">++</span>i) { melSpectrum[i] <span class="synStatement">=</span> <span class="synConstant">10f</span> <span class="synStatement">*</span> math.log10(melSpectrum[i]); } Algorithm.DCT(melSpectrum, <span class="synStatement">out</span> <span class="synType">var</span> melCepstrum); ... <span class="synPreProc">#if ULIPSYNC_DEBUG</span> dataWithZeroPadding.CopyTo(debugData); spectrum.CopyTo(debugSpectrum); melSpectrum.CopyTo(debugMelSpectrum); melCepstrum.CopyTo(debugMelCepstrum); <span class="synPreProc">#endif</span> ... } } </pre> <p>これで <code>NativeArray&lt;float&gt;</code> で色々な計算結果を外部から取得できるようになりました。そしてこれをファイルに書き出す<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>用の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を追加します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>[RequireComponent(<span class="synStatement">typeof</span><span class="synType">(uLipSync)</span>)] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">DebugDump </span><span class="synStatement">:</span> MonoBehaviour { <span class="synType">public</span> uLipSync lipsync { <span class="synStatement">get</span>; <span class="synType">private</span> <span class="synStatement">set</span>; } <span class="synType">public</span> <span class="synType">string</span> prefix <span class="synStatement">=</span> <span class="synConstant">&quot;&quot;</span>; <span class="synType">public</span> <span class="synType">string</span> dataFile <span class="synStatement">=</span> <span class="synConstant">&quot;data.csv&quot;</span>; <span class="synType">public</span> <span class="synType">string</span> spectrumFile <span class="synStatement">=</span> <span class="synConstant">&quot;spectrum.csv&quot;</span>; <span class="synType">public</span> <span class="synType">string</span> melSpectrumFile <span class="synStatement">=</span> <span class="synConstant">&quot;mel-spectrum.csv&quot;</span>; <span class="synType">public</span> <span class="synType">string</span> melCepstrumFile <span class="synStatement">=</span> <span class="synConstant">&quot;mel-cepstrum.csv&quot;</span>; <span class="synType">public</span> <span class="synType">string</span> mfccFile <span class="synStatement">=</span> <span class="synConstant">&quot;mfcc.csv&quot;</span>; <span class="synType">void</span> Start() { lipsync <span class="synStatement">=</span> GetComponent&lt;uLipSync&gt;(); } <span class="synType">public</span> <span class="synType">void</span> DumpAll() { DumpData(); DumpSpectrum(); DumpMelSpectrum(); DumpMelCepstrum(); DumpMfcc(); } <span class="synType">public</span> <span class="synType">void</span> DumpData() { <span class="synPreProc">#if ULIPSYNC_DEBUG</span> <span class="synStatement">if</span> (<span class="synType">string</span>.IsNullOrEmpty(dataFile)) <span class="synStatement">return</span>; <span class="synType">var</span> fileName <span class="synStatement">=</span> <span class="synConstant">$&quot;</span><span class="synSpecial">{</span>prefix<span class="synSpecial">}{</span>dataFile<span class="synSpecial">}</span><span class="synConstant">&quot;</span>; <span class="synType">var</span> sw <span class="synStatement">=</span> <span class="synStatement">new</span> StreamWriter(fileName); <span class="synType">var</span> data <span class="synStatement">=</span> lipsync.data; <span class="synType">var</span> dt <span class="synStatement">=</span> <span class="synConstant">1f</span> <span class="synStatement">/</span> (lipsync.profile.targetSampleRate <span class="synStatement">*</span> <span class="synConstant">2</span>); <span class="synStatement">for</span> (<span class="synType">int</span> i <span class="synStatement">=</span> <span class="synConstant">0</span>; i <span class="synStatement">&lt;</span> data.Length; <span class="synStatement">++</span>i) { <span class="synType">var</span> t <span class="synStatement">=</span> dt <span class="synStatement">*</span> i; <span class="synType">var</span> val <span class="synStatement">=</span> data[i]; sw.WriteLine(<span class="synConstant">$&quot;</span><span class="synSpecial">{</span>t<span class="synSpecial">}</span><span class="synConstant">,</span><span class="synSpecial">{</span>val<span class="synSpecial">}</span><span class="synConstant">&quot;</span>); } sw.Close(); Debug.Log(<span class="synConstant">$&quot;</span><span class="synSpecial">{</span>fileName<span class="synSpecial">}</span><span class="synConstant"> was created.&quot;</span>); <span class="synPreProc">#endif</span> } <span class="synType">public</span> <span class="synType">void</span> DumpSpectrum() { <span class="synPreProc">#if ULIPSYNC_DEBUG</span> <span class="synStatement">if</span> (<span class="synType">string</span>.IsNullOrEmpty(spectrumFile)) <span class="synStatement">return</span>; <span class="synType">var</span> fileName <span class="synStatement">=</span> <span class="synConstant">$&quot;</span><span class="synSpecial">{</span>prefix<span class="synSpecial">}{</span>spectrumFile<span class="synSpecial">}</span><span class="synConstant">&quot;</span>; <span class="synType">var</span> sw <span class="synStatement">=</span> <span class="synStatement">new</span> StreamWriter(fileName); <span class="synType">var</span> spectrum <span class="synStatement">=</span> lipsync.spectrum; <span class="synType">var</span> df <span class="synStatement">=</span> (<span class="synType">float</span>)lipsync.profile.targetSampleRate <span class="synStatement">/</span> spectrum.Length; <span class="synStatement">for</span> (<span class="synType">int</span> i <span class="synStatement">=</span> <span class="synConstant">0</span>; i <span class="synStatement">&lt;</span> spectrum.Length; <span class="synStatement">++</span>i) { <span class="synType">var</span> f <span class="synStatement">=</span> df <span class="synStatement">*</span> i; <span class="synType">var</span> val <span class="synStatement">=</span> math.log(spectrum[i]); sw.WriteLine(<span class="synConstant">$&quot;</span><span class="synSpecial">{</span>f<span class="synSpecial">}</span><span class="synConstant">,</span><span class="synSpecial">{</span>val<span class="synSpecial">}</span><span class="synConstant">&quot;</span>); } sw.Close(); Debug.Log(<span class="synConstant">$&quot;</span><span class="synSpecial">{</span>fileName<span class="synSpecial">}</span><span class="synConstant"> was created.&quot;</span>); <span class="synPreProc">#endif</span> } <span class="synType">public</span> <span class="synType">void</span> DumpMelSpectrum() { <span class="synPreProc">#if ULIPSYNC_DEBUG</span> ... <span class="synPreProc">#endif</span> } <span class="synType">public</span> <span class="synType">void</span> DumpMelCepstrum() { <span class="synPreProc">#if ULIPSYNC_DEBUG</span> ... <span class="synPreProc">#endif</span> } <span class="synType">public</span> <span class="synType">void</span> DumpMfcc() { <span class="synPreProc">#if ULIPSYNC_DEBUG</span> ... <span class="synPreProc">#endif</span> } } </pre> <p>数値を <a class="keyword" href="http://d.hatena.ne.jp/keyword/CSV">CSV</a> に書き出すだけのシンプルな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>です。あとはエディタ拡張も書いてこれらを一括 / 個別で簡単にファイルに出力できるようにしておきます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230331012205" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230331/20230331012205.png" width="1018" height="368" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>また、同様に <code>Profile</code> や <code>uLipSync</code> のエディタ拡張にも <code>ULIPSYNC_DEBUG</code> が立っている時だけ追加される MFCC 出力の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>を追加しておきます。これによって、プロファイルに登録されている MFCC や現在の数フレームの間に認識された MFCC をダンプすることが出来るようになります。たとえば <code>uLipSync</code> のエディタ拡張はこんな感じです。</p> <h5 id="uLipSyncEditorcs">uLipSyncEditor.cs</h5> <pre class="code lang-cs" data-lang="cs" data-unlink>[CustomEditor(<span class="synStatement">typeof</span><span class="synType">(uLipSync)</span>)] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">uLipSyncEditor </span><span class="synStatement">:</span> Editor { ... <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnInspectorGUI() { ... <span class="synStatement">if</span> (EditorUtil.Foldout(<span class="synConstant">&quot;Runtime Information&quot;</span>, <span class="synConstant">false</span>)) { ... <span class="synStatement">if</span> (Application.isPlaying) { DrawCurrentMfcc(); } ... } ... } <span class="synType">void</span> DrawCurrentMfcc() { ... <span class="synPreProc">#if ULIPSYNC_DEBUG</span> EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); <span class="synStatement">if</span> (GUILayout.Button(<span class="synConstant">&quot;Dump&quot;</span>)) { <span class="synType">var</span> date <span class="synStatement">=</span> System.DateTime.Now; <span class="synType">var</span> filename <span class="synStatement">=</span> <span class="synConstant">$&quot;</span><span class="synSpecial">{</span>date<span class="synSpecial">:yyyyMMddHHmmss}</span><span class="synConstant">.csv&quot;</span>; <span class="synType">var</span> sw <span class="synStatement">=</span> <span class="synStatement">new</span> StreamWriter(filename); <span class="synType">var</span> sb <span class="synStatement">=</span> <span class="synStatement">new</span> StringBuilder(); Debugging.DebugUtil.DumpMfccData(sb, _mfccData); sw.Write(sb); sw.Close(); Debug.Log(<span class="synConstant">$&quot;</span><span class="synSpecial">{</span>filename<span class="synSpecial">}</span><span class="synConstant"> was created.&quot;</span>); } EditorGUILayout.EndHorizontal(); <span class="synPreProc">#endif</span> } ... } </pre> <p>これで次のようにエディタ拡張にボタンが追加されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230331012416" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230331/20230331012416.png" width="1200" height="446" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230331013241" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230331/20230331013241.png" width="1200" height="390" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="再度-Jupyter-Notebook-に戻って検証">再度 Jupyter Notebook に戻って検証</h3> <p>先ほど追加した仕組みを使って Profile を <a class="keyword" href="http://d.hatena.ne.jp/keyword/CSV">CSV</a> を出力し、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Python">Python</a> 側で読み取って見てみます。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> matplotlib.pyplot <span class="synStatement">as</span> plt <span class="synPreProc">import</span> pandas <span class="synStatement">as</span> pd <span class="synStatement">def</span> <span class="synIdentifier">draw</span>(df, aspect, cmin=cmin, cmax=cmax): plt.clf() im = plt.imshow( df, cmap=<span class="synConstant">'gnuplot'</span>, vmin=cmin, vmax=cmax, interpolation=<span class="synConstant">'bicubic'</span>, aspect=aspect) plt.yticks([]) plt.xticks([]) plt.tight_layout(pad=<span class="synConstant">0</span>, h_pad=<span class="synConstant">0.01</span>, w_pad=<span class="synConstant">0</span>) plt.show() <span class="synStatement">for</span> phoneme <span class="synStatement">in</span> [<span class="synConstant">'A'</span>, <span class="synConstant">'I'</span>, <span class="synConstant">'U'</span>, <span class="synConstant">'E'</span>, <span class="synConstant">'O'</span>]: path = f<span class="synConstant">'data/DebugProfile-{phoneme}.csv'</span> df = pd.read_csv(path, header=<span class="synIdentifier">None</span>) draw(df, <span class="synConstant">0.05</span>, -<span class="synConstant">40</span>, <span class="synConstant">40</span>) </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230331014814" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230331/20230331014814.png" width="1200" height="651" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>最初に Librosa で見たものと似たような値が得られていることがわかります!<code>uLipSync</code> の Dump 機能を使って、もう少し長いデータで同じ「O」の音素を見てみます。</p> <pre class="code lang-python" data-lang="python" data-unlink>df = pd.read_csv(<span class="synConstant">'data/20230331012825.csv'</span>, header=<span class="synIdentifier">None</span>) draw(df, <span class="synConstant">0.05</span>) </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230331015043" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230331/20230331015043.png" width="1200" height="343" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>概ね良さそうな感じがします。ぴったりと Librosa に併せることが目的ではなく、あくまで MFCC を得ることで音素比較が出来るようにすることが目的なので、これで要件は満たせそうです。</p> <p>検証の過程では、こうして得られた MFCC 比較を行う上で、どういった比較関数が良さそうかの検証も行ってみました(L1ノルム、L2ノルム、コサイン類似度、および MFCC の各係数を標準化した場合 / しない場合など)。検証した感じだと、コサイン類似度(標準化なし)が最終的に得られるスコアも 0 ~ 1 であり良さそうな感じでした。下記は、事前にはっきり発音して取得しておいた「あいうえお」の MFCC と、別の感じで発音した「あいうえお」のデータを比較したときのスコア(コサイン類似度 = <a class="keyword" href="http://d.hatena.ne.jp/keyword/%C6%E2%C0%D1">内積</a>)です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230331/20230331015923.png" width="1200" height="621" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>にもよるかもしれないので、ひとまずこれらは <code>Profile</code> で選択できるオプションとした上でユーザが選択できる形にしておこうかなと思います。</p> <h2 id="おわりに">おわりに</h2> <p>今月末リリースを目標にしていたのですが、思っていたよりも時間がかかってしまったので、来月リリースします。今回は触れませんでしたが、他にも色々とアップデートがありますので、併せて色々紹介できればと思います。</p> <p>また、今回検証するに当たり、ChatGPT Model:GPT-4 のお世話になりました。特に <a class="keyword" href="http://d.hatena.ne.jp/keyword/Python">Python</a> 周りと音声信号処理周りの知識の学習についはこれ無しでは 10 倍くらい進みが違っただろうな、と思うくらい新しい学習及びコーディングの体験でした。今後もライセンス周りなどは気にしつつ、うまく活用していきたいです。</p> hecomi HLSLToolsForVisualStudioConfigGenerator v1.1.0 をリリースしました hatenablog://entry/4207112889966357415 2023-02-25T16:36:37+09:00 2023-02-25T16:36:37+09:00 はじめに HLSL Tools / HLSL Tools for Visual Studio を使うと、VS Code または Visual Studio でシェーダを書く際に補完が効くようになります。 marketplace.visualstudio.com marketplace.visualstudio.com これらを Unity のプロジェクトで使用する際は lshadertoolsconfig.json という設定ファイルを作成し、Unity がシェーダ内で暗黙的にパス解決してくれている部分を補助してあげる記述をする必要があります(どのディレクトリを Include するか記述する… <h2 id="はじめに">はじめに</h2> <p><strong>HLSL Tools / HLSL Tools for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Visual%20Studio">Visual Studio</a></strong> を使うと、<strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/VS%20Code">VS Code</a></strong> または <strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/Visual%20Studio">Visual Studio</a></strong> でシェーダを書く際に補完が効くようになります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3DTimGJones.hlsltools" title="HLSL Tools - Visual Studio Marketplace" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://marketplace.visualstudio.com/items?itemName=TimGJones.hlsltools">marketplace.visualstudio.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmarketplace.visualstudio.com%2Fitems%3FitemName%3DTimGJones.HLSLToolsforVisualStudio" title="HLSL Tools for Visual Studio - Visual Studio Marketplace" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://marketplace.visualstudio.com/items?itemName=TimGJones.HLSLToolsforVisualStudio">marketplace.visualstudio.com</a></cite></p> <p>これらを Unity のプロジェクトで使用する際は <code>lshadertoolsconfig.json</code> という設定ファイルを作成し、Unity がシェーダ内で暗黙的にパス解決してくれている部分を補助してあげる記述をする必要があります(どの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを Include するか記述するなど)。この設定ファイルの作成及びパス解決の補助を簡単にするためのエディタ拡張として、以前 <strong>HLSLToolsForVisualStudioConfigGenerator</strong> というものを作成しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2020%2F12%2F20%2F000908" title="Unity 向けに HLSL Tools for Visual Studio を便利にするアセットを作ってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2020/12/20/000908">tips.hecomi.com</a></cite></p> <p>今回は、本エディタ拡張の更新内容の紹介を簡単にしたいと思います。</p> <h2 id="ダウンロード">ダウンロード</h2> <p>少しバグが有ったので執筆時点では v.1.1.1 が最新となります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FHLSLToolsForVisualStudioConfigGenerator%2Freleases" title="Releases · hecomi/HLSLToolsForVisualStudioConfigGenerator" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/HLSLToolsForVisualStudioConfigGenerator/releases">github.com</a></cite></p> <h2 id="更新">更新</h2> <h3 id="git-URL-で追加したソースも追加するよう修正">git URL で追加したソースも追加するよう修正</h3> <p><a href="https://twitter.com/sp4ghet">@sp4ghet</a> さんに PR を頂きました。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>経由で追加されたパッケージは <code>Library/PackageCache</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リに <code>com.unity.burst@1.8.2</code> といったバージョン名が付与された名前で保存されています。一方、git URL から追加されたパッケージは、<code>com.hecomi.uraymarching@41762cdc4a</code> のように、10 文字のハッシュが付与されています。この前者のパターンしかサポートしておらず、git URL 経由のパッケージの include 解決ができていなかった問題が解決されました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FHLSLToolsForVisualStudioConfigGenerator%2Fpull%2F2" title="Use 10 character short hash for version string of git packages by sp4ghet · Pull Request #2 · hecomi/HLSLToolsForVisualStudioConfigGenerator" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/HLSLToolsForVisualStudioConfigGenerator/pull/2">github.com</a></cite></p> <h3 id="Core-RP--URP--HDRP--ShaderGraph-が含まれるよう修正">Core RP / URP / HDRP / ShaderGraph が含まれるよう修正</h3> <p>Core RP / URP / HDRP は builtin パッケージとなったにも関わらず、builtin パッケージは除外するようになってしまっていました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FHLSLToolsForVisualStudioConfigGenerator%2Fissues%2F1" title="Tool doesn&#39;t generate symlinks for com.unity.rendering-pipelines.* · Issue #1 · hecomi/HLSLToolsForVisualStudioConfigGenerator" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/HLSLToolsForVisualStudioConfigGenerator/issues/1">github.com</a></cite></p> <p>このため builtin パッケージもリストに含めるようにしました。ただシェーダに関係のないパッケージが多く含まれてしまうため、チェックリストで選べるような形にしました。また、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DB%A5%EF%A5%A4%A5%C8%A5%EA%A5%B9%A5%C8">ホワイトリスト</a>形式で必要なパッケージだけが最初から選択される方式にしています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230225141403" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230225/20230225141403.png" width="1200" height="1003" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="MacVSCodeでも動作するように修正"><a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a>(<a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a>)でも動作するように修正</h3> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a> でのパスの取り扱いをサポートしていなかったので修正しました。これで <a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a> 上の <a class="keyword" href="http://d.hatena.ne.jp/keyword/VS%20Code">VS Code</a> でも動作するようになりました。</p> <h2 id="おわりに">おわりに</h2> <p>バグ報告や使い勝手の要望などありましたら対応しますので、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a> や <a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> 上の issue などでご連絡ください。</p> hecomi uLipSync の新機能解説(実行時のセットアップ / タイムライン上の自動クリップ追加機能) hatenablog://entry/4207112889950760658 2023-01-06T01:31:15+09:00 2023-01-06T02:06:16+09:00 はじめに リリース 実行時のセットアップ SkinnedMeshRenderer 経由で指定 VRM の場合 所感 タイムライン上への自動クリップ追加 uLipSync おわりに はじめに uLipSync は Unity 上で音声データ / 音声入力をもとにリップシンクを実現するアセットです。 tips.hecomi.com 今回は要望を頂いていた以下の 2 つの機能追加・サンプル更新をしましたので解説します。 実行時にセットアップしたい Timeline でのリップシンククリップ配置を簡単にしたい リリース v2.6.0 をリリースしました。 github.com 実行時のセットアップ 例… <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#リリース">リリース</a></li> <li><a href="#実行時のセットアップ">実行時のセットアップ</a><ul> <li><a href="#SkinnedMeshRenderer-経由で指定">SkinnedMeshRenderer 経由で指定</a></li> <li><a href="#VRM-の場合">VRM の場合</a></li> <li><a href="#所感">所感</a></li> </ul> </li> <li><a href="#タイムライン上への自動クリップ追加">タイムライン上への自動クリップ追加</a><ul> <li><a href="#uLipSync">uLipSync</a></li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="はじめに">はじめに</h2> <p><strong>uLipSync</strong> は Unity 上で音声データ / 音声入力をもとに<strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a></strong>を実現するアセットです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F01%2F30%2F152519" title="uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/01/30/152519">tips.hecomi.com</a></cite></p> <p>今回は要望を頂いていた以下の 2 つの機能追加・サンプル更新をしましたので解説します。</p> <ul> <li><strong>実行時にセットアップ</strong>したい</li> <li><strong>Timeline での<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>クリップ配置を簡単に</strong>したい</li> </ul> <h2 id="リリース">リリース</h2> <p>v2.6.0 をリリースしました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Freleases%2Ftag%2Fv2.6.0" title="Release v2.6.0 has been released! · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/releases/tag/v2.6.0">github.com</a></cite></p> <h2 id="実行時のセットアップ">実行時のセットアップ</h2> <p>例えば <a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> をロードしてそこに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>をセットアップしたい場合、これまでは <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> が足りておらず自前で頑張る必要がありました。これを解決するために、いくつか <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> を生やし、それらを使うサンプルを <strong>10. Runtime Setup</strong> に追加しました。あくまでサンプルなのでそのまま使える感じではありませんが...、また色々と要望を聞きつつアップデートしていこうと思います。</p> <h3 id="SkinnedMeshRenderer-経由で指定">SkinnedMeshRenderer 経由で指定</h3> <p>方針としては、</p> <ol> <li><code>uLipSyncBlendShape</code> をアタッチ</li> <li>そこに <code>SkinneMeshRenderer</code> を登録</li> <li><code>AddBlendShape(string phoneme, string blendShape)</code> で <code>Profile</code> に登録された音素名と、それに対応する <code>SkinnedMeshRenderer</code> の BlendShape を紐付け</li> <li><code>uLipSync</code> をアタッチ</li> <li><code>Profile</code> を登録</li> <li><code>uLipSyncBlendShape</code> のコールバックを登録</li> </ol> <p>となります。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> System.Collections.Generic; <span class="synType">public</span> <span class="synType">class</span> <span class="synType">uLipSyncBlendShapeRuntimeSetupExample </span><span class="synStatement">:</span> MonoBehaviour { <span class="synComment">// リップシンクを追加したいゲームオブジェクト(AudioSource </span> <span class="synType">public</span> GameObject target; <span class="synComment">// バインドしたいプロファイル</span> <span class="synType">public</span> uLipSync.Profile profile; <span class="synComment">// ブレンドシェイプを含む SkinnedMeshRenderer</span> <span class="synType">public</span> <span class="synType">string</span> skinnedMeshRendererName <span class="synStatement">=</span> <span class="synConstant">&quot;MTH_DEF&quot;</span>; <span class="synComment">// とりあえず何かしら適当なデータがあるとする</span> [System.Serializable] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">PhonemeBlendShapeInfo</span> { <span class="synType">public</span> <span class="synType">string</span> phoneme; <span class="synType">public</span> <span class="synType">string</span> blendShape; } <span class="synType">public</span> List&lt;PhonemeBlendShapeInfo&gt; phonemeBlendShapeTable <span class="synStatement">=</span> <span class="synStatement">new</span> List&lt;PhonemeBlendShapeInfo&gt;(); uLipSync.uLipSync _lipsync; uLipSync.uLipSyncBlendShape _blendShape; <span class="synType">void</span> Start() { <span class="synStatement">if</span> (<span class="synStatement">!</span>target) <span class="synStatement">return</span>; SetupBlendShpae(); SetupLipSync(); } <span class="synType">void</span> SetupBlendShpae() { <span class="synComment">// 指定した名前の GameObject を検索</span> <span class="synType">var</span> targetTform <span class="synStatement">=</span> uLipSync.Util.FindChildRecursively( target.transform, skinnedMeshRendererName); <span class="synStatement">if</span> (<span class="synStatement">!</span>targetTform) { Debug.LogWarning( <span class="synConstant">$&quot;There is no GameObject named </span><span class="synSpecial">\&quot;{</span>skinnedMeshRendererName<span class="synSpecial">}\&quot;</span><span class="synConstant">&quot;</span>); <span class="synStatement">return</span>; } <span class="synComment">// SkinnedMeshRenderer を取得</span> <span class="synType">var</span> smr <span class="synStatement">=</span> targetTform.GetComponent&lt;SkinnedMeshRenderer&gt;(); <span class="synStatement">if</span> (<span class="synStatement">!</span>smr) { Debug.LogWarning( <span class="synConstant">$&quot;</span><span class="synSpecial">\&quot;{</span>skinnedMeshRendererName<span class="synSpecial">}\&quot;</span><span class="synConstant"> does not have SkinnedMeshRenderer.&quot;</span>); <span class="synStatement">return</span>; } <span class="synComment">// uLipSyncBlendShape をつける</span> _blendShape <span class="synStatement">=</span> target.AddComponent&lt;uLipSync.uLipSyncBlendShape&gt;(); _blendShape.skinnedMeshRenderer <span class="synStatement">=</span> smr; <span class="synComment">// 何かしらのデータを使って Phoneme とブレンドシェイプ名をバインド</span> <span class="synStatement">foreach</span> (<span class="synType">var</span> info <span class="synStatement">in</span> phonemeBlendShapeTable) { _blendShape.AddBlendShape(info.phoneme, info.blendShape); } } <span class="synType">void</span> SetupLipSync() { <span class="synStatement">if</span> (<span class="synStatement">!</span>_blendShape) <span class="synStatement">return</span>; <span class="synComment">// uLipSync をつける</span> _lipsync <span class="synStatement">=</span> target.AddComponent&lt;uLipSync.uLipSync&gt;(); <span class="synComment">// プロファイルを指定</span> _lipsync.profile <span class="synStatement">=</span> profile; <span class="synComment">// uLipSyncBlendShape のコールバックを登録</span> _lipsync.onLipSyncUpdate.AddListener(_blendShape.OnLipSyncUpdate); } } </pre> <p>実際はユーザに任せる段では <code>SkinnedMeshRendere</code> や BlendShape のリストを提供する UI などを用意する必要があるかもしれません。その際、音素名のリストは <code>Profile.GetPhonemeNames()</code> から取得することが出来ます。</p> <h3 id="VRM-の場合"><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> の場合</h3> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> の場合は少し簡単で、<a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> に登録されている情報を指定するだけで良いです。例えば <a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> 0.x 向け <code>uLipSyncBlendShapeVRM</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を使う場合は以下のようにします。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">class</span> <span class="synType">uLipSyncBlendShapeVRMRuntimeSetupExample </span><span class="synStatement">:</span> MonoBehaviour { ... <span class="synComment">// VRM 用のスクリプト</span> uLipSync.uLipSyncBlendShapeVRM _blendShape; ... <span class="synType">void</span> SetupBlendShpae() { _blendShape <span class="synStatement">=</span> target.AddComponent&lt;uLipSync.uLipSyncBlendShapeVRM&gt;(); <span class="synComment">// 音素と VRM のブレンドシェイプ名をバインド</span> <span class="synStatement">foreach</span> (<span class="synType">var</span> info <span class="synStatement">in</span> phonemeBlendShapeTable) { _blendShape.AddBlendShape(info.phoneme, info.blendShapeClip); } } ... } </pre> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> 1.0 向けに <code>uLipSyncExpressionVRM</code> を使って Expression を利用する場合も、同様に <code>AddBlendShape()</code> すれば良いです(将来的に <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> 名は変更になるかもしれません…)。</p> <h3 id="所感">所感</h3> <p><code>SkinnedMeshRenderer</code> 直接利用の場合は、対応する BlendShape を検索・セットアップする手順が面倒そうですね。どれが口のどの形に対応するかはモデルごとにまちまちですし、特にルールが有るわけでもありません。名前自体も <code>blendShape1.MTH_A</code> のように長くて分かりづらいです。</p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> の方は簡単で、<a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> 0.x の場合は <code>VRM.BlendShapeAvatar</code> で定義された <code>VRM.BlendShapeClip</code> が <code>A</code> や <code>I</code> といったシンプルな名前を持っているのでよりユーザフレンドリーです。<a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> 1.0 では BlendShape が Expression という呼称に代わっており、<code>aa</code>、<code>ih</code> といった国際標準名で与える形になります。また、BlendShape でなくジョイントを動かしたり UV を動かしたりする場合でも対応しているのでより汎用的です。コードも短くなりますし良いですね。</p> <h2 id="タイムライン上への自動クリップ追加">タイムライン上への自動クリップ追加</h2> <p><strong>Audio Track</strong> に対応する形で <strong>uLipSync Track</strong> を自動追加したい、という要望を頂きましたので作ってみました。</p> <h3 id="uLipSync">uLipSync</h3> <p><strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> > uLipSync > Timeline Setup Helper</strong> でウィンドウを開きます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230103182558" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230103/20230103182558.png" width="848" height="480" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>このようなウィンドウが表示されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230103210604" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230103/20230103210604.png" width="1008" height="888" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p><strong>Timeline</strong> となっているフィールドに Timeline アセットを指定すると、そこに含まれる <strong>Audio Track</strong> および <strong>uLipSync Track</strong> がそれぞれプルダウンリストから選択できるようになります。追加していない場合は適宜追加してください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230104205515" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230104/20230104205515.png" width="1004" height="582" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p><strong>Profile</strong> には <code>BakedData</code> を自動生成する際に利用する <code>Profile</code>(どの声で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>を行うかのデータ)を指定します。<strong>Use Exiting Asset</strong> をチェックしている場合は、対象の <code>AudioClip</code> を利用した <code>BakedData</code> が存在した場合、新規生成せずにそれを利用します。<strong>Output Directory</strong> には新規生成した <code>BakedData</code> を保存する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを指定してください。</p> <p>これで次のように Timeline が自動セットアップされるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20230105122217" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20230105/20230105122217.gif" width="804" height="844" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h2 id="おわりに">おわりに</h2> <p>ランタイムセットアップの方に関しては、あとは <code>Profile</code> のランタイムでの構築、保存、読込のサンプルを用意しようと思います。</p> <p>今回のように生の利用者の声がいただけると、今後も色々と改善に繋げられると思いますので、ぜひお気軽に <a href="https://twitter.com/hecomi">Twitter</a> などでお声がけください。</p> hecomi uLipSync で Animator を使ったリップシンクができるように更新してみた hatenablog://entry/4207112889949029556 2022-12-28T23:38:03+09:00 2023-01-05T16:38:57+09:00 はじめに uLipSync v2.5.1 をリリースしました。 github.com 以下の 2 点のアップデートが含まれています。 Animator の BlendTree を使ったリップシンクのサポート これまでは BlendShape 指定でしたが Animator のステートでリップシンクさせることが可能になります VRM の Expression 指定によるリップシンクのサポート VRM の BlendShape ではなく Expression で指定できるコンポーネントが追加されました それぞれ PR を頂きました、ありがとうございます。 github.com github.com… <h2 id="はじめに">はじめに</h2> <p><strong>uLipSync v2.5.1</strong> をリリースしました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Freleases%2Ftag%2Fv2.5.1" title="Release v2.5.1 has been released! · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/releases/tag/v2.5.1">github.com</a></cite></p> <p>以下の 2 点のアップデートが含まれています。</p> <ul> <li><strong>Animator の BlendTree を使った<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>のサポート</strong> <ul> <li>これまでは BlendShape 指定でしたが Animator のステートで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>させることが可能になります</li> </ul> </li> <li><strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> の Expression 指定による<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>のサポート</strong> <ul> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> の BlendShape ではなく Expression で指定できる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が追加されました</li> </ul> </li> </ul> <p>それぞれ PR を頂きました、ありがとうございます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Fpull%2F24" title="Add support for Animator by liudger · Pull Request #24 · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/pull/24">github.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Fpull%2F27" title="Add vrm expression sample by mkc1370 · Pull Request #27 · hecomi/uLipSync" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/pull/27">github.com</a></cite></p> <p>本記事では、これらのうち、ちょっと解説が必要な Animator 利用についてセットアップの方法や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>についての解説を行おうと思います。uLipSync そのものの使い方については以下の記事をご参照ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F01%2F30%2F152519" title="uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/01/30/152519">tips.hecomi.com</a></cite></p> <h2 id="概要">概要</h2> <p>BlendTree を組んであげると Animator へ音素に応じたパラメタが渡り、次のように<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>させることが出来ます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221228213045" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221228/20221228213045.gif" width="938" height="486" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>Animator を作ることの利点としては次のような点が挙げられます。</p> <ul> <li><strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプがセットアップされていないモデルでアニメーションベースで動作</strong>させられる</li> <li><strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>のステートとアニメーションのステートの切り分け</strong>ができる <ul> <li>普段はアニメーションに応じた表情をしつつ、必要になったタイミングで口パクのリアルタイム解析ステートへと遷移が可能</li> </ul> </li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221228221520" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221228/20221228221520.gif" width="719" height="480" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>これらの設定例は <strong>09. Animator</strong> に含まれています。</p> <h2 id="設定">設定</h2> <h3 id="Animator-の設定">Animator の設定</h3> <p>まずは Animator を作成します。Animator 自体についての詳細については省きます<a href="#f-b8412bde" name="fn-b8412bde" title="公式解説などをご参照ください https://learn.unity.com/tutorial/1-3-animation-systems-core-concepts-animation-blending-jp">*1</a>が、だいたい次のような感じになるかと思います。</p> <ul> <li><code>AnimatorController</code> を準備</li> <li>顔(または口のみ)の <a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Mask を設定したレイヤを作成</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>ステートとその他のステートを作成</li> <li>音素に対応する Float パラメタを追加して<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>ステートをセットアップ <ul> <li>後述の uLipSync 側からはこの Float に 0 ~ 1 の値が渡される形になります</li> </ul> </li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221228222906" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221228/20221228222906.png" width="1200" height="651" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221228223902" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221228/20221228223902.png" width="1200" height="629" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="uLipSyncAnimator-の設定">uLipSyncAnimator の設定</h3> <p><code>uLipSync</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>をセットアップした上で、<code>uLipSyncAnimator</code> という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>をアタッチします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221228224147" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221228/20221228224147.png" width="1040" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p><strong>Animator</strong> セクションでは、<strong>Find From Children</strong> をチェックしている場合はプルダウンリストから<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>以下自身を含む子供にアタッチされた <code>Animator</code> が表示されるので、口パクさせたい GameObject を選択してください。直接指定したい場合は <strong>Find From Children</strong> のチェックを外して表示される <code>Animator</code> 指定フィールドに対象の <code>Animator</code> をセットしてください。</p> <p><strong>Parameters</strong> セクションは他の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>と共通です。マイクが反応してほしいボリューム帯(<strong>Volume Min/Max</strong>)と、口パクの応答速度(<strong>Smoothness</strong>)を指定します。</p> <p>そして <strong>Animator Contorller Parameters</strong> セクションで、先程セットアップした <code>AnimatorController</code> と認識した音素の紐付けを行います。必要な音素分のエントリを作成し、それぞれ <strong>Phoneme</strong> に uLipSync の Profile で登録した音素を、<strong>Parameter</strong> には <code>AnimatorController</code> の Float パラメタを指定します。</p> <p>最後に <code>uLipSync</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <strong>On Lip Sync Update</strong> イベントにこの <code>uLipSyncAnimator.OnLipSyncUpdate</code> を指定すれば完了です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221228230327" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221228/20221228230327.png" width="850" height="382" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h2 id="余談-開発中に詰まったバグ修正">余談: 開発中に詰まったバグ修正</h2> <h3 id="Animator-is-not-playing-an-AnimatorController">Animator is not playing an AnimatorController</h3> <p><code>uLipSyncAnimator</code> では Editor <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>で <code>Animator</code> から色々情報を取得しています(プルダウンリストに表示するパラメタ名など)。しかしながら、この情報取得箇所でシーンをセーブしたときなどに <code>Animator is not playing an AnimatorController</code> と表示されます。これは(おそらく)Animator がリセットされることに起因するようなのですが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EF%A1%BC%A5%AF%A5%A2%A5%E9%A5%A6%A5%F3%A5%C9">ワークアラウンド</a>としては <a href="https://docs.unity3d.com/ja/2021.2/ScriptReference/Animator.Rebind.html">Animator.Rebind()</a> 呼ぶことで切り抜けることができるようです。私は、<code>OnInspectorGUI()</code> 中で次のように <code>Animator</code> が初期化されていなければ <code>Rebind()</code> するようにコードを付け加えました。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">var</span> animator <span class="synStatement">=</span> ...; <span class="synStatement">if</span> (<span class="synStatement">!</span>animator.isInitialized) { animator.Rebind(); } </pre> <p>もし他に良い方法をご存知でしたら教えていただけると助かります。</p> <h2 id="おわりに">おわりに</h2> <p>次は動的に uLipSync をセットアップしたいというお話を見かけたのでそちらを色々対応しようと思います。<a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> をランタイムロードした場合にどうアタッチするか、またランタイム<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>をどうするか、もう少し色々な方に使ってもらえるようにそのあたりもまとめたいですね。</p> <div class="footnote"> <p class="footnote"><a href="#fn-b8412bde" name="f-b8412bde" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">公式解説などをご参照ください <a href="https://learn.unity.com/tutorial/1-3-animation-systems-core-concepts-animation-blending-jp">https://learn.unity.com/tutorial/1-3-animation-systems-core-concepts-animation-blending-jp</a></span></p> </div> hecomi 結婚式のプロフィールムービーを Unity で作ってみた hatenablog://entry/4207112889940275756 2022-11-30T23:36:16+09:00 2022-11-30T23:43:57+09:00 はじめに 私事で恐縮ですが、先日、コロナ禍で 2 年半ほど延期していた結婚式をしました。そこでのプロフィールムービー制作を Unity でやってみましたので、本記事ではそのお話をしようと思います。 過去に友人の結婚式のムービー制作を任された時は After Effects か Premiere で作っていたのですが、それぞれ本 1 冊読んだ程度なので、自分の能力的にはスライドショー的に画像や文字を配置する程度が限界でした。自分たちのものはスプラトゥーン風のムービーにしたいなぁと思い、もう少し色々やれるかも & 勉強にもなるかもと思って Unity でやってみることにした次第です。プライバシーの… <h2 id="はじめに">はじめに</h2> <p>私事で恐縮ですが、先日、コロナ禍で 2 年半ほど延期していた結婚式をしました。そこでの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%D5%A5%A3%A1%BC%A5%EB%A5%E0%A1%BC%A5%D3%A1%BC">プロフィールムービー</a>制作を Unity でやってみましたので、本記事ではそのお話をしようと思います。</p> <p>過去に友人の結婚式のムービー制作を任された時は <a class="keyword" href="http://d.hatena.ne.jp/keyword/After%20Effects">After Effects</a> か Premiere で作っていたのですが、それぞれ本 1 冊読んだ程度なので、自分の能力的にはスライドショー的に画像や文字を配置する程度が限界でした。自分たちのものは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%D7%A5%E9%A5%C8%A5%A5%A1%BC%A5%F3">スプラトゥーン</a>風のムービーにしたいなぁと思い、もう少し色々やれるかも &amp; 勉強にもなるかもと思って Unity でやってみることにした次第です。プライバシーの関係でムービーの共有はできないのですが...、部分部分を簡単にご紹介したいと思います。</p> <h2 id="概要">概要</h2> <p>基本方針は次のような感じです。</p> <ul> <li>全体は Timeline を使って作成</li> <li>出力は <a href="https://docs.unity3d.com/ja/Packages/com.unity.recorder@2.6/manual/index.html">Unity Recorder</a> を使って mp4 化</li> <li>各表示オブジェクトの表現はシェーダグラフを使って作成(URP)</li> <li>写真入れ替え・コメント入れ替えは直前まで出来るようにエディタ拡張から自動生成</li> </ul> <h2 id="オープニング部分">オープニング部分</h2> <p>こんな感じの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%D7%A5%E9%A5%C8%A5%A5%A1%BC%A5%F3">スプラトゥーン</a> 2 風のオープニングを作ってみました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221129003045" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221129/20221129003045.gif" width="813" height="453" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>ここからカクカクと自分らの写真が動いて挨拶するシーンが入るのですが、そこは割愛します。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B7%A5%DE%A5%B7%A5%DE">シマシマ</a>模様はテクスチャなしでシェーダグラフで作ってみました。黒い縞々はワールド座標系で XY を基準にして作っています。ワールド座標系にしておくとこれを適用したオブジェクトは一体化したみたいな感じになるので、マスクっぽい使い方ができます(滴りのオブジェクト部分にも適用してます)。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221129015640" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221129/20221129015640.png" width="1200" height="579" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>UV の X * Y をして斜めのグラデーションを作り、Multiply して数値を大きくしたあと Fraction してあげると縞々になります。Multiply する大きさを制御することで縞々の量もコン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>でき、Add した値も足してあげるとスクロールします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221129020036" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221129/20221129020036.png" width="1200" height="658" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>パキッとさせるために Step したあと、白黒部分それぞれに色を付ければ好きな色の縞々が出来る感じです。ワールド座標ではなくローカル座標や UV 座標を使ってあげれば回転させられる縞々も作れるので、上記 GIF のところではこれららを組み合わせている感じです。</p> <h2 id="名前紹介部分">名前紹介部分</h2> <p>こんな感じのにしてみました(実際はインクがあたったところからジワッと出るように調整しましたが、ブログ用に文字を HECOMI に変えた関係で場所が合わなくなってます…)</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130225624" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130225624.gif" width="640" height="361" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="背景表現">背景表現</h3> <p>背景の縞々は使いまわしです。その前面にジワーッと垂れてくる背景があります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130225817" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130225817.gif" width="720" height="406" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>ここは全然シェーダグラフでやる必要はないのですが、頭の体操のためにテクスチャなしでシェーダだけで形を作ってみました。縦方向に長さの違う縞々を作成して、端っこ部分は Rounded Rectangle ノードで丸いカーブ部分を作り、足し合わせることによって滴るような形状を作っています。途中で色々パラメタで制御するようにして、これらをタイムラインでキーフレームを打って利用しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221129022305" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221129/20221129022305.png" width="1200" height="472" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>モヤモヤしているところは Voronoi を加工したものを互い違いにスクロールして足し合わせ、これを Step することで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E1%A5%BF%A5%DC%A1%BC%A5%EB">メタボール</a>みたいにモニョモニョする何かができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130005601" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130005601.gif" width="720" height="261" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="名前">名前</h3> <p>名前はインクに合わせてジワッと出てくるようにしてます。これには次のようなテクスチャを作っています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130010035" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130010035.png" width="1200" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>RGB チャネルにそれぞれ、</p> <ul> <li>G: 文字全体</li> <li>B: 文字がじわっと出てくる順番を表すマスク(青が強いところから G が見えるようになる)</li> <li>R: インクの垂れ表現、G に足し合わせる(赤が強いところから見えるようになる)</li> </ul> <p>といった内容を入れておき、次のようなシェーダグラフを作ってこれらを参照してコン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130015812" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130015812.png" width="1200" height="634" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>B と R に与える<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%EC%A5%C3%A5%B7%A5%E7%A5%EB%A5%C9">スレッショルド</a>パラメタを作成し、これをパーティクルエフェクトや弾と組み合わせて Timeline 上でキーフレームアニメーションしています。</p> <h3 id="弾">弾</h3> <p>飛んでくる弾は基本的には4分割のタイルテクスチャをアニメーションさせている感じですが、ちょっと情報量を増やすためにその上で UV にノイズをかけてモニョモニョさせてみました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130225247" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130225247.gif" width="1200" height="372" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="その他">その他</h3> <p>シェーダグラフでのアウトライン表現はこちらの記事にまとめています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2020%2F02%2F24%2F140607" title="Unity で Shader Graph の Custom Function Node を試してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2020/02/24/140607">tips.hecomi.com</a></cite></p> <p>これに加えてアウトライン色も含む全体の色をコン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>できるようにして、屈んだ写真のときに色をジワッと変えて<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%AB">イカ</a>と<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%EF%A5%C3%A5%D7">スワップ</a>してます。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%AB">イカ</a>はモデル作って普通にアニメーションで動かしてます。</p> <h2 id="スライドショー部分">スライドショー部分</h2> <h3 id="作った表現">作った表現</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221128231627" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221128/20221128231627.gif" width="428" height="243" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="インク表現">インク表現</h3> <p>まずインクがピチャピチャと付く表現からです。インク素材はシルエットデザインさんのものをお借りしました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkage-design.com%2F2015%2F01%2F01%2Fpaint1%2F" title="スプラトゥーン風のペンキ跡 – SILHOUETTE DESIGN" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://kage-design.com/2015/01/01/paint1/">kage-design.com</a></cite></p> <p>これをタイルテクスチャとして読み込めるようにして、こんな感じのジワジワッと広がる表現をシェーダーグラフで作ってみました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221127185845" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221127/20221127185845.gif" width="571" height="510" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>実装としては、円状にグラデーションがかったノイズを multiply して smoothstep をかけているだけですが、インクパターンごとに中心位置が違うので、円状のグラデーション中心もいじれるようにしておきました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221127190915" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221127/20221127190915.png" width="1200" height="548" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221128233329" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221128/20221128233329.png" width="1200" height="551" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>なお、文字も同じようにノイズテクスチャでジワッと出るようにしておきます(画像見づらくてすみません)。やっていることは同じで、ノイズを multiply して smoothstep しているだけです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221128233941" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221128/20221128233941.png" width="1200" height="557" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>これらをタイムライン上で組み立てていきます。飛んでくるインクは単純なタイルテクスチャのアニメーションで表現し(これも連番 index で操作しやすいようにシェーダグラフで適当なシェーダを作っておきます)、飛んでくる感じの軌跡をキーフレームアニメーションで作成、着弾時のパーティクルエフェクトを追加したりすればピチャピチャ感が出ました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221128234952" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221128/20221128234952.gif" width="520" height="541" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>スプラッシュボムもモデルを作成してこちらは普通に爆発アニメーションを作ってそれをタイムライン上で流し爆発する瞬間にインクと差し替えればそれっぽく見えました。</p> <h2 id="写真の入れ替え">写真の入れ替え</h2> <p>上記のようにいくつかのパターンを Timeline を包含した Prefab として作成しておき、これを親側の Timeline 上に並べるところを自動化してみました。とはいっても、ボタンを押すと 0 から生成するというようなものではなく、少し手抜きして、事前にセットアップされた Timeline に並べられたアイテムをセットアップボタンを押したときにエディタ上で登録された情報をもとに GameObject の差し替え(左右どちら方向に弾を撃つのかボムを爆発させるのかの演出など)、写真・コメント・インクの色の変更を行う形にしています。</p> <p>Preafab はこんな感じで Timeline を含んでいます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130023130" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130023130.png" width="1200" height="874" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>そしてスライドショーの Timeline はこんな感じで並べておきます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130223648" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130223648.png" width="1200" height="935" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>Prefab のルート要素は Timeline を持っていませんが、Control Track に配置した Control Playable Asset の Control Children をチェックしておくと、子要素に配置した Timeline を制御してくれるようになります。ここに置く GameObject をエディタ拡張のボタンをポチッと押したら置き換えるような仕組みを作ります。</p> <p>まずはこんな感じでどのようなスライドショーのパターンが有るかの Prefab を登録し...</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130230609" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130230609.png" width="1072" height="764" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>どのパターンの Prefab を使って、どのテクスチャ・どんなコメントを載せたいかを入力していきます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221130230942" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221130/20221130230942.png" width="846" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>最後に Rebuild Timeline を押すと、次のような処理が走ります。</p> <ul> <li>まずは Timeline に配置されたクリップを収集してリストを作成</li> <li>リストの中身を時系列に並び替え</li> <li>次に入力されたデータを見ていって選択した Prefab の GameObject を生成</li> <li>生成したオブジェクトのテクスチャとコメントを書き換え</li> <li>対応するクリップの Source Game Object をこれと差し替えていく</li> </ul> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">void</span> SetupTimeline() { ... <span class="synComment">// playableAsset.outputs を見てクリップを収集していく</span> <span class="synType">var</span> director <span class="synStatement">=</span> gameObject.GetComponent&lt;PlayableDirector&gt;(); <span class="synType">var</span> asset <span class="synStatement">=</span> director.playableAsset; <span class="synType">var</span> clips <span class="synStatement">=</span> <span class="synStatement">new</span> List&lt;TimelineClip&gt;(); <span class="synStatement">foreach</span> (<span class="synType">var</span> output <span class="synStatement">in</span> asset.outputs) { <span class="synComment">// Picture と名前のついたクリップだけ見る</span> <span class="synStatement">if</span> (<span class="synStatement">!</span>output.streamName.Contains(<span class="synConstant">&quot;Picture&quot;</span>)) <span class="synStatement">continue</span>; <span class="synType">var</span> track <span class="synStatement">=</span> output.sourceObject <span class="synStatement">as</span> <span class="synType">ControlTrack</span>; <span class="synStatement">foreach</span> (<span class="synType">var</span> clip <span class="synStatement">in</span> track.GetClips()) { clips.Add(clip); } } <span class="synComment">// 時系列に並び替え</span> clips <span class="synStatement">=</span> clips.OrderBy(x <span class="synStatement">=&gt;</span> x.start).ToList(); <span class="synComment">// 入力された情報を取得</span> <span class="synType">var</span> generator <span class="synStatement">=</span> target <span class="synStatement">as</span> <span class="synType">PicturesGenerator</span>; <span class="synType">var</span> pictures <span class="synStatement">=</span> generator.pictures; <span class="synComment">// クリップの情報を置き換えていく</span> <span class="synType">int</span> nc <span class="synStatement">=</span> clips.Count; <span class="synType">int</span> np <span class="synStatement">=</span> pictures.Count; <span class="synStatement">for</span> (<span class="synType">int</span> i <span class="synStatement">=</span> <span class="synConstant">0</span>; i <span class="synStatement">&lt;</span> nc; <span class="synStatement">++</span>i) { <span class="synComment">// ControlPlayableAsset を取得</span> <span class="synType">var</span> clip <span class="synStatement">=</span> clips[i]; <span class="synType">var</span> playableAsset <span class="synStatement">=</span> clip.asset <span class="synStatement">as</span> <span class="synType">ControlPlayableAsset</span>; <span class="synStatement">if</span> (playableAsset <span class="synStatement">==</span> <span class="synConstant">null</span>) <span class="synStatement">continue</span>; ... <span class="synComment">// 指定された Prefab を選択</span> <span class="synType">var</span> prefab <span class="synStatement">=</span> generator.prefabs.FirstOrDefault( x <span class="synStatement">=&gt;</span> x.type <span class="synStatement">==</span> data.type).prefab; ... <span class="synComment">// GameObject を生成</span> <span class="synType">var</span> go <span class="synStatement">=</span> Instantiate(prefab, transform); go.SetActive(<span class="synConstant">false</span>); <span class="synComment">// 入力された情報をもとにテクスチャやコメントを書き換え</span> <span class="synType">var</span> setup <span class="synStatement">=</span> go.GetComponent&lt;PictureTimelineSetup&gt;(); <span class="synStatement">if</span> (setup) { setup.Setup(data); } <span class="synComment">// ControlPlayableAsset の GameObject を入れ替え</span> director.SetReferenceValue( playableAsset.sourceGameObject.exposedName, go); } } </pre> <p>このあたりのコード全文は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%AB">イカ</a>になります。</p> <ul> <li>コード: <a href="https://gist.github.com/hecomi/fd3caf8aa0339455c441d95188d71f8b">PicturesGenerator Example &middot; GitHub</a></li> </ul> <p>こういったタイムライン要素の書き換えについてはテラシュールブログさんに詳しく書かれています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsubakit1.hateblo.jp%2Fentry%2F2017%2F04%2F26%2F221548" title="【Unity】Timelineに動的に生成したオブジェクトをバインドする - テラシュールブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tsubakit1.hateblo.jp/entry/2017/04/26/221548">tsubakit1.hateblo.jp</a></cite></p> <h2 id="映像出力">映像出力</h2> <p>公式の Unity Recorder を使うと簡単に mp4 が生成できます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2FPackages%2Fcom.unity.recorder%402.6%2Fmanual%2Findex.html" title="Unity Recorder について | Unity Recorder | 2.6.0-exp.4" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/Packages/com.unity.recorder@2.6/manual/index.html">docs.unity3d.com</a></cite></p> <p>Timeline 連携も用意されていて、Recorder トラック と Recorder クリップを使って指定した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B6%E8%B4%D6">区間</a>を録画・録音することも出来ますが、私は通常のゲーム画面キャプチャで行いました。</p> <h2 id="おわりに">おわりに</h2> <p>基本的にはオブジェクトをおいてキーフレームアニメーションをして...の Timeline の操作は他の映像編集ソフトと変わらないので、意外とすんなり動きの部分は作れた気がします。Prefab は Nested Prefab が出来てから簡単に編集が出来るようになりましたし、Timeline はネストしても簡単に編集できるのが良いですね。また、色々こだわりたいところは Unity の知識を活用してアレコレ出来るので、映像制作専門ではない Unity 使いの人は、なにかムービー作成機会があったときの1つの選択肢として結構良いのではないかと思いました。デメリットとしてはちょっとしたプリセットのエフェクト(例えば画像間の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B8%A5%B7%A5%E7%A5%F3">トランジション</a>や良い感じの効果など)は当たり前ですが全然ないので、全部自前で作る必要があり、簡単なスライドショーであったら圧倒的に動画編集ソフトのほうが楽かなと思います。ただ、限られたスケジュールの中で切羽詰まってやることで、強制的にシェーダグラフや Timeline 周りのノウハウが溜まるので心に余裕があったらオススメです。</p> hecomi uLipSync 内の AudioClip をトリミング・再生するスクリプトの解説 hatenablog://entry/4207112889932239552 2022-10-31T02:19:57+09:00 2022-10-31T02:29:00+09:00 はじめに 今回は小ネタです。uLipSync のキャリブレーション用にオーディオデータの選択した範囲のループ再生機能を作ったのですが、その内容をまとめておきます。元記事はこちら: tips.hecomi.com 機能について こんな感じでドラッグして再生したい箇所を選択、クロスフェード具合を調整して、Play を押すと選択範囲がループ再生されるというものです。uLipSync のキャリブレーションでは現在再生されている音を解析するため、このような仕組みが必要でした。 解説 選択された音をトリミングした AudioClip を生成するランタイムコード部と、どこをトリミングするか選択する UI の… <h2 id="はじめに">はじめに</h2> <p>今回は小ネタです。<a href="https://github.com/hecomi/uLipSync">uLipSync</a> の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>用にオーディオデータの選択した範囲のループ再生機能を作ったのですが、その内容をまとめておきます。元記事はこちら:</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F01%2F30%2F152519" title="uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/01/30/152519">tips.hecomi.com</a></cite></p> <h2 id="機能について">機能について</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123143058" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123143058.gif" width="550" height="212" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>こんな感じでドラッグして再生したい箇所を選択、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%ED%A5%B9%A5%D5%A5%A7%A1%BC%A5%C9">クロスフェード</a>具合を調整して、Play を押すと選択範囲がループ再生されるというものです。uLipSync の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>では現在再生されている音を解析するため、このような仕組みが必要でした。</p> <h2 id="解説">解説</h2> <p>選択された音をトリミングした <code>AudioClip</code> を生成するランタイムコード部と、どこをトリミングするか選択する UI の部分のエディタコード部があります。コード全文は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>からご参照ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync" title="GitHub - hecomi/uLipSync: A MFCC-based LipSync plugin for Unity using Burst Compiler" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync">github.com</a></cite></p> <h3 id="ランタイムコード">ランタイムコード</h3> <p>まずはランタイムコード部からですが、それほど長くは無いので全文を貼ってみます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; [RequireComponent(<span class="synStatement">typeof</span><span class="synType">(AudioSource)</span>)] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">uLipSyncCalibrationAudioPlayer </span><span class="synStatement">:</span> MonoBehaviour { <span class="synType">public</span> AudioClip clip; <span class="synType">public</span> <span class="synType">float</span> start <span class="synStatement">=</span> <span class="synConstant">0f</span>; <span class="synType">public</span> <span class="synType">float</span> end <span class="synStatement">=</span> <span class="synConstant">1f</span>; <span class="synType">public</span> <span class="synType">float</span> crossFadeDuration <span class="synStatement">=</span> <span class="synConstant">0.05f</span>; AudioClip _tmpClip; <span class="synType">float</span>[] _data; <span class="synType">int</span> _currentPos <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synType">bool</span> _audioReadCalled <span class="synStatement">=</span> <span class="synConstant">false</span>; <span class="synType">int</span> _sampleRate <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synType">int</span> _sampleCount <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synType">int</span> _channels <span class="synStatement">=</span> <span class="synConstant">1</span>; <span class="synType">int</span> _crossFadeDataCount <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synType">int</span> playDataSampleCount <span class="synStatement">=&gt;</span> _sampleCount <span class="synStatement">-</span> _crossFadeDataCount; <span class="synType">public</span> <span class="synType">bool</span> isPlaying { <span class="synStatement">get</span> { <span class="synType">var</span> source <span class="synStatement">=</span> GetComponent&lt;AudioSource&gt;(); <span class="synStatement">return</span> source <span class="synStatement">?</span> source.isPlaying <span class="synStatement">:</span> <span class="synConstant">false</span>; } } <span class="synType">void</span> OnEnable() { Apply(); } <span class="synType">void</span> OnDisable() { Destroy(_tmpClip); } <span class="synType">void</span> Update() { <span class="synStatement">if</span> (<span class="synStatement">!</span>_audioReadCalled) <span class="synStatement">return</span>; _sampleRate <span class="synStatement">=</span> AudioSettings.outputSampleRate; } <span class="synType">public</span> <span class="synType">void</span> Apply() { <span class="synStatement">if</span> (<span class="synStatement">!</span>clip) <span class="synStatement">return</span>; <span class="synType">var</span> source <span class="synStatement">=</span> GetComponent&lt;AudioSource&gt;(); <span class="synStatement">if</span> (<span class="synStatement">!</span>source) <span class="synStatement">return</span>; <span class="synType">var</span> startPos <span class="synStatement">=</span> (<span class="synType">int</span>)(clip.samples <span class="synStatement">*</span> start); <span class="synType">var</span> endPos <span class="synStatement">=</span> (<span class="synType">int</span>)(clip.samples <span class="synStatement">*</span> end); <span class="synType">var</span> freq <span class="synStatement">=</span> clip.frequency; _sampleCount <span class="synStatement">=</span> endPos <span class="synStatement">-</span> startPos; _channels <span class="synStatement">=</span> clip.channels; _crossFadeDataCount <span class="synStatement">=</span> (<span class="synType">int</span>)(_sampleRate <span class="synStatement">*</span> crossFadeDuration); _crossFadeDataCount <span class="synStatement">=</span> Mathf.Min(_crossFadeDataCount, _sampleCount <span class="synStatement">/</span> <span class="synConstant">2</span> <span class="synStatement">-</span> <span class="synConstant">1</span>); _data <span class="synStatement">=</span> <span class="synStatement">new</span> float[_sampleCount <span class="synStatement">*</span> _channels]; clip.GetData(_data, startPos); <span class="synType">var</span> name <span class="synStatement">=</span> <span class="synConstant">$&quot;</span><span class="synSpecial">{</span>clip.name<span class="synSpecial">}</span><span class="synConstant">-</span><span class="synSpecial">{</span>startPos<span class="synSpecial">}</span><span class="synConstant">-</span><span class="synSpecial">{</span>endPos<span class="synSpecial">}</span><span class="synConstant">&quot;</span>; _tmpClip <span class="synStatement">=</span> AudioClip.Create( name, playDataSampleCount, _channels, freq, <span class="synConstant">true</span>, OnAudioRead, OnAudioSetPosition); source.clip <span class="synStatement">=</span> _tmpClip; source.loop <span class="synStatement">=</span> <span class="synConstant">true</span>; source.Play(); } <span class="synType">void</span> OnAudioRead(<span class="synType">float</span>[] data) { _audioReadCalled <span class="synStatement">=</span> <span class="synConstant">true</span>; <span class="synStatement">for</span> (<span class="synType">int</span> i <span class="synStatement">=</span> <span class="synConstant">0</span>; i <span class="synStatement">&lt;</span> data.Length <span class="synStatement">/</span> _channels; <span class="synStatement">++</span>i) { <span class="synStatement">for</span> (<span class="synType">int</span> ch <span class="synStatement">=</span> <span class="synConstant">0</span>; ch <span class="synStatement">&lt;</span> _channels; <span class="synStatement">++</span>ch) { <span class="synType">int</span> index <span class="synStatement">=</span> i <span class="synStatement">*</span> _channels <span class="synStatement">+</span> ch; <span class="synStatement">if</span> (_currentPos <span class="synStatement">&lt;</span> _crossFadeDataCount) { <span class="synType">float</span> t <span class="synStatement">=</span> (<span class="synType">float</span>)_currentPos <span class="synStatement">/</span> _crossFadeDataCount; <span class="synType">float</span> sin <span class="synStatement">=</span> Mathf.Sin(Mathf.PI <span class="synStatement">*</span> <span class="synConstant">0.5f</span> <span class="synStatement">*</span> t); <span class="synType">float</span> cos <span class="synStatement">=</span> Mathf.Cos(Mathf.PI <span class="synStatement">*</span> <span class="synConstant">0.5f</span> <span class="synStatement">*</span> t); <span class="synType">int</span> indexS <span class="synStatement">=</span> _currentPos; <span class="synType">int</span> indexE <span class="synStatement">=</span> _sampleCount <span class="synStatement">-</span> (_crossFadeDataCount <span class="synStatement">-</span> _currentPos); <span class="synType">float</span> dataS <span class="synStatement">=</span> _data[indexS <span class="synStatement">*</span> _channels <span class="synStatement">+</span> ch]; <span class="synType">float</span> dataE <span class="synStatement">=</span> _data[indexE <span class="synStatement">*</span> _channels <span class="synStatement">+</span> ch]; data[index] <span class="synStatement">=</span> dataS <span class="synStatement">*</span> sin <span class="synStatement">+</span> dataE <span class="synStatement">*</span> cos; } <span class="synStatement">else</span> { data[index] <span class="synStatement">=</span> _data[_currentPos <span class="synStatement">*</span> _channels <span class="synStatement">+</span> ch]; } } _currentPos <span class="synStatement">=</span> (_currentPos <span class="synStatement">+</span> <span class="synConstant">1</span>) % playDataSampleCount; } } <span class="synType">void</span> OnAudioSetPosition(<span class="synType">int</span> newPosition) { _currentPos <span class="synStatement">=</span> newPosition; } <span class="synType">public</span> <span class="synType">void</span> Pause() { <span class="synType">var</span> source <span class="synStatement">=</span> GetComponent&lt;AudioSource&gt;(); <span class="synStatement">if</span> (<span class="synStatement">!</span>source) <span class="synStatement">return</span>; source.Pause(); } <span class="synType">public</span> <span class="synType">void</span> UnPause() { <span class="synType">var</span> source <span class="synStatement">=</span> GetComponent&lt;AudioSource&gt;(); <span class="synStatement">if</span> (<span class="synStatement">!</span>source) <span class="synStatement">return</span>; source.UnPause(); } } </pre> <p>次のような流れになっています。</p> <ul> <li><code>start</code> / <code>end</code> はエディタ側からセット(0.0 ~ 1.0 の値)</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%ED%A5%B9%A5%D5%A5%A7%A1%BC%A5%C9">クロスフェード</a>分を考慮したデータ長をサンプルカウントから計算</li> <li><code>float[]</code> のバッファを用意し、<code>GetData()</code> で対象の範囲のオーディオデータを取得</li> <li>再生は <code>AudioClip.Create()</code> で <code>OnAudioRead()</code> や <code>OnAudioSetPosition()</code> を指定したストリーミング形式で再生 <ul> <li>ここは事前にデータを含むオーディオクリップを作成してしまっても良いかもしれません。</li> </ul> </li> </ul> <h3 id="エディタコード">エディタコード</h3> <p>波形描画については以前記事でも触れましたが、次のように <code>AudioUtil.GetMinMaxData()</code> および <code>AudioCurveRendering.DrawMinMaxFilledCurve()</code> を使います。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">static</span> <span class="synType">class</span> <span class="synType">EditorUtil</span> { ... <span class="synType">public</span> <span class="synType">class</span> <span class="synType">DrawWaveOption</span> { <span class="synType">public</span> System.Func&lt;<span class="synType">float</span>, Color&gt; colorFunc; <span class="synType">public</span> <span class="synType">float</span> waveScale; } <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> DrawWave(Rect rect, AudioClip clip, DrawWaveOption option) { ... <span class="synType">var</span> minMaxData <span class="synStatement">=</span> AudioUtil.GetMinMaxData(clip); ... AudioCurveRendering.AudioMinMaxCurveAndColorEvaluator dlg <span class="synStatement">=</span> <span class="synType">delegate</span>( <span class="synType">float</span> x, <span class="synStatement">out</span> Color col, <span class="synStatement">out</span> <span class="synType">float</span> minValue, <span class="synStatement">out</span> <span class="synType">float</span> maxValue) { col <span class="synStatement">=</span> option.colorFunc(x); ... minValue <span class="synStatement">=</span> ...; maxValue <span class="synStatement">=</span> ...; ... }; <span class="synComment">// 描画処理</span> AudioCurveRendering.DrawMinMaxFilledCurve(rect, dlg); } } </pre> <p>この上で次のようなエディタ拡張を書きます(こちらも少し長いですがほぼ全文を載せます)。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEditor; <span class="synStatement">using</span> System.Collections.Generic; [CustomEditor(<span class="synStatement">typeof</span><span class="synType">(uLipSyncCalibrationAudioPlayer)</span>)] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">uLipSyncCalibrationAudioPlayerEditor </span><span class="synStatement">:</span> Editor { uLipSyncCalibrationAudioPlayer player { <span class="synStatement">get</span> { <span class="synStatement">return</span> target <span class="synStatement">as</span> <span class="synType">uLipSyncCalibrationAudioPlayer</span>; } } <span class="synType">bool</span> _requireRepaint <span class="synStatement">=</span> <span class="synConstant">false</span>; <span class="synType">bool</span> _requireApply <span class="synStatement">=</span> <span class="synConstant">false</span>; <span class="synType">bool</span> _isDraggingStart <span class="synStatement">=</span> <span class="synConstant">false</span>; <span class="synType">bool</span> _isDraggingEnd <span class="synStatement">=</span> <span class="synConstant">false</span>; <span class="synType">bool</span> isDragging <span class="synStatement">=&gt;</span> _isDraggingStart <span class="synStatement">||</span> _isDraggingEnd; ... <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnInspectorGUI() { _requireRepaint <span class="synStatement">=</span> <span class="synConstant">false</span>; serializedObject.Update(); DrawClip(); DrawPlayAndStop(); DrawWave(); EditorGUILayout.Separator(); DrawParameters(); <span class="synStatement">if</span> (Application.isPlaying <span class="synStatement">&amp;&amp;</span> _requireApply) { player.Apply(); _requireApply <span class="synStatement">=</span> <span class="synConstant">false</span>; } serializedObject.ApplyModifiedProperties(); <span class="synStatement">if</span> (_requireRepaint) { Repaint(); } <span class="synStatement">else</span> { EditorUtility.SetDirty(target); } <span class="synStatement">if</span> (isDragging) { player.Pause(); } <span class="synStatement">else</span> { player.UnPause(); } ... } ... <span class="synType">void</span> DrawClip() { <span class="synType">var</span> nextClip <span class="synStatement">=</span> (AudioClip)EditorGUILayout.ObjectField(<span class="synConstant">&quot;Clip&quot;</span>, player.clip, <span class="synStatement">typeof</span><span class="synType">(AudioClip)</span>, <span class="synConstant">true</span>); <span class="synStatement">if</span> (nextClip <span class="synStatement">==</span> player.clip) <span class="synStatement">return</span>; player.clip <span class="synStatement">=</span> nextClip; _requireApply <span class="synStatement">=</span> <span class="synConstant">true</span>; } <span class="synType">void</span> DrawPlayAndStop() { EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); <span class="synStatement">if</span> (GUILayout.Button(<span class="synConstant">&quot; Play &quot;</span>)) { AudioUtil.PlayClip(player.clip); } <span class="synStatement">if</span> (GUILayout.Button(<span class="synConstant">&quot; Stop &quot;</span>)) { AudioUtil.StopClip(player.clip); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Separator(); } <span class="synType">void</span> DrawWave() { <span class="synType">var</span> rect <span class="synStatement">=</span> EditorGUILayout.GetControlRect(GUILayout.Height(<span class="synConstant">100</span>)); EditorUtil.DrawBackgroundRect(rect); <span class="synStatement">if</span> (<span class="synStatement">!</span>player.clip) <span class="synStatement">return</span>; EditorUtil.DrawWave(rect, player.clip, <span class="synStatement">new</span> EditorUtil.DrawWaveOption()); <span class="synType">var</span> preWaveStart <span class="synStatement">=</span> player.start; <span class="synType">var</span> preWaveEnd <span class="synStatement">=</span> player.end; DrawTrimArea(rect, <span class="synConstant">true</span>, <span class="synStatement">ref</span> player.start, <span class="synStatement">ref</span> _isDraggingStart, <span class="synStatement">ref</span> _isDraggingEnd); DrawTrimArea(rect, <span class="synConstant">false</span>, <span class="synStatement">ref</span> player.end, <span class="synStatement">ref</span> _isDraggingEnd, <span class="synStatement">ref</span> _isDraggingStart); DrawCrossFadeArea(rect); player.start <span class="synStatement">=</span> Mathf.Clamp(player.start, <span class="synConstant">0f</span>, preWaveEnd <span class="synStatement">-</span> <span class="synConstant">0.001f</span>); player.end <span class="synStatement">=</span> Mathf.Clamp(player.end, preWaveStart <span class="synStatement">+</span> <span class="synConstant">0.001f</span>, <span class="synConstant">1f</span>); } <span class="synType">void</span> DrawTrimArea(Rect rect, <span class="synType">bool</span> isStart, <span class="synStatement">ref</span> <span class="synType">float</span> range, <span class="synStatement">ref</span> <span class="synType">bool</span> isDraggingSelf, <span class="synStatement">ref</span> <span class="synType">bool</span> isDraggingOther) { <span class="synType">var</span> trimArea <span class="synStatement">=</span> rect; <span class="synStatement">if</span> (isStart) { trimArea.width <span class="synStatement">*=</span> range; } <span class="synStatement">else</span> { trimArea.width <span class="synStatement">*=</span> <span class="synConstant">1f</span> <span class="synStatement">-</span> range; trimArea.x <span class="synStatement">+=</span> rect.width <span class="synStatement">-</span> trimArea.width; } EditorGUI.DrawRect(trimArea, <span class="synStatement">new</span> Color(<span class="synConstant">0f</span>, <span class="synConstant">0f</span>, <span class="synConstant">0f</span>, <span class="synConstant">0.7f</span>)); <span class="synType">var</span> deltaPixels <span class="synStatement">=</span> ProcessDrag(trimArea, <span class="synStatement">ref</span> isDraggingSelf, <span class="synStatement">ref</span> isDraggingOther); range <span class="synStatement">+=</span> deltaPixels <span class="synStatement">/</span> rect.width; <span class="synType">var</span> borderRect <span class="synStatement">=</span> trimArea; <span class="synStatement">if</span> (isStart) { borderRect.x <span class="synStatement">+=</span> trimArea.width; } borderRect.width <span class="synStatement">=</span> <span class="synConstant">1</span>; <span class="synType">var</span> borderColor <span class="synStatement">=</span> isDraggingSelf <span class="synStatement">?</span> <span class="synStatement">new</span> Color(<span class="synConstant">1f</span>, <span class="synConstant">0f</span>, <span class="synConstant">0f</span>, <span class="synConstant">1f</span>) <span class="synStatement">:</span> <span class="synStatement">new</span> Color(<span class="synConstant">1f</span>, <span class="synConstant">1f</span>, <span class="synConstant">1f</span>, <span class="synConstant">0.5f</span>); EditorGUI.DrawRect(borderRect, borderColor); } <span class="synType">float</span> ProcessDrag(Rect dragRect, <span class="synStatement">ref</span> <span class="synType">bool</span> isDraggingSelf, <span class="synStatement">ref</span> <span class="synType">bool</span> isDraggingOther) { <span class="synType">float</span> delta <span class="synStatement">=</span> <span class="synConstant">0f</span>; <span class="synType">var</span> mouseRect <span class="synStatement">=</span> dragRect; mouseRect.x <span class="synStatement">-=</span> <span class="synConstant">10</span>; mouseRect.width <span class="synStatement">+=</span> <span class="synConstant">20</span>; EditorGUIUtility.AddCursorRect(mouseRect, MouseCursor.SplitResizeLeftRight); <span class="synStatement">if</span> (<span class="synStatement">!</span>isDraggingOther <span class="synStatement">&amp;&amp;</span> Event.current.type <span class="synStatement">==</span> EventType.MouseDrag) { <span class="synStatement">if</span> (mouseRect.Contains(Event.current.mousePosition)) { isDraggingSelf <span class="synStatement">=</span> <span class="synConstant">true</span>; } <span class="synStatement">if</span> (isDraggingSelf) { delta <span class="synStatement">=</span> Event.current.delta.x; _requireApply <span class="synStatement">=</span> <span class="synConstant">true</span>; } _requireRepaint <span class="synStatement">=</span> <span class="synConstant">true</span>; } <span class="synStatement">else</span> <span class="synStatement">if</span> (Event.current.type <span class="synStatement">==</span> EventType.MouseUp) { isDraggingSelf <span class="synStatement">=</span> <span class="synConstant">false</span>; } <span class="synStatement">return</span> delta; } <span class="synType">void</span> DrawCrossFadeArea(Rect rect) { <span class="synType">var</span> range <span class="synStatement">=</span> player.crossFadeDuration <span class="synStatement">/</span> player.clip.length; range <span class="synStatement">=</span> Mathf.Min(range, player.end <span class="synStatement">-</span> player.start); rect.x <span class="synStatement">+=</span> (player.end <span class="synStatement">-</span> range) <span class="synStatement">*</span> rect.width; rect.width <span class="synStatement">*=</span> range; EditorGUI.DrawRect(rect, <span class="synStatement">new</span> Color(<span class="synConstant">0f</span>, <span class="synConstant">1f</span>, <span class="synConstant">1f</span>, <span class="synConstant">0.2f</span>)); } <span class="synType">void</span> DrawParameters() { <span class="synType">float</span> preDuration <span class="synStatement">=</span> player.crossFadeDuration; player.crossFadeDuration <span class="synStatement">=</span> EditorGUILayout.Slider(<span class="synConstant">&quot;Cross Fade&quot;</span>, preDuration, <span class="synConstant">0f</span>, <span class="synConstant">0.1f</span>); <span class="synStatement">if</span> (player.crossFadeDuration <span class="synStatement">!=</span> preDuration) { _requireApply <span class="synStatement">=</span> <span class="synConstant">true</span>; } } } </pre> <p>コードの大部分は UI の構築ですが、キモは <code>ProcessDrag()</code> です。<code>EditorGUIUtility.AddCursorRect()</code> では指定したエリアのカーソル形状を変更できます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FScriptReference%2FEditorGUIUtility.AddCursorRect.html" title="EditorGUIUtility-AddCursorRect - Unity スクリプトリファレンス" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/ScriptReference/EditorGUIUtility.AddCursorRect.html">docs.unity3d.com</a></cite></p> <p>この上で、<code>Event.current.type</code> が <code>EventType.MouseDrag</code> かどうかのチェックと、<code>Event.current.mousePosition</code> が対象の領域に含まれているかどうかを検証してドラッグ中かどうかの判定を行っています。結構、愚直な実装になっていて、もう少し抽象化された <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> があると良いのですが...、UI Toolkit の例などをみても結構ゴリッと書いてるので無いのかな、と考えています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FManual%2FUIE-create-drag-and-drop-ui.html" title="Unity - Manual: Create a drag-and-drop UI inside a custom Editor window" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Manual/UIE-create-drag-and-drop-ui.html">docs.unity3d.com</a></cite></p> <p>こうしてドラッグ出来るようにした上で <code>start</code> / <code>end</code> をランタイムコード側へ渡して実行すればトリミングした範囲のオーディオデータの再生が可能となりました。</p> <h2 id="おわりに">おわりに</h2> <p>最近少し忙しくネタが枯渇しているので、小さめの記事になりましたが...、来月からはまた新しい内容を色々書いていきたいです。</p> hecomi Unity の WebGL で動的にサウンドを生成・再生する方法を調べてみた hatenablog://entry/4207112889923134032 2022-09-30T01:41:15+09:00 2022-09-30T01:45:10+09:00 はじめに 通常、Unity で動的にサウンドを生成したいときは、AudioClip の Create で PCMReaderCallback を渡すか、MonoBehaviour の OnAudioFilterRead 中でバッファをセットすることで可能です。 docs.unity3d.com docs.unity3d.com かなり古い記事ですが、以前解説も書きました(一部有料機能と述べているところは現状はすべて無料です) tips.hecomi.com AudioClip に SetData して再生することも出来ますが、初期化時ではなく毎フレームデータを与えたい場合はこれは利用できません… <h2 id="はじめに">はじめに</h2> <p>通常、Unity で動的に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B5%A5%A6%A5%F3%A5%C9">サウンド</a>を生成したいときは、<code>AudioClip</code> の <code>Create</code> で <code>PCMReaderCallback</code> を渡すか、<code>MonoBehaviour</code> の <code>OnAudioFilterRead</code> 中でバッファをセットすることで可能です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FScriptReference%2FAudioClip.Create.html" title="AudioClip-Create - Unity スクリプトリファレンス" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/ScriptReference/AudioClip.Create.html">docs.unity3d.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FScriptReference%2FMonoBehaviour.OnAudioFilterRead.html" title="MonoBehaviour-OnAudioFilterRead(float[], int) - Unity スクリプトリファレンス" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/ScriptReference/MonoBehaviour.OnAudioFilterRead.html">docs.unity3d.com</a></cite></p> <p>かなり古い記事ですが、以前解説も書きました(一部有料機能と述べているところは現状はすべて無料です)</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2014%2F11%2F11%2F021147" title="Unity のオーディオの再生・エフェクト・解析周りについてまとめてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2014/11/11/021147">tips.hecomi.com</a></cite></p> <p><code>AudioClip</code> に <code>SetData</code> して再生することも出来ますが、初期化時ではなく毎フレームデータを与えたい場合はこれは利用できません。正確にはセットして再生はできるのですが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B5%A5%A6%A5%F3%A5%C9">サウンド</a>の読み取り・再生自体は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B5%A5%A6%A5%F3%A5%C9">サウンド</a>のスレッドで行われているため、<code>Update</code> などのメインスレッドから与えるとうまく再生されない(途切れたりする)からです。上記の機能を利用することでうまく再生できるようになります。</p> <p>しかしながら <a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> 書き出しの場合、<a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> のスレッドの制約があるからか、これらの機能は残念ながら利用できません。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fwebgl-audio.html" title="Audio in WebGL - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/webgl-audio.html">docs.unity3d.com</a></cite></p> <blockquote><p>Unity <a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> partially supports AudioClip.Create. Browsers don’t support dynamic streaming, so to use AudioClip.Create, set the Stream to false.</p></blockquote> <p>本記事では、それでも <a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ビルドで動的に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B5%A5%A6%A5%F3%A5%C9">サウンド</a>を生成・再生したい人向けの記事になります。</p> <h2 id="サンプル">サンプル</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FUnityWebGLAudioStream" title="GitHub - hecomi/UnityWebGLAudioStream: A simple sample that uses the Web Audio API to play float[] sent from Unity in a WebGL build" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/UnityWebGLAudioStream">github.com</a></cite></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220930/20220930012301.png" width="1200" height="814" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Start を押すと声が再生され、ゲージをスライドするとボリュームが変わります。内部的には毎フレーム Unity 側から jslib を経由してデータを渡し、それを Web Audio <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> で再生しています。</p> <h2 id="構成の概要">構成の概要</h2> <p>再生には Web Audio <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> を使います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fja%2Fdocs%2FWeb%2FAPI%2FWeb_Audio_API" title="Web Audio API - Web API | MDN" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API">developer.mozilla.org</a></cite></p> <p>本記事では詳しい解説はしませんが...、シンプルな audio タグによる音声再生とは異なり、音を生成したり、エフェクトを掛けたり...、といった柔軟な操作が可能な <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> 体系です。</p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> と Unity の橋渡しには jslib の仕組みを利用します。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fwebgl-interactingwithbrowserscripting.html" title="Interaction with browser scripting - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/webgl-interactingwithbrowserscripting.html">docs.unity3d.com</a></cite></p> <p>これをもとに次のようなサンプルを作ってみます。</p> <ul> <li>Unity から jslib 経由で毎フレーム <code>float[]</code> を <a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> 側へ送る</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> 側(jslib 内部)で Web Audio <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> を使い送られてきた <code>float[]</code> バッファを再生</li> <li>送るバッファはとりあえずは <code>AudioClip</code> を事前処理して <code>float[]</code> にしたものを小分けにして送る</li> </ul> <p>プロジェクト構成は以下のようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220929/20220929232947.png" width="682" height="674" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="具体的な処理">具体的な処理</h2> <h3 id="jslib">jslib</h3> <p>次のように、初期化関数(<code>Init</code>)と再生関数(<code>Play</code>)を用意しておきます。おまけでボリューム変更(<code>SetVolume</code>)も用意しています。</p> <pre class="code js" data-lang="js" data-unlink>const AudioStreamPlugin = { ctx: null, sampleRate: 44100, initDelay: 0.5, scheduledTime: 0.0, volume: 1.0, Init: function(sampleRate, initDelay) { if (this.ctx) return; this.ctx = new window.AudioContext(); this.sampleRate = sampleRate; this.initDelay = initDelay; console.log(&#34;Init: &#34;, sampleRate, initDelay); }, Play: function(offset, size) { if (!this.ctx) return; const arrayF32 = new Float32Array(size); for (let i = 0; i &lt; size; ++i) { arrayF32[i] = HEAPF32[(offset &gt;&gt; 2) + i]; } const gain = this.ctx.createGain(); if (isFinite(this.volume)) { gain.gain.value = this.volume; } gain.connect(this.ctx.destination); const buf = this.ctx.createBuffer(1, size, sampleRate); buf.getChannelData(0).set(arrayF32); const src = this.ctx.createBufferSource(); src.buffer = buf; src.connect(gain); const t = this.ctx.currentTime; if (t &lt; this.scheduledTime) { this.scheduledTime += buf.duration; } else { this.scheduledTime = t + buf.duration + initDelay; } src.start(this.scheduledTime); console.log(&#34;Play&#34;, this.scheduledTime); }, SetVolume: function(volume) { this.volume = volume; } }; mergeInto(LibraryManager.library, AudioStreamPlugin);</pre> <p>Web Audio <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> の流れとしては、まずは初期化時にコンテキストを生成しておきます。<code>Play()</code> は Unity からは <code>float[]</code> を渡すのですが、こちらでは <code>offset</code> が渡ってきて、それを使って <code>HEAPF32</code> から取り出す形になります。これを <code>createBufferSource()</code> して作成したバッファにセットし、ソースを作成します。Web Audio <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> は <code>connect</code> でノードをつないでいく流れになるので、ソース → ゲイン(ボリューム調整)→ 出力、とつないで、最後にソースをスタートすることで再生が出来ます。再生は毎フレーム、チャンクごとに行い、それぞれのバッファの大きさを見て再生時間を計算、<code>scheduledTime</code> でディレイを計算して遅延再生することで、それぞれのチャンクの再生がいい感じに繋がります。このとき、全体を少し遅らせて(<code>initDelay</code>)再生することでバッファ時間を確保しています。これによって少し音声の送信にズレが生じても、その分を吸収してプチプチ途切れず再生できます。</p> <p>こちらのソースがとても参考になりました。</p> <ul> <li><a href="https://gist.github.com/ykst/6e80e3566bd6b9d63d19">WebAudio+WebSocket&#x3067;&#x30D6;&#x30E9;&#x30A6;&#x30B6;&#x3078;&#x306E;&#x97F3;&#x58F0;&#x30EA;&#x30A2;&#x30EB;&#x30BF;&#x30A4;&#x30E0;&#x30B9;&#x30C8;&#x30EA;&#x30FC;&#x30DF;&#x30F3;&#x30B0;&#x3092;&#x5B9F;&#x88C5;&#x3059;&#x308B; &middot; GitHub</a></li> </ul> <h3 id="Unity">Unity</h3> <p>Unity 側では、この jslib を使う関数を用意します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> System.Runtime.InteropServices; <span class="synType">namespace</span> WebGLAudioStream { <span class="synType">static</span> <span class="synType">public</span> <span class="synType">class</span> <span class="synType">Lib</span> { <span class="synPreProc">#if UNITY_EDITOR</span> <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> Init(<span class="synType">int</span> sampleRate, <span class="synType">float</span> initDelay) { Debug.Log(<span class="synConstant">$&quot;Init(</span><span class="synSpecial">{</span>sampleRate<span class="synSpecial">}</span><span class="synConstant">, </span><span class="synSpecial">{</span>initDelay<span class="synSpecial">}</span><span class="synConstant">)&quot;</span>); } <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> Play(<span class="synType">float</span>[] array, <span class="synType">int</span> size) { Debug.Log(<span class="synConstant">$&quot;Play(</span><span class="synSpecial">{</span>array<span class="synSpecial">}</span><span class="synConstant">, </span><span class="synSpecial">{</span>size<span class="synSpecial">}</span><span class="synConstant">)&quot;</span>); } <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> SetVolume(<span class="synType">float</span> volume) { Debug.Log(<span class="synConstant">$&quot;SetVolume(</span><span class="synSpecial">{</span>volume<span class="synSpecial">}</span><span class="synConstant">)&quot;</span>); } <span class="synPreProc">#else</span> [DllImport(<span class="synConstant">&quot;__Internal&quot;</span>)] <span class="synType">public</span> <span class="synType">static</span> <span class="synType">extern</span> <span class="synType">void</span> Init(<span class="synType">int</span> sampleRate, <span class="synType">float</span> initDelay); [DllImport(<span class="synConstant">&quot;__Internal&quot;</span>)] <span class="synType">public</span> <span class="synType">static</span> <span class="synType">extern</span> <span class="synType">void</span> Play(<span class="synType">float</span>[] array, <span class="synType">int</span> size); [DllImport(<span class="synConstant">&quot;__Internal&quot;</span>)] <span class="synType">public</span> <span class="synType">static</span> <span class="synType">extern</span> <span class="synType">void</span> SetVolume(<span class="synType">float</span> volume); <span class="synPreProc">#endif</span> } } </pre> <p><code>[DllImport("__Internal")]</code> で <a class="keyword" href="http://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> 側の関数をそのまま呼び出すことが出来ます。さて、これを次のように利用します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> System; <span class="synType">namespace</span> WebGLAudioStream { <span class="synType">public</span> <span class="synType">class</span> <span class="synType">AudioBufferSender </span><span class="synStatement">:</span> MonoBehaviour { <span class="synType">public</span> AudioData audioData; <span class="synType">public</span> <span class="synType">float</span> initDelay <span class="synStatement">=</span> <span class="synConstant">0.5f</span>; <span class="synType">public</span> <span class="synType">int</span> chunkSize <span class="synStatement">=</span> <span class="synConstant">2048</span>; <span class="synType">float</span>[] _buf <span class="synStatement">=</span> <span class="synConstant">null</span>; <span class="synType">int</span> _clipPos <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synType">int</span> _bufPos <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synType">public</span> <span class="synType">void</span> Init() { <span class="synStatement">if</span> (audioData <span class="synStatement">==</span> <span class="synConstant">null</span> <span class="synStatement">||</span> <span class="synStatement">!</span>audioData.clip) <span class="synStatement">return</span>; _buf <span class="synStatement">=</span> <span class="synStatement">new</span> float[chunkSize]; Lib.Init(audioData.clip.frequency, initDelay); } <span class="synType">public</span> <span class="synType">void</span> SetVolume(<span class="synType">float</span> volume) { Lib.SetVolume(volume); } <span class="synType">void</span> Update() { <span class="synStatement">if</span> (audioData <span class="synStatement">==</span> <span class="synConstant">null</span>) <span class="synStatement">return</span>; <span class="synType">var</span> clip <span class="synStatement">=</span> audioData.clip; <span class="synStatement">if</span> (clip <span class="synStatement">==</span> <span class="synConstant">null</span>) <span class="synStatement">return</span>; <span class="synStatement">if</span> (_buf <span class="synStatement">==</span> <span class="synConstant">null</span>) <span class="synStatement">return</span>; <span class="synType">int</span> n <span class="synStatement">=</span> (<span class="synType">int</span>)(clip.frequency <span class="synStatement">*</span> Time.unscaledDeltaTime); <span class="synStatement">if</span> (n <span class="synStatement">&gt;</span> clip.samples) <span class="synStatement">return</span>; <span class="synStatement">if</span> (_clipPos <span class="synStatement">+</span> n <span class="synStatement">&gt;=</span> clip.samples) n <span class="synStatement">=</span> clip.samples <span class="synStatement">-</span> _clipPos; <span class="synType">var</span> data <span class="synStatement">=</span> <span class="synStatement">new</span> float[n]; System.Array.Copy(audioData.data, _clipPos, data, <span class="synConstant">0</span>, n); _clipPos <span class="synStatement">+=</span> n; <span class="synStatement">if</span> (_clipPos <span class="synStatement">&gt;=</span> clip.samples) _clipPos <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synType">var</span> len <span class="synStatement">=</span> n; <span class="synStatement">if</span> (_bufPos <span class="synStatement">+</span> len <span class="synStatement">&gt;=</span> chunkSize) len <span class="synStatement">=</span> chunkSize <span class="synStatement">-</span> _bufPos; Array.Copy(data, <span class="synConstant">0</span>, _buf, _bufPos, len); _bufPos <span class="synStatement">+=</span> len; <span class="synStatement">if</span> (_bufPos <span class="synStatement">&gt;=</span> chunkSize) { Lib.Play(_buf, chunkSize); Array.Clear(_buf, <span class="synConstant">0</span>, chunkSize); _bufPos <span class="synStatement">=</span> <span class="synConstant">0</span>; <span class="synType">var</span> rest <span class="synStatement">=</span> n <span class="synStatement">-</span> len; <span class="synStatement">if</span> (rest <span class="synStatement">&gt;</span> <span class="synConstant">0</span>) { Array.Copy(data, len <span class="synStatement">-</span> <span class="synConstant">1</span>, _buf, <span class="synConstant">0</span>, rest); _bufPos <span class="synStatement">+=</span> rest; } } } } } </pre> <p><code>AudioData</code> から 1 フレーム分のオーディオのバッファを取り出し、これを <code>float[]</code> につめて <code>Play()</code> を呼んでいます(ループ再生するようにしています)。ここでいう <code>AudioData</code> は予め <code>AudioClip</code> から <code>GetData()</code> して <code>float[]</code> を格納しておいた <code>ScriptableObject</code> です。<a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> では <code>AudioClip.GetData()</code> が出来なかったのでサンプルのために少し回りくどいことをしています。実際は数式を書いて生成したり、ストリーミングで渡ってきたデータを入れたり、用途に応じて変更してください。なお、<code>AudioData</code> は次のような感じです。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synType">namespace</span> WebGLAudioStream { [CreateAssetMenu(menuName <span class="synStatement">=</span> <span class="synConstant">&quot;AudioData&quot;</span>)] <span class="synType">public</span> <span class="synType">class</span> <span class="synType">AudioData </span><span class="synStatement">:</span> ScriptableObject { <span class="synType">public</span> AudioClip clip; <span class="synType">public</span> <span class="synType">float</span>[] data; [ContextMenu(<span class="synConstant">&quot;Decode&quot;</span>)] <span class="synType">public</span> <span class="synType">void</span> Decode() { <span class="synStatement">if</span> (<span class="synStatement">!</span>clip) <span class="synStatement">return</span>; data <span class="synStatement">=</span> <span class="synStatement">new</span> float[clip.samples]; clip.GetData(data, <span class="synConstant">0</span>); } } } </pre> <h2 id="おわりに">おわりに</h2> <p>Unity 5 のときに頑張って <code>Application.ExternalCall()</code> したり、長い <a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ビルドを待ったり、といった時以来に <a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> ビルドを触りましたが、現在はビルドも早く、jslib も作成が簡単でかなり開発しやすくなっていました。昔の記事はこちら(なんと執筆時から約 8 年...):</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2014%2F12%2F08%2F002719" title="Unity 5 x WebGL について詳しく調べてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2014/12/08/002719">tips.hecomi.com</a></cite></p> <p>また、Web 周りも Web Audio <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> に限らず便利な <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> が色々とありますので、アイディア次第で面白いものが作れそうですね。例えば Web <a class="keyword" href="http://d.hatena.ne.jp/keyword/Bluetooth">Bluetooth</a> で接続したデ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>の情報を Unity ベースで作成した <a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> でグリグリ動かす、とかも比較的簡単にできそうです。</p> hecomi uLipSync で 2D キャラクタ向けにテクスチャや UV を変更できるコンポーネントを作った hatenablog://entry/4207112889913474353 2022-08-31T23:25:53+09:00 2022-09-01T02:28:25+09:00 はじめに Unity でリアルタイム / 事前解析対応のリップシンクを行うためのプラグインである uLipSync ですが、これまでは付属スクリプトとしては BlendShape を操作するものしかありませんでした。テクスチャを使った立ち絵などをリップシンクさせたい場合は、自分で Custom Event の仕組みを使って作る必要がありました。 github.com しかしながらこういったユースケースへの要望を時折見かけたので、テクスチャ更新向けのスクリプトを作成することにしました。 なお、uLipSync の基本的な使い方については以下のエントリをご参照ください。 tips.hecomi.c… <h2 id="はじめに">はじめに</h2> <p><strong>Unity</strong> でリアルタイム / 事前解析対応の<strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a></strong>を行うための<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>である <strong>uLipSync</strong> ですが、これまでは付属<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>としては BlendShape を操作するものしかありませんでした。テクスチャを使った立ち絵などを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>させたい場合は、自分で Custom Event の仕組みを使って作る必要がありました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync%2Ftree%2Fb766c92d3d3637ee395fe2ab56db027d7660da88%23custom-event" title="GitHub - hecomi/uLipSync at b766c92d3d3637ee395fe2ab56db027d7660da88" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync/tree/b766c92d3d3637ee395fe2ab56db027d7660da88#custom-event">github.com</a></cite></p> <p>しかしながらこういった<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>への要望を時折見かけたので、テクスチャ更新向けの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を作成することにしました。</p> <p>なお、uLipSync の基本的な使い方については以下のエントリをご参照ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F01%2F30%2F152519" title="uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/01/30/152519">tips.hecomi.com</a></cite></p> <h2 id="コード">コード</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync" title="GitHub - hecomi/uLipSync: A MFCC-based LipSync plugin for Unity using Burst Compiler" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync">github.com</a></cite></p> <p>執筆時点では v2.4.0 が最新となります。Sprite の対応やタイルテクスチャを使った UV のためにアニメーション作成機能も追々作成していきます。</p> <h2 id="デモ">デモ</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220831005326" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220831/20220831005326.gif" width="715" height="368" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>こんにちは、と喋っています。</p> <h2 id="使い方">使い方</h2> <p><code>08. Texture</code> に含まれているシーンがサンプルになります。<code>uLipSyncTexture</code> という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が <code>Body &gt; Mouth</code> にアタッチされています。</p> <h5 id="テクスチャを差し替えるパターン">テクスチャを差し替えるパターン</h5> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220901/20220901022631.png" width="957" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h5 id="タイルテクスチャで-UV-を変更するパターン">タイルテクスチャで UV を変更するパターン</h5> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220901/20220901022721.png" width="966" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このようにテクスチャ差し替え及び UV 変更それぞれに対して、割り当てたいテクスチャを画像で確認しながらセットできます(ハイブリッドも出来ます)。</p> <ul> <li>Update Method <ul> <li>更新を呼ぶタイミングです。<code>Late Update</code> や <code>Update</code>、また LipSync の解析が終わったタイミング(<code>Lip Sync Update Event</code>) と言ったようないくつかのタイミングの中から選ぶことが出来ます。アニメーションと併用しないのであれば基本的には <code>Update</code> で問題ないと思います。</li> </ul> </li> <li>Renderer <ul> <li>更新したい対象の <code>Renderer</code> を指定します。内部的にはマテリアルが実行時に複製され、メインテクスチャが更新されます。</li> </ul> </li> <li>Parameters <ul> <li>Min Volume <ul> <li>更新する最低のボリューム値(Log10)です。</li> </ul> </li> <li>Min Duration <ul> <li>この時間は少なくとも同じ口の形をキープします</li> </ul> </li> </ul> </li> <li>Textures <ul> <li>ここで割り当てたいテクスチャの選択をします</li> <li>Phoneme -<code>Profile</code> に対応する音素(e.g. 「A」「I」)を記入します。 <ul> <li>空文字列("")の場合は音声入力がないときの設定として扱われます。</li> </ul> </li> <li>Texture <ul> <li>変更したいテクスチャを指定します</li> <li>無指定の場合はマテリアルにセットされている初期テクスチャが利用されます</li> </ul> </li> <li>UV Scale <ul> <li>UV のスケールです。タイルテクスチャの場合は指定してください。</li> </ul> </li> <li>UV Offset <ul> <li>UV のオフセットです。タイルテクスチャの場合は指定してください。</li> </ul> </li> </ul> </li> </ul> <h2 id="おわりに">おわりに</h2> <p>その他、Animator 更新機能のプルリクも頂いたので、引き続き機能を拡充していきたいと思います。</p> hecomi uRaymarching の Depth Prepass 対応をした hatenablog://entry/4207112889904323852 2022-07-30T23:07:28+09:00 2022-08-11T11:50:55+09:00 はじめに 以前の記事で uRaymarching の URP における Depth Prepass 問題を挙げました。 tips.hecomi.com Depth Prepass が有効になっている際は、Depth は事前に DepthOnly パス(または DepthNormals パス)で出力され、実際に色を決定する ForwardLit パスでは ZTest Equal かつ ZWrite Off で描画されます。最終的に他のポリゴンで覆われてしまうようなピクセルの重い計算を省略できるメリットがあります。詳しくは公式動画をご覧ください。 logmi.jp 通常は頂点シェーダから渡ってきた… <h2 id="はじめに">はじめに</h2> <p>以前の記事で <strong>uRaymarching</strong> の URP における <strong>Depth Prepass</strong> 問題を挙げました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F05%2F31%2F003815" title="uRaymarching の URP 向け機能更新をしてみた(Deferred / SSAO / Decal / Clear Coat) - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/05/31/003815">tips.hecomi.com</a></cite></p> <p>Depth Prepass が有効になっている際は、Depth は事前に <code>DepthOnly</code> パス(または <code>DepthNormals</code> パス)で出力され、実際に色を決定する <code>ForwardLit</code> パスでは <code>ZTest Equal</code> かつ <code>ZWrite Off</code> で描画されます。最終的に他のポリゴンで覆われてしまうような<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>の重い計算を省略できるメリットがあります。詳しくは公式動画をご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Flogmi.jp%2Ftech%2Farticles%2F326449" title="「Depth Prepass」「Deferred Rendering」「Forward+」 URP 12.xのアプデで充実した、レンダリング方式のオプション " class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://logmi.jp/tech/articles/326449">logmi.jp</a></cite></p> <p>通常は頂点シェーダから渡ってきた情報から自動的にデプスが計算され <code>ZTest Equal</code> が成功しますが、uRaymarching では <code>SV_Depth</code> セマンティクスを使ってこのデプスをいじっています。この結果、計算結果のデプスがこの事前のパスと色決定のパスで少しずれてしまう関係で、何もその<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>に結果的に書き込まれない形になり、クリアした結果がそのまま出力されガビガビになってしまうという状況になっていました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220730215028" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220730/20220730215028.png" width="1198" height="704" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>これに対して <a href="https://twitter.com/lil_xyzw">lil &#x3055;&#x3093;</a>より以下のコメントを貰いました。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">SV_Depth問題、雑に対処するならForwardパスで<br><br>float cameradepth = _CameraDepthTexture[uint2(input.positionCS.xy)].r;<br>o.depth = abs(ray.depth - cameradepth) &lt; 1e-4 ? cameradepth : ray.depth;<br><br>みたいな感じになるのかな……?(<a class="keyword" href="http://d.hatena.ne.jp/keyword/VR">VR</a>対応させるならもうちょっとちゃんと書かないといけない)</p>&mdash; lil (@lil_xyzw) <a href="https://twitter.com/lil_xyzw/status/1531342628029362176?ref_src=twsrc%5Etfw">2022年5月30日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>今回はこちらを試してみます。</p> <h2 id="対応">対応</h2> <p>uRaymarching は uShaderTemplate というシェーダジェネレータを使っています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2017%2F10%2F24%2F014057" title="Unity のシェーダをテンプレートファイルから生成するエディタ拡張をつくってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2017/10/24/014057">tips.hecomi.com</a></cite></p> <p>まず、この uShaderTemplate 向けに以下の行を追加しました。</p> <pre class="code lang-c" data-lang="c" data-unlink>@<span class="synStatement">if</span> CheckDepthPrepass : <span class="synConstant">false</span> <span class="synPreProc">#define CHECK_DEPTH_PREPASS</span> @endif </pre> <p>これにより、Conditions セクションに <em>Check Depth Prepass</em> を追加できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220730215539" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220730/20220730215539.png" width="1168" height="650" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>殆どの方は uShaderTemplate とは...?だと思いますが、ここでの目的としては <code>CHECK_DEPTH_PREPASS</code> というマクロの有無を見て、チェックが必要なときは追加の計算を行いたく、そのためのフラグの ON/OFF をエディタに追加したいというものです(本当は組み込みのマクロが提供されていると良いのですが見当たらず...)。</p> <p>そして ForwardLit パスでこのマクロを見るように以下のように書き換えます。</p> <pre class="code lang-c" data-lang="c" data-unlink>FragOutput <span class="synIdentifier">Frag</span>(Varyings input) { ... RaymarchInfo ray; <span class="synIdentifier">INITIALIZE_RAYMARCH_INFO</span>(ray, input, _Loop, _MinDistance); <span class="synIdentifier">Raymarch</span>(ray); ... FragOutput o; o.color = color; <span class="synPreProc">#ifdef CHECK_DEPTH_PREPASS</span> float2 uv = input.positionSS.xy / input.positionSS.w; <span class="synType">float</span> depth = <span class="synIdentifier">SAMPLE_DEPTH_TEXTURE</span>( _CameraDepthTexture, sampler_CameraDepthTexture, uv); o.depth = <span class="synIdentifier">abs</span>(ray.depth - depth) &lt; <span class="synConstant">1e-4</span> ? depth : ray.depth; <span class="synPreProc">#else</span> o.depth = ray.depth; <span class="synPreProc">#endif</span> <span class="synStatement">return</span> o; } </pre> <p>マクロが定義されている場合は、スクリーンスペースの UV を求めて <code>_CameraDepthTexture</code> から事前パスで書き込まれたデプスを取得、レイマーチングのデプスとそれを比べて差が小さい場合は現在書き込まれているデプスをそのまま採用、という流れです。これで <em>Check Depth Prepass</em> をチェックして再<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>すればデプスが一致してガビガビがなくなりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220730220709" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220730/20220730220709.png" width="1182" height="730" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h2 id="問題点">問題点</h2> <p>しかしながらカメラを近づけると次のようにまたガビガビになってしまいます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220730222111" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220730/20220730222111.gif" width="715" height="368" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>これはデプスは近い場所ほど精度が良くなるように書き込む値が<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C8%F3%C0%FE%B7%C1">非線形</a>になっているからで、ワールド空間で同じズレだったとしても、カメラに近い際はデプスバッファ上のズレが大きくなります(これによって高い精度を保てているわけです)。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.nvidia.com%2Fcontent%2Fdepth-precision-visualized" title="Depth Precision Visualized" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://developer.nvidia.com/content/depth-precision-visualized">developer.nvidia.com</a></cite></p> <p>先程固定値で <code>1e-4</code> として比較していた値よりも上回るケースが出てきて、そこで深度テストが一致せずガビガビになってしまったというわけですね。</p> <p>これに対しても <a href="https://twitter.com/lil_xyzw">lil &#x3055;&#x3093;</a>が考察していらっしゃいました。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">近づいたときに誤差が大きくなるので、<br><br>o.depth = abs(ray.depth - cameradepth) &lt; saturate(ray.depth * ray.depth) ? cameradepth : ray.depth;<br><br>が良さそうな感じがした</p>&mdash; lil (@lil_xyzw) <a href="https://twitter.com/lil_xyzw/status/1531345651661144065?ref_src=twsrc%5Etfw">2022年5月30日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>以下のようにコードを変更します。</p> <pre class="code lang-c" data-lang="c" data-unlink>FragOutput <span class="synIdentifier">Frag</span>(Varyings input) { ... <span class="synPreProc">#ifdef CHECK_DEPTH_PREPASS</span> ... <span class="synType">float</span> delta = <span class="synIdentifier">saturate</span>(ray.depth * ray.depth); o.depth = <span class="synIdentifier">abs</span>(ray.depth - depth) &lt; delta ? depth : ray.depth; <span class="synPreProc">#else</span> .. <span class="synPreProc">#endif</span> ... } </pre> <p>差分は固定値ではなく深度値の 2 乗を利用しています。これによりカメラに近いときは大きい値、遠いときは小さい値となり深度テストがクリアされるようになりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220730224237" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220730/20220730224237.gif" width="715" height="368" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <p>これで Depth Prepass を ON にしていても SSAO なども掛かるようになります(前回の記事だと Transparent 推奨だったので掛かりませんでした)。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220730225308" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220730/20220730225308.png" width="1184" height="716" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h2 id="おわりに">おわりに</h2> <p>レイマーチングは基本的にはループが重く、結局事前のパスでそれが回っており、DepthPrepass で計算が軽くなる、ということはあまり期待できないかもしれないですが...、覚えておくのには面白いテクニックだと思います。<a class="keyword" href="http://d.hatena.ne.jp/keyword/VR">VR</a> 対応はまた後ほどテストして対応します。</p> hecomi uLipSync の Timeline エディタのパフォーマンス改善を行ってみた hatenablog://entry/13574176438102513897 2022-06-30T00:52:51+09:00 2022-06-30T02:23:13+09:00 はじめに 先日、uLipSync のタイムラインの機能で長いデータを割り当てるとパフォーマンスが低下する、とのコメントを頂きました。実際に 3 分程のクリップを割り当ててみると顕著にパフォーマンスが落ちています。 原因を調べてみると、どうやら波形描画に用いている AudioCurveRendering の実行が重いようです。これはアンチエイリアスつきの波形描画を行ってくれるものなのですが、テクスチャを返す API ではなく、指定した領域(Rect)に波形画像を描画するものになります。そのためキャッシュもできず、描画更新が起こるタイミングで毎回生成処理が走ってしまいます。 そこで、今回はこのテク… <h2>はじめに</h2> <p>先日、<strong><a href="https://github.com/hecomi/uLipSync">uLipSync</a></strong> のタイムラインの機能で長いデータを割り当てるとパフォーマンスが低下する、とのコメントを頂きました。実際に 3 分程のクリップを割り当ててみると顕著にパフォーマンスが落ちています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220615/20220615014959.png" width="1200" height="950" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>原因を調べてみると、どうやら波形描画に用いている <code>AudioCurveRendering</code> の実行が重いようです。これは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%F3%A5%C1%A5%A8%A5%A4%A5%EA%A5%A2%A5%B9">アンチエイリアス</a>つきの波形描画を行ってくれるものなのですが、テクスチャを返す <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> ではなく、指定した領域(<code>Rect</code>)に波形画像を描画するものになります。そのためキャッシュもできず、描画更新が起こるタイミングで毎回生成処理が走ってしまいます。</p> <p>そこで、今回はこのテクスチャを自前で生成することで処理の改善を試みてみました。愚直な <code>Texture2D.SetPixels()</code> を使う方式から <code>Texture2D.GetPixelData()</code> の利用、そして Job + Burst 化まで行います。本エントリは解説というよりは作業の備忘録になります。</p> <h2>ダウンロード</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync" title="GitHub - hecomi/uLipSync: A MFCC-based LipSync plugin for Unity using Burst Compiler" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync">github.com</a></cite></p> <h2>問題点</h2> <p>以前はこんな感じで Timeline のエディタ描画の更新があるたびに <code>AudioUtil.GetMinMaxData()</code> および <code>AudioCurveRendering.DrawMinMaxFilledCurve()</code> が呼ばれていました。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>CustomTimelineEditor(<span class="synStatement">typeof</span>(uLipSyncClip))] <span class="synType">public</span> <span class="synType">class</span> uLipSyncClipTimelineEditor : ClipEditor { ... <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> DrawBackground(TimelineClip clip, ClipBackgroundRegion region) { var ls = clip.asset <span class="synStatement">as</span> uLipSyncClip; var data = ls.bakedData; ... EditorUtil.DrawWave(rect, data.audioClip, <span class="synStatement">new</span> EditorUtil.DrawWaveOption() { <span class="synComment">// ... 色を付ける処理</span> }); } } <span class="synType">public</span> <span class="synType">static</span> <span class="synType">class</span> EditorUtil { ... <span class="synType">public</span> <span class="synType">class</span> DrawWaveOption { <span class="synType">public</span> System.Func&lt;<span class="synType">float</span>, Color&gt; colorFunc; <span class="synType">public</span> <span class="synType">float</span> waveScale; } <span class="synType">public</span> <span class="synType">static</span> <span class="synType">void</span> DrawWave(Rect rect, AudioClip clip, DrawWaveOption option) { ... <span class="synComment">// 波形取得</span> var minMaxData = AudioUtil.GetMinMaxData(clip); ... AudioCurveRendering.AudioMinMaxCurveAndColorEvaluator dlg = <span class="synType">delegate</span>( <span class="synType">float</span> x, <span class="synStatement">out</span> Color col, <span class="synStatement">out</span> <span class="synType">float</span> minValue, <span class="synStatement">out</span> <span class="synType">float</span> maxValue) { col = option.colorFunc(x); ... minValue = ...; maxValue = ...; ... }; <span class="synComment">// 描画処理</span> AudioCurveRendering.DrawMinMaxFilledCurve(rect, dlg); } } </pre> <p>描画の更新は例えば Timeline 再生中などは毎フレーム行われています。また内部では <code>uLipSync.BakedData</code> を参照してそれぞれの時間にどのような音素があるかによって色付けをしています。これが結構重く、3 分程度の歌のようなデータを描画している場合は私の環境では 20 <a class="keyword" href="http://d.hatena.ne.jp/keyword/fps">fps</a> 程度までパフォーマンスが低下してしまっていました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FScriptReference%2FAudioCurveRendering.DrawMinMaxFilledCurve.html" title="AudioCurveRendering-DrawMinMaxFilledCurve - Unity スクリプトリファレンス" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/ScriptReference/AudioCurveRendering.DrawMinMaxFilledCurve.html">docs.unity3d.com</a></cite></p> <p>このように Timeline など毎フレーム更新しうる場所に表示する項目の計算が重い場合は他にも色々ありそうですね。</p> <h2>対処法</h2> <p>以前、Timeline の装飾の記事でも書いたのですが、生成したテクスチャをキャッシュすることでパフォーマンスを改善することができます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F03%2F28%2F235336" title="Unity で Timeline のカスタムトラックおよびクリップを作成して見た目をキレイにしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/03/28/235336">tips.hecomi.com</a></cite></p> <p>要は先程は毎回テクスチャ生成処理が走ってしまっていたのですが、これを変更がない限り使い回す、というものです。タイムラインのズーム率が変わって横幅が変わったり、データが変更されて波形が変わるタイミングでテクスチャを再生成するようにしておきます。</p> <p>ランタイムでももしかしたら使う機会があるかもしれないと思い、<code>Texture2D</code> の生成関数を <code>BakedData</code> に次のように追加してみました。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>[System.Serializable] <span class="synType">public</span> <span class="synType">struct</span> BakedFrame { <span class="synType">public</span> <span class="synType">float</span> volume; <span class="synType">public</span> List&lt;BakedPhonemeRatio&gt; phonemes; ... } ... <span class="synType">public</span> <span class="synType">class</span> BakedData : ScriptableObject { <span class="synType">public</span> BakedFrame GetFrame(<span class="synType">float</span> t) { ... } ... <span class="synType">public</span> <span class="synType">static</span> Color[] phonemeColors = <span class="synStatement">new</span> Color[] { Color.red, Color.cyan, Color.yellow, Color.magenta, Color.green, Color.blue, Color.gray, }; <span class="synType">public</span> Texture2D CreateTexture(<span class="synType">int</span> width, <span class="synType">int</span> height) { ... var colors = <span class="synStatement">new</span> Color[width * height]; var currentColor = <span class="synStatement">new</span> Color(); var smooth = <span class="synConstant">0.15f</span>; <span class="synStatement">for</span> (<span class="synType">int</span> x = <span class="synConstant">0</span>; x &lt; width; ++x) { var t = (<span class="synType">float</span>)x / width * duration; var frame = GetFrame(t); var targetColor = <span class="synStatement">new</span> Color(); <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i &lt; frame.phonemes.Count; ++i) { var colorIndex = i % phonemeColors.Length; targetColor += phonemeColors[colorIndex] * frame.phonemes[i].ratio; } currentColor += (targetColor - currentColor) * smooth; <span class="synStatement">for</span> (<span class="synType">int</span> y = <span class="synConstant">0</span>; y &lt; height; ++y) { var index = width * y + x; var color = currentColor; var dy = ((<span class="synType">float</span>)y - height / <span class="synConstant">2f</span>) / (height / <span class="synConstant">2f</span>); dy = Mathf.Abs(dy); dy = Mathf.Pow(dy, <span class="synConstant">2f</span>); color.a = dy &gt; frame.volume ? <span class="synConstant">0f</span> : <span class="synConstant">1f</span>; colors[index] = color; } } var tex = <span class="synStatement">new</span> Texture2D(width, height); tex.SetPixels(colors); tex.Apply(); <span class="synStatement">return</span> tex; } } </pre> <p>まずはテクスチャ生成は愚直な <code>Color[]</code> 配列を <code>Texture2D.SetPixels()</code> する方式で書いてみました。テクスチャは音量で適当に上下を大きくして音素で色付けするコードが書いてあります。</p> <p>この生成したテクスチャをキャッシュしながら表示する場所は <code>ClipEditor</code> 側で行います。少し長いですが次のように初回、クリップが変化したとき、ズーム率などで横幅や縦幅が大きく変化したときにテクスチャを再生成するようにしています。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>[CustomTimelineEditor(<span class="synStatement">typeof</span>(uLipSyncClip))] <span class="synType">public</span> <span class="synType">class</span> uLipSyncClipTimelineEditor : ClipEditor { <span class="synType">internal</span> <span class="synType">class</span> TextureCache { <span class="synType">public</span> Texture2D texture; <span class="synType">public</span> <span class="synType">bool</span> forceUpdate = <span class="synConstant">false</span>; } Dictionary&lt;uLipSyncClip, TextureCache&gt; _textures = <span class="synStatement">new</span> Dictionary&lt;uLipSyncClip, TextureCache&gt;(); <span class="synType">void</span> RemoveCachedTexture(uLipSyncClip clip) { <span class="synStatement">if</span> (!_textures.ContainsKey(clip)) <span class="synStatement">return</span>; var cache = _textures[clip]; Object.DestroyImmediate(cache.texture); _textures.Remove(clip); } Texture2D CreateCachedTexture( uLipSyncClip clip, <span class="synType">int</span> width, <span class="synType">int</span> height) { RemoveCachedTexture(clip); var data = clip.bakedData; <span class="synStatement">if</span> (!data) <span class="synStatement">return</span> <span class="synConstant">null</span>; width = Mathf.Clamp(width, <span class="synConstant">128</span>, <span class="synConstant">4096</span>); var tex = data.CreateTexture(width, height); var cache = <span class="synStatement">new</span> TextureCache { texture = tex }; _textures.Add(clip, cache); <span class="synStatement">return</span> tex; } Texture2D GetOrCreateCachedTexture( uLipSyncClip clip, <span class="synType">int</span> width, <span class="synType">int</span> height) { <span class="synStatement">if</span> (!_textures.ContainsKey(clip)) { <span class="synStatement">return</span> CreateCachedTexture(clip, width, height); } var cache = _textures[clip]; <span class="synStatement">if</span> (cache.forceUpdate) { <span class="synStatement">return</span> CreateCachedTexture(clip, width, height); } var dw = Mathf.Abs(cache.texture.width - width); var dh = Mathf.Abs(cache.texture.height - height); <span class="synStatement">if</span> (dw &gt; <span class="synConstant">10</span> || dh &gt; <span class="synConstant">10</span>) { <span class="synStatement">return</span> CreateCachedTexture(clip, width, height); } <span class="synStatement">return</span> cache.texture; } <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> DrawBackground( TimelineClip clip, ClipBackgroundRegion region) { DrawBackground(region); DrawWave(clip, region); } <span class="synType">void</span> DrawBackground(ClipBackgroundRegion region) { EditorUtil.DrawBackgroundRect( region.position, <span class="synStatement">new</span> Color(<span class="synConstant">0f</span>, <span class="synConstant">0f</span>, <span class="synConstant">0f</span>, <span class="synConstant">0.3f</span>), Color.clear); } <span class="synType">void</span> DrawWave( TimelineClip timelineClip, ClipBackgroundRegion region) { var clip = timelineClip.asset <span class="synStatement">as</span> uLipSyncClip; var data = clip.bakedData; <span class="synStatement">if</span> (!data) <span class="synStatement">return</span>; var audioClip = data.audioClip; <span class="synStatement">if</span> (!audioClip) <span class="synStatement">return</span>; var rect = region.position; var duration = region.endTime - region.startTime; var width = (<span class="synType">float</span>)(rect.width * audioClip.length / duration); var left = Mathf.Max( (<span class="synType">float</span>)timelineClip.clipIn, (<span class="synType">float</span>)region.startTime); var offset = (<span class="synType">float</span>)(width * left / audioClip.length); rect.x -= offset; rect.width = width; var tex = GetOrCreateCachedTexture( clip, (<span class="synType">int</span>)rect.width, (<span class="synType">int</span>)rect.height); <span class="synStatement">if</span> (!tex) <span class="synStatement">return</span>; GUI.DrawTexture(rect, tex); } <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnClipChanged(TimelineClip timelineClip) { var clip = timelineClip.asset <span class="synStatement">as</span> uLipSyncClip; <span class="synStatement">if</span> (!_textures.ContainsKey(clip)) <span class="synStatement">return</span>; _textures[clip].forceUpdate = <span class="synConstant">true</span>; } } </pre> <p>これで見た目もそこそこにパフォーマンス改善することができました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220629/20220629005258.png" width="1200" height="543" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2><code>Texture2D.GetPixelData()</code> 方式</h2> <p><code>Color[]</code> に詰めて <code>SetPixels()</code> するのもオーバーヘッドがあるので、<code>Texture2D.GetPixelData()</code> を使ってみます。これは直接カラーバッファを書き換え可能な  <code>NativeArray</code> を返してくれる便利関数です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FScriptReference%2FTexture2D.GetPixelData.html" title="Texture2D-GetPixelData - Unity スクリプトリファレンス" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/ScriptReference/Texture2D.GetPixelData.html">docs.unity3d.com</a></cite></p> <p>以下のように書けば古い Unity では <code>SetPixels()</code> を、対応しているバージョンでは <code>Texture2D.GetPixelData()</code> を使うようにできます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> Texture2D CreateTexture(<span class="synType">int</span> width, <span class="synType">int</span> height) { ... var tex = <span class="synStatement">new</span> Texture2D(width, height); <span class="synPreProc">#if UNITY_2020_1_OR_NEWER</span> var colors = tex.GetPixelData&lt;Color32&gt;(<span class="synConstant">0</span>); <span class="synPreProc">#else</span> var colors = <span class="synStatement">new</span> Color32[width * height]; <span class="synPreProc">#endif</span> ... <span class="synStatement">for</span> (<span class="synType">int</span> x = <span class="synConstant">0</span>; x &lt; width; ++x) { ... colors[index] = color; ... } <span class="synPreProc">#if !UNITY_2020_1_OR_NEWER</span> tex.SetPixels(colors); <span class="synPreProc">#endif</span> tex.Apply(); <span class="synStatement">return</span> tex; } </pre> <h2>ジョブ化</h2> <p>この <code>Texture2D.GetPixelData()</code> とジョブを組み合わせた公式サンプルが以下に公開されています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FUnity-Technologies%2FUnityTextureAccessApiExamples" title="GitHub - Unity-Technologies/UnityTextureAccessApiExamples: Example project for Unity 2020.1+ texture data access APIs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/Unity-Technologies/UnityTextureAccessApiExamples">github.com</a></cite></p> <p>これを元に Burst + Job 化をしてみましょう。ちょっと uLipSync 特有コードが多くあまり参考にならないかもしれませんが...。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> Unity.Burst; <span class="synStatement">using</span> Unity.Collections; <span class="synStatement">using</span> Unity.Jobs; <span class="synStatement">using</span> Unity.Mathematics; ... [BurstCompile] ... <span class="synType">public</span> <span class="synType">class</span> BakedData : ScriptableObject { ... [BurstCompile] <span class="synType">struct</span> CreateTextureJob : IJob { [WriteOnly] <span class="synType">public</span> NativeArray&lt;Color32&gt; texColors; [DeallocateOnJobCompletion][ReadOnly] <span class="synType">public</span> NativeArray&lt;Color&gt; phonemeColors; [DeallocateOnJobCompletion][ReadOnly] <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; phonemeRatios; [DeallocateOnJobCompletion][ReadOnly] <span class="synType">public</span> NativeArray&lt;<span class="synType">float</span>&gt; volumes; [ReadOnly] <span class="synType">public</span> <span class="synType">int</span> width; [ReadOnly] <span class="synType">public</span> <span class="synType">int</span> height; [ReadOnly] <span class="synType">public</span> <span class="synType">int</span> phonemeCount; [ReadOnly] <span class="synType">public</span> <span class="synType">float</span> smooth; <span class="synType">public</span> <span class="synType">void</span> Execute() { var currentColor = <span class="synStatement">new</span> Color(); <span class="synStatement">for</span> (<span class="synType">int</span> x = <span class="synConstant">0</span>; x &lt; width; ++x) { var targetColor = <span class="synStatement">new</span> Color(); <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i &lt; phonemeCount; ++i) { var colorIndex = i % phonemeColors.Length; var ratioIndex = x * phonemeCount + i; var color = phonemeColors[colorIndex]; <span class="synType">float</span> ratio = phonemeRatios[ratioIndex]; targetColor += color * ratio; } currentColor += (targetColor - currentColor) * smooth; <span class="synStatement">for</span> (<span class="synType">int</span> y = <span class="synConstant">0</span>; y &lt; height; ++y) { var index = width * y + x; var color = currentColor; var dy = ((<span class="synType">float</span>)y - height / <span class="synConstant">2f</span>) / (height / <span class="synConstant">2f</span>); dy = math.abs(dy); dy = math.pow(dy, <span class="synConstant">2f</span>); color.a = dy &gt; volumes[x] ? <span class="synConstant">0f</span> : <span class="synConstant">1f</span>; texColors[index] = color; } } } } <span class="synType">public</span> Texture2D CreateTexture(<span class="synType">int</span> width, <span class="synType">int</span> height) { <span class="synStatement">if</span> (!isValid) <span class="synStatement">return</span> Texture2D.whiteTexture; var tex = <span class="synStatement">new</span> Texture2D(width, height); var texColors = tex.GetPixelData&lt;Color32&gt;(<span class="synConstant">0</span>); var phonemeColorsTmp = <span class="synStatement">new</span> NativeArray&lt;Color&gt;( phonemeColors, Allocator.TempJob); <span class="synType">int</span> phonemeCount = frames[<span class="synConstant">0</span>].phonemes.Count; var phonemeRatiosTmp = <span class="synStatement">new</span> NativeArray&lt;<span class="synType">float</span>&gt;( width * phonemeCount, Allocator.TempJob); var volumesTmp = <span class="synStatement">new</span> NativeArray&lt;<span class="synType">float</span>&gt;( width, Allocator.TempJob); <span class="synStatement">for</span> (<span class="synType">int</span> x = <span class="synConstant">0</span>; x &lt; width; ++x) { var t = (<span class="synType">float</span>)x / width * duration; var frame = GetFrame(t); <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i &lt; phonemeCount; ++i) { <span class="synType">int</span> index = x * phonemeCount + i; phonemeRatiosTmp[index] = frame.phonemes[i].ratio; } volumesTmp[x] = frame.volume; } var job = <span class="synStatement">new</span> CreateTextureJob() { texColors = texColors, phonemeColors = phonemeColorsTmp, phonemeRatios = phonemeRatiosTmp, volumes = volumesTmp, width = width, height = height, phonemeCount = phonemeCount, smooth = <span class="synConstant">0.15f</span>, }; job.Schedule().Complete(); tex.Apply(); <span class="synStatement">return</span> tex; } } </pre> <p>次のような手順で Job + Burst 化します。</p> <ul> <li>通常の配列は <code>NativeArray</code> に変換</li> <li>入力として必要なデータは事前に別途 <code>NativeArray</code> に詰めておく</li> <li>2 次元配列は長くした 1 次元 <code>NativeArray</code> にしておく</li> <li><code>Allocator.TempJob</code> と <code>DeallocateOnJobCompletion</code> で確保したメモリは自動解放</li> </ul> <p>比較的お手軽に高速化ができますね。また、今回のケースでは色のスムージングのため前の色を利用する形になっている関係で、<code>IJobParallelFor</code> にはしていません。</p> <h2>おわりに</h2> <p>ランタイムのパフォーマンスばかりに気を配っていましたが、エディタのパフォーマンスも大事ですね。。特に今回のように変な描画をするようなケースではパフォーマンスが顕著に落ちる可能性があるので、これからはいろんなデータを試しながら検証をしていこうと思いました。</p> hecomi uRaymarching の URP 向け機能更新をしてみた(Deferred / SSAO / Decal / Clear Coat) hatenablog://entry/13574176438096946466 2022-05-31T00:38:15+09:00 2022-05-31T00:42:09+09:00 はじめに 前回の記事で、Deferred を始めとする URP の新機能の調査を行いました。 tips.hecomi.com 本記事ではこの調査をもとに更新した uRaymarching の各機能やどういう処理が内部的にされているか、また注意事項などを紹介します。いくつか前回の記事と重複するところもありますが、ご了承ください。 ダウンロード github.com Depth Normals まず、DepthNormals パスの追加を行いました。これは深度だけでなく法線も必要な機能が選択された時に実行されるパスです。これが直接ユーザーの目に見えるわけではありませんが、例えば後述する Rende… <h2>はじめに</h2> <p>前回の記事で、Deferred を始めとする URP の新機能の調査を行いました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F04%2F30%2F233225" title="URP に新しく追加された Deferred レンダリングなどについてざっくりと調べてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/04/30/233225">tips.hecomi.com</a></cite></p> <p>本記事ではこの調査をもとに更新した uRaymarching の各機能やどういう処理が内部的にされているか、また注意事項などを紹介します。いくつか前回の記事と重複するところもありますが、ご了承ください。</p> <h2>ダウンロード</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuRaymarching%2Freleases%2Ftag%2Fv2.3.0" title="Release v2.3.0 has been released · hecomi/uRaymarching" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uRaymarching/releases/tag/v2.3.0">github.com</a></cite></p> <h2>Depth Normals </h2> <p>まず、<code>DepthNormals</code> パスの追加を行いました。これは深度だけでなく法線も必要な機能が選択された時に実行されるパスです。これが直接ユーザーの目に見えるわけではありませんが、例えば後述する Renderer Feature の SSAO の Source に Depth ではなく Depth Normals を選択したときや、Decal を ON にしたときに使われます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220529/20220529144535.png" width="890" height="564" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220529/20220529144942.gif" width="1200" height="829" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>レイマーチングに対して <code>DepthNormals</code> パスの追加は、すでに Depth パスを持っている場合は簡単で、出力構造体を用意して深度に加えて法線も書き出せばよいだけです。短いので全文を載せておきます。</p> <h5>ShaderLab</h5> <pre class="code lang-c" data-lang="c" data-unlink>Pass { Name <span class="synConstant">&quot;DepthNormals&quot;</span> Tags { <span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;DepthNormals&quot;</span> } ZWrite On Cull [_Cull] HLSLPROGRAM <span class="synPreProc">#pragma shader_feature _ALPHATEST_ON</span> <span class="synPreProc">#pragma multi_compile_fragment _ _GBUFFER_NORMALS_OCT</span> <span class="synPreProc">#pragma multi_compile_instancing</span> <span class="synPreProc">#pragma prefer_hlslcc gles</span> <span class="synPreProc">#pragma exclude_renderers d3d11_9x</span> <span class="synPreProc">#pragma target </span><span class="synConstant">2.0</span> <span class="synPreProc">#pragma vertex Vert</span> <span class="synPreProc">#pragma fragment Frag</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;&lt;RaymarchingShaderDirectory&gt;/DepthNormals.hlsl&quot;</span> ENDHLSL } </pre> <h5>HLSL</h5> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#ifndef URAYMARCHING_DEPTH_NORMALS_HLSL</span> <span class="synPreProc">#define URAYMARCHING_DEPTH_NORMALS_HLSL</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;./Primitives.hlsl&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;./Raymarching.hlsl&quot;</span> <span class="synType">int</span> _Loop; <span class="synType">float</span> _MinDistance; float4 _Color; <span class="synType">struct</span> Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; UNITY_VERTEX_INPUT_INSTANCE_ID }; <span class="synType">struct</span> Varyings { float4 positionCS : SV_POSITION; float4 positionSS : TEXCOORD0; float3 normalWS : TEXCOORD1; float3 positionWS : TEXCOORD2; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; <span class="synType">struct</span> FragOutput { float4 normal : SV_Target; <span class="synType">float</span> depth : SV_Depth; }; Varyings <span class="synIdentifier">Vert</span>(Attributes input) { Varyings output = (Varyings)<span class="synConstant">0</span>; <span class="synIdentifier">UNITY_SETUP_INSTANCE_ID</span>(input); <span class="synIdentifier">UNITY_TRANSFER_INSTANCE_ID</span>(input, output); <span class="synIdentifier">UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO</span>(output); VertexPositionInputs vertexInput = <span class="synIdentifier">GetVertexPositionInputs</span>(input.positionOS.xyz); output.positionCS = vertexInput.positionCS; output.positionWS = <span class="synIdentifier">TransformObjectToWorld</span>(input.positionOS.xyz); output.normalWS = <span class="synIdentifier">TransformObjectToWorldNormal</span>(input.normalOS); output.positionSS = <span class="synIdentifier">ComputeNonStereoScreenPos</span>(output.positionCS); output.positionSS.z = -<span class="synIdentifier">TransformWorldToView</span>(output.positionWS).z; <span class="synStatement">return</span> output; } FragOutput <span class="synIdentifier">Frag</span>(Varyings input) { <span class="synIdentifier">UNITY_SETUP_INSTANCE_ID</span>(input); <span class="synIdentifier">UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX</span>(input); RaymarchInfo ray; <span class="synIdentifier">INITIALIZE_RAYMARCH_INFO</span>(ray, input, _Loop, _MinDistance); <span class="synIdentifier">Raymarch</span>(ray); float3 normal = <span class="synIdentifier">NormalizeNormalPerPixel</span>(<span class="synIdentifier">DecodeNormalWS</span>(ray.normal)); FragOutput o; o.normal = <span class="synIdentifier">float4</span>(normal, <span class="synConstant">0.0</span>); o.depth = ray.depth; <span class="synStatement">return</span> o; } <span class="synPreProc">#endif</span> </pre> <p>適切に書き込まれているのが分かります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220514/20220514215916.png" width="1128" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>SSAO</h2> <p><code>DepthNormals</code> パスを追加したので、<code>Depth Normals</code> を <code>Source</code> とした SSAO が反映されるようにしました(<code>Depth</code> を <code>Source</code> とした場合は、<code>DepthOnly</code> パスがあれば問題ありません)。基本的には <code>Depth</code> より <code>Depth Normals</code> のほうがキレイな SSAO が適用されます。</p> <p>内部的には次のように SSAO のテクスチャ(<code>_SSAO_OcclusionTexture</code>)が作成されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220529/20220529190940.gif" width="911" height="861" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ただこれだけでは最終的な画に SSAO は反映されません。SSAO を有効にするには次の pragma 文を ShaderLab の <code>FowardLit</code> パスに挿入する必要があります。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION</span> </pre> <p>こうすると、<code>_SSAO_OcclusionTexture</code> が与えられ、<code>ForwardLit</code> パス中でこのテクスチャに書き込まれたオクルージョンを取り出して出力に適用されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220529/20220529190654.png" width="1200" height="1181" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>Decal</h2> <p>Decal は Renderer Feature で追加でき、実現方法(<code>Technique</code>)としては 2 種類の方法があります。1 つは <code>DBuffer</code> を使う方法、もう 1 つはポストプロセス的に <code>Screen Space</code> で貼り付ける方法です。どちらも両方対応しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220529/20220529235020.png" width="916" height="294" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><code>Screen Space</code> の場合は、<code>_CameraDepthTexture</code> を参照して<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>用のオブジェクトとの交差点に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>テクスチャを描画するため、新規で必要な対応はありませんでした。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220424/20220424233956.gif" width="1200" height="855" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>一方で <code>DBuffer</code> を使う方は、オブジェクト描画前の DBuffer Render ステージで、事前生成した <code>_CameraDepthTexture</code> を参照し、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>オブジェクトの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%D9%A5%C9">アルベド</a>や法線を書き込みます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220424/20220424233509.gif" width="1200" height="855" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>HLSL 中では次のように <code>ApplyDecalToSurfaceData()</code> を呼ぶだけです。</p> <pre class="code lang-c" data-lang="c" data-unlink>FragOutput <span class="synIdentifier">Frag</span>(Varyings input) { ... <span class="synPreProc">#ifdef _DBUFFER</span> <span class="synIdentifier">ApplyDecalToSurfaceData</span>(input.positionCS, surfaceData, inputData); <span class="synPreProc">#endif</span> half4 color = <span class="synIdentifier">UniversalFragmentPBR</span>(inputData, surfaceData); color.rgb = <span class="synIdentifier">MixFog</span>(color.rgb, inputData.fogCoord); color.a = <span class="synIdentifier">OutputAlpha</span>(color.a, _Surface); FragOutput o; o.color = color; o.depth = ray.depth; <span class="synStatement">return</span> o; } </pre> <p>DBuffer 向けには SSAO のときと同じ用に ShaderLab 側に pragma 文追加が必要です。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3</span> </pre> <h2>Depth Prepass</h2> <p><code>Depth Priming Mode</code> を <code>Forced</code> にして Depth Prepass を ON にしてみます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220531/20220531001912.png" width="916" height="530" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220531/20220531001800.png" width="1200" height="830" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>壊れます...。これは <code>SV_Depth</code> を経由して出力したデプスは <code>ZTest Equal</code> でうまく一致させられない問題があるからのようです。以下の記事でも少し触れています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2016%2F09%2F10%2F191006" title="Unity でスクリーンスペースのブーリアン演算をやってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2016/09/10/191006">tips.hecomi.com</a></cite></p> <p>なので、Depth Prepass ありの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>をしたい場合は、レイマーチング用のオブジェクトはこの対象とならないように描画する必要があります。最もシンプルな解決法はキューを <code>Geometry</code> ではなく <code>Transparent</code> にすることです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220531/20220531003422.png" width="1200" height="523" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これで描画が崩れることはなくなりますが、副作用としてデプスが事前に書き込まれない関係で SSAO や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>などは対応できなくなります。もしどなたか良い解決法をご存知でしたら教えていただけると嬉しいです。。</p> <h2>Clear Coat</h2> <p>Complex Lit に追加されている機能であるクリアコートに対応しました。これはニスを塗ったような層のある表面光沢的な表現を行う機能です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220530/20220530225413.gif" width="1200" height="603" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これは <code>_CLEARCOAT</code> を ON にすると適用されます。次のように ON / OFF 出来るフラグ(<code>_ClearCoat</code>)を用意してインスペクタから ON / OFF 出来るようにしています。</p> <pre class="code lang-c" data-lang="c" data-unlink>Shader <span class="synConstant">&quot;Raymarching/&lt;Name&gt;&quot;</span> { ... Properties { ... [Toggle] <span class="synIdentifier">_ClearCoat</span>(<span class="synConstant">&quot;Clear Coat&quot;</span>, Float) = <span class="synConstant">0.0</span> [HideInInspector] <span class="synIdentifier">_ClearCoatMap</span>(<span class="synConstant">&quot;Clear Coat Map&quot;</span>, 2D) = <span class="synConstant">&quot;white&quot;</span> {} <span class="synIdentifier">_ClearCoatMask</span>(<span class="synConstant">&quot;Clear Coat Mask&quot;</span>, <span class="synIdentifier">Range</span>(<span class="synConstant">0.0</span>, <span class="synConstant">1.0</span>)) = <span class="synConstant">0.0</span> <span class="synIdentifier">_ClearCoatSmoothness</span>(<span class="synConstant">&quot;Clear Coat Smoothness&quot;</span>, <span class="synIdentifier">Range</span>(<span class="synConstant">0.0</span>, <span class="synConstant">1.0</span>)) = <span class="synConstant">1.0</span> ... } ... Pass { Name <span class="synConstant">&quot;ForwardLit&quot;</span> ... HLSLPROGRAM ... <span class="synPreProc">#pragma shader_feature_local_fragment _CLEARCOAT_ON</span> <span class="synPreProc">#ifdef _CLEARCOAT_ON</span> <span class="synPreProc">#define _CLEARCOAT</span> <span class="synPreProc">#endif</span> ... } ... } </pre> <h2>Deferred</h2> <p>今回の目玉はこの Deferred の対応です。思ったよりは簡単に対応できました。</p> <p>Deferred は Lit シェーダを参考に書きました。詳しい説明は<a href="https://tips.hecomi.com/entry/2022/04/30/233225">&#x524D;&#x56DE;&#x306E;&#x8A18;&#x4E8B;</a>に譲るとして、ここではそれほど長くないので(200 行程なので)全文を載せてみます。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#ifndef URAYMARCHING_DEFERRED_LIT_HLSL</span> <span class="synPreProc">#define URAYMARCHING_DEFERRED_LIT_HLSL</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityGBuffer.hlsl&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;./Primitives.hlsl&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;./Raymarching.hlsl&quot;</span> <span class="synType">int</span> _Loop; <span class="synType">float</span> _MinDistance; float4 _Color; <span class="synType">struct</span> Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; float2 staticLightmapUV : TEXCOORD1; float2 dynamicLightmapUV : TEXCOORD2; UNITY_VERTEX_INPUT_INSTANCE_ID }; <span class="synType">struct</span> Varyings { float4 positionCS : SV_POSITION; float4 positionSS : TEXCOORD0; float3 positionWS : TEXCOORD1; <span class="synComment">// xyz: posWS</span> half3 normalWS : TEXCOORD2; <span class="synComment">// xyz: normal, w: viewDir.x</span> <span class="synPreProc">#ifdef _ADDITIONAL_LIGHTS_VERTEX</span> half3 vertexLighting : TEXCOORD3; <span class="synComment">// xyz: vertex light</span> <span class="synPreProc">#endif</span> <span class="synIdentifier">DECLARE_LIGHTMAP_OR_SH</span>(staticLightmapUV, vertexSH, <span class="synConstant">4</span>); <span class="synPreProc">#ifdef DYNAMICLIGHTMAP_ON</span> float2 dynamicLightmapUV : TEXCOORD5; <span class="synComment">// Dynamic lightmap UVs</span> <span class="synPreProc">#endif</span> UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; <span class="synType">struct</span> CustomFragOutput { half4 GBuffer0 : SV_Target0; half4 GBuffer1 : SV_Target1; half4 GBuffer2 : SV_Target2; half4 GBuffer3 : SV_Target3; <span class="synPreProc">#ifdef GBUFFER_OPTIONAL_SLOT_1</span> GBUFFER_OPTIONAL_SLOT_1_TYPE GBuffer4 : SV_Target4; <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef GBUFFER_OPTIONAL_SLOT_2</span> half4 GBuffer5 : SV_Target5; <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef GBUFFER_OPTIONAL_SLOT_3</span> half4 GBuffer6 : SV_Target6; <span class="synPreProc">#endif</span> <span class="synType">float</span> depth : SV_Depth; }; Varyings <span class="synIdentifier">Vert</span>(Attributes input) { Varyings output = (Varyings)<span class="synConstant">0</span>; <span class="synIdentifier">UNITY_SETUP_INSTANCE_ID</span>(input); <span class="synIdentifier">UNITY_TRANSFER_INSTANCE_ID</span>(input, output); <span class="synIdentifier">UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO</span>(output); VertexPositionInputs vertexInput = <span class="synIdentifier">GetVertexPositionInputs</span>(input.positionOS.xyz); VertexNormalInputs normalInput= <span class="synIdentifier">GetVertexNormalInputs</span>(input.normalOS, input.tangentOS); output.positionCS = vertexInput.positionCS; output.positionWS = vertexInput.positionWS; output.positionSS = <span class="synIdentifier">ComputeNonStereoScreenPos</span>(output.positionCS); output.positionSS.z = -<span class="synIdentifier">TransformWorldToView</span>(output.positionWS).z; output.normalWS = <span class="synIdentifier">NormalizeNormalPerVertex</span>(normalInput.normalWS); <span class="synIdentifier">OUTPUT_LIGHTMAP_UV</span>( input.staticLightmapUV, unity_LightmapST, output.staticLightmapUV); <span class="synPreProc">#ifdef DYNAMICLIGHTMAP_ON</span> output.dynamicLightmapUV = input.dynamicLightmapUV.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw; <span class="synPreProc">#endif</span> <span class="synIdentifier">OUTPUT_SH</span>(output.normalWS.xyz, output.vertexSH); <span class="synPreProc">#ifdef _ADDITIONAL_LIGHTS_VERTEX</span> half3 vertexLight = <span class="synIdentifier">VertexLighting</span>( vertexInput.positionWS, normalInput.normalWS); output.vertexLighting = vertexLight; <span class="synPreProc">#endif</span> <span class="synStatement">return</span> output; } CustomFragOutput <span class="synIdentifier">Frag</span>(Varyings input) { <span class="synIdentifier">UNITY_SETUP_INSTANCE_ID</span>(input); <span class="synIdentifier">UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX</span>(input); RaymarchInfo ray; <span class="synIdentifier">INITIALIZE_RAYMARCH_INFO</span>(ray, input, _Loop, _MinDistance); <span class="synIdentifier">Raymarch</span>(ray); InputData inputData = (InputData)<span class="synConstant">0</span>; inputData.positionWS = ray.endPos; inputData.positionCS = <span class="synIdentifier">TransformWorldToHClip</span>(ray.endPos); inputData.normalWS = <span class="synIdentifier">NormalizeNormalPerPixel</span>(<span class="synIdentifier">DecodeNormalWS</span>(ray.normal)); inputData.viewDirectionWS = <span class="synIdentifier">SafeNormalize</span>(<span class="synIdentifier">GetCameraPosition</span>() - ray.endPos); inputData.shadowCoord = <span class="synIdentifier">TransformWorldToShadowCoord</span>(ray.endPos); <span class="synPreProc">#ifdef _ADDITIONAL_LIGHTS_VERTEX</span> inputData.vertexLighting = input.vertexLighting.xyz; <span class="synPreProc">#else</span> inputData.vertexLighting = <span class="synIdentifier">half3</span>(<span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>); <span class="synPreProc">#endif</span> inputData.fogCoord = <span class="synConstant">0</span>; <span class="synPreProc">#if defined(DYNAMICLIGHTMAP_ON)</span> inputData.bakedGI = <span class="synIdentifier">SAMPLE_GI</span>( input.staticLightmapUV, input.dynamicLightmapUV, input.vertexSH, inputData.normalWS); <span class="synPreProc">#else</span> inputData.bakedGI = <span class="synIdentifier">SAMPLE_GI</span>( input.staticLightmapUV, input.vertexSH, inputData.normalWS); <span class="synPreProc">#endif</span> inputData.normalizedScreenSpaceUV = <span class="synIdentifier">GetNormalizedScreenSpaceUV</span>(input.positionCS); inputData.shadowMask = <span class="synIdentifier">SAMPLE_SHADOWMASK</span>(input.staticLightmapUV); <span class="synPreProc">#if defined(DEBUG_DISPLAY)</span> <span class="synPreProc">#if defined(DYNAMICLIGHTMAP_ON)</span> inputData.dynamicLightmapUV = input.dynamicLightmapUV; <span class="synPreProc">#endif</span> <span class="synPreProc">#if defined(LIGHTMAP_ON)</span> inputData.staticLightmapUV = input.staticLightmapUV; <span class="synPreProc">#else</span> inputData.vertexSH = input.vertexSH; <span class="synPreProc">#endif</span> <span class="synPreProc">#endif</span> SurfaceData surfaceData; <span class="synIdentifier">InitializeStandardLitSurfaceData</span>(<span class="synIdentifier">float2</span>(<span class="synConstant">0</span>, <span class="synConstant">0</span>), surfaceData); <span class="synPreProc">#ifdef POST_EFFECT</span> <span class="synIdentifier">POST_EFFECT</span>(ray, surfaceData); <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef _DBUFFER</span> <span class="synIdentifier">ApplyDecalToSurfaceData</span>(input.positionCS, surfaceData, inputData); <span class="synPreProc">#endif</span> BRDFData brdfData; <span class="synIdentifier">InitializeBRDFData</span>( surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.alpha, brdfData); Light mainLight = <span class="synIdentifier">GetMainLight</span>( inputData.shadowCoord, inputData.positionWS, inputData.shadowMask); <span class="synIdentifier">MixRealtimeAndBakedGI</span>( mainLight, inputData.normalWS, inputData.bakedGI, inputData.shadowMask); half3 color = <span class="synIdentifier">GlobalIllumination</span>( brdfData, inputData.bakedGI, surfaceData.occlusion, inputData.positionWS, inputData.normalWS, inputData.viewDirectionWS); FragmentOutput baseOutput = <span class="synIdentifier">BRDFDataToGbuffer</span>( brdfData, inputData, surfaceData.smoothness, surfaceData.emission + color, surfaceData.occlusion); CustomFragOutput output = (CustomFragOutput)<span class="synConstant">0</span>; output.GBuffer0 = baseOutput.GBuffer0; output.GBuffer1 = baseOutput.GBuffer1; output.GBuffer2 = baseOutput.GBuffer2; output.GBuffer3 = baseOutput.GBuffer3; <span class="synPreProc">#ifdef GBUFFER_OPTIONAL_SLOT_1</span> output.GBuffer4 = baseOutput.GBuffer4; <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef GBUFFER_OPTIONAL_SLOT_2</span> output.GBuffer5 = baseOutput.GBuffer5; <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef GBUFFER_OPTIONAL_SLOT_3</span> output.GBuffer6 = baseOutput.GBuffer6; <span class="synPreProc">#endif</span> output.depth = ray.depth + <span class="synConstant">1e-7</span>; <span class="synStatement">return</span> output; } <span class="synPreProc">#endif</span> </pre> <p>基本的には Forward のときと同じく<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%D5%A5%A7%A5%B9">サーフェス</a>データを集めて <a class="keyword" href="http://d.hatena.ne.jp/keyword/BRDF">BRDF</a> の計算を行っています。詰める先が Forward のときは color でしたが、Deferred では GBuffer に詰めています。ここでの味噌は <code>output.depth</code> ですごい小さい数字を足しているところです。これをしない場合は次のような画になります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220530/20220530231929.png" width="696" height="480" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>先程の Depth Prepass と同じ感じになってますね。何やら DepthNormalPrepass でデプスを描画した後にデプスはクリアされずに Render GBuffer で <code>ZTest LessEqual</code> で描画される関係で、深度が完全には一致せずにちょっとだけずれて描画されないエリアが生まれてしまっているみたいです。これを避けるためにすこーしだけデプスをいじっています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220530/20220530235445.png" width="1060" height="682" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>おわりに</h2> <p>URP はシェーダがシンプルなので比較的メンテがしやすいのが良いですね。次は Object Motion <a class="keyword" href="http://d.hatena.ne.jp/keyword/Blur">Blur</a> に期待してます。</p> hecomi URP に新しく追加された Deferred レンダリングなどについてざっくりと調べてみた hatenablog://entry/13574176438085327832 2022-04-30T23:32:25+09:00 2022-05-14T18:24:06+09:00 はじめに 先日 Unity 2021 LTS が発表されましたね。 blog.unity.com Unity 2021 には数多くの機能追加がありますが、このうちの一つが URP への Deferred Rendering の正式サポートです(実験的に以前から追加はされていたようです)。 docs.unity3d.com ドキュメントはこちらです。 docs.unity3d.com 本ブログではレンダリングパイプラインを読んでいこう企画を何度かやってまして、URP も Unlit / Lit (Forward) については以下の記事を書きました。 tips.hecomi.com 今回は続きで、… <h2>はじめに</h2> <p>先日 <strong>Unity 2021 LTS</strong> が発表されましたね。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.unity.com%2Fja%2Fnews%2Fintroducing-unity-2021-lts" title="Unity 2021 LTS のご紹介 | Unity Blog" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://blog.unity.com/ja/news/introducing-unity-2021-lts">blog.unity.com</a></cite></p> <p>Unity 2021 には数多くの機能追加がありますが、このうちの一つが <strong>URP</strong> への <strong>Deferred Rendering</strong> の正式サポートです(実験的に以前から追加はされていたようです)。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4012.0%2Fmanual%2Fwhats-new%2Furp-whats-new.html" title="What&#39;s new in URP 12 (Unity 2021.2) | Universal RP | 12.0.0" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/whats-new/urp-whats-new.html">docs.unity3d.com</a></cite></p> <p>ドキュメントはこちらです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4012.0%2Fmanual%2Frendering%2Fdeferred-rendering-path.html" title="Deferred Rendering Path in URP | Universal RP | 12.0.0" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/rendering/deferred-rendering-path.html">docs.unity3d.com</a></cite></p> <p>本ブログでは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>パイプラインを読んでいこう企画を何度かやってまして、URP も Unlit / Lit (Forward) については以下の記事を書きました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2019%2F10%2F27%2F152520" title="Unity の Universal Render Pipeline のレンダリング周りについて勉強してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2019/10/27/152520">tips.hecomi.com</a></cite></p> <p>今回は続きで、Deferred を始めとして、前回執筆時から更新があり追加された DepthNormals パスや<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>などについて書いていこうと思います。ただし、今回は、マクロの分岐なども含めてものすごい深堀りするところまではやらずに、全体の流れを見るにとどめます(たまに細かく見ます)。なお、体系的に URP の更新履歴を追いたい方には、以下の <a href="https://twitter.com/lil_xyzw">lil</a> さんによるまとめが大変素晴らしくまとまっています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Flilxyzw%2FShader-MEMO" title="GitHub - lilxyzw/Shader-MEMO: Unityでのシェーダー開発に関するメモ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/lilxyzw/Shader-MEMO">github.com</a></cite></p> <h2>環境</h2> <ul> <li>Unity 2021.3.0f1</li> <li>Universal RP 12.1.6</li> </ul> <h2>Lit シェーダのパスの外観</h2> <p><code>Lit</code> シェーダの方は色んな技法に対応しており追うのが大変なので、シンプルな <code>SimpleLit</code> シェーダを見ていきます。まずは ShaderLab の全体構造をざっくり見てみます。</p> <pre class="code lang-c" data-lang="c" data-unlink>Shader <span class="synConstant">&quot;Universal Render Pipeline/Simple Lit&quot;</span> { Properties { ... } SubShader { Tags { <span class="synConstant">&quot;RenderType&quot;</span> = <span class="synConstant">&quot;Opaque&quot;</span> <span class="synConstant">&quot;RenderPipeline&quot;</span> = <span class="synConstant">&quot;UniversalPipeline&quot;</span> <span class="synConstant">&quot;UniversalMaterialType&quot;</span> = <span class="synConstant">&quot;SimpleLit&quot;</span> <span class="synConstant">&quot;IgnoreProjector&quot;</span> = <span class="synConstant">&quot;True&quot;</span> <span class="synConstant">&quot;ShaderModel&quot;</span>=<span class="synConstant">&quot;4.5&quot;</span> } LOD <span class="synConstant">300</span> Pass { Name <span class="synConstant">&quot;ForwardLit&quot;</span> Tags {<span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;UniversalForward&quot;</span>} .... } Pass { Name <span class="synConstant">&quot;ShadowCaster&quot;</span> Tags {<span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;ShadowCaster&quot;</span>} ... } Pass { Name <span class="synConstant">&quot;GBuffer&quot;</span> Tags {<span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;UniversalGBuffer&quot;</span>} } Pass { Name <span class="synConstant">&quot;DepthOnly&quot;</span> Tags {<span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;DepthOnly&quot;</span>} ... } Pass { Name <span class="synConstant">&quot;DepthNormals&quot;</span> Tags {<span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;DepthNormals&quot;</span>} } Pass { Name <span class="synConstant">&quot;Meta&quot;</span> Tags {<span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;Meta&quot;</span>} ... } Pass { Name <span class="synConstant">&quot;Universal2D&quot;</span> Tags { <span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;Universal2D&quot;</span> } ... } } SubShader { Tags { ... <span class="synConstant">&quot;ShaderModel&quot;</span>=<span class="synConstant">&quot;2.0&quot;</span> } ... } FallBack <span class="synConstant">&quot;Hidden/Universal Render Pipeline/FallbackError&quot;</span> CustomEditor <span class="synConstant">&quot;UnityEditor.Rendering.Universal.ShaderGUI.LitShader&quot;</span> } </pre> <p>この <code>UniversalGBuffer</code> なパスが今回見ていく Deferred のコアとなるパスになります。ただ、前回の記事と比べて <code>DepthNormals</code> や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>なども増えているのでこちらも合わせて見ていきたいと思います。</p> <h2>DepthNormals パス</h2> <h3>Depth Normals パスとは</h3> <p>URP 10.0.x より追加されたパスです。<code>_CameraNormalsTexture</code> というテクスチャに出力が書き込まれ、ポストプロセスなどで利用されます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4013.0%2Fmanual%2Fupgrade-guide-10-1-x.html" title="Upgrading to version 10.1.x of the Universal Render Pipeline | Universal RP | 13.0.0" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@13.0/manual/upgrade-guide-10-1-x.html">docs.unity3d.com</a></cite></p> <p>例えば SSAO の Render Feature では <code>Source</code> で <code>Depth</code> か <code>Depth Normals</code> かを選択できます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsomeiyoshino.info%2Fentry%2F2020%2F12%2F19%2F223701" title="#unity Unity2020.2リリース記念でURP10の新機能であるSSAOを有効化してみる(Renderer Feature機能概説) - 土屋つかさの技術ブログは今か無しか" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://someiyoshino.info/entry/2020/12/19/223701">someiyoshino.info</a></cite></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220420/20220420021803.png" width="1066" height="416" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><code>Depth</code> と <code>Depth Normals</code> のどちらを使うかによってプラットフォーム次第でパフォーマンス差(ほとんどのケースであまり変わらない)やビジュアルの差(一般的に <code>Depth Normals</code> のほうがキレイ)が生じます。詳しくは以下のドキュメントに記述されています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4010.0%2Fmanual%2Fpost-processing-ssao.html" title="Ambient Occlusion | Universal RP | 10.0.0-preview.26" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@10.0/manual/post-processing-ssao.html">docs.unity3d.com</a></cite></p> <p>例えば執筆時点では、<a href="https://github.com/hecomi/uRaymarching">uRaymarching</a> は <code>Depth Normals</code> パスに対応していないので、<code>Depth Normals</code> が指定されたときは対応するパスがないので法線情報が出力されず、SSAO が適用されないという状態になっています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220424/20220424154013.png" width="1200" height="583" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>コード</h3> <p>コードを詳細に見てみましょう。</p> <pre class="code lang-c" data-lang="c" data-unlink>Pass { Name <span class="synConstant">&quot;DepthNormals&quot;</span> Tags {<span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;DepthNormals&quot;</span>} ZWrite On Cull[_Cull] HLSLPROGRAM <span class="synPreProc">#pragma exclude_renderers gles gles3 glcore</span> <span class="synPreProc">#pragma target </span><span class="synConstant">4.5</span> <span class="synPreProc">#pragma vertex DepthNormalsVertex</span> <span class="synPreProc">#pragma fragment DepthNormalsFragment</span> <span class="synPreProc">#pragma shader_feature_local _NORMALMAP</span> <span class="synPreProc">#pragma shader_feature_local_fragment _ALPHATEST_ON</span> <span class="synPreProc">#pragma shader_feature_local_fragment _GLOSSINESS_FROM_BASE_ALPHA</span> <span class="synPreProc">#pragma multi_compile_instancing</span> <span class="synPreProc">#pragma multi_compile _ DOTS_INSTANCING_ON</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitInput.hlsl&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitDepthNormalsPass.hlsl&quot;</span> } </pre> <p>パス本体は <em>SimpleLitDepthNormalsPass.hlsl</em> に記述されています。説明のために簡略化したコードを見てみます。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl&quot;</span> <span class="synType">struct</span> Attributes { float4 positionOS : POSITION; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; float3 normal : NORMAL; UNITY_VERTEX_INPUT_INSTANCE_ID }; <span class="synType">struct</span> Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD1; <span class="synPreProc">#ifdef _NORMALMAP</span> half4 normalWS : TEXCOORD2; <span class="synComment">// xyz: normal, w: viewDir.x</span> half4 tangentWS : TEXCOORD3; <span class="synComment">// xyz: tangent, w: viewDir.y</span> half4 bitangentWS : TEXCOORD4; <span class="synComment">// xyz: bitangent, w: viewDir.z</span> <span class="synPreProc">#else</span> half3 normalWS : TEXCOORD2; half3 viewDir : TEXCOORD3; <span class="synPreProc">#endif</span> UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; Varyings <span class="synIdentifier">DepthNormalsVertex</span>(Attributes input) { Varyings output = (Varyings)<span class="synConstant">0</span>; <span class="synIdentifier">UNITY_SETUP_INSTANCE_ID</span>(input); <span class="synIdentifier">UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO</span>(output); output.uv = <span class="synIdentifier">TRANSFORM_TEX</span>(input.texcoord, _BaseMap); output.positionCS = <span class="synIdentifier">TransformObjectToHClip</span>(input.positionOS.xyz); VertexPositionInputs vertexInput = <span class="synIdentifier">GetVertexPositionInputs</span>(input.positionOS.xyz); VertexNormalInputs normalInput = <span class="synIdentifier">GetVertexNormalInputs</span>(input.normal, input.tangentOS); half3 viewDirWS = <span class="synIdentifier">GetWorldSpaceNormalizeViewDir</span>(vertexInput.positionWS); <span class="synPreProc">#if defined(_NORMALMAP)</span> output.normalWS = <span class="synIdentifier">half4</span>(normalInput.normalWS, viewDirWS.x); output.tangentWS = <span class="synIdentifier">half4</span>(normalInput.tangentWS, viewDirWS.y); output.bitangentWS = <span class="synIdentifier">half4</span>(normalInput.bitangentWS, viewDirWS.z); <span class="synPreProc">#else</span> output.normalWS = <span class="synIdentifier">half3</span>(<span class="synIdentifier">NormalizeNormalPerVertex</span>(normalInput.normalWS)); <span class="synPreProc">#endif</span> <span class="synStatement">return</span> output; } half4 <span class="synIdentifier">DepthNormalsFragment</span>(Varyings input) : SV_TARGET { <span class="synIdentifier">UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX</span>(input); <span class="synIdentifier">Alpha</span>( <span class="synIdentifier">SampleAlbedoAlpha</span>( input.uv, <span class="synIdentifier">TEXTURE2D_ARGS</span>(_BaseMap, sampler_BaseMap)).a, _BaseColor, _Cutoff); ... float2 uv = input.uv; <span class="synPreProc">#if defined(_NORMALMAP)</span> half3 normalTS = <span class="synIdentifier">SampleNormal</span>( uv, <span class="synIdentifier">TEXTURE2D_ARGS</span>(_BumpMap, sampler_BumpMap)); half3 normalWS = <span class="synIdentifier">TransformTangentToWorld</span>( normalTS, <span class="synIdentifier">half3x3</span>( input.tangentWS.xyz, input.bitangentWS.xyz, input.normalWS.xyz)); <span class="synPreProc">#else</span> half3 normalWS = input.normalWS; <span class="synPreProc">#endif</span> normalWS = <span class="synIdentifier">NormalizeNormalPerPixel</span>(normalWS); <span class="synStatement">return</span> <span class="synIdentifier">half4</span>(normalWS, <span class="synConstant">0.0</span>); } </pre> <p>ちょっとコードが多く見えるかもしれませんが、ここで重要なのはフラグメントシェーダの出力の <code>return half4(normalWS, 0.0)</code> です。要は法線を計算して <code>SV_Target</code> として値を書き出しているところですね。計算に関しては他の Pass と変わらない(むしろ色の計算が無い)ので分かりやすいと思います。このパスを追加することで法線情報が出力される様になり、SSAO など法線情報を必要とするポストプロセスがうまく動くようになります。</p> <h3>デプスの上書きをしたい場合</h3> <p>レイマーチングの場合など、Depth の値をポリゴンの値(頂点シェーダの <code>SV_Position</code> で出力された値)から変更したい場合は、次のように出力構造体を用意すれば可能です。</p> <pre class="code lang-c" data-lang="c" data-unlink>... <span class="synType">struct</span> FragOutput { float4 normal : SV_Target; <span class="synType">float</span> depth : SV_Depth; }; ... FragOutput <span class="synIdentifier">Frag</span>(Varyings input) { ... float3 normalWS = ...; ... FragOutput o; o.normal = <span class="synIdentifier">float4</span>(normalWS, <span class="synConstant">0.0</span>); o.depth = ray.depth; <span class="synStatement">return</span> o; } </pre> <p>こうして任意のデプス出力でも SSAO が効くようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220424/20220424164925.png" width="1200" height="845" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220424/20220424165114.png" width="1200" height="821" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>なお、SSAO を実際に反映するには、ShaderLab の <code>ForwardLit</code> パスに次の行を追加しておく必要があります。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION</span> </pre> <h3>SSAO <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>の流れ</h3> <p>では、この法線出力とそれを利用するポストプロセス(正確には URP では Render Feature) である SSAO の流れを見てみましょう。</p> <p>まず各オブジェクトの <code>Depth Normals</code> パスが実行され、<code>_CameraDepthTexture</code> および <code>_CameraNormalsTexture</code> が生成されます。この 2 つを入力として受け取り、SSAO の計算パスが走ります(ブラーのため複数パスが実行されます)。その出力が <code>_SSAO_OcclusionTexture</code> となります。そして後段のオブジェクトごとの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>ステージ(<code>DrawOpaqueObjects</code>)で、それぞれのオブジェクトのシェーダで <code>_SCREEN_SPACE_OCCLUSION</code> キーワードが ON の時にこの <code>_SSAO_OcclusionTexture</code> を参照して出力色へ反映される流れになっています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220424/20220424225732.gif" width="1200" height="855" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ポストプロセスで行うのではなく、マテリアル単位で SSAO 適用の選択ができるので、ステンシルマスクなど使わずに必要なものだけ適応できるのが良いですね(例えばキャラの顔は除外、など)。</p> <h2><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a></h2> <h3>URP <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>について</h3> <p>こちらの Unity Japan さん公式動画でも言及されていますが、URP 12 より HDRP で先行搭載されていた<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>システムが追加されました。</p> <p><iframe width="720" height="540" src="https://www.youtube.com/embed/kchozuXTf4c?wmode=transparent" frameborder="0" allowfullscreen></iframe></p> <p>ドキュメントはこちら:</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4012.0%2Fmanual%2Frenderer-feature-decal.html" title="Decal Renderer Feature | Universal RP | 12.0.0" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/renderer-feature-decal.html">docs.unity3d.com</a></cite></p> <p>追加の仕方はこちらの記事を見ると良くわかります:</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Flight11.hatenadiary.com%2Fentry%2F2021%2F11%2F17%2F195730" title="【Unity】【URP】デカールを使ってオブジェクトにテクスチャを貼り付ける - LIGHT11" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://light11.hatenadiary.com/entry/2021/11/17/195730">light11.hatenadiary.com</a></cite></p> <h3>コードの対応</h3> <p>シェーダ側でこれに対応するのは簡単で、次のように <code>_DBUFFER</code> 部を追加するだけです。</p> <pre class="code lang-c" data-lang="c" data-unlink>float4 <span class="synIdentifier">Frag</span>(Varyings input) : SV_TARGET { ... <span class="synPreProc">#ifdef _DBUFFER</span> <span class="synIdentifier">ApplyDecalToSurfaceData</span>(input.positionCS, surfaceData, inputData); <span class="synPreProc">#endif</span> ... } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220424/20220424232937.png" width="1200" height="667" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>なお、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>を有効にするには以下のキーワードを有効にする必要があります。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3</span> </pre> <h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>の流れ</h3> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%AB%A1%BC%A5%EB">デカール</a>の手法は 2 種類、D-Buffer を使う方法と Screen Space で後処理でやる方法があります。D-Buffer を使う方法を見てみると、次のように DBuffer Render ステージで <code>_CameraDepthTexture</code> を参照しながら Decal Projector の画を投影したバッファ(<code>_DBufferTexture0</code> など、色や法線など)を生成します。これを使って通常のレンダーステージでこのバッファを参照しながら画を出力します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220424/20220424233509.gif" width="1200" height="855" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>もうひとつの手法の Screen Space の方では、オブジェクトごとの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>までは通常通り行い、その後にポストプロセス的に投影する形になります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220424/20220424233956.gif" width="1200" height="855" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>D-Buffer 形式ではフラグメントシェーダ内でパラメタとして入ってくるので、どのように投影するか好き勝手に改変できる形になります(適応しないマスクを用意したり、部分的にアルファで薄くしたり、UV をずらしたりなど)。一方で、<code>_DepthNormals</code> テクスチャを要求したりすることからパフォーマンス観点では少し劣ります。Screen Space の方はパフォーマンスが稼げる代わりに一括処理になるので細かい調整ができなかったりクオリティ面で少し劣るのがデメリットですね。</p> <h2>Depth Prepass </h2> <p>(2022/05/14 追記)</p> <h3>はじめに</h3> <p>色を決定するフラグメントシェーダの計算は PBR などを思い浮かべるとそれなりのコストが必要です。Depth Prepass は、この計算をする前にまず深度だけ計算して最終的にどのオブジェクトが最前面に描画されるかを求め、そのオブジェクトに対してのみカラーの計算を行う手法です。URP 12 から追加されました。詳しくは公式の keijiro さんによる解説をご参照ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Flearning.unity3d.jp%2F8234%2F" title="URP総点検!〜最近こんなの増えてました〜 | Unity Learning Materials" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://learning.unity3d.jp/8234/">learning.unity3d.jp</a></cite></p> <h3>ON の仕方</h3> <p>Universal Rendering Data アセットで Rendering > Rendering Path > Depth Priming Mode の選択で Depth Prepass を使用するかが指定できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220514/20220514152130.png" width="872" height="542" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>Disabled <ul> <li>Depth Prepass を使用しない</li> </ul> </li> <li>Forced <ul> <li>Depth Prepass を常に使用</li> </ul> </li> <li>Auto <ul> <li>既に事前パスがあるようなケース(例えば SSAO のように DepthNormals 計算が先駆けて必要なケース)のみ Depth Prepass を使用</li> </ul> </li> </ul> <h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>の流れ</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220514/20220514180414.gif" width="784" height="645" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このように事前にデプス生成のパスが差し込まれます(法線も必要なときは <code>DepthOnly</code> ではなく <code>DepthNormals</code> パスが代わりに使われます)。よく見てみると、DrawOpaqueObjects では <code>ZWrite Off</code> および <code>ZTest Equal</code> になっています。事前に作成したデプステクスチャを参照してデプスはそれをそのまま使い、デプスが一致する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>シェーダのみが起動されて計算が行われる、という流れになっているのが分かりますね。</p> <h2>G-Buffer パス</h2> <h3>概要</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4012.0%2Fmanual%2Frendering%2Fdeferred-rendering-path.html" title="Deferred Rendering Path in URP | Universal RP | 12.0.0" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/rendering/deferred-rendering-path.html">docs.unity3d.com</a></cite></p> <p>Deferred <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>が追加されました。URP は Legacy パイプラインと比べて(制限個数付き)複数ライトを 1 パスで使えるメリットがありますが、それでも大きい空間で動的なライトをたくさん描画するようなゲームだと厳しいです。そういったゲームへ向けても選択肢が増えるのは良いですね。Deferred <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>そのものについては <a class="keyword" href="http://d.hatena.ne.jp/keyword/Wikipedia">Wikipedia</a> がとても分かり易いのでそちらをご参照ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fja.wikipedia.org%2Fwiki%2F%25E9%2581%2585%25E5%25BB%25B6%25E3%2582%25B7%25E3%2582%25A7%25E3%2583%25BC%25E3%2583%2587%25E3%2582%25A3%25E3%2583%25B3%25E3%2582%25B0" title="遅延シェーディング - Wikipedia" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://ja.wikipedia.org/wiki/%E9%81%85%E5%BB%B6%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0">ja.wikipedia.org</a></cite></p> <h3>コード</h3> <p>では Deferred <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>のための G-Buffer を出力するパスを見てみましょう。</p> <pre class="code lang-c" data-lang="c" data-unlink>Pass { Name <span class="synConstant">&quot;GBuffer&quot;</span> Tags {<span class="synConstant">&quot;LightMode&quot;</span> = <span class="synConstant">&quot;UniversalGBuffer&quot;</span> } ZWrite [_ZWrite] ZTest LEqual Cull [_Cull] HLSLPROGRAM <span class="synPreProc">#pragma exclude_renderers gles gles3 glcore</span> <span class="synPreProc">#pragma target </span><span class="synConstant">4.5</span> <span class="synPreProc">#pragma shader_feature_local_fragment _ALPHATEST_ON</span> <span class="synPreProc">#pragma shader_feature_local_fragment _ _SPECGLOSSMAP _SPECULAR_COLOR</span> <span class="synPreProc">#pragma shader_feature_local_fragment _GLOSSINESS_FROM_BASE_ALPHA</span> <span class="synPreProc">#pragma shader_feature_local _NORMALMAP</span> <span class="synPreProc">#pragma shader_feature_local_fragment _EMISSION</span> <span class="synPreProc">#pragma shader_feature_local _RECEIVE_SHADOWS_OFF</span> <span class="synPreProc">#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN</span> <span class="synPreProc">#pragma multi_compile_fragment _ _SHADOWS_SOFT</span> <span class="synPreProc">#pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3</span> <span class="synPreProc">#pragma multi_compile_fragment _ _LIGHT_LAYERS</span> <span class="synPreProc">#pragma multi_compile _ DIRLIGHTMAP_COMBINED</span> <span class="synPreProc">#pragma multi_compile _ LIGHTMAP_ON</span> <span class="synPreProc">#pragma multi_compile _ DYNAMICLIGHTMAP_ON</span> <span class="synPreProc">#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING</span> <span class="synPreProc">#pragma multi_compile _ SHADOWS_SHADOWMASK</span> <span class="synPreProc">#pragma multi_compile_fragment _ _GBUFFER_NORMALS_OCT</span> <span class="synPreProc">#pragma multi_compile_fragment _ _RENDER_PASS_ENABLED</span> <span class="synPreProc">#pragma multi_compile_instancing</span> <span class="synPreProc">#pragma instancing_options renderinglayer</span> <span class="synPreProc">#pragma multi_compile _ DOTS_INSTANCING_ON</span> <span class="synPreProc">#pragma vertex LitPassVertexSimple</span> <span class="synPreProc">#pragma fragment LitPassFragmentSimple</span> <span class="synPreProc">#define BUMP_SCALE_NOT_SUPPORTED </span><span class="synConstant">1</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitInput.hlsl&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitGBufferPass.hlsl&quot;</span> ENDHLSL } </pre> <p>頂点・フラグメントシェーダは <em>SimpleLitGBufferPass.hlsl</em> に書かれているのでそちらを見てみます。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl&quot;</span> <span class="synPreProc">#include </span><span class="synConstant">&quot;Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityGBuffer.hlsl&quot;</span> <span class="synType">struct</span> Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; float2 texcoord : TEXCOORD0; float2 staticLightmapUV : TEXCOORD1; float2 dynamicLightmapUV : TEXCOORD2; UNITY_VERTEX_INPUT_INSTANCE_ID }; <span class="synType">struct</span> Varyings { float2 uv : TEXCOORD0; float3 posWS : TEXCOORD1; <span class="synComment">// xyz: posWS</span> <span class="synPreProc">#ifdef _NORMALMAP</span> half4 normal : TEXCOORD2; <span class="synComment">// xyz: normal, w: viewDir.x</span> half4 tangent : TEXCOORD3; <span class="synComment">// xyz: tangent, w: viewDir.y</span> half4 bitangent : TEXCOORD4; <span class="synComment">// xyz: bitangent, w: viewDir.z</span> <span class="synPreProc">#else</span> half3 normal : TEXCOORD2; <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef _ADDITIONAL_LIGHTS_VERTEX</span> half3 vertexLighting : TEXCOORD5; <span class="synComment">// xyz: vertex light</span> <span class="synPreProc">#endif</span> <span class="synPreProc">#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)</span> float4 shadowCoord : TEXCOORD6; <span class="synPreProc">#endif</span> <span class="synIdentifier">DECLARE_LIGHTMAP_OR_SH</span>(staticLightmapUV, vertexSH, <span class="synConstant">7</span>); <span class="synPreProc">#ifdef DYNAMICLIGHTMAP_ON</span> float2 dynamicLightmapUV : TEXCOORD8; <span class="synComment">// Dynamic lightmap UVs</span> <span class="synPreProc">#endif</span> float4 positionCS : SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; <span class="synType">void</span> <span class="synIdentifier">InitializeInputData</span>( Varyings input, half3 normalTS, out InputData inputData) { inputData = (InputData)<span class="synConstant">0</span>; inputData.positionWS = input.posWS; inputData.positionCS = input.positionCS; <span class="synPreProc">#ifdef _NORMALMAP</span> half3 viewDirWS = <span class="synIdentifier">half3</span>( input.normal.w, input.tangent.w, input.bitangent.w); inputData.normalWS = <span class="synIdentifier">TransformTangentToWorld</span>( normalTS, <span class="synIdentifier">half3x3</span>( input.tangent.xyz, input.bitangent.xyz, input.normal.xyz)); <span class="synPreProc">#else</span> half3 viewDirWS = <span class="synIdentifier">GetWorldSpaceNormalizeViewDir</span>(inputData.positionWS); inputData.normalWS = input.normal; <span class="synPreProc">#endif</span> inputData.normalWS = <span class="synIdentifier">NormalizeNormalPerPixel</span>(inputData.normalWS); viewDirWS = <span class="synIdentifier">SafeNormalize</span>(viewDirWS); inputData.viewDirectionWS = viewDirWS; <span class="synPreProc">#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)</span> inputData.shadowCoord = input.shadowCoord; <span class="synPreProc">#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)</span> inputData.shadowCoord = <span class="synIdentifier">TransformWorldToShadowCoord</span>(inputData.positionWS); <span class="synPreProc">#else</span> inputData.shadowCoord = <span class="synIdentifier">float4</span>(<span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>); <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef _ADDITIONAL_LIGHTS_VERTEX</span> inputData.vertexLighting = input.vertexLighting.xyz; <span class="synPreProc">#else</span> inputData.vertexLighting = <span class="synIdentifier">half3</span>(<span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>); <span class="synPreProc">#endif</span> inputData.fogCoord = <span class="synConstant">0</span>; <span class="synComment">// we don't apply fog in the gbuffer pass</span> <span class="synPreProc">#if defined(DYNAMICLIGHTMAP_ON)</span> inputData.bakedGI = <span class="synIdentifier">SAMPLE_GI</span>( input.staticLightmapUV, input.dynamicLightmapUV, input.vertexSH, inputData.normalWS); <span class="synPreProc">#else</span> inputData.bakedGI = <span class="synIdentifier">SAMPLE_GI</span>( input.staticLightmapUV, input.vertexSH, inputData.normalWS); <span class="synPreProc">#endif</span> inputData.normalizedScreenSpaceUV = <span class="synIdentifier">GetNormalizedScreenSpaceUV</span>(input.positionCS); inputData.shadowMask = <span class="synIdentifier">SAMPLE_SHADOWMASK</span>(input.staticLightmapUV); <span class="synPreProc">#if defined(DEBUG_DISPLAY)</span> <span class="synPreProc">#if defined(DYNAMICLIGHTMAP_ON)</span> inputData.dynamicLightmapUV = input.dynamicLightmapUV; <span class="synPreProc">#endif</span> <span class="synPreProc">#if defined(LIGHTMAP_ON)</span> inputData.staticLightmapUV = input.staticLightmapUV; <span class="synPreProc">#else</span> inputData.vertexSH = input.vertexSH; <span class="synPreProc">#endif</span> <span class="synPreProc">#endif</span> } Varyings <span class="synIdentifier">LitPassVertexSimple</span>(Attributes input) { Varyings output = (Varyings)<span class="synConstant">0</span>; <span class="synIdentifier">UNITY_SETUP_INSTANCE_ID</span>(input); <span class="synIdentifier">UNITY_TRANSFER_INSTANCE_ID</span>(input, output); <span class="synIdentifier">UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO</span>(output); VertexPositionInputs vertexInput = <span class="synIdentifier">GetVertexPositionInputs</span>(input.positionOS.xyz); VertexNormalInputs normalInput = <span class="synIdentifier">GetVertexNormalInputs</span>(input.normalOS, input.tangentOS); output.uv = <span class="synIdentifier">TRANSFORM_TEX</span>(input.texcoord, _BaseMap); output.posWS.xyz = vertexInput.positionWS; output.positionCS = vertexInput.positionCS; <span class="synPreProc">#ifdef _NORMALMAP</span> half3 viewDirWS = <span class="synIdentifier">GetWorldSpaceNormalizeViewDir</span>(vertexInput.positionWS); output.normal = <span class="synIdentifier">half4</span>(normalInput.normalWS, viewDirWS.x); output.tangent = <span class="synIdentifier">half4</span>(normalInput.tangentWS, viewDirWS.y); output.bitangent = <span class="synIdentifier">half4</span>(normalInput.bitangentWS, viewDirWS.z); <span class="synPreProc">#else</span> output.normal = <span class="synIdentifier">NormalizeNormalPerVertex</span>(normalInput.normalWS); <span class="synPreProc">#endif</span> <span class="synIdentifier">OUTPUT_LIGHTMAP_UV</span>( input.staticLightmapUV, unity_LightmapST, output.staticLightmapUV); <span class="synPreProc">#ifdef DYNAMICLIGHTMAP_ON</span> output.dynamicLightmapUV = input.dynamicLightmapUV.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw; <span class="synPreProc">#endif</span> <span class="synIdentifier">OUTPUT_SH</span>(output.normal.xyz, output.vertexSH); <span class="synPreProc">#ifdef _ADDITIONAL_LIGHTS_VERTEX</span> half3 vertexLight = <span class="synIdentifier">VertexLighting</span>( vertexInput.positionWS, normalInput.normalWS); output.vertexLighting = vertexLight; <span class="synPreProc">#endif</span> <span class="synPreProc">#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)</span> output.shadowCoord = <span class="synIdentifier">GetShadowCoord</span>(vertexInput); <span class="synPreProc">#endif</span> <span class="synStatement">return</span> output; } FragmentOutput <span class="synIdentifier">LitPassFragmentSimple</span>(Varyings input) { <span class="synIdentifier">UNITY_SETUP_INSTANCE_ID</span>(input); <span class="synIdentifier">UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX</span>(input); SurfaceData surfaceData; <span class="synIdentifier">InitializeSimpleLitSurfaceData</span>(input.uv, surfaceData); InputData inputData; <span class="synIdentifier">InitializeInputData</span>(input, surfaceData.normalTS, inputData); <span class="synIdentifier">SETUP_DEBUG_TEXTURE_DATA</span>(inputData, input.uv, _BaseMap); <span class="synPreProc">#ifdef _DBUFFER</span> <span class="synIdentifier">ApplyDecalToSurfaceData</span>(input.positionCS, surfaceData, inputData); <span class="synPreProc">#endif</span> Light mainLight = <span class="synIdentifier">GetMainLight</span>( inputData.shadowCoord, inputData.positionWS, inputData.shadowMask); <span class="synIdentifier">MixRealtimeAndBakedGI</span>( mainLight, inputData.normalWS, inputData.bakedGI, inputData.shadowMask); half4 color = <span class="synIdentifier">half4</span>( inputData.bakedGI * surfaceData.albedo + surfaceData.emission, surfaceData.alpha); <span class="synStatement">return</span> <span class="synIdentifier">SurfaceDataToGbuffer</span>( surfaceData, inputData, color.rgb, kLightingSimpleLit); } </pre> <p>ちょっと長いので迷子になりますが...、G-Buffer パスの見どころはフラグメントシェーダです。<code>LitPassFragmentSimple</code> を見てみると、<code>FragmentOutput</code> が出力構造体として指定されています。これは以下のようになっています。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synType">struct</span> FragmentOutput { half4 GBuffer0 : SV_Target0; half4 GBuffer1 : SV_Target1; half4 GBuffer2 : SV_Target2; half4 GBuffer3 : SV_Target3; <span class="synPreProc">#ifdef GBUFFER_OPTIONAL_SLOT_1</span> GBUFFER_OPTIONAL_SLOT_1_TYPE GBuffer4 : SV_Target4; <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef GBUFFER_OPTIONAL_SLOT_2</span> half4 GBuffer5 : SV_Target5; <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef GBUFFER_OPTIONAL_SLOT_3</span> half4 GBuffer6 : SV_Target6; <span class="synPreProc">#endif</span> }; </pre> <p>基本 4 つ、最大 7 個の G-Buffer の出力ができるようになっています。ここに対して <code>SurfaceDataToGbuffer()</code> で出力するための情報を色々かき集めている、というのが全体像です。最初にこの出力部である <code>SurfaceDataToGbuffer()</code> を見てみましょう。</p> <pre class="code lang-c" data-lang="c" data-unlink>FragmentOutput <span class="synIdentifier">SurfaceDataToGbuffer</span>( SurfaceData surfaceData, InputData inputData, half3 globalIllumination, <span class="synType">int</span> lightingMode) { half3 packedNormalWS = <span class="synIdentifier">PackNormal</span>(inputData.normalWS); uint materialFlags = <span class="synConstant">0</span>; <span class="synPreProc">#ifdef _RECEIVE_SHADOWS_OFF</span> materialFlags |= kMaterialFlagReceiveShadowsOff; <span class="synPreProc">#endif</span> <span class="synPreProc">#if defined(LIGHTMAP_ON) &amp;&amp; defined(_MIXED_LIGHTING_SUBTRACTIVE)</span> materialFlags |= kMaterialFlagSubtractiveMixedLighting; <span class="synPreProc">#endif</span> <span class="synComment">// albedo albedo albedo flags</span> <span class="synComment">// specular specular specular occlusion</span> <span class="synComment">// normal normal normal smoothness</span> <span class="synComment">// GI GI GI [optional]</span> FragmentOutput output; output.GBuffer0 = <span class="synIdentifier">half4</span>(surfaceData.albedo.rgb, <span class="synIdentifier">PackMaterialFlags</span>(materialFlags)); output.GBuffer1 = <span class="synIdentifier">half4</span>(surfaceData.specular.rgb, surfaceData.occlusion); output.GBuffer2 = <span class="synIdentifier">half4</span>(packedNormalWS, surfaceData.smoothness); output.GBuffer3 = <span class="synIdentifier">half4</span>(globalIllumination, <span class="synConstant">1</span>); <span class="synPreProc">#if _RENDER_PASS_ENABLED</span> output.GBuffer4 = inputData.positionCS.z; <span class="synPreProc">#endif</span> <span class="synPreProc">#if OUTPUT_SHADOWMASK</span> <span class="synComment">// will have unity_ProbesOcclusion value if subtractive lighting is used (baked)</span> output.GBUFFER_SHADOWMASK = inputData.shadowMask; <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef _LIGHT_LAYERS</span> uint renderingLayers = <span class="synIdentifier">GetMeshRenderingLightLayer</span>(); output.GBUFFER_LIGHT_LAYERS = <span class="synIdentifier">float4</span>( (renderingLayers &amp; <span class="synConstant">0x000000FF</span>) / <span class="synConstant">255.0</span>, <span class="synConstant">0.0</span>, <span class="synConstant">0.0</span>, <span class="synConstant">0.0</span>); <span class="synPreProc">#endif</span> <span class="synStatement">return</span> output; } </pre> <p>G-Buffer のレイアウトとしては、Albedo / Specular / Normal / GI が RGB チャンネルに、マテリアルのフラグ / Occlusion / Smoothness がアルファチャネルに書き込まれています。あとは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>オプションに応じて、5 ~ 7 チャンネルに追加の情報が書き込まれる形式ですね(+ デプス・ステンシル用の出力があります)。より詳細な説明は<a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/rendering/deferred-rendering-path.html">&#x30C9;&#x30AD;&#x30E5;&#x30E1;&#x30F3;&#x30C8;</a>に記載されています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220429/20220429172229.png" width="690" height="554" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>順番に見ていきましょう。</p> <h3>GBuffer 0</h3> <p>ここには Albedo とマテリアルのフラグが書き込まれます。まずは Albedo から見ましょう。ここには <code>SurfaceData.albedo</code> が書き込まれます。<code>SurfaceData</code> は <code>InitializeSimpleLitSurfaceData()</code> によって初期化されます。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synType">inline</span> <span class="synType">void</span> <span class="synIdentifier">InitializeSimpleLitSurfaceData</span>(float2 uv, out SurfaceData outSurfaceData) { outSurfaceData = (SurfaceData)<span class="synConstant">0</span>; half4 albedoAlpha = <span class="synIdentifier">SampleAlbedoAlpha</span>( uv, <span class="synIdentifier">TEXTURE2D_ARGS</span>(_BaseMap, sampler_BaseMap)); outSurfaceData.alpha = albedoAlpha.a * _BaseColor.a; <span class="synIdentifier">AlphaDiscard</span>(outSurfaceData.alpha, _Cutoff); outSurfaceData.albedo = albedoAlpha.rgb * _BaseColor.rgb; outSurfaceData.albedo = <span class="synIdentifier">AlphaModulate</span>( outSurfaceData.albedo, outSurfaceData.alpha); half4 specularSmoothness = <span class="synIdentifier">SampleSpecularSmoothness</span>( uv, outSurfaceData.alpha, _SpecColor, <span class="synIdentifier">TEXTURE2D_ARGS</span>(_SpecGlossMap, sampler_SpecGlossMap)); outSurfaceData.metallic = <span class="synConstant">0.0</span>; outSurfaceData.specular = specularSmoothness.rgb; outSurfaceData.smoothness = specularSmoothness.a; outSurfaceData.normalTS = <span class="synIdentifier">SampleNormal</span>( uv, <span class="synIdentifier">TEXTURE2D_ARGS</span>(_BumpMap, sampler_BumpMap)); outSurfaceData.occlusion = <span class="synConstant">1.0</span>; outSurfaceData.emission = <span class="synIdentifier">SampleEmission</span>( uv, _EmissionColor.rgb, <span class="synIdentifier">TEXTURE2D_ARGS</span>(_EmissionMap, sampler_EmissionMap)); } </pre> <p>基本的には <code>_BaseColor</code> を設定しています。<code>_ALPHAMODULATE_ON</code> による <code>AlphaModulate()</code> は乗算<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>の関係なので飛ばします。</p> <p>マテリアルのフラグは以下のように定義されています。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#define kMaterialFlagReceiveShadowsOff </span><span class="synConstant">1</span><span class="synPreProc"> </span><span class="synComment">// Does not receive dynamic shadows</span> <span class="synPreProc">#define kMaterialFlagSpecularHighlightsOff </span><span class="synConstant">2</span><span class="synPreProc"> </span><span class="synComment">// Does not receivce specular</span> <span class="synPreProc">#define kMaterialFlagSubtractiveMixedLighting </span><span class="synConstant">4</span><span class="synPreProc"> </span><span class="synComment">// The geometry uses subtractive mixed lighting</span> <span class="synPreProc">#define kMaterialFlagSpecularSetup </span><span class="synConstant">8</span><span class="synPreProc"> </span><span class="synComment">// Lit material use specular setup instead of metallic setup</span> <span class="synType">float</span> <span class="synIdentifier">PackMaterialFlags</span>(uint materialFlags) { <span class="synStatement">return</span> materialFlags * (<span class="synConstant">1.0</span>h / <span class="synConstant">255.0</span>h); } uint <span class="synIdentifier">UnpackMaterialFlags</span>(<span class="synType">float</span> packedMaterialFlags) { <span class="synStatement">return</span> <span class="synIdentifier">uint</span>((packedMaterialFlags * <span class="synConstant">255.0</span>h) + <span class="synConstant">0.5</span>h); } </pre> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>のオプションをビットでフラグを立てておき、float に変換・逆変換して G-Buffer に乗せておきます。これで Deferred の後段のシェーディングのタイミングでこのフラグを参照して、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>単位で異なる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>が出来るわけですね。</p> <h3>GBuffer 1</h3> <p>GBuffer 1 には Specular と Occlusion が格納されています。こちらも同様に <code>SurfaceData</code> から渡ってくるマテリアルごとの情報になってます。</p> <p>Simple Lit シェーダでは Spcular の値が書き込まれるだけですが、Lit シェーダでは Specular ワークフローか Metallic ワークフローかで書き込まれる値が変わります(Metallic の場合は R のみ使われ、GB は使われません)。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2F2021.3%2FManual%2FStandardShaderMetallicVsSpecular.html" title="Metallic と Specular のワークフロー - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/2021.3/Manual/StandardShaderMetallicVsSpecular.html">docs.unity3d.com</a></cite></p> <p>Occlusion に関しては、テクスチャとして焼き込まれた値と SSAO による値が統合されて出力されるようです。</p> <h3>GBuffer 2</h3> <p>GBuffer 2 には法線と Smoothness が格納されています。Smoothness の方は同様に <code>SurfaceData</code> から渡ってくるものですが、法線の方は少しオプションがあるので見てみましょう。</p> <p>法線は <code>InputData</code> に格納されています。法線は法線マップがあることも考慮しながら頂点シェーダから渡ってくる情報も使って<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>単位で算出します。法線が G-Buffer へと送られる過程を見てみましょう。</p> <pre class="code lang-c" data-lang="c" data-unlink>Varyings <span class="synIdentifier">LitPassVertexSimple</span>(Attributes input) { ... VertexNormalInputs normalInput = <span class="synIdentifier">GetVertexNormalInputs</span>(input.normalOS, input.tangentOS); ... <span class="synPreProc">#ifdef _NORMALMAP</span> half3 viewDirWS = <span class="synIdentifier">GetWorldSpaceNormalizeViewDir</span>(vertexInput.positionWS); output.normal = <span class="synIdentifier">half4</span>(normalInput.normalWS, viewDirWS.x); output.tangent = <span class="synIdentifier">half4</span>(normalInput.tangentWS, viewDirWS.y); output.bitangent = <span class="synIdentifier">half4</span>(normalInput.bitangentWS, viewDirWS.z); <span class="synPreProc">#else</span> output.normal = <span class="synIdentifier">NormalizeNormalPerVertex</span>(normalInput.normalWS); <span class="synPreProc">#endif</span> ... } FragmentOutput <span class="synIdentifier">LitPassFragmentSimple</span>(Varyings input) { ... InputData inputData; <span class="synIdentifier">InitializeInputData</span>(input, surfaceData.normalTS, inputData); ... <span class="synStatement">return</span> <span class="synIdentifier">SurfaceDataToGbuffer</span>(..., inputData, ...); }; <span class="synType">void</span> <span class="synIdentifier">InitializeInputData</span>( Varyings input, half3 normalTS, out InputData inputData) { ... <span class="synPreProc">#ifdef _NORMALMAP</span> half3 viewDirWS = <span class="synIdentifier">half3</span>( input.normal.w, input.tangent.w, input.bitangent.w); inputData.normalWS = <span class="synIdentifier">TransformTangentToWorld</span>( normalTS, <span class="synIdentifier">half3x3</span>( input.tangent.xyz, input.bitangent.xyz, input.normal.xyz)); <span class="synPreProc">#else</span> half3 viewDirWS = <span class="synIdentifier">GetWorldSpaceNormalizeViewDir</span>(inputData.positionWS); inputData.normalWS = input.normal; <span class="synPreProc">#endif</span> inputData.normalWS = <span class="synIdentifier">NormalizeNormalPerPixel</span>(inputData.normalWS); ... } FragmentOutput <span class="synIdentifier">SurfaceDataToGbuffer</span>(..., InputData inputData, ...) { half3 packedNormalWS = <span class="synIdentifier">PackNormal</span>(inputData.normalWS); ... output.GBuffer2 = <span class="synIdentifier">half4</span>(packedNormalWS, ...); } </pre> <p><code>NormalizeNormalPerVertex</code> も <code>NormalizeNormalPerPixel()</code> も今のところは内部では <code>normalize()</code> しているだけです。法線マップが必要なときは <code>tangent</code> と <code>bitangent</code> が必要な点などは通常と同じですが、おもしろポイントは <code>SurfaceDataToGbuffer()</code> の中で行っている <code>PackNormal()</code> です。これの実装を見てみましょう。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synPreProc">#ifdef _GBUFFER_NORMALS_OCT</span> half3 <span class="synIdentifier">PackNormal</span>(half3 n) { float2 octNormalWS = <span class="synIdentifier">PackNormalOctQuadEncode</span>(n); float2 remappedOctNormalWS = <span class="synIdentifier">saturate</span>(octNormalWS * <span class="synConstant">0.5</span> + <span class="synConstant">0.5</span>); <span class="synStatement">return</span> <span class="synIdentifier">half3</span>(<span class="synIdentifier">PackFloat2To888</span>(remappedOctNormalWS)); } half3 <span class="synIdentifier">UnpackNormal</span>(half3 pn) { half2 remappedOctNormalWS = <span class="synIdentifier">half2</span>(<span class="synIdentifier">Unpack888ToFloat2</span>(pn)); half2 octNormalWS = remappedOctNormalWS.xy * <span class="synIdentifier">half</span>(<span class="synConstant">2.0</span>) - <span class="synIdentifier">half</span>(<span class="synConstant">1.0</span>); <span class="synStatement">return</span> <span class="synIdentifier">half3</span>(<span class="synIdentifier">UnpackNormalOctQuadEncode</span>(octNormalWS)); } <span class="synPreProc">#else</span> half3 <span class="synIdentifier">PackNormal</span>(half3 n) { <span class="synStatement">return</span> n; } half3 <span class="synIdentifier">UnpackNormal</span>(half3 pn) { <span class="synStatement">return</span> pn; } <span class="synPreProc">#endif</span> </pre> <p><code>_GBUFFER_NORMALS_OCT</code> というマクロが設定されている時に、<code>PackNormalOctQuadEncode()</code> というコードを経由しています。これは G-Buffer 上の法線の格納領域は 8 bit x 3 = 24 bit しかないのですが、八面体に射影した 2 次元ベクトル(<code>float2</code>)にパックしたものを <code>half3</code> に変換、実際の使用時(ライティング時)に逆変換することで精度向上を行うための仕組みです。Universal Rendering Data で <code>Accurate G-Buffer normals</code> というチェックをオンにすることでマクロが ON になります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220429/20220429174613.png" width="838" height="428" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ON / OFF の違いはドキュメントを御覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4012.0%2Fmanual%2Frendering%2Fdeferred-rendering-path.html%23accurate-g-buffer-normals" title="Deferred Rendering Path in URP | Universal RP | 12.0.0" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/rendering/deferred-rendering-path.html#accurate-g-buffer-normals">docs.unity3d.com</a></cite></p> <p>論文はこちら:</p> <ul> <li><a href="https://jcgt.org/published/0003/02/01/paper.pdf">Cigolle, Zina H., et al. &quot;A survey of efficient representations for independent unit vectors.&quot; Journal of Computer Graphics Techniques 3.2 (2014).</a></li> </ul> <p>パックに関連したコードはこちら:</p> <pre class="code lang-c" data-lang="c" data-unlink>float2 <span class="synIdentifier">PackNormalOctQuadEncode</span>(float3 n) { n *= <span class="synIdentifier">rcp</span>(<span class="synIdentifier">max</span>(<span class="synIdentifier">dot</span>(<span class="synIdentifier">abs</span>(n), <span class="synConstant">1.0</span>), <span class="synConstant">1e-6</span>)); <span class="synType">float</span> t = <span class="synIdentifier">saturate</span>(-n.z); <span class="synStatement">return</span> n.xy + (n.xy &gt;= <span class="synConstant">0.0</span> ? t : -t); } float3 <span class="synIdentifier">UnpackNormalOctQuadEncode</span>(float2 f) { float3 n = <span class="synIdentifier">float3</span>(f.x, f.y, <span class="synConstant">1.0</span> - <span class="synIdentifier">abs</span>(f.x) - <span class="synIdentifier">abs</span>(f.y)); <span class="synType">float</span> t = <span class="synIdentifier">max</span>(-n.z, <span class="synConstant">0.0</span>); n.xy += n.xy &gt;= <span class="synConstant">0.0</span> ? -t.xx : t.xx; <span class="synStatement">return</span> <span class="synIdentifier">normalize</span>(n); } uint3 <span class="synIdentifier">PackFloat2To888UInt</span>(float2 f) { uint2 i = (uint2)(f * <span class="synConstant">4095.5</span>); uint2 hi = i &gt;&gt; <span class="synConstant">8</span>; uint2 lo = i &amp; <span class="synConstant">255</span>; uint3 cb = <span class="synIdentifier">uint3</span>(lo, hi.x | (hi.y &lt;&lt; <span class="synConstant">4</span>)); <span class="synStatement">return</span> cb; } float3 <span class="synIdentifier">PackFloat2To888</span>(float2 f) { <span class="synStatement">return</span> <span class="synIdentifier">PackFloat2To888UInt</span>(f) / <span class="synConstant">255.0</span>; } float2 <span class="synIdentifier">Unpack888UIntToFloat2</span>(uint3 x) { uint hi = x.z &gt;&gt; <span class="synConstant">4</span>; uint lo = x.z &amp; <span class="synConstant">15</span>; uint2 cb = x.xy | <span class="synIdentifier">uint2</span>(lo &lt;&lt; <span class="synConstant">8</span>, hi &lt;&lt; <span class="synConstant">8</span>); <span class="synStatement">return</span> cb / <span class="synConstant">4095.0</span>; } float2 <span class="synIdentifier">Unpack888ToFloat2</span>(float3 x) { uint3 i = (uint3)(x * <span class="synConstant">255.5</span>); <span class="synStatement">return</span> <span class="synIdentifier">Unpack888UIntToFloat2</span>(i); } </pre> <p>面白いですね。計算負荷が少し上がる(デスクトップアプリだとほとんど影響がないくらい)代わりに法線精度が上がりライティングがきれいになるので、リリースプラットフォームに応じて検討すると良さそうです。なお、ON にすると法線の出力が面白い感じになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220430/20220430003901.png" width="736" height="520" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>GBuffer 3</h3> <p>GBuffer 3 には Emissive やベイクしたライティングに関する情報が格納されます。関連コードのみ抜き出して見てみます。</p> <pre class="code lang-c" data-lang="c" data-unlink>Varyings <span class="synIdentifier">LitPassVertexSimple</span>(Attributes input) { ... <span class="synIdentifier">OUTPUT_LIGHTMAP_UV</span>( input.staticLightmapUV, unity_LightmapST, output.staticLightmapUV); <span class="synPreProc">#ifdef DYNAMICLIGHTMAP_ON</span> output.dynamicLightmapUV = input.dynamicLightmapUV.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw; <span class="synPreProc">#endif</span> <span class="synIdentifier">OUTPUT_SH</span>(output.normal.xyz, output.vertexSH); <span class="synPreProc">#ifdef _ADDITIONAL_LIGHTS_VERTEX</span> half3 vertexLight = <span class="synIdentifier">VertexLighting</span>(vertexInput.positionWS, normalInput.normalWS); output.vertexLighting = vertexLight; <span class="synPreProc">#endif</span> <span class="synPreProc">#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)</span> output.shadowCoord = <span class="synIdentifier">GetShadowCoord</span>(vertexInput); <span class="synPreProc">#endif</span> ... } FragmentOutput <span class="synIdentifier">LitPassFragmentSimple</span>(Varyings input) { SurfaceData surfaceData; <span class="synIdentifier">InitializeSimpleLitSurfaceData</span>(input.uv, surfaceData); InputData inputData; <span class="synIdentifier">InitializeInputData</span>(input, surfaceData.normalTS, inputData); <span class="synIdentifier">SETUP_DEBUG_TEXTURE_DATA</span>(inputData, input.uv, _BaseMap); <span class="synPreProc">#ifdef _DBUFFER</span> <span class="synIdentifier">ApplyDecalToSurfaceData</span>(input.positionCS, surfaceData, inputData); <span class="synPreProc">#endif</span> Light mainLight = <span class="synIdentifier">GetMainLight</span>( inputData.shadowCoord, inputData.positionWS, inputData.shadowMask); <span class="synIdentifier">MixRealtimeAndBakedGI</span>( mainLight, inputData.normalWS, inputData.bakedGI, inputData.shadowMask); half4 color = <span class="synIdentifier">half4</span>( inputData.bakedGI * surfaceData.albedo + surfaceData.emission, surfaceData.alpha); <span class="synStatement">return</span> <span class="synIdentifier">SurfaceDataToGbuffer</span>(..., color.rgb, ...); }; <span class="synType">void</span> <span class="synIdentifier">InitializeInputData</span>(Varyings input, half3 normalTS, out InputData inputData) { ... <span class="synPreProc">#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)</span> inputData.shadowCoord = input.shadowCoord; <span class="synPreProc">#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)</span> inputData.shadowCoord = <span class="synIdentifier">TransformWorldToShadowCoord</span>(inputData.positionWS); <span class="synPreProc">#else</span> inputData.shadowCoord = <span class="synIdentifier">float4</span>(<span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>); <span class="synPreProc">#endif</span> <span class="synPreProc">#ifdef _ADDITIONAL_LIGHTS_VERTEX</span> inputData.vertexLighting = input.vertexLighting.xyz; <span class="synPreProc">#else</span> inputData.vertexLighting = <span class="synIdentifier">half3</span>(<span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>); <span class="synPreProc">#endif</span> ... <span class="synPreProc">#if defined(DYNAMICLIGHTMAP_ON)</span> inputData.bakedGI = <span class="synIdentifier">SAMPLE_GI</span>( input.staticLightmapUV, input.dynamicLightmapUV, input.vertexSH, inputData.normalWS); <span class="synPreProc">#else</span> inputData.bakedGI = <span class="synIdentifier">SAMPLE_GI</span>( input.staticLightmapUV, input.vertexSH, inputData.normalWS); <span class="synPreProc">#endif</span> inputData.normalizedScreenSpaceUV = <span class="synIdentifier">GetNormalizedScreenSpaceUV</span>(input.positionCS); inputData.shadowMask = <span class="synIdentifier">SAMPLE_SHADOWMASK</span>(input.staticLightmapUV); ... } </pre> <p>おおまかな流れとしては、<code>InputData.bakedGI</code> にライトマップや球面調和ライティングを書き込み、それを <code>albedo</code> とかけて得られた値を <code>emissive</code> と一緒に書き込んでいる形です。しかしながら途中で <code>MixRealtimeAndBakedGI()</code> というものを経由しています。これは以下のような関数です。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synComment">// 後方互換性のため</span> <span class="synType">void</span> <span class="synIdentifier">MixRealtimeAndBakedGI</span>(inout Light light, half3 normalWS, inout half3 bakedGI, half4 shadowMask) { <span class="synIdentifier">MixRealtimeAndBakedGI</span>(light, normalWS, bakedGI); } <span class="synType">void</span> <span class="synIdentifier">MixRealtimeAndBakedGI</span>(inout Light light, half3 normalWS, inout half3 bakedGI) { <span class="synPreProc">#if defined(LIGHTMAP_ON) &amp;&amp; defined(_MIXED_LIGHTING_SUBTRACTIVE)</span> bakedGI = <span class="synIdentifier">SubtractDirectMainLightFromLightmap</span>(light, normalWS, bakedGI); <span class="synPreProc">#endif</span> } </pre> <p><code>LIGHTMAP_ON</code> かつ <code>_MIXED_LIGHTING_SUBTRACTIVE</code> の時にのみ <code>SubtractDirectMainLightFromLightmap()</code> というのが実行されてますね。これは静的オブジェクト向けで、Mixed Lighting > Lighting Mode が Subtractive の時にベイク影とリアルタイム影をマージする処理を行うところです。このモードのときには Realtime Shadow Color で指定した色で影が制御できるようになります。<a href="https://twitter.com/t_tutiya">&#x571F;&#x5C4B;&#x3064;&#x304B;&#x3055;</a>さんの解説を読むと分かりやすいです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsomeiyoshino.info%2Fentry%2F2020%2F08%2F16%2F181046" title="#unity Mixed LightingのSubtractiveモードでのリアルタイムシャドウ色の手動調整について解説する - 土屋つかさの技術ブログは今か無しか" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://someiyoshino.info/entry/2020/08/16/181046">someiyoshino.info</a></cite></p> <p>自分は GI 周りはあまり深く追ったことが無いのでいずれ調べたいです。</p> <h3>GBuffer 4</h3> <p>Lighting Mode が Subtractive か Shadow Mask に設定されている際にこの G-Buffer が追加されます。具体的にどう使われるかは追えていません。。なお、Deferred <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>では、パフォーマンスの観点からこの 2 つの Lighting Mode は推奨されておらず、Baked Indirect を使うことが推奨されています。</p> <h3>GBuffer 5</h3> <p>これは Light Layer 用の G-Buffer になります。URP アセットで Advanced > Light Layers を ON にすると使える、オブジェクト単位にライトが影響するかしないかを決められる機能です。詳細はドキュメントを御覧ください:</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4012.0%2Fmanual%2Frendering%2Fdeferred-rendering-path.html%23light-layers" title="Deferred Rendering Path in URP | Universal RP | 12.0.0" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/rendering/deferred-rendering-path.html#light-layers">docs.unity3d.com</a></cite></p> <h3>GBuffer 6</h3> <p>Vulkan を使ったデ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>向けに用意された G-Buffer のようです。</p> <h2>Deferred <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a></h2> <h3>Deferred Pass</h3> <p>さて、こうして出力した G-Buffer は後段の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>パス(Deferred Pass)で実際に画として出力されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220429/20220429234601.png" width="1200" height="891" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>は <em>com.unity.render-pipelines.universal/Shaders/Utils/StencilDeferred.shader</em> に記述されているシェーダで行います。外観としてはこんな感じです。</p> <pre class="code lang-c" data-lang="c" data-unlink>Shader <span class="synConstant">&quot;Hidden/Universal Render Pipeline/StencilDeferred&quot;</span> { ... SubShader { Tags { <span class="synConstant">&quot;RenderType&quot;</span> = <span class="synConstant">&quot;Opaque&quot;</span> <span class="synConstant">&quot;RenderPipeline&quot;</span> = <span class="synConstant">&quot;UniversalPipeline&quot;</span> } Pass { Name <span class="synConstant">&quot;Stencil Volume&quot;</span> ... } Pass { Name <span class="synConstant">&quot;Deferred Punctual Light (Lit)&quot;</span> ... } Pass { Name <span class="synConstant">&quot;Deferred Punctual Light (SimpleLit)&quot;</span> ... } Pass { Name <span class="synConstant">&quot;Deferred Directional Light (Lit)&quot;</span> ... } Pass { Name <span class="synConstant">&quot;Deferred Directional Light (SimpleLit)&quot;</span> ... } Pass { Name <span class="synConstant">&quot;Fog&quot;</span> ... } Pass { Name <span class="synConstant">&quot;ClearStencilPartial&quot;</span> ... } Pass { Name <span class="synConstant">&quot;SSAOOnly&quot;</span> ... } } FallBack <span class="synConstant">&quot;Hidden/Universal Render Pipeline/FallbackError&quot;</span> } </pre> <p>いくつかのパスがありますね。実はマテリアル毎に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>時にステンシルが書き込まれており、それに応じたパスが実行される仕組みのようです。例えば <code>Lit</code> シェーダなら 32(0b100000)、<code>SimpleLit</code> シェーダなら 64(0b1000000)といった具合です。そのステンシルに対応した Deferred のパスが使われてライティングが行われます。パフォーマンスのためかバッファがクリアされないのでちょっと見づらいですが、Frame Debugger で見ると、ステンシル値の書き込みや参照している様子を確認できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220430/20220430003110.gif" width="822" height="859" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>しかしながら <code>Lit</code> や <code>SimpleLit</code> の ShaderLab を見てみてもステンシルの書き込み部分はありません。。これは ShaderLab 内の Tags の <code>UniversalMaterialType</code> を参照して、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>側の <em>Packages/com.unity.render-pipelines.universal/Runtime/Passes/GBufferPass.cs</em> 内でステンシルが上書きされているようです。<code>Lit</code> と <code>SimpleLit</code> の ShaderLab の該当部分を少し見てみましょう。</p> <h6>Simple Lit</h6> <pre class="code lang-c" data-lang="c" data-unlink>Shader <span class="synConstant">&quot;Universal Render Pipeline/Simple Lit&quot;</span> { ... SubShader { Tags { <span class="synConstant">&quot;RenderType&quot;</span> = <span class="synConstant">&quot;Opaque&quot;</span> <span class="synConstant">&quot;RenderPipeline&quot;</span> = <span class="synConstant">&quot;UniversalPipeline&quot;</span> <span class="synConstant">&quot;UniversalMaterialType&quot;</span> = <span class="synConstant">&quot;SimpleLit&quot;</span> <span class="synConstant">&quot;IgnoreProjector&quot;</span> = <span class="synConstant">&quot;True&quot;</span> <span class="synConstant">&quot;ShaderModel&quot;</span>=<span class="synConstant">&quot;5.5&quot;</span> } ... } ... } </pre> <h6>Lit</h6> <pre class="code lang-c" data-lang="c" data-unlink>Shader <span class="synConstant">&quot;Universal Render Pipeline/Lit&quot;</span> { ... SubShader { Tags { <span class="synConstant">&quot;RenderType&quot;</span> = <span class="synConstant">&quot;Opaque&quot;</span> <span class="synConstant">&quot;RenderPipeline&quot;</span> = <span class="synConstant">&quot;UniversalPipeline&quot;</span> <span class="synConstant">&quot;UniversalMaterialType&quot;</span> = <span class="synConstant">&quot;Lit&quot;</span> <span class="synConstant">&quot;IgnoreProjector&quot;</span> = <span class="synConstant">&quot;True&quot;</span> <span class="synConstant">&quot;ShaderModel&quot;</span>=<span class="synConstant">&quot;4.5&quot;</span> } } ... } </pre> <p>こんな感じで分かれています。あとは出てきていないタイプとしては <code>Unlit</code> があります。なので自作のシェーダを作る際は出力するステンシル値を切り替えるために、この <code>UniversalMaterialType</code> に何を指定するか注意する必要があります。</p> <p>一方 Deferred Pass では次のように普通に ShaderLab 内の <code>Stencil</code> ブロックで扱うシェーダを切り分けています。</p> <pre class="code lang-c" data-lang="c" data-unlink>Pass { Name <span class="synConstant">&quot;Deferred Directional Light (Lit)&quot;</span> ZTest NotEqual ZWrite Off Cull Off Blend One SrcAlpha, Zero One BlendOp Add, Add Stencil { Ref [_LitDirStencilRef] ReadMask [_LitDirStencilReadMask] WriteMask [_LitDirStencilWriteMask] Comp Equal Pass Keep Fail Keep ZFail Keep } HLSLPROGRAM <span class="synPreProc">#pragma exclude_renderers gles gles3 glcore</span> <span class="synPreProc">#pragma target </span><span class="synConstant">4.5</span> <span class="synPreProc">#pragma multi_compile_fragment _DEFERRED_STENCIL</span> <span class="synPreProc">#pragma multi_compile _DIRECTIONAL</span> <span class="synPreProc">#pragma multi_compile_fragment _LIT</span> <span class="synPreProc">#pragma multi_compile_fragment _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN</span> <span class="synPreProc">#pragma multi_compile_fragment _ _DEFERRED_MAIN_LIGHT</span> ... <span class="synPreProc">#pragma vertex Vertex</span> <span class="synPreProc">#pragma fragment DeferredShading</span> ENDHLSL } Pass { Name <span class="synConstant">&quot;Deferred Directional Light (SimpleLit)&quot;</span> ZTest NotEqual ZWrite Off Cull Off Blend One SrcAlpha, Zero One BlendOp Add, Add Stencil { Ref [_SimpleLitDirStencilRef] ReadMask [_SimpleLitDirStencilReadMask] WriteMask [_SimpleLitDirStencilWriteMask] Comp Equal Pass Keep Fail Keep ZFail Keep } HLSLPROGRAM <span class="synPreProc">#pragma exclude_renderers gles gles3 glcore</span> <span class="synPreProc">#pragma target </span><span class="synConstant">4.5</span> <span class="synPreProc">#pragma multi_compile_fragment _DEFERRED_STENCIL</span> <span class="synPreProc">#pragma multi_compile _DIRECTIONAL</span> <span class="synPreProc">#pragma multi_compile_fragment _SIMPLELIT</span> <span class="synPreProc">#pragma multi_compile_fragment _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN</span> <span class="synPreProc">#pragma multi_compile_fragment _ _DEFERRED_MAIN_LIGHT</span> ... <span class="synPreProc">#pragma vertex Vertex</span> <span class="synPreProc">#pragma fragment DeferredShading</span> ENDHLSL } </pre> <p>では、<code>Lit</code> / <code>SimpleLit</code> で共通で使われているフラグメントシェーダ実装の <code>DeferredShading</code> を見てみましょう。</p> <pre class="code lang-c" data-lang="c" data-unlink>half4 <span class="synIdentifier">DeferredShading</span>(Varyings input) : SV_Target { ... half4 gbuffer0 = <span class="synIdentifier">SAMPLE_TEXTURE2D_X_LOD</span>(_GBuffer0, ...); half4 gbuffer1 = <span class="synIdentifier">SAMPLE_TEXTURE2D_X_LOD</span>(_GBuffer1, ...); half4 gbuffer2 = <span class="synIdentifier">SAMPLE_TEXTURE2D_X_LOD</span>(_GBuffer2, ...); ... half3 color = <span class="synConstant">0.0</span>.xxx; half alpha = <span class="synConstant">1.0</span>; ... InputData inputData = <span class="synIdentifier">InputDataFromGbufferAndWorldPosition</span>(gbuffer2, posWS.xyz); ... <span class="synPreProc">#if defined(_LIT)</span> BRDFData brdfData = <span class="synIdentifier">BRDFDataFromGbuffer</span>( gbuffer0, gbuffer1, gbuffer2); color = <span class="synIdentifier">LightingPhysicallyBased</span>( brdfData, unityLight, inputData.normalWS, inputData.viewDirectionWS, materialSpecularHighlightsOff); <span class="synPreProc">#elif defined(_SIMPLELIT)</span> SurfaceData surfaceData = <span class="synIdentifier">SurfaceDataFromGbuffer</span>( gbuffer0, gbuffer1, gbuffer2, kLightingSimpleLit); half3 attenuatedLightColor = ...; half3 diffuseColor = ...; half smoothness = ...; half3 specularColor = ...; color = diffuseColor * surfaceData.albedo + specularColor; <span class="synPreProc">#endif</span> <span class="synStatement">return</span> <span class="synIdentifier">half4</span>(color, alpha); } </pre> <p>こんな形で <code>SimpleLit</code> 側は単純な足し合わせの計算をしているだけのものになります。一方、<code>Lit</code> の方は物理シェーディングを行っています。先程の GIF 画像では Directional ライトのみでしたが、Deferred の真骨頂はたくさんのライトがあるケースなのでそれも見てみます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220430/20220430215727.gif" width="822" height="859" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Deferred Punctual Light パスが実行されるようになりました。こちらも <code>Lit</code> と <code>Simple Lit</code> のパスが存在しています。これでなんとなく全体の流れが分かりましたね。</p> <h3>GBuffer3?</h3> <p>なお、GBuffer3 が見当たらない?と思うかもしれませんが、こちらは Legacy パスと同じで出力用のレンダーターゲットと共有されています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220430/20220430220838.png" width="1200" height="448" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ライトの計算時には <code>Blend One One</code> で加算<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>となっており、各ライトの影響が足されていくのが分かります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220430/20220430222600.png" width="1200" height="527" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このレンダーターゲットは Skybox 描画時も出力として指定され、最終的には Post Process 時に <code>_SourceTex</code> として与えられ、最終的な画が作られる流れです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220430/20220430222923.png" width="1200" height="640" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>Lit シェーダについて(追記: 2022/05/01)</h2> <p>主に <code>SimpleLit</code> について見てきましたが、<code>Lit</code> も見てみましたので少しだけですが追記です。</p> <p><code>SimpleLit</code> でも <code>Lit</code> でも入力パラメタから <code>SurfaceData</code> を初期化するのは同じです。ただ詰める情報量が違っていまして次のような感じです。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synComment">// SimpleLit</span> <span class="synIdentifier">CBUFFER_START</span>(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half4 _SpecColor; half4 _EmissionColor; half _Cutoff; half _Surface; CBUFFER_END <span class="synComment">// Lit</span> <span class="synIdentifier">CBUFFER_START</span>(UnityPerMaterial) float4 _BaseMap_ST; float4 _DetailAlbedoMap_ST; half4 _BaseColor; half4 _SpecColor; half4 _EmissionColor; half _Cutoff; half _Smoothness; half _Metallic; half _BumpScale; half _Parallax; half _OcclusionStrength; half _ClearCoatMask; half _ClearCoatSmoothness; half _DetailAlbedoMapScale; half _DetailNormalMapScale; half _Surface; CBUFFER_END </pre> <p><code>Lit</code> の方は金属やスムースネス、視差・ディテールマップやクリアコートなど色々な機能を変数としてサポートしていますね(クリアコートの適用は <a href="https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@12.0/manual/shader-complex-lit.html">ComplexLit</a> シェーダでのサポートですが)。この関係で <code>SimpleLit</code> は <code>InitializeSimpleLitSurfaceData()</code> で初期化していたところが <code>Lit</code> では <code>InitializeStandardLitSurfaceData()</code> になります。これは Forward <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>と共<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>されています。</p> <p>フラグメントシェーダを概観するとこんな感じになります。</p> <pre class="code lang-c" data-lang="c" data-unlink>FragmentOutput <span class="synIdentifier">LitGBufferPassFragment</span>(Varyings input) { ... SurfaceData surfaceData; <span class="synIdentifier">InitializeStandardLitSurfaceData</span>(input.uv, surfaceData); ... BRDFData brdfData; <span class="synIdentifier">InitializeBRDFData</span>( surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.alpha, brdfData); Light mainLight = <span class="synIdentifier">GetMainLight</span>(...); <span class="synIdentifier">MixRealtimeAndBakedGI</span>(...); half3 color = <span class="synIdentifier">GlobalIllumination</span>( brdfData, inputData.bakedGI, surfaceData.occlusion, inputData.positionWS, inputData.normalWS, inputData.viewDirectionWS); <span class="synStatement">return</span> <span class="synIdentifier">BRDFDataToGbuffer</span>( brdfData, inputData, surfaceData.smoothness, surfaceData.emission + color, surfaceData.occlusion); } </pre> <p>シンプルな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%D9%A5%C9">アルベド</a>とスペキュラだけの計算部が <a class="keyword" href="http://d.hatena.ne.jp/keyword/BRDF">BRDF</a> によるものに置き換えられています。ちなみに <code>GlobalIllumination()</code> も Forward <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>と共通の関数になっています。内部では URP は関数が小分けにされているのでわかりやすくて良いですね。</p> <h2>おわりに</h2> <p>端折って見ていった感じがありますが、全体像は薄っすらと見えてきましたね。Legacy の Deferred と比べてもだいぶ違うものになっているのが分かりました。今後、実際にどのようなゲームで使われていくのかも見ていきたいです。</p> <p>次はここで学習したことをベースに uRaymarching の Deferred 周りのアップデートを行おうと思います。</p> hecomi Unity で Timeline のカスタムトラックおよびクリップを作成して見た目をキレイにしてみた hatenablog://entry/13574176438076832759 2022-03-28T23:53:36+09:00 2022-03-28T23:53:36+09:00 はじめに 前々回の記事で Unity でリップシンクを行うためのアセットである uLipSync のタイムライン対応について紹介しました。 tips.hecomi.com ここでは次のように現在どのリップシンクデータを喋らせているか分かるように色々装飾しています。 本記事では、カスタム Track および Clip の作り方と、その見た目のカスタマイズの方法について紹介します。それぞれの詳細を解説すると言うよりは、全体の勘所をつかめるような内容を目指しています。 デモ こんな見た目のカラーをブレンドするタイムラインが作成できるようになります。プロジェクトは以下から取得できます。 github.… <h2>はじめに</h2> <p>前々回の記事で <strong>Unity</strong> で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>を行うためのアセットである uLipSync の<strong>タイムライン</strong>対応について紹介しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F01%2F30%2F152519" title="uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/01/30/152519">tips.hecomi.com</a></cite></p> <p>ここでは次のように現在どの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>データを喋らせているか分かるように色々装飾しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220326/20220326143450.png" width="1200" height="710" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>本記事では、カスタム Track および Clip の作り方と、その見た目のカスタマイズの方法について紹介します。それぞれの詳細を解説すると言うよりは、全体の勘所をつかめるような内容を目指しています。</p> <h2>デモ</h2> <p>こんな見た目のカラーを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>するタイムラインが作成できるようになります。プロジェクトは以下から取得できます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FUnityTimelineExample" title="GitHub - hecomi/UnityTimelineExample: A simple example of creating custom Track and Clip" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/UnityTimelineExample">github.com</a></cite></p> <h2>タイムラインのおさらい</h2> <p>タイムラインは <strong><a href="https://docs.unity3d.com/ja/current/Manual/Playables.html">Playable API</a></strong> をもとに構築されています。ドキュメントの説明を借りると次のようなものです。Playable <a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> はアニメーションなどにも使われている、複数のデータからの入力をミックス、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>、ときには手を加えて、最終的に 1 つの出力として再生できるという抽象化された仕組みです。タイムラインでは、アニメーション以外にも、オーディオや、オブジェクトの操作、その他時間に関係あるいろいろなものがこの仕組みに落とし込まれて、それをタイムラインアセットという形にして保存・再利用できるようになっています。</p> <p>このように適度に抽象化されていることもあり、カスタムトラックおよびクリップを作る際の登場人物はちょっと多くて主に 4 人います。ざっくりとはこんな感じです。</p> <ul> <li>Track <ul> <li>タイムライン上に追加できる行のアセット。中に指定したクリップを配置できる。</li> </ul> </li> <li>Clip <ul> <li>Track 上に配置できる範囲を持つアセット。</li> </ul> </li> <li>Playable Behaviour <ul> <li>Clip のふるまい</li> </ul> </li> <li>Mixer <ul> <li>複数の Clip(Playable Behaviour)の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a></li> </ul> </li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220326/20220326145653.png" width="1200" height="402" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>見た目的にはトラックとクリップの 2 人かな?と思うと倍がいるので混乱してしまうと思いますが、往々にして、こういうのは説明を見ても良くわからないことが多いので、具体的にひとつずつ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%E0%A5%A2%A5%C3%A5%D7">ボトムアップ</a>で作成して見ていくことにしましょう。</p> <h2>カスタムトラックとクリップの作成</h2> <p>まずは、とてもシンプルなトラックとクリップを作成する手順を経て、それぞれの役割を理解します。</p> <h3>トラックの作成</h3> <p>まずはトラックを作成していきます。次のような<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を作成してみましょう。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine.Timeline; <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { } </pre> <p>すると Timeline 上で左上の + を押下または右クリックをした際に、このトラックが項目として出てきて追加できるようになります(ちなみに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%CC%BE%C1%B0%B6%F5%B4%D6">名前空間</a>でくくると階層メニューが作れます)。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220326/20220326152018.png" width="1200" height="497" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>クリップの作成</h3> <p>次にここに配置できるクリップを作成しましょう。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Playables; <span class="synType">public</span> <span class="synType">class</span> CustomClip : PlayableAsset { <span class="synType">public</span> <span class="synType">override</span> Playable CreatePlayable(PlayableGraph graph, GameObject go) { <span class="synStatement">return</span> ScriptPlayable&lt;CustomBehaviour&gt;.Create(graph); } } </pre> <p>ここでは、<code>CustomBehaviour</code> というこれから作成する <code>PlayableBehaviour</code> 派生クラスを内包した <a href="https://docs.unity3d.com/ja/current/Manual/Playables-ScriptPlayable.html">ScriptPlayable</a> という Playable を作っています。ではこの <code>CustomBehaviour</code> を作成しましょう。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Playables; <span class="synType">public</span> <span class="synType">class</span> CustomBehaviour : PlayableBehaviour { <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> ProcessFrame(Playable playable, FrameData info, <span class="synType">object</span> playerData) { Debug.Log($<span class="synConstant">&quot;Time: {playable.GetTime()}&quot;</span>); } } </pre> <p>中身は現在の Playable の時間を出力するだけのシンプルなものになっています。</p> <p>最後に、トラックにこのクリップを追加できるようにします。次の属性をトラックに付けます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... [TrackClipType(<span class="synStatement">typeof</span>(CustomClip))] <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { } </pre> <p>これで準備が整いました。エディタに戻って先ほど作成したカスタムトラック上で右クリックをしてみてください。「Add CustomClip」という項目が追加され、クリップが追加できるようになりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220326/20220326160322.png" width="1200" height="430" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これでタイムライン上をなぞってみると、Console に時間が出力されているのが分かると思います。</p> <h3>整理</h3> <p>既に少し分かりづらくなってるかもしれないのでここで一度整理します。まず、Track は指定したクラスの Clip を配置できる箱で、ここは比較的問題ないと思います。Clip は <code>PlayableAsset</code> を継承していることからも分かるようにアセットです。ただ、コードを見ると分かるように Playable を生成する役割も担っています。そして、この Playable で処理する実体が <code>PlayableBehaviour</code> です。</p> <h3>Track と Clip に変数を追加</h3> <p>さて、Track と Clip はアセットと言ったようにこれらには通常の <code>MonoBehaviour</code> と同じように public または <code>[SerializeField]</code> 属性をつけた変数を用意することでインスペクタに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B7%A5%EA%A5%A2%A5%E9%A5%A4%A5%BA">シリアライズ</a>可能なフィールドを表示できます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { <span class="synType">public</span> <span class="synType">float</span> floatValue = <span class="synConstant">0f</span>; <span class="synType">public</span> <span class="synType">string</span> textValue = <span class="synConstant">&quot;Test&quot;</span>; } </pre> <p>属性もそのまま使えます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Playables; <span class="synType">public</span> <span class="synType">class</span> CustomClip : PlayableAsset { [Range(<span class="synConstant">0f</span>, <span class="synConstant">1f</span>)] <span class="synType">public</span> <span class="synType">float</span> <span class="synStatement">value</span> = <span class="synConstant">0f</span>; ... } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220326/20220326163019.png" width="1026" height="232" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220326/20220326163034.png" width="1022" height="694" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>ミキサーの作成</h2> <p>次に 4 人目の登場人物の Mixer を見ていきましょう。ただこのままでは何を合成すれば良いか分からない感じなので、サンプルとして分かりやすいように次のようにしてみましょう。</p> <ul> <li>色の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>を行う</li> <li>Clip には <code>Gradient</code> をセット</li> <li>Behaviour は現在の時間をもとに <code>Gradient</code> を評価した色を返す</li> </ul> <h3>Clip / Behaviour の修正</h3> <p>Clip には <code>Gradient</code> を格納できるようにします。また、<code>PlayableBehaviour</code> 側でこの値を使えるように準備します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Playables; <span class="synType">public</span> <span class="synType">class</span> CustomClip : PlayableAsset { <span class="synType">public</span> Gradient gradient; <span class="synType">public</span> <span class="synType">override</span> Playable CreatePlayable(PlayableGraph graph, GameObject go) { var playable = ScriptPlayable&lt;CustomBehaviour&gt;.Create(graph); var behaviour = playable.GetBehaviour(); behaviour.clip = <span class="synStatement">this</span>; <span class="synStatement">return</span> playable; } } </pre> <p><code>ScriptPlayable</code> で内包された <code>PlayableBehaviour</code> は <code>GetBehaviour()</code> 経由で取ってこれます。なので、ここで自身を与えてあげたり、必要な情報をセットしてあげたりすることで、ユーザがエディタ上で指定したパラメタを <code>PlayableBehaviour</code> のコードで使える形に持っていくことができます。<code>MonoBehaviour</code> の場合は自身が<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B7%A5%EA%A5%A2%A5%E9%A5%A4%A5%BA">シリアライズ</a>機能とロジックを兼ね備えていたので分かりやすかったですが、ここが別れているのでちょっと分かりづらいですね。</p> <p>次に <code>PlayableBehaviour</code> 側を修正します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Playables; <span class="synType">public</span> <span class="synType">class</span> CustomBehaviour : PlayableBehaviour { <span class="synType">public</span> CustomClip clip { get; set; } <span class="synType">public</span> Color outputColor { get; <span class="synType">private</span> set; } <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> ProcessFrame(Playable playable, FrameData info, <span class="synType">object</span> playerData) { var t = playable.GetTime(); var d = playable.GetDuration(); var a = (<span class="synType">float</span>)(t / d); outputColor = clip.gradient.Evaluate(a); } } </pre> <p>Clip がもらえたので <code>Gradient</code> も確認できます。先程は詳しい説明を割愛しましたが <code>ProcessFrame()</code> では <code>Playable</code> が渡ってきます。ここに現在再生している時間や、合計の時間といった再生に関する情報が色々含まれています。これらを使い、<code>Gradient</code> から時間経過に応じた色を得て、<code>outputColor</code> に詰めておきます。ちなみに <code>FrameData</code> は <code>deltaTime</code> や再生スピードといったそのフレーム単位の情報が入ってきます。<code>playerData</code> に関しては後述します。</p> <p>これで準備完了です。あとは Mixer からこの <code>outputColor</code> を参照し、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>を行いましょう。</p> <h3>Mixer の作成</h3> <p>Mixer は Track から生成しますので、Track のコードを修正しましょう。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Timeline; <span class="synStatement">using</span> UnityEngine.Playables; [TrackClipType(<span class="synStatement">typeof</span>(CustomClip))] <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { <span class="synType">public</span> <span class="synType">override</span> Playable CreateTrackMixer(PlayableGraph graph, GameObject go, <span class="synType">int</span> inputCount) { <span class="synStatement">return</span> ScriptPlayable&lt;CustomMixer&gt;.Create(graph, inputCount); } } </pre> <p>こちらも Clip のときと同じように <code>ScriptPlayable</code> を生成しています。Playables の世界では <code>Playable</code> たちが PlayableGraph 上でノード接続されて最終的にアウトプットされる形になっており、Mixer は <code>inputCount</code> 個の Clip から生成された <code>Playable</code> をまとめ上げる <code>Playable</code> になっているという感じですね。アニメーション<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>ツリーでも同じように複数のアニメーションをまとめ上げるミキサーがいるのと同じような感じです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327012722.png" width="1200" height="843" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2F2019.4%2FManual%2FPlayables-Graph.html" title="PlayableGraph - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/2019.4/Manual/Playables-Graph.html">docs.unity3d.com</a></cite></p> <p>では Mixer のコードを書いていきましょう。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Playables; <span class="synType">public</span> <span class="synType">class</span> CustomMixer : PlayableBehaviour { <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> ProcessFrame(Playable playable, FrameData info, <span class="synType">object</span> playerData) { var color = Color.clear; <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i &lt; playable.GetInputCount(); i++) { var sp = (ScriptPlayable&lt;CustomBehaviour&gt;)playable.GetInput(i); var behaviour = sp.GetBehaviour(); var weight = playable.GetInputWeight(i); color += behaviour.outputColor * weight; } var code = ColorUtility.ToHtmlStringRGBA(color); Debug.Log($<span class="synConstant">&quot;&lt;color=#{code}&gt;{code}&lt;/color&gt;&quot;</span>); } } </pre> <p>先程の PlayableGraph の図を見ても分かるように、Mixer には複数個の <code>Playable</code> が入力として与えられます。<code>GetInputCount()</code> ではこの数を知ることができ、Timeline の世界では Track に配置された Clip がそれに当たります。<code>GetInput(int index)</code> でインデックスを与えてあげると、その入力の <code>Playable</code> を取得することができます。このトラックには <code>CustomBehaviour</code> しか配置できないのでキャストし、ここから <code>GetBehaviour()</code> で <code>PlayableBehaviour</code> を取り出します。更に、<code>GetInputWeight(int index)</code> で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>時のウェイトが取得できるので、これを <code>outputColor</code> にかけて<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>してあげれば、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>した色を取り出すことができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327014033.gif" width="1143" height="442" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>GameObject との<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">バインディング</a></h2> <h3>マテリアルの色を変えてみる</h3> <p>Timeline はそのままではアセットです。なのでシーン中のどのオブジェクトをどのように変更するかは、紐付けをする必要があります。これを行うのが、<code>TrackBindingType</code> 属性です。具体的に見てみましょう。<code>Renderer</code> のマテリアルに先程のカラーを指定するようにしてみます。まずは Track に次のように属性を追加します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... [TrackClipType(<span class="synStatement">typeof</span>(CustomClip))] [TrackBindingType(<span class="synStatement">typeof</span>(Renderer))] <span class="synComment">// これ</span> <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { ... } </pre> <p>するとトラックの項目に指定した型のオブジェクトを指定できるフィールドが追加されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327015600.png" width="1200" height="204" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ここに適当な(<code>Renderer</code> を持っている)ゲームオブジェクトをドラッグドロップしましょう。そして Mixer 側でこの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">バインディング</a>された情報を使います。マテリアルをいじる際は、直接 <code>renderer.material</code> をいじると代入のたびに Material のコピーが生じてしまい、更にエディタモードでは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E1%A5%E2%A5%EA%A5%EA%A1%BC%A5%AF">メモリリーク</a>を産むことからエラーが出力されてしまうので、専用のマテリアルを作ってそれを使う形にしておきます。また、<code>OnBehaviourPause()</code> というタイムラインの再生が終わった際に呼び出されるコールバックのタイミングで破棄します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomMixer : PlayableBehaviour { Material _mat = <span class="synConstant">null</span>; <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnBehaviourPause(Playable playable, FrameData info) { <span class="synStatement">if</span> (_mat) Object.DestroyImmediate(_mat); } <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> ProcessFrame(Playable playable, FrameData info, <span class="synType">object</span> playerData) { var renderer = playerData <span class="synStatement">as</span> Renderer; <span class="synStatement">if</span> (!renderer) <span class="synStatement">return</span>; <span class="synStatement">if</span> (!_mat) { _mat = <span class="synStatement">new</span> Material(renderer.sharedMaterial); renderer.material = _mat; } ... _mat.color = color; } } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327020332.gif" width="889" height="372" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>なお、この<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">バインディング</a>情報はシーンに配置された <code>PlayableDirector</code> 単位で保存されています。先程、Timeline はアセットと言いましたが、このアセットをもとに実際に再生を行うのが <code>PlayableDirector</code> で、何を動かすのかみたいな情報はここに保存されるわけですね。どのような<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">バインディング</a>がなされているかはインスペクタで確認できます(Bindings に自動で表示されるようになります)。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327132209.png" width="910" height="402" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>変える前の情報を覚えておく</h3> <p>タイムラインの再生が終わったら再生中に変更されたパラメタは元に戻したくなると思います。そのために予めこれらのパラメタを登録できる仕組みが用意されています。Track の <code>GatherProperties()</code> という関数を override し、引数として渡ってくる <code>PlayableDirector</code> から<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">バインディング</a>情報を抜き出し、 <code>IPropertyCollector</code> にそれをもとに必要な変数を登録します。</p> <ul> <li><a href="https://docs.unity3d.com/ja/2018.4/ScriptReference/Timeline.IPropertyPreview.GatherProperties.html">Timeline.IPropertyPreview-GatherProperties - Unity &#x30B9;&#x30AF;&#x30EA;&#x30D7;&#x30C8;&#x30EA;&#x30D5;&#x30A1;&#x30EC;&#x30F3;&#x30B9;</a></li> <li><a href="https://docs.unity3d.com/ja/2018.4/ScriptReference/Timeline.IPropertyCollector.html">Timeline.IPropertyCollector - Unity &#x30B9;&#x30AF;&#x30EA;&#x30D7;&#x30C8;&#x30EA;&#x30D5;&#x30A1;&#x30EC;&#x30F3;&#x30B9;</a></li> </ul> <h6>参考</h6> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Frarudonet%2Fitems%2Fd1715e60cca91cd9384e" title="Unityタイムライン拡張入門 ~自作トラックの作り方~ - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/rarudonet/items/d1715e60cca91cd9384e">qiita.com</a></cite></p> <p>今回はマテリアルをいじっていたのでそれを元に戻す処理をしてみます。コードを見てみましょう。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { ... <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> GatherProperties(PlayableDirector director, IPropertyCollector driver) { var renderer = director.GetGenericBinding(<span class="synStatement">this</span>) <span class="synStatement">as</span> Renderer; driver.AddFromName&lt;Renderer&gt;(renderer.gameObject, <span class="synConstant">&quot;m_Materials.Array.data[0]&quot;</span>); <span class="synStatement">base</span>.GatherProperties(director, driver); } } </pre> <p>...、<code>m_Materials.Array.data[0]</code> ってなんだ?と思うかもしれませんが、これは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B7%A5%EA%A5%A2%A5%E9%A5%A4%A5%BA">シリアライズ</a>されるフィールド名で、.meta ファイルやシーンである .unity を見ると確認できます。他にも例えば <code>m_IsActive</code> など普段は見ない文字列がこれに該当します。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synPreProc">...</span> <span class="synIdentifier">MeshRenderer</span><span class="synSpecial">:</span> <span class="synIdentifier">m_ObjectHideFlags</span><span class="synSpecial">:</span> <span class="synConstant">0</span> <span class="synIdentifier">m_CorrespondingSourceObject</span><span class="synSpecial">:</span> <span class="synSpecial">{</span><span class="synIdentifier">fileID</span><span class="synSpecial">:</span> <span class="synConstant">0</span><span class="synSpecial">}</span> ... <span class="synIdentifier">m_Materials</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synSpecial">{</span><span class="synIdentifier">fileID</span><span class="synSpecial">:</span> <span class="synConstant">2100000</span>, <span class="synIdentifier">guid</span><span class="synSpecial">:</span> 08899ed4ee530452f9a7a13d31f4556a, <span class="synIdentifier">type</span><span class="synSpecial">:</span> <span class="synConstant">2</span><span class="synSpecial">}</span> <span class="synIdentifier">m_StaticBatchInfo</span><span class="synSpecial">:</span> <span class="synIdentifier">firstSubMesh</span><span class="synSpecial">:</span> <span class="synConstant">0</span> <span class="synIdentifier">subMeshCount</span><span class="synSpecial">:</span> <span class="synConstant">0</span> ... </pre> <p>配列のアクセスは先程のように <code>Array.data[x]</code> のようにアクセスするようですね。</p> <ul> <li>参考: <a href="https://forum.unity.com/threads/how-to-use-ipropertycollector-to-set-preview-for-renderers.1055312/">How to use IPropertyCollector to set preview for renderers? - Unity Forum</a></li> </ul> <p>ただ、調べるのはしんどいですしドキュメントにも乗っていないので、次のように自分で予め保存しておいたものに差し替えるほうが分かりやすいかもしれません(今回のケースでは <code>Material</code> 生成もしてましたしむしろこちらのほうがコードは短い)。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Playables; <span class="synType">public</span> <span class="synType">class</span> CustomMixer : PlayableBehaviour { Renderer _renderer = <span class="synConstant">null</span>; Material _origMat = <span class="synConstant">null</span>; Material _mat = <span class="synConstant">null</span>; <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnBehaviourPause(Playable playable, FrameData info) { <span class="synStatement">if</span> (_mat) Object.DestroyImmediate(_mat); <span class="synStatement">if</span> (_renderer) _renderer.material = _origMat; } <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> ProcessFrame(Playable playable, FrameData info, <span class="synType">object</span> playerData) { var renderer = playerData <span class="synStatement">as</span> Renderer; <span class="synStatement">if</span> (!renderer) <span class="synStatement">return</span>; <span class="synStatement">if</span> (!_mat) { _renderer = renderer; _origMat = renderer.sharedMaterial; _mat = <span class="synStatement">new</span> Material(_origMat); renderer.material = _mat; } ... _mat.color = color; } } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327190258.gif" width="889" height="372" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>Track と Clip の見た目をきれいにする</h2> <p>さて、このままでは実際にタイムラインをシークしてみないとどんな色になるか分かりません。また、カスタム Track が増えてくると味気ない見た目が並んでしまい、どれがどれか分かりづらくなってしまいます。そこで、色々装飾する方法を見ていきましょう。</p> <h3>色をつける</h3> <p>Track の左端や Clip の下側に帯があるのですが、ここの色は次のように <code>TrackColor</code> 属性をつけることで指定できます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... [TrackColor(<span class="synConstant">0.8f</span>, <span class="synConstant">0.2f</span>, <span class="synConstant">0.2f</span>)] <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { ... } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327190905.png" width="1182" height="478" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>アイコンを変える</h3> <p>Track には <code>TrackEditor</code> というエディタ拡張が用意されています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.timeline%401.7%2Fapi%2FUnityEditor.Timeline.TrackEditor.html" title="Class TrackEditor | Timeline | 1.7.1" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.timeline@1.7/api/UnityEditor.Timeline.TrackEditor.html">docs.unity3d.com</a></cite></p> <p>ここでアイコンも指定できます。また先程 <code>TrackColor</code> 属性で与えた色も、代わりにここでも指定することができます(Clip には適用されずに Track のみ変わります)。その他は高さやエラーテキスト、トラックの名前といったものも指定可能です。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Timeline; <span class="synStatement">using</span> UnityEditor.Timeline; [CustomTimelineEditor(<span class="synStatement">typeof</span>(CustomTrack))] <span class="synType">public</span> <span class="synType">class</span> CustomTrackEditor : TrackEditor { Texture2D _iconTexture; <span class="synType">public</span> <span class="synType">override</span> TrackDrawOptions GetTrackOptions(TrackAsset track, Object binding) { track.name = <span class="synConstant">&quot;CustomTrack&quot;</span>; <span class="synStatement">if</span> (!_iconTexture) { _iconTexture = Resources.Load&lt;Texture2D&gt;(<span class="synConstant">&quot;CustomTrack-Icon&quot;</span>); } var options = <span class="synStatement">base</span>.GetTrackOptions(track, binding); options.trackColor = Color.magenta; options.icon = _iconTexture; <span class="synStatement">return</span> options; } } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327195525.png" width="1196" height="492" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>Clip の背景を変える</h3> <p>Track と同様に Clip にも <code>ClipEditor</code> というものがあります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.timeline%401.1%2Fapi%2FUnityEditor.Timeline.ClipEditor.html" title="Class ClipEditor | Package Manager UI website" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.timeline@1.1/api/UnityEditor.Timeline.ClipEditor.html">docs.unity3d.com</a></cite></p> <p>こちらは色々と解説記事がありますのでとても参考になります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fjukey17%2Fitems%2Fe4db4028aeb650819bc5" title="[Unity][Timeline] Unity2019.2から使えるようになったClipEditorでクリップの見た目を拡張する - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/jukey17/items/e4db4028aeb650819bc5">qiita.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Flight11.hatenadiary.com%2Fentry%2F2021%2F08%2F31%2F192405" title="【Unity】【エディタ拡張】TimelineのClipの見た目を拡張する - LIGHT11" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://light11.hatenadiary.com/entry/2021/08/31/192405">light11.hatenadiary.com</a></cite></p> <p>具体的には、<code>DrawBackground()</code> をオーバーライドすることで背景描画を拡張することができたり、<code>GetClipOptions()</code> で <a href="https://docs.unity3d.com/Packages/com.unity.timeline@1.5/api/UnityEditor.Timeline.ClipDrawOptions.html">ClipDrawOptions</a> を返すことで見た目に関する様々なカスタマイズを行うことができます。</p> <p>次のようなコードを書けば背景にテクスチャを描画することができます。テクスチャでなくとも、例えば、<code>Handles</code> などを使って○を書いたりグラフを書いたり(<a href="https://tips.hecomi.com/?page=1404136500">&#x53C2;&#x8003;</a>)することができます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... [CustomTimelineEditor(<span class="synStatement">typeof</span>(CustomClip))] <span class="synType">public</span> <span class="synType">class</span> CustomClipEditor : ClipEditor { <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> DrawBackground(TimelineClip clip, ClipBackgroundRegion region) { var tex = <span class="synStatement">new</span> Texture2D(...); GUI.DrawTexture(region.position, tex); } } </pre> <p>実際は、作成した画像をキャッシュしたり、今回のケースではグラデーション用のテクスチャを作ったりとやることが増えます。少しコードが長くなってしまいますが、 - ハイ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%A4%A5%C8%A5%AB%A5%E9%A1%BC">ライトカラー</a>は透明 - <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>部分がアルファでオーバーラップするようにグラデーションテクスチャを生成 といった感じでコードを書いてみます。なお、この Editor は複数の Clip で共有されるようなので、生成したテクスチャは <code>Dictionary</code> で持つようにしています。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEngine.Timeline; <span class="synStatement">using</span> UnityEditor.Timeline; <span class="synStatement">using</span> System.Collections.Generic; [CustomTimelineEditor(<span class="synStatement">typeof</span>(CustomClip))] <span class="synType">public</span> <span class="synType">class</span> CustomClipEditor : ClipEditor { Dictionary&lt;CustomClip, Texture2D&gt; _textures = <span class="synStatement">new</span> Dictionary&lt;CustomClip, Texture2D&gt;(); <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> DrawBackground(TimelineClip clip, ClipBackgroundRegion region) { var tex = GetGradientTexture(clip); <span class="synStatement">if</span> (tex) GUI.DrawTexture(region.position, tex); } <span class="synType">public</span> <span class="synType">override</span> ClipDrawOptions GetClipOptions(TimelineClip clip) { var options = <span class="synStatement">base</span>.GetClipOptions(clip); options.highlightColor = Color.clear; <span class="synStatement">return</span> options; } <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnClipChanged(TimelineClip clip) { GetGradientTexture(clip, <span class="synConstant">true</span>); } Texture2D GetGradientTexture(TimelineClip clip, <span class="synType">bool</span> update = <span class="synConstant">false</span>) { Texture2D tex = Texture2D.whiteTexture; var customClip = clip.asset <span class="synStatement">as</span> CustomClip; <span class="synStatement">if</span> (!customClip) <span class="synStatement">return</span> tex; var gradient = customClip.gradient; <span class="synStatement">if</span> (gradient == <span class="synConstant">null</span>) <span class="synStatement">return</span> tex; <span class="synStatement">if</span> (update) { _textures.Remove(customClip); } <span class="synStatement">else</span> { _textures.TryGetValue(customClip, <span class="synStatement">out</span> tex); <span class="synStatement">if</span> (tex) <span class="synStatement">return</span> tex; } var b = (<span class="synType">float</span>)(clip.blendInDuration / clip.duration); tex = <span class="synStatement">new</span> Texture2D(<span class="synConstant">128</span>, <span class="synConstant">1</span>); <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i &lt; tex.width; ++i) { var t = (<span class="synType">float</span>)i / tex.width; var color = customClip.gradient.Evaluate(t); <span class="synStatement">if</span> (b &gt; <span class="synConstant">0f</span>) color.a = Mathf.Min(t / b, <span class="synConstant">1f</span>); tex.SetPixel(i, <span class="synConstant">0</span>, color); } tex.Apply(); _textures.Add(customClip, tex); <span class="synStatement">return</span> tex; } } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327234015.png" width="1200" height="370" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>なお、冒頭の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>での波形描画に関しては <code>AudioCurveRendering</code> という便利なものがありましたので、それを利用して描画しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2FRijicho_nl%2Fitems%2F3c51befd7bbe63c00878" title="【Unity】AudioClipの波形プレビューを描画する【Editor拡張】 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/Rijicho_nl/items/3c51befd7bbe63c00878">qiita.com</a></cite></p> <h3>Clip の表示文字を変える</h3> <p>Clip に表示される文字列はデフォルトではその Clip の名前が表示されています。ここにはデータの名前など任意の文字列を自動挿入したいケースがあると思います。例えば今回のケース邪魔なので空白にしたいところです。これは、Clip の <code>displayName</code> を変更することで可能なようです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fousttrue%2Fitems%2Fac2f0b3847e76a36b1f0" title="UnityのTimelineTrackを実装する - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/ousttrue/items/ac2f0b3847e76a36b1f0">qiita.com</a></cite></p> <p>差し込む場所は色々あると思いますが、同じように Track 内でやってみます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { <span class="synType">public</span> <span class="synType">override</span> Playable CreateTrackMixer(PlayableGraph graph, GameObject go, <span class="synType">int</span> inputCount) { <span class="synStatement">foreach</span> (var clip <span class="synStatement">in</span> GetClips()) { clip.displayName = <span class="synConstant">&quot; &quot;</span>; } ... } } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220327/20220327235109.png" width="1196" height="362" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>あくまで表示される文字を強制的に書き換えるみたいな方法なので、ユーザーがクリップの名前を変更したあと、再度この Mixer が生成される(Timeline にフォーカスが合うなど)場合に、再度空白に置き換わってしまいます。これを避けたい場合は次のように、<code>ClipEditor</code> の <code>OnCreate()</code> を利用して Clip が生成されたときのみに空白指定するようにしてみます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomClipEditor : ClipEditor { ... <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom) { clip.displayName = <span class="synConstant">&quot; &quot;</span>; } ... } </pre> <p>これならば後で名前を書き換えたときにその名前もキープされるようになります。他にも生成タイミングにフックできる箇所は色々あるのでそこで指定しても良いと思います。</p> <h3>追加する際の文字列を変えたい</h3> <p>Track や Clip を追加する際に表示される "Custom Track" や "Add Custom Clip" といった文字列は、そのクラス名から自動生成されます。クラス名によっては長すぎたり、変なところで空白区切りが入ってしまうケースも有り、明示的に指定したいケースもあると思います。これは <code>DisplayName</code> 属性を各クラスにつけることで実現できます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synPreProc">#if UNITY_EDITOR</span> <span class="synStatement">using</span> System.ComponentModel; <span class="synPreProc">#endif</span> ... <span class="synPreProc">#if UNITY_EDITOR</span> [DisplayName(<span class="synConstant">&quot;Color Gradation Track&quot;</span>)] <span class="synPreProc">#endif</span> <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { ... } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220328/20220328000216.png" width="1190" height="516" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synPreProc">#if UNITY_EDITOR</span> <span class="synStatement">using</span> System.ComponentModel; <span class="synPreProc">#endif</span> <span class="synPreProc">#if UNITY_EDITOR</span> [DisplayName(<span class="synConstant">&quot;Color Gradation Clip&quot;</span>)] <span class="synPreProc">#endif</span> <span class="synType">public</span> <span class="synType">class</span> CustomClip : PlayableAsset { ... } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220328/20220328000232.png" width="1200" height="636" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>インスペクタの表示を変える</h3> <p>Track や Clip などはインスペクタにそのままパラメタを出せる話を先程しましたが、同様にこのエディタ拡張も <code>MonoBehaviour</code> のときと完全に同じく <code>Editor</code> 派生の <code>CustomEditor</code> 属性を付けたクラスを作ることで可能です。詳細は割愛します。</p> <h2>Clip の振る舞いを変更する</h2> <h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>やクリップなどのオプションを指定</h3> <p><code>ITimelineClipAsset</code> を派生元に追加することで <code>clipCaps</code> という機能にアクセスできるようになります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.timeline%401.7%2Fapi%2FUnityEngine.Timeline.ClipCaps.html%3Fq%3DClipCaps" title="Enum ClipCaps | Timeline | 1.7.1" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/com.unity.timeline@1.7/api/UnityEngine.Timeline.ClipCaps.html?q=ClipCaps">docs.unity3d.com</a></cite></p> <ul> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>できるかどうか(Blend)</li> <li>スピード変更できるかどうか(SpeedMultiplier)</li> <li>ループできるかどうか(Looping)</li> <li>トリムできるかどうか(ClipIn)</li> <li>クリップのない範囲外の挙動(Extrapolation)</li> <li>クリップをスケールしたときの挙動は SpeedMultipler になるかどうか(AutoScale)</li> </ul> <p>といったことが指定できます。これらは次のように OR でつないで <code>clipCaps</code> プロパティで返すようにします。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomClip : PlayableAsset, ITimelineClipAsset { ... <span class="synType">public</span> ClipCaps clipCaps { get =&gt; ClipCaps.Blending | ClipCaps.Extrapolation | ClipCaps.ClipIn; } ... } </pre> <p>例えば <code>Blending</code> を除外してみます。するとこのように<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>の表示がなくなり、実際に Mixer に入ってくる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>値も 0 / 1 になります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220328/20220328013510.png" width="1194" height="344" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>また、<code>Extrapolation</code> を追加してみます。すると、UI に Animation Extrapolation という項目が追加され、ここで範囲外の挙動を Hold(端の値をキープ)したり Loop(同じ挙動を繰り返し)したり、PingPong(逆再生、順再生を繰り返し)できるようになります。これまでは、範囲外に行った際は合計 weight が 0 となり、色が真っ黒になってしまっていましたが、その挙動をどうするか選択できるようになります。選択した挙動はタイムライン上でアイコンによって表現されています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220328/20220328234615.png" width="1196" height="354" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>1 点、Loop や PingPong した際には <code>Playable.GetDudration()</code> していたところで <code>Infinity</code> がやってきてしまうので、代わりに直接 <code>TimelineClip</code> から <code>duration</code> を取得するように修正が必要です。<code>Playable</code> からなぜ取得できないかは謎ですが…(もう少しきれいな方法もあるかもしれません)。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomTrack : TrackAsset { <span class="synType">public</span> <span class="synType">override</span> Playable CreateTrackMixer(PlayableGraph graph, GameObject go, <span class="synType">int</span> inputCount) { <span class="synStatement">foreach</span> (var timelineClip <span class="synStatement">in</span> GetClips()) { var clip = timelineClip.asset <span class="synStatement">as</span> CustomClip; clip.durationInTrack = (<span class="synType">float</span>)timelineClip.duration; } ... } } </pre> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomClip : PlayableAsset, ITimelineClipAsset { ... <span class="synType">public</span> <span class="synType">float</span> durationInTrack { get; set; } ... } </pre> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomBehaviour : PlayableBehaviour { ... <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> ProcessFrame(Playable playable, FrameData info, <span class="synType">object</span> playerData) { var t = playable.GetTime(); var d = clip.durationInTrack; var a = (<span class="synType">float</span>)(t / d); outputColor = clip.gradient.Evaluate(a); } } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220328/20220328232052.gif" width="889" height="372" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>データを指定してクリップを追加</h3> <p>Clip に public なオブジェクトの変数を追加すると、オブジェクトをドラッグドロップしたり右クリックからそのオブジェクトを指定して Clip を作成することが可能になります(そのオブジェクトがデフォルトでセットされた状態の Clip が作成される)。今回のサンプルだとあまり恩恵が無いですが、データを配置するような Clip を生成する際は結構便利だと思います。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomClip : PlayableAsset { <span class="synType">public</span> GameObject hoge; <span class="synType">public</span> Material fuga; ... } </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220328/20220328003441.png" width="1192" height="714" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>追加する際のデフォルトの長さを指定</h3> <p><code>duration</code> プロパティをオーバーライドすると配置時の長さを指定できます。データなどで長さを指定できる場合はここで値をセットしてしまうのが良いかと思います。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>... <span class="synType">public</span> <span class="synType">class</span> CustomClip : PlayableAsset { ... <span class="synType">public</span> <span class="synType">override</span> <span class="synType">double</span> duration { get =&gt; data ? data.duration : <span class="synStatement">base</span>.duration; } ... } </pre> <h2>その他</h2> <h3>Default Playables</h3> <p>解説した Track や Clip などの雛形を生成する Wizard を提供してくれたり、いくつか便利なサンプルが含まれているパッケージです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fassetstore.unity.com%2Fpackages%2Fessentials%2Fdefault-playables-95266%3Flocale%3Dja-JP" title="Default Playables | Unity公式アセット | Unity Asset Store" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://assetstore.unity.com/packages/essentials/default-playables-95266?locale=ja-JP">assetstore.unity.com</a></cite></p> <h3>参考資料</h3> <p>公式の Unite Tokyo 2018 での講演がとても参考になります。</p> <p><iframe width="720" height="540" src="https://www.youtube.com/embed/6SPpjSKy9LI?wmode=transparent" frameborder="0" allowfullscreen></iframe></p> <h2>おわりに</h2> <p>便利なタイムライン要素を作成したり、見た目を工夫して利用する人の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B6%A5%D3%A5%EA%A5%C6%A5%A3">ユーザビリティ</a>を上げるのは制作プロセスにおいて様々な恩恵がありそうですね。他にもいろいろな便利な機能があれば追記したいと思いますので、ご存じの方はぜひご教授ください。</p> hecomi uLipSync にアニメーションベイク機能を追加してみた hatenablog://entry/13574176438063884576 2022-02-19T20:25:58+09:00 2022-02-19T20:26:38+09:00 はじめに 前回は uLipSync で、リップシンクの解析結果のベイク機能の追加とタイムラインのサポートを行いました。 tips.hecomi.com 今回は、このベイク結果を元に、ブレンドシェイプを動かすアニメーションへと変換する仕組みを作りました。アニメーションとして保存できれば他のアニメーションとの組み合わせもしやすく、既存のワークフローにも組み込め、また後でキーを動かして調整もしやすくなります。本記事では使い方や仕組みなどを紹介します。 デモ 07. Animation Bake というシーンがサンプルシーンになります。 変換ウィンドウ 変換には、Animator コンポーネント、どの… <h2>はじめに</h2> <p>前回は <strong>uLipSync</strong> で、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>の解析結果のベイク機能の追加とタイムラインのサポートを行いました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F01%2F30%2F152519" title="uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/01/30/152519">tips.hecomi.com</a></cite></p> <p>今回は、このベイク結果を元に、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプを動かす<strong>アニメーションへと変換する</strong>仕組みを作りました。アニメーションとして保存できれば他のアニメーションとの組み合わせもしやすく、既存のワークフローにも組み込め、また後でキーを動かして調整もしやすくなります。本記事では使い方や仕組みなどを紹介します。</p> <h2>デモ</h2> <p><em>07. Animation Bake</em> というシーンがサンプルシーンになります。</p> <h3>変換ウィンドウ</h3> <p>変換には、<code>Animator</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>、どの母音がどの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプに対応しているかセットアップした <code>uLipSyncBlendShape</code>、そして Bake したデータのリストが必要です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220216/20220216003902.png" alt="f:id:hecomi:20220216003902p:plain" width="1200" height="854" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>作成されたアニメーション</h3> <p>サンプリングレートや変化として記録する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A4%B7%A4%AD%A4%A4%C3%CD">しきい値</a>が指定できるので、キーを間引いた形のアニメーションも作成できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220216/20220216003214.gif" alt="f:id:hecomi:20220216003214g:plain" width="795" height="470" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>再生</h3> <p>生の解析結果を再生するよりも適当に間引いた結果ローパスがかかったような形になり滑らかになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220216/20220216003932.gif" alt="f:id:hecomi:20220216003932g:plain" width="795" height="470" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>© Unity Technologies Japan/UCL</p> <h2>ダウンロード</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync" title="GitHub - hecomi/uLipSync: A MFCC-based LipSync plugin for Unity using Burst Compiler" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync">github.com</a></cite></p> <p>v2.1.0 で機能追加を行いました。</p> <h2>使い方</h2> <p><em>Window > uLipSync > Animation Clip Generator</em> を選択し、<strong>uLipSync Animation Clip Generator</strong> ウィンドウを開きます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220216/20220216231453.png" alt="f:id:hecomi:20220216231453p:plain" width="1200" height="857" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>アニメーションベイクをするには、uLipSync のセットアップを行ったシーンを開いておく必要があります。その上で、シーンにある<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>をセットしていきます。</p> <ul> <li><strong>Animator</strong> <ul> <li>シーンに存在する <code>Animator</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を選択します。</li> <li>この <code>Animator</code> を起点としたアニメーションクリップが作成されます。</li> </ul> </li> <li><strong>Blend Shape</strong> <ul> <li>シーンに存在する <code>uLipSyncBlendShape</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を選択します。</li> <li>セットアップの方法は<a href="https://tips.hecomi.com/entry/2022/01/30/152519">&#x524D;&#x56DE;&#x306E;&#x8A18;&#x4E8B;</a>をご参照ください。</li> </ul> </li> <li><strong>Baked Data List</strong> <ul> <li>アニメーションクリップに変換したい<code>BakedData</code> アセットを選択します。</li> </ul> </li> <li><strong>Sample Frame Rate</strong> <ul> <li>キーを打つサンプリングレートを指定します。</li> <li>60 なら 60 フレームで、30 なら 30 フレームでサンプリングを行います。</li> </ul> </li> <li><strong>Threshold</strong> <ul> <li>ウェイトがこの値だけ変化したときのみキーを打ちます</li> <li>ウェイトの最大値は 100 になりますので、10 なら 10% 変化したら、という意味合いになります。</li> </ul> </li> <li><strong>Output Directory</strong> <ul> <li>ベイクしたアニメーションクリップを出力する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを指定します。</li> <li>空の場合は <em>Assets</em> 以下に作成します。</li> </ul> </li> </ul> <p>セットアップ例です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220216/20220216004618.png" alt="f:id:hecomi:20220216004618p:plain" width="1200" height="853" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><em>Threshold</em> を 0、10、20 と変化させると次のようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220216/20220216235118.gif" alt="f:id:hecomi:20220216235118g:plain" width="809" height="471" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>仕組み</h2> <p>以下の記事を参考にさせていただきました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmebiustos.hatenablog.com%2Fentry%2F2015%2F09%2F16%2F230000" title="スクリプトでアニメーションクリップファイルを作成する - MEBIUSTOSのブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://mebiustos.hatenablog.com/entry/2015/09/16/230000">mebiustos.hatenablog.com</a></cite></p> <p>Unity でエディタ拡張からアニメーションを作成するには、次のような手順をたどります。</p> <ol> <li><code>AnimationClip</code> を作成</li> <li><code>EditorCurveBinding</code> でどの項目にキーを打つかを指定</li> <li><code>AnimationCurve</code> にキーを打ち必要に応じて傾きも調整</li> <li>2 と 3 をセットにして <code>AnimationClip</code> に登録、必要な数だけこれを繰り返す</li> <li><code>AssetDatabase</code> を使ってアセットとして保存</li> </ol> <p>となります。具体的に本アセットでアニメーションクリップを作成する流れは以下のような形になります(わかりやすいように一部改変)。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">internal</span> <span class="synType">class</span> AnimationCurveBindingData { <span class="synType">public</span> EditorCurveBinding binding; <span class="synType">public</span> AnimationCurve curve; } <span class="synType">public</span> <span class="synType">class</span> AnimationWizard : ScriptableWizard { ... <span class="synComment">// 書き込みには / 区切りでルート要素からのオブジェクト名が入る</span> <span class="synComment">// 例えばユニティちゃんなら Character1_Reference/Character1_Hips/...(略).../MTH_DEF といった具合</span> <span class="synComment">// これをルート要素(Animtor)を指定して得るためのメソッド</span> <span class="synType">string</span> GetRelativeHierarchyPath(GameObject target, GameObject parent) { <span class="synStatement">if</span> (!target || !parent) <span class="synStatement">return</span> <span class="synConstant">&quot;&quot;</span>; <span class="synType">string</span> path = <span class="synConstant">&quot;&quot;</span>; var gameObj = target; <span class="synStatement">while</span> (gameObj &amp;&amp; gameObj != parent) { <span class="synStatement">if</span> (!<span class="synType">string</span>.IsNullOrEmpty(path)) path = <span class="synConstant">&quot;/&quot;</span> + path; path = gameObj.name + path; gameObj = gameObj.transform.parent.gameObject; } <span class="synStatement">if</span> (gameObj != parent) <span class="synStatement">return</span> <span class="synConstant">&quot;&quot;</span>; <span class="synStatement">return</span> path; } <span class="synType">void</span> OnWizardOtherButton() { ... var bindingBase = <span class="synStatement">new</span> EditorCurveBinding(); bindingBase.path = GetRelativeHierarchyPath( blendShape.skinnedMeshRenderer.gameObject, animator.gameObject); bindingBase.type = <span class="synStatement">typeof</span>(SkinnedMeshRenderer); ... <span class="synStatement">foreach</span> (var bakedData <span class="synStatement">in</span> bakedDataList) { ... <span class="synComment">// 最終的に保存されるアセットとなる</span> <span class="synComment">// ここにキーを登録したカーブを登録していく</span> var clip = <span class="synStatement">new</span> AnimationClip(); <span class="synComment">// 各カーブの情報を一時保存しておくリスト</span> var dataList = <span class="synStatement">new</span> Dictionary&lt;<span class="synType">int</span>, AnimationCurveBindingData&gt;(); <span class="synComment">// 登録されているブレンドシェイプ</span> <span class="synStatement">foreach</span> (var bs <span class="synStatement">in</span> blendShape.blendShapes) { <span class="synStatement">if</span> (bs.index &lt; <span class="synConstant">0</span>) <span class="synStatement">continue</span>; <span class="synComment">// ブレンドシェイプ毎の EditorCurveBinding を作成</span> var mesh = blendShape.skinnedMeshRenderer.sharedMesh; var name = mesh.GetBlendShapeName(bs.index); var binding = bindingBase; binding.propertyName = <span class="synConstant">&quot;blendShape.&quot;</span> + name; <span class="synComment">// カーブと組み合わせておく</span> dataList.Add(bs.index, <span class="synStatement">new</span> AnimationCurveBindingData() { binding = binding, curve = <span class="synStatement">new</span> AnimationCurve(), }); } blendShape.OnAnimationBakeStart(); <span class="synComment">// サンプリング / 間引き用の情報</span> <span class="synType">float</span> dt = <span class="synConstant">1f</span> / sampleFrameRate; <span class="synStatement">for</span> (<span class="synType">float</span> time = <span class="synConstant">0f</span>; time &lt;= bakedData.duration; time += dt) { <span class="synComment">// BakedData からブレンドシェイプの値を取得</span> var frame = bakedData.GetFrame(time); var info = BakedData.GetLipSyncInfo(frame); blendShape.OnAnimationBakeUpdate(info, dt); var weights = blendShape.GetAnimationBakeBlendShapes(); <span class="synComment">// 登録しておいた各ブレンドシェイプ毎に見ていく</span> <span class="synComment">// 実際はここに threshold に応じた間引き処理が含まれるので</span> <span class="synComment">// 興味ある方はコードを覗いてみてください</span> <span class="synStatement">foreach</span> (var kv <span class="synStatement">in</span> dataList) { var index = kv.Key; var curve = kv.Value.curve; curve.AddKey(time, weights[index]); } } blendShape.OnAnimationBakeEnd(); <span class="synStatement">foreach</span> (var kv <span class="synStatement">in</span> dataList) { var data = kv.Value; var binding = data.binding; var curve = data.curve; <span class="synStatement">for</span> (<span class="synType">int</span> j = <span class="synConstant">0</span>; j &lt; curve.length; ++j) { var key = curve[j]; key.weightedMode = WeightedMode.Both; key.inWeight = key.outWeight = <span class="synConstant">1f</span> / <span class="synConstant">3f</span>; <span class="synComment">// 端点や上下ではカーブの傾きは 0 にする</span> <span class="synStatement">if</span> (j == <span class="synConstant">0</span> || j == curve.length - <span class="synConstant">1</span> || key.<span class="synStatement">value</span> &lt; <span class="synConstant">1f</span> || key.<span class="synStatement">value</span> &gt; <span class="synConstant">99f</span>) { key.inTangent = key.outTangent = <span class="synConstant">0f</span>; } <span class="synStatement">else</span> { var prevKey = curve[j - <span class="synConstant">1</span>]; var nextKey = curve[j + <span class="synConstant">1</span>]; <span class="synComment">// 山や谷となっている点の傾きは 0 にする</span> <span class="synStatement">if</span> ((key.<span class="synStatement">value</span> - prevKey.<span class="synStatement">value</span>) * (nextKey.<span class="synStatement">value</span> - key.<span class="synStatement">value</span>) &lt; <span class="synConstant">0f</span>) { key.inTangent = key.outTangent = <span class="synConstant">0f</span>; } <span class="synComment">// それ以外は前後のキーを見て傾きを求める</span> <span class="synStatement">else</span> { var a = (nextKey.<span class="synStatement">value</span> - prevKey.<span class="synStatement">value</span>) / (nextKey.time - prevKey.time); key.inTangent = key.outTangent = a; } } curve.MoveKey(j, key); } <span class="synComment">// 作成したカーブを指定したバインディング(どのブレンドシェイプか)と紐付けて</span> <span class="synComment">// アニメーションクリップに登録する</span> AnimationUtility.SetEditorCurve(clip, data.binding, data.curve); } <span class="synComment">// アセットとして保存</span> var path = $<span class="synConstant">&quot;{outputDirectory}/{bakedData.name}.anim&quot;</span>; path = AssetDatabase.GenerateUniqueAssetPath(path); AssetDatabase.CreateAsset(clip, path); } ... AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } ... } </pre> <h2>おわりに</h2> <p>これで一通り考えていた機能がとりあえず揃いました。あとは MFCC の類似度判定部分の改善や、併せてキャリブなしでもそれなりに動くプロファイル作成などを行いたいですが、またしばらくして気力が出てきたらやろうかな、と思います。</p> hecomi uLipSync でリップシンクの事前計算およびタイムライン対応をしてみた hatenablog://entry/13574176438053426498 2022-01-30T15:25:19+09:00 2022-12-28T23:43:17+09:00 はじめに 昨年に Unity 上でリアルタイムにリップシンクを行う uLipSync というものをリリースしました。 tips.hecomi.com AudioClip の入力を Job System と Burst コンパイラを利用してリアルタイムに解析して MFCC と呼ばれる特徴量を抽出し、予めキャリブレーションして作成しておいたプロファイルと照らし合わせて一番近い音素を推定、それをブレンドシェイプへと反映させる、という仕組みのものでした。ただ、マイク入力など、その場で毎回異なる音声波形が与えられる場合は良いのですが、歌やカットシーンのような予め決まった音源があるケースにおいてはリアルタ… <h2 id="はじめに">はじめに</h2> <p>昨年に Unity 上で<strong>リアルタイムに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a></strong>を行う <strong>uLipSync</strong> というものをリリースしました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2021%2F02%2F27%2F144722" title="Unity でリアルタイムにリップシンクを行うプラグインを更に作り直してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2021/02/27/144722">tips.hecomi.com</a></cite></p> <p><code>AudioClip</code> の入力を <strong>Job System</strong> と <strong>Burst</strong> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%E9">コンパイラ</a>を利用してリアルタイムに解析して <strong>MFCC</strong> と呼ばれる特徴量を抽出し、予め<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>して作成しておいたプロファイルと照らし合わせて一番近い音素を推定、それを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプへと反映させる、という仕組みのものでした。ただ、マイク入力など、その場で毎回異なる音声波形が与えられる場合は良いのですが、歌や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AB%A5%C3%A5%C8%A5%B7%A1%BC">カットシー</a>ンのような予め決まった音源があるケースにおいてはリアルタイムに計算する必要はなく、結果を事前に確認できるという利点の上でも処理負荷削減の面でも、予め計算しておけると便利です。</p> <p>そこで、今回新しく事前計算を行う仕組みを導入し、またタイムラインの対応も行いました。併せてコードや UI、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>支援<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の追加、サンプルの追加修正(<a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> 対応など)も行い、v2 としてリリースしました。本記事では新しくなった uLipSync の使い方の説明を行っていきます。</p> <h2 id="uLipSync-の特徴">uLipSync の特徴</h2> <p>以下のような特徴があります。</p> <ul> <li><strong>Job System</strong> と <strong>Burst</strong> を使いネイティブ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>は不使用で OS を問わずに高速動作可能</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>することでキャ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ毎の<strong>プロファイル作成が可能</strong></li> <li>🆕 <strong>ランタイム解析</strong>、<strong>事前ベイク処理</strong>どちらも利用可能</li> <li>🆕 事前ベイク処理の場合、<strong>Timeline との連携</strong>可能</li> </ul> <p><a href="https://tips.hecomi.com/entry/2016/02/16/202634">OVRLipSync</a> などと比べると設定が多くなってしまう(+ 汎用的な精度は劣る)のですが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>によって認識させたい音が調整可能だったり、対応プラットフォームが多かったり、Timeline との連携が出来たりといった点が差別化ポイントです。</p> <p>以下、もう少し雰囲気を掴んで頂けるよう画像と一緒に各機能を紹介します:</p> <h3 id="リップシンク"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a></h3> <p>登録した音素に併せた<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>が出来ます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126010216.gif" width="695" height="437" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="プロファイルの設定">プロファイルの設定</h3> <p>キャ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ毎にプロファイルを作成し、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>することで調整が出来ます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220127015053" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220127/20220127015053.png" width="1130" height="1160" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="リアルタイム解析">リアルタイム解析</h3> <p><code>AudioSource</code> で再生された音声波形を JobSystem と Burst Compiler の恩恵でそこそこ高速にリアルタイムで解析し、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプへ反映します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220127015123" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220127/20220127015123.png" width="1200" height="488" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="マイク入力">マイク入力</h3> <p>マイク入力を反映する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が付属していますので、配信用途などにも使えると思います。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220127015142" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220127/20220127015142.png" width="1200" height="288" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="VRM-対応"><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> 対応</h3> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> 用の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプを動かす<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>がサンプル中に付属しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220127/20220127015242.gif" width="550" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="事前計算">事前計算</h3> <p>単純な <code>AudioClip</code> 再生向けには、事前に計算しておくことが出来ます。結果が確認できて調整しやすかったりパフォーマンス面での利点などがあります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220127015558" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220127/20220127015558.png" width="1102" height="526" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="タイムライン連携">タイムライン連携</h3> <p>また事前計算データとの組み合わせでタイムライン連携が可能です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126023942.gif" width="784" height="378" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="追記20220219-アニメーションベイク">追記(2022/02/19): アニメーションベイク</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F02%2F19%2F202558" title="uLipSync にアニメーションベイク機能を追加してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/02/19/202558">tips.hecomi.com</a></cite></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220216/20220216003214.gif" width="795" height="470" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="追記20220831テクスチャ変更">追記(2022/08/31):テクスチャ変更</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F08%2F31%2F232553" title="uLipSync で 2D キャラクタ向けにテクスチャや UV を変更できるコンポーネントを作った - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/08/31/232553">tips.hecomi.com</a></cite></p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220831005326" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220831/20220831005326.gif" width="715" height="368" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="追記20221228Animator">追記(2022/12/28):Animator</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2022%2F12%2F28%2F233803" title="uLipSync で Animator を使ったリップシンクができるように更新してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2022/12/28/233803">tips.hecomi.com</a></cite></p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20221228213045" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20221228/20221228213045.gif" width="938" height="486" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h2 id="ダウンロード">ダウンロード</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuLipSync" title="GitHub - hecomi/uLipSync: MFCC-based LipSync plug-in for Unity using Job System and Burst Compiler" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uLipSync">github.com</a></cite></p> <p>Package Manager からインストールもしくは .unitypackage をダウンロード・プロジェクトへ展開する方法のどちらかで導入できます。Package Manager をオススメしますが、慣れていない人は .unitypackage をお使いください(ただし、この場合も Package Manager 上で必要な公式パッケージをインストールする必要があります)。</p> <h3 id="Package-Manager">Package Manager</h3> <p>導入する方法はどちらかをお選びください。個人的にはバージョン管理が出来るので Scoped Registry 登録がおすすめです。執筆時のバージョンは <strong>v2.0.2</strong> になります</p> <h6 id="Scoped-Registry">Scoped Registry</h6> <ul> <li><em>Project Settings > Package Manager</em> から Scoped Registries に以下を登録 <ul> <li>Name: <code>hecomi</code></li> <li>URL: <code>https://registry.npmjs.com</code></li> <li>Scope: <code>com.hecomi</code></li> </ul> </li> <li>Package Manager の <em>Packages: My Registries</em> から uLipSync をインストール</li> <li>必要なサンプルはインストール後の画面から適宜インストール</li> </ul> <h6 id="Git-URL">Git URL</h6> <ul> <li><em>Add package from git URL</em> で以下の URL を追加 <ul> <li><strong><a href="https://github.com/hecomi/uLipSync.git#upm">https://github.com/hecomi/uLipSync.git#upm</a></strong></li> </ul> </li> <li>必要なサンプルはインストール後の画面から適宜インストール</li> </ul> <p>UPM についての詳細は以下の記事をご参照下さい:</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2021%2F12%2F19%2F003344" title="Unity で自作ライブラリの Package Manager / Scoped Registry 対応をしてみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2021/12/19/003344">tips.hecomi.com</a></cite></p> <h3 id="unitypackage-のダウンロード">.unitypackage のダウンロード</h3> <ul> <li>最新のバージョンを <a href="https://github.com/hecomi/uLipSync/releases">Releases</a> からダウンロード、本体と必要なサンプルをプロジェクトに展開</li> <li>Package Mananager から Mathematics および Burst をインストール</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220122/20220122152035.png" width="1200" height="338" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="使い方---ランタイム解析編">使い方 - ランタイム解析編</h2> <h3 id="仕組み">仕組み</h3> <p>Unity 組み込みの <strong>AudioSource</strong> で音が再生されると、同じ GameObject にアタッチされた<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <strong><a href="https://docs.unity3d.com/jp/current/ScriptReference/MonoBehaviour.OnAudioFilterRead.html">OnAudioFilterRead()</a></strong> というメソッドにその音のバッファが入ってきます。このバッファを書き換えてリ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A1%BC%A5%D6">バーブ</a>処理を施したりといったことが可能なのですが、今どんな波形が再生されているかもわかるので、これを解析して <strong>メル周波数ケプストラム係数</strong>(Mel-Frequency Cepstrum Coefficients、<strong>MFCC</strong>)という人間の声道特性を表す特徴量を計算します。つまり、うまく計算すれば再生されている現在の波形が「あ」なら「あ」っぽいパラメタが、「え」なら「え」っぽいパラメタが得られるわけですね(母音以外にも「すー」みたいな子音も分かります)。これを予め登録しておいた「あいうえお」それぞれのパラメタと比較し、現在の音にそれぞれの音素がどれだけ近いかを算出、それを <code>SkinnedMeshRenderer</code> の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプへと反映させれば<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>が可能となるわけです。マイクからの入力を AudioSource に流し込んであげれば、今喋っている声に合わせて口パクも可能になります。</p> <p>この解析を行う<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が <code>uLipSync</code>、この<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>にセットする各音素パラメタのデータが <code>Profile</code>、そして<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプを動かすための<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が <code>uLipSyncBlendShape</code> となります。また、マイクの音声を再生する <code>uLipSyncMicrophone</code> アセットも用意してあります。これらを図解するとこんな感じです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220121/20220121014940.png" width="1200" height="652" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>なので登場人物としては大きく分けて「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>されたデータ」、「データをもとに音声解析をする人」、「解析された結果を使ってなにかする人」の 3 種類がいる形です。ではこれを踏まえてランタイム解析を行って<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>をさせるための手順について見ていきましょう。</p> <h3 id="セットアップ">セットアップ</h3> <p>まずは、ユニティちゃんでセットアップしてみましょう。サンプルシーンは <em>Samples / 01. Play AudioClip / 01-1. Play Audio Clip</em> になります。UPM からインストールした人は <em>Samples / 00. Common</em> サンプルをインストールしておいてください(ユニティちゃんのアセットが含まれています)。</p> <p>まず、ユニティちゃんを配置した後、<code>AudioSource</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を任意のゲームオブジェクト(音がなる場所)に追加し、クリップを設定して音がなるようにします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220122152758" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220122/20220122152758.png" width="1200" height="765" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>次に同じゲームオブジェクトに <code>uLipSync</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を追加します。ここに<code>Profile</code> を指定するのですが、今は <em>uLipSync-Profile-UnityChan</em> をリストから選択して<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンしてください(Male など違うものを指定すると正しく<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>出来ません)。これで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>の解析が行われるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220122145416" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220122/20220122145416.png" width="1171" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>次に、解析結果を受け取って<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプに反応するようセットアップします。<code>uLipSyncBlendShape</code> をユニティちゃんの <code>SkinnedMeshRenderer</code> のルートに配置し、<em>Sinned Mesh Renderer</em> から対象となる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプである <code>MTH_DEF</code> を選択、<em>Blend Shapes > Phoneme - BlendShape Table</em> で A、I、U、E、O、N、 -の計 7 つの項目を + をポチポチして追加します(N は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A1%D6%A4%F3%A1%D7">「ん」</a>、- はノイズを登録してあります)。そして、それぞれの音素に対応する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプを画像のように選択します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220122150714" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220122/20220122150714.png" width="1021" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>最後に、この 2 つをつなげます。<code>uLipSync</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <em>Parameters > On Lip Sync Updated (LipSyncInfo)</em> という場所で + を押してイベントを 1 つ追加し、<em>None (Object)</em> となっているところに <code>uLipSyncBlendShape</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>がついているゲームオブジェクト(または<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>)をドラッグドロップします。<em>No Function</em> のプルダウンリストから <code>uLipSyncBlendShape</code> を探し、その中にある <code>OnLipSyncUpdate</code> を選択します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220122150108" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220122/20220122150108.png" width="1200" height="1127" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>これでシーンを実行すると口を動かしながら喋ってくれるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20210109183643" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20210109/20210109183643.gif" width="640" height="480" loading="lazy" title="" class="hatena-fotolife" style="width:720px" itemprop="image"></a></span></p> <h3 id="口パクの反応の調整">口パクの反応の調整</h3> <p>認識するボリュームや口の反応速度は <code>uLipSyncBlendShape</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の、<em>Paramteters</em> の中で行なえます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123020256" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123020256.png" width="1200" height="289" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <ul> <li><em>Volume Min/Max (Log10)</em> <ul> <li>認識する最小と最大(口が閉じている / 最も大きく開く)のボリュームを設定(Log10 しているので、0.1 なら -1、0.01 なら -2 となってます)</li> </ul> </li> <li><em>Smoothness</em> <ul> <li>口パクの応答速度、小さすぎるとパクパクしすぎてしまうが、逆に大きいほど滑らかになるが追従性が悪くなるので適当な値を選んでください</li> </ul> </li> </ul> <p>ボリュームに関しては、現在・最大・最小の音量がどのくらいだったかという情報が <code>uLipSync</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <em>Runtime Information</em> という中で見ることが出来ますので、これを参考に設定してみてください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123134854" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123134854.png" width="1200" height="488" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="オーディオソースの位置">オーディオソースの位置</h3> <p><code>AudioSource</code> は口の位置に、<code>uLipSync</code> はどこか別のゲームオブジェクトにアタッチしたいケースもあると思います。その際は、少し面倒ですが、<code>uLipSyncAudioSource</code> という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を <code>AudioSource</code> と同じゲームオブジェクトに追加し、<code>uLipSync</code> の <em>Parameters > Audio Source Proxy</em> にこれをセットしてください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123235659" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123235659.png" width="1108" height="388" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p><em>Samples / 03. AudioSource Proxy</em> がサンプルシーンとなります</p> <p>中身としては <code>uLipSyncAudioSource</code> が <code>AudioSource</code> の結果を <code>OnAudioFilterRead</code> で拾ってきて、それをイベント経由で <code>uLipSync</code> に渡しています。</p> <h3 id="マイクの利用">マイクの利用</h3> <p>マイクを入力としたい場合は <code>uLipSyncMicrophone</code> という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を <code>uLipSync</code> と同じゲームオブジェクトに追加します。これはマイク入力をクリップとした <code>AudioSource</code> を生成する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>です。サンプルシーンは <em>Samples / 02-1. Mic Input</em> になります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123014221" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123014221.png" width="1200" height="173" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p><em>Device</em> から入力に使用するデ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>を選択、<em>Is Auto Start</em> にチェックが入っていると自動で開始します。マイク入力の開始・停止は実行時に次のような UI になるので、<em>Stop Mic</em> / <em>Start Mic</em> を押下することで可能です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123014540" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123014540.png" width="1200" height="288" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>から行う場合は、次のように <code>uLipSync.MicUtil.GetDeviceList()</code> を利用して使用するマイクを特定し、そのインデックスをこの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <code>index</code> に指定して渡して <code>StartRecord()</code> / <code>StopRecord()</code> することで、開始・停止を行ってください。</p> <p>なお、マイクの入力はそのままでは自分の発話から少し遅れて Unity 内で再生されてしまいます。配信などで声は別のソフトでキャプチャしたものを利用したいケースなどは、<code>uLipSync</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の、<em>Parameters > Output Sound Gain</em> を 0 にしてください。<code>AudioSource</code> の <em>Volume</em> を 0 にしてしまうと、<code>OnAudioFilterRead()</code> に渡ってくるデータが無音になってしまい解析が出来ないので、解析後に 0 にする処理をこれで行います。</p> <p><code>uLipSync</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <em>Profile > Profile</em> で適当なサンプル中のプロファイル(男性なら <em>Male</em>、女性なら <em>Female</em> など)を選択し実行すると、声に合わせて口パクすると思います。ただ、個人にあった音声ではないので、デフォルトの音声だと精度は良くないかもしれません。次にこれを自身の声に合わせた<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>データを作成する方法について見ていきます。</p> <h2 id="キャリブレーション"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a></h2> <p>先ほどは元から用意してあった <code>Profile</code> データを使ったのですが、他の音声向け(他の声優さんのデータや自分の声)に調整したデータを作成する方法を次に紹介します。</p> <h3 id="プロファイルの作成">プロファイルの作成</h3> <p><code>uLipSync</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の <em>Profile > Profile > Create</em> ボタンを押すとデータが Assets のルートに作成され、そのプロファイルがセットされます。Project ウィンドウから右クリック > <em>uLipSync</em> > <em>Profile</em> でも作成できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123115343" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123115343.png" width="1200" height="226" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>次に認識させたい音素を <em>Profile > MFCC > MFCCs</em> に登録していきます。基本的には AIUEO で良いと思いますが、息用の音素(「-」など適当な文字)も追加しておくと息が当たってしまったときに口がパクパクしてしまうのを抑制できるのでおすすめです。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A1%D6%A4%F3%A1%D7">「ん」</a>を表す N も登録しても良いかもしれません。登録する文字は <code>uLipSyncBlendShape</code> と一致させさえすれば良いので、アルファベットでもひらがなでもカタカナでも構いません。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123141021" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123141021.png" width="940" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>次に、作成した音素をそれぞれ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>していきます。</p> <h3 id="マイク入力を使った調整">マイク入力を使った調整</h3> <p>まずはマイクを使う方法です。<code>uLipSyncMicrophone</code> をオブジェクトに追加してください。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>は実行時に行うので、この状態でゲームを開始します。すると各音素の右側に <em>Calib</em> というボタンが現れるので、マイクに向かってそれぞれの音素の音を喋りながらこのボタンを押下し続けてください。A なら「あーーーー」、I なら「いーーーーーー」といった具合のものです。ノイズだったら何も喋らなかったり息を吹きかけたりといった感じですね。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123142528.gif" width="1080" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>予め <code>uLipSyncBlendShape</code> を設定していれば、こんな感じでだんだん口が合っていくのが面白いです。<code>Profile</code> は <code>ScriptableObject</code> なので実行を停止してもデータは保存されます。</p> <p>なお、地声と裏声などで口が一致させられない場合は、同名の複数音素を <code>Profile</code> に登録することも出来ますので適宜調整してください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123143307" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123143307.png" width="1171" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <h3 id="音声データを使った調整">音声データを使った調整</h3> <p>次に音声データを使った<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>方法です。「あーーーーー」や「いーーーーー」と喋ってくれる音声があればそれをループ再生した状態で同様に <em>Calib</em> ボタンを押下してください。ただ大体のケースではそんな音声が無いので既存の音声の「あーー」っぽい部分や「いーー」っぽい部分をトリミングして再生することで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>を実現したいです。そこで便利な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が <code>uLipSyncCalibrationAudioPlayer</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>です。これは音声波形を見ながら再生したい部分をちょっと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%ED%A5%B9%A5%D5%A5%A7%A1%BC%A5%C9">クロスフェード</a>させながらループ再生してくれる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123143058" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123143058.gif" width="550" height="212" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>境界をドラッグしながら「あー」と言っていそうな部分を選択し、その状態でそれぞれの音素の <em>Calib</em> ボタンを押下して <code>Profile</code> に MFCC を登録してください。</p> <h3 id="キャリブレーションのコツ"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>のコツ</h3> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>する際は以下のことに気をつけると良いです。</p> <ul> <li>なるべくノイズのない環境で行う</li> <li>登録した MFCC がなるべく一定になってるようにする <ul> <li>横軸 12 次元があるフレームの MFCC で、内部的には縦の平均を取って比較します</li> </ul> </li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>後に何度かチェックしてうまく行かない音素は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>し直したり別の音素として登録したりする <ul> <li>複数の同名音素が登録できるので声色を変えたときに合わない場合は、それらを追加で登録してみてください</li> <li>どうしても合わないときは誤った音素が無いか確認する <ul> <li>同名の音素で MFCC の色のパターンがぜんぜん違うものは間違っている可能性があります(同じ音素は似たようなパターンになるはず)</li> </ul> </li> </ul> </li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>後のチェック時は <em>Runtime Information</em> を折りたたんでください <ul> <li>毎フレームエディタの再描画が行われるのでフレームレートが 60 を割ることがあります</li> </ul> </li> </ul> <h2 id="リップシンクの事前計算"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>の事前計算</h2> <p>さて、これまではランタイムの処理について見てきました。ここからは事前計算によるデータの制作について見ていきます。</p> <h3 id="仕組み-1">仕組み</h3> <p>オーディオデータがある場合は毎フレームどんな解析結果が渡ってくるか事前に計算可能なので、これを <code>BakedData</code> という <code>ScriptableObject</code> に焼き込んでおきます。実行時にはランタイムで解析していた <code>uLipSync</code> の代わりに、<code>uLipSyncBakedDataPlayer</code> という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を介して再生するようにします。これは <code>uLipSync</code> と同様に解析結果をイベントで通知することができるため、<code>uLipSyncBlendShape</code> を登録しておくことで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>が実現できます。この流れを図示すると以下のようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123233536.png" width="1200" height="581" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>JobSystem は実行中でなくても動かすことが出来るのが良いですね。</p> <h3 id="セットアップ-1">セットアップ</h3> <p>サンプルシーンは <em>Samples / 05. Bake</em> になります。<em>Project Window</em> から <em>Create > uLipSync > BakedData</em> で <code>BakedData</code> を作成できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220125231719" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220125/20220125231719.png" width="1200" height="863" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220125231852" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220125/20220125231852.png" width="1014" height="1044" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>ここで、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>した <code>Profile</code> とその音声である <code>AudioClip</code> を指定し、<em>Bake</em> ボタンを押下するとデータの解析が行われデータが完成します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220125232304" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220125/20220125232304.gif" width="569" height="517" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>比較的うまくいくと以下のようなデータが出来ます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220126004929" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126004929.png" width="1200" height="574" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>このデータを <code>uLipSyncBakedDataPlayer</code> にセットします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220126005734" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126005734.png" width="765" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>これで再生すれば OK です。エディタ上で何度もチェックしたい場合は <em>Play</em> ボタンを押下、別<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>から再生したい場合は <code>Play()</code> を呼んでください。</p> <h3 id="結果">結果</h3> <p>何個か母音が間違っていても正直それほど変には見えずそれっぽく見えます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126010216.gif" width="695" height="437" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="パラメタ">パラメタ</h3> <p><em>Time Offset</em> というスライダを調整すると、口パクのタイミングを早めたり遅くしたり出来ます。ランタイム解析だと声より先に口を開けておく、みたいな調整はできませんが、事前解析だと少し早めて口を開けられるので少し自然に見せたり調整ができます。</p> <h3 id="一括変換">一括変換①</h3> <p>すべてのキャ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タボイスの <code>AudioClip</code> を一括で <code>BakedData</code> へ変換したいケースもあると思います。その際は、<em>Window > uLipSync > Baked Data Generator</em> を利用してください。</p> <p>一括変換する際に使用したい <code>Profile</code> を選択し、対象となる <code>AudioClip</code> を選択します。<em>Input Type</em> が <em>List</em> の場合は直接 <code>AudioClip* を登録してください(*Project* ウィンドウから複数選択でドラッグドロップすると楽です)。*Directory* にした場合は、ファイルダイアログが開きディレクトリを指定でき、そのディレクトリ以下の</code>AudioClip` を自動で列挙してくれます。</p> <p><em>Generate</em> ボタンを押すと変換が始まります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220127/20220127010319.gif" width="559" height="396" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="一括変換-1">一括変換②</h3> <p>既に作成したデータが有る際、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>を見直して <code>Profile</code> を変更するケースも出てくると思います。この際は、各プロファイルの <em>Baked Data</em> タブの中に <em>Reconvert</em> ボタンがあるので、それを押下することで一括変換することが可能です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220127/20220127014940.png" width="1200" height="711" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="タイムライン連携-1">タイムライン連携</h2> <p><code>BakedData</code> を使用することで、Timeline 連携も可能になりました。</p> <h3 id="仕組み-2">仕組み</h3> <p>Timeline に専用のトラックとクリップを追加できるようにしました。その上でこのタイムラインのデータを使ってどのオブジェクトを動かすのかという<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">バインディング</a>を行わなければなりません。そこで、再生情報を受け取って <code>uLipSyncBlendShape</code> へと通知するための <code>uLipSyncTimelineEvent</code> という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を導入しました。図示すると以下のような流れになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126021906.png" width="1200" height="516" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="セットアップ-2">セットアップ</h3> <p>Timeline 上のトラックのエリアで右クリックし、<em>uLipSync.Timeline > U Lip Sync Clip</em> から専用のトラックを追加します。このトラックで右クリックしクリップを追加します。<code>BakedData</code> を直接ドラッグドロップしても OK です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126020358.gif" width="784" height="403" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>追加したトラックを選択するとインスペクタには次のような UI が表示されます。<code>BakedData</code> の差し替えもここで出来ます</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220126014406" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126014406.png" width="763" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>そしてこれを実際に再生して口パク出来るように<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">バインディング</a>をしていきます。<code>uLipSyncTimelineEvent</code> を何らかのゲームオブジェクトに追加します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220126022401" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126022401.png" width="1136" height="268" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>このとき、<em>On Lip Sync Update (LipSyncInfo)</em> には <code>uLipSyncBlendShape</code> を登録しておきます。</p> <p>そして <code>PlayableDirector</code> のついているゲームオブジェクトをクリックし、Timeline ウィンドウの <code>uLipSyncTrack</code> 上の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">バインディング</a>するスロットに、そのゲームオブジェクトをドラッグドロップします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220126023021" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126023021.png" width="1200" height="358" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>これで <code>uLipSyncTimelineEvent</code> へ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>情報が飛んでくるようになり、<code>uLipSyncBlendShape</code> への接続が出来ました。再生はランタイムでなくても出来ますのでアニメーションと併せたり調整が出来ます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220126/20220126023942.gif" width="784" height="378" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="VRM-対応-1"><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> 対応</h2> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプは <code>VRMBlendShapeProxy</code> という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を介して制御されます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fvirtualcast.jp%2Fwiki%2Fvrm%2Fsetting%2Fblendshap" title="VRMのブレンドシェイプ設定 [VirtualCast]" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://virtualcast.jp/wiki/vrm/setting/blendshap">virtualcast.jp</a></cite></p> <p><code>uLipSyncBlendShape</code> では <code>SkinnedMeshRenderer</code> を直接弄ってましたが、代わりにこれを使うよう修正した <code>uLipSyncBlendShapeVRM</code> という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が <em>Samples/04. <a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a></em> に含まれています。<em>04. <a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a></em> シーンは <a class="keyword" href="http://d.hatena.ne.jp/keyword/VRM">VRM</a> のセットアップをして<a href="https://3d.nicovideo.jp/works/td32797">&#x30CB;&#x30B3;&#x30CB;&#x7ACB;&#x4F53;&#x3061;&#x3083;&#x3093;</a>をインポートしていれば再生できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220123150546" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123150546.png" width="1026" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220123/20220123150601.gif" width="550" height="400" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="その他-Tips">その他 Tips</h2> <h3 id="独自イベントの追加">独自イベントの追加</h3> <p><code>uLipSyncBlendShape</code> は 3D ですが、代わりにテクスチャアニメーションをさせたい、みたいなケースでは自分で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を書いて対応させることが出来ます。具体的には <code>uLipSync.LipSyncInfo</code> を受け取る関数を用意した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を用意し、これを <code>uLipSync</code> や <code>uLipSyncBakedDataPlayer</code> の<code>OnLipSyncUpdate(LipSyncInfo)</code> に登録します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220124015840" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220124/20220124015840.png" width="1200" height="728" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>以下は認識した結果を <code>Debug.Log()</code> 出力する簡単な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>の例です。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> uLipSync; <span class="synType">public</span> <span class="synType">class</span> <span class="synType">DebugPrintLipSyncInfo </span><span class="synStatement">:</span> MonoBehaviour { <span class="synType">public</span> <span class="synType">void</span> OnLipSyncUpdate(LipSyncInfo info) { <span class="synStatement">if</span> (<span class="synStatement">!</span>isActiveAndEnabled) <span class="synStatement">return</span>; <span class="synStatement">if</span> (info.volume <span class="synStatement">&lt;</span> Mathf.Epsilon) <span class="synStatement">return</span>; Debug.LogFormat(<span class="synConstant">$&quot;PHENOME: </span><span class="synSpecial">{</span>info.phoneme<span class="synSpecial">}</span><span class="synConstant">, VOL: </span><span class="synSpecial">{</span>info.volume<span class="synSpecial">}</span><span class="synConstant"> &quot;</span>); } } </pre> <p><code>LipSyncInfo</code> は次のような構造体です。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">struct</span> LipSyncInfo { <span class="synType">public</span> <span class="synType">string</span> phoneme; <span class="synComment">// 最も近い音素</span> <span class="synType">public</span> <span class="synType">float</span> volume; <span class="synComment">// 正規化されたボリューム(0 ~ 1)</span> <span class="synType">public</span> <span class="synType">float</span> rawVolume; <span class="synComment">// ボリュームの生値</span> <span class="synType">public</span> Dictionary&lt;<span class="synType">string</span>, <span class="synType">float</span>&gt; phonemeRatios; <span class="synComment">// 音素とその割合のテーブル</span> } </pre> <h3 id="JSON-への保存--読み込み"><a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a> への保存 / 読み込み</h3> <p>外部ファイルとのやり取り用に、<code>Profile</code> の <a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a> への保存・読み込みも備えています。エディタからは <em>Import / Export <a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a></em> タブから保存・読み込みしたい <a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a> を指定し、<em>Import</em> または <em>Export</em> ボタンを押下してください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/hecomi/20220127232709" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220127/20220127232709.png" width="1128" height="162" loading="lazy" title="" class="hatena-fotolife" style="width:640px" itemprop="image"></a></span></p> <p>コードで行いたい場合は次のようなコードで可能です。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">var</span> lipSync <span class="synStatement">=</span> GetComponent&lt;uLipSync&gt;(); <span class="synType">var</span> profile <span class="synStatement">=</span> lipSync.profile; <span class="synComment">// Export</span> profile.Export(path); <span class="synComment">// Import</span> profile.Import(path); </pre> <h3 id="ランタイムでのキャリブレーション">ランタイムでの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a></h3> <p>ランタイムで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E3%A5%EA%A5%D6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">キャリブレーション</a>を行いたいときは、次のように <code>uLipSync</code> に対して <code>uLipSync.RequestCalibration(int index)</code> でリク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを行うことで可能です。現在再生されている音から計算した MFCC が指定した音素に設定されます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink>lipSync <span class="synStatement">=</span> GetComponent&lt;uLipSync&gt;(); <span class="synStatement">for</span> (<span class="synType">int</span> i <span class="synStatement">=</span> <span class="synConstant">0</span>; i <span class="synStatement">&lt;</span> lipSync.profile.mfccs.Count; <span class="synStatement">++</span>i) { <span class="synType">var</span> key <span class="synStatement">=</span> (KeyCode)((<span class="synType">int</span>)(KeyCode.Alpha1) <span class="synStatement">+</span> i); <span class="synStatement">if</span> (Input.GetKey(key)) lipSync.RequestCalibration(i); } </pre> <p>実際の動作は <em>CalibrationByKeyboardInput.cs</em> で確認してください。また、ビルド後の <code>Profile</code> の保存や復元は <a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a> で行うのが良いかと思います。</p> <h3 id="WebGL-での動作"><a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> での動作</h3> <p><a href="https://twitter.com/uezochan">&#x3046;&#x3048;&#x305E;&#x3046;&#x3055;&#x3093;</a>が <a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> とのつなぎを試して下さってました。少し手を入れると <a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a> でも動作できるようです。ご調査ありがとうございます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fuezo%2FuLipSyncWebGL" title="GitHub - uezo/uLipSyncWebGL: An experimental plugin to use uLipSync on WebGL platform." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/uezo/uLipSyncWebGL">github.com</a></cite></p> <h3 id="Mac-でのビルド"><a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a> でのビルド</h3> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a> でビルド時に次のようなエラーに出くわすことがあります。</p> <blockquote><p>Building Library/Bee/artifacts/MacStandalonePlayerBuildProgram/Features/uLipSync.Runtime-FeaturesChecked.txt failed with output: Failed because this command failed to write the following output files: Library/Bee/artifacts/MacStandalonePlayerBuildProgram/Features/uLipSync.Runtime-FeaturesChecked.txt</p></blockquote> <p>これはマイクのアクセスコードに関連している可能性がありますが、<em>Project Settings > Player</em> の <em>Other Settings > <a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a> Configuration > Microphone Usage Description</em> に何かを書くと修正されます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20220125/20220125225604.png" width="1200" height="515" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="前回からの小さな変更">前回からの小さな変更</h2> <p>これまでの利用者の方向けに上記新機能以外の差分情報です。</p> <h5 id="パラメタの整理">パラメタの整理</h5> <p>前回からは <code>Profile</code> に登録する情報と <code>uLipSyncBlendShape</code> に登録する情報の切り分けをもっときれいにしました。具体的には、MFCC の算出に必要な情報のみを <code>Profile</code> に格納するようにし、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>関連のパラメタなどは <code>uLipSyncBlendShape</code> が受け持つようにしました。また併せて不要なパラメタは廃止し、極力設定項目が少なくなるよう調整しました。</p> <h5 id="ブレンドシェイプへの反映改善"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプへの反映改善</h5> <p>以前は、解析の結果から最も近い単一の母音を選んでいましたが、「い」と「え」の中間みたいな音だったら口がパラパラ切り替わってしまっていたので、距離をそれぞれ計算し、本当に中間だったら 0.5、0.5 となるような形で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプに反映されるように修正しました。</p> <h5 id="リスト表示改善">リスト表示改善</h5> <p>後は細かいですが、自前で書いていたリストの UI も <code>ReorderableList</code> を使うようにしてより直感的に追加・削除・並び替えを出来るようにしました。</p> <h2 id="おわりに">おわりに</h2> <p>バージョンを重ねる毎に破壊的変更をしていて申し訳ありません…。次の大きな更新はいつになるか分かりませんが、現状プロファイルに登録された各音素との誤差を調べて最小になるものを選択する部分を、何かしら賢い手法にするところをやりたいと考えています(そもそも最小距離というのがかなり手抜きではあります…)。また、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>シェイプのアニメーションへの変換も考えています。こちらはマイナーバージョンによるアップデートで近日中に行う予定です。</p> <p>今回、タイムラインの拡張なども試して色々と面白かったので、この辺りはまた別記事にまとめたいと思います。</p> <h2 id="ライセンス">ライセンス</h2> <p>© Unity Technologies Japan/UCL</p> hecomi Unity で自作ライブラリの Package Manager / Scoped Registry 対応をしてみた hatenablog://entry/13574176438039544623 2021-12-19T00:33:44+09:00 2021-12-21T11:31:26+09:00 Unity Advent Calender 2021 この記事は「Unity Advent Calendar 2021」19 日目の記事です。 qiita.com 18 日目は @Azukiidxさんによる以下の記事でした。 qiita.com 私は(残念ながら非推奨になってしまった)UNET のサーバー向けに Linux 上でヘッドレスで動かすビルドを作ったことはありましたが、Linux 上で動くエディタはまだ試したことはありませんでした。Linux の開発環境やサポートが必要になったら試してみようと思います。 はじめに 前々回の記事で、自作ライブラリの Package Manager(UP… <h2>Unity Advent Calender 2021</h2> <p>この記事は「<strong>Unity Advent Calendar 2021</strong>」<strong>19 日目</strong>の記事です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fadvent-calendar%2F2021%2Funity" title="Calendar for Unity | Advent Calendar 2021 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/advent-calendar/2021/unity">qiita.com</a></cite></p> <p>18 日目は <a href="https://twitter.com/Azukiidx">@Azukiidx</a>さんによる以下の記事でした。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2FAzuQiita%2Fitems%2F16ae1154e99c27731cb7" title="俺だってLinuxでゲーム開発したいんだよ! Linux版UnityEditorを実務で使ってみた - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/AzuQiita/items/16ae1154e99c27731cb7">qiita.com</a></cite></p> <p>私は(残念ながら非推奨になってしまった)<a href="https://tips.hecomi.com/entry/2015/08/14/220030">UNET &#x306E;&#x30B5;&#x30FC;&#x30D0;&#x30FC;</a>向けに <a class="keyword" href="http://d.hatena.ne.jp/keyword/Linux">Linux</a> 上でヘッドレスで動かすビルドを作ったことはありましたが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Linux">Linux</a> 上で動くエディタはまだ試したことはありませんでした。<a class="keyword" href="http://d.hatena.ne.jp/keyword/Linux">Linux</a> の開発環境やサポートが必要になったら試してみようと思います。</p> <h2>はじめに</h2> <p>前々回の記事で、自作ライブラリの <strong>Package Manager</strong>(<strong>UPM</strong>)対応についての記事を書きました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2021%2F10%2F29%2F001304" title="Unity で .unitypackage で配布していたアセットを Package Manager 対応してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2021/10/29/001304">tips.hecomi.com</a></cite></p> <p>Package Manager の登場により、元々はビルトインだった数々の機能(e.g. URP/HDRP などの新<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>パイプライン、新しい Input System、XR 関連機能、Timeline、Cinemachine など)がオプションとして提供され、必要な機能だけ自分のプロジェクトで使えるようになりました。またそれだけに留まらず、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>も同じ仕組みの上でアセットを配布できるようになり、結果としてユーザは必要な機能を <a href="https://docs.unity3d.com/ja/current/Manual/upm-ui.html">Package Manager &#x306E; GUI</a> 上で簡単に追加・削除できるようになりました。更に裏側では、それらの<a href="https://docs.unity3d.com/ja/current/Manual/upm-ui-update2.html">&#x30D0;&#x30FC;&#x30B8;&#x30E7;&#x30F3;&#x7BA1;&#x7406;</a>が適切に行われ、<a href="https://docs.unity3d.com/ja/current/Manual/upm-dependencies.html">&#x4F9D;&#x5B58;&#x95A2;&#x4FC2;</a>が適切に解決され、また<a href="https://docs.unity3d.com/ja/current/Manual/upm-cache.html">&#x30B0;&#x30ED;&#x30FC;&#x30D0;&#x30EB;&#x30AD;&#x30E3;&#x30C3;&#x30B7;&#x30E5;</a>を通じて異なるプロジェクト間でアセットが共有され、...といった具合に色々と便利になっています。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>に関しては対応しているライブラリはそれほど多くないですが、それでも徐々に増えてきている印象です。こういったパッケージマネージャの仕組みは Unity 以外の環境では珍しいものではないですが、コードだけでなく GUID を通じてアセットも含めて依存関係解決できるようになっており、かなり便利だなと感じています。</p> <p>前述の記事の執筆後、一月かけて .unitypackage 形式で配布しているすべての自分の Unity プロジェクトのメンテナンスと UPM 対応を行いました。また、<a href="https://www.npmjs.com/">npmjs</a> へ登録を行い、<a href="https://docs.unity3d.com/ja/current/Manual/upm-scoped.html">&#x30B9;&#x30B3;&#x30FC;&#x30D7;&#x4ED8;&#x304D;&#x30EC;&#x30B8;&#x30B9;&#x30C8;&#x30EA;</a>対応を行いました。本エントリではこうして UPM 対応した結果、ユーザ視点および開発者視点から、どのようにライブラリとして配布されているアセットの利用が変わるかについて、具体的な例を通じて見ていければと思います。また、UPM 対応を行う上で各ライブラリで対応した小話も後半に書こうと思います。</p> <p>なお、Package Manager のついては 1 日目の <a href="https://twitter.com/RyotaMurohoshi">@RyotaMurohoshi</a> さんのエントリでも触れられていますので併せてお読みください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2FRyotaMurohoshi%2Fitems%2F2b7853db9c42283a9670" title="Unity 2021におけるパッケージ関連の変更について - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/RyotaMurohoshi/items/2b7853db9c42283a9670">qiita.com</a></cite></p> <h2>パッケージ利用者側の視点</h2> <h3>パッケージの追加(削除)</h3> <p>スコープ付き<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>(Scoped Registries)は、Unity 公式のライブラリを配布するデフォルトの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>に加えて、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>製のパッケージを配布する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>を使用することを可能にします。例えば以下のように Project Settings > Package Manager > Scoped Registries に登録してみます。</p> <ul> <li>Name: <code>hecomi</code></li> <li>URL: <code>https://registry.npmjs.com</code></li> <li>Scope(s): <code>com.hecomi</code></li> </ul> <p>すると、Package Manager の Packages: My Registries から以下のように私が配布している(npmjs に登録している)パッケージが見えるようになります。同様に他の開発者のスコープ付き<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>も登録することでリストに様々なパッケージが並ぶようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211208/20211208004104.gif" alt="f:id:hecomi:20211208004104g:plain" width="1094" height="713" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>例として、ランタイムに REPL によるコード<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>を可能にする uREPL を インストールして入れてみましょう。インストールすると、Packages の中に入るので、この中から必要なアセットを引っ張ってきます。uREPL では Prefab(+ <code>EventSystem</code>)を置くと使えるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211208/20211208010555.gif" alt="f:id:hecomi:20211208010555g:plain" width="1094" height="713" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>必要がなくなったら Package Manager で Remove するだけです。Assets <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを汚さずに、簡単にインストール・アンインストールが出来る感じですね。</p> <h3>サンプルのインポート</h3> <p>その他にも必要なサンプルだけインポートしやすい、というメリットもあります。例えばレイマーチングしたオブジェクト用のシェーダを簡単に生成できる uRaymarching を例に見てみます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2016%2F10%2F11%2F225541" title="Unity でレイマーチングするシェーダを簡単に作成できるツールを作ってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2016/10/11/225541">tips.hecomi.com</a></cite></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211208/20211208232450.gif" alt="f:id:hecomi:20211208232450g:plain" width="800" height="521" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>インポートしたあとに Samples を見るとインポート可能なサンプルが列挙されています。例えば現在利用している<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>パイプラインが Forward(Legacy)の場合、Common と Forward をインストールすると、それ用のサンプルのみがプロジェクトに追加されます(Deferred や URP など別のパイプラインのアセットはインポートしないで済む)。追加される場所は、<em>Samples > [パッケージ名] > [パッケージバージョン] > [サンプル名]</em> となっています。</p> <h3>アップデート</h3> <p>使用しているライブラリに更新があると、ライブラリ名の右側に↑アイコンを表示することで教えてくれます。<em>Update [new version]</em> ボタンを押下すると更新され、新しいライブラリが使用されるようになります。自分で逐一個別に情報を取りに行かなくてもこうして一覧化されると便利ですね。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211219/20211219002249.png" alt="f:id:hecomi:20211219002249p:plain" width="1200" height="654" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>なお、古いバージョンを使用したい際はライブラリの項目を展開し、<em>See other versions</em> を押下して古いバージョンを表示、選択しインストールしてください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211219/20211219002454.gif" alt="f:id:hecomi:20211219002454g:plain" width="916" height="497" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>依存関係の解決</h3> <p>面倒くさい依存関係を自動で解決してくれます。例えば先程見た uRaymarching は uShaderTemplate というシェーダジェネレータライブラリに依存しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2017%2F10%2F24%2F014057" title="Unity のシェーダをテンプレートファイルから生成するエディタ拡張をつくってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2017/10/24/014057">tips.hecomi.com</a></cite></p> <p>これまでは、.unitypackage などを個別にダウンロードしてこないとなりませんでしたが、先程見たように欲しいパッケージを入れるだけで依存ライブラリは自動的にインストールされます。例えば先ほど uRaymarching のみをインストールした場合でも、次のように uShaderTemplate もインストールされている状態になります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211208/20211208234258.png" alt="f:id:hecomi:20211208234258p:plain" width="1200" height="784" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>また、自分でインストールしたパッケージを削除すると自動的に依存パッケージも削除されるようになっています。ちなみにこの依存関係は、<em>Show Dependencies</em> をチェックすると、依存しているもの(Is using)と依存されているもの(Used by)が見れるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211219/20211219002928.gif" alt="f:id:hecomi:20211219002928g:plain" width="916" height="497" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>グローバルキャッシュ</h3> <p>こうしてインストールしたパッケージは特定の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ以下に格納され、同じバージョンのものは異なるプロジェクト間であっても共有されます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fupm-cache.html" title="グローバルキャッシュ - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/upm-cache.html">docs.unity3d.com</a></cite></p> <p>場所は以下になります。</p> <table> <thead> <tr> <th> OS </th> <th> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ </th> </tr> </thead> <tbody> <tr> <td> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> (システムユーザーアカウント以外) </td> <td> %LOCALAPPDATA%\Unity\cache </td> </tr> <tr> <td> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> (システムユーザーアカウント) </td> <td> %ALLUSERSPROFILE%\Unity\cache </td> </tr> <tr> <td> <a class="keyword" href="http://d.hatena.ne.jp/keyword/macOS">macOS</a> </td> <td> $HOME/Library/Unity/cache </td> </tr> <tr> <td> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Linux">Linux</a> </td> <td> $HOME/.config/unity3d/cache </td> </tr> </tbody> </table> <p>例えば <a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a> ではこんな感じです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211209/20211209002137.png" alt="f:id:hecomi:20211209002137p:plain" width="1200" height="249" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>デメリット</h3> <p>パッケージを導入したあとに、そのパッケージを直接色々いじるようなケースには向いていません。一応、パッケージの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>はいじれますが、先に見たようにグローバルキャッシュを直接いじることになるので、他のプロジェクトでも変更が反映されてしまいます。このようなケースでは従来どおり直接 Assets 以下に追加する方式が良いでしょう。</p> <h2>開発者側の視点</h2> <p>開発者側にも色々と利点があります。ユーザが上記の様に良い体験を得られる(簡単に使ってもらえる)というのがもちろん一番大きいところですが、他にもあるのでみてみましょう。</p> <h3>依存ライブラリを指定できる</h3> <p>先程の uRaymarching の例のように、依存ライブラリを指定することでプロジェクトをシンプルに保つことが出来ます。以前は開発時に依存ライブラリを自分の Assets に含まなければならなかったのですが、依存ライブラリはパッケージとして切り分けることによって開発プロジェクトをクリーンに保つことが出来ます。</p> <p>また、自分のプロジェクト間の依存関係だけではなく、Unity 公式のライブラリへの依存関係も解決できます。<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>を行う uLipSync というライブラリでは <a href="https://docs.unity3d.com/Manual/com.unity.burst.html">Burst</a> と <a href="https://docs.unity3d.com/Manual/com.unity.jobs.html">Job</a> を使うことでバックグラウンドスレッドで高速に音声信号処理を行う、ということをしています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2021%2F02%2F27%2F144722" title="Unity でリアルタイムにリップシンクを行うプラグインを更に作り直してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2021/02/27/144722">tips.hecomi.com</a></cite></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211216/20211216223854.gif" alt="f:id:hecomi:20211216223854g:plain" width="640" height="480" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この Burst と <a class="keyword" href="http://d.hatena.ne.jp/keyword/SIMD">SIMD</a> の効いた計算を行うことの出来る数学関数を提供する <a href="https://docs.unity3d.com/Manual/com.unity.mathematics.html">com.unity.mathematics</a> に依存しているので、これまではユーザに README にこんな画像を貼ることでこれらをインストールするよう促す必要がありました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211212/20211212145648.png" alt="f:id:hecomi:20211212145648p:plain" width="1200" height="338" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>しかしながら <code>package.json</code> に<a href="https://github.com/hecomi/uLipSync/blob/upm%401.1.4/package.json#L14-L17">&#x6B21;&#x306E;&#x3088;&#x3046;&#x306B;&#x4F9D;&#x5B58;&#x95A2;&#x4FC2;&#x3092;&#x8A18;&#x8FF0;</a>することでユーザ側でこれらを個別にインストールする必要はなくなります。また、依存ライブラリのバージョンが上がったとしてもそれらの管理責任は Unity 側にお願いできるようになります。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> ... &quot;<span class="synStatement">dependencies</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">com.unity.burst</span>&quot;: &quot;<span class="synConstant">1.4.11</span>&quot;, &quot;<span class="synStatement">com.unity.mathematics</span>&quot;: &quot;<span class="synConstant">1.2.5</span>&quot; <span class="synSpecial">}</span>, ... <span class="synSpecial">}</span> </pre> <p>スコープ付き<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>をまたいでの依存関係解決についてはまだ試したことがないので分かり次第追記します。</p> <h3>サンプルで別のライブラリを分離できる</h3> <p>動作として依存していなくてもサンプルとして他のライブラリに依存したいときがあります。例えば <a class="keyword" href="http://d.hatena.ne.jp/keyword/NVIDIA">NVIDIA</a> のハードウェアエンコーダ / <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%B3%A1%BC%A5%C0">デコーダ</a>である NVENC/NVDEC の実装である <a href="https://github.com/NVIDIA/NvPipe/tree/deprecated">NvPipe</a> <a href="#f-606f5c94" name="fn-606f5c94" title="現在は Deprecated になってしまいました... https://github.com/NVIDIA/NvPipe">*1</a> を使って <code>Texture2D</code> を H264 へと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%F3%A5%B3%A1%BC%A5%C9">エンコード</a> / デコードしてくれるライブラリの uNvPipe を例に取ってみてみます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2019%2F07%2F30%2F011112" title="NvPipe で Unity のテクスチャを H.264 にハードウェアエンコード / デコードしてみる - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2019/07/30/011112">tips.hecomi.com</a></cite></p> <p>ライブラリの実装としては<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%F3%A5%B3%A1%BC%A5%C9">エンコード</a>とデコードの機能を提供するところができれば良いですが、サンプルとしてはネットワークを通じて<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%F3%A5%B3%A1%BC%A5%C9">エンコード</a>されたデータを送ってデコードするところを含めたいなと考えました。ネットワーク部分はシンプルに <a class="keyword" href="http://d.hatena.ne.jp/keyword/UDP">UDP</a> で送ろうと思い、それを簡単に出来る <a href="https://tips.hecomi.com/entry/2021/11/29/234527">uOSC</a> をプロジェクトに含めてサンプルを書いていました。ただこの uOSC は本プロジェクトの本質ではないのでプロジェクトに含めるのはちょっと汚い感じもします。</p> <p>しかしながらパッケージのサンプル提供の機能を使うと、uOSC が必要でないサンプル、必要なサンプルと切り分けることができ、この結果ユーザは自分が欲しい物だけインストールすることができます。なのでネットワークのテストをしたい人だけ uOSC をダウンロードしたあとにサンプルを見てね、とお願いする形式にすることが出来るわけですね。依存パッケージを特定のサンプルにのみ含める、みたいな形式でも良いと思います。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211212/20211212205930.png" alt="f:id:hecomi:20211212205930p:plain" width="1200" height="731" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ここでは <code>02. uOSC + Network</code> というサンプルをインポートすると uOSC なしだとエラーが出ますが入れれば動く、という感じになってます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211216/20211216224235.gif" alt="f:id:hecomi:20211216224235g:plain" width="1200" height="597" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>とは言えサンプル名で追加ライブラリが必要感を出すしか出来ず...、サンプルインポート時に依存ライブラリをインストールしたりしてほしいですが...、流石にやりすぎな気もするので、せめて <code>package.json</code> に書いた description の表示くらいはしてもらって、ユーザにインストールしてねメッセージは表示したいところです(Unity さんお願いします...)。</p> <h3>デプロイは意外と簡単</h3> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> で管理している人は <a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions を使うと簡単にデプロイ出来ます。UPM 対応した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構造を作るところは<a href="https://tips.hecomi.com/entry/2021/10/29/001304">&#x524D;&#x56DE;&#x306E;&#x8A18;&#x4E8B;</a>に書きましたが、そこから npmjs にアップロードするところは、次のような <a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions でのステップを追加し、必要な Secret を追加すれば簡単にデプロイ出来ます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create UPM branches and run NPM publish <span class="synPreProc">...</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">update</span><span class="synSpecial">:</span> ... <span class="synIdentifier">steps</span><span class="synSpecial">:</span> ... <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> NPM publish <span class="synIdentifier">run</span><span class="synSpecial">:</span> npm publish --access public <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">NODE_AUTH_TOKEN</span><span class="synSpecial">:</span> ${{secrets.NPM_TOKEN}} </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211212/20211212151429.png" alt="f:id:hecomi:20211212151429p:plain" width="1200" height="578" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>私は tag の push にフックして UPM ブランチ作成と npmjs へのデプロイを次のような <a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions で行っています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuOSC%2Fblob%2Fv2.0.3%2F.github%2Fworkflows%2Fmain.yml%23L33-L36" title="uOSC/main.yml at v2.0.3 · hecomi/uOSC" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uOSC/blob/v2.0.3/.github/workflows/main.yml#L33-L36">github.com</a></cite></p> <h3>かっこいい</h3> <p>自分のライブラリがズラッと並ぶのを見ると満足感があります!(これが一番の理由かも)</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211212/20211212150303.png" alt="f:id:hecomi:20211212150303p:plain" width="1200" height="784" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>利用状況が見れる</h3> <p>npmjs にアップロードすると週間ダウンロード数が見れます。たくさんダウンロードされているとちょっとうれしいですね。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211212/20211212151814.png" alt="f:id:hecomi:20211212151814p:plain" width="1200" height="681" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>対応パッケージでの具体的事例</h2> <p>対応したライブラリの中で具体例や、問題があったものに関しての事例を紹介します。</p> <h3>HLSLToolsForVisualStudioConfigGenerator</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2020%2F12%2F20%2F000908" title="Unity 向けに HLSL Tools for Visual Studio を便利にするアセットを作ってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2020/12/20/000908">tips.hecomi.com</a></cite></p> <p>これは HLSL の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%BF%A5%C3%A5%AF%A5%B9">シンタックス</a>ハイライトや補間をしてくれる <a href="https://marketplace.visualstudio.com/items?itemName=TimGJones.HLSLToolsforVisualStudio">HLSL Tools for Visual Studio</a> 向けに Unity プロジェクト用の設定ファイルを生成してくれるツールです。こういうプロジェクト自体に影響を与えないツール、みたいなものは特に Assets 以下に入れないで単に Package として入れるのが良いですね。</p> <h3>uTouchInjection での XR 依存</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2018%2F07%2F21%2F171632" title="Unity で Windows のマルチタッチ操作をエミュレートできる uTouchInjection を作った - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2018/07/21/171632">tips.hecomi.com</a></cite></p> <p>uTouchInjection は <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> 向けにマルチタッチのエミュレーションを行うためのライブラリです。メインの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>としては <a class="keyword" href="http://d.hatena.ne.jp/keyword/VR">VR</a> のデスクトップアプリを作ることで、以前は <a class="keyword" href="http://d.hatena.ne.jp/keyword/VR">VR</a> 向けのデモとして uDesktopDuplication と Oculus Integration の組み合わせを試すために、この 2 つは自分でダウンロードしてきてね、というスタンスで公開していました。</p> <p>UPM 対応にあたっては、Unity 上で uDesktopDuplication と XR Interaction Toolkit を Package Manager から自分でインストールしてね、という形式にしました。これで Unity 上でセットアップが完結するようになり、Oculus への依存がなくなりました!ただし、XR Interaction Toolkit を使うためにちょっと手順が多くて大変ですが...。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuTouchInjection%2Ftree%2Fupm%25401.0.2%2FSamples~%2F02.%2520uDesktopDuplication%2520%252B%2520VR" title="uTouchInjection/Samples~/02. uDesktopDuplication + VR at upm@1.0.2 · hecomi/uTouchInjection" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uTouchInjection/tree/upm%401.0.2/Samples~/02.%20uDesktopDuplication%20%2B%20VR">github.com</a></cite></p> <h3>uRaymarching のサンプルでのインクルードパス</h3> <p>これまでは <em>Assets/uRaymarching/...</em> みたいなパスを期待してシェーダーのインクルードを行ったサンプル(Assets からの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C0%E4%C2%D0%A5%D1%A5%B9">絶対パス</a>)となっており、UPM 対応するとこれが大きく変わってしまいます。ただ、uRaymarching ではシェーダ生成時にパスを解決する仕組みを入れているので、シェーダーを再生成すれば問題ありません。ただこれをユーザーにやらせるのは一旦エラーも吐いてしまいますし微妙なので、サンプル読み込み時に自動で <a href="https://docs.unity3d.com/ja/current/ScriptReference/AssetPostprocessor.html">AssetPosptocessor</a> を使って再生成が走るようにしました。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">class</span> GeneratorAssetImportProcessor : AssetPostprocessor { <span class="synType">const</span> <span class="synType">string</span> ext = <span class="synConstant">&quot;.asset&quot;</span>; <span class="synType">const</span> <span class="synType">string</span> dir = <span class="synConstant">&quot;uRaymarching&quot;</span>; <span class="synType">static</span> <span class="synType">void</span> OnPostprocessAllAssets( <span class="synType">string</span>[] importedAssets, <span class="synType">string</span>[] deletedAssets, <span class="synType">string</span>[] movedAssets, <span class="synType">string</span>[] movedFromAssetPaths) { <span class="synStatement">foreach</span> (<span class="synType">string</span> str <span class="synStatement">in</span> importedAssets) { <span class="synStatement">if</span> (!str.EndsWith(ext) || !str.Contains(dir)) <span class="synStatement">continue</span>; var generator = AssetDatabase.LoadAssetAtPath&lt;uShaderTemplate.Generator&gt;(str); <span class="synStatement">if</span> (!generator) <span class="synStatement">continue</span>; var editor = Editor.CreateEditor(generator) <span class="synStatement">as</span> uShaderTemplate.GeneratorEditor; editor.Reconvert(); <span class="synComment">// 新しく読み込まれたジェネレータでシェーダの再生成を行う</span> } } } </pre> <p>この<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>をサンプルの <em>Common</em> に含め、Common を始めにインポートしてもらえていれば以降のサンプルは自動で変換が走るようになりうまく動くようになります。ただこれもまだ欠点があり...、Common → 別のサンプルと Package Manager で素早くポチポチと Import すると Common 内にある上記<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>が終わる前に別のサンプルもインポートしてしまって再変換がされない...、というものです。。ここは良い回避策が思いつかなかったので、しばらくは質問されたら答える運用でカバーしようと思っています。。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211216/20211216222941.png" alt="f:id:hecomi:20211216222941p:plain" width="1200" height="732" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>サンプル間の依存解決</h3> <p>上記のような問題を解決するために、サンプル間の依存解決が出来たら良いのになぁ...、などと思っています。上記のように、<em>Common</em> はいずれかのサンプルがインポートされたら一緒にインポートしてほしい<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>や、まとめて 1 つのサンプルとするにはでかすぎるけれどサンプルとしては分けたい依存関係のあるものなど、こういったケースにどう対応すれば良いか悩ましい時があります。</p> <p>悩みつつサンプルも多くなってしまって面倒になり、結局 <em>Samples</em> と名前をつけた 1 つのサンプルにしてしまったケースもあります。もしかすると小分けなど考えずにこれが大体のケースの正解なのかもしれない...と思ったりもしてきています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211216/20211216223523.png" alt="f:id:hecomi:20211216223523p:plain" width="1200" height="734" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>uDllExporter 断念</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2018%2F04%2F16%2F004749" title="Unity 上で DLL(マネージドプラグイン)をビルドするエディタ拡張を作ってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2018/04/16/004749">tips.hecomi.com</a></cite></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a> の Managed な DLL のビルドを簡単に行うツールを以前作ったのですが、2019.2 から Unity の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構造が変わってしまいそのままだと動かずサポートも諦めました...。<code>package.json</code> には<a href="https://docs.unity3d.com/ja/Manual/upm-manifestPkg.html">&#x52D5;&#x4F5C;&#x3059;&#x308B; Unity &#x306E;&#x30D0;&#x30FC;&#x30B8;&#x30E7;&#x30F3;&#x3092;&#x8A18;&#x8FF0;&#x3067;&#x304D;&#x308B;</a>のですが、特定の Unity バージョンまでは動くけど以降は動きませんよ、みたいな指定は今の所出来ません。泣く泣くサポートできない場合にもこういう指定ができると良いなぁと思ったりしました。</p> <h3>組み合わせのサンプルが作りやすい</h3> <p>どれかの特定のライブラリのサンプル、というわけではなく複数のライブラリにまたがった例を作るときにも良かったです。例えば以前書いたこのデスクトップキャプチャした <code>Texture2D</code> を H264 に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%F3%A5%B3%A1%BC%A5%C9">エンコード</a>し、パケット分割して <a class="keyword" href="http://d.hatena.ne.jp/keyword/UDP">UDP</a> で送信、リモート側でそれで結合、デコードして <code>Texture2D</code> として表示、というサンプルを見てみます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2019%2F09%2F27%2F235955" title="uDesktopDuplication でキャプチャしたデスクトップ画像をリモートに転送してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2019/09/27/235955">tips.hecomi.com</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FUnityRemoteDesktopDuplication" title="GitHub - hecomi/UnityRemoteDesktopDuplication: An Unity example to send a desktop image to a remote PC using Desktop Duplication API and NVENC/NVDEC." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/UnityRemoteDesktopDuplication">github.com</a></cite> <img src="https://raw.githubusercontent.com/wiki/hecomi/UnityRemoteDesktopDuplication/UnityRemoteDesktopDuplication.gif" /></p> <p>以前は、ゴチャッと <code>Assets</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ以下に複数の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>が配置されていました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211216/20211216225215.png" alt="f:id:hecomi:20211216225215p:plain" width="1200" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>それがこれらの依存関係を UPM に追いやることで、<code>Assets</code> 以下にはこのプロジェクト固有の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>のみが置かれるようになりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211216/20211216225432.png" alt="f:id:hecomi:20211216225432p:plain" width="1200" height="175" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>プロジェクト的にとてもクリーンですね。ただデメリットも有り、以前の方式のほうが各ライブラリ含めて色々改変しながら結合動作を試したいときに便利で、各ライブラリの完成度が結構高くないと、あっちのパッケージを更新して、こっちのプロジェクトで Package Manager でアップデートして...みたいな行ったり来たり状態になり効率が落ちてしまいました。まぁこれは Unity に限った話ではないと思いますので、どんな場合でも用法用量を良く守って適材適所が大事ですね。。</p> <h2>おわりに</h2> <p>改めて 2020.3 LTS をベースに十数個のライブラリの UPM + スコープ付き<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>対応を行ったのですが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%C5%AA">機械的</a>に対応出来ないものもあり、結構時間がかかってしまいました。ただ、対応するとより多くの人が便利に使えるだろうと思いますし、今後の自分の開発も効率化出来ることが分かりました。</p> <p>ちょっとこれまで制作物の宣伝記事みたいな感じになり長くなってしまいましたが、以上で終わりになります。ここまで読んでいただきありがとうございました。</p> <p>明日 20 日目は <a href="https://twitter.com/coposuke/media">@coposuke</a> さんによる「Jump Flooding Algorithmについて」になります!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcoposuke.hateblo.jp%2Fentry%2F2021%2F12%2F21%2F011119" title="Jump Flooding Algorithm - コポうぇぶろぐ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://coposuke.hateblo.jp/entry/2021/12/21/011119">coposuke.hateblo.jp</a></cite></p> <div class="footnote"> <p class="footnote"><a href="#fn-606f5c94" name="f-606f5c94" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">現在は Deprecated になってしまいました... <a href="https://github.com/NVIDIA/NvPipe">https://github.com/NVIDIA/NvPipe</a></span></p> </div> hecomi uOSC v2 をリリースしました hatenablog://entry/13574176438037595750 2021-11-29T23:45:27+09:00 2021-12-11T18:41:32+09:00 はじめに uOSC は、Unity 向けの OSC 実装です。数ある Unity 向け OSC 実装の中でもシンプルな使い方が出来るように目指して作ったものでした。 tips.hecomi.com github.com EVMC4U を始めとし、思っていたよりも多くのプロジェクトにて使っていただいており嬉しい限りです。しかしながら長らく動的なアドレスやポート変更といった単純な機能も対応しないままになっていました。。そこで前回のエントリにて UPM 対応のためのメンテをして久しぶりに触ったのもあり、色々足りなかった機能の追加を行うことにしました。本エントリでは新しくなった uOSC v2 の使い… <h2>はじめに</h2> <p><strong>uOSC</strong> は、<strong>Unity </strong>向けの <strong>OSC </strong>実装です。数ある Unity 向け OSC 実装の中でもシンプルな使い方が出来るように目指して作ったものでした。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2017%2F08%2F20%2F193823" title="Unity 向けの OSC 実装を作ってみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2017/08/20/193823">tips.hecomi.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuOSC" title="GitHub - hecomi/uOSC: OSC server / client implementation for Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uOSC">github.com</a></cite></p> <p><a href="https://github.com/gpsnmeajp/EasyVirtualMotionCaptureForUnity">EVMC4U</a> を始めとし、思っていたよりも多くのプロジェクトにて使っていただいており嬉しい限りです。しかしながら長らく動的なアドレスやポート変更といった単純な機能も対応しないままになっていました。。そこで<a href="https://tips.hecomi.com/entry/2021/10/29/001304">前回のエントリ</a>にて UPM 対応のためのメンテをして久しぶりに触ったのもあり、色々足りなかった機能の追加を行うことにしました。本エントリでは新しくなった uOSC v2 の使い方について紹介していきます。</p> <h2>アップデート内容</h2> <p>詳しくは各章で書きますが、v1 からは以下の内容が更新されています。</p> <ul> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B8%E5%CA%FD%B8%DF%B4%B9">後方互換</a>性はキープ(v1 用に書いたコードはそのまま動きます)</li> <li>動的なアドレスやポートの変更に対応</li> <li>手動でサーバの開始・停止に対応(これまでは enabled の ON/OFF)</li> <li>サーバやクライアントの開始・停止イベントの追加</li> <li>メッセージを処理するリスナのインスペクタからの登録</li> <li>カスタムエディタの追加(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>用に届いたメッセージを簡単にエディタ上で確認など)</li> <li>データ送信間隔やキューのサイズ変更などの変数を追加</li> </ul> <h2>インストール方法</h2> <p>執筆時点の最新バージョンは v2.0.3 になります。インストール方法は 3 種類ありますが、2 つめの Scoped Registries の登録がおすすめです。</p> <h3>.unitypackage のダウンロードと展開</h3> <p>従来どおり .unitypackage ファイルをダウンロードしプロジェクトに取り込む方法です。以下のリリースページから最新版をダウンロードしてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuOSC%2Freleases" title="Releases · hecomi/uOSC" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uOSC/releases">github.com</a></cite></p> <h3>Package Manager で Scoped Registry の登録(推奨)</h3> <p>Package Manager から Advanced Project Settings に移動します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129015506.png" alt="f:id:hecomi:20211129015506p:plain" width="1200" height="585" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ここで Scoped Registries に次のように<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>を追加してください。</p> <ul> <li>Name: <code>hecomi</code></li> <li>URL: <code>https://registry.npmjs.com</code></li> <li>Scope: <code>com.hecomi</code></li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129015644.png" alt="f:id:hecomi:20211129015644p:plain" width="1200" height="693" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>すると、Package Manager の My Registries で uOSC が見えるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129015934.png" alt="f:id:hecomi:20211129015934p:plain" width="1200" height="584" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Install を押下してプロジェクトに追加すると、Packages の中にプロジェクトが追加されます。この方法の利点は、以降 uOSC 側に更新が入った際にそれを検知し、Package Manager から簡単にアップデートが出来るようになることです。初期設定の手間は少しかかりますがおすすめです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129020954.png" alt="f:id:hecomi:20211129020954p:plain" width="1200" height="654" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>各サンプルは Samples からプロジェクトに取り込むことができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129022039.png" alt="f:id:hecomi:20211129022039p:plain" width="1200" height="575" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>Package Manager で Git URL の登録</h3> <p>Scoped Registries の登録は面倒...、でも .unitypackage で直接プロジェクトに追加も散らかるので嫌だ...、という方は Package Manager で「Add package from git URL...」を選択し、以下の URL を入力すると簡単にパッケージとしてプロジェクトに追加できます。</p> <ul> <li><a href="https://github.com/hecomi/uOSC.git#upm">https://github.com/hecomi/uOSC.git#upm</a></li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129014953.png" alt="f:id:hecomi:20211129014953p:plain" width="1200" height="585" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ただこの方法ではバージョン更新の検知はできません。</p> <h3>External Tools の設定</h3> <p>Package Manager で追加した場合は、External Tools でこれらのパッケージをプロジェクトに含む設定をしないと補完が効きませんのでご注意ください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129015412.png" alt="f:id:hecomi:20211129015412p:plain" width="1200" height="717" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>基本的な使い方</h2> <h3>データの受信</h3> <p>サンプルシーンは <code>Basic</code> サンプルの <code>Server</code> シーンになります。データの受信には <code>uOscServer</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を利用します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129025025.png" alt="f:id:hecomi:20211129025025p:plain" width="928" height="1045" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>待ち受けしたいポートを Port に入力し実行すればメッセージ待受用のスレッドが動きます。サーバに届いたメッセージを受け取るには <code>uOscServer.onDataReceived</code> にリスナの登録を行います。次のような<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を作成します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synType">public</span> <span class="synType">class</span> ServerTest : MonoBehaviour { <span class="synType">void</span> Start() { var server = GetComponent&lt;uOSC.uOscServer&gt;(); server.onDataReceived.AddListener(OnDataReceived); } <span class="synType">public</span> <span class="synType">void</span> OnDataReceived(uOSC.Message message) { <span class="synComment">// address (e.g. /uOSC/hoge)</span> var msg = message.address + <span class="synConstant">&quot;: &quot;</span>; <span class="synComment">// arguments (object array)</span> <span class="synStatement">foreach</span> (var <span class="synStatement">value</span> <span class="synStatement">in</span> message.values) { <span class="synStatement">if</span> (<span class="synStatement">value</span> <span class="synStatement">is</span> <span class="synType">int</span>) msg += (<span class="synType">int</span>)<span class="synStatement">value</span>; <span class="synStatement">else</span> <span class="synStatement">if</span> (<span class="synStatement">value</span> <span class="synStatement">is</span> <span class="synType">float</span>) msg += (<span class="synType">float</span>)<span class="synStatement">value</span>; <span class="synStatement">else</span> <span class="synStatement">if</span> (<span class="synStatement">value</span> <span class="synStatement">is</span> <span class="synType">string</span>) msg += (<span class="synType">string</span>)<span class="synStatement">value</span>; <span class="synStatement">else</span> <span class="synStatement">if</span> (<span class="synStatement">value</span> <span class="synStatement">is</span> <span class="synType">byte</span>[]) msg += <span class="synConstant">&quot;byte[&quot;</span> + ((<span class="synType">byte</span>[])<span class="synStatement">value</span>).Length + <span class="synConstant">&quot;]&quot;</span>; } Debug.Log(msg); } } </pre> <p>これでメッセージが届くたびにログが出力されます。なお、<code>Start()</code> でやっているように自分で <code>GetComponent</code> してこなくても、インスペクタからも登録できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129025108.png" alt="f:id:hecomi:20211129025108p:plain" width="929" height="535" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>届いたメッセージは最大 100 件まで、Status > Messages の中から確認することができます。以下は <a href="https://hexler.net/touchosc">TouchOSC</a> からメッセージを送ってみた様子です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129024836.gif" alt="f:id:hecomi:20211129024836g:plain" width="822" height="422" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>データの送信</h3> <p>サンプルシーンは <code>Basic</code> サンプルの <code>Client</code> シーンになります。データの受信には <code>uOscClient</code> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を利用します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211129/20211129223737.png" alt="f:id:hecomi:20211129223737p:plain" width="909" height="655" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>データを送信したいアドレス(<a class="keyword" href="http://d.hatena.ne.jp/keyword/IPv4">IPv4</a> / <a class="keyword" href="http://d.hatena.ne.jp/keyword/IPv6">IPv6</a>)とポートを指定するとメッセージ送信用のスレッドが動きます。その上で次のようにメッセージを送信します。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synType">public</span> <span class="synType">class</span> ClientTest : MonoBehaviour { <span class="synType">void</span> Update() { var client = GetComponent&lt;uOSC.uOscClient&gt;(); client.Send(<span class="synConstant">&quot;/uOSC/test&quot;</span>, <span class="synConstant">10</span>, <span class="synConstant">&quot;hoge&quot;</span>, <span class="synConstant">1.234f</span>, <span class="synStatement">new</span> <span class="synType">byte</span>[] { <span class="synConstant">1</span>, <span class="synConstant">2</span>, <span class="synConstant">3</span> }); } } </pre> <p>送るメッセージは可変個で、<code>int</code>、<code>float</code>、<code>string</code>、<code>byte[]</code> のいずれかに対応しています。</p> <p>基本的には以上になります。</p> <h2>Tips</h2> <p>以下、より詳細な Tips になります。</p> <h3>開始・終了</h3> <p><code>uOscServer.autoStart</code> が ON の場合、サーバは開始時に自動的にスタートします。もし手動でスタートさせたい場合はこれを OFF にし、手動で <code>uOscServer.StartServer()</code> を呼んでください。止めたい場合は <code>uOscServer.StopServer()</code> です。インスペクタから開始・停止したい場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C1%A5%A7%A5%C3%A5%AF%A5%DC%A5%C3%A5%AF%A5%B9">チェックボックス</a>を ON / OFF してください。サーバの開始・停止は <code>uOscServer.onServerStarted</code> / <code>uOscServer.onServerStopped</code> にて検知可能です。ポートがかぶっていて開始ができない場合もあるので、その状況は <code>uOscServer.isRunning</code> をチェックして判断してください。</p> <p>クライアントの場合はあまり<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>がないかもしれませんが、<code>uOscClient.StartClient()</code> / <code>uOscClient.StopClient()</code> で開始・停止が可能で、開始・停止イベントは <code>uOscClient.onClientStarted</code> / <code>uOscClient.onClientStopped</code> から取得可能です。</p> <h3>動的なアドレスやポートの変更</h3> <p><code>uOscServer</code> / <code>uOscClient</code> の <code>port</code> や <code>address</code> を変更した場合は、自動的に再接続を行います(タイミングは変更後の <code>Update()</code> タイミングです)。インスペクタ上から変更した場合も同じです。</p> <h3>OSC Bundle</h3> <p>OSC Bundle は複数のメッセージを一つのパケットにまとめることのできる仕組みです。uOSC ではサーバ側では内部で Bundle を展開して単一のアドレス、データ、タイムスタンプのペアとしてリスナに登録したコールバックを呼ぶようにしています。</p> <p>送信側では以下のようにして Bundle を作成・送信することができます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> uOSC; <span class="synType">public</span> <span class="synType">class</span> ClientBundleTest : MonoBehaviour { <span class="synType">void</span> Update() { var client = GetComponent&lt;uOscClient&gt;(); <span class="synComment">// Bundle (root)</span> <span class="synComment">// - Bundle 1 -&gt; Now</span> <span class="synComment">// - Message 1-1</span> <span class="synComment">// - Message 1-2</span> <span class="synComment">// - Message 1-3</span> <span class="synComment">// - Bundle 2 -&gt; 10 sec after</span> <span class="synComment">// - Message 2-1</span> <span class="synComment">// - Message 2-2</span> <span class="synComment">// - Message 2-3</span> <span class="synComment">// - Message 3 -&gt; Immediate</span> var bundle1 = <span class="synStatement">new</span> Bundle(Timestamp.Now); bundle1.Add(<span class="synStatement">new</span> Message(<span class="synConstant">&quot;/uOSC/root/bundle1/message1&quot;</span>, <span class="synConstant">123</span>, <span class="synConstant">&quot;hoge&quot;</span>, <span class="synStatement">new</span> <span class="synType">byte</span>[] { <span class="synConstant">1</span>, <span class="synConstant">2</span>, <span class="synConstant">3</span>, <span class="synConstant">4</span> })); bundle1.Add(<span class="synStatement">new</span> Message(<span class="synConstant">&quot;/uOSC/root/bundle1/message2&quot;</span>, <span class="synConstant">1.2345f</span>)); bundle1.Add(<span class="synStatement">new</span> Message(<span class="synConstant">&quot;/uOSC/root/bundle1/message3&quot;</span>, <span class="synConstant">&quot;abcdefghijklmn&quot;</span>)); var date2 = System.DateTime.UtcNow.AddSeconds(<span class="synConstant">10</span>); var timestamp2 = Timestamp.CreateFromDateTime(date2); var bundle2 = <span class="synStatement">new</span> Bundle(timestamp2); bundle2.Add(<span class="synStatement">new</span> Message(<span class="synConstant">&quot;/uOSC/root/bundle2/message1&quot;</span>, <span class="synConstant">234</span>, <span class="synConstant">&quot;fuga&quot;</span>, <span class="synStatement">new</span> <span class="synType">byte</span>[] { <span class="synConstant">2</span>, <span class="synConstant">3</span>, <span class="synConstant">4</span> })); bundle2.Add(<span class="synStatement">new</span> Message(<span class="synConstant">&quot;/uOSC/root/bundle2/message2&quot;</span>, <span class="synConstant">2.3456f</span>)); bundle2.Add(<span class="synStatement">new</span> Message(<span class="synConstant">&quot;/uOSC/root/bundle2/message3&quot;</span>, <span class="synConstant">&quot;opqrstuvwxyz&quot;</span>)); var root = <span class="synStatement">new</span> Bundle(Timestamp.Immediate); root.Add(bundle1); root.Add(bundle2); root.Add(<span class="synStatement">new</span> Message(<span class="synConstant">&quot;/uOSC/root/message3&quot;</span>)); client.Send(root); <span class="synComment">// uOSC サーバで受け取った場合は OnDataReceived が 7 回呼ばれる</span> } } </pre> <h3>タイムスタンプ</h3> <p>OSC Bundle は上述したようにタイムスタンプを持つことができます。サーバ側では次のようにして <code>DateTime</code> として受け取ることができます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> uOSC; <span class="synType">public</span> <span class="synType">class</span> ServerTest : MonoBehaviour { ... <span class="synType">void</span> OnDataReceived(Message message) { var dateTime = message.timestamp.ToLocalTime(); } } </pre> <h3>データ送信用の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AD%A5%E5%A1%BC%A5%B5%A5%A4">キューサイ</a>ズおよび送信間隔</h3> <p><code>uOscClient.Send()</code> は即時送信はされずに、いったんキューに登録され、バックグラウンドスレッドで取り出しながら送信を行います。バックグラウンドスレッドはおおよそ 1ms 毎に処理されています(Unity の Update 更新間隔は 16 ms)。このキューのデフォルトの最大サイズは 100 となっており、それより大きい数のメッセージを例えば for 文の中で送ろうとすると古いメッセージから消えてしまいます。より多くのデータを送りたい場合は <code>uOscClient.maxQueueSize</code> を変更してください(インスペクタからも変更可能です)。</p> <p>また、大きな <code>byte[]</code> のデータを同時に大量に送ろうとすると <a class="keyword" href="http://d.hatena.ne.jp/keyword/UDP">UDP</a> なこともあるからかローカルであろうとロストしてしまうケースが多々あります。この際は <code>uOscClient.dataTransmissionInterval</code>(ミリ秒)を設定してメッセージの送信間隔を空けるようにしてください。詳細は後述します。</p> <h3>送信可能なデータの最大サイズ</h3> <p>送信可能なデータサイズは、65,536 byte から IP ヘッダ 20 byte、<a class="keyword" href="http://d.hatena.ne.jp/keyword/UDP">UDP</a> ヘッダ 8 byte、OSC ヘッダ(可変長、アドレスの長さなどで変わる)を除いた大きさになります。これ以上大きなデータを送りたい場合は適当な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>を利用したりしながらデータを分割・並び替え・復元する必要があります。私の方で簡単なサンプルである <a href="https://github.com/hecomi/uPacketDivision">uPacketDivision</a> というものを用意してみました。これについては後述します。</p> <h3>データが受信できない?</h3> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D5%A5%A1%A5%A4%A5%A2%A5%A6%A5%A9%A1%BC%A5%EB">ファイアウォール</a>の設定を確認してください。Unity やビルドした exe で許可をし忘れていることはよくあります。</p> <h2>コード例</h2> <p><code>int</code> や <code>string</code> といったデータを送るのは簡単だと思うので、<code>byte[]</code> を送る参考例を紹介したいと思います。</p> <h3>テクスチャの送信①</h3> <p>まずはテクスチャを <a class="keyword" href="http://d.hatena.ne.jp/keyword/PNG">PNG</a> に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%F3%A5%B3%A1%BC%A5%C9">エンコード</a>して送信する方法です。<a class="keyword" href="http://d.hatena.ne.jp/keyword/PNG">PNG</a> に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%F3%A5%B3%A1%BC%A5%C9">エンコード</a>することでそのままでは 65,000 byte を超えてしまうようなデータも圧縮されてパケットに収まるようになります(大きすぎると超えますが)。</p> <h5>送信側</h5> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> uOSC; <span class="synType">public</span> <span class="synType">class</span> ClientBlobTest : MonoBehaviour { [SerializeField] Texture2D texture; <span class="synType">byte</span>[] byteTexture_; <span class="synType">void</span> Start() { byteTexture_ = texture.EncodeToPNG(); } <span class="synType">void</span> Update() { var client = GetComponent&lt;uOscClient&gt;(); client.Send(<span class="synConstant">&quot;/uOSC/blob&quot;</span>, byteTexture_); } } </pre> <h5>受信側</h5> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> uOSC; <span class="synType">public</span> <span class="synType">class</span> ServerBlobTest : MonoBehaviour { Texture2D texture_; <span class="synType">void</span> Start() { var server = GetComponent&lt;uOscServer&gt;(); server.onDataReceived.AddListener(OnDataReceived); texture_ = <span class="synStatement">new</span> Texture2D(<span class="synConstant">256</span>, <span class="synConstant">256</span>, TextureFormat.ARGB32, <span class="synConstant">true</span>); var renderer = GetComponent&lt;Renderer&gt;(); renderer.material.mainTexture = texture_; } <span class="synType">void</span> OnDataReceived(Message message) { <span class="synStatement">if</span> (message.address == <span class="synConstant">&quot;/uOSC/blob&quot;</span>) { var byteTexture = (<span class="synType">byte</span>[])message.values[<span class="synConstant">0</span>]; texture_.LoadImage(byteTexture); } } } </pre> <h3>テクスチャの送信②</h3> <p>より大きなデータを送りたい場合はパケット分割をします。簡単な <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> 向けの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>として uPacketDivision というものを作ってみました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuPacketDivision" title="GitHub - hecomi/uPacketDivision: A native plugin for Unity that provides simple packet division and restoration." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uPacketDivision">github.com</a></cite></p> <p>これは以下の記事の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を整理して分割したものです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2019%2F09%2F27%2F235955" title="uDesktopDuplication でキャプチャしたデスクトップ画像をリモートに転送してみた - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2019/09/27/235955">tips.hecomi.com</a></cite></p> <h5>送信側</h5> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> System.Runtime.InteropServices; <span class="synStatement">using</span> uPacketDivision; <span class="synStatement">using</span> uOSC; <span class="synType">public</span> <span class="synType">class</span> Sender : MonoBehaviour { [SerializeField] <span class="synType">uint</span> packetSize = <span class="synConstant">65000</span>; [SerializeField] uOscClient client; [SerializeField] <span class="synType">int</span> sendFrameInterval = <span class="synConstant">10</span>; Divider _divider = <span class="synStatement">new</span> Divider(); <span class="synType">int</span> _width = <span class="synConstant">0</span>; <span class="synType">int</span> _height = <span class="synConstant">0</span>; <span class="synType">int</span> count = <span class="synConstant">0</span>; <span class="synType">void</span> Update() { <span class="synStatement">if</span> (count-- &gt; <span class="synConstant">0</span>) <span class="synStatement">return</span>; Divide(); Send(); count = sendFrameInterval; } <span class="synType">void</span> Divide() { var renderer = GetComponent&lt;Renderer&gt;(); <span class="synStatement">if</span> (!renderer) <span class="synStatement">return</span>; var texture = renderer.sharedMaterial.mainTexture <span class="synStatement">as</span> Texture2D; <span class="synStatement">if</span> (!texture) <span class="synStatement">return</span>; _width = texture.width; _height = texture.height; var pixels = texture.GetPixels32(); _divider.maxPacketSize = packetSize; _divider.Divide(pixels); } <span class="synType">void</span> Send() { <span class="synStatement">if</span> (!client) <span class="synStatement">return</span>; client.Send(<span class="synConstant">&quot;/Size&quot;</span>, _width, _height); <span class="synStatement">for</span> (<span class="synType">uint</span> i = <span class="synConstant">0</span>; i &lt; _divider.GetChunkCount(); ++i) { client.Send(<span class="synConstant">&quot;/Data&quot;</span>, _divider.GetChunk(i)); } } } </pre> <h5>受信側</h5> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synType">public</span> <span class="synType">class</span> Receiver : MonoBehaviour { [SerializeField] <span class="synType">uint</span> timeout = <span class="synConstant">100</span>; Assembler _assembler = <span class="synStatement">new</span> Assembler(); Texture2D _texture; <span class="synType">void</span> Update() { _assembler.timeout = timeout; } <span class="synType">public</span> <span class="synType">void</span> OnDataReceived(uOSC.Message message) { <span class="synStatement">if</span> (message.address == <span class="synConstant">&quot;/Size&quot;</span>) { var w = (<span class="synType">int</span>)message.values[<span class="synConstant">0</span>]; var h = (<span class="synType">int</span>)message.values[<span class="synConstant">1</span>]; OnSize(w, h); } <span class="synStatement">else</span> <span class="synStatement">if</span> (message.address == <span class="synConstant">&quot;/Data&quot;</span>) { var data = (<span class="synType">byte</span>[])message.values[<span class="synConstant">0</span>]; OnData(data); CheckEvent(); } } <span class="synType">void</span> OnSize(<span class="synType">int</span> w, <span class="synType">int</span> h) { <span class="synStatement">if</span> (_texture &amp;&amp; _texture.width == w &amp;&amp; _texture.height == h) <span class="synStatement">return</span>; _texture = <span class="synStatement">new</span> Texture2D(w, h); } <span class="synType">void</span> OnData(<span class="synType">byte</span>[] data) { _assembler.Add(data); } <span class="synType">void</span> CheckEvent() { <span class="synStatement">switch</span> (_assembler.GetEventType()) { <span class="synStatement">case</span> EventType.FrameCompleted: { OnDataAssembled(_assembler.GetAssembledData&lt;Color32&gt;()); <span class="synStatement">break</span>; } <span class="synStatement">case</span> EventType.PacketLoss: { var type = _assembler.GetLossType(); Debug.LogWarning(<span class="synConstant">&quot;Loss: &quot;</span> + type); <span class="synStatement">break</span>; } <span class="synStatement">default</span>: { <span class="synStatement">break</span>; } } } <span class="synType">void</span> OnDataAssembled(Color32[] pixels) { <span class="synStatement">if</span> (!_texture) <span class="synStatement">return</span>; <span class="synStatement">if</span> (pixels.Length != _texture.width * _texture.height) <span class="synStatement">return</span>; _texture.SetPixels32(pixels); _texture.Apply(); GetComponent&lt;Renderer&gt;().material.mainTexture = _texture; } } </pre> <p>なお、Tips の章でも書きましたが、<code>dataTransmissionInterval</code> は 2ms 程度に指定しておく必要があります。</p> <p>こんな感じでより大きいデータを適当に分割・復元して送ることが出来るようになります。ただ <a class="keyword" href="http://d.hatena.ne.jp/keyword/UDP">UDP</a> なのでパケットロスはしますので、使い場所は選ぶと思います。</p> <h2>おわりに</h2> <p>今回は対応しませんでしたが、v3 は <code>object</code> 利用によるボックス化によるオーバーヘッド回避の対応をしたいと思います。他にも要望などありましたら <a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a> などでお気軽に話しかけてください。</p> hecomi Unity で .unitypackage で配布していたアセットを Package Manager 対応してみた hatenablog://entry/13574176438023595777 2021-10-29T00:13:04+09:00 2021-10-29T00:13:04+09:00 はじめに 先日 Package Manager 対応のプルリクをいただいたきました。恥ずかしながら今まで Package Manger の対応およびその調査はサボってきており、あまり知識がない状態でしたので、これを機に色々と調べて配布する側としての自分なりの利用方針を決めていこうと思いました。以下、Unity Package Manager を省略して UPM と書きます(公式では一部を除き*1あまり UPM という表記は使われていないようですが、開発者の間では普通に使われている印象です)。 本エントリでは、既に色々配布しているライブラリがある上での立場から UPM についての調査を行った内容… <h2>はじめに</h2> <p>先日 <strong>Package Manager</strong> 対応のプルリクをいただいたきました。恥ずかしながら今まで Package Manger の対応およびその調査はサボってきており、あまり知識がない状態でしたので、これを機に色々と調べて配布する側としての自分なりの利用方針を決めていこうと思いました。以下、Unity Package Manager を省略して <strong>UPM </strong>と書きます(公式では一部を除き<a href="#f-8b716ef4" name="fn-8b716ef4" title="https://docs.unity3d.com/ja/current/ScriptReference/PackageManager.Requests.Request.html">*1</a>あまり UPM という表記は使われていないようですが、開発者の間では普通に使われている印象です)。</p> <p>本エントリでは、既に色々配布しているライブラリがある上での立場から UPM についての調査を行った内容をまとめます。また、その上で自分は <a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions を使ったパッケージのリリースを行う方針に決めたのですが、その道筋や具体的なコードを紹介していきます。</p> <h2>UPM の基本</h2> <p>UPM そのものについては既に世に色々と解説がありますので、本記事では基本のところ(使い方など)の解説は省きます。公式にとても分かりやすいドキュメントがありますので、そちらを参照いただくのをおすすめします。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2FPackages.html" title="Unity の Package Manager - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/Packages.html">docs.unity3d.com</a></cite></p> <p>簡単に述べると、これまで .unitypackage や直接インポートしたりしながら Assets 以下に整理して置いていた依存ライブラリが、Package Manager という仕組みを通じて、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GUI">GUI</a> を通じてライブラリを簡単に追加・削除でき、それらのバージョン管理が適切に行われ、依存関係が適切に解決され、またグローバルキャッシュを通じて異なるプロジェクト間でアセットが共有され、...といった具合に色々と便利になります。数々のアップデートを通じて、かなり使いやすいものになったようです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211017/20211017224343.png" alt="f:id:hecomi:20211017224343p:plain" width="1200" height="545" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>他の言語やプラットフォームではパッケージ管理の概念は以前から存在しています。例えば Node.js では npm(Node Package Manager)というパッケージ管理システムを通じて、ライブラリの管理を行うことが出来ます。ちなみに UPM のバックエンドでも、この npm を利用しているようです。npm はインストールや管理だけでなく<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>(<a href="https://www.npmjs.com/">npmjs</a>)も持っており、各開発者が作成したライブラリを <code>npm publish</code> してホストしてもらえるのですが、Unity でもこの npmjs にライブラリを置いても良い?みたいです。</p> <p><a href="https://note.com/fuqunaga/n/n683474b8a663">Package Manager&#x306B;&#x81EA;&#x4F5C;&#x30D1;&#x30C3;&#x30B1;&#x30FC;&#x30B8;&#x3092;&#x8FFD;&#x52A0;&#x3059;&#x308B;2019&#x5E74;&#x7248;&#xFF5C;fuqunaga&#xFF5C;note</a></p> <p>また、非公式<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>である OpenUPM などもあり Project Settings から Scoped Registry を登録すると Package Manager から見えるようになります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopenupm.com%2F" title="Open Source Unity Package Registry (UPM)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://openupm.com/">openupm.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fshiena%2Farticles%2Funity-openupm" title="UnityでOpenUPMを簡単に利用できるようになった" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://zenn.dev/shiena/articles/unity-openupm">zenn.dev</a></cite></p> <p>パッケージのソースとしては、このような<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>ベースのものの他に以下のようなものにも対応しています。</p> <ul> <li><strong>ビルトイン</strong> <ul> <li>Unity 公式の機能</li> </ul> </li> <li><strong>埋め込み</strong> <ul> <li>Packages <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リに自分で入れたもの</li> </ul> </li> <li><strong>ローカル</strong> <ul> <li>PC 上の任意のフォルダ</li> </ul> </li> <li><strong>ローカル tarball</strong> <ul> <li>特殊用途</li> </ul> </li> <li><strong>Git</strong> <ul> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> などの Git <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>から直接追加</li> </ul> </li> </ul> <p>Unity が提供するものだけでなく、PC 上の任意のフォルダや Packages <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リも読めるのでコードの依存関係を疎にしたりチーム開発する上でも便利そうですね。また、加えて Git が利用できるようになったのはライブラリ開発者・利用者双方から便利で素晴らしいです。また後で解説しますが<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>上の任意の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを指定できる仕組みになっています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fupm-ui-giturl.html" title="Git URL からのインストール - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/upm-ui-giturl.html">docs.unity3d.com</a></cite></p> <p>どういったソースかはパッケージ名の右に表示されるのでわかりやすいです(ローカルだと Custom、Git だと git などのラベル)。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211023/20211023140911.png" alt="f:id:hecomi:20211023140911p:plain" width="1200" height="506" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211023/20211023140932.png" alt="f:id:hecomi:20211023140932p:plain" width="1200" height="271" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このようにとても便利になった UPM ですが、ライブラリ開発者側は気をつける点がたくさんあります。次にそれらを見ていきましょう。</p> <h2>UPM 対応の基本</h2> <p>パッケージ作成についての基本は以下のドキュメントにまとまっています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2FCustomPackages.html" title="カスタムパッケージの作成 - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/CustomPackages.html">docs.unity3d.com</a></cite></p> <h3>パッケージ名</h3> <p>パッケージ名を決める必要があり、これには 2 つの表記があります。一つは表示用で、今までのように例えば拙作のものであれば uRaymarching、uREPL、uDesktopDuplication といったような名前がそれにあたります。もう一つは正式名で、逆<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>表記で表されるものです。<em>com.hecomi.uraymarching</em>、<em>com.hecomi.urepl</em> といったようなものになります。日本語のドキュメントだと「<em>com.&lt;company-name></em>」から始まるものにすると書いてありますが、英語のドキュメントを見ると、別に com に限らず<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>なら何でも良いようです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2F2019.4%2FDocumentation%2FManual%2Fcus-naming.html" title="Unity - Manual: Naming your package" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/2019.4/Documentation/Manual/cus-naming.html">docs.unity3d.com</a></cite></p> <h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構造</h3> <p>次に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構造を大きく変える必要があります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fcus-layout.html" title="パッケージレイアウト - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/cus-layout.html">docs.unity3d.com</a></cite></p> <pre class="code lang-c" data-lang="c" data-unlink>&lt;root&gt; ├── package.json ├── README.md ├── CHANGELOG.md ├── LICENSE.md ├── Third Party Notices.md ├── Editor │ ├── [company-name].[package-name].Editor.asmdef │ └── EditorExample.cs ├── Runtime │ ├── [company-name].[package-name].asmdef │ └── RuntimeExample.cs ├── Tests │ ├── Editor │ │ ├── [company-name].[package-name].Editor.Tests.asmdef │ │ └── EditorExampleTest.cs │ └── Runtime │ ├── [company-name].[package-name].Tests.asmdef │ └── RuntimeExampleTest.cs ├── Samples~ │ ├── SampleFolder1 │ ├── SampleFolder2 │ └── ... └── Documentation~ └── [package-name].md </pre> <p>ライブラリとしての情報が記述されたパッケージ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>ファイル <code>package.json</code> と、この<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構造のいくつかのファイルから情報をパースし、Package Manager がうまく管理してくれるようになる感じです。これまでは <code>Scripts</code> などの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リの下に置かれることの多かった<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>は <code>Runtime</code> 以下に置くようにし、asmdef を合わせて配置する形にします。<code>Editor</code> 以下にエディタ用の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>と asmdef を置くようにすることでそれぞれの分離がきれいになります。サンプルは <code>Samples~</code> 以下に配置し、どのようなサンプルがあるかを <code>package.json</code> に記述します。こうすると利用者側は必要なサンプルのみ Package Manager 上でポチポチしてインポートできるようになります。</p> <h3>package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a></h3> <p>パッケージの名前やバージョン、作者、依存関係などの情報をここに記述します。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fupm-manifestPkg.html" title="パッケージマニフェスト - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/upm-manifestPkg.html">docs.unity3d.com</a></cite></p> <p>内容としては次のようなものになります。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">com.unity.example</span>&quot;, &quot;<span class="synStatement">version</span>&quot;: &quot;<span class="synConstant">1.2.3</span>&quot;, &quot;<span class="synStatement">displayName</span>&quot;: &quot;<span class="synConstant">Package Example</span>&quot;, &quot;<span class="synStatement">description</span>&quot;: &quot;<span class="synConstant">This is an example package</span>&quot;, &quot;<span class="synStatement">unity</span>&quot;: &quot;<span class="synConstant">2019.1</span>&quot;, &quot;<span class="synStatement">unityRelease</span>&quot;: &quot;<span class="synConstant">0b5</span>&quot;, &quot;<span class="synStatement">dependencies</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">com.unity.some-package</span>&quot;: &quot;<span class="synConstant">1.0.0</span>&quot;, &quot;<span class="synStatement">com.unity.other-package</span>&quot;: &quot;<span class="synConstant">2.0.0</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">keywords</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">keyword1</span>&quot;, &quot;<span class="synConstant">keyword2</span>&quot;, &quot;<span class="synConstant">keyword3</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">author</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">Unity</span>&quot;, &quot;<span class="synStatement">email</span>&quot;: &quot;<span class="synConstant">unity@example.com</span>&quot;, &quot;<span class="synStatement">url</span>&quot;: &quot;<span class="synConstant">https://www.unity3d.com</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p>これはローカルパッケージなら <a class="keyword" href="http://d.hatena.ne.jp/keyword/GUI">GUI</a> 上で編集が可能です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211023/20211023141840.png" alt="f:id:hecomi:20211023141840p:plain" width="937" height="770" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><code>library</code> や <code>module</code>、<code>tool</code> などのタイプを指定できる <code>type</code> だけは何をどういう基準で選択すればよいかの説明がまだないので自分は空にしています。。</p> <h3>配布</h3> <p>さて、こうして<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構造を整理し、package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a> や関連するファイルを追加した後、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> など何らかの Git の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に公開します。この<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の URL を利用者側に Package Manager 上で入力してもらう形になります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fupm-ui-giturl.html" title="Git URL からのインストール - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/upm-ui-giturl.html">docs.unity3d.com</a></cite></p> <p>ただ、Unity のプロジェクトとして Git にあげているプロジェクトの場合は <code>package.json</code> がルートではなく Assets/ 以下に置かれていることもあると思います。以下は uOSC の例です:</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuOSC%2Ftree%2F95f19876dbe93ca5d41a32ce17a867010e38e554%2FAssets%2FuOSC%2FScripts" title="uOSC/Assets/uOSC/Scripts at 95f19876dbe93ca5d41a32ce17a867010e38e554 · hecomi/uOSC" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uOSC/tree/95f19876dbe93ca5d41a32ce17a867010e38e554/Assets/uOSC/Scripts">github.com</a></cite></p> <p>このような場合は、次のように <code>package.json</code> の置かれた場所を示す URL にするとインポートできます:</p> <ul> <li><a href="https://github.com/hecomi/uOSC?path=/Assets/uOSC/Scripts">https://github.com/hecomi/uOSC?path=/Assets/uOSC/Scripts</a></li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211023/20211023143207.png" alt="f:id:hecomi:20211023143207p:plain" width="1200" height="278" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Git <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に置く以外にも冒頭で述べたように npmjs や OpenUPM に登録すると、更新があった際にユーザが <a class="keyword" href="http://d.hatena.ne.jp/keyword/GUI">GUI</a> 上で更新ボタンを押して更新できるようになります。これはまた別途検証します。</p> <p>さて、これまでの情報でプロジェクト全体の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構造を次に考えていきたいと思います。</p> <h2>開発<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構造</h2> <h3>Assets に含める方式</h3> <p>先述のプロジェクトの構造を見てみます。</p> <pre class="code lang-c" data-lang="c" data-unlink>&lt;Repository&gt; ├── Assets │ └── uOSC │ ├── Examples │ └── Scripts │ ├── package.json │ ├── uOSC.asmdef │ ├── uOscClient.cs │ ├── ... ├── ... </pre> <p>この構造の問題点は Examples が<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リの外側にありパッケージに含められていない点です。これをパッケージにサンプルとして含めようとすると次のようになります。</p> <pre class="code lang-c" data-lang="c" data-unlink>&lt;Repository&gt; ├── Assets │ └── uOSC │ ├── package.json │ ├── Runtime │ │ ├── uOSC.asmdef │ │ ├── uOscClient.cs │ │ └── ... │ └── Samples~ │ ├── Sample <span class="synConstant">1</span> │ ├── Sample <span class="synConstant">2</span> │ ├── Sample <span class="synConstant">3</span> │ └── ... ├── ... </pre> <p>これで <code>package.json</code> に Sample 1~3 の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを記述すればサンプルとしてポチポチとインポートできるようになります。しかしながらこの構造では<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ名に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C1%A5%EB%A5%C0">チルダ</a>がついていることから Unity のインポートから除外されてしまうため、Unity の Project ウィンドウからこれらサンプルが見えなくなってしまいます。これはライブラリ開発側として困りますし、.meta も生成されないので、一度<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C1%A5%EB%A5%C0">チルダ</a>を外して調整、コミット前に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C1%A5%EB%A5%C0">チルダ</a>を追加...、などとやる必要が出てきてしまいます。面倒ですしリネームし忘れなどの事故も起きそうですね。</p> <h3>パッケージのみ管理方式</h3> <p>パッケージ部分のみ管理する方式を採用しているプロジェクトもあります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ffuqunaga%2FRapidGUI" title="GitHub - fuqunaga/RapidGUI: Unity OnGUI(IMGUI) extensions for Rapid prototyping/development" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/fuqunaga/RapidGUI">github.com</a></cite></p> <p>これをローカルパッケージとして取り込み利用、外側のプロジェクトは自身の PC 上で開発、という方式ですね。自分はネイティブ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>の開発プロジェクトも含むケースが多く、Packages の外側にそれを置きたいのでこの方式はしないかな、と判断しました。</p> <h3>Packages から<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%DC%A5%EA%A5%C3%A5%AF%A5%EA%A5%F3%A5%AF">シンボリックリンク</a>方式</h3> <p>どうしようか困っていたら <a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a> でご助言をいただきました。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">これみたいにPackagesに本体を配置してAssetsにsymlinkを張ると開発しやすいですよ<a href="https://t.co/RAa2CSd0sX">https://t.co/RAa2CSd0sX</a></p>&mdash; KOGA Mitsuhiro (@shiena) <a href="https://twitter.com/shiena/status/1449431902293684224?ref_src=twsrc%5Etfw">2021年10月16日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>この方式にすると以下のような構造になります。</p> <pre class="code lang-c" data-lang="c" data-unlink>&lt;Repository&gt; ├── Packages │ └── com.hecomi.uosc │ ├── package.json │ ├── Runtime │ │ ├── uOSC.asmdef │ │ ├── uOscClient.cs │ │ └── ... │ └── Samples~ ├── Assets │ │ └── uOSC ↓ │ └── Samples [~Samples] │ ├── Sample <span class="synConstant">1</span> │ ├── Sample <span class="synConstant">2</span> │ ├── Sample <span class="synConstant">3</span> │ └── ... ├── ... </pre> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C1%A5%EB%A5%C0">チルダ</a>を含まない形で Assets にサンプルの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リをインポートできるので、適切に .meta も作成されます。また、ローカル方式でパッケージを作成すると、Project ウィンドウから直接パッケージ内のファイルを修正したり、新たにファイルを追加したりも出来るので通常通り開発が可能です。ただ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%DC%A5%EA%A5%C3%A5%AF%A5%EA%A5%F3%A5%AF">シンボリックリンク</a>を使うと Unity から警告メッセージが表示されます。</p> <blockquote><p>Assets/uOSC/Samples is a symbolic link. Using symlinks in Unity projects may cause your project to become corrupted if you create multiple references to the same asset, use recursive symlinks or use symlinks to share assets between projects used with different versions of Unity. Make sure you know what you are doing.</p></blockquote> <p>利用者側にはこれは表示されないですし、このケースでは衝突もないので無視してしまってもよいかと思います。</p> <p>ただ、Package による配布と従来の .unitypackage の配布を両立したい、となってくると問題が出てきます。レガシーな環境や Package Manager を使わない開発をしていたり、インポートしたあとに色々と<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を修正して使いたい人は、以前と同じく .unitypackage で直接プロジェクトに取り込みたいと思うかもしれません(実際はローカルパッケージ化すれば編集できます)。しかしながらこの構造にしてしまうと .unitypackage が作りづらくなってしまいます。.unitypackage は Packages 以下のものを含めてパッケージングすることは出来ないからです。.unitypackage を配布しない場合はお手軽でかなり良い選択肢だと思います。</p> <h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions で配布用のブランチを作成する方式</h3> <p>こちらの記事ではかなり進んだ内容が紹介されてました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fgoma_recorder%2Fitems%2Fd4dd1a3d1c04726f55b5" title="UnityのCustom Packageの作り方と自動リリースの方法 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/goma_recorder/items/d4dd1a3d1c04726f55b5">qiita.com</a></cite></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions を使ってメインブランチにコミットがあった際、そのコミットメッセージに応じて自動的にバージョンを付与、upm ブランチへ <code>git subtree split</code> を使って必要な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リのみ切り出し必要に応じて <code>README.md</code> の移動や <code>Samples~</code> へのリネームも行い、更に npmjs のリリースまで行うというものです。</p> <h2>自分のパッケージ作成方針</h2> <p>これまでの調査を踏まえて自分なりの構造を考えていきます。自分のやりたいことは以下のような形です:</p> <ol> <li>UPM での動作を確認するため、コアはローカルパッケージの形式で Packages 以下から見える形にしたい</li> <li>Samples は UPM でインポートする形式にしたいが、開発時のプロジェクトでは開発しやすいように普通にプロジェクトに含まれるようにしたい</li> <li>UPM 使わない人のために .unitypackage を簡単に作れるようにしたい</li> <li>できれば 1 <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>、1 プロジェクトで開発したい</li> <li>ネイティブ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>の開発も同じ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で管理したい</li> <li>README.md は 1 つにしたい(<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> のプロジェクトのルートでの表示用と、パッケージのルート<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リの配置用を分けたくない)</li> </ol> <p>前項のいずれの方法を使っても、全部同時に実現するのは難しそうです(今は思いつきませんでした)。色々と検討したのですが、最後の <a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions で自動的に UPM 用のリリースブランチを作成し、そこに必要な構造を作成する方式がとても良さそうに思えました。</p> <p>元記事ではセマンティックバージョニング含めかなりしっかり作られていますが、自分は結構ゆるく運用してるので次のような感じで行こうかと思います。</p> <ul> <li>1(ローカルパッケージとして開発)を諦める</li> <li>Runtime / Editor / Samples といったフォルダ構成を利用する(Samples~ にはしない)</li> <li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> へバージョン付きの Tag を push した時に、自動的にパッケージ用の構造を作成する(<code>git subtree</code> で必要なファイルだけ切り出す &amp; Samples を Samples~ へリネームするなど</li> <li>OpenUPM や npmjs へのリリースは取り敢えず様子見</li> </ul> <p>これにより、開発は次のような構造にします。</p> <pre class="code lang-c" data-lang="c" data-unlink>&lt;Repository&gt; ├── README.md ├── Assets │ └── uOSC │ ├── package.json │ ├── Runtime │ │ ├── uOSC.asmdef │ │ ├── uOscClient.cs │ │ └── ... │ └── Samples │ ├── Sample <span class="synConstant">1</span> │ ├── Sample <span class="synConstant">2</span> │ ├── Sample <span class="synConstant">3</span> │ └── ... ├── ... </pre> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構造だけ変えておきます。.unitypackage はいつもどおり Unity 上で適当なタイミングで手動で作成します。</p> <p>タグを push したときはそれをイベントとして次のようなブランチを作成します。作成したブランチは upm ブランチには最新を、また upm/v1.2.3 のようにバージョン付きのものも同時にリリースして古いバージョンを使いたい人は使えるようにします。ブランチの構造は <code>git subtree</code> で切り出した上記 uOSC <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ内のファイル及び必要なファイル(<code>README.md</code> や <code>package.json</code>)を配置するようにします。</p> <pre class="code lang-c" data-lang="c" data-unlink>&lt;Repository&gt; ├── README.md ├── package.json ├── Runtime │ ├── uOSC.asmdef │ ├── uOscClient.cs │ └── ... ├─ Samples~ │ ├── Sample <span class="synConstant">1</span> │ ├── Sample <span class="synConstant">2</span> │ ├── Sample <span class="synConstant">3</span> │ └── ... ├── ... </pre> <h2><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の作成</h2> <h3>環境セットアップ</h3> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の記事は、様々な業種で使われる関係上たくさんの解説がされていますし、公式のドキュメントも充実しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fja%2Factions" title="GitHub Actionsのドキュメント - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/ja/actions">docs.github.com</a></cite></p> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> 上で開発するのは大変なので、自分は act を使ってローカルでワークフローのテストを行いました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fnektos%2Fact" title="GitHub - nektos/act: Run your GitHub Actions locally 🚀" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/nektos/act">github.com</a></cite></p> <p>動作には Docker が必要ですが、昨今は <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> でも Docker Desktop for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> のセットアップがポチポチするだけで簡単に終わるので導入は難しくありませんでした。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.docker.jp%2Fdocker-for-windows%2Finstall.html" title="Windows に Docker Desktop をインストール — Docker-docs-ja 19.03 ドキュメント" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.docker.jp/docker-for-windows/install.html">docs.docker.jp</a></cite></p> <h3><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の作成</h3> <p>次のようなアクションを作成します。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Update-UPM-Branch <span class="synComment"># v1.2.3 のようなタグがプッシュされたら起動</span> <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">push</span><span class="synSpecial">:</span> <span class="synIdentifier">tags</span><span class="synSpecial">:</span> <span class="synStatement">- </span>v* <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">MAIN_BRANCH</span><span class="synSpecial">:</span> main <span class="synIdentifier">UPM_BRANCH</span><span class="synSpecial">:</span> upm <span class="synIdentifier">PKG_ROOT_DIR</span><span class="synSpecial">:</span> Assets/uOSC <span class="synIdentifier">SAMPLES_DIR</span><span class="synSpecial">:</span> Samples <span class="synIdentifier">DOC_FILES</span><span class="synSpecial">:</span> README.md CHANGELOG.md LICENSE.md <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">test</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synComment"> # 最新を取得</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Checkout <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v2 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">fetch-depth</span><span class="synSpecial">:</span> <span class="synConstant">0</span> <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> git checkout main <span class="synComment"> # イベントを起動したタグを steps.tag.outputs.name に格納</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Tag name <span class="synIdentifier">id</span><span class="synSpecial">:</span> tag <span class="synIdentifier">run</span><span class="synSpecial">:</span> echo ::set-output name=name::${GITHUB_REF#refs/tags/v} <span class="synComment"> # Git の設定</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Git config <span class="synIdentifier">run</span><span class="synSpecial">:</span> | git config user.name <span class="synConstant">&quot;github-actions[bot]&quot;</span> git config user.email <span class="synConstant">&quot;github-actions[bot]@users.noreply.github.com&quot;</span> <span class="synComment"> # UPM 用のブランチを作成</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create UPM branches <span class="synIdentifier">run</span><span class="synSpecial">:</span> | <span class="synComment"> # 古いブランチを削除</span> git branch -D $UPM_BRANCH <span class="synType">&amp;&gt;</span> /dev/<span class="synConstant">null</span> || echo $UPM_BRANCH branch is not found <span class="synComment"> # Assets/uOSC 以下を upm ブランチに切り出す</span> git subtree split -P <span class="synConstant">&quot;$PKG_ROOT_DIR&quot;</span> -b $UPM_BRANCH <span class="synComment"> # 切り出したブランチに移動</span> git checkout $UPM_BRANCH <span class="synComment"> # メインブランチの方にあった README などをインポート</span> for file in $DOC_FILES; do git checkout $MAIN_BRANCH $file <span class="synType">&amp;&gt;</span> /dev/<span class="synConstant">null</span> || echo $file is not found done <span class="synComment"> # Samples ディレクトリを Samples~ に改名</span> git mv $SAMPLES_DIR Samples~ <span class="synType">&amp;&gt;</span> /dev/<span class="synConstant">null</span> || echo $SAMPLES_DIR is not found <span class="synComment"> # Samples.meta だけ別途インポートされるので不要(他の .meta は必要)</span> rm Samples.meta <span class="synComment"> # package.json のバージョンを置換</span> sed -i -e <span class="synConstant">&quot;s/</span><span class="synSpecial">\&quot;</span><span class="synConstant">version</span><span class="synSpecial">\&quot;</span><span class="synConstant">:.*$/</span><span class="synSpecial">\&quot;</span><span class="synConstant">version</span><span class="synSpecial">\&quot;</span><span class="synConstant">: </span><span class="synSpecial">\&quot;</span><span class="synConstant">$TAG</span><span class="synSpecial">\&quot;</span><span class="synConstant">,/&quot;</span> package.json || echo package.json is not foundあ <span class="synComment"> # タグ名とともにコミット</span> git commit -am <span class="synConstant">&quot;release $TAG.&quot;</span> <span class="synComment"> # GitHub へ push</span> git push -f origin $UPM_BRANCH <span class="synComment"> # タグ付きのブランチも作成して push</span> git checkout -b $UPM_BRANCH@$TAG git push -f origin $UPM_BRANCH@$TAG <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">TAG</span><span class="synSpecial">:</span> ${{ steps.tag.outputs.name }} </pre> <p>これで例えば v1.2.3 を push すると、自動的に upm ブランチと upm@1.2.3 ブランチが作成され、これを <code>https://github.com/hecomi/uOSC.git#upm</code> といった URL で Package Manager 上でインポートできるようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211026/20211026231355.png" alt="f:id:hecomi:20211026231355p:plain" width="1200" height="743" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20211026/20211026231328.png" alt="f:id:hecomi:20211026231328p:plain" width="1200" height="422" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>meta の追加</h3> <p>ただ、まだ 1 点だけ問題があります。それは <code>README.md</code> の meta ファイルが存在していないので、<code>README.md</code> がインポートされない点です…。</p> <blockquote><p>Asset Packages/com.hecomi.uosc/README.md has no meta file, but it's in an immutable folder. The asset will be ignored.</p></blockquote> <p>ちょっとお行儀が悪いですが、<code>README.md.meta</code> は <code>package.json.meta</code> の UUID だけ変えて生成する形にしてしまいましょう。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synPreProc">...</span> for file in $DOC_FILES; do git checkout $MAIN_BRANCH $file <span class="synType">&amp;&gt;</span> /dev/<span class="synConstant">null</span> || echo $file is not found <span class="synComment"> # .meta ファイルを作成(package.json.meta を改変)</span> if <span class="synSpecial">[</span> -f $file <span class="synSpecial">]</span>; then cp package.json.meta $file.meta UUID=$(cat /proc/sys/kernel/random/uuid | tr -d <span class="synConstant">'-'</span>) sed -i -e <span class="synConstant">&quot;s/guid:.*$/guid: $UUID/&quot;</span> $file.meta git add $file.meta fi done <span class="synPreProc">...</span> </pre> <p>これで警告もなくインポートができるようになりました。そのうち <code>Samples~</code> も自動生成するとかもやりたいです。</p> <h3>Composite Actions 化</h3> <p>さて、1 つのプロジェクトならこれで良いですが複数のプロジェクトでこのコードを全部コピペするのはちょっとしんどいです。バグが見つかったときも直して回る必要があるのも微妙です。</p> <p>そこで、Composite Action を使ってこの一連の流れを使い回せるようにしてみようと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fen%2Factions%2Fcreating-actions%2Fcreating-a-composite-action" title="Creating a composite action - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/en/actions/creating-actions/creating-a-composite-action">docs.github.com</a></cite></p> <p>先程のコードの <code>env</code> を <code>inputs</code> にして外から与えられるようにし、<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>部分は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B7%A5%A7%A5%EB%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">シェルスクリプト</a>のファイルとして分離しました。この Composite Action は以下のように別の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を用意しておきます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2Fcreate-upm-branch-action" title="GitHub - hecomi/create-upm-branch-action: A Github Actions to create release branches for Unity Package Manager" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/create-upm-branch-action">github.com</a></cite></p> <p>この上でそれぞれの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>側のアクションを次のように修正します。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Update-UPM-Branch <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">push</span><span class="synSpecial">:</span> <span class="synIdentifier">tags</span><span class="synSpecial">:</span> <span class="synStatement">- </span>v* <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">update</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Checkout <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v2 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">fetch-depth</span><span class="synSpecial">:</span> <span class="synConstant">0</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Tag name <span class="synIdentifier">id</span><span class="synSpecial">:</span> tag <span class="synIdentifier">run</span><span class="synSpecial">:</span> echo ::set-output name=name::${GITHUB_REF#refs/tags/v} <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create UPM Branches <span class="synIdentifier">uses</span><span class="synSpecial">:</span> hecomi/create-upm-branch-action@main <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">git-tag</span><span class="synSpecial">:</span> ${{ steps.tag.outputs.name }} <span class="synIdentifier">pkg-root-dir-path</span><span class="synSpecial">:</span> Assets/uOSC </pre> <p>とても短くなりました!あとはこのアクションを各<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に同じように設定していけば同じコードを使って tag を push するだけでデプロイ出来るようになります。同じ仕組みでやりたい方がいらっしゃいましたらご活用ください。</p> <h2>設定例</h2> <p>本記事のコードでもいくつか出てきた uOSC で使い始めてみました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuOSC" title="GitHub - hecomi/uOSC: OSC server / client implementation for Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uOSC">github.com</a></cite></p> <p>無事動き始めています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FuOSC%2Factions" title="Actions · hecomi/uOSC" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/uOSC/actions">github.com</a></cite></p> <h2>その他</h2> <h3>パッケージ管理用の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>が書ける</h3> <p>  <code>Unity.PackageManager.Client</code> を使うと特定のパッケージをプロジェクトに追加したり、パッケージのソースごとに列挙したりといったことが可能です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2Fja%2Fcurrent%2FManual%2Fupm-api.html" title="パッケージ用のスクリプティング API - Unity マニュアル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/ja/current/Manual/upm-api.html">docs.unity3d.com</a></cite></p> <p>上記ページではより進んだサンプルとして、ローカルでないパッケージをプロジェクトの Packages フォルダに取り込み、編集可能にするための<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>が紹介されています。</p> <h2>おわりに</h2> <p>Package Manager について調べてみましたが、想定していたよりも便利で、自分のプロジェクトではアセットはすべてこれで管理したほうが良さそうだなぁ、と思うようになりました。同じように思う人のためにも、ポチポチと時間を見つけて配布しているライブラリをメンテしていこうと思います。また、今回は UPM 用のリリースブランチを作成するところまででしたが、後々 OpenUPM への登録、unitypackage の作成も自動化していきたいなぁと思っています。</p> <div class="footnote"> <p class="footnote"><a href="#fn-8b716ef4" name="f-8b716ef4" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://docs.unity3d.com/ja/current/ScriptReference/PackageManager.Requests.Request.html">https://docs.unity3d.com/ja/current/ScriptReference/PackageManager.Requests.Request.html</a></span></p> </div> hecomi Unity で DICOM 画像をレンダリングしてみる hatenablog://entry/13574176438016006897 2021-09-27T00:20:49+09:00 2021-09-27T00:20:49+09:00 はじめに 先日、Twitter で手持ちの DICOM 画像を以前書いたボリュームレンダリングの記事のように描画したい、というご相談をいただき試してみましたので内容を共有します。 tips.hecomi.com DICOM(ダイコム)とは「Digital Imaging and COmmunications in Medicine」の略で、CT や MRI などのデータを保存する画像や通信プロトコルといった医用の標準規格です。今回の場合は、MRI のスライス画像が DICOM という形式のもと保存されているといった状況で、それをどうレンダリングすればよいのか、というお話でした。 ja.wiki… <h2>はじめに</h2> <p>先日、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Twitter">Twitter</a> で手持ちの <strong>DICOM</strong> 画像を以前書いた<strong>ボリューム<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a></strong>の記事のように描画したい、というご相談をいただき試してみましたので内容を共有します。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2018%2F01%2F05%2F192332" title="Unity でボリュームレンダリングをしてみる - vol.1 データ表示 - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2018/01/05/192332">tips.hecomi.com</a></cite></p> <p>DICOM(ダイコム)とは「Digital Imaging and COmmunications in Medicine」の略で、CT や <a class="keyword" href="http://d.hatena.ne.jp/keyword/MRI">MRI</a> などのデータを保存する画像や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C4%CC%BF%AE%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">通信プロトコル</a>といった医用の標準規格です。今回の場合は、<a class="keyword" href="http://d.hatena.ne.jp/keyword/MRI">MRI</a> のスライス画像が DICOM という形式のもと保存されているといった状況で、それをどう<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>すればよいのか、というお話でした。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fja.wikipedia.org%2Fwiki%2FDICOM" title="DICOM - Wikipedia" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://ja.wikipedia.org/wiki/DICOM">ja.wikipedia.org</a></cite></p> <h2>デモ</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20210927/20210927001258.gif" alt="f:id:hecomi:20210927001258g:plain" width="720" height="405" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20210927/20210927001311.gif" alt="f:id:hecomi:20210927001311g:plain" width="720" height="405" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>ダウンロード</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fhecomi%2FUnityDICOMVolumeRendering" title="GitHub - hecomi/UnityDICOMVolumeRendering: This is a simple project for volume rendering of DICOM data in Unity." class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/hecomi/UnityDICOMVolumeRendering">github.com</a></cite></p> <h2>情報収集</h2> <p>Unity で DICOM を扱うことに関してはインターネット上にいくつか情報がありました。</p> <h3>Unite Tokyo 2018</h3> <p>カンファレンスイベントである Unite Tokyo 2018 では、DICOM に関連した講演を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C5%EC%B5%FE%C2%E7%B3%D8">東京大学</a>医学部<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C7%BE%BF%C0%B7%D0%B3%B0%B2%CA">脳神経外科</a>の金太一<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BD%F5%B6%B5">助教</a>がされていました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcgworld.jp%2Ffeature%2F201805-unite-03medical.html" title="Unityを駆使する脳神経外科医が語る、医療現場での3DCG活用例とその可能性~Unite Tokyo 2018レポート(3)~ | 特集 | CGWORLD.jp" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://cgworld.jp/feature/201805-unite-03medical.html">cgworld.jp</a></cite></p> <p><iframe width="720" height="540" src="https://www.youtube.com/embed/SZP3LwuSn1o?wmode=transparent" frameborder="0" allowfullscreen></iframe></p> <p>この中で、Kompath 社と協力して作られた DICOM を読み込める Simple DICOM Loader や、マーチングキューブ法でメッシュ化する High Speed CPU-based Marching cubes が紹介されていました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fassetstore.unity.com%2Fpackages%2Ftools%2Finput-management%2Fsimple-dicom-loader-116915%3Flocale%3Dja-JP" title="Simple DICOM Loader | 入出力管理 | Unity Asset Store" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://assetstore.unity.com/packages/tools/input-management/simple-dicom-loader-116915?locale=ja-JP">assetstore.unity.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fassetstore.unity.com%2Fpackages%2Ftools%2Fmodeling%2Fhigh-speed-cpu-based-marching-cubes-117483%3Flocale%3Dja-JP" title="High Speed CPU-based Marching cubes | モデリング | Unity Asset Store" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://assetstore.unity.com/packages/tools/modeling/high-speed-cpu-based-marching-cubes-117483?locale=ja-JP">assetstore.unity.com</a></cite></p> <h3>fo-dicom の利用</h3> <p>また、Q&amp;A サイトの teratail では正に似たような質問がされていました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fteratail.com%2Fquestions%2F304997" title="unity 3D ボリュームレンダリング rawデータからテクスチャ3Dへの変換|teratail" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://teratail.com/questions/304997">teratail.com</a></cite></p> <p>こちらでは回答者さんが fo-dicom という無料の DICOM ローダーを使って <code>Texture3D</code> 化をされていました。</p> <h2>方針</h2> <p>上記 fo-dicom を使って同様に <code>Texture3D</code> 化を行う方針で進めることにしました。</p> <p>私の記事では PVM という形式で配布されているデータを RAW 形式に変換、更にそれをパースするみたいな部分に記事の多くの分量が割かれてしまっていましたが、本質はいくつかの工程を経てデータを <code>Texture3D</code> 化したあとの、それをシェーダでどのように<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>するかというところにあるので、大きな方針としては変わりません。</p> <p>つまり、DICOM データを fo-dicom を利用して Texture3D に変換する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を書き、その <code>Texture3D</code> を入力として以前の記事と同じコードで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>を行います。</p> <h2>データのダウンロード</h2> <p>ご相談頂いた方のデータは諸事情でブログでは使えないため、本記事では配布されているデータで試してみることにします。上記質問回答の文中にもあるページから「Visible Female CT Datasets > Regional Tar Files (download) > Head」をダウンロード・展開しておきます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmri.radiology.uiowa.edu%2Fvisible_human_datasets.html" title="MR Research Facility :: Visible Human Project Datasets" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://mri.radiology.uiowa.edu/visible_human_datasets.html">mri.radiology.uiowa.edu</a></cite></p> <h2>fo-dicom のインポート</h2> <p>fo-dicom 本体は <a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> にて<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>として配布されています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ffo-dicom%2Ffo-dicom" title="GitHub - fo-dicom/fo-dicom: Fellow Oak DICOM for .NET, .NET Core, Universal Windows, Android, iOS, Mono and Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/fo-dicom/fo-dicom">github.com</a></cite></p> <p>Unity 向けのパッケージは Asset Store にて配布されています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fassetstore.unity.com%2Fpackages%2Ftools%2Fintegration%2Ffo-dicom-60902" title="fo-dicom | Integration | Unity Asset Store" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://assetstore.unity.com/packages/tools/integration/fo-dicom-60902">assetstore.unity.com</a></cite></p> <p>Asset Store からパッケージをインストールし、プロジェクトに展開しましょう。これで fo-dicom を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>から使えるようになります。</p> <h2>インポート<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>の記述とデータのインポート</h2> <h3>方針</h3> <p>次にデータのインポート<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を書きます。先程の展開したデータの中身を見ると大量の .dcm ファイルが有ることが分かります。それぞれのファイルがスライスした画になるので、1 枚 1 枚を <code>Texture2D</code> となるようインポートし、これを後で結合して <code>Texture3D</code> になるようにします。</p> <p>teratail で回答者さんが便利な Editor <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を書いていらっしゃいましたが、DICOM 以外の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>でもわかりやすいように最小限のコードで変換をしてみたいと思います。なので、.dcm を <code>Texture2D</code> としてインポートする<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>および、複数の <code>Texture2D</code> をまとめて <code>Texture3D</code> にする<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>の 2 つを別々に最小限のコードで書いてみます。</p> <h3>.dcm ファイルのインポート</h3> <p>まずは、.dcm を <code>Texture2D</code> としてインポートしてみます。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> System; [UnityEditor.AssetImporters.ScriptedImporter(<span class="synConstant">1</span>, <span class="synConstant">&quot;dcm&quot;</span>)] <span class="synType">public</span> <span class="synType">class</span> DicomImporter: UnityEditor.AssetImporters.ScriptedImporter { <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnImportAsset(UnityEditor.AssetImporters.AssetImportContext ctx) { <span class="synStatement">try</span> { var image = <span class="synStatement">new</span> Dicom.Imaging.DicomImage(ctx.assetPath); var tex2d = image.RenderImage().As&lt;Texture2D&gt;(); ctx.AddObjectToAsset(<span class="synConstant">&quot;Volume&quot;</span>, tex2d); } <span class="synStatement">catch</span> (Exception e) { Debug.LogException(e); } } } </pre> <p>fo-dicom のドキュメントを見ると <code>DicomImage</code> を作成し、これを <code>RenderImage().As&lt;Texture2D&gt;()</code> するだけで Unity の <code>Texture2D</code> に変換できるようになっています。これを <code>AssetImportContext</code> に登録することで .dcm ファイルが <code>Texture2D</code> として使えるようになります。</p> <h3>Texture3D へ結合</h3> <p>ウィンドウを作っても良いのですが、シンプルに <code>MonoBehaviour</code> の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>とそのエディタ拡張の組み合わせで片付けます。まず、次のような<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を作ります。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> System.Collections.Generic; <span class="synType">public</span> <span class="synType">class</span> Texture2DArrayToTexture3DConverter : MonoBehaviour { <span class="synType">public</span> List&lt;Texture2D&gt; texture2DArray; } </pre> <p>そして、このエディタ拡張でインスペクタから登録された <code>Texture2D</code> の配列を結合して <code>Texture3D</code> にします。</p> <pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine; <span class="synStatement">using</span> UnityEditor; [CustomEditor(<span class="synStatement">typeof</span>(Texture2DArrayToTexture3DConverter))] <span class="synType">public</span> <span class="synType">class</span> Texture2DArrayToTexture3DConverterEditor : Editor { <span class="synType">string</span> error; <span class="synType">public</span> <span class="synType">override</span> <span class="synType">void</span> OnInspectorGUI() { <span class="synStatement">base</span>.OnInspectorGUI(); <span class="synStatement">if</span> (GUILayout.Button(<span class="synConstant">&quot;Convert&quot;</span>)) { error = <span class="synConstant">&quot;&quot;</span>; Convert(); } <span class="synStatement">if</span> (!<span class="synType">string</span>.IsNullOrEmpty(error)) { EditorGUILayout.HelpBox(error, MessageType.Error); } } <span class="synType">bool</span> Convert() { var converter = target <span class="synStatement">as</span> Texture2DArrayToTexture3DConverter; var tex2dArray = converter.texture2DArray; <span class="synStatement">if</span> (tex2dArray.Count == <span class="synConstant">0</span>) { error = <span class="synConstant">&quot;no image&quot;</span>; <span class="synStatement">return</span> <span class="synConstant">false</span>; } tex2dArray.Reverse(); var w = tex2dArray[<span class="synConstant">0</span>].width; var h = tex2dArray[<span class="synConstant">0</span>].height; var d = tex2dArray.Count; var format = tex2dArray[<span class="synConstant">0</span>].format; var colors = <span class="synStatement">new</span> Color32[w * h * d]; <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i &lt; d; ++i) { var tex2d = tex2dArray[i]; <span class="synStatement">if</span> (tex2d.width != w || tex2d.height != h) { error = <span class="synConstant">&quot;texture size error&quot;</span>; <span class="synStatement">return</span> <span class="synConstant">false</span>; } <span class="synStatement">if</span> (tex2d.format != format) { error = <span class="synConstant">&quot;texture format error&quot;</span>; <span class="synStatement">return</span> <span class="synConstant">false</span>; } tex2d.GetPixels32().CopyTo(colors, w * h * i); } var tex3d = <span class="synStatement">new</span> Texture3D(w, h, d, format, <span class="synConstant">false</span>); tex3d.SetPixels32(colors); tex3d.Apply(); var path = EditorUtility.SaveFilePanelInProject( <span class="synConstant">&quot;Texture3D&quot;</span>, $<span class="synConstant">&quot;New Texture3D Asset&quot;</span>, <span class="synConstant">&quot;asset&quot;</span>, <span class="synConstant">&quot;Save Texture3D&quot;</span>); AssetDatabase.CreateAsset(tex3d, path); AssetDatabase.SaveAssets(); <span class="synStatement">return</span> <span class="synConstant">true</span>; } } </pre> <p>こうして次のように適当なシーンを作成、DICOM ファイルを先ほど作成した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>にドラッグして登録し、エディタ拡張で作成した Convert ボタンを押下します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hecomi/20210926/20210926230117.png" alt="f:id:hecomi:20210926230117p:plain" width="1200" height="1081" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これで、<code>Texture3D</code> が生成されます。</p> <h2><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a></h2> <p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>は以下の記事のものを使います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftips.hecomi.com%2Fentry%2F2018%2F01%2F28%2F134115" title="Unity でボリュームレンダリングをしてみる - vol.3 色付け / シェーディング - 凹みTips" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tips.hecomi.com/entry/2018/01/28/134115">tips.hecomi.com</a></cite></p> <p>少しだけ改変しました。おさらいのためコードを貼っておきます。</p> <pre class="code lang-c" data-lang="c" data-unlink>Shader <span class="synConstant">&quot;VolumeRendering/Basic&quot;</span> { Properties { [Header(Rendering)] _Volume(<span class="synConstant">&quot;Volume&quot;</span>, 3D) = <span class="synConstant">&quot;&quot;</span> {} _Transfer(<span class="synConstant">&quot;Transfer&quot;</span>, 2D) = <span class="synConstant">&quot;&quot;</span> {} _Iteration(<span class="synConstant">&quot;Iteration&quot;</span>, Int) = <span class="synConstant">10</span> _Intensity(<span class="synConstant">&quot;Intensity&quot;</span>, Range(<span class="synConstant">0.0</span>, <span class="synConstant">1.0</span>)) = <span class="synConstant">0.1</span> [Enum(UnityEngine.Rendering.BlendMode)] _BlendSrc (<span class="synConstant">&quot;Blend Src&quot;</span>, Float) = <span class="synConstant">5</span> [Enum(UnityEngine.Rendering.BlendMode)] _BlendDst (<span class="synConstant">&quot;Blend Dst&quot;</span>, Float) = <span class="synConstant">10</span> [Header(Ranges)] _MinX(<span class="synConstant">&quot;MinX&quot;</span>, Range(<span class="synConstant">0</span>, <span class="synConstant">1</span>)) = <span class="synConstant">0.0</span> _MaxX(<span class="synConstant">&quot;MaxX&quot;</span>, Range(<span class="synConstant">0</span>, <span class="synConstant">1</span>)) = <span class="synConstant">1.0</span> _MinY(<span class="synConstant">&quot;MinY&quot;</span>, Range(<span class="synConstant">0</span>, <span class="synConstant">1</span>)) = <span class="synConstant">0.0</span> _MaxY(<span class="synConstant">&quot;MaxY&quot;</span>, Range(<span class="synConstant">0</span>, <span class="synConstant">1</span>)) = <span class="synConstant">1.0</span> _MinZ(<span class="synConstant">&quot;MinZ&quot;</span>, Range(<span class="synConstant">0</span>, <span class="synConstant">1</span>)) = <span class="synConstant">0.0</span> _MaxZ(<span class="synConstant">&quot;MaxZ&quot;</span>, Range(<span class="synConstant">0</span>, <span class="synConstant">1</span>)) = <span class="synConstant">1.0</span> } CGINCLUDE <span class="synPreProc">#include </span><span class="synConstant">&quot;UnityCG.cginc&quot;</span> <span class="synType">struct</span> appdata { float4 vertex : POSITION; }; <span class="synType">struct</span> v2f { float4 vertex : SV_POSITION; float4 localPos : TEXCOORD0; float4 worldPos : TEXCOORD1; }; sampler3D _Volume; sampler2D _Transfer; <span class="synType">int</span> _Iteration; <span class="synType">float</span> _Intensity; <span class="synType">float</span> _MinX, _MaxX, _MinY, _MaxY, _MinZ, _MaxZ; <span class="synType">struct</span> Ray { float3 from; float3 dir; <span class="synType">float</span> tmax; }; <span class="synType">void</span> intersection(inout Ray ray) { float3 invDir = <span class="synConstant">1.0</span> / ray.dir; float3 t1 = (-<span class="synConstant">0.5</span> - ray.from) * invDir; float3 t2 = (+<span class="synConstant">0.5</span> - ray.from) * invDir; float3 tmax3 = max(t1, t2); float2 tmax2 = min(tmax3.xx, tmax3.yz); ray.tmax = min(tmax2.x, tmax2.y); } <span class="synType">inline</span> <span class="synType">float</span> sampleVolume(float3 pos) { <span class="synType">float</span> x = step(pos.x, _MaxX) * step(_MinX, pos.x); <span class="synType">float</span> y = step(pos.y, _MaxY) * step(_MinY, pos.y); <span class="synType">float</span> z = step(pos.z, _MaxZ) * step(_MinZ, pos.z); <span class="synStatement">return</span> tex3D(_Volume, pos).r * (x * y * z); } <span class="synType">inline</span> float4 transferFunction(<span class="synType">float</span> t) { <span class="synStatement">return</span> tex2D(_Transfer, float2(t, <span class="synConstant">0</span>)); } v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.localPos = v.vertex; o.worldPos = mul(unity_ObjectToWorld, v.vertex); <span class="synStatement">return</span> o; } float4 frag(v2f i) : SV_Target { float3 worldDir = i.worldPos - _WorldSpaceCameraPos; float3 localDir = normalize(mul(unity_WorldToObject, worldDir)); Ray ray; ray.from = i.localPos; ray.dir = localDir; intersection(ray); <span class="synType">int</span> n = _Iteration * ray.tmax / sqrt(<span class="synConstant">3</span>); float3 localStep = localDir * ray.tmax / n; float3 localPos = i.localPos; float4 output = <span class="synConstant">0</span>; [loop] <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i &lt; n; ++i) { <span class="synType">float</span> volume = sampleVolume(localPos + <span class="synConstant">0.5</span>); float4 color = transferFunction(volume) * volume * _Intensity; output += (<span class="synConstant">1.0</span> - output.a) * color; localPos += localStep; } <span class="synStatement">return</span> output; } ENDCG SubShader { Tags { <span class="synConstant">&quot;Queue&quot;</span> = <span class="synConstant">&quot;Transparent&quot;</span> <span class="synConstant">&quot;RenderType&quot;</span> = <span class="synConstant">&quot;Transparent&quot;</span> } Pass { Cull Back ZWrite Off ZTest LEqual Blend [_BlendSrc] [_BlendDst] Lighting Off CGPROGRAM <span class="synPreProc">#pragma vertex vert</span> <span class="synPreProc">#pragma fragment frag</span> ENDCG } } } </pre> <p>これで Transfer Function で色を設定すると狙った密度の場所に指定した色が付く形になります。詳しくはデモプロジェクトをダウンロードしてご参照ください。</p> <h2>おわりに</h2> <p>密度を表す <code>Texture3D</code> が作れさえすれば形式はどんなものでも同じ用に描画出来ます。そのうちマーチングキューブによるメッシュ化の方も試してみたいです。</p> hecomi