おもちゃラボ

Unityで遊びを作ってます

【Unity】Macで使えるNative Pluginを作る(OpenCV編)

UnityでOpenCVを使いたい場合は、OpenCV for Unityを使う方法と、自分で使いたい関数だけをNative Pluginとして用意する方法があります。ここでは、アセットを使わず、Native PluginからOpenCVの機能を使う方法を紹介します。

UnityでOpenCVを使ったNative Pluginを作る手順は次のようになります。

Homebrewをインストール

コマンドラインで次のコマンドを打ってHomebrewをインストールします

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

パッケージを最新にするため、Homebrewをアップデートします。

brew update

OpenCVをインストールする

brew install -v cmake
brew tap homebrew/science
brew install opencv3

これで/usr/local/Cellar/opencv3以下に、OpenCVのライブラリ一式がインストールされました。

XcodeでNative Pluginを作る

続いて、Unityから受け取った画像をNative Pluginでグレースケールに変換して、それをUnityに返すプログラムを作ります。

f:id:nn_hokuson:20170512201403j:plain:w500

Unityで使うMacのNative PluginはBundleとして作成します。Xcodeを起動してMacOS>Framewrok & Library>Bundleを選択してください。「Product Name」は「TestPlugin」にしておきます。

f:id:nn_hokuson:20170512192724p:plain:w400

XCodeからOpenCVが使えるようにパスを通しておきましょう。ナビゲーションビューでTestPluginを選択し、「Build Settings」→「Header Search Path」の欄に「/usr/local/Cellar/opencv3/3.2.0/include/」と入力します(バージョンは適宜書き換えてください)。

f:id:nn_hokuson:20170512200032j:plain

次に「Build Phasesを」選択し、Link Binary With Librariesの欄に使用するライブラリをドラッグ&ドロップします。とりあえず次のものをドラッグ&ドロップしておきましょう。

・libopencv_core.3.2.0.dlyb
・libopencv_highgui.3.2.0.dlyb
・libopencv_imageproc.3.2.0.dlyb

 続いてNative Pluginとして使うためのプログラム(Plugin.cpp)とヘッダファイル(Plugin.pch)を作成します。Unityから使えるようにヘッダファイルは「extern "C"」でくくっておいてください。

#include "Plugin.pch"
#include <opencv2/opencv.hpp>

void conv(unsigned char* arr, int w, int h)
{
    cv::Mat img(cv::Size(w, h), CV_8UC4, cv::Scalar(0, 0, 0));
    cv::Mat gray(cv::Size(w, h), CV_8UC1, cv::Scalar(0, 0, 0));

    // char* -> Mat
    int i = 0;
    for(int y = 0; y < img.rows; ++y){
        for(int x = 0; x < img.cols; ++x){
            for(int c = 0; c < img.channels(); ++c){
                img.data[ y * img.step + x * img.elemSize() + c ] = arr[i++];
            }
        }
    }
    
    // RGBA -> GRAY
    cv::cvtColor(img, gray, CV_RGBA2GRAY);
    cv::cvtColor(gray, img, CV_GRAY2RGBA);
    
    // Mat -> char*
    i = 0;
    for(int y = 0; y < img.rows; ++y){
        for(int x = 0; x < img.cols; ++x){
            for(int c = 0; c < img.channels(); ++c){
                arr[i++] = img.data[ y * img.step + x * img.elemSize() + c ];
            }
        }
    }
}
extern "C" {
    void conv(unsigned char* arr, int w, int h);
}

ここではグレースケールに変換するconv関数を作っています。最終的にはこのconv関数をUnity側から呼び出す事になります。引数にはピクセルの配列と画像の縦幅・横幅を受け取っています。Unity側ではcv::Mat型は使えないため、ここではunsigned charの配列として、画像のピクセルデータをやり取りしています。conv関数の内部ではOpenCVを使ってグレースケールに変換した後、再度ピクセル配列に書き戻しています。

メニューバーから「Product」→「Build」を選択し、プロジェクトをビルドするとProductsフォルダにTestPlugin.bundleが生成されます。

f:id:nn_hokuson:20170512193700p:plain:w250

UnityからNative Pluginを叩いて実行する

Unityでプロジェクトを作成し、Assetフォルダの下にPluginフォルダを作ってください。作成したPluginフォルダの中に先程の「TestPlugin.bundle」を入れます。このとき、XcodeのProductsフォルダからUnityに直接ドラッグ&ドロップすることが出来ます。

f:id:nn_hokuson:20170512194037p:plain:w500

最後にこのバンドルをNative Pluginとして使用するためのスクリプトを作成します。ImageProcessing.csという名前でスクリプトを作り、次のプログラムを入力して下さい。

