太郎Work

Unityとかで困ったこと等を残しておきます

GenerateHLSL完全に理解した

GenerateHLSLとは?

RenderPipelineのコードを見ているとあちこちで見かけるAttributeで、名前からして自動生成?
便利そうなので調べてみたのですが一切記事が出ず、公式リファレンスもほぼ説明がないのでまとめました
Unityは2021.3.14を使用しています

docs.unity3d.com


使い方

  1. 適当なクラス、または構造体を定義して GenerateHLSLAttribute を付ける
  2. Edit -> Rendering -> Generate Shader Includes で再生成
  3. C#ファイルと同階層にhlslファイルが生成される

機能

引数と実装を見ながら全て網羅したつもりです

構造体生成

特にGenerateHLSLに引数をつけなければC#と同じ構造体がhlslにも定義されます
Vector4やMatrix4x4はそれぞれfloat4, float4x4に変換され、それ以外の定義は型が同じフィールドがあれば自動で生成されます
自作の構造体の場合はフィールド名がコメントとして追加されるので分かりやすいです

また、配列の定義方法は特殊で fixedHLSLArrayAttribute をフィールドにつけることで変換されます
HLSLArray側で型を指定するのが重要です

public struct MyVector
{
    public float foo;
    public float bar;
    public float baz;
}

[GenerateHLSL]
public unsafe struct TestA
{
    public uint a1; // uint
    public bool a2; // bool
    public float a3; // float
    public Vector2 a4; // vector
    public Vector3 a5; // vector
    public Vector4 a6; // vector
    public Matrix4x4 a7; // matrix
    public float4 a8; // mathematics vector

    //public float4x4 a9; // float4を中で持っているのでコンバート失敗
    public int a10;

    //public ulong a11; // 64bitは不可
    public MyVector a12; // 自前の構造体

    [HLSLArray(32, typeof(Vector4))]
    public fixed float a13[32 * 4]; // 配列
}

↓生成コード

// Generated from TestA
// PackingRules = Exact
struct TestA
{
    uint a1;
    bool a2;
    float a3;
    float2 a4;
    float3 a5;
    float4 a6;
    float4x4 a7;
    float4 a8; // x: x y: y z: z w: w 
    int a10;
    float3 a12; // x: foo y: bar z: baz 
    float4 a13[32];
};

PackingRules

初期値はExactになっていますがAggressiveを指定すると型ごとに4つずつまとまります
ただし、いい感じに並べ替えるとかは無く、少しでも間にint等を混ぜると生成できません…
これいる?(URP, HDRPでは使われていなかった)

[GenerateHLSL(PackingRules.Aggressive)]
public struct TestB
{
    public float b1;
    public Vector3 b2;
    public Vector4 b3;
    public Matrix4x4 b4;
    public float4 b5;
    public MyVector b6;
    public float b7;
    public int b8;
    public int b9;
    public int b10;
    public int b11;
}

↓生成コード

// Generated from TestB
// PackingRules = Aggressive
struct TestB
{
    float4 b1_b2;
    float4 b3;
    float4x4 b4;
    float4 b5; // x: x y: y z: z w: w 
    float4 b6_b7; // x: foo y: bar z: baz 
    int4 b8_b9_b10_b11;
};

needAccessors

これをfalseにするとgetter的な関数が生成されなくなります
デフォルトだと構造体の下に以下のような関数も生成されますがこれが無くなります

//
// Accessors for TestA
//
uint GetA1(TestA value)
{
    return value.a1;
}
bool GetA2(TestA value)
{
    return value.a2;
}

needSetters

これをtrueにすると文字通りSetterも生成されます

//
// Accessors for TestD
//
float GetD1(TestD value)
{
    return value.d1;
}
//
// Setters for TestD
//
void SetD1(float newValue, inout TestD dest )
{
    dest.d1 = newValue;
}
//
// Setters for TestD
//
void InitD1(float newValue, inout TestD dest )
{
    dest.d1 = newValue;
}

needParamDebug

デバッグ用の関数が同時に生成されます

//
// Debug functions
//
void GetGeneratedTestEDebug(uint paramId, TestE teste, inout float3 result, inout bool needLinearToSRGB)
{
    switch (paramId)
    {
        case DEBUGVIEW_TESTE_E1:
            result = teste.e1.xxx;
            break;
    }
}

paramDefinesStart

needParamDebugのパラメータを何番目から生成するかを指定できるようです

omitStructDeclaration

structではなく変数定義になりました
が、存在しない構造体のGetterは生成されるのでコンパイルエラーになります

// Generated from TestF
// PackingRules = Exact
    float f1;
    float f2;
    float f3;
    float f4;

containsPackedFields

uintやfloatに指定Bitでパックすることが可能になります
使用する場合はPackingAttributeをフィールドにつけると関数が生成されます
最近は高速化のためにパックすることが多いのでうまく使えばかなり便利な気がします

[GenerateHLSL(containsPackedFields = true)]
public class TestG
{
    [Packing("g1A", FieldPacking.PackedUint, 8)]
    [Packing("g1B", FieldPacking.PackedUint, 8, 8)]
    [Packing("g1C", FieldPacking.PackedUint, 8, 16)]
    [Packing("g1D", FieldPacking.PackedUint, 8, 24)]
    public uint g1;
}

↓生成コード

