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

Bevy 0.18 Visibility Culling + LOD統合システム|描画コマンド削減とメモリ効率の両立実装【2026年5月】

Bevy 0.18の新Visibility Culling + LOD統合システムの実装を解説。フラスタムカリング、オクルージョンカリング、LOD自動切替により描画コマンドを60%削減し、メモリ使用量も40%低減する実装手法を実例付きで紹介。

約10分で読めます

Bevy 0.18で実現する次世代カリングシステム

2026年5月にリリースされたBevy 0.18では、Visibility CullingとLOD(Level of Detail)システムが統合され、大規模なオープンワールドゲームでの描画効率が劇的に向上しました。従来は別々のシステムとして実装されていたカリング処理とLOD切り替えが、単一のECSパイプラインで処理されるようになり、描画コマンドの発行回数を最大60%削減、GPUメモリ使用量も40%低減することが公式ベンチマークで確認されています。

この記事では、Bevy 0.18の新しいVisibility Culling + LODシステムの内部実装を詳細に解説し、実際のゲーム開発で活用するための実装パターンとパフォーマンスチューニング手法を紹介します。公式ドキュメントやGitHubのPull Request #12847の実装を基に、実践的な最適化テクニックを掘り下げます。

Visibility Cullingの3段階処理アーキテクチャ

Bevy 0.18のVisibility Cullingシステムは、以下の3段階で構成されています。

1. フラスタムカリング(Frustum Culling)

カメラの視錐台外にあるオブジェクトを除外する基本的なカリング手法です。Bevy 0.18では、AABB(Axis-Aligned Bounding Box)とカメラのフラスタム平面との交差判定が、SIMD命令(AVX2/NEON)を活用した並列処理で実装されています。

use bevy::prelude::*;
use bevy::render::primitives::Aabb;
use bevy::render::view::VisibleEntities;

#[derive(Component)]
struct CullingConfig {
    frustum_margin: f32, // フラスタム境界のマージン(ポップイン防止)
}

fn setup_frustum_culling(mut commands: Commands) {
    commands.spawn((
        Camera3dBundle {
            transform: Transform::from_xyz(0.0, 50.0, 100.0)
                .looking_at(Vec3::ZERO, Vec3::Y),
            ..default()
        },
        CullingConfig {
            frustum_margin: 5.0, // 5ユニットの余裕を持たせる
        },
    ));
}

// カスタムフラスタムカリングシステム
fn custom_frustum_culling(
    camera_query: Query<(&Camera, &GlobalTransform, &CullingConfig)>,
    mut visible_query: Query<(&Aabb, &GlobalTransform, &mut Visibility)>,
) {
    for (camera, cam_transform, config) in camera_query.iter() {
        if let Some(frustum) = camera.logical_viewport_rect() {
            // フラスタム平面の計算(マージン適用)
            let projection = camera.projection_matrix();
            let view = cam_transform.compute_matrix().inverse();
            let view_proj = projection * view;
            
            for (aabb, transform, mut visibility) in visible_query.iter_mut() {
                let world_aabb = aabb.transform(transform.compute_matrix());
                
                // SIMD最適化された交差判定(bevy内部実装)
                let is_visible = frustum_intersects_aabb_with_margin(
                    &view_proj,
                    &world_aabb,
                    config.frustum_margin,
                );
                
                *visibility = if is_visible {
                    Visibility::Visible
                } else {
                    Visibility::Hidden
                };
            }
        }
    }
}

// ヘルパー関数(簡略化した実装例)
fn frustum_intersects_aabb_with_margin(
    view_proj: &Mat4,
    aabb: &Aabb,
    margin: f32,
) -> bool {
    // 実際の実装ではSIMD命令を使用
    let expanded_aabb = Aabb {
        center: aabb.center,
        half_extents: aabb.half_extents + Vec3::splat(margin),
    };
    
    // 6つのフラスタム平面との交差判定
    // (実装の詳細は省略)
    true
}

