├── .github └── workflows │ └── rust.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── models │ ├── README.md │ ├── disc.scad │ └── disc.stl ├── examples ├── spinning_disc.rs └── spinning_disc_wireframe.rs └── src └── lib.rs /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Format check 18 | run: cargo fmt --verbose --check 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Build example 22 | run: cargo build --verbose --example spinning_disc 23 | - name: Build (wireframe feature) 24 | run: cargo build --verbose --features wireframe 25 | - name: Build example (wireframe feature) 26 | run: cargo build --verbose --example spinning_disc_wireframe --features wireframe 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | *~ 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_stl" 3 | version = "0.16.0" 4 | authors = ["Niklas Cathor "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "STL loader for bevy, based on stl_io" 8 | repository = "https://github.com/nilclass/bevy_stl" 9 | readme = "README.md" 10 | resolver = "2" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | bevy = { git = "https://github.com/bevyengine/bevy", default-features = false, features = [ 16 | "bevy_asset", 17 | "bevy_render", 18 | ] } 19 | thiserror = "2.0" 20 | stl_io = "0.8.3" 21 | 22 | [dev-dependencies] 23 | bevy = { git = "https://github.com/bevyengine/bevy", default-features = false, features = [ 24 | "bevy_asset", 25 | "bevy_core_pipeline", 26 | "bevy_pbr", 27 | "bevy_winit", 28 | "bevy_window", 29 | "tonemapping_luts", 30 | "x11", 31 | ] } 32 | 33 | [features] 34 | default = ["wireframe"] 35 | 36 | # Along with the triangle mesh, generate an additional wireframe mesh (a PrimitiveTopology::LineList). 37 | # The wireframe mesh can be accessed by `asset_server.load("my-model.stl#wireframe")`. 38 | wireframe = [] 39 | 40 | [[example]] 41 | name = "spinning_disc" 42 | path = "examples/spinning_disc.rs" 43 | 44 | [[example]] 45 | name = "spinning_disc_wireframe" 46 | path = "examples/spinning_disc_wireframe.rs" 47 | required-features = ["wireframe"] 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Niklas Cathor 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. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_stl 2 | 3 | [![Crate version](https://img.shields.io/crates/v/bevy_stl?style=flat-square)](https://crates.io/crates/bevy_stl/) 4 | 5 | ![Crate license](https://img.shields.io/crates/l/bevy_stl?style=flat-square) 6 | 7 | A [STL](https://en.wikipedia.org/wiki/STL_(file_format)) loader for [bevy](https://bevyengine.org/). 8 | 9 | STL is a very simple format, which supports only triangular geometry (positions + normals), without any color / uv / texture information. 10 | 11 | It is supported as an output format by most CAD software. 12 | 13 | ## Alternatives 14 | 15 | - by default bevy can load [glTF scenes](https://en.wikipedia.org/wiki/GlTF), which is a much better choice if you are looking for a way to load more complex models / scenes, including materials, animations, etc. 16 | - [bevy_obj](https://crates.io/crates/bevy_obj) can load [Wavefront .obj files](https://en.wikipedia.org/wiki/Wavefront_.obj_file), which can carry more information than STL (such as color, material, UV coordinates) 17 | 18 | ## Usage 19 | 20 | 1. Add `bevy_stl` to your `Cargo.toml` 21 | 2. Add `bevy_stl::StlPlugin` plugin to the bevy `App` 22 | 3. Load STL assets by passing paths with ".stl" extension to `asset_server.load(..)` 23 | 24 | ### Example 25 | 26 | ```rust 27 | fn main() { 28 | App::new() 29 | .add_plugins(bevy_stl::StlPlugin) 30 | .add_systems(Startup, setup) 31 | // ... 32 | .run(); 33 | } 34 | 35 | fn setup(commands: Commands, asset_server: Res, mut materials: ResMut>) { 36 | commands.spawn(( 37 | Mesh3d(asset_server.load("disc.stl")), 38 | MeshMaterial3d(Color::rgb(0.9, 0.4, 0.3).into()), 39 | )); 40 | } 41 | ``` 42 | 43 | You can find a more complete example in `examples/spinning_disc.rs` - use `cargo run --example spinning_disc --release` to run it. 44 | 45 | ## Optional Features 46 | 47 | ### Wireframe 48 | 49 | By default `bevy_stl` produces a triangle mesh (`PrimitiveTopology::TriangleList`). 50 | When the **optional** `wireframe` feature is enabled, an additional line mesh is produced (`PrimitiveTopology::LineList`). 51 | 52 | The feature can be enabled via Cargo.toml: 53 | ``` 54 | [dependencies.bevy_stl] 55 | features = ["wireframe"] 56 | ``` 57 | 58 | When enabled, the mesh can be accessed by appending the wireframe label to the path passed to the asset loader: 59 | ``` 60 | // This returns the triangle mesh (the default): 61 | asset_server.load("disc.stl") 62 | 63 | // This returns the wireframe mesh: 64 | asset_server.load("disc.stl#wireframe") 65 | ``` 66 | -------------------------------------------------------------------------------- /assets/models/README.md: -------------------------------------------------------------------------------- 1 | `disc.scad` contains an [openscad](https://www.openscad.org/) model. 2 | 3 | After making changes to it, run the following to regenerate `disc.stl`: 4 | ```sh 5 | openscad assets/models/disc.scad -o assets/models/disc.stl 6 | ``` 7 | -------------------------------------------------------------------------------- /assets/models/disc.scad: -------------------------------------------------------------------------------- 1 | $fn = 100; 2 | 3 | module ring(radius, width) { 4 | difference() { 5 | circle(r = radius); 6 | circle(r = radius - width); 7 | } 8 | } 9 | 10 | module pizza_slice(radius, angle) { 11 | if (angle > 90) { 12 | union() { 13 | pizza_slice(radius, 90); 14 | rotate(90) pizza_slice(radius, angle - 90); 15 | } 16 | } else { 17 | intersection() { 18 | circle(r = radius); 19 | polygon([ 20 | [0, 0], 21 | [radius * 2, 0], 22 | [radius * 2 * cos(angle), radius * 2 * sin(angle)] 23 | ]); 24 | } 25 | } 26 | } 27 | 28 | module segments(n, radius, width) { 29 | let (angle = 360 / (n * 2)) { 30 | for (i = [1 : n]) { 31 | rotate((i - 1) * 2 * angle) intersection() { 32 | pizza_slice(radius, angle); 33 | ring(radius, width); 34 | } 35 | } 36 | } 37 | } 38 | 39 | module segment_rings(n, width, spacing, min_radius) { 40 | for (i = [1 : n]) { 41 | segments(pow(2, i - 1), min_radius + ((i - 1) * (width + spacing)), width); 42 | } 43 | } 44 | 45 | module disc() { 46 | linear_extrude(height = 2) { 47 | difference() { 48 | circle(r = 42); 49 | circle(r = 6); 50 | segment_rings(8, 0.8, 3, 13); 51 | } 52 | } 53 | } 54 | 55 | disc(); 56 | -------------------------------------------------------------------------------- /examples/spinning_disc.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_stl::StlPlugin; 3 | use core::f32::consts::PI; 4 | use std::time::Duration; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins(DefaultPlugins) 9 | .add_plugins(StlPlugin) 10 | .insert_resource(SpinTimer(Timer::from_seconds( 11 | 1.0 / 60.0, 12 | TimerMode::Repeating, 13 | ))) 14 | .add_systems(Startup, setup) 15 | .add_systems(Update, spin_disc) 16 | .run(); 17 | } 18 | 19 | #[derive(Component)] 20 | struct Disc { 21 | angle: f32, 22 | } 23 | 24 | #[derive(Resource)] 25 | struct SpinTimer(Timer); 26 | 27 | fn setup( 28 | mut commands: Commands, 29 | asset_server: Res, 30 | mut materials: ResMut>, 31 | ) { 32 | commands.spawn(( 33 | Mesh3d(asset_server.load("models/disc.stl")), 34 | MeshMaterial3d(materials.add(Color::srgb(0.9, 0.4, 0.3))), 35 | Transform::from_rotation(Quat::from_rotation_z(0.0)), 36 | Disc { angle: 0.0 }, 37 | )); 38 | commands.spawn(( 39 | Transform::from_xyz(30.0, 0.0, 20.0), 40 | PointLight { 41 | range: 40.0, 42 | ..Default::default() 43 | }, 44 | )); 45 | commands.spawn(( 46 | Transform::from_translation(Vec3::new(0.0, -100.0, 100.0)) 47 | .looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y), 48 | Camera3d::default(), 49 | Msaa::Sample4, 50 | )); 51 | } 52 | 53 | fn spin_disc( 54 | time: Res