🦀

chip10. 3Dをviewport内に描画する

2023/08/20に公開

はじめに

2023/08/20時点の内容です。

  • rustc 1.71.1
  • bevy 0.11.2
    bevyは開発初期段階のOSSで、まだまだ破壊的なアップデートが入ります。
    でも、面白いですよ。
    0.11の2回目のバグフィックス版(0.11.2) がリリースれてました。

前回

chip9. フルスクリーン⇔ウィンドウ切替

viewportって何?

ざっくり言えば、ウィンドウ内で部分的に描画するよう制限する機能?
これが
こうなる‥‥わかりにくい?
bevy::render::camera::Camera構造体のフィールドにpub viewport: Option<Viewport>,があり、デフォルトではNoneで、その場合はウィンドウ内側をフルフル使って描画されます。
しかしbevy::render::camera::Viewport構造体を使って矩形のエリアを設定すると、描画をその内側に限定することができます。
viewportを設定するときの注意点は、矩形を指定する際の座標系です。ウィンドウ左上隅が原点座標でY軸が下向きになります。
https://docs.rs/bevy/0.11.2/bevy/render/camera/struct.Viewport.html

3Dをviewport内に描画する

今回のキモは3Dカメラ作成時にviewportを設定することですが、その変更がソースコードのあっちこっちに飛び火しました。
bevyの座標系は地味に面倒ですね。
それともしかしたらTextBundleあたりにバグが潜在してるかもしれません。(今回TextBundleの使用をやめてText2dBundleへ変更しました)

main.rs
//external crates
use bevy::
{   prelude::*,
    render::*, render::settings::*, render::camera::*,
    core_pipeline::clear_color::*,
    input::mouse::*,
    sprite::*,
    window::WindowMode::*,
};

//standard library
use std::f32::consts::*;

//internal submodules
mod spawn_objs;
mod const_defs;
use const_defs::*;
mod catch_input;

//------------------------------------------------------------------------------

fn main()
{   //Note:手元の環境だとVulkanのままでは影が描画されなかったので、DX12へ切り替えた。
    let backends = Some ( Backends::DX12 );
    let wgpu_settings = WgpuSettings { backends, ..default() };
    let backend_dx12 = RenderPlugin { wgpu_settings };

    App::new()
        //DefaultPluginsに各種の面倒を見てもらう
        .add_plugins
        (   DefaultPlugins
                //Note:この行をコメントアウトするとデフォルトのbackend
                .set( backend_dx12 )
        )

        //各種オブジェクトを作成する
        .add_systems
        (   Startup, 
            (   spawn_objs::camera3d_and_light, //3Dカメラとライト
                spawn_objs::locked_chest,       //3Dオブジェクト(宝箱)
                spawn_objs::camera2d,           //2Dカメラ(情報表示用)
                spawn_objs::display_board,      //UIテキスト(情報表示用)
            )
        )

        //メインルーチンを登録する
        .add_systems
        (   Update,
            (   (   (   catch_input::from_keyboard, //極座標を更新(キー入力)
                        catch_input::from_mouse,    //極座標を更新(マウス)
                        catch_input::from_gamepad,  //極座標を更新(ゲームパッド)
                    ),
                    move_orbit_camera,              //極座標カメラを移動
                )
                .chain(), //実行順を固定

                bevy::window::close_on_esc, //[ESC]キーで終了
                toggle_window_mode,         //ウィンドウとフルスクリーンの切換
                show_parameter,             //情報を表示
                show_gizmos,                //ギズモの表示
            )
        )

        //アプリを実行する
        .run();
}

//------------------------------------------------------------------------------

//極座標の型
#[derive( Clone, Copy )]
struct Orbit
{   r    : f32, //極座標のr(注目点からカメラまでの距離)
    theta: f32, //極座標のΘ(注目点から見たカメラの垂直角度)
    phi  : f32, //極座標のφ(注目点から見たカメラの水平角度)
}

//極座標から直交座標へ変換するメソッド
impl Orbit
{   fn into_vec3( self ) -> Vec3
    {   let x = self.r * self.theta.sin() * self.phi.sin();
        let y = self.r * self.theta.cos() * -1.0;
        let z = self.r * self.theta.sin() * self.phi.cos();
        Vec3::new( x, y, z )
    }
}

//------------------------------------------------------------------------------

//極座標カメラに付けるComponent
#[derive( Component )]
pub struct OrbitCamera { orbit: Orbit }

