36
@flankids

[Unity]聖剣3TOMのキャラの「アナログ感」シェーダーを模写してみた

image.png
先月、聖剣伝説3 TRIALS of MANA のスマホ版がリリースされました。
スーファミ版リアルタイム世代なのもあって楽しく遊んでます!

フレームレートの安定ためにコンシューマ版に比べて一部ポストエフェクトを切っていたり色域が違うように見えたりと、細かに調整されていて、スマホでもスムーズに遊べていて感動です・・・。(コンシューマ版開発時点でスマホ対応も構想に入っていたらしい)

聖剣3TOMを遊んでいて印象的だったのが、光沢を感じるような独特の陰影の付け方でした。
デュランパーティ.gif ホークアイパーティ.gif
「何かグラフィックに関するインタビューとかないかなぁ」と探したところ、 UNREAL FEST EXTREME 2020 WINTER でドンピシャな講演をされていたようで、そのアーカイブが見つかりました!(36:07〜)
ありがたや…。
 

 
講演の中でシェーダーに関する細かいお話があって良い足がかりになったので、聖剣3TOMのキャラクターのシェーダーを模写してみました。

できたビジュアル

tom_shader.gif
こんな感じになりました。
左がシンプルなテクスチャのみでの表示で、右が聖剣3TOM風シェーダーを反映した表示です。

前述の講演によると、今時の3Dゲームっぽいリッチさとしてモデルの陰影や物理ベースっぽい質感表現など、3Dだからできる塗り方の特徴は活かしつつ、手書きイラスト風の ハンドメイドっぽさ暖かみ のある絵を目指していたそうです。
アートのコンセプトとしてそれらを総じて「アナログ感」と表現していて、言い換えれば「できるだけ3Dポリゴンっぽく見えないようにした」とのことでした。素敵。

聖剣3TOMのシェーダーの機能

講演ではシェーダーの機能として大きく3つの話をされていました。

  • シェーディング
  • アウトライン
  • エミッシブ(ライトの影響緩和)

件の「光沢を感じるような独特の陰影」は1つめの シェーディング で表現されているようだったので、今回は主にその部分の再現をやります。

■ 描画の工程

  1. 陰1
  2. 陰2
  3. 陰のマスク
  4. リムライト
  5. シャドウ(ハーフランバート)
  6. スペキュラ
  7. アウトライン

の7つの工程で描画されています。

表示 概要
1_テクスチャ.png ■工程0 - テクスチャのみ
シェーダーで特別なことをせず、テクスチャの色をそのまま出している状態
2_陰1.png ■工程1 - 陰1
視線方向に依存して体のフチに色を乗算して陰影を付ける工程です。
3_陰2.png ■工程2 - 陰2
陰1の上から更に色を乗せます。
2段階に分けて陰影を付けるために挟んでいる工程です。
4_陰のマスク.png ■工程3 - 陰のマスク
陰1、陰2を外側からマスクして元の色にする工程です。
後述する「稜線」の表現のために行っています。
5_リムライト.png ■工程4 - リムライト
視線方向とライトの方向を使うシンプルなハイライト付けの工程です。
ライト方向に近い体のフチに白っぽい色(ライトに色)が乗ります。
6_シャドウ(ハーフランバート).png ■工程5 - シャドウ
ライトの方向を使って塗るシンプルなシャドウです。
暗くなりすぎないようによく使われているHalf-Lambertという手法で濃さを調整しています。
7_スペキュラ.png ■工程6 - スペキュラ(光の反射)
光沢を調整してモデルの材質に合わせた質感表現するための工程です。
Phong鏡面反射という手法を使います。
8_アウトライン.png ■工程7 - アウトライン
モデルの輪郭を塗って印象をクッキリさせています。
単色ではなく、接する頂点の色を使ったアウトラインを引いています。

工程ごとにシェーダーを書いていきます。

