「前へ 0.8 m/s、旋回 1.6 rad/s(約 92 度/秒)で走れ」と言われたとき、左右の車輪をそれぞれ何 m/s で回せばいいか。この逆算を逆運動学(inverse kinematics:結果側=車体の動きから、入力側=車輪や関節の動きを求める向きの計算)と呼びます。式は2行です。この記事のコードだけで動きます(前回を読んでいなくて構いません。必要な式と関数は再掲します)。
ただし、2行で終わりません。実装すると3か所で引っかかります。
| 引っかかる所 | 何が起きるか | この記事で出る数字 |
|---|---|---|
| ① 車輪には上限がある | 上限を超えた車輪だけ切り詰めると、曲がる半径が変わる | 旋回半径 0.500 m の指令が 0.584 m になり、2秒後に 0.457 m ずれる |
| ② 行き先の姿勢を指定したい | 欲しい数字は3つ、回せる車輪は2つ。1本の円弧では届かない | (2 m, 1 m, 90°) を狙うと向きは 53.13° にしかならない |
| ③ それでも届かせたい | 区間を分ければ届く。ただし分け方は1つに決まらない | 3区間なら 4.366 秒、2区間なら 3.541 秒 |
図1:前回と今回の関係。上の往復は2行の式で閉じますが、右の点線(姿勢から車輪速へ)は素直には閉じません。それがこの記事の後半です
動作環境
Python 3.10.9
numpy 1.26.4
pip install numpy だけで全部動きます。シミュレータは使いません。
記号(前回と同じ)

図2:差動二輪(differential drive:左右の車輪の速度差で向きを変える方式)の記号。位置と向きは車体中心の1点で代表させる
| 記号 | 意味 | 単位 |
|---|---|---|
| $v_l,\ v_r$ | 左右の車輪の周速(車輪の外周が地面をこする速さ) | m/s |
| $L$ | トレッド(左右車輪の間隔) | m |
| $v$ | 車体の前進速度 | m/s |
| $\omega$ | 旋回角速度(オメガ:1秒あたり何ラジアン向きが変わるか。$\pi$ ラジアン=180度) | rad/s |
| $x,\ y,\ \theta$ | 車体中心の位置と向き。3つ組を姿勢(pose)と呼ぶ | m, m, rad |
数値は前回と同じく $L = 0.555$ m(Husky という移動ロボットの実寸)を使います。Husky は4輪ですが、左右で同じ速度を与える点は差動二輪と同じなので、寸法だけ借ります(4輪ゆえに式どおりには回らないことは末尾で触れます)。
① 前進速度と旋回角速度から、左右の車輪速へ:式は2行
前回導いた順運動学は、左右の車輪速から $v$ と $\omega$ を出す2本の式でした。
$$v = \frac{v_r + v_l}{2}, \qquad \omega = \frac{v_r - v_l}{L}$$
平均が前進、差が旋回です。今回はこの2本を $v_l,\ v_r$ について解き直すだけです。2本目から $v_r - v_l = \omega L$、1本目から $v_r + v_l = 2v$。辺々足すと $v_r$ が、引くと $v_l$ が出ます。
$$v_l = v - \frac{\omega L}{2}, \qquad v_r = v + \frac{\omega L}{2}$$
前進ぶんを両輪に等しく配り、旋回ぶんを左右に $\pm\omega L/2$ ずつ振り分ける、それだけです。左に曲がるとき($\omega > 0$)は右車輪が速くなります。
import numpy as np
TREAD = 0.555 # 左右車輪の間隔 [m](Husky というロボットの実寸。前回と同じ)
R_WHEEL = 0.1651 # 車輪の半径 [m](同上)
V_MAX = 1.0 # 車輪の周速の上限 [m/s](モータの限界。この記事では 1.0 と仮定)
def wheel_speeds(v: float, omega: float, tread: float = TREAD) -> tuple[float, float]:
"""前進速度 v [m/s] と旋回角速度 omega [rad/s] から、左右の車輪の周速 [m/s] を求める。"""
return v - omega * tread / 2.0, v + omega * tread / 2.0
def body_twist(v_l: float, v_r: float, tread: float = TREAD) -> tuple[float, float]:
"""前回の順運動学。左右の車輪速から v と omega へ(逆算が合っているかの検算に使う)。"""
return (v_r + v_l) / 2.0, (v_r - v_l) / tread
合っているかは、前回の例で往復させれば分かります。前回は左 0.6 m/s・右 1.0 m/s から $v = 0.8$、$\omega = 0.7207$ を出しました。それを今回の式に入れて、0.6 と 1.0 に戻れば正解です。
v, omega = body_twist(0.6, 1.0) # 前回の例:左 0.6 / 右 1.0
v_l, v_r = wheel_speeds(v, omega) # 今回の式で戻す
print(f"v = {v:.4f} m/s, omega = {omega:.4f} rad/s")
print(f"逆算した車輪速 左 {v_l:.6f} 右 {v_r:.6f} m/s")
print(f"モータへ渡す角速度 左 {v_l / R_WHEEL:.4f} 右 {v_r / R_WHEEL:.4f} rad/s")
v = 0.8000 m/s, omega = 0.7207 rad/s
逆算した車輪速 左 0.600000 右 1.000000 m/s
モータへ渡す角速度 左 3.6342 右 6.0569 rad/s
往復が閉じました。最後の行は、モータに渡す値への換算です。モータの指令は「周速 m/s」ではなく「車輪の角速度 rad/s」なので、周速を車輪半径で割ります。
同じ「逆運動学」という言葉をロボットアームでも使います。
アームの逆運動学は三角関数を解く必要があって解が複数出ますが、差動二輪は一次式なので解は1つに決まります。ここまでは、アームより簡単です。 難しくなるのはここからです。
② 車輪には上限がある:素直にクリップすると旋回半径が変わる
モータには最高回転数があります。この記事では車輪の周速の上限を $v_{\max} = 1.0$ m/s とします。
上の2行の式を見ると、右車輪は $v + \omega L/2$ です。前進と旋回が上限を分け合っていることになります。大きいほうの車輪が上限に収まる条件は、
$$|v| + \frac{|\omega| L}{2} \le v_{\max}$$
です。$L/2 = 0.2775$ m なので、たとえば $\omega = 1.6$ rad/s で回るなら旋回ぶんだけで 0.444 m/s を使い、前進に残るのは 0.556 m/s までです。
ここで「前へ 0.8 m/s・旋回 1.6 rad/s」(旋回半径 $R = v/\omega = 0.5$ m)を要求してみます。
v_cmd, omega_cmd = 0.8, 1.6 # 旋回半径 0.5 m で回りたい
v_l, v_r = wheel_speeds(v_cmd, omega_cmd)
print(f"要求どおりの車輪速 左 {v_l:.4f} 右 {v_r:.4f} m/s (右が上限 {V_MAX} を超える)")
要求どおりの車輪速 左 0.3560 右 1.2440 m/s (右が上限 1.0 を超える)
右車輪が 1.244 m/s で上限を超えました。ここで手が伸びるのが np.clip です。それをやると、曲がる半径が変わります。
結論を先に書くと、左右を同じ比率で縮めてください。 軌跡が変わらず、到着が遅れるだけで済みます。ロボットに限らず、2つの成分を別々に切り詰めるとベクトルの向きが変わる、という話です。旋回半径は $v/\omega$ の比なので、比を守るには両方を同じ倍率で縮めるしかありません。以下はその根拠です。
3つの縮め方を比べる
| 縮め方 | やること | 何を守り、何を捨てるか |
|---|---|---|
| (a) クリップ | 上限を超えた車輪だけ切り詰める | 何も守らない。$v$ も $\omega$ も比率も変わる |
| (b) 比率保持 | 左右を同じ比率で縮め、大きいほうを上限に合わせる | $v/\omega$(=旋回半径)を守る。速さを捨てる |
| (c) 旋回優先 | $\omega$ は要求どおり、$v$ を上限に収まるまで削る | $\omega$(=向きの変化)を守る。半径を捨てる |
3つともコードにします。検算には前回導いた step_exact(一定入力のあいだ車体は円弧を描く、という閉じた式)を使うので、そのまま再掲します。
def step_exact(pose: np.ndarray, v: float, omega: float, dt: float) -> np.ndarray:
"""前回の順運動学。1ステップぶん、円弧として厳密に進める(検算用に再掲)。"""
x, y, th = pose
if abs(omega) < 1e-12: # まっすぐのときは 0 割りになるので直線へ
return np.array([x + v * np.cos(th) * dt, y + v * np.sin(th) * dt, th])
radius = v / omega
th_new = th + omega * dt
return np.array([x + radius * (np.sin(th_new) - np.sin(th)),
y - radius * (np.cos(th_new) - np.cos(th)),
th_new])
def limit_clip(v_l: float, v_r: float, v_max: float = V_MAX) -> tuple[float, float]:
"""(a) 上限を超えた車輪だけを切り詰める。"""
return float(np.clip(v_l, -v_max, v_max)), float(np.clip(v_r, -v_max, v_max))
def limit_scale(v_l: float, v_r: float, v_max: float = V_MAX) -> tuple[float, float]:
"""(b) 左右を同じ比率で縮めて、大きいほうを上限に合わせる。"""
biggest = max(abs(v_l), abs(v_r))
if biggest <= v_max:
return v_l, v_r
scale = v_max / biggest
return v_l * scale, v_r * scale
def limit_turn_first(v_l: float, v_r: float, v_max: float = V_MAX) -> tuple[float, float]:
"""(c) omega を守り、v だけを上限に収まるまで削る。"""
v, omega = body_twist(v_l, v_r)
v_room = v_max - abs(omega) * TREAD / 2.0 # 旋回ぶんを引いた、前進に使える残り
v = float(np.clip(v, -v_room, v_room))
return wheel_speeds(v, omega)
target = step_exact(np.zeros(3), v_cmd, omega_cmd, 2.0) # 上限が無ければ 2 秒後にここ
for name, limiter in [("(a) クリップ", limit_clip), ("(b) 比率保持", limit_scale),
("(c) 旋回優先", limit_turn_first)]:
l, r = limiter(v_l, v_r)
v_act, omega_act = body_twist(l, r) # 実際に車体が動く v, omega
pose = step_exact(np.zeros(3), v_act, omega_act, 2.0)
print(f"{name} 左 {l:.4f} 右 {r:.4f} → v={v_act:.4f} omega={omega_act:.4f}"
f" 旋回半径={v_act / omega_act:.4f} m")
print(f" 2秒後のずれ 位置 {np.linalg.norm(pose[:2] - target[:2]):.3f} m"
f" 向き {np.degrees(pose[2] - target[2]):+.1f}°")
(a) クリップ 左 0.3560 右 1.0000 → v=0.6780 omega=1.1604 旋回半径=0.5843 m
2秒後のずれ 位置 0.457 m 向き -50.4°
(b) 比率保持 左 0.2862 右 1.0000 → v=0.6431 omega=1.2862 旋回半径=0.5000 m
2秒後のずれ 位置 0.309 m 向き -36.0°
(c) 旋回優先 左 0.1120 右 1.0000 → v=0.5560 omega=1.6000 旋回半径=0.3475 m
2秒後のずれ 位置 0.305 m 向き +0.0°
(b) にも 0.309 m のずれが出ていますが、旋回半径の列が 0.5000 のままである点を見てください。これは同じ円の上で遅れているだけのずれで、時間を伸ばせば消えます(すぐ下で確かめます)。

