docs/bevy/examples/3d/3d_shapes.rs 10.6 K · 300 lines · raw · history

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
16 use std::f32::consts::PI;
17
18 #[cfg(not(target_arch = "wasm32"))]
19 use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin};
20 use 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
28 fn 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)]
50 struct Shape;
51
52 const SHAPES_X_EXTENT: f32 = 14.0;
53 const EXTRUSION_X_EXTENT: f32 = 14.0;
54 const Z_EXTENT: f32 = 8.0;
55 const THICKNESS: f32 = 0.1;
56
57 fn 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
225 fn 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
232 fn 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"))]
261 fn 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)]
271 enum Row {
272 Front,
273 Middle,
274 Rear,
275 }
276
277 impl 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
295 fn 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 }