おもちゃラボ

Unityで遊びを作ってます

【Unityシェーダ入門】デプスバッファの内容を表示する

デプスバッファ(深度バッファ、Zバッファ)とはカメラからオブジェクトまでの距離(深度値、Z値)を格納したバッファになります。

f:id:nn_hokuson:20180208191631j:plain:w600

このデプスバッファは被写界深度のエフェクトや影の計算など、さまざまな場面で使われています。Unityを使っていると影やエフェクトなどは自動的に計算してくれるので、あまり馴染みがないかもしれません・・・

ただ、エフェクトを作るときに必要になることが時々あります。そこで、ここではデプスバッファ(Zバッファ)の内容を表示する方法を書いていきます。

デプスバッファの値を表示する流れ

上にも書いたとおり、デプスバッファとはカメラからオブジェクトまでの距離を格納したバッファです。この距離をZ値といいUnityの場合0〜1で取得できます。カメラからの距離が近ければ0、遠くなるに従って値が大きくなっていきます。つまり、近くにあるものほど黒く表示されるのですね。

f:id:nn_hokuson:20180208193944j:plain

デプスバッファの値はシェーダの_CameraDepthTexture変数で取得できます。そこで、この値を表示するまでの流れは次のようになります。

  1. Z値を取得して表示するシェーダを作る
  2. ポストエフェクト用のC#スクリプトを作る
  3. C#スクリプトにdepthシェーダを指定する

それでは1ステップずつ見ていきましょう。

Z値を取得して表示するシェーダを作る

まずはデプスバッファから値(Z値)を取得し表示するシェーダを作成しましょう。プロジェクトウィンドウで右クリックし、Create→Shader→Unlit Shaderを選択して、depth.shaderという名前で保存してください。また、作成したdepth.shaderを右クリックしてCreate→MaterialでUnlit_depth.matというマテリアルを作成しておきます。

f:id:nn_hokuson:20180208192022p:plain:w200

続いて今作成したdepth.shaderを開いて次のプログラムを入力してください。

Shader "depth"
{
        SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _CameraDepthTexture;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture,i.uv));
                return fixed4(depth,depth,depth,1);
            }
            ENDCG
        }
    }
}

通常の頂点・フラグメントシェーダを見慣れている方にとっては非常にシンプルな内容になっていると思います。

特徴としては、sampler2D _CameraDepthTexture;としてデプスバッファのテクスチャを宣言し、フラグメントシェーダでtex2D関数を使ってデプスバッファからZ値を取り出しています。最後にデプス値をRGBAの色に変換して出力しているだけです。

ポストエフェクト用のC#スクリプトを作る

デプスバッファの内容はポストエフェクトとして表示するのでした。そこで、ポストエフェクト用のスクリプトを作りましょう。

プロジェクトウィンドウで右クリックし、Create→Script→C# Scriptを選択して、DispDepth.csというファイルを作成してください。

作成したC#スクリプトに次のプログラムを入力します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class DispDepth : MonoBehaviour {
    public Material mat;
    void Start () 
    {
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
    }

    public void OnRenderImage(RenderTexture source, RenderTexture dest)
    {                
        Graphics.Blit(source, dest, mat);
    }
}

OnRenderImageの中でGraphics.Blitメソッドを使ってポストエフェクトをかける方法は定石です。ポストエフェクトの基本的な方法は次の記事を参考にしてください。

nn-hokuson.hatenablog.com

簡単に説明すると、OnRenderImage の第一引数であるsourceにはレンダリング後の画像が入っていいます。このsource画像に対してmatで指定したシェーダの処理をして最終出力結果をdestに出力するイメージです。

f:id:nn_hokuson:20180208194757j:plain:w650

またStartメソッドの中で、カメラがデプステクスチャを生成するように次の一行を追加しています。これを書かないと_CameraDepthTextureでデプス値を取得することが出来ません。

_camera.depthTextureMode |= DepthTextureMode.Depth;

C#スクリプトにdepthシェーダを指定する

DispDepth.csのスクリプトが入力できたら、これをヒエラルキービューのMain Cameraにドラッグ&ドロップしてアタッチしましょう。

