惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
C
Check Point Blog
月光博客
月光博客
Y
Y Combinator Blog
I
InfoQ
B
Blog RSS Feed
P
Proofpoint News Feed
腾讯CDC
博客园 - Franky
MyScale Blog
MyScale Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
云风的 BLOG
云风的 BLOG
罗磊的独立博客
B
Blog
人人都是产品经理
人人都是产品经理
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
Recent Announcements
Recent Announcements
美团技术团队
大猫的无限游戏
大猫的无限游戏

博客园 - longyue

[BF学院_卷1] -- 第30讲:mixTable() 总结 —— Mixer 完整控制模型 [BF学院_卷1] -- 第29讲:applyMixToMotors() —— 最终 Motor Output 生成 [BF学院_卷1] -- 第28讲:applyMixerAdjustment() —— 为什么 Mixer 需要重新分配控制权限 [BF学院_卷1] -- 第27讲:PID Output 如何进入 Mixer —— 从控制需求到 motorMix [BF学院_卷1] -- 第26讲:calculateThrottleAndCurrentMotorEndpoints() —— Mixer 如何确定油门基础范围 [BF学院_卷1] -- 第25讲:mixTable() 总览 —— Mixer 在控制循环中的位置 [BF学院_卷1] -- 第24讲:mixer_init.c —— 飞控启动时如何准备 Mixer [BF学院_卷1] -- 第23讲:Mixer 数学模型 —— motorMixer_t 如何形成 Motor Output [BF学院_卷1] -- 第22讲:Betaflight 如何描述 Mixer —— mixer.h 数据结构 [BF学院_卷1] -- 第21讲:四旋翼为什么可以通过电机控制姿态 —— Mixer 背后的物理基础 [BF学院_卷1] -- 第20讲:从 PID Output 到 Motor Command(电机指令)——为什么飞控需要 Mixer [BF学院_卷1] -- 第19讲:PID Controller(PID控制器)——飞控到底如何理解误差并控制飞机? [BF学院_卷1] -- 第18讲:PID Controller 的最后一环——为什么飞控还需要 Feedforward(前馈控制)? [BF学院_卷1] -- 第17讲:D Term(微分控制项)——飞控如何提前感知运动变化? [BF学院_卷1] -- 第16讲:I Term(积分控制项)——飞机为什么总是差一点? [BF学院_卷1] -- 第14讲:Rate Error(角速度误差)——PID 真正控制的到底是什么? [BF学院_卷1] -- 第15讲:P Term(比例控制项)——飞控是如何利用误差开始控制飞机的? [BF学院_卷1] -- 第13讲:Outer Loop 的终点——为什么还要读取 Current Angular Rate(当前角速度)? [BF学院_卷1] -- 第十二讲:Yaw Compensation(偏航补偿) [BF学院_卷1] -- 第十一讲:Current Angular Rate(当前角速度)——飞控怎样知道飞机现在转得有多快? [BF学院_卷1] -- 第十讲:Angle Error(姿态角误差)如何变成 Desired Angular Rate(目标角速度) [BF学院_卷1] -- 第九讲:Current Angle(当前姿态角)遇见 Target Angle(目标姿态角) [BF学院_卷1] -- 第八讲:飞手打杆以后,飞控首先得到什么? [BF学院_卷1] -- 第七讲:为什么 Pitch Flip 只在 180° 看起来正确 [BF学院_卷1] -- 第7讲脚本 [BF学院_卷1] -- 第6讲:为什么 Pitch Flip 会骗人 [BF学院_卷1] -- 第6讲脚本 [BF学院_卷1] -- 第五讲:第一次修正——Pitch Flip 为什么看起来有效 [BF学院_卷1] -- 第5讲脚本 - longyue [BF学院_卷1] -- 第4讲:为什么"绕飞机自己转"不等于"朝世界转" - longyue
[BF学院_卷1] -- 第12讲脚本
longyue · 2026-08-03 · via 博客园 - longyue
import numpy as np
import matplotlib.pyplot as plt

