DDA

DDA

DDA(Digital Differential Analyzer)は、整数演算のみで図形を描画するアルゴリズムです。
ロジックの間違いが有れば指摘してくださると有り難いです。

準備

open Graphics

(* Enter待ち *)
let wait () = let dummy = read_line () in ()

(* 10×10で点を描く *)
let pset x y = fill_rect (x * 10) (y * 10) 10 10

let _ = 
  open_graph "";
  pset 20 20;
  wait ()

空白の窓に小さな黒い四角が描かれてるはずです。
ターミナルの方に戻ってEnter押せば終了します。

Graphicsに豊富に用意された描画関数は置いておいて、このpsetだけを使って線や円や、もっと複雑なものを描いていく、という筋書きです。
意義を疑った人は、シミュレーションゲームで直線上に並んだ敵を攻撃する魔法は使用禁止。

注意事項として、Graphicsライブラリでは左下が原点になります。

O'Caml

型推論がありますと単に記述量が減るだけでなく、アルゴリズムの本質部分だけがコードに残るため非常にありがたいです。
またO'Camlのいいところとして、適当に堕落しててその気になれば破壊的代入やforループが使えてしまうところがあります。
HaskellやCleanは清廉潔白なので、関数型言語そのものを勉強するにはそっちのほうがいいのですが、他の事をやるときは堕落していてくれた方が有り難く……というわけでここでは、O'Camlを、型推論を行ってくれる手続き型言語として使います。

コンパイル手順

次のように、graphics.cmxa(バイトコード版は.cma)をリンクします。

ocamlopt graphics.cmxa g.ml

直線

...略...

(* 符号 *)
let sign x = if x > 0 then 1 else -1

(* 線を引く *)
let line x1 y1 x2 y2 =
  let w = abs (x2 - x1) in
  let h = abs (y2 - y1) in
  let step_x = sign (x2 - x1) in
  let step_y = sign (y2 - y1) in
  if w >= h then
    let d = ref (w / 2) in
    let x = ref x1 in
    let y = ref y1 in
    while !x <> x2 do
      pset !x !y;
      d := !d + h;
      if !d >= w then (
        y := !y + step_y;
        d := !d - w
      );
      x := !x + step_x
    done;
    pset !x !y
  else
    let d = ref (h / 2) in
    let x = ref x1 in
    let y = ref y1 in
    while !y <> y2 do
      pset !x !y;
      d := !d + w;
      if !d >= h then (
        x := !x + step_x;
        d := !d - h
      );
      y := !y + step_y
    done;
    pset !x !y

let _ = 
  open_graph "";
  line 0 0 10 10;
  line 0 0 25 10;
  line 0 20 30 5;
  line 40 25 30 5;
  wait ()

x, yそれぞれのケースで最後のpsetを取り除けば、WindowsのGDIみたく最後の点は描かない仕様にできます。
分数計算を分母倍して行ってるだけなのですが、ここではdひとつで済ませていますが、変数をふたつ使うバリエーションもあるようです。
dの初期値を幅の半分にしてる(1/2から始めてる)のは、始点と終点付近でバランスを取るためです。
中心から対象に計算して計算量を半分にする方法もありますが、その場合接続点となる真ん中に違和感が生じる場合ありなので、結局この方法が「見慣れた」線が引けるのでは無いでしょうか。

四角

...略...

let rectangle x1 y1 x2 y2 =
  let (xs, xe) = if x1 <= x2 then (x1, x2) else (x2, x1) in
  let (ys, ye) = if y1 <= y2 then (y1, y2) else (y2, y1) in
  for x = xs to xe do pset x y1; pset x y2 done;
  for y = ys + 1 to ye - 1 do pset x1 y; pset x2 y done

let rectangle_fill x1 y1 x2 y2 =
  let (xs, xe) = if x1 <= x2 then (x1, x2) else (x2, x1) in
  let (ys, ye) = if y1 <= y2 then (y1, y2) else (y2, y1) in
  for x = xs to xe do
    for y = ys to ye do
      pset x y
    done
  done

let _ = 
  open_graph "";
  rectangle 25 15 15 25;
  rectangle_fill 10 10 20 20;
  wait ()

