HLSL Tools for Visual Studio を URP に対応させてみた

皆さんこんにちは。
私が HLSL Tools for Visual Studio を URP に対応させようとした時に、ネットで調べても情報が少なすぎてめちゃくちゃ苦労したので、皆さんが同じように苦労しないで済むように私がやった事を記事としてまとめようと思います。
ご参考になれば幸いです。

## 1. HLSL Tools for Visual Studio の導入
まずは HLSL Tools for Visual Studio の導入から説明していこうと思います。
既に済んでいる方は飛ばしていただいて構いません。

まずは何でもいいので Visual Studio を開きます。
すると左上はこんな感じになってると思うので、「拡張機能」→「拡張機能の管理」をクリックします。
![Visual Studio の左上らへん.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/7a61e113-2762-0897-bbad-d9b2cd9fcf20.png)

拡張機能マネージャーが立ち上がるので、検索ボックスに「HLSL Tools」と打ちます。
すると、HLSL Tools for Visual Studio が一番上に出てきます。
私はインストール後なので表示が違いますが、画像右側の赤枠の辺りに「インストール」と書かれていると思うので、それを押してインストールし、いったんVisual Studio を閉じてください。
![HLSL Tools for Visual Studio の管理.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/aad860d8-9ba5-7537-ff1a-e9d29d2ba378.png)


閉じた後に VSIX Installer が勝手に立ち上がるので、「Modify」を押してしばらく待っていれば、インストール完了です。「close」を押して閉じましょう。


## 2. HLSL Tools for Visual Studio の設定
まずは「ツール」→「オプション」を開きます。
![オプション.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/d76104cf-348c-6bef-3988-2a1fd6a8b5c3.png)

「テキストエディタ―」→「ファイル拡張子」を開きます。
「エディター」を「HLSL Editor」にして、「拡張子」に「hlsl」と入力し、右側の「追加」ボタン（画像の「適用」と同じ位置にあるボタン）をクリックします。
下の画像のように、列が追加されれば成功です。
右下の「OK」をクリックして閉じます。
![拡張子と紐づけ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/cf67b187-0502-0301-82b8-52607a19f6d6.png)


## 3. HLSL コンポーネントの削除
実際に必要かどうかはわかりませんが、毎回警告が出てくるのが嫌な人はやってください。
最初から入ってない人は飛ばして大丈夫です。
入っているかどうか分からない人は確認しましょう。

「Visual Studio Installer」を起動します。
どこにあるか分からない人は、Windows の検索ボックスで「Visual Studio Installer」と打てば出てくると思います。
（出てこなかったら https://visualstudio.microsoft.com/ja/downloads/?cid=learn-onpage-download-install-visual-studio-page-cta からインストールしてください。基本的には「コミュニティ」の下にある「無料ダウンロード」をクリックすればいいと思います。）

自分が使っているバージョンの「変更」をクリックします。
![変更.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/492e800b-37de-345f-44d1-7447b53bcb64.png)

ウィンドウ右側から「HLSLツール」の項目を探してチェックを外します。通常は「Unityによるゲーム開発」か「C++によるゲーム開発」のオプションの所にあると思います。
チェックを外すと右下の「閉じる」が「変更」になるので、それをクリックしてください。
権限とか聞かれてもOKを押しとけばいいです。
![HLSLツールの削除.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/630b6e88-3918-c28a-0d06-9fe9b40405b7.png)

変更が終わったら、Visual Studio Installer を閉じましょう。


## 4. shadertoolsconfig.json の作成
ここからが本題です。ここら辺で詰まった人も多いと思います。

まずは、自分が使用しているUnityProjectのルートディレクトリに行き、「ShaderIncludes」フォルダと「shadertoolsconfig.json」ファイルを作成します。
![ファイル作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/f91b798c-4c2b-2892-f68a-4c98444f6240.png)

ShaderIncludesの中に、更に「Packages」というフォルダを作成します。
![Packages.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/829e8a71-f37b-0724-5b8b-342b3ce66ecd.png)

