メインコンテンツへスキップ
Tech Playground
ゲーム開発

Rust Bevy 0.16 レンダリンググラフ破壊的変更マイグレーション完全版

Bevy 0.16の大規模なレンダリンググラフAPI刷新を徹底解説。Node trait変更、RenderGraphContext再設計、エッジ依存管理の新方式を実装例とともに紹介。

約8分で読めます

Bevy 0.16(2026年3月リリース)は、レンダリンググラフシステムに大規模な破壊的変更を導入しました。Node trait の根本的な再設計、RenderGraphContext の責任分離、エッジ依存管理の型安全化により、カスタムレンダーパスの実装方法が大きく変わっています。

本記事では、公式マイグレーションガイドと実際のコードベース変更を基に、0.15以前のレンダリンググラフコードを0.16へ移行する際の具体的な手順と新設計思想を解説します。

Bevy 0.16 レンダリンググラフ刷新の背景

Bevy 0.16のレンダリンググラフ改修は、以下の課題解決を目的としています。

0.15以前の問題点:

  • Node::run()&mut RenderGraphContext を受け取り、責任が曖昧
  • ノード間依存関係がランタイムエラーになりやすい(型安全性の欠如)
  • GPU同期制御とグラフトラバーサルのロジックが混在
  • カスタムノード実装時に不要な権限を持ちすぎる

0.16の改善点:

  • Node::run()RenderContext のみ受け取る(責任の明確化)
  • ViewNode trait による View 単位のレンダリング最適化
  • エッジ依存を input() メソッドで宣言的に管理
  • RenderGraphContext はグラフ構築専用に限定

以下のダイアグラムは、0.15と0.16のNode trait設計の違いを示しています。

graph TD
    A["Bevy 0.15 Node::run"] --> B["&mut RenderGraphContext<br/>(グラフ操作権限あり)"]
    A --> C["&RenderContext<br/>(GPU操作)"]
    A --> D["&World<br/>(ECSアクセス)"]
    
    E["Bevy 0.16 Node::run"] --> F["RenderContext<br/>(GPU操作のみ)"]
    E --> G["&World<br/>(ECSアクセス)"]
    
    H["Bevy 0.16 ViewNode::run"] --> I["&ViewEntity<br/>(View固有データ)"]
    H --> F
    H --> G
    
    style B fill:#ffcccc
    style F fill:#ccffcc

上図のように、0.16では RenderGraphContext へのアクセスが run() から削除され、ノードはグラフ構造を変更できなくなりました。これにより、レンダリング実行時の予測可能性が向上しています。

Node trait の破壊的変更とマイグレーション

0.15の実装パターン

use bevy::render::{
    render_graph::{Node, NodeRunError, RenderGraphContext},
    renderer::RenderContext,
};

struct MyCustomNode;

impl Node for MyCustomNode {
    fn run(
        &self,
        graph: &mut RenderGraphContext,
        render_context: &mut RenderContext,
        world: &World,
    ) -> Result<(), NodeRunError> {
        // graph経由でスロット取得(非推奨パターン)
        let view_entity = graph.view_entity();
        
        // レンダリングロジック
        let command_encoder = render_context.command_encoder();
        // ...
        
        Ok(())
    }
}

0.16の新実装パターン

use bevy::render::{
    render_graph::{Node, NodeRunError, RenderGraphApp},
    renderer::RenderContext,
};

struct MyCustomNode;

impl Node for MyCustomNode {
    fn run(
        &self,
        _graph: (), // 0.16では削除されたため空タプル
        render_context: &mut RenderContext,
        world: &World,
    ) -> Result<(), NodeRunError> {
        // RenderContext経由で直接リソース取得
        let view_entities = world.query::<&ViewTarget>();
        
        let mut command_encoder = render_context.command_encoder();
        // GPU コマンド発行
        
        Ok(())
    }
}

重要な変更点:

  • RenderGraphContext パラメータが完全削除
  • View情報は World クエリから直接取得
  • ノードはグラフ構造を変更できない(構築時のみ変更可能)

ViewNode trait による View 単位最適化

Bevy 0.16では、カメラ・ライトなどView単位でレンダリングを行うノード向けに ViewNode trait が新設されました。

use bevy::render::{
    render_graph::{ViewNode, NodeRunError},
    view::ViewTarget,
};

struct PostProcessNode;

impl ViewNode for PostProcessNode {
    type ViewQuery = &'static ViewTarget;
    
    fn run(
        &self,
        _graph: (),
        render_context: &mut RenderContext,
        view_target: &ViewTarget, // View固有データ
        world: &World,
    ) -> Result<(), NodeRunError> {
        // view_targetから直接テクスチャ取得
        let source = view_target.main_texture();
        
        // ポストプロセスパス実行
        let mut pass = render_context.begin_tracked_render_pass(
            RenderPassDescriptor {
                label: Some("post_process_pass"),
                color_attachments: &[/* ... */],
                // ...
            }
        );
        
        Ok(())
    }
}

以下のシーケンス図は、ViewNodeの実行フローを示しています。

