LoginSignup
6
11

More than 3 years have passed since last update.

サーフェイスシェーダー入門編

Last updated at Posted at 2019-10-24

そもそもシェーダーとは?

一言で表すと「描画方法を記述したプログラム」

実際の処理を行うのはGPUが担当する
処理は並列(GPU自体が超並列特化のため)

使用する言語

UnityのShaderLabを使う。ShaderLabは宣言型言語※1。
Cg(C for Grapchics)※2、HLSL(High Level Shading Language)※3を組み合わせたようなものになっている
ShaderLabはOpenGL向けに書き出される端末の場合、HLSL2GLSL※4でコンパイルされ、GLSL※5に書き出される仕組みになっています。

※1 宣言型言語とは、プログラムの中で使用する変数や定数などの名称や領域、属性などを、あらかじめ定める必要がある言語。
※2 Cgとは、NVIDIAが開発していたシェーディング言語。C言語ベース 意味はそのままでグラフィックスのためのC言語
※3 HLSLとは、マクロソフトによって開発されたDirectXで使われるシェーディング言語 NVIDIAと協力して開発していたのでCgと似てる
※4 HLSL2GLSLとは、HLSL言語をGLSL言語にするトランスレータ(意味や内容を変えずに変換するもの)のこと。
※5 GLSLとは、OpenGLとの親和性を持つシェーダ記述言語で、C言語ライクな独自の文法によって記述します。

使えるシェーダー

Unityのシェーダーには4種類あります。

Surface Shader

Unity 独自色の強いシェーダで、端的には簡単なシェーダ

Vertex・Fragment / Unlit Shader

Unity上ではUnlit Shaderと書かれているが、一般的なバーテックスシェーダーとフラグメントシェーダー
バーテックスシェーダーでは頂点を動かして物を変形させたりアニメーションさせる。頂点に色が打たれていたらその色にする。
フラグメントシェーダーでは光や影の影響を考慮して色を付けたり。輪郭を付けたりボカしたりする。フラグメントを操作するシェーダー

Image Effect Shader

Image Effect Shader は最終的に出力されるカメラやテクスチャ画像に適用するためのシェーダ。ポストエフェクトと呼ばれることもあります。シェーダのソースコードの構造は Vertex Fragment Shader と同じで、多くの場合に Fragment Shader を編集し実装します。
例えば画面全体の色相(色調)を変更したり、画面全体を一律にぼかしたりすることができます。 それ以外にもシーン(画面)遷移するときの効果、フェードイン・アウト、モザイク、ディストーションなどが実現できます。

Compute Shader

最も高度で複雑なシェーダ
特定のプラットフォームで GPU を使うために利用したり、 あるいはレンダリングパイプラインとは異なる目的と手段によって GPU を使った処理を実現するために使います。

Surface Shader(サーフェイスシェーダー)について

Unity専用のシェーダー
光源の影響や影の付き方、細かい色の付き方など、細かいことを考えなくても良い利点があります。 一方でそれらを細かく制御することは困難になります。

それらを考えなくてもいい理由は、頂点シェーダーを省略できて、サーフェイスシェーダー用の命令で、フラグメントシェーダーを生成してくれるため
20191003-124820.jpg
https://www.youtube.com/watch?v=wUx_Y9BgC7k

高度なシェーダーが作りたい場合はVertex・Fragment / Unlit Shaderを使います。

使える変数の型

説明
float 高精度 浮動小数点値 32bit
複雑な関数などに利用
half 中精度 16bit
-60000 ~ 60000小数点以下約3桁
方向 オブジェクト空間位置に利用
fixed 低精度 11bit
-2.0 ~ 2.0 1/256精度
色情報 単純な制御に利用

こちらに例文含めて使える変数についていろいろ書かれています。
https://qiita.com/nmxi/items/bfd10a3b3f519878e74e

実際のコード