//極座標カメラの初期位置
impl Default for OrbitCamera
{   fn default() -> Self
    {   Self
        {   orbit: Orbit
            {   r    : ORBIT_CAMERA_INIT_R,
                theta: ORBIT_CAMERA_INIT_THETA,
                phi  : ORBIT_CAMERA_INIT_PHI,
            }
        }
    }
}

//UIテキストに付けるComponent
#[derive( Component )]
struct DisplayBoard;

//------------------------------------------------------------------------------

//ギズモを使って枠を表示
fn show_gizmos( mut gizmos: Gizmos )
{   gizmos.rect_2d
    (   Vec2::ZERO,    //position
        0.0,           //rotation
        VIEWPORT_SIZE, //size
        Color::YELLOW, //color
    );
}

//------------------------------------------------------------------------------

//ウィンドウとフルスクリーンの切換(トグル動作)
pub fn toggle_window_mode
(   mut q_window: Query<&mut Window>,
    inkey: Res<Input<KeyCode>>,
    inbtn: Res<Input<GamepadButton>>,
    gamepads: Res<Gamepads>,
)
{   let Ok( mut window ) = q_window.get_single_mut() else { return };

    //[Alt]+[Enter]キーの状態
    let is_key_pressed =
        ( inkey.pressed( KeyCode::AltRight ) || inkey.pressed( KeyCode::AltLeft ) )
            && inkey.just_pressed( KeyCode::Return );

    //ゲームパッドは抜き挿しでIDが変わるので.iter()で回す
    let button_type = GamepadButtonType::Select; //ps4[SHARE]
    let mut is_gp_button_pressed = false;
    for gamepad in gamepads.iter()
    {   let button = GamepadButton { gamepad, button_type };
        is_gp_button_pressed = inbtn.just_pressed( button );
        if is_gp_button_pressed { break }
    }

    //入力がないなら
    if ! is_key_pressed && ! is_gp_button_pressed { return }

    //ウィンドウとフルスクリーンを切り替える
    window.mode = match window.mode
    {   Windowed => SizedFullscreen, //or BorderlessFullscreen, Fullscreen
        _        => Windowed,
    };
}

//------------------------------------------------------------------------------

//極座標カメラを動かす
fn move_orbit_camera
(   mut q_camera: Query<( &OrbitCamera, &mut Transform )>,
)
{   let Ok ( ( camera, mut transform ) ) = q_camera.get_single_mut() else { return };

    //カメラの位置と向きを更新する
    let translation = camera.orbit.into_vec3();
    *transform = Transform::from_translation( translation )
        .looking_at( Vec3::ZERO, Vec3::Y );
}

//------------------------------------------------------------------------------

//極座標の情報を表示する
fn show_parameter
(   mut q_text: Query<&mut Text, With<DisplayBoard>>,
    q_camera: Query<&OrbitCamera>,
    q_window: Query<&Window>,
    gamepads: Res<Gamepads>,
)
{   let Ok ( mut text ) = q_text.get_single_mut() else { return };
    let Ok ( camera ) = q_camera.get_single() else { return };
    let orbit = &camera.orbit;
    let Ok( window ) = q_window.get_single() else { return };

    //極座標の情報
    let r     = orbit.r;
    let theta = orbit.theta.to_degrees(); //ラジアンから度へ変換
    let phi   = orbit.phi.to_degrees();   //ラジアンから度へ変換
    let info  = format!( " r:{r:3.02}\n theta:{theta:06.02}\n phi:{phi:06.02}" );

    //ウィンドウの解像度の情報
    let whs = format!
    (   "\n width:{}\n height:{}\n scale:{}",
        window.width(),
        window.height(),
        window.scale_factor(),
    );

    //ゲームパッドの接続状態
    let mut pads = "\n Gamepads:".to_string();
    for gamepad in gamepads.iter()
    {   let Some ( name ) = gamepads.name( gamepad ) else { continue };
        pads = format!( "{pads}\n - ID:{} {}", gamepad.id, name );
    }

    //表示の更新
    text.sections[ 0 ].value = format!( "{info}{whs}{pads}" );
}
const_defs.rs
use super::*;

//2Dカメラの画像を3Dカメラの画像の上にのせる(レンダリングの順位)
pub const CAMERA2D_ORDER: isize = 1;
pub const CAMERA3D_ORDER: isize = 0;

