ベクタグラフィックスその5

端の処理はあるが接続点の処理が無いので今回はこれを作る。

各種フラグ

まずPointクラスに項目を追加して各種フラグを持てるようにする。

  class Point < Struct.new(:v, :dv, :dmv, :len, :left, :inner_bevel, :bevel, :corner);end

lenは線の長さでフラグではないが、その次の4つはフラグとなる。それぞれ、左回りフラグ、接続点が形状にめり込むフラグ、接続点の処理をするフラグ、接続点の処理が可能フラグ、みたいな意味合いとなる。calculate_joinsの中でcorner以外の3つを設定する。cornerはベジェ曲線を構成するときに使うので今回はflatten_pathsの中でtrue固定にしている。

  private def calculate_joins(w, line_join, miter_limit)
    iw = w > 0 ? 1.0 / w : 0
  
    @subpaths.each do |subpath|
      # 点の両側の線の中間を算出する
      ([subpath.points.last] + subpath.points).each_cons(2) do |p0, p1|
        p1.dmv = (p0.dv.rperp + p1.dv.rperp) * 0.5
        dmr2 = p1.dmv.x * p1.dmv.x + p1.dmv.y * p1.dmv.y
        if dmr2 > 0.000001
          scale = 1.0 / dmr2
          if scale > 600.0
            scale = 600.0
          end
          p1.dmv *= scale
        end

        # 左回りフラグ
        cross = p1.dv.x * p0.dv.y - p0.dv.x * p1.dv.y
        if cross > 0
          p1.left = true
        end

        # 交点が算出できないフラグ?
        limit = [1.01, [p0.len, p1.len].min * iw].max
        if dmr2 * limit * limit < 1.0
          p1.inner_bevel = true
        end

        # join処理が必要フラグ
        if p1.corner
          if dmr2 * miter_limit * miter_limit < 1.0 or line_join == :bevel or line_join == :round
            p1.bevel = true
          end
        end
      end
    end
  end

expand_stroke

頂点を生成するところでこのフラグとOreVGのインスタンス変数@line_joinを見て丸い接続点(round)と切り捨てる接続点(bevel)の2種類を呼び分ける。ループするほうだけ抜粋。

  private def expand_stroke(w, line_cap, line_join, miter_limit)
    ncap = curve_divs(w, Math::PI, 0.25)

    calculate_joins(w, line_join, miter_limit)

    @subpaths.each do |subpath|
      if subpath.closed # ループしている場合
        # 頂点生成
        ([subpath.points.last] + subpath.points).each_cons(2) do |p0, p1|
          if p1.inner_bevel or p1.bevel
            if line_join == :round
              subpath.round_join(p0, p1, w, w, ncap)
            else
              subpath.bevel_join(p0, p1, w, w)
            end
          else
            subpath.verts << p1.v + p1.dmv * w
            subpath.verts << p1.v - p1.dmv * w
          end
        end

        subpath.verts << subpath.verts[0]
        subpath.verts << subpath.verts[1]

join系メソッドの引数でwが2個あるのは無駄に見えるが、これは塗りつぶし系の処理で呼ぶときに違う値を入れて使うっぽい。

bevel_join

大きく左回りと右回りにロジックが分かれていて、bevelフラグが立っているかどうかで更に分かれる。左右のロジックの違いは符号と頂点順だけである。

    # bevel join
    def bevel_join(p0, p1, lw, rw)
      dlv0 = p0.dv.rperp
      dlv1 = p1.dv.rperp

      if p1.left
        lv0, lv1 = choose_bevel(p1.inner_bevel, p0, p1, lw)
        @verts << lv0
        @verts << p1.v - dlv0 * rw

        if p1.bevel
          @verts << lv0
          @verts << p1.v - dlv0 * lw
          @verts << lv1
          @verts << p1.v - dlv1 * lw
        else
          lv = p1.v - p1.dmv * rw
          @verts << p1.v
          @verts << p1.v - dlv0 * rw
          @verts << rv
          @verts << rv
          @verts << p1.v
          @verts << p1.v - dlv1 * rw
        end
  
        @verts << lv1
        @verts << p1.v - dlv1 * rw
      else
        rv0, rv1 = choose_bevel(p1.inner_bevel, p0, p1, -rw)
        @verts << p1.v + dlv0 * lw
        @verts << rv0
  
        if p1.bevel
          @verts << p1.v + dlv0 * lw
          @verts << rv0
          @verts << p1.v + dlv1 * lw
          @verts << rv1
        else
          lv = p1.v + p1.dmv * lw
          @verts << p1.v + dlv0 * lw
          @verts << p1.v
          @verts << lv
          @verts << lv
          @verts << p1.v + dlv1 * lw
          @verts << p1.v
        end
  
        @verts << p1.v + dlv1 * lw
        @verts << rv1
      end
    end
  end