2. オクルージョンカリング(Occlusion Culling)

他のオブジェクトに完全に隠れているオブジェクトを除外する手法です。Bevy 0.18では、GPU駆動のHierarchical Z-Buffer(Hi-Z)を使用した2パスレンダリングが実装されています。

use bevy::render::render_resource::*;
use bevy::render::renderer::RenderDevice;

#[derive(Component)]
struct OcclusionCullingConfig {
    enable_hi_z: bool,
    hi_z_mip_levels: u32,
}

fn setup_occlusion_culling(
    mut commands: Commands,
    render_device: Res<RenderDevice>,
) {
    // Hi-Zバッファのテクスチャ作成
    let hi_z_texture = render_device.create_texture(&TextureDescriptor {
        label: Some("hi_z_buffer"),
        size: Extent3d {
            width: 1024,
            height: 1024,
            depth_or_array_layers: 1,
        },
        mip_level_count: 6, // 1024 -> 512 -> 256 -> ... -> 32
        sample_count: 1,
        dimension: TextureDimension::D2,
        format: TextureFormat::R32Float,
        usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
        view_formats: &[],
    });
    
    commands.insert_resource(OcclusionCullingConfig {
        enable_hi_z: true,
        hi_z_mip_levels: 6,
    });
}

// オクルージョンカリング用のComputeシェーダー(WGSL)
const OCCLUSION_CULLING_SHADER: &str = r#"
@group(0) @binding(0) var hi_z_texture: texture_2d<f32>;
@group(0) @binding(1) var<storage, read> aabbs: array<vec4<f32>>;
@group(0) @binding(2) var<storage, read_write> visibility: array<u32>;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
    let idx = global_id.x;
    let aabb = aabbs[idx];
    
    // AABBをスクリーン空間に投影
    let screen_min = project_to_screen(aabb.xy);
    let screen_max = project_to_screen(aabb.zw);
    
    // 適切なMIPレベルを選択
    let screen_size = screen_max - screen_min;
    let mip_level = u32(log2(max(screen_size.x, screen_size.y)));
    
    // Hi-Zバッファから深度を取得
    let hi_z_depth = textureSampleLevel(
        hi_z_texture,
        (screen_min + screen_max) * 0.5,
        f32(mip_level)
    ).r;
    
    // AABBの最近接深度と比較
    let aabb_depth = compute_aabb_depth(aabb);
    visibility[idx] = u32(aabb_depth < hi_z_depth);
}
"#;

3. 距離ベースカリング(Distance-Based Culling)

カメラから一定距離以上離れた小さなオブジェクトを除外する手法です。Bevy 0.18では、これがLODシステムと統合されています。

以下のダイアグラムは、Visibility Cullingの3段階処理フローを示しています。

flowchart TD
    A["全エンティティ<br/>(10万オブジェクト)"] --> B["フラスタムカリング"]
    B -->|視錐台内| C["6万オブジェクト"]
    B -->|視錐台外| D["除外: 4万"]
    C --> E["オクルージョンカリング"]
    E -->|可視| F["3万オブジェクト"]
    E -->|遮蔽| G["除外: 3万"]
    F --> H["距離ベースカリング"]
    H -->|描画範囲内| I["2万オブジェクト"]
    H -->|範囲外| J["除外: 1万"]
    I --> K["LODレベル選択"]
    K --> L["最終描画コマンド発行"]
    
    style A fill:#e1f5ff
    style L fill:#d4edda
    style D fill:#f8d7da
    style G fill:#f8d7da
    style J fill:#f8d7da

このフローにより、元の10万オブジェクトから最終的に2万オブジェクトまで絞り込まれ、描画コマンド数が80%削減されます。

LODシステムとの統合実装

Bevy 0.18の最大の革新は、Visibility CullingとLODシステムが単一のECSパイプラインで処理される点です。これにより、カリング判定とLODレベル選択が同時に行われ、CPUオーバーヘッドが大幅に削減されました。

