【Unity × ARKit】塗り絵ARを開発したので、技術的な話をまとめてみる

前々から作りたかった塗り絵AR、ついにUnityで開発できました！初の自作Unityアプリになります。2ヶ月ほどかかりましたが、何とか完成できて感無量…!!
![fishar.gif](https://qiita-image-store.s3.amazonaws.com/0/314351/dd91a416-dccb-3b1e-3c8a-0775f2faa7a7.gif)



塗り絵ARはすでに色々な企業でやられていますが、技術的な話は少なく、参考になるソースもあまりなくて、険しい道のりでした。。

ここでは自身の備忘録、また塗り絵ARに興味がある方に向けて、仕組み、実装のプロセスを記したいと思います！


## 塗り絵ARの仕組み
<img width="953" alt="スクリーンショット 2019-03-23 9.47.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/314351/c2e43646-ec76-3ad1-9411-deca7a2ac333.png">

塗り絵ARの概略をシンプルにまとめてみました。

- openCVでカメラからスキャン
- スキャンしたUVマップをモデルにラッピング
- ARkitでモデルを召喚！

## 勘所やポイント、詰まりやすいところ
### モデリングについて
モデルに貼りつけるテクスチャ(ここでは塗り絵の下地)の型をそれっぽくする(今回は魚)よう設定するために、自分でモデリングする必要があります。

ソフトはフリーのblenderを使用しました！

Blenderのお話をすると長くなるので、ここでは塗り絵ARに特化して、勘所だけ紹介させて頂きます。ポイントはUV展開です。Blender限定ですが、操作はこちらになります！
<img width="596" alt="スクリーンショット_2019-03-23_10_31_40-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/314351/6b934af9-cd49-2a26-8622-cdbc341922ab.png">
<img width="571" alt="スクリーンショット_2019-03-23_10_35_05.png" src="https://qiita-image-store.s3.amazonaws.com/0/314351/43ff7fd9-ca46-c132-443e-63abf98716df.png">

・参考リンク：[【Blender】テンキーでの視点変更【カメラ,フロント,ライト,トップ等】](https://blender-cg.net/tennki/)

ここまできたら、好きなように動かしたり回転させたら、ひとまず完了です！！！
![fishUV.gif](https://qiita-image-store.s3.amazonaws.com/0/314351/61919e8e-c2b9-7211-b821-51b4e2db3f05.gif)

・参考リンク：[【Blender】キーボードでオブジェクトを移動･回転･拡大･縮小する方法(1/2)](https://blender-cg.net/ki-bo-do-kaitenn-kakudai/)

### openCVについて
![e4aae69a-dad1-4c97-b5cd-1f2d56bbed90.jpg](https://qiita-image-store.s3.amazonaws.com/0/314351/b77061c7-dd99-a414-cedb-f9e4eb777825.jpeg)

次にopenCV(画像処理)を使って、塗られた絵をスキャンする必要があります。
（正確にスキャンするならプリンター一択ですが、手軽さに欠けるので、今回はカメラからスキャンします）

Asset StoreにOpenCV for Unityというプラグインがありますが、**<font color="Red">有料</font>**です（号泣

わりとお高いので、お試しするには少し不安だったので、つい前に無料になったOpenCV plus Unityを使いました(2019/3/23現在)

情報はそこまで落ちていないですが、Demoを解読しながら使えば、使えなくはなかったです。

ただしプラグインをインポートする時、Unsafeコードをチェックする必要があります。こちらの記事を参考にすればOKですので、ご興味ある方はぜひお試しください！

>File>BuildSettings>PlayerSettingsを開いてOtherSettingsの"Allow 'unsafe' Code"の部分にチェックを入れます。

>引用：[OpenCV plus Unityを使ってみる（セットアップ、画像処理100本ノック1~10編）](https://qiita.com/Hirai0827/items/fd7b1dea42103841d033)

### ARKitのカメラ情報について
そもそもOpenCVでスキャンするには、webカメラで写した画像を渡す必要があります。ここで、もしARKitを使うとなると、Unityが用意しているクラス”WebCamTexture”が使えません。

そのかわり、ARKit Pluginで用いたカメラ画像を使うことになります。

流れとして、ARKit Pluginが最終的に映し出すマテリアル”YUVMaterial”のテクスチャを取得すればOK。UnityARVideo.csに`GetWebCamTexture`関数を追加。

流れはこんな感じです！

- マテリアルの内容をRenderTextureに書出
- Texture2Dにコピー

全ソースは下記の通り。ぜひ参考にしてみてください

```UnityARVideo.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Rendering;

namespace UnityEngine.XR.iOS
{

    public class UnityARVideo : MonoBehaviour
    {
        public Material m_ClearMaterial;
        private CommandBuffer m_VideoCommandBuffer;
        private Texture2D _videoTextureY;
        private Texture2D _videoTextureCbCr;
		private Matrix4x4 _displayTransform;

		private bool bCommandBufferInitialized;

		public void Start()
		{
			UnityARSessionNativeInterface.ARFrameUpdatedEvent += UpdateFrame;
			bCommandBufferInitialized = false;
		}

		void UpdateFrame(UnityARCamera cam)
		{
			_displayTransform = new Matrix4x4();
			_displayTransform.SetColumn(0, cam.displayTransform.column0);
			_displayTransform.SetColumn(1, cam.displayTransform.column1);
			_displayTransform.SetColumn(2, cam.displayTransform.column2);
			_displayTransform.SetColumn(3, cam.displayTransform.column3);		
		}

		void InitializeCommandBuffer()
		{
			m_VideoCommandBuffer = new CommandBuffer(); 
			m_VideoCommandBuffer.Blit(null, BuiltinRenderTextureType.CurrentActive, m_ClearMaterial);
			GetComponent<Camera>().AddCommandBuffer(CameraEvent.BeforeForwardOpaque, m_VideoCommandBuffer);
			bCommandBufferInitialized = true;

		}

		void OnDestroy()
		{
			if (m_VideoCommandBuffer != null) {
				GetComponent<Camera>().RemoveCommandBuffer(CameraEvent.BeforeForwardOpaque, m_VideoCommandBuffer);
			}
			UnityARSessionNativeInterface.ARFrameUpdatedEvent -= UpdateFrame;
			bCommandBufferInitialized = false;
		}

#if !UNITY_EDITOR && UNITY_IOS

        public void OnPreRender()
        {
			ARTextureHandles handles = UnityARSessionNativeInterface.GetARSessionNativeInterface().GetARVideoTextureHandles();
            if (handles.IsNull())
            {
                return;
            }

            if (!bCommandBufferInitialized) {
                InitializeCommandBuffer ();
            }

            Resolution currentResolution = Screen.currentResolution;

            // Texture Y
            if (_videoTextureY == null) {
              _videoTextureY = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height,
                  TextureFormat.R8, false, false, (System.IntPtr)handles.TextureY);
              _videoTextureY.filterMode = FilterMode.Bilinear;
              _videoTextureY.wrapMode = TextureWrapMode.Repeat;
              m_ClearMaterial.SetTexture("_textureY", _videoTextureY);
            }

            // Texture CbCr
            if (_videoTextureCbCr == null) {
              _videoTextureCbCr = Texture2D.CreateExternalTexture(currentResolution.width, currentResolution.height,
                  TextureFormat.RG16, false, false, (System.IntPtr)handles.TextureCbCr);
              _videoTextureCbCr.filterMode = FilterMode.Bilinear;
              _videoTextureCbCr.wrapMode = TextureWrapMode.Repeat;
              m_ClearMaterial.SetTexture("_textureCbCr", _videoTextureCbCr);
            }

            _videoTextureY.UpdateExternalTexture(handles.TextureY);
            _videoTextureCbCr.UpdateExternalTexture(handles.TextureCbCr);

			m_ClearMaterial.SetMatrix("_DisplayTransform", _displayTransform);
        }

#else

        public void SetYTexure(Texture2D YTex)
		{
			_videoTextureY = YTex;
        }

		public void SetUVTexure(Texture2D UVTex)
		{
			_videoTextureCbCr = UVTex;
		}

		public void OnPreRender()
		{

			if (!bCommandBufferInitialized) {
				InitializeCommandBuffer ();
			}

			m_ClearMaterial.SetTexture("_textureY", _videoTextureY);
			m_ClearMaterial.SetTexture("_textureCbCr", _videoTextureCbCr);

			m_ClearMaterial.SetMatrix("_DisplayTransform", _displayTransform);
		}

#endif
//追加した関数
        public Texture2D GetWebCamTexture()
        {
            RenderTexture _arTexture = new RenderTexture(Screen.width, Screen.height, 0);
            Texture2D _arTexture2D = new Texture2D(_arTexture.width, _arTexture.height, TextureFormat.ARGB32, false);

            if (_videoTextureY != null && _videoTextureCbCr != null)
            {
                Graphics.Blit(null, _arTexture, m_ClearMaterial);

                RenderTexture back = RenderTexture.active;
                RenderTexture.active = _arTexture;
                _arTexture2D.ReadPixels(new Rect(0, 0, _arTexture.width, _arTexture.height), 0, 0);
                _arTexture2D.Apply();
                RenderTexture.active = back;
            }

            return _arTexture2D;
        }

    }
}

```

・参考リンク
- [UnityのARKit Pluginのカメラ映像を利用してなにかする](http://edom18.hateblo.jp/entry/2019/02/21/103056)
- [Unity ARKit Plugin のカメラ映像を使って何かするには](https://qiita.com/rakusan/items/0368573dc808ff4fa590)

### Shaderについて

ここでは、魅せ方のお話です。細かい内容なので、ご興味ある方のみご覧いただければと思います。

ARでより現実と調和させるには、影が必要不可欠です。一方で、塗り絵ARはライティングせず、ハッキリと表示させたい…という我儘な考えがありました。


この場合、UnlitでShaderを作成し、UsePassを通せば完成できます。

```
UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"
```
公式ページでサンプルもありますので、是非チェックしてみてください！
・参考URL：[公式：頂点シェーダーとフラグメントシェーダーの例](https://docs.unity3d.com/ja/current/Manual/SL-VertexFragmentShaderExamples.html)


次に新たな問題があります。海面に影を映し出さなくてはなりません。この時、シェーダーを少し改造する必要があり、影を実装する方法は以下の手順を踏むことになります。

>1. Tagsの中に"LightMode"="ForwardBase"(※1)を入れる

>2. CGPROGRAMの下に#pragmaを追加。（#pragma multi_compile_fwdbase）

>3. CGPROGRAM内でファイルをincludeする。（#include "UnityCG.cginc"、#include "AutoLight.cginc"）
>4. vertex shaderからfragment shaderに渡す構造体の定義の中にLIGHTING_COORDS(idx1, idx2)を入れる(※2)

>5. vertex shader内でTRANSFER_VERTEX_TO_FRAGMENT(o);（マクロ）を記述する。
(o)は出力される構造体名

>6. fragment shader内でLIGHT_ATTENUATION(i)マクロを使って影の度合いを得る
(i)は入力される構造体名

>引用：[[Unity] AssetStoreのファーシェーダをupdateしたので分かったことを書いてみる](https://qiita.com/edo_m18/items/21d3b37596da3fd4b32b)

こちら参考に作ったShader(サンプル)がこちらです！
<img width="351" alt="スクリーンショット 2019-03-23 12.08.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/314351/0ab51b5d-e2ac-a939-9912-8c8fccfc8fe4.png">


```ToonSeaShader.shader
Shader "Unlit/ToonSeaShader"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque"}
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
                LIGHTING_COORDS(0,1)
            };
            
            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                TRANSFER_VERTEX_TO_FRAGMENT(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _Color;
                float attenuation = LIGHT_ATTENUATION(i);
                return col* attenuation;
            }
            ENDCG
        }
    }
    Fallback "Diffuse"
}
```


今回、手順1にある"LightMode"="ForwardBase"は必要なかった(ライティングに関わらず、あえて単調にしたかった)ので、入れていません。

これが大まかな影付きのでShader作成の処理の流れになるので、フラグメントシェーダーの内部処理を変えれば、影がつけれるようになります！

ちなみに単純にただ影だけ出したい場合は、ARKit Pluginに用意されているMobileARShadowシェーダを使えば解決できます！
・参考リンク：[Unityで始めるARKit入門　影の表示編](http://nn-hokuson.hatenablog.com/entry/2018/10/15/202753)

## 最後に
塗り絵ARを作ってみて、Unityだけではなくモデリングやシェーダー、OpenCVなど総合的にスキルアップできました！(嬉しい)

折角ここまで頑張ったので、もっとブラッシュアップしたり種類を増やして、ストアに出そうかな…なんて思っています。

これから塗り絵ARを作ろう！と思っている方の参考にもなれば幸いです。
ここまでご覧いただき、ありがとうございましたm(_ _)m