├── .gitignore ├── docs └── demo1.webp ├── README.md ├── Cargo.toml ├── src └── lib.rs ├── examples └── demo.rs └── assets └── models └── Monkey.gltf /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /docs/demo1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aevyrie/bevy_frustum_culling/HEAD/docs/demo1.webp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_frustum_culling 2 | 3 | ![demo](docs/demo1.webp) 4 | 5 | Bevy plugin for frustum culling. See the examples/demo.rs for usage. Requires the aevyrie/bevy_mod_bounding plugin to generate bounding volumes for your meshes, as shown in the demo. 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_frustum_culling" 3 | version = "0.1.0" 4 | authors = ["Aevyrie Roessler "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | bevy = { version = "0.5", default-features = false, features = ["render"] } 9 | bevy_mod_bounding = "0.1" 10 | 11 | [dev-dependencies] 12 | bevy = { version = "0.5", default-features = false, features = [ 13 | "bevy_wgpu", 14 | "bevy_winit", 15 | "bevy_gltf", 16 | "x11", 17 | ] } 18 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use bevy::{prelude::*, render::camera::Camera, tasks::ComputeTaskPool}; 2 | pub use bevy_mod_bounding::*; 3 | use std::marker::PhantomData; 4 | 5 | #[derive(Default)] 6 | pub struct FrustumCullingPlugin(PhantomData); 7 | impl Plugin for FrustumCullingPlugin { 8 | fn build(&self, app: &mut AppBuilder) { 9 | app.add_system_to_stage( 10 | CoreStage::PostUpdate, 11 | frustum_culling:: 12 | .system() 13 | .after(BoundingSystem::UpdateBounds) 14 | .after(bevy::transform::TransformSystem::TransformPropagate) 15 | .before(bevy::render::RenderSystem::VisibleEntities), 16 | ); 17 | } 18 | } 19 | 20 | pub struct FrustumCulling; 21 | 22 | fn frustum_culling( 23 | pool: Res, 24 | camera_query: Query<(&Camera, &GlobalTransform), With>, 25 | mut bound_vol_query: Query<(&T, &GlobalTransform, &mut Visible)>, 26 | ) { 27 | // TODO: only compute frustum on camera change. Can store in a frustum component. 28 | for (camera, camera_position) in camera_query.iter() { 29 | let ndc_to_world: Mat4 = 30 | camera_position.compute_matrix() * camera.projection_matrix.inverse(); 31 | // Near/Far, Top/Bottom, Left/Right 32 | let nbl_world = ndc_to_world.project_point3(Vec3::new(-1.0, -1.0, -1.0)); 33 | let nbr_world = ndc_to_world.project_point3(Vec3::new(1.0, -1.0, -1.0)); 34 | let ntl_world = ndc_to_world.project_point3(Vec3::new(-1.0, 1.0, -1.0)); 35 | let fbl_world = ndc_to_world.project_point3(Vec3::new(-1.0, -1.0, 1.0)); 36 | let ftr_world = ndc_to_world.project_point3(Vec3::new(1.0, 1.0, 1.0)); 37 | let ftl_world = ndc_to_world.project_point3(Vec3::new(-1.0, 1.0, 1.0)); 38 | let fbr_world = ndc_to_world.project_point3(Vec3::new(1.0, -1.0, 1.0)); 39 | let ntr_world = ndc_to_world.project_point3(Vec3::new(1.0, 1.0, -1.0)); 40 | // Compute plane normals 41 | let near_plane = (nbr_world - nbl_world) 42 | .cross(ntl_world - nbl_world) 43 | .normalize(); 44 | let far_plane = (fbr_world - ftr_world) 45 | .cross(ftl_world - ftr_world) 46 | .normalize(); 47 | let top_plane = (ftl_world - ftr_world) 48 | .cross(ntr_world - ftr_world) 49 | .normalize(); 50 | let bottom_plane = (fbl_world - nbl_world) 51 | .cross(nbr_world - nbl_world) 52 | .normalize(); 53 | let right_plane = (ntr_world - ftr_world) 54 | .cross(fbr_world - ftr_world) 55 | .normalize(); 56 | let left_plane = (ntl_world - nbl_world) 57 | .cross(fbl_world - nbl_world) 58 | .normalize(); 59 | 60 | let frustum_plane_list = [ 61 | (nbl_world, left_plane), 62 | (ftr_world, right_plane), 63 | (nbl_world, bottom_plane), 64 | (ftr_world, top_plane), 65 | (nbl_world, near_plane), 66 | (ftr_world, far_plane), 67 | ]; 68 | 69 | // If a bounding volume is entirely outside of any camera frustum plane, it is not visible. 70 | bound_vol_query.par_for_each_mut( 71 | &pool, 72 | 32, 73 | |(bound_vol, bound_vol_position, mut visible)| { 74 | for (plane_point, plane_normal) in frustum_plane_list.iter() { 75 | if bound_vol.outside_plane(bound_vol_position, *plane_point, *plane_normal) { 76 | visible.is_visible = false; 77 | return; 78 | } 79 | } 80 | visible.is_visible = true; 81 | }, 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, 3 | prelude::*, 4 | }; 5 | 6 | use bevy_frustum_culling::*; 7 | 8 | fn main() { 9 | App::build() 10 | .insert_resource(WindowDescriptor { 11 | vsync: false, 12 | ..Default::default() 13 | }) 14 | //.insert_resource(ReportExecutionOrderAmbiguities) 15 | .add_plugins(DefaultPlugins) 16 | .add_plugin(BoundingVolumePlugin::::default()) 17 | .add_plugin(FrustumCullingPlugin::::default()) 18 | .add_startup_system(setup.system()) 19 | .add_system(camera_rotation_system.system()) 20 | .add_system(mesh_rotation_system.system()) 21 | .add_plugin(FrameTimeDiagnosticsPlugin::default()) 22 | .add_plugin(LogDiagnosticsPlugin::default()) 23 | .run(); 24 | } 25 | 26 | fn setup( 27 | mut commands: Commands, 28 | mut meshes: ResMut>, 29 | asset_server: Res, 30 | mut materials: ResMut>, 31 | ) { 32 | let mesh_path = "models/Monkey.gltf#Mesh0/Primitive0"; 33 | let _scenes: Vec = asset_server.load_folder("models").unwrap(); 34 | let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 1.0 })); 35 | let cube_material_handle = materials.add(Color::rgb(0.8, 0.7, 0.6).into()); 36 | let mesh_handle = asset_server.get_handle(mesh_path); 37 | 38 | commands.spawn().insert_bundle(PerspectiveCameraBundle { 39 | transform: Transform::from_matrix(Mat4::face_toward( 40 | Vec3::new(10.0, 10.0, 10.0), 41 | Vec3::new(0.0, 0.0, 0.0), 42 | Vec3::new(0.0, 1.0, 0.0), 43 | )), 44 | ..Default::default() 45 | }); 46 | commands 47 | .spawn() 48 | .insert_bundle(PerspectiveCameraBundle { 49 | camera: bevy::render::camera::Camera { 50 | name: Some("Secondary".to_string()), 51 | ..Default::default() 52 | }, 53 | transform: Transform::from_matrix(Mat4::face_toward( 54 | Vec3::new(0.0, 0.0, 0.0), 55 | Vec3::new(0.0, 0.0, 1.0), 56 | Vec3::new(0.0, 1.0, 0.0), 57 | )), 58 | ..Default::default() 59 | }) 60 | .insert(FrustumCulling) 61 | .insert(CameraRotator) 62 | .with_children(|parent| { 63 | parent.spawn().insert_bundle(PbrBundle { 64 | mesh: cube_handle, 65 | material: cube_material_handle, 66 | ..Default::default() 67 | }); 68 | }) 69 | .insert_bundle(LightBundle { 70 | transform: Transform::from_translation(Vec3::new(4.0, 8.0, 4.0)), 71 | ..Default::default() 72 | }); 73 | 74 | for x in -10..10 { 75 | for y in -10..10 { 76 | for z in -10..10 { 77 | if !(x == 0 && y == 0 && z == 0) { 78 | commands 79 | .spawn() 80 | .insert_bundle(PbrBundle { 81 | mesh: mesh_handle.clone(), 82 | material: materials.add(Color::rgb(1.0, 1.0, 1.0).into()), 83 | transform: Transform::from_translation(Vec3::new( 84 | x as f32 * 2.0, 85 | y as f32 * 2.0, 86 | z as f32 * 2.0, 87 | )), 88 | ..Default::default() 89 | }) 90 | .insert(MeshRotator) 91 | // Manually set the bounding volume of the mesh. We can pre-compute the 92 | // bounds and specify them. Computing for every mesh makes startup slow. 93 | .insert(obb::Obb::default()) 94 | .insert(debug::DebugBounds); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | struct CameraRotator; 102 | 103 | fn camera_rotation_system(time: Res