From aaa8f708333234bea8ff6877675253fb16c3d37e Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Mon, 14 Jul 2025 17:36:35 +0200 Subject: [PATCH] WIP: Fix glTF model forward direction --- Cargo.toml | 11 ++ crates/bevy_animation/src/animatable.rs | 6 + crates/bevy_gltf/src/loader/gltf_ext/scene.rs | 1 + .../src/components/global_transform.rs | 6 + .../src/components/transform.rs | 46 ++++- examples/3d/motion_blur.rs | 4 +- examples/3d/parallax_mapping.rs | 4 + examples/3d/tonemapping.rs | 4 +- examples/camera/camera_orbit.rs | 2 +- examples/gizmos/axes.rs | 2 + examples/helpers/camera_controller.rs | 2 +- examples/math/custom_primitives.rs | 2 + examples/mobile/src/lib.rs | 2 +- .../stress_tests/many_animated_sprites.rs | 1 + examples/stress_tests/many_sprites.rs | 1 + examples/stress_tests/many_text2d.rs | 1 + examples/transforms/model_forward.rs | 180 ++++++++++++++++++ examples/transforms/transform.rs | 2 +- 18 files changed, 266 insertions(+), 11 deletions(-) create mode 100644 examples/transforms/model_forward.rs diff --git a/Cargo.toml b/Cargo.toml index f047040bdc9f7..f317da2808b46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3278,6 +3278,17 @@ description = "A demonstration of Transform's axis-alignment feature" category = "Transforms" wasm = true +[[example]] +name = "model_forward" +path = "examples/transforms/model_forward.rs" +doc-scrape-examples = true + +[package.metadata.example.model_forward] +name = "model_forward" +description = "Illustrates when to use camera_forward vs model_forward, and different cases for model_forward" +category = "Transforms" +wasm = true + [[example]] name = "scale" path = "examples/transforms/scale.rs" diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index a345c5fce4f0a..4edab47a85aba 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -136,6 +136,7 @@ impl Animatable for Transform { translation: Vec3::interpolate(&a.translation, &b.translation, t), rotation: Quat::interpolate(&a.rotation, &b.rotation, t), scale: Vec3::interpolate(&a.scale, &b.scale, t), + flip_model_forward: a.flip_model_forward, } } @@ -143,8 +144,12 @@ impl Animatable for Transform { let mut translation = Vec3A::ZERO; let mut scale = Vec3A::ZERO; let mut rotation = Quat::IDENTITY; + let mut flip_model_forward = None; for input in inputs { + if flip_model_forward.is_none() { + flip_model_forward = Some(input.value.flip_model_forward); + } if input.additive { translation += input.weight * Vec3A::from(input.value.translation); scale += input.weight * Vec3A::from(input.value.scale); @@ -165,6 +170,7 @@ impl Animatable for Transform { translation: Vec3::from(translation), rotation, scale: Vec3::from(scale), + flip_model_forward: flip_model_forward.unwrap_or_default(), } } } diff --git a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs index 3fce51d52706b..da6e1f1cc9cd6 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs @@ -42,6 +42,7 @@ pub(crate) fn node_transform(node: &Node, convert_coordinates: bool) -> Transfor translation: Vec3::from(translation), rotation: bevy_math::Quat::from_array(rotation), scale: Vec3::from(scale), + flip_model_forward: true, }, }; if convert_coordinates { diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index cd7db6ef71b09..0ec9f4d50a5af 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -136,6 +136,7 @@ impl GlobalTransform { translation, rotation, scale, + ..Default::default() } } @@ -193,6 +194,7 @@ impl GlobalTransform { translation, rotation, scale, + ..Default::default() } } @@ -367,11 +369,13 @@ mod test { translation: Vec3::new(1034.0, 34.0, -1324.34), rotation: Quat::from_euler(XYZ, 1.0, 0.9, 2.1), scale: Vec3::new(1.0, 1.0, 1.0), + ..Default::default() }); let t2 = GlobalTransform::from(Transform { translation: Vec3::new(0.0, -54.493, 324.34), rotation: Quat::from_euler(XYZ, 1.9, 0.3, 3.0), scale: Vec3::new(1.345, 1.345, 1.345), + ..Default::default() }); let retransformed = reparent_to_same(t1, t2); assert!( @@ -387,11 +391,13 @@ mod test { translation: Vec3::new(1034.0, 34.0, -1324.34), rotation: Quat::from_euler(XYZ, 0.8, 1.9, 2.1), scale: Vec3::new(10.9, 10.9, 10.9), + ..Default::default() }); let t2 = GlobalTransform::from(Transform { translation: Vec3::new(28.0, -54.493, 324.34), rotation: Quat::from_euler(XYZ, 0.0, 3.1, 0.1), scale: Vec3::new(0.9, 0.9, 0.9), + ..Default::default() }); // goal: find `X` such as `t2 * X = t1` let reparented = t1.reparented_to(&t2); diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index bc161e9e8c738..fd82c5318940d 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -98,6 +98,11 @@ pub struct Transform { /// /// [`scale`]: https://github.com/bevyengine/bevy/blob/latest/examples/transforms/scale.rs pub scale: Vec3, + /// Whether the model forward direction is flipped from -z to +z. + /// + /// glTF specifies that models have a forward direction of +z whereas cameras and lights have -z. + /// This option allows the glTF importer and other usages to make appropriate adjustments. + pub flip_model_forward: bool, } impl Transform { @@ -106,6 +111,7 @@ impl Transform { translation: Vec3::ZERO, rotation: Quat::IDENTITY, scale: Vec3::ONE, + flip_model_forward: false, }; /// Creates a new [`Transform`] at the position `(x, y, z)`. In 2d, the `z` component @@ -126,6 +132,7 @@ impl Transform { translation, rotation, scale, + flip_model_forward: false, } } @@ -317,16 +324,44 @@ impl Transform { /// Equivalent to [`-local_z()`][Transform::local_z] #[inline] - pub fn forward(&self) -> Dir3 { + pub fn camera_forward(&self) -> Dir3 { -self.local_z() } /// Equivalent to [`local_z()`][Transform::local_z] #[inline] - pub fn back(&self) -> Dir3 { + pub fn camera_back(&self) -> Dir3 { self.local_z() } + /// Equivalent to [`-local_z()`][Transform::local_z] if `flip_model_forward` is false, + /// else [`local_z()`][Transform::local_z] + /// + /// glTF has opposing forward directions for cameras and lights, and for models. Model + /// forward is +z, whereas camera and light forward is -z. + #[inline] + pub fn model_forward(&self) -> Dir3 { + if self.flip_model_forward { + self.local_z() + } else { + -self.local_z() + } + } + + /// Equivalent to [`local_z()`][Transform::local_z] if `flip_model_forward` is false, + /// else [`-local_z()`][Transform::local_z] + /// + /// glTF has opposing forward directions for cameras and lights, and for models. Model + /// forward is +z, whereas camera and light forward is -z. Back is the opposite of this. + #[inline] + pub fn model_back(&self) -> Dir3 { + if self.flip_model_forward { + -self.local_z() + } else { + self.local_z() + } + } + /// Rotates this [`Transform`] by the given rotation. /// /// If this [`Transform`] has a parent, the `rotation` is relative to the rotation of the parent. @@ -469,7 +504,11 @@ impl Transform { /// * if `direction` is parallel with `up`, an orthogonal vector is used as the "right" direction #[inline] pub fn look_to(&mut self, direction: impl TryInto, up: impl TryInto) { - let back = -direction.try_into().unwrap_or(Dir3::NEG_Z); + let back = if self.flip_model_forward { + direction.try_into().unwrap_or(Dir3::NEG_Z) + } else { + -direction.try_into().unwrap_or(Dir3::NEG_Z) + }; let up = up.try_into().unwrap_or(Dir3::Y); let right = up .cross(back.into()) @@ -572,6 +611,7 @@ impl Transform { translation, rotation, scale, + flip_model_forward: self.flip_model_forward, } } diff --git a/examples/3d/motion_blur.rs b/examples/3d/motion_blur.rs index 529ae85499f11..fb31621426676 100644 --- a/examples/3d/motion_blur.rs +++ b/examples/3d/motion_blur.rs @@ -343,8 +343,8 @@ fn move_camera( } CameraMode::Chase => { transform.translation = - tracked.translation + Vec3::new(0.0, 0.15, 0.0) + tracked.back() * 0.6; - transform.look_to(tracked.forward(), Vec3::Y); + tracked.translation + Vec3::new(0.0, 0.15, 0.0) + tracked.camera_back() * 0.6; + transform.look_to(tracked.camera_forward(), Vec3::Y); if let Projection::Perspective(perspective) = &mut *projection { perspective.fov = 1.0; } diff --git a/examples/3d/parallax_mapping.rs b/examples/3d/parallax_mapping.rs index b0dd376691f39..aad1e96a1e917 100644 --- a/examples/3d/parallax_mapping.rs +++ b/examples/3d/parallax_mapping.rs @@ -166,21 +166,25 @@ const CAMERA_POSITIONS: &[Transform] = &[ translation: Vec3::new(1.5, 1.5, 1.5), rotation: Quat::from_xyzw(-0.279, 0.364, 0.115, 0.880), scale: Vec3::ONE, + flip_model_forward: false, }, Transform { translation: Vec3::new(2.4, 0.0, 0.2), rotation: Quat::from_xyzw(0.094, 0.676, 0.116, 0.721), scale: Vec3::ONE, + flip_model_forward: false, }, Transform { translation: Vec3::new(2.4, 2.6, -4.3), rotation: Quat::from_xyzw(0.170, 0.908, 0.308, 0.225), scale: Vec3::ONE, + flip_model_forward: false, }, Transform { translation: Vec3::new(-1.0, 0.8, -1.2), rotation: Quat::from_xyzw(-0.004, 0.909, 0.247, -0.335), scale: Vec3::ONE, + flip_model_forward: false, }, ]; diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 987ceda70da70..a82b786302734 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -139,7 +139,7 @@ fn setup_color_gradient_scene( camera_transform: Res, ) { let mut transform = camera_transform.0; - transform.translation += *transform.forward(); + transform.translation += *transform.camera_forward(); commands.spawn(( Mesh3d(meshes.add(Rectangle::new(0.7, 0.7))), @@ -157,7 +157,7 @@ fn setup_image_viewer_scene( camera_transform: Res, ) { let mut transform = camera_transform.0; - transform.translation += *transform.forward(); + transform.translation += *transform.camera_forward(); // exr/hdr viewer (exr requires enabling bevy feature) commands.spawn(( diff --git a/examples/camera/camera_orbit.rs b/examples/camera/camera_orbit.rs index d2adcf0804dba..2acce2982ff37 100644 --- a/examples/camera/camera_orbit.rs +++ b/examples/camera/camera_orbit.rs @@ -137,5 +137,5 @@ fn orbit( // Adjust the translation to maintain the correct orientation toward the orbit target. // In our example it's a static target, but this could easily be customized. let target = Vec3::ZERO; - camera.translation = target - camera.forward() * camera_settings.orbit_distance; + camera.translation = target - camera.camera_forward() * camera_settings.orbit_distance; } diff --git a/examples/gizmos/axes.rs b/examples/gizmos/axes.rs index d90d498ec9984..e845619973ba4 100644 --- a/examples/gizmos/axes.rs +++ b/examples/gizmos/axes.rs @@ -143,6 +143,7 @@ fn random_transform(rng: &mut impl Rng) -> Transform { translation: random_translation(rng), rotation: random_rotation(rng), scale: random_scale(rng), + ..default() } } @@ -216,5 +217,6 @@ fn interpolate_transforms(t1: Transform, t2: Transform, t: f32) -> Transform { translation, rotation, scale, + flip_model_forward: t1.flip_model_forward, } } diff --git a/examples/helpers/camera_controller.rs b/examples/helpers/camera_controller.rs index 3f6e4ed477487..d5353a6c167d3 100644 --- a/examples/helpers/camera_controller.rs +++ b/examples/helpers/camera_controller.rs @@ -216,7 +216,7 @@ fn run_camera_controller( // Apply movement update if controller.velocity != Vec3::ZERO { - let forward = *transform.forward(); + let forward = *transform.camera_forward(); let right = *transform.right(); transform.translation += controller.velocity.x * dt * right + controller.velocity.y * dt * Vec3::Y diff --git a/examples/math/custom_primitives.rs b/examples/math/custom_primitives.rs index cf6d0d5aaffaa..3ed9ab0d2cc12 100644 --- a/examples/math/custom_primitives.rs +++ b/examples/math/custom_primitives.rs @@ -31,6 +31,7 @@ const TRANSFORM_2D: Transform = Transform { translation: Vec3::ZERO, rotation: Quat::IDENTITY, scale: Vec3::ONE, + flip_model_forward: false, }; // The projection used for the camera in 2D const PROJECTION_2D: Projection = Projection::Orthographic(OrthographicProjection { @@ -54,6 +55,7 @@ const TRANSFORM_3D: Transform = Transform { // The camera is pointing at the 3D shape rotation: Quat::from_xyzw(-0.14521316, -0.0, -0.0, 0.98940045), scale: Vec3::ONE, + flip_model_forward: false, }; // The projection used for the camera in 3D const PROJECTION_3D: Projection = Projection::Perspective(PerspectiveProjection { diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index de831525528d8..54c4abbc1d762 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -87,7 +87,7 @@ fn touch_camera( } // Rotation gestures only work on iOS for rotation in rotations.read() { - let forward = camera_transform.forward(); + let forward = camera_transform.camera_forward(); camera_transform.rotate_axis(forward, rotation.0 / 10.0); } } diff --git a/examples/stress_tests/many_animated_sprites.rs b/examples/stress_tests/many_animated_sprites.rs index e34a03195ae27..5422734fd7daa 100644 --- a/examples/stress_tests/many_animated_sprites.rs +++ b/examples/stress_tests/many_animated_sprites.rs @@ -92,6 +92,7 @@ fn setup( translation, rotation, scale, + ..default() }, AnimationTimer(timer), )); diff --git a/examples/stress_tests/many_sprites.rs b/examples/stress_tests/many_sprites.rs index 5bf65efb2d357..47264f6ccf091 100644 --- a/examples/stress_tests/many_sprites.rs +++ b/examples/stress_tests/many_sprites.rs @@ -96,6 +96,7 @@ fn setup(mut commands: Commands, assets: Res, color_tint: Res, args: Res) { translation, rotation, scale, + ..default() }, )); } diff --git a/examples/transforms/model_forward.rs b/examples/transforms/model_forward.rs new file mode 100644 index 0000000000000..4f9bf84d153f1 --- /dev/null +++ b/examples/transforms/model_forward.rs @@ -0,0 +1,180 @@ +//! Shows the difference between Transform camera forward and model forward. + +use std::f32::consts::PI; + +use bevy::{color::palettes::basic::YELLOW, prelude::*}; + +// A struct for additional data of for a moving cube. +#[derive(Component)] +struct OrbitState { + start_pos: Vec3, + move_speed: f32, + turn_speed: f32, +} + +// A struct adding information to a scalable entity, +// that will be stationary at the center of the scene. +#[derive(Component)] +struct Center { + max_size: f32, + min_size: f32, + scale_factor: f32, +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + move_orbiters, + rotate_orbiters, + scale_down_sphere_proportional_to_cube_travel_distance, + ) + .chain(), + ) + .run(); +} + +#[derive(Component)] +struct Helmet; + +// Startup system to setup the scene and spawn all relevant entities. +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, +) { + // Add an object (sphere) for visualizing scaling. + commands.spawn(( + Mesh3d(meshes.add(Sphere::new(3.0).mesh().ico(32).unwrap())), + MeshMaterial3d(materials.add(Color::from(YELLOW))), + Transform::from_translation(Vec3::ZERO), + Center { + max_size: 1.0, + min_size: 0.1, + scale_factor: 0.05, + }, + )); + + // Add the cube to visualize rotation and translation. + // This cube will circle around the center_sphere + // by changing its rotation each frame and moving forward. + // Define a start transform for an orbiting cube, that's away from our central object (sphere) + // and rotate it so it will be able to move around the sphere and not towards it. + let cube_spawn = + Transform::from_translation(Vec3::Z * -10.0).with_rotation(Quat::from_rotation_y(PI / 2.)); + commands.spawn(( + Mesh3d(meshes.add(Cuboid::default())), + MeshMaterial3d(materials.add(Color::WHITE)), + cube_spawn, + OrbitState { + start_pos: cube_spawn.translation, + move_speed: 2.0, + turn_speed: 0.2, + }, + )); + + // Spawn a camera looking at the entities to show what's happening in this example. + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 10.0, 20.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + // Add a light source for better 3d visibility. + commands.spawn(( + DirectionalLight::default(), + Transform::from_xyz(3.0, 3.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + // Add the helmet to visualize rotation and translation using the glTF model forward convention of +z. + // This helmet will circle around the center_sphere + // by changing its rotation each frame and moving forward along the model_forward direction. + // Define a start transform for an orbiting helmet, that's away from our central object (sphere) + // and rotate it so it will be able to move around the sphere and not towards it. + // Note that it orbits in the opposite direction to the cube due to the model_forward directions being + // flipped. + let mut helmet_spawn = Transform::from_translation(Vec3::Z * -10.0) + .with_rotation(Quat::from_rotation_y(PI / 2.)) + .with_scale(Vec3::splat(2.0)); + helmet_spawn.flip_model_forward = true; + commands.spawn(( + helmet_spawn, + Visibility::default(), + OrbitState { + start_pos: helmet_spawn.translation, + move_speed: 2.0, + turn_speed: 0.2, + }, + SceneRoot( + asset_server + .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")), + ), + )); +} + +// This system will move the orbiter forward. +fn move_orbiters(mut orbiters: Query<(&mut Transform, &mut OrbitState)>, timer: Res