エンジニア

マテリアルIDマップを生成して、エッジを検出する

投稿日:2014年6月25日 更新日:

エンジニアブログを担当する廣田です。

今回はMaterialにIDを割り振り、 Material ID Mapを作成してエッジの検出をしてみようと思います。
エッジ検出には法線マップや深度マップを利用した物もありますが、
マテリアルの境界に線をつける事で綺麗に見えるとの事なので、今回はマテリアルIDエッジを選びました。
ポストエフェクトとして実装するので、UnityProライセンスが必要となります。

◆Material ID Mapの作成

今回はシェーダーTagを使い、描画する際にシェーダーを置換してIDMapを実装します。
まずはMaterial ID Map用のスクリプトを作成します。

MaterialIDRender.cs

/// <summary>  
/// マテリアルIDレンダー  
/// </summary>  
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Camera))]
public class MaterialIDRender : MonoBehaviour
{
   #region 変数  

    // 置き換えシェーダー  
    [SerializeField]
    private Shader replaceShader = null;

    // マテリアルID用レンダーテクスチャ  
    private RenderTexture texture;

    // メインカメラ  
    private Camera mainCam;

    // マテリアルIDカメラ  
    private Camera materialIDCam;

    // メインカメラトランスフォーム  
    private Transform mainCamTrans;

    // マテリアルIDカメラトランスフォーム  
    private Transform materialIDCamTrans;

   #endregion

   #region プロパティ  

    /// <summary>  
    /// マテリアルID用レンダーテクスチャプロパティ  
    /// </summary>  
    /// <value>マテリアルIDマップ</value>  
    public RenderTexture MaterialIDMap
    {
        get{ return texture; }
    }

   #endregion

    /// <summary>  
    /// 初期化処理  
    /// </summary>  
    private void Awake()
    {
        // レンダーテクスチャを作成  
        texture = new RenderTexture (Screen.width, Screen.height, 24, RenderTextureFormat.Default);
        texture.enableRandomWrite = false;
        texture.Create ();

        // 各種カメラを取得  
        mainCam = Camera.main;
        materialIDCam = camera;

        // メインカメラのデータをコピー  
        materialIDCam.CopyFrom (mainCam);

        // カメラのトランスフォームを取得  
        mainCamTrans = mainCam.transform;
        materialIDCamTrans = materialIDCam.transform;

        // マテリアルIDカメラにレンダーテクスチャをセット  
        materialIDCam.targetTexture = texture;

        materialIDCam.depthTextureMode = DepthTextureMode.None;

        // マテリアルIDカメラにクリアフラグをセット  
        materialIDCam.clearFlags = CameraClearFlags.SolidColor;
        materialIDCam.backgroundColor = new Color (0.0f, 0.0f, 0.0f, 0.0f);

        // マテリアルIDカメラに置き換えシェーダーをセット、Tagは"MaterialID"  
        materialIDCam.SetReplacementShader (replaceShader, "MaterialID");
    }

    /// <summary>  
    /// 破棄処理  
    /// </summary>  
    private void OnDestroy()
    {
        Destroy (texture);
    }

    /// <summary>  
    /// 定期更新処理  
    /// </summary>  
    private void FixedUpdate()
    {
        // スクリーンサイズが変更されたら、レンダーテクスチャを作り直す  
        if (texture.width != Screen.width ||
            texture.height != Screen.height)
        {
            Destroy(texture);
            texture = new RenderTexture (Screen.width, Screen.height, 24, RenderTextureFormat.Default);
            texture.enableRandomWrite = false;
            texture.Create ();

            // マテリアルIDカメラにレンダーテクスチャをセット  
            materialIDCam.targetTexture = texture;

        }
    }

    /// <summary>  
    /// 描画前処理  
    /// </summary>  
    private void OnPreRender()
    {
        // カメラを更新  
        UpdateCamera ();
    }

