🛩️

Pythonで創る!究極の3D航空機シミュレーター

に公開

Pythonで創る!!究極の3D航空機シミュレーター:高精度A320モデルと没入型カメラワークで空を舞う[非AIエンジニアによるAI時代のコーディング]

はじめに

この記事では、Pythonを駆使し、エアバス式A320型機を模擬し飛行を極限までリアルにシミュレーションする、究極の3D航空機シミュレーターの構築について解説します。シミュレーターのメインループを制御するスクリプト、A320型機の詳細モデルを定義するスクリプト、そして没入感を極限まで高めるカメラワークを実装するスクリプト、これら3つのスクリプトを軸に、航空機の運動力学、3Dモデルの詳細な作成、そしてダイナミックな視点変化の演出まで、航空シミュレーションの核心に迫ります。

1. シミュレーターの全貌:3つのPythonスクリプトの協奏

シミュレーターの中心となるPythonスクリプトは、Pythonによる3Dシーンの創造、別のPythonスクリプトで精密に設計されたA320型機のモデルの配置、キーボードという指先から伝わるパイロットの意志を機体の姿勢制御へと変換、そしてカメラシステムを制御するPythonスクリプトが生み出すダイナミックな視点変化の演出、これら全てを統合し、一つの壮大な飛行体験へと昇華させます。

今回生成したスクリプトファイル

  • シミュレーションを司るmain関数
  • 3D Modelの基本的なデータ群
  • 座標系と視点を管理するカメラ関数

2. Pythonの魔法:3D空間を創り出す

Pythonは、3Dオブジェクトを簡単に作成し、アニメーションを制御するための強力なライブラリ「vpython」の力も借りて、3D空間に無限の可能性を秘めたキャンバスを広げます。メインスクリプトはこの力を使い、航空機が舞う舞台を構築し、A320型機を定義するスクリプトはこの舞台上で息を吹き込まれる航空機の姿を、カメラシステムを制御するスクリプトはこの舞台を様々な角度から捉え、物語を紡ぎ出すカメラワークを実現します。

3. A320型機の設計図:詳細モデルの定義

A320型機を定義するPythonスクリプトには、A320Model クラスという名の設計士が、機体の形状、大きさ、そして細部に至るまで、その全てを記述しています。このクラスは、A320型機の各部位を、現実の設計図に基づいて極めて精密にモデル化します。

3.1. 機体の基本構造

A320Model クラスは、胴体(Fuselage)、翼(Wings)、尾翼(Tail)、エンジン(Engines)、着陸装置(LandingGear)といった主要な構成要素を定義します。それぞれの要素は、さらに細かなパーツに分割され、階層構造を形成します。

def __init__(self):
    """A320モデルの初期化
    座標系:
    - X軸:機首方向が正(前方)
    - Y軸:右翼方向が正(右方)
    - Z軸:上方向が正(上方)
    単位:メートル
    原点:機首
    """
    print("Starting A320Model initialization...")  # デバッグ用
    
    # 基本サイズの設定
    self.length = Fuselage.LENGTH  # 全長
    self.wingspan = Wings.SPAN  # 翼幅
    self.height = Fuselage.HEIGHT  # 全高
    self.fuselage_diameter = Fuselage.DIAMETER  # 胴体直径
    
    print(f"Basic dimensions set: length={self.length}, wingspan={self.wingspan}, height={self.height}")
    
    # 重心位置の設定(機首からの距離)
    self.cg_position = self.length * Fuselage.CG_POSITION
    self.center_of_gravity = vector(self.cg_position, 0, 0)
    
    print(f"CG position set: {self.center_of_gravity}")
    
    # パーツの階層構造を初期化
    self.parts = {
        'fuselage': {
            'main': None,
            'cockpit': None,
            'tail_cone': None
        },
        'wings': {
            'right': {
                'assembly': None,
                'components': {
                    'main': None,
                    'controls': {
                        'low_speed_aileron': None,
                        'flap': None,
                        'slat': None,
                        'spoiler': None
                    }
                }
            },
            'left': { ... }
        },
        'tail': {
            'vertical': {
                'assembly': None,  # 垂直尾翼と制御面のcompound
                'components': {
                    'main': None,
                    'controls': {
                        'rudder': None
                    }
                }
            },
            'horizontal': {
                'assembly': None,  # 水平尾翼と制御面のcompound
                'components': {
                    'main': {
                        'right': None,
                        'left': None
                    },
                    'controls': {
                        'elevator_right': None,
                        'elevator_left': None
                    }
                }
            }
        },
        'engines': { ... },
        'landing_gear': { ... }
    }

