Skip to content

Add test for invalid skinned meshes #18763

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
233 changes: 233 additions & 0 deletions tests/3d/test_invalid_skinned_mesh.rs
Original file line number Diff line number Diff line change
@@ -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));
}
}