├── .github └── workflows │ └── ci.yml ├── .gitignore ├── 3d_shapes.png ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── 3d_shapes.rs └── simple.rs └── src ├── edge_detection.wgsl ├── lib.rs └── perlin_noise.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | os: [windows-latest, ubuntu-latest, macos-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/cache@v4 16 | with: 17 | path: | 18 | ~/.cargo/bin/ 19 | ~/.cargo/registry/index/ 20 | ~/.cargo/registry/cache/ 21 | ~/.cargo/git/db/ 22 | target/ 23 | key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }} 24 | - uses: dtolnay/rust-toolchain@master 25 | with: 26 | toolchain: stable 27 | - name: Install alsa and udev 28 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 29 | if: runner.os == 'linux' 30 | - name: Build & run tests 31 | run: cargo test 32 | # all-doc-tests: 33 | # runs-on: ubuntu-latest 34 | # steps: 35 | # - uses: actions/checkout@v4 36 | # - uses: actions/cache@v4 37 | # with: 38 | # path: | 39 | # ~/.cargo/bin/ 40 | # ~/.cargo/registry/index/ 41 | # ~/.cargo/registry/cache/ 42 | # ~/.cargo/git/db/ 43 | # target/ 44 | # key: ubuntu-latest-cargo-all-doc-tests-${{ hashFiles('**/Cargo.toml') }} 45 | # - uses: dtolnay/rust-toolchain@master 46 | # with: 47 | # toolchain: stable 48 | # - name: Install alsa and udev 49 | # run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 50 | # - name: Run doc tests with all features (this also compiles README examples) 51 | # run: cargo test --doc --all-features 52 | lint: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | - uses: actions/cache@v4 57 | with: 58 | path: | 59 | ~/.cargo/bin/ 60 | ~/.cargo/registry/index/ 61 | ~/.cargo/registry/cache/ 62 | ~/.cargo/git/db/ 63 | target/ 64 | key: ubuntu-latest-cargo-lint-${{ hashFiles('**/Cargo.toml') }} 65 | - uses: dtolnay/rust-toolchain@master 66 | with: 67 | toolchain: stable 68 | components: rustfmt, clippy 69 | - name: Install alsa and udev 70 | run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev 71 | - name: Run clippy 72 | run: cargo clippy --workspace --all-targets --all-features -- -Dwarnings 73 | - name: Check format 74 | run: cargo fmt --all -- --check -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .cargo/ -------------------------------------------------------------------------------- /3d_shapes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenPocketGamer/bevy_edge_detection/fb96ba2fe7a9a9929259029ee0dd12dfaf2e6270/3d_shapes.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_edge_detection" 3 | version = "0.15.4" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | authors = ["AllenPocketGamer "] 7 | description = "A bevy plugin adding edge detection post processing effect" 8 | homepage = "https://github.com/AllenPocketGamer/bevy_edge_detection" 9 | keywords = ["bevy", "plugin", "post-processing", "edge-detection"] 10 | categories = ["game-engines", "graphics", "rendering"] 11 | 12 | [dependencies] 13 | bevy = "0.15.1" 14 | 15 | [dev-dependencies] 16 | bevy_egui = "0.32.0" 17 | bevy_panorbit_camera = { version = "0.21.*", features = ["bevy_egui"] } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Allen Pocket 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_edge_detection 2 | 3 | `bevy_edge_detection` is a Bevy plugin that provides edge detection post-processing using a 3x3 Sobel filter. This plugin is designed to enhance your Bevy projects by adding visually distinct edges to your 3D scenes, making them more stylized or easier to analyze. 4 | 5 | ![3d_shapes](./3d_shapes.png) 6 | 7 | ## Features 8 | 9 | * __Edge Detection__: Utilizes a 3x3 Sobel filter to detect edges based on depth, normal, and color variations. 10 | 11 | * __Customizable Thresholds__: Adjustable thresholds for depth, normal, and color to fine-tune edge detection. 12 | 13 | * __Post-Processing Integration__: Seamlessly integrates with Bevy's post-processing pipeline. 14 | 15 | ## Usage 16 | 17 | 0. Add bevy_edge_detection to your Cargo.toml: 18 | 19 | ```rust 20 | [dependencies] 21 | bevy_edge_detection = "0.15.1" 22 | ``` 23 | 24 | 1. Add the EdgeDetectionPlugin to your Bevy app: 25 | 26 | ```rust 27 | use bevy::prelude::*; 28 | use bevy_edge_detection::EdgeDetectionPlugin; 29 | 30 | fn main() { 31 | App::new() 32 | .add_plugins(DefaultPlugins) 33 | .add_plugin(EdgeDetectionPlugin::default()) 34 | .run(); 35 | } 36 | ``` 37 | 38 | 2. Add `EdgeDetection` to `Camera`: 39 | 40 | ```rust 41 | commands.spawn(( 42 | Camera3d::default(), 43 | Transform::default(), 44 | EdgeDetection::default(), 45 | )); 46 | ``` 47 | 48 | ## Example 49 | 50 | ```rust 51 | cargo run --example 3d_shapes 52 | ``` 53 | 54 | ## License 55 | 56 | This project is licensed under the [MIT License](./LICENSE). 57 | 58 | ## Acknowledgments 59 | 60 | Thanks to [IceSentry](https://github.com/IceSentry), this project was inspired by [bevy_mod_edge_detection](https://github.com/IceSentry/bevy_mod_edge_detection). -------------------------------------------------------------------------------- /examples/3d_shapes.rs: -------------------------------------------------------------------------------- 1 | //! This example comes from [3d_shapes](https://github.com/bevyengine/bevy/blob/main/examples/3d/3d_shapes.rs) 2 | 3 | use std::f32::consts::PI; 4 | 5 | use bevy::{ 6 | color::palettes::basic::SILVER, 7 | core_pipeline::{core_3d::graph::Node3d, smaa::Smaa}, 8 | input::common_conditions::input_toggle_active, 9 | prelude::*, 10 | render::{ 11 | render_asset::RenderAssetUsages, 12 | render_resource::{Extent3d, TextureDimension, TextureFormat}, 13 | }, 14 | }; 15 | use bevy_edge_detection::{EdgeDetection, EdgeDetectionPlugin}; 16 | use bevy_egui::{egui, EguiContexts, EguiPlugin}; 17 | use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; 18 | 19 | fn main() { 20 | App::new() 21 | .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) 22 | .add_plugins(EdgeDetectionPlugin { 23 | // If you wish to apply Smaa anti-aliasing after edge detection, 24 | // please ensure that the rendering order of [`EdgeDetectionNode`] is set before [`SmaaNode`]. 25 | before: Node3d::Smaa, 26 | }) 27 | .add_plugins(EguiPlugin) 28 | .add_plugins(PanOrbitCameraPlugin) 29 | .add_systems(Startup, (setup, spawn_text)) 30 | .add_systems( 31 | Update, 32 | ( 33 | rotate.run_if(input_toggle_active(false, KeyCode::Space)), 34 | edge_detection_ui, 35 | ), 36 | ) 37 | .run(); 38 | } 39 | 40 | /// A marker component for our shapes so we can query them separately from the ground plane 41 | #[derive(Component)] 42 | struct Shape; 43 | 44 | const SHAPES_X_EXTENT: f32 = 14.0; 45 | const EXTRUSION_X_EXTENT: f32 = 16.0; 46 | const Z_EXTENT: f32 = 5.0; 47 | 48 | fn setup( 49 | mut commands: Commands, 50 | mut meshes: ResMut>, 51 | mut images: ResMut>, 52 | mut materials: ResMut>, 53 | ) { 54 | let debug_material = materials.add(StandardMaterial { 55 | base_color_texture: Some(images.add(uv_debug_texture())), 56 | ..default() 57 | }); 58 | 59 | let shapes = [ 60 | meshes.add(Cuboid::default()), 61 | meshes.add(Tetrahedron::default()), 62 | meshes.add(Capsule3d::default()), 63 | meshes.add(Torus::default()), 64 | meshes.add(Cylinder::default()), 65 | meshes.add(Cone::default()), 66 | meshes.add(ConicalFrustum::default()), 67 | meshes.add(Sphere::default().mesh().ico(5).unwrap()), 68 | meshes.add(Sphere::default().mesh().uv(32, 18)), 69 | ]; 70 | 71 | let extrusions = [ 72 | meshes.add(Extrusion::new(Rectangle::default(), 1.)), 73 | meshes.add(Extrusion::new(Capsule2d::default(), 1.)), 74 | meshes.add(Extrusion::new(Annulus::default(), 1.)), 75 | meshes.add(Extrusion::new(Circle::default(), 1.)), 76 | meshes.add(Extrusion::new(Ellipse::default(), 1.)), 77 | meshes.add(Extrusion::new(RegularPolygon::default(), 1.)), 78 | meshes.add(Extrusion::new(Triangle2d::default(), 1.)), 79 | ]; 80 | 81 | let num_shapes = shapes.len(); 82 | 83 | for (i, shape) in shapes.into_iter().enumerate() { 84 | commands.spawn(( 85 | Mesh3d(shape), 86 | MeshMaterial3d(debug_material.clone()), 87 | Transform::from_xyz( 88 | -SHAPES_X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * SHAPES_X_EXTENT, 89 | 2.0, 90 | Z_EXTENT / 2., 91 | ) 92 | .with_rotation(Quat::from_rotation_x(-PI / 4.)), 93 | Shape, 94 | )); 95 | } 96 | 97 | let num_extrusions = extrusions.len(); 98 | 99 | for (i, shape) in extrusions.into_iter().enumerate() { 100 | commands.spawn(( 101 | Mesh3d(shape), 102 | MeshMaterial3d(debug_material.clone()), 103 | Transform::from_xyz( 104 | -EXTRUSION_X_EXTENT / 2. 105 | + i as f32 / (num_extrusions - 1) as f32 * EXTRUSION_X_EXTENT, 106 | 2.0, 107 | -Z_EXTENT / 2., 108 | ) 109 | .with_rotation(Quat::from_rotation_x(-PI / 4.)), 110 | Shape, 111 | )); 112 | } 113 | 114 | commands.spawn(( 115 | PointLight { 116 | shadows_enabled: true, 117 | intensity: 10_000_000., 118 | range: 100.0, 119 | shadow_depth_bias: 0.2, 120 | ..default() 121 | }, 122 | Transform::from_xyz(8.0, 16.0, 8.0), 123 | )); 124 | 125 | // ground plane 126 | commands.spawn(( 127 | Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))), 128 | MeshMaterial3d(materials.add(Color::from(SILVER))), 129 | )); 130 | 131 | commands.spawn(( 132 | Camera3d::default(), 133 | Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), 134 | Camera { 135 | clear_color: Color::WHITE.into(), 136 | ..default() 137 | }, 138 | // [`EdgeDetectionNode`] supports `Msaa``, and you can enable it at any time, for example: 139 | // Msaa::default(), 140 | Msaa::Off, 141 | EdgeDetection::default(), 142 | Smaa::default(), 143 | // to control camera 144 | PanOrbitCamera::default(), 145 | )); 146 | } 147 | 148 | fn spawn_text(mut commands: Commands) { 149 | commands.spawn(( 150 | Text::new("Press Space to turn on/off rotation!"), 151 | Node { 152 | position_type: PositionType::Absolute, 153 | bottom: Val::Px(12.0), 154 | left: Val::Px(12.0), 155 | ..default() 156 | }, 157 | )); 158 | } 159 | 160 | fn rotate(mut query: Query<&mut Transform, With>, time: Res