3.2. 胴体の作成

胴体は、円筒や円錐といった基本的な3D形状を組み合わせることで生成されます。create_fuselage メソッドでは、胴体を複数の断面に分割し、それぞれの断面の位置や半径を計算することで、滑らかな形状を実現しています。

    def create_fuselage(self):
        """胴体の作成(機首位置を基準に)"""
        print("  Calculating fuselage dimensions...")  # デバッグ用
        
        # 胴体各部の長さを計算
        nose_length = self.length * Fuselage.NOSE_SECTION_RATIO  # コックピット部分
        main_length = self.length * Fuselage.MAIN_SECTION_RATIO  # 主胴体部分
        tail_length = self.length * Fuselage.TAIL_CONE_RATIO    # テールコーン部分
        
        print(f"  Section lengths: nose={nose_length}, main={main_length}, tail={tail_length}")  # デバッグ用
        
        # コックピット部分(先細りの円錐形状)
        print("  Creating cockpit sections...")  # デバッグ用
        cockpit_sections = 32  # 分割数
        cockpit_section_length = nose_length / cockpit_sections
        cockpit_parts = []
        
        for i in range(cockpit_sections):
            section_ratio = i / (cockpit_sections - 1)  # 0.0 から 1.0
            # 直径を徐々に大きくする(前から後ろに向かって)
            min_radius = self.fuselage_diameter * 0.05  # 最小半径を胴体直径の5%に設定
            section_radius = min_radius + (self.fuselage_diameter/2 - min_radius) * section_ratio
            
            # コックピットの位置(機首を基準に)
            section_x = i * cockpit_section_length
            
            if i == 0:  # デバッグ用
                print(f"  First cockpit section: pos=({section_x}, 0, 0), radius={section_radius}")
            
            section = cylinder(
                pos=vector(section_x, 0, 0),
                axis=vector(cockpit_section_length, 0, 0),  # X軸方向(前)
                radius=section_radius,
                up=vector(0, 0, 1),  # Z軸方向(上)
                color=color.gray(0.8)  # 淡いグレー
            )
            cockpit_parts.append(section)
        
        self.parts['fuselage']['cockpit'] = compound(cockpit_parts)
        print("  Cockpit created")  # デバッグ用
        
        # 主胴体部分(円筒形状)
        print("  Creating main fuselage...")  # デバッグ用
        main_fuselage_x = nose_length  # 機首から主胴体開始位置まで
        self.parts['fuselage']['main'] = cylinder(
            pos=vector(main_fuselage_x, 0, 0),
            axis=vector(main_length, 0, 0),  # X軸方向(前)
            radius=self.fuselage_diameter/2,
            up=vector(0, 0, 1),  # Z軸方向(上)
            color=color.gray(0.8)  # 淡いグレー
        )
        print(f"  Main fuselage created at: ({main_fuselage_x}, 0, 0)")  # デバッグ用
        
        # テールコーン部分(後ろに向かって先細り)
        print("  Creating tail cone sections...")  # デバッグ用
        tail_sections = 64  # 分割数を64に維持
        tail_section_length = tail_length / tail_sections
        tail_cone_parts = []  # 一時的なパーツリスト
        
        for i in range(tail_sections):
            section_ratio = i / (tail_sections - 1)  # 0.0 から 1.0
            # 直径を徐々に小さくする(前から後ろに向かって)
            min_radius = self.fuselage_diameter * 0.05  # 最小半径を設定
            section_radius = max(min_radius, (self.fuselage_diameter/2) * (1 - section_ratio))
            
            # テールコーンの位置(機首を基準に)
            section_x = nose_length + main_length + (i * tail_section_length)
            # 上方向へのオフセットを追加(後方に行くほど上方向にシフト)
            section_z_offset = section_ratio * self.fuselage_diameter * 0.25  # 最大で胴体直径の1/4まで上方にシフト
            
            section = cylinder(
                pos=vector(section_x, 0, section_z_offset),
                axis=vector(tail_section_length, 0, 0),  # X軸方向(前)
                radius=section_radius,
                up=vector(0, 0, 1),  # Z軸方向(上)
                color=color.gray(0.5)  # より暗いグレー
            )
            tail_cone_parts.append(section)
            if i % 16 == 0:  # 16セクションごとに進捗を表示
                print(f"  Created tail section {i}/{tail_sections}")
        
        # テールコーンをcompoundオブジェクトとして作成
        print(f"  Creating tail cone compound from {len(tail_cone_parts)} parts")
        self.parts['fuselage']['tail_cone'] = compound(tail_cone_parts)
        print("  Tail cone created")
        print("  Moving to next component...")

