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