mieki256's diary



2014/05/31() [n年前の日記]

#1 [dxruby][cg_tools] DXRubyでswfアニメを再生する

DXRubyでswf(Flashアニメ)を再生してみたり。ビットマップ画像を表示するswfしか再生できないけど、ある程度動いてるので一応アップロード。

スクリーンショット。 :

以下が、 _元swf を、DXRuby上で再生した際のスクリーンショット。(GIFアニメにするために、減色・フレームレート・画像サイズを落としてあります)

swfparse_demo.gif
雰囲気ぐらいは伝わるかな…。

実験の動機。 :

「DXRubyに動画再生機能をつけてほしい」という要望をどこかで目にして、「動画ファイル再生は無理でも、swf再生ならできるんじゃないかな…。60FPSで画像をグリグリ動かせるんだし…」と思ったのがキッカケ。

実際使うかどうかは別にして、自分の思いつきが、本当に実現可能かどうか、なんだか気になったわけで。

処理の概要。 :

  • swfmill で swf を xml に変換。
  • Ruby + Nokogiri を使って xml を解析しつつ、DXRuby で描画。

制限事項。 :

以下は対応済み。
  • ビットマップ画像の移動・拡大縮小・回転・傾斜・半透明表示
  • 輝度変更 (明度変更)、色変更
  • 簡素な階層のスプライト (MovieClip)
  • 静止テキスト (デバイスフォント "_ゴシック", "_明朝", "_等幅" 等を指定していて、入れ子になってない簡素なもの)

以下は未対応。
  • シェイプ(ベクターデータ)
  • サウンド
  • ActionScript
  • 複雑な階層のスプライト(MovieClip)
  • テキストの拡大縮小・回転・傾斜、入れ子構造

ソースやサンプルデータ一式。ライセンス。 :

zip にして置いときます。 swfや画像のファイルサイズが大き過ぎたので、Dropbox に置いてみました。 Dropboxのサービス内容が一部変わったので自宅サーバに置き直しました。(2017/03/19)


_swf_parse_20140531.zip (67MB)

自分が書いたソースに関しては、Public Domain ってことでよろしくです。

ソース。 :

_playxmlmovie.rb
require "nokogiri"
require "dxruby"
require "find"
require 'benchmark'

# swfから変換したxmlを読み込んでDXRubyで再生するクラス
#
# @note swf から xml への変換は、swfmill を利用。
# @note DXRuby、Nokogiri が必要。
# @note License : Public domain
#
# @example
#   require "playxmlmove"
#   m = PlayXmlMovie.new("hoge.xml")
#   m.init
#   Window.loop do
#     break unless m.play
#   end
#
class PlayXmlMovie
  DBG = false
  TMDBG = false
  BMDBG = false

  # 論理ピクセル値
  TWIP = 20.0

  # ----------------------------------------
  # 色変更をするためのShader
  #
  class SpriteShader < Shader

    hlsl = <<EOS
  float4 g_mul;
  float4 g_ofs;
  texture tex0;

  sampler Samp0 = sampler_state
  {
   Texture =<tex0>;
  };

  float4 PS(float2 input : TEXCOORD0) : COLOR0
  {
    float4 output;

    output = tex2D( Samp0, input );
    output.r = (output.r * g_mul[0]) + g_ofs[0];
    output.g = (output.g * g_mul[1]) + g_ofs[1];
    output.b = (output.b * g_mul[2]) + g_ofs[2];
    output.a = (output.a * g_mul[3]) + g_ofs[3];
    return output;
  }

  technique
  {
   pass
   {
    PixelShader = compile ps_2_0 PS();
   }
  }