3.3. 翼の作成

翼は、より複雑な形状を持つため、細かなセクションに分割して生成されます。create_wings メソッドでは、翼の形状を定義するパラメータ(翼弦長、テーパー比、後退角、上反角など)に基づいて、各セクションの位置や大きさを計算します。

def create_wings(self):
    """主翼の作成"""
    # ...
    section = box(
        pos=vector(wing_x + sweep_offset, section_pos_y, wing_z + dihedral_height),
        size=vector((local_chord + next_chord)/2, section_span, wing_thickness),
        axis=vector(1, 0, 0),
        up=vector(0, 0, 1),
        color=color.gray(0.8)
    )
    # ...

3.4. 制御翼の作成

エルロン、エレベーター、ラダーといった制御翼も、翼と同様にセクションに分割して生成されます。これらの翼は、機体の姿勢を制御するために、シミュレーション中に回転させることができます。

def create_control_surfaces(self):
    """制御面の作成"""
    # ...
    section = box(
        pos=vector(
            wing_x + sweep_offset,
            section_y,
            wing_z + dihedral_offset
        ),
        size=vector(
            Wings.ROOT_CHORD * ControlSurfaces.LOW_SPEED_AILERON_CHORD,
            section_span,
            Wings.ROOT_CHORD * ControlSurfaces.LOW_SPEED_AILERON_THICKNESS
        ),
        axis=vector(1, 0, 0),
        up=vector(0, 0, 1),
        color=color.gray(0.7)
    )
    # ...

4. 航空力学と数式の舞:姿勢制御の裏側

メインスクリプトでパイロットの操作を受け、A320型機を定義するPythonスクリプトで定義された機体を操るためには、航空機の運動を支配する力学と、それを表現する数式が不可欠です。ロール、ピッチ、ヨーという3つの回転を、クォータニオンという数学的なツールを用いて計算することで、シミュレーターは現実世界の飛行を忠実に再現します。

4.1. 姿勢の表現:ロール、ピッチ、ヨー

航空機の姿勢は、ロール(Roll)、ピッチ(Pitch)、ヨー(Yaw)という3つの角度で表現されます。

  • ロール: 機体の進行方向を軸とした回転(バンク)
  • ピッチ: 機体の左右方向を軸とした回転(機首上げ・下げ)
  • ヨー: 機体の上下方向を軸とした回転(機首方向の変化)

これらの角度を組み合わせることで、3次元空間内での機体の姿勢を完全に記述できます。

4.2. クォータニオンによる回転計算

