おもちゃラボ

Unityで遊びを作ってます

【Unityシェーダ入門】オブジェクトが重なった部分をくり抜く

特定のオブジェクトと重なった部分を透明にくり抜くシェーダを紹介します。このシェーダを使えば、次のように、好きな形で別のオブジェクトをくり抜くことができます。

f:id:nn_hokuson:20170601215112j:plain:w500

オブジェクトの形にくり抜くためには

今回のシェーダの原理は非常に簡単です。まずは抜きたい形のオブジェクトをA、抜かれる側のオブジェクトをBとしておきましょう。

f:id:nn_hokuson:20170601215300p:plain:w300

まずは、オブジェクトAをデプスバッファにだけ書き込み、カラーバッファには書き込まないようにします。これにより、画面には表示されないけれどもデプスバッファには書き込まれた状態になります。

f:id:nn_hokuson:20170601215505p:plain:w400

続いて、オブジェクトBを通常通り描画します。デプスバッファにはすでにオブジェクトAの情報が書き込まれているため、この部分だけは描画されないことになります。

f:id:nn_hokuson:20170601215547p:plain:w400

シェーダの作成

まずはシーンビューに抜くオブジェクト(球)と、抜かれるオブジェクト(立方体)を配置しておきましょう。

f:id:nn_hokuson:20170601215727j:plain

続いてシェーダとマテリアルを作成します。プロジェクトビューで右クリックして「Cutout.shader」そのシェーダをアタッチしたマテリアル「Custom_Cutout」を作成します。「Cutout.shader」には次のプログラムを入力してください。

Shader "Custom/Cutout" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
    }

    SubShader {
        Tags {"Queue" = "Geometry-1"}

        Pass{
            Zwrite On
            ColorMask 0
        }
    }
}

シェーダプログラムが作成できたら、球にアタッチしてください。球が透明になったと思います。

f:id:nn_hokuson:20170601220036j:plain:w500

床は透明にせずに残したい

この方法では球と重なったオブジェクトが全てくり抜かれることになります。例えば、くり抜いた立方体の下側に床を表示したい場合はどうすればよいでしょうか。普通に床を描画すると床までくり抜かれてしまいます。

f:id:nn_hokuson:20170601220153j:plain

球の情報がデプスバッファに書かれることで、それ以降に描画するオブジェクトは描画されなくなるのでした。ということは、球の情報をデプスバッファに書き込む前に床を描画すれば良さそうです。

描画する順番はシェーダのRenderQueueで指定できます。床用のシェーダファイルとマテリアルを作成しましょう。床のシェーダファイルはプロジェクトビューで右クリックし、「Create」→「Shader」→「Standard Surf Shader」を選択して作成後、次のようにTagを書き換えてください。

	Tags { "Queue"="Geometry-2" }

これにより、Render Queueの値は次のようになり、床→球→立方体の順序で描画される様になります。

オブジェクト Render Queue
背景画像 1998
1999
立方体 2000

作成した床用のマテリアルを床オブジェクトにアタッチして実行すると、実行結果は次のようになります。

f:id:nn_hokuson:20170601215112j:plain

動かしてみるとこんな感じです!

f:id:nn_hokuson:20170601220240g:plain

OpenCV超入門 サンプルプログラム集

OpenCVはバージョンが1.0のころに使っていましたが、久しぶりに使おうとするとバージョンが3.0になって、ガラッと様子が変わってしまっていることに気がついた。

  • IplImageはどこへいった?
  • cvのプリヘッダがなくなった!?
  • Matってメモリの割り当てはどうするんだ?
  • ピクセルへのアクセスってどうやるの?

などなど、疑問が山のようにでてきました。そんな疑問に答えるべく、OpenCV3の使い方とサンプルをまとめてみたので、どうぞ活用してやって下さい。ちなみに、サンプル写真にはレナ嬢ではなく京都の風景を使用しています(笑)

画像を表示する

f:id:nn_hokuson:20170529235347j:plain
OpenCV3を使って、画像を表示するだけのプログラムです。

void main()
{
    Mat img = imread("img.png");
    imshow("image",img);
    waitKey(0);
}

OpenCV3からは画像を格納する型としてIplImageではなくMatを使います。画像の読み込みにはimread関数を使います。引数は次のようになります。

■imread(filename, flag)

引数 意味
filename 読み込みファイル名
flag >0: 3チャンネルカラー
=0: グレースケール
<0 読み込み画像に従う


画像の表示はimshow関数で、第1引数にウインドウ名、第2引数に表示したいMatを指定します。

■imshow(name, img)

引数 意味
name ウインドウの名前
img 表示する画像

