ベクトルと行列の案

昔っから何度も出ては消えてを繰り返しているネタである。作ってみていまいちなので却下してという話で、動かすだけならすぐにでもできるが、それが扱いやすいかというと微妙になってしまう。
これがなぜ扱いにくいかというと、もともとが数学の道具であって、そのままではゲームプログラミングと地味なところで噛み合わないからなのではないかと考えている。
そこで、数学的な動きはそのままに、数学的にあり得ない部分をゲーム的に補間するという荒技で機能追加してみた。こんな感じに動く。

assert(Vector.new(1,2) + Vector.new(1,2) == Vector.new(2,4))
assert(Vector.new(1,2) + Vector.new(1,2,3) == Vector.new(2,4)) #
assert(Vector.new(1,2,3) + Vector.new(1,2) == Vector.new(2,4,3)) #
assert(Vector.new(1,2,3) + 5 == Vector.new(6,7,8))
assert(Vector.new(1,1) * Matrix.transration(1,2) == Vector.new(2,3)) #
assert(Vector.new(1,1,1) * Matrix.transration(1,2) == Vector.new(2,3,1))
assert(Vector.new(2,2,2) * Matrix.transration(1,2,3) == Vector.new(3,4,5)) #
assert(Vector.new(2,2,2,1) * Matrix.transration(1,2,3) == Vector.new(3,4,5,1))
assert(Vector.new(5,5).rotate(45) == Vector.new(5,5) * Matrix.rotation(45)) #

右端に#がついている行が数学的にはエラーにならなければならない部分である。でもこのように動いてくれるなら、ゲーム用としては扱いやすくなるのではないか。
もともと、たとえば2要素のベクトルは2Dの座標を表すのに使うが、行列との演算で平行移動させることができない。3要素目に1をいれたベクトルじゃないといけないからだ。しかし3要素目に1をいれたベクトルはそれ同士を足すと3要素目が2になってしまうのでなにかと面倒だ。
素数が足りない部分にたいして、自動的に0なり1なりを補間して、平行移動とかができるようにしてやれば面倒さが無くなる。まあ、むちゃくちゃだ。怒られそうだ。

ついでにSpriteクラスにメソッドを追加して座標をベクトル指定できるようにもしてみた。こんな感じに動く。

s = Sprite.new
s.xy = Vector.new(1,2)
assert(s.x == 1 && s.y == 2)
assert(s.xy == Vector.new(1,2))

s.x = 5
assert(s.xy == Vector.new(5,2))

s.xyz = Vector.new(10,20,30)
assert(s.x == 10 && s.y == 20 && s.z == 30)
assert(s.xyz == Vector.new(10,20,30))

s.x = 50
assert(s.xyz == Vector.new(50,20,30))

上と組み合わせればわりと自由に座標を操作することができそうである。とりあえずRubyで書いたコードを置いておくが、機能は足りてないし遅いしエラーチェックしてないしでこれを使うのはおすすめしない。DXRuby1.5devにでも作って入れて試してみようと思っているところ。
コードは長いので続きに。

require 'dxruby'

class Vector
  @@to_deg = Math::PI / 180.0

  def initialize(*v)
    @vec = v
  end

  def rotate(angle, center_x = 0, center_y = 0)
    rad = @@to_deg * angle
    sin = Math.sin(rad)
    cos = Math.cos(rad)
    tempx = @vec[0] - center_x
    tempy = @vec[1] - center_y
    x = tempx * cos - tempy * sin + center_x
    y = tempx * sin + tempy * cos + center_y
    Vector.new(x, y)
  end

  def +(v)
    case v
    when Vector
      Vector.new(*@vec.map.with_index{|s,i|s+(v[i]?v[i]:0)})
    when Array
      Vector.new(*@vec.map.with_index{|s,i|s+(v[i]?v[i]:0)})
    when Numeric
      Vector.new(*@vec.map{|s|s+v})
    else
      nil
    end
  end

  def *(matrix)
    if matrix.size > @vec.size
      temp = @vec + [1]
    else
      temp = @vec
    end

    result = []
    for i in 0...(matrix.size)
      data = 0
      for j in 0...(temp.size)
        data += temp[j] * matrix[j][i]
      end
      result.push(data)
    end

    Vector.new(*result[0, @vec.size])
  end

  def [](i)
    @vec[i]
  end

  def size
    @vec.size
  end

  def to_a
    @vec
  end

  def x
    @vec[0]
  end
  def y
    @vec[1]
  end
  def z
    @vec[2]
  end
  def w
    @vec[3]
  end
  def xy
    Vector.new(@vec[0..1])
  end
  def xyz
    Vector.new(@vec[0..2])
  end

  def ==(t)
    @vec == t.to_a
  end
