rustgame-developmentbevy

Bevy first-person player model is not visible


I've been trying to make an FPS game with bevy. I ran into an issue on the second day, which is that if I attach the player's arm and weapon model to the camera, then it is not visible. It appears that bevy's renderer is algorithmically making it hidden, even though that should not happen. How can I fix this?

I've tried to force it to be visible using a system in the CheckVisibility set, but that doesn't seem to work either. I'm not sure what I am getting wrong here.

I detached the fps model from the camera to test, and sure enough, it renders fine until I stand close to it, where it is actually supposed to be. Then, it disappears. :-(

Here's my code:

use crate::character_controller::{
    CharacterControllerBundle, CharacterControllerPlugin, DirectionLooker,
};
use bevy::{
    ecs::event::ManualEventReader,
    input::mouse::MouseMotion,
    prelude::*,
    render::view::{check_visibility, VisibilitySystems::CheckVisibility},
    window::{CursorGrabMode, PrimaryWindow},
};
use bevy_xpbd_3d::{math::Scalar, prelude::*};

/// Marker component representing a `Camera` attached to the player
#[derive(Component)]
pub struct FPSCam;

/// Marker component representing the player
#[derive(Component)]
pub struct Player;

/// Marker component representing the player's view model
#[derive(Component)]
pub struct PlayerViewModel;

pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<InputState>()
            .init_resource::<LookSettings>()
            .add_plugins(CharacterControllerPlugin)
            .add_systems(Startup, setup)
            .add_systems(Update, (grab, look))
            .add_systems(
                PostUpdate,
                make_visible.in_set(CheckVisibility).after(check_visibility),
            );
    }
}

#[derive(Resource, Default)]
struct InputState {
    reader_motion: ManualEventReader<MouseMotion>,
}

#[derive(Resource)]
pub struct LookSettings {
    pub sensitivity: f32,
}

impl Default for LookSettings {
    fn default() -> Self {
        Self {
            sensitivity: 0.00006,
        }
    }
}

fn setup(mut commands: Commands, assets: Res<AssetServer>) {
    // player
    let player = commands
        .spawn((
            SpatialBundle {
                transform: Transform::from_xyz(0.0, 1.5, 0.0),
                ..default()
            },
            Player,
            DirectionLooker,
            CharacterControllerBundle::new(Collider::capsule(1.0, 0.4))
                .with_movement(30.0, 0.92, 7.0, (30.0 as Scalar).to_radians()),
            Friction::ZERO.with_combine_rule(CoefficientCombine::Min),
            Restitution::ZERO.with_combine_rule(CoefficientCombine::Min),
            GravityScale(2.0),
        ))
        .id();

    let mut fps_model_transform = Transform::from_xyz(0.0, 0.7, 0.0);
    fps_model_transform.rotate_y(180.0_f32.to_radians());

    let _fps_model = commands
        .spawn((
            SceneBundle {
                scene: assets.load("mp5.glb#Scene0"),
                transform: fps_model_transform,
                ..default()
            },
            PlayerViewModel,
        ))
        .id();

    // camera
    let camera = commands
        .spawn((
            Camera3dBundle {
                projection: PerspectiveProjection {
                    fov: 80.0_f32.to_radians(),
                    near: 0.001,
                    ..default()
                }
                .into(),
                transform: Transform::from_xyz(0.0, 0.5, 0.0),
                ..default()
            },
            FPSCam,
        ))
        .id();
    commands.entity(player).push_children(&[camera]);
}

fn make_visible(mut query: Query<&mut ViewVisibility, With<PlayerViewModel>>) {
    for mut visibility in &mut query {
        visibility.set();
    }
}

fn grab(
    mut windows: Query<&mut Window>,
    keys: Res<Input<KeyCode>>,
    mouse: Res<Input<MouseButton>>,
) {
    let mut window = windows.single_mut();

    if mouse.just_pressed(MouseButton::Right) {
        window.cursor.visible = false;
        window.cursor.grab_mode = CursorGrabMode::Locked;
    } else if keys.just_pressed(KeyCode::Escape) {
        window.cursor.visible = true;
        window.cursor.grab_mode = CursorGrabMode::None;
    }
}

fn look(
    settings: Res<LookSettings>,
    primary_window: Query<&Window, With<PrimaryWindow>>,
    motion: Res<Events<MouseMotion>>,
    mut state: ResMut<InputState>,
    mut player_query: Query<(&mut Transform, With<Player>, Without<FPSCam>)>,
    mut camera_query: Query<(&mut Transform, With<FPSCam>, Without<Player>)>,
) {
    if let Ok(window) = primary_window.get_single() {
        for ev in state.reader_motion.read(&motion) {
            for (mut player_transform, _, _) in player_query.iter_mut() {
                let mut yaw =
                    player_transform.rotation.to_euler(EulerRot::YXZ).0;

                match window.cursor.grab_mode {
                    CursorGrabMode::None => (),
                    _ => {
                        // Using smallest of height or width ensures equal
                        // vertical and horizontal sensitivity
                        let window_scale = window.height().min(window.width());
                        yaw -=
                            (settings.sensitivity * ev.delta.x * window_scale)
                                .to_radians();
                    }
                }

                player_transform.rotation = Quat::from_axis_angle(Vec3::Y, yaw);
            }

            for (mut camera_transform, _, _) in camera_query.iter_mut() {
                let mut pitch =
                    camera_transform.rotation.to_euler(EulerRot::YXZ).1;

                match window.cursor.grab_mode {
                    CursorGrabMode::None => (),
                    _ => {
                        // Using smallest of height or width ensures equal
                        // vertical and horizontal sensitivity
                        let window_scale = window.height().min(window.width());
                        pitch -=
                            (settings.sensitivity * ev.delta.y * window_scale)
                                .to_radians();
                    }
                }

                camera_transform.rotation =
                    Quat::from_axis_angle(Vec3::X, pitch.clamp(-1.54, 1.54));
            }
        }
    } else {
        warn!("Primary window not found!");
    }
}


Solution

  • The reason this was happening was because of Bevy's automatic frustum culling. It can be easily fixed by adding NoFrustumCulling component to the Mesh. However, since I wasn't using a plain Mesh but instead a glTF Scene, I used bevy-scene-hook to apply the NoFrustumCulling component to anything that had a Handle<Mesh>. It now works.

        let fps_model = commands
            .spawn(HookedSceneBundle {
                scene: SceneBundle {
                    scene: assets.load("mp5.glb#Scene0"),
                    transform: fps_model_transform,
                    ..default()
                },
                hook: SceneHook::new(|entity, commands| {
                    if entity.get::<Handle<Mesh>>().is_some() {
                        commands.insert(NoFrustumCulling);
                    }
                }),
            })
            .id();
    

    Of course, make sure to import the appropriate types from bevy_scene_hook crate. Also, you need to add the HookPlugin as well.