ほら、一応書いときたいじゃないですか、ネ。
難しい本はこういうのを飛躍するから難しいという説もあるし。(←ない)

http://fussy.web.fc2.com/algo/algo2-2.htmより。

...略...

let ellipse center_x center_y radius_x radius_y = 
  let product_4_b = (4 * radius_x * radius_x) / (radius_y * radius_y) in
  let product_2_b = product_4_b / 2 in
  let product_1_b = product_2_b / 2 in
  let x = ref radius_x in
  let y = ref 0 in
  let f = ref (-2 * radius_x + 1 + product_2_b) in
  let h = ref (-4 * radius_x + 2 + product_1_b) in
  while !x >= 0 do
    pset (center_x + !x) (center_y + !y);
    pset (center_x - !x) (center_y + !y);
    pset (center_x + !x) (center_y - !y);
    pset (center_x - !x) (center_y - !y);
    if !f < 0 then (
      y := !y + 1;
      f := !f + product_4_b * !y + product_2_b;
      h := !h + product_4_b * !y;
    ) else if !h >= 0 then (
      x := !x - 1;
      f := !f - 4 * !x;
      h := !h - 4 * !x - 2;
    ) else (
      x := !x - 1;
      y := !y + 1;
      f := !f + product_4_b * !y - 4 * !x + product_2_b;
      h := !h + product_4_b * !y - 4 * !x + 2;
    )
  done

...略...

BASICのCIRCLE文よろしく半径単位でいいならこれでいいんですよ。
しかし、これですと、直径を1ピクセル単位で指定できないのです。
半径を指定する関係上、直径はどうしても2ピクセル単位に……。

しかしながら、ベジエ曲線等で円を近似するのは(浮動小数点演算を使うとDDAで無くなるとか野暮なことは置いといて)、オーバースペックな気もしますし、何より僕の手に余ります。

……というわけで、セコイ技で。
上記アルゴリズムですと、直径は、半径 * 2 + 1 (+ 1 は、中心から対象に描くから)となってます。
ならば、直径が偶数の時は、適切にずらしてやればOK……うーむ。

...略...

let ellipse x1 y1 x2 y2 = 
  let center_x = (x1 + x2) / 2 in
  let center_y = (y1 + y2) / 2 in
  let radius_x = abs (x1 - x2) / 2 in
  let radius_y = abs (y1 - y2) / 2 in
  let (diff_x1, diff_x2) = if x1 <= x2 
    then (x1 - (center_x - radius_x), x2 - (center_x + radius_x))
    else (x2 - (center_x - radius_x), x1 - (center_x + radius_x)) in
  let (diff_y1, diff_y2) = if y1 <= y2 
    then (y1 - (center_y - radius_y), y2 - (center_y + radius_y))
    else (y2 - (center_y - radius_y), y1 - (center_y + radius_y)) in
  let product_4_b = (4 * radius_x * radius_x) / (radius_y * radius_y) in
  let product_2_b = product_4_b / 2 in
  let product_1_b = product_2_b / 2 in
  let x = ref radius_x in
  let y = ref 0 in
  let f = ref (-2 * radius_x + 1 + product_2_b) in
  let h = ref (-4 * radius_x + 2 + product_1_b) in
  while !x >= 0 do
    pset (center_x - !x - diff_x1) (center_y - !y - diff_y1);
    pset (center_x + !x + diff_x2) (center_y - !y - diff_y1);
    pset (center_x - !x - diff_x1) (center_y + !y + diff_y2);
    pset (center_x + !x + diff_x2) (center_y + !y + diff_y2);
    if !f < 0 then (
      y := !y + 1;
      f := !f + product_4_b * !y + product_2_b;
      h := !h + product_4_b * !y;
    ) else if !h >= 0 then (
      x := !x - 1;
      f := !f - 4 * !x;
      h := !h - 4 * !x - 2;
    ) else (
      x := !x - 1;
      y := !y + 1;
      f := !f + product_4_b * !y - 4 * !x + product_2_b;
      h := !h + product_4_b * !y - 4 * !x + 2;
    )
  done

let _ = 
  open_graph "";
  ellipse 25 15 15 25;
  ellipse 10 10 21 21;
  ellipse 1 10 30 21;
  wait ()

こういうのを力業というのだろうなあ……。