Skip to content
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

Horizontally Squeezed Rendering in split screen #6367

Closed
terhechte opened this issue Oct 25, 2022 · 4 comments
Closed

Horizontally Squeezed Rendering in split screen #6367

terhechte opened this issue Oct 25, 2022 · 4 comments
Labels
A-Rendering Drawing game state to the screen C-Bug An unexpected or incorrect behavior

Comments

@terhechte
Copy link

terhechte commented Oct 25, 2022

Bevy version

Bevy Main (7989cb2)

[Optional] Relevant system information

M1 MacBook Pro (macOS 12.6):

AdapterInfo { name: "Apple M1 Max", vendor: 0, device: 0, device_type: IntegratedGpu, driver: "", driver_info: "", backend: Metal }

What you did

I am working on split screen game with 1-4 players. I'm seeing weird squeezed rendering in 2 player mode (where the screen is split vertically in the middle into higher than wider blocks) but not in 4 player mode (where the screen is split into wider than higher blocks in the exact same size of the window). Here's a screenshot of what the weird rendering looks like:

Screenshot 2022-10-23 at 16 00 12

Similarly, here's the same screen with 4 players

Screenshot 2022-10-23 at 16 00 22

As soon as I re-size the 2-player window, the artefact disappears and everything looks correct.

I tried to re-implement the gist of what I'm doing in a minimal project, but I can't replicate the issue there. I will share this minimal-working-version nonetheless to give an overview of what my code does.

I tried a variety of things, including manually setting the camera sizes, resetting cameras, changing the order of systems etc, to no avail. One difference between my minimal project and the actual project is that the actual project:

  • Initially installs another camera in a menu screen (and then removes it)
  • Loads meshes from the assets
  • Caches some meshes.

My gut feeling is that somehow the initial camera (that is the size of the window) caches some attributes and then the new split screen cameras (that are of a different size) have some wrong attributes cached until I resize the window and the cache is reset. But I can't replicate this behaviour and don't know enough about bevy to understand where to look.

Would love some help. Maybe I need to reset something somewhere that I don't know about? Thanks in advance!

The Minimal (but working) code:

use bevy::core_pipeline::clear_color::ClearColorConfig;
use bevy::ecs::schedule::ShouldRun;
use bevy::render::camera::Viewport;
use bevy::window::{close_on_esc, WindowId, WindowResized};
use bevy::{log::LogSettings, prelude::*};

const SIZE: f32 = 0.25;
const PLAYER_Y: f32 = 4.5;

#[derive(Component)]
struct PlayerCamera(usize);

#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)]
enum GameState {
    Loading,
    Running,
}

impl GameState {
    fn is_running(state: Res<State<GameState>>) -> ShouldRun {
        (state.current() == &GameState::Running).into()
    }
}

fn main() {
    App::new()
        .insert_resource(LogSettings {
            filter: "info,wgpu_core=warn,wgpu_hal=warn,pacbomber=debug".into(),
            level: bevy::log::Level::DEBUG,
        })
        .insert_resource(ClearColor(Color::rgb(20. / 255., 20. / 255., 20. / 255.)))
        .insert_resource(WindowDescriptor {
            title: "Test".to_string(),
            width: 900.,
            height: 660.,
            resizable: true,
            ..default()
        })
        .add_plugins(DefaultPlugins)
        // .add_plugin(LogDiagnosticsPlugin::default())
        // .add_plugin(FrameTimeDiagnosticsPlugin::default())
        .add_system_set(SystemSet::on_enter(GameState::Loading).with_system(build_level_system))
        .add_system_set(SystemSet::on_enter(GameState::Running).with_system(set_camera_viewports))
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(GameState::is_running)
                .with_system(keyboard_system)
                .with_system(move_camera_system)
                .with_system(close_on_esc)
                .with_system(detect_resize_events),
        )
        .add_state(GameState::Loading)
        .run();
}

#[derive(Component)]
struct Player;

