ベクタグラフィックス

新年あけましておめでとうございます。今年もよろしくお願いいたします。
さて、ちょと前に作ったラスタライザを、NanoVGを参考にしながら手を入れていってベクタグラフィックスにチャレンジしよう。

基本的な流れ

NanoVGではnvgBeginFrame〜nvgBeginPath〜描画指定〜nvgStroke or nvgFill〜nvgEndFrameという呼び出しでベクタグラフィックスを描画する。
nvgBeginFrameでパラメータの初期化などをして、nvgBeginPathでパスをクリアして、nvgMoveTo、nvgLineToなどで絵を指定して、nvgStrokeかnvgFillで各種計算&バックエンドにデータ受け渡し、nvgEndFrameで実際の描画、となる。NanoVGでは本体とバックエンドが分離していて、標準のバックエンドはOpenGLを扱うが、DirectX11用のバックエンドなども作られている。
パラメータとかパスとか難しいことは後回しにして、とりあえず今の状態で線は引けているので、move_toとline_toを実装して、内部をNanoVG的な構造に作り直してみよう。

内部の構造

nvgMoveTo、nvgLineToなどの関数では内部のコマンド配列にコマンドを追加(nvg__appendCommands関数)するだけ、となっている。このコマンド列はnvgStrokeなどの中で呼ばれるnvg__flattenPaths関数で点の配列へ変換され、nvg__expandStroke関数でバックエンド用の頂点配列に変換されて、バックエンドに渡される。
このような多層構成になっているのは、それぞれの層でやるべきことがあるからである。具体的にはnvg__appendCommandsではコマンド内の座標をアフィン変換し、nvg__flattenPathsではベジェ曲線を直線の集合に展開し、nvg__expandStrokeでは線の始点・終点の処理や接続の計算をする。上位でやったほうが簡単で速くなることはなるべく上位でやる、ということだ。たぶん。

とりあえず構造の作り直し

今の線描画をそのまま残しておくなら頂点配列を生成するところは必要無いのでexpand_strokeは無くていいとして、append_commandsとflatten_pathsを追加してみよう。
initializeでコマンド配列と点配列のインスタンス変数を追加して

  def initialize(l)
    @image = Image.new(l, l, C_WHITE)
    @triangles = []
    @commands = []
    @width = 1
    @points = []
  end

コマンドと点のクラスを追加して、

  class MoveTo < Struct.new(:x, :y);end
  class LineTo < Struct.new(:x, :y);end
  class Point < Struct.new(:x, :y);end

append_commandsはこう

  private def append_commands(cmd)
    @commands << cmd
  end

コマンドの追加はこんな感じ

  def move_to(x, y)
    append_commands(MoveTo.new(x, y))
  end

  def line_to(x, y)
    append_commands(LineTo.new(x, y))
  end

点配列を生成する関数flatten_paths

  private def flatten_paths
    @commands.each do |cmd|
      case cmd
      when MoveTo
        @points << Point.new(cmd.x, cmd.y)
      when LineTo
        @points << Point.new(cmd.x, cmd.y)
      end
    end
  end

線を生成するstrokeをこのようにすると

  def stroke
    flatten_paths
    
    @points.each_cons(2) do |p1, p2|
      line(p1.x, p1.y, p2.x, p2.y)
    end
  end

このようなコードで

vg = OreVG.new(50)
vg.move_to(2, 5)
vg.line_to(42, 25)
vg.line_to(22, 40)
vg.width = 10
vg.stroke
Window.loop do
  Viewer.draw(vg.image, vg.triangles)
end

このような画面になる。

思ったこと

NanoVGと同様に層を作っただけで中身はほとんどカラッポである。MoveToとLineToとPointのクラスが全部同じとかふざけてんのかって感じだが、今は何もしていないからそうなのであって、今後違いが出てくる。はず。
初めは線を連続で描画できるようにするつもりは無かったのだが、each_consで処理しとけばいいんじゃね?って思ってやってみたら繋がるようになった。
今回のコード全体はこちら