クリスタルっぽいモデルを簡易実装してみる@Unity

# はじめに
株式会社Craft Eggでクライアントエンジニアをしております佐竹です！
本記事は [Craft Egg Advent Calendar 2022](https://qiita.com/advent-calendar/2022/craftegg) の12/12の記事となります。

【12/11】[野部薫](https://note.com/lively_lily246)さん[「長期運用アプリゲームにおいて、仕様作成の際にあらかじめ決めておきたい7項目」](https://note.com/lively_lily246/n/nd2aa2462e586)
【12/13】[かおりん](https://note.com/poposoboro)さん[「8年目UIデザイナーが考える、ジョイン後間もないチームで最速で成果を出すための５つの心得」](https://note.com/poposoboro/n/ncdd71ec3c568)

# 目次
[実装&動作確認環境](#実装動作確認環境)
[実装のきっかけ](#実装のきっかけ)
[出来上がったもの](#出来上がったもの)
[実装解説](#実装解説)
　　[方針＆要件決め](#方針要件決め)
　　[事前準備](#事前準備)
　　　　[シェーダについて](#シェーダについて)
　　　　[カメラについて](#カメラについて)
　　　　[マテリアルについて](#マテリアルについて)
　　[シェーダの実装](#シェーダの実装)
　　　　[半透明になった弊害を修正](#半透明になった弊害を修正)
　　　　[キャプチャテクスチャのスクリーン投影](#キャプチャテクスチャのスクリーン投影)
　　　　[スクリーン投影用UVにオフセットを加えて歪みを発生させる](#スクリーン投影用uvにオフセットを加えて歪みを発生させる)
　　　　[RGB別々に歪み量を変えてプリズム的な表現を加える](#rgb別々に歪み量を変えてプリズム的な表現を加える)
　　　　[各種パラメータをシェーダプロパティとして定義](#各種パラメータをシェーダプロパティとして定義)
　　[パラメータ調整用コンポーネントの実装](#パラメータ調整用コンポーネントの実装)
[まとめ](#まとめ)
[実装物](#実装物)
　　[シェーダ](#シェーダ)
　　[コンポーネントスクリプト](#コンポーネントスクリプト)

# 実装&動作確認環境

- Unity 2020.3.31f1
    - URP 10.9.0

# 実装のきっかけ
ある日、以下のようなやりとりがありました。

_「一部分だけクリスタル**風**になっているモデルを作りたいんだけど」_
_「クリスタル風？それっぽく見えれば簡易実装でいい？」_
_「うん」_

空間歪み系の表現を用いれば低コストで要望を叶えられるかも…という安易な考えで実装して依頼された人に見てもらったところ「おおお」と喜ばれました。どストレートに簡易実装してみただけだったのに驚かれたので、この手の空間歪み系のシェーディングって意外と知られてないのかも、と思い今回記事化しました。

# 出来上がったもの

https://twitter.com/aluminum1981/status/1597528779211296768

静止させてじっくり観察すると全然クリスタルではない別のナニカです。なので本格的なクリスタル系マテリアルのシェーディングについて知りたい方は、以降の説明はあまり参考にならないと思われます。すみません。

ただ、動いている分には透明感のあるそれっぽいオブジェクトと認識できると思います。（できるといいな）

# 実装解説
### 方針＆要件決め
- 屈折表現だけでなんとかする
    - 反射表現は高負荷だったり環境マップ等の別アセットが必要だったりするので回避
    - 実装コストを安めに
- 屈折表現
    - 空間歪みエフェクトの表現をモデルに適用
    - プリズムっぽい表現とか
        - 簡易色収差

よって、以下の方針で対応してみることにしました。

- カラーバッファキャプチャが行われるような設定
- URPのLitシェーダに空間歪みが適用されるように改造
    - キャプチャ後に描画されるように
    - キャプチャテクスチャのスクリーン投影
        - 投影用UVに法線方向に応じたオフセットを付けて歪むように（簡易屈折）
        - RGB毎にオフセットを変えられるように調整（簡易色収差）

### 事前準備
今回は、URPの標準サンプルである「3D Sample Scene (URP)」を元に、その中の **「Jigsaw」** というオブジェクトをクリスタルっぽいモデルにしてみようと思います。

#### シェーダについて
今回用意するシェーダはURPのLitシェーダをベースにするため、シェーダに手を加える前準備として一旦ベースとなるシェーダをまるっとコピーして別名のシェーダとして保存します。

URP/LitシェーダはProjectビューのPackages以下から参照できます。
![スクリーンショット 2022-11-21 19.55.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/3743192f-3baf-9d53-5ce6-fe3a387c6d28.png)
`Packages/Universal　RP/Shaders/`以下の`Lit.shader`と`LitForwardPass.hlsl`をAssets以下に別名でコピーしましょう。

コピー先とファイル名は何でも構いませんが、以降の説明の都合上以下のようにします。
コピー先：`Assets/ExampleAssets/Shaders/`
ファイル名：`CrystalLit.shader`, `CrystalLitForwardPass.hlsl`

シェーダ名とインクルードファイルのパス変更のため、`CrystalLit.shader`をテキストエディタで開きましょう。冒頭に記載されているシェーダ名を以下のように変更します。
```hlsl
Shader "CustomShader/CrystalLit"
```
また、コピーしたhlslファイルを参照するように、140行目付近を以下のように変更します。
```hlsl
            #pragma vertex LitPassVertex
            #pragma fragment LitPassFragment

            #include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
            #include "CrystalLitForwardPass.hlsl" // <- ここが変更箇所
```

#### カメラについて
シーン内MainCameraを選択し、Cameraコンポーネントの設定を変えます。

Rendering内の`Opaque Texture`の項目を`On`にして下さい。
![スクリーンショット 2022-11-21 19.14.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/b0eda97e-f6b0-c575-37de-9722569da11a.png)
この項目をONにすることで、描画フロー内でカラーバッファのキャプチャが必ず行われ、シェーダ上で`_CameraOpaqueTexture`という名前でキャプチャテクスチャが使用できるようになります。

この項目のデフォルトは`Use Pipeline Settings`となっているので、URPAssets内の設定でこの項目をONにしている場合、変更の必要はありません。

#### マテリアルについて
最初に用意したシェーダを今回適用する「Jigsaw」オブジェクトのマテリアルに設定します。

該当マテリアルは`Assets/ExampleAssets/Materials/`以下にある`Jigsaw_Mat`です。シェーダの項目を選択し、プルダウンメニューから`CustomShader/CrystalLit`を選んで下さい。

また、上記カメラ設定でカラーバッファキャプチャが行われるようになりましたが、キャプチャのタイミングはテクスチャ名から分かるように不透明描画後となります。よって、不透明として描画されるモデルでは`_CameraOpaqueTexture`は使用できません。シェーダ変更と併せて、`SurfaceType`の項目を`Transparent`にして半透明扱いにします。
![スクリーンショット 2022-11-21 19.29.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/02259af6-4037-390f-3dfb-16e2540e49b5.png)

これで、CrystalLitシェーダの内容を変更するとJigsawモデルに反映され、なおかつ`_CameraOpaqueTexture`を使用できる環境となりました。

### シェーダの実装

#### 半透明になった弊害を修正
「Jigsaw」を半透明にするとモデル表示が微妙に壊れます。これは、深度バッファへの書き込み（ZWrite）がOFFになって、本来隠れているべきメッシュが描画されてしまっているため発生しています。
（左：単に半透明にした場合、右：半透明かつZWriteをONで指定した場合）
![スクリーンショット 2022-11-22 14.42.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/8765191f-0ebd-764f-92a8-0a83f7d73425.png)![スクリーンショット 2022-11-22 14.42.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/3d85957b-9384-827a-894e-76f72d2aa120.png)

今回のシェーダは半透明のタイミングで描画したいだけであって、描画内容的には不透明扱いとしたいため、シェーダ側でしっかり深度バッファ書き込み指定を行います。（ブレンドも行わないので、ついでにブレンドOFF指定も）

`CrystalLit.shader`の90行目付近を以下のように書き換えます。
```hlsl
//          Blend[_SrcBlend][_DstBlend] // 元の記述
            Blend One Zero
//          ZWrite[_ZWrite] // 元の記述
            ZWrite On
```

#### キャプチャテクスチャのスクリーン投影
次は「Jigsaw」にキャプチャテクスチャをスクリーン空間上にそのまま投影させてみます。

「テクスチャのスクリーン投影」については、以下のサイトをはじめ多くの技術ブログで解説されています。なので、本記事では特に詳細説明しません。

https://light11.hatenadiary.com/entry/2018/06/13/235543

要するに、**スクリーン空間上の頂点位置を[0,1]範囲に変換し、そのままUVとして扱ってテクスチャマッピングする**ということになります。

つまり、ライティングやベースカラーなどの加工を無視してキャプチャテクスチャを使ってスクリーン投影すると、**オブジェクトが完全透過しているような見た目になります。** ライト由来の陰影がついているので3Dオブジェクトの存在がわかりますが、この陰影を無くすと一目瞭然です。ベースカラーを操作するとちゃんと反映され、モデルが描画されているということが確認できます。
（左：陰影あり、中央：陰影を消してベースカラー白、右：陰影を消してベースカラー赤）
![スクリーンショット 2022-11-22 14.45.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/c44538d4-a4db-3e16-d605-fe5465e1e806.png)![スクリーンショット 2022-11-21 21.55.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/9fa8d1b7-3cd0-a048-5110-e253d295855b.png)![スクリーンショット 2022-11-21 21.56.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/aa69b148-e149-e4af-2d63-7074354b0e47.png)

キャプチャテクスチャとそのサンプラを宣言し、シェーダ内で使用できるようにする必要があるため、`CrystalLitForwardPass.hlsl`内、`Attributes`定義前に以下を記載します。
```hlsl
// キャプチャテクスチャ
TEXTURE2D(_CameraOpaqueTexture);
SAMPLER(sampler_CameraOpaqueTexture);
```

スクリーン投影用の座標位置を頂点シェーダで計算しフラグメントシェーダ側に渡します。`Varyings`定義内に`grabPos`という定義を追加し、頂点シェーダ内で計算します。
```hlsl
struct Varyings
{
    float2 uv                       : TEXCOORD0;
    DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 1);

#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
    float3 positionWS               : TEXCOORD2;
#endif

    float3 normalWS                 : TEXCOORD3;
#if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR)
    float4 tangentWS                : TEXCOORD4;    // xyz: tangent, w: sign
#endif
    float3 viewDirWS                : TEXCOORD5;

    half4 fogFactorAndVertexLight   : TEXCOORD6; // x: fogFactor, yzw: vertex light

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    float4 shadowCoord              : TEXCOORD7;
#endif

#if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
    float3 viewDirTS                : TEXCOORD8;
#endif

    float2 grabPos                  : TEXCOORD9; // <-これが追加した項目

    float4 positionCS               : SV_POSITION;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};
```
```hlsl
    // 頂点シェーダ内、return行の直前に以下2行を挿入
    half4 screenPos = ComputeScreenPos(output.positionCS);
    output.grabPos = screenPos.xy / screenPos.w;

    return output;
```

フラグメントシェーダ側で`grabPos`を使用してキャプチャテクスチャをサンプリングします。`SurfaceData`の`albedo`にはベーステクスチャ＆ベースカラーが反映された情報が入っているため、その情報を書き換えることで、他の処理の影響を適切に受けつつ改変が可能です。
```hlsl
    SurfaceData surfaceData;
    InitializeStandardLitSurfaceData(input.uv, surfaceData);

    InputData inputData;
    InitializeInputData(input, surfaceData.normalTS, inputData);

    // フラグメントシェーダ内のこの位置に挿入
    // albedoに_CameraOpaqueTextureのスクリーン投影を適用
    surfaceData.albedo = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, input.grabPos).rgb * _BaseColor;

    half4 color = UniversalFragmentPBR(inputData, surfaceData);
```

#### スクリーン投影用UVにオフセットを加えて歪みを発生させる
これまでの実装で完全透過しているように見えるということは、UVをほんの少しズラせば空間が歪んだような見た目になります。例えば、陰影を消した状態でUV両方に0.01だけ均等にオフセットを加えると以下のようになります。
![スクリーンショット 2022-11-21 23.23.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/cda8fb1c-a2b8-5624-2ea9-c32686b2e571.png)

全ピクセル定数のオフセットだと2D的に歪んではいるけれど屈折してる感が出ないので、3Dならではの要素を用いてオフセット計算する必要がありそうです。

この表現、実はエフェクトでは歪み（Distortion）エフェクトとして一般的なもので、その場合は歪み方向と量をオフセットマップとしてテクスチャに持たせて調整したりしますが、今回はなるべく既存アセットのみで実装することを目指しているので、今回はモデル法線を活用してみます。

歪み方向/量共にビュー空間における法線方向XYをベースに、量については大きくなりすぎないように固定値のスケールを掛けます。ビュー空間の法線方向XYを使用するため、視線方向と向き合っている法線は歪みが少なく、向き合っていない法線は歪みが大きくなります。つまり屈折してる感が出てきます。

まず、コードが散らからないようにキャプチャテクスチャのサンプリング＆カラー算出用の関数を`SampleGrabTexture`という名前で用意します。`surfaceData.albedo`への代入はこの関数経由で行います。
```hlsl
    surfaceData.albedo = SampleGrabTexture(inputData, input.grabPos) * _BaseColor;
```
上記の`SampleGrabTexture`の実装は一旦以下のようにします。引数として受け取っている`InputData`には法線マップ考慮済みの法線や視線方向などが格納済みです。（`InitializeInputData`実行後に格納される）自前で計算せず、流用できるものはしっかり流用していきます。
```hlsl
half3 SampleGrabTexture(InputData inputData, half2 grabPos)
{
    // キャプチャテクスチャのサンプリング
    // view空間法線方向にUVを歪ませる

    float3 normalVS = TransformWorldToViewDir(inputData.normalWS, false);
    half2 distortionOffset = normalVS.xy * 0.1h;

    half2 grabUv = grabPos + distortionOffset;
    half4 color = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, grabUv);
    return color.rgb;
}
```
ここまでの実装で、以下のような見た目となります。
（左：ライティングあり、右：ライティングなし）
![スクリーンショット 2022-11-22 14.51.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/809c0ccd-5fe6-8241-4aed-cc2164107eeb.png)![スクリーンショット 2022-11-22 15.20.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/c4b520d1-8d87-2057-d743-09941a250f61.png)

#### RGB別々に歪み量を変えてプリズム的な表現を加える
上記までの実装でもクリスタルっぽく見えなくはないので、求める表現の需要に応じて後はパラメータ化対応だけ行って完了！とすることもできそうです。ただ、それだけだと記事としてちょっとつまらないので、珍しめの加工を入れてみます。

RGBそれぞれで歪み量を変えることで、下画像のようなプリズムっぽい表現を加えます。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/9b6d8f89-7159-7589-db66-f1d4c0e49a74.jpeg">

（著作者：[Freepik](https://jp.freepik.com/free-photo/lights-prisms-effect-close-up_13397817.htm#from_view=detail_serie)）


RGB毎にオフセット補正値を定義して、個別にサンプリングします。
```hlsl
half3 SampleGrabTexture(InputData inputData, half2 grabPos)
{
    // キャプチャテクスチャのサンプリング
    // view空間法線方向にUVを歪ませる
    // RGB別に歪み量を変化させ擬似プリズム的な表現ができるように

    float3 normalVS = TransformWorldToViewDir(inputData.normalWS, false);
    half2 distortionR = normalVS.xy * 0.10h;
    half2 distortionG = normalVS.xy * 0.12h;
    half2 distortionB = normalVS.xy * 0.14h;

    half2 grabUvR = grabPos + distortionR;
    half2 grabUvG = grabPos + distortionG;
    half2 grabUvB = grabPos + distortionB;
    half colorR = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, grabUvR).r;
    half colorG = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, grabUvG).g;
    half colorB = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, grabUvB).b;

    return half3(colorR, colorG, colorB);
}
```
:::note warn
テクスチャサンプリング回数を増やしているため、負荷的には少し高くなるので注意してください。
:::

上記コードを適用すると以下のような見た目になります。この記事内での見た目としては、これで完成となります。
（左：ライティングあり、右：ライティングなし）
![スクリーンショット 2022-11-22 15.27.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/b31912be-b2c6-f49a-b5f7-3cf54e1923d2.png)![スクリーンショット 2022-11-22 15.25.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2298208/7b2846e4-0880-2543-c5c7-ec524ce5805b.png)

「ライティングあり」のほうが暗くて微妙な見た目となっているのは、ライト方向などの設定によるもの＆普通に不透明物としてライティング計算しているのが原因なので、この辺りはライトやマテリアルを調整するか、平行光等の通常ライティングを無視してスペキュラだけが反映されるようなライティング計算にすると良さそうです。（本記事ではそこまでカスタマイズしません）

#### 各種パラメータをシェーダプロパティとして定義
基本のシェーダ実装はこれで完了ですが、歪み量などの調整をマテリアル個別に行えるようになっていません。調整したいパラメータをまとめて変数化し、シェーダプロパティとして定義します。今回パラメータ化するのは歪みスケール量だけになります。（全体に影響する基準スケールとRGB別に差異を付けるためだけのスケール）

`_CameraOpaqueTexture`関連の宣言を行った辺りに、以下を定義します。
```hlsl
// キャプチャテクスチャの歪み関連パラメータ
half4 _GrabDistortionParams;
#define GrabDistortionBase (_GrabDistortionParams.w)
#define GrabDistortionRGB (_GrabDistortionParams.rgb)
```

`SampleGrabTexture`内の定数使用箇所を以下のように修正します。
```hlsl
    half3 distortionScale = GrabDistortionRGB + GrabDistortionBase;
    half2 distortionR = normalVS.xy * distortionScale.r;
    half2 distortionG = normalVS.xy * distortionScale.g;
    half2 distortionB = normalVS.xy * distortionScale.b;
```

また、マテリアルプロパティとしても定義してマテリアル経由でパラメータ変更できるようにしておきます。`CrystalLit.shader`の5〜70行目付近に各種プロパティが定義されています。この中に、上記で定義した`_GrabDistortionParams`を加えます。
```hlsl
        _GrabDistortionParams("Grab Distortion Params", Vector) = (0.00, 0.02, 0.04, 0.1)
```

これでシェーダ側の実装は完了です。

### パラメータ調整用コンポーネントの実装
上記の独自追加したマテリアルプロパティは、本来は該当マテリアル選択時にインスペクタ上から調整できると使いやすいはずです。

ただ、今回作成したCrystalLitシェーダはURP/Litをベースにしており、CustomEditorの設定がURP/Litに対応したものとなっているため、独自に追加したプロパティがインスペクタ表示対象に含まれていません。CustomEditor設定を外せは全プロパティがインスペクタに表示されますが、URP/Litシェーダのインスペクタ上の使いやすさも諦める事になってしまいます。

しっかりと対応するのであれば、専用のCustomEditorスクリプトを実装するべきですが、URP/Lit用CustomEditorはinternalにて実装されているため、残念ながらお手軽に拡張する等ができません…

なので、この記事上では、該当オブジェクトにアタッチすればCrystalLitのパラメータ編集が可能となるコンポーネントを用意してお茶を濁します。

Rendererに設定されている全マテリアルに対して、該当プロパティが存在していれば設定を行うコンポーネントになります。（スクリプト全体は[こちら](#コンポーネントスクリプト)）OnValidateを定義しているので、コンポーネントのインスペクタからのパラメータ変更はすぐ反映されます。

このコンポーネントを使用すると以下のように調整できます。

https://twitter.com/aluminum1981/status/1597529425104764930

# まとめ
知見がない状態で見ると一見複雑そうに思える空間歪み系のシェーディングですが、タネが分かれば実はとても単純だったりします。歪みの各種計算も色々工夫してみると違った見え方になるかもしれません。少しでもなにかの参考になれば幸いです。

よりクリスタルっぽいシェーディングを目指すのであれば、しっかりカッティングされた宝石モデル等を使い、スペキュラ等のライティングも特殊化した上で調整したいところですね。

# 実装物

### シェーダ
最終的なシェーダ実装`CrystalLitForwardPass.hlsl`の全体を置いておきます。`CrystalLit.shader`の方にはあまり手を加えていない＆コード全体が無駄に長いため、記事中の改変箇所を参照して下さい。
```ｈｌｓｌ
#ifndef UNIVERSAL_CRYSTAL_FORWARD_LIT_PASS_INCLUDED
#define UNIVERSAL_CRYSTAL_FORWARD_LIT_PASS_INCLUDED

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"

// GLES2 has limited amount of interpolators
#if defined(_PARALLAXMAP) && !defined(SHADER_API_GLES)
#define REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR
#endif

#if (defined(_NORMALMAP) || (defined(_PARALLAXMAP) && !defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR))) || defined(_DETAIL)
#define REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR
#endif

// keep this file in sync with LitGBufferPass.hlsl

// キャプチャテクスチャ
TEXTURE2D(_CameraOpaqueTexture);
SAMPLER(sampler_CameraOpaqueTexture);
// キャプチャテクスチャの歪み関連パラメータ
half4 _GrabDistortionParams;
#define GrabDistortionBase (_GrabDistortionParams.w)
#define GrabDistortionRGB (_GrabDistortionParams.rgb)

struct Attributes
{
    float4 positionOS   : POSITION;
    float3 normalOS     : NORMAL;
    float4 tangentOS    : TANGENT;
    float2 texcoord     : TEXCOORD0;
    float2 lightmapUV   : TEXCOORD1;
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
    float2 uv                       : TEXCOORD0;
    DECLARE_LIGHTMAP_OR_SH(lightmapUV, vertexSH, 1);

#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
    float3 positionWS               : TEXCOORD2;
#endif

    float3 normalWS                 : TEXCOORD3;
#if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR)
    float4 tangentWS                : TEXCOORD4;    // xyz: tangent, w: sign
#endif
    float3 viewDirWS                : TEXCOORD5;

    half4 fogFactorAndVertexLight   : TEXCOORD6; // x: fogFactor, yzw: vertex light

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    float4 shadowCoord              : TEXCOORD7;
#endif

#if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
    float3 viewDirTS                : TEXCOORD8;
#endif

    float2 grabPos                  : TEXCOORD9;

    float4 positionCS               : SV_POSITION;
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

void InitializeInputData(Varyings input, half3 normalTS, out InputData inputData)
{
    inputData = (InputData)0;

#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
    inputData.positionWS = input.positionWS;
#endif

    half3 viewDirWS = SafeNormalize(input.viewDirWS);
#if defined(_NORMALMAP) || defined(_DETAIL)
    float sgn = input.tangentWS.w;      // should be either +1 or -1
    float3 bitangent = sgn * cross(input.normalWS.xyz, input.tangentWS.xyz);
    inputData.normalWS = TransformTangentToWorld(normalTS, half3x3(input.tangentWS.xyz, bitangent.xyz, input.normalWS.xyz));
#else
    inputData.normalWS = input.normalWS;
#endif

    inputData.normalWS = NormalizeNormalPerPixel(inputData.normalWS);
    inputData.viewDirectionWS = viewDirWS;

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    inputData.shadowCoord = input.shadowCoord;
#elif defined(MAIN_LIGHT_CALCULATE_SHADOWS)
    inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS);
#else
    inputData.shadowCoord = float4(0, 0, 0, 0);
#endif

    inputData.fogCoord = input.fogFactorAndVertexLight.x;
    inputData.vertexLighting = input.fogFactorAndVertexLight.yzw;
    inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS);
    inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);
    inputData.shadowMask = SAMPLE_SHADOWMASK(input.lightmapUV);
}

half3 SampleGrabTexture(InputData inputData, half2 grabPos)
{
    // キャプチャテクスチャのサンプリング
    // view空間法線方向にUVを歪ませる
    // RGB別に歪み量を変化させ擬似プリズム的な表現ができるように
    // 視線方向と向かい合っていない面ほど色ズレを目立たせる

    float3 normalVS = TransformWorldToViewDir(inputData.normalWS, false);
    half3 distortionScale = GrabDistortionRGB + GrabDistortionBase;
    half2 distortionR = normalVS.xy * distortionScale.r;
    half2 distortionG = normalVS.xy * distortionScale.g;
    half2 distortionB = normalVS.xy * distortionScale.b;

    half2 grabUvR = grabPos + distortionR;
    half2 grabUvG = grabPos + distortionG;
    half2 grabUvB = grabPos + distortionB;
    half colorR = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, grabUvR).r;
    half colorG = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, grabUvG).g;
    half colorB = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, grabUvB).b;

    return half3(colorR, colorG, colorB);
}

///////////////////////////////////////////////////////////////////////////////
//                  Vertex and Fragment functions                            //
///////////////////////////////////////////////////////////////////////////////

// Used in Standard (Physically Based) shader
Varyings LitPassVertex(Attributes input)
{
    Varyings output = (Varyings)0;

    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);

    // normalWS and tangentWS already normalize.
    // this is required to avoid skewing the direction during interpolation
    // also required for per-vertex lighting and SH evaluation
    VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);

    half3 viewDirWS = GetWorldSpaceViewDir(vertexInput.positionWS);
    half3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS);
    half fogFactor = ComputeFogFactor(vertexInput.positionCS.z);

    output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap);

    // already normalized from normal transform to WS.
    output.normalWS = normalInput.normalWS;
    output.viewDirWS = viewDirWS;
#if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR) || defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
    real sign = input.tangentOS.w * GetOddNegativeScale();
    half4 tangentWS = half4(normalInput.tangentWS.xyz, sign);
#endif
#if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR)
    output.tangentWS = tangentWS;
#endif

#if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
    half3 viewDirTS = GetViewDirectionTangentSpace(tangentWS, output.normalWS, viewDirWS);
    output.viewDirTS = viewDirTS;
#endif

    OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV);
    OUTPUT_SH(output.normalWS.xyz, output.vertexSH);

    output.fogFactorAndVertexLight = half4(fogFactor, vertexLight);

#if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR)
    output.positionWS = vertexInput.positionWS;
#endif

#if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR)
    output.shadowCoord = GetShadowCoord(vertexInput);
#endif

    output.positionCS = vertexInput.positionCS;

    half4 screenPos = ComputeScreenPos(output.positionCS);
    output.grabPos = screenPos.xy / screenPos.w;

    return output;
}

// Used in Standard (Physically Based) shader
half4 LitPassFragment(Varyings input) : SV_Target
{
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

#if defined(_PARALLAXMAP)
#if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR)
    half3 viewDirTS = input.viewDirTS;
#else
    half3 viewDirTS = GetViewDirectionTangentSpace(input.tangentWS, input.normalWS, input.viewDirWS);
#endif
    ApplyPerPixelDisplacement(viewDirTS, input.uv);
#endif

    SurfaceData surfaceData;
    InitializeStandardLitSurfaceData(input.uv, surfaceData);

    InputData inputData;
    InitializeInputData(input, surfaceData.normalTS, inputData);

    // albedoに_CameraOpaqueTextureを使用した疑似透過カラーを入れてクリスタル感を出す
    surfaceData.albedo = SampleGrabTexture(inputData, input.grabPos) * _BaseColor;

    half4 color = UniversalFragmentPBR(inputData, surfaceData);

    color.rgb = MixFog(color.rgb, inputData.fogCoord);
    color.a = OutputAlpha(color.a, _Surface);

    return color;
}
#endif
```

### コンポーネントスクリプト
```cs
using System;
using UnityEngine;

public class CrystalParametersSetter : MonoBehaviour
{
    private static readonly int GrabDistortionParamsShaderID = Shader.PropertyToID("_GrabDistortionParams");

    [SerializeField]
    [Range(-0.5f, 0.5f)]
    private float distortionBase = 0.0f;

    [SerializeField]
    [Range(-0.1f, 0.1f)]
    private float distortionR = 0.0f;

    [SerializeField]
    [Range(-0.1f, 0.1f)]
    private float distortionG = 0.0f;

    [SerializeField]
    [Range(-0.1f, 0.1f)]
    private float distortionB = 0.0f;

    private void Awake()
    {
        foreachMaterials(setupParameters);
    }

    private void Update()
    {
        foreachMaterials(setParameters);
    }

    private void OnValidate()
    {
        foreachMaterials(setParameters);
    }

    private void foreachMaterials(Action<Material> action)
    {
        if (action == null)
        {
            return;
        }

        Renderer renderer = GetComponent<Renderer>();
        if (renderer == null)
        {
            return;
        }

        int materialCount = renderer.sharedMaterials?.Length ?? 0;
        if (materialCount <= 0)
        {
            return;
        }

        foreach (Material material in renderer.sharedMaterials)
        {
            action.Invoke(material);
        }
    }

    private void setupParameters(Material material)
    {
        if (!material.HasProperty(GrabDistortionParamsShaderID))
        {
            return;
        }

        Vector4 distortionParams = material.GetVector(GrabDistortionParamsShaderID);
        distortionBase = distortionParams.w;
        distortionR = distortionParams.x;
        distortionG = distortionParams.y;
        distortionB = distortionParams.z;
    }

    private void setParameters(Material material)
    {
        if (!material.HasProperty(GrabDistortionParamsShaderID))
        {
            return;
        }

        Vector4 distortionParams = new Vector4(distortionR, distortionG, distortionB, distortionBase);
        material.SetVector(GrabDistortionParamsShaderID, distortionParams);
    }
}
```