sequenceDiagram
    participant RG as RenderGraph
    participant VN as ViewNode
    participant World as ECS World
    participant RC as RenderContext
    participant GPU
    
    RG->>World: Query<ViewTarget>実行
    World-->>RG: View Entity取得
    
    loop 各Viewに対して
        RG->>VN: run(view_target, render_context, world)
        VN->>World: クエリでリソース取得
        World-->>VN: Uniform/Texture返却
        VN->>RC: begin_tracked_render_pass()
        RC->>GPU: RenderPass作成
        VN->>RC: draw_commands発行
        RC->>GPU: コマンド送信
        GPU-->>RC: 完了通知
    end
    
    RG->>RG: 次のノードへ

ViewNode の利点:

  • View固有データへの型安全なアクセス
  • 複数カメラ環境での自動並列化(内部実装)
  • クエリの重複排除によるECSアクセスコスト削減

エッジ依存管理の新方式と型安全性

0.16では、ノード間依存を input() メソッドで事前宣言する方式に変更されました。

0.15のエッジ依存(ランタイムエラーのリスク)

// プラグイン初期化時
app.world.resource_mut::<RenderGraph>().add_node_edge(
    SHADOW_PASS,
    MAIN_PASS, // 依存先ノード
);

// run()内でスロット取得(存在チェック無し)
let shadow_map = graph.get_input_texture(0)?; // 実行時エラーの可能性

0.16のエッジ依存(コンパイル時型チェック)

use bevy::render::render_graph::{Node, RenderLabel};

#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct ShadowPass;

#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct MainPass;

impl Node for MainPassNode {
    fn input(&self) -> Vec<SlotInfo> {
        vec![
            SlotInfo::new("shadow_map", SlotType::TextureView),
        ]
    }
    
    fn run(
        &self,
        _graph: (),
        render_context: &mut RenderContext,
        world: &World,
    ) -> Result<(), NodeRunError> {
        // input()で宣言済みのため、必ず存在する
        let shadow_map = render_context.get_input_texture(0);
        
        // シャドウマップを使用したレンダリング
        Ok(())
    }
}

// グラフ構築時
render_app.add_render_graph_edges(ShadowPass, MainPass);

型安全性の向上:

  • input() で宣言されたスロットは必ず解決される
  • 未接続のエッジは起動時にパニック(ランタイムエラーを回避)
  • IDE補完によるスロット名のタイポ防止

以下の状態遷移図は、0.16のグラフバリデーションフローを示しています。

stateDiagram-v2
    [*] --> GraphConstruction: App起動
    
    GraphConstruction --> NodeRegistration: add_render_graph_node()
    NodeRegistration --> EdgeRegistration: add_render_graph_edges()
    EdgeRegistration --> Validation: グラフ構築完了
    
    Validation --> ValidGraph: すべてのinput()が解決
    Validation --> Panic: 未解決のエッジあり
    
    ValidGraph --> Execution: レンダリング開始
    Execution --> [*]
    
    Panic --> [*]: プログラム終了
    
    note right of Validation
        0.16では起動時に
        エッジの整合性を検証
    end note

RenderGraphContext の責任分離と構築フロー

0.16では、RenderGraphContext はグラフ構築専用となり、実行時には使用されません。

グラフ構築の新パターン

use bevy::{
    prelude::*,
    render::{
        RenderApp,
        render_graph::{RenderGraphApp, RenderLabel},
    },
};

#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
enum CustomGraphNodes {
    PrePass,
    MainPass,
    PostProcess,
}

pub struct CustomRenderPlugin;

impl Plugin for CustomRenderPlugin {
    fn build(&self, app: &mut App) {
        let render_app = app.sub_app_mut(RenderApp);
        
        // ノード登録
        render_app
            .add_render_graph_node::<PrePassNode>(
                CustomGraphNodes::PrePass
            )
            .add_render_graph_node::<MainPassNode>(
                CustomGraphNodes::MainPass
            )
            .add_render_graph_node::<PostProcessNode>(
                CustomGraphNodes::PostProcess
            );
        
        // エッジ依存定義(宣言的)
        render_app.add_render_graph_edges(
            CustomGraphNodes::PrePass,
            CustomGraphNodes::MainPass,
        );
        render_app.add_render_graph_edges(
            CustomGraphNodes::MainPass,
            CustomGraphNodes::PostProcess,
        );
    }
}

以下のフローチャートは、0.16のレンダリンググラフ構築と実行の分離を示しています。

flowchart TD
    A["App起動"] --> B["Plugin::build()"]
    B --> C["RenderApp取得"]
    C --> D["add_render_graph_node<br/>(ノード登録)"]
    D --> E["add_render_graph_edges<br/>(依存定義)"]
    E --> F["グラフバリデーション"]
    
    F --> G{エッジ整合性OK?}
    G -->|No| H["Panic(起動失敗)"]
    G -->|Yes| I["実行可能グラフ構築完了"]
    
    I --> J["レンダリングループ開始"]
    J --> K["各ノードのrun()実行"]
    K --> L["RenderContext経由で<br/>GPUコマンド発行"]
    L --> M["次フレーム"]
    M --> J
    
    style F fill:#ffffcc
    style G fill:#ccffff
    style H fill:#ffcccc
    style K fill:#ccffcc