解説ではパラメータや入力/出力構造体の定義は省略します。
記事の最後にシェーダー全体をまとめて載せるので、定義周りの確認や、とりあえず試したいという方はそちらをご利用ください!

工程0 - テクスチャのみ

1_テクスチャ.png
まずはこの表示を作る下記のシンプルなUnlitシェーダーを用意します。

TOM.shader
Shader "Custom/TOM"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
        }
        LOD 100

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

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

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

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.fogFactor = ComputeFogFactor(o.vertex.z);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                // sample the texture
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                // apply fog
                col.rgb = MixFog(col.rgb, i.fogFactor);
                return col;
            }
            ENDHLSL
        }
    }
}

これでテクスチャをシンプルに表示するシェーダーになります。

ノーマルマップの対応

今後陰影に関する処理をいろいろ書きますが、ノーマルマップを使って細かな凹凸があるとよりその効果が強く出るので、ノーマルマップを使えるように処理を追加しておきます。

TOM.shader
v2f vert (appdata v)
{
    //~~省略~~

    o.normal = TransformObjectToWorldNormal(v.normal);
    o.uvNormal = TRANSFORM_TEX(v.uv, _BumpMap);
    o.tangent = v.tangent;
    o.tangent.xyz = TransformObjectToWorldDir(v.tangent.xyz);
    o.binormal = normalize(cross(v.normal, v.tangent.xyz) * v.tangent.w * unity_WorldTransformParams.w);
    return o;
}

float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // ノーマルマップから法線情報を取得する
    float3 localNormal = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uvNormal), _BumpScale);
    // タンジェントスペースの法線をワールドスペースに変換する
    i.normal = i.tangent * localNormal.x + i.binormal * localNormal.y + i.normal * localNormal.z;

    //~~省略~~
    return col;
}
  • 入力頂点構造体をフラグメントシェーダで使えるように出力頂点構造体に適宜変化をして代入
    • binormalの計算が独特。セマンティクスで直接取得できないのでこんな感じに
  • UnpackNormalScaleを使ってノーマルマップの法線情報を取得 → 変換して反映

これでノーマルマップが使えるようになります。

工程1 - 陰1

Before After
1_テクスチャ.png 2_陰1.png

視線方向に依存して体のフチに色を乗算して陰影を付ける工程です。
リムライトの陰版だと言ったほうが分かりやすそうですね。

TOM.shader
v2f vert (appdata v)
{
    //~~省略~~

    o.viewDir = normalize(-GetViewForwardDir());
    return o;
}

float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // 陰1の計算をする
    float limPower = 1 - max(0, dot(i.normal, i.viewDir));
    float limShadePower = inverseLerp(_LimShadeMinPower1, 1, limPower);
    limShadePower = min(limShadePower * _LimShadePowerWeight1, 1);
    col.rgb = lerp(col.rgb, col.rgb * _LimShadeColor1, limShadePower * _LimShadeColorWeight1);

    //~~省略~~
    return col;
}

視線方向viewDirと法線の内積normalを取り1からその値を引くことで、視点に対して横を向いているところほど値が大きい陰係数limPowerを作っています。
それを下記コードのように現在色colと、colに陰色_LimShadeColor1をかけた色のlerpのレート値に使えばリム陰自体は完成します。

col.rgb = lerp(col.rgb, col.rgb * _LimShadeColor1, limPower);

しかし、このシンプルな計算だと陰によってできる陰影の層が作れず、視線に対して完全に横を向いてる辺りしか陰色がハッキリ乗りません。

聖剣3TOMの陰は、一種のトゥーンシェーディングっぽい「層のある陰影」を作るための目的があると解釈しているので、

  • 陰色_LimShadeColor1の影響範囲 → _LimShadeMinPower1
  • 完全に陰色_LimShadeColor1になる範囲の広さ → _LimShadeColorWeight1

の2つのパラメータを追加して、細かく陰の層が作れるようにしました。

float limShadePower = inverseLerp(_LimShadeMinPower1, 1, limPower);

