画面を切断するポストプロセスのシェーダコード

画面を切断した様に見せるための、ポストプロセス用シェーダコードを作成しました。

https://x.com/ScreenPocket/status/1730859411101614268?s=20

# 注意点

今回はシェーダコードが結構大きくなってしまったので、シェーダコードの解説のみとなります。
組み込みの際は

https://qiita.com/ScreenPocket/items/4cefb11cb14aec2f7e24

のRendererFeature.csのコードなどを参考に、シェーダにパラメータを渡すための仕組みをご準備した上でご利用ください。

# シェーダコードについて
という事で、早速シェーダコードを張り付けます。

```hlsl:Slash.shader
Shader "ScreenPocket/URP/PostProcess/Slash"
{
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

            #pragma vertex Vert
            #pragma fragment Frag

            float _Weight;
            float _Angle;
            float _PositionX;
            float _PositionY;

            float _SlideWeight;
            float _SlideLength;

            float _OpenWeight;
            float _OpenSize;
            float4 _OpenColor;
            
            half4 Frag(Varyings input) : SV_Target
            {
                //回転行列を作成
                const half angleCos = cos(radians(_Angle));
                const half angleSin = sin(radians(_Angle));
                const half2x2 rotateMatrix = half2x2(angleCos, -angleSin, angleSin, angleCos);
                //ずらす向きの単位ベクトルを回す
                float2 slideDirection = mul(float2(0,1),rotateMatrix);

                //Aspect比を無視する（画面に合わせた傾き(45度指定で画面対角線を結ぶような切断）にしたいならこの計算は不要）
                const float aspect = _ScreenParams.x/_ScreenParams.y;
                slideDirection = normalize(float2(slideDirection.x,slideDirection.y * aspect));

                const float2 uv = input.texcoord;
                const float2 position = uv - float2(_PositionX,_PositionY);
                //UVの位置とずらす向きの単位ベクトルとの外積で、ピクセルが切断線のどっち側かが求まる
                const float3 crossUV = cross(float3(position,0),float3(slideDirection,0));
                //符号を取得
                const float signUv = sign(crossUV.z);

                //スライドでずらす
                const float slideLengthValue = _SlideLength * _SlideWeight * _Weight;
                const float2 slideLength = slideDirection * signUv * slideLengthValue;


                //切り傷の開く向きのベクトルも外積で求めておく
                const float3 openDirection = cross(float3(0,0,1),float3(slideDirection,0));

                //切り口を開く
                const float openSizeValue = _OpenSize * _OpenWeight * _Weight;
                const float2 openSize = openDirection * signUv * openSizeValue;


                //UVを確定
                const float2 finalUv = uv + (slideLength + openSize);
                float4 col = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, finalUv);

                //Slashの内側を単色で塗りつぶす
                //射影する
                float p = dot(openSize, position) / (openSizeValue * openSizeValue);
                //切り口の内側を黒、外側を白にする
                p = floor( saturate( abs(p) ) );

                //切り口の色と元の色で塗分ける
                col = lerp(_OpenColor, col, p);
            }
            ENDHLSL
        }
    }
}

```
各行にはコメントを書いておりますが、せっかくなので部分毎に合わせて解説を入れてみましょう。


## 切断する線のベクトルを作る

```hlsl
                //回転行列を作成
                const half angleCos = cos(radians(_Angle));
                const half angleSin = sin(radians(_Angle));
                const half2x2 rotateMatrix = half2x2(angleCos, -angleSin, angleSin, angleCos);
                //ずらす向きの単位ベクトルを回す
                float2 slideDirection = mul(float2(0,1),rotateMatrix);
```
先ずは、 **全ての基準となる切断する方向のベクトル** を作成します。
ベクトルを回転する方法については↓参考にさせて頂きました。

https://zenn.dev/kento_o/articles/64c8d585924c60633342

_Angleをラジアンに変更しているのと、0度で縦にしたいからfloat2(0,1)を回転させているところがポイントかな。
0度で真横にしたいならfloat2(1,0)に修正してください。

## アスペクト比に依存しない傾きにする

```hlsl
                //Aspect比を無視する（画面に合わせた傾き(45度指定で画面対角線を結ぶような切断）にしたいならこの計算は不要）
                const float aspect = _ScreenParams.x/_ScreenParams.y;
                slideDirection = normalize(float2(slideDirection.x,slideDirection.y * aspect));
```
ただ、そのままだと、例えばアスペクト比が縦長の画面で45度方向に切断しようとした際に、Viewportベースのマッピングなので厳密な45度にはならないのがカッコ悪い（図の赤ベクトル）

![20231202.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/52d65722-92a2-42db-b50d-13e8bf121239.png)
という事で、アスペクト比でベクトルを補正して、正規化する事で、画面アスペクト比に依存しないベクトル向き角度に補正を行います。

## ベクトル外積で切断線の右か左かを判定する

```hlsl
                const float2 uv = input.texcoord;
                const float2 position = uv - float2(_PositionX,_PositionY);
                //UVの位置とずらす向きの単位ベクトルとの外積で、ピクセルが切断線のどっち側かが求まる
                const float3 crossUV = cross(float3(position,0),float3(slideDirection,0));
                //符号を取得
                const float signUv = sign(crossUV.z);
```
外積の特性として **「ベクトルAと、ベクトルBの外積を取ると、Aに対してBが右か左かで奥行きを示すベクトルの向きが切り替わる」** という性質が有ります。面の当たり判定などでよく使うアレです。

