Skip to main content

3d_shapes/
3d_shapes.rs

1//! Here we use shape primitives to generate meshes for 3d objects as well as attaching a runtime-generated patterned texture to each 3d object.
2//!
3//! "Shape primitives" here are just the mathematical definition of certain shapes, they're not meshes on their own! A sphere with radius `1.0` can be defined with [`Sphere::new(1.0)`][Sphere::new] but all this does is store the radius. So we need to turn these descriptions of shapes into meshes.
4//!
5//! While a shape is not a mesh, turning it into one in Bevy is easy. In this example we call [`meshes.add(/* Shape here! */)`][`Assets<A>::add`] on the shape, which works because the [`Assets<A>::add`] method takes anything that can be turned into the asset type it stores. There's an implementation for [`From`] on shape primitives into [`Mesh`], so that will get called internally by [`Assets<A>::add`].
6//!
7//! [`Extrusion`] lets us turn 2D shape primitives into versions of those shapes that have volume by extruding them. A 1x1 square that gets wrapped in this with an extrusion depth of 2 will give us a rectangular prism of size 1x1x2, but here we're just extruding these 2d shapes by depth 1.
8//!
9//! The material applied to these shapes is a texture that we generate at run time by looping through a "palette" of RGBA values (stored adjacent to each other in the array) and writing values to positions in another array that represents the buffer for an 8x8 texture. This texture is then registered with the assets system just one time, with that [`Handle<StandardMaterial>`] then applied to all the shapes in this example.
10//!
11//! The mesh and material are [`Handle<Mesh>`] and [`Handle<StandardMaterial>`] at the moment, neither of which implement `Component` on their own. Handles are put behind "newtypes" to prevent ambiguity, as some entities might want to have handles to meshes (or images, or materials etc.) for different purposes! All we need to do to make them rendering-relevant components is wrap the mesh handle and the material handle in [`Mesh3d`] and [`MeshMaterial3d`] respectively.
12//!
13//! You can toggle wireframes with the space bar except on wasm. Wasm does not support
14//! `POLYGON_MODE_LINE` on the gpu.
15
16use std::f32::consts::PI;
17
18#[cfg(not(target_arch = "wasm32"))]
19use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin};
20use bevy::{
21    asset::RenderAssetUsages,
22    color::palettes::basic::SILVER,
23    input::common_conditions::{input_just_pressed, input_toggle_active},
24    prelude::*,
25    render::render_resource::{Extent3d, TextureDimension, TextureFormat},
26};
27
28fn main() {
29    App::new()
30        .add_plugins((
31            DefaultPlugins.set(ImagePlugin::default_nearest()),
32            #[cfg(not(target_arch = "wasm32"))]
33            WireframePlugin::default(),
34        ))
35        .add_systems(Startup, setup)
36        .add_systems(
37            Update,
38            (
39                rotate.run_if(input_toggle_active(true, KeyCode::KeyR)),
40                advance_rows.run_if(input_just_pressed(KeyCode::Tab)),
41                #[cfg(not(target_arch = "wasm32"))]
42                toggle_wireframe,
43            ),
44        )
45        .run();
46}
47
48/// A marker component for our shapes so we can query them separately from the ground plane
49#[derive(Component)]
50struct Shape;
51
52const SHAPES_X_EXTENT: f32 = 14.0;
53const EXTRUSION_X_EXTENT: f32 = 14.0;
54const Z_EXTENT: f32 = 8.0;
55const THICKNESS: f32 = 0.1;
56
57fn setup(
58    mut commands: Commands,
59    mut meshes: ResMut<Assets<Mesh>>,
60    mut images: ResMut<Assets<Image>>,
61    mut materials: ResMut<Assets<StandardMaterial>>,
62) {
63    let debug_material = materials.add(StandardMaterial {
64        base_color_texture: Some(images.add(uv_debug_texture())),
65        ..default()
66    });
67
68    let shapes = [
69        meshes.add(Cuboid::default()),
70        meshes.add(Tetrahedron::default()),
71        meshes.add(Capsule3d::default()),
72        meshes.add(Torus::default()),
73        meshes.add(Cylinder::default()),
74        meshes.add(Cone::default()),
75        meshes.add(ConicalFrustum::default()),
76        meshes.add(Sphere::default().mesh().ico(5).unwrap()),
77        meshes.add(Sphere::default().mesh().uv(32, 18)),
78        meshes.add(Segment3d::default()),
79        meshes.add(Polyline3d::new(vec![
80            Vec3::new(-0.5, 0.0, 0.0),
81            Vec3::new(0.5, 0.0, 0.0),
82            Vec3::new(0.0, 0.5, 0.0),
83        ])),
84    ];
85
86    let extrusions = [
87        meshes.add(Extrusion::new(Rectangle::default(), 1.)),
88        meshes.add(Extrusion::new(Capsule2d::default(), 1.)),
89        meshes.add(Extrusion::new(Annulus::default(), 1.)),
90        meshes.add(Extrusion::new(Circle::default(), 1.)),
91        meshes.add(Extrusion::new(Ellipse::default(), 1.)),
92        meshes.add(Extrusion::new(RegularPolygon::default(), 1.)),
93        meshes.add(Extrusion::new(Triangle2d::default(), 1.)),
94        meshes.add(Extrusion::new(
95            ConvexPolygon::new(vec![
96                Vec2::new(0.0, 0.8),
97                Vec2::new(-0.47, 0.25),
98                Vec2::new(-0.47, -0.65),
99                Vec2::new(0.47, -0.65),
100                Vec2::new(0.47, 0.25),
101            ])
102            .unwrap(),
103            1.0,
104        )),
105    ];
106
107    let ring_extrusions = [
108        meshes.add(Extrusion::new(Rectangle::default().to_ring(THICKNESS), 1.)),
109        meshes.add(Extrusion::new(Capsule2d::default().to_ring(THICKNESS), 1.)),
110        meshes.add(Extrusion::new(
111            Ring::new(Circle::new(1.0), Circle::new(0.5)),
112            1.,
113        )),
114        meshes.add(Extrusion::new(Circle::default().to_ring(THICKNESS), 1.)),
115        meshes.add(Extrusion::new(
116            {
117                // This is an approximation; Ellipse does not implement Inset as concentric ellipses do not have parallel curves
118                let outer = Ellipse::default();
119                let mut inner = outer;
120                inner.half_size -= Vec2::splat(THICKNESS);
121                Ring::new(outer, inner)
122            },
123            1.,
124        )),
125        meshes.add(Extrusion::new(
126            RegularPolygon::default().to_ring(THICKNESS),
127            1.,
128        )),
129        meshes.add(Extrusion::new(Triangle2d::default().to_ring(THICKNESS), 1.)),
130    ];
131
132    let num_shapes = shapes.len();
133
134    for (i, shape) in shapes.into_iter().enumerate() {
135        commands.spawn((
136            Mesh3d(shape),
137            MeshMaterial3d(debug_material.clone()),
138            Transform::from_xyz(
139                -SHAPES_X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * SHAPES_X_EXTENT,
140                2.0,
141                Row::Front.z(),
142            )
143            .with_rotation(Quat::from_rotation_x(-PI / 4.)),
144            Shape,
145            Row::Front,
146        ));
147    }
148
149    let num_extrusions = extrusions.len();
150
151    for (i, shape) in extrusions.into_iter().enumerate() {
152        commands.spawn((
153            Mesh3d(shape),
154            MeshMaterial3d(debug_material.clone()),
155            Transform::from_xyz(
156                -EXTRUSION_X_EXTENT / 2.
157                    + i as f32 / (num_extrusions - 1) as f32 * EXTRUSION_X_EXTENT,
158                2.0,
159                Row::Middle.z(),
160            )
161            .with_rotation(Quat::from_rotation_x(-PI / 4.)),
162            Shape,
163            Row::Middle,
164        ));
165    }
166
167    let num_ring_extrusions = ring_extrusions.len();
168
169    for (i, shape) in ring_extrusions.into_iter().enumerate() {
170        commands.spawn((
171            Mesh3d(shape),
172            MeshMaterial3d(debug_material.clone()),
173            Transform::from_xyz(
174                -EXTRUSION_X_EXTENT / 2.
175                    + i as f32 / (num_ring_extrusions - 1) as f32 * EXTRUSION_X_EXTENT,
176                2.0,
177                Row::Rear.z(),
178            )
179            .with_rotation(Quat::from_rotation_x(-PI / 4.)),
180            Shape,
181            Row::Rear,
182        ));
183    }
184
185    commands.spawn((
186        PointLight {
187            shadow_maps_enabled: true,
188            intensity: 10_000_000.,
189            range: 100.0,
190            shadow_depth_bias: 0.2,
191            ..default()
192        },
193        Transform::from_xyz(8.0, 16.0, 8.0),
194    ));
195
196    // ground plane
197    commands.spawn((
198        Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))),
199        MeshMaterial3d(materials.add(Color::from(SILVER))),
200    ));
201
202    commands.spawn((
203        Camera3d::default(),
204        Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
205    ));
206
207    let mut text = "\
208        Press 'R' to pause/resume rotation\n\
209        Press 'Tab' to cycle through rows"
210        .to_string();
211    #[cfg(not(target_arch = "wasm32"))]
212    text.push_str("\nPress 'Space' to toggle wireframes");
213
214    commands.spawn((
215        Text::new(text),
216        Node {
217            position_type: PositionType::Absolute,
218            top: px(12),
219            left: px(12),
220            ..default()
221        },
222    ));
223}
224
225fn rotate(mut query: Query<&mut Transform, With<Shape>>, time: Res<Time>) {
226    for mut transform in &mut query {
227        transform.rotate_y(time.delta_secs() / 2.);
228    }
229}
230
231/// Creates a colorful test pattern
232fn uv_debug_texture() -> Image {
233    const TEXTURE_SIZE: usize = 8;
234
235    let mut palette: [u8; 32] = [
236        255, 102, 159, 255, 255, 159, 102, 255, 236, 255, 102, 255, 121, 255, 102, 255, 102, 255,
237        198, 255, 102, 198, 255, 255, 121, 102, 255, 255, 236, 102, 255, 255,
238    ];
239
240    let mut texture_data = [0; TEXTURE_SIZE * TEXTURE_SIZE * 4];
241    for y in 0..TEXTURE_SIZE {
242        let offset = TEXTURE_SIZE * y * 4;
243        texture_data[offset..(offset + TEXTURE_SIZE * 4)].copy_from_slice(&palette);
244        palette.rotate_right(4);
245    }
246
247    Image::new_fill(
248        Extent3d {
249            width: TEXTURE_SIZE as u32,
250            height: TEXTURE_SIZE as u32,
251            depth_or_array_layers: 1,
252        },
253        TextureDimension::D2,
254        &texture_data,
255        TextureFormat::Rgba8UnormSrgb,
256        RenderAssetUsages::RENDER_WORLD,
257    )
258}
259
260#[cfg(not(target_arch = "wasm32"))]
261fn toggle_wireframe(
262    mut wireframe_config: ResMut<WireframeConfig>,
263    keyboard: Res<ButtonInput<KeyCode>>,
264) {
265    if keyboard.just_pressed(KeyCode::Space) {
266        wireframe_config.global = !wireframe_config.global;
267    }
268}
269
270#[derive(Component, Clone, Copy)]
271enum Row {
272    Front,
273    Middle,
274    Rear,
275}
276
277impl Row {
278    fn z(self) -> f32 {
279        match self {
280            Row::Front => Z_EXTENT / 2.,
281            Row::Middle => 0.,
282            Row::Rear => -Z_EXTENT / 2.,
283        }
284    }
285
286    fn advance(self) -> Self {
287        match self {
288            Row::Front => Row::Rear,
289            Row::Middle => Row::Front,
290            Row::Rear => Row::Middle,
291        }
292    }
293}
294
295fn advance_rows(mut shapes: Query<(&mut Row, &mut Transform), With<Shape>>) {
296    for (mut row, mut transform) in &mut shapes {
297        *row = row.advance();
298        transform.translation.z = row.z();
299    }
300}