まずlimPowerを、_LimShadeMinPower1の値が開始点0になるようにinverseLerpにかけて調整しています。
これによって
* _LimShadeMinPower1が大きいほど陰の影響が始まるのがフチに近くなる(内側がマスクされるイメージ)
* _LimShadeMinPower1が小さいほど陰の影響が始まるのが視線方向に近くなる
という挙動のパラメータとして扱えるようになります。

inverseLerpは、HLSLの標準関数に用意されてないので、自前実装しています。記事末尾のCustom.cgincを参照してください

limShadePower = min(limShadePower * _LimShadePowerWeight1, 1);

その上でlimShadePower_LimShadePowerWeight1をかけることで陰色が最大まで反映される内積位置が視線方向に近くなります。
つまり、
* _LimShadePowerWeight1が大きいほど陰色が完全に反映されている範囲が視線方向の頂点に向かって広くなる
* _LimShadePowerWeight1が小さいほど陰色が完全に反映されている範囲がフチに近くなる
という調整に使えるパラメータになります。

工程2 - 陰2

Before After
2_陰1.png 3_陰2.png

陰1の上から更に色を乗せます。
2段階に分けて陰影を付けるために挟んでいる工程です。

TOM.shader
float4 frag (v2f i) : SV_Target
{
    // sample the texture
    float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
    // テクスチャから取得したオリジナルの色を保持
    float4 albedo = col;

    //~~省略~~

    // 陰2の計算をする
    limShadePower = inverseLerp(_LimShadeMinPower2, 1, limPower);
    limShadePower = min(limShadePower * _LimShadePowerWeight2, 1);
    col.rgb = lerp(col.rgb, albedo.rgb * _LimShadeColor2, limShadePower * _LimShadeColorWeight2);

    //~~省略~~
    return col;
}

陰1と同じ働きをするシェーディングをもうひと枠分設定するだけで、陰1の処理とほとんど変わりません。

特殊な点としては、最後のlerpの計算で第二引数にalbedoのカラーを使っています。
これは、陰1の計算後の色を使うと陰色の調整が難しくなるため、テクスチャのオリジナルの色を使うようにする目的で実装しています。

工程3 - 陰のマスク

Before After
3_陰2.png 4_陰のマスク.png

陰1、陰2を外側からマスクして元の色にする工程です。
これは講演によると「稜線」の表現に一役買っているとのことでした。

※稜線とは、デッサン用語における 形の変化する場所を表す線や濃淡の変化 みたいな意味みたいです
https://dessinlaboratory.com/knack/ridge-con.html

TOM.shader
float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // 陰のマスク
    float limShadeMaskPower = inverseLerp(_LimShadeMaskMinPower, 1, limPower);
    limShadeMaskPower = min(limShadeMaskPower * _LimShadeMaskPowerWeight, 1);
    col.rgb = lerp(col.rgb, albedo.rgb, limShadeMaskPower);

    //~~省略~~
    return col;
}

陰1,2と同じような要領で塗りの領域を計算し、対象の範囲をオリジナルのテクスチャの色に戻す計算(=陰のマスク)をしています。

工程4 - リムライト

Before After
4_陰のマスク.png 5_リムライト.png

視線方向とライトの方向を使うシンプルなハイライト付けの工程です。
ライト方向に近い体のフチに白っぽい色(ライトに色)が乗ります。

TOM.shader
float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // リムライト
    Light light = GetMainLight();
    float limLightPower= 1 - max(0, dot(i.normal, -light.direction));
    float3 limLight = pow(saturate(limPower * limLightPower), _LimLightPower) * light.color;
    col.rgb += limLight * _LimLightWeight;

    //~~省略~~
    return col;
}

ライト方向と視線方向をベクトル合成してリムの計算に使います。
_LimLightPowerをパラメータとして乗数にすることで、リムのグラデが影響する範囲を調整できるようにしています。

リムライトは必要?