//2Dカメラの画像の背景を透過させる
pub const CAMERA2D_BGCOLOR: ClearColorConfig = ClearColorConfig::None;

//光源
pub const LIGHT_BRIGHTNESS: f32 = 15000.0; //明るさ
pub const LIGHT_POSITION: Vec3 = Vec3::new( 30.0, 100.0, 40.0 ); //位置

//UIテキスト
pub const UI_TEXT_FONT_SIZE: f32 = 50.0;

//極座標カメラの設定値
pub const ORBIT_CAMERA_INIT_R    : f32 = 3.0;      //初期値
pub const ORBIT_CAMERA_INIT_THETA: f32 = PI * 0.7; //初期値(ラジアン)
pub const ORBIT_CAMERA_INIT_PHI  : f32 = 0.0;      //初期値(ラジアン)

pub const ORBIT_CAMERA_MAX_R    : f32 = 5.0;       //最大値
pub const ORBIT_CAMERA_MIN_R    : f32 = 1.0;       //最小値
pub const ORBIT_CAMERA_MAX_THETA: f32 = PI * 0.99; //最大値(ラジアン)
pub const ORBIT_CAMERA_MIN_THETA: f32 = PI * 0.51; //最小値(ラジアン)

//マウスからの入力値の感度調整用係数
pub const MOUSE_WHEEL_Y_COEF : f32 = 0.1;
pub const MOUSE_MOTION_Y_COEF: f32 = 0.01;
pub const MOUSE_MOTION_X_COEF: f32 = 0.01;

//viewportの設定値(表示エリアの矩形)
pub const VIEWPORT_WIDTH : f32  = 600.0;
pub const VIEWPORT_HEIGHT: f32  = 600.0;
pub const VIEWPORT_SIZE  : Vec2 = Vec2::new( VIEWPORT_WIDTH, VIEWPORT_HEIGHT );
spawn_objs.rs
use super::*;

//3Dカメラと光源を作る
pub fn camera3d_and_light
(   q_window: Query<&Window>,
    mut cmds: Commands,
)
{   //viewportの設定値(表示エリアの矩形)を作る
    let Ok( window ) = q_window.get_single() else { return };
    let x = ( window.width()  - VIEWPORT_WIDTH  ) as u32 / 2; //表示エリアの左上X座標
    let y = ( window.height() - VIEWPORT_HEIGHT ) as u32 / 2; //表示エリアの左上Y座標
    let physical_position = UVec2 { x, y };
    let physical_size = VIEWPORT_SIZE.as_uvec2();
    let viewport = Some ( Viewport { physical_position, physical_size, ..default() } );

    //3Dカメラ
    let orbit_camera = OrbitCamera::default();
    let vec3 = orbit_camera.orbit.into_vec3();
    cmds.spawn( ( Camera3dBundle::default(), orbit_camera ) )
        .insert( Camera { order: CAMERA3D_ORDER, viewport, ..default() } )
        .insert
        (   Transform::from_translation( vec3 )    //カメラの位置
                .looking_at( Vec3::ZERO, Vec3::Y ) //カメラレンズの向き
        );

    //光源
    let light = DirectionalLight
    {   illuminance: LIGHT_BRIGHTNESS,
        shadows_enabled: true, //影の描画を有効化
        ..default()
    };
    cmds.spawn( DirectionalLightBundle::default() )
        .insert( light )
        .insert
        (   Transform::from_translation( LIGHT_POSITION ) //光源の位置
                .looking_at( Vec3::ZERO, Vec3::Z )        //光源の向き
        );
}

//------------------------------------------------------------------------------

//2Dカメラを作る
pub fn camera2d( mut cmds: Commands )
{   cmds.spawn( Camera2dBundle::default() )
        .insert( Camera { order: CAMERA2D_ORDER, ..default() } )
        .insert( Camera2d { clear_color: CAMERA2D_BGCOLOR } );
}

//UIテキストを作る
pub fn display_board
(   q_window: Query<&Window>,
    mut cmds: Commands,
)
{   let textstyle = TextStyle { font_size: UI_TEXT_FONT_SIZE, ..default() };
    let text = Text::from_section( "", textstyle ); //placeholderのみ

    //Cameraにviewportを設定したらテキスト表示がバグったので(変に拡大して二重表示された)、
    //TextBundleの使用をやめてText2dBundleへ変更した。(機能的にやりたいことは実現できる)
    //Text2dBundleは座標原点がウィンドウ中央になるのでテキストを左上に寄せるるため、
    //transformとtext_anchorを追加設定した。
    let Ok( window ) = q_window.get_single() else { return };
    let translation = Vec3::new( window.width() / -2.0, window.height() / 2.0, 0.0 );
    let transform = Transform::from_translation( translation );
    let text_anchor = Anchor::TopLeft;

    //Text2dBundleを作る
    cmds.spawn( ( Text2dBundle { text, transform, text_anchor, ..default() }, DisplayBoard ) );
}

