Irrlichtにおいて、オブジェクト(ISceneNode)の回転系は、基本的にオイラー角で表現され、回転順はX軸⇒Y軸⇒Z軸、単位は度(degree)である。
これは使い易いとは言い難く、特に、Z軸が最後に来てしまうために、Bank(Z軸回転)が事実上死んでいる。
出来れば、一般的なFPS型のゲームで使い易いHeading-Pitch-Bank(Z軸⇒X軸⇒Y軸)ないしクォータニオンで指定したい。
Irrlichtにはquaternionクラスが用意されており、quaternionからXYZ回転系への変換メソッドも用意されているので、これを利用する。任意の回転系からXYZ回転系に変換したい場合、(任意の回転系)⇒(クォータニオン)⇒(XYZ回転系)のように、クォータニオン経由で変換できる。
このためのコードを下記に示す。
using namespace irr::core;
struct rotationHPB {
float h, p, b;
rotationHPB(float h_ = 0, float p_ = 0, float b_ = 0) {
h = h_; p = p_; b = b_;
}
rotationHPB& operator += (rotationHPB& a) {
h += a.h; p += a.p; b += a.b;
return *this;
}
rotationHPB& operator -= (rotationHPB& a) {
h -= a.h; p -= a.p; b -= a.b;
return *this;
}
quaternion getQuaternion(void) {
float sh = sin(h/2);
float ch = cos(h/2);
float sp = sin(p/2);
float cp = cos(p/2);
float sb = sin(b/2);
float cb = cos(b/2);
return quaternion(
-(ch * sp * cb) + (sh * cp * sb),
(sh * cb * cp) + (ch * sp * sb),
(ch * cp * sb) + (sh * sp * cb),
(ch * cb * cp) - (sh * sp * sb) );
}
};
void setRotation(irr::scene::ISceneNode* pNode, const irr::core::quaternion& q) {
vector3df euler;
q.toEuler(euler);
euler *= RADTODEG;
pNode->setRotation(euler);
}
ここで定義したrotationHPBクラスはLightWave3D等と互換性のあるZ⇒X⇒Y回転を表現している。
使い方は下記の通り
rotationHPB hpb(PI/2, PI/4, PI/6);
setRotation(pMesh, hpb.getQuaternion());
上記のrotationHPBクラスは、マウスでエイムするようなFPSタイプのゲームに向いている。
フライトシミュレータ等、宙返り可能な操縦系のゲームの場合は、直接クォータニオンで管理すべき。
Irrlichtは、一般的なエンジン同様World⇒View⇒Projectionという順序で座標変換を行なっており、Scene manager経由でメッシュを表示させている分には、それを意識する必要は無い。
しかし、実際の3Dアプリケーションにおいては、空間上の一点がどういうスクリーン座標に変換されるか、またはその逆を知りたい局面が多く出現する。この場合、Irrlichtの座標変換についての理解が必要となる。
一般的な3Dエンジンと同じである。ただし、たとえ2Dの描画命令であっても、変換済み頂点の描画命令を内部的に使わないという点に留意が必要。「変換済み頂点」とは、D3DFVF_XYZRHWやD3DTLVERTEXなどのことである。
この影響により、ビューポートを設定した際の動きが、やや癖のある物となっている。
3D座標を指定するメッシュ描画命令のみを使っている分には、これを意識する必要は無い。イメージやテキストの描画といった、2D系描画命令を使う場合には注意が必要。
結論から言えば、2D座標を指定する描画命令は、原則としてビューポートを解除してから実行すべきである。
指定された頂点の空間座標から、画面上の座標に変換する実際のコードは下記のようになる。
Irrlichtには同次座標を表現する型が存在しないため、vector4dクラスを作成した。
template <class T>
class vector4d {
public:
union {
struct {
T X, Y, Z, W;
};
T f[4];
};
vector4d(void) : X(0), Y(0), Z(0), W(0) {}
vector4d(T x, T y, T z, T w = T(1)) { X = x; Y = y; Z = z; W = w; }
vector4d(const core::vector3d<T>& v3) { X = v3.X; Y = v3.Y; Z = v3.Z; W = 1; }
};
template <class T>
void transform4D(const core::CMatrix4<T>& M, T* out, const T* in) {
out[0] = in[0]*M[0] + in[1]*M[4] + in[2]*M[8] + in[3]*M[12];
out[1] = in[0]*M[1] + in[1]*M[5] + in[2]*M[9] + in[3]*M[13];
out[2] = in[0]*M[2] + in[1]*M[6] + in[2]*M[10] + in[3]*M[14];
out[3] = in[0]*M[3] + in[1]*M[7] + in[2]*M[11] + in[3]*M[15];
}
bool transformTo2D(IrrlichtDevice* pDevice, scene::ICameraSceneNode* pCamera,
core::vector2d<s32>* dst, const core::vector3df& src,
const rect<s32>* pViewPort) {
vector4d<f32> src4(src), v1, v2;
transform4D(pCamera->getViewMatrix(), v1.f, src4.f);
if(v1.Z <= 0) return false;
transform4D(pCamera->getProjectionMatrix(), v2.f, v1.f);
if(pViewPort) {
dst->X = s32(pViewPort->UpperLeftCorner.X + (1 + v2.X / v2.W) / 2.0f * pViewPort->getWidth());
dst->Y = s32(pViewPort->UpperLeftCorner.Y + (1 - v2.Y / v2.W) / 2.0f * pViewPort->getHeight());
}
else {
dimension2d<u32> d2Screen = pDevice->getVideoDriver()->getCurrentRenderTargetSize();
dst->X = s32(d2Screen.Width / 2 + v2.X / v2.W * d2Screen.Width / 2);
dst->Y = s32(d2Screen.Height / 2 - v2.Y / v2.W * d2Screen.Height / 2);
}
return true;
}
付属サンプルの"18.SplitScreen"は画面を4分割しているので、画面のアスペクト比は変わらない。しかし、例えば、横に2分割など、元の画面サイズとアスペクト比が異なる場合は、正しいアスペクト比を設定し直す必要がある。これをしないと、描画内容の縦横比が不正なものになる。
アスペクト比の設定はICameraSceneNode::setAspectRatioで行なう。
pCamera->setAspectRatio((float)viewportWidth / viewportHeight);
ビューポートを設定した状態で、2Dの描画命令を実行した場合、画面全体の座標系がビューポート内に縮小された状態で座標指定することになる。
例えば、画面全体が1920 x 1080の状態において、右半分をビューポートとした場合、ビューポート左上の隅(画面上辺の中央)が(0,0)となる。
ビューポートの右下の隅は(959, 1079)ではなく(1919, 1079)である。座標系が左右から押し潰されたような状態となるだけでなく、通常の方法ではサイズ指定できないフォント類も含めてビューポートのサイズに縮小される。