2017/04/16(日) [n年前の日記]
#1 [ruby][dxruby] Ruby+cairo(rcairo)を勉強し直し
矩形グラデ塗り+境界線描画を自分で書くのが面倒になってきたので、既存の描画ライブラリを使いたいなと。そこで、cairo(rcairo)はどうだろうと。
_Rubyist Magazine - cairo: 2 次元画像描画ライブラリ
一応、以前に少し触った記憶もあるのだけど。
_mieki256's diary - cairoの出力結果をファイルを作らずにDXRubyに渡せた
_mieki256's diary - DXRuby と cairo と rsvg2 で svgを描画して表示してみたり
_mieki256's diary - DXRubyとcairoでswfのシェイプを描画してみる実験
使い方をすっかり忘れてるので再度勉強し直し。
環境は Windows10 x64 + Ruby 2.2.6 p396。
_Rubyist Magazine - cairo: 2 次元画像描画ライブラリ
一応、以前に少し触った記憶もあるのだけど。
_mieki256's diary - cairoの出力結果をファイルを作らずにDXRubyに渡せた
_mieki256's diary - DXRuby と cairo と rsvg2 で svgを描画して表示してみたり
_mieki256's diary - DXRubyとcairoでswfのシェイプを描画してみる実験
使い方をすっかり忘れてるので再度勉強し直し。
環境は Windows10 x64 + Ruby 2.2.6 p396。
◎ rcairoのインストール。 :
Ruby 2.x を使っているなら、おそらく以下でインストールできる、ような気がする。
もしかすると以下も必要になるのだろうか。そのへんちょっとよく分からず。
gem install cairo --platform=x86-mingw32
もしかすると以下も必要になるのだろうか。そのへんちょっとよく分からず。
gem install glib2 gem install gdk_pixbuf2 gem install rsvg2
◎ 使い方の基本。 :
require 'cairo' surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, 640, 480) # フォーマット, 幅, 高さ context = Cairo::Context.new(surface) context.set_source_rgba(1.0, 0.0, 0.0, 1.0) # R,G,B,A。0.0から1.0で指定 context.rectangle(0, 0, 16, 8) # x, y, 幅, 高さ context.fill # 塗り潰しサーフェイスを作って、コンテキストを作って、そのコンテキストを利用してアレコレ描画、という流れになるらしい。
◎ DXRubyのImageに変換して結果を表示。 :
cairo の描画結果を DXRuby の Image に変換して、その場で表示して結果を確認したい。
cairo には png出力する機能があるし、DXRuby には png を読み込む機能があるので…。pngファイルを一旦HDDに出力して、ソレを読み込み直せばなんとかなる。けど。一々画像ファイルをHDDに書き出してまた読み込むのもなんだかアレで。
StringIOを使うと、文字列に、ファイルと似たインターフェイスを持たせることができるらしい。つまり、メモリ上にファイル(の中身)が存在してるかのような状態にできるので、その仕組みを利用する。
_cairo_helloworld_dxruby.rb
表示できた。
もちろん、一旦pngに変換して出力 → pngを読み込み、てな回りくどいことをしてるので、60FPSでリアルタイムに描画内容を変更して表示、みたいなことは速度的にできないだろうけど。一応、cairo で描画して DXRuby で表示するだけならできなくもない、ということで。
cairo には png出力する機能があるし、DXRuby には png を読み込む機能があるので…。pngファイルを一旦HDDに出力して、ソレを読み込み直せばなんとかなる。けど。一々画像ファイルをHDDに書き出してまた読み込むのもなんだかアレで。
StringIOを使うと、文字列に、ファイルと似たインターフェイスを持たせることができるらしい。つまり、メモリ上にファイル(の中身)が存在してるかのような状態にできるので、その仕組みを利用する。
_cairo_helloworld_dxruby.rb
# cairo(rcairo)の描画テスト
require 'cairo'
require 'stringio'
require 'dxruby'
w, h = 640, 480
# サーフェイス作成
surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, w, h)
# コンテキスト作成。コレを使って描画していく
context = Cairo::Context.new(surface)
# 背景相当を描画。サーフェイス全体を透明色で塗り潰し
context.set_source_rgba(0, 0, 0, 0) # r, g, b, a
context.rectangle(0, 0, w, h) # 矩形を指定
context.fill # 塗り潰し
# 赤丸を描画
context.set_source_rgb(1, 0, 0) # r, g, b
radius = h / 3
context.arc(w / 2, h / 2, radius, 0, 2 * Math::PI) # 円を指定
context.fill
# StringIOを使ってpng出力
# StringIOを使うと、文字列にファイルと同様のインターフェイスを持たせられる
# つまり、メモリ上にファイルを作成しているのと似た状態になる
temp = StringIO.new("", 'w+')
surface.write_to_png(temp)
temp.rewind # アクセス位置を最初に戻す
# DXRubyのImageとして読み込む
img = Image.loadFromFileInMemory(temp.read)
temp.close
# DXRubyで表示
Window.bgcolor = [64, 64, 64]
Window.loop do
break if Input.keyPush?(K_ESCAPE)
Window.draw(0, 0, img)
end
表示できた。
もちろん、一旦pngに変換して出力 → pngを読み込み、てな回りくどいことをしてるので、60FPSでリアルタイムに描画内容を変更して表示、みたいなことは速度的にできないだろうけど。一応、cairo で描画して DXRuby で表示するだけならできなくもない、ということで。
◎ Ruby/Tkで使って結果を表示。 :
Ruby/Tk を使って cairo の描画結果を表示確認したい。
いやまあ、Ruby/Tk自体がキャンバスにアレコレ描画する機能を持ってるらしいので、Ruby/Tk を使うならわざわざ cairo で描かなくてもいいんじゃないのって気もしてくるけど、せっかくだから…。
これも、StringIO を使ってpng出力後、TkPhotoImage にデータを渡すやり方で表示できなくもない模様。ただし、Tk で png を読み込むためには、Tk拡張(この場合 tkimg?) が必要になるので注意。
_cairo_helloworld_tk.rb
一応表示できた。DXRubyのソレと比べると妙に時間がかかるけど…。
ちなみに、RMagick (Ruby から ImageMagick を扱えるライブラリ) を使って変換する事例も見かけたのだけど、Windows上では RMagick をインストールするのが大変、というか時期によってはハマるので、RMagick の利用は極力避けたいところ。
_RMagick を Windows にインストールする方法 - 君の瞳はまるでルビー - Ruby 関連まとめサイト
もっとも、Windows上では、Ruby/Tk の Tk拡張インストール作業自体が結構面倒臭いわけで…。「毒を食らわば皿まで」的に RMagick を使ってしまうのもアリだろうか…。
_Ruby/TkでTk拡張を使っているRubyスクリプトをexe化 - ござるのブログ
_mieki256's diary - Ruby/Tkで拡張Tkを使えるようにするために少し試したり
そもそも Ruby で GUI は鬼門…。Ruby本体のバージョンが上がるたびに放棄されていったGUIライブラリが死屍累々。なので、Ruby/Tk を使うという選択自体がどうなんだろうと思ったり思わなかったり。
ちなみに Python の場合は、tkinter + pycairo + PIL(Pillow) を使って、PILの ImageTk で cairo のサーフェイスを Tk で扱える状態に変換できるらしい。
_python - Cairo with tkinter? - Stack Overflow
いやまあ、Ruby/Tk自体がキャンバスにアレコレ描画する機能を持ってるらしいので、Ruby/Tk を使うならわざわざ cairo で描かなくてもいいんじゃないのって気もしてくるけど、せっかくだから…。
これも、StringIO を使ってpng出力後、TkPhotoImage にデータを渡すやり方で表示できなくもない模様。ただし、Tk で png を読み込むためには、Tk拡張(この場合 tkimg?) が必要になるので注意。
_cairo_helloworld_tk.rb
# cairo(rcairo) + Ruby/Tk の描画テスト
require 'cairo'
require 'tk'
require "tkextlib/tkimg/png"
require 'stringio'
w, h = 640, 480
# ----------------------------------------
# cairoによる描画
# サーフェイス作成
surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, w, h)
# コンテキスト作成。コレを使って描画していく
context = Cairo::Context.new(surface)
# 背景相当を描画。サーフェイス全体を塗り潰し
context.set_source_rgba(0, 0, 0, 0) # r, g, b, a
context.rectangle(0, 0, w, h) # 矩形を指定
context.fill # 塗り潰し
# 赤丸を描画
context.set_source_rgb(1, 0, 0) # r, g, b
radius = h / 3
context.arc(w / 2, h / 2, radius, 0, 2 * Math::PI) # 円を指定
context.fill
# ----------------------------------------
# Tk関係の設定
# Tkでキャンバス作成
canvas = TkCanvas.new(:width => w, :height => h).pack
# フルカラー画像を扱えるクラスを生成
img = TkPhotoImage.new(width: w, height: h)
# cairoの描画結果をStringIOを使ってpng出力後、
# TkPhotoImage の data に渡す
StringIO.open { |io|
surface.write_to_png(io)
img.data = Tk.BinaryString(io.string)
}
x, y = w / 2, h / 2 # 画像の中心が描画位置の基準になるらしい
TkcImage.new(canvas, x, y, :image => img)
# Tkのウインドウを表示
Tk.mainloop
一応表示できた。DXRubyのソレと比べると妙に時間がかかるけど…。
ちなみに、RMagick (Ruby から ImageMagick を扱えるライブラリ) を使って変換する事例も見かけたのだけど、Windows上では RMagick をインストールするのが大変、というか時期によってはハマるので、RMagick の利用は極力避けたいところ。
_RMagick を Windows にインストールする方法 - 君の瞳はまるでルビー - Ruby 関連まとめサイト
もっとも、Windows上では、Ruby/Tk の Tk拡張インストール作業自体が結構面倒臭いわけで…。「毒を食らわば皿まで」的に RMagick を使ってしまうのもアリだろうか…。
_Ruby/TkでTk拡張を使っているRubyスクリプトをexe化 - ござるのブログ
_mieki256's diary - Ruby/Tkで拡張Tkを使えるようにするために少し試したり
そもそも Ruby で GUI は鬼門…。Ruby本体のバージョンが上がるたびに放棄されていったGUIライブラリが死屍累々。なので、Ruby/Tk を使うという選択自体がどうなんだろうと思ったり思わなかったり。
ちなみに Python の場合は、tkinter + pycairo + PIL(Pillow) を使って、PILの ImageTk で cairo のサーフェイスを Tk で扱える状態に変換できるらしい。
_python - Cairo with tkinter? - Stack Overflow
◎ データを取り出したい。 :
cairo の描画結果を、DXRuby なり Ruby/Tk なりで一応表示できたけど。一旦 png にしてまた読んで、てのがアレだなと…。どうせ内部ではビットマップデータを持っているに違いないのだから、ソレを取り出して変換して、とかやれたらいいよなと。
cairo のサーフェイスからデータを取り出すのは、ImageSurface#data でできるらしい。
_バイトオーダ - ビッグエンディアン/リトルエディアン
Cairoのドキュメントを眺めてたら気になる記述が。
_Cairo: A Vector Graphics Library: Image Surfaces
つまり、今回はリトルエンディアンのCPUで動かしてるから、ARGB とは逆の並び、BGRA に見えたのだろう。
cairo のサーフェイスからデータを取り出すのは、ImageSurface#data でできるらしい。
irb(main):001:0> require 'cairo' => true irb(main):002:0> surface = Cairo::ImageSurface.new(8, 8) => #<Cairo::ImageSurface:0x34d88a8> irb(main):003:0> context = Cairo::Context.new(surface) => #<Cairo::Context:0x331fec0> irb(main):004:0> context.set_source_rgb(0.75, 0.5, 0.25) => #<Cairo::Context:0x331fec0> irb(main):005:0> context.rectangle(0, 0, 1, 1) => #<Cairo::Context:0x331fec0> irb(main):006:0> context.fill => #<Cairo::Context:0x331fec0> irb(main):007:0> surface.data => "@\x80\xBF\xFF\x00\x00\x00\x00\x00\x00\x00\x00 (中略) \x00\x00\x00\x00" irb(main):008:0>Windows10 x64上では、BGRA の順で並んでるように見える。が、このあたりはOSやCPUによって違う可能性もありそうな。エンディアンが関係してそうな予感…。
_バイトオーダ - ビッグエンディアン/リトルエディアン
Cairoのドキュメントを眺めてたら気になる記述が。
_Cairo: A Vector Graphics Library: Image Surfaces
CAIRO_FORMAT_ARGB3232bitの上位から A,R,G,B の順で並んでいて、「native-endian」で記録されてる、と書いてあるのかな。予想は当たってた。
each pixel is a 32-bit quantity, with alpha in the upper 8 bits, then red, then green, then blue.
The 32-bit quantities are stored native-endian.
Pre-multiplied alpha is used.
(That is, 50% transparent red is 0x80800000, not 0x80ff0000.) (Since 1.0)
つまり、今回はリトルエンディアンのCPUで動かしてるから、ARGB とは逆の並び、BGRA に見えたのだろう。
◎ 一旦png出力せずにDXRubyのImageに変換。 :
上記を踏まえて、cairo のサーフェイスを DXRuby の Image に変換してみる。
_packテンプレート文字列 を眺めたところ、unpack("L*") を使えば、エンディアンに依存した32bit符号無し整数、の配列が得られそうな雰囲気。後は、32bitをARGBに分解していけば…。
_cairo_helloworld_dxruby2.rb
表示された。合っているっぽい。
さて、png出力するソレと、Rubyでループを回して変換するソレ。どちらが速度的にマシなのだろう。ベンチマークを取ってみたり。おそらく、複雑な画像を描いた場合はpng圧縮/展開で更に時間がかかるだろうから、多少は複雑さを増した描画内容に変更してみたりもして。
_cairo_helloworld_dxruby3.rb
一旦png出力して読み込み直すほうが、全然速い…。ループを回して変換するソレは約5倍遅いのか…。
各ライブラリが持ってる機能はおそらくCで書かれてるから、処理に無駄があるように見えても、Rubyでゴチョゴチョやるよりは各ライブラリの機能を素直に使ったほうがまだ速い、ということかなと。しかし、まさかpng圧縮/展開してるソレに負けるとは…。今更ではあるけど、やっぱりRubyって遅いんだなあ…。
それと、予想通り、VGA程度の画像サイズですら 1/60秒 = 0.0167秒以内では処理が全然終わらないことも分かった。cairoのサーフェイスからDXRubyのImageに変換するだけでも0.5秒かかってるから…。例えばゲーム等に使ったとして、毎フレーム描画内容を変更したら、1〜2FPSになってしまう。話にならない。
そのあたりを考えると、FLASHって凄かったのだなと。ベクターデータをアンチエイリアスをかけながら描画しても、高フレームレートで動いてた印象が…。
_packテンプレート文字列 を眺めたところ、unpack("L*") を使えば、エンディアンに依存した32bit符号無し整数、の配列が得られそうな雰囲気。後は、32bitをARGBに分解していけば…。
_cairo_helloworld_dxruby2.rb
# cairo(rcairo) + DXRuby の描画テストその2
# サーフェイスのデータを変換してDXRubyのImageにしてみる例
require 'cairo'
require 'dxruby'
w, h = 640, 480
surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, w, h)
ct = Cairo::Context.new(surface)
ct.set_source_color(Cairo::Color::RED)
radius = h / 3
ct.arc(w / 2, h / 2, radius, 0, 2 * Math::PI)
ct.fill
# DXRubyのImageに変換。StringIOを使わない版
src = surface.data.unpack("L*") # 32bit符号無し整数の配列を得る
dst = []
(w * h).times do |i|
argb = src[i]
a = (argb >> 24) & 0x0ff
r = (argb >> 16) & 0x0ff
g = (argb >> 8) & 0x0ff
b = argb & 0x0ff
dst << a << r << g << b
end
# 配列からDXRubyのImageを作成
img = Image.createFromArray(w, h, dst)
# DXRubyで表示
Window.bgcolor = [64, 64, 64]
Window.loop do
break if Input.keyPush?(K_ESCAPE)
Window.draw(0, 0, img)
end
表示された。合っているっぽい。
さて、png出力するソレと、Rubyでループを回して変換するソレ。どちらが速度的にマシなのだろう。ベンチマークを取ってみたり。おそらく、複雑な画像を描いた場合はpng圧縮/展開で更に時間がかかるだろうから、多少は複雑さを増した描画内容に変更してみたりもして。
_cairo_helloworld_dxruby3.rb
ruby cairo_helloworld_dxruby3.rb
user system total real
cairo draw 0.031000 0.000000 0.031000 ( 0.021152)
use StringIO 0.047000 0.000000 0.047000 ( 0.048209)
not StringIO 0.235000 0.000000 0.235000 ( 0.242087)
一旦png出力して読み込み直すほうが、全然速い…。ループを回して変換するソレは約5倍遅いのか…。
各ライブラリが持ってる機能はおそらくCで書かれてるから、処理に無駄があるように見えても、Rubyでゴチョゴチョやるよりは各ライブラリの機能を素直に使ったほうがまだ速い、ということかなと。しかし、まさかpng圧縮/展開してるソレに負けるとは…。今更ではあるけど、やっぱりRubyって遅いんだなあ…。
それと、予想通り、VGA程度の画像サイズですら 1/60秒 = 0.0167秒以内では処理が全然終わらないことも分かった。cairoのサーフェイスからDXRubyのImageに変換するだけでも0.5秒かかってるから…。例えばゲーム等に使ったとして、毎フレーム描画内容を変更したら、1〜2FPSになってしまう。話にならない。
そのあたりを考えると、FLASHって凄かったのだなと。ベクターデータをアンチエイリアスをかけながら描画しても、高フレームレートで動いてた印象が…。
◎ 参考ページ。 :
[ ツッコむ ]
以上です。