EOS

    @@core = Shader::Core.new(hlsl, {:g_mul => :float, :g_ofs => :float})

    def initialize
      super(@@core, "TShader")
      self.g_mul = [1.0, 1.0, 1.0, 1.0]
      self.g_ofs = [0.0, 0.0, 0.0, 0.0]
      @col = [1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0]
      class << self
        protected :g_mul, :g_mul=, :g_ofs, :g_ofs=
      end
    end

    def set_color(a)
      v = 256.0
      self.g_mul = [ a[0] / v, a[1] / v, a[2] / v, a[3] / v]
      v = 255.0
      self.g_ofs = [ a[4] / v, a[5] / v, a[6] / v, a[7] / v]
      @col = a
    end
  end

  # ----------------------------------------
  # MATRIX情報格納用クラス
  #
  class TMatrix
    attr_accessor :trans_x, :trans_y
    attr_accessor :scale_x, :scale_y
    attr_accessor :skew_x, :skew_y
    attr_accessor :color

    def initialize(trans_x = 0, trans_y = 0,
                   scale_x = 1, scale_y = 1,
                   skew_x = 0, skew_y = 0,
                   mul_r = 256, mul_g = 256, mul_b = 256, mul_a = 256,
                   ofs_r = 0, ofs_g = 0, ofs_b = 0, ofs_a = 0)
      self.trans_x, self.trans_y = trans_x,trans_y
      self.scale_x, self.scale_y = scale_x, scale_y
      self.skew_x, self.skew_y = skew_x, skew_y
      self.color = [ mul_r, mul_g, mul_b, mul_a, ofs_r, ofs_g, ofs_b, ofs_a]
    end

    #
    # 属性値を更新
    #
    # @param [Hash] v 属性と値のハッシュ
    #
    def set_value(v)
      self.trans_x = v["transX"].to_f if v.has_key?("transX")
      self.trans_y = v["transY"].to_f if v.has_key?("transY")
      self.scale_x = v["scaleX"].to_f if v.has_key?("scaleX")
      self.scale_y = v["scaleY"].to_f if v.has_key?("scaleY")
      self.skew_x = (v.has_key?("skewX"))? v["skewX"].to_f : 0
      self.skew_y = (v.has_key?("skewY"))? v["skewY"].to_f : 0

      [
        "factorRed", "factorGreen", "factorBlue", "factorAlpha",
        "offsetRed", "offsetGreen", "offsetBlue", "offsetAlpha"
      ].each_with_index do |s, i|
        self.color[i] = v[s].to_f if v.has_key?(s)
      end
    end

    #
    # 座標変換
    #
    # @param [Number] x x座標
    # @param [Number] y y座標
    # @return [Array] 座標変換後のx,y座標
    #
    def trans(x, y)
      nx = (x * self.scale_x) + (y * self.skew_y) + self.trans_x
      ny = (x * self.skew_x) + (y * self.scale_y) + self.trans_y
      return nx, ny
    end

    #
    # 色変換パラメータを更新
    #
    # @param [Array] c 色変更情報の配列
    # @return [Array] 変更後の色変更情報配列
    #
    def trans_color(c)
      nc = Array.new(8)
      for i in 0..3 do
        nc[i] = c[i] * self.color[i] / 256.0
        nc[i] = 512 if nc[i] > 512
        nc[i] = -512 if nc[i] < -512
      end
      for i in 4..7 do
        nc[i] = c[i] + self.color[i]
        nc[i] = 255 if nc[i] > 255
        nc[i] = -256 if nc[i] < -256
      end
      return nc
    end

    #
    # 色変換が必要かどうかを返す
    #
    # @param [Array] c 色変換情報配列
    # @return [Boolean] trueなら色変換が必要。falseなら色変換は不要。
    #
    def TMatrix.trans_color?(c)
      [
        [0, 256.0], [1, 256.0], [2, 256.0],
        [4, 0.0], [5, 0.0], [6, 0.0]
      ].each do |n|
        return true if c[n[0]] != n[1]
      end
      return false
    end

    #
    # 色変換情報を格納するハッシュを初期化する
    #
    # @param [Hash] v 色変換情報格納用ハッシュ
    # @return [Hash] 初期化したハッシュを返す
    #
    def TMatrix.init_color(v)
      v.merge!({ "factorRed" => 256, "factorGreen" => 256,
          "factorBlue" => 256, "factorAlpha" => 256,
          "offsetRed" => 0, "offsetGreen" => 0,
          "offsetBlue" => 0, "offsetAlpha" => 0 })
      return v
    end
  end

  # ----------------------------------------
  # 画像を格納するキャラobjクラス
  #
  class CharaObjImage
    attr_accessor :id, :src, :image

    def initialize(id, fname)
      self.id = id
      self.src = fname
      self.image = Image.load(fname)
      # puts "id[#{id}] Load Image ok #{fname}"
    end
  end

  # ----------------------------------------
  # シェイプを格納するクラス
  #
  class CharaObjShape
    attr_accessor :id, :imgs

    def initialize(id, imgs)
      self.id = id
      self.imgs = imgs
    end
  end

  # ----------------------------------------
  # スプライト(MovieClip)を格納するクラス
  #
  class CharaObjSprite
    attr_accessor :id
    attr_accessor :start_node

    def initialize(id, start_node)
      self.id = id
      self.start_node = start_node
    end
  end

  # ----------------------------------------
  # フォント名を格納するクラス
  #
  class CharaObjFont
    attr_accessor :id, :fontname

    def initialize(id, fontname)
      self.id = id
      self.fontname = fontname
      puts "id[#{id}] font [#{fontname}]" if PlayXmlMovie::DBG
    end
  end

  # ----------------------------------------
  # テキストを格納するクラス
  #
  class CharaObjText
    attr_accessor :id, :font, :text, :size, :fontname, :col

    def initialize(id, text, size, fontname, col = [0, 0, 0, 255])
      self.id = id
      self.text = text
      self.size = size
      self.fontname = fontname
      self.font = Font.new(self.size, fontname)
      self.col = col
      if PlayXmlMovie::DBG
        puts "id[#{id}] DefineEditText \"#{text}\",#{size},\"#{fontname}\""
      end
    end
  end

  # ----------------------------------------
  # 描画用クラス
  #
  class MySprite

    attr_accessor :objs
    attr_accessor :cobj
    attr_accessor :pobj
    attr_accessor :z
    attr_accessor :mat
    attr_accessor :sprs
    attr_accessor :rt
    attr_accessor :shader
    attr_accessor :enable_loop

    # コンストラクタ
    #
    # @param [Hash] objs キャラobj情報が入ったハッシュ
    # @param [Number] cid 発生させたいキャラobjのID
    # @param [Object] pobj 親スプライト。nilなら親は居ない
    # @param [Number] depth 描画深度
    # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ
    #
    def initialize(objs, cid, pobj, depth, v)
      self.objs = objs
      self.cobj = self.objs[cid]
      self.pobj = pobj
      self.z = depth
      self.mat = TMatrix.new
      self.sprs = Hash.new
      self.rt = nil
      self.shader = nil
      update(depth, v)
    end

    # 描画に必要な値を設定
    #
    # @param [depth] depth 描画深度
    # @param [Hash] v MATRIX情報と半透明度情報を格納したハッシュ
    #
    def update(depth, v)
      self.z = depth
      self.mat.set_value(v)
    end

    # RenderTargetを解放
    #
    def dispose
      if self.sprs != nil
        self.sprs.keys {|k| self.sprs[k].dispose }
      end

      if self.rt != nil
        self.rt.dispose unless self.rt.disposed?
      end
    end

    # 拡大縮小、半透明、回転描画
    #
    # @param [Object] img Imageクラス
    # @param [Object] im_mat TMatrixクラス
    #
    def draw_image(img, img_mat)
      pobj = self.pobj

      # 矩形の4点を配列にする
      w = img.width
      h = img.height
      p = [0, 0, w, 0, w, h, 0, h]

      c = Marshal.load(Marshal.dump(self.mat.color))

      # Shapeに割り当てられたMATRIXで変形
      4.times do |i|
        p[i * 2], p[i * 2 + 1] = img_mat.trans(p[i * 2], p[i * 2 + 1])
      end

      # 自身のMATRIXで変形
      4.times do |i|
        p[i * 2], p[i * 2 + 1] = self.mat.trans(p[i * 2], p[i * 2 + 1])
      end

      # 親スプライトのMATRIXで変形
      while pobj != nil
        4.times do |i|
          p[i * 2], p[i * 2 + 1] = pobj.mat.trans(p[i * 2], p[i * 2 + 1])
        end
        c = pobj.mat.trans_color(c)
        pobj = pobj.pobj
      end

      # 論理ピクセル値から画面座標に変換
      8.times {|i| p[i] /= PlayXmlMovie::TWIP }

      if TMatrix.trans_color?(c)
        # 色変換が必要
        # 色変更用のShaderはアルファチャンネル付き画像描画がおかしいので、
        # 色変更が必要な時だけ使うことにする。
        self.rt = RenderTarget.new(Window.width, Window.height) if self.rt == nil
        self.shader = SpriteShader.new if self.shader == nil
        self.rt.drawMorph(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7],
                          img, :dividex => 3, :dividey => 3)
        self.rt.update
        self.shader.set_color(c)
        Window.drawEx(0, 0, self.rt, :shader => self.shader)
      else
        # 色変換は不要
        a = (255 * c[3] / 256.0).to_i
        if a != 255
          # 半透明描画
          Window.drawMorph(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7],
                           img, :dividex => 3, :dividey => 3,
                           :alpha => a, :blend => :alpha)
        else
          # 不透明描画
          Window.drawMorph(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7],
                           img, :dividex => 3, :dividey => 3)
        end
      end
    end

    # フォントを描画
    #
    # @param [String] text 描画文字列
    # @param [Object] font Fontオブジェクト
    # @param [Array] c RGBA配列
    #
    def draw_font(text, font, c)
      x = self.mat.trans_x / PlayXmlMovie::TWIP
      y = self.mat.trans_y / PlayXmlMovie::TWIP
      Window.drawFontEx(x, y, text, font,
                        :color => [c[0], c[1], c[2]],
                        :alpha => c[3], :blend => :alpha, :aa => true)
    end

    # 再生描画処理
    #
    # @param [Hash] objs キャラobj情報が入ったハッシュ
    # @return [Boolean] trueなら再生中, falseなら再生終了
    #
    def play
      return true
    end
  end

  # ----------------------------------------
  # 画像描画obj
  #
  class ImageObj < MySprite

    # コンストラクタ
    #
    # @param [Hash] objs キャラobj情報が入ったハッシュ
    # @param [Number] cid 発生させたいキャラobjのID
    # @param [Object] pobj 親スプライト。nilなら親は居ない
    # @param [Number] depth 描画深度
    # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ
    #
    def initialize(objs, cid, pobj, depth, v)
      super(objs, cid, pobj, depth, v)
    end

    # 再生描画処理
    #
    # @return [Boolean] trueなら再生中, falseなら再生終了
    #
    def play
      draw_image(self.cobj.image, TMatrix.new)
      return true
    end
  end

  # ----------------------------------------
  # シェイプ描画obj
  #
  class ShapeObj < MySprite

    # コンストラクタ
    #
    # @param [Hash] objs キャラobj情報が入ったハッシュ
    # @param [Number] cid 発生させたいキャラobjのID
    # @param [Object] pobj 親スプライト。nilなら親は居ない
    # @param [Number] depth 描画深度
    # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ
    #
    def initialize(objs, cid, pobj, depth, v)
      super(objs, cid, pobj, depth, v)
    end

    # 再生描画処理
    #
    # @return [Boolean] trueなら再生中, falseなら再生終了
    #
    def play
      self.cobj.imgs.each do |v|
        draw_image(v[0].image, v[1])
      end
      return true
    end
  end

  # ----------------------------------------
  # スプライト描画obj
  #
  class SpriteObj < MySprite
    attr_accessor :node
    attr_accessor :now_frame
    attr_accessor :enable_loop

    # コンストラクタ
    #
    # @param [Hash] objs キャラobj情報が入ったハッシュ
    # @param [Number] cid 発生させたいキャラobjのID
    # @param [Object] cobj 発生させたいキャラobj
    # @param [Object] pobj 親スプライト。nilなら親は居ない
    # @param [Number] depth 描画深度
    # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ
    # @param [Boolean] looped trueならループ再生
    #
    def initialize(objs, cid, pobj, depth, v, looped = true)
      super(objs, cid, pobj, depth, v)
      self.node = cobj.start_node
      self.now_frame = 0
      self.enable_loop = looped
    end

    # 再生描画処理
    #
    # @return [Boolean] trueなら再生中, falseなら再生終了
    #
    def play
      pobj = self
      endfg = false

      until endfg

        case self.node.name
        when "PlaceObject2"
          # シェイプやスプライトを配置、もしくは移動
          nn = self.node
          rn = nn["replace"].to_i
          depth = nn["depth"].to_i
          id = nn["objectID"]
          id = id.to_i if id != nil

          # 属性を取り出す
          v = {}
          nn.children.each do |n|
            n.children.each do |m|
              if m.name == "Transform"
                # 変形情報
                m.keys.each do |k|
                  a = m.attribute(k)
                  v[a.name] = a.value.to_f
                end
              elsif m.name == "ColorTransform2"
                # 色変換情報
                v = TMatrix.init_color(v)
                m.keys.each do |k|
                  a = m.attribute(k)
                  v[a.name] = a.value.to_f
                end
              end
            end
          end

          if id != nil
            abort "Error: Not found ChrObj[#{id}]" unless self.objs.has_key?(id)

            case self.objs[id]
            when PlayXmlMovie::CharaObjImage
              self.sprs[depth] = ImageObj.new(self.objs, id, pobj, depth, v)
            when PlayXmlMovie::CharaObjShape
              self.sprs[depth] = ShapeObj.new(self.objs, id, pobj, depth, v)
            when PlayXmlMovie::CharaObjSprite
              self.sprs[depth] = SpriteObj.new(self.objs, id, pobj, depth, v)
            when PlayXmlMovie::CharaObjText
              self.sprs[depth] = TextObj.new(self.objs, id, pobj, depth, v)
            end
          elsif rn == 1
            self.sprs[depth].update(depth, v) if self.sprs.has_key?(depth)
          end

          if TMDBG
            s = "PLACE, replace=#{rn}, depth=#{depth}, id=#{id}, {"
            v.each_key {|k| s += "#{k} => #{v[k]}, "}
            s += "}"
            puts s
          end

        when "RemoveObject2"
          # シェイプやスプライトを削除
          depth = node["depth"].to_i
          if self.sprs.has_key?(depth)
            self.sprs[depth].dispose
            self.sprs.delete(depth)
          end
          puts "REMOVE, depth=#{depth}" if TMDBG

        when "ShowFrame"
          # 現在フレームを描画
          self.now_frame += 1
          puts "ShowFrame #{now_frame}" if TMDBG
          endfg = true

        when "End"
          # タグが無くなった
          endfg = true
          puts "End" if TMDBG
          break

        else
          # puts node_name
        end

        # 次のノードを読む
        self.node = self.node.next
      end

      # ウインドウに描画
      key_list = self.sprs.keys.sort # depthでソート
      key_list.each {|k| self.sprs[k].play }

      return true if self.node.name != "End"

      # 再生終了
      sprs = Hash.new
      if self.enable_loop
        self.node = self.cobj.start_node
        self.now_frame = 0
      end
      return false
    end
  end

  # ----------------------------------------
  # テキスト描画obj
  #
  class TextObj < MySprite

    # コンストラクタ
    #
    # @param [Hash] objs キャラobj情報が入ったハッシュ
    # @param [Number] cid 発生させたいキャラobjのID
    # @param [Object] pobj 親スプライト。nilなら親は居ない
    # @param [Number] depth 描画深度
    # @param [Hash] v MATRIX情報+半透明度を格納したハッシュ
    #
    def initialize(objs, cid, pobj, depth, v)
      super(objs, cid, pobj, depth, v)
    end

    # 再生描画処理
    #
    # @return [Boolean] trueなら再生中, falseなら再生終了
    #
    def play
      o = self.cobj
      draw_font(o.text, o.font, o.col)
      return true
    end
  end

  # ----------------------------------------
  # コンストラクタ
  #
  # @param [String] infile xmlファイルのパス
  #
  def initialize(infile)
    @infile = infile
    abort "Error : Not found #{infile}" unless File.exist?(infile)

    if BMDBG
      Benchmark.bm(15) do |x|
        x.report("xml parse:") {
          @doc = Nokogiri::XML(File.open(infile)) {|config| config.noblanks}
        }

        x.report("get fps") {
          @framerate, @frames = get_framerate(@doc)
          @wdw_w, @wdw_h = get_widnow_size(@doc)
          @bgcol = get_bgcolor(@doc)
        }

        @objs = Hash.new

        x.report("load image:") { load_image(@doc, infile) }
        x.report("load font:") { load_font(@doc) }
        x.report("load shape:") { load_shape(@doc) }

        x.report("load sprite:") {
          load_sprite(@doc)
          n = @doc.at("//swf/Header/tags/PlaceObject2")
          @objs[0] = CharaObjSprite.new(0, n)
        }

      end
    else
      # xmlを解析
      @doc = Nokogiri::XML(File.open(infile)) {|config| config.noblanks}
      @framerate, @frames = get_framerate(@doc)
      @wdw_w, @wdw_h = get_widnow_size(@doc)
      @bgcol = get_bgcolor(@doc)

      # 画像、シェイプ、スプライトを登録
      @objs = Hash.new
      load_image(@doc, infile)
      load_font(@doc)
      load_shape(@doc)
      load_sprite(@doc)

      # rootスプライトを登録
      puts "\nid[0] DefineSprite" if DBG
      n = @doc.at("//swf/Header/tags/PlaceObject2")
      @objs[0] = CharaObjSprite.new(0, n)
    end

  end

  #
  # フレームレート、フレーム数を取得
  #
  # @param [Object] doc xmlノード情報
  # @return [Array] フレームレート, フレーム数を配列で返す
  #
  def get_framerate(doc)
    n = doc.at("//Header")
    fpsv = n["framerate"].to_i
    frms = n["frames"].to_i
    puts "framerate=#{fpsv} , frames=#{frms}" if DBG
    return fpsv, frms
  end

  #
  # 画面サイズを取得
  #
  # @param [Object] doc xmlノード情報
  # @return [Array] 画面横幅, 画面縦幅を配列で返す
  #
  def get_widnow_size(doc)
    n = doc.at("//Header/size/Rectangle")
    # x = n["left"].to_i / TWIP
    # y = n["top"].to_i / TWIP
    w = n["right"].to_i / TWIP
    h = n["bottom"].to_i / TWIP
    puts "Window size : #{w}, #{h}" if DBG
    return w, h
  end

  #
  # 背景色を取得
  #
  # @param [Object] doc xmlノード情報
  # @return [Array] 背景色red, green, blueを配列で返す
  #
  def get_bgcolor(doc)
    n = doc.at("//SetBackgroundColor/color/Color")
    c = []
    ["red", "green", "blue"].each {|k| c.push(n[k].to_i)}
    return c
  end

  #
  # 画像をロード
  #
  # @param [Object] doc xmlノード情報
  # @param [String] infile xmlファイルパス
  #
  def load_image(doc, infile)
    imglist = get_img_file_list(infile) # 画像ファイルパスの一覧を取得

    # 検索するタグ名を列挙
    taglst = "//DefineBitsJPEG2|//DefineBitsLossless2|//DefineBitsLossless"

    doc.xpath(taglst).each do |n|
      id = n["objectID"].to_i
      n.content = ""

      unless imglist.has_key?(id)
        # IDと一致する画像ファイルパスが存在しない
        puts "Not found image file : id = #{id}"
        next
      end

      # IDと一致する画像ファイルパスが存在する
      fn = imglist[id]
      n["src"] = fn
      @objs[id] = CharaObjImage.new(id, fn)
      puts "id[#{id}] Load Image : #{fn}" if DBG
    end
  end

  #
  # xmlファイルパスを元にして、全画像ファイルのパス一覧を取得
  #
  # @param [String] xmlファイルパス
  # @return [Hash] 画像ファイルパスを格納したハッシュ
  #
  def get_img_file_list(infile)
    bsn = File.basename(infile, ".xml") # ファイルの拡張子を取り除いた文字列を取得
    dir = File::dirname(infile) # ファイルのフォルダパスを取得
    imglist = {}

    # サブフォルダ以下も探索して、拡張子が画像ファイル形式なら記録していく
    Find.find(dir) do |f|
      next unless FileTest.file?(f)
      imglist[$1.to_i] = f if /#{bsn}\[(\d+)\]\.(png|jpg|bmp|gif)/ =~ f
    end

    return imglist
  end

  #
  # フォント情報やテキスト情報を読み込み
  #
  # @param [Object] doc xmlノード情報
  #
  def load_font(doc)
    lst = Hash.new
    lst.merge!(
      {
        "_ゴシック" => "MS UI Gothic",
        "_明朝" => "MS P明朝",
        "_等幅" => "MS ゴシック",
        "_sans" => "Arial",
        "_serif" => "Times New Roman",
        "_typewriter" => "Courier"
      })

    # 使用している全フォント名を取得
    doc.xpath("//DefineFont2|//DefineFont3").each do |n|
      id = n["objectID"].to_i
      fnt = n["name"].encode("Windows-31J") # xmlはutf-8なのでsjisに変換
      if lst.has_key?(fnt)
        fnt = lst[fnt]
        @objs[id] = CharaObjFont.new(id, fnt)
      else
        @objs[id] = CharaObjFont.new(id, fnt)
      end
    end

    # 使用している静止テキスト情報を取得
    doc.xpath("//DefineEditText").each do |n|
      id = n["objectID"].to_i
      fid = n["fontRef"].to_i # フォント種類ID
      abort "Error: Not found font [#{fid}]" unless @objs.has_key?(fid)

      size = n["fontHeight"].to_f / PlayXmlMovie::TWIP

      # Flash CS5で作成したswfは、initialText に html が入っている。
      # htmlタグを除去して描画文字列を取得する
      nm = Nokogiri::HTML(n["initialText"])
      text = nm.content.encode("Windows-31J")

      col = [0, 0, 0, 255]
      c = n.at(".//color/Color")
      if c != nil
        col[0] = c["red"].to_i
        col[1] = c["green"].to_i
        col[2] = c["blue"].to_i
        col[3] = c["alpha"].to_i
      end
      fontname = @objs[fid].fontname
      @objs[id] = CharaObjText.new(id, text, size, fontname, col)
    end
  end

  #
  # 画像を内包するShapeを生成
  #
  # @param [Object] doc xmlノード情報
  #
  def load_shape(doc)
    doc.xpath("//DefineShape|//DefineShape2").each do |n|
      id = n["objectID"].to_i
      sn = n.xpath(".//bounds/Rectangle")[0]
      # x = sn["left"].to_i / TWIP
      # y = sn["top"].to_i / TWIP
      w = sn["right"].to_i / TWIP
      h = sn["bottom"].to_i / TWIP

      imgs = []
      bn = n.xpath(".//styles/StyleList/fillStyles")
      bn.xpath(".//ClippedBitmap|.//ClippedBitmap2").each do |m|
        img_id = m["objectID"].to_i
        if img_id == 65535
          # objectID が 65535 の時がある。正体不明
          puts "id[#{id}] Unknown Image [#{img_id}]" if DBG
          next
        end

        unless @objs.has_key?(img_id)
          # 画像が存在しない
          abort "Error : id[#{id}] Not found Image [#{img_id}] #{w} x #{h}"
        end

        # 画像が存在するので、シェイプに割り当てる
        # MATRXI情報も反映させる
        puts "id[#{id}] Shape = bitmap[#{img_id}]" if DBG

        dic = {}
        mn = m.at(".//matrix/Transform")
        mn.keys.each do |k|
          atr = mn.attribute(k)
          dic[atr.name] = atr.value.to_f
        end
        v = TMatrix.new
        v.set_value(dic)
        imgs.push([@objs[img_id], v])
      end
      @objs[id] = CharaObjShape.new(id, imgs)
    end
  end

  #
  # スプライトを登録
  #
  def load_sprite(doc)
    doc.xpath("//DefineSprite").each do |n|
      id = n["objectID"].to_i
      frames = n["frames"].to_i
      puts "\nid[#{id}] DefineSprite, frames=#{frames}" if DBG
      node = n.at(".//PlaceObject2")
      @objs[id] = CharaObjSprite.new(id, node)
    end
  end

  # 再生開始のための初期化処理
  #
  # @note fpsや背景色が変更されるので、再生が終わったら本来のfps・背景色に戻すこと
  #
  # @param [Boolean] init_size ウインドウサイズをxmlに従って変更するかどうか
  # @param [Boolean] init_fps フレームレートをxmlに従って変更するかどうか
  # @param [Boolean] init_bgcol 背景色をxmlに従って変更するかどうか
  #
  def init(init_size = false, init_fps = true, init_bgcol = true)
    @now_frame = 0
    @root_sprite = SpriteObj.new(@objs, 0, nil, 0, {})

    Window.resize(@wdw_w, @wdw_h) if init_size
    Window.fps = @framerate if init_fps
    Window.bgcolor = [@bgcol[0], @bgcol[1], @bgcol[2]] if init_bgcol
  end

  # 再生描画処理
  #
  # @note メインループから毎フレーム呼ぶこと
  #
  # @return [Boolean] trueなら再生中, falseなら再生終了
  #
  def play
    return @root_sprite.play
  end

  #
  # 多少整理したxmlを標準出力にダンプする
  #
  def dump
    puts @doc.to_xml
  end