設計思想の変化:

  • 構築フェーズ(build time)と実行フェーズ(run time)の明確な分離
  • グラフ構造の不変性保証(実行中の変更不可)
  • 依存解決の事前検証によるランタイムエラー削減

実践的なマイグレーション例:カスタムポストプロセス

0.15のカスタムブルームエフェクトを0.16へ移行する完全な例を示します。

0.15のコード

// 0.15のポストプロセスノード
pub struct BloomNode {
    threshold: f32,
}

impl Node for BloomNode {
    fn run(
        &self,
        graph: &mut RenderGraphContext,
        render_context: &mut RenderContext,
        world: &World,
    ) -> Result<(), NodeRunError> {
        let view_entity = graph.view_entity();
        let view_target = world.get::<ViewTarget>(view_entity).unwrap();
        
        // 入力テクスチャ取得(スロット0)
        let input_texture = graph.get_input_texture(0)?;
        
        // ブルーム処理
        // ...
        
        Ok(())
    }
}

0.16のコード

// 0.16のViewNode実装
pub struct BloomNode {
    threshold: f32,
}

impl ViewNode for BloomNode {
    type ViewQuery = &'static ViewTarget;
    
    fn run(
        &self,
        _graph: (),
        render_context: &mut RenderContext,
        view_target: &ViewTarget,
        world: &World,
    ) -> Result<(), NodeRunError> {
        // Viewクエリから自動取得されたview_targetを使用
        let input_texture = view_target.main_texture();
        
        // Bloom設定取得
        let bloom_settings = world.resource::<BloomSettings>();
        
        // ダウンサンプリングパス
        let downsampled = self.downsample_pass(
            render_context,
            input_texture,
            bloom_settings.threshold,
        )?;
        
        // アップサンプリング+ブレンド
        self.upsample_and_blend_pass(
            render_context,
            downsampled,
            view_target.out_texture(),
        )?;
        
        Ok(())
    }
}

// プラグイン登録
impl Plugin for BloomPlugin {
    fn build(&self, app: &mut App) {
        let render_app = app.sub_app_mut(RenderApp);
        
        render_app
            .add_render_graph_node::<ViewNodeRunner<BloomNode>>(
                Core3d, // ビルトイングラフに追加
                BloomLabel,
            )
            .add_render_graph_edges(
                Core3d,
                (
                    Node3d::Tonemapping, // 依存元
                    BloomLabel,          // 自ノード
                    Node3d::EndMainPass, // 依存先
                ),
            );
    }
}

マイグレーションのポイント:

  1. NodeViewNode への変換(View単位処理のため)
  2. graph.view_entity() → ViewQuery型パラメータ
  3. graph.get_input_texture()view_target.main_texture()
  4. エッジ定義を add_render_graph_edges() で明示化

パフォーマンス影響と最適化指針

Bevy 0.16のレンダリンググラフ改修による実測パフォーマンス影響(公式ベンチマーク “many_cubes” より):

項目0.150.16変化
グラフトラバーサル時間0.12ms0.08ms-33%
View単位ノード実行0.45ms0.31ms-31%
エッジ依存解決ランタイムコンパイル時0ms
メモリアロケーション512KB/frame384KB/frame-25%

最適化指針:

  • ViewNode を積極活用: 複数カメラ環境で自動最適化の恩恵
  • input() で依存を明示: ランタイムチェック削減
  • World クエリの効率化: QueryState のキャッシュで反復取得を高速化
// クエリのキャッシュ例
pub struct OptimizedNode {
    view_query: QueryState<&'static ViewTarget>,
}

impl FromWorld for OptimizedNode {
    fn from_world(world: &mut World) -> Self {
        Self {
            view_query: world.query(),
        }
    }
}

impl ViewNode for OptimizedNode {
    type ViewQuery = &'static ViewTarget;
    
    fn run(&self, /* ... */) -> Result<(), NodeRunError> {
        // 事前構築済みクエリを使用(毎フレームのクエリ構築コスト削減)
        for view_target in self.view_query.iter(world) {
            // 処理
        }
        Ok(())
    }
}

まとめ

Bevy 0.16のレンダリンググラフ刷新における重要ポイント:

  • Node::run() から RenderGraphContext が削除 — グラフ構造変更は構築時のみに限定
  • ViewNode trait の導入 — View単位レンダリングの型安全性と最適化を両立
  • エッジ依存の宣言的管理input() メソッドによるコンパイル時検証
  • 責任の明確化 — グラフ構築(build time)と実行(run time)の完全分離
  • パフォーマンス向上 — トラバーサル時間33%削減、メモリ使用量25%削減

マイグレーション作業は破壊的ですが、型安全性の向上と実行時エラーの削減により、長期的な保守性が大幅に改善されます。既存のカスタムレンダーパスは、本記事のパターンに従うことで段階的に移行可能です。

参考リンク

#Rust #Bevy #レンダリンググラフ #GPU最適化 #マイグレーション
シェア: