
マイケル
みなさんこんにちは!
まいけるです!
まいけるです!

エレキベア
お久しぶりクマ〜〜〜

マイケル
今回は前回に引き続きC++で3Dゲームを作る予定でしたが・・・
↑前回の記事

マイケル
3Dゲーム開発はとにかく覚えることが多い!
基本処理を作りながら、どうまとめるかでずっと悩んでいました…
基本処理を作りながら、どうまとめるかでずっと悩んでいました…

エレキベア
一からだとそりゃそうなるクマ・・・

マイケル
というわけで、下記のように追加で2回に分けて
3Dゲーム開発基礎編を書くことにしました!
3Dゲーム開発基礎編を書くことにしました!
[今後の予定]
第三回 3Dゲーム開発基礎 OpenGLと座標変換編
第四回 3Dゲーム開発基礎 fbx読込とシェーダ編
第五回 3Dゲーム開発編(予定)

エレキベア
しばらく座学クマか〜〜〜

マイケル
今回は下記図の緑色の範囲を解説していきます!
モデル読込やシェーダの話は次回に回して、
主に全体の流れと座標変換周りを対象としています。
モデル読込やシェーダの話は次回に回して、
主に全体の流れと座標変換周りを対象としています。
↑今回の記事の範囲

マイケル
下記にサンプルコードも用意しています!
3Dモデルを描画するだけですが、勉強しながらなのもあり
1ヶ月近くかかっています・・・
3Dモデルを描画するだけですが、勉強しながらなのもあり
1ヶ月近くかかっています・・・
↑数種類のシェーダ描画も行なっているがそれは次回解説予定である

エレキベア
おつかれさまクマ・・・
参考書籍

マイケル
基本的に理論や考え方は下記書籍をベースに作成しています。

エレキベア
これまでも使っていたクマね

マイケル
そしてところどころ下記2冊とも照らし合わせながら
整理して実装しました!
整理して実装しました!

マイケル
どの本もゲーム開発の基本が
広く書かれているためおすすめです!
広く書かれているためおすすめです!

エレキベア
読み応えがありそうクマ〜〜〜
ソース全体構成

マイケル
作成したプロジェクトの構成としては
下記のようになっています。
下記のようになっています。

マイケル
2Dゲーム開発の時と基本構成は同じですが、
3Dモデル用のクラスを複数作成しています!
また、描画関連の処理に関しては Rendererクラス に分割しました!
3Dモデル用のクラスを複数作成しています!
また、描画関連の処理に関しては Rendererクラス に分割しました!

エレキベア
多くはなったが必要最低限って感じクマね
3Dグラフィックスの基本

マイケル
3DグラフィックスはSDLでは扱えないため、
OpenGL と組み合わせることで3Dモデルを描画します。
そのためまずは座標変換の話に入る前に、
そのようなグラフィックスライブラリを使った3D描画の考え方を解説します!
OpenGL と組み合わせることで3Dモデルを描画します。
そのためまずは座標変換の話に入る前に、
そのようなグラフィックスライブラリを使った3D描画の考え方を解説します!

エレキベア
ついに3Dグラフィックスクマ〜〜〜
頂点座標と三角ポリゴン

マイケル
まずSDLとの描画と比較して大きく考え方が異なる点があります。
それは
1. 頂点ごとに座標情報を持つ
2. ポリゴン単位で描画する
の2点になります。
それは
1. 頂点ごとに座標情報を持つ
2. ポリゴン単位で描画する
の2点になります。
頂点ごとに座標情報を持つ

マイケル
位置座標のみで描画できていたこれまでとは異なり、
3Dグラフィックスでは頂点ごとに座標情報を持たせるのが基本となります。
3Dグラフィックスでは頂点ごとに座標情報を持たせるのが基本となります。
↑(左)全体の位置座標のみ (右)頂点ごとの位置座標

エレキベア
それは急に複雑になりそうクマね

マイケル
今回は位置座標のみ持たせているけど、
法線やUV座標を持たせることもできるよ!
これらはライティングに必要な情報となるので、次回解説していきます!
法線やUV座標を持たせることもできるよ!
これらはライティングに必要な情報となるので、次回解説していきます!
ポリゴン単位で描画する

マイケル
そしていくつかの頂点座標のまとまりごとに、
ポリゴン単位での描画 を行います!
ポリゴン単位での描画 を行います!

