4. 2次元の座標変換

前の2章では、画像が何で出来ているか、つまり画素・標本・色を見てきました。
此の章からは、シーンの中の形がどうやって画像の中の自分の場所に辿り着くのかを見ていきます。
其の為の道具が座標変換です。図形の全ての点に新しい座標を与える規則の事です。
先ずは方眼紙1枚に全てが収まる2次元から始めて、第5章で3次元に進みます。

此の章は、シラバスの「座標変換とパイプライン」の中の2次元座標変換を扱います。
そして此の後の全ての土台にもなります。第5章の3次元の変換は座標が1つ増えただけの同じ考え方で、第6章のビューイングパイプラインはまさに此処で学ぶ行列の連鎖です。
小さな数で「此の点は何処に行くか」「どの順番か」「何が保存されるか」を問い易い分野なので、検定対策として確実に押さえておきたい所です。

行列そのものは線型代数シリーズで既に作りました。拡大縮小・回転・せん断・鏡映は第8章、同次座標と平行移動は第9章、行列式と逆行列は第10章です。
此処で改めて導出はしません。
代わりに簡単に振り返った後、CGの世界と検定が見る視点、つまり座標系、順序、変換の分類、そして画面への写像という観点から見ていきます。

4.1 2次元の座標系

座標系とは、原点と軸の組の事で、其のお陰で全ての点を数で呼べる様になります。
CGのコツは、座標系を1つだけ使うのではない事です。
仕事の各部分に一番都合の良い座標系があって、点は変換によって其の間を運ばれます。

1つの家をローカル座標系でモデリングし、ワールド座標系に2回配置した図

  • ローカル座標系(モデリング座標系とも言います):1つの物体を設計する時の座標系です。左の家は床の真ん中を原点にして描いたので、モデリングも回転もし易くなっています。
  • ワールド座標系:シーン全体で共有する1つの座標系です。全ての物体は其れぞれの変換、つまりモデリング変換によって此処に置かれます。同じ家が2回現れていて、1つは 𝐓(2,1) で移動し、もう1つは 𝐓(7,2)𝐑(90°) で回してから移動しています。小さな赤と緑の矢印は、ワールドから見た各コピー自身のローカルな軸です。
  • デバイス座標系(スクリーン座標系とも言います):出力装置の画素の座標です。普通は整数で、原点は左上、そして y は下向きに増えます。ディスプレイ(及び殆どの画像ファイル)が其の順番で行を並べているからです。

引っ掛かり易いのは、此のy下向きのデバイス座標系です。
ワールド座標系は数学に従って(y上向き)、デバイス座標系はモニターに従う(y下向き)ので、途中の何処かで画像を上下反転しなければなりません。何処でやるかは4.5節で見ます。
又、全てのシステムが同じ訳でもありません。OpenGLのウィンドウ座標は原点が左下で、yは上向きです。
「スクリーン座標」と言う言葉を見たら、原点がどの角にあるのかを必ず確認しましょう。

何故全部を最初からワールド座標系でモデリングしないのでしょうか?
そうすると、100箇所に置いた家は同じ数の100個のコピーになり、家を1つ動かすだけで其の全ての点を書き換える事になるからです。
ローカル座標系があれば、物体は1回だけ保存すれば良く、配置・移動・アニメーションは小さな行列1つを変えるだけで済みます。
同じ考え方を何段も重ねると階層構造になります。世界の中の車の中の車輪、と言った具合です(第13章では此れをキャラクタの骨格に使います)。

4.2 基本変換と同次座標の行列

此の章の2次元の変換は全て、同次座標に作用する3×3行列で書きます。
点 (x,y) に3つ目の座標 w=1 を付けると、平行移動さえ行列の掛け算になります(何故そうなるのかは線型代数シリーズ第9章で説明しました):

[x′y′1]=[abtxcdty001][xy1]x′=ax+by+txy′=cx+dy+ty

左上の2×2の部分が線型部分(拡大縮小・回転・せん断・鏡映)、右の列が平行移動で、一番下の行は (0,0,1) のままです。
線型代数シリーズと同じく、行列を左に置く列ベクトルを使います。逆の書き方をしている本の読み方は4.6節で紹介します。

基本変換を並べると次の通りです:

変換 行列 何をするか
平行移動 𝐓(tx,ty) [10tx01ty001] 全ての点に (tx,ty) を足す
拡大縮小 𝐒(sx,sy) [sx000sy0001] xを sx 倍、yを sy 倍する(原点中心)
回転 𝐑(θ) [cosθ−sinθ0sinθcosθ0001] 原点の周りに反時計回りに θ 回す
x方向のせん断 𝐇x(k) [1k0010001] 各点を高さの k 倍だけ横にずらす
y方向のせん断 𝐇y(k) [100k10001] 各点をxの k 倍だけ上にずらす
x軸に関する鏡映 [1000−10001] (x,y)↦(x,−y)
y軸に関する鏡映 [−100010001] (x,y)↦(−x,y)
直線 y=x に関する鏡映 [010100001] (x,y)↦(y,x)

其れぞれが何をするかを見る為に、全てを同じ小さな三角形に適用してみます:A=(1,1)、B=(3,1)、C=(1,2)。
A が直角で、A→B→C の順に回ると反時計回りです。
以下の図は全て本物の行列を掛けて描いたので、どの点も計算と照らし合わせられます。

平行移動

平行移動は、全ての点を同じベクトル 𝐭 だけ動かします。
𝐭=(3,1) の場合:

𝐓(3,1)[111]=[1⋅1+0⋅1+3⋅10⋅1+1⋅1+1⋅11]=[421]

残りの2つの角も同じ様に計算すると、B′=(6,2)、C′=(4,3) になります。

三角形ABCをベクトル3, 1だけ平行移動した図

3本の矢印は全て同じベクトルなので、三角形は大きさも形も向きも変わりません。
基本変換の中で線型ではないのは平行移動だけです。原点を動かしてしまうので、2×2行列では表せません。
3つ目の座標を足す理由は、まさに其れです。

