四角形の自由変形を考える

前にやろうとして挫折したものだが。
http://d.hatena.ne.jp/mirichi/20090927/p1


まずはおさらいから。
なぜ三角形のポリゴンで四角形の自由変形ができないのかというと、自由変形された四角形の全てのピクセルが、4点の位置関係によって補正されるから、だ。
DirectXは三角形ポリゴンを組み合わせることで画面を作る。四角形の自由変形をするには、描画しようとする三角形はそれ以外のもう一つの点によって影響を受ける必要がある。
とりあえずDirectXにはその機能が無い。


今回はアルゴリズムから考えようということで、ソフトウェア処理で自由変形を実現する方向性で行ってみる。
たとえばこのようなコードを書く。

#! ruby -Ks
require 'dxruby'

Window.width = 320
Window.height = 240

Vertex = Struct.new(:x, :y)

v = [
  [100,70],
  [200,70],
  [200,170],
  [100,170]
].map do |ary|
  Vertex.new(*ary)
end

#v = [
#  [100,70],
#  [200,70],
#  [300,230],
#  [100,170]
#].map do |ary|
#  Vertex.new(*ary)
#end

image = Image.new(320,240)
image.line(v[0].x, v[0].y, v[1].x, v[1].y, [255,255,255])
image.line(v[1].x, v[1].y, v[2].x, v[2].y, [255,255,255])
image.line(v[2].x, v[2].y, v[3].x, v[3].y, [255,255,255])
image.line(v[3].x, v[3].y, v[0].x, v[0].y, [255,255,255])

for i in 1..9
  image.line(v[0].x + (v[1].x - v[0].x) * i / 10, v[0].y + (v[1].y - v[0].y) * i / 10,
             v[3].x + (v[2].x - v[3].x) * i / 10, v[3].y + (v[2].y - v[3].y) * i / 10, [255,255,255])
  image.line(v[0].x + (v[3].x - v[0].x) * i / 10, v[0].y + (v[3].y - v[0].y) * i / 10,
             v[1].x + (v[2].x - v[1].x) * i / 10, v[1].y + (v[2].y - v[1].y) * i / 10, [255,255,255])
end

Window.loop do
  Window.draw(0,0,image)
end


見た目はただの正方形を縦横10分割した絵だが、アルゴリズムは4つの辺をそれぞれ10分割して、対応する点を結んでいる。
従って、コードのコメントアウトの部分を有効にして右下の点を更に右下に引っ張ってやるとこうなる。

この画像は3D回転では生成できない特殊な変形だ。だから三角形を組み合わせるDirectXの固定機能パイプラインでは、初めの絵を回転する方法でこれを描画することはできない。
上の画像は単純に縦横10分割しているが、これを画像の描画と考えて、元画像のサイズぶんだけ分割する、とイメージする。
そうすると四角形の自由変形というのは、4点の位置関係によって全てのピクセルの縦横拡大率が連続的に変化する処理なのだ、ということがわかるような気がしてくる。
ではその、連続的に変化する拡大率を画像の転送に応用するとどうなるか、というと、以下のコード。

#! ruby -Ks
require 'dxruby'

Window.width = 320
Window.height = 240

Vertex = Struct.new(:x, :y)

v = [
  [100,70],
  [200,70],
  [300,230],
  [100,170]
].map do |ary|
  Vertex.new(*ary)
end

font = Font.new(100)
image_a = Image.new(100,100).drawFont(0,0,"",font)

image = Image.new(320,240)
image.line(v[0].x, v[0].y, v[1].x, v[1].y, [255,255,255])
image.line(v[1].x, v[1].y, v[2].x, v[2].y, [255,255,255])
image.line(v[2].x, v[2].y, v[3].x, v[3].y, [255,255,255])
image.line(v[3].x, v[3].y, v[0].x, v[0].y, [255,255,255])

for y in 1...image_a.height
  for x in 1...image_a.width
    wx = (x.to_f / image_a.width)  # xの進んだ割合
    wy = (y.to_f / image_a.height) # yの進んだ割合

    tx = v[0].x + (v[1].x - v[0].x) * wx # 上の線上のx位置
    ty = v[0].y + (v[1].y - v[0].y) * wy # 上の線上のy位置
    bx = v[3].x + (v[2].x - v[3].x) * wx # 下の線上のx位置
    by = v[3].y + (v[2].y - v[3].y) * wy # 下の線上のy位置

    lx = v[0].x + (v[3].x - v[0].x) * wx # 左の線上のx位置
    ly = v[0].y + (v[3].y - v[0].y) * wy # 左の線上のy位置
    rx = v[1].x + (v[2].x - v[1].x) * wx # 右の線上のx位置
    ry = v[1].y + (v[2].y - v[1].y) * wy # 右の線上のy位置

    # ↑にたいして、割合分の中間を求める
    image[(bx - tx) * wy + tx, (ry - ly) * wx + ly] = image_a[x, y]
  end
end

Window.loop do
  Window.draw(0,0,image)
end

実行するとこうなる。

点と点の間に隙間があるのは、元画像1ピクセルに対して転送先を1ピクセル割り出して描画しているからだ。
これをすると、転送先の面積が大きくなったときに隙間ができる。回転処理を自分で考えてソフトウェアで初めて書くと、このような現象に見舞われることが多い。
逆に考えて、転送先の1ピクセルに対して転送元のどの点を取得するか、という計算をしなければならないのだが、自由な4点指定で描画されるピクセルをはじき出すのも、転送元を計算するのも、けっこう面倒な話だ。
でも、転送先のピクセルごとに転送元を計算するのは、ハードウェアとしてシェーダがやっていることズバリそのものだから、うまいこと情報を渡して計算できればシェーダで可能かもしれない。
いまのとこ思いつかないが。
とりあえず今回はここまで。