//------------------------------------------------------------------------------

//3Dオブジェクトを作る(宝箱)
pub fn locked_chest
(   mut cmds: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
)
{   //地面
    cmds.spawn( PbrBundle::default() )
        .insert( meshes.add( shape::Plane::from_size( 2.0 ).into() ) )
        .insert( Transform::from_translation( Vec3::ZERO ) )
        .insert( materials.add( Color::rgb( 0.5, 0.7, 0.3 ).into() ) );

    //宝箱
    cmds.spawn( PbrBundle::default() )
        .insert( materials.add( Color::NONE.into() ) ) //透明
        .insert( Transform::from_translation( Vec3::new( 0.0, 0.5, 0.0 ) ) )
        .with_children
        (   | cmds |
            {   //本体
                let shape_box = shape::Box::new( 0.7, 0.3, 0.4 );
                cmds.spawn( PbrBundle::default() )
                    .insert( meshes.add( shape_box.into() ) )
                    .insert( Transform::from_translation( Vec3::Y * -0.35 ) )
                    .insert( materials.add( Color::MAROON.into() ) );

                //上蓋
                let shape_cylinder = shape::Cylinder { height: 0.695, radius: 0.195, ..default() };
                cmds.spawn( PbrBundle::default() )
                    .insert( meshes.add( shape_cylinder.into() ) )
                    .insert
                    (   Transform::from_translation( Vec3::Y * -0.2 )
                            .with_rotation( Quat::from_rotation_z( PI * 0.5 ) )
                    )
                    .insert( materials.add( Color::MAROON.into() ) );

                //錠前
                let shape_cube = shape::Cube::new( 0.1 );
                cmds.spawn( PbrBundle::default() )
                    .insert( meshes.add( shape_cube.into() ) )
                    .insert( Transform::from_translation( Vec3::Y * -0.2 + Vec3::Z * 0.17 ) )
                    .insert( materials.add( Color::GRAY.into() ) )
                    .with_children
                    (   | cmds |
                        {   //鍵穴
                            let cylinder = shape::Cylinder { height: 0.11, radius: 0.01, ..default() };
                            cmds.spawn( PbrBundle::default() )
                                .insert( meshes.add( cylinder.into() ) )
                                .insert
                                (   Transform::from_translation( Vec3::Y * 0.02 )
                                        .with_rotation( Quat::from_rotation_x( PI * 0.5 ) )
                                )
                                .insert( materials.add( Color::BLACK.into() ) );

                            let shape_box = shape::Box::new( 0.01, 0.04, 0.11 );
                            cmds.spawn( PbrBundle::default() )
                                .insert( meshes.add( shape_box.into() ) )
                                .insert( Transform::from_translation( Vec3::Y * 0.0 ) )
                                .insert( materials.add( Color::BLACK.into() ) );
                        }
                    );
            }
        );
}
catch_input.rs
catch_input.rs
use super::*;