マイケル
この考えは多角形のポリゴンにも適用できますが、
基本となるのは三角ポリゴンでの描画です。
例えば、2D描画の場合は下記のように
2つのポリゴンに分割して描画処理を行います。
基本となるのは三角ポリゴンでの描画です。
例えば、2D描画の場合は下記のように
2つのポリゴンに分割して描画処理を行います。
↑2つの三角ポリゴンに分けて描画する

エレキベア
2Dの描画でも分ける必要があるクマね〜〜

マイケル
確かに2Dで分割するのは手間に感じてしまうかもしれないね。
でもこれは 3D描画にとても効率的な手段 で、
下記のように 全て三角ポリゴンに分割することで、
大量の計算をせずに描画することができる んだ!
でもこれは 3D描画にとても効率的な手段 で、
下記のように 全て三角ポリゴンに分割することで、
大量の計算をせずに描画することができる んだ!
↑3Dグラフィクスのポリゴン分割

エレキベア
この情報を全ての3Dモデルが持っているなんて壮大クマ〜〜〜
頂点バッファとインデックスバッファ

マイケル
次はこの座標情報をどのように渡すのかについて!
下記はOpenGLの例ですが、画面の座標情報は デカルト座標系といって、
-1〜+1の範囲 で持っています。
下記はOpenGLの例ですが、画面の座標情報は デカルト座標系といって、
-1〜+1の範囲 で持っています。
↑OpenGLの座標情報(デカルト座標系)

マイケル
そしてこれは画面の大きさに関係なくこの範囲で決まっているので、
全て0.5で指定したとしても、画面比率によって大きさは変動します。
全て0.5で指定したとしても、画面比率によって大きさは変動します。
↑画面比率によって大きさは変動する

エレキベア
最終的には座標位置を計算して渡さないといけないクマね

マイケル
その通り!
そして上の図を見て感づいた方もいるかもしれませんが、
三角ポリゴンを描画するにあたっては同じ頂点を何度か指定 することになります。
そして上の図を見て感づいた方もいるかもしれませんが、
三角ポリゴンを描画するにあたっては同じ頂点を何度か指定 することになります。

マイケル
そのため、下記のように各頂点に番号を振って管理することで、
バッファする情報を削減 します。
バッファする情報を削減 します。
↑各頂点に番号を割り振る(インデックスバッファ)

マイケル
このように振った番号でポリゴン描画を持ったものを インデックスバッファ といい、
対して各頂点の座標情報は 頂点バッファ と呼びます。
この四角形の例をソースコードで表すと下記のようにして渡します!
対して各頂点の座標情報は 頂点バッファ と呼びます。
この四角形の例をソースコードで表すと下記のようにして渡します!

マイケル
これで設定した情報がシェーダに渡されて、描画処理を行うわけです!

エレキベア
なんとなく仕組みが分かってきたクマ〜〜
シェーダ

マイケル
そしてシェーダでは受け取った座標情報を元に計算を行い、描画を行います。
頂点シェーダ、フラグメントシェーダ というように役割が分かれているのですが、
これは次回詳しく解説していきます!
頂点シェーダ、フラグメントシェーダ というように役割が分かれているのですが、
これは次回詳しく解説していきます!
↑シェーダの描画プロセス

マイケル
例として、全面で青色で出力するだけの
シンプルなプログラム例を描きに載せておきます!
シンプルなプログラム例を描きに載せておきます!

エレキベア
座標計算と色合い調整で分かれているのクマね
GLEWの使用

マイケル
これらの機能をSDL+OpenGLの組み合わせで実装しましたが、
GLEWというOpenGLの拡張ライブラリを使用することで簡単に機能を有効化
することができます。
GLEWというOpenGLの拡張ライブラリを使用することで簡単に機能を有効化
することができます。

マイケル
下記はサンプルプロジェクトの初期化例になります。
それぞれ glewInitでGLEWの初期化、SDL_GL_SetAttributeで描画の設定
を行なっています。
それぞれ glewInitでGLEWの初期化、SDL_GL_SetAttributeで描画の設定
を行なっています。
↑OpenGL機能の初期化

エレキベア
OpenGLを簡単に使えるようにするライブラリクマね
Zバッファとアルファブレンド

マイケル
そして最後に補足となりますが、
3D描画ではZバッファという仕組みを利用して
手前にある座標のみ描画するようにしています。
3D描画ではZバッファという仕組みを利用して
手前にある座標のみ描画するようにしています。

