アバターカメラの仕組み(Texture渡し編
先日、Mirrativさんとの合同勉強会があり、そこで「アバターカメラの仕組み」というタイトルで発表がありました。
本エントリでは、その勉強会でお話しした内容を再構成して公開します。
前半ではアバターカメラ機能の紹介、後半ではアバターカメラ機能を支えるTexture渡しについて実装に触れながらメリットを紹介します。
アバターカメラとは?
撮影した写真にアバターを合成してシェアできる機能です。
編集画面では、テキストや自分のQRコードを追加したり、アバターや背景、文字、QRコードの大きさや位置を自由に操作できます。
ユーザーの反応
Texture渡しとは?
このアバターカメラ機能を支えている技術の1つにTexture渡しがあります。
Texture渡しとは、UnityからネイティブにTextureを渡し、ネイティブでいい感じの処理をするREALITY内の仕組みです。※1
アバターカメラでは、背景とアバターの撮影画面はUnity、編集画面はネイティブで実装しているので、撮影画面から編集画面に遷移する際に、Texture渡しが実行されます。
Texture渡しの実装
Texture渡しは、UnityのTexture.GetNativeTexturePtrを利用して、テクスチャリソースへのネイティブポインタを取得し、ネイティブに渡して処理します。
取得できるポインタはプラットフォームごとに異なり、iOSではid<MTLTexture>が取得できます。
取得したポインタはUnityのNative Pluginの仕組みを使って、ネイティブに渡します。
Unity側で、[DllImport("__Internal")] をつけたstatic externなTextureのポインタを送信する関数を宣言します。
[DllImport("__Internal")]
private static extern void _ui_sendTexture(IntPtr texture);
// ~~中略~~
public static void SendTexture(Texture texture)
{
_ui_sendTexture(texture.GetNativeTexturePtr());
}
Unityで定義した関数をObjective-Cで実装します。
受け取ったポインタをid<MTLTexture>にキャストし、Swiftに送信します。
void _ui_sendTexture(void *texture) {
id<MTLTexture> tex = (__bridge id<MTLTexture>)texture;
[[RealityAvatar shared] sendTexture:tex];
}
送信されたTextureを受け取り、処理します。
class RealityAvatar {
static let shared = RealityAvatar()
func send(texture: MTLTexture) {
// UnityのTextureを取得
// このTextureを活用する
}
}
ネイティブでのTextureの活用(png編)
受け取ったMTLTextureをCoreImage Frameworkを利用してpngに変換することが可能です。
例えば、REALITYアプリでは、アバターカメラ機能の他に配信のスクショ機能で活用されています。
let texture: MTLTexture = /*取得したtexture*/
let context = CIContext()
let ci = CIImage(mtlTexture: texture, options: [:])!
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
let data = context.pngRepresentation(of: ci,
format: .RGBA8,
colorSpace: colorSpace,
options: [:])!
たしかに、UnityのTexture2D.EncodeToPNGを使っても、pngに変換することが可能です。
RenderTexture renderTexture
var current = RenderTexture.active;
RenderTexture.active = renderTexture;
var texture2D = new Texture2D(renderTexture.width, renderTexture.height);
texture2D.ReadPixels(new Rect(0, 0, texture2D.width, texture2D.height), 0, 0, false);
texture2D.Apply();
RenderTexture.active = current;
var data = texture2D.EncodeToPNG();
しかし、ネイティブで処理する事で、高速にpngに変換することが可能です。
先ほど紹介したTexture2D.EncodeToPNGを使う手法では、RenderTextureからpngに変換するまでに、0.2sec ~ 0.3sec必要です。EncodeToPNGの処理が8割以上を占めます。
これに加えて、アルバムに保存する場合は取得したデータをNativeに渡す時間も必要です。
一方で、CoreImage Frameworkを使う手法では、Textureを渡す時間込みで、0.07sec ~ 0.09secで実行可能です。
Texture渡しをしてネイティブで処理することで、Unityで処理する場合に比べて、最大で約3倍高速にpngに変換することが可能です。
ネイティブでのTextureの活用(UI編)
REALITYでは、受け取ったMTLTextureをUIImageに変換することで、ネイティブのUIとして表示しています。
iOSのアバターカメラ機能では、UIImageViewを使って、Unityで撮影したアバターや背景を拡大縮小させています。
Unityでは実装が難しい複雑なUIも、ネイティブで実装することで実現する事ができました。
MTLTextureからUIImageの変換は、CIImageとCGImageを経由させます。
let texture: MTLTexture = /*取得したtexture*/
let context = CIContext()
let ci = CIImage(mtlTexture: texture, options: [:])!
let cg = context.createCGImage(ci, from: ci.extent)!
let ui = UIImage(cgImage: cg)
この変換は、0.01sec ~ 0.02secで実行可能なので、フレームレートの低下など気にせず使う事が可能です。
まとめ
前半ではアバターカメラ機能の紹介、後半ではアバターカメラ機能を支えるTexture渡しについて実装に触れながらメリットを紹介しました。
REALITYアプリでは、Unityとネイティブを組み合わせることで、高速なメディア処理や、Unityで撮影した画像を高度なUIで利用する事を実現しています。
もし一緒にUnityとネイティブを組み合わせて快適なユーザ体験をつくってくれる、REALITYに興味のあるエンジニアの方がいらっしゃいましたら、下記のリンクからお気軽に話を聞きにきてくれると嬉しいです。
→ REALITYエンジニアカジュアル面談お申し込みフォームはこちら