グレースケール変換

f:id:nn_hokuson:20170531195046j:plain:w300 f:id:nn_hokuson:20170531195217j:plain:w300
画像をグレースケールに変換するプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat dst;
    cvtColor(img, dst, COLOR_RGB2GRAY);
    imshow("image",dst);
    waitKey(0);
}

グレースケール変換にはcvtColor関数を使います。第3引数に指定するコードで変換前と変換後の画像フォーマットを指定します。ここではRGBからグレースケールに変換するため、COLOR_RGB2GRAYを指定しています。第3引数を変更することでHSVなどのフォーマットに変換することも出来ます。

■cvtColor(src, dat, code)

引数 意味
src 入力画像
dst 出力画像
code RGB→GRAY COLOR_RGB2GRAY
RGB→HSV COLOR_RGB2HSV
・・・など

ここで、出力画像のメモリ割り当てを行っていないことに注目して下さい。IplImageとは異なり、Matでは自動的にメモリの割り当て&開放を行ってくれるようになっています。

2値化

f:id:nn_hokuson:20170530065445j:plain f:id:nn_hokuson:20170530065451j:plain
画像を白黒の2値に変換するプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat gray, dst;
    cvtColor(img, gray, COLOR_RGB2GRAY);
    threshold(gray, dst, 80, 200, 0);
    imshow("image",dst);
    waitKey(0);
}

画像を2値化するには、まずグレースケールに変換し、その画像に対してしきい値以上なら該当するピクセルを白、しきい値以下なら黒にする処理を行います。この処理はthreshold関数で行っています。

■threshold(src, dat, thresh, max, type)

引数 意味
src 入力画像
dst 出力画像
thresh 閾値
max 明度の最大値
type THRESH_BINARY
THRESH_BINARY_INV
・・・など

リサイズ

f:id:nn_hokuson:20170531195512j:plain:w300  f:id:nn_hokuson:20170531195512j:plain:w150
画像のサイズを変更するプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat dst;
    resize(img, dst, Size(150, 100));
    imshow("image",dst);
    waitKey(0);
}

画像のサイズを変更するにはresize関数の第3引数に、出力画像のサイズを指定します。あとはこの関数がメモリ割り当てなども自動的に行なってくれます。

■resize(src, dat, size)

引数 意味
src 入力画像
dst 出力画像
size 出力画像のサイズ

トリミング

f:id:nn_hokuson:20170531201225j:plain:w300   f:id:nn_hokuson:20170531201234j:plain
画像の一部をトリミングするためのプログラムです。

void main()
{
    Mat img = imread("img.jpg");
    Mat dst(img, Rect(50, 100, 200, 200));;
    imshow("result", dst);
    waitKey(0);
}

Matクラスのコンストラクタに元画像の変数と、トリミングする長方形をRect型で指定します。

図形を描画する

f:id:nn_hokuson:20170530234921p:plain:w300
円や長方形、直線などを描画するプログラムです。

void main()
{
    Mat img(512,512, CV_8UC3, Scalar(100, 100, 100));
    circle(img, Point(256, 256), 100, Scalar(200, 100, 100), 5);
    line(img, Point(0, 0), Point(512, 512), Scalar(0, 200, 0), 3, 4);
    rectangle(img, Point(100, 100), Point(300, 200), Scalar(0, 0, 200), 2, 4);
    imshow("image",img);
    waitKey(0);
}

図形を描画するためのキャンバスを最初に作ります。ここでは大きさを指定してMat型の変数を作成しています。
円を描くにはcircle関数、長方形はrectangle関数、線はline関数を使います。それぞれの関数の引数は次のようになります。図形を塗りつぶすにはticknessの値をマイナスにします。

■circle(img, center, radius, coor, tickness)

引数 意味
img 描画する画像
center 円の中心座標
radius 円の半径
color 円の色
tickness >=0: 線の太さ
<0: 塗りつぶし

■rectangle(img, pt1, pt2, coor, tickness)

引数 意味
img 描画する画像
pt1 左上座標
pt2 右下座標
color 円の色
tickness >=0: 線の太さ
<0: 塗りつぶし

■line(img, pt1, pt2, coor, tickness)

引数 意味
img 描画する画像
pt1 開始座標
pt2 終点座標
color 円の色
tickness >=0: 線の太さ
<0: 塗りつぶし

画像の保存

f:id:nn_hokuson:20170531201717j:plain:w300
画像を保存するプログラムです。

void main()
{
  Mat img = imread("img.png");
  imwrite("img.jpg", img);
}

画像を保存するにはimwriteを使用します。第1引数には保存ファイル名を指定します。拡張子によって自動的に保存形式が選択されます。