講演内でリムライトは「陰のマスク」の工程と同一のものとして説明されていたため、もしかしたらこの工程は不要かもしれないです。
ただ、聖剣3TOMをプレイしてる中で「陰のマスク」だけでは説明できない白く塗られたリムライトっぽい部分が目についたので、今回は独自にリムライトも別途必要と解釈して追加しました。

▼型や左胸のあたりにリムライトが効いてそうに見える
limlight.png

工程5 - シャドウ(ハーフランバート)

Before After
5_リムライト.png 6_シャドウ(ハーフランバート).png

ライトの方向を使って塗るシンプルなシャドウです。
暗くなりすぎないようによく使われているHalf-Lambertという手法で濃さを調整しています。

TOM.shader
float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // Half-Lambert拡散反射光
    float3 diffuseLight = CalcHalfLambertDiffuse(light.direction, light.color, i.normal);
    col.rgb *= diffuseLight + _AmbientColor;

    //~~省略~~
    return col;
}
Custom.cginc
// HalfLambert拡散反射光を計算する
float3 CalcHalfLambertDiffuse(float3 lightDirection, float3 lightColor, float3 normal)
{
    // ピクセルの法線とライトの方向の内積を計算する
    float t = dot(normal, lightDirection);
    // 内積の値を0以上の値にする
    t = max(0.0f, t);
    t = pow(t * 0.5 + 0.5, 2);
    // 拡散反射光を計算する
    return lightColor * t;
}

ライトへ向かうベクトルに対して外向きのベクトルほど暗くなるLambert拡散反射光に対して、ざっくり言うと陰影の暗い部分の下限を底上げするような手法がHalf-Lambert拡散反射光です。

Lambert Half-Lambert
6_シャドウ(ランバート).png 6_シャドウ(ハーフランバート).png
t = pow(t * 0.5 + 0.5, 2);

CalcHalfLambertDiffuseの上記のコードがHalf-Lambert特有の値の調整計算をしているところです。
(詳しくは下記ページなどを参照)

工程6 - スペキュラ

Before After
6_シャドウ(ハーフランバート).png 7_スペキュラ.png

光沢を調整してモデルの材質に合わせた質感表現するための工程です。
Phong鏡面反射という手法を使います。

TOM.shader
v2f vert (appdata v)
{
    //~~省略~~

    o.toEye = normalize(GetWorldSpaceViewDir(TransformObjectToWorld(v.vertex.xyz)));
    return o;
}

float4 frag (v2f i) : SV_Target
{
    //~~省略~~

    // Half-Lambert拡散反射光
    float3 diffuseLight = CalcHalfLambertDiffuse(light.direction, light.color, i.normal);

    // 減衰なしのPhong鏡面反射光
    float shinePower = lerp(0.5, 10, _Smoothness);
    float3 specularLight = CalcPhongSpecular(-light.direction, light.color, i.toEye, i.normal, shinePower);
    specularLight = lerp(0, specularLight, _SpecularRate);

    col.rgb *= diffuseLight + specularLight + _AmbientColor;

    //~~省略~~
    return col;
}
Custom.cginc
// Phong鏡面反射光を計算する
float3 CalcPhongSpecular(float3 lightDirection, float3 lightColor, float3 toEye, float3 normal, float shinePower)
{
    // 反射ベクトルを求める
    float3 refVec = reflect(lightDirection, normal);
    // 光が当たったサーフェイスから視点に伸びるベクトルを求める
    toEye = normalize(toEye);
    // 鏡面反射の強さを求める
    float t = dot(refVec, toEye);
    // 鏡面反射の強さを0以上の数値にする
    t = max(0.0f, t);
    // 鏡面反射の強さを絞る
    t = pow(t, shinePower);
    // 鏡面反射光を求める
    return lightColor * t;
}

