🦀

Rust + wgpu v28 でハードウェアレイトレーシングに入門する

に公開

Rust + wgpu v28 でハードウェアレイトレーシングに入門する

RustのWebGPU実装である wgpu は日々進化していますが、v28時点でも ハードウェアアクセラレーションを利用したレイトレーシング(Ray Query / Inline Ray Tracing) に関する日本語情報はほぼ皆無です。

「wgpu レイトレ」で検索しても出てくるのはCompute Shaderで交差判定を自作する「ソフトウェアレイトレ」ばかり……。
しかし、私たちは RTコア を使いたいのです。

この記事では、wgpu v28 の experimental 機能を使い、TLAS/BLAS を構築してハードウェアレイトレーシングを行うための最小構成の実装を紹介します。

この記事では以下を扱います。

  • wgpu v28 の experimental Ray Query を有効化する方法
  • BLAS / TLAS を正しく構築するための実践的な設計
  • Ray Query のヒット情報から法線・マテリアルを復元する方法

GitHubリポジトリはこちらです(タグ v0.1 が本記事に対応する最小構成版です)。
https://github.com/kokutoupan/fast-raytracing-wgpu/tree/v0.1

Cornell Boxをハードウェアレイトレでレンダリングした様子

動作環境と前提

wgpu v28 の Ray Query(ハードウェアレイトレーシング)を使用するには、
Vulkan バックエンドが有効である必要があります。
また、GPU がハードウェアレイトレーシング(RT コア相当)に対応していることが前提となります。

本記事の実装は、以下の環境で動作を確認しています。

  • NVIDIA GeForce RTX 2070 (Windows 11)

  • AMD Radeon 890M Graphics (RADV GFX1150)

    • OS: EndeavourOS x86_64
    • Driver: Mesa 25.3.3-arch1.1

1. 依存関係のセットアップ

まずは Cargo.toml です。wgpu28.0.0 を指定します。
数学ライブラリには glam、ウィンドウ管理には winit を使用します。

cargo.toml
[dependencies]
winit = "0.30"
wgpu = { version = "28.0.0" }
pollster = "0.4.0"
glam = "0.30"
bytemuck = { version = "1.24", features = ["derive"] }
env_logger = "0.11"
log = "0.4"

2. デバイスの初期化と機能有効化

ここが最初の難関です。wgpu でレイトレーシングを使うには、アダプター取得時とデバイス作成時に明示的に機能を要求する必要があります。

インスタンスとアダプターの作成

Vulkan バックエンドを優先してインスタンスを作成します。

src/wgpu_ctx.rs
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
    backends: wgpu::Backends::VULKAN, // Windows/LinuxならVulkan推奨
    flags: wgpu::InstanceFlags::from_env_or_default(),
    ..Default::default()
});

// ... Surface作成 ...

let adapter = instance
    .request_adapter(&wgpu::RequestAdapterOptions {
        compatible_surface: Some(&surface),
        ..Default::default()
    })
    .await
    .unwrap();

ここで、取得したアダプターが本当にレイトレーシングに対応しているかチェックしておくと親切です。

let features = adapter.features();
if features.contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY) {
    println!("✅ Hardware Ray Tracing (Ray Query) is supported!");
} else {
    panic!("❌ Hardware Ray Tracing is NOT supported on this adapter.");
}

デバイスの作成 (DeviceDescriptor)

ここが重要です。request_device を呼ぶ際に、以下の設定を行います。

  1. required_features: EXPERIMENTAL_RAY_QUERY を追加。
  2. required_limits: 加速構造体(AS)のために必要な最小リミットを設定。これを忘れると実行時にパニックします。
src/wgpu_ctx.rs
let (device, queue) = adapter
    .request_device(&wgpu::DeviceDescriptor {
        label: None,
        // 必須機能としてRay Queryを指定
        required_features: wgpu::Features::EXPERIMENTAL_RAY_QUERY,
        
        // 加速構造体のためのリミット設定を自動で行うヘルパーを使用
        required_limits: wgpu::Limits::default()
            .using_minimum_supported_acceleration_structure_values(),
        
        // unsafeブロックでExperimental機能を許可
        experimental_features: unsafe { wgpu::ExperimentalFeatures::enabled() },
        
        ..Default::default()
    })
    .await
    .unwrap();

experimental_featureswgpu のバージョンによっては要求の仕方が変わる可能性がありますが、v28時点ではこのように unsafe ブロック経由で有効化フラグを渡す必要があります。

⚠️ ExperimentalFeatures の有効化について

wgpu v28 の Ray Query は experimental 機能のため、
デバイス作成時に
experimental_features: ExperimentalFeatures::enabled()
を指定する必要があります。

私はこれを入れ忘れていて、原因不明のエラーにしばらく詰まりました。