拡大縮小

拡大縮小(スケーリング)は、各座標を其れぞれの倍率で掛けます。
sx=sy なら均等(形は其のままで大きさだけが変わる)、そうでなければ非均等(形が引き伸ばされる)です。
均等な 𝐒(2,2) では、三角形は A′=(2,2)、B′=(6,2)、C′=(2,4) になります。
非均等な 𝐒(1.5,2) では:

A′=(1.5⋅1,2⋅1)=(1.5,2),B′=(1.5⋅3,2⋅1)=(4.5,2),C′=(1.5⋅1,2⋅2)=(1.5,4)

三角形ABCを原点中心に2倍の均等拡大と、1.5倍と2倍の非均等拡大をした図

点線を見て下さい。どの点も原点を通る直線に沿って滑っています。
拡大縮小の行列は常に原点を中心に拡大縮小するので、原点にない図形は原点から離れて(倍率が1未満なら近付いて)移動もします。
左では三角形の2辺が2と1から4と2になりました。形は同じで、大きさが2倍です。
右では3と2になったので、三角形の形が変わりました。
図形を「其の場で」、つまり自分の中心や角を中心に拡大縮小するには、合成変換が必要です(4.3節)。
倍率0は全てを軸の上に押し潰し、負の倍率は鏡に映します。鏡映は其処から来ています。

回転

角度 θ の回転は、全ての点を原点の周りに回します。
y上向きの座標系では、正の θ は反時計回りで、時計回りに θ 回す事は単に 𝐑(−θ) です。
θ=90° なら cosθ=0、sinθ=1 なので、(x,y)↦(−y,x) となります:

A′=(−1,1),B′=(−1,3),C′=(−2,1)

三角形ABCを原点の周りに90度回転した図

各頂点は原点を中心とする円に沿って移動し、三角形の大きさと形は変わりません。
もう少し扱いにくい角度、θ=30° もやってみましょう。cos30°=32≈0.866、sin30°=0.5 です:

A′=(1⋅0.866−1⋅0.5,1⋅0.5+1⋅0.866)=(0.366,1.366)B′=(3⋅0.866−1⋅0.5,3⋅0.5+1⋅0.866)=(2.098,2.366)

辺 AB の長さは2でした。
回転後は A′ から B′ までで、差は (1.732,1)、長さは 1.7322+12=3+1=2 です。
回転なので、当然長さは同じです。

せん断

せん断(スキューとも言います)は、点を軸に平行に、其の軸からの距離に比例した量だけずらします。トランプの山を横から押し倒す様なイメージです。
𝐇x(1) では、各点が高さの1倍だけ右に動きます:x′=x+y、y′=y。

A′=(1+1,1)=(2,1),B′=(3+1,1)=(4,1),C′=(1+2,2)=(3,2)

三角形ABCをkが1のx方向せん断で変形した図

同じ高さの点は全て同じ量だけ動くので、水平な線は水平なままです(自分の上でずれるだけです)。
A の直角は無くなり、三角形は傾きました。
然し底辺は2のまま、高さは1のままなので、面積は変わりません。
𝐇y(k) はxとyの役割を入れ替えた物で、y′=y+kx です。

鏡映

鏡映(反転、対称変換とも言います)は、点を直線に関して鏡に映します。
x軸に関する鏡映はyの符号を、y軸に関する鏡映はxの符号を反転し、対角線 y=x に関する鏡映は2つの座標を入れ替えます:

x軸:A′=(1,−1),B′=(3,−1),C′=(1,−2)y=x:A′=(1,1),B′=(1,3),C′=(2,1)

三角形ABCをx軸及び直線y = xに関して鏡映した図

A=(1,1) は直線 y=x の上にあるので、此の鏡映では動きません。鏡の線の上の点は決して動かないのです。
次に、鏡映した三角形を A′→B′→C′ の順に回ってみて下さい。
どちらの図でも時計回りになっていて、元の三角形は反時計回りでした。
此れが鏡映の特徴で、向きを逆にします。左手を右手にしてしまうのです。
どんな回転にも其れは出来ません。

1つ罠があります。「原点に関する対称移動」(x,y)↦(−x,−y) は鏡映の様に聞こえますが、其の行列は 𝐒(−1,−1)=𝐑(180°) です。
半回転なので、向きは保たれます。
2次元では、両方の軸を反転すると回転、片方だけを反転すると鏡映です。

行列式:面積と向き

線型部分の2×2の行列式 ad−bc(線型代数シリーズ第10章)は、上で見た2つの事を1つの数にまとめてくれます。
其の絶対値は全ての面積が何倍になるかを表し、符号は向きが保たれる(正)か逆になる(負)かを表します。

変換 行列式 面積 向き
𝐓(tx,ty) 1 変わらない 保たれる
𝐒(sx,sy) sxsy sxsy 倍 sxsy>0 なら保たれる
𝐑(θ) cos2θ+sin2θ=1 変わらない 保たれる
𝐇x(k)、𝐇y(k) 1 変わらない 保たれる
鏡映 −1 変わらない 逆になる

此の三角形の面積は 12⋅2⋅1=1 です。
𝐒(2,2) の後は2辺が4と2で面積は4、確かに det=2⋅2=4 です。
𝐒(1.5,2) の後は2辺が3と2で面積は3、det=1.5⋅2=3 です。

4.3 合成変換と順序

実際の変換が基本変換1つだけで済む事は滅多にありません。
「スプライトを回してから、其の位置に置く」は2つの変換で、合成変換は其の積です:

𝐩′=𝐁(𝐀𝐩)=(𝐁𝐀)𝐩

列ベクトルでは点に一番近い行列が最初に作用するので、「先に 𝐀、次に 𝐁」は 𝐁𝐀 と書き、積は右から左へ読みます。
掛け終われば、何段の変換でも1つの3×3行列になります。
平行移動を行列に詰め込んだのは正に其の為です。物体1つにつき行列1つで、何千もの頂点に適用出来ます。

