Skip to main content

test_invalid_skinned_mesh/
test_invalid_skinned_mesh.rs

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