public class ImageProcessing : MonoBehaviour {

    [DllImport ("TestPlugin0")]
    private static extern void conv(IntPtr img, int w, int h);

    private Color32[] pixels_;
    private GCHandle pixels_handle_;
    private IntPtr pixels_ptr_ = IntPtr.Zero;
    private Texture2D mainTexture;

    void Start () {

        // ピクセルデータを取り出す
        Texture2D mainTexture = (Texture2D) GetComponent<Renderer> ().material.mainTexture;
        mainTexture.filterMode = FilterMode.Point;
        pixels_ = mainTexture.GetPixels32();
        pixels_handle_ = GCHandle.Alloc(pixels_, GCHandleType.Pinned);
        pixels_ptr_ = pixels_handle_.AddrOfPinnedObject();
    
        int w = mainTexture.width;
        int h = mainTexture.height;

        // Native Pluginを呼び出す
        conv (pixels_ptr_, w, h);

        // ピクセルデータからテクスチャを作る
        Texture2D change_texture = new Texture2D (w, h, TextureFormat.RGBA32, false);
        change_texture.filterMode = FilterMode.Point;
        change_texture.SetPixels32 (pixels_);
        change_texture.Apply ();

        GetComponent<Renderer> ().material.mainTexture = change_texture;
    }
}

このスクリプトではテクスチャからピクセルデータを取り出し、IntPtr型に変換してからNative Pluginにデータを渡しています。Native Pluginの中でグレースケール変換して、そのデータを再度Unity側でテクスチャにセットしています。

このスクリプトをPlaneオブジェクトにアタッチしてください。planeには適当な画像を配置しておきます。

f:id:nn_hokuson:20170512194449j:plain

実行すると、Planeに設定した画像が、グレースケールの画像に変換されました。なんか白黒にすると遺影みたいですが・・・(笑)

f:id:nn_hokuson:20170512194455p:plain:w400

プラグインのロードには注意が必要です!

Unityでは一度スクリプトからNative Pluginをロードすると、Unityを再起動するまでUnloadされない仕様になっています。

Once a native plugin is loaded from script, it's never unloaded. if you deselect a native plugin and it's already loaded, please restart Unity.

f:id:nn_hokuson:20170512194614p:plain:w350

つまり、Native Pluginのプログラムを更新して、古いものと置き換えたとしてもUnityを再起動するまでは古いプラグインが使われます・・・・ひどい

私は毎回、Pluginの名前を変えてからプロジェクトにドラッグ&ドロップしています。なぜUnityをだましだまし使わなくてはいけないのか・・・

まとめ

この記事では、Native PluginでOpenCVを使う方法を紹介しました。また、OpenCV for Unityは$95でAsset Storeで販売されています。Native Pluginなんて作るの面倒くさいよ、という方はこちらを使ってみて下さい。

f:id:nn_hokuson:20170512192332j:plain:w400

[asin:4877833986:detail]

【Unityシェーダ入門】画面をセピア色にするポストエフェクトを作る

今回はスクリーンをセピア調にするポストエフェクトシェーダを作ります。ちなみに・・・Wikipediaによるとセピアとはイカスミのことらしいです。

セピア(sepia)とは、イカ墨のこと。また、イカ墨由来の黒褐色をも意味する。かつてモノクロ写真などにこの色のインクが用いられたために、古い写真は褪色し淡い褐色になった。

ゲーム画面をセピア色に変換(ポストエフェクト)した結果は次のようになります。

f:id:nn_hokuson:20170508224217j:plain

ポストエフェクトを使って画面をグレースケール変換する方法は、こちらの記事で紹介しているので参考にしてみて下さい。

nn-hokuson.hatenablog.com

描画する画像をフックする

カメラに映った画像を取得してポストエフェクトをかけるために、画面をレンダリングする途中で画像をフックして画像処理します。

f:id:nn_hokuson:20170508233734p:plain

プロジェクトビューで「PostEffect.cs」というファイルを作成し、次のプログラムを入力してください。

using UnityEngine;
using System.Collections;

public class PostEffect : MonoBehaviour {

	public Material sepia;

	void OnRenderImage(RenderTexture src, RenderTexture dest)
	{
		Graphics.Blit (src, dest, sepia);
	}
}

上記の「ポストエフェクトで画面をグレースケール化する」のページでも説明していますが、OnRenderImageメソッドはレンダリングが完了した後に呼び出されるメソッドで、この中でBlitメソッドを使ってポストエフェクトをかけています。Blitメソッドはsrc画像に第三引数で指定したポストエフェクトをかけてdest画像に書き込みます。

