Edited at

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

前々から作りたかった塗り絵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