From f2b8c7609204f73511a275d57541681c043f562d Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:06:46 +0000 Subject: [PATCH 01/10] Added a test that triggers bevy/#16929. --- Cargo.toml | 8 + tests/3d/invalid_skinned_mesh.rs | 369 +++++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 tests/3d/invalid_skinned_mesh.rs diff --git a/Cargo.toml b/Cargo.toml index ba2384675c617..0d4fb87e22c08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4098,6 +4098,14 @@ description = "Demonstrates specular tints and maps" category = "3D Rendering" wasm = true +[[example]] +name = "invalid_skinned_mesh" +path = "tests/3d/invalid_skinned_mesh.rs" +doc-scrape-examples = true + +[package.metadata.example.invalid_skinned_mesh] +hidden = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/tests/3d/invalid_skinned_mesh.rs b/tests/3d/invalid_skinned_mesh.rs new file mode 100644 index 0000000000000..ed68dd268d21d --- /dev/null +++ b/tests/3d/invalid_skinned_mesh.rs @@ -0,0 +1,369 @@ +//! Create invalid skinned meshes to test renderer behaviour. + +use bevy::{ + core_pipeline::{ + motion_blur::MotionBlur, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + }, + math::ops, + pbr::DefaultOpaqueRendererMethod, + prelude::*, + render::{ + camera::ScalingMode, + mesh::{ + skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, + Indices, PrimitiveTopology, VertexAttributeValues, + }, + render_asset::RenderAssetUsages, + }, +}; +use std::f32::consts::TAU; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(AmbientLight { + brightness: 20_000.0, + ..default() + }) + .insert_resource(Globals::default()) + .add_systems(Startup, (setup_environment, setup_meshes)) + .add_systems( + Update, + ( + update_animated_joints, + update_render_mode, + update_motion_blur, + update_text, + ), + ) + .run(); +} + +fn setup_environment( + mut commands: Commands, + mut mesh_assets: ResMut>, + mut material_assets: ResMut>, +) { + commands.spawn(( + Text::default(), + 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: 18.0, + min_height: 6.0, + }, + ..OrthographicProjection::default_3d() + }), + default_motion_blur(), + // MSAA is incompatible with deferred rendering. + Msaa::Off, + )); + + // Add a directional light to make sure we exercise the renderer's lighting 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 skinned meshes so that we can see their 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.1 * 0.5, 0.3 * 0.5, 0.1 * 0.5), + reflectance: 0.2, + ..default() + })), + )); +} + +fn setup_meshes( + mut commands: Commands, + mut mesh_assets: ResMut>, + mut material_assets: ResMut>, + mut inverse_bindposes_assets: ResMut>, +) { + let unskinned_mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_POSITION, + vec![ + [-0.35, -0.35, 0.0], + [0.35, -0.35, 0.0], + [-0.35, 0.35, 0.0], + [0.35, 0.35, 0.0], + [-0.5, 1.0, 0.0], + [0.5, 1.0, 0.0], + [-0.5, 2.0, 0.0], + [0.5, 2.0, 0.0], + ], + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 8]); + + 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], + ) + .with_inserted_indices(Indices::U16(vec![0, 1, 3, 0, 3, 2, 4, 5, 7, 4, 7, 6])); + + 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.5, 0.0)), + ]); + + let material_handle = material_assets.add(StandardMaterial { + cull_mode: None, + ..default() + }); + + // Mesh 0: Normal. + // Mesh 1: Asset is missing joint index and joint weight attributes. + // Mesh 2: Entity is missing SkinnedMesh component. + // Mesh 3: One joint entity deleted. + + for mesh_index in 0..4 { + let transform = Transform::from_xyz(((mesh_index as f32) - 1.5) * 4.0, 0.0, 0.0); + + let joint_0 = commands.spawn(transform).id(); + + let joint_1 = commands + .spawn((ChildOf(joint_0), AnimatedJoint, Transform::IDENTITY)) + .id(); + + let mesh_handle = match mesh_index { + 1 => &unskinned_mesh_handle, + _ => &skinned_mesh_handle, + }; + + let mut entity_commands = commands.spawn(( + Mesh3d(mesh_handle.clone()), + MeshMaterial3d(material_handle.clone()), + transform, + )); + + if mesh_index != 2 { + entity_commands.insert(SkinnedMesh { + inverse_bindposes: inverse_bindposes_handle.clone(), + joints: vec![joint_0, joint_1], + }); + } + + if mesh_index == 3 { + commands.entity(joint_1).despawn(); + } + } +} + +fn default_motion_blur() -> MotionBlur { + MotionBlur { + // Use an unrealistically large shutter angle so that motion blur is clearly visible. + shutter_angle: 4.0, + samples: 2, + #[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding: Default::default(), + } +} + +#[derive(Component)] +struct AnimatedJoint; + +fn update_animated_joints(time: Res