Shader "Custom/HogeHogeShader" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType" = "Opaque" }
        LOD 200
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0
        struct Input
        {
            float2 uv_MainTex;
        };

        sampler2D _MainTex;
        void surf(Input IN, inout SurfaceOutputStandard o)
        {
            half4 c = tex2D(_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Shader "Custom/HogeHogeShader" {}

シェーダーの名前

Properties {}

インスペクタ上から指定したものを設定できる。
変数名("Inspector上の表示名", 変数の種類) = デフォルト値
のルールで記述します。

以下使用できるもの

//Int
name ("display name", Int) = number

//Float
name ("display name", Float) = number

//Vector
name ("display name", Vector) = (number,number,number,number)

//Float Range
//インスペクタ上でスライドバーが_min_から_max_として表示されます
name ("display name", Range (min, max)) = number

//Color
//インスペクタ上でカラーピッカーが表示されます
name ("display name", Color) = (number,number,number,number)

//Texture 2D
name ("display name", 2D) = "name" { options }

//Texture 3D
name ("display name", 3D) = "name" { options }

//Cubemap
name ("display name", Cube) = "name" { options }

シェーダープロパティアトリビュート

[HideInInspector]_Hoge ("HogeValue", int) = 0

↑のHideInInspectorのようなものをシェーダープロパティアトリビュートといいます。
HideInInspectorと設定するとインスペクタ上から表示が消えるようになります。

他にも、EnumToggleを扱えるようにすることもできます。

シェーダプロパティアトリビュートについてはこちらが詳しく書いてあります。
https://qiita.com/luckin/items/96f0ce9e1ac86f9b51fc

SubShader {}

シェーダー本体を記述するところ。一つのSubShaderに1本分のシェーダコードが入ります。
SubShader枠が複数あると、Unityがそのシェーダーが動くか上から1つ1つチェックして動くものを適用してくれます。

FallBack

全て動かなかったら最後の"FallBack"に飛びます。
"FallBack"は滑り止めみたいなもので、すべてのシェーダーが動かなかった時、ここで指定したシェーダーを使うようにしてくれます。

fall.jpg
http://tsumikiseisaku.com/blog/shader-tutorial-lod/

Tags {}

Queue

Tags { "Queue" = "Transparent" }

レンダリング順を指定できます。

半透明のオブジェクトを最後に描画しないと、透明部分が正しく描画されないのを防止するのに使ったりします。

「Background」→「Geometry」→「AlphaTest」→「Transparent」→「Overlay」の順で描画されます。
各キューは内部で整数インデックスにより表現され,
Background は 1000, Geometry は 2000, AlphaTest は 2450, Transparent は 3000,そしてOverlay は 4000です。
インデックス管理なので、以下のような定義の仕方も可能です。

//Transparentの次に描画
Tags { "Queue" = "Transparent+1" }

20161005200634.png
http://nn-hokuson.hatenablog.com/entry/2016/10/05/201022

RenderType

//キーバリュー関係
Tags { "RenderType" = "Opaque" }

カメラのDepthTexture生成で使われています。Camera.depthTextureModeでDepthTextureMode.Depthを指定するとできます。
内部ではReplacedShadersを使っているのでその時にRenderTypeタグを使います。
https://docs.unity3d.com/jp/460/Manual/SL-CameraDepthTexture.html

ReplacedShaders(置き換えシェーダー)
ReplacedShadersとは、SubShaderに定義されたタグで判定し、シェーダを置き換えて描画する仕組みです。

基本的にはそのシェーダーの用途通りのバリューを設定します。
タグは以下に列挙されています。
https://docs.unity3d.com/ja/current/Manual/SL-ShaderReplacement.html
例えば、半透明にするシェーダーを自作した場合は

Tags { "RenderType" = "Transparent" }

にすると、半透明のシェーダーを置き換える処理を実行した場合に自作のシェーダーも置き換え処理の対象になります。

置き換え処理をするときはこのようになります。

replace.cs
using UnityEngine;

public class ReplacementShaderTest : MonoBehaviour
{
    void Start ()
    {
        //置き換えるシェーダーのパスを指定する
        var replaceShader = Shader.Find ("Custom/replace");

        //第一引数:シェーダー 第二引数:入れ替え対象のキー(先ほど定義したTasgの左辺値)
        //第二引数を空文字にするとすべてのシェーダーに対して置き換え処理が走ります。
        //指定したタグが定義されていないシェーダーは置き換えに含まれなくなります。
        Camera.main.SetReplacementShader (replaceShader, "RenderType");
    }

}

こちらの例だとRenderTypeをキーにしてシェーダーを置き換えます。
置き換え前のシェーダーに定義されているタグが"RenderType" = "Transparent"だった場合
以下のシェーダーの"RenderType" = "Transparent"が設定されているシェーダーに置き換わります。

replace.shader
Shader "Custom/replace"
{
    //上から順にタグを見て置き換えれるか判定
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag
            #include "UnityCG.cginc"
            fixed4 frag(v2f_img i) : SV_Target
            {
                return fixed4(1, 0, 0, 1);
            }
            ENDCG
        }
    }

    SubShader
    {
        //例文だとRenderTypeにTransparentが設定されているので、このシェーダーに置き換わる。結果オブジェクトは緑色になる
        Tags { "RenderType" = "Transparent" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag
            #include "UnityCG.cginc"
            fixed4 frag(v2f_img i) : SV_Target
            {
                return fixed4(0, 1, 0, 1);
            }
            ENDCG
        }
    }
}