順序が大事

行列の積は可換ではありません(線型代数シリーズ第7章)。其れが実際にどう見えるかを確かめましょう。
𝐑=𝐑(90°) と 𝐓=𝐓(3,1) を両方の順番で掛けます:

𝐓𝐑=[103011001][0−10100001]=[0−13101001]𝐑𝐓=[0−10100001][103011001]=[0−1−1103001]

線型部分はどちらも同じ回転ですが、平行移動の列が違います。
𝐑𝐓 では、回転が後に来るので、平行移動 (3,1) 自体が90°回されて (−1,3) になっています。
A=(1,1) に適用すると:

𝐓𝐑A=(0⋅1−1⋅1+3,1⋅1+0⋅1+1)=(2,2)𝐑𝐓A=(0⋅1−1⋅1−1,1⋅1+0⋅1+3)=(−2,4)

回転してから平行移動するT Rと、平行移動してから回転するR Tの比較

左では、三角形は先ず原点の近くで其の場で回り(オレンジの点線)、其れから (3,1) だけ動いて A′=(2,2)、B′=(2,4)、C′=(1,2) になります。
右では、先ず (4,2) から (6,2) の所に動き(オレンジの点線)、其の後の回転でワールドの原点の周りを大きな弧を描いて振り回され、A′′=(−2,4)、B′′=(−2,6)、C′′=(−3,4) になります。
同じ2つの変換なのに、結果が違います。
物体を置く時の普通のルールは、原点にある内に拡大縮小と回転をして、平行移動を最後にする事、つまり 𝐌=𝐓𝐑𝐒 です(3次元版は線型代数シリーズ第9章にあります)。

可換な組もあります。平行移動同士、原点中心の回転同士、拡大縮小同士、そして回転と均等な拡大縮小です。
然し其れは例外なので、確かめるまではどの順序も重要だと思って下さい。

任意の点を中心とした回転と拡大縮小

回転と拡大縮小は常に原点を中心に行われます。
別の点 𝐩(蝶番、スプライトの中心、ウィンドウの角など)を中心に回したい時は、3段のサンドイッチを使います:

𝐌=𝐓(𝐩)𝐑(θ)𝐓(−𝐩)

右から読むと、𝐩 を原点に動かし、其処で回し、元に戻す、です。
三角形を自分の角 A=(1,1) を中心に90°回し、B=(3,1) を追ってみましょう:

  1. 𝐓(−1,−1):(3,1)→(2,0)。
  2. 𝐑(90°):(2,0)→(0,2)。
  3. 𝐓(1,1):(0,2)→(1,3)。

3つの行列を掛け合わせると、同じ事を1回で出来ます。
線型部分は 𝐑 で、平行移動は 𝐩−𝐑𝐩=(1,1)−(−1,1)=(2,0) です:

𝐓(1,1)𝐑(90°)𝐓(−1,−1)=[0−12100001]

3つの角全てで確認します:A′=(0−1+2,1+0)=(1,1)、B′=(0−1+2,3+0)=(1,3)、C′=(0−2+2,1+0)=(0,1)。

三角形ABCを自分の頂点Aを中心に90度回転した図

中心の A は動かず、他の角が其の周りを回ります。
点を中心とした拡大縮小も同じで 𝐓(𝐩)𝐒𝐓(−𝐩) ですし、原点を通らない直線に関する鏡映も同じ方法で作れます。

逆変換

逆変換は、変換を元に戻します。
基本変換なら、動作を取り消すだけなので計算無しで書けます:

変換 逆変換
𝐓(tx,ty) 𝐓(−tx,−ty)
𝐒(sx,sy) 𝐒(1/sx,1/sy)(倍率に0が無い場合のみ)
𝐑(θ) 𝐑(−θ)=𝐑(θ)⊤
𝐇x(k) 𝐇x(−k)
鏡映 自分自身(2回映すと元に戻る)

合成変換は、靴下より先に靴を脱ぐ様に、逆の順序で取り消します(線型代数シリーズ第10章):

(𝐁𝐀)−1=𝐀−1𝐁−1

先程の点を中心とした回転なら、(𝐓(𝐩)𝐑(90°)𝐓(−𝐩))−1=𝐓(𝐩)𝐑(−90°)𝐓(−𝐩)、つまり同じ点を中心に逆に回す事です。
其の行列の線型部分は (x,y) を (y,−x) に送る 𝐑(−90°) で、平行移動は 𝐩−𝐑(−90°)𝐩=(1,1)−(1,−1)=(0,2) です:

[010−102001][131]=[0+3+0−1+0+21]=[311]

B′=(1,3) が B=(3,1) に戻りました。

図形を動かすか、座標系を動かすか

どの変換にも2通りの読み方があり、CGでは両方が出てきます。

  • 図形を動かす(幾何学的な変換):座標系は其のままで、点が動く。「三角形を90°回す」。
  • 座標系を取り替える:点は其の場に留まり、別の座標系で表し直す。「家から見ると、此の点の座標は幾つか?」

此の2つは互いに逆の関係です。
最初の図の右の家は 𝐌=𝐓(7,2)𝐑(90°) で置かれていました。
図形を動かすと読めば、𝐌 は屋根の先をローカルの (0,2) からワールドの 𝐑(90°)(0,2)+(7,2)=(−2,0)+(7,2)=(5,2) に運びます。
座標の取り替えと読めば、𝐌 はローカル座標をワールド座標に変換し、𝐌−1=𝐑(−90°)𝐓(−7,−2) はワールドからローカルに変換します。
先に 𝐓(−7,−2)、次に (x,y) を (y,−x) に送る 𝐑(−90°) です:

(5,2)→(5−7,2−2)=(−2,0)→(0,−(−2))=(0,2)