図3:同じ要求を、上限 1.0 m/s の下で2秒走らせた軌跡。灰色の太線が上限なしの理想。赤の (a) は外側へ膨らんで別の円になり、青の (b) は理想と同じ円の上を遅れて進む。緑の (c) は向きだけ合って半径が縮む
(a) は右車輪だけを削ったので、左右の差が縮みました。差は $\omega$ そのものです。$\omega$ が 1.6 から 1.16 に落ちて $v$ は 0.68 なので、旋回半径は $0.68/1.16 = 0.584$ m。要求した 0.5 m の円とは別の円を走っています。 2秒後には位置が 0.457 m、向きが 50.4 度ずれました。
(b) は左右をどちらも 0.8039 倍にしたので、平均も差も 0.8039 倍です。$v/\omega$ は変わらず、旋回半径は 0.5 m のままです。2秒時点では 0.309 m 手前にいますが、同じ円の上を遅れているだけです。走らせる時間を $2.0 / 0.8039 = 2.488$ 秒に伸ばすと、理想の位置に一致します。
(c) は $\omega = 1.6$ を守ったので向きのずれは 0.0 度ですが、$v$ を 0.556 まで削ったぶん半径が 0.347 m に縮み、要求より内側の小さな円を回っています。
l, r = limit_scale(v_l, v_r)
v_act, omega_act = body_twist(l, r)
scale = v_act / v_cmd # 0.8039
pose = step_exact(np.zeros(3), v_act, omega_act, 2.0 / scale)
print(f"(b) を {2.0 / scale:.4f} 秒走らせる → ずれ {np.linalg.norm(pose[:2] - target[:2]):.2e} m")
(b) を 2.4880 秒走らせる → ずれ 0.00e+00 m
⚠️ (a) の 0.457 m は、上限を超えている間ずっと積もります。 経路をたどる側のプログラム($v$ と $\omega$ の指令を出す側)から見ると「軌跡は同じで遅い」(b) のほうが圧倒的に扱いやすく、指令が速すぎてもここで自動的に減速されます。(c) の旋回優先は、向きを合わせることが最優先の場面(その場旋回・障害物の直前)でだけ使います。
式が2行で済むぶん、上限の処理は後回しにしがちです。ここを clip で済ませたコードは、直進では正しく動き、きつく曲がるときだけ指令と違う円を描くので、症状が出るまで気づきません。
③ 姿勢を指定する:欲しい数字は3つ、回せる車輪は2つ
ここまでは「速度」の逆算です。前回の記事の末尾で「行き先を先に決めて走らせる」と予告したので、それをやります。
行き先は姿勢 $(x,\ y,\ \theta)$ の3つの数字です。回せるのは $v_l,\ v_r$ の2つ。数が合いません。 前回、車輪は横に滑れないという制約を非ホロノミック拘束(nonholonomic constraint:速度に掛かる、位置の式には書き直せない制約)と呼びました。逆運動学の側から見ると、その制約はこの「3対2」の形で現れます。
具体的に見ます。原点・向き 0 から出発して、一定の $v,\ \omega$ で走ると、前回③のとおり軌跡は円弧です。円弧1本で点 $(x,\ y)$ に着いたとき、向き $\theta$ は選べません。図で描くと一目です。