ちなみに置き換えれるものがなかった場合は表示されなくなります。
RenderTypeは置き換えシェーダーで使うから何をしているかを書いておこうと思っておけば大丈夫です。

置き換えシェーダーについてはこのサイトが一番わかりやすかったです。
https://www.shibuya24.info/entry/replaced_shaders

その他タグ

他は頻繁に使うことがないので、箇条書きします。
- DisableBatching バッチ可否
- ForceNoShadowCasting シャドウの投影可否
- IgnoreProjector プロジェクターが投影するかどうかフラグ
- CanUseSpriteAtlas シェーダがSpriteを使っていた場合に不具合ある場合はFalseを使用せよとのこと
- PreviewType マテリアルインスペクタのPreview画面の3D形状

補足

一度に複数のタグを設定することも可能です。
その場合はこのように記載します。

Tags { "RenderType"="Opaque" "Queue"="Geometry"}

参考
https://www.shibuya24.info/entry/shader_pass_tag

LOD

Level of Detailを略したものですが、一般的な"カメラの距離によってポリゴン数を変動させる"あのLODとは別物です。
正式名称は"シェーダーLOD"といいます。
値は高度なグラフィック機能の要求がされるものほど高い値を設定します。基準は公式のドキュメントに書かれてあります。
https://docs.unity3d.com/ja/current/Manual/SL-ShaderLOD.html

なぜこのようなことをする必要があるかというと、ハードウェア側でシェーダーがサポートされていても重かったりすることがあるからです。
その時にシェーダーLODを使って、強制的にシェーダーのレベルを下げればパフォーマンスを確保する事ができます。

LODTest.shader
Shader "Custom/LODTest"
{
    SubShader {
        LOD 500
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0
        struct Input
        {
            float2 uv_MainTex;
        };

        void surf(Input IN, inout SurfaceOutputStandard o)
        {
            o.Albedo = fixed4(1, 0, 0, 1);
        }
        ENDCG
    }

    SubShader {
        LOD 200
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0
        struct Input
        {
            float2 uv_MainTex;
        };

        void surf(Input IN, inout SurfaceOutputStandard o)
        {
            o.Albedo = fixed4(0, 1, 0, 1);
        }
        ENDCG
    }
    FallBack "Diffuse"
}
    [SerializeField] Renderer render;
    void Start()
    {
        //200以下のLODにしか対応しない
        render.sharedMaterial.shader.maximumLOD = 200;
    }

上の例だとmaximumLODを200にしたので、緑色になります。

LODの設定方法はrenderのマテリアルから直で編集します。
ここで設定した最大LODより大きいシェーダーは使われません。
最大LODよりも低く、できるだけ高度なシェーダーを選択します。(LOD 100と150のシェーダーがあった場合150が選択されます)
もしも使えるシェーダーがない場合はFallBackに設定されているシェーダーが適用されます。(例文でMAXを100にした場合など)

値の決め方ですが、ドキュメントを参考に自分が行った処理から決めるのがいいです。
(Diffuseぐらいしかしていないから200!みたいな感じで)

ちなみに必須のパラメータではないので、なくても動きます。

CGPROGRAM~ENDCG

CG言語の開始~終了を意味しています。
逆にこのエリア以外はCG言語じゃなく、ShaderLabで書かれています。

#pragma surface surfaceFunction lightModel [optionalparams]

サーフェースシェーダーの関数をsurfaceFunctionに知らせます。
例文だとsurfがサーフェースシェーダーの関数と書いています。
サーフェースシェーダーはsurf、バーテックスシェーダーはvert、フラグメントシェーダーはflagとしていることが多いです。

lightModel

ライティングの方法を指定できます。
ライティングの方法によって、サーフェースシェーダーの関数の構造体が変わります。