# ==========================================================
# Script03
#
# Earth Z Projection onto Body X
#
# Goal:
# Explain why:
#
# Projection
#
# =
#
# sin(Pitch)
#
# (Geometry only. No math proof here.)
# ==========================================================

# ==========================================================
# Aircraft Attitude
# ==========================================================

roll_deg = 30
pitch_deg = 45
yaw_deg = 20

roll = np.deg2rad(roll_deg)
pitch = np.deg2rad(pitch_deg)
yaw = np.deg2rad(yaw_deg)

# ==========================================================
# Earth Coordinate
# ==========================================================

earth_x = np.array([1.0, 0.0, 0.0])
earth_y = np.array([0.0, 1.0, 0.0])
earth_z = np.array([0.0, 0.0, 1.0])

# ==========================================================
# Rotation Matrix
#
# Positive Pitch
#
# =
#
# Nose Up
# ==========================================================

Rx = np.array([
    [1, 0, 0],
    [0, np.cos(roll), -np.sin(roll)],
    [0, np.sin(roll),  np.cos(roll)]
])

Ry = np.array([
    [ np.cos(pitch), 0, -np.sin(pitch)],
    [0,              1, 0],
    [ np.sin(pitch), 0,  np.cos(pitch)]
])

Rz = np.array([
    [ np.cos(yaw), -np.sin(yaw), 0],
    [ np.sin(yaw),  np.cos(yaw), 0],
    [0,             0,           1]
])

R = Rz @ Ry @ Rx

# ==========================================================
# Body Coordinate
# ==========================================================

body_x = R @ earth_x
body_y = R @ earth_y
body_z = R @ earth_z

# ==========================================================
# Aircraft
# ==========================================================

nose = body_x
tail = -body_x

# ==========================================================
# Figure Layout
# ==========================================================

fig = plt.figure(
    figsize=(14,7)
)

# ==========================================================
# Reality View
# ==========================================================

ax_real = plt.subplot2grid(
    (1,2),
    (0,0),
    projection='3d'
)

# ==========================================================
# Body Reference View
# ==========================================================

ax_body = plt.subplot2grid(
    (1,2),
    (0,1),
    projection='3d'
)

# ==========================================================
# Ground Plane
# ==========================================================

xx, yy = np.meshgrid(
    np.linspace(-1.2,1.2,10),
    np.linspace(-1.2,1.2,10)
)

zz = np.zeros_like(xx)

for ax in [ax_real, ax_body]:

    ax.plot_surface(
        xx,
        yy,
        zz,

        color='lightgray',
        alpha=0.12,
        edgecolor='none'
    )

# ==========================================================
# Axis Drawing Helper
# ==========================================================

def draw_axis(
        ax,
        vec,
        color,
        label,

        lw=2,
        alpha=1.0,
        ls='-'
):

    ax.plot(
        [0, vec[0]],
        [0, vec[1]],
        [0, vec[2]],

        color=color,
        linewidth=lw,
        alpha=alpha,
        linestyle=ls
    )

    ax.text(
        vec[0]*1.06,
        vec[1]*1.06,
        vec[2]*1.06,

        label,

        fontsize=9,

        color=color,
        alpha=alpha
    )
    
# ==========================================================
# Reality View
# ==========================================================

# ----------------------------------------------------------
# Earth Coordinate (Background)
# ----------------------------------------------------------

draw_axis(
    ax_real,
    earth_x,
    "red",
    "Earth X",

    lw=1.5,
    ls="--"
)

draw_axis(
    ax_real,
    earth_y,
    "green",
    "Earth Y",

    lw=1.5,
    ls="--"
)

draw_axis(
    ax_real,
    earth_z,
    "blue",
    "Earth Z",

    lw=1.5,
    ls="--"
)

# ----------------------------------------------------------
# Body Coordinate
# ----------------------------------------------------------