end

class Matrix
  @@to_deg = Math::PI / 180.0

  def initialize(*arr)
    @arr = Array.new(arr.size) {|i| Vector.new(*arr[i])}
  end

  def *(a)
    result = []
    for i in 0...(a.size)
      result.push(@arr[i] * a)
    end
    Matrix.new(*result)
  end

  def [](i)
    @arr[i]
  end

  def size
    @arr.size
  end

  def self.rotation(angle)
    rad = @@to_deg * angle
    cos = Math.cos(rad)
    sin = Math.sin(rad)
    Matrix.new(
     [ cos, sin, 0],
     [-sin, cos, 0],
     [   0,   0, 1]
    )
  end

  def self.rotation_z(angle)
    rad = @@to_deg * angle
    cos = Math.cos(rad)
    sin = Math.sin(rad)
    Matrix.new(
     [ cos, sin, 0, 0],
     [-sin, cos, 0, 0],
     [   0,   0, 1, 0],
     [   0,   0, 0, 1]
    )
  end

  def self.rotation_x(angle)
    rad = @@to_deg * angle
    cos = Math.cos(rad)
    sin = Math.sin(rad)
    Matrix.new(
     [   1,   0,   0, 0],
     [   0, cos, sin, 0],
     [   0,-sin, cos, 0],
     [   0,   0,   0, 1]
    )
  end

  def self.rotation_y(angle)
    rad = @@to_deg * angle
    cos = Math.cos(rad)
    sin = Math.sin(rad)
    Matrix.new(
     [ cos,   0,-sin, 0],
     [   0,   1,   0, 0],
     [ sin,   0, cos, 0],
     [   0,   0,   0, 1]
    )
  end

  def self.transration(x, y, z = nil)
    if z
      Matrix.new(
       [   1,   0,   0,   0],
       [   0,   1,   0,   0],
       [   0,   0,   1,   0],
       [   x,   y,   z,   1]
      )
    else
      Matrix.new(
       [   1,   0,   0],
       [   0,   1,   0],
       [   x,   y,   1]
      )
    end
  end

  def to_a
    @arr.map {|v|v.to_a}.flatten
  end
end

class Sprite
  def xy=(v)
    self.x = v.x
    self.y = v.y
  end
  def xy
    Vector.new(self.x, self.y)
  end
  def xyz=(v)
    self.x = v.x
    self.y = v.y
    self.z = v.z
  end
  def xyz
    Vector.new(self.x, self.y, self.z)
  end
end

def assert(t)
  raise "error" unless t
end

assert(Vector.new(1,2) + Vector.new(1,2) == Vector.new(2,4))
assert(Vector.new(1,2) + Vector.new(1,2,3) == Vector.new(2,4)) #
assert(Vector.new(1,2,3) + Vector.new(1,2) == Vector.new(2,4,3)) #
assert(Vector.new(1,2,3) + 5 == Vector.new(6,7,8))
assert(Vector.new(1,1) * Matrix.transration(1,2) == Vector.new(2,3)) #
assert(Vector.new(1,1,1) * Matrix.transration(1,2) == Vector.new(2,3,1))
assert(Vector.new(2,2,2) * Matrix.transration(1,2,3) == Vector.new(3,4,5)) #
assert(Vector.new(2,2,2,1) * Matrix.transration(1,2,3) == Vector.new(3,4,5,1))
assert(Vector.new(5,5).rotate(45) == Vector.new(5,5) * Matrix.rotation(45)) #

s = Sprite.new
s.xy = Vector.new(1,2)
assert(s.x == 1 && s.y == 2)
assert(s.xy == Vector.new(1,2))

s.x = 5
assert(s.xy == Vector.new(5,2))

s.xyz = Vector.new(10,20,30)
assert(s.x == 10 && s.y == 20 && s.z == 30)
assert(s.xyz == Vector.new(10,20,30))

s.x = 50
assert(s.xyz == Vector.new(50,20,30))