A320Model クラスの update_attitude メソッドでは、これらの回転をクォータニオンを用いて計算しています。クォータニオンは、回転軸と回転角を用いて回転を表現する方法であり、オイラー角を用いる場合に発生するジンバルロックという問題を回避できます。

def quaternion_from_axis_angle(axis, angle):
    """回転軸と角度からクォータニオンを生成"""
    s = sin(angle/2)
    return [
        cos(angle/2),
        axis.x * s,
        axis.y * s,
        axis.z * s
    ]

def rotate_vector_by_quaternion(v, q):
    """ベクトルをクォータニオンで回転"""
    w, x, y, z = q
    return vector(
        (1 - 2*y*y - 2*z*z)*v.x + (2*x*y - 2*w*z)*v.y + (2*x*z + 2*w*y)*v.z,
        (2*x*y + 2*w*z)*v.x + (1 - 2*x*x - 2*z*z)*v.y + (2*y*z - 2*w*x)*v.z,
        (2*x*z - 2*w*y)*v.x + (2*y*z + 2*w*x)*v.y + (1 - 2*x*x - 2*y*y)*v.z
    )

def update_attitude(self, roll=None, pitch=None, yaw=None):
    """姿勢を更新する"""
    if roll is not None:
        self.roll = roll
    if pitch is not None:
        self.pitch = pitch
    if yaw is not None:
        self.yaw = yaw

    # 重心位置を取得
    cg = self.cg_position

    # 全パーツを重心分だけ原点方向に移動
    for part in self.all_parts:
        part.pos = part.pos - vector(cg, 0, 0)

    # 回転を適用(既存の回転処理)
    for part in self.all_parts:
        # 初期状態からの回転を計算
        initial_pos = self.initial_states[part]['pos']
        initial_axis = self.initial_states[part]['axis']
        initial_up = self.initial_states[part]['up']
            
        # 重心を原点とした位置での回転
        rotated_pos = self._rotate_vector(initial_pos - vector(cg, 0, 0), self.roll, self.pitch, self.yaw)
        rotated_axis = self._rotate_vector(initial_axis, self.roll, self.pitch, self.yaw)
        rotated_up = self._rotate_vector(initial_up, self.roll, self.pitch, self.yaw)
            
        # パーツの位置と向きを更新
        part.pos = rotated_pos + vector(cg, 0, 0)  # 重心分を戻す
        part.axis = rotated_axis
        part.up = rotated_up

4.3. 機体座標系の計算

カメラシステムの制御スクリプト (camera_system.py) では、機体の姿勢に基づいてカメラの位置や向きを計算するために、機体の座標系を算出しています。この座標系は、機体の進行方向(forward)、右方向(right)、上方向(up)を表す3つのベクトルから構成されます。

# 機体の座標系を計算
forward = vector(cos(yaw)*cos(pitch),
              sin(yaw)*cos(pitch),
              sin(pitch))

right = vector(cos(yaw)*sin(pitch)*sin(roll) - sin(yaw)*cos(roll),
             sin(yaw)*sin(pitch)*sin(roll) + cos(yaw)*cos(roll),
             -cos(pitch)*sin(roll))

up = vector(cos(yaw)*sin(pitch)*cos(roll) + sin(yaw)*sin(roll),
           sin(yaw)*sin(pitch)*cos(roll) - cos(yaw)*sin(roll),
           cos(pitch)*cos(roll))

5. 操縦翼の息吹:エルロン、エレベーター、ラダー

エルロン、エレベーター、ラダー。これらの操縦翼は、パイロットの意志を機体に伝えるインターフェースです。メインPythonスクリプトは、キーボードからの入力という形でパイロットの指示を受け取り、A320型機を定義するPythonスクリプトは、これらの翼が機体の姿勢に与える影響を計算し、Pythonがそれを3D空間に描き出します。

5.1. 操縦翼の定義