f:id:nn_hokuson:20180208192639j:plain

最後にスクリプト中のMat変数にdepth.matを指定します。Main CameraにアタッチしたDepthスクリプトのMat欄にdepth.matをドラッグ&ドロップしてください。

f:id:nn_hokuson:20180208192923j:plain

実行すると次のようにゲーム画面にデプスバッファの値が表示されます。

f:id:nn_hokuson:20180208192958p:plain:w400

デプスバッファを使った応用例は西川善司さんの「ゲーム制作者になるための3Dグラフィックス技術」にいくつか例が載っています。合わせて参考にしてみてください。

[asin:4844333542:detail]
[asin:4844329510:detail]

【Unityシェーダ入門】アウトラインシェーダを作る

トゥーンシェーダなどを使ってアニメ調の塗りをする場合、モデルにアウトラインを付けたいことがあります。ここでは、モデルと背景の境界線上にアウトラインを表示するシェーダを作る方法を紹介します。

f:id:nn_hokuson:20180205200341j:plain

今回の記事の内容は次のとおりです。

アウトラインの概要

モデルにアウトラインをつけるには次のように幾つかの方法があります。

  • ステンシルバッファを使う
  • 法線情報から境界を計算する
  • 少し膨らませたモデルを使う
  • ポストエフェクトで輪郭抽出する

ここでは直感的に分かりやすい3つめの「少し膨らませたモデルを使う」方法を紹介したいと思います。この方法は2パス(モデルを2回描画する)でアウトラインを描画します。

まず1パス目では、法線方向にモデルを少し膨らませてから、モデルの裏面を塗りつぶしで描画します。これが輪郭線になります。

f:id:nn_hokuson:20180206194757p:plain:w500

続いて2パス目では通常通りモデルを描画します。このときモデルの表面のみを描画するようにします。1パス目では少し大きめのモデルを描画しているため、この部分がはみ出てアウトラインになります

f:id:nn_hokuson:20180206194804p:plain:w500

このように、1パス目で法線方向に拡大したモデルを塗りつぶして描画し、2パス目で通常通り描画することで、アウトラインのついたモデルを作ることが出来ます。直感的に理解しやすいですね!

また、ステンシルバッファを使ってアウトラインを表示する方法は(Unityについての記事ではありませんが)こちらの記事が参考になるかと思います。

wgld.org

ポストエフェクトとして輪郭抽出する方法は「DirectX 9 シェーダプログラミングブック」で詳しく解説されています(こちらもUnityではありませが)

[asin:4839912475:detail]

それでは実際にシェーダを書いていきましょう。

シェーダファイルの準備

続いてアウトライン用のシェーダを作ります。プロジェクトビューで右クリック→Create→Shader→Unlit Shaderを選択し、作成したファイル名をoutline.shaderに変更します。

続いてこのシェーダファイルをアタッチするマテリアルも作成しましょう。outline.shaderを選択した状態で右クリック→Create→Materialを選択すると、Unlit_outlineというマテリアルが作成されます。

f:id:nn_hokuson:20180205200457p:plain:w200

outline.shaderが作成できたら、次のシェーダプログラムを入力してください。ここでは頂点・フラグメントシェーダを使って記述しています。

Shader "outline"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Cull Front

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                v.vertex += float4(v.normal * 0.04f, 0);   
                o.vertex = UnityObjectToClipPos(v.vertex); 
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = fixed4(0.1,0.1,0.1,1);                
                return col;
            }
            ENDCG
        }

        Pass
        {
            Cull Back

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {                
                half nl = max(0, dot(i.normal, _WorldSpaceLightPos0.xyz));
                if( nl <= 0.01f ) nl = 0.1f;
                else if( nl <= 0.3f ) nl = 0.3f;
                else nl = 1.0f;
                fixed4 col = fixed4(nl, nl, nl, 1);
                return col;
            }
            ENDCG
        }
    }

1パス目の頂点シェーダではモデルを頂点方向にすこし膨らませています。ここでは裏面のみを描画するため「Cull Front」を指定していることに注意してください。