ゲームが「マウスは此の回転したボタンの中にあるか?」を調べるのは此の方法です。マウスの位置をボタンのローカル座標に変換すれば、ボタンはただの長方形です。
覚えておきたいルール:座標軸を θ 回すと、全ての点の座標は、点を −θ 回した時と同じ様に変わります。
「座標系を回転する」と書いてsinの符号が逆の行列を載せている本は間違っていません。2つ目の読み方をしているだけです。
第5章のカメラは、同じ考え方の3次元版です(線型代数シリーズ第11章)。

4.4 変換の分類と保存される性質

此処までは個々の変換を集めてきました。
検定の為にも、変換が画像に何を出来て何を出来ないかを理解する為にも、変換を分類し、各分類が図形のどの性質を保存するかを知っておく方が役に立ちます。分類は後に行く程大きくなります。
各分類の例を1つずつ単位正方形に適用すると、次の様になります:

単位正方形に剛体変換、相似変換、アフィン変換、射影変換を適用した図

  • 剛体変換(ユークリッド変換とも言います)は、回転と平行移動の任意の組み合わせです。机の上の固い物体の様に図形を動かすので、長さ・角度・面積が全て変わりません。鏡映も含めて、より大きな集まりを等長変換又は合同変換と呼ぶ本もあります。厳密には本物の固い物体は鏡に映せないので、「剛体」は普通、回転と平行移動だけを指します。
  • 相似変換は、其れに均等な拡大縮小を加えた物です。長さは変わりますが全て同じ倍率なので、角度と長さの比が残ります。形は同じで、大きさだけが変わります。
  • アフィン変換は、任意の線型変換の後に平行移動をした物で、非均等な拡大縮小とせん断が加わります。行列の一番下の行は (0,0,1) で、残りの6つの数は自由です。正方形は平行四辺形になります。長さと角度は失われますが、直線は直線のまま、平行線は平行のままで、点が線分を分ける比も保たれます(中点は中点のままです)。全ての面積が同じ倍率(行列式の絶対値)で変わるので、面積の比も保たれます。
  • 射影変換(ホモグラフィとも言います)は、一番下の行も含めた一般の3×3行列を使い、其の後に w で割ります。直線はやはり直線のままですが、平行線が交わる事があり、等間隔は詰められます。残るのは直線上の4点の複比です。下で説明します。

分類は入れ子になっています。全ての剛体変換は相似変換で、全ての相似変換はアフィン変換で、全てのアフィン変換は(一番下の行が (0,0,1) の)射影変換です。
線型代数シリーズ第8章の線型変換は、平行移動の無い特別な場合としてアフィン変換の中に含まれ、原点を必ず其の場に留めるのは線型変換だけです。

分類 構成要素 自由度(2次元) 保存する物 保存しない物
剛体 回転、平行移動 3 長さ、角度、面積、向き 位置、方向
相似 +均等な拡大縮小 4 角度、長さの比、形 長さ、面積
アフィン +非均等な拡大縮小、せん断 6 直線、平行性、直線上の分割比、面積比 長さ、角度
射影 +一般の一番下の行、wで割る 8 直線、接続関係、複比 平行性、分割比

自由度は独立な数の個数です。角度1つと2次元の平行移動で3、倍率で4つ目、アフィン行列は自由な成分が6つ、射影行列は成分が9つですが自由度は8です(何故かは課題5で考えます)。
アフィン変換が(1直線上に無い)3組の対応点で、射影変換が(どの3点も1直線上に無い)4組の対応点で決まるのも其の為です。1組の対応点から式が2つ得られるからです。

アフィン変換の例:せん断で残る物

4.2節のせん断 𝐇x(1) を使います。
BC の中点は M=(3+12,1+22)=(2,1.5) です。
せん断で其れは M′=(2+1.5,1.5)=(3.5,1.5) に移り、(4,1) から (3,2) までの B′C′ の中点は (4+32,1+22)=(3.5,1.5) です。
同じ点、つまり中点は中点のままでした。

然し BC の長さは 22+12=5≈2.236 で、B′C′ の長さは 12+12=2≈1.414 です。
そして A′B′=(2,0) と A′C′=(1,1) の間の A′ の角度は、90°ではなく arccos22⋅2=45° です。
長さと角度は失われ、直線上の比は保たれる。此れがアフィン変換です。

射影変換の例:透視で見た床

射影変換は一番下の行も使うので、掛け算の後の w は普通1ではなく、其れで割ります(線型代数シリーズ第9章の9.8節)。
w に y+1 を入れる次の行列を使います:

𝐏=[100010011]𝐏[xy1]=[xyy+1]→(xy+1,yy+1)

角が (−1,0)、(1,0)、(1,2)、(−1,2) の正方形に適用します。
下の2つの角は y=0 なので w=1 で、其の場に留まります。
上の2つの角は w=3 で、(1,2)→(13,23)、(−1,2)→(−13,23) になります。
正方形は、タイル張りの床を透視で見た様な台形になりました:

正方形の格子を射影行列Pで床の様な台形に写し、縦の線が消失点0, 1で交わる図

  • 全ての直線は直線のままです。
  • 縦の線 x=−1 と x=1 は平行でしたが、其の像は (0,1) で交わります。此れが消失点です。此の点はy方向の「無限遠点」の像で、𝐏(0,1,0)=(0,1,1) です。平行性は保存されません。
  • 横の線は此の例では偶々水平のままですが、等間隔ではなくなります。y=0,0.5,1,1.5,2 は 0,13,12,35,23 に移ります。遠くの列程詰められます(短縮)。

残るのは複比です。
直線上の位置 a,b,c,d にある4点の複比は

(a,b;c,d)=(c−a)(d−b)(c−b)(d−a)

です。
y軸上の 0,0.5,1,1.5 を取ります。
変換前:(1−0)(1.5−0.5)(1−0.5)(1.5−0)=10.75=43。
変換後の 0,13,12,35 では:(12)(35−13)(12−13)(35)=12⋅41516⋅35=2/151/10=43。
間隔は変わっても、複比は変わりません。
(本によって4点の並べ方が違うので、式には幾つかの形がありますが、どの形も保存されます。)