LODコンポーネントの定義

use bevy::prelude::*;
use std::ops::Range;

#[derive(Component, Clone)]
struct LodLevels {
    levels: Vec<LodLevel>,
}

#[derive(Clone)]
struct LodLevel {
    mesh: Handle<Mesh>,
    distance_range: Range<f32>, // カメラからの距離範囲
    screen_coverage: f32,        // スクリーン占有率の閾値
}

impl LodLevels {
    fn new(meshes: Vec<(Handle<Mesh>, f32, f32)>) -> Self {
        Self {
            levels: meshes
                .into_iter()
                .enumerate()
                .map(|(idx, (mesh, min_dist, max_dist))| LodLevel {
                    mesh,
                    distance_range: min_dist..max_dist,
                    screen_coverage: 1.0 / (2.0_f32.powi(idx as i32)),
                })
                .collect(),
        }
    }
}

// LOD付きオブジェクトのスポーン
fn spawn_lod_object(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // 4段階のLODメッシュ
    let lod0 = meshes.add(Sphere::new(1.0).mesh().uv(128, 128)); // 高解像度
    let lod1 = meshes.add(Sphere::new(1.0).mesh().uv(64, 64));
    let lod2 = meshes.add(Sphere::new(1.0).mesh().uv(32, 32));
    let lod3 = meshes.add(Sphere::new(1.0).mesh().uv(16, 16));   // 低解像度
    
    let material = materials.add(StandardMaterial {
        base_color: Color::srgb(0.8, 0.2, 0.2),
        ..default()
    });
    
    for x in -50..50 {
        for z in -50..50 {
            commands.spawn((
                PbrBundle {
                    transform: Transform::from_xyz(x as f32 * 10.0, 0.0, z as f32 * 10.0),
                    material: material.clone(),
                    ..default()
                },
                LodLevels::new(vec![
                    (lod0.clone(), 0.0, 50.0),    // 0-50m: LOD0
                    (lod1.clone(), 50.0, 150.0),  // 50-150m: LOD1
                    (lod2.clone(), 150.0, 300.0), // 150-300m: LOD2
                    (lod3.clone(), 300.0, 500.0), // 300-500m: LOD3
                ]),
                Aabb::from_min_max(Vec3::splat(-1.0), Vec3::splat(1.0)),
            ));
        }
    }
}

統合カリング+LOD選択システム

use bevy::render::view::VisibleEntities;

#[derive(Component)]
struct ActiveLodLevel(usize);

fn integrated_culling_and_lod(
    camera_query: Query<(&Camera, &GlobalTransform)>,
    mut entity_query: Query<(
        Entity,
        &GlobalTransform,
        &Aabb,
        &LodLevels,
        &mut Handle<Mesh>,
        &mut Visibility,
        Option<&mut ActiveLodLevel>,
    )>,
    mut commands: Commands,
) {
    for (camera, cam_transform) in camera_query.iter() {
        let cam_pos = cam_transform.translation();
        let view_proj = camera.projection_matrix() 
            * cam_transform.compute_matrix().inverse();
        
        for (entity, transform, aabb, lod_levels, mut mesh, mut visibility, active_lod) in 
            entity_query.iter_mut() 
        {
            let world_pos = transform.translation();
            let distance = cam_pos.distance(world_pos);
            
            // Step 1: フラスタムカリング
            let world_aabb = aabb.transform(transform.compute_matrix());
            if !frustum_contains_aabb(&view_proj, &world_aabb) {
                *visibility = Visibility::Hidden;
                continue;
            }
            
            // Step 2: 距離ベースLOD選択
            let selected_lod = lod_levels.levels
                .iter()
                .position(|level| level.distance_range.contains(&distance));
            
            if let Some(lod_idx) = selected_lod {
                *visibility = Visibility::Visible;
                *mesh = lod_levels.levels[lod_idx].mesh.clone();
                
                // LODレベルの変更を記録
                if let Some(mut active) = active_lod {
                    active.0 = lod_idx;
                } else {
                    commands.entity(entity).insert(ActiveLodLevel(lod_idx));
                }
            } else {
                // 全LOD範囲外(500m以遠)
                *visibility = Visibility::Hidden;
            }
        }
    }
}

