🌋

Slang+VulkanでRaytracing

に公開

Slangとは

Slangとは(2025年現在)Khronos Groupが管理しているShader言語です。特徴としてマルチプラットフォームを主軸に置いており、GLSLやHLSL,CUDA,SPIR-Vなど様々なGPU向け言語に変換可能な中間言語的な立ち位置として開発されています。

https://shader-slang.org/

Slangの書き方は基本的にはHLSLを踏襲しており、Resource(Opaque Type)やvector,matrixなどの名前や扱い方はHLSLです。そこにいくつかGenericやInterface,Propertyなどオブジェクト指向的な機能や追加されているというような感じです(イメージ的にはHLSL++, HLSL#)。GLSLやHLSLに比べモダンな書き方ができますし、自動微分に対応しているなど様々興味深い機能が沢山ありますので、次世代のShader言語として最近注目されています。

今回の記事はSlangの入門として、Slangを使ったRaytracingについて解説していこうと思います。流れとしてはとてもシンプルなGLSLのシェーダーをSlangで置き換えるという感じでやっていきます。Slang自体の解説は都度、必要な分だけやる感じで進めていきます。

開発環境

今回の環境は以下の通りです。Vulkan Raytracingで実装を行っていきます。

  • Vulkan 1.4.304.1
  • Visual Studio 2022

SlangはVulkanのspirvと同様にシェーダーファイルを事前にコンパイルする必要があります。ありがたいことにSlangのコンパイラーはVulkanSDK/(Version)/Bin/slangc.exeという実行ファイルとしてVulkanSDKに内包されています。環境パスも通されており、コマンドプロンプトでslangcとして呼び出すことができます。例えばshader.slangというファイルをSPIR-Vのファイルshader.spvにコンパイルする場合はこんな感じのコマンドで行います。

slangc shader.slang -target spirv -o shader.spv

今回はSPIR-V形式以外には取り扱いませんがGLSLやHLSL、(未確認ですが)CUDAとかにも変換することができるみたいです。

Raytracing

それでは実際にRaytracingのShaderをslangに置き換えていきます。この記事ではVulkan自体の説明はしません。Vulkan RaytracingのチュートリアルとしてはNishiki氏による「ゼロからVulkan Ray Tracing」という記事がとても良いのでこちらを参考にしてください。

https://zenn.dev/nishiki/books/f468197dca2dd8

Vulkan Raytracingにおいて、最低限RayGen, Miss, ClosestHit Shaderの3つのShaderが必要です。今回、トライアングルだけを持つTLASだけを受け取り、トライアングルの重心座標を表示するシンプルなRayGenとMiss, ClosestHit Shaderを以下のように用意しました。これと等価なSlangシェーダーを書いていきましょう。

raygen.rgen
#version 460
#extension GL_EXT_ray_tracing : enable


struct Payload{
    vec3 color;
};

layout(location = 0) rayPayloadEXT Payload payload;

layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS;
layout(binding = 1, set = 0, rgba8) uniform image2D image;

void main()
{
    vec2 uv = (vec2(gl_LaunchIDEXT.xy) + vec2(0.5)) / vec2(gl_LaunchSizeEXT.xy);
    vec3 origin = vec3(0, 0, 5);
    vec3 target = vec3(uv * 2.0 - 1.0, 2);
    vec3 direction = normalize(target - origin);

    traceRayEXT(
        topLevelAS,
        gl_RayFlagsOpaqueEXT,
        0xff,
        0, 0, 0,
        origin,
        0.001,
        direction,
        10000.0,
        0 
    );

    uvec2 id = gl_LaunchIDEXT.xy;
    vec3 color = payload.color;
    imageStore(image, ivec2(id), vec4(color, 0.0));
}
closesthit.rchit
#version 460
#extension GL_EXT_ray_tracing : enable

struct Payload{
    vec3 color;
};

layout(location = 0) rayPayloadInEXT Payload payload;

hitAttributeEXT vec3 attribs;

void main()
{
    vec3 baryCoords = vec3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y);
    payload.color = baryCoords;
}
miss.rmiss
#version 460
#extension GL_EXT_ray_tracing : enable

struct Payload{
    vec3 color;
};

layout(location = 0) rayPayloadInEXT Payload payload;

void main()
{
    payload.color = vec3(0.0, 0.0, 0.0);
}

まずRayGenコードを書いていきましょう。まず、使用するResourceに対するbindingの設定を行います。SlangではデフォルトではDirectX向けのbinding設定で書きますが、Vulkan向けのbinding設定をAttribute([]で囲まれた識別子)で書くことができます。

[vk::binding(a, b)]

これはVulkan向けのbinidngをしますというものでこれはGLSLで言う所の

layout(binding = a, set = b)

と等価なbindingです。Raygenで定義されていたBindingはこんな感じですので

layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS;
layout(binding = 1, set = 0, rgba8) uniform image2D image;

Slangでは次のようにBindingを書く必要があります。

[vk_binding(0, 0)]
RaytracingAccelerationStructure topLevelAS;

[vk_binding(1, 0)]
RWTexture2D<float4> outputTexture;