bevelフラグが立ってないのにbevel_joinが呼ばれるってどゆこと?って感じだが、inner_bevelが立っていると呼ばれる。line_joinが:miterの時にinner_bevelが立つとここに来るわけだ。なのでif bevelのelse側は:miter指定時の形状がめり込んでるときの特殊処理と考えればたぶん正解だろう。:bevel指定時のinner_bevelはthen側だが、この場合はchoose_bevel内で細工がされているようだ。このへんはNanoVGのコードをそのまま持ってきたような状態なのできちんと理解できているかどうか怪しい。choose_bevelはこんな感じ。

    def choose_bevel(bevel, p0, p1, w)
      if bevel
        [
          p1.v + p0.dv.rperp * w,
          p1.v + p1.dv.rperp * w
        ]
      else
        [
          p1.v + p1.dmv * w,
          p1.v + p1.dmv * w
        ]
      end
    end

round_join

こっちは:miter指定の時に来ないのでちょっと簡単だがその代わりに回転する処理があるのでそのぶん面倒になる。

    def round_join(p0, p1, lw, rw, ncap)
      dlv0 = p0.dv.rperp
      dlv1 = p1.dv.rperp
      if p1.left
        lv0, lv1 = choose_bevel(p1.inner_bevel, p0, p1, lw)
        a0 = Math.atan2(-dlv0.y, -dlv0.x)
        a1 = Math.atan2(-dlv1.y, -dlv1.x)
        a1 -= Math::PI*2 if a1 > a0
        
        @verts << lv0
        @verts << p1.v - dlv0 * rw
        
        n = (((a0 - a1) / Math::PI) * ncap).ceil
        n = 2 if n < 2
        n = ncap if n > ncap
        
        n.times do |i|
          u = i / (n - 1.0)
          a = a0 + u * (a1 - a0)
          rx = p1.v.x + Math.cos(a) * rw
          ry = p1.v.y + Math.sin(a) * rw
          @verts << p1.v
          @verts << Vector.new(rx, ry)
        end

        @verts << lv1
        @verts << p1.v - dlv1 * rw
      else
        rv0, rv1 = choose_bevel(p1.inner_bevel, p0, p1, -rw)
        a0 = Math.atan2(dlv0.y, dlv0.x)
        a1 = Math.atan2(dlv1.y, dlv1.x)
        a1 = Math::PI*2 if a1 < a0
        
        @verts << p1.v + dlv0 * rw
        @verts << rv0
        
        n = (((a1 - a0) / Math::PI) * ncap).ceil
        n = 2 if n < 2
        n = ncap if n > ncap
        
        n.times do |i|
          u = i / (n - 1.0)
          a = a0 + u * (a1 - a0)
          lx = p1.v.x + Math.cos(a) * rw
          ly = p1.v.y + Math.sin(a) * rw
          @verts << Vector.new(lx, ly)
          @verts << p1.v
        end

        @verts << p1.v + dlv1 * rw
        @verts << rv1
      end
    end

結果

面倒だったので:round指定の場合の結果だけ。

miterとかの仕組みはHTML5Canvasと同様なのでこのへんのサイトlineJoinとかmiterLimitとかを調べるとよくわかると思う。そもそもNanoVGはHTML5CanvasライクなAPIなのでかなり似ている。
今回のコードはこちら