fn frustum_contains_aabb(view_proj: &Mat4, aabb: &Aabb) -> bool {
    // 簡略化した実装(実際はSIMD最適化)
    let corners = [
        Vec3::new(aabb.center.x - aabb.half_extents.x, aabb.center.y - aabb.half_extents.y, aabb.center.z - aabb.half_extents.z),
        Vec3::new(aabb.center.x + aabb.half_extents.x, aabb.center.y - aabb.half_extents.y, aabb.center.z - aabb.half_extents.z),
        Vec3::new(aabb.center.x - aabb.half_extents.x, aabb.center.y + aabb.half_extents.y, aabb.center.z - aabb.half_extents.z),
        Vec3::new(aabb.center.x + aabb.half_extents.x, aabb.center.y + aabb.half_extents.y, aabb.center.z - aabb.half_extents.z),
        Vec3::new(aabb.center.x - aabb.half_extents.x, aabb.center.y - aabb.half_extents.y, aabb.center.z + aabb.half_extents.z),
        Vec3::new(aabb.center.x + aabb.half_extents.x, aabb.center.y - aabb.half_extents.y, aabb.center.z + aabb.half_extents.z),
        Vec3::new(aabb.center.x - aabb.half_extents.x, aabb.center.y + aabb.half_extents.y, aabb.center.z + aabb.half_extents.z),
        Vec3::new(aabb.center.x + aabb.half_extents.x, aabb.center.y + aabb.half_extents.y, aabb.center.z + aabb.half_extents.z),
    ];
    
    corners.iter().any(|&corner| {
        let clip_pos = view_proj.project_point3(corner);
        clip_pos.x.abs() <= 1.0 && clip_pos.y.abs() <= 1.0 && clip_pos.z >= 0.0 && clip_pos.z <= 1.0
    })
}

以下のシーケンス図は、統合カリング+LOD選択の処理フローを示しています。

sequenceDiagram
    participant Camera
    participant CullingSystem as Culling System
    participant LODSelector as LOD Selector
    participant GPU
    
    Camera->>CullingSystem: カメラ情報(位置・視錐台)
    CullingSystem->>CullingSystem: フラスタムカリング実行
    CullingSystem->>LODSelector: 可視エンティティリスト
    LODSelector->>LODSelector: 距離計算
    LODSelector->>LODSelector: スクリーン占有率計算
    LODSelector->>LODSelector: 最適LODレベル選択
    LODSelector->>GPU: メッシュ切替+描画コマンド
    GPU-->>Camera: レンダリング結果
    
    Note over CullingSystem,LODSelector: 単一ECSパイプラインで処理

このシーケンスにより、従来は2つの独立したシステムで処理していたカリングとLOD選択が、単一のパイプラインで効率的に実行されます。

メモリ効率の最適化テクニック

Bevy 0.18のLODシステムでは、メモリ使用量を最小化するための複数の戦略が実装されています。

1. メッシュの遅延ロード

すべてのLODレベルのメッシュを事前にロードするのではなく、必要になったタイミングでロードする戦略です。

use bevy::asset::{AssetServer, LoadState};

#[derive(Component)]
struct LodAsyncLoader {
    lod_paths: Vec<String>,
    loaded_levels: Vec<Option<Handle<Mesh>>>,
    loading_state: Vec<LoadState>,
}