end

if $0 == __FILE__
  # ----------------------------------------
  # 使い方の例

  m = PlayXmlMovie.new("swfdata/movie0.xml")
  # m = PlayXmlMovie.new("swfdata/movie1.xml")
  # m = PlayXmlMovie.new("swfdata/movie2.xml")
  # m = PlayXmlMovie.new("swfdata/movie3.xml")
  # m = PlayXmlMovie.new("swfdata/movie4.xml")

  # 再生開始のための初期化
  # fpsや背景色が変更されるので、
  # 再生が終わったら本来のfps・背景色に戻すこと
  m.init

  # メインループ
  Window.loop do
    break if Input.keyPush?(K_ESCAPE) # ESCキーで終了

    # 1フレーム分描画
    unless m.play
      m.init # 再生が終了したら、再度頭から再生
    end

  end
end

使い方の例。
require_relative "playxmlmovie"

m = PlayXmlMovie.new("hoge.xml")
m.init

Window.loop do
    break unless m.play
end

その他詳細は、 _readme.html を読んでください。

備考。 :

サンプルswf内の静止画像作成については、 _マウスコンピューター G-Tuneシリーズ公式キャラクター G-Tuneちゃん (Tuneちゃん?) を使わせてもらいました。…と書いておけばいいのかな? リンクも貼ってあるから、これで利用条件は満たしてるはず。たぶん。