ベクタグラフィックスその4

現状ではパスは1つだけでOreVGクラスで扱っているが、NanoVGはサブパスを複数持つような構造になっている。これはたぶん閉じたり閉じなかったりする複数のパスを一まとめにしてグラデーションしたりするためにそうなっているんじゃないかと思うのだが、今回はこのサブパス機能を追加してみよう。でもグラデーションはずっと先。まだ色も指定できないし。

SubPath

サブパスはそれぞれ別々に点の配列とclosedフラグを持つ。サブパスが生成されるのはMoveToコマンドのタイミングとなる。このへんはHTML5Canvasと同様である。また、線の接続が途切れるので頂点配列もサブパス単位になる。まずはSubPathクラスを作る。

  class SubPath
    attr_accessor :points, :closed, :verts
    
    def initialize
      @points = []
      @closed = false
      @verts = []
    end

    # ぶつ切りの始点
    def butt_cap_start(p0, dv, w, d)
      pv = p0 - dv * d
      dlv = dv.rperp * w
      @verts << pv + dlv - dv
      @verts << pv - dlv - dv
      @verts << pv + dlv
      @verts << pv - dlv
    end
  
    # ぶつ切りの終点
    def butt_cap_end(p0, dv, w, d)
      pv = p0 + dv * d
      dlv = dv.rperp * w
      @verts << pv + dlv
      @verts << pv - dlv
      @verts << pv + dlv + dv
      @verts << pv - dlv + dv
    end
  
    # 丸い始点
    def round_cap_start(p0, dv, w, ncap)
      dlv = dv.rperp
      ncap.times do |i|
        a = i / (ncap - 1.0) * Math::PI
        @verts << p0 - dlv.rotate(a) * w
        @verts << p0
      end
      @verts << p0 + dlv * w
      @verts << p0 - dlv * w
    end
    
    # 丸い終点
    def round_cap_end(p0, dv, w, ncap)
      dlv = dv.rperp
      @verts << p0 + dlv * w
      @verts << p0 - dlv * w
      ncap.times do |i|
        a = i / (ncap - 1.0) * -Math::PI
        @verts << p0
        @verts << p0 - dlv.rotate(a) * w
      end
    end
  end

考えるのが面倒だったので始点終点の処理はこっちに移動した。他のいろんな処理も移動すべきな気はするが、そのうち気が向いたらやる(いい加減)。

flatten_paths

MoveToコマンドを処理するタイミングでSubPathを生成する。OreVGクラスには@subpathsインスタンス変数を追加して配列を入れておく。また、従来の@pointsを扱う場面はすべてサブパス内のpointsを使うように変更する。これはcalculate_joinsやexpand_strokeも同様となるので省略する。

  private def flatten_paths
    @commands.each do |cmd|
      case cmd
      when CmdMoveTo
        @subpaths << SubPath.new # 新規サブパス追加
        @subpaths.last.points << Point.new(cmd)
      when CmdLineTo
        @subpaths.last.points << Point.new(cmd)
      when CmdClose
        @subpaths.last.closed = true
      end
    end

    @subpaths.each do |subpath|
      # 最初と最後の点が同じ位置だったら最後を捨ててループしていることにする
      if subpath.points[0] == subpath.points[-1]
        subpath.points.pop
        subpath.closed = true
      end
  
      # 点の次の線の向きを計算
      # ループしない場合は最後の点の情報は使われない
      ([subpath.points.last] + subpath.points).each_cons(2) do |p0, p1|
        p0.dv = (p1.v - p0.v).normalize
      end
    end
  end

render_stroke

render_strokeも同様にサブパス単位になるが、ここでサブパス単位にわけるとサブパスが変わる際に線が強制的に途切れるようになる。

  private def render_stroke
    @subpaths.each do |subpath|
      subpath.verts.each_cons(3).with_index do |ary, i|
        if i.even?
          p1, p0, p2 = ary
        else
          p0, p1, p2 = ary
        end
        triangle(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y)
      end
    end
  end

結果

