From 74dedb284143c2af8b8aeacf4a3c653aa74b7e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Sat, 19 Oct 2024 21:32:03 +0200 Subject: [PATCH] Testbed for 3d (#15993) # Objective - Progress towards #15918 - Add tests for 3d ## Solution - Add tests that cover lights, bloom, gltf and animation - Removed examples `contributors` and `load_gltf` as they don't contribute additional checks to CI ## Testing - `CI_TESTING_CONFIG=.github/example-run/testbed_3d.ron cargo run --example testbed_3d --features "bevy_ci_testing"` --- .github/example-run/contributors.ron | 5 - .github/example-run/load_gltf.ron | 8 - .github/example-run/testbed_3d.ron | 12 ++ Cargo.toml | 8 + examples/testbed/3d.rs | 302 +++++++++++++++++++++++++++ 5 files changed, 322 insertions(+), 13 deletions(-) delete mode 100644 .github/example-run/contributors.ron delete mode 100644 .github/example-run/load_gltf.ron create mode 100644 .github/example-run/testbed_3d.ron create mode 100644 examples/testbed/3d.rs diff --git a/.github/example-run/contributors.ron b/.github/example-run/contributors.ron deleted file mode 100644 index 2d50dc7fd0..0000000000 --- a/.github/example-run/contributors.ron +++ /dev/null @@ -1,5 +0,0 @@ -( - events: [ - (900, AppExit), - ] -) diff --git a/.github/example-run/load_gltf.ron b/.github/example-run/load_gltf.ron deleted file mode 100644 index 72d09118ca..0000000000 --- a/.github/example-run/load_gltf.ron +++ /dev/null @@ -1,8 +0,0 @@ -( - setup: ( - frame_time: Some(0.03), - ), - events: [ - (300, AppExit), - ] -) diff --git a/.github/example-run/testbed_3d.ron b/.github/example-run/testbed_3d.ron new file mode 100644 index 0000000000..467e2fe98f --- /dev/null +++ b/.github/example-run/testbed_3d.ron @@ -0,0 +1,12 @@ +( + events: [ + (100, Screenshot), + (200, Custom("switch_scene")), + (300, Screenshot), + (400, Custom("switch_scene")), + (500, Screenshot), + (600, Custom("switch_scene")), + (700, Screenshot), + (800, AppExit), + ] +) diff --git a/Cargo.toml b/Cargo.toml index 6c7361f723..e5e4bcaeaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3844,3 +3844,11 @@ doc-scrape-examples = true [package.metadata.example.testbed_2d] hidden = true + +[[example]] +name = "testbed_3d" +path = "examples/testbed/3d.rs" +doc-scrape-examples = true + +[package.metadata.example.testbed_3d] +hidden = true diff --git a/examples/testbed/3d.rs b/examples/testbed/3d.rs new file mode 100644 index 0000000000..ff96f10807 --- /dev/null +++ b/examples/testbed/3d.rs @@ -0,0 +1,302 @@ +//! 3d testbed +//! +//! You can switch scene by pressing the spacebar + +#[cfg(feature = "bevy_ci_testing")] +use bevy::dev_tools::ci_testing::CiTestingCustomEvent; +use bevy::prelude::*; + +fn main() { + let mut app = App::new(); + app.add_plugins((DefaultPlugins,)) + .init_state::() + .enable_state_scoped_entities::() + .add_systems(OnEnter(Scene::Light), light::setup) + .add_systems(OnEnter(Scene::Gltf), gltf::setup) + .add_systems(OnEnter(Scene::Animation), animation::setup) + .add_systems(Update, switch_scene); + + // Those scenes don't work in CI on Windows runners + #[cfg(not(all(feature = "bevy_ci_testing", target_os = "windows")))] + app.add_systems(OnEnter(Scene::Bloom), bloom::setup); + + app.run(); +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, States, Default)] +enum Scene { + #[default] + Light, + Bloom, + Gltf, + Animation, +} + +fn switch_scene( + keyboard: Res>, + #[cfg(feature = "bevy_ci_testing")] mut ci_events: EventReader, + scene: Res>, + mut next_scene: ResMut>, +) { + let mut should_switch = false; + should_switch |= keyboard.just_pressed(KeyCode::Space); + #[cfg(feature = "bevy_ci_testing")] + { + should_switch |= ci_events.read().any(|event| match event { + CiTestingCustomEvent(event) => event == "switch_scene", + }); + } + if should_switch { + info!("Switching scene"); + next_scene.set(match scene.get() { + Scene::Light => Scene::Bloom, + Scene::Bloom => Scene::Gltf, + Scene::Gltf => Scene::Animation, + Scene::Animation => Scene::Light, + }); + } +} + +mod light { + use std::f32::consts::PI; + + use bevy::{ + color::palettes::css::{DEEP_PINK, LIME, RED}, + prelude::*, + }; + + const CURRENT_SCENE: super::Scene = super::Scene::Light; + + pub fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + ) { + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::WHITE, + perceptual_roughness: 1.0, + ..default() + })), + StateScoped(CURRENT_SCENE), + )); + + commands.spawn(( + Mesh3d(meshes.add(Cuboid::default())), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: DEEP_PINK.into(), + ..default() + })), + Transform::from_xyz(0.0, 1.0, 0.0), + StateScoped(CURRENT_SCENE), + )); + + commands.spawn(( + PointLight { + intensity: 100_000.0, + color: RED.into(), + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(1.0, 2.0, 0.0), + StateScoped(CURRENT_SCENE), + )); + + commands.spawn(( + SpotLight { + intensity: 100_000.0, + color: LIME.into(), + shadows_enabled: true, + inner_angle: 0.6, + outer_angle: 0.8, + ..default() + }, + Transform::from_xyz(-1.0, 2.0, 0.0).looking_at(Vec3::new(-1.0, 0.0, 0.0), Vec3::Z), + StateScoped(CURRENT_SCENE), + )); + + commands.spawn(( + DirectionalLight { + illuminance: light_consts::lux::OVERCAST_DAY, + shadows_enabled: true, + ..default() + }, + Transform { + translation: Vec3::new(0.0, 2.0, 0.0), + rotation: Quat::from_rotation_x(-PI / 4.), + ..default() + }, + StateScoped(CURRENT_SCENE), + )); + + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + StateScoped(CURRENT_SCENE), + )); + } +} + +mod bloom { + use bevy::{ + core_pipeline::{bloom::Bloom, tonemapping::Tonemapping}, + prelude::*, + }; + + const CURRENT_SCENE: super::Scene = super::Scene::Bloom; + + pub fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + ) { + commands.spawn(( + Camera3d::default(), + Camera { + hdr: true, + ..default() + }, + Tonemapping::TonyMcMapface, + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + Bloom::NATURAL, + StateScoped(CURRENT_SCENE), + )); + + let material_emissive1 = materials.add(StandardMaterial { + emissive: LinearRgba::rgb(13.99, 5.32, 2.0), + ..default() + }); + let material_emissive2 = materials.add(StandardMaterial { + emissive: LinearRgba::rgb(2.0, 13.99, 5.32), + ..default() + }); + + let mesh = meshes.add(Sphere::new(0.5).mesh().ico(5).unwrap()); + + for z in -2..3_i32 { + let material = match (z % 2).abs() { + 0 => material_emissive1.clone(), + 1 => material_emissive2.clone(), + _ => unreachable!(), + }; + + commands.spawn(( + Mesh3d(mesh.clone()), + MeshMaterial3d(material), + Transform::from_xyz(z as f32 * 2.0, 0.0, 0.0), + StateScoped(CURRENT_SCENE), + )); + } + } +} + +mod gltf { + use bevy::prelude::*; + + const CURRENT_SCENE: super::Scene = super::Scene::Gltf; + + pub fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), + specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + intensity: 250.0, + ..default() + }, + StateScoped(CURRENT_SCENE), + )); + + commands.spawn(( + DirectionalLight { + shadows_enabled: true, + ..default() + }, + StateScoped(CURRENT_SCENE), + )); + commands.spawn(( + SceneRoot(asset_server.load( + GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"), + )), + StateScoped(CURRENT_SCENE), + )); + } +} + +mod animation { + use std::{f32::consts::PI, time::Duration}; + + use bevy::{prelude::*, scene::SceneInstanceReady}; + + const CURRENT_SCENE: super::Scene = super::Scene::Animation; + const FOX_PATH: &str = "models/animated/Fox.glb"; + + #[derive(Resource)] + struct Animation { + animation: AnimationNodeIndex, + graph: Handle, + } + + pub fn setup( + mut commands: Commands, + asset_server: Res, + mut graphs: ResMut>, + ) { + let (graph, node) = AnimationGraph::from_clip( + asset_server.load(GltfAssetLabel::Animation(2).from_asset(FOX_PATH)), + ); + + let graph_handle = graphs.add(graph); + commands.insert_resource(Animation { + animation: node, + graph: graph_handle, + }); + + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y), + StateScoped(CURRENT_SCENE), + )); + + commands.spawn(( + Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)), + DirectionalLight { + shadows_enabled: true, + ..default() + }, + StateScoped(CURRENT_SCENE), + )); + + commands + .spawn(( + SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_PATH))), + StateScoped(CURRENT_SCENE), + )) + .observe(pause_animation_frame); + } + + fn pause_animation_frame( + trigger: Trigger, + children: Query<&Children>, + mut commands: Commands, + animation: Res, + mut players: Query<(Entity, &mut AnimationPlayer)>, + ) { + let entity = children.get(trigger.entity()).unwrap()[0]; + let entity = children.get(entity).unwrap()[0]; + + let (entity, mut player) = players.get_mut(entity).unwrap(); + let mut transitions = AnimationTransitions::new(); + transitions + .play(&mut player, animation.animation, Duration::ZERO) + .seek_to(0.5) + .pause(); + + commands + .entity(entity) + .insert(AnimationGraphHandle(animation.graph.clone())) + .insert(transitions); + } +}