ここまで書いといてアレだけど。 :

自分でコレを使うかというと、たぶん使わないような気がする…。たしかにグリグリ動いたけど…。

ただ、コレを書いてみたことで、巷で見かける swf → HTML5 + JavaScript 変換ツールが、大体どんな感じのことをしているのか、そのあたりぼんやり分かってきたように思います。結構勉強になったというか。

Ruby でSWFバイナリを直接扱える、 _SwfRuby なるライブラリも使えたら、swfmill で一旦xmlに変換しなくてもいいのかもしれんですけど。
  • SwfRuby は RMagick が必要。
  • しかし、Windows で RMagick を導入するのは面倒。ImageMagick の特定バージョンを要求される。
という問題があるので…。そう考えると、xml に変換しといてソレを渡す、こういうやり方もアリかなと。xmlをエディタで開けば、どこで何の指定がされてるか調べられるというメリットもありますし。

2017/03/19追記。 :

DropboxのPublicフォルダが死んだのでファイルを置き直し。

#2 [flash][neta] Flashについて(余計な)一言言っとくか

「今頃Flashかよwww」と言い出す人が居そうな気もするので、一応、今の自分の考えをメモ。

Flashにも色々あって。 :

Flashにも色々あって。
  • 最終的な再生ファイル形式としての Flash (swf)
  • 中間ファイルとしての Flash (swf)
  • アニメ制作アプリとしての Flash
  • デスクトップアプリを作成できる基幹技術としての Flash (Adobe AIR)
  • その他色々。