fn async_lod_loading(
    mut query: Query<(&GlobalTransform, &mut LodAsyncLoader, &mut Handle<Mesh>)>,
    camera_query: Query<&GlobalTransform, With<Camera>>,
    asset_server: Res<AssetServer>,
) {
    let cam_pos = camera_query.single().translation();
    
    for (transform, mut loader, mut mesh) in query.iter_mut() {
        let distance = cam_pos.distance(transform.translation());
        
        // 必要なLODレベルを計算
        let required_lod = calculate_required_lod(distance);
        
        // まだロードされていない場合はロード開始
        if loader.loaded_levels[required_lod].is_none() {
            let handle: Handle<Mesh> = asset_server.load(&loader.lod_paths[required_lod]);
            loader.loaded_levels[required_lod] = Some(handle.clone());
            loader.loading_state[required_lod] = LoadState::Loading;
        }
        
        // ロード完了確認
        if let Some(handle) = &loader.loaded_levels[required_lod] {
            if let LoadState::Loaded = asset_server.get_load_state(handle.id()) {
                *mesh = handle.clone();
            }
        }
        
        // 不要なLODレベルのアンロード(メモリ節約)
        for (idx, loaded) in loader.loaded_levels.iter_mut().enumerate() {
            if idx != required_lod && idx.abs_diff(required_lod) > 1 {
                // 現在のLODから2段階以上離れているものはアンロード
                *loaded = None;
            }
        }
    }
}

fn calculate_required_lod(distance: f32) -> usize {
    match distance {
        d if d < 50.0 => 0,
        d if d < 150.0 => 1,
        d if d < 300.0 => 2,
        _ => 3,
    }
}

2. GPUインスタンシングとの組み合わせ

同じLODレベルのオブジェクトをグループ化し、インスタンシング描画することでドローコール数を削減します。

use bevy::render::render_resource::*;
use std::collections::HashMap;

#[derive(Resource, Default)]
struct LodInstanceGroups {
    groups: HashMap<(AssetId<Mesh>, usize), Vec<Mat4>>, // (メッシュID, LODレベル) -> 変換行列リスト
}

fn group_instances_by_lod(
    query: Query<(&Handle<Mesh>, &ActiveLodLevel, &GlobalTransform), With<Visibility>>,
    mut instance_groups: ResMut<LodInstanceGroups>,
) {
    instance_groups.groups.clear();
    
    for (mesh, lod_level, transform) in query.iter() {
        let key = (mesh.id(), lod_level.0);
        instance_groups.groups
            .entry(key)
            .or_insert_with(Vec::new)
            .push(transform.compute_matrix());
    }
}

// インスタンスバッファの作成(レンダリングシステム内)
fn create_instance_buffers(
    instance_groups: Res<LodInstanceGroups>,
    render_device: Res<RenderDevice>,
) {
    for ((mesh_id, lod_level), transforms) in instance_groups.groups.iter() {
        if transforms.len() < 10 {
            // インスタンス数が少ない場合は通常描画
            continue;
        }
        
        // インスタンスバッファ作成
        let instance_data: Vec<[f32; 16]> = transforms
            .iter()
            .map(|mat| mat.to_cols_array())
            .collect();
        
        let instance_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
            label: Some(&format!("lod_instance_buffer_{}_{}", mesh_id.index(), lod_level)),
            contents: bytemuck::cast_slice(&instance_data),
            usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
        });
        
        // 単一ドローコールで全インスタンスを描画
        // (実装の詳細は省略)
    }
}

3. 動的メッシュ簡略化

実行時にメッシュを簡略化することで、メモリ使用量をさらに削減します。Bevy 0.18では、meshoptライブラリを統合したメッシュ簡略化機能が追加されています。

use bevy::render::mesh::{Mesh, VertexAttributeValues};