図4:原点から (2, 1) へ向かう円弧は1本しかない(中心 (0, 2.5)・半径 2.5 m)。その円弧で着いたときの向きは 53.13 度で、欲しい 90 度には 36.87 度足りない。破線が弦で、出発点で弦と接線($x$ 軸)のなす角が中心角 $\phi$ の半分になっている
式にすると、原点から円弧で $(x,\ y)$ に着いたときの向きは
$$\theta_{\text{arc}} = 2,\mathrm{atan2}(y,\ x)$$
です(接弦定理:円の接線と弦のなす角は、その弦に対する中心角の半分、という性質です。出発の接線が $x$ 軸なので、弦の角度 $\mathrm{atan2}(y, x)$ の2倍が中心角=向きの変化になります。忘れていても、図4の矢印の差だけ見れば結論は同じです)。
半径は弦の長さから逆算します。長さ $\sqrt{x^2 + y^2}$ の弦は、半径 $R$・中心角 $\phi$ の円では $2R\sin(\phi/2)$ に等しいので、$R = \sqrt{x^2 + y^2} ,/, 2\sin(\phi/2)$ です。
x_t, y_t = 2.0, 1.0
phi = 2 * np.arctan2(y_t, x_t) # 円弧1本で (x_t, y_t) に着いたときの向き
radius = np.hypot(x_t, y_t) / (2 * np.sin(phi / 2)) # 弦の長さ = 2R sin(φ/2) を R について解く
print(f"(2, 1) に円弧1本で着くと、向きは {np.degrees(phi):.2f}°、半径は {radius:.4f} m に決まる")
(2, 1) に円弧1本で着くと、向きは 53.13°、半径は 2.5000 m に決まる
$(x, y)$ を決めた時点で $\theta$ は 53.13 度に決まりました。90 度で着きたければ、一定入力1本では不可能です。これが、前回の順運動学を「逆に解く」だけでは行き先を指定できない理由です。
図5:入力が2つで欲しい数が3つなら、時間方向に自由度を足すしかない
④ 回転→直進→回転に分けると届く
入力を時間で切り替えれば、区間の数だけ自由度が増えます。いちばん素直な分け方は、車庫入れと同じ3手です。
- 回転:その場で目標の方向を向く($v = 0$、$\omega$ だけ)
- 直進:目標の点まで走る($\omega = 0$、$v$ だけ)
- 回転:その場で目標の向きに合わせる
各区間は一定入力なので、車輪速は①の2行で出ます。その場回転は $v = 0$ なので $v_l = -\omega L/2$、$v_r = +\omega L/2$、つまり左右を逆向きに同じ速さで回すことです。
def plan_turn_drive_turn(x_t: float, y_t: float, th_t: float,
v_drive: float = 0.8, omega_turn: float = 1.0) -> list[tuple[float, float, float]]:
"""原点・向き 0 から目標姿勢 (x_t, y_t, th_t) へ、回転→直進→回転で届く (v, omega, 秒数) の列。"""
th_1 = np.arctan2(y_t, x_t) # まず目標の方向を向く
dist = np.hypot(x_t, y_t) # 直進する距離
th_2 = th_t - th_1 # 着いてから目標の向きへ回す角度
return [(0.0, omega_turn * np.sign(th_1), abs(th_1) / omega_turn),
(v_drive, 0.0, dist / v_drive),
(0.0, omega_turn * np.sign(th_2), abs(th_2) / omega_turn)]
plan = plan_turn_drive_turn(2.0, 1.0, np.radians(90))
pose = np.zeros(3)
for i, (v, omega, seconds) in enumerate(plan, 1):
v_l, v_r = wheel_speeds(v, omega)
pose = step_exact(pose, v, omega, seconds) # 前回の順運動学で「本当に着くか」を検算
print(f"区間{i}: 左 {v_l:+.4f} 右 {v_r:+.4f} m/s を {seconds:.4f} 秒"
f" → x={pose[0]:.4f} y={pose[1]:.4f} θ={np.degrees(pose[2]):.2f}°")
print(f"合計 {sum(s[2] for s in plan):.4f} 秒")
区間1: 左 -0.2775 右 +0.2775 m/s を 0.4636 秒 → x=0.0000 y=0.0000 θ=26.57°
区間2: 左 +0.8000 右 +0.8000 m/s を 2.7951 秒 → x=2.0000 y=1.0000 θ=26.57°
区間3: 左 -0.2775 右 +0.2775 m/s を 1.1071 秒 → x=2.0000 y=1.0000 θ=90.00°
合計 4.3659 秒
(2, 1, 90°) に誤差ゼロで届きました。検算に使ったのは前回の順運動学です。逆運動学の答えは、順運動学に入れて戻れば正しさが確かめられます。 アームの記事でも同じことをやっていて、この往復が2本の記事を1組にする理由です。
分け方は1つに決まらない
3区間でなくてもよく、たとえば「円弧で点に着いてから、向きだけ回す」2区間でも届きます。③で出した円弧(向き 53.13 度で着く)を使い、残り 36.87 度をその場で回します。
def plan_arc_turn(x_t: float, y_t: float, th_t: float,
v_drive: float = 0.8, omega_turn: float = 1.0) -> list[tuple[float, float, float]]:
"""円弧1本で (x_t, y_t) へ着き、最後に向きだけ合わせる 2 区間の列。"""
phi = 2 * np.arctan2(y_t, x_t)
radius = np.hypot(x_t, y_t) / (2 * np.sin(phi / 2))
omega_arc = v_drive / radius
th_2 = th_t - phi
return [(v_drive, omega_arc, phi / omega_arc),
(0.0, omega_turn * np.sign(th_2), abs(th_2) / omega_turn)]
plan2 = plan_arc_turn(2.0, 1.0, np.radians(90))
pose = np.zeros(3)
for i, (v, omega, seconds) in enumerate(plan2, 1):
v_l, v_r = wheel_speeds(v, omega)
pose = step_exact(pose, v, omega, seconds)
print(f"区間{i}: 左 {v_l:+.4f} 右 {v_r:+.4f} m/s を {seconds:.4f} 秒"
f" → x={pose[0]:.4f} y={pose[1]:.4f} θ={np.degrees(pose[2]):.2f}°")
print(f"合計 {sum(s[2] for s in plan2):.4f} 秒")
区間1: 左 +0.7112 右 +0.8888 m/s を 2.8978 秒 → x=2.0000 y=1.0000 θ=53.13°
区間2: 左 -0.2775 右 +0.2775 m/s を 0.6435 秒 → x=2.0000 y=1.0000 θ=90.00°
合計 3.5413 秒