CGで各分類が現れる所

  • 剛体変換:固い物体やカメラの配置とアニメーション、剛体の物理シミュレーション、キャラクタを変形させずに動かす事。
  • 相似変換:モデルの均等な拡大縮小、地図やUIのズーム、縦横比を保った画像のリサイズ。
  • アフィン変換:非均等な拡大縮小を含むスプライトやモデルの変換、テクスチャ座標の変換、せん断で作る斜体の文字、画像編集ソフトの「自由変形」、画像処理での画像の回転や拡大縮小(第15章)、SVG・PDF・CSSのtransform。
  • 射影変換:透視投影(第5章)、画像編集ソフトの「遠近法」ツール、プロジェクターの台形補正、斜めから撮った書類の補正、写真を繋ぎ合わせたパノラマ。

4.5 ウィンドウ・ビューポート変換

2次元のシーンを描くには、見たいワールドの長方形、つまりウィンドウと、其れを表示するデバイス上の長方形、つまりビューポートを決めます。
ウィンドウ・ビューポート変換は、一方をもう一方に写します。
(注意:此の「ウィンドウ」はワールドの領域で、OSのウィンドウではありません。)

ワールド座標系のウィンドウを、yが下向きのデバイス座標系のビューポートに写す図

式

ウィンドウを xw∈[xw,min,xw,max]、yw∈[yw,min,yw,max](y上向き)、ビューポートをデバイスの画素で xv∈[xv,min,xv,max]、yv∈[yv,min,yv,max](y下向き)とします。
倍率は大きさの比です:

sx=xv,max−xv,minxw,max−xw,minsy=yv,max−yv,minyw,max−yw,min

そして写像は次の通りです:

xv=xv,min+sx(xw−xw,min)yv=yv,min+sy(yw,max−yw)

デバイスのyは上から始まるので、yの式はウィンドウの上端から測っています。
デバイスがy上向きなら、xと同じく yv,min+sy(yw−yw,min) になります。

行列で書くと3つの基本変換です。ウィンドウの角を原点に動かし、拡大縮小し(反転の為に −sy)、ビューポートの角に動かします:

𝐕=𝐓(xv,min,yv,max)𝐒(sx,−sy)𝐓(−xw,min,−yw,min)

ウィンドウの下端(yw=yw,min)はビューポートの下端(一番大きな画素の行 yv,max)に来ます。反転は此処から来ています。

計算例

ウィンドウ:xw∈[0,4]、yw∈[0,3]。
ビューポート:600 × 400の画面上の xv∈[100,500]、yv∈[50,350]。
すると sx=4004=100、sy=3003=100 で、

𝐕=𝐓(100,350)𝐒(100,−100)=[10001000−100350001]

三角形は:

A=(1,1)→(100⋅1+100,−100⋅1+350)=(200,250)B=(3,1)→(100⋅3+100,−100⋅1+350)=(400,250)C=(1,2)→(100⋅1+100,−100⋅2+350)=(200,150)

C はワールドで A より高い所にあり、デバイスでは小さい行番号になるので、画面では A の上に描かれます。画像は正しい向きで表示されます。
反転しないと C は250行目、A は150行目になって上下逆さまの画像になります。自作の2Dレンダラーで最もよくあるバグの1つです。

det𝐕=100⋅(−100)=−10000 が負である事に注目して下さい。
画素の数値で見ると、A′→B′→C′ は時計回りになっています。画面のy軸が下向きなので、画面上では反時計回りに見えるにも関わらずです。
回転でも同じ事が起こります。デバイス座標で直接 𝐑(+θ) を適用すると、モニター上では時計回りに見えます。
y下向きの2Dライブラリ(例えばHTMLのcanvas)が正の角度を時計回りと説明するのは其の為です。

アスペクト比

アスペクト比は幅を高さで割った物です。
上のウィンドウは 4:3、ビューポートは 400:300=4:3 なので sx=sy となり、何も歪みません。
今度はビューポートが正方形、xv∈[0,400]、yv∈[0,400] だとします。
すると sx=4004=100 ですが sy=4003≈133.3 となり、ワールドの半径1の円は幅200画素、高さ約266.7画素の楕円になります。
sx≠sy なので、ウィンドウ・ビューポート変換は相似変換ではなくなり、形が歪むのです。

よくある解決法は、両方の軸に1つの倍率、小さい方の s=min(100,133.3)=100 を使う事です。
するとウィンドウは 400×300 画素を占め、残りの 400−300=100 行は上下50画素ずつの空の帯になります(レターボックス)。
もう1つの方法は、ウィンドウのアスペクト比がビューポートと合うまでウィンドウを広げる(又は高くする)事で、帯の代わりにワールドがより広く見えます。

実際の使われ方

3次元では、同じ写像がパイプラインの最後の段階です。GPUは−1から1までの正規化デバイス座標を、OpenGLなら glViewport、Vulkanなら VkViewport で設定したビューポートに写します(線型代数シリーズ第12章。第6章で1つの頂点を最後まで追います)。
2次元では、地図のパンはウィンドウを動かす事、ズームはウィンドウを小さく又は大きくする事、画面分割のマルチプレイは同じワールドを複数のビューポートに描く事です。

4.6 行ベクトルと列ベクトル

全ての本やAPIや検定の図が此処と同じ書き方をする訳ではありません。
もう1つのよくある書き方は、行ベクトルを使って行列を右に置きます:

[x′y′1]=[xy1]𝐌row

どちらも同じ変換を表していて、行列は互いの転置、𝐌row=𝐌⊤ になっているだけです。
従って行ベクトル形式では平行移動は一番下の行にあり、回転のマイナス記号は反対の角に移ります:

𝐓row=[100010txty1]𝐑row(θ)=[cosθsinθ0−sinθcosθ0001]

順序も逆になります。
(𝐁𝐀)⊤=𝐀⊤𝐁⊤(線型代数シリーズ第7章)なので、「先に 𝐀、次に 𝐁」は、列ベクトルでは 𝐁𝐀、行ベクトルでは左から右へ読む 𝐀row𝐁row です。
モデル行列 𝐓𝐑𝐒 は 𝐒row𝐑row𝐓row になります。