「shadertoolsconfig.json」の中身は以下のように記述します。
「"ShaderIncludes"」は先ほど作成したフォルダと同じ名前にしないと上手く動きません。
（下の画像は Unity6 LTS の場合です。別のバージョンを使用する際には、エクスプローラーから直接パスを確認してください。）
```json:shadertoolsconfig.json
{
	"hlsl.additionalIncludeDirectories": [
		".",
		"C:\\Program Files\\Unity\\Hub\\Editor\\6000.0.23f1\\Editor\\Data\\CGIncludes",
		"ShaderIncludes"
	]
}
```
記述が終わったら、Visual Studio を一度閉じてください。
試していないのでわかりませんが、一度閉じないと変更が反映されない可能性があります。

## 5. シンボリックリンクの作成
コマンドプロンプトを管理者として実行します。
Windows なら左下の検索ボックスに「cmd」と打てば出てきます。
「管理者として実行」をクリックします。
デバイスに変更を加えますか？とか聞かれますがOKを押しとけばいいです。
![cmd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/0d851d43-c92e-d2c6-ff47-7715867cea6f.png)

ここからはコマンドを打ち込んでいきます。
**「UnityProject」はあなたが使用しているプロジェクトのルートフォルダだと思ってください。**

最初は「cd」コマンドを使って、先程作った「ShaderIncludes」→「Packages」まで移動します。
「Packages」を右クリックしてパスをコピーし、コマンドプロンプト上で右クリックすればパスが貼り付けられると思います。
ちなみに、cd は change directory の略です。
```powershell:管理者：コマンドプロンプト
C:\Windows\System32>cd C:\～\UnityProject\ShaderIncludes\Packages
```

次に、「mklink」コマンドを使ってシンボリックリンクを作成します。
mklink の１つ目の値は作る際に付けられる名前、２つ目の値は実際に紐づけられるパスです。
基本的には下の3つを作れば大丈夫だと思います。
（追記：こちらの PackageCache 内にあるフォルダ名は後ろに「@~~~」という文字列が付いていることがあるようです。下のコマンドでうまく動かなければ確認してみてください。）
```powershell:管理者：コマンドプロンプト
C:\～\UnityProject\ShaderIncludes\Packages>mklink /d com.unity.render-pipelines.core ..\..\Library\PackageCache\com.unity.render-pipelines.core
C:\～\UnityProject\ShaderIncludes\Packages>mklink /d com.unity.render-pipelines.universal ..\..\Library\PackageCache\com.unity.render-pipelines.universal
C:\～\UnityProject\ShaderIncludes\Packages>mklink /d com.unity.shadergraph ..\..\Library\PackageCache\com.unity.shadergraph
```

最後にフォルダを確認し、｢Packages｣の中身がこのような状態になっていれば成功です。
![Packages中身.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/36384bfa-3be7-08e4-590d-c9b0db534615.png)

## 7. 最後の仕上げ！いよいよシェーダーを書く
今回はこちらのシェーダー（ https://qiita.com/flankids/items/a92b14834792a10798e5 ）をお手本にしつつ、どうすれば Intellisense がちゃんと機能するのかを見ていきましょう。

まず始めに、なんでもいいので .shader ファイルを作成します。
そして、同じ場所に同じ名前の .hlsl ファイルを作成します。
場所が分からない場合は、Unity 上で .shader ファイルを作ったフォルダに行き、何もない所で右クリックして「Show in Exploler」を押せばエクスプローラーが開くので、そこで .txt ファイルとかを作って拡張子ごと名前を変更すればいいと思います。
今回は MyUnlit.shader と MyUnlit.hlsl を作成してみました。
（どう考えてもシェーダーの中身がUnlitじゃないのは気にしないでください...）
![shader and hlsl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/0c0844f2-b9ab-2956-f234-e851a1a4810d.png)

実際のフォルダの中身がこのようになっていればOKです。
![check shader and hlsl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/dc713dd7-c5bc-2dca-9b1f-7ce389f03ff6.png)

