ゲームプログラミング/3Dグラフィック/平面スクリーン投影のプログラム例
本ページの目的
[編集]本科目『ゲームプログラミング』は、ゲームクリエイターのための教材ではなくプログラマーのための教材です。
よって、単にゲーム用の3D-CGを作るだけなら、本ページの方法は不要です。また、ゲームで3D描画をしたいなら、Windowsの場合ならDirectXなど既存の3Dエンジンがあります。
しかし本ページでは、「今は自分は3Dエンジンを作れないが、将来的には自分で3Dエンジンを作りたいなあ」とか思ってるような人を念頭においています(ただし、本頁ページの水準では、到底ですが商用の3Dエンジンの質には及びません)。
プログラム例
[編集]高校3年や大学1年理系では、回転行列を習う場合が多いので、ついつい回転のプログラムを早く書きたいと、読者は思いがちであろう。
しかし、さきに回転のプログラムを書いてしまうと、コード中の式が複雑になるので、デバッグがしづらくなる。
回転のプログラムを書くのは、後回しにしよう。
回転に入る前の前準備
[編集]すごく簡単な例から
[編集]まず、Windowsプログラミングで画像を表示したい場合、Windowa API の機能を使う。
Visual C++ でWindows API を使う場合、
case WM_PAINT:
という節が最初から用意されているので、そこに書き込めばいい。
さて、私たちは3Dグラフィックを表示したいのであった。
まず、
□ 被写体 ↑ カメラ
のような位置関係にある場合を、とにかく表示しよう。
コードは例として、下記のようになるだろう。
- コード例 (抜粋)
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
HBRUSH brasi_parts_2;
brasi_parts_2 = CreateSolidBrush(RGB(255, 100, 100)); // 壁の表示用のピンク色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
int hisyatai_onViewX = 200; int hisyatai_onViewY = 150;
Rectangle(hdc, hisyatai_onViewX, hisyatai_onViewY, hisyatai_onViewX+10, hisyatai_onViewY+20); // 基準の状態
EndPaint(hWnd, &ps);
}
break;
ピンク色の□がウィンドウに表示されただろうか?
もし、位置がウィンドウ中央でなければ、変数 int hisyatai_onViewX = 200; int hisyatai_onViewY = 150; の数値を適切な値にいじって、真ん中あたりに来るようにしてほしい。(上記コード例では、パソコンの画面が小さい場合などの小さめのウィンドウの場合でも表示できるように、実際の市販のパソコンでのウィンドウの初期設定よりも、小さめの座標位置に表示するようにしてある。)
さて、まず私たちは、キーボードの右ボタン(→)を押したら、カメラが右方向に移動するようにしよう。
つまり、→ボタンを押したら、下図のようにカメラが右にカニ歩きで移動するとする。
□ 被写体 ↑ カメラ
このようにカメラが右にカニ歩きした場合、被写体は視界の中では左のほうに移動して見えるハズである。
このような処理は、下記のように
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
HBRUSH brasi_parts_2;
brasi_parts_2 = CreateSolidBrush(RGB(255, 100, 100)); // 壁の表示用のピンク色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
int hisyatai_onViewX = 200; int hisyatai_onViewY = 150;
Rectangle(hdc, hisyatai_onViewX - Camera_x_deffer, hisyatai_onViewY, hisyatai_onViewX+10 -Camera_x_deffer, hisyatai_onViewY+20);
EndPaint(hWnd, &ps);
}
break;
case WM_KEYDOWN:
switch (wParam) {
case VK_RIGHT:
/* ここに右(→)キー入力後の処理を書く*/
Camera_x_deffer = Camera_x_deffer + 10;
break;
case VK_LEFT:
/* ここに左(←)キー入力後の処理を書く*/
Camera_x_deffer = Camera_x_deffer - 10;
break;
} // これは switch (wParam) の終わりのカッコ
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
break; // これは WM_KEYDOWN: からのbreak
- ※ 変数 Camera_x_deffer などはあらかじめ、グローバル変数として定義しておく。
のように実装できる。
このように、カメラの向きが一定のままで 位置が平行移動するだけなら、三角関数はまだ不要である。(カメラの向き変更などを処理するときに、ようやく三角関数を使う。)
私たちの目的はけっして数学的な技巧をアピールすることではなく、あくまで役立つプログラムを書くことなので、三角関数を使わなくても実装できる処理なら、まだ三角関数を使う必要は無い。
被写体の奥行き方向に複数ある場合
[編集]□ 被写体1 □ 被写体2 ↑ カメラ
のように、被写体が2つ、奥行き方向になるとしよう。さきほどの例と同様に、カメラがカニ歩きしたとしよう。
□ 被写体1 □ 被写体2 ↑ カメラ
すると、カメラ位置の視界のなかでは、視界内での、被写体映像の左方向への移動量が違っているハズである。
そして、その移動量は、奥にある被写体Aのほうが、小さいハズである(たとえば電車に乗って風景をながめるとき、遠くの風景ほどゆっくり移動するように見える。)
移動量には、平面投影の公式を使えばいい。
実装は簡単で、 case WM_PAINT: の節を、下記のように、上記の公式を使って書き換えればいい。
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
HBRUSH brasi_parts_2;
brasi_parts_2 = CreateSolidBrush(RGB(100, 100, 255)); // 壁の表示用のブルー色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
int hisyatai_onViewX = 200; int hisyatai_onViewY = 150; // これは単なるウィンドウ内での初期の表示位置の調整用
Rectangle(hdc, hisyatai_onViewX - (hisyatai1_Z / screen_z_Zahyou) * Camera_x_deffer, hisyatai_onViewY, hisyatai_onViewX+20 - (hisyatai1_Z / screen_z_Zahyou) * Camera_x_deffer, hisyatai_onViewY+30);
brasi_parts_2 = CreateSolidBrush(RGB(255, 100, 100)); // 壁の表示用のピンク色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
Rectangle(hdc, hisyatai_onViewX - (hisyatai2_Z / screen_z_Zahyou) * Camera_x_deffer, hisyatai_onViewY, hisyatai_onViewX + 10 - (hisyatai2_Z / screen_z_Zahyou) * Camera_x_deffer, hisyatai_onViewY + 20);
EndPaint(hWnd, &ps);
}
break;
もし、いちいち「hisyatai1_Z」とか「hisyatai2_Z」のように変数を被写体の個数だけ変数宣言するのがイヤならば、配列や構造体配列などを使って宣言するプログラムに書き換えればいい。
なお、あらかじめグローバル変数領域で「hisyatai1_Z」とか「hisyatai2_Z」を宣言しておく必要がある(数値が必要なら初期値などの数値も代入しておく)。
なお、上記コード例では、奥側の被写体を少しうすい青色、手前側の被写体を少しうすいピンク色にしてある。
奥側の被写体のほうが、サイズを大きくしてある。
このように、もしカメラの移動パターンがカニ歩きだけなら、けっこう簡単に3Dプログラムを作れる。
被写体に近づく場合
[編集]カメラが被写体に近づいた場合、投影面もカメラと一緒に近づく。
なので、カメラと投影面の距離はつねに一定である。
なので相対的には、カメラが被写体に近づいたときは、相対的に言い換えれば、被写体がカメラに近づいたともいえる。
こう相対的に考えたほうが、投影面の位置を固定したまま(このときカメラ位置も固定したまま)で済むので、計算がラクである。
私たちは結局、図のような投影でのスクリーン上での座標を計算することになる。
これをxについて解けば、
- ⇔
- ⇔
- ⇔
図では簡略化のためxとzの2次元だけにしたが、yについても同様の計算法で求められる。
なので同様にしてyを求め、
xとyをまとめて、
行列でまとめれば
となる。
カメラから見て、投影された点がどこにあるかが視界内の情報で重要であるので、上式の第1項の xc と yc とを移項して、
もし計算の単純化のため、もしカメラ位置を0とすれば、
となり、冒頭の節で紹介した投影面上での拡大縮小率の式になる。
なお、xb,ybが変数の場合にはzbも変数になることに注意せよ。(zsにつられて変数と解釈すると、プログラムすべきコードが思いつきづらくなる。)
あるいは、式を
のように書き換えて区別でもしよう。
上記の計算をもとに、プログラムを書きなおして1点ずつ投影位置を計算するプログラムに書き換えると、だいたい、下記のような感じになる。
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
int pro1X = (screen_z_Zahyou - Camera_z) / (hisyatai1_Z - Camera_z) * (hisyatai1_X - Camera_x) ; // 点Aのスクリーン投影のx座標
int pro1Y = (screen_z_Zahyou - Camera_z) / (hisyatai1_Z - Camera_z) * (hisyatai1_Y - Camera_y) ; // 点Aのスクリーン投影のy座標
int pro2X = (screen_z_Zahyou - Camera_z) / (hisyatai2_Z - Camera_z) * (hisyatai2_X - Camera_x); // 点Bのスクリーン投影のx座標
int pro2Y = (screen_z_Zahyou - Camera_z) / (hisyatai2_Z - Camera_z) * (hisyatai2_Y - Camera_y); // 点Bのスクリーン投影のy座標
HBRUSH brasi_parts_2;
brasi_parts_2 = CreateSolidBrush(RGB(100, 100, 255)); // 壁の表示用のブルー色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
int hisyatai_onViewX = 200; int hisyatai_onViewY = 150;
Rectangle(hdc,
hisyatai_onViewX + pro1X , hisyatai_onViewY + pro1Y,
hisyatai_onViewX + pro2X , hisyatai_onViewY + pro2Y + 30); // 基準の状態
EndPaint(hWnd, &ps);
}
break;
case WM_KEYDOWN:
//if (game_mode == opening_mode) {
switch (wParam) {
case VK_UP: // 「上キーが入力されたら」という意味
// Camera_z_deffer = Camera_z_deffer -5;
hisyatai1_Z = hisyatai1_Z + 5;
hisyatai2_Z = hisyatai2_Z + 5;
break; // これは VK_UP: からのbreak
case VK_DOWN:
// Camera_z_deffer = Camera_z_deffer + 5;
hisyatai1_Z = hisyatai1_Z - 5;
hisyatai2_Z = hisyatai2_Z - 5;
break; // これは VK_DOWN: からのbreak
case VK_RIGHT:
/* ここに右(→)キー入力後の処理を書く*/
Camera_x = Camera_x + 10;
break; // これは VK_RIGHT からのbreak
case VK_LEFT:
/* ここに左(←)キー入力後の処理を書く*/
Camera_x = Camera_x - 10;
break; // これは VK_LEFT: からのbreak
} // これは switch (wParam) の終わりのカッコ
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
break; // これは WM_KEYDOWN: からのbreak
もし線分ABがスクリーンに平行なら(あくまで平行な場合だけに限る)、カメラをx座標をどんなにカニ歩きで動かしても、表示位置が左右に変わるだけで、大きさは一定のままである(中学レベルの相似計算で証明できる)。
裏を返せば、線分ABを平行にセットして、カメラをカニ歩きで左右に動かしてみたときに、もし線分ABの大きさが変わってしまえば、計算をミスってる。
構造体にしよう
[編集]このあとに回転処理を導入すると式が複雑になってくるので、まだ式の簡単な今のうちに、構造体を導入したコードにして構造化しておこう。
将来的に被写体の点の数が何十個や何百個にもなるので、配列を用いて効率化する必要がある。なので、ついでに構造体をもちいて、関連する変数もひとまとめにしてしまおう。
構造体配列にしておこう。(『C言語/構造体・共用体#構造体の配列』)
まず、冒頭のグローバル変数の領域に、下記のように構造体の宣言および構造体変数の宣言をしておこう。
struct point_zahyou
{
double x_zahyou;
double y_zahyou;
double z_zahyou;
}; // こっちは構造体の宣言
static struct point_zahyou point_zahyou_list[30]; // こっちは構造体変数の宣言。被写体の座標
static struct point_zahyou touei_zahyou_list[30]; // 構造体変数の宣言。投影面の座標
そしてさらに、エントリポイント wWinMain の「// TODO: ここにコードを挿入してください。」に、構造体変数の初期値を導入しておこう。構造体変数の初期値の代入は、グローバル領域では不可能な場合があるので、 wWinMain 側で代入することになる。
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: ここにコードを挿入してください。
point_zahyou_list[1].x_zahyou = hisyatai1_X; //数値で指定しても可能. 例では,すでに作成ずみの変数を流用した
point_zahyou_list[1].y_zahyou = hisyatai1_Y;
point_zahyou_list[1].z_zahyou = hisyatai1_Z;
point_zahyou_list[2].x_zahyou = hisyatai2_X;
point_zahyou_list[2].y_zahyou = hisyatai2_Y;
point_zahyou_list[2].z_zahyou = hisyatai2_Z;
そして、 case WM_PAINT: は上記の構造体を使えば下記のように書き換わる。
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
// int pro1X;
touei_zahyou_list[1].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[1].z_zahyou - Camera_z) * (hisyatai1_X - Camera_x);
// int pro1Y;
touei_zahyou_list[1].y_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[1].z_zahyou - Camera_z) * (hisyatai1_Y - Camera_y);
// int pro2X
touei_zahyou_list[2].x_zahyou = (screen_z_Zahyou - Camera_z) / (hisyatai2_Z - Camera_z) * (hisyatai2_X - Camera_x);
// int pro2Y
touei_zahyou_list[2].y_zahyou = (screen_z_Zahyou - Camera_z) / (hisyatai2_Z - Camera_z) * (hisyatai2_Y - Camera_y);
HBRUSH brasi_parts_2;
brasi_parts_2 = CreateSolidBrush(RGB(100, 100, 255)); // 壁の表示用のブルー色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
int hisyatai_onViewX = 200; int hisyatai_onViewY = 150;
Rectangle(hdc,
hisyatai_onViewX + touei_zahyou_list[1].x_zahyou, hisyatai_onViewY + touei_zahyou_list[1].y_zahyou,
hisyatai_onViewX + touei_zahyou_list[2].x_zahyou, hisyatai_onViewY + touei_zahyou_list[2].y_zahyou + 30); // 基準の状態
EndPaint(hWnd, &ps);
}
break;
上面図がデバッグに必要
[編集]デバッグのために、上面図が必要です。上空から見下ろした視点でのカメラ位置と、カメラ向きと、被写体の位置とを図示した、上面図(じょうめんず、top view)が必要です。
今度の私たちの自作3Dゲームプログラミングでこのあと回転機能などが加わり、動かし方が複雑になってきたりすると、上面図などの補助図の無い状態でデバッグを確認するのが、だんだん困難になってきます(あるいは、事実上は不可能か)。
上下方向をy成分とすれば、
つまり、(x,z)成分を表示すればいいわけです。
私たちの作る必要のある機能は単に、
- カメラ現在位置の(x,z)成分の表示
- カメラの向きの(x,z)成分図示
- 被写体の位置の(x,z)成分の図示
- スクリーンのz座標の図示
これらを、作成するアプリのウィンドウの空いてる場所にも表示すればイイだけです。
これらのプログラムの作成は簡単で、まずプログラムで矢印の作図プログラムでも書いておき、その矢印を、カメラ現在位置の変更にともなって矢印を移動させたり、カメラの向き変更にともなって、矢印の向きも同じ向きの同じ角度だけ回転させるプログラムを作ればいいだけです。
下記のような感じのコードを、WM_PAINT: 節に加えればいいだけです。
int jyoumen_x = 300; int jyoumen_z = 100; // 上面図の基準点
MoveToEx(hdc, jyoumen_x + Camera_x, jyoumen_z + Camera_z, NULL); // 矢軸の矢先側
LineTo(hdc, jyoumen_x + Camera_x, 150 + Camera_z); // 矢の尻
MoveToEx(hdc, jyoumen_x + Camera_x, jyoumen_z + Camera_z, NULL); // 矢先
LineTo(hdc, jyoumen_x + Camera_x+10, jyoumen_z + Camera_z+10);// 矢先の右側
MoveToEx(hdc, jyoumen_x + hisyatai_n1p1_X, jyoumen_z + hisyatai_n1p1_Z, NULL); // 被写体1の片側
LineTo(hdc, jyoumen_x + hisyatai_n1p2_X, jyoumen_z + hisyatai_n1p1_Z);
MoveToEx(hdc, jyoumen_x + hisyatai_n2p1_X, jyoumen_z + hisyatai_n2p1_Z, NULL); // 被写体11の片側
LineTo(hdc, jyoumen_x + hisyatai_n2p2_X, jyoumen_z + hisyatai_n2p1_Z);
MoveToEx(hdc, jyoumen_x + 150, jyoumen_z + screen_z_Zahyou, NULL); // スクリーンの片側
LineTo(hdc, jyoumen_x + 300, jyoumen_z + screen_z_Zahyou);
「上面図」とも言いますが、日本のゲーム業界的には和製英語で「マッパー」と言ったほうが、通じやすいかもしれません。なお、「マッパー」とは元々はRPGゲーム『女神転生』シリーズの魔法の名称で、3Dダンジョンの探検中に、プレイヤーに等角図でダンジョンの地図(マップ)と、現在地とを見せる画像を表示する機能のことです(もともと、そういうマップ補助を使える魔法の名前だった)。
どうせ私たちの自作ゲーム完成後にも、地図(マップ)の機能での現在地表示など流用するだろうから、とりあえず「マッパー」と呼んでおきましょう。
ちなみに古い1980年代のファミコン時代のウィザードリィや女神転生などの3Dダンジョンでの3D表示のアルゴリズムは、三角関数などを用いていない方式で、擬似3D(ぎじスリーディー)という手法ですので、私たちが今学ぼうとしている3Dプログラミングとは違う方式です。これはこれで、処理の負担軽減などには必要ですが、とりあえず、この節では擬似3Dについての解説は省略します。
なお、もしもフライトシミュレート的なゲームを作るなら、きっと上面図に加えて、さらに側面図の表示機能なども必要になるかもしれません。
なお、
被写体は2個以上でプログラムしよう
[編集]今までの説明では、説明の単純化のために被写体の個数を1個に限ってきたが、実際にデバッグする場合には、被写体を位置を変えて2個にしてみて、カメラを移動してみて、正しく見えるかを確認してみよう。
いちばん簡単な方法は、平行な2個の被写体で、x方向の位置を離してみて、
- カメラ例 1
□ 被写体1 □ 被写体2 ↑ カメラ
みたいに配置したとき、
たとえばカメラを右に移動して、
□ 被写体1 □ 被写体2 ↑ カメラ
被写体1と被写体2のあいだのx位置にカメラがあれば、当然に、2つの被写体は離れて見えるハズである。
- カメラ例 2
いっぽう、カメラを左に移動して、
□ 被写体1 □ 被写体2 ↑ カメラ
みたいな、一直線上に2個の被写体とカメラが配置される状態になった時は、当然、手前の被写体2によって、奥の被写体は隠れるハズである。
あなたの自作したプログラムがそうなるか、確認してみよう。
- カメラ例 3
もっとカメラを左にしてみて、さらにカメラを前方にすすめると
□ 被写体1 □ 被写体2 ↑ カメラ
今度は、さっきまで隠れていた被写体1が、すこし見えるようになる。
- コツ
上記のような配置例の暗黙の前提として、被写体の色をそれぞれ別個の色にしておいて、見て区別できるようにしておこう。大きさも、多少は違えておくと、区別しやすいかもしれない。
なお、コンピュータ処理では先に描画した画像は、あとから描画した画像によって上書きされる。なので、上記のカメラ例の配置の場合には、奥にある被写体1のほうを先に描画する必要がある。
- まとめ
ここまでの説明を、コードにすると、下記のようになるでしょう。まだ回転は導入してない状態です。
- コード例
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: HDC を使用する描画コードをここに追加してください...
/* ここから上面図の描画コード */
int jyoumen_x = 300; int jyoumen_z = 100; // 上面図(マッパー)の基準点
MoveToEx(hdc, jyoumen_x + Camera_x, jyoumen_z + Camera_z, NULL); // 矢軸の矢先側
LineTo(hdc, jyoumen_x + Camera_x, 150 + Camera_z); // 矢の尻
MoveToEx(hdc, jyoumen_x + Camera_x, jyoumen_z + Camera_z, NULL); // 矢先
LineTo(hdc, jyoumen_x + Camera_x+10, jyoumen_z + Camera_z+10);// 矢先の右側
MoveToEx(hdc, jyoumen_x + hisyatai_n1p1_X, jyoumen_z + hisyatai_n1p1_Z, NULL); // 被写体1の片側
LineTo(hdc, jyoumen_x + hisyatai_n1p2_X, jyoumen_z + hisyatai_n1p1_Z);
MoveToEx(hdc, jyoumen_x + hisyatai_n2p1_X, jyoumen_z + hisyatai_n2p1_Z, NULL); // 被写体11の片側
LineTo(hdc, jyoumen_x + hisyatai_n2p2_X, jyoumen_z + hisyatai_n2p1_Z);
MoveToEx(hdc, jyoumen_x + 150, jyoumen_z + screen_z_Zahyou, NULL); // スクリーンの片側
LineTo(hdc, jyoumen_x + 300, jyoumen_z + screen_z_Zahyou);
/* ここから投影の座標計算のコード */
// 投影面の内での座標
// pro1 // 被写体1の第1点
touei_zahyou_list[1].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[1].z_zahyou - Camera_z) * (point_zahyou_list[1].x_zahyou - Camera_x) ;
touei_zahyou_list[1].y_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[1].z_zahyou - Camera_z) * (point_zahyou_list[1].y_zahyou - Camera_y) ;
// pro2X // 被写体1の第2点
touei_zahyou_list[2].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[2].z_zahyou - Camera_z) * (point_zahyou_list[2].x_zahyou - Camera_x) ;
touei_zahyou_list[2].y_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[2].z_zahyou - Camera_z) * (point_zahyou_list[2].y_zahyou - Camera_y) ;
// pro3 // 被写体2の第1点
touei_zahyou_list[3].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[3].z_zahyou - Camera_z) * (point_zahyou_list[3].x_zahyou - Camera_x) ;
touei_zahyou_list[3].y_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[3].z_zahyou - Camera_z) * (point_zahyou_list[3].y_zahyou - Camera_y) ;
// pro4 // 被写体2の第2点
touei_zahyou_list[4].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[4].z_zahyou - Camera_z) * (point_zahyou_list[4].x_zahyou - Camera_x) ;
touei_zahyou_list[4].y_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[4].z_zahyou - Camera_z) * (point_zahyou_list[4].y_zahyou - Camera_y) ;
/* ここから視界画像の描画処理のコード */
HBRUSH brasi_parts_2;
int hisyatai_onViewX = 350; int hisyatai_onViewY = 150;
// 奥の被写体は緑色にした
brasi_parts_2 = CreateSolidBrush(RGB(100, 255, 100)); // 壁の表示用の緑色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
Rectangle(hdc,
hisyatai_onViewX + touei_zahyou_list[3].x_zahyou, hisyatai_onViewY + touei_zahyou_list[3].y_zahyou,
hisyatai_onViewX + touei_zahyou_list[4].x_zahyou, hisyatai_onViewY + touei_zahyou_list[4].y_zahyou + 40); // 基準の状態
// 手前の被写体はピンクにした
brasi_parts_2 = CreateSolidBrush(RGB(100, 100, 255)); // 壁の表示用のブルー色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
Rectangle(hdc,
hisyatai_onViewX + touei_zahyou_list[1].x_zahyou, hisyatai_onViewY + touei_zahyou_list[1].y_zahyou,
hisyatai_onViewX + touei_zahyou_list[2].x_zahyou, hisyatai_onViewY + touei_zahyou_list[2].y_zahyou + 30); // 基準の状態
brasi_parts_2 = CreateSolidBrush(RGB(255, 100, 100)); // 壁の表示用のピンク色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
EndPaint(hWnd, &ps);
}
break;
case WM_KEYDOWN:
switch (wParam) {
case VK_UP: // 「上キーが入力されたら」という意味
Camera_z = Camera_z -5;
screen_z_Zahyou = screen_z_Zahyou - 5;
break; // これは VK_UP: からのbreak
case VK_DOWN:
Camera_z = Camera_z + 5;
screen_z_Zahyou = screen_z_Zahyou + 5;
break; // これは VK_DOWN: からのbreak
case VK_RIGHT:
Camera_x = Camera_x + 10;
break; // これは VK_RIGHT からのbreak
case VK_LEFT:
Camera_x = Camera_x - 10;
break; // これは VK_LEFT: からのbreak
} // これは switch (wParam) の終わりのカッコ
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
break; // これは WM_KEYDOWN: からのbreak
このほかグローバル変数に必要な構造体および構造体変数の定義をしておいてください。また、 wWinMain 節などに、それぞれの構造体変数の初期値を代入しておいてください。
回転させよう
[編集]線形代数
[編集]- ※ タイトルは「線形代数」だが、内容は高校レベルの「行列」の初歩であり、座標幾何学の内容である。タイトルが「行列」だと、人などの並びと誤解されかねないので、あえて学術的に「線形代数」とした。
回転をさせるには、カメラを右方向にある角度だけ回転させた場合、視界内での被写体は逆方向である左方向に回転することになるので、つまり視界内のすべての被写体のすべての点はすべてカメラを中心に逆方向である左方向に公転することになる。
計算の簡単化のために、カメラの向きを変えた場合の視界の計算法では、カメラの代わりに被写体を逆方向に公転させるアルゴリズムに置き換えたほうが、アルゴリズムが簡単に設計できる。
ベクトル計算などを用いてカメラの向き角度と被写体の角度との差し引きの計算アルゴリズムを考えるよりも、カメラの向きを固定して代わりに被写体を逆方向に公転させるほうが計算がラクである。
投影してから公転した結果と、公転してから公転した結果とでは、計算結果が違ってしまう。(証明は、たとえば2次元に簡略化して、作図すれば分かる。)
(数学の理論としては、大学1年くらいで習う行列の理論に対応する。行列の積(かけ算のこと)の計算順序の交換は一般に、交換すると計算結果が変わってしまう。行列AとBで、積ABと積BAは異なる行列である。普遍的な場合の、幾何学的な操作に対応する行列の順序交換の証明のレベルは、大学1~2年のレベルになるので、このwikiの当ページでは証明を省略する。直交行列などの交換の問題になる。)
必ず先に被写体を公転させてから、あとからスクリーンに投影すること。
プログラムでアルゴリズムを書くときも先に公転させてから、あとから投影させることになる。
y軸を中心に回転させるとき、座標である点の位置の変わる座標軸成分は、x座標成分とz座標成分である。
数学の理論により回転行列の公式は
である(『高等学校数学C/行列#回転行列』)。
実際にこの公式を3D-CGの回転計算で使うときは、原点を中心とした回転の公式
の形で使うだろう。
または、
の形で使うだろう。
y軸を中心にした回転(公転)の場合、x軸成分とz軸成分とが移動するので、式は、移動後の(x,z)座標の位置は
になる。
さらに、回転(公転)の中心になる座標は、カメラ位置の座標なので、カメラ位置の(x,y,z)座標成分をそれぞれ xc, yc ,zc とすると、移動後の(x,z)座標の位置は
である。
しかし視界を決めるのは、投影面上の座標のカメラからの相対的な位置であるので、視界は
の式で決まる。
- 特別な場合
さて、私たちは初期のカメラの向きを、画面中で真上の向き(↑)にしている場合、このときは初期値を となり、カメラ位置xcについて、 になるので、したがって、この場合には、回転の公式は
となる。
さて、もしz軸の向きを画面の下向きにとれば、カメラの向きが初期値で上向きの場合には
である。なので、
が、このような場合(z軸の向きが画面下向き。カメラが上向きの場合)での回転の公式になる。
なお、べつにプログラム中の変換式をこの の場合の公式に置き換えなくても構わない。回転行列の公式を使えば、条件が上述のとおりなら、結果的にこの場合と同じ結果になるということである。
上面図の回転のプログラムは、(抜粋すると)下記のようになる。WM_PAINT 節の適切な場所に、下記のように書く。
// sin() と cos() は sincos() で同時に求められ、こちらの方が低レイテンシ
double sin_kaitenKaku = 0.0, cos_kaitenKaku = 0.0;
sincos(kaitenKaku, &sin_kaitenKaku, &cos_kaitenKaku);
// y軸中心での回転後の座標の計算
kaiten_arrow_x = cos_kaitenKaku * (0) + (-1) * sin_kaitenKaku * (-1 * arrowLength);
// kaiten_zahyou_list[1].y_zahyou = point_zahyou_list[1].y_zahyou; // 中心軸なので、そのまま。
kaiten_arrow_z = sin_kaitenKaku * (0) + cos_kaitenKaku * (-1 * arrowLength);
// 回転する上面図のデバッグ用の描画プログラム
int offsetRotX = 150; // 比較のため回転前の矢印も残したいので、位置をx方向に150だけズラした
MoveToEx(hdc, offsetRotX + arrowSiri_X + kaiten_arrow_x, arrowSiri_Z + kaiten_arrow_z , NULL); // 矢先側
LineTo(hdc, offsetRotX + arrowSiri_X, arrowSiri_Z ); // 矢の尻
// 矢印先端の三角部分を描くのがメンドウなので、
// 代わりに矢の根元(カメラ側)にミドリ色の四角を描画することで代用
HBRUSH brasi_parts_2;
brasi_parts_2 = CreateSolidBrush(RGB(100, 255, 100)); // 壁の表示用のgreen色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
なお、あらかじめグローバル変数で回転角を宣言しておく必要がある。C言語の角度計算ではラジアン単位なので、角度は実数型で宣言する必要がある。
なので double kaitenKaku;
のように宣言する必要がある。
また、この回転角を増減するためのキーボード入力が必要なので、 case WM_KEYDOWN: に、たとえば下記のようなコードを追加する必要がある。(原理的にはマウス入力でもいいが、教えるのがメンドウくさい。)
case 'W':
/* 反時計まわりの回転*/
kaitenKaku = kaitenKaku + 0.2;
break; // これは VK_LEFT: からのbreak
case 'Q':
/* 順時計まわりの回転*/
kaitenKaku = kaitenKaku - 0.2;
break; // これは VK_LEFT: からのbreak
なので、上下左右のキー入力コードとあわせると、下記のようになる。
case WM_KEYDOWN:
switch (wParam) {
case VK_UP: // 「上キーが入力されたら」という意味
Camera_z = Camera_z -5;
screen_z_Zahyou = screen_z_Zahyou - 5;
/* ここに上(↑)キー入力後の処理を書く*/
break; // これは VK_UP: からのbreak
case VK_DOWN:
Camera_z = Camera_z + 5;
screen_z_Zahyou = screen_z_Zahyou + 5;
/* ここに下(↓)キー入力後の処理を書く*/
break; // これは VK_DOWN: からのbreak
case VK_RIGHT:
/* ここに右(→)キー入力後の処理を書く*/
Camera_x = Camera_x + 10;
break; // これは VK_RIGHT からのbreak
case VK_LEFT:
/* ここに左(←)キー入力後の処理を書く*/
Camera_x = Camera_x - 10;
break; // これは VK_LEFT: からのbreak
case 'W':
/* 反時計まわりの回転*/
kaitenKaku = kaitenKaku + 0.2;
break; // これは VK_LEFT: からのbreak
case 'Q':
/* 反時計まわりの回転*/
kaitenKaku = kaitenKaku - 0.2;
break; // これは VK_LEFT: からのbreak
} // これは switch (wParam) の終わりのカッコ
//}
// MessageBox(NULL, TEXT("aaaaaatにいる。"), TEXT("キーテスト"), MB_OK);
InvalidateRect(hWnd, NULL, TRUE);
UpdateWindow(hWnd);
break; // これは WM_KEYDOWN: からのbreak
さて、上面図の回転のテストに成功したら、同じ方法で、視界中の被写体も公転させればいい。視界を回転させるためには、カメラの自転ではなく、被写体すべての点を公転させたほうが(人間プログラマー側の検証のための)計算がラクである。
視界の回転のコード例は、下記のようになる。
double sin_kaitenKaku = 0.0, cos_kaitenKaku = 0.0;
sincos(kaitenKaku, &sin_kaitenKaku, &cos_kaitenKaku);
// 先に被写体を回転させる
kaiten_zahyou_list[1].x_zahyou = cos_kaitenKaku * (point_zahyou_list[1].x_zahyou - Camera_x) + (-1) * sin_kaitenKaku * (point_zahyou_list[1].z_zahyou - Camera_z) + Camera_x;
kaiten_zahyou_list[1].y_zahyou = point_zahyou_list[1].y_zahyou ;
kaiten_zahyou_list[1].z_zahyou = sin_kaitenKaku * (point_zahyou_list[1].x_zahyou - Camera_x) + cos (kaitenKaku) * (point_zahyou_list[1].z_zahyou - Camera_z) + Camera_z;
kaiten_zahyou_list[2].x_zahyou = cos_kaitenKaku * (point_zahyou_list[2].x_zahyou - Camera_x) + (-1) * sin_kaitenKaku * (point_zahyou_list[2].z_zahyou - Camera_z) + Camera_x ;
kaiten_zahyou_list[2].y_zahyou = point_zahyou_list[2].y_zahyou;
kaiten_zahyou_list[2].z_zahyou = sin_kaitenKaku * (point_zahyou_list[2].x_zahyou - Camera_x) + cos_kaitenKaku * (point_zahyou_list[2].z_zahyou - Camera_z) + Camera_z;
kaiten_zahyou_list[3].x_zahyou = cos_kaitenKaku * (point_zahyou_list[3].x_zahyou - Camera_x) + (-1) * sin_kaitenKaku * (point_zahyou_list[3].z_zahyou - Camera_z) + Camera_x;
kaiten_zahyou_list[3].y_zahyou = point_zahyou_list[3].y_zahyou;
kaiten_zahyou_list[3].z_zahyou = sin_kaitenKaku * (point_zahyou_list[3].x_zahyou - Camera_x) + cos_kaitenKaku * (point_zahyou_list[3].z_zahyou - Camera_z) + Camera_z;
kaiten_zahyou_list[4].x_zahyou = cos_kaitenKaku * (point_zahyou_list[4].x_zahyou - Camera_x) + (-1) * sin_kaitenKaku * (point_zahyou_list[4].z_zahyou - Camera_z) + Camera_x;
kaiten_zahyou_list[4].y_zahyou = point_zahyou_list[4].y_zahyou;
kaiten_zahyou_list[4].z_zahyou = sin_kaitenKaku * (point_zahyou_list[4].x_zahyou - Camera_x) + cos_kaitenKaku * (point_zahyou_list[4].z_zahyou - Camera_z) + Camera_z;
// 被写体の回転のあとから、スクリーンに投影する
//int pro1
touei_zahyou_list[1].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[1].z_zahyou - Camera_z) * (kaiten_zahyou_list[1].x_zahyou - Camera_x) ;
touei_zahyou_list[1].y_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[1].z_zahyou - Camera_z) * (kaiten_zahyou_list[1].y_zahyou - Camera_y) ;
// int pro2X
touei_zahyou_list[2].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[2].z_zahyou - Camera_z) * (kaiten_zahyou_list[2].x_zahyou - Camera_x) ;
touei_zahyou_list[2].y_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[2].z_zahyou - Camera_z) * (kaiten_zahyou_list[2].y_zahyou - Camera_y) ;
// int pro3
touei_zahyou_list[3].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[3].z_zahyou - Camera_z) * (kaiten_zahyou_list[3].x_zahyou - Camera_x) ;
touei_zahyou_list[3].y_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[3].z_zahyou - Camera_z) * (kaiten_zahyou_list[3].y_zahyou - Camera_y) ;
// int pro4
touei_zahyou_list[4].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[4].z_zahyou - Camera_z) * (kaiten_zahyou_list[4].x_zahyou - Camera_x) ;
touei_zahyou_list[4].y_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[4].z_zahyou - Camera_z) * (kaiten_zahyou_list[4].y_zahyou - Camera_y) ;
// HBRUSH brasi_parts_2;
int hisyatai_onViewX = 350; int hisyatai_onViewY = 150;
brasi_parts_2 = CreateSolidBrush(RGB(100, 255, 100)); // 壁の表示用のgreen色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
Rectangle(hdc,
hisyatai_onViewX + touei_zahyou_list[3].x_zahyou, hisyatai_onViewY + touei_zahyou_list[3].y_zahyou,
hisyatai_onViewX + touei_zahyou_list[4].x_zahyou, hisyatai_onViewY + touei_zahyou_list[4].y_zahyou + 40); // 基準の状態
brasi_parts_2 = CreateSolidBrush(RGB(100, 100, 255)); // 壁の表示用のブルー色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
Rectangle(hdc,
hisyatai_onViewX + touei_zahyou_list[1].x_zahyou, hisyatai_onViewY + touei_zahyou_list[1].y_zahyou,
hisyatai_onViewX + touei_zahyou_list[2].x_zahyou, hisyatai_onViewY + touei_zahyou_list[2].y_zahyou + 30); // 基準の状態
被写体の投影の式でも、式
- touei_zahyou_list[1].x_zahyou = (screen_z_Zahyou - Camera_z) / (point_zahyou_list[1].z_zahyou - Camera_z) * (kaiten_zahyou_list[1].x_zahyou - Camera_x) ;
で、 係数の分母で「point_zahyou_list[1].z_zahyou - Camera_z」というふうに、回転前の座標位置 point_zahyou_list[1].z_zahyou を参照しているが、けっしてこれはミスではなく、平面投影では、このように回転前の座標値を係数に代入しなければならない。
(人間の目に近いと思われる)円柱投影や球面投影では、回転前の座標値を参照して投影するのは不合理だが、しかし平面投影では、むしろ、このように回転前の座標値を係数に代入しないと不合理になってしまう(試せばワカル)。理由を簡単に言うと、拡大率が平面投影では、カメラからの距離ではなく、スクリーンからの距離で決まるので、拡大率を一定するためにはスクリーンからの距離を一定にする必要がある。
なお、このプログラムだとカメラの反対側にある被写体も描画してしまう。なので、それら反対側の描画をカットする機能を、今後のプログラミングで追加する必要がある。
描画カットの方法は簡単で、単に被写体の回転後のz座標がマイナスになる場合には、描画しないように、if文で記述すればいい。
描画のrectangle命令を、下記のように if 文で書き換えればいい。つまり、描画する場合の条件として、回転後のz座標 カメラ座標 の数値がプラスの場合にだけ描画するようにプログラミングすればいい。
if (kaiten_zahyou_list[3].z_zahyou <= Camera_z) {
Rectangle(hdc,
hisyatai_onViewX + touei_zahyou_list[3].x_zahyou, hisyatai_onViewY + touei_zahyou_list[3].y_zahyou,
hisyatai_onViewX + touei_zahyou_list[4].x_zahyou, hisyatai_onViewY + touei_zahyou_list[4].y_zahyou + 40); // 基準の状態
}
brasi_parts_2 = CreateSolidBrush(RGB(100, 100, 255)); // 壁の表示用のブルー色のブラシを作成
SelectObject(hdc, brasi_parts_2); // ウィンドウhdcと、さきほど作成したブラシを関連づけ
if (kaiten_zahyou_list[1].z_zahyou <= Camera_z) {
Rectangle(hdc,
hisyatai_onViewX + touei_zahyou_list[1].x_zahyou, hisyatai_onViewY + touei_zahyou_list[1].y_zahyou,
hisyatai_onViewX + touei_zahyou_list[2].x_zahyou, hisyatai_onViewY + touei_zahyou_list[2].y_zahyou + 30); // 基準の状態
}
この方法でも、カメラが被写体に移りこんだ場合も正しく描画できる(理由は後述する)。式変形をして内積計算に変形もできる。
では、なぜこのように被写体の裏側にカメラの回りこんだ場合ですらも、上記のif判定式によってカメラが被写体の裏側に回り込んだ場合でも、正しく判定できるのか。
その理由は、不等式
- kaiten_zahyou_list[3].z_zahyou <= Camera_z
を式変形すると、
- (kaiten_zahyou_list[3].z_zahyou - Camera_z ) * 1 <= 0
となり、さらに変形して
- (kaiten_zahyou_list[3].z_zahyou - Camera_z ) * (-1) >= 0
となり、最終的にベクトルの内積の形に変形できて、
- (kaiten_zahyou_list[3].z_zahyou - Camera_z ) * (-1) + (kaiten_zahyou_list[3].x_zahyou - Camera_x ) * (0) >= 0
と式変形できる。2項目は0で掛け算しているので、2項目の積は0である。
最後の4番目の式は、これはベクトルの内積であり、
- ベクトル (x,y) = (kaiten_zahyou_list[3].z_zahyou - Camera_z , kaiten_zahyou_list[3].x_zahyou - Camera_x) つまりカメラ位置ベクトルから被写体の公転後の位置ベクトルとの差分ベクトルと、
- カメラの向きベクトル(-1,0)との内積である(カメラを時点させた場合には、代わりに被写体を公転させるように設計したため、このような計算ではカメラの向きベクトルは(-1,0)になる)。
要するに、ベクトルの内積の符号の正負を使って、描画するか否かを判定しているだけにすぎない。
望遠と広角のハナシ
[編集]平面投影の特徴として、回転体の拡大縮小比率が不正確である。
たとえば、構図A
□ 被写体 -----------------------------スクリーン ↑ カメラ
と、構図B
□ 被写体 -----------------------------スクリーン ↑ カメラ
とでは、被写体がスクリーンにより近くにある構図Bのほうが、大きく見えてしまう。
なぜなら、(中学レベルの)相似比によって拡大縮小率が決まるので、つまりスクリーンとの距離によって拡大縮小率が決まるからである。
現実の人間の視界では、どう考えても、カメラから遠くにある被写体は小さく見えるハズであるが、しかし、平面投影では(カメラとの距離ではなく)、スクリーンとの距離によって拡大縮小率が決まる。
このように、平面投影では、実際の人間の視界とは異なる。
しかし、それでも私たちは、あえて平面投影を使うべきである。球面投影に対応するPC映像デバイスは無いし、円柱投影では縦横比が不正確になるからである(そのため、円柱投影は視界内にある回転体や球体・円盤などを描写するのが難しい)。
なお、平面投影を補正しようとしても無駄、あるいは補正にすらも欠点がある。
たとえば容易に思いつく案としては、「カメラ前方の視界中心にある被写体を、中心に近いほど拡大表示させよう」とするアルゴリズムの導入だろうが、これには下記のような欠点がある。
□ □ -----------------------------スクリーン ↑ カメラ
欠点とは、もし2個以上の被写体が近くどうしで並びあってる場合に、もし中心側の被写体を拡大したとすると、中心に近い側の被写体によって、その隣の被写体が隠される現象が発生しうる(現実の人間の視界では、そんな現象は起こりえない)。
結局、どうあがいても、平面投影では、どこかの描写が不正確になる。
ならば、視界の端っこに、不正確な部分を寄せたほうがイイ。好都合なことに、平面投影では、視界の中心ちかくは比較的に正確である。平面投影で不正確になるのは、視界の端っこである。
なので、平面投影のアルゴリズムは、あまり余計にイジラナイでいるほうが安全だし、管理もラクである。
地図の投影法の問題と同じで、本来は人間の二つの目による視界は球面投影に近いと考えられるので、たとえば地球儀を地図に置き換えたら原理的に不正確になるように、球面投影の視界を平面投影あるいは円柱投影に置き換えたら、どうあがいても不正確になる。
なので、あまり視野角を大きくしすぎないほうがイイ。
どうしても大きな視野が必要な場合は、対策として例えば望遠鏡のように、遠くから眺めるようプログラムにして、視野角そのものは広げないようにしたほうがイイだろう。
なお、テレビ局用などのレンズ付き撮影ビデオカメラなどでも、望遠的なレンズを使って遠くから被写体を写す方式である(実際に望遠レンズかどうかは知らないが、すくなくても決して広角レンズではない)。(よく、テレビ番組などの制作裏話などの紹介の番組などで、スタジオセットの撮影で、セットから結構離れた場所でカメラマンが やや大きい撮影カメラで撮影しているシーンがときどきがあるだろう。バズーカ砲みたいに肩に担いだり、あるいはカメラを荷台に載せて撮影するカメラをもってるアレ。あの距離みたいに、実際の撮影でも、けっこうカメラマンは離れている。)
人間の目を、実物のカメラですら(人間の目を完全に)再現するのは、原理的に無理なので、カメラですらも、望遠的に距離を離すことによって視野角を小さめにして、誤差を縮小している。
なお、ポケットサイズのカメラのレンズは通常、広角レンズである。片手で持てるようなハンディサイズのカメラも通常、広角レンズである。
なので、望遠と間違えて広角で撮影されたコンテンツを参考にしないように。
望遠レンズによる撮影は、望遠カメラを配置できるようなスタジオを借りて撮影する必要があるので予算がけっこう大きくかかっているので(テレビ局って、けっこう凄いんですよ)、いっぽう低予算の業界たとえばアダルトビデオ産業などでは広角レンズによる撮影の場合も多い。[2]
なお広角レンズは、その名のとおり視野角の広いレンズで、これはこれで、視野を広く撮影したい場合(たとえば団体旅行などでの記念写真など)には必要であろう。
レンズには視野角に種類があることに気をつけるようしよう。
なお、説明の都合上、「視野角」と言ったが、テレビなどの撮影業界では「画角」(がかく)という。