マイケル
これは便利な反面、半透明なオブジェクトは使用できないといった問題点もあります。
そのため、
3D描画ではZバッファ有効化&アルファブレンド無効化、
2D描画ではZバッファ無効化&アルファブレンド有効化
と切り替えて描画処理を行なっています。
そのため、
3D描画ではZバッファ有効化&アルファブレンド無効化、
2D描画ではZバッファ無効化&アルファブレンド有効化
と切り替えて描画処理を行なっています。
↑Zバッファ、アルファブレンドの有効切替

マイケル
これで基本的な流れとしては以上になります!

エレキベア
ざっくりと分かった気はするクマ〜〜・・・
座標変換

マイケル
基本の流れがわかったところで、座標変換の解説に入っていきます!

エレキベア
なんだか難しそうクマ〜〜〜
座標変換の考え方

マイケル
座標変換はざっくりいうと、
モデル自体の頂点座標(ローカル座標)から
-1〜+1までの描画する座標(クリップ座標)に変換するまでの行程
のことなんだ!
モデル自体の頂点座標(ローカル座標)から
-1〜+1までの描画する座標(クリップ座標)に変換するまでの行程
のことなんだ!
↑座標変換のプロセス

マイケル
ローカル座標→ワールド座標→クリップ座標と変換していき、
下記は図で例を表したものになります。
(あくまでイメージで座標値についてはこの通りになるわけではないです。)
下記は図で例を表したものになります。
(あくまでイメージで座標値についてはこの通りになるわけではないです。)

マイケル
まず モデルを読み込んだ時の頂点座標 は下記のように、
オブジェクト自体の位置(原点)を0,0として位置が決まっています。
オブジェクト自体の位置(原点)を0,0として位置が決まっています。
↑ローカル座標のイメージ

マイケル
そしてそのオブジェクトは ワールド上の任意の位置に移動させたり、
回転・拡大縮小を行った際に各頂点の座標も変化 することになります。
これをワールド座標といいます。
回転・拡大縮小を行った際に各頂点の座標も変化 することになります。
これをワールド座標といいます。
↑ワールド座標のイメージ

マイケル
変換した座標は最終的に画面に出力するため、
-1〜+1の範囲に変換する必要があります。
このような画面に出力するための座標を クリップ座標 といいます。
-1〜+1の範囲に変換する必要があります。
このような画面に出力するための座標を クリップ座標 といいます。
↑クリップ座標のイメージ

マイケル
このように出力するためには座標情報を変換していく必要があるわけです。

エレキベア
長い道のりクマね〜〜〜〜

マイケル
そしてこの計算を行うには数学で出てきた 行列式(Matrix) を使用することで、
非常に計算しやすくなります。
3Dグラフィックスでは x,y,zの情報にwを加えた4×4の行列で計算 することが多いです。
そのため下記のように行列クラスを使って計算を行うことになります。
非常に計算しやすくなります。
3Dグラフィックスでは x,y,zの情報にwを加えた4×4の行列で計算 することが多いです。
そのため下記のように行列クラスを使って計算を行うことになります。
↑4×4行列クラス

エレキベア
行列式懐かしクマ〜〜〜〜
ワールド座標変換

マイケル
まずはワールド座標への変換について!
オブジェクトの変換としては
・拡大縮小
・回転
・平行移動
の3種類があり、それぞれ下記の行列式で表すことができます!
オブジェクトの変換としては
・拡大縮小
・回転
・平行移動
の3種類があり、それぞれ下記の行列式で表すことができます!
拡大縮小
回転
平行移動

マイケル
これらの行列式を掛け合わせることで、
ワールド変換用の行列式としてまとめる ことができます!
ワールド変換用の行列式としてまとめる ことができます!

エレキベア
オブジェクト側で1つの行列にまとめられたら
計算が楽になりそうクマね
計算が楽になりそうクマね
クォータニオンによる回転

マイケル
しかし、上記の回転の行列式には1点問題があります。
それは ジンバルロック といって、
各回転軸の向きによって回転の自由度が下がってしまうことがある ということです。
それは ジンバルロック といって、
各回転軸の向きによって回転の自由度が下がってしまうことがある ということです。

マイケル
そのため3D描画の回転は代わりに クォータニオン
という表現方法を用いることが多いです。
という表現方法を用いることが多いです。

