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

#2 [cg_tools] フリーで使えるドローソフトを試用してみたり

Drawgraphic 2 がちょっとアレなソフトだったので、もしかするとフリーソフトのほうがグッドだったりしないのかなと疑念が湧いて。ググっていたら、いくつかのドローソフトが気になったので、それぞれ試用してみたりして。

環境は、Windows7 x64。

ちなみに、自分、普段は、 _Inkscape 0.48.2_LibreOffice Draw を使ってます。

Dynamic Draw を試用。 :

_Dynamic Draw

Drawgraphic 2 との比較だけど。
  • ビットマップ画像保存時、背景を透明にして出力できる。(ファイル → 選択部品の画像をエクスポート、を選べばビットマップ画像として保存できる)
  • ビットマップ画像保存時、アンチエイリアスを有効にして出力できる。
  • svgエクスポートにも対応。
  • 一瞬で起動する。爆速。
このあたりは、Dynamic Draw > Drawgraphic 2 かも。

ただ、操作性については若干難有り。
  • LibreOffice Draw ほど酷くはないけど、Drawgraphic 2 に比べると、色や線幅の指定が時々元に戻ってしまうので、連続での作業がしづらい。
  • メニューの表記が独特・Windowsの一般的なアプリから乖離していて、パッと見で「難しそう」と感じてしまう。ショートカットキーの割り当てを強調して伝えたいのは理解できるけど…。「F.ファイル E.編集 V.表示」より、「ファイル(F) 編集(E) 表示(V)」のほうが良いのでは…。
  • アイコンが全て白黒なので、ツールボタンの場所や定義が把握しにくい。昔の*NIX系ソフトでよく見かけた、ストイックなノリに近いというか。
特にUIの見た目は、親切心でやってみたことが逆効果になってる印象。他の一般的なアプリと合わせたほうがウケがイイ場面ってあるよなと…。まあ、CG関係のソフトは、えてしてUIに独自性を盛り込み過ぎてアレなことになる事例が多いのですけど。例えば Poser とか Bryce なんて、もっと酷かったし。…そのあたりを考えると、Drawgraphic 2 はバランス感覚があるように思えて好印象なのですが。

そんな感じで、操作性・作業のしやすさに関しては、Drawgraphic 2 > Dynamic Draw > LibreOffice Draw、という印象。

他に気づいた点をメモ。
  • 半透明塗りは無いらしい。
  • アンチエイリアス有効、かつ、背景を透明にしてビットマップ画像を出力すると、境界が汚くなる。いわゆるフリンジ?が出ちゃう。
要するに、アルファチャンネルが絡んでくる機能の実装は難有りかなと…。

グリッドをどの階層に描画するかという問題。 :

Drawgraphic 2 も、Dynamic Draw も、図形を塗り潰すとグリッドが隠れてしまって、途端に作業しづらくなるのだけど。

これが Inkscape なら、図形を塗り潰してもグリッド線が上に表示され続けるので、作業が快適だなと気付いたり。その代り(?)、Inkscape は全てがモッサリですが。

Drawgraphic 2 は、図形のアウトラインだけ描画するモード(ワイヤーフレーム表示)があるので、グリッドに頼った作業をするのであれば、そのモードで表示してしまうのもアリかなと。

たしか、FREEHAND にも簡易描画モードがあったような気がする。昔のドローソフトは、PCスペックが低くても作業できるようにするために、描画処理を軽くする仕様も求められたのだろうけど。結果的に、グリッドに頼った作業をする時も、回避策が存在している状態になったと言えるのかも。

Dynamic Draw には、アウトラインだけ描画するモードも、グリッドを最前面に描画する設定も、どちらも無いように見えました。

ParaDrawを試用。 :

_ParaDraw

  • グリッド表示・吸着機能がある。かつ、図形を塗り潰してもグリッドは手前に表示されてるので作業しやすい。
  • ビットマップ画像保存時は背景を透明にすることもできる。
  • アンチエイリアスもかかってる。
ゲーム等の素材画像を作る際には便利に使えるかもしれず。

ただ、ベクターデータとして swf か pdr でしか保存できないあたりが厳しい…。

pdr/swf/svg関係の変換ツール。 :

_PDR2SVG