Phong鏡面反射光は、ライト方向、法線、対象の頂点から視線位置へのベクトルを使ってハイライトを作る手法です。
※「頂点から視線位置へのベクトルtoEye」は「視線ベクトルviewDir」と異なるものなので注意
滑らかさ_Smoothnessのパラメータでハイライトの大きさや強さを調整して、部位ごとの質感を作ることができます

工程7 - アウトライン

Before After
7_スペキュラ.png 8_アウトライン.png

モデルの輪郭を塗って印象をクッキリさせています。
単色ではなく、接する頂点の色を使ったアウトラインを引いています。

聖剣3TOMではポストエフェクトでアウトラインを作る方法を使っていると講演で語られていましたが、今回はシェーダーだけで完結させるためにパスを増やして法線方向に拡大する方法でアウトラインを作ります。

TOM.shader
v2f vert (appdata v)
{
    v2f o = (v2f)0;
    // アウトラインの分だけ法線方向に拡大する
    o.vertex = TransformObjectToHClip(v.vertex + v.normal * (_OutlineWidth / 100));
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    return o;
}

float4 frag (v2f i) : SV_Target
{
    // sample the texture
    float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
    // 表面の色にアウトライン色をブレンドして使う
    return col * _OutlineColor;
}

ポストエフェクトでのアウトラインの作り方については下記などで紹介されています。

まとめと課題

tom_shader_sequence.gif

アートのコンセプトである「アナログ感」の要素として、陰1,2やマスク、リムライトなどでイラストっぽさを出しつつも、シャドウやスペキュラによるPBRっぽさをブレンドして独特のビジュアルを作っていたことが分かって面白かったです。

ただ、講演で話されていた表現で再現できなかった課題がいくつかありました。

1. テクスチャがAlbedoになっていない

今回のシェーダーはちょっと物理ベースっぽい思想が入っているので、モデルのメインテクスチャは陰影が入っていない物質そのものの色で構成されている、いわゆる Albedo Map であることが望ましいです。
陰影はノーマルマップやモデルそのものの凹凸を使ってシェーディングで表示されるべきです。

今回使ったSDユニティちゃんのモデルは、シェーディングされなくてもそれなりに綺麗に見えるように陰影が入ったテクスチャが用意されていました。
Gimpを使って多少陰影を落としたものを使ったんですが、それでも完全には Albedo Map にはできませんでした。

▲シェーディングを一切してなくてもテクスチャに書き込まれた陰影が表示されてしまっている

ライトの角度やキャラの向きで動的に陰影が作られることがこの手のシェーディングの良いところ(リッチに見えるところ)なので、ここは結構大事そうです。
(絵の担保のために一定の陰影を残すことはあると思いますが)

2. ノーマルマップにタッチ感を出すフィルター

講演の中で、ノーマルマップに特別なフィルターをかけることで陰影の変化具合にデザインの「タッチ感」を出す工夫が施されてると語られていました。
Gimpでノーマルマップに「油絵化」のフィルタをかけてみたりしましたが、良い感じにならず…。
ここはデザイナーさんに意見を聞いたりして再現したいところです。

3. アウトライン

「アナログ感」を出すために、線の太さを一定ではなくムラがあるように調整してるとのことでした。
これも再現できていないので取り入れたいです。

4. ライトの影響具合


ライトの色や明るさは反映されるようになっていますが、ちょっと影響度が強すぎるようです。
聖剣3TOMのキャラクターはライトの影響度を抑えて、色味の変化の範囲がある程度担保されるようになっているようなので、この辺りも重要そうです。

いずれこれらの課題にも対応したいなと思います。
今回はここまで!

シェーダー全文

TOM.shader