ライティング方法 説明 使う構造体
Lambert ランバート(非PBR)でライティングを計算 SurfaceOutput
BlinnPhong ブリンフォン(非PBR)でライティングを計算 SurfaceOutput
Standard UnityのStandardシェーダで使っているPBR計算 SurfaceOutputStandard
StandardSpecular スペキュラセットアップのPBR計算 SurfaceOutputStandardSpecular

詳しい構造体の中身についてはまた後程説明します。

PBR(Physically-Based Rendering)
物理ベースレンダリング(Physically-based rendering:PBR)とは物体表面における光の反射や媒質内における散乱などの物理現象,光源からシーンを経てカメラに入射する光の伝搬などを計測して数式でモデル化したものを用いてレンダリングすることです.物理ベースレンダリングに使用されるモデルは様々で,物理現象を詳細に表現したモデルほど現実と見分けがつかないほどの映像を作成することが可能ですが,計算にとても時間がかかります。
https://qiita.com/mebiusbox2/items/e7063c5dfe1424e0d01a

カスタムライティング
ライティングは自作できます。
カスタムライティングについては実際のトゥーンシェーダのソースにコメントいれて軽く説明しておきます。
詳細な説明は以下のサイト参照
http://nn-hokuson.hatenablog.com/entry/2017/03/27/204255

Toon.shader
Shader "Custom/Toon" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
         //トゥーンシェーダーで使うRampテクスチャ
        _RampTex ("Ramp", 2D) = "white"{}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        //ToonRampというカスタムライティングを使う宣言
        #pragma surface surf ToonRamp
        #pragma target 3.0

        sampler2D _MainTex;
        sampler2D _RampTex;

        struct Input {
            float2 uv_MainTex;
        };

        fixed4 _Color;

        //カスタムライティングの関数名はLightingを頭につける必要あり。ここの関数名を宣言で使う
        //関数名のところ以外テンプレ。StandardSurfaceOutputは使えないのでSurfaceOutputを使用する
        fixed4 LightingToonRamp (SurfaceOutput s, fixed3 lightDir, fixed atten)
        {
            half d = dot(s.Normal, lightDir)*0.5 + 0.5;
            fixed3 ramp = tex2D(_RampTex, fixed2(d, 0.5)).rgb;
            fixed4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * ramp;
            c.a = 0;
            return c;
        }

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

optionalparams

サーフェースシェーダー設定を細かくできる
種類が多く、すべて把握していないので、わかっている範囲で説明します。

  • alpha 半透明シェーダーで使います。alphaにも種類がありますが、やりたいことに合わせて選びましょう
  • vertex:name サーフェースシェーダーでバーテックスシェーダーをいじりたいときに使います。nameと同じ関数をシェーダー内に定義する必要があります。
  • fullforwardshadows デフォルトのシェーダーについています。フォワードレンダリング時の影(ドロップシャドウ)を通常はディレクショナルライト以外は表示しませんが、このオプションがあればスポットライトやポイントライトでも影が表示されるようになる

オプション一覧
https://docs.unity3d.com/ja/current/Manual/SL-SurfaceShaders.html

#pragma target X.Y

Shader Compilation Target Levelsというものを指定しています。

こちらを見る限りだとレベルに応じてシェーダーモデルが変わります。
https://docs.unity3d.com/ja/current/Manual/SL-ShaderCompileTargets.html

シェーダーモデルによって、使えるテクスチャの数や機能が違います。単純に、高い方が色々できます。
テッセレーションなどDirectX11の機能を使いたい場合は5.0にする必要があります。
大体のモバイル端末ではShader Model 3.0までは動くようです。ダメなのは一部のOpenGL ES 2.0端末とのこと。(以下参照)
https://forum.unity.com/threads/is-it-ok-to-use-pragma-target-3-0-in-shader-for-mobile-game.448547/

struct Input{...}

Input構造体といいます。次のsurf関数の引数に何を入れ込むかを書きます。
使わなくても定義する必要があるので、適当に1つ定義だけして使わないこともあります。

書き方

型 名       名 前 説明 注意点 例文
float2 uv***** uv座標の取得 変数名を"uv"から始まる必要がある※1。
名前部分はPropertiesで指定したテクスチャ名
float2 uv_CubeTexture;
float3 worldPos ワールド座標 "worldPos"という名前にする必要がある float3 worldPos;
float4 screenPos スクリーン座標 float4の順番は(X,Y,Z,W)。
Wは"screenPos"という名前にする必要がある
float4 screenPos;
float4 color : COLOR 頂点カラー float4の順番は(R,G,B,A)。
変数名の後に":COLOR"というセマンティクス※2をつける必要あり。
float4 col : COLOR;
float3 viewDir ビュー方向 "viewDir"という名前にする必要がある float3 viewDir;
float3 worldNormal オブジェクトの法線ベクトル "worldNormal"という名前にする必要がある※3 float3 worldNormal;
float3 worldRefl 視線ベクトルがオブジェクトに反射した時のベクトル "worldRefl"という名前にする必要がある※3 float3 worldRefl;