//ゲームパッドによって極座標カメラの位置を更新する
pub fn from_gamepad
(   mut q_camera: Query<&mut OrbitCamera>,
    time: Res<Time>,
    axis_button: Res<Axis<GamepadButton>>,
    axis_stick : Res<Axis<GamepadAxis>>,
    gamepads: Res<Gamepads>,
)
{   let Ok ( mut camera ) = q_camera.get_single_mut() else { return };
    let orbit = &mut camera.orbit;

    let time_delta = time.delta().as_secs_f32(); //前回の実行からの経過時間

    //ゲームパッドは抜き挿しでIDが変わるので.iter()で回す
    for gamepad in gamepads.iter()
    {   //左トリガーでズームイン
        let button_type = GamepadButtonType::LeftTrigger2;
        let button = GamepadButton { gamepad, button_type };
        if let Some ( value ) = axis_button.get( button )
        {   orbit.r -= value * time_delta;
            orbit.r = orbit.r.max( ORBIT_CAMERA_MIN_R );
        }

        //右トリガーでズームアウト
        let button_type = GamepadButtonType::RightTrigger2; 
        let button = GamepadButton { gamepad, button_type };
        if let Some ( value ) = axis_button.get( button )
        {   orbit.r += value * time_delta;
            orbit.r = orbit.r.min( ORBIT_CAMERA_MAX_R );
        }

        //左スティックのY軸で上下首振り
        let axis_type = GamepadAxisType::LeftStickY;
        let stick_y = GamepadAxis { gamepad, axis_type };
        if let Some ( value ) = axis_stick.get( stick_y )
        {   orbit.theta += value * time_delta;
            orbit.theta = orbit.theta
                .min( ORBIT_CAMERA_MAX_THETA )
                .max( ORBIT_CAMERA_MIN_THETA );
        }

        //左スティックのX軸で左右回転
        let axis_type = GamepadAxisType::LeftStickX;
        let stick_x = GamepadAxis { gamepad, axis_type };
        if let Some ( value ) = axis_stick.get( stick_x )
        {   orbit.phi -= value * time_delta;
            orbit.phi -= if orbit.phi >= TAU { TAU } else { 0.0 };
            orbit.phi += if orbit.phi <  0.0 { TAU } else { 0.0 };
        }
    }
}

//------------------------------------------------------------------------------

//マウス入力によって極座標カメラの位置を更新する
pub fn from_mouse
(   mut q_camera: Query<&mut OrbitCamera>,
    mouse_nutton: Res<Input<MouseButton>>,
    mut e_mouse_motion: EventReader<MouseMotion>,
    mut e_mouse_wheel: EventReader<MouseWheel>,
)
{   let Ok ( mut camera ) = q_camera.get_single_mut() else { return };
    let orbit = &mut camera.orbit;

    //ホイール
    for mouse_wheel in e_mouse_wheel.iter()
    {   orbit.r += mouse_wheel.y * MOUSE_WHEEL_Y_COEF; //感度良すぎるので
        orbit.r = orbit.r
            .min( ORBIT_CAMERA_MAX_R )
            .max( ORBIT_CAMERA_MIN_R );
    }

    //右ボタンが押されていないなら
    if ! mouse_nutton.pressed( MouseButton::Left ) { return }

    //マウスの上下左右
    for mouse_motion in e_mouse_motion.iter()
    {   //上下首振り
        orbit.theta += mouse_motion.delta.y * MOUSE_MOTION_Y_COEF; //感度良すぎるので
        orbit.theta = orbit.theta
            .min( ORBIT_CAMERA_MAX_THETA )
            .max( ORBIT_CAMERA_MIN_THETA );

        //左右回転
        orbit.phi -= mouse_motion.delta.x * MOUSE_MOTION_X_COEF; //感度良すぎるので
        orbit.phi -= if orbit.phi >= TAU { TAU } else { 0.0 };
        orbit.phi += if orbit.phi <  0.0 { TAU } else { 0.0 };
    }
}

//------------------------------------------------------------------------------

//キー入力によって極座標カメラの位置を更新する
pub fn from_keyboard
(   mut q_camera: Query<&mut OrbitCamera>,
    time: Res<Time>,
    inkey: Res<Input<KeyCode>>,
)
{   let Ok ( mut camera ) = q_camera.get_single_mut() else { return };
    let orbit = &mut camera.orbit;

    let time_delta = time.delta().as_secs_f32(); //前回の実行からの経過時間

    for keycode in inkey.get_pressed()
    {   match keycode
        {   KeyCode::Z =>
                orbit.r = ( orbit.r + time_delta ).min( ORBIT_CAMERA_MAX_R ),
            KeyCode::X =>
                orbit.r = ( orbit.r - time_delta ).max( ORBIT_CAMERA_MIN_R ),
            KeyCode::Up =>
                orbit.theta = ( orbit.theta + time_delta ).min( ORBIT_CAMERA_MAX_THETA ),
            KeyCode::Down =>
                orbit.theta = ( orbit.theta - time_delta ).max( ORBIT_CAMERA_MIN_THETA ),
            KeyCode::Right =>
            {   orbit.phi -= time_delta;
                orbit.phi += if orbit.phi < 0.0 { TAU } else { 0.0 };
            }
            KeyCode::Left =>
            {   orbit.phi += time_delta;
                orbit.phi -= if orbit.phi >= TAU { TAU } else { 0.0 };
            }
            _ => (),
        }
    }
}

Discussion