なお、using_minimum_supported_acceleration_structure_values
を指定しない場合、AS 用バッファサイズが 0 として扱われ、
TLAS / BLAS 作成時に実行時パニックが発生します。

3. シーンの構築とデータ管理

デバイスの準備ができたら、レイトレーシングの定番「コーネルボックス」を構築していきます。

ハードウェアレイトレーシングの実装では、単に形状を作るだけでなく、「シェーダーから頂点データへどうアクセスするか」 を設計するのが最大のポイントです。Ray Queryのヒット結果からは「どのプリミティブに当たったか」しか分からず、法線やUV座標は自動では取得できないからです。

このセクションでは、シーン構築と同時に、効率的なデータアクセスのための Global Buffer戦略 についても解説します。

3-1. 形状 (BLAS) の作成とアライメント

まずは、シーンに登場する基本形状(平面、立方体、球体)の BLAS (Bottom-Level Acceleration Structure) を作成します。これはメッシュの頂点データそのものを保持する構造体です。

重要なのは頂点データの定義です。 wgpu で扱う際、Rust側の頂点構造体は [f32; 4] (16バイト) アライメントで定義することを強く推奨します。

src/geometry.rs
// 頂点データ (32バイトサイズ / 16バイトアライメント)
// ※WGSLの構造体配列要件(vec4基準)に合わせるため、明示的にパディングを含めます
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
    pub pos: [f32; 4],    // xyz + padding(w)
    pub normal: [f32; 4], // xyz + padding(w)
}

これは、後ほどコンピュートシェーダーからこのバッファを storage バッファとして直接読み込む際に、トラブルを防ぐためです。

BLASの作成(リソース確保)は create_blas で行います。今回は geometry.rs にヘルパー関数を用意し、各形状のBLASを構築しています。

// src/scene.rs
pub fn create_cornell_box(device: &wgpu::Device, queue: &wgpu::Queue) -> SceneResources {
    // 1. 各ジオメトリの生成とBLAS構築
    let plane = geometry::create_plane_blas(device);
    let cube = geometry::create_cube_blas(device);
    let sphere = geometry::create_sphere_blas(device, 3);
    // ...
}

3-2. Global Vertex Buffer 戦略

次に、シェーダー内での法線補間の準備を行います。
メッシュごとに別々の頂点バッファを持っていると、シェーダーにバインドする際にスロット数が足りなくなります(WebGPUの制限は通常8〜16程度)。

そこで、シーン内の全メッシュの頂点とインデックスをそれぞれ1つの巨大なバッファ(Global Buffer)に結合 してしまいます。

src/scene.rs
    // 全メッシュのデータを1つにまとめる
    let mut global_vertices = Vec::new();
    let mut global_indices = Vec::new();
    let mut mesh_infos = Vec::new(); // 各メッシュのオフセット情報を記録

    let mut current_v_offset = 0;
    let mut current_i_offset = 0;

    // ヘルパー:メッシュ情報を登録してバッファに追加
    let mut add_mesh_to_global = |vertices: &[Vertex], indices: &[u32]| {
        mesh_infos.push(MeshInfo {
            vertex_offset: current_v_offset,
            index_offset: current_i_offset,
            pad: [0; 2],
        });
        global_vertices.extend_from_slice(vertices);
        global_indices.extend_from_slice(indices);
        
        current_v_offset += vertices.len() as u32;
        current_i_offset += indices.len() as u32;
    };

    // 各メッシュを追加 (Mesh ID: 0=Plane, 1=Cube, 2=Sphere)
    add_mesh_to_global(&plane.vertices, &plane.indices);
    add_mesh_to_global(&cube.vertices, &cube.indices);
    add_mesh_to_global(&sphere.vertices, &sphere.indices);

    // Storage Buffer としてGPUに転送
    let global_vertex_buffer = device.create_buffer_init(/* ... */);

これにより、シェーダー側では Mesh ID さえ分かれば、mesh_infos[mesh_id] を参照して正しい頂点データにアクセスできるようになります。

3-3. 配置 (TLAS) と IDパッキング

最後に、作成したBLASをシーン内に配置して TLAS (Top-Level Acceleration Structure) を構築します。
コーネルボックスの壁や床は、すべて「平面BLAS」を回転・移動させたインスタンスとして表現します。

ここで重要なのが、各インスタンスに割り当てる custom_data です。
wgpu の型定義上は u32 ですが、ハードウェア仕様上、有効なのは 下位24bit のみです。Vulkan / DXR の仕様では InstanceCustomIndex は 24bit までしか保証されないため、wgpu でも同様の制約があります。
ここに「Mesh ID」と「Material ID」をビット演算でパックして埋め込みます。

