JKになりたい

何か書きたいことを書きます。主にWeb方面の技術系記事が多いかも。

Shaderやっていく〜テクスチャを貼るだけのShader〜

はじめに

Shaderを勉強してみようと思い、先日のGWでPacktから出ている「Unity2018 Shaders and Effects Cookbook」を一通り読んで動かしました。
で、まあ一回読んで動かしただけじゃ概ね理解できてないので、復習を兼ねて適当にピックアップしてアウトプットしたいと思います。
何回かに続いて書いていく予定なんですけど、更新なくなったら飽きたんだなって思ってください。

なぜShaderを勉強しようと思ったのか?

日常的にはバックエンド側をやることが多いので(ゲームではない)、Shaderとの接点はもちろんUnityとの接点も全然ありませんでした。
大学時代に仕事で避けゲー一本と、個人で簡単なADVを開発した事はあるので全く触ったことがないわけではないんですけど、ほぼUnityの事はわからない状態です。

しかし、最近個人で3DのVTuber試してみたり(【Live2D】VTuberのつくりかた!【360度動画】 - YouTube)とか、職場のゼミ制度(googleの20%ルールみたいなやつ)でVRゼミに入ってみたりと突然VRやUnityとの接点が増えたわけです。。

そこで、表現の幅を広げるため、スタンドアロンVRバイスで高クオリティかつ高速な表現を実現するために勉強をはじめてみた、って感じです。

あとは、偶然raymarchingの例を見て感動した、っていうのが結構強い動機だったりします。。 Unity でレイマーチングするシェーダを簡単に作成できるツールを作ってみた - 凹みTips こちらの記事の例など感動します。
凹さんが生成ツールなるものを公開されておられるのですが、エンジニアは往々にして1回は自分で全て実装しないと気がすまない人種なので、自分も実装できるように頑張っていきたいです。

Surface Shaderって?

そもそもUnityにおいてShaderはShaderLabというCgとHLSLの異型(だいたい似てるらしい)の言語で記述します。
で、Shaderではライティングや影などの煩雑な処理を実装していかないといけないのですが、Surface Shaderを使うとパラメータを指定するだけで、その辺を自動生成してくれます。
https://docs.unity3d.com/ja/current/Manual/SL-SurfaceShaders.html

後々はちゃんと読むとして、しばらくは目をつぶってSurfaceShaderによってShaderに慣れていきたいと思います。

テクスチャを貼るShader

最初は、テクスチャを貼るだけのShaderを見ていきたいと思います。

2D画像のテクスチャはどのように3Dモデルへマッピングされるのか

ポリゴンの各頂点にはシェーダがアクセスして使用できるデータを格納することができます。
ここのデータにuv座標のデータが入っています。これはu,vそれぞれで0-1の値を取り、2D画像のマッピング位置を示しています。
ポリゴンの内側の点に関しては、頂点データからGPUが最も近しいuv座標を補完するようです。

uv座標のデータを3Dモデルに含んでいない場合は当然ですがテクスチャマッピングは行なえません。

詳細は テクスチャマッピング · けんごのお屋敷 の記事が詳しく非常にわかりやすいです。

実装

ShaderのテンプレートはShaders→Standard Surface Shaderから作成しています。
で、何とこのテンプレートの状態で既にテクスチャを貼れるShaderになっています。
ということで、実装することは何もありません><

今回は、テンプレートコードを見てShaderLabのシンタックスを見ていきたいと思います。

全体のコードとしては以下。

Shader "Custom/TextureShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Propertiesブロック

最初にあるのはPropertiesのブロックです。
これは、Inspectorウィンドウに表示してUnity側から変更することが可能なパラメータになります。もちろん必要なければ省略可能です。

今回は以下の4つが記述されています。
シンタックスは「プロパティアトリビュート プロパティ名 (“表示名”, データ型) = 初期値」です。
プロパティアトリビュートは省略可能な要素で、今回は全て省略されています。

_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0

だいたい自明な感じがしますが、2Dという型だけ普段は見ないと思います。
これが2Dのテクスチャを格納するデータ型です。
初期値には「”white”, “black”, “gray”, “bump”」という単色のテクスチャが指定可能です。
Inspectorにはテクスチャの画像に加えて、TilingとOffsetというパラメータが表示されます。
Tilingでテクスチャを何枚表示するか、Offsetはテクスチャの原点位置を指定します。

プロパティアトリビュートについては、ShaderLab: Properties - Unity マニュアル の「プロパティーの属性とDrawer」を参照できます。
Inspectorにパラメータを表示させないようにしたりだとか、テクスチャが法線マップで有ることを示したりとか(法線マップじゃなかったら警告が出る)のような、補助的な役割を担うみたいです。

SubShaderブロック