複数の定義があり得るので、分けて考えないといかんよなと。

Webブラウザ上で再生するための Flash (swf) は、たしかに死滅寸前だと自分も思っているのですが。
  • 皆が持ってるスマホのブラウザ上で再生できないってのは、やっぱり痛い。
  • アニメを再生したいなら、そもそも動画ファイルを再生しちゃえばいい。
だから、そういう定義の Flash なら、たしかに「今頃」だよなと。

しかし、「簡易アニメ制作ソフト」としての Flash や、別の最終フォーマットに変換するための「中間ファイル」としての Flash (swf)は、今でも使えるわけで。

例えばの話。psdファイルはブラウザで開けないけど。だからと言って「Photoshopは死んだ」と口走ってたら馬鹿丸出し。同様に、制作環境としての Flash は、今後も(しばらくは)選択肢としてフツーにアリで。Webブラウザで開けないからソレは死んだ、てな結論は、あまりに視野が狭過ぎる。

それに、昔は Adobe製の有償製品しか無かったですけど、今はフリーで使えるFlash制作ソフトも存在してますから、お金の無い学生さんにも優しいし。これがもし、HTML5 + JavaScript用の何かを吐き出してくれる簡易アニメ制作ソフトだったら、Flash と比べたら、まだちょっと弱いよなと。

