実装の過程

本項では、swizzle 指定可能なベクトル演算クラスを 実装する過程についてまとめます。 興味がなければ、読み飛ばして頂いても差し支えありません。

動機付け

まずはじめに、swizzle 指定可能なベクトル演算クラスが必要となる事例を挙げ、 実装するに至る動機付けの部分を明確にしておきます。

3D プログラミングでは 4 次元までのベクトル演算を頻繁に利用します。 しかし以下のように、単純に 4 次元ベクトルの構造体を定義し、 演算過程を x y z w の各成分ごとに記述することは、非常に面倒です。
	typedef struct {
	    float x ;
	    float y ;
	    float z ;
	    float w ;
	} Vec_t ;

	(中略)

	/* ベクトルの加算 */
	vecC.x = vecA.x + vecB.x ;
	vecC.y = vecA.y + vecB.y ;
	vecC.z = vecA.z + vecB.z ;
	vecC.w = vecA.w + vecB.w ;
この問題を解決するには、 一般に C++ の operator を使います。 上記のベクトル加算部分の記述は、適切な operator を定義しておくことにより、
	/* ベクトルの加算 */
	vecC = vecA + vecB ;
のように 1 行で記述できるようになります。

しかし実際には、このような手法のみでは解決できない問題があります。 3D プログラミングにおけるベクトル演算では、 常に x y z w の 4 成分を対象とするとは限りません。 例えば、同次座標系では、x y z 成分と w 成分を個別に扱いたい場合があります。 テクスチャ座標系では、x y の 2 成分のみ扱いたい場合があります。

つまり演算を記述する際に、 その都度、演算対象となる成分をコンパイラに伝えられるような仕組みが求められます。

演算対象となる成分をコンパイラに伝える仕組み

今回の実装では、シェーダ言語で用いられる手法を真似ることにします。

具体的には、演算対象とする成分を示す「swizzle」という概念を導入します。 そして、swizzle パターン別に専用のベクトルクラスを用意し、 swizzle パターン別に専用の operator を定義します。

swizzle 指定方法(ベクトルの場合)

任意の swizzle 指定でアクセスできる参照を返すメソッドを実装し、 これを用いて swizzle 指定を行います。 具体的には、
	vecA.xyz()
と記述すれば swizzle = xyz 型のベクトルの参照が得られるようにします。

別の手法として、union を用いる手法が考えられます。 具体的には、
	vecA.xyz
と記述すれば、vecA に対して swizzle = xyz 型のベクトルと 見なしてアクセスできるようにするという手法です。 この手法では、先のメソッドを用いる手法よりもずっとスマートに記述できますが、 コンパイラ側の制限事項(注1)に引っかかるため、残念ながら利用できません。
(注1) union の構成要素は、P.O.D. であることが規定されており、 デフォルトコンストラクタを持つことは禁止されています。 デフォルトコンストラクタを定義しなくても、 代入演算子を定義すると、 暗黙のうちにデフォルトコンストラクタが定義されてしまうため、 回避不能です。 (VC++6.0 固有の問題なのかもしれませんが。)

swizzle 指定方法(行列の場合)

行列の swizzle 指定は、 ベクトルのように成分の並びを個別指定する手法ではなく、 次元数の指定のみを行うインターフェースとします。

具体的には、
	matA.m2x2()
と記述すれば、 2 行 2 列の行列の参照が得られるようなインターフェースとします。

ここで注意が必要なのですが、 行列成分のメモリ配置を変更することなく、 swizzle 指定により演算解釈上の次元数をコントロールできる仕掛けにする必要があります。

swizzle 指定メソッド全種を展開

swizzle 指定メソッドは、 swizzle の総パターン数分必要になります。 ベクトルで 808 種類、行列で 32 種類必要になります。 これらをあらかじめ定義しておく必要があります。 スマートではありませんが、他に方法がありません。

今回の実装では、 perl スクリプトを用いて、 全 swizzle パターンに対応するメソッドのコードを展開しました。

template を活用

演算対象の swizzle パターンごとに operator を定義する必要がありますが、 これをそのまま記述することは不可能なので、 template を活用し、 swizzle パターンに対応する operator をコンパイラに生成させます。

inline 展開、そしてコンパイラの最適化頼み

すべてのメソッドを inline 展開させます。 inline 指定子では不十分なので、 VC++ の __forceinline 指定子を用います。

inline 展開することにより、 全計算過程をコンパイラに最適化させることが可能になります。 後述しますが、この効果は絶大です。

また、コンパイラの最適化効率が上がるように、 コードの記述自体を調整します。 一時的に生成した中間値などが最適化により確実に消去されているか、 コンパイル結果のアセンブリコードを確認しながら、 調整する必要があります。

次元数が一致しない場合の動作に配慮

例えば、次のような記述をした場合の動作を考える必要があります。
	/* 次元が一致しない */
	vecC.xyzw() = vecA.xyz() + vecB.xy();
同様に、行列についても、次元が一致しない場合の動作を考える必要があります。
	/* 乗算不能 */
	matC.m4x4() = matA.m2x4() * matB.m2x4();
今回の実装では、 次元が一致しない演算結果は不定(予測困難)となっています。

(本来なら、これらのような記述はコンパイル段階でエラーと見なしたいところですが、 コンパイラ内部エラーにより、実装を断念しました。)