これでTLASや出力用のStorageTextureのbindingができました。ここからRayGen Shaderを書いていきます。Slangでは1つのファイルに複数のシェーダーを書くことができ、各ShaderのEntry PointをEntry Point Attributeによって設定することができます。

Entry Pointとなる関数の前に次のようなAttributeを書く必要があります。

[shader("シェーダーステージ名")]

raygenという文字を指定すればRayGenのEntryPointとしてraygenMainという関数を指定することができます。

[shader("raygen")]
void raygenMain(){
~
}

ちなみにGLSLやSPIRVの変換後を見るとEntryPointの名前はSlang側の名前に限らずmainになるみたいです。Vulkanでの読み込みの際は注意してください。

ここからはSlang特有の書き方というよりかはHLSLへの書き換えという感じです。基本的にSlangで提供される関数や書き方はHLSLに踏襲しているので次のように書くことができます。

struct Payload
{
    float3 color;
};

[shader("raygen")]
void raygenMain()
{
    uint2 launchIndex = DispatchRaysIndex().xy;
    uint2 launchSize = DispatchRaysDimensions().xy;
    float2 uv = float2(launchIndex) / float2(launchSize);
    uv = uv * 2.0 - 1.0;

    Payload payload;

    RayDesc rayDesc;
    rayDesc.Origin = float3(0.0, 0.0, -2.0);
    rayDesc.Direction = normalize(float3(uv, 1.0));
    rayDesc.TMin = 0.001;
    rayDesc.TMax = 10000.0;

    TraceRay(topLevelAS, RAY_FLAG_NONE, 0xFF, 0, 0, 0, rayDesc, payload);

    float3 color = payload.color;

    outputTexture[launchIndex] = float4(color, 1.0);
}

それぞれのEntry Pointを指定することを忘れなければ、Miss,ClosestHitもHLSLの書き方で変換すれば問題ありません。

[shader("miss")]
void missMain(inout Payload payload)
{
    payload.color = float3(0.0,0.0,0.0);
}

[shader("closesthit")]
void closestHitMain(in BuiltInTriangleIntersectionAttributes attr, inout Payload payload)
{
    float2 bary = attr.barycentrics;
    payload.color = float3(1.0 - bary.x - bary.y, bary.x, bary.y);
}

コード全体はこんな感じになります

raytrace.slang
[vk_binding(0, 0)]
RaytracingAccelerationStructure topLevelAS;

[vk_binding(1, 0)]
RWTexture2D<float4> outputTexture;

struct Payload
{
    float3 color;
};

[shader("raygen")]
void raygenMain()
{
    uint2 launchIndex = DispatchRaysIndex().xy;
    uint2 launchSize = DispatchRaysDimensions().xy;
    float2 uv = float2(launchIndex) / float2(launchSize);
    uv = uv * 2.0 - 1.0;

    Payload payload;

    RayDesc rayDesc;
    rayDesc.Origin = float3(0.0, 0.0, -2.0);
    rayDesc.Direction = normalize(float3(uv, 1.0));
    rayDesc.TMin = 0.001;
    rayDesc.TMax = 10000.0;

    TraceRay(topLevelAS, RAY_FLAG_NONE, 0xFF, 0, 0, 0, rayDesc, payload);

    float3 color = payload.color;

    outputTexture[launchIndex] = float4(color, 1.0);
}

[shader("miss")]
void missMain(inout Payload payload)
{
    payload.color = float3(0.0,0.0,0.0);
}

[shader("closesthit")]
void closestHitMain(in BuiltInTriangleIntersectionAttributes attr, inout Payload payload)
{
    float2 bary = attr.barycentrics;
    payload.color = float3(1.0 - bary.x - bary.y, bary.x, bary.y);
}

このファイルをslangcでコンパイルしてあげればOKです。1つのファイル内に複数のShaderが書かれていますが、特に分ける必要はなく1つのSPIR-Vファイルに出力しても大丈夫です。Vulkan側がShaderStageを指定して読み込むため、それに応じて自動的に必要なEntryPointを判別してくれます。(地味にファイルが多くなりがちなのでありがたい)

slangc raytrace.slang -target spirv -o raytrace.spv

Vulkanでこのspvファイルを普通のShaderと同様に読み込めば、問題なく同じ画像が出力されます。

終わりに

ここまで読んでいただきありがとうございます。参考になれば幸いです。

今回使用したSlangの範囲ではそこまでありがたみを感じることはないかもしれませんが、(CUDA以外)既存のShader言語にはなかったオブジェクト指向的な機能や自動微分など様々な機能があるのでもっと高度なことをするとSlangの有用性が出てきそうだと感じます。いつかその辺も触ってみたいですね。

Slangを使っていて個人的に一番嬉しいと感じたのはVisual Studio, VS codeの標準のExtensionの補間がちゃんとしているところです。私はGLSLでよく書いていたのですが拡張機能などが入ってくると補間が存在せず、Vulkanの書き方なんかすると(表示上)エラーまみれで最悪のコーディング環境だったのですが、Slangは標準でVulkanやRaytracingなど近年のグラフィックス関連に対応してくれています。かなり良い開発体験でした。Slang流行ってほしい。

Reference

https://shader-slang.org/slang/user-guide/

Slang公式のRaytracing Sample
https://github.com/shader-slang/slang/tree/master/tests/vkray

Discussion