mieki256's diary



2014/06/02(月) [n年前の日記]

#1 [dxruby][ruby][cg_tools] DXRubyとcairoでswfのシェイプを描画してみる実験

_2014/05/29 に、DXRubyでswfのシェイプを描画する実験をしたのだけど。あまりにも処理が遅い上に、描画も汚くてしょんぼりだったわけですが。 _cairo と組み合わせて使えば、もっと速く、もっと綺麗にできるんじゃないかと思えたので試してみたり。

こんな感じになりました。右側が、cairoを使って描画した結果。
shapeparse_width_cairo.png
さすがに美しい…。cairo、凄いなあ。

ソースは以下。

_shapeparse_with_cairo.rb
require 'nokogiri'
require 'dxruby'
require 'cairo'
require 'tempfile'
require_relative 'bezierdraw'

# swfのシェイプを解析してImageに描画するクラス(cairo使用版)
#
class ShapeParse
  DBG = false
  DBG2 = true
  TWIP = 20.0
  attr_accessor :id
  attr_accessor :width
  attr_accessor :left
  attr_accessor :top
  attr_accessor :height
  attr_accessor :image
  attr_accessor :draw_data
  attr_accessor :clip_enable
  attr_accessor :surface
  attr_accessor :context

  def initialize(node, clip_enable = true)
    self.clip_enable = clip_enable
    self.id = node["objectID"].to_i
    puts "id[#{self.id}] #{node.name}" if DBG

    n = node.at(".//bounds/Rectangle")
    self.left = (n["left"].to_f / ShapeParse::TWIP).to_i
    self.top = (n["top"].to_f / ShapeParse::TWIP).to_i
    self.width = (n["right"].to_f / ShapeParse::TWIP).to_i
    self.height = (n["bottom"].to_f / ShapeParse::TWIP).to_i
    puts "size : (x,y)=#{self.left},#{self.top} (w,h)=#{self.width},#{self.height}" if DBG

    self.width = Window.width if self.width < Window.width
    self.height = Window.height if self.height < Window.height

    x0, y0 = 0, 0
    x1, y1 = 0, 0
    x2, y2 = 0, 0
    f0_enable = true
    f1_enable = true
    l_enable = true
    fc = [[0, 0, 0, 0], [0, 0, 0, 0]]
    lc = [0, 0, 0, 0]
    lwidth = ShapeParse::TWIP
    lines = []
    self.draw_data = []

    n = node.at(".//shapes/Shape/edges").child

    while n != nil
      case n.name
      when "ShapeSetup"
        unless lines.empty?
          save_draw_data(lines, f0_enable, f1_enable, l_enable,
                         lwidth, fc[0], fc[1], lc)
          lines = []
        end

        s = "#{n.name} "
        x0 = n["x"].to_f if n.key?("x")
        y0 = n["y"].to_f if n.key?("y")
        f0_enable = (n["fillStyle0"] == "1")? true : false
        f1_enable = (n["fillStyle1"] == "1")? true : false
        l_enable =  (n["lineStyle"] == "1")? true : false

        if false
          s += "(x,y)=#{x0/TWIP}, #{y0/TWIP} ("
          s += (f0_enable)? "f0 " : "   "
          s += (f1_enable)? "f1 " : "   "
          s += (l_enable)? "l" : " "
          s += ")"
          puts s
        end

        n.xpath(".//fillStyles").each do |nn|
          nn.xpath(".//Solid/color/Color").each_with_index do |m, i|
            fc[i] = get_color(m)
          end
        end

        n.xpath(".//lineStyles").each do |nn|
          nn.xpath(".//LineStyle").each do |m|
            lwidth = m["width"].to_i if m.key?("width")
            m.xpath(".//color//Color").each do |c|
              lc = get_color(c)
            end
          end
        end

        if n.key?("x") and n.key?("y")
          lines.push([x0, y0])
        end

      when "CurveTo"
        # 二次ベジェ曲線
        x1 = x0 + n["x1"].to_f
        y1 = y0 + n["y1"].to_f
        x2 = x1 + n["x2"].to_f
        y2 = y1 + n["y2"].to_f
        lst = BezierDraw.calc(x0, y0, x1, y1, x2, y2)
        lines.concat(lst.slice(1..-1))
        x0 = x2
        y0 = y2

      when "LineTo"
        # 直線
        x1 = x0 + n["x"].to_f
        y1 = y0 + n["y"].to_f
        lines.push([x1, y1])
        x0 = x1
        y0 = y1

      else
        puts "!! Unknown Tag : #{n.name}"
      end
      n = n.next
    end

    draw(false)
  end

  # 色配列を取得
  #
  # @param [Object] node 「Color」ノード
  # @return [Array] A,R,G,Bが入った配列
  #
  def get_color(node)
    col = [255, 0, 0, 0] # ARGB
    ["alpha", "red", "green", "blue"].each_with_index do |s,i|
      col[i] = node[s].to_f if node.key?(s)
    end
    return col
  end

  # 描画用の直線データを記録
  #
  # @param [Array] lines 直線データ配列
  # @param [Boolean] fe0 塗り潰し0をするか否か
  # @param [Boolean] fe1 塗り潰し1をするか否か
  # @param [Boolean] le 線を描くか否か
  # @param [Number] lw 線幅
  # @param [Array] fc0 塗り潰し0の色配列
  # @param [Array] fc1 塗り潰し1の色配列
  # @param [Array] lc1 線描画の色配列
  #
  def save_draw_data(lines, fe0, fe1, le, lw, fc0, fc1, lc)
    np = []
    ox, oy = nil, nil
    lines.each do |p|
      x, y = p
      x /= TWIP
      y /= TWIP
      np.push([x, y]) if x != ox or y != oy
      ox, oy = x, y
    end

    # 記録
    self.draw_data.push([np, fe0, fe1, le, lw, fc0.dup, fc1.dup, lc.dup])
  end

  # Imageオブジェクトに描画
  #
  # @param [Boolean] clear_enable trueならImageをクリアしてから描画
  #
  def draw(clear_enable = true, scale_x = 1.0, scale_y = 1.0)
    # self.image.clear if clear_enable

    # cairoのサーフェスを確保
    self.surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32,
                                           self.width, self.height)
    self.context = Cairo::Context.new(surface)

    self.draw_data.each do |d|
      p, fe0, fe1, le, lw, fc0, fc1, lc = d
      np = []
      p.each { |q| np.push([q[0] * scale_x, q[1] * scale_y]) }
      draw_line(self.context, np, fe0, fe1, le, lw, fc0, fc1, lc)
    end

    # 一時ファイル作成
    temp = Tempfile.new(['shapeparse', '.png'])
    temp.binmode
    temp.close

    # pngファイルに出力
    self.surface.write_to_png(temp.path)

    # DXRubyで読み込み
    self.image = Image.load(temp.path)
    temp.close(true)
  end

  # 色配列の数値を変換して返す
  #
  # @note 255までの値を持っていた色配列を、1.0までの値に変換する
  #
  # @param [Array] c 色配列ARGB
  # @return [Array] 色配列RGBA
  #
  def get_rgba(c)
    r = []
    c.each do |n|
      m = n / 255.0
      m = 0.0 if m < 0.0
      m = 1.0 if m > 1.0
      r.push(m)
    end
    return r[1], r[2], r[3], r[0]
  end

  # 多角形を描画
  #
  # @param [Object] ct cairoのcontext
  # @param [Array] p 頂点座標が入った配列。
  # @param [Boolean] f0_enable trueなら塗り潰し有、falseなら塗り潰し無
  # @param [Boolean] f1_enable trueなら塗り潰し有、falseなら塗り潰し無
  # @param [Boolean] l_enable trueなら線を描く、falseなら線を描かない
  # @param [Number] lwidth 線幅
  # @param [Array] fc0 色配列。A,R,G,B
  # @param [Array] fc1 色配列。A,R,G,B
  # @param [Array] lc 色配列。A,R,G,B
  #
  def draw_line(ct, p, f0_enable, f1_enable, l_enable,
                lwidth, fc0, fc1, lc)
    return if (!f0_enable and !l_enable)

    ct.set_line_join(Cairo::LineJoin::ROUND) # 結合点の種類を指定
    ct.set_line_cap(Cairo::LineCap::ROUND) # 線の終点の種類を指定

    lw = lwidth / TWIP
    lw = 1.0 if lw < 1.0 # swfは1.0より細い線幅も1.0の線幅で描画するらしい
    ct.set_line_width(lw) # 線幅を指定

    # 塗る色を指定
    if f0_enable
      ct.set_source_rgba(get_rgba(fc0))
    else
      ct.set_source_rgba(get_rgba(lc))
    end

    x0 , y0 = p[0]
    ct.move_to(x0, y0)
    1.step(p.size - 1, 1) do |i|
      ct.line_to(p[i][0], p[i][1])
    end

    if f0_enable
      # 塗り潰し
      ct.fill_preserve
    end

    # 線を描画
    ct.set_source_rgba(get_rgba(lc)) if l_enable

    ct.stroke
  end
end

if $0 == __FILE__
  # ----------------------------------------
  # 動作テスト

  font = Font.new(14)

  # swfmillで swf → xml変換したxmlを解析
  # infile = "swfdata/test_pdr1.xml"
  infile = "swfdata/test_pdr2.xml"
  doc = Nokogiri::XML(File.open(infile)) {|cfg| cfg.noblanks}

  objs = Hash.new
  doc.xpath("//DefineShape3").each do |node|
    pdr = ShapeParse.new(node, false) # ベクターデータを解析して画像化
    objs[pdr.id] = pdr
  end

  redraw_fg = false
  ang = 0
  scale = 1.0

  Window.bgcolor = [0, 190, 118]
  Window.fps = 60

  # メインループ
  Window.loop do
    break if Input.keyPush?(K_ESCAPE)

    # Rキー押しでスケールを変えて再描画
    redraw_fg = (Input.keyDown?(K_R))? true : false

    objs.each_value do |o|
      # ベクターデータを再描画できなくもないが、遅くて実用にならない
      o.draw(true, scale, scale) if redraw_fg
      Window.draw(0, 0, o.image)
    end

    s = "#{Window.real_fps} FPS / CPU: #{Window.getLoad.to_i}%"
    Window.drawFont(4, 4, s, font)

    ang += 10
    scale = Math.cos(ang * Math::PI / 180.0) + 1.5
  end