エレキベア
クォータニオン・・・噂には聞いたことがあるクマ・・・。

マイケル
クォータニオンは下記のように、ベクトルとスカラーの成分を持ち、
正規化された回転軸axisと回転θで計算 することができます。
正規化された回転軸axisと回転θで計算 することができます。
↑クォータニオンの構成

マイケル
そしてクォータニオンは、
クォータニオン同士を連結することで回転を加える ことができます。
クォータニオンqにクォータニオンrを加えた時の計算式 は下記になります。
クォータニオン同士を連結することで回転を加える ことができます。
クォータニオンqにクォータニオンrを加えた時の計算式 は下記になります。
↑クォータニオンに回転を加える

エレキベア
よくわからないクマがこの計算式で回転させることができるクマね

マイケル
クォータニオンの概念は難しいから、
とりあえず使い方だけ覚えてぼちぼち勉強しましょう…。。
とりあえず使い方だけ覚えてぼちぼち勉強しましょう…。。

マイケル
以上のことをクラスにしたものが以下になります。
↑クォータニオンクラス

マイケル
また、回転軸の単位軸は下記のように定数として定義しておくと便利です!
↑単位軸の定義

マイケル
そして最後に、クォータニオンは行列式でも定義 することができます!
下記式を使用することで、拡大縮小や平行移動と同様、1つのワールド変換行列としてまとめることができるということです。
下記式を使用することで、拡大縮小や平行移動と同様、1つのワールド変換行列としてまとめることができるということです。

マイケル
ワールド変換で使用する数式は以上になります!

エレキベア
クォータニオンは置いといて分かってきたクマ〜〜〜
クリップ座標変換

マイケル
次はクリップ座標の変換を行ないます!
3D描画では、見える向きや範囲を指定するビュー行列と、
3Dから2Dの画面に投影するための射影行列
の2つを掛け合わせることで表現することができます!
3D描画では、見える向きや範囲を指定するビュー行列と、
3Dから2Dの画面に投影するための射影行列
の2つを掛け合わせることで表現することができます!
ビュー射影行列(2D)

マイケル
まずは簡単な例として2Dの場合をあげてみます!
これは3Dから2Dへの変換は不要のため、1つの行列で表すことができます。
これは3Dから2Dへの変換は不要のため、1つの行列で表すことができます。

マイケル
画面の幅でそれぞれ調整してあげるだけの式になります。

エレキベア
これはシンプルでわかりやすいクマね

マイケル
2D用の変換式として定義しておきましょう!
ビュー行列

マイケル
3Dでは先ほど言った通り、ビュー行列と射影行列の2種類が必要になります。
まずビュー行列についてですが、3Dの場合は 描画範囲を決めるために、視点の位置と向きの情報が必要となります。
まずビュー行列についてですが、3Dの場合は 描画範囲を決めるために、視点の位置と向きの情報が必要となります。
↑視点の位置と向き

マイケル
これをサンプルプロジェクトではCameraクラスとして定義しています。
そしてビュー行列の式は下記のように表すことができます。
そしてビュー行列の式は下記のように表すことができます。

エレキベア
ターゲットへの向きとカメラの回転情報から計算するクマね
射影行列

マイケル
そして次に3Dから2Dに変換するための射影行列について!
これは主な射影方法として、
・正射影
・透視射影
の2種類があります!
これは主な射影方法として、
・正射影
・透視射影
の2種類があります!
・正射影の場合

マイケル
正射影は奥行きが無く、カメラへの距離に関わらず同じ大きさになる射影方法 です。
near、farを定義することでカメラの見える範囲を指定しています!
↑正射影の見える範囲

マイケル
行列式は下記のように定義できます。
・透視射影の場合

マイケル
そして透視射影は奥行きがある射影方法です。
3Dゲームではこちらの方が馴染みがあるかと思います。
3Dゲームではこちらの方が馴染みがあるかと思います。

マイケル
near、farに加えて、垂直画角(fov)を指定することで
カメラ中心の角度を指定します。
カメラ中心の角度を指定します。
↑透視射影の見える範囲

マイケル
行列式は少し複雑になりますが、下記で定義できます。

マイケル
これで一通り必要な行列式は揃いました!
あとはこれらを使って計算していきましょう!
あとはこれらを使って計算していきましょう!