fn simplify_mesh_at_runtime(
    original_mesh: &Mesh,
    target_triangle_count: usize,
) -> Mesh {
    // 元のメッシュからデータ取得
    let positions = original_mesh
        .attribute(Mesh::ATTRIBUTE_POSITION)
        .and_then(|attr| {
            if let VertexAttributeValues::Float32x3(pos) = attr {
                Some(pos.clone())
            } else {
                None
            }
        })
        .unwrap();
    
    let indices = original_mesh.indices().unwrap();
    
    // meshoptを使用したメッシュ簡略化(実際の実装)
    let simplified_indices = meshopt::simplify(
        &indices.iter().collect::<Vec<_>>(),
        &positions,
        target_triangle_count,
        0.01, // エラー閾値
    );
    
    // 新しいメッシュの構築
    let mut new_mesh = Mesh::new(
        bevy::render::render_resource::PrimitiveTopology::TriangleList,
        Default::default(),
    );
    new_mesh.insert_attribute(
        Mesh::ATTRIBUTE_POSITION,
        VertexAttributeValues::Float32x3(positions),
    );
    new_mesh.insert_indices(bevy::render::mesh::Indices::U32(simplified_indices));
    
    new_mesh
}

以下のダイアグラムは、LODシステムのメモリ管理戦略を示しています。

flowchart LR
    A["カメラ位置"] --> B["距離計算"]
    B --> C{"必要LODレベル"}
    C -->|LOD0<br/>0-50m| D["高解像度メッシュ<br/>8MB"]
    C -->|LOD1<br/>50-150m| E["中解像度メッシュ<br/>2MB"]
    C -->|LOD2<br/>150-300m| F["低解像度メッシュ<br/>500KB"]
    C -->|LOD3<br/>300m+| G["最低解像度メッシュ<br/>100KB"]
    
    D --> H["遅延ロード管理"]
    E --> H
    F --> H
    G --> H
    
    H --> I["不要LODアンロード"]
    I --> J["メモリ使用量<br/>最大8MB→平均1.5MB"]
    
    style A fill:#e1f5ff
    style J fill:#d4edda
    style I fill:#fff3cd

このメモリ管理により、1オブジェクトあたりの平均メモリ使用量が8MBから1.5MBに削減され、10万オブジェクトのシーンで合計650GBの削減が実現されます。

パフォーマンスベンチマークと最適化結果

Bevy 0.18の公式ベンチマーク(2026年5月2日公開)では、以下の環境で測定が行われました。

テスト環境:

  • CPU: AMD Ryzen 9 7950X
  • GPU: NVIDIA RTX 4090
  • メモリ: 64GB DDR5-6000
  • OS: Ubuntu 24.04 LTS
  • シーン: 10万オブジェクト(各4段階LOD)、オープンワールド地形

測定結果:

指標Bevy 0.17(従来)Bevy 0.18(新)改善率
描画コマンド数45,00018,000-60%
GPU使用率92%68%-26%
メモリ使用量12.8GB7.6GB-41%
フレームレート(平均)48 FPS87 FPS+81%
フレームレート(最低)32 FPS71 FPS+122%

特に注目すべきは、フレームレートの最低値が2倍以上に向上した点です。これは、統合カリングシステムによってフレーム間のパフォーマンスのばらつきが大幅に減少したことを示しています。

カリング段階別の効果分析

公式ベンチマークでは、各カリング段階がどれだけのオブジェクトを除外したかも記録されています。

graph TD
    A["初期オブジェクト数<br/>100,000"] -->|フラスタムカリング| B["58,000<br/>(42%除外)"]
    B -->|オクルージョンカリング| C["31,000<br/>(27%除外)"]
    C -->|距離ベースカリング| D["18,000<br/>(13%除外)"]
    D --> E["最終描画オブジェクト<br/>18,000(82%削減)"]
    
    style A fill:#f8d7da
    style E fill:#d4edda

この結果から、フラスタムカリングが最も効果的(42%除外)で、次にオクルージョンカリング(27%除外)が続くことがわかります。

実装時の注意点とトラブルシューティング

LOD遷移時のポップイン防止

LODレベルが切り替わる際に、視覚的な不連続性(ポップイン)が発生する問題への対処法です。

#[derive(Component)]
struct LodTransition {
    from_lod: usize,
    to_lod: usize,
    progress: f32,        // 0.0 ~ 1.0
    transition_speed: f32, // 秒あたりの進行速度
}

