ベクタグラフィックスその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のコードを見るときの参考になりそうな気もするが、現状では中身がほとんど無いのであんまり参考にはならない気がする。
今回のソースはこちら