続いて MyUnlit.shader と MyUnlit.hlsl の中身を書いていきます。
シェーダーの具体的な処理については解説しません。（というか、まだそんなにシェーダー詳しくありません...）
```MyUnlit.shader
Shader "Unlit/MyUnlit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags 
        {
            "RenderType"="Opaque"
            "RenderPipeline"="UniversalPipeline"
        }
        LOD 100

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


            //--------------------------- HLSLプログラム記述開始
            HLSLPROGRAM

            // 頂点シェーダー
            #pragma vertex vert

            // ピクセル (フラグメント) シェーダー
            #pragma fragment frag
            
            // フォグを使用する
            #pragma multi_compile_fog

            // シェーダーのコンパイル中のみ、この define が hlsl ファイル側で認識されます
            // これを使用して、インテリセンスを有効にするための include を行います
            #ifndef SHADER_COMPILING
            	#define SHADER_COMPILING
            #endif

            // hlslが記述されたファイルのインクルード
            #include_with_pragmas "/Assets/～/MyUnlit.hlsl"


            ENDHLSL
            //--------------------------- HLSLプログラム記述終了
        }
    }
}
```
```hlsl:MyUnlit.hlsl
#pragma once

// これらの define や include はインテリセンスを有効にする為のものであり、
// シェーダーのコンパイル中にこれらの処理が行われるとエラーの原因なります。
// (UnityCG.cginc をこのif文の外部で include するとエラーが出ますが、詳しい理由は分かりません。)
#ifndef SHADER_COMPILING
    #define SHADER_API_D3D11
    #include "UnityCG.cginc"
#endif

// シェーダーのインクルード
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"


// 頂点シェーダ―への入力
struct Attributes
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
	float3 normal : NORMAL;
};

// 頂点シェーダ―からフラグメントシェーダーへの出力
struct Varyings
{
    float2 uv : TEXCOORD0;
    float fogFactor: TEXCOORD1;
    float4 vertex : SV_POSITION;
	float3 normal : NORMAL;
};


// パラメータの定義
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

// コンスタントバッファの定義
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
CBUFFER_END


// 頂点シェーダー
Varyings vert (Attributes IN)
{
    Varyings OUT;
    
    OUT.vertex = TransformObjectToHClip(IN.vertex.xyz);
    OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);
    OUT.normal = TransformObjectToWorldNormal(IN.normal);
    OUT.fogFactor = ComputeFogFactor(OUT.vertex.z);
    
    return OUT;
}

// ピクセルシェーダー (フラグメントシェーダー)
float4 frag (Varyings IN) : SV_Target
{
    // テクスチャをサンプリング
    float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv);
    
    //------------------------------------------
    // ライト情報取得
	Light light = GetMainLight();
    
    // 法線とライト方向の内積
	float dotNormalLight = dot(IN.normal, light.direction);
    
    // 内積を0以上の値にする
	dotNormalLight = max(0, dotNormalLight);
    
    // 拡散反射
	float3 diffuse = light.color * dotNormalLight;
    
    // 拡散反射光を反映
	color.rgb *= diffuse;
    //------------------------------------------
    
    // フォグを適用
    color.rgb = MixFog(color.rgb, IN.fogFactor);
    
    // 色を返す
    return color;
}
```
これで MyUnlit.hlsl 内で変な赤線とかエラーとか出てなければ成功です！お疲れ様でした！


## .shader と .hlsl の解説
ここから先は半分くらい自己満足で書いてるので、読まなくても大丈夫です。

先ほど書いた .shader と .hlsl の中で一番重要な部分は、MyUnlit.shader ファイルに書かれている「#define SHADER_COMPILING」です。
**これは私が独自に考えた方法です。（えっへん。）**

![コンパ・イラ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726681/3d6dc6e6-0ec5-45d7-e6bd-1bb6d1cb5144.png)

#define で定義された「SHADER_COMPILING」は、コードを書いている最中は .shader ファイル内でしか認識されません。なぜなら、**.hlsl 内では .shader を認識していない**からです。

一方で、.shader がコンパイルされる際には「#define SHADER_COMPILING」を通過した後、.hlsl の中身をコンパイルしに行くので、**コンパイル時には .hlsl 内で「SHADER_COMPILING」が存在していることになります**。