両方で同じ点になるか確かめましょう。
B=(3,1) を90°回してから (3,1) だけ平行移動します。列ベクトルでは4.3節の通り 𝐓𝐑B=(2,4) でした。
行ベクトルでは:

[311][010−100001]=[−131][−131][100010311]=[241]

同じ点 (2,4) を、横向きに書いただけです。

本や図がどちらの書き方を使っているかの見分け方:

  • ベクトルは行列の右にある列か、左にある行か?
  • 平行移動は最後の列にあるか(列ベクトル)、一番下の行にあるか(行ベクトル)?
  • 回転の向きは、書き方が分かるまでマイナス記号の位置で判断しない事。[cosθsinθ−sinθcosθ] は、行ベクトルなら反時計回り、列ベクトルなら時計回りです。

OpenGLのドキュメント、GLSL、そして此のシリーズは列ベクトルを使い、Direct3Dの伝統的な数学ライブラリは行ベクトルを使います。
行列をメモリに行ごとに並べるか列ごとに並べるかは別の問題で(線型代数シリーズ第6章で少し触れました)、数学は変わりません。

4.7 実践と検定対策

2次元の変換に出会う場所

2次元の変換は2Dゲームだけでなく、何処にでもあります。
スプライトは位置・回転・拡大率を持ち、其れらは 𝐓𝐑𝐒 にまとめられます。シーングラフは親の行列を子に掛けるので、剣は其れを持つ手に付いて行きます。
ベクターグラフィックスの形式はアフィン行列を其のまま保存します。SVGの matrix(a, b, c, d, e, f)、PDFやPostScriptの現在の変換行列(CTM)、canvasの setTransform(a, b, c, d, e, f)、CSSの matrix() は全て同じアフィン写像 x′=ax+cy+e、y′=bx+dy+f を表します。
フォントのレンダラーはグリフの輪郭を指定された大きさに拡大縮小し、偽の斜体をせん断で作ります。
シェーダーではテクスチャ座標を回したり拡大縮小したりずらしたりするのは日常茶飯事で、画像処理では画像全体をアフィン変換や射影変換で変形します(第15章)。
シェーダー側はGLSLシリーズ第7章で、スプライトはOpenGLシリーズ第25章で自分で作ります。

C++:linalg.hhで2次元の変換

線型代数シリーズのC++ヘッダーには Mat4 しかありませんが、問題ありません。2次元の変換は、zに触れない3次元の変換に過ぎないからです。
3×3行列は4×4行列の1・2・4番目の行と列に収まるので、translate({tx, ty, 0})、scale({sx, sy, 1})、rotateZ(angle) が其のまま 𝐓、𝐒、𝐑 で、2次元の点は Vec4{x, y, 0, 1} です。
次のプログラムは此の章の計算例を繰り返します:

#include "linalg.hh"
#include <cmath>
#include <cstdio>
#include <numbers>

// A 2D point (x, y) lives on the plane z = 0, with w = 1.
Vec4 point2(float x, float y) { return {x, y, 0.0f, 1.0f}; }

// Round to 2 decimals so that tiny float errors (and -0) print as 0.
float tidy(float v) {
  float r = std::round(v * 100.0f) / 100.0f;
  return r == 0.0f ? 0.0f : r;
}

void printTriangle(const char *name, const Mat4 &m) {
  const Vec4 tri[3] = {point2(1, 1), point2(3, 1), point2(1, 2)};
  printf("%-10s", name);
  for (const Vec4 &p : tri) {
    Vec4 q = m * p;
    printf(" (%g, %g)", tidy(q.x), tidy(q.y));
  }
  printf("\n");
}

// Window [wx0, wx1] x [wy0, wy1] in world coordinates (y up)
// -> viewport [vx0, vx1] x [vy0, vy1] in device pixels (y down).
Mat4 windowToViewport(float wx0, float wy0, float wx1, float wy1,
                      float vx0, float vy0, float vx1, float vy1) {
  float sx = (vx1 - vx0) / (wx1 - wx0);
  float sy = (vy1 - vy0) / (wy1 - wy0);
  return translate({vx0, vy1, 0.0f}) * scale({sx, -sy, 1.0f}) * translate({-wx0, -wy0, 0.0f});
}

int main() {
  const float quarter = std::numbers::pi_v<float> / 2.0f; // 90 degrees
  Mat4 r = rotateZ(quarter);
  Mat4 t = translate({3.0f, 1.0f, 0.0f});

  printTriangle("T * R", t * r);   // rotate first, then translate
  printTriangle("R * T", r * t);   // translate first, then rotate

  Mat4 pivot = translate({1.0f, 1.0f, 0.0f}) * r * translate({-1.0f, -1.0f, 0.0f});
  printTriangle("pivot A", pivot);
  printTriangle("undo", inverse(pivot) * pivot);

  printTriangle("viewport", windowToViewport(0, 0, 4, 3, 100, 50, 500, 350));
  return 0;
}

期待される出力:

T * R      (2, 2) (2, 4) (1, 2)
R * T      (-2, 4) (-2, 6) (-3, 4)
pivot A    (1, 1) (1, 3) (0, 1)
undo       (1, 1) (3, 1) (1, 2)
viewport   (200, 250) (400, 250) (200, 150)

どの行も計算例と一致しています。4.3節の2つの順序、A を中心とした回転と其の逆変換、そして4.5節のウィンドウ・ビューポート変換です。
std::numbers::pi_v はC++20組み込みの π です。float での cos90° は正確に0にはならないので、tidy を挟んでいます。

シェーダー関数:点を中心とした回転