pdr (ParaDraw形式) → svg変換ができる。試してみたところ、一部の線データが消滅(したように見えた)けど、そこそこsvgに変換してくれた。 _SVG終章 - パラフラノススメ - Yahoo!ブログ という記事によると、Flashとsvgは線の太さ(ストロークの太さ)について仕様が違うので再現は難しいらしい。

_SWF2SVG

swf → svg変換ができる。Adobe AIR製アプリ。 _FlashファイルからシェイプをSVGとして抜き出す - SWF2SVG MOONGIFT によると、コマンドラインツールだそうで、GUIは無し。以下のようなバッチファイルを、PATHの通ったところに置いて使ってみたり。

swf2svg.bat
@set SWF2SVGPATH="C:\Program Files (x86)\SWF2SVG\SWF2SVG.exe"
%SWF2SVGPATH% %1 %2 %3 %4 %5 %6 %7 %8 %9
こちらも比較的、swfの見た目を再現してくれた。

J.S Draw を試用。 :

_J.S Draw
_すらすらプログラマー J.S Draw

操作性・作業のしやすさに関しては、LibreOffice Draw と似た感じで、現時点ではあまりよろしくなく。図形を置くたびに色設定その他がリセットされたり、図形を塗り潰すとグリッドが隠れてしまったりで。

鋭意開発中のソフトらしく、機能面はまだまだこれから、らしい。しかし、それでも…。
  • 描画画面はアンチエイリアスがかかっている。
  • 保存形式としてxmlが選べる。
新世代らしさを感じるドローソフトだなと…。今後の発展に期待。

#3 [anime][neta] 「謎の支援者」ネタ

夢の中で、「盲目の美少女が、周囲からアレコレ助けてもらうのだけど、実はその周囲とは」てなネタを見て。目が覚めてから、ちょっと考え込んだり。

元ネタは、何だろうなあ…。

とりあえず、自分が夢でソレを見たのは、「HUNTERxHUNTER」の影響じゃないかなと思ったのだけど。学生時代にも、山口美由紀先生の短編作品でそういうのを見たような気もするし。子供の頃に、怪談の類でそういうネタを見ていたような気もするし。何にせよ、メジャーな元ネタ、様々なアレンジ作品があるような気がする。

「『周囲』の正体は何か?」という問いに対して、各人がどんな答えを出してくるかで、なんちゃって性格診断もできるかもしれないなと。幽霊だったり、ロボットだったり、モンスターだったり、あるいは、れっきとした人間だけど裏があったり、等々色々思いつくだろうけど。どれを思いついたのかで、その人の嗜好が分かりそう。真っ先に幽霊を思いつく人と、ロボットを思いつく人では、明らかに属性が違うだろから、「貴方は○○派です」と、テキトーな決めつけで分類して会話のネタにできたりしそう。ていうかソレってロールシャッハテストなのかな。

こういうネタは、小説だと表現しやすいのだろうな。「彼」「彼女」とだけ記述しておいて、実はこうだったのです、てな見せ方がしやすいし。小説は、本来の設定とは全く異なるイメージを読者が想像してしまうように、誘導することがたやすい。文字情報しかないから、意図的に誤解させる、という手管が使える。

漫画やアニメだと、ちょっと難しいよなと。「HUNTERxHUNTER」のように、本来そこにある状況をそのまま描く感じになって、怪談っぽい印象が随分と弱くなってしまう。

いや、違う見せ方もあるか…。当人の思い込みをそのまま視覚化しちゃうソレ。読者側はうっすらと状況が分かってるのに、提示されてる光景が異質なので、そのギャップでクラクラしてしまう。高野文子先生の「田辺のつる」…でしたっけか? あの見せ方。

別に、視覚的な見せ方で凝る必要はないのか…。「あしながおじさん」だって、謎の支援者ネタだよな…。

要するに、支援者の正体を隠しておいて、それを開示していくことで、話は作れるのだろうなと。自分が夢で見た「盲目の美少女」というソレは、支援者の姿を隠すために要する設定でしかなく。別の何かで隠せれば、それで済んでしまうのかも。

と、ここまで書いて、ふと思い出したけど。この手の元ネタは、「耳なし芳一」じゃないのかな…。

この記事へのツッコミ

Re: 「謎の支援者」ネタ by 名無しさん    2014/06/03 15:31
星新一でも似たような話がありましたね。
http://occult-atoaji.sakura.ne.jp/?p=8746

以上、1 日分です。

過去ログ表示

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