めーぷるのおもちゃばこ

- アイドルになりたいエンジニア女子の制作日記 -

【Unity】Shaderでオブジェクトの交差位置に色をつける

はじめに

 
オブジェクトとオブジェクトがぶつかる境目に色をつけるシェーダーの作り方を紹介します!🐼

f:id:maplesyrup-cs6:20200809142016g:plain
今回のイメージ図

  
今回のサンプルプロジェクトはこちら↓
github.com

 

作り方

デプスバッファ、つまりカメラからの深度値を使用します。
交差するオブジェクト(今回のイメージ図でいうウサギ)のカメラからの深度値とプレーンの深度値とを比較して、その距離に応じて色をつけます。

 

準備

まずシーンにプレーンとオブジェクトを用意し、交差するように配置します。

f:id:maplesyrup-cs6:20200809150451p:plain
Planeとオブジェクトの準備

  
ヒエラルキーからCreate> C# Scriptでスクリプトを作成し、DepthTexture以下を記述します。

using UnityEngine;

[ExecuteInEditMode]
public class DepthTexture : MonoBehaviour 
{
  private Camera cam;

  void Start () 
  {
    cam = GetComponent<Camera>();
    cam.depthTextureMode = DepthTextureMode.Depth;
  }
}

 
このスクリプトをカメラにセットします。
すると、Cameraコンポーネントの一番下のところに以下のような表示が現れます。

f:id:maplesyrup-cs6:20200809152009p:plain
深度テクスチャ生成モード

 
これによりカメラが深度テクスチャを生成します。
 
このとき交差対象のオブジェクトのマテリアルに適応するシェーダーは影を扱えるものでないといけません(つまりUnlitはだめ)。Unityのドキュメントによると、

デプステクスチャは、シャドウキャスターのレンダリングに使用するのと同じシェーダー内パスを使用してレンダリングされます(“ShadowCaster” pass type)。したがって拡張機能により、シェーダーがシャドウキャスティング(影付け)をサポートしていない場合(例えば、シェーダーかフォールバックのいずれにおいてもシャドウキャスターのパスがない)、そのシェーダーを使用するオブジェクトにデプステクスチャは表示されません。
参照元:
カメラの深度テクスチャ - Unity マニュアル

という理由があるからです。

  

シェーダーを書く

つづいてシェーダーを書いていきます。
プロジェクトウィンドウから Create>Shader>Unlit Shader でShaderをつくります。

Shader "Custom/DepthFade"
{
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)
        _EdgeColor("EdgeColor", Color) = (1, 1, 1, 1)
        _DepthFactor("Depth Factor", float) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            uniform sampler2D _CameraDepthTexture;
            fixed4 _Color; 
            fixed4 _EdgeColor; 
            float _DepthFactor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.screenPos = ComputeScreenPos(o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float4 depthSample = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos));
                half depth = LinearEyeDepth(depthSample);
                half screenDepth = depth - i.screenPos.w;
                float foamLine = 1 - saturate(_DepthFactor * screenDepth);
                fixed4 col = lerp(_Color, _EdgeColor, foamLine);
                // fixed4 col = _Color + foamLine * _EdgeColor;   //EdgeをEmissionにしたいばあいはこっち
                return col;
            }
            ENDCG
        }
    }
}

 
シェーダーが書けたら、シェーダーの上で右クリックからCreate>Materialとするとそのシェーダーが適用されたマテリアル が作成されます。
それをプレーンに貼り付けて、インスペクタから設定します。

f:id:maplesyrup-cs6:20200809162837p:plain
インスペクタから設定

  
Color:プレーンのベースの色です。
_EdgeColor:エッジの色です。
Depth Facor :エッジの色の範囲の調整
 
です。

  

シェーダーの解説

uniform sampler2D _CameraDepthTexture;

ここでデプスバッファのテクスチャを宣言しています。
  

o.screenPos = ComputeScreenPos(o.vertex);

ComputeScreenPos()はUnity.cgincに定義されている関数で、xyが0〜wに変換されるらしいです。

  

 SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos));

Z方向の膨らみを正規化することで2D平面(ディスプレイ)のどの位置に該当オブジェクトのピクセルがくるのかを計算しています。深度テクスチャをレンダリングしていると考えると良さそうです。プレーンはTransparentなので、ウサギより後にレンダリングされます。つまりこの時点ではウサギのみがレンダリングされている状態です。そのウサギのカメラ深度値をスクリーンポジションに変換しています。

   

half depth = LinearEyeDepth(depthSample);

LinearEyeDepth()はZ バッファ値をリニア化するために使用します。
先ほどのレンダリングされた深度テクスチャを渡すと各ピクセルのカメラからの深度を計算します。これで、ウサギのディスプレイ深度が算出されました。
  

half screenDepth = depth - i.screenPos.w;

depthがウサギの深度値なので、そこからi.screenPos.w(平面の深度値)を引くことで、プレーンとウサギとの距離がでます。距離がわかれば、交差している部分(つまり値が0の部分)もわかりますよね。


float foamLine = 1 - saturate(_DepthFactor * screenDepth);

上記の流れを経て算出されたscreenDepth_DepthFactorを掛けて調整しつつ、それを0から1になるようにクランプします。そしてこのままだと値がオブジェクトの交差部分に近くにつれて0になっていくのですが、扱いづらいのでオブジェクトと交差している部分を1、離れるほど値が0に近づくようにしたいので、1から引いています。

 
そして最後に出した値を色に反映させています。  

fixed4 col = lerp(_Color, _EdgeColor, foamLine);
// fixed4 col = _Color + foamLine * _EdgeColor;   //EdgeをEmissionにしたいばあいはこっち

lerpで線形補完させれば以下のように滑らかにエッジの色をつけることができます。

f:id:maplesyrup-cs6:20200809222619p:plain
線形補間させた見た目

    
ベースの色である_Colorに加算した場合はEmissionとしてエッジの色をくっきりつけることができます。

f:id:maplesyrup-cs6:20200809222645p:plain
_Colorに加算した見た目

  
どっちもかっこいいですね!
以上です!