Windowsターミナルのカスタマイズ with hlsl

# Windowsターミナルhlsl拡張
Windowsターミナルでは外観をシェーダーを使ってアニメーションさせることができるようになっていて、かなり自由度の高い外観にすることが可能です。
今回は前回作成した画面横揺れエフェクトを元にWindowsターミナルをレトロ感あふれる砂嵐風の外観にしてみたいと思います。

# 横揺れエフェクトについて
今回使用するシェーダーは以下の動画を参考にしたものとなっています。
<iframe width="560" height="315" src="https://www.youtube.com/embed/Ylq2y6Qez3s?si=J6fky1DfwaactFKp" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
hlslが使えることにより、Unityなどの記事を参考にできるのは非常に便利だと思います。
Unityのシェーダー記事にはいろいろなものがあるので、気に入ったものから試してみてはいかがでしょうか。

# WindowsTerminal組み込みエフェクト
WindowsTerminalの設定をいろいろいじった人ならわかるかもしれませんが、WindowsTerminalには既存のレトロエフェクトがあります。
実はこれもhlslのシェーダーにより効果がつけられていて、Microsoftが無料で公開しています。
![スクリーンショット 2024-12-06 211413.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949075/b2e38c01-4e2f-c79b-02d9-195dc7a4c033.png)
以下のリンクがそのシェーダーのソースコードになります。
[WindowsTerminal PixelShaders](https://github.com/microsoft/terminal/blob/main/samples/PixelShaders/Retro.hlsl)

今回はこれを元に改造して横揺れエフェクトを作るようにします。

# シェーダー作成
hlslは以下の通りになります。
```hlsl
// The original retro pixel shader
Texture2D shaderTexture;
SamplerState samplerState;

cbuffer PixelShaderSettings {
    float time;
    float scale;
    float2 resolution;
    float4 background;
};

#define SCANLINE_FACTOR 0.5f
#define SCALED_SCANLINE_PERIOD scale
#define SCALED_GAUSSIAN_SIGMA (2.0f * scale)

static const float M_PI = 3.14159265f;

// ブラー処理(レトロエフェクトに既存)
float Gaussian2D(float x, float y, float sigma) {
    return 1 / (sigma * sqrt(2 * M_PI)) * exp(-0.5 * (x * x + y * y) / sigma / sigma);
}

// ブラー処理(レトロエフェクトに既存)
float4 Blur(Texture2D input, float2 tex_coord, float sigma) {
    float width, height;
    shaderTexture.GetDimensions(width, height);

    float texelWidth = 1.0f / width;
    float texelHeight = 1.0f / height;

    float4 color = {
        0, 0, 0, 0
    };

    float sampleCount = 13;

    for (float x = 0; x < sampleCount; x++) {
        float2 samplePos = {
            0, 0
        };
        samplePos.x = tex_coord.x + (x - sampleCount / 2.0f) * texelWidth;

        for (float y = 0; y < sampleCount; y++) {
            samplePos.y = tex_coord.y + (y - sampleCount / 2.0f) * texelHeight;
            color += input.Sample(samplerState, samplePos) * Gaussian2D(x - sampleCount / 2.0f, y - sampleCount / 2.0f, sigma);
        }
    }

    return color;
}

float SquareWave(float y) {
    return 1.0f - (floor(y + time*10 / SCALED_SCANLINE_PERIOD) % 2.0f) * SCANLINE_FACTOR;
}

// 走査線を作成する関数
float4 Scanline(float4 color, float4 pos) {
    float wave = SquareWave(pos.y);

    if (length(color.rgb) < 0.2f && false) {
        return color + wave * 0.1f;
    }
    else {
        return color * wave;
    }
}

// ノイズを作成する関数
float2 gradientNoise_dir(float2 p) {
    p = p % 289;
    float x = (34 * p.x + 1) * p.x % 289 + p.y;
    x = (34 * x + 1) * x % 289;
    x = frac(x / 41) * 2 - 1;
    return normalize(float2(x - floor(x + 0.5), abs(x) - 0.5));
}

// ノイズを作成する関数
float gradientNoise_func(float2 p) {
    float2 ip = floor(p);
    float2 fp = frac(p);
    float d00 = dot(gradientNoise_dir(ip), fp);
    float d01 = dot(gradientNoise_dir(ip + float2(0, 1)), fp - float2(0, 1));
    float d10 = dot(gradientNoise_dir(ip + float2(1, 0)), fp - float2(1, 0));
    float d11 = dot(gradientNoise_dir(ip + float2(1, 1)), fp - float2(1, 1));
    fp = fp * fp * fp * (fp * (fp * 6 - 15) + 10);
    return lerp(lerp(d00, d01, fp.y), lerp(d10, d11, fp.y), fp.x);
}

// ノイズを作成する関数
float GradientNoise(float2 UV, float Scale) {
    return gradientNoise_func(UV * Scale) + 0.5;
}

// 入力をリマップする関数
float4 Remap_Float(float4 In, float2 inMinMax, float2 outMinMax) {
    return outMinMax.x + (In - inMinMax.x) * (outMinMax.y - outMinMax.x) / (inMinMax.y - inMinMax.x);
}

// 揺れをランダムに起こさせるための関数
float StepLighting() {
    float step = GradientNoise(time * 10, 1.0f);
    return step * step;
}

// main関数
float4 main(float4 pos : SV_POSITION, float2 tex : TEXCOORD) : SV_TARGET
{
    float gradientNoise = Remap_Float(GradientNoise(tex.y * time * 10, 1.0f), float2(0,1), float2(-0.005f,0.005f)) * StepLighting();
    float2 uv = tex - gradientNoise;
    // TODO:GH#3930 Make these configurable in some way.
    float4 color = shaderTexture.Sample(samplerState, uv);
    color += Blur(shaderTexture, uv, SCALED_GAUSSIAN_SIGMA) * 0.3f;
    color = Scanline(color, pos);

    return color;
}
```
WindowsTerminalでは自身の現在映している画面がテクスチャになり、上部の`samplerState`で入力されます。
`shaderTexture.Sample(samplerState, TEXCOORD)`で画面のテクスチャが入手できます。
今回はTEXCOORDにノイズを加えてやることで、横揺れの動きを表現します。
ですので、`main()`に与えられた`tex`にノイズを加えて`Sample()`の`TEXCOORD`に入れている形となります。
その後既存のブラー処理と`Scanline`処理を加えて出力している形となります。

# ノイズ処理等について
今回ノイズを作成するにあたって、Unityが開発した`ShaderGraph`のマニュアルに載ってある関数をコピペしてノイズ処理を行っています。
その他、`Remap`についてもUnityのマニュアルに載っていた関数になります。
参考としてノイズ関数のリンクを貼っておきます。
[Unity ShaderGraph GradientNoise](https://docs.unity3d.com/ja/Packages/com.unity.shadergraph@10.0/manual/Gradient-Noise-Node.html)
Unityのマニュアルでは`Out float out`で結果を返していますが、戻り値の型を記述し、`return`で値を返す形としていますが何でもよいです。

あとは`main()`で上記の動画と同様の処理を行えば、まったく同じようなエフェクトがWindowsTerminalで使えるようになります。

# WindowsTerminalに登録
それでは最後に作成したhlslをWindowsTerminalに登録する方法です。
WidowsTerminalの設定を開き、メニューの一番下に「Jsonファイルを開く」があると思うのでこれをクリックして開きます。
そのファイルの`profiles`,`defaults`の中に`"experimental.pixelShaderPath": "{先ほど作成したhlslのパス}",`と書き足せば反映されます。
一応その箇所だけ載せておきます
```diff_json
"profiles": 
    {
        "defaults": 
        {
            "antialiasingMode": "cleartype",
            "colorScheme": "Pro Light",
+           "experimental.pixelShaderPath": "C:\\Users\\VScode\\RetroPixel.hlsl",
            "experimental.retroTerminalEffect": false,
            "font": 
            {
                "face": "Consolas"
            },
            "intenseTextStyle": "none",
            "opacity": 30,
            "padding": "12",
            "useAcrylic": true
        },
        "list": 
        [
            {
                "commandline": "%SystemRoot%\\System32\\cmd.exe",
                "elevate": false,
                "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}",
                "hidden": false,
                "name": "CommandPrompt"
            },
            ...
        ]
    },
    "schemes": ...
```
以上でWindowsターミナルの外観変更ができました。