そして、詳しい理由は分かりませんが、コンパイル中に「#include "UnityCG.cginc"」の処理が入ると何故かシェーダーがエラーになります。

なので、コンパイル中のみ「#ifndef SHADER_COMPILING」によってコンパイル中のみ「#include "UnityCG.cginc"」の処理をはじくことで、シェーダーのエラーを出さずに UnityCG.cginc 内の定義に関する Intellisense も働かせることができるようになるというわけです。

　　　　　　　・
　　　　　　　・
　　　　　　　・

ところで、MyUnlit.hlsl 内には「#define SHADER_API_D3D11」という記述がありますよね？
これについても少しお話ししておきます。

～/ShaderIncludes/Packages/com.unity.render-pipelines.core/Common.hlsl の中には、以下のような記述があります。
```c#:Common.hlsl
// Include language header
#if defined (SHADER_API_GAMECORE)
#include "Packages/com.unity.render-pipelines.gamecore/ShaderLibrary/API/GameCore.hlsl"
#elif defined(SHADER_API_XBOXONE)
#include "Packages/com.unity.render-pipelines.xboxone/ShaderLibrary/API/XBoxOne.hlsl"
#elif defined(SHADER_API_PS4)
#include "Packages/com.unity.render-pipelines.ps4/ShaderLibrary/API/PSSL.hlsl"
#elif defined(SHADER_API_PS5)
#include "Packages/com.unity.render-pipelines.ps5/ShaderLibrary/API/PSSL.hlsl"
#elif defined(SHADER_API_D3D11)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/API/D3D11.hlsl"
#elif defined(SHADER_API_METAL)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/API/Metal.hlsl"
#elif defined(SHADER_API_VULKAN)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/API/Vulkan.hlsl"
#elif defined(SHADER_API_SWITCH)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/API/Switch.hlsl"
#elif defined(SHADER_API_GLCORE)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/API/GLCore.hlsl"
#elif defined(SHADER_API_GLES3)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/API/GLES3.hlsl"
#elif defined(SHADER_API_WEBGPU)
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/API/WebGPU.hlsl"
#else
#error unsupported shader api
#endif
```
これらのうちのどれか1つが定義されていないと、TEXTURE, SAMPLER, CBUFFER_START, CBUFFER_END などのインテリセンスが働かなくなります。
私の使っているプロジェクトでは DirectX12 を使用しているので、おそらく SHADER_API_D3D11 を定義しておけばいいのかなと思って書きましたが、Unity の開発環境によっては変えた方がいいかもしれません。



## 終わりに
初投稿です。よろしくお願いします。
私自身、Unityのシェーダーをインテリセンスなしで書くのが嫌すぎてめちゃくちゃ調べたのですが、少なくとも日本語でこの件について書かれているものは2～3件しかなく、英語で書かれている記事もそんなに多くは無かったので、最終的にはUnityの内部のプログラムと長時間の激闘を余儀なくされました。
この件に関する記事が少なすぎて他の人も辛い思いをするだろうなということで、「皆を救えるのは私しかいない！」と思って初めて記事を書いてみました。
初投稿で至らぬ点も多いかと思いますが、ご容赦ください。
また、今回の「#define SHADER_COMPILING」を使用する方法はかなりの力業だと思われるので、もっと良い方法があれば教えていただけると幸いです。
ご覧いただきありがとうございました！
PS.こちらの方法はGitHubに対応していないので、方法が分かればまた更新します。（シンボリックリンクをGitHubで共有すると、他者の端末上では変なことになるみたいです。）

## 参考にさせていただいたサイト
●Unity 向けに HLSL Tools for Visual Studio を便利にするアセットを作ってみた
　https://tips.hecomi.com/entry/2020/12/20/000908

●HLSL Tools for Visual Studioで快適なUnityシェーダー開発をしていく
　https://qiita.com/sp4ghet/items/4470a197d309ae3a0780
 
●HLSL Tools for Visual Studio の README ページ
　https://github.com/tgjones/hlsltools

●[Unity]URPで始める！シェーダー入門
　https://qiita.com/flankids/items/a92b14834792a10798e5