UnityアプリをRiftからGo/Questへ移植する人向けのTips集

こんにちは。株式会社Synamonでエンジニアをしております、岡村(@Sokuhatiku)と申します。

ついに本日5/21に、Oculus Questが発売されましたね!弊社にもまだ届いておりません!2019年6月2日追記:先週届きました!!

Questはスタンドアロンデバイスでありながら、デスクトップPCを要求するOculus Riftと同等レベルの操作が可能なデバイスです。VRを始めるための初期投資も少なく使う手間も少ないことから、多くの人がQuestを購入することを予測し、新たなユーザーを迎え入れるためにRiftで展開しているコンテンツをOculus Questに移植したい!という気持ちになったり、そういう要望を受ける方も多い事かと思います。幸いOculus Quest実機が無くてもGoと全く同じ環境で開発は出来るので、作業自体は今からでも始められるのですが、問題になってくるのが、Androidの仕様や、処理速度です。 デスクトップとスタンドアロンという厳しい制約を乗り越えるため、Unity及びOculusは様々な機能を提供してくれていますが、それらを正しく扱うためにはまだまだ不具合も多く、日本語のドキュメントも少ないように感じます。

本記事ではOculus Rift向けのUnityアプリケーションをGo/Questに移植する際の、アセットにあまり手を入れずに出来る負荷削減を中心としたTipsおよび、自分がそれを実践した際に踏み抜いた地雷から得た知見をまとめてみました。

Unityのバージョンは2018.4を前提としています。

続きを読む

PUN Classic → PUN 2アップデート手順覚書

こんにちは。株式会社Synamonのエンジニアの岡村(@Sokuhatiku)です。

今年の1月頃に、Gotanda.unity#10 というイベントがあったのですが、そちらに初登壇LT枠で登壇させて頂き、PUN2をお勧めして参りました。

最近、ようやく弊社内でとあるプロジェクトのPUN(PUN Classic)→ PUN2へのアップデートを決行しました。今回はその際の手順や詰まったポイントを覚書しておきます。

移行のメリット

以下のLT資料をご覧ください。 learning.unity3d.jp

資料の要点は、

  • プロジェクトがフォルダ構造、名前空間、アセンブリ的に綺麗になる。
  • APIが洗練される。

です。

(注意)移行するかどうかの判断

既存のプロジェクトをPUN2にアップデートする利点は、現時点では上記のみです。新しい機能が増えたり、性能が大きく向上することはありません。PUN(PUN Classic)のサポートは今後も継続して行われるので*1、すぐに使えなくなるという事もありません。プロジェクトによってはアップデートコストの方が高くついてしまうため、アップデートを行うかどうかは慎重に判断してください。

(新規プロジェクトを立ち上げるのであればPUN2にした方がよいと思います。)

今回作業したプロジェクトは、長期の運用を前提としていたため、アップデートに踏み切りました。

移行に適している環境

PUN→PUN2では名前空間の変更、コールバックの変更など、様々な破壊的変更が含まれています。既存のソースのPUNに対する依存は出来るだけ少ないほうが良いでしょう。

  • PUNに対する直接の依存を避けている環境
  • コード量が少ない環境

逆に、

  • PUNをソースコード中のあちこちで直接触っている環境
  • 特に、Photon.PunBehaviourを使い、PunBehaviourが提供するコールバックも利用している環境

は辛いです。PUNへの参照が増えるほどソースコードの修正が辛くなるので覚悟しましょう。

弊社では

弊社でPhotonを使っているプロダクトは、ネットワーク接続時以外のほぼ全てのAPIを一段階ラップして使用していました。

これは過去に他のネットワークエンジンへの置き換えを検討していた為であるのですが、今回それのお陰でPUN→PUN2の移行も楽になり、思いがけぬ副産物でした。


それでは、以下より実際の作業の流れを追って説明していきます。

作業前には必ずgitにコミット等、バックアップを取ってください。

作業環境

  • Unity 2018.3.11f1
  • PUN 1.95 → 2.9
  • PUN Voice 1.18.1 → 2.5.1

※ 全て2019年4月8日時点の最新版。

ステップ

  • PUNの削除
  • PUN2のインポート
  • プロジェクトの修正

(注意)PUNのアセットを編集して使っている場合

以降の作業に入る前に、編集して使っているアセットを洗い出すことをお勧めします。大事なアセットがある場合は、事前にPhotonとは関係のない場所に退避、もしくは編集した項目をどこかに控えておきましょう。