とは言え、ならばFlashがアニメ制作ツールとして至高の存在かと問われたら、自分は首を捻ってしまうのですけど。どうしてあんなに使いづらいのですかね…。たぶん、絵描きさんの視点で作ってないよな…。古い版しか触ったことないけど、最新版は改良されてるのだろうか…。

何が作れるか云々。 :

「Flashでろくなアニメなんか作れるわけねえだろwww」とdisる人も居るかもしれないなと。そういう人は、URA氏がディレクターをした「猫物語」OPや「ニセコイ」の2番目のOPを見るべき。 *1 有名アニメーターのりょーちも氏の仕事ぶりも確認すべき。

「Flashでろくなアニメなんか」発言は、「Photoshopを買えばボクもイラストレーターになれる」発言と大差ないよなと。どのツールを使うかで作品の中身が劇的に変わるようなら誰も苦労しねえんだよクソが! と思うわけですよ。ツールの選択で変わってくるのは、作業効率ぐらいなもんでして。

これがもし、「Flashでアニメ作るの結構シンドイよ」という発言だったら、自分もちょっと同意するかも。なんだかサクサクと作業できないというか、トライ&エラーが面倒臭いUI仕様があちこちに点在していて…。

JavaScriptも死にかけてましたな。 :

JavaScript の歴史を思い返せば、「今頃Flashかよ」発言って危ないよなと。一時期、JavaScript って死滅寸前でしたよ。まさかこういう使い方もできたとは。当時 JavaScript をdisってた人達って、一体何なんだろうと…。自分もその中に含まれてますけど。