end

データ一式も置いときます。

_swf_shape_parse_20140602.zip (434KB)

アンチエイリアスをかけながら描画してくれるライブラリなだけあって、さすがに綺麗。しかも、CPU負荷率を見る限りでは、自前で線描画や多角形塗りをするよりも、40倍ぐらい速くなりました。

それでもやっぱり、リアルタイムに描画するのは速度的に無理なので、実用にはならないのですけど。どう考えても、最初からレンダリング済みのビットマップ画像を用意しておくべきだなと。

更に、このやり方、現状ではかなりトホホな点があって。cairo で描画した後、結果を一旦 pngファイルでHDDに保存して、それを DXRuby の Image にロードし直していたりするわけで。

これがもし、cairo の Surface から、pngファイルとして書き出すべきバイナリデータを直接取得できれば…。あるいは、Surface内容をRGBAの配列で取得できれば、わざわざクソ遅いHDDにアクセスしなくても済むのですけど。残念ながら、そのようなメソッドは見つからなくて。

それにしても、swfのシェイプをこうして描画できるということは、svgも描画できたりするのかしら。たしか、svg を解析・描画する Rubyライブラリも存在していたはずなので、ちと試してみたいところ。

メモリマップドファイルは使えないかな。 :

メモリ上に仮想ファイルシステムの類でも作れたら、HDDにアクセスしなくても済むんじゃないかと思えてきたのだけど。そういうことができるライブラリってあるのかしら。

_メモリマップトファイル - Wikipedia には、
Ruby : Mmap というgem(ライブラリ)があり、メモリマップトファイルを実装している。

メモリマップトファイル - Wikipedia より

という記述があったのだけど。

_Rubyist Magazine - Ruby Library Report 【第 4 回】 Win32Utils
_djberg96/win32-mmap - GitHub

もしかして、Windows + Ruby なら、win32-mmap てのが使える…?

DXRuby + cairo の組み合わせ。 :

cairo の使い方については、 _rcairo 事始め - takihiro日記 を参考にさせてもらって試していたのですけど。せっかくだから、DXRuby で結果表示をするサンプルも置いときます。

_test_cairo1.rb
# cairo のテスト
#
# 以下を参考にしました。
#
# rcairo 事始め - takihiro日記
# http://d.hatena.ne.jp/takihiro/20100331/1269992290

require 'cairo'
require 'dxruby'
require 'tempfile'

w, h = 640 - 32, 480 - 32
surface = Cairo::ImageSurface.new(w, h)
context = Cairo::Context.new(surface)

# 背景色を指定
context.set_source_rgba(0, 0, 0, 0) # 透明
context.rectangle(0, 0, w, h)
context.fill

# 四角を書く
context.set_source_color(Cairo::Color::RED)
x, y, rw, rh = 20, 20, w / 2, h / 2
context.rectangle(x, y, rw, rh)
context.fill_preserve
context.set_source_color(Cairo::Color::BLACK)
context.stroke

# 円を書く
context.set_source_color(Cairo::Color::GREEN)
r = h / 3
x, y, rw, rh = 100, 30, 50, 50
context.arc(w/2, h/2, r, 0, 2 * Math::PI)
context.fill_preserve
context.set_source_color(Cairo::Color::BLUE)
context.stroke

# 多角形
context.set_source_color(Cairo::Color::YELLOW)
context.move_to(30, 30)
context.line_to(20, 60)
context.line_to(40, 80)
context.line_to(80, 40)
context.line_to(60, 30)
context.line_to(30, 30)
context.fill_preserve
context.set_source_color(Cairo::Color::PURPLE)
context.stroke

# 重ねる画像を設定
surface2 = Cairo::ImageSurface.from_png('sample2.png')
context.set_source(surface2,
                   w - surface2.width - 24,
                   h - surface2.height - 24)
context.paint

# 文字配置
context.set_source_rgb(255, 255, 255)
context.font_size = 26
context.move_to(10, 150)
context.show_text('文字') # フォントを指定してないので文字化けする

context.move_to(10, 180)
context.select_font_face('メイリオ', 0, 0)
context.show_text('メイリオフォント')

# ----------------------------------------
# 一時ファイルを作成
temp = Tempfile.new(['test', '.png'])
temp.binmode
temp.close

# pngファイルに出力
surface.write_to_png(temp.path)

# DXRubyで読み込み
img = Image.load(temp.path)
temp.close(true)

# DXRubyで表示
Window.bgcolor = C_BLUE
Window.loop do
  break if Input.keyPush?(K_ESCAPE)
  Window.draw(16, 16, img)
end

以下のような感じになります。
cairo_sample.png

以上です。

過去ログ表示

Prev - 2014/06 - Next
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project