TOM.shader
Shader "Custom/TOM"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}

        _BumpMap ("Normal Map", 2D) = "bump" {}
        _BumpScale ("Normal Scale", Range(0, 2)) = 1

        _LimShadeColor1 ("リム陰の色 ベース", Color) = (0,0,0,1)
        _LimShadeColorWeight1 ("リム陰色の影響度 ベース", Range(0, 1)) = 0.5
        _LimShadeMinPower1 ("リム陰のグラデ範囲 ベース", Range(0, 1)) = 0.3
        _LimShadePowerWeight1 ("最濃リム陰の太さ ベース", Range(1, 10)) = 10

        _LimShadeColor2 ("リム陰の色 外側", Color) = (0,0,0,1)
        _LimShadeColorWeight2 ("リム陰色の影響度 外側", Range(0, 1)) = 0.8
        _LimShadeMinPower2 ("リム陰のグラデ範囲 外側", Range(0, 1)) = 0.3
        _LimShadePowerWeight2 ("最濃リム陰の太さ 外側", Range(1, 10)) = 2

        _LimShadeMaskMinPower ("リム陰マスクのグラデ範囲", Range(0, 1)) = 0.3
        _LimShadeMaskPowerWeight ("最濃リム陰マスクの太さ", Range(1, 10)) = 2

        _LimLightWeight ("リムライトの影響度", Range(0, 1)) = 0.5
        _LimLightPower ("リムライトのグラデ範囲", Range(1, 5)) = 3

        _AmbientColor ("Ambient Color", Color) = (0.5,0.5,0.5,1)

        _Smoothness ("Smoothness", Range(0, 1)) = 0.5
        _SpecularRate ("スペキュラーの影響度", Range(0, 1)) = 0.3

        _OutlineWidth ("Outline Width", Range(0, 1)) = 0.1
        _OutlineColor ("Outline Color", Color) = (0,0,0,1)
    }
    SubShader
    {
        Tags {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
        }
        LOD 100

        Pass
        {
            // 前面をカリング
            Cull Front

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct appdata
            {
                half4 vertex : POSITION;
                half3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

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

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;

            half _OutlineWidth;
            half4 _OutlineColor;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o = (v2f)0;

                // アウトラインの分だけ法線方向に拡大する
                o.vertex = TransformObjectToHClip(v.vertex + v.normal * (_OutlineWidth / 100));
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                // sample the texture
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);

                return col * _OutlineColor;
            }
            ENDHLSL
        }

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Custom.cginc"

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

                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float fogFactor: TEXCOORD1;
                float4 vertex : SV_POSITION;

                float3 normal : NORMAL;
                float2 uvNormal : TEXCOORD2;
                float4 tangent  : TANGENT;
                float3 binormal : TEXCOORD3;

                float3 viewDir : TEXCOORD4;

                float3 toEye : TEXCOORD5;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            TEXTURE2D(_BumpMap);
            SAMPLER(sampler_BumpMap);

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;

            float4 _BumpMap_ST;
            float _BumpScale;

            float3 _LimShadeColor1;
            float _LimShadeColorWeight1;
            float _LimShadeMinPower1;
            float _LimShadePowerWeight1;

            float3 _LimShadeColor2;
            float _LimShadeColorWeight2;
            float _LimShadeMinPower2;
            float _LimShadePowerWeight2;

            float _LimShadeMaskMinPower;
            float _LimShadeMaskPowerWeight;

            float _LimLightPower;
            float _LimLightWeight;

            float3 _AmbientColor;

            float _Smoothness;
            float _SpecularRate;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.fogFactor = ComputeFogFactor(o.vertex.z);

                o.normal = TransformObjectToWorldNormal(v.normal);
                o.uvNormal = TRANSFORM_TEX(v.uv, _BumpMap);
                o.tangent = v.tangent;
                o.tangent.xyz = TransformObjectToWorldDir(v.tangent.xyz);
                o.binormal = normalize(cross(v.normal, v.tangent.xyz) * v.tangent.w * unity_WorldTransformParams.w);

                o.viewDir = normalize(-GetViewForwardDir());

                o.toEye = normalize(GetWorldSpaceViewDir(TransformObjectToWorld(v.vertex.xyz)));
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                // sample the texture
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);

                // ノーマルマップから法線情報を取得する
                float3 localNormal = UnpackNormalScale(SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uvNormal), _BumpScale);
                // タンジェントスペースの法線をワールドスペースに変換する
                i.normal = i.tangent * localNormal.x + i.binormal * localNormal.y + i.normal * localNormal.z;

                float4 albedo = col;

                // 陰1の計算をする
                float limPower = 1 - max(0, dot(i.normal, i.viewDir));
                float limShadePower = inverseLerp(_LimShadeMinPower1, 1, limPower);
                limShadePower = min(limShadePower * _LimShadePowerWeight1, 1);
                col.rgb = lerp(col.rgb, albedo.rgb * _LimShadeColor1, limShadePower * _LimShadeColorWeight1);

                // 陰2の計算をする
                limShadePower = inverseLerp(_LimShadeMinPower2, 1, limPower);
                limShadePower = min(limShadePower * _LimShadePowerWeight2, 1);
                col.rgb = lerp(col.rgb, albedo.rgb * _LimShadeColor2, limShadePower * _LimShadeColorWeight2);

                // 陰のマスク
                float limShadeMaskPower = inverseLerp(_LimShadeMaskMinPower, 1, limPower);
                limShadeMaskPower = min(limShadeMaskPower * _LimShadeMaskPowerWeight, 1);
                col.rgb = lerp(col.rgb, albedo.rgb, limShadeMaskPower);

                // リムライト
                Light light = GetMainLight();
                float limLightPower= 1 - max(0, dot(i.normal, -light.direction));
                float3 limLight = pow(saturate(limPower * limLightPower), _LimLightPower) * light.color;
                col.rgb += limLight * _LimLightWeight;

                // Half-Lambert拡散反射光
                float3 diffuseLight = CalcHalfLambertDiffuse(light.direction, light.color, i.normal);
                float shinePower = lerp(0.5, 10, _Smoothness);
                float3 specularLight = CalcPhongSpecular(-light.direction, light.color, i.toEye, i.normal, shinePower);
                specularLight = lerp(0, specularLight, _SpecularRate);
                col.rgb *= diffuseLight + _AmbientColor + specularLight;

                // apply fog
                col.rgb = MixFog(col.rgb, i.fogFactor);
                return col;
            }
            ENDHLSL
        }
    }
}

