Add test for invalid skinned meshes (#18763)
## Objective Add a test that would have caught #16929 and #18712. ## Solution The PR adds a `test_invalid_skinned_mesh` example that creates various valid and invalid skinned meshes. This is designed to catch panics via CI, and can be inspected visually. It also tests skinned meshes + motion blur.  The screenshot shows all the tests, but two are currently disabled as they cause panics. #18074 will re-enable them. ### Concerns - The test is not currently suitable for screenshot comparison. - I didn't add the test to CI. I'm a bit unsure if this should be part of the PR or a follow up discussion. - Visual inspection requires understanding why some meshes are deliberately broken and what that looks like. - I wasn't sure about naming conventions. I put `test` in the name so it's not confused with a real example. ## Testing ``` cargo run --example test_invalid_skinned_mesh ``` Tested on Win10/Nvidia, across Vulkan, WebGL/Chrome, WebGPU/Chrome.
This commit is contained in:
parent
831073105f
commit
0f75142560
@ -4325,6 +4325,14 @@ description = "Demonstrates specular tints and maps"
|
||||
category = "3D Rendering"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "test_invalid_skinned_mesh"
|
||||
path = "tests/3d/test_invalid_skinned_mesh.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.test_invalid_skinned_mesh]
|
||||
hidden = true
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = "z"
|
||||
|
233
tests/3d/test_invalid_skinned_mesh.rs
Normal file
233
tests/3d/test_invalid_skinned_mesh.rs
Normal file
@ -0,0 +1,233 @@
|
||||
//! Test that the renderer can handle various invalid skinned meshes
|
||||
|
||||
use bevy::{
|
||||
core_pipeline::motion_blur::MotionBlur,
|
||||
math::ops,
|
||||
prelude::*,
|
||||
render::{
|
||||
camera::ScalingMode,
|
||||
mesh::{
|
||||
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
|
||||
Indices, PrimitiveTopology, VertexAttributeValues,
|
||||
},
|
||||
render_asset::RenderAssetUsages,
|
||||
},
|
||||
};
|
||||
use core::f32::consts::TAU;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.insert_resource(AmbientLight {
|
||||
brightness: 20_000.0,
|
||||
..default()
|
||||
})
|
||||
.add_systems(Startup, (setup_environment, setup_meshes))
|
||||
.add_systems(Update, update_animated_joints)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup_environment(
|
||||
mut commands: Commands,
|
||||
mut mesh_assets: ResMut<Assets<Mesh>>,
|
||||
mut material_assets: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
let description = "(left to right)\n\
|
||||
0: Normal skinned mesh.\n\
|
||||
1: Mesh asset is missing skinning attributes.\n\
|
||||
2: One joint entity is missing.\n\
|
||||
3: Mesh entity is missing SkinnedMesh component.";
|
||||
|
||||
commands.spawn((
|
||||
Text::new(description),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(12.0),
|
||||
left: Val::Px(12.0),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
Transform::from_xyz(0.0, 0.0, 1.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
|
||||
Projection::Orthographic(OrthographicProjection {
|
||||
scaling_mode: ScalingMode::AutoMin {
|
||||
min_width: 19.0,
|
||||
min_height: 6.0,
|
||||
},
|
||||
..OrthographicProjection::default_3d()
|
||||
}),
|
||||
// Add motion blur so we can check if it's working for skinned meshes.
|
||||
// This also exercises the renderer's prepass path.
|
||||
MotionBlur {
|
||||
// Use an unrealistically large shutter angle so that motion blur is clearly visible.
|
||||
shutter_angle: 3.0,
|
||||
samples: 2,
|
||||
},
|
||||
// MSAA and MotionBlur together are not compatible on WebGL.
|
||||
#[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
|
||||
Msaa::Off,
|
||||
));
|
||||
|
||||
// Add a directional light to make sure we exercise the renderer's shadow path.
|
||||
commands.spawn((
|
||||
Transform::from_xyz(1.0, 1.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
DirectionalLight {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Add a plane behind the meshes so we can see the shadows.
|
||||
commands.spawn((
|
||||
Transform::from_xyz(0.0, 0.0, -1.0),
|
||||
Mesh3d(mesh_assets.add(Plane3d::default().mesh().size(100.0, 100.0).normal(Dir3::Z))),
|
||||
MeshMaterial3d(material_assets.add(StandardMaterial {
|
||||
base_color: Color::srgb(0.05, 0.05, 0.15),
|
||||
reflectance: 0.2,
|
||||
..default()
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
fn setup_meshes(
|
||||
mut commands: Commands,
|
||||
mut mesh_assets: ResMut<Assets<Mesh>>,
|
||||
mut material_assets: ResMut<Assets<StandardMaterial>>,
|
||||
mut inverse_bindposes_assets: ResMut<Assets<SkinnedMeshInverseBindposes>>,
|
||||
) {
|
||||
// Create a mesh with two rectangles.
|
||||
let unskinned_mesh = Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetUsages::default(),
|
||||
)
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_POSITION,
|
||||
vec![
|
||||
[-0.3, -0.3, 0.0],
|
||||
[0.3, -0.3, 0.0],
|
||||
[-0.3, 0.3, 0.0],
|
||||
[0.3, 0.3, 0.0],
|
||||
[-0.4, 0.8, 0.0],
|
||||
[0.4, 0.8, 0.0],
|
||||
[-0.4, 1.8, 0.0],
|
||||
[0.4, 1.8, 0.0],
|
||||
],
|
||||
)
|
||||
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 8])
|
||||
.with_inserted_indices(Indices::U16(vec![0, 1, 3, 0, 3, 2, 4, 5, 7, 4, 7, 6]));
|
||||
|
||||
// Copy the mesh and add skinning attributes that bind each rectangle to a joint.
|
||||
let skinned_mesh = unskinned_mesh
|
||||
.clone()
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_JOINT_INDEX,
|
||||
VertexAttributeValues::Uint16x4(vec![
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[1, 0, 0, 0],
|
||||
[1, 0, 0, 0],
|
||||
[1, 0, 0, 0],
|
||||
[1, 0, 0, 0],
|
||||
]),
|
||||
)
|
||||
.with_inserted_attribute(
|
||||
Mesh::ATTRIBUTE_JOINT_WEIGHT,
|
||||
vec![[1.00, 0.00, 0.0, 0.0]; 8],
|
||||
);
|
||||
|
||||
let unskinned_mesh_handle = mesh_assets.add(unskinned_mesh);
|
||||
let skinned_mesh_handle = mesh_assets.add(skinned_mesh);
|
||||
|
||||
let inverse_bindposes_handle = inverse_bindposes_assets.add(vec![
|
||||
Mat4::IDENTITY,
|
||||
Mat4::from_translation(Vec3::new(0.0, -1.3, 0.0)),
|
||||
]);
|
||||
|
||||
let mesh_material_handle = material_assets.add(StandardMaterial::default());
|
||||
|
||||
let background_material_handle = material_assets.add(StandardMaterial {
|
||||
base_color: Color::srgb(0.05, 0.15, 0.05),
|
||||
reflectance: 0.2,
|
||||
..default()
|
||||
});
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Variation {
|
||||
Normal,
|
||||
MissingMeshAttributes,
|
||||
MissingJointEntity,
|
||||
MissingSkinnedMeshComponent,
|
||||
}
|
||||
|
||||
for (index, variation) in [
|
||||
Variation::Normal,
|
||||
Variation::MissingMeshAttributes,
|
||||
Variation::MissingJointEntity,
|
||||
Variation::MissingSkinnedMeshComponent,
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
// Skip variations that are currently broken. See https://github.com/bevyengine/bevy/issues/16929,
|
||||
// https://github.com/bevyengine/bevy/pull/18074.
|
||||
if (variation == Variation::MissingSkinnedMeshComponent)
|
||||
|| (variation == Variation::MissingMeshAttributes)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let transform = Transform::from_xyz(((index as f32) - 1.5) * 4.5, 0.0, 0.0);
|
||||
|
||||
let joint_0 = commands.spawn(transform).id();
|
||||
|
||||
let joint_1 = commands
|
||||
.spawn((ChildOf(joint_0), AnimatedJoint, Transform::IDENTITY))
|
||||
.id();
|
||||
|
||||
if variation == Variation::MissingJointEntity {
|
||||
commands.entity(joint_1).despawn();
|
||||
}
|
||||
|
||||
let mesh_handle = match variation {
|
||||
Variation::MissingMeshAttributes => &unskinned_mesh_handle,
|
||||
_ => &skinned_mesh_handle,
|
||||
};
|
||||
|
||||
let mut entity_commands = commands.spawn((
|
||||
Mesh3d(mesh_handle.clone()),
|
||||
MeshMaterial3d(mesh_material_handle.clone()),
|
||||
transform,
|
||||
));
|
||||
|
||||
if variation != Variation::MissingSkinnedMeshComponent {
|
||||
entity_commands.insert(SkinnedMesh {
|
||||
inverse_bindposes: inverse_bindposes_handle.clone(),
|
||||
joints: vec![joint_0, joint_1],
|
||||
});
|
||||
}
|
||||
|
||||
// Add a square behind the mesh to distinguish it from the other meshes.
|
||||
commands.spawn((
|
||||
Transform::from_xyz(transform.translation.x, transform.translation.y, -0.8),
|
||||
Mesh3d(mesh_assets.add(Plane3d::default().mesh().size(4.3, 4.3).normal(Dir3::Z))),
|
||||
MeshMaterial3d(background_material_handle.clone()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct AnimatedJoint;
|
||||
|
||||
fn update_animated_joints(time: Res<Time>, query: Query<&mut Transform, With<AnimatedJoint>>) {
|
||||
for mut transform in query {
|
||||
let angle = TAU * 4.0 * ops::cos((time.elapsed_secs() / 8.0) * TAU);
|
||||
let rotation = Quat::from_rotation_z(angle);
|
||||
|
||||
transform.rotation = rotation;
|
||||
transform.translation = rotation.mul_vec3(Vec3::new(0.0, 1.3, 0.0));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user