なので、Flash も、「え? そんな場所で、そんな使い方が?」てな展開があるかもしれないなと。そもそも、最初はアニメ制作ソフトだったはずが、どうして ActionScript が云々、てな話に…。既に現状ですら、俺の知ってる Flash と違う…。

それにしても、HTML5 + JavaScript で書いた何かしらが、ブラウザの互換性の問題で大変なことになってる話を見かけるたびに、「Flash が使えたら、こんなことにはならなかったのに…」と思えてしまって。Adobeだけが互換性問題で苦労して、こっちは楽ができたのになあ。ジョブズを筆頭に、無意味に Flash をdisってた人達のせいで、より多くのプログラマー・デザイナーが地獄を見る展開に。

*1: 今もFlash使ってるか分からんし、もしかするとAEなのかなと思わないでもないけど、動かし方やシルエットで見せていくソレはどう見てもFlashアニメ文化だよなと。

この記事へのツッコミ

はじめまして by hoshi    2014/06/04 13:54
hoshi(またはhoshi-sano)と申します。
「DXRubyで動画が再生できたら云々」などとこっそり発言していた一人です。
いつも日記の更新を楽しみにさせていただいています。

Flashは一時期結構使っていただけに、衰退しつつあるのは私も残念に思います。
はやりの「艦これ」がFlashで動作したりとまだ完全に死に向かっているわけではなさそうですが、
だからといって第一線に戻れそうな空気でもないですよね。

DXRubyでのswf再生の一連の日記、まだ細部は読めていませんがすごくワクワクしました。
特に何か意見があるというわけでないのですが、この一連の日記ですごい喜んだ奴がいるということをお伝えしたかった次第でした。

#3 [cg_tools] Google Web Designer、Adobe Edge Animate を試用

前々からどういうツールか気になってたので試用してみようかなと。ちなみに環境は、Windows7 x64。

結論から先に書く。 :

個人的に、この手のツールは以下の2点が気になっていて。
  • 連番画像をレンダリングできるか。これができれば、動画編集ソフト等に渡す素材や、スプライトシートが作れる。
  • 出力したファイルを、Webブラウザ以外でも再生することができそうか。
要するに、他の何かと連携できるかどうかが気になるわけで。

連番画像を出力する機能はどちらも持ってなかった。しかしこれは、ツールの性質からして仕方ないのかも。

これらのツールは、Flashと違って、タイムラインには時間しか表示されてなくて。つまり、「フレーム」「コマ」という概念が無い。フレームが無いのだから、フレームを連番画像として出力できるはずもなく。 *1 なので、現状では本質的に、既存のアニメ制作ツールと異なる存在かもしれないなと。「アニメ制作ツール」として紹介されることが多いけど、コレは、アニメ映像ではないアニメを作るツールではないかと。

また、出力ファイルは html だったり js だったりするのだけど…。出力ファイルは難読化がされていたりして、さっぱり読めないのだけど。作業ファイル自体は難読化がされてないので、まだなんとか頑張れば解析できそうな気もしたり。

Google Web Designerを試用した際のメモ。 :

_Google Web Designer から、googlewebdesigner_win.exe をDL。897KBのファイルなので、たぶんダウンローダーなのだと思う。実行したら、おそらくは本体のダウンロードとインストールが行われた。

インストール直後の現在のバージョンは、1.0.6.0428。