■imwrite(filename, img)

引数 意味
filename 保存するファイル名
img 保存する画像

画像の上下左右反転

f:id:nn_hokuson:20170529232958j:plain f:id:nn_hokuson:20170529233006j:plain
画像を上下反転・左右反転(フリップ)するプログラムです。

void main()
{
   Mat img = imread("img.png");
    Mat dst;
    flip(img, dst,0);
    imshow("image",dst);
    waitKey(0);
}

画像を反転するにはflip関数を使います。filp関数の第3引数に反転方向を指定します。反転方向の指定は次のようになります。

■flip(src, dat, code)

引数 意味
src 入力画像
dst 出力画像
code 反転方向の指定
> 0: 左右の反転
= 0 上下の反転
< 0 左右上下の反転

ぼかしフィルタ

f:id:nn_hokuson:20170529233430j:plain f:id:nn_hokuson:20170529233437j:plain
ボックスフィルタを使って画像をぼかすプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat dst;
    blur(img, dst, Size(7,7));
    imshow("image",dst);
    waitKey(0);
}

ボックスフィルタを使って画像をぼかすにはblur関数を使います。ボックスフィルタは周辺のピクセルの平均値を現在のピクセルの値にします。どの範囲の平均値をとるかは、第3引数にぼかしフィルタのカーネルサイズで指定します。

■blur(src, dat, ksize)

引数 意味
src 入力画像
dst 出力画像
ksize ボックスフィルタのカーネルサイズを指定します

ガウシアンフィルタ

f:id:nn_hokuson:20170530065030j:plain f:id:nn_hokuson:20170530065053j:plain
ガウシアンフィルタを使って画像をぼかすプログラムです。

void main()
{
     Mat img = imread("img.png");
     Mat dst;
     GaussianBlur(img, dst, Size(7,7), 10, 10);
     imshow("image",dst);
     waitKey(0);
}

ガウシアンフィルタを使って画像をぼかすにはgaussianBlur関数を使います。ボックスフィルタに比べて綺麗にぼけ、またぼけの量を微調整できるのが特徴です。調整項目は次のようになります。

■gaussianBlur(src, dat, ksize, sigma)

引数 意味
src 入力画像
dst 出力画像
ksize ガウシアンフィルタのカーネルサイズを指定します
sigma ガウシアンフィルタのシグマ値を指定します

Sobelフィルタ

f:id:nn_hokuson:20170531202927j:plain:w300 f:id:nn_hokuson:20170531202936j:plain:w300
Sobelフィルタを使って画像から輪郭線を抽出するプログラムです。

void main()
{
    Mat dst, gray;
    Mat img = imread("img.jpg");
    cvtColor(img, gray, COLOR_RGB2GRAY);
    Sobel(gray, dst, -1, 1, 0);
    imshow("image",dst);
    waitKey(0);
}

Sobelフィルタを使うことで画像中からエッジを検出することが出来ます。第4引数と第5引数に横方向と縦方向の微分係数を指定します。上のサンプルでは(1, 0)を指定しているので、縦方向のエッジが検出できます。逆に(0, 1)を指定すると横方向のエッジを検出できます。

全てのエッジを検出するには、縦と横の両方向に処理が必要になります。Sobelフィルタの計算は一次微分なので、ノイズの影響を受けにくく比較的処理速度も速いです。

■sobel(src, dat, depth, dx, dy)

引数 意味
src 入力画像
dst 出力画像
depth 出力画像のビット深度
dx 横方向の微分係数
dy 縦方向の微分係数

Cannyフィルタ

f:id:nn_hokuson:20170531202927j:plain:w300 f:id:nn_hokuson:20170531204417j:plain:w300
Cannyフィルタを使って画像から輪郭線を抽出するプログラムです。

void main()
{
    Mat img = imread("img.jpg");
    Mat dst, gray;
    cvtColor(img, gray, COLOR_RGB2GRAY);
    Canny(gray, dst, 40, 150);
    imshow("image",dst);
    waitKey(0);
}

CannyフィルタはSobelフィルタやLaplacianフィルタと比べて非常に綺麗にエッジを検出することが出来ますが・・・それに比例して処理負荷と処理時間も大きくなります。とにかく綺麗にエッジを抽出したいときはCannyフィルタを使うと良いでしょう。

■ Canny(src, dat, thresold1, thresold2)

引数 意味
src 入力画像
dst 出力画像
thresold1 閾値1
thresold2 閾値2

透視変換