draw_axis(
    ax_real,
    body_x,
    "cyan",
    "Body X",

    lw=2.5
)

draw_axis(
    ax_real,
    body_y,
    "gray",
    "Body Y",

    lw=2.5
)

draw_axis(
    ax_real,
    body_z,
    "purple",
    "Body Z",

    lw=2.5
)

# ==========================================================
# Projection of Body X onto Earth XY Plane
# ==========================================================

proj_exy = np.array([
    body_x[0],
    body_x[1],
    0.0
])

# Projection Vector

ax_real.plot(
    [0, proj_exy[0]],
    [0, proj_exy[1]],
    [0, proj_exy[2]],
    color="orange",
    linewidth=2
)

ax_real.scatter(
    proj_exy[0],
    proj_exy[1],
    proj_exy[2],
    color="orange",
    s=40
)

ax_real.text(
    proj_exy[0],
    proj_exy[1],
    proj_exy[2]-0.05,
    "ProjEXY",
    color="orange",
    fontsize=9
)

# Vertical Line

ax_real.plot(
    [body_x[0], proj_exy[0]],
    [body_x[1], proj_exy[1]],
    [body_x[2], proj_exy[2]],
    "--",
    color="gray",
    linewidth=1.5
)

# ==========================================================
# Pitch Arc (Body X -> ProjEXY)
# ==========================================================

pitch_arc_radius = 0.25

# Body X 单位向量
v1 = body_x / np.linalg.norm(body_x)

# ProjEXY 单位向量
v2 = proj_exy / np.linalg.norm(proj_exy)

# 圆弧采样
theta = np.linspace(
    0,
    pitch,
    80
)

arc = []

for t in theta:

    vec = (
        np.sin(pitch - t) * v2 +
        np.sin(t) * v1
    ) / np.sin(pitch)

    vec = vec / np.linalg.norm(vec)

    arc.append(
        pitch_arc_radius * vec
    )

arc = np.array(arc)

ax_real.plot(
    arc[:,0],
    arc[:,1],
    arc[:,2],
    color="black",
    linewidth=1.5
)