ここにShader本体を記述していきます。1シェーダにつき1SubShaderです。
SubShader宣言は複数行うことができ、その際は上から順にハードウェアがサポートするShaderかどうかを判定していき、一番最初に通ったものが表示されるようです。
最終的にどれも表示不可能だった場合、最後のFallBackに記載したシェーダが選択されます。

上から順番に見ていきます。

Tags { "RenderType"="Opaque" }

Tags宣言ではレンダリングが行われるタイミングや方法を指定することが可能です。
RenderTypeタグでは、Replaced Shadersといった特定のShaderを置き換えてくれる機能を利用するために用います。
とりあえず、普通のShaderはOpaqueに指定、透過を含むShaderの場合はTransparentを指定しておきます。
この2つは今後も頻用します。
その他指定できる要素は以下の公式docに記載されています。
Replaced Shaders でのレンダリング - Unity マニュアル

LOD宣言

Level of Detailの略で、距離に応じて使用するSubShaderを切り替えるときに使います。
遠くにあるときに高品質なShaderを使用してもあまり意味がない、、みたいな最適化ニーズのために存在します。
因みに、LODの値は自動的に設定するものではなく、ScriptからmaximumLODを設定しないといけません。
ゲーム中のUpdate内などで、距離が変わったらRendererの.mterial.shader.maximumLODの値を設定し直すみたいなコードを書く必要があります。
この値が、ShaderLODの値より大きい場合、そのShaderが適用されます。
この例だと、maximumLODが200以上であればこのShaderが適用されます。
また、仮にShaderLODが100と200のものがあって、maximumLODが200であれば、LOD200のShaderが適用されます。

ちなみに遠くにある場合ローポリにする方でも同じようにLODという言葉を使います。LOD - Unity マニュアル
Shaderの方はシェーダーLODって言葉で使い分けてるみたいですね。 シェーダー LOD - Unity マニュアル

CGPROGRAM

以降、ENDCGまでHLSL/Cgで記述するという意味。ここからがShader本体の記述になります。

#pragma surface surf Standard fullforwardshadows

コンパイラディレクティブです。
#pragma surface ... でこれがSurfaceShaderであることを示しています。
以降いくつかパラメータを宣言しないといけません。

1つ目のsurfに相当するのが、SurfaceFunctionの名前です。surfと書いておくとvoid surf (Input IN, inout SurfaceOutput o)というシグネチャでSurfaceFunctionを定義しておく必要があります。
これは必須パラメータです。

struct SurfaceOutput
{
    fixed3 Albedo;  // ディフューズ色
    fixed3 Normal;  // 書き込まれる場合は、接線空間法線
    fixed3 Emission;
    half Specular;  //  0..1 の範囲のスペキュラーパワー
    fixed Gloss;    // スペキュラー強度
    fixed Alpha;    // 透明度のアルファ
};

次のStandardはライティングモードの記述です。
物理ベースのライティングモデルである StandardとStandardSpecularはビルドインされており特にこれ以上の記述は必要ありません。
物理ベースのライティングモデルとは、自分も厳密には全然良くわかってないのですが、光の作用を物理的な現象をなるべく近似しようと実装しているライティングモデル・・みたいな認識です。エネルギーの保存、フレネル反射、平面を防ぐ方法などの物理的な法則に従って設計されています。

Standard Shaderでは、硬質表面を念頭に設計されており、石やガラスなどを想定したマテリアルを扱うことを想定しており、StandardSpecularは名前の通り金属など鏡面反射を伴うマテリアルを想定しているものです。

逆に、物理ベースでないライティングモデルとしてLambertBlinnPhongが指定できます。(これらの実装方法はそのうち書きます、多分) Lambert反射は古典的かつ軽量なライティングモデルです。Wikipedia曰く、ランバート反射表面の輝度は、どの角度から見ても一定であるとのことです。
シンプルな数式で表現できますが、物理的な正しさを念頭においてるわけではありませんので物理ベースのランディングモデルではありません。

Standardに指定した場合は、SurfaceFunctionのoutputの型(出力構造体)をSurfaceOutputStandardにする必要があります。

struct SurfaceOutputStandard
{
    fixed3 Albedo;      // ベース (ディフューズかスペキュラー) カラー
    fixed3 Normal;      // 書き込まれる場合は、接線空間法線
    half3 Emission;
    half Metallic;      // 0=非メタル, 1=メタル
    half Smoothness;    // 0=粗い, 1=滑らか
    half Occlusion;     // オクルージョン (デフォルト 1)
    fixed Alpha;        // 透明度のアルファ
};

また、StandardSpecularにした場合は、出力構造体はSurfaceOutputStandardSpecularにする必要があります。

struct SurfaceOutputStandardSpecular
{
    fixed3 Albedo;      // ディフューズ色
    fixed3 Specular;    // スペキュラー色
    fixed3 Normal;      // 書き込まれる場合は、接線空間法線
    half3 Emission;
    half Smoothness;    // 0=粗い, 1=滑らか
    half Occlusion;     // オクルージョン (デフォルト 1)
    fixed Alpha;        // 透明度のアルファ
};

