
音に反応する AR マスクをサクッとつくる | Audio Reactive FaceMask with AR Foundation
※ 2020/8/1時点での動作検証になります.今後の Unity のバージョンアップなどにより再現できない可能性が点はご留意ください.
こんにちは.やっと梅雨が明けたようで,蝉も絶好調ですね.
1週前は早く梅雨明けてくれと項垂れていましたが,すでに夏の日差しに敗北しつつあり,さっさと秋にならないかなと思ったり・・
そういえば,1年前の夏に何をエントリーしていたか振り返ってみると,「」した.毎年,夏の到来後,早々に暑さに敗北している己はだいぶ虚弱です.数年後の夏とか,到来とともに死ぬのではないかと思ってしまう.
まあでも,夏に Active に外で遊ぶのではなく,毎年,技術力 UP に集中して取り組めるのでそれはそれで良いのではないかと考えている.
と,しょうもない前書きが長くなってしまったが,ここでは、Unity AR Foundation を使って Audio Reactive な AR マスクをつくる方法について触れます.Unity AR Foundation については、前に書いたこちらを参考までにご参照ください.
やりたいことのおさらい
では,早速,実装について触れていきましょう.と,その前にゴールとゴール達成のための検討ポイントを書いておきます(ここからの内容ですが,AR Foundation に慣れ親しんでいる前提で記載していきますので,不明点などあればお気軽にご連絡ください)
今回の検証環境は以下のような感じです.
- Unity:2019.3.0f6 Personal
- AR Foundation:3.1.0 – preview.4
- ARKit XR Plugin:3.0.0 – preview.4
- ARKit Face Tracking:3.0.1
- 検証機器:iPad Pro (第2世代)
- 検証機器(OS version):iPadOS 13.6
今回のゴールは,「音量に応じて顔に貼り付けるテクスチャを変更する」です。
実装していくにあたって,巨人の肩に乗ってしまえということで,ベースのプロジェクトは Dilmer Valecillos さんの github にある「Face Tracking Generating Masks」を参考にすることにします.
で,ゴールを達成するために検討すべきポイントは以下となります.
- iPad のマイクから音をリアルタイムに取得したい.
- 取得した音の強さに応じて AR で顔に貼るテクスチャを動的に変更したい.
ベースとして使うプロジェクトについて
上述した検討ポイントの2つについて触れる前に,ベースとして活用させて頂くプロジェクトの具体的な変更点(追加点)に触れておきましょう.
Dilmer Valecillos さんがスワップすると顔のテクスチャが変更されるデモを作ってくださっているので,これを参考にします。
Audio Reactive にするにあたり,スクリプトを1つだけ追加します.
FaceToggle.cs という C# script を新規作成しておきましょう.
で,AR Session Origin にアタッチしましょう.
では,上述の2つの検討ポイントを FaceToggle.cs に追記していきます.
IPAD のマイクから音をリアルタイムに取得したい
iPad からリアルタイムに音を取得するにはどうすれば良いのか.
このモチベーションで色々と調べたのですが,Unity 公式サポートでのこのやり取りが非常に参考になりました.
How Do I Get Unity To Playback A Microphone Input In Real Time?
基本的には,先程のリンクの通りです.もう少し,具体的に説明すると「Microphone.StartとAudioClip.GetDataの組み合わせ」で実現します(iPad で録音アプリを作りたい際などにもこの方法で実現するようです)
言葉で言ってもあれなので,コードを記載するとこんな感じ( Github gists の便利さに今更気づいた)
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.XR.ARFoundation; | |
using System; | |
[RequireComponent(typeof(ARFaceManager))] | |
public class ToggleFace : MonoBehaviour | |
{ | |
[Header("Effect Configuration")] | |
// 音量のための変数 | |
[SerializeField] public float vol = 1.0f; | |
// 音量に対して掛ける倍率(ここは気持ち的に音による変化を調整したいので用意した) | |
[SerializeField, Range(0f, 10f)] float m_gain = 10.0f; | |
// Audio Clip やサンプリング周波数,バッファサイズの準備 | |
// マイクで拾った音は Audio Clip 変数に保存されるのでその変数を用意する | |
AudioClip clip; | |
int head = 0; | |
const int samplingFrequency = 44100; | |
const int lengthSeconds = 1; | |
float[] processBuffer = new float[256]; | |
float[] microphoneBuffer = new float[lengthSeconds * samplingFrequency]; | |
float m_volumeRate; | |
// face toggle | |
public GameObject[] faces; | |
[SerializeField] | |
private ARFaceManager arFaceManager; | |
[SerializeField] | |
public FaceMaterial[] materials; | |
private float timer; | |
[SerializeField] | |
private float frequency = 1.0f; | |
[SerializeField] | |
private float maxUntilSpawn = 1.0f; | |
private int index = -1; | |
void Awake() | |
{ | |
// Audio Clip で準備 | |
clip = Microphone.Start(null, true, 1, samplingFrequency); | |
// null ならデバイス名はデフォルト | |
while (Microphone.GetPosition(null) < 0) { } | |
// face | |
arFaceManager = GetComponent<ARFaceManager>(); | |
arFaceManager.facePrefab.GetComponent<MeshRenderer>().material = materials[0].Material; | |
} | |
void Update() | |
{ | |
//【検証ポイント1】iPad のマイクから音をリアルタイムに取得したい | |
var position = Microphone.GetPosition(null); | |
if (position < 0 || head == position) { | |
return; | |
} | |
// Buffer に音声データを取り込む | |
clip.GetData(microphoneBuffer, 0); | |
while (GetDataLength(microphoneBuffer.Length, head, position) > processBuffer.Length) { | |
var remain = microphoneBuffer.Length - head; | |
if (remain < processBuffer.Length) { | |
Array.Copy(microphoneBuffer, head, processBuffer, 0, remain); | |
Array.Copy(microphoneBuffer, 0, processBuffer, remain, processBuffer.Length - remain); | |
} else { | |
Array.Copy(microphoneBuffer, head, processBuffer, 0, processBuffer.Length); | |
} | |
head += processBuffer.Length; | |
if (head > microphoneBuffer.Length) { | |
head -= microphoneBuffer.Length; | |
} | |
} | |
float sum = 0f; | |
for (int i = 0; i < processBuffer.Length; ++i) | |
{ | |
sum += Mathf.Abs(processBuffer[i]); // データ(波形)の絶対値を足す | |
} | |
// データ数で割ったものに倍率をかけて音量とする | |
m_volumeRate = Mathf.Clamp01(sum * m_gain / (float)processBuffer.Length); | |
vol = m_volumeRate; | |
// 【検証ポイント2】取得した音の強さに応じて AR で顔に貼るテクスチャを動的に変更したい | |
// 音量が vol 以上であれば利用するマテリアルを変更する | |
// 現状は 0.4 としているがここのしきい値は任意で指定できることが望ましい | |
if(vol > 0.4){ | |
if(index + 1 == materials.Length) | |
{ | |
index = -1; | |
} | |
index++; | |
foreach(ARFace face in arFaceManager.trackables) | |
{ | |
face.GetComponent<MeshRenderer>().material = materials[index].Material; | |
} | |
} | |
} | |
static int GetDataLength(int bufferLength, int head, int tail) { | |
if (head < tail) { | |
return tail - head; | |
} else { | |
return bufferLength - head + tail; | |
} | |
} | |
} | |
[System.Serializable] | |
public class FaceMaterial | |
{ | |
public Material Material; | |
public string Name; | |
} |
取得した音の強さに応じて AR で顔に貼るテクスチャを動的に変更したい
検討ポイント2に関しては,上記の97~110行目で実現しています.
これまた非常にシンプルで,音量がある閾値を超えたなら,予め用意していた配列の何番目を参照するかを示す index を ++ する,index が配列サイズになったらindex = -1として初期化するようにしています.
これによって,materialの参照するマテリアルを順繰り順繰り変更していく,というやり方です.
音量がしきい値以上超え続ければ,早いスピードで絵柄が切り替わりますし,超えなければ,同じ絵柄のままとなり,音に応じて絵柄変化に強弱がつきます.
こんな感じで結構簡単にできてしまいます.
テクスチャ表面を DEFORM してみる
これはただのおまけ程度なのですが,活用するテクスチャ(shader)を音に応じて変形するような感じで書けば,以下の動画のような少し変わった AR フェイスマスクも作成できちゃいます.
こんな感じで,先程のスクリプトに追加で,shader へ音量の変数をバインドするようにしてあげるだけです.
shader は好きなものを用意してあげましょう.
まとめ
音に反応する AR マスクですが,デモ動画を insta に挙げたところ反響が良く,New York の zerospace さんのアカウントで取り挙げてくださいました.
あとは,劇場関連の方々(これも海外)からもご連絡頂いたりなど,エンタメの演出には相性が良さそうです.
この夏も引き続き,Unity を活用した制作事例と実現方法について紹介していきたいと思います.
それでは,良い夏を!