※1 テクスチャが複数ある場合はuvが1枚目のテクスチャ、uv2が2枚目のテクスチャという意味合いになります。

※2 その変数がどういうものかを表すために使うもの。vertexシェーダーとflagmentシェーダーだとよく使います。
https://qiita.com/sune2/items/fa5d50d9ea9bd48761b2

※3 vert関数内で出力する"Normal"(法線ベクトル)に書き込みを行う場合に、それを踏まえた反射ベクトルを取得するにはInput構造体にINTERNAL_DATAを含めて、vert関数でWorldReflectionVectorを使う必要があります。
https://docs.unity3d.com/jp/460/Manual/SL-SurfaceShaderExamples.html

sampler2D _MainTex;

Propertiesで宣言したテクスチャや値を使用する場合、Propertiesで宣言した名前と同じ名前でSubShaderの中で宣言しなければいけません。
2Dテクスチャの場合はsampler2D

void surf(Input IN, inout SurfaceOutputStandard o)

サーフェイスシェーダーの実処理部分です。
1ピクセルごとにsurfメソッドが実行されます。

"SurfaceOutputStandard"部分はアウトプット構造体といい、ここは指定したlightModelによって変わります。

アウトプット構造体の種類とその中身

//[LightModel]Lambert, [LightModel]BlinnPhong
struct SurfaceOutput
{
    fixed3 Albedo; // 拡散反射光(=Diffuse)
    fixed3 Normal; // 法線ベクトル
    fixed3 Emission; // エミッション
    half Specular; // スペキュラ
    fixed Gloss; // 輝き
    fixed Alpha; // 透過度
};

//[LightModel]Standard
struct SurfaceOutputStandard
{
    fixed3 Albedo;
    fixed3 Normal;
    half3 Emission;
    half Metallic;//範囲は0~1で、1に近づけるほど金属っぽくなる
    half Smoothness;//範囲は0~1で、1に近づけるほどつるつるした表現になる
    half Occlusion;//オクルージョン(遮蔽)、デフォルトは1で0の場合は完全遮蔽
    fixed Alpha;
};

//[LightModel]StandardSpecular
struct SurfaceOutputStandardSpecular
{
    fixed3 Albedo;
    fixed3 Specular;
    fixed3 Normal;
    half3 Emission;
    half Smoothness;
    half Occlusion;
    fixed Alpha;
};

サーフェイスシェーダーでは、このアウトプット構造体の中身に値を入れることで処理を反映させます。

最初の例文の処理を見ていきます。

void surf(Input IN, inout SurfaceOutputStandard o)
{
    //tex2D関数を使って、UV座標IN.uv_MainTexから_MainTex上のピクセル色を取得します。
    half4 c = tex2D(_MainTex, IN.uv_MainTex);

    //Albedoに色を設定して適用させます。
     o.Albedo = c.rgb;

    //Alphaに透明度を設定して適用させます。
    o.Alpha = c.a;
}

このように、アウトプット構造体の中身に入れ込むだけで大丈夫です。

また、シェーダーには定義済みの値が多数存在し、その中の"_Time"というのを使えば時間が取得でき、これを使うことでアニメーションするシェーダーも書けます。

定義済みの値一覧はコチラにあります。
https://docs.unity3d.com/jp/460/Manual/SL-BuiltinValues.html

まとめ

最近サーフェイスシェーダーについて勉強したのでまとめて書きました!

勉強してみて思ったのが、サーフェイスシェーダーを書くこと自体は簡単ですが、お作法や使える関数など覚えるのが大変だなーと思いました。
覚えることが多すぎるので、理解してから書くよりやりながら覚えるほうがわかりやすかったです。
あと数学さんの出番もかなり多いので、最初は頭がいっぱいになります・・・(私も法線って何?ってレベルから始めました)

こちらのサイトを参考に1個ずつシェーダーを作っていくのが分かりやすかったです。
http://nn-hokuson.hatenablog.com/entry/2018/02/15/140037

6
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
11