最後、fullforwardshadowsですが公式doc曰く

フォワード レンダリングパスで、すべてのライトシャドウをサポートします。デフォルトでは、フォワードレンダリングで、シェーダーは一方向からのシャドウしかサポートしません (内部シェーダー変数の数を節約するため)。フォワードレンダリングでポイントかスポットライトが必要な場合は、このディレクティブを使用します。

らしいのですが、何故これがテンプレート時に既に挿入されてるのかはよくわかりません。
フォワードレンダリングでポイントかスポットライトが必要なシチュエーションというのが現状よくわかっていないので今後の課題にしたいと思います・・(例などわかる方いればご教授頂きたいです・・)

更にその下#pragma target 3.0ですが、コンパイルのターゲットレベルです。
大体のモバイル端末でも3.0くらいまでは動くらしいので、とりあえず3.0にしておいて差し支えなさそうです?(わからん) DirectXOpenGLのバージョンの対応などは公式docへ。
シェーダーコンパイルターゲットレベル - Unity マニュアル

変数マッピング

以下の部分です。

sampler2D _MainTex;

struct Input
{
    float2 uv_MainTex;
};

half _Glossiness;
half _Metallic;
fixed4 _Color;

プロパティとして宣言した変数をSubShader内で使用するためには、マッピングが必要です。
上記のように書くことで、同名のプロパティが自動的にマッピングされます。
2Dはsampler2D型にマッピングされ、Colorはfloat4/half4/fixed4にマッピングされます。
rangeは単一のfloat/half/fixedにマッピングされます。

Input構造体ですが、この構造体がSurfaceFunctionの入力値になります。
使用したいプロパティをここで宣言しておきます。

float2 uv_MainTexですが、「uv+テクスチャの名前」でテクスチャ座標(UV座標)が入力値として入ります。
つまり、この入力値を使用することで、テクスチャのどの座標の部分を表示すればモデルに正しくマッピングできるのかがわかります。

その他、入力値として取れるものは様々です。新らしいものを使うときに都度みていきたいと思います。
ドキュメントにinput構造体が何を取ることができるのかが書かれているので、気になる方はそちらを。
サーフェスシェーダーの記述 - Unity マニュアル

GPUインスタンシング

UNITY_INSTANCING_BUFFER_START(Props)からENDまでの間に記述します。
テンプレートでは何も書かれてないので、丸ごと消しても問題ありません。

この機能を使うと、少ないドローコールで同じメッシュ、マテリアルのモデルを複数一気にレンダリングすることができます。
木とか草など、同じオブジェクトを大量に表示したいときに使用するようです。
今回は使用しないのでスルーします。また今度遊んでみます!
ドキュメントはこちら。GPU インスタンシング - Unity マニュアル

SurfaceFunction

やっと本丸にきました・・・。
ここで、出力構造体(今回はSurfaceOutputStandard)に値をセットすることで、見た目を変えていきます。

SurfaceOutoputStandard構造体の定義を再掲しておきます。

struct SurfaceOutputStandard
{
    fixed3 Albedo;      // ベース (ディフューズかスペキュラー) カラー
    fixed3 Normal;      // 書き込まれる場合は、接線空間法線
    half3 Emission;
    half Metallic;      // 0=非メタル, 1=メタル
    half Smoothness;    // 0=粗い, 1=滑らか
    half Occlusion;     // オクルージョン (デフォルト 1)
    fixed Alpha;        // 透明度のアルファ
};
void surf (Input IN, inout SurfaceOutputStandard o)
{
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    // Metallic and smoothness come from slider variables
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = c.a;
}

重要なのは最初の2行だけで、MetallicやSmoothnessなどはインスペクタで設定された値を入れてるだけですね。

fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;

見ての通り、ここでモデルの表面を何色で描画するか、を決定しています。
Cgの関数であるtex2Dの第一引数にテクスチャデータ、第二引数にサンプリングする部分の座標を入力しています。
これでサンプリング部分のRGBA値が取れます。で、_Colorをかけてるのは色合いを変更するためですね。TintColorです。
で、o.Albedoにその取得したカラーを入れたら反映されます。

.rgbみたいな文法はスィズル演算子と呼ばれ、ベクトル型の各要素にアクセスするために用いられます。
rgbaのアルファベットの組み合わせ以外に、xyzwを用いることもできます。
例えばfloat4(a, b, c, d).x → float aになりますし、float3(a, b, c).yz→float2(b, c)になります。

おわりに

今回はSurfaceShaderのテンプレートを読んでみました。
もし飽きる事がなければ、様々なShaderをピックアップしつつ書いていきたいと思います。