|
| 1 | +//! Test that the renderer can handle various invalid skinned meshes |
| 2 | +
|
| 3 | +use bevy::{ |
| 4 | + core_pipeline::motion_blur::MotionBlur, |
| 5 | + math::ops, |
| 6 | + prelude::*, |
| 7 | + render::{ |
| 8 | + camera::ScalingMode, |
| 9 | + mesh::{ |
| 10 | + skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, |
| 11 | + Indices, PrimitiveTopology, VertexAttributeValues, |
| 12 | + }, |
| 13 | + render_asset::RenderAssetUsages, |
| 14 | + }, |
| 15 | +}; |
| 16 | +use core::f32::consts::TAU; |
| 17 | + |
| 18 | +fn main() { |
| 19 | + App::new() |
| 20 | + .add_plugins(DefaultPlugins) |
| 21 | + .insert_resource(AmbientLight { |
| 22 | + brightness: 20_000.0, |
| 23 | + ..default() |
| 24 | + }) |
| 25 | + .add_systems(Startup, (setup_environment, setup_meshes)) |
| 26 | + .add_systems(Update, update_animated_joints) |
| 27 | + .run(); |
| 28 | +} |
| 29 | + |
| 30 | +fn setup_environment( |
| 31 | + mut commands: Commands, |
| 32 | + mut mesh_assets: ResMut<Assets<Mesh>>, |
| 33 | + mut material_assets: ResMut<Assets<StandardMaterial>>, |
| 34 | +) { |
| 35 | + let description = "(left to right)\n\ |
| 36 | + 0: Normal skinned mesh.\n\ |
| 37 | + 1: Mesh asset is missing skinning attributes.\n\ |
| 38 | + 2: One joint entity is missing.\n\ |
| 39 | + 3: Mesh entity is missing SkinnedMesh component."; |
| 40 | + |
| 41 | + commands.spawn(( |
| 42 | + Text::new(description), |
| 43 | + Node { |
| 44 | + position_type: PositionType::Absolute, |
| 45 | + top: Val::Px(12.0), |
| 46 | + left: Val::Px(12.0), |
| 47 | + ..default() |
| 48 | + }, |
| 49 | + )); |
| 50 | + |
| 51 | + commands.spawn(( |
| 52 | + Camera3d::default(), |
| 53 | + Transform::from_xyz(0.0, 0.0, 1.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y), |
| 54 | + Projection::Orthographic(OrthographicProjection { |
| 55 | + scaling_mode: ScalingMode::AutoMin { |
| 56 | + min_width: 19.0, |
| 57 | + min_height: 6.0, |
| 58 | + }, |
| 59 | + ..OrthographicProjection::default_3d() |
| 60 | + }), |
| 61 | + // Add motion blur so we can check if it's working for skinned meshes. |
| 62 | + // This also exercises the renderer's prepass path. |
| 63 | + MotionBlur { |
| 64 | + // Use an unrealistically large shutter angle so that motion blur is clearly visible. |
| 65 | + shutter_angle: 3.0, |
| 66 | + samples: 2, |
| 67 | + }, |
| 68 | + // MSAA and MotionBlur together are not compatible on WebGL. |
| 69 | + #[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))] |
| 70 | + Msaa::Off, |
| 71 | + )); |
| 72 | + |
| 73 | + // Add a directional light to make sure we exercise the renderer's shadow path. |
| 74 | + commands.spawn(( |
| 75 | + Transform::from_xyz(1.0, 1.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y), |
| 76 | + DirectionalLight { |
| 77 | + shadows_enabled: true, |
| 78 | + ..default() |
| 79 | + }, |
| 80 | + )); |
| 81 | + |
| 82 | + // Add a plane behind the meshes so we can see the shadows. |
| 83 | + commands.spawn(( |
| 84 | + Transform::from_xyz(0.0, 0.0, -1.0), |
| 85 | + Mesh3d(mesh_assets.add(Plane3d::default().mesh().size(100.0, 100.0).normal(Dir3::Z))), |
| 86 | + MeshMaterial3d(material_assets.add(StandardMaterial { |
| 87 | + base_color: Color::srgb(0.05, 0.05, 0.15), |
| 88 | + reflectance: 0.2, |
| 89 | + ..default() |
| 90 | + })), |
| 91 | + )); |
| 92 | +} |
| 93 | + |
| 94 | +fn setup_meshes( |
| 95 | + mut commands: Commands, |
| 96 | + mut mesh_assets: ResMut<Assets<Mesh>>, |
| 97 | + mut material_assets: ResMut<Assets<StandardMaterial>>, |
| 98 | + mut inverse_bindposes_assets: ResMut<Assets<SkinnedMeshInverseBindposes>>, |
| 99 | +) { |
| 100 | + // Create a mesh with two rectangles. |
| 101 | + let unskinned_mesh = Mesh::new( |
| 102 | + PrimitiveTopology::TriangleList, |
| 103 | + RenderAssetUsages::default(), |
| 104 | + ) |
| 105 | + .with_inserted_attribute( |
| 106 | + Mesh::ATTRIBUTE_POSITION, |
| 107 | + vec![ |
| 108 | + [-0.3, -0.3, 0.0], |
| 109 | + [0.3, -0.3, 0.0], |
| 110 | + [-0.3, 0.3, 0.0], |
| 111 | + [0.3, 0.3, 0.0], |
| 112 | + [-0.4, 0.8, 0.0], |
| 113 | + [0.4, 0.8, 0.0], |
| 114 | + [-0.4, 1.8, 0.0], |
| 115 | + [0.4, 1.8, 0.0], |
| 116 | + ], |
| 117 | + ) |
| 118 | + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 8]) |
| 119 | + .with_inserted_indices(Indices::U16(vec![0, 1, 3, 0, 3, 2, 4, 5, 7, 4, 7, 6])); |
| 120 | + |
| 121 | + // Copy the mesh and add skinning attributes that bind each rectangle to a joint. |
| 122 | + let skinned_mesh = unskinned_mesh |
| 123 | + .clone() |
| 124 | + .with_inserted_attribute( |
| 125 | + Mesh::ATTRIBUTE_JOINT_INDEX, |
| 126 | + VertexAttributeValues::Uint16x4(vec![ |
| 127 | + [0, 0, 0, 0], |
| 128 | + [0, 0, 0, 0], |
| 129 | + [0, 0, 0, 0], |
| 130 | + [0, 0, 0, 0], |
| 131 | + [1, 0, 0, 0], |
| 132 | + [1, 0, 0, 0], |
| 133 | + [1, 0, 0, 0], |
| 134 | + [1, 0, 0, 0], |
| 135 | + ]), |
| 136 | + ) |
| 137 | + .with_inserted_attribute( |
| 138 | + Mesh::ATTRIBUTE_JOINT_WEIGHT, |
| 139 | + vec![[1.00, 0.00, 0.0, 0.0]; 8], |
| 140 | + ); |
| 141 | + |
| 142 | + let unskinned_mesh_handle = mesh_assets.add(unskinned_mesh); |
| 143 | + let skinned_mesh_handle = mesh_assets.add(skinned_mesh); |
| 144 | + |
| 145 | + let inverse_bindposes_handle = inverse_bindposes_assets.add(vec![ |
| 146 | + Mat4::IDENTITY, |
| 147 | + Mat4::from_translation(Vec3::new(0.0, -1.3, 0.0)), |
| 148 | + ]); |
| 149 | + |
| 150 | + let mesh_material_handle = material_assets.add(StandardMaterial::default()); |
| 151 | + |
| 152 | + let background_material_handle = material_assets.add(StandardMaterial { |
| 153 | + base_color: Color::srgb(0.05, 0.15, 0.05), |
| 154 | + reflectance: 0.2, |
| 155 | + ..default() |
| 156 | + }); |
| 157 | + |
| 158 | + #[derive(PartialEq)] |
| 159 | + enum Variation { |
| 160 | + Normal, |
| 161 | + MissingMeshAttributes, |
| 162 | + MissingJointEntity, |
| 163 | + MissingSkinnedMeshComponent, |
| 164 | + } |
| 165 | + |
| 166 | + for (index, variation) in [ |
| 167 | + Variation::Normal, |
| 168 | + Variation::MissingMeshAttributes, |
| 169 | + Variation::MissingJointEntity, |
| 170 | + Variation::MissingSkinnedMeshComponent, |
| 171 | + ] |
| 172 | + .into_iter() |
| 173 | + .enumerate() |
| 174 | + { |
| 175 | + // Skip variations that are currently broken. See https://github.com/bevyengine/bevy/issues/16929, |
| 176 | + // https://github.com/bevyengine/bevy/pull/18074. |
| 177 | + if (variation == Variation::MissingSkinnedMeshComponent) |
| 178 | + || (variation == Variation::MissingMeshAttributes) |
| 179 | + { |
| 180 | + continue; |
| 181 | + } |
| 182 | + |
| 183 | + let transform = Transform::from_xyz(((index as f32) - 1.5) * 4.5, 0.0, 0.0); |
| 184 | + |
| 185 | + let joint_0 = commands.spawn(transform).id(); |
| 186 | + |
| 187 | + let joint_1 = commands |
| 188 | + .spawn((ChildOf(joint_0), AnimatedJoint, Transform::IDENTITY)) |
| 189 | + .id(); |
| 190 | + |
| 191 | + if variation == Variation::MissingJointEntity { |
| 192 | + commands.entity(joint_1).despawn(); |
| 193 | + } |
| 194 | + |
| 195 | + let mesh_handle = match variation { |
| 196 | + Variation::MissingMeshAttributes => &unskinned_mesh_handle, |
| 197 | + _ => &skinned_mesh_handle, |
| 198 | + }; |
| 199 | + |
| 200 | + let mut entity_commands = commands.spawn(( |
| 201 | + Mesh3d(mesh_handle.clone()), |
| 202 | + MeshMaterial3d(mesh_material_handle.clone()), |
| 203 | + transform, |
| 204 | + )); |
| 205 | + |
| 206 | + if variation != Variation::MissingSkinnedMeshComponent { |
| 207 | + entity_commands.insert(SkinnedMesh { |
| 208 | + inverse_bindposes: inverse_bindposes_handle.clone(), |
| 209 | + joints: vec![joint_0, joint_1], |
| 210 | + }); |
| 211 | + } |
| 212 | + |
| 213 | + // Add a square behind the mesh to distinguish it from the other meshes. |
| 214 | + commands.spawn(( |
| 215 | + Transform::from_xyz(transform.translation.x, transform.translation.y, -0.8), |
| 216 | + Mesh3d(mesh_assets.add(Plane3d::default().mesh().size(4.3, 4.3).normal(Dir3::Z))), |
| 217 | + MeshMaterial3d(background_material_handle.clone()), |
| 218 | + )); |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +#[derive(Component)] |
| 223 | +struct AnimatedJoint; |
| 224 | + |
| 225 | +fn update_animated_joints(time: Res<Time>, query: Query<&mut Transform, With<AnimatedJoint>>) { |
| 226 | + for mut transform in query { |
| 227 | + let angle = TAU * 4.0 * ops::cos((time.elapsed_secs() / 8.0) * TAU); |
| 228 | + let rotation = Quat::from_rotation_z(angle); |
| 229 | + |
| 230 | + transform.rotation = rotation; |
| 231 | + transform.translation = rotation.mul_vec3(Vec3::new(0.0, 1.3, 0.0)); |
| 232 | + } |
| 233 | +} |
0 commit comments