Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

前々から作りたかった塗り絵AR、ついにUnityで開発できました!初の自作Unityアプリになります。2ヶ月ほどかかりましたが、何とか完成できて感無量…!!
fishar.gif

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

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

塗り絵ARの仕組み

スクリーンショット 2019-03-23 9.47.46.png

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

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

勘所やポイント、詰まりやすいところ

モデリングについて

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

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

Blenderのお話をすると長くなるので、ここでは塗り絵ARに特化して、勘所だけ紹介させて頂きます。ポイントはUV展開です。Blender限定ですが、操作はこちらになります!
スクリーンショット_2019-03-23_10_31_40-2.png
スクリーンショット_2019-03-23_10_35_05.png

・参考リンク:【Blender】テンキーでの視点変更【カメラ,フロント,ライト,トップ等】

ここまできたら、好きなように動かしたり回転させたら、ひとまず完了です!!!
fishUV.gif

・参考リンク:【Blender】キーボードでオブジェクトを移動・回転・拡大・縮小する方法(1/2)

openCVについて

e4aae69a-dad1-4c97-b5cd-1f2d56bbed90.jpg

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

Asset StoreにOpenCV for Unityというプラグインがありますが、有料です(号泣

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

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

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

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

引用:OpenCV plus Unityを使ってみる(セットアップ、画像処理100本ノック1~10編)

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のカメラ映像を利用してなにかする
- Unity ARKit Plugin のカメラ映像を使って何かするには

Shaderについて

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

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

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

UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"

公式ページでサンプルもありますので、是非チェックしてみてください!
・参考URL:公式:頂点シェーダーとフラグメントシェーダーの例

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

  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したので分かったことを書いてみる

こちら参考に作ったShader(サンプル)がこちらです!
スクリーンショット 2019-03-23 12.08.18.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入門 影の表示編

最後に

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

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

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

guru_taka
ぐるたかです。0→1を作る人に憧れ、2018.10からプログラミングを学び、今はとある会社で、webサービスを開発中。家ではARアプリやwebサービス、拡張機能など色々と個人開発してます。前まではUnityに夢中でしたが、最近はNuxtにハマっています。代表作はおっぱい関数ジェネレーターと塗り絵AR。
https://gurutaka-log.com/
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。
https://admin-guild.slack.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away