vg = OreVG.new(50)
vg.move_to(4, 4)
vg.line_to(45, 8)
vg.move_to(20, 15)
vg.line_to(44, 35)
vg.line_to(10, 40)
vg.width = 5
vg.line_cap = :round
vg.close_path
vg.stroke
Window.loop do
  Viewer.draw(vg.image, vg.triangles, vg.subpaths.map{|sp|sp.points}.flatten)
end

このようなコードで

という感じの描画ができるようになる。現時点ではコードがややこしくなるだけでメリットは無いが、まあ、最終的にはおそらく必要になるのでいつ実装してもたいして違いは無い。
今回のコードはこちら

ベクタグラフィックスその3

端の処理がbuttだけなのでroundとsquareを追加しよう。ついでにclose_pathでループを作れるようにもしてみる。

ループ処理

NanoVGではnvgClosePath関数を呼ぶことでそのパスのclosedフラグを立てる。これが立っていると最初の点と最後の点が自動的に接続されてループを構成するようになる。まずCloseコマンドを作る。

  class CmdMoveTo < Vector;end
  class CmdLineTo < Vector;end
  class CmdClose;end
  class Point < Struct.new(:v, :dv, :dmv);end

クラスがコマンドなのか何なのかわからなくなってしまうので頭にCmdをつけるようにした。ユーザが呼ぶメソッドはclose_path。

  def close_path
    append_commands(CmdClose.new)
    self
  end

flatten_pathsはこのようになる。

  private def flatten_paths
    @commands.each do |cmd|
      case cmd
      when CmdMoveTo
        @points << Point.new(cmd)
      when CmdLineTo
        @points << Point.new(cmd)
      when CmdClose
        @closed = true
      end
    end

    # 最初と最後の点が同じ位置だったら最後を捨ててループしていることにする
    if @points[0] == @points[-1]
      @points.pop
      @closed = true
    end

    # 点の次の線の向きを計算
    # ループしない場合は最後の点の情報は使われない
    ([@points.last] + @points).each_cons(2) do |p0, p1|
      p0.dv = (p1.v - p0.v).normalize
    end
  end

最初と最後の点が同じ場合の処理は必要なのかどうなのかがわからないが、NanoVGに入っているので入れておく。expand_strokeはこう。

  private def expand_stroke(w)
    calculate_joins

    if @closed # ループしている場合
      # 頂点生成
      (@points + [@points.first]).each do |p0|
        @verts << p0.v + p0.dmv * w
        @verts << p0.v - p0.dmv * w
      end
  
    else # していない場合
      ncap = curve_divs(w, Math::PI, 0.25)

      # 始点の処理
      p0 = @points[0]
      case @line_cap
      when :butt
        butt_cap_start(p0.v, p0.dv, w, -0.5)
      when :square
        butt_cap_start(p0.v, p0.dv, w, w - 1)
      when :round
        round_cap_start(p0.v, p0.dv, w, ncap)
      end
  
      # 中間の頂点生成
      @points[1..-2].each do |p0|
        @verts << p0.v + p0.dmv * w
        @verts << p0.v - p0.dmv * w
      end
  
      # 終点の処理
      p0 = @points[-2]
      p1 = @points[-1]
      case @line_cap
      when :butt
        butt_cap_end(p1.v, p0.dv, w, -0.5)
      when :square
        butt_cap_end(p1.v, p0.dv, w, w - 1)
      when :round
        round_cap_end(p1.v, p0.dv, w, ncap)
      end
    end
  end

端の処理の追加が既に入ってしまっているが、@closedを見て大きく2つに分岐する。ループする場合は端の処理は必要無い。

round

expand_strokeの中でroundとsquareの分岐が追加されていた。square対応のためにやはりbuttの処理は始点と終点をわける必要があった。わけて引数を変えるだけでsquareは対応できるのだが、roundはさすがにそうもいかない。まずexpand_strokeの中で呼んでいるcurve_divsから。

  # 弧の点の数を算出する
  private def curve_divs(r, arc, tol)
    da = Math.acos(r / (r + tol)) * 2
    [2, (arc / da).ceil].max
  end