f:id:nn_hokuson:20170531205709j:plain:w300 f:id:nn_hokuson:20170531205722j:plain:w270
画像を透視変換によって変形するプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat dst;
    Point2f srcPoint[] =
    {
        Point(0,        0),
        Point(0,        img.cols),
        Point(img.rows, img.cols),
        Point(img.rows, 0),
    };
    Point2f dstPoint[] =
    {
        Point(100, 100),
        Point(0,   300),
        Point(400, 450),
        Point(500, 50),
    };

    Mat H = getPerspectiveTransform(srcPoint, dstPoint);
    warpPerspective( img, dst, H, Size(512, 512));
    imshow("image", dst);
    waitKey(0);
}

OpenCVで透視変換を行うためには、まず変換前の4点と変換後の4点の座標を指定します。これらの座標をgetPerspectiveTransform関数に渡すことで、透視変換行列が得られます。この透視変換行列をwarpPerspectiveに渡すことで、画像を変形することができます。

透視変換をよく使う場面としては、QRコードの解析などX軸やY軸に沿って処理を進めたいときに使います。オリジナル画像で処理をすすめると、数学的に大変になるので、一度透視変換で正方形に変形してから、処理をすることでプログラムが簡単になります。

■ getPerspectiveTransform(src, dat)

引数 意味
src 入力座標の配列
dst 出力座標の配列
戻り値 透視変換行列

■ warpPerspective(src, dat, M, size)

引数 意味
src 入力画像
dst 出力画像
M 透視変換行列
size 出力画像のサイズ

ヒストグラムの平滑化

f:id:nn_hokuson:20170530235904j:plain:w300 f:id:nn_hokuson:20170530235917j:plain:w300
グレースケール画像のヒストグラムを均一化するプログラムです。

void main
    Mat img = imread("img.png");
    Mat gray, dst;
    cvtColor(img, gray, COLOR_RGB2GRAY);
    equalizeHist(gray, dst);
    imshow("image",dst);
    waitKey(0);
}

ヒストグラムを均一化することで、とコントラストを向上させたり、画像の全体的な明るさのバランスを改善することが出来ます。変換前のヒストグラム(左)と変換後のヒストグラム(右)は次のようになります。

f:id:nn_hokuson:20170531210247p:plain:w200 f:id:nn_hokuson:20170531210253p:plain:w200

均一化したことで、ヒストグラムが左右に広がって平らになったことがわかると思います。OpneCV3でヒストグラムの均一化を行うためにはequalizeHist関数を使います。

■ equalizeHist(src, dat)

引数 意味
src 入力画像
dst 出力画像

参考図書

今回は次の本を参考にさせていただきました!

【Unity】シーンが遷移したことを検知する

あるシーンから別のシーンへ遷移するタイミングで何か処理を実行したい場合、シーンが遷移したことを検知する必要があります。Unity5.4からはシーン遷移にSceneManagerを使います。そして、このSceneMangerには「activeSceneChanged」「sceneLoaded」「sceneUnloaded」という3つのデリゲートが用意されています。

デリゲートとは特定のイベントが起こった場合に呼び出すメソッドを指定できる仕組みです。Unityではオブジェクト同士が衝突したときにはOnColliderEnterメソッドなどが自動的に呼ばれますが、このように既に決まったメソッドを呼び出すのではなく、プログラマが自由に呼ばれるメソッドを自由に決められる仕組みがデリゲートになります。

シーン遷移を検知するデリゲートと、登録したメソッドの呼ばれるタイミングは次のようになります。

デリゲート タイミング
activeSceneChanged アクティブなシーンが変更されたとき
sceneLoaded シーンが読み込まれたとき
sceneUnloaded シーンが破棄されたとき


これらのデリゲートの登録方法と、デリゲートの書き方は次のようになります。

ng System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Test : MonoBehaviour {

    void Start()
    {
        SceneManager.activeSceneChanged += OnActiveSceneChanged;
        SceneManager.sceneLoaded              += OnSceneLoaded;
        SceneManager.sceneUnloaded          += OnSceneUnloaded;    
    }

    void OnActiveSceneChanged( Scene prevScene, Scene nextScene )
    {
        Debug.Log ( prevScene.name + "->"  + nextScene.name );
    }

    void OnSceneLoaded( Scene scene, LoadSceneMode mode )
    {
        Debug.Log ( scene.name + " scene loaded");
    }

    void OnSceneUnloaded( Scene scene )
    {
        Debug.Log ( scene.name + " scene unloaded");
    }
}

Startメソッドの中で デリゲートに呼び出すメソッドを登録しています。Startメソッドに続く3つのメソッドがデリゲートになります。デリゲートの名前は自由に決めてOKですが、戻り値と引数の型は決まっているので注意して下さい。