fn smooth_lod_transition(
    mut query: Query<(&mut LodTransition, &mut Visibility, &LodLevels, &mut Handle<Mesh>)>,
    time: Res<Time>,
) {
    for (mut transition, mut visibility, lod_levels, mut mesh) in query.iter_mut() {
        transition.progress += time.delta_seconds() * transition.speed;
        
        if transition.progress >= 1.0 {
            // 遷移完了
            *mesh = lod_levels.levels[transition.to_lod].mesh.clone();
            transition.progress = 1.0;
        } else {
            // クロスフェードまたはディザリングを使用
            // (シェーダー側での処理が必要)
            *visibility = Visibility::Visible;
        }
    }
}

// シェーダー側のディザリング実装(WGSL)
const LOD_TRANSITION_SHADER: &str = r#"
@group(1) @binding(0) var<uniform> transition_progress: f32;

@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
    // Bayer行列によるディザリング
    let dither_pattern = array<f32, 16>(
        0.0/16.0,  8.0/16.0,  2.0/16.0, 10.0/16.0,
        12.0/16.0, 4.0/16.0, 14.0/16.0,  6.0/16.0,
        3.0/16.0, 11.0/16.0,  1.0/16.0,  9.0/16.0,
        15.0/16.0, 7.0/16.0, 13.0/16.0,  5.0/16.0,
    );
    
    let x = u32(in.position.x) % 4u;
    let y = u32(in.position.y) % 4u;
    let dither_value = dither_pattern[y * 4u + x];
    
    if dither_value > transition_progress {
        discard;
    }
    
    return vec4<f32>(in.color.rgb, 1.0);
}
"#;

Hi-Zバッファの更新タイミング

オクルージョンカリングの精度を保つため、Hi-Zバッファは適切なタイミングで更新する必要があります。

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

fn setup_hi_z_update_schedule(
    mut render_graph: ResMut<RenderGraph>,
) {
    // レンダリンググラフにHi-Z更新ノードを追加
    render_graph.add_node("hi_z_update", HiZUpdateNode);
    
    // 深度プレパス後、メイン描画前に実行
    render_graph.add_node_edge("depth_prepass", "hi_z_update");
    render_graph.add_node_edge("hi_z_update", "main_pass");
}

struct HiZUpdateNode;

impl Node for HiZUpdateNode {
    fn run(
        &self,
        graph: &mut bevy::render::render_graph::RenderGraphContext,
        render_context: &mut bevy::render::renderer::RenderContext,
        world: &bevy::ecs::world::World,
    ) -> Result<(), bevy::render::render_graph::NodeRunError> {
        // 深度バッファからHi-Zミップマップを生成
        // (Compute Shaderによるダウンサンプリング)
        
        Ok(())
    }
}

まとめ

Bevy 0.18のVisibility Culling + LOD統合システムは、大規模なゲーム開発におけるパフォーマンスとメモリ効率の両立を実現する画期的な機能です。主要なポイントを以下にまとめます。

  • 描画コマンド60%削減: フラスタムカリング、オクルージョンカリング、距離ベースカリングの3段階処理により、10万オブジェクトから1.8万オブジェクトに絞り込み
  • メモリ使用量41%削減: 遅延ロード、動的アンロード、メッシュ簡略化により、12.8GBから7.6GBに削減
  • フレームレート81%向上: 統合ECSパイプラインにより、48 FPSから87 FPSに改善
  • ポップイン防止: ディザリングとクロスフェードによる滑らかなLOD遷移
  • GPU駆動処理: Hi-Zバッファとコンピュートシェーダーによる効率的なオクルージョンカリング

これらの機能を適切に組み合わせることで、オープンワールドゲームや大規模シミュレーションにおいても、高フレームレートと低メモリ使用量を両立できます。Bevy 0.18のリリースにより、Rustゲーム開発のパフォーマンス最適化の選択肢がさらに広がりました。

参考リンク

#Rust #Bevy #GPU最適化 #Visibility Culling #LOD
シェア: