├── .gitignore ├── rust-toolchain.toml ├── demo.png ├── Cargo.toml ├── LICENSE-MIT ├── src ├── lib.rs ├── grid.wgsl └── render.rs ├── examples ├── render_layers.rs └── simple.rs ├── .github └── workflows │ └── rust.yml ├── README.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .idea/ -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | profile = "default" 3 | channel = "stable" 4 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForesightMiningSoftwareCorporation/bevy_infinite_grid/HEAD/demo.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_infinite_grid" 3 | version = "0.17.0" 4 | authors = [ 5 | "Nile ", 6 | "Foresight Mining Software Corporation", 7 | ] 8 | edition = "2021" 9 | description = "A 3D infinite grid for Bevy" 10 | license = "MIT OR Apache-2.0" 11 | repository = "https://github.com/ForesightMiningSoftwareCorporation/bevy_infinite_grid" 12 | documentation = "https://docs.rs/bevy_infinite_grid" 13 | keywords = ["bevy"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | bevy = { version = "0.17", default-features = false, features = [ 19 | "bevy_render", 20 | "bevy_core_pipeline", 21 | "bevy_pbr", 22 | "bevy_asset", 23 | "bevy_image", 24 | "bevy_log", 25 | ] } 26 | 27 | [dev-dependencies] 28 | bevy = { version = "0.17", default-features = false, features = [ 29 | "bevy_winit", 30 | "bevy_window", 31 | "x11", 32 | "tonemapping_luts", 33 | "ktx2", 34 | "zstd_rust", 35 | ] } 36 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod render; 2 | 3 | use bevy::{ 4 | camera::visibility::{self, NoFrustumCulling, VisibilityClass}, 5 | prelude::*, 6 | render::{sync_world::SyncToRenderWorld, view::RenderVisibleEntities}, 7 | }; 8 | 9 | pub struct InfiniteGridPlugin; 10 | 11 | impl Plugin for InfiniteGridPlugin { 12 | fn build(&self, _app: &mut App) {} 13 | 14 | fn finish(&self, app: &mut App) { 15 | render::render_app_builder(app); 16 | } 17 | } 18 | 19 | #[derive(Component, Default)] 20 | pub struct InfiniteGrid; 21 | 22 | #[derive(Component, Copy, Clone)] 23 | #[require(VisibilityClass)] 24 | #[component(on_add = visibility::add_visibility_class::)] 25 | pub struct InfiniteGridSettings { 26 | pub x_axis_color: Color, 27 | pub z_axis_color: Color, 28 | pub minor_line_color: Color, 29 | pub major_line_color: Color, 30 | pub fadeout_distance: f32, 31 | pub dot_fadeout_strength: f32, 32 | pub scale: f32, 33 | } 34 | 35 | impl Default for InfiniteGridSettings { 36 | fn default() -> Self { 37 | Self { 38 | x_axis_color: Color::srgb(1.0, 0.2, 0.2), 39 | z_axis_color: Color::srgb(0.2, 0.2, 1.0), 40 | minor_line_color: Color::srgb(0.1, 0.1, 0.1), 41 | major_line_color: Color::srgb(0.25, 0.25, 0.25), 42 | fadeout_distance: 100., 43 | dot_fadeout_strength: 0.25, 44 | scale: 1., 45 | } 46 | } 47 | } 48 | 49 | #[derive(Bundle, Default)] 50 | pub struct InfiniteGridBundle { 51 | pub transform: Transform, 52 | pub global_transform: GlobalTransform, 53 | pub settings: InfiniteGridSettings, 54 | pub grid: InfiniteGrid, 55 | pub visibility: Visibility, 56 | pub view_visibility: ViewVisibility, 57 | pub inherited_visibility: InheritedVisibility, 58 | pub shadow_casters: RenderVisibleEntities, 59 | pub no_frustum_culling: NoFrustumCulling, 60 | pub sync_to_render_world: SyncToRenderWorld, 61 | } 62 | -------------------------------------------------------------------------------- /examples/render_layers.rs: -------------------------------------------------------------------------------- 1 | use bevy::{camera::visibility::RenderLayers, prelude::*, render::view::Hdr}; 2 | use bevy_infinite_grid::{InfiniteGridBundle, InfiniteGridPlugin}; 3 | 4 | fn main() { 5 | App::new() 6 | .add_plugins((DefaultPlugins, InfiniteGridPlugin)) 7 | .add_systems(Startup, setup_system) 8 | .add_systems(Update, toggle_layers) 9 | .run(); 10 | } 11 | 12 | fn setup_system( 13 | mut commands: Commands, 14 | mut meshes: ResMut>, 15 | mut standard_materials: ResMut>, 16 | ) { 17 | commands.spawn((InfiniteGridBundle::default(), RenderLayers::layer(1))); 18 | 19 | commands.spawn(( 20 | Camera3d::default(), 21 | Hdr, 22 | Transform::from_xyz(0.0, 4.37, 14.77), 23 | RenderLayers::layer(1), 24 | )); 25 | 26 | commands.spawn(( 27 | DirectionalLight { ..default() }, 28 | Transform::from_translation(Vec3::X * 15. + Vec3::Y * 20.).looking_at(Vec3::ZERO, Vec3::Y), 29 | )); 30 | 31 | // cube 32 | commands.spawn(( 33 | Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 34 | MeshMaterial3d(standard_materials.add(StandardMaterial::default())), 35 | Transform::from_xyz(3.0, 4.0, 0.0) 36 | .with_rotation(Quat::from_rotation_arc(Vec3::Y, Vec3::ONE.normalize())) 37 | .with_scale(Vec3::splat(1.5)), 38 | )); 39 | 40 | commands.spawn(( 41 | Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 42 | MeshMaterial3d(standard_materials.add(StandardMaterial::default())), 43 | Transform::from_xyz(0.0, 2.0, 0.0), 44 | )); 45 | } 46 | 47 | fn toggle_layers(mut q: Query<&mut RenderLayers, With>, input: Res>) { 48 | for mut render_layers in &mut q { 49 | if input.just_pressed(KeyCode::KeyT) { 50 | if render_layers.intersects(&RenderLayers::layer(1)) { 51 | *render_layers = RenderLayers::layer(0); 52 | } else { 53 | *render_layers = RenderLayers::layer(1); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | setup: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Update Packages 12 | run: sudo apt-get update -yq 13 | - name: Install dependencies 14 | run: sudo apt-get install -yq --no-install-recommends libudev-dev libasound2-dev libxcb-composite0-dev 15 | 16 | format: 17 | runs-on: ubuntu-latest 18 | needs: [setup] 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: dtolnay/rust-toolchain@stable 22 | - uses: Swatinem/rust-cache@v2.7.0 23 | - run: rustup component add rustfmt 24 | - run: cargo fmt --all -- --check 25 | 26 | check: 27 | runs-on: ubuntu-latest 28 | needs: [setup] 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: dtolnay/rust-toolchain@stable 32 | - uses: Swatinem/rust-cache@v2.7.0 33 | - run: cargo check --all-features --all-targets 34 | 35 | check-no-defaults: 36 | runs-on: ubuntu-latest 37 | needs: [setup] 38 | steps: 39 | - uses: actions/checkout@v3 40 | - uses: dtolnay/rust-toolchain@stable 41 | - uses: Swatinem/rust-cache@v2.7.0 42 | - run: cargo check --no-default-features --all-targets 43 | 44 | clippy: 45 | runs-on: ubuntu-latest 46 | needs: [setup] 47 | steps: 48 | - uses: actions/checkout@v3 49 | - uses: dtolnay/rust-toolchain@stable 50 | - uses: Swatinem/rust-cache@v2.7.0 51 | - run: rustup component add clippy 52 | - run: cargo clippy --all-features --all-targets -- -D warnings 53 | 54 | doc: 55 | runs-on: ubuntu-latest 56 | needs: [setup] 57 | steps: 58 | - uses: actions/checkout@v3 59 | - uses: dtolnay/rust-toolchain@stable 60 | - uses: Swatinem/rust-cache@v2.7.0 61 | - run: cargo doc --all-features --no-deps 62 | env: 63 | RUSTDOCFLAGS: -D warnings 64 | 65 | test: 66 | runs-on: ubuntu-latest 67 | needs: [setup] 68 | steps: 69 | - uses: actions/checkout@v3 70 | - uses: dtolnay/rust-toolchain@stable 71 | - uses: Swatinem/rust-cache@v2.7.0 72 | - run: cargo test --all-features 73 | 74 | doctest: 75 | runs-on: ubuntu-latest 76 | needs: [setup] 77 | steps: 78 | - uses: actions/checkout@v3 79 | - uses: dtolnay/rust-toolchain@stable 80 | - uses: Swatinem/rust-cache@v2.7.0 81 | - run: cargo test --all-features --doc 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Bevy Infinite Grid 4 | 5 | **Simple 3D infinite grid for bevy** 6 | 7 | [![crates.io](https://img.shields.io/crates/v/bevy_infinite_grid)](https://crates.io/crates/bevy_infinite_grid) 8 | [![docs.rs](https://docs.rs/bevy_infinite_grid/badge.svg)](https://docs.rs/bevy_infinite_grid) 9 | [![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking) 10 | [![CI-CD](https://github.com/ForesightMiningSoftwareCorporation/bevy_infinite_grid/actions/workflows/release.yml/badge.svg)](https://github.com/ForesightMiningSoftwareCorporation/bevy_infinite_grid/actions/workflows/release.yml) 11 | 12 | ![demo](demo.png) 13 | 14 |
15 | 16 | # Demo 17 | 18 | Run a simple implementation of this grid by cloning this repository and running: 19 | 20 | ```shell 21 | cargo run --example simple 22 | ``` 23 | 24 | # Features 25 | 26 | - Easily spawn an infinite grid aligned to the world origin and axes 27 | - Spawn an unlimited number of axes aligned to arbitrary coordinate spaces 28 | 29 | # Usage 30 | 31 | Add the plugin to the `[dependencies]` in `Cargo.toml` 32 | 33 | ```toml 34 | bevy_infinite_grid = { git = "https://github.com/ForesightMiningSoftwareCorporation/bevy_infinite_grid", branch = "main" } 35 | ``` 36 | 37 | Insert the infinite grid plugin after the default plugins. 38 | 39 | ```rust 40 | .add_plugins(InfiniteGridPlugin) 41 | ``` 42 | 43 | And spawn the grid to see the results. 44 | 45 | ```rust 46 | commands.spawn(InfiniteGridBundle::new( 47 | materials.add(InfiniteGridMaterial::default()), 48 | )); 49 | ``` 50 | 51 | See the [simple](examples/simple.rs) demo for an example of a minimal implementation. 52 | 53 | ## Bevy Version Support 54 | 55 | We intend to track the `main` branch of Bevy. PRs supporting this are welcome! 56 | 57 | | bevy | bevy_infinite_grid | 58 | | ---- | ------------------ | 59 | | 0.17 | 0.16 | 60 | | 0.16 | 0.15 | 61 | | 0.15 | 0.14 | 62 | | 0.14 | 0.13 | 63 | | 0.13 | 0.11, 0.12 | 64 | | 0.12 | 0.10 | 65 | | 0.11 | 0.8, 0.9 | 66 | 67 | ## License 68 | 69 | bevy_infinite_grid is free and open source! All code in this repository is dual-licensed under either: 70 | 71 | - MIT License ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) 72 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) 73 | 74 | at your option. This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are very good reasons to include both. 75 | 76 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 77 | -------------------------------------------------------------------------------- /src/grid.wgsl: -------------------------------------------------------------------------------- 1 | struct InfiniteGridPosition { 2 | planar_rotation_matrix: mat3x3, 3 | origin: vec3, 4 | normal: vec3, 5 | 6 | }; 7 | 8 | struct InfiniteGridSettings { 9 | scale: f32, 10 | // 1 / fadeout_distance 11 | dist_fadeout_const: f32, 12 | dot_fadeout_const: f32, 13 | x_axis_col: vec3, 14 | z_axis_col: vec3, 15 | minor_line_col: vec4, 16 | major_line_col: vec4, 17 | 18 | }; 19 | 20 | struct View { 21 | projection: mat4x4, 22 | inverse_projection: mat4x4, 23 | view: mat4x4, 24 | inverse_view: mat4x4, 25 | world_position: vec3, 26 | }; 27 | 28 | @group(0) @binding(0) var view: View; 29 | 30 | @group(1) @binding(0) var grid_position: InfiniteGridPosition; 31 | @group(1) @binding(1) var grid_settings: InfiniteGridSettings; 32 | 33 | struct Vertex { 34 | @builtin(vertex_index) index: u32, 35 | }; 36 | 37 | fn unproject_point(p: vec3) -> vec3 { 38 | let unprojected = view.view * view.inverse_projection * vec4(p, 1.0); 39 | return unprojected.xyz / unprojected.w; 40 | } 41 | 42 | struct VertexOutput { 43 | @builtin(position) clip_position: vec4, 44 | @location(0) near_point: vec3, 45 | @location(1) far_point: vec3, 46 | }; 47 | 48 | @vertex 49 | fn vertex(vertex: Vertex) -> VertexOutput { 50 | // 0 1 2 1 2 3 51 | var grid_plane = array, 4>( 52 | vec3(-1., -1., 1.), 53 | vec3(-1., 1., 1.), 54 | vec3(1., -1., 1.), 55 | vec3(1., 1., 1.) 56 | ); 57 | let p = grid_plane[vertex.index].xyz; 58 | 59 | var out: VertexOutput; 60 | 61 | out.clip_position = vec4(p, 1.); 62 | out.near_point = unproject_point(p); 63 | out.far_point = unproject_point(vec3(p.xy, 0.001)); // unprojecting on the far plane 64 | return out; 65 | } 66 | 67 | struct FragmentOutput { 68 | @location(0) color: vec4, 69 | @builtin(frag_depth) depth: f32, 70 | }; 71 | 72 | @fragment 73 | fn fragment(in: VertexOutput) -> FragmentOutput { 74 | let ray_origin = in.near_point; 75 | let ray_direction = normalize(in.far_point - in.near_point); 76 | let plane_normal = grid_position.normal; 77 | let plane_origin = grid_position.origin; 78 | 79 | let denominator = dot(ray_direction, plane_normal); 80 | let point_to_point = plane_origin - ray_origin; 81 | let t = dot(plane_normal, point_to_point) / denominator; 82 | let frag_pos_3d = ray_direction * t + ray_origin; 83 | 84 | let planar_offset = frag_pos_3d - plane_origin; 85 | let rotation_matrix = grid_position.planar_rotation_matrix; 86 | let plane_coords = (grid_position.planar_rotation_matrix * planar_offset).xz; 87 | 88 | 89 | let view_space_pos = view.inverse_view * vec4(frag_pos_3d, 1.); 90 | let clip_space_pos = view.projection * view_space_pos; 91 | let clip_depth = clip_space_pos.z / clip_space_pos.w; 92 | let real_depth = -view_space_pos.z; 93 | 94 | var out: FragmentOutput; 95 | 96 | out.depth = clip_depth; 97 | 98 | let scale = grid_settings.scale; 99 | let coord = plane_coords * scale; // use the scale variable to set the distance between the lines 100 | let derivative = fwidth(coord); 101 | let grid = abs(fract(coord - 0.5) - 0.5) / derivative; 102 | let lne = min(grid.x, grid.y); 103 | 104 | let derivative2 = fwidth(coord * 0.1); 105 | let grid2 = abs(fract((coord * 0.1) - 0.5) - 0.5) / derivative2; 106 | let mg_line = min(grid2.x, grid2.y); 107 | 108 | let grid3 = abs(coord) / derivative; 109 | let axis_line = min(grid3.x, grid3.y); 110 | 111 | var alpha = vec3(1.0) - min(vec3(axis_line, mg_line, lne), vec3(1.0)); 112 | alpha.y *= (1.0 - alpha.x) * grid_settings.major_line_col.a; 113 | alpha.z *= (1.0 - (alpha.x + alpha.y)) * grid_settings.minor_line_col.a; 114 | 115 | let dist_fadeout = min(1., 1. - grid_settings.dist_fadeout_const * real_depth); 116 | let dot_fadeout = abs(dot(grid_position.normal, normalize(view.world_position - frag_pos_3d))); 117 | let alpha_fadeout = mix(dist_fadeout, 1., dot_fadeout) * min(grid_settings.dot_fadeout_const * dot_fadeout, 1.); 118 | 119 | let a_0 = alpha.x + alpha.y + alpha.z; 120 | alpha /= a_0; 121 | // On MacOS the line above could generate NaNs and render as black instead of transparent 122 | alpha = clamp(alpha, vec3(0.0), vec3(1.0)); 123 | let axis_color = mix(grid_settings.x_axis_col, grid_settings.z_axis_col, step(grid3.x, grid3.y)); 124 | var grid_color = vec4( 125 | axis_color * alpha.x + grid_settings.major_line_col.rgb * alpha.y + grid_settings.minor_line_col.rgb * alpha.z, 126 | max(a_0 * alpha_fadeout, 0.0), 127 | ); 128 | out.color = grid_color; 129 | 130 | return out; 131 | } 132 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use bevy::{prelude::*, render::view::Hdr}; 2 | use bevy_infinite_grid::{InfiniteGridBundle, InfiniteGridPlugin}; 3 | use camera_controller::{CameraController, CameraControllerPlugin}; 4 | 5 | fn main() { 6 | App::new() 7 | .add_plugins((DefaultPlugins, CameraControllerPlugin, InfiniteGridPlugin)) 8 | .add_systems(Startup, setup_system) 9 | .run(); 10 | } 11 | 12 | fn setup_system( 13 | mut commands: Commands, 14 | mut meshes: ResMut>, 15 | mut standard_materials: ResMut>, 16 | ) { 17 | commands.spawn(InfiniteGridBundle::default()); 18 | 19 | commands.spawn(( 20 | Camera3d::default(), 21 | Hdr, 22 | Transform::from_xyz(0.0, 4.37, 14.77), 23 | CameraController::default(), 24 | )); 25 | 26 | commands.spawn(( 27 | DirectionalLight { ..default() }, 28 | Transform::from_translation(Vec3::X * 15. + Vec3::Y * 20.).looking_at(Vec3::ZERO, Vec3::Y), 29 | )); 30 | 31 | // cube 32 | commands.spawn(( 33 | Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 34 | MeshMaterial3d(standard_materials.add(StandardMaterial::default())), 35 | Transform::from_xyz(3.0, 4.0, 0.0) 36 | .with_rotation(Quat::from_rotation_arc(Vec3::Y, Vec3::ONE.normalize())) 37 | .with_scale(Vec3::splat(1.5)), 38 | )); 39 | 40 | commands.spawn(( 41 | Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 42 | MeshMaterial3d(standard_materials.add(StandardMaterial::default())), 43 | Transform::from_xyz(0.0, 2.0, 0.0), 44 | )); 45 | 46 | commands.spawn(( 47 | Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 48 | MeshMaterial3d(standard_materials.add(StandardMaterial::default())), 49 | Transform::from_xyz(0.0, -2.0, 0.0), 50 | )); 51 | } 52 | 53 | // This is a simplified version of the camera controller used in bevy examples 54 | mod camera_controller { 55 | use bevy::{input::mouse::MouseMotion, prelude::*}; 56 | 57 | use std::f32::consts::*; 58 | 59 | pub const RADIANS_PER_DOT: f32 = 1.0 / 180.0; 60 | 61 | #[derive(Component)] 62 | pub struct CameraController { 63 | pub pitch: f32, 64 | pub yaw: f32, 65 | pub velocity: Vec3, 66 | } 67 | 68 | impl Default for CameraController { 69 | fn default() -> Self { 70 | Self { 71 | pitch: 0.0, 72 | yaw: 0.0, 73 | velocity: Vec3::ZERO, 74 | } 75 | } 76 | } 77 | 78 | pub struct CameraControllerPlugin; 79 | 80 | impl Plugin for CameraControllerPlugin { 81 | fn build(&self, app: &mut App) { 82 | app.add_systems(Update, camera_controller); 83 | } 84 | } 85 | 86 | fn camera_controller( 87 | time: Res