🦀

chip6. 3Dオブジェクト「宝箱」を表示する

2023/08/02に公開

はじめに

2024/09/29時点の内容です。

  • rustc 1.81.0
  • bevy 0.14.2
    bevyは開発初期段階のOSSで、まだまだ破壊的なアップデートが入ります。
    でも、面白いですよ。

前回

chip5. bevyのプリミティブ・シェイプ

3Dオブジェクト「宝箱」を表示する

基本図形の組み合わせでそれっぽく見せるのはなかなか大変。

宝箱‥‥に見える?
main.rsと同じディレクトリにspawn_chest3d.rsを置いてください。
都合によりモジュール分けたので‥‥ (^_^;) 。

main.rs
//external crates
use bevy::
{   prelude::*,
    log::LogPlugin,
    color::palettes::*,
};

//standard library
use std::f32::consts::{ TAU, PI }; //360°、180°

//submodules
mod spawn_chest3d;
use spawn_chest3d::*;

//メイン関数
fn main() -> AppExit
{   App::new()
        .add_plugins
        (   DefaultPlugins //アプリの基本的な面倒を見てもらう
                .set( LogPlugin { filter: "error".into(), ..default() } ),
        )
        .add_systems( Startup, spawn_chest3d )    //3Dオブジェクトを作る
        .add_systems( Update, move_orbit_camera ) //極座標カメラを動かす
        .run()
}

//極座標の型
#[derive( Clone, Copy )]
struct Orbit
{   r    : f32, //中心とカメラの距離
    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 )]
struct OrbitCamera { orbit: Orbit }

impl Default for OrbitCamera
{   fn default() -> Self
    {   Self { orbit: Orbit { r: 2.5, theta: PI * 0.75, phi: 0.0 } }
    }
}

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

    //前回からの経過時間により角度を進める
    let time_delta = time.delta().as_secs_f32();
    let angle_delta = TAU * time_delta * 0.2;

    //等速円運動
    camera.orbit.phi += angle_delta;
    camera.orbit.phi -= if camera.orbit.phi > TAU { TAU } else { 0.0 };

    //カメラの位置と姿勢を更新する
    let camera_vec3 = camera.orbit.into_vec3();
    *transform = Transform::from_translation( camera_vec3 ).looking_at( Vec3::ZERO, Vec3::Y );
}
spawn_chest3d.rs
use super::*;

//3Dオブジェクトを作る
pub fn spawn_chest3d
(   mut cmds: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
)
{   //3Dカメラとライトをspawnする
    let camera = OrbitCamera::default();
    let camera_vec3 = camera.orbit.into_vec3();
    cmds.spawn( ( Camera3dBundle::default(), camera ) )
        .insert( Transform::from_translation( camera_vec3 ).looking_at( Vec3::ZERO, Vec3::Y ) );

    let light_vec3 = Vec3::new( 30.0, 100.0, 40.0 ); //ライトの位置
    let illuminance = 3000.0; //ライトの明るさ
    let shadows_enabled = true; //影の描画アリ
    cmds.spawn( DirectionalLightBundle::default() )
        .insert( DirectionalLight { illuminance, shadows_enabled, ..default() } )
        .insert( Transform::from_translation( light_vec3 ).looking_at( Vec3::ZERO, Vec3::Y ) );

    //地面
    cmds.spawn( PbrBundle::default() )
        .insert( meshes.add( Plane3d::new( Vec3::Y, Vec2::splat( 1.0 ) ) ) )
        .insert( Transform::from_translation( Vec3::ZERO ) )
        .insert( materials.add( Color::Srgba( css::YELLOW_GREEN ) ) );

    //宝箱
    cmds.spawn( PbrBundle::default() ) //透明な親を作る
        .insert( materials.add( Color::NONE ) )
        .insert( Transform::from_translation( Vec3::Y * 0.5 ) )
        .with_children //子の中に鍵付き宝箱を作る
        (   | cmds |
            {   //本体
                cmds.spawn( PbrBundle::default() )
                    .insert( meshes.add( Cuboid::new( 0.7, 0.3, 0.4 ) ) )
                    .insert( Transform::from_translation( Vec3::Y * -0.35 ) )
                    .insert( materials.add( Color::Srgba ( css::MAROON ) ) );

                //上蓋
                let z90 = Quat::from_rotation_z( PI * 0.5 );
                cmds.spawn( PbrBundle::default() )
                    .insert( meshes.add( Cylinder::new( 0.195, 0.695 ) ) )
                    .insert( Transform::from_translation( Vec3::Y * -0.2 ).with_rotation( z90 ) )
                    .insert( materials.add( Color::Srgba ( css::MAROON ) ) );

                //錠前
                cmds.spawn( PbrBundle::default() )
                    .insert( meshes.add( Cuboid::from_length( 0.1 ) ) )
                    .insert( Transform::from_translation( Vec3::Y * -0.2 + Vec3::Z * 0.17 ) )
                    .insert( materials.add( Color::Srgba ( css::GRAY ) ) )
                    .with_children
                    (   | cmds |
                        {   //鍵穴
                            let x90 = Quat::from_rotation_x( PI * 0.5 );
                            cmds.spawn( PbrBundle::default() )
                                .insert( meshes.add( Cylinder::new( 0.01, 0.11 ) ) )
                                .insert( Transform::from_translation( Vec3::Y * 0.02 ).with_rotation( x90 ) )
                                .insert( materials.add( Color::BLACK ) );
                            cmds.spawn( PbrBundle::default() )
                                .insert( meshes.add( Cuboid::new( 0.01, 0.04, 0.11 ) ) )
                                .insert( Transform::from_translation( Vec3::ZERO ) )
                                .insert( materials.add( Color::BLACK ) );
                        }
                    );
            }
        );
}

極座標カメラ

人工衛星が地球にカメラを向けながら軌道を回るように、今回は宝箱にカメラを向けながら周回させています。コツは3つ:

  • カメラの位置を極座標で管理する
  • 極座標から直交座標へ変換して、カメラの位置をセットする
  • カメラの姿勢を.looking_at()を使って宝箱へ向ける

極座標は、地球に例えると緯度経度そして地球の半径ですね。
数学は門外漢ですがネットで調べると親切なサイトが色々教えてくれます。
3つの要素を.sin().cos()でアレヤコレヤすると直交座標へ変換できるんですね。

  • r:球体の半径
  • Θ:球体の中心から見て、垂直方向の角度(0°~180°)
  • φ:球体の中心から見て、水平方向の角度(0°~360°)

なおライトにも姿勢(光の向き)があるので、カメラと同様にspawnの際に.looking_at()を使っています。

Discussion