スクリプトが保存できたら、PostEffect.csをカメラオブジェクトにアタッチしてください。

f:id:nn_hokuson:20170508230018j:plain:w550

Unityでセピアシェーダとマテリアルを作る

画面をセピア色にするためのシェーダを作ります。プロジェクトビューで「右クリック」→「Create」→「Shader」→「Standard Surface Shader」を選択し、作成したファイル名をSepiaに変更します。続いてこのシェーダファイルをアタッチするマテリアルも作成しましょう。monoToneのシェーダをを選択した状態で「右クリック」→「Create」→「Material」を選択して「Custom_Sepia」を作成します。

f:id:nn_hokuson:20170508225601p:plain:w180
Sepia.shaderを開いて次のスクリプトを入力してください。

Shader "Custom/sepia" {
    Properties {
    	_Darkness("Dark", Range(0, 0.1)) = 0.04
    	_Strength("Strength", Range(0.05, 0.15)) = 0.05
        _MainTex("MainTex", 2D) = ""{}
    }

    SubShader {
        Pass {
            CGPROGRAM

            #include "UnityCG.cginc"

            #pragma vertex vert_img
            #pragma fragment frag

            sampler2D _MainTex;
            half _Darkness;
            half _Strength;

            fixed4 frag(v2f_img i) : COLOR {
                fixed4 c = tex2D(_MainTex, i.uv);
                half gray = c.r * 0.3 + c.g * 0.6 + c.b * 0.1 - _Darkness;
                gray = ( gray < 0 ) ? 0 : gray;

                half R = gray + _Strength;
                half B = gray - _Strength;

                R = ( R > 1.0 ) ? 1.0 : R;
                B = ( B < 0 ) ? 0 : B;
                c.rgb = fixed3(R, gray, B);
                return c;
            }

            ENDCG
        }
    }
}

上記のプログラムでははセピア変換処理はフラグメントシェーダ(fragメソッドの中)で行っています。このフラグメントシェーダでは、次の3ステップで画像をセピア色に変換しています。

  1. 画像のグレースケール変換
  2. 明度を修正
  3. グレースケール値から赤成分を足し、青成分を減らす

ステップ2で使用する画像を暗くする量(Darkness)と、Step3で使用するセピア色の強さ(Strength)はUnityのインスペクタからも変更できるように、プロパティの部分で宣言しています。

グレースケール化に関しては下記のページを参考にして下さい。

nn-hokuson.hatenablog.com

画像をセピア色に変換するアルゴリズムは、各ピクセルの赤成分を足して、青成分を引くだけです。数式としては非常に簡単ですが、案外それっぽい見た目になりますよ〜!

カメラにセピア用マテリアルをセットする

作成したマテリアルを、カメラにアタッチしたPostEffectスクリプトのSepia欄にドラッグ&ドロップします。これで、画面をセピア色にするポストエフェクトをかける準備ができました。

f:id:nn_hokuson:20170508231327j:plain:w550

実行結果は次のようになります。ポストエフェクトの効果で画面がセピア色になっていますね。

f:id:nn_hokuson:20170508233259j:plain

先ほど、シェーダファイルで「画面の明度」と「セピア色の強さ」はプロパティに設定したので、インスペクタから調整可能です。

f:id:nn_hokuson:20170508233020p:plain:w300

パラメータを調節することで同じセピア色でも色々な表情を作れるので、自分のゲームに合った色合いを見つけてみてくださいね〜!

f:id:nn_hokuson:20170508232951j:plain

【Unityシェーダ入門】ステンシルバッファを使って隠れた部分を描く

プレイヤがゲームステージに配置されている障害物などの後ろ側に回り込むと、見えなくて操作しづらくなってしまいます。そこで、障害物によって隠れた部分は影だけ描いたりします。この記事では、Unityで隠れた部分を影で描画する方法を紹介します。

f:id:nn_hokuson:20170502184709p:plain:w550

今回の記事は次のようになります。

ステンシルバッファの原理

ステンシルバッファといっても、特殊なものではなく、一般的な画像と同じと考えて問題ありません。画像と違うのは、ステンシルバッファは画面に表示するためのものではなく、描画する際に使用する「裏方の値」ということです。

では、どのようにしてスプライトの裏側の画像を描画するかを説明します。

まずは画面上にブロックを描くと同時に、ステンシルバッファには「1」を書き込みます。これによりブロックの位置のステンシルバッファは「1」になります。

f:id:nn_hokuson:20170502181425p:plain:w550