Custom.cginc

Custom.cginc
float inverseLerp(float a, float b, float value) {
    return saturate((value-a)/(b-a));
}

// HalfLambert拡散反射光を計算する
float3 CalcHalfLambertDiffuse(float3 lightDirection, float3 lightColor, float3 normal)
{
    // ピクセルの法線とライトの方向の内積を計算する
    float t = dot(normal, lightDirection);

    // 内積の値を0以上の値にする
    t = max(0.0f, t);

    t = pow(t * 0.5 + 0.5, 2);

    // 拡散反射光を計算する
    return lightColor * t;
}

// Phong鏡面反射光を計算する
float3 CalcPhongSpecular(float3 lightDirection, float3 lightColor, float3 toEye, float3 normal, float shinePower)
{
    // 反射ベクトルを求める
    float3 refVec = reflect(lightDirection, normal);

    // 光が当たったサーフェイスから視点に伸びるベクトルを求める
    toEye = normalize(toEye);

    // 鏡面反射の強さを求める
    float t = dot(refVec, toEye);

    // 鏡面反射の強さを0以上の数値にする
    t = max(0.0f, t);

    // 鏡面反射の強さを絞る
    t = pow(t, shinePower);

    // 鏡面反射光を求める
    return lightColor * t;
}
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
flankids
スマホのゲーム作る仕事してます。最近はシェーダーのネタに興味あり。操作性やカメラワークに関する具体的な実装や小細工について書くことが多いです。noteに講演レポートなども書いてますので、よろしければそちらも是非ご覧ください! https://note.com/flankids

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
SORACOMを使ったIoTにチャレンジしよう!
~
自社サービスの技術スタック公開
~