そういえばNanoVGで端の処理にあるaaという引数は省略した。こいつはDevicePixelRatioというものが影響するパラメータで、どうやら高解像度の画面(Retinaディスプレイとか)に対応するためのものらしい。が、実際NanoVGでこの値をいじると半透明になったりしてよくわからないし俺には関係無いので省いている。んで、ここに出てくるtolというのもその関連の値で、まあ、高解像度じゃない場合は0.25になるのでそれで固定にしてある。必要そうなら後で追加する。
次にround_cap_startとend。

  # 丸い始点
  private def round_cap_start(p0, dv, w, ncap)
    dlv = dv.rperp
    ncap.times do |i|
      a = i / (ncap - 1.0) * Math::PI
      @verts << p0 - dlv.rotate(a) * w
      @verts << p0
    end
    @verts << p0 + dlv * w
    @verts << p0 - dlv * w
  end
  
  # 丸い終点
  private def round_cap_end(p0, dv, w, ncap)
    dlv = dv.rperp
    @verts << p0 + dlv * w
    @verts << p0 - dlv * w
    ncap.times do |i|
      a = i / (ncap - 1.0) * -Math::PI
      @verts << p0
      @verts << p0 - dlv.rotate(a) * w
    end
  end

NanoVGのコードは例によって見た目ややこしい計算式になっているのだが、実際のところ回転処理をしているだけなのでVectorに回転用メソッドを追加して回転させるようにした。ちなみにNanoVGのコードはこんな感じ。

static NVGvertex* nvg__roundCapStart(NVGvertex* dst, NVGpoint* p, float dx, float dy, float w, int ncap, float aa)
{
	int i;
	float px = p->x;
	float py = p->y;
	float dlx = dy;
	float dly = -dx;
	NVG_NOTUSED(aa);
	for (i = 0; i < ncap; i++) {
		float a = i/(float)(ncap-1)*NVG_PI;
		float ax = cosf(a) * w, ay = sinf(a) * w;
		nvg__vset(dst, px - dlx*ax - dx*ay, py - dly*ax - dy*ay, 0,1); dst++;
		nvg__vset(dst, px, py, 0.5f,1); dst++;
	}
	nvg__vset(dst, px + dlx*w, py + dly*w, 0,1); dst++;
	nvg__vset(dst, px - dlx*w, py - dly*w, 1,1); dst++;
	return dst;
}

nvg__vsetの引数の後ろの2つが省略されているが、これはバックエンドに渡すテクスチャ座標で、vは1固定。実際Strokeの場合の描画処理ではvは使っていなくて、uの使いどころもアンチエイリアス版のバックエンドで端を検出するのに使われるだけである。今回はとりあえず必要無いかな〜と思って省略している。必要そうなら後で追加する。

結果

今回のコードはこちら。動かすとこんな感じになる。

close_pathを追加すると始点と終点が接続されてループするようになる。

ベクタグラフィックスその2

DXRubyタグにしているが結果の確認に使っているだけで別にDXRubyである必要性も何も無いんだよな。いずれはNanoVGみたいに本体とバックエンドを分離してみたいところ。まあいずれ。
今回は前回手を抜いた頂点配列の生成のところを作ってみよう。

頂点配列と三角形

NanoVGの頂点情報はOpenGLで使うTriangleStripの頂点配列を生成するようになっている。TriangleStripはググるといっぱい出てくるが、連続した三角形を少ない頂点数で指定する方法である。とりあえず三角形を描画するラスタライザがあるので、TriangleStrip用頂点配列を受け取って三角形を描画するメソッドを作ることになる。これはさほど難しくなくて、こんな感じ。

  private def render_stroke
    @verts.each_cons(3).with_index do |ary, i|
      if i.even?
        p1, p0, p2 = ary
      else
        p0, p1, p2 = ary
      end
      triangle(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y)
    end
  end

TriangleStripの指定方法では偶数番目の三角形が逆周りになってしまうので、これを補正する必要がある。

ベクトル化