アニメ作成作業は簡単。
  • 下のサムネイル横の「+」ボタンを押していけばキーフレームを増やせる。
  • そのキーフレームでオブジェクトの位置変更等をすれば、その間の位置移動等を補間してくれる。
  • サムネイルとサムネイルの間のボタンを押せば、何秒間、どんな動きをさせるのかグラフ表示してくれる。
  • 秒数の変更もできるし、グラフ部分をマウスでクリックすれば動きの種類も順々に変わっていく。
最初はクイックモード?とやらで作業してたけど、下の方に小さく「詳細」と書かれたボタンをクリックしたら、クイックモードが解除されてタイムライン表示がされるようになった。各オブジェクトがレイヤーのような状態でタイムラインに表示されてるので、このほうが分かりやすいような気もする。
  • タイムライン上で右クリックすれば、キーフレームの挿入、動きの種類の変更などができる。
  • タイムライン上でキーフレームのマークをドラッグすれば、キーフレーム位置を変更できる。

ファイル → 公開を選べば、htmlファイルとして保存できる。何を出力してくれるのか、そこがそもそも分かってなかったのだけど。どうやらhtmlファイルを1つ出力するツールだった模様。

出力されたhtmlを眺めてゲロを吐きそうに。チンプンカンプン。

作業用に保存される html ファイルをぼんやり眺めていたら、なんだかちょっと分かってきた。
  • @-webkit-keyframes なる記述がキーフレームの情報だと予想。
  • キーフレームに、12.7451% とか 49.0196% とか書いてあるのは、全体時間に対してどこにキーフレームがあるのかを割合で示しているのではないか。
  • html本体部分の canvas てのがシェイプに相当するらしい。シェイプの形状はcssで定義。
よくよく見てみると、「公開」で出力されたhtmlファイルも、css部分に、アニメをするために必要な情報が列挙されているように思えてきた。css内がデータみたいなものなのかな…。

解析してどうにかしようという気にはならないけれど、やろうと思えばどうにかできるのかもしれない。バイナリファイルではなくテキストファイルで保存されてると、こういう時にありがたい。今時、バイナリファイルで出力することは極力避けるべきなのだと再認識しました。

連番画像の出力機能は無し。

現状、広告制作ツールのベータ版として公開されているのだし、CGツールとして捉えるほうが間違ってる気もしてきたり。見た目それっぽいけど、ヨチヨチ歩きみたいなものだよな…。どこかのまとめサイトで、「GoogleがAdobeとWebデザイナー殺しに来た」なんて煽りタイトル記事を見かけたけど。仮にそうだとしても、幼児が水鉄砲でピュピュッと撃ってる段階のような…。今後の成長に期待、みたいな。

Adobe Edge Animate を試用した際のメモ。 :

最新版の Adobe Edge Animate CC はおそらく有償ソフトらしいのだけど。Adobe Edge Animate 1.5 までは、Adobe ID を持ってる人なら無料で使えるらしいので、試しにDLしてインストールしてみたり。ちなみに、そのうちDLできなくなるという話も見かけました。

インストール時に、「PCの再起動が要求されてる」云々のエラーメッセージが表示されて。OS再起動しないとダメなのかなと再起動してから試したけれど、やはり同じメッセージが。とりあえず「無視」ボタンを押したら、インストールが先に進んだ。何だったんだろう…。まあ、以前 Flash CS5 をインストールした時も、謎のエラーが出てなかなか解決できなかったし。 *2 Adobe製だから、どうせこんなもんだろう…。

Google Web Designer に比べると、さすがに随分作り込んである印象。操作方法は Flash のノリに近いので、戸惑う部分はほとんど無かった。

こちらも、連番画像の出力機能は無し。

パブリッシュされたファイルを確認したら、1つの html と、複数のjsになっていた。jsファイルは難読化されていて何が何だか。

作業用ファイルは難読化がされてなかったので、なんとか理解できそう。hoge_edge.js を開くと、timelines なる記述が見えたので、たぶんこのファイルにアニメさせるための情報が入っているのではないかなと。もしかして、これって json だったりするのかな?

今思い出したけど、もしかして Adobe は、この手のファイルフォーマットの解析作業を禁止してたりしないかな…。禁止してるのだとしたら、そういう面から、他のソフトとの連携は無理ってことになりそうな。

*1: 別途フレームレートを指定して書き出すことは可能だろうけど。それは、数式によってオブジェクトが連続で動いてる光景を、フレームレートに従ってサンプリングすることになるので、アニメ映像じゃなくて実写映像に近いはず。
*2: Flash CS5のインストール時にエラーが出る問題は、Flash Player の古いセットアッププログラムがテンポラリフォルダに残ってると発生、したようなおぼろげな記憶が。

#4 [anime] ライダー鎧武を視聴

今日も何話か消化。

ドリアンライダーの人が生身で変なポーズを取りながらジャンプして出現したり、弟子の人が気をつけポーズで放物線を描いていく図の突飛さに笑いました。どうしてこんな光景を思いついたのか…。素晴らしい。子供さんの反応が気になるところ。

森について解説する回で、それぞれ別の場所に居るはずのキャラクター達が、謎についての説明を繋げていくあたりに感心。こういう見せ方は、自分、なんだか好きでして。たしかコードギアスでもやっていて当時感心した記憶が。こういうことができるあたりが ―― 場所も時間も跳躍して一つに繋げることができてしまうのが映像作品の持つ面白さ、のような気がしたり。…まあ、映像作品において、あらゆるシーンは、異なるカットの繋がりで構成されているわけで。それを脚本レベルでもやってみた事例の一つ、かもしれないのですが。

以上、1 日分です。

過去ログ表示

Prev - 2014/05 - 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 31

カテゴリで表示

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


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

Powered by hns-2.19.6, HyperNikkiSystem Project