fn build_level_system(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut state: ResMut<State<GameState>>,
) {
    info!("build level");
    // 4 players
    for player in 0..=1 {
        let clear_color = if player == 0 {
            ClearColorConfig::Default
        } else {
            ClearColorConfig::None
        };
        commands
            .spawn(Camera3dBundle {
                transform: Transform::from_xyz(0.0, PLAYER_Y, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
                camera: Camera {
                    // Renders the right camera after the left camera, which has a default priority of 0
                    priority: (player as isize),
                    ..default()
                },
                camera_3d: Camera3d {
                    clear_color,
                    ..Default::default()
                },
                ..default()
            })
            .insert(PlayerCamera(player));
    }

    let player = meshes.add(Mesh::from(shape::Cube { size: SIZE }));
    let player_material = materials.add(StandardMaterial {
        base_color: Color::YELLOW,
        metallic: 0.5,
        reflectance: 0.15,
        ..Default::default()
    });

    commands
        .spawn(PbrBundle {
            mesh: player,
            material: player_material,
            transform: Transform::from_xyz(10. * SIZE, 0.5, 10. * SIZE),
            ..Default::default()
        })
        .insert(Player);

    let floor = meshes.add(Mesh::from(shape::Plane { size: SIZE - 0.01 }));
    let floor_material = materials.add(StandardMaterial {
        base_color: Color::RED,
        metallic: 0.5,
        reflectance: 0.15,
        perceptual_roughness: 0.5,
        ..Default::default()
    });

    let offset = SIZE * -10.;
    let (mut x, mut z) = (offset, offset);
    for zc in 0..=128 {
        z += SIZE;
        for xc in 0..=128 {
            x += SIZE;
            commands.spawn(PbrBundle {
                mesh: floor.clone(),
                material: floor_material.clone(),
                transform: Transform::from_xyz(x, -0.1, z),
                ..Default::default()
            });
            // every corner a light
            match (zc, xc) {
                (1, 1) => spawn_light(&mut commands, x, z),
                (1, 127) => spawn_light(&mut commands, x, z),
                (127, 127) => spawn_light(&mut commands, x, z),
                (127, 1) => spawn_light(&mut commands, x, z),
                (64, 64) => spawn_light(&mut commands, x, z),
                (64, 1) => spawn_light(&mut commands, x, z),
                (1, 64) => spawn_light(&mut commands, x, z),
                _ => (),
            }
        }
        x = offset;
    }
    state.overwrite_replace(GameState::Running).unwrap();
}

fn spawn_light(commands: &mut Commands, x: f32, z: f32) {
    commands.spawn(PointLightBundle {
        point_light: PointLight {
            intensity: 350.0,
            shadows_enabled: true,
            ..default()
        },
        transform: Transform::from_xyz(x, 1.0, z),
        ..default()
    });
}

fn keyboard_system(
    keyboard_input: Res<Input<KeyCode>>,
    mut player: Query<&mut Transform, With<Player>>,
) {
    let speed = 0.15;
    for (code, (d_x, d_z)) in [
        (KeyCode::Left, (-1, 0)),
        (KeyCode::Right, (1, 0)),
        (KeyCode::Up, (0, -1)),
        (KeyCode::Down, (0, 1)),
    ] {
        if keyboard_input.pressed(code) || keyboard_input.just_pressed(code) {
            let (x, z) = ((d_x as f32) * speed, (d_z as f32) * speed);
            for mut transform in player.iter_mut() {
                transform.translation.x += x;
                transform.translation.z += z;
            }
        }
    }
}

fn move_camera_system(
    player_query: Query<&Transform, (With<Player>, Without<PlayerCamera>)>,
    mut camera_query: Query<(&Camera, &mut Transform), With<PlayerCamera>>,
) {
    for transform in player_query.iter() {
        let player_pos = transform.translation;
        for (_, mut transform) in camera_query.iter_mut() {
            *transform =
                Transform::from_xyz(player_pos.x, PLAYER_Y - player_pos.y, player_pos.z + 3.0)
                    .looking_at(player_pos, Vec3::Y);
        }
    }
}

fn detect_resize_events(
    windows: Res<Windows>,
    mut resize_events: EventReader<WindowResized>,
    camera_query: Query<(&mut Camera, &PlayerCamera)>,
) {
    for resize_event in resize_events.iter() {
        if resize_event.id == WindowId::primary() {
            info!("resize event");
            set_camera_viewports(windows, camera_query);
            break;
        }
    }
}

fn set_camera_viewports(
    windows: Res<Windows>,
    mut camera_query: Query<(&mut Camera, &PlayerCamera)>,
) {
    info!("set viewport");
    let window = windows.primary();
    let (w, h) = (window.physical_width(), window.physical_height());

    fn pad(x: u32, y: u32, w: u32, h: u32) -> (u32, u32, u32, u32) {
        let padding = 4;
        (x + padding, y + padding, w - padding * 2, h - padding * 2)
    }

    let stupid_map: &[&[(u32, u32, u32, u32)]] = &[
        // Easy mode: One player
        &[pad(0, 0, w, h)],
        // Two Players
        &[pad(0, 0, w / 2, h), pad(w / 2, 0, w / 2, h)],
        // Three players
        &[
            pad(0, 0, w / 2, h / 2),
            pad(w / 2, 0, w / 2, h / 2),
            pad(0, h / 2, w / 2, h / 2),
        ],
        // Four players
        &[
            pad(0, 0, w / 2, h / 2),
            pad(w / 2, 0, w / 2, h / 2),
            pad(0, h / 2, w / 2, h / 2),
            pad(w / 2, h / 2, w / 2, h / 2),
        ],
    ];

    let total = camera_query.iter().count();
    for (mut camera, player_camera) in camera_query.iter_mut() {
        let (x, y, w, h) = stupid_map[total - 1][player_camera.0];
        camera.viewport = Some(Viewport {
            physical_position: UVec2::new(x, y),
            physical_size: UVec2::new(w, h),
            ..default()
        });
    }
}
@terhechte terhechte added C-Bug An unexpected or incorrect behavior S-Needs-Triage This issue needs to be labelled labels Oct 25, 2022
@terhechte
Copy link
Author

I just found a solution. If I already set the viewport during camera initialisation, then the bug doesn't appear:

camera: Camera {
                    priority: (index as isize),
                    viewport: calculate_viewport(index),
                    ..default()
                },

So it seems the bug is that creating a camera without a viewport and then setting the viewport at a later time (e.g. a couple of frames after) is not applied correctly.

@rparrett rparrett added A-Rendering Drawing game state to the screen and removed S-Needs-Triage This issue needs to be labelled labels Oct 25, 2022
@rparrett
Copy link
Contributor

I think this may be a duplicate of #5944 and a fix just worked its way through CI. Could you test with the latest commit?

@james7132
Copy link
Member

@terhechte gentle ping to test this with the latest main. #5944 has been closed.

@terhechte
Copy link
Author

Oh yeah, sorry, I forgot about this issue. This is working fine now. Thanks for reminding me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen C-Bug An unexpected or incorrect behavior
Projects
None yet
Development

No branches or pull requests

3 participants