シェーダーでは、点を中心とした回転は便利な小さい関数になります。例えばテクスチャや2次元の図形を其の中心の周りに回す時です。
4.3節の式から、𝐓(𝐜)𝐑(θ)𝐓(−𝐜) を直接組み立てます。線型部分は 𝐑、平行移動は 𝐜−𝐑𝐜 です。
コンストラクタに注意して下さい。SlangとHLSLの float3x3(...) は行ごとに、GLSLの mat3(...) は列ごとに埋めるので、同じ行列でも数の並びが違います。

float3x3 rotateAbout(float2 c, float angle) {
  float co = cos(angle);
  float si = sin(angle);
  // translation part: c - R c
  float2 t = c - float2(co * c.x - si * c.y, si * c.x + co * c.y);
  // float3x3(...) fills row by row
  return float3x3(
     co, -si, t.x,
     si,  co, t.y,
    0.0, 0.0, 1.0);
}

float2 transformPoint(float3x3 m, float2 p) {
  return mul(m, float3(p, 1.0)).xy;
}
mat3 rotateAbout(vec2 c, float angle) {
  float co = cos(angle);
  float si = sin(angle);
  // translation part: c - R c
  vec2 t = c - vec2(co * c.x - si * c.y, si * c.x + co * c.y);
  // mat3(...) fills column by column
  return mat3(
     co,  si, 0.0,
    -si,  co, 0.0,
    t.x, t.y, 1.0);
}

vec2 transformPoint(mat3 m, vec2 p) {
  return (m * vec3(p, 1.0)).xy;
}
float3x3 rotateAbout(float2 c, float angle) {
  float co = cos(angle);
  float si = sin(angle);
  // translation part: c - R c
  float2 t = c - float2(co * c.x - si * c.y, si * c.x + co * c.y);
  // float3x3(...) fills row by row
  return float3x3(
     co, -si, t.x,
     si,  co, t.y,
    0.0, 0.0, 1.0);
}

float2 transformPoint(float3x3 m, float2 p) {
  return mul(m, float3(p, 1.0)).xy;
}

c = (1, 1)、angle = π/2 なら、transformPoint は4.3節の B′ と同じく (3, 1) を (1, 3) に送ります。
SlangとHLSLでは m * v は成分ごとの掛け算になる事を忘れないで下さい。行列の積は mul(m, v) です。

検定のポイント

  • 順序。列ベクトルでは「先に 𝐀、次に 𝐁」は 𝐁𝐀。平行移動と回転は可換ではなく、平行移動の後の回転は平行移動のベクトルまで回してしまう。
  • 原点中心。拡大縮小と回転の行列は原点を中心に作用するので、原点から離れた図形は移動もする。点 𝐩 を中心にするなら 𝐓(𝐩)𝐗𝐓(−𝐩)。
  • 逆変換。𝐓(−𝐭)、𝐒(1/sx,1/sy)、𝐑(−θ)=𝐑⊤、鏡映は自分自身が逆で、合成変換は逆の順序で取り消す。
  • 回転の符号。[cosθ−sinθsinθcosθ] はy上向きの座標系で列ベクトルなら反時計回り。同じ数がy下向きの画面では時計回りに見え、行ベクトルの本ではマイナス記号が反対の角にある。
  • 鏡映と回転。符号を1つ反転(行列式−1)なら鏡映で向きが逆になる。両方反転なら180°回転。
  • 各分類が保存する物。剛体:長さと角度。相似:角度と長さの比。アフィン:直線、平行性、直線上の分割比。射影:直線と複比、但し平行性は保存しない。小さい分類は、其れより大きい分類が保存する物を全て保存する。
  • ウィンドウ・ビューポート変換。軸ごとに倍率=ビューポートの大きさ÷ウィンドウの大きさ。y下向きのデバイスでは反転が必要。xとyの倍率が違うと形が歪む。
  • 書き方の違い。行ベクトルの行列は転置で、左から右へ読み、平行移動は一番下の行にある。

4.8 まとめ

  • CGでは複数の座標系を使う。物体ごとのローカル(モデリング)座標系、シーンのワールド座標系、画素のデバイス(スクリーン)座標系。デバイス座標系は普通、原点が左上でyが下向き。
  • 同次座標 (x,y,1) を使えば、平行移動・拡大縮小・回転・せん断・鏡映は全て3×3行列で、其の連鎖も1つの行列になる。
  • 拡大縮小と回転は原点を中心に作用する。y上向きの座標系では正の回転は反時計回り。鏡映は向きを逆にし、行列式は面積の倍率と其の符号を表す。
  • 合成変換は右から左へ読む積で、順序が重要。点 𝐩 を中心とする変換は 𝐓(𝐩)𝐗𝐓(−𝐩)、逆変換は逆の順序で取り消す。
  • 図形を動かす事と座標系を取り替える事は、同じ行列の逆の読み方。𝐌 はローカルからワールドへ、𝐌−1 はワールドからローカルへ変換する。
  • 剛体 ⊂ 相似 ⊂ アフィン ⊂ 射影で、自由度は3、4、6、8。アフィン変換は平行性と直線上の比を保ち、射影変換は直線と複比だけを保つ。
  • ウィンドウ・ビューポート変換は、ワールドの長方形をデバイスの長方形に拡大縮小と移動で写し、y下向きのデバイスではyを反転する。倍率が揃っていないとアスペクト比が歪む。
  • 行ベクトルの書き方では、行列は転置で、左から右へ読む。

4.9 用語

