CA.unity #5 でシェーダーの発表をしました

CA.unity #5 でシェーダーの発表をしました

はじめに

第二事業部 エンジニアの永留(ながとめ)と申します。
普段は、Unityを用いたゲーム開発に従事しています。

CyberAgentさんが主催するUnityの勉強会 CA.untiy #5にて、シェーダーの発表をさせていただきました。

CA.unity #5

発表について

大量のシェーダーを1つにまとめるという業務を行う過程で得たノウハウについてお話ししました。

発表は以下になります

キャラクター向けの汎用トゥーンシェーダーを作った話

発表を行った理由について

大量のシェーダーを1つにまとめるという業務を行う過程で、様々なノウハウを得られたと感じました。
シェーダーに関する技術ノウハウはまだまだ少なく、これを対外向けに発表する価値は高いと感じたため、発表を行うことにいたしました。

実装の細かい話

発表は、15分という制約があったため、細かい実装については説明しきれませんでした。
今回、実装の細かい話について紹介したいと思います。

シェーダーの実装について

汎用キャラシェーダー(UberToon.shader)は、主に3つのパスで構成されています。

パス1 : 輪郭線の描画を行うOutlineパス
パス2 : キャラクターの面の描画を行うForwardパス
パス3 : シャドウマップのレンダリングを行う ShadowCasterパス

それぞれのパスの実装自体は、cgincファイル内に記述されています。

 

シェーダーコード

UberToon.shader の実装コードは以下のようなものになっています。
シェーダーが長いので、途中を省略しています。 (…と書かれた部分が省略した箇所になります)

それぞれのPassで cgincファイルをincludeしています。

Shader "Aiming/Character/UberToon" {
    Properties
    {
      ...
    }

    SubShader {
        Tags {
            "RenderType"="Opaque"
        }
        Pass {
            Name "Outline"
            ...
            CGPROGRAM
            ...
            #include "UberToon_Outline.cginc"
            ...
            ENDCG
        }
            
        Pass {
            Name "FORWARD"
            ...
            CGPROGRAM
            ...
            #include "UberToon_Forward.cginc"
            ...
            ENDCG
        }

        Pass {
            Name "ShadowCaster"
            ...
            CGPROGRAM
            ...
            #include "UberToon_ShadowCaster.cginc"
            ...
            ENDCG
        }
    }
    FallBack "Legacy Shaders/VertexLit"
    CustomEditor "Aiming.Div2.UberToonShader.Editor.UberToonGUI"
}
    

 

頂点データ表示機能の実装の紹介

発表では、頂点データのデバッグ表示機能を紹介しました。
こちらの実装の詳細について紹介いたします。

シェーダーキーワードの定義(.cginc)

頂点データの表示機能は、UberToon_Forward.cginc(キャラクターの面の描画シェーダー)に実装しています。

まず、UberToon_Forward.cgincの先頭に、以下のようなshader_feature を列挙します。

#pragma shader_feature DEBUG_VERTEX_COLOR // 頂点カラー表示
#pragma shader_feature DEBUG_VERTEX_WORLD_NORMAL // 法線(ワールドスペース)の表示
#pragma shader_feature DEBUG_VERTEX_OBJECT_NORMAL // 法線(オブジェクトスペース)の表示
#pragma shader_feature DEBUG_VERTEX_UVNORMAL // UVセットに格納された法線の表示
#pragma shader_feature DEBUG_VERTEX_UV1 // UV1の表示
#pragma shader_feature DEBUG_VERTEX_UV2 // UV2の表示
#pragma shader_feature DEBUG_VERTEX_UV3 // UV3の表示
#pragma shader_feature DEBUG_VERTEX_UV4 // UV4の表示
#pragma shader_feature DEBUG_VERTEX_NL // 頂点normalとライトベクトルの内積を表示

 

シェーダーバリアントの実装(.cginc)

次に、frag関数内にデバッグ用の処理を埋め込みます。
以下は、 DEBUG_VERTEX_COLOR というシェーダーキーワードが有効だった場合に、
頂点カラーをデバッグ表示する実装例になります。

VertexOutput vert (VertexInput v) {
    VertexOutput o = (VertexOutput)0;
    o.vertex = mul(unity_ObjectToWorld, v.vertex);

    #if defined(DEBUG_VERTEX_COLOR)
        o.color = v.color; // デバッグ用に表示したい情報を格納
    #endif   
    
    return o;
}

float4 frag(VertexOutput i, fixed facing : VFACE) : SV_TARGET {
    #if defined(DEBUG_VERTEX_COLOR)
            return i.color; // デバッグ用に表示したい情報が入っている
    #endif

   (通常のシェーディング処理)
}

 

実際は、表示したい頂点データ分のシェーダーバリアントを定義しています。
デバッグ機能が有効な場合に、i.colorにデバッグで表示したい情報が入っています。