crossUV.zを表示してみると下記の感じ
<img width="200" alt="スクリーンショット 2023-12-02 154127.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/4119401a-576b-a264-b1c2-5abf04c0a76b.png">
画面中央(切断線)を0として、左負、右正となっています。
コレをsign()で-1、+1に変換しておきます。
signUVを表示するとこんな感じ。

<img width="200" alt="スクリーンショット 2023-12-02 154546.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/66e602af-9720-6846-9601-eef083431af6.png">


## 切り口と平行にスライドでずらす

```hlsl
                //スライドでずらす
                const float slideLengthValue = _SlideLength * _SlideWeight * _Weight;
                const float2 slideLength = slideDirection * signUv * slideLengthValue;
```
ココは図解するまでもなく、先ほど作った符号に合わせて、スライドする距離(slideLength)を作成しています。
先ほどの符号を利用する事で、切断面の左右でずらす向きを逆にすることが出来るわけです。

## ベクトル外積で、切り口を開く方向のベクトルを取得する

```hlsl
                //切り傷の開く向きのベクトルも外積で求めておく
                const float3 openDirection = cross(float3(0,0,1),float3(slideDirection,0));
```
外積のもう一つの性質 **「ベクトルA、Bの外積で求まるベクトルは、ABと垂直の方向のベクトルとなる」** を利用して、切断方向ベクトルと画面の奥向きベクトル(float3(0,0,1))で切断線と垂直のベクトルを求めます。
コレが切り口を開く方向(openDirection)となるわけです。
![20231202b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/b6262b7f-da45-ba9d-083f-c34b7f31a0b8.png)
フレミングの左手で言うと

 * 中指が奥向きベクトル
 * 人差し指が切断方向ベクトル
 * 親指が、その外積で求まるベクトル

になる感じと言うか。

## 開く方向にズラす

```hlsl
                //切り口を開く
                const float openSizeValue = _OpenSize * _OpenWeight * _Weight;
                const float2 openSize = openDirection * signUv * openSizeValue;
```
切り口を開く際には、スライド方向のズラシしで使った符号(signUV)も活用しましょう。
これにより、線の左右で開く向きが逆になります。

## ずらした上でサンプリング

```hlsl
                //UVを確定
                const float2 finalUv = uv + (slideLength + openSize);
                float4 col = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, finalUv);
```

必要な情報が求まったのでテクスチャをサンプリングします。
openSizeをある程度広げた状態で、このcolを表示するとこんな感じ。
<img width="200" alt="スクリーンショット 2023-12-02 155848.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/b333ac92-6a86-599a-be59-a469bd15d504.png">
広げた分、左右に映像がズレていますが、赤枠部分は切断の向こうが見えるべき範囲なので、最後に塗り分けを行いましょう。

## ベクトル射影で、切り口の内側か外側を判定する

```hlsl
                //射影する
                float p = dot(openSize, position) / (openSizeValue * openSizeValue);
                //切り口の内側を黒、外側を白にする
                p = floor( saturate( abs(p) ) );
```

**あるベクトルAを、もう一つのベクトルBに射影する事で、ベクトルBの線上におけるベクトルAの割合を取得する**ことが出来ます。
**射影直後のabs(p)** を表示するとこんな感じ。
※負数だと見えないのでabs()で絶対値化しています
<img width="200" alt="スクリーンショット 2023-12-02 162038.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/f2e81c3d-d91b-5cf2-5e47-e3906e14fbc2.png">
切断線赤を0として、openSizeの範囲にかけて1になる模様が出来ました。
後はこれを **saturate()で0~1にした上でfloor()で少数部を捨てて1と0にする** ことで下記の模様が出来ます

<img width="200" alt="スクリーンショット 2023-12-02 162827.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/7cc98bd2-f57e-97c0-8b4c-038a6c904f10.png">

## 色合成して完成！

```hlsl
                //切り口の色と元の色で塗分ける
                col = lerp(_OpenColor, col, p);
```

後は上図の白黒画像を用いて色を合成する事で完成となります。
黒にOpenColor、白にズラしてサンプリングした画像を張り付けて出来上がり！

![スクリーンショット 2023-12-02 163235.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61266/cdc15482-fb3d-d214-a27a-766fb329fa47.png)


# 終わりに

という事で、画面切り裂きポストプロセスでした。
よくある手法としては一度RenderTextureに描画したものをポリゴンに張り付けて動かす、とう方式が有りますが、それをしなくとも分断できるのは中々利点ではないでしょうか。
単色塗りつぶしを活かして、画面転換にも利用できるのではないかな？と考えています。白でスパッと切った後で、白フェードインで次の画面へ、とかね。

次回作でどこかに使いたいですね～

# 追記

「画面外の描画されていない部分が引き延ばされませんか？」というツッコミを頂きましたが **正にその通りです**。ご了承の上でご利用ください。
画面端に近づけば近づくほどズラし幅を抑えるとか、
画面領域外を黒で塗るとか、
切断する時だけ画面端を塗りつぶすとかもできるのですが、
今回の記事の趣旨としては **「ベクトル外積、射影で切断したようなマッピングが出来るよ！」** が一番伝えたい事のため、敢えて対策の詳細については記載しないでおこうと思います。（もし記載するなら別記事かな…）

良い感じのカスタマイズを是非チャレンジしてみて下さい！