mid = arc[len(arc)//2]

ax_real.text(
    mid[0],
    mid[1],
    mid[2]+0.03,
    f"{pitch_deg:.0f}°",
    fontsize=10
)

ax_real.set_title(
    "Reality View"
)

# ==========================================================
# Body Reference View
# ==========================================================

# ----------------------------------------------------------
# Body Coordinate (Fixed)
# ----------------------------------------------------------

draw_axis(
    ax_body,
    np.array([1,0,0]),

    "cyan",
    "Body X",

    lw=2.5
)

draw_axis(
    ax_body,
    np.array([0,1,0]),

    "gray",
    "Body Y",

    lw=2.5
)

draw_axis(
    ax_body,
    np.array([0,0,1]),

    "purple",
    "Body Z",

    lw=2.5
)

# ----------------------------------------------------------
# Earth Coordinate
#
# Body Frame
#
# Earth rotates backwards
# ----------------------------------------------------------

earth_x_body = R.T @ earth_x
earth_y_body = R.T @ earth_y
earth_z_body = R.T @ earth_z

draw_axis(
    ax_body,
    earth_x_body,

    "gray",
    "Earth X",

    lw=1,
    alpha=0.40,
    ls="--"
)

draw_axis(
    ax_body,
    earth_y_body,

    "gray",
    "Earth Y",

    lw=1,
    alpha=0.40,
    ls="--"
)

draw_axis(
    ax_body,
    earth_z_body,

    "blue",
    "EZ",

    lw=1.5,
    ls="--"
)

# ==========================================================
# Projection
#
# Earth Z
#
# projected onto
#
# Body X
# ==========================================================

projection = np.dot(
    earth_z_body,
    np.array([1,0,0])
)

proj_point = np.array([
    projection,
    0,
    0
])

# ----------------------------------------------------------
# Pitch Angle (EZ <-> ProjYZ)
# ----------------------------------------------------------

pitch_arc_radius = 0.32

# EZ方向单位向量
v1 = earth_z_body / np.linalg.norm(earth_z_body)

# ProjYZ方向单位向量
v2 = proj_yz / np.linalg.norm(proj_yz)

theta = np.linspace(
    0,
    1,
    80
)

arc = []

for t in theta:

    vec = (
        (1-t)*v2 +
        t*v1
    )

    vec = vec / np.linalg.norm(vec)

    arc.append(
        pitch_arc_radius * vec
    )

arc = np.array(arc)

ax_body.plot(
    arc[:,0],
    arc[:,1],
    arc[:,2],
    color="black",
    linewidth=1.5
)

mid = arc[len(arc)//2]

ax_body.text(
    mid[0],
    mid[1],
    mid[2]+0.03,
    f"{pitch_deg:.0f}°",
    fontsize=10
)

# ----------------------------------------------------------
# Earth Z Projection on Body YZ Plane
# ----------------------------------------------------------

earth_z_body = R.T @ np.array([0.0, 0.0, 1.0])

proj_yz = np.array([
    0.0,
    earth_z_body[1],
    earth_z_body[2]
])

# ============================
# Body X End Point
# ============================

body_x_point = np.array([
    projection,
    0,
    0
])

ax_body.scatter(
    body_x_point[0],
    body_x_point[1],
    body_x_point[2],
    color="gray",
    s=40
)

ax_body.text(
    body_x_point[0],
    body_x_point[1],
    body_x_point[2]-0.05,
    "BodyXPt",
    color="black",
    fontsize=9
)

# ==========================================================
# 绘制 Earth Z 到 Body YZ 的垂线
# ==========================================================
ax_body.scatter(
    earth_z_body[0],
    earth_z_body[1],
    earth_z_body[2],
    color="orange",
    s=40
)

ax_body.plot(
    [earth_z_body[0], 0],
    [earth_z_body[1], earth_z_body[1]],
    [earth_z_body[2], earth_z_body[2]],
    color="gray",
    linestyle="--",
    linewidth=1.5
)

mid = (
    earth_z_body +
    np.array([0, earth_z_body[1], earth_z_body[2]])
) / 2

ax_body.text(
    mid[0],
    mid[1],
    mid[2] + 0.03,
    "ProjLineYZ",
    color="gray",
    fontsize=8
)

# Projection vector
ax_body.plot(
    [0, proj_yz[0]],
    [0, proj_yz[1]],
    [0, proj_yz[2]],
    color="orange",
    linewidth=2
)

# Projection point
ax_body.scatter(
    proj_yz[0],
    proj_yz[1],
    proj_yz[2],
    color="orange",
    s=40
)

ax_body.text(
    proj_yz[0],
    proj_yz[1],
    proj_yz[2] + 0.05,
    "ProjYZ",
    color="orange",
    fontsize=9
)

# ----------------------------------------------------------
# Top Edge of Parallelogram
# ----------------------------------------------------------

ax_body.plot(
    [earth_z_body[0], body_x_point[0]],
    [earth_z_body[1], body_x_point[1]],
    [earth_z_body[2], body_x_point[2]],
    "--",
    color="gray",
    linewidth=1.3
)

ax_body.set_title(
    "Body Reference View"
)

# ==========================================================
# 3D Axis
# ==========================================================

for ax in [ax_real, ax_body]:

    ax.set_xlim(-1.2,1.2)
    ax.set_ylim(-1.2,1.2)
    ax.set_zlim(-1.2,1.2)

    ax.set_box_aspect([1,1,1])

# ==========================================================
# Better View Angle
# ==========================================================

ax_real.view_init(
    elev=24,
    azim=-55
)

ax_body.view_init(
    elev=18,
    azim=-45
)

# ==========================================================
# Figure Title
# ==========================================================

fig.suptitle(
    "Script03 : Earth Z Projection onto Body X",
    fontsize=16
)

plt.tight_layout()

plt.show()