src/scene.rs
    // IDのパッキング: 上位16bit=MeshID, 下位16bit=MatID
    // ※実質24bitなのでMeshIDは上位8bit程度に収めるのが安全
    let encode_id = |mesh_id: u32, mat_id: u32| (mesh_id << 16) | mat_id;

    // 床 (Mesh=0:Plane, Mat=3:White)
    tlas[0] = Some(wgpu::TlasInstance::new(
        &plane_blas,
        Mat4::from_translation(Vec3::new(0.0, -1.0, 0.0)) * Mat4::from_scale(Vec3::splat(2.0)),
        encode_id(0, 3), // <--- ここでIDを指定
        0xff
    ));

    // ガラスの球体 (Mesh=2:Sphere, Mat=4:Glass)
    tlas[7] = Some(wgpu::TlasInstance::new(
        &sphere.blas,
        Mat4::from_translation(Vec3::new(0.4, -0.65, 0.3)) * Mat4::from_scale(Vec3::splat(0.75)),
        encode_id(2, 4),
        0xff
    ));

こうすることで、シェーダーでレイがヒットした際、hit.instance_custom_data を見るだけで、「どの形状データを使って法線補間すべきか」と「どのマテリアル色で塗ればいいか」が即座に分かります。

補足:BLAS / TLAS のビルドについて

本実装では、BLAS / TLAS をバッファとして作成した後、
CommandEncoder::build_acceleration_structures(...) を呼び出して
実際に GPU 上に加速構造体を構築しています。

このビルド処理は CommandEncoder に記録されるため、
フレームごとに再構築するか、静的シーンでは一度だけ行うかを
用途に応じて選択できます。

3-4. マテリアルの定義

マテリアル情報はシンプルな構造体の配列として定義し、Storage Bufferに格納します。

src/scene.rs
pub struct MaterialUniform {
    pub color: [f32; 4],
    pub emission: [f32; 4],
    pub extra: [f32; 4], // x: type, y: fuzz, z: ior, ...
}

今回の実装では type フィールドで Lambert(拡散反射)、Metal(金属)、Dielectric(ガラス/誘電体)を切り替えています。

4. コンピュートシェーダー (WGSL) での実装

データ構造の準備が整ったので、WGSLで実際にレイを飛ばし、パストレーシングを行います。

Ray Queryの記述

シェーダーのファイル先頭enable wgpu_ray_query; を宣言します。
そして、コンピュートシェーダー内で ray_query オブジェクトを初期化し、rayQueryProceed を呼ぶだけで、GPUが高速に交差判定を行ってくれます。

src/shader.wgsl
    var rq: ray_query;
    
    // TLAS (Top-Level Acceleration Structure) に対してクエリを発行
    rayQueryInitialize(&rq, tlas, RayDesc(0u, 0xFFu, T_MIN, T_MAX, r.origin, r.dir));
    
    // 交差判定の実行
    rayQueryProceed(&rq);

    // 結果の取得
    let hit = rayQueryGetCommittedIntersection(&rq);

    if hit.kind == 0u {
        return vec3f(0.0); // ミス(背景)
    }

頂点法線の補間とシェーディング

Global Buffer と IDパッキングのおかげで、ヒット情報からの法線取得は以下のように記述できます。
重心座標 (hit.barycentrics) を使って3頂点の法線をブレンドすることで、滑らかな曲面表現が可能になります。

src/shader.wgsl
// 1. ヒット情報からIDを復元
let raw_id = hit.instance_custom_data;
let mesh_id = raw_id >> 16u; 
// (mat_id は raw_id & 0xFFFFu で取得可能)

// 2. MeshInfoからオフセットを取得
let mesh_info = mesh_infos[mesh_id];
let idx_offset = mesh_info.index_offset + hit.primitive_index * 3u;

// 3. インデックスバッファから3頂点のIDを取得
let i0 = indices[idx_offset + 0u] + mesh_info.vertex_offset;
let i1 = indices[idx_offset + 1u] + mesh_info.vertex_offset;
let i2 = indices[idx_offset + 2u] + mesh_info.vertex_offset;

// 4. 頂点バッファから法線を取得して補間
let n0 = vertices[i0].normal.xyz;
let n1 = vertices[i1].normal.xyz;
let n2 = vertices[i2].normal.xyz;

let u = hit.barycentrics.x;
let v = hit.barycentrics.y;
let w = 1.0 - u - v;

let local_normal = normalize(n0 * w + n1 * u + n2 * v);

あとはこの local_normal をワールド座標系に変換し、マテリアルの種類に応じた散乱(反射・屈折)計算を行うことで、物理ベースのレンダリングが完成します。

5. まとめ

wgpu v28 の EXPERIMENTAL_RAY_QUERY を使うことで、Rustのみで、かつクロスプラットフォームを意識したハードウェアレイトレーシングの実装が可能になります。

まだ experimental な機能ではありますが、Compute Shader で自在にレイを飛ばせる Ray Query は非常に強力で、既存のレンダリングパイプラインへの組み込みも容易です。

ぜひ、RustでのGPUプログラミングに挑戦してみてください!

参考文献

Discussion