頂点シェーダに入力される頂点座標と法線方向はローカル座標系です。そこでローカル座標系でモデルを膨張させてから、UnityObjectToClipPos関数を使って頂点座標をワールド座標系に変換しています。

1パス目のフラグメントシェーダは、特にシェーディングの計算などはせず、黒色でベタ塗りしているだけです。このフラグメントシェーダで指定した色がアウトラインの色になります。

続いて2パス目ではモデルを通常通り描画します。ここではトゥーン調の表示にするためフラグメントシェーダでランバートの計算をした後、3段階に階調化して表示しています。トゥーンシェーダについては次の記事で解説しているので合わせて参考にしてください。

nn-hokuson.hatenablog.com

モデルにマテリアルをアタッチする

アウトラインシェーダが作れたら、マテリアルをモデルにアタッチしてみてください。ここでは球とトーラスをシーンに配置して、そこにoutlineマテリアルをドラッグ&ドロップしました。
f:id:nn_hokuson:20180205201651j:plain:w570

アウトラインシェーダの描画結果はこのようになりました。アウトラインがあるだけで、通常のトゥーンシェーダよりも引き締まって見えますね!

f:id:nn_hokuson:20180205200341j:plain:w550

まとめ

今回はトゥーンシェーダと合わせて使えるアウトラインシェーダを紹介しました。アウトラインを描画するには2パス必要にはなるものの、シェーダプログラムは分かりやすかったのではないでしょうか。次はステンシルバッファを使ったアウトラインシェーダも紹介したいと思います。

[asin:B01ET5I2FQ:detail]

【Unity】シリアル通信でハマるポイント3選

Unityでシリアル通信をするときに、良くハマるポイントをまとめました。案外シリアル通信には落とし穴がたくさんある・・・のです。転ばぬ先の杖ということで、ご査収ください(笑)

Unityでシリアル通信を使うときの具体的なプログラムはこちらの記事を参考にして下さい。

nn-hokuson.hatenablog.com

では、1つめの落とし穴からいきます。

シリアルポートのオープンに失敗する

ちゃんとシリアルデバイスを繋いでいるのに、SerialPort.Openでエラーになるケースです。Windowsでシリアル通信をする場合、COM[数字]という名前をよく使いますが、UnityではCOM0〜COM9までしか認識することができません。

その場合はCOMポート名を修正するのが手っ取り早いです。ポート名を変えるには「デスクトップ左下のWindowsマークを右クリック」→「デバイスマネージャ」からシリアルポート名を右クリックしてプロパティウインドウを表示します。「ポートの設定」→「詳細設定」→「COMポート番号」と進み、COM9以下のポートを選択しましょう。

f:id:nn_hokuson:20180201202240j:plain:w300   f:id:nn_hokuson:20180201202254j:plain:w300

Serialクラスを使うとコンパイルエラーがでる

Unityでは標準で.NET 2.0のサブセットを使うように設定されていますが、サブセットにはSerialPortクラスが含まれていません。そのため「using System;」としても次のようなエラーが出ます。

error CS0246: The type or namespace name `SerialPort' could not be found. Are you missing an assembly reference?

f:id:nn_hokuson:20180201202842p:plain

したがって、シリアル通信を使う場合は使用する.NETのバージョンを.NET 2.0 Subsetから.NET 2.0に変更する必要があります。

メニューバーからEdit -> Project Settings -> Playerを選択し、API Compatibility Levelを.NET 2.0に変更してください。

f:id:nn_hokuson:20170912190634p:plain:w350

通信中にフリーズする

UnityでSerialPortクラスのReadメソッドやByteToReadメソッドを使うとフリーズすることがあります。ReadLineメソッドを使うとフリーズを回避できます。

(下の記事ではReadTimeout=1としたときにReadLineがフリーズすると書かれていましたが、2018年2月現在問題なく動作しています)
qiita.com

まとめ

ということで、Unityのシリアル通信には落とし穴がたくさんあります!というお話でした。でもArduinoなど外部デバイスと通信するにはシリアル通信が手軽ですし、出来ることの幅も広がるので、積極的に使っていきたいですね〜