3度目のVectorとMatrix
かつてDXRuby1.1devとDXRubyFwで作ってはみたが未だ本体に含まれないVectorとMatrix。なぜかというと、どうにも微妙感が拭いきれないから。
2D用だったり3D用だったりしつつ、数学的な通常の計算に加えてアフィン変換を簡単にできるように工夫はした。が、なんかいまいち使いにくい。
RubyにMatrixライブラリが標準添付されているというのもある。Rubyで書かれているので遅い。ベクトル・行列なんてメジャーなのだから、そういう高速なライブラリがあってもおかしくはない。調べてみたらNArrayが使えるようだ。そういえばMiyakoはNArrayを使ってたっけ。でもアフィン変換は無いようだ。そんなもんいちいち書いてられない。結局のところ、自分で作ることになる。
いままでの実装ではVectorとMatrixはImmutableにした。破壊的メソッドが無いということだ。HLSLを意識すると、部分的な書き換えがあると便利そうな気はするのだが、あっちは変数が値を持つから、代入は値のコピーになる。Rubyは参照を持つ。Rubyistであれば他から参照している配列を書き換えて変なバグに遭遇したことぐらい、一度はあるんじゃないだろうか。便利だけど、危ない。
HLSLを少しいじった感覚として、値の部分書き換えが発生するのは色の操作がほとんどだ。座標を扱う場合にはそんなことはあまりしない。Vectorを使う場合、恐らくその用途は座標管理であって、色ではない。
従来のVectorとMatrixの微妙感の原因は、数学的すぎたところじゃないかと思っている。数学的に正しいVectorと、数学的に正しいMatrix。そういうクラスを応用して計算を行う。
しかし、それらのみ切り出して高速なVector/Matrixライブラリとして数値演算に使ってもらいたいわけではなく、ゲームプログラミングに使いたいのだ。できればDXRuby本体に組み込んで。それこそゲーム専用に。
座標を回転するために回転行列を作って、Vectorと演算すればVectorが回転する。回転したいだけなのになぜそんな回りくどいことをしなければならないのか。数学的にはそういう手順を踏むからだ。なぜ数学的じゃなければならないのか。特に理由は無い。これは数学の呪いだ。
その呪縛から解き放たれてみよう。
Vectorオブジェクトは角度を与えたら単体で回転処理ができてもいい。ついでに中心点が指定できるともっと手軽になるんじゃないか。Vector#rotate_with_center(angle, x=0, y=0)みたいな感じはどうだろう。
Matrixのほうはどうだ。いちいち生成してから*するとか面倒だ。Matrix.create_transration(x, y).rotate(angle).transrate(x, y)とかメソッドチェインで生成できればラクだ。これを3D用にできれば頂点シェーダを扱うのもラクになる。
そもそもDXRubyはCで毎回毎回DirectXのコードを書くのが面倒だったから作った。何か作るたびにCで書くのが面倒だから作った。Cは面倒だからだ。Rubyのほうがラクだからだ。俺がラクだからだ。数学的でなくていい。何かに縛られなくていい。もっとラクになればいい。そうだ。もっとラクになれるものを作りたいのだ。俺がラクになるために。俺がラクになれるように。
最低限の実装をしてみた。とりあえずイメージをつかむためにRubyで。
class Vector attr_reader :x, :y def initialize(x, y) @x = x @y = y end def rotate(angle) x = @x * Math.cos(Math::PI / 180 * angle) - @y * Math.sin(Math::PI / 180 * angle) y = @x * Math.sin(Math::PI / 180 * angle) + @y * Math.cos(Math::PI / 180 * angle) Vector.new(x, y) end def +(v) case v when Vector Vector.new(@x + v.x, @y + v.y) when Numeric Vector.new(@x + v, @y + v) when Array Vector.new(@x + v[0], @y + v[1]) else nil end end def to_a [@x, @y] end end
これを使うと星の座標を計算する部分はこのようになる。
x1 = [] y1 = [] (0..4).each do |i| x1[i], y1[i] = (Vector.new(0, -50).rotate(i * 72 - 36) + [image.width/2, image.height/2]).to_a end sx = x1[0] + (x1[1] - x1[0]) / 2 - image.width/2 sy = y1[0] - (x1[1] - x1[0]) / 2 * Math.tan(Math::PI / 180 * 72) - image.height/2 x2 = [] y2 = [] (0..4).each do |i| x2[i], y2[i] = (Vector.new(sx, sy).rotate(i * 72) + [image.width/2, image.height/2]).to_a end
回転の計算が駆逐できた。これはラクだ。気に入った。