// フラグメントシェーダー
float4 frag(VertexOutput_Forward i, fixed facing : VFACE) : SV_TARGET {
    // デバッグ表示が有効な場合、i.colorにデバッグで表示したい情報が入っている. これをそのままreturnすることで、表示したい情報を画面に出すことができる
    #if defined(DEBUG_VERTEX_COLOR)
            return i.color;
    #elif defined(DEBUG_VERTEX_WORLD_NORMAL)
            return i.color;
    #elif defined(DEBUG_VERTEX_OBJECT_NORMAL)
            return i.color;
    #elif defined(DEBUG_VERTEX_UVNORMAL)
            return i.color;
    #elif defined(DEBUG_VERTEX_UV1)
            return i.color;
    #elif defined(DEBUG_VERTEX_UV2)
            return i.color;
    #elif defined(DEBUG_VERTEX_UV3)
            return i.color;
    #elif defined(DEBUG_VERTEX_UV4)
            return i.color;
    #elif defined(DEBUG_VERTEX_NL)
            return i.color;
    #endif

    (通常のfrag処理)
}

シェーダー側の対応は以上になります。

enumの定義(C#)

シェーダーバリアントを切り替えるためには、エディター拡張用のC#コードを書く必要があります。

まず、デバッグ機能を指定するためのenumを定義します。

public enum DebugShowVertexMode
{
    None = 0, // なし
    COLOR, // 頂点カラー表示
    WORLD_NORMAL, // 法線の表示 (ワールドスペース)
    OBJECT_NORMAL, // 法線の表示 (オブジェクトスペース)
    UVNORMAL, // UVセットに格納された法線の表示 (UV3, UV4)
    UV1, // UV1の表示
    UV2, // UV2の表示
    UV3, // UV2の表示
    UV4, // UV2の表示
    NL, // 頂点normalとライトベクトルの内積を表示
}

 

デバッグ機能を切り替えるためのEditorWindowも作成します。

デバッグウィンドウの実装(C#)

using UnityEditor;
using UnityEngine;

namespace Aiming.Div2.UberToonShader.Editor
{
    /// <summary>
    /// UberToonShaderのデバッグ用のウィンドウ
    /// </summary>
    public class UberToonDebugWindow : EditorWindow
    {
        private DebugShowVertexMode _vertexMode = DebugShowVertexMode.None;
        
        public static class KeywordPrefix
        {
            public const string DebugShowVertex = "DEBUG_VERTEX";
        }
        
        public enum DebugShowVertexMode
        {
            None = 0, // なし
            COLOR, // 頂点カラー表示
            WORLD_NORMAL, // 法線の表示 (ワールドスペース)
            OBJECT_NORMAL, // 法線の表示 (オブジェクトスペース)
            UVNORMAL, // UVセットに格納された法線の表示 (UV3, UV4)
            UV1, // UV1の表示
            UV2, // UV2の表示
            UV3, // UV2の表示
            UV4, // UV2の表示
            NL, // 頂点normalとライトベクトルの内積を表示
        }

        [MenuItem("Tools/UberToon Debug Window")]
        public static void Open()
        {
            var window = GetWindow<UberToonDebugWindow>();
            window.titleContent = new GUIContent("UberToon Debug Window");
        }
        
        private void OnGUI()
        {
            EditorGUILayout.LabelField("■ メッシュの頂点データ デバッグ表示");
            _vertexMode = (DebugShowVertexMode)EditorGUILayout.EnumPopup("表示 対象", _vertexMode);
            
            if (GUILayout.Button("Set"))
            {
                SetKeywordEnum(KeywordPrefix.DebugShowVertex, (int)_vertexMode, typeof(DebugShowVertexMode));
                SceneView.RepaintAll();
            }
        }
    }
}

 

/// <summary>
/// ShaderのKeywordを設定
/// </summary>
public static void SetKeywordEnum(string keywordPrefix, int intValue, Type enumType)
{
    foreach (Enum item in System.Enum.GetValues(enumType))
    {
        var keyword = GetKeywordName(keywordPrefix, item);
        if (Convert.ToInt32(item) == intValue)
        {
            Shader.EnableKeyword(keyword);
            _logMessage.AppendLine($"Shader.EnableKeyword: {keyword}");
        }
        else
        {
            Shader.DisableKeyword(keyword);
            _logMessage.AppendLine($"Shader.DisableKeyword: {keyword}");
        }
    }
}

/// <summary>
/// シェーダーキーワード名を取得
/// </summary>
public static string GetKeywordName(string prefix, System.Enum item)
{
    var keyword = prefix + "_" + item;
    return keyword.ToUpper();
}

 

おわりに

弊社では随時、ゲームづくりへの情熱と、高い技術を追求する意欲を持ったエンジニアを募集しております。
興味のある方は是非、Aimingで一緒に働きましょう!

Aiming採用ページはこちら:
株式会社Aiming 採用情報