編集されがちなアセット(独断)

  • Assets/Photon Unity Networking/Resources/PhotonServerSettings.asset
    • appID等、サーバーへの接続設定
  • Assets/Photon Unity Networking/Plugins/PhotonNetwork/CustomTypes.cs
    • 独自型をPhotonで送受信する為のシリアライズ、デシリアライズ処理
  • Assets/Photon Unity Networking/Demos/*
    • PUNのサンプルアセット
  • Assets/PUNVoice/DemoVoice/*
    • PUN Voiceのサンプルアセット
  • Assets/Plugins/PhotonVoiceApi/Platforms/Unity/MicWrapper.cs
    • Microphoneクラスの競合回避

また、PUN2のインポート時に、退避したはずのアセットが更新されてしまう事があります。PUN2のアセットは全てAssets/Photon以下にインストールされます。インポート画面上でそれ以外の場所のファイルを更新しようとしているのに気づいたら、ファイルの更新チェックを外して下さい。

f:id:Sokuhatiku:20190410163239p:plain
Photonフォルダの外にあるファイルは全てチェックを外す
ただし、この場合、該当アセットのAPI変更に対する対応は自力で行う必要があります。

PUNの削除

Assets直下で削除すべきフォルダは以下の3つです。

  • Assets/Photon Unity Networking
  • Assets/PhotonChatApi
  • Assets/PUNVoice

Pluginsフォルダ内のアセットの削除

PUN Classic及び、PUN VoiceはAssets/Pluginsフォルダを使用しています。昔はライブラリファイルをこのフォルダ以下に置く必要があったのですが、現在のUnityのバージョンでは不要となっています。PUN2では一切Pluginsフォルダを使用しませんので、中に入っているPUNのアセットを全て削除してしまいましょう。

Pulginsフォルダに含まれているdllファイルは、Unityに読み込まれている間は削除できません。UnityEditorを一旦終了してから作業しましょう。

Pluginsフォルダ内における、PUN及びPUN Voiceが追加するファイルは以下の通りです。

(*が付いているものはPUNが追加したファイル。それ以外はPUN Voice。)

Plugins
│  opus-COPYING
│  *Photon3Unity3D.dll
│  *Photon3Unity3D.xml
│  *release_history.txt
│  
├─Android
│  │  
│  └─libs
│      │  audioinaec.aar
│      │  
│      ├─arm64-v8a
│      │      libopus_egpv.so
│      │      libwebrtc-audio.so
│      │      
│      ├─armeabi-v7a
│      │      libopus_egpv.so
│      │      
│      └─x86
│              libopus_egpv.so
│              
├─AudioIn.bundle
│  │  
│  └─Contents
│      │  Info.plist
│      │  
│      ├─MacOS
│      │      AudioIn
│      │      
│      └─_CodeSignature
│              CodeResources
│              
├─iOS
│  │  libopus_egpv.a
│  │  
│  └─Photon
│          AudioIn.h
│          AudioIn.mm
│          ForceToSpeaker.h
│          ForceToSpeaker.m
│          iOSStub.c
│          
├─libspeexdsp.bundle
│  │  build.txt
│  │  
│  └─Contents
│      │  Info.plist
│      │  
│      └─MacOS
│              libspeexdsp
│              
├─Metro
│      *Photon3Unity3D.dll
│      *Photon3Unity3D.pri
│      
├─opus_egpv.bundle
│  │  
│  └─Contents
│      │  Info.plist
│      │  
│      └─MacOS
│              opus_egpv
│              
├─PhotonLoadbalancingApi
│      Extensions.cs
│      FriendInfo.cs
│      LoadBalancingClient.cs
│      LoadBalancingPeer.cs
│      PhotonPing.cs
│      Player.cs
│      Room.cs
│      RoomInfo.cs
│      WebRpc.cs
│      
├─PhotonVoiceApi
│  │  LoadBalancingFrontend.cs
│  │  
│  ├─Core
│  │  │  AudioUtil.cs
│  │  │  ObjectPool.cs
│  │  │  OpusCodec.cs
│  │  │  SpeexProcessor.cs
│  │  │  Voice.cs
│  │  │  VoiceAudio.cs
│  │  │  VoiceClient.cs
│  │  │  VoiceCodec.cs
│  │  │  VoiceFramed.cs
│  │  │  VoiceInfo.cs
│  │  │  VoiceSourceAdapter.cs
│  │  │  
│  │  └─POpusCodec
│  │      │  OpusDecoder.cs
│  │      │  OpusEncoder.cs
│  │      │  OpusException.cs
│  │      │  OpusWrapper.cs
│  │      │  
│  │      └─Enums
│  │              Bandwidth.cs
│  │              Channels.cs
│  │              Complexity.cs
│  │              Delay.cs
│  │              ForceChannels.cs
│  │              OpusApplicationType.cs
│  │              OpusCtlGetRequest.cs
│  │              OpusCtlSetRequest.cs
│  │              OpusStatusCode.cs
│  │              SamplingRate.cs
│  │              SignalHint.cs
│  │              
│  └─Platforms
│      │  
│      └─Unity
│              AndroidAudioInAEC.cs
│              AppleAudioInPusher.cs
│              AppleAudioInReader.cs
│              AudioClipWrapper.cs
│              AudioInEnumerator.cs
│              AudioOutCapture.cs
│              AudioStreamPlayer.cs
│              IOSAudioForceToSpeaker.cs
│              MicWrapper.cs
│              SpeexDSP.cs
│              WindowsAudioIn.cs
│              
├─WebSocket
│      *websocket-sharp.dll
│      *websocket-sharp.README
│      *WebSocket.cs
│      *WebSocket.jslib
│      
├─WSA
│  │  
│  ├─ARM
│  │      opus_egpv.dll
│  │      
│  ├─x64
│  │      opus_egpv.dll
│  │      
│  └─x86
│          opus_egpv.dll
│          
├─x86
│      AudioIn.dll
│      libopus_egpv.so
│      opus_egpv.dll
│      
└─x86_64
        AudioIn.dll
        libopus_egpv.so
        libspeexdsp.dll
        opus_egpv.dll
        

PUN2のインポート

削除が終わったら、UnityEditorを立ち上げ、PUN2をインポートします。

Photonのスクリプトが全て無くなった為、コンパイルエラーが多数出ているかと思いますが、いったん無視します。AssetStoreからPUN2及びPhoton Voice2のパッケージをダウンロード、インポートしましょう。

プロジェクトの修正

コンパイルエラーの修正及びAPI変更への対応を行います。 修正内容は多岐に渡りますので、基本的な修正内容は公式の移行ガイドを参照してください。ここでは、移行作業で特に引っかかった箇所を紹介します。

PhotonNetwork.ConnectUsingSettings()が引数を取らなくなった

この関数はPhotonServerSettingsで設定された内容を利用してPhotonへの接続を行うものです。PUNでは第一引数でGameVersionを指定する必要があるのですが、PUN2では引数を受け取らないよう変更されました。GameVersionの指定もPhotonServerSettings上で行うよう変更されています。

ちなみに、PhotonNetwork.GameVersionというプロパティがあり、サーバー接続時のGameVersionにはこの値が使われるのですが、ConnectUsingSettings()関数の実行時、PhotonServerSettings内の値で上書きされてしまうことに注意して下さい。ここを間違えると、別のバージョンに設定したつもりでも同じ部屋に繋がるようになってしまいます。簡単な対策としては、接続前にPhotonNetwork.PhotonServerSettings.AppSettings.AppVersionを書き換えるようにすると良いようです。

各種拡張メソッドがグローバル名前空間から消えた

PUNは拡張メソッドをいくつか提供しています。それらの関数は名前空間で区切られておらず、PUNに依存していないはずのコードが知らず知らずのうちに依存していた……!という事態が起こりやすくなっていました。PUN2では、これらはPhoton.Pun及びPhoton.Realtime名前空間内に整理され、移動しました。依存していたコードはエラーになってしまいます。

Photon.Pun名前空間に移動した拡張メソッド

  • ParameterInfo[] GetCachedParemeters(this MethodInfo mo)
  • PhotonView[] GetPhotonViewsInChildren(this UnityEngine.GameObject go)
  • PhotonView GetPhotonView(this UnityEngine.GameObject go)
  • bool AlmostEquals(this Vector3 target, Vector3 second, float sqrMagnitudePrecision)
  • bool AlmostEquals(this Vector2 target, Vector2 second, float sqrMagnitudePrecision)
  • bool AlmostEquals(this Quaternion target, Quaternion second, float maxAngle)
  • bool AlmostEquals(this float target, float second, float floatDiff)

Photon.Realtime名前空間に移動した拡張メソッド

  • void Merge(this IDictionary target, IDictionary addHash)
  • void MergeStringKeys(this IDictionary target, IDictionary addHash)
  • string ToStringFull(this IDictionary origin)
  • string ToStringFull(this object[] data)
  • Hashtable StripToStringKeys(this IDictionary original)
  • void StripKeysWithNullValues(this IDictionary original)
  • bool Contains(this int[] target, int nr)

廃止された拡張メソッド

  • bool GetActive(this GameObject target)

元からPhotonへの参照を持っているべき箇所であれば、名前空間とアセンブリ参照を足してやることで復活させる事が出来ます。そうでない場合は自分たちの名前空間内に同じメソッドを定義してやることで修正すると良いかと思います。

以上

PUN→PUN2のアップデート手順の覚え書きでした。今回アップデートしたプロジェクトは元々PUNへの依存が少なかった為比較的楽に移行出来ました。他のプロジェクトも同じように移行できるか分かりませんが、少しでも参考になれば幸いです。

LipSyncをDIYする(後編)

 こんにちは。

 株式会社Synamonでエンジニアをしております、渡辺(@mochi_neko_7)と申します。 

 

 本記事は

 

synamon.hatenablog.com

 

の後編になります。

 まずは確認も含めて簡単に前編のおさらいをしましょう。

 

前編のおさらい

 リップシンクをするにあたって、音声データを解析してその母音を推定して口を動かそうというのが今回紹介する手法の大きな流れになります。

 母音を解析するための手法はフォルマント解析と呼ばれ、今回は次のような流れで音声データの処理を行います。

 

  1. 音声データの取得
  2. 必要なデータの抽出
  3. 正規化
  4. 高域強調(pre-emphasis)
  5. ハミング窓にかける
  6. 自己相関関数からLPC係数と残差分散を計算
  7. LPC係数と残差分散をデジタルフィルタにかけてスペクトル包絡線を計算
  8. ピークの抽出
  9. 母音の推定

 

 前編では1から6までの処理で、実際の音声データから、いくつかの前処理ののち、LPC係数 a [ i ] と残差分散 E [ i ] が得られました。

 

後編はフォルマント解析の手順の7からになります。

 

 

7. デジタルフィルタ

 LPCによって得られたLPC係数と残差分散から、実際のスペクトルの包絡線を求めなくてはなりません。

 そこで用いるのが、デジタルフィルタと呼ばれるものです。

 正直、著者はこのデジタルフィルタとは縁がなかったので、結果やるべきことを述べるだけになってしまうことをご容赦ください。

 

 まずは、Z変換と呼ばれる変換があります。

  \displaystyle X [ z ] = \sum_{n = 1}^{N} z^{-n} x [ n ]  

 ここで、 Xが出力、 zが変換を特徴づける複素数、 x [ n ] が入力、 Tが入力データの長さになります。

 この zとして z = e^{i \omega}を用いる際には、よく知られた離散フーリエ変換(DFT)の変換の式

   \displaystyle X [ \omega ] = \sum_{t = 1}^{T} e^{- i \omega t} x [ t ]

に帰着します。

 

 デジタルフィルタにはいくつも種類があるようなのですが、Z変換を用いたデジタルフィルタとして、次のようなものが知られています。

  \displaystyle H [ z ] = \frac{A [ z ]}{B [ z ]}

   \displaystyle A [ z ] =   \sum_{n = 0}^{N} z^{-n} a [ n ] \ , \  B [ z ] =   \sum_{m = 0}^{M} z^{-m} b [ m ] \ , \ b [ 0 ] = 1

 ここで、 a bが二種類の入力になります。

 これを、 aとして残差分散 E [ i ]  bとしてLPC係数 a [ i ]  zとして z = e^{i \omega}を用いて、各 \omegaに対して計算することによって、目的の周波数の応答が得られるようです。*1

 aは係数なので無次元、 Eが振幅なので長さの次元、出力は周波数なので長さの逆次元となり、丁度次元的には合っているのは分かります。

 

(2018/9/17追記)

これってよく見たら分母分子それぞれただのフーリエ変換ですね。

するとFFTなどを用いた方が早いかもしれません。

 

 フーリエ変換を紹介したところでも述べましたが、得られるスペクトルの出力は一般には複素数です。

 ですので情報としては、絶対値(振幅) |z|と位相 \thetaの二つがあります。

  \displaystyle z = |z| e^{ i \theta}

 今回の解析では位相の情報は必要ありませんので、フィルタにかけた出力の絶対値のみ取り出します。

 したがって最終的には、残差分散 E [ i ] とLPC係数 a [ i ] を用いて、次を計算します。

  \displaystyle |H [ \omega ] | = \left| \frac{ \sum_{t = 0}^{T - 1} e^{ - i \omega t} E [ t ]  }{  \sum_{t = 0}^{T - 1} e^{ - i \omega t} a [ t ]  } \right|

 

*複素数に馴染みのない方のために、絶対値 | \ | の計算のためには、直接複素数を計算するよりは、有名なEulerの関係式

  \displaystyle e^{i \theta} = \cos \theta + i \sin \theta

 を用いて、

  \displaystyle |z| = \sqrt{ Re(z)^2 + Im(z)^2 } 

 のように自分で計算しても構いません。

 Re(z)は実部、つまり e^{i \theta} cos \thetaに置き換えて和を取ったもので、  Im(z)は虚部、つまり e^{i \theta} sin \thetaに置き換えて和をとったものになります。

 これを各周波数で、分母分子でそれぞれ和を計算して比をとればよいです。

 

 実際にこれまでの処理の後の音声のスペクトル包絡線を描いてみると、

       f:id:mochinekos:20180909134326p:plain

のようになだらかな曲線が得られます。(先ほど載せたLPC次数50のものと同じです)

 始めのFFTによるスペクトルと比較してみると分かりやすのではないでしょうか?

 このはじめの2つのピークが、目的の第1・第2フォルマント周波数になります。

 

 

8. ピーク抽出

 これで周波数の分布が得られましたが、フォルマント解析ではそのピークに着目します。

 周波数の小さいものから順に、第一フォルマント周波数(f1)、第二フォルマント周波数(f2)、...のように呼ばれます。

 母音を推定するには、第二フォルマントまで取ればいいようです。*2*3

  少し上でも触れましたが、得られる周波数分布は配列のインデックス毎で、その解像度は実際に用いるサンプル数によって決まりました。

 すると単純にピークの値を取るだけでは、その精度は完全にその解像度で決まってしまいます。

 ですが実際にはその解像度をそこまで良くするのは負荷的にも苦しい場合が多いので、ここで少し工夫をしましょう。*4

  

 まずは、低解像度で得られるピーク位置に対して、それに隣り合う区間内に本当のピーク位置があると仮定します。

 そして得られたピーク位置とその両端の位置を全て通る放物線を仮定し、その放物線のピークの位置で真のピーク位置を推定します。

 

 放物線の式(二次関数)を思い出すと、例えば

  \displaystyle y = a x^2 + b x + c

  \displaystyle \ \ = a (x - p)^2 + q \ , \ p = - \frac{b}{2a} \ , \ q = c - \frac{b^2}{4a}

のように xを横軸、 yを縦軸の座標として書いた場合には、平方完成した (p, q)が放物線のピークの座標に対応します。

 未知の定数は a, b, cの3つありますので、放物線を通る3点の座標が分かっていればこれらの定数が決まります。*5 

 

 実際の計算では、 x軸に対応する周波数の幅が固定なので少し簡略化できます。

 元のピークの x座標を0に取り、その両端の x座標を-1、1とすると、

  y[-1] = a - b + c

  y[0] = c

  y[1] = a + b + c

のような関係から a, b, cおよび p, qが容易に求まります。

  \displaystyle c = y[0]

  \displaystyle a = \frac{y[1] + y[-1]}{2} - c

  \displaystyle b = \frac{y[1] - y[-1]}{2}

最後に得られた pはこの設定の場合には周波数の幅に対する比に対応しますので、始めに得られたピーク \omega_{peak}から、周波数解像度 \delta \omegaを用いて

  \hat{\omega}_{peak} = \omega_{peak} + p \delta \omega  

のように、ピーク位置の周波数を推定します。 

 

 

f:id:mochinekos:20180913180234p:plain

 

 

 このようにして、ピーク位置を推定しつつ、周波数の小さいところから二つ抽出してフォルマント周波数とします。

 

*場合によっては周波数が0の辺りにピーク(第0フォルマント)ができてしまう場合がありますが、基本的にはそれは無視して構わないと思います。*6

 

 

9. 母音の推定

  いよいよ2つのフォルマント周波数が得られましたので、これから母音を推定します。

  まず人の母音の周波数には、次の図のような傾向があるとされています。

 

f:id:mochinekos:20180915134847p:plain

 

引用元:

http://www.sps.sie.dendai.ac.jp/blog/wp-content/uploads/2008/04/kiso_b.pdf

 

 

 性別によって少し分布が平行にずれますが、基本的には同じような構造をもっていると知られています。

 そのような構造を持つ理由は、人の発声のメカニズムにあるようです。

 声はまず声帯で振動が発生しますが、喉、口腔、鼻、唇などでその音は共鳴します。

 特に母音は口の形で特徴付けられますので、口の形による共鳴が特徴的なスペクトルとなり、フォルマント周波数として観測できるということです。

 従って、得られらフォルマント周波数と分布を比較して、その音声がどの母音に属するのか判定すればよいです。

 

 理想的には、大勢の声をサンプリングして普遍的な分布を抽出して用いるのがよいと思われます。

 現実的には難しい場合もありますので、妥協策としては個人の特性に合わせて調整(キャリブレーション)するのも考えられます。

 経験的には、個人なら傾向が分かりやすいのでそれほど難しくはないと思います。 

 一番愚直な判定方法としては、各母音の集合の重心を決め、その中から最も近い母音と推定する方法が考えられます。

 より適切な分布の用い方、判定方法もあるのではと思いますが、今回は時間の都合でそこまであれこれ試したりはできませんでした。

 

 

フォルマント解析の手法に関する解説は 以上になります。

 

 

負荷軽減

 音声はデータ数が多く、特に周波数解析をするための変換は二次元的な計算量になることが多いので、負荷が高くなりがちです。

 特にVRに使いたい場合には、フレームレートの低下はVR酔いの原因となりますので、処理が重すぎる場合には採用するのは難しくなります。

 筆者が最初に書いた分かりやすさ重視のコードでは、サンプル数1024で、ムラはありますがだいたい毎フレーム13ms程度の負荷でした。*7

 当然他のスクリプトやグラフィックの処理の負荷も実際にはありますし、これでは到底使いものになりません。

 そこで、最適化で負荷が軽減できないか調べてみることにします。

 

 具体的な検討をする前に、まずはどのような処理が負荷が大きいのか把握しておく必要があります。

 例えば今回のケースでは、

  1. 音声データそのものが大きい配列である
  2. 配列全体に渡る処理が多い
  3. 特に周波数に変換する際には、おおよそ二次元的な反復処理が必要
  4. 単体の計算負荷が重いものもある(コサインや平方根など)

などが考えられます。

 

 まずは一番素直に、サンプル数が多いのではと疑うことができます。

 Unityのデフォルトの1フレームでの1チャンネルあたりのサンプル数が1024でした。

 これを半分の512に減らしてみると、おおよそ2/3程度の負荷になりました。*8

 ただし、上で説明しましたがサンプル数と周波数解像度はトレードオフの関係にあります。

 サンプル数を減らす場合は、実際の母音の判定に影響の出ない範囲で行ってください。

 

 大きな最適化としては、計算アルゴリズム自体の検討が考えられます。

 例えばフォルマント周波数を求める方法はいくつかありますが、周波数に変換する処理はかなり負荷が大きいので、できるだけ少ない回数にするか、高速に計算できるアルゴリズムを用いるべきです。

 既に知られている方法を用いるのであればいいのですが、新たなアルゴリズムを作るのはさすがに大変になります。

 今回紹介したLPCを用いた方法は比較的処理の少ないアルゴリズムのはずです。*9

 

 次に考えられるのは、処理の並列化です。

 音声データはベクトルであり、周波数への変換はおおよそ行列的な計算になりますので、for文などによる反復処理の負荷が大部分を占めます。

 そこで、反復処理をスレッド分割するということが考えられます。

 簡単に実装するなら、例えばC#ならfor文をSystem.Threading.Tasks.Parallel.Forに置き換えるなどができます。

 他にもUnityでの実装は、最近追加されたJobSystemを用いる方法や、Compote Shaderを使う方法も考えられます。

 ただし注意しなければならないのは、全部を全部並列化すればいいという訳ではないということです。

 並列化はその性質上、反復する処理がそれぞれ独立しているのが望ましいです。

 同一の変数にアクセスする際などには注意する必要があります。

 並列化の際の注意点は広く知られていると思いますので、詳細はその手の参考書などを読んでいただければと思います。

 筆者がParallel.Forを用いて並列化したところでは、13msから4ms程度まで負荷を軽減することができました。

 

 ここから更に最適化をしたい場合には、計算回数をできるだけ減らしたり、個々の処理をできるだけ軽くするなど、細かな調整をすることになります。

 筆者はあまり詳しくはないのですが、コンパイラやアセンブリのレベルで負荷を減らすなどの工夫はできると思います。*10

 要求されるパフォーマンス次第で工夫して頂ければと思います。

 

 あるいは最適化の上で負荷が現実的ではない場合には、音声解析的なアプローチを断念しなければならないこともあるかもしれません。

 だいぶクオリティは落ちますが、例えば音声の音量のみ観測してそれっぽく口の形を動かすだけというのも可能です。

 そのあたりも優先順位次第では検討しなければならない場合も出てくると思います。

 最新版のNEUTRANSではマルチプレイとネットワークの都合から、β版として簡易版のリップシンク実装と、併せてアバターの目の瞬きも実装しています。*11

 

 

課題

 最後に、今後の課題をいくつか挙げます。

  1. 母音の推定のアルゴリズムの研究が不十分
  2. まだ最適化が十分でない(依然負荷は高い)
  3. マルチプレイ・ネットワークの対応が不十分

 

 音声の解析そのものはそこそこ安定はしたのですが、母音の判定方法の工夫をする時間が取れませんでした。

 フォルマント分布図の用意の仕方、判定方法、機械学習的なアプローチなど、まだまだやれることはありそうです。

 

 最適化も当初に比べたら大きく軽減はできましたが、他の処理と比較すると圧倒的に負荷は高いです。

 UnityのJobSystemもまだ試していませんし、逆アセンブリなどの詳細な解析もまだなので、最適化の余地はあると思います。

 

 マルチプレイ環境下で遅延なしに解析を行うなら、他人の音声の解析までしている余裕はありません。

 実際の音声の発音に遅延しないようにするには、音声の送受信のタイミングや順序に気を付ける必要があります。

 

 最終的にはこれらの課題を解決しつつ、本格的なリップシンクをネットワーク・マルチプレイ環境下で提供できればと思っています。

 

 

最後に

 かなり長くなってしまいましたが、いかがだったでしょうか。

 単に必要な処理を述べるのではなく、その意味を理解することに重点を置いて解説したつもりです。

 実際の実装をするにあたって知っておくべきこと、注意すべきことをできるだけ詳細に述べたつもりですが、至らない点もあるかもしれません。

 よくわからない部分や、間違っている個所など、気になるところがありましたらお気軽にご連絡していただければと思います。

 最後まで読んで頂きありがとうございました。 

 

 

*おまけ

 Unityでマイクの音声を取得する際、UnityEngine.Microphoneクラスを利用するのがお手軽ですが、いくつか注意点があります。

 ネットで指摘しているものを見かけなかったので、参考になれば幸いです。

 AudioClipはstaticメソッドのMicrophone.Start(...)によって取得できるのですが、既に他所でこれを利用している場合には、古いほうのAudioClipの内容が更新されなくなってしまいます。

 結果として古い音声がループ再生されるなどの現象を引き起こす場合があります。

 例えばPhotonVoiceを利用しているのであれば、注意が要ります。

 回避策としては、マイク音源を複数が利用できるよう代理でキャッシュするクラスを作成し、Microphoneクラスを直接用いないようにすればよいです。

 ただしその場合にはPhotonVoice側の実装に手を入れる必要があり、バージョンの更新の度に修正しなければならない恐れがありますので注意してください。

 かといってマイクを利用するクラスたちが、わざわざPhotonVoiceなどの特定のものに音声を聞きにいくのもかなり不自然ですので悩ましいです。

 

 

(2018/9/17)誤字修正

 

参考

[1] 「音声情報処理」古川貞煕(森北出版株式会社)

[2] 「実験音声学のための音声分析」平坂文男(関東学院大学出版会) 

 

 http://www.sps.sie.dendai.ac.jp/blog/wp-content/uploads/2008/04/kiso_b.pdf

 

*1:zの選び方はともかく、なぜこれでよいのか正直よく理解はできていません。そもそもデジタルフィルタ自体に馴染みがないからでしょうか?

*2:詳しくは知りませんが、子音を推定するときにはf3、f4まで見るようです

*3:あるいは多くとれば母音の推定の精度が上がると言っている人もいます(そこまで検証できていませんが)

*4:何も考えず得られるピークを用いてもそこまで悪くはないのですが

*5:三元の連立方程式ですね

*6:それを防ぐ手法もあるようです

*7:これだけで明らかに90fps = 11.1msを超えていますね...

*8:最適化後、UnityEditor上で測りました

*9:例えばケプストラムを用いる方法では、何度かフーリエ変換をする必要があります。

*10:その場合にはコードの可読性が犠牲になることも

*11:自分の声のみのリップシンクに限るなら問題はないレベルです

LipSyncをDIYする(前編)

 こんにちは。

 株式会社Synamonでエンジニアをしております、渡辺(@mochi_neko_7)と申します。 

 

 VRでは自分の好きなアバターになれるというのは大きな魅力の一つではないかと思いますが、せっかくのアバターも固い表情のままではちょっともったいないですよね。

 そこで今回はアバターに表情を付ける一つの方法として、プレイヤーの話している声からアバターの口を動かすリップシンクとよばれる手法についてご紹介したいと思います。

 今日では、リップシンクを手軽に利用できる環境もいくつかありますが、その中でこのお話をさせていただくのは、ブラックボックスをあまり使いたくないという想いと、パフォーマンスなどの要求から自分で実装したい場合があるのではないかと思ったからです。

 ネットにも技術的な部分を詳しく解説している記事があまり見つからず、筆者自身未知の領域で苦労した経験から、初心者の方にも分かりやすく説明できればと思います。

 

 本記事は、リップシンクを自分で実装したい、でも音声の専門知識があるわけではない、数学なら少しは分かるから何をやっているのか理解しながら実装したい、という趣旨でお話をします。

 前提となる知識ですが、簡単な音の知識*1に加えて、ちゃんと理解するのであれば線形代数、フーリエ変換の知識があるのが望ましいです。

 弊社の『NEUTRANS』はUnityで開発しておりますのでUnityの話にも少し触れますが、基本的な部分は開発環境・言語には依りません。

 

 なお今回の記事を書くにあたって、凹みさんの以下の記事も参考にさせていただきました。

tips.hecomi.com

 紹介する全てではありませんが、一部こちらにはC++のサンプルコードも掲載されていらっしゃいますので、併せて参考にしていただければと思います。 

 

 また、筆者は音声解析は門外漢ですので、間違ったことも多分に含まれるかもしれません。

 筆者の知識の範囲内で理解している部分しか説明できませんので、あまり詳しく触れていない部分もあります。

 もし間違いや不適切な部分にお気づきの方がいらっしゃいましたら、ご指摘いただけると幸いです。

 

 本記事は前編・後編の二章立てになります。

 少し前置きが長くなりましたが、それでは本題に入りたいと思います。

 

 

リップシンクとは

 リップシンクとは、人の話す口の形とアバターの口の形を同期(Synchronize)させようというものです。

 いわゆる”口パク”ですね。

 たとえばアニメや最近のバーチャルYoutuberなんかでも、セリフを喋っている時は口が動きますよね。

 口が動いているのといないのでは、そのキャラの存在感が大きく変わります。

 セリフが決まっているならそれに合わせて動かせばいいだけですが、リアルタイムの音声に動きを合わせようとすると、どうしても音声データを解析する必要が出てきます。

 特にリアルタイム性の強い生配信やVRでは、これをリアルタイムに行わなくてはなしません。

 そこで、どのようにその音声データを解析して、話している口の形を調べるのか、という話になります。

 今回紹介するのは、そのようなアプローチの一つになります。

 

 

解析手法

 最もシンプルなリップシンクは、口の形を決める大きな要因である母音を知りたいというところから始まります。

 そこで用いられるのがフォルマント(母音)解析と呼ばれるものです。

フォルマント解析にもいくつか手法がありますが、ここでは線形予測器(LPC)を用いる方法を採用します。

 LPC係数からスペクトル包絡線を求め、そのピーク位置から第一フォルマント周波数(f1)と第二フォルマント周波数(f2)を決定し母音を推定します。

 スペクトル包絡線を用いるのは、要するにスペクトル分布をなだらかにしてその特徴を捉えようということです。

 このf1、f2の分布には、性別差はあれど大まかに母音毎の構造があるとされているので、これから母音を推定しようということです。

 

 今回紹介する具体的な手順は次のようになります。

  1. 音声データの取得
  2. 必要なデータの抽出
  3. 正規化
  4. 高域強調(pre-emphasis)
  5. ハミング窓にかける
  6. 自己相関関数からLPC係数と残差分散を計算
  7. LPC係数と残差分散をデジタルフィルタにかけてスペクトル包絡線を計算
  8. ピークの抽出
  9. 母音の推定

 大雑把に言えば、

・データの取得(1, 2)

・前処理(3, 4, 5)

・スペクトル分析(6, 7, 8)

・推定(9)

という流れです。

 

 

1.音声データの取得

 まずは音声データがどのようなものか少し確認したいと思います。

 Unityでは、AudioClipと呼ばれる形式で音声データは扱われます。

 保存したデータも、マイクからの入力でも一緒です。

 データ本体はAudioClip.GetDataで取得ができますが、中身は-1fから1fの範囲のfloat型の配列になります。

 floatの各値は音の振動の変位を表しており、その絶対値が振幅になります。

 配列のインデックス(整数)がチャンネル*2毎の時間*3に対応しています。

 インデックス毎の時間間隔はサンプリング周波数*4によって決まります。

 Unityでなくても、基本的には生の音声データは同じような形式と思われます。 

 

 デジタル化が少し適当ですが、ざっくり図にしてみるとこのような感じでしょうか。

f:id:mochinekos:20180913171041p:plain

 

 音声の解析をする際には、変位の信号としてだけではなく、これをフーリエ変換して周波数の分布を調べるスペクトル分析がよく行われます。

 離散化されたデータに対するフーリエ変換の高速なアルゴリズムとしては、FFTが有名ですね。

 時間に対するフーリエ変換なので、得られるのはインデックスが周波数を表す配列になります。*5

 一般にはこの出力の値は複素数ですが、絶対値をとることで各周波数の強度が得られます。

 今回紹介する手法では、デジタルフィルタと呼ばれる別の手法がこの周波数解析に対応しています。

 

 実際に解析をする際に音声データを取得する方法はいくつかありますが、例えばUnityでは、

  1. AudioClip.GetDataで直接
  2. AudioSource.GetOutputData
  3. OnAudioFilterReadイベントで受けとる
  4. マイクの音源をOSの機能で取得

などでしょうか。

 リアルタイムの音声はもちろん、予め記録しておいた音源でも解析自体は変わりません。

 

 

2.必要な音声データの抽出

 チャンネルは1つあれば十分ですので、複数チャンネルある音声データの場合には、まず1チャンネル分に限定しましょう。

 また、データのサンプルの数もあまり多いとその分解析の負荷が大きくなってしまうので、適宜必要な数に絞ります。

 

 ここで注意しなければならないのは、この抽出するサンプル数が周波数の解像度に関係している、ということです。

 例えば、元の音声データのサンプリング周波数が44.1kHzの時に、サンプル数を1024と選ぶとします。

 それから得られる周波数分布の配列の数はサンプル数と同じ1024で、その周波数領域はサンプリング周波数から0~44.1kHzになります。 

 したがって周波数解像度は、

  \Delta \omega = \omega_{sampling} / N

  \hspace{17pt} = 44.1 \text{kHz} / 1024 \sim 43.1 \text{Hz}

になります。 

 この解像度がフォルマント解析に十分なのか注意する必要があります。

 実際には各母音の構造のオーダーは小さいところで100-200Hz程度のようなので、この例だと許容範囲かなと思われます。

 

*今回の手法ではピーク抽出で少し補完をするので、実際にはこれより少し解像度が上がります。

 

 

3.正規化

 音声の入力の大きさはものによってまちまちですが、音声の解析では基本的にはその大きさのスケールはあまり影響しません。

 実際今回知りたいのは周波数の分布であって、大きさそのものではありません。

 極端に値が小さいと計算精度的にもあまりよろしくありません。

 そこで始めに-1~1の範囲に値が収まるようにリスケールします。

 具体的には、絶対値の一番大きい値ですべての信号を割ればいいだけです。

 

  弊社の社員の「あ」の声をサンプリングしてみると、

       f:id:mochinekos:20180909132056p:plain

のようになります。

 特徴的な周期性が見られますね。

 これをFFTしてスペクトルをUnityのLineRendererで描いて見てみると、

       f:id:mochinekos:20180909132402p:plain

少し線が見づらいですが、低周波部分に大きなピークがあるのが分かると思います。

 ちなみに今回お見せするスペクトルのグラフは全て、全体44.1kHzの1/10の部分を切り取ったものになりますので、横軸は最大4410Hz程度です。

 

 

4. 高域強調(pre-emphasis)

 音声解析では、実際の解析を始める前にいくつか前処理を行います。

 この高域強調はその一つです。

 その意味は高域(=高周波数領域)の成分を強調しようというものです。

 フォルマント解析では少し高域のピークも見る必要があるので、これらの大きさを相対的に大きくできると解析しやすくで嬉しいです。

 

 実際には次のように処理します。

  A'[t] = A[t] - a A[t - 1]

 ここで、 A[ \ ] が信号の配列(変位)、 tがインデックス(時間)、 aが定数係数で、これには1に近い0.95などの値が用いられるようです。

 直前の時刻の変位の値に近いものを差し引くので、時間に対して変化が緩やかな信号は小さくなります。

 つまり、周期の大きい(=周波数の小さい)成分が小さくなるので、結果として高域が強調されることになります。

 

 先ほどの「あ」の音声を実際に高域強調してみると、

       f:id:mochinekos:20180909133053p:plain

のようになります。

 直接音声の波形を見てもどうなったか分かりにくいので、これをFFTしてみると、

       f:id:mochinekos:20180909133313p:plain

 のようになりました。

 ピーク以外の周波数、特にピーク直後の部分と、少し離れた高周波数の部分の成分が強調されて分かりやすくなったのがお分かりかと思います。

 

 

5. ハミング窓

 周波数応答を得るにあたっては、基本的にはフーリエ変換(あるいはそれに相当する変換)を行う必要があります。

 特に有限区間のフーリエ変換では、区間外での周期性が仮定されています。

 そのため、信号の周期性(具体的には両端のデータの繋がり)が悪いと、得られるデータの性質が悪くなってしまいます。

 そこで行われるのが時間窓をかける処理です。

 今回はその中でも有名なハミング窓関数

  \displaystyle W[t] = 0.54 - 0.46 \cos \left( \frac{ 2 \pi t }{ T - 1 }  \right)  

を用いることにします。

 ここで、 tがインデックス、 Tがサンプル数になります。 

 この窓関数をかけるので、実際には

  A'[t] = W[ t ] A[ t ]

のような計算を行います。

 

 また今回も、先ほどの高域強調後の音声に対して、実際にハミング窓をかけてみてみましょう。

        f:id:mochinekos:20180909133745p:plain 

 このように両端がすぼんで、両端での繋がりが良くなりました。

 

 

6. LPC

 今回の手法のメインはこのLPC(Linear Predictive Coding:線形予測器)を用いる部分です。

 詳しい解説はこちら

aidiary.hatenablog.com

にもありますし、全部説明すると数式ばかりで長くなるので、細かな理論はこちらを参照して頂きつつ、ここでは概要と注意点を述べるにとどめます。

 

 まずLPCとは、過去の信号から未来の信号を推定する方法の一つです。

 具体的には次のような式を用います。

  \displaystyle \hat{ y } [ t ] = - \sum_{i = 1}^{k} a [ i ] y [ t - i ]

 ここで、 \hat{ y } [ t ] が時刻 tの信号の推定値、 y [ t - i ] が過去の時刻( t - i)での信号、 a [ i ] がLPC係数、 k がLPC次数になります。

 この推定値 \hat{ y } [ t ] と実際の値 y [ t ] との誤差が最小になるように、LPC係数 a [ i ] を選ぶのがこのLPCの課題になります。

 線形という名前なのは、過去の信号の線形和、つまり定数係数の一次の和になっているからですね。

 

 これを具体的に計算してみると、最終的にはYule-Walker方程式と呼ばれる形の行列方程式を解くことに帰着します。

 幸いなことに、この方程式を効率的に解くLevinson-Durbin再帰法と呼ばれるアルゴリズムが知られていますので、これを用いましょう。

 このような良いアルゴリズムが計算負荷を減らしてくれるのが、LPCを用いる上で嬉しい点です。

 詳しい計算方法は、先ほどのリンク先を参照していただければと思います。*6

 ただし、このアルゴリズムは逐次的なものですので、計算を並列化するのにはあまり向いていません。*7

 この手法によって実際の信号をLPCの推定にかけ、少しなだらかなスペクトルの包絡線を得ようということになります。

 

 ここで、まだ触れていないLPC次数 kに関して少し説明しましょう。

 先ほどのLPCの推定の式の和を見ていただけるとお分かりかと思いますが、LPC次数はどの程度過去までの信号を考慮するのか、といったものになります。

 これが小さすぎると、情報が少ないので大雑把な結果しか得られなくなり、逆に大きすぎると推定が強くなり実際のデータとの差が小さくなります*8

 今回欲しいのはあくまで大まかな傾向なので、このLPC次数をいい感じの値に設定する必要があります。

 参考文献[2]では、このLPC次数の目安はサンプリング周波数(kHz)+4、5程度と言われています。

 例えばサンプリング周波数が44.1kHzなら、 44 + 5 = 49程度でしょうか。

 目安なので、実際のピークの出方を見ながら微調整するのがよさそうですが、実験的には確かにこの程度の値がちょうどいい感じになりました。

 

 LPC次数を変えた結果を実際に比較して見てみましょう。

 LPC次数の目安に近い50では、最終的なスペクトル包絡線は、例えば

       f:id:mochinekos:20180909142623p:plain

のようになります。

 ここで次数を小さくして30にしてみると、

       f:id:mochinekos:20180909142715p:plain

 のように低周波での2つ目のピークが見えなくなってしまいます。

 逆に次数を大きくして100にしてみると、

       f:id:mochinekos:20180909143217p:plain

のように余分なピークが表れてしまいます。

 どのようなピークが欲しいのかによってLPC次数の決め方は変わってしまいます。

 これを全ての母音で、安定して欲しいフォルマント周波数が見えるよう次数を調整しなければなりません。

 何かしら基準がないとかなり迷ってしまうところですので、上で紹介した目安を参考にしていただけるといいかなと思います。

 

 こうして最終的には、LPC係数 a [ i ] と残差分散 E [ i ] が得られます。

 

 

 

 少し話が長くなってしまいましたので、中途半端ではありますが続きは後半でお話させていただきたいと思います。

 

 後編はこちらです。

 

synamon.hatenablog.com

 

 

参考

[1] 「音声情報処理」古川貞煕(森北出版株式会社) 

[2] 「実験音声学のための音声分析」平坂文男(関東学院大学出版会) 

 

 

*1:音の振幅や周波数などの意味や単位など

*2:例えばステレオだとLRの2チャンネルなど

*3:デジタルデータですので、当然離散化(量子化)されています

*4:一秒間に何回データを取得するか

*5:フーリエ変換では、時間<->周波数、位置<->運動量などのように変換します

*6:詳しい途中計算が知りたい方がいましたら載せますね

*7:並列化し易い方法をご存知の方がいらっしゃいましたら、教えていただけると嬉しいです

*8:当然その分計算コストが高くなりますし、今回の趣旨には合いません。

Unity×Verdaccioで自作UnityPackageをバージョン管理する

f:id:Sokuhatiku:20180815160400p:plainこんにちは、株式会社Synamonのエンジニアの岡村(@Sokuhatiku)です。

前回、任意のフォルダにPackage.jsonを定義することで、ローカルパス参照でUnityPackageManagerに載せることが出来る方法を書きました。

ただし、その方法はローカルのファイルパスを直接指定してパッケージとして扱うというもので、バージョン管理やチーム内での共有のしやすさに難がありました。

今回、その部分の解決策を見つけました。それと近いタイミングで、@kohki_nakajiさんに前回の記事を紹介して頂けて、さらに、今回の記事にも興味があると言って頂けたため、手順を纏めてみました。

おことわり

※今回はUnityとnpmの狭間に自作パッケージを無理やりねじ込む所に焦点を当てている為、LAN内に公開する方法には触れていません。ご了承ください。

※今回紹介する方法はハックです。公式サポートされている機能ではないので、今後情報が古くなったり、利用できなくなる可能性が大いにあります。

自作パッケージレジストリ

前回、UnityPackageManager(以下、upm)はnpmの仕組みを利用しているという所まで調査できていました。

upmとnpmのもつjsonを比べてみると非常によく似ていた為、npmのレジストリを立て、Nodeの代わりにUnityPackageを置けば、そこからダウンロード出来るのではないかという予想を立てました。

そこで、Verdaccioというツールを利用して、ローカル環境にnpmレジストリを立ててみたところ、いくつかハマりどころがあったものの、最終的には上手くいきました。 github.com

Verdaccioは、npmのレジストリを好きな場所に立てる事ができるツールです。また、uplinkとして他のレジストリを設定した場合、自身が持っていないパッケージをそちらからダウンロードすることが出来るようになります。

ハマりどころ

先にハマりどころを紹介しておきます。

upmはポート付きアドレスを認識しない

Verdaccioを立てると4873番ポートで起動するのですが、そのままlocalhost:4873をUnityにレジストリとして設定しても、パッケージの読み込みに失敗します。 簡単な解決方法は、verdaccio側に80番ポートで待受させることです。

upmはスコープを認識しない

npmには@{ユーザー名}/{名前}というパッケージ名にする事でパッケージ名の重複を防ぐ、スコープという機能がありますが、これをupmは認識しません。不正なパッケージ名として弾かれてしまいます。

VerdaccioのuplinkにUnityのレジストリを登録しないとプロジェクトオープンに失敗する

Unityはプロジェクトを開く際に、パッケージの問い合わせを行います。その際、Unity側のパッケージが取得できなければ、ロード失敗になります。

VerdaccioにUnityのレジストリを登録しているとパッケージのアップロードに失敗する

Verdaccioは、パッケージのアップロード時に既存のパッケージを確認するのですが、Unityのレジストリが返してくるjsonを解釈できないらしく、エラーが出てアップロードが止まってしまいます。 今回はその解決策として、unity公式のパッケージ名に必ず付いている、「com.unity」ドメインにのみuplinkを設定することで対処しています。(なので、com.unityでパッケージをアップロードしようとすると失敗します。)

Unity上のGUIに自作パッケージは出てこない

UnityのPackage Manager GUIのAllタブに表示されるパッケージ一覧はどうやら別の手段で取得しているらしく、自作パッケージをアップロードしてもリストに表示されません。インストールにはmanifest.jsonを弄る必要があります。

…さあ、これらを踏まえてレジストリを立てましょう。

レジストリを立てる

今回はnpmの事を知らない人向けに(僕もよく知らなかったので…)インストール方法から書いていこうと思います。環境はWindows10です。

Verdaccioはnpmからインストールしますので、まずnpmを入手する必要があります。npmはNode.jsのパッケージマネージャーであり、Node.jsをインストールすると付いてきます。

Node.js

Node.jsのインストールが終わったらnpmコマンドが使えるようになるので、コマンドプロンプトを開いてverdaccioをインストールします。

npm install -g verdaccio

npmの提供するアプリ(Node)はデフォルトだとコマンドを叩いたディレクトリにインストールされますが、今回は-g引数を使い、PCのどこからでも触れるようグローバルインストールを行います。

インストールが終わったら起動します。

verdaccio

f:id:Sokuhatiku:20180814211838p:plain

何も設定をいじっていない状態なので、localhost:4873でアクセス出来るはずです。 f:id:Sokuhatiku:20180815145236p:plain

設定の変更

Verdaccioを一度立ち上げたことで設定ファイルが作成されます。UnityPackageを配布するために、設定を変更しましょう。一度Verdaccioを落とします。

設定ファイルは、%APPDATA%\verdaccio\config.yamlに生成されているはずです。

ファイルを開き、次のように書き換えます。

Verdaccio for Unitypackage

(リビジョンでどのように書き換えたか分かります。)

完了したら、もう一度Verdaccioを起動しましょう。設定ファイルでポートを変更したので、今度はポート無しのlocalhostでアクセスできるようになります。

ユーザーの作成

次にアップロードするためのユーザーを作成します。

npm adduser --registry http://localhost/

User名、Password、Emailアドレスを順番に聞かれるので設定してください。このユーザー情報はVerdaccioのwebページにサインインするのに使うと共に、Verdaccioにパッケージをアップロードする際にも自動的に使用されます。

パッケージのアップロード

パッケージの作り方に関しては、前回の記事を参照してください。

Unity Package Managerに自作Packageを登録する方法 - Synamon’s Engineer blog

パッケージを作ったら、package.jsonのあるディレクトリに移動して、以下のコマンドを叩きます。

npm publish --registry "http://localhost/"

これでパッケージのアップロードは完了です。

インポート先のプロジェクトの設定

後は、作ったパッケージをインポートしたいプロジェクトの、Packages\manifest.jsonを設定します。

dependenciesに、"{パッケージ名}": "{パッケージのバージョン}"を追加して、dependenciesキーの後ろに"registry": "http://localhost/"を追加してください。

このようになるはずです(dependenciesの中身はプロジェクトによって異なります。)

{
  "dependencies": {
    "com.test.testpackage": "0.8.2",
    "com.unity.package-manager-ui": "1.9.11",
    "com.unity.postprocessing": "2.0.10-preview",
    "com.unity.progrids": "3.0.3-preview.0"
  },
  "registry": "http://localhost/"
}

"com.test.testpackage": "0.8.2"の部分を自作パッケージの名前とバージョンに変換してください。あと、"registry"の前のコンマを忘れずに!)

以上です。お疲れ様でした。これでUnityを起動すると、パッケージが読み込まれているはずです。

バージョンを変えて複数回アップロードすると、UI上からバージョン切替も出来ます。

ちなみにアップロードしたパッケージの削除は以下のコマンドです。

npm unpublish {パッケージ名} --force --registry "http://localhost/"

あとがき

今回、自作パッケージのバージョン管理や、バージョン名でインストール(=ローカルにファイルをコピーしてくる必要が無い)まで解決することが出来ましたが、まだ問題点があります。

個人的に大きいのはパッケージ間の依存関係を解決できない事です。パッケージ内のdependencyに他のパッケージを指定してもロードして貰えません。 流石にここはupmの内部挙動のため、外からハックして解決するのは難しいのでは、と感じています。

とはいえ、今回で、チーム内で前提アセットを配布+git管理するといった使い方はかなりやりやすくなったと思います。Unityの公式対応が待てない方は是非試してみてください。

また、上手くいかなかったり、もっといい方法があれば是非教えてください。

100万ポリゴンのCADデータを3000ポリゴンにリトポする

こんにちは。株式会社Synamonで開発責任者(CDO)をしている西口([@nishiguchi3d])です。

みなさんはVRでCADのデータを見たいと思ったことはありませんか?

Unityにインポートして表示させるだけなら大して難しくないですがやはり問題になるのがそのポリゴン数と質感付けの部分じゃないでしょうか?

今回は実際に使用したデータをもとにVRに適したデータを作成しUnityで表示するところまで解説してみたいと思います。 

 ポリゴン数を削る

こちらが今回使用するBECKHOFFのEPCのCADデータです。細部まで作り込んであるのでこの段階で1,150,374ポリゴンあります。

f:id:Synamon:20180727015713j:plain

 ポリゴン数もさることながらこのディティールの3角ポリゴンのデータをUV展開するのかと思うとゾッとしますね!なのでこれをリトポ(リトポロジー)して可能な限りポリゴン数を削減していきます。

そしてこれがリトポ後のデータ3,132ポリゴンです(画像は3角化した後のものです)

f:id:Synamon:20180727020157j:plain

リトポといいましたが本当はハイポリデータを下敷きにして全部1から作り直しました、形状的にそのほうが早く綺麗にできそうと判断したからです。シルエットが変わらないように大きいディティールはそのまま、テクスチャで表現できそうな小さい凸凹は大胆に削って再構成していくのがポイントです。

f:id:Synamon:20180728165054g:plain

中央に見えるくぼみは最初削った状態で作ったんですがVRに持っていって確認したらNormalマップだけでは穴が空いていないのがバレバレだったので空けました。

 

UVを展開します、3000ポリゴン程度ならUV展開も楽々ですね!

f:id:Synamon:20180728140041j:plain f:id:Synamon:20180728140335j:plain 

画像は3角ポリゴン化した後のものになりますが、実際には三角ポリゴン化する前のほうが連続面の選択やエッジの選択がやりやすいので開きやすいです。

もしリトポせずに3角ポリゴンで構成されたハイポリのモデルをUV展開するとなると大変です。自動展開ツール等あるので出来ないわけではないですが細かくUVを制御することができなくなるので後の工程で苦労するかもしれません。

テクスチャにディティールをベイクする

 UVも開いたのでハイポリのディティールをローポリモデルにベイクしていきます。

f:id:Synamon:20180728143130j:plain f:id:Synamon:20180728143149j:plain

まずはNormalマップ、MODOとSubstance Painterの2つで焼き比べてみましたが今回はMODOのNormalを採用しました。どうしてもうまく焼けない部分は設定を変えながら焼いた複数枚のマップをPhotoshopで合成してます。一発でうまく焼くのはなかなか難しいです。ちなみにオブジェクトを3角ポリゴンに変換せずにNormalをベイクしてUnityに持っていくと見た目が変わってしまったりするのでベイク前に3角ポリゴンに変換してから焼いてます。

f:id:Synamon:20180728143334j:plain f:id:Synamon:20180728143401j:plain

Ambient occlusion(AO)はSubstance Painterのものを採用しました、これも2つのソフトで焼いてみて綺麗だった方を選びました。今回のモデルは凹凸が多いのでAOが入ると立体感が出ていい感じですね。

 

あとはSubstance Painterで質感をつければ完成です。後はUnityに持っていってStandardShaderのマテリアルで設定して終了。

f:id:Synamon:20180728144142j:plain

f:id:Synamon:20180728144733j:plain

 上:Substance painter 下:Unity 

 まとめ

最近はオートリトポやプロシージャルの技術も進んできているのでもっと簡単に軽くて綺麗なデータを作れるようになるかもしれませんが、やはり主役となるモデルは一手間かけるとぐっと存在感が増すので、時間をかけて作り込むか簡単に済ませるかは登場するオブジェクトの重要度によって選択するといいと思います。

メッシュ情報を焼いたテクスチャを使ってTexturePaintする

こんにちは。株式会社Synamonでエンジニアとして『NEUTRANS』の開発をしている岡村(@Sokuhatiku)です。今回は、過去のイベント参加時に作った、立体的なメッシュに対して自然なテクスチャペイントを行う手法を紹介します。

Unity内で立体的なメッシュ(キャラクターなど)に対して動的にペイントする時、一般的には描画したい点のメッシュ情報を取得し、UV座標を使ってテクスチャに書き込むという方法が取られると思います。

その際に

  1. UVが重複していることにより他の部分にも同時に描画されてしまう
  2. UVの切れ目でペイントが途切れてしまう
  3. UVが伸びてペイントが歪んでしまう

といった、UVに起因するいくつかの問題が出てきます。

1に関しては、重複が起こらないようUV展開をすることで、手間をかければ解決することが可能です。しかし、立体物に対してペイントする場合、どうしてもメッシュの何処かにUVの切れ目や歪みが現れてしまい、2、3の問題が出てきてしまいます。

去年(2017年)開催された「例のカノジョ ハッカソン」というイベントに参加した際、例のカノジョや例の部屋をインク的なもので塗りたくる必要があったのですが、扇風機のような複雑なオブジェクトに対してペイントをしようとすると、UVの問題が顕著に出てしまいました。

続きを読む