エレキベア
やっと準備完了クマ〜〜〜〜
計算処理

マイケル
それでは計算処理をコーディングしていきましょう!
ワールド変換座標

マイケル
ワールド座標変換は処理を行う順番も重要になり、
基本的には 拡大縮小→回転→平行移動 の順番で行います。
最初の位置を V0 とした場合、下記のような計算になります。
基本的には 拡大縮小→回転→平行移動 の順番で行います。
最初の位置を V0 とした場合、下記のような計算になります。
↑ワールド座標の計算

マイケル
これを見てお気づきの通り、
ワールド変換行列は Mt*Mr*Msとなり、処理とは逆の順番で掛け合わせる必要がある ことが分かります。
ワールド変換行列は Mt*Mr*Msとなり、処理とは逆の順番で掛け合わせる必要がある ことが分かります。

エレキベア
なんだか混乱してしまうクマ〜〜〜

マイケル
この計算がどうしても混乱してしまうという方は、
逆マトリクスを使用する という手もあります。
下記のように行列式の項目を入れ替えることで表現でき、
処理の順番で掛け合わせることができるようになります!
逆マトリクスを使用する という手もあります。
下記のように行列式の項目を入れ替えることで表現でき、
処理の順番で掛け合わせることができるようになります!
↑逆マトリクスの計算は逆になる

マイケル
参考書によっては逆マトリクスの状態で
公式が載っている場合もあるので注意しましょう!
今回は 逆に掛け合わせるということもしっかり意識するために、一般的な行列式で計算 します。
公式が載っている場合もあるので注意しましょう!
今回は 逆に掛け合わせるということもしっかり意識するために、一般的な行列式で計算 します。

エレキベア
計算方法を体に染み込ませるクマね

マイケル
計算は各オブジェクトごとに行う必要があるため、
サンプルプロジェクトではActorクラス内で定義 しています!
サンプルプロジェクトではActorクラス内で定義 しています!
クリップ変換座標

マイケル
そして クリップ座標についてはワールドに1つあればいいため、
描画クラス内で計算して持っておきます。
描画クラス内で計算して持っておきます。

マイケル
ただし ビュー行列についてはカメラの位置や向きにとって変化するため、
Cameraクラス内で再設定するよう記述しましょう!
Cameraクラス内で再設定するよう記述しましょう!

エレキベア
ワールドごとに持つことで計算量を減らすクマね
シェーダでの計算

マイケル
最後にワールド変換座標、クリップ変換座標をシェーダに設定します!
クリップ変換座標については、ビュー変換→射影変換の順番になりますが、
ワールド変換座標と同様、逆に掛け合わせることには注意しましょう!
クリップ変換座標については、ビュー変換→射影変換の順番になりますが、
ワールド変換座標と同様、逆に掛け合わせることには注意しましょう!
↑クリップ座標変換

マイケル
そして受け取った座標情報を元にシェーダ内で計算を行います!
こちらもローカル座標→ワールド座標→クリップ座標という順番の逆で掛け合わせます!
こちらもローカル座標→ワールド座標→クリップ座標という順番の逆で掛け合わせます!

マイケル
これで座標変換の計算は以上になります!!

エレキベア
長い道のりだったクマ〜〜〜〜〜〜

マイケル
なお2D描画の際には、
2D用のビュー射影行列を設定しなおすようにしましょう!
2D用のビュー射影行列を設定しなおすようにしましょう!
↑2D描画を行う場合
おわりに

マイケル
というわけで今回は3Dグラフィックスの基本と座標変換について解説しました!
どうだったかな??
どうだったかな??

エレキベア
計算式が多くて頭が痛いクマ〜〜〜
でもなんとなく基本は分かってきたクマ
でもなんとなく基本は分かってきたクマ

マイケル
普段Unityとか触ってると想像以上に大変に感じたと思う・・・
ゲームエンジン内部で普段見えない部分!
きっと基礎は役に立つからゆっくり試していこう!
ゲームエンジン内部で普段見えない部分!
きっと基礎は役に立つからゆっくり試していこう!

マイケル
それでは今日はこの辺で!
アデュー!!!
アデュー!!!

エレキベア
クマ〜〜〜〜〜〜〜
【C++】第三回 C++を使ったゲーム開発 〜3Dゲーム開発基礎 OpenGLと座標変換編〜 〜完〜
※続きはこちら!