    /// <summary>  
    /// カメラのアップデート  
    /// </summary>  
    private void UpdateCamera()
    {
        // カメラを更新  
        materialIDCamTrans.position = mainCamTrans.position;
        materialIDCamTrans.rotation = mainCamTrans.rotation;

        materialIDCam.rect = mainCam.rect;
        materialIDCam.fieldOfView = mainCam.fieldOfView;
        materialIDCam.nearClipPlane = mainCam.nearClipPlane;
        materialIDCam.farClipPlane = mainCam.farClipPlane;
    }
}

このスクリプトは、"MaterialID"のシェーダータグを持つシェーダーを置き換えてIDを書き出しています。

// マテリアルIDカメラに置き換えシェーダーをセット、Tagは"MaterialID"  
materialIDCam.SetReplacementShader (replaceShader, "MaterialID");

特定のタグを持つシェーダーを置き換えるには、CameraのSetReplacementShaderメソッドを使用します。
第1引数には置き換えるシェーダーを、第2引数には置き換え用のシェーダータグを指定します。
(http://docs.unity3d.com/ScriptReference/Camera.SetReplacementShader.html)

Tags { "MaterialID"="Render" }

上記の様にシェーダーで "キー"="値" としてタグを設定します。
この様にタグを指定すると、後から特定のタグを持っているシェーダーだけ置き換えるという事ができます。

次にMaterialIDを作成するシェーダーです。

MaterialIDDiffuse.shader

Shader "MaterialID/Diffuse"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Id ("MaterialID(0 - 30000)", float) = 0
    }

    SubShader
    {
        // タグに "キー"="値" を設定しておく、今回は"MaterialID"  
        Tags { "MaterialID"="Render" }
        LOD 200

        CGPROGRAM
       #pragma surface surf Lambert  

        sampler2D _MainTex; // テクスチャ  
        float _Id;          // マテリアルID  

        // 入力用構造体  
        struct Input
        {
            float2 uv_MainTex;
        };

        // サーフェイスシェーダ  
        void surf (Input IN, inout SurfaceOutput o)
        {
            half4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

上記のシェーダーが実際にディフューズとして使うシェーダーです。
タグとIDプロパティ/変数を追加している以外は初期のままです。
なので、既存のシェーダーにタグとIDプロパティ/変数を追加すれば基本的には使える様になります。
このシェーダーが後に下記のシェーダーに置き換わります。

MaterialIDRender.shader

Shader "MaterialID/MaterialIDRender"
{
    Properties
    {
    }

    CGINCLUDE
   #include "UnityCG.cginc"

    float _Id; // マテリアルID  

    // データ構造体  
    struct v2f
    {
        float4 pos : SV_POSITION;
        float4 materialID  : COLOR0;
    };

    // 頂点シェーダー  
    v2f vert( appdata_img v )
    {
        v2f o;
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

        float id = clamp(_Id, 0.0, 30000.0);

        // マテリアルIDをアルファに格納  
        o.materialID.a = id / 30000.0;

        return o;
    }

    // フラグメントシェーダー  
    half4 frag (v2f i) : COLOR
    {
        return i.materialID;
    }

    ENDCG

    SubShader
    {
        // タグに "キー"="値" を設定しておく、今回は"MaterialID"  
        Tags { "MaterialID"="Render" }
        LOD 200

        Pass
        {
            CGPROGRAM
           #pragma vertex vert  
           #pragma fragment frag  
           #pragma fragmentoption ARB_precision_hint_fastest  
           #pragma target 3.0  

            ENDCG
        }
    }

    FallBack "Diffuse"
}

上記が置き換え用のシェーダーです。
やっている事は単純で、マテリアルIDを0〜1の範囲に収めてアルファ値として書き込んでいます。
これでマテリアルIDを書き出す準備ができました。

マテリアルIDマップを生成するには、マップ生成用のカメラを作成します。
作成したカメラにMaterialIDRenderコンポーネントを追加して、
置き換え用のシェーダー(MaterialIDRender.shader)をインスペクタからセットすれば完了です。

◆エッジ検出

次にマップを元にエッジを検出する機能です。
今回はエッジ検出に8近傍ラプラシアンフィルタを使用しました。

-1 -1 -1
-1 8 -1
-1 -1 -1

詳しい解説は割愛しますが、
このような係数をかけて、その結果を使用してエッジを検出しています。

8近傍以外にも4近傍のラプラシアンフィルタもあります。

0 -1 0
-1 4 -1
0 -1 0

(エッジ検出について:http://ja.wikipedia.org/wiki/エッジ検出)

それでは、エッジ検出の機能を実装しましょう。
まずはメインカメラにセットするエッジ検出コンポーネントを作成します。

MaterialIDEdge.cs

using UnityEngine;
using System.Collections;

public class MaterialIDEdge : MonoBehaviour
{
   #region 変数  

    // マテリアルIDエッジシェーダー  
    [SerializeField]
    private Shader materialEdgeShader = null;

    // マテリアルIDカメラ  
    [SerializeField]
    private Transform materialIDCamera = null;

    // ラインカラー  
    [SerializeField]
    private Color lineColor = Color.black;

    // ラインの閾値  
    [SerializeField]
    [Range(0.0001f, 1.0f)]
    private float threshold = 0.01f;

    // ラプラシアン係数の倍率  
    [SerializeField]
    [Range(1.0f, 10.0f)]
    private float amountMagnification = 1.0f;

    // ラインサイズ  
    [SerializeField]
    [Range(0.0f, 10.0f)]
    private float lineSize = 1.0f;

    // エッジのみを表示するかのフラグ  
    [SerializeField]
    private bool isShowEdge = false;

    // シェーダーマテリアル  
    private Material shaderMat = null;

    // マテリアルIDレンダー  
    private MaterialIDRender materialIdRender = null;

   #endregion

   #region メソッド  

    /// <summary>  
    /// 初期化処理  
    /// </summary>  
    private void Awake()
    {
        if (camera == null)
        {
            Debug.Log ("カメラコンポーネントが存在しません。");
            Destroy (this);
            return;
        }

        if (materialIDCamera == null)
        {
            Debug.Log ("マテリアルIDカメラが存在しません。");
            Destroy (this);
            return;
        }

        // マテリアルIDレンダーを取得  
        materialIdRender = materialIDCamera.GetComponent<MaterialIDRender> ();

        if (materialIdRender == null)
        {
            Debug.Log ("マテリアルIDレンダーが存在しません。");
            Destroy (this);
            return;
        }

        // シェーダーマテリアルの作成  
        if (materialEdgeShader)
        {
            shaderMat = new Material (materialEdgeShader);
        }
    }

    /// <summary>  
    /// 破棄処理  
    /// </summary>  
    private void OnDestroy()
    {
        DestroyImmediate (shaderMat);
    }

    /// <summary>  
    /// レンダーイメージイベント  
    /// </summary>  
    /// <param name="src">ソースレンダーテクスチャ</param>  
    /// <param name="dest">デスティネーションレンダーテクスチャ</param>  
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        // シェーダに各種データを設定  
        shaderMat.SetTexture ("_MaterialIDMap", materialIdRender.MaterialIDMap);
        shaderMat.SetColor ("_LineColor", lineColor);
        shaderMat.SetFloat ("_LineSize", lineSize);
        shaderMat.SetFloat ("_Threshold", threshold);
        shaderMat.SetFloat ("_AmountMagnification", amountMagnification);

        if (isShowEdge)
        {
            shaderMat.SetFloat ("_IsShowEdge", 1.0f);
        }
        else
        {
            shaderMat.SetFloat ("_IsShowEdge", 0.0f);
        }

        // シェーダーを使って書き込み  
        Graphics.Blit (src, dest, shaderMat);
    }

   #endregion
}

上記のコンポーネントはOnRenderImageでエッジ検出シェーダーに各種データを設定しています。

次にエッジ検出シェーダーです。

MaterialIDEdge.shader

Shader "Hidden/MaterialIDEdge"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "" {}
        _MaterialIDMap ("Material ID Map (RGB)", 2D) = "" {}
    }

    CGINCLUDE
   #include "UnityCG.cginc"

    // データ構造体  
    struct v2f
    {
        float4 pos : SV_POSITION;
        float2 uv  : TEXCOORD0;
    };

    sampler2D _MainTex;         // テクスチャ  
    sampler2D _MaterialIDMap;   // マテリアルIDマップテクスチャ  
    float4 _MainTex_TexelSize;  // テクセルサイズ  
    float _LineSize;            // ラインサイズ  
    float4 _LineColor;          // ラインカラー  
    float _Threshold;           // ラプラシアンの閾値  
    float _AmountMagnification; // ラプラシアンの倍率  
    float _IsShowEdge;          // エッジ表示フラグ  

    // 頂点シェーダー  
    v2f vert( appdata_img v )
    {
        v2f o;
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
        o.uv =  v.texcoord.xy;

        return o;
    }

    // フラグメントシェーダー  
    half4 frag (v2f i) : COLOR
    {
        // テクセルサイズを計算  
        float2 texelSize = _MainTex_TexelSize * _LineSize;

        // テクスチャカラーとエッジ表示用カラーを作成  
        half4 c = tex2D (_MainTex, i.uv);
        half4 edge = float4(1.0, 1.0, 1.0, 1.0);

        float amount = 0;

        // 8近傍で係数を取得  
        amount += tex2D (_MaterialIDMap, i.uv).a * 8.0;
        amount += -tex2D (_MaterialIDMap, i.uv + float2( texelSize.x,  texelSize.y)).a;
        amount += -tex2D (_MaterialIDMap, i.uv + float2(-texelSize.x, -texelSize.y)).a;
        amount += -tex2D (_MaterialIDMap, i.uv + float2(-texelSize.x,  texelSize.y)).a;
        amount += -tex2D (_MaterialIDMap, i.uv + float2( texelSize.x, -texelSize.y)).a;
        amount += -tex2D (_MaterialIDMap, i.uv + float2( 0.0,  texelSize.y)).a;
        amount += -tex2D (_MaterialIDMap, i.uv + float2( 0.0, -texelSize.y)).a;
        amount += -tex2D (_MaterialIDMap, i.uv + float2( texelSize.x,  0.0)).a;
        amount += -tex2D (_MaterialIDMap, i.uv + float2(-texelSize.x,  0.0)).a;

        // 係数が指定の閾値を超えたら、エッジとする  
        if(abs(amount) * _AmountMagnification >= _Threshold)
        {
            c.rgb = _LineColor.rgb;
            edge.rgb = _LineColor.rgb;
        }

        return lerp(c, edge, _IsShowEdge);
    }

    ENDCG

    SubShader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }

            CGPROGRAM

           #pragma vertex vert  
           #pragma fragment frag  
           #pragma fragmentoption ARB_precision_hint_fastest  
           #pragma target 3.0  

            ENDCG
        }
    }
    Fallback off
}