図6:同じ目標へ届く2つの走らせ方。緑が回転→直進→回転(4.366 秒)、青が円弧→回転(3.541 秒)。矢印は 0.5 秒ごとの姿勢で、緑は最初の 0.46 秒間は原点で回っているだけ
| 走らせ方 | 区間数 | 合計時間 | 特徴 |
|---|---|---|---|
| 回転→直進→回転 | 3 | 4.366 秒 | どんな目標にも届く。止まって回る時間が無駄になる |
| 円弧→回転 | 2 | 3.541 秒(19% 短い) | 走りながら曲がるので速い。真後ろの目標には使えない(次節) |
同じ目標に対して答えが2つ出ました。アームの逆運動学で解が複数出るのと似ていますが、事情が違います。アームは「式の解が複数ある」のに対し、差動二輪は**「区間の切り方が無数にある」**のです。どれを選ぶかは、時間・滑らかさ・障害物といった逆運動学の外側の基準で決まります。それが経路計画の仕事で、次回の主題です。
この記事の式でできないこと
区間の継ぎ目で、車輪速が一瞬で跳びます。 回転→直進→回転の左右車輪速を時間で描くとこうなります。

図7:回転→直進→回転の左右車輪速。0.46 秒の時点で左車輪は -0.28 から +0.80 m/s へ一瞬で変わる。実機のモータはこの階段を追えない
実機のモータには加速の限界があるので、この指令をそのまま送ると継ぎ目で滑るか遅れるかします。加減速を入れて滑らかにつなぐ話は、この記事の式(一定入力の区間をつなぐ)の外です。
円弧→回転は、真後ろの目標で破綻します。 目標を $(-1,\ 0,\ 0°)$ にすると $\theta_{\text{arc}} = 2,\mathrm{atan2}(0, -1) = 360°$、つまり $\sin(\phi/2) = 0$ で半径が無限大になります。真後ろの点を通る円弧は、出発点で $x$ 軸に接するものが存在しません。回転→直進→回転なら 7.533 秒で届きます(後ろを向いて 3.14 秒、走って 1.25 秒、向き直して 3.14 秒)。
円弧→回転は上限を無視しています。 目標 $(0,\ 1,\ 180°)$ なら円弧の半径は 0.5 m で、②と同じ $\omega = 1.6$ が要ります。右車輪の要求は 1.244 m/s で上限超えです。②の比率保持を掛けると 1.963 秒の計画が 2.442 秒に伸びます(plan_arc_turn(0.0, 1.0, np.radians(180)) の区間1の車輪速に limit_scale を通すと出ます)。逆運動学の2行と上限の処理は、区間計画のあとにもう一度通します。
そして前回と同じく、4輪固定で横滑りするロボットには、この式は係数調整では合いません。 前回、PyBullet(物理シミュレータ)上の Husky に左 0.6 / 右 1.0 m/s を与えて測ると、車体の旋回角速度は式の予測の約 1/2.6 でした。寸法を借りた Husky 自身が、この式どおりには回らないロボットです。左右の車輪速を「正しく」逆算しても、車体はその $\omega$ で回りません。
まとめ
- 差動二輪の逆運動学は2行です。$v_l = v - \omega L/2$、$v_r = v + \omega L/2$。前進ぶんを両輪に等しく配り、旋回ぶんを左右に振り分けます。モータには周速を車輪半径で割った角速度を渡します
- 車輪の上限を超えたら、左右を同じ比率で縮めます。 上限を超えた側だけクリップすると旋回半径が 0.500 → 0.584 m に変わり、2秒で 0.457 m ずれます。比率を保てば軌跡は同じで、到着が 2.000 → 2.488 秒に遅れるだけです
- 欲しい姿勢は $(x, y, \theta)$ の3つ、回せる車輪は2つ。1本の円弧では $\theta$ を選べません((2, 1) に着く向きは 53.13 度に決まる)
- 入力を時間で切り替えれば届きます。回転→直進→回転なら 4.366 秒、円弧→回転なら 3.541 秒。分け方は1つに決まらないので、選ぶ基準は逆運動学の外にあります
- 逆運動学の答えは、前回の順運動学に入れて戻せば検算できます
本シリーズの全体像は、まとめ記事から辿れます。