ベクタグラフィックスその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
ベクタグラフィックスその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とffiでDirectXを叩いてみた
・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