English 日本語 備考
coordinate transformation 座標変換 各点に新しい座標を与える規則
coordinate system 座標系 原点と軸の組
local (modeling) coordinate system ローカル座標系(モデリング座標系) 物体を設計する時の座標
world coordinate system ワールド座標系 シーン全体の座標
device (screen) coordinate system デバイス座標系(スクリーン座標系) 画素の座標。原点は左上、y下向きが多い
modeling transformation モデリング変換 ローカルからワールドへ
homogeneous coordinates 同次座標 (x,y,1)、wで割る
translation 平行移動 基本変換の中で唯一線型ではない
scaling 拡大縮小(スケーリング) 均等又は非均等、原点中心
rotation 回転 y上向きでは正の角度が反時計回り
shear せん断(スキュー) 単位行列に対角外の数を1つ足した物
reflection 鏡映(反転) 行列式−1、向きを逆にする
composite transformation 合成変換 行列の積、順序が重要
inverse transformation 逆変換 変換を取り消す、積は逆順
linear transformation 線型変換 原点を動かさない
rigid transformation 剛体変換 回転と平行移動
similarity transformation 相似変換 剛体変換+均等な拡大縮小
affine transformation アフィン変換 線型変換+平行移動
projective transformation 射影変換 一般の3×3行列とwでの除算
cross ratio 複比 射影変換で保存される
vanishing point 消失点 平行線の像が交わる点
degrees of freedom 自由度 独立なパラメータの数
window ウィンドウ 表示するワールドの領域
viewport ビューポート 描画するデバイスの領域
window–viewport transformation ウィンドウ・ビューポート変換 ウィンドウをビューポートに写す
aspect ratio アスペクト比 幅÷高さ
row vector / column vector 行ベクトル/列ベクトル 2つの書き方

4.10 課題

  1. ★ 𝐓(−2,3)、𝐒(3,3)、𝐑(180°)、y軸に関する鏡映、𝐇y(2) の同次座標の3×3行列を書き、其れぞれを点 (1,2) に適用し、各行列の逆行列を書いて下さい。
  2. ★★ 三角形 A=(1,1)、B=(3,1)、C=(1,2) を自分の角 A を中心に2倍に拡大する行列を作り、新しい角を求めて下さい。A が動かない事と、面積が4倍になった事を確認して下さい。
  3. ★★ 直線 y=x に関する鏡映の行列とx軸に関する鏡映の行列を、両方の順序で掛けて下さい。どちらの積も回転である事を示し、其の角度を答え、2つの鏡映が必ず回転になる理由を行列式で説明して下さい。
  4. ★★ ある変換は正方形 (0,0)、(1,0)、(1,1)、(0,1) を (0,0)、(2,0)、(3,1)、(1,1) に写します。其の3×3行列を求め、属する一番小さい分類(剛体・相似・アフィン・射影)を答え、保存されない正方形の性質を1つ挙げて下さい。
  5. ★★★ 射影変換の3×3行列には成分が9つありますが、自由度は8しかありません。行列全体を2倍して点に適用すると何が起こるかを使って其の理由を説明し、更に4組の対応点で行列が決まる理由を説明して下さい。

選択問題

問1 ★ 次のうち、全てのアフィン変換で保存されるが、全ての射影変換で保存されるとは限らない物はどれか。

あ 直線は直線に写る。
い 平行な直線は平行なままである。
う 直線上の4点の複比。
え 2直線の成す角。
お 2点間の距離。

問2 ★★ 列ベクトルを用いて、点 (2,0) を原点の周りに反時計回りに90°回転し、其の後 (1,2) だけ平行移動した。点は何処に移るか。

あ (−2,3)
い (3,2)
う (2,−3)
え (1,0)
お (1,4)

問3 ★★ ワールド座標系(y上向き)のウィンドウ 0≤x≤8、0≤y≤6 を、原点が左上でyが下向きの800 × 600画素の画面全体に写す。ワールドの点 (2,1.5) は画面の何処に表示されるか。

あ (200,150)
い (450,200)
う (400,300)
え (200,450)
お (150,200)

4.11 実践課題

紙の上でL字をローカル座標から画面まで変換する。
方眼紙、鉛筆、定規、電卓を用意して下さい。
図形は角が6つあるL字です:P1=(0,0)、P2=(2,0)、P3=(2,1)、P4=(1,1)、P5=(1,3)、P6=(0,3)。
どの方向にも対称ではないので、全ての回転と鏡映が目で分かります。

  1. 両方の軸を−4から6まで描き、L字を描いて下さい。6つの辺の長さと面積(4になる筈です)を書き留めて下さい。
  2. 角 P2 を中心とした90°回転 𝐌1=𝐓(2,0)𝐑(90°)𝐓(−2,0) を、3つの行列を手で掛けて作って下さい。6つの角全てに適用し、結果を別の色で描いて下さい。
  3. 𝐌1 を確認します:P2 は動かず、6つの辺の長さと面積は変わらず、P1→…→P6 の順番は同じ向き(反時計回り)に回る筈です。
  4. 𝐌2=𝐒(2,2)𝐌1 を作り、適用して描いて下さい。全ての辺が2倍になり、全ての角が直角のままで、面積が16である事を確認し、其の2×2部分の det と比べて下さい。最後の列は (4,−4,1) になる筈です。
  5. 別に、元のL字にせん断 𝐇x(0.5) を適用して描いて下さい。縦の辺が傾いても互いに平行なままである事、直角が無くなった事、面積が4のままである事を確認して下さい。
  6. 𝐌1、𝐌2、𝐇x(0.5) の属する一番小さい分類を答え、其れぞれを図から分かる性質1つで裏付けて下さい。
  7. 𝐌2−1 を基本変換の逆変換の積として正しい順序で書き、掛け算して、P5 の像を元に戻して下さい。再び (1,3) にならなければいけません。
  8. 2枚目の紙を、原点が左上でyが下向きの16 × 12マスの「画面」とします。ウィンドウ −3≤x≤5、−5≤y≤1 を画面全体に写すウィンドウ・ビューポート行列 𝐕 を作り、𝐕𝐌2 を計算して、6つの画面上の点を描いて下さい。

最終的に、ワールド座標系の図が3つ、画面の紙の図が1つ、そして行列の小さな表が出来ている筈です。
画面の図は自分で確認出来ます:6つの点が全て16 × 12の画面の中にあり、L字は 𝐌2 の図と全く同じに見え(上下逆さまでも鏡映でもなく)、ワールドの1単位が2マスの長さになり、面積は64マスです。
最後に、𝐕𝐌2 の2×2部分の det を計算して下さい。図は鏡映されていないのに負になります。其の理由を1文で説明して下さい。


コメント(0)

まだコメントはありません。