このシェーダーはマテリアルIDマップを使用してラプラシアンフィルタをかけてエッジの検出をしています。
これでエッジを検出する為の準備が整いました。
実際に使用するにはメインカメラにMaterialIDEdgeコンポーネントを追加して、
インスペクタ上でMaterialIDEdge.shaderとマテリアルマップ用カメラを設定してやるだけです。

実行すると分かりますが、一部エッジが出ないない部分があります(球同士やキューブ同士等)。
これは設定しているマテリアルIDが同一の為です。
この様にマテリアルIDを同じにしてやると、その境界にはエッジが出ません。
これを利用する事で、出したい場所や出したくない場所を指定する事ができる様になります。

さて、荒削りでまだまだ改良の余地がありますが、
とりあえずマテリアルIDマップを生成してエッジの検出をすることができました。
今は1マテリアルにつき1IDですが、マテリアルに最初からマテリアル用のIDマップを持たせる事によって、
1つのマテリアルでも複数のIDを持たせたりする事も可能です。
これを使用する事で特定の部位のIDを共通化させ、エッジを検出させない等のコントロールができます。
深度エッジや法線エッジと組み合わせるのもよさげです。

次回も、Unityで何かしらやってみようと思います。
それではまた次回!

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア
-

© WonderPlanet Inc.