続いて、ユニティちゃんを描画する際には、ステンシルバッファを参照します。描画しようとしている位置のステンシルバッファの値が「1」だった場合は、そのピクセルを黒く塗りつぶし、ステンシルバッファの値が「0」だった場合は、ユニティちゃんのテクスチャを描画します。

f:id:nn_hokuson:20170502182031p:plain:w550

実際にはステンシルバッファの値をif文で分けることは出来ないので、Stencilプロパティを利用して2回描画します。詳しくは下のプログラムを参考にしてみて下さい。

ステンシルバッファ用のシェーダを作る

ここでは、ブロック用のシェーダとユニティちゃん用のシェーダを作ります。プロジェクトビューで右クリックし、「Create」→「Shader」→「Standard Surface Shader」を選択、「Unitychan」という名前で保存します。同様に「Block」シェーダも作成しましょう。

f:id:nn_hokuson:20170502182746p:plain:w150

作成した「Unitychan」シェーダを選択した状態で、右クリック→「Create」→「Material」でシェーダに対応するマテリアル(Custom_Unitychan)を作ります。同様に「Custom_Block」も作りましょう。

f:id:nn_hokuson:20170502182757p:plain:w150

それぞれのマテリアルをユニティちゃんとブロックにアタッチして、それぞれのテクスチャをセットして下さい。

ブロックのシェーダを作る

では、まずBlockシェーダから見ていきましょう。

Shader "Unlit/Block"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Stencil
            {
                Ref 1
                Comp Always
                Pass Replace
            }

            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag
            
            #include "UnityCG.cginc"

            sampler2D _MainTex;

            fixed4 frag (v2f_img i) : COLOR
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

Blockシェーダの中では、ブロックを描画する位置のステンシルバッファを「1」で塗りつぶすように設定しています。ステンシルバッファの操作はStencilのプロパティを使用します。Stencilのプロパティの宣言は次のようになります。
 

Ref ステンシルに書き込む値
Comp 使用する比較関数
Pass 比較関数が真のときにの操作

 
ここでは、「Ref 1」としてステンシルバッファを「1」で塗りつぶすことを宣言しています。また、「Comp Always」「Pass Replace」としてブロックを描画する位置は常にステンシルバッファを強制的に「1」にすることを指定しています。

ブロックを描画し終わった時点でステンシルバッファは次のようになります。

f:id:nn_hokuson:20170502183109p:plain:w280

ユニティちゃんのシェーダを作る

続いて、ユニティちゃんのシェーダを作ります。

Shader "Unlit/Unitychan"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent"
                "Queue" = "Transparent"
                "IgnoreProjector"="True" }
        LOD 100
        Blend SrcAlpha OneMinusSrcAlpha 
        ZWrite Off
        Cull off

        Pass
        {
            Stencil
            {
                Ref 1
                Comp Equal
            }

            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;

            fixed4 frag (v2f_img i) : SV_Target
            {
                float alpha = tex2D(_MainTex, i.uv).a;
                fixed4 col = fixed4(0,0,0,alpha);
                return col;
            }
            ENDCG
        }

        Pass
        {
            Stencil
            {
                Ref 0
                Comp Equal
            }

            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;

            fixed4 frag (v2f_img i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

ユニティちゃんシェーダの方はSubShaderの中に2つのPassがあります。このPassブロック一つが「塗り」の1回に相当します。つまりPassが2つあるので、一度塗って、乾いてから更に色を乗せる二度塗りになります。

上にも書いたように、ステンシルバッファの値を元にif文で分けることは出来ません。次のように「影」と「テクスチャ」の描画を分けて2度塗りすることでオブジェクトに隠れた部分も描画します。

  1. 影の部分を描画する
  2. ユニティちゃんのテクスチャ部分を描画する

まずは1Pass目では「Ref 1」「Comp Equal」としてステンシルバッファが「1」の部分だけを描画します。ステンシルバッファが「1」の部分とはブロックを描画した部分でしたね。

f:id:nn_hokuson:20170502183818p:plain:w550

描画はフラグメントシェーダの中で行っています。1Pass目のフラグメントシェーダでは、テクスチャの透明度を参照して黒く塗りつぶしています。

続いて2Pass目では「Ref 0」「 Comp Equal」としてステンシルバッファが「0」の部分だけを描画します。ステンシルバッファが「1」の部分とはブロック以外の部分です。

f:id:nn_hokuson:20170502184134p:plain:w550

フラグメントシェーダの中では、単純にユニティちゃんのテクスチャを描画しているだけです。

このように、ステンシルバッファを使って2度塗りをすることで、オブジェクトの後ろに回り込んだときは影を描画する「背景透過シェーダ」を作ることが出来ました。

f:id:nn_hokuson:20170502184545g:plain