A320型機を定義するPythonスクリプトでは、エルロン、エレベーター、ラダーといった操縦翼が、機体モデルの一部として定義されています。これらの翼は、create_control_surfaces メソッドで生成され、それぞれ特定の軸を中心に回転するように設定されています。

def create_control_surfaces(self):
    """制御面の作成"""
    # ...
    # 左エルロンのセクションを作成
    left_aileron_sections = []
    for i in range(num_aileron_sections):
        # ...
        section = box(
            # ...
        )
        left_aileron_sections.append(section)
    # ...

5.2. 操縦翼の制御

メインスクリプトの main 関数では、キーボード入力に応じて操縦翼の角度を変化させ、update_control_surfaces メソッドを呼び出してその結果を機体モデルに反映させています。

# 制御面の入力値を初期化
control_inputs = {
    'low_speed_aileron': 0.0,
    'elevator': 0.0,
    'rudder': 0.0
}

# エルロンの制御(左右矢印キー)
if 'left' in current_keys: control_inputs['low_speed_aileron'] = 1.0
if 'right' in current_keys: control_inputs['low_speed_aileron'] = -1.0

# エレベーターの制御(上下矢印キー)
if 'up' in current_keys: control_inputs['elevator'] = 1.0
if 'down' in current_keys: control_inputs['elevator'] = -1.0

# ラダーの制御(,と.キー)
if ',' in current_keys: control_inputs['rudder'] = -1.0
if '.' in current_keys: control_inputs['rudder'] = 1.0

# 制御面を更新
aircraft.update_control_surfaces(control_inputs)

5.3. 操縦翼の回転

update_control_surfaces メソッドでは、入力された角度に基づいて操縦翼を回転させます。この回転は、vpython の rotate メソッドを用いて行われます。

def update_control_surfaces(self, control_inputs):
    """コントロールサーフェスの更新"""
    # ...
    # 左エルロンの回転
    self.parts['wings']['left']['components']['controls']['low_speed_aileron'].rotate(
        angle=left_aileron_deflection,
        axis=aileron_axis,
        origin=self.parts['wings']['left']['components']['controls']['low_speed_aileron'].pos
    )
    # ...

6. シミュレーション結果:ダイナミックな飛行体験

シミュレーションを実行すると、キーボードの操作に応じて、A320型機が3次元空間内でダイナミックに動き回る様子がリアルタイムに表示されます。ロール、ピッチ、ヨーの滑らかな変化、そしてエルロン、エレベーター、ラダーといった操縦翼の動きが、視覚的に鮮やかに再現されます。コックピットからの臨場感あふれる眺め、機体を追尾するダイナミックなカメラワーク、そして管制塔から全体を見渡す戦略的な視点。これらの要素が融合することで、シミュレーターは、まるで本物のA320型機を操縦しているかのような、没入感あふれる飛行体験を提供します。

7. 創造の翼を広げる:シミュレーションの未来

シミュレーターを構成する3つのPythonスクリプトは、創造の翼を広げるための出発点です。より高度な航空力学モデルの導入、自動操縦システムの開発、気象条件の再現、そしてVR技術との融合。シミュレーションの世界は、あなたのアイデアと情熱によって、どこまでも進化し続けるでしょう。

まとめ

この記事は、3つのPythonスクリプトが織りなす、究極の3D航空機シミュレーター構築の物語です。Pythonが描く3D空間、A320型機を定義するPythonスクリプトが語る航空機の設計、そしてカメラシステムを制御するPythonスクリプトが操る視点の魔術。これらの要素が生成AI自身が融合することで、私たちは空を飛ぶという人類の夢を、限りなく現実に近い形で、極めて簡単に体験できるようになるのです。
AIとの会話だけでここまでのプログラムを生成できる、そんな今回の開発が、皆さんの思い描く生成AIの可能性をさらに広げたのではないでしょうか。
皆さんのアイディアもAIの力を借りて現実世界に昇華させてみましょう。

Discussion