NanoVGはx座標とy座標を全て別々に計算しているから計算式がとにかく多い。実際にはベクトルとしてまとめてしまえば計算は痛快に減る。よって、Rubyで実装するコードは簡単なVectorクラスを作ってそれを使って計算するようにしてみる。

  class Vector < Array
    def x;self[0];end
    def y;self[1];end
    def x=(v);self[0]=v;end
    def y=(v);self[1]=v;end

    def initialize(x, y)
      self[0] = x
      self[1] = y
    end

    def +(v)
      Vector.new(self[0] + v.x, self[1] + v.y)
    end

    def -(v)
      Vector.new(self[0] - v.x, self[1] - v.y)
    end

    def *(v)
      Vector.new(self[0] * v, self[1] * v)
    end

    def /(v)
      Vector.new(self[0] / v, self[1] / v)
    end

    def normalize
      dx = self[0]
      dy = self[1]
      len = Math.sqrt(dx * dx + dy * dy)
      if len > 1e-6
        ilen = 1.0 / len
        dx *= ilen
        dy *= ilen
      end
      Vector.new(dx, dy)
    end

    def rperp
      Vector.new(self[1], -self[0])
    end
  end

別々に計算したほうがメソッド呼び出しが減って速いのかもしれんけどな。

頂点計算の準備

ベクトル化に伴い、MoveTo/LineToクラスをVectorクラスの派生とした。

  class MoveTo < Vector;end
  class LineTo < Vector;end
  class Point < Struct.new(:v, :dv, :dmv);end

PointクラスはVectorを3つ持つ。点の位置と、そこから始まる線の向きを表すベクトルと、その点の前後の線を繋ぐ接続点のベクトルである。んで、flatten_pathsメソッドの中でこのうちのdvを計算しておく。

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

    # 点の次の線の向きを計算
    # ループしない場合は最後の点の情報は使われない
    ([@points.last] + @points).each_cons(2) do |p0, p1|
      p0.dv = (p1.v - p0.v).normalize
    end
  end

MoveToクラスもLineToクラスもx/yでアクセスできるVectorには変わり無いのでPointのvにそのまま入れることにした。

頂点計算

ここからが本番。まずstrokeをこのように作り直す。

  def stroke
    flatten_paths
    expand_stroke(@width / 2.0)
    render_stroke
  end

expand_stroke登場である。さっき作ったrender_strokeも呼ぶ。すなわちTriangleStripの頂点配列はexpand_strokeの中で作るという決意の表れである。expand_strokeはこのような感じ。

  private def expand_stroke(w)
    calculate_joins

    # 始点の処理(BUTT固定)
    p0 = @points[0]
    butt_cap(p0.v, p0.dv, w, -0.5)

    # 中間の頂点生成
    @points[1..-2].each do |p0|
      @verts << p0.v + p0.dmv * w
      @verts << p0.v - p0.dmv * w
    end

    # 終点の処理(BUTT固定)
    p0 = @points[-2]
    p1 = @points[-1]
    butt_cap(p1.v, p0.dv, w, -0.5)
  end

まずcalculate_joinsを呼び出して、その後に始点の処理、中間の処理、終点の処理と続く。始点・終点の処理のbuttというのはぶつ切りする処理で、他にはsquareとかroundとかがある。まだ作っていない。calculate_joinsはこう。

  private def calculate_joins
    # 点の両側の線の中間を算出する
    ([@points.last] + @points).each_cons(2) do |p0, p1|
      p1.dmv = (p0.dv.rperp + p1.dv.rperp) * 0.5
      dmr2 = p1.dmv.x * p1.dmv.x + p1.dmv.y * p1.dmv.y
      if dmr2 > 0.000001
        scale = 1.0 / dmr2
        if scale > 600.0
          scale = 600.0
        end
        p1.dmv *= scale
      end
    end
  end