// Generated from TestG
// PackingRules = Exact
struct TestG
{
    uint g1;
};

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl"
//
// Accessors for packed fields
//
uint Getg1A(in TestG testg)
{
    return BitFieldExtract(testg.g1, 0, 8);
}
uint Getg1B(in TestG testg)
{
    return BitFieldExtract(testg.g1, 8, 8);
}
uint Getg1C(in TestG testg)
{
    return BitFieldExtract(testg.g1, 16, 8);
}
uint Getg1D(in TestG testg)
{
    return BitFieldExtract(testg.g1, 24, 8);
}
//
// Setters for packed fields
//
void Setg1A(uint newg1A, inout TestG testg)
{
    testg.g1 = BitFieldInsert(255 , (newg1A) , testg.g1);
}
void Setg1B(uint newg1B, inout TestG testg)
{
    testg.g1 = BitFieldInsert(255 << 8, (newg1B) << 8, testg.g1);
}
void Setg1C(uint newg1C, inout TestG testg)
{
    testg.g1 = BitFieldInsert(255 << 16, (newg1C) << 16, testg.g1);
}
void Setg1D(uint newg1D, inout TestG testg)
{
    testg.g1 = BitFieldInsert(255 << 24, (newg1D) << 24, testg.g1);
}
//
// Init functions for packed fields.
// Important: Init functions assume the field is filled with 0s, use setters otherwise. 
//
void Initg1A(uint newg1A, inout TestG testg)
{
    testg.g1 |= (newg1A) ;
}
void Initg1B(uint newg1B, inout TestG testg)
{
    testg.g1 |= (newg1B) << 8;
}
void Initg1C(uint newg1C, inout TestG testg)
{
    testg.g1 |= (newg1C) << 16;
}
void Initg1D(uint newg1D, inout TestG testg)
{
    testg.g1 |= (newg1D) << 24;
}

generateCBuffer

構造体ではなくConstantBufferとして定義
needAccessorsはfalseにしておいた方がよさそうです

[GenerateHLSL(generateCBuffer = true, needAccessors = false)]
public struct TestH
{
    public Vector4 h1;
}

↓生成コード

// Generated from TestH
// PackingRules = Exact
CBUFFER_START(TestH)
    float4 h1;
CBUFFER_END

少し脱線しますが自前のConstant Bufferを描画に使用したい場合は以下のようになります
最小コードにしているので実際はRendererFeature等でちゃんとCommandBufferを通して指定した方がいいです
ConstantBufferクラスを使用すれば簡単にComputeBufferを生成してくれます

public class TestDraw : MonoBehaviour
{
    void Update()
    {
        TestH testH = new TestH
        {
            h1 = Color.HSVToRGB((Time.time / 3f) % 1f, 1f, 1f),
        };
        ConstantBuffer.PushGlobal(testH, Shader.PropertyToID("TestH"));
    }

    private void OnDestroy()
    {
        ConstantBuffer.ReleaseAll();
    }
}

以下がシェーダファイルになります
自動生成されたCBufferが定義されているhlslをincludeしたらC#側で定義したh1にそのままアクセスできます

HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

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

struct appdata
{
    float4 vertex : POSITION;
};

struct v2f
{
    float4 vertex : SV_POSITION;
};

v2f vert(appdata v)
{
    v2f o;
    o.vertex = GetVertexPositionInputs(v.vertex.xyz).positionCS;
    return o;
}

half4 frag(v2f i) : SV_Target
{
    return h1;
}
ENDHLSL

constantRegister

ConstantBufferのRegisterインデックスを明示的に指定できるようです
自前で全てRenderPipelineを実装する場合は必要そうです

sourcePath

デフォルトの出力先はC#と同階層ですが、ファイル名を指定できます
別のディレクトリにも出力できました

SurfaceDataAttributes

フィールドにSurfaceDataAttributesを付けることで型の変更ができます
precision以外のパラメータはデバッグ用のパラメータとして使用されていました

[GenerateHLSL(needParamDebug = true)]
public struct TestI
{
    [SurfaceDataAttributes("foo")]
    public Vector4 i1;

    [SurfaceDataAttributes("bar", precision = FieldPrecision.Half)]
    public Vector4 i2;

    [SurfaceDataAttributes("baz", precision = FieldPrecision.Real)]
    public Vector4 i3;
}

↓生成コード

//
// TestI:  static fields
//
#define DEBUGVIEW_TESTI_FOO (1)
#define DEBUGVIEW_TESTI_BAR (2)
#define DEBUGVIEW_TESTI_BAZ (3)

// Generated from TestI
// PackingRules = Exact
struct TestI
{
    float4 i1;
    half4 i2;
    real4 i3;
};

列挙型

EnumにGenerateHLSLを付けると挙動が変化します
それぞれの値がそのまま定数として定義されます

[GenerateHLSL]
public enum EnumA
{
    E1,
    E2,
    E3 = 100,
}

↓生成コード

//
// EnumA:  static fields
//
#define ENUMA_E1 (0)
#define ENUMA_E2 (1)
#define ENUMA_E3 (100)

定数

staticフィールドはそのまま定数として定義されます
命名にクラス名が含まれないので別クラスで同一名を付けないよう気をつける必要があります
k_~, s_~ で命名すると最初の二文字が消されて定義されます

[GenerateHLSL]
public struct TestJ
{
    public static int J1 = 123;
    public static float s_J2 = 23.4f;
    public static float k_J3 = 3.14f;
}

↓生成コード

//
// TestJ:  static fields
//
#define J1 (123)
#define J2 (23.4)
#define J3 (3.14)

まとめ

おそらくこれで現状の実装は網羅できたかと思います
便利そうだな〜とずっと眺めてた機能だったので今後は使いこなしたいです
全てまとまったコードも置いておきます
このファイルを適当なところに置いてGenerateするだけでhlslが出来上がります
GenerateTestA.cs · GitHub