この計算は実際よくわかっていないのだが、線の接続点における頂点を算出している。これはベクトル情報であり、このベクトルに線の太さの半分をかけて点の位置に加算することで接続頂点位置が算出できる。何を言っているのかはまあ、描画結果を見るとなんとなくわかるかもしれない。次はbutt_cap。

  private def butt_cap(p0, dv, w, d)
    pv = p0 - dv * d
    dlv = dv.rperp * w
    @verts << pv + dlv - dv
    @verts << pv - dlv - dv
    @verts << pv + dlv
    @verts << pv - dlv
  end

NanoVGでは始点用と終点用にわかれていたのだが、なんか見た目はできてるのに頂点順がおかしい現象で試行錯誤していたら「これ1つでいいんじゃね?」ってなってまとめた。buttだけじゃなくてsquareの時にも使うからひょっとしたらその場合におかしくなるかもしれない。ていうかたぶんなる。その時に分ける。ともあれ、1ピクセルぶんぐらい線を伸ばす意味はさっぱりわからない。なんでなんだろうな。それはそれとして、これで実行すると以下のような描画になる。

中間点の計算をするcalculate_joinsを作ってTriangleStripで三角形を指定するようにしたので接続がちゃんとできるようになった。端の処理はあってもなくても変わらんのかもしれんけど、実はこっちをやるのがメインだったのだが端の処理作ったら接続できるようになってしまった。

おしまい

NanoVGを見ながら作っているので同様の名称のメソッドがあり、やっている処理も同じ場所、という感じになっている。NanoVGのコードを見るときの参考になりそうな気もするが、現状では中身がほとんど無いのであんまり参考にはならない気がする。
今回のソースはこちら

ベクタグラフィックス

新年あけましておめでとうございます。今年もよろしくお願いいたします。
さて、ちょと前に作ったラスタライザを、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で処理しとけばいいんじゃね?って思ってやってみたら繋がるようになった。
今回のコード全体はこちら

1年まとめ

今年は特に何もやってないな〜と思いつつ、1年まとめ。
物理エンジンを作ってみて壮絶に討ち死にした
コンポーネント指向を考えてみた
・DXRuby1.4.3〜1.4.5リリース
・連鎖性言語というのを作ってみた
・FiddleとffiDirectXを叩いてみた
・NanoVGのソース読んで勉強した
んー。

連鎖性言語、手元ではOrectorと名前が付いているが、Quatationの扱いが非常に難しかった。とにかくこいつのせいで実行時まで実行するコードが確定しないのでコンパイルのしようがない。Factorはそのへんどうやってるのかな。読もうと思っても処理系がFactorで書かれていて読めない。どうにも修行が足りない。
あと、Qiitaに初めて記事を書いた。ゲームライブラリとかのAdventCalendarなのだが、他のんがすごすぎて余裕で霞む。ああいう本格的なやつと比べたらそりゃもうオモチャみたいなもんなのでしょうがないのだが、しかしそれにしても書くことが無くて困った。んでもまあ、よかったことが一つあって、あれを書いてみたことで「そういえばDXRubyは完全に俺の趣味だし他の人のために作ったわけじゃないし今までの機能追加も基本的には俺がやりたいことをするのに機能が足りないから追加したようなもんだよな?」という、ある意味初心を思い出した。学習用に使われてるとか初心者向けというのは結果論であって狙ったもんではないし、結果論と言っても現状であり経過なわけで、今後そうあり続けるかというとそうかもしれないしそうでないかもしれない。俺が突然統計とか分析に目覚めて何故かそんな機能が追加されても誰も文句は言えないのである。さすがに無いと思うけど。
ということで来年もやりたいことをやる年でありますように。

ラスタライザを作る

もう年末なのだが、ちょっと前に作ってみたラスタライザの話。といってもすごいしょぼいもので、とりあえず太さを指定した直線をDXRubyのImageに描画するというもの。

ようするに三角形2個で四角を描いているだけなのだが、それだったらImageのメソッドでできるんじゃね?って感じで、じゃあなんでこれを作ったのかというと、NanoVGみたいなベクタグラフィックスを作ってみたくて、グラデーションとかやるのに自前で描画する必要があって、とりあえず初期型という状態、なわけだ。
まあ、Rubyでベクタグラフィックス作ってなんかいいことあるのかっつーとよくわからんのだけども、そもそも趣味が作ってみたいものを作る、というものなわけで、作ってみた結果についてはどうでもいいのである。
ちなみにこのコードのキモはエッジファンクションというやつで、ラスタライザの基本となる古典的な判定計算である。高速化は色々とできる余地はあるがとりあえず普通に結果が見れるのでいずれ遅いと思ったときに直す。

require 'dxruby'

# ラスタライズ結果を見やすく描画する
module Viewer
  def self.draw(image, triangles)
    s = 400.0 / image.width
    Window.mag_filter = TEXF_POINT
    Window.draw_ex(50, 50, image, scale_x:s, scale_y:s, center_x:0, center_y:0)
    image.width.times do |c|
      Window.draw_line(c * s + 50, 50, c * s + 50, image.width * s + 50, [100, 100, 100])
      Window.draw_line(50, c * s + 50, image.width * s + 50, c * s + 50, [100, 100, 100])
    end
    
    triangles.each do |ary|
      x1, y1, x2, y2, x3, y3 = *ary
      Window.draw_line(x1 * s + 50, y1 * s + 50, x2 * s + 50, y2 * s + 50, C_RED)
      Window.draw_line(x2 * s + 50, y2 * s + 50, x3 * s + 50, y3 * s + 50, C_RED)
      Window.draw_line(x3 * s + 50, y3 * s + 50, x1 * s + 50, y1 * s + 50, C_RED)
    end
  end
end

class OreVG
  attr_accessor :image, :triangles

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

  # EdgeFunction
  # E(x, y) = (x - X) * dy - (y - Y) * dx
  # E(x, y) <= 0で中と判定。3つのエッジの内側であれば塗る。
  def rasterize(x1, y1, x2, y2, x3, y3)
    dx12 = x2 - x1
    dy12 = y2 - y1
    dx23 = x3 - x2
    dy23 = y3 - y2
    dx31 = x1 - x3
    dy31 = y1 - y3

    # 基点。ピクセルの中心で計算する。
    e1 = (0.5 - x1) * dy12 - (0.5 - y1) * dx12
    e2 = (0.5 - x2) * dy23 - (0.5 - y2) * dx23
    e3 = (0.5 - x3) * dy31 - (0.5 - y3) * dx31

    @image.height.times do |y|
      et1 = e1
      et2 = e2
      et3 = e3
      @image.width.times do |x|
        yield x, y if et1 <= 0 and et2 <= 0 and et3 <= 0
        et1 += dy12
        et2 += dy23
        et3 += dy31
      end
      e1 -= dx12
      e2 -= dx23
      e3 -= dx31
    end
  end

  def triangle(x1, y1, x2, y2, x3, y3)
    self.rasterize(x1, y1, x2, y2, x3, y3) do |x, y|
      @image[x,y] = [100, 100, 255]
    end
    @triangles << [x1, y1, x2, y2, x3, y3]
  end

  def line(x1, y1, x2, y2, w)
    dx = x2 - x1
    dy = y2 - y1
    len = Math.sqrt(dx * dx + dy * dy)
    dmx = dx / len
    dmy = dy / len

    tx1 = x1 + dmy * w * 0.5
    ty1 = y1 - dmx * w * 0.5
    tx2 = x1 - dmy * w * 0.5
    ty2 = y1 + dmx * w * 0.5
    tx3 = x2 + dmy * w * 0.5
    ty3 = y2 - dmx * w * 0.5
    tx4 = x2 - dmy * w * 0.5
    ty4 = y2 + dmx * w * 0.5
    self.triangle(tx1, ty1, tx3, ty3, tx2, ty2)
    self.triangle(tx2, ty2, tx3, ty3, tx4, ty4)
  end

end



vg = OreVG.new(50)
#viewer.triangle(1, 1.1, 9.1, 2.8, 4.2, 9.4)
#viewer.triangle(11, 11.1, 19.1, 12.8, 14.2, 19.4)
vg.line(2, 5, 42, 25, 10)
Window.loop do
  Viewer.draw(vg.image, vg.triangles)
end