Home > Flash > Flash : バーチャルアイドルになる!(その1) - FreeTrack + GlovePIE + Papervision3D + TTS

Flash : バーチャルアイドルになる!(その1) - FreeTrack + GlovePIE + Papervision3D + TTS

  • Posted at: 2008年7月16日 03:13
  • Update: 2008年9月29日 04:50

080629_hatsune1.gif

Flashの勉強会。大阪てら子16「アイドル!アイドル!」で発表した、「IRヘッドトラッキング+Papervision3D+TTSでバーチャルアイドルの中の人になってみよう。」の内容をまとめてみたよ。(いまごろ?とかいうな)。えーだいぶ長い。。

今回のテーマはアイドル。ってことだったんで、ストレートにPV3Dでバーチャルアイドルでも作ってみる→でもPV3Dでアニメーションとか面倒そう。。→あーヘッドトラッキングとかやってみたいんだよ→生の人間の動きをトレースさせたら、中の人がいるっぽく見えるんじゃね?楽できるんじゃね?→ついでにOSCパケットをリアルタイムで扱うテストもしよう。うん。という感じで始めました。

実装優先。ライブラリとアプリで実現できるなら連携。できた構成がこつら。

080709_hatsune2.gif

ヘッドトラッキング部は、アクターの頭に装着したマーカーをWEBカメラで入力→WEBカムを使うヘッドトラッキングのフリーウェア「FreeTrack」で解析(6軸のデータが出力)→「GlovePIE」で受けて、OSC(UDP)で送信→鯖で受けてTCPで送信→Flashで受けて、3Dオブジェクトの動きに反映→動くよ。

080714_hatsune3.gif

TTS部はFlash側からテキストを入力→鯖で受けて、Yahoo!日本語形態素解析APIに投げる→返ってきたデータから、よみがなだけを抽出→ひらがな1文字とローマ字読みを紐付け→「FreeTTS」で読ませて再生→しゃべるよ。こんな感じ。

IRヘッドトラッキング

モーションキャプチャした頭の動きに合わせて、画像内の物体等をマッチムーブさせます。3Dの場合、顔の向きや位置に合わせてカメラを動かしたり、オブジェクトを回転、移動させます。顔をそちらに向けると画像内の3D空間を覗き見るように、主観視点での表示をさせることができます。フライトシュミレーター、レースゲームやシューティングゲーム等、主観視点のゲームで多く使われます。頭の動きに対応させたカメラの移動や、HMDと組み合わせることで、プレイヤーがゲーム世界にいるように体現できるつーもんです。TrackIRのように市販されているものあります。こちらは頭に赤外線反射板の付いたガジェットを付け、センサーでキャプチャする方式です。公式サイトでは反射板が付いた帽子や、ヘッドセットに装着できるタイプもあります。

最近だと、カーネギーメロン大学の学生Johnny Chung Lee氏が、Wiiリモコンを使ったヘッドトラッキングを実現させたステキなアレですよ。

今回はWEBカメラとポインタに下のようなガジェットを使います。ポインタには3個のLED。底辺の2点はカメラ側、上の1点は少し手前に配置して、三角形を形作るようにします。詳しくは「FreeTrack」公式ページのユーザーの例を参考にするといいです。ガジェットは顔の側面に付けたりもできます。認識できる角度が限られるんで、正面での配置で進めます。

080718_to-fu1.jpg

この3つのポインタを付けたガジェットを頭に装着して、固定されたWEBカメラで撮影します。正面から見たとき、LEDの発光点3点を結ぶ3角形を3次元空間に立つ平面と仮定します。キャプチャした映像は2次元ですので、頭を傾けると点の位置が移動し、その3角形も平面を傾けたように歪んで見えます。この平面の傾きを計算で割り出せると、2次元上の3点の座標だけで、3次元空間の頭の座標、傾きが求めることができます。マーカーが3つで角度と位置を合わせた x, y, z, ロール(Roll), ピッチ(Pitch), ヨー(Yaw)の6軸の動き(6DOF)が計算できるよーです。

FreeTrackでトラッキング

トラッキングには、WEBカメラと赤外線LEDを使うオープソースのFreeTrackを使います。

・FreeTrackの設定

080710_freetrack1.gif

3PointsCapを選択して、ガジェットの大きさに合わせて長さを入力します。

080710_freetrack2.gif

カメラを選択してStartでキャプチャ、トラッキングが開始されます。光点だけ表示されるよう適度にカメラの設定を調整します。

GlovePIEからOSCで送る

080710_glovepie1.gif

次に、ゲーム用のデバイス、Wiiリモコン、TrackIR、モーションキャプチャ、MIDI機器などなど、いろんな入力機器をコントロール→入出力ができる「GlovePIE」で、FreeTrackから出力されるデータを、次のJava側が受け取れるように、OSCプロトコルにして出力しなおします。FreeTrackのデータはDirectX経由(?)で、GlovePIEのTrackIRプロパティを利用して取得できるようです。

OSC(Open Sound Control)は、UDP/IP通信でネットワークを介し、複数のコンピューター、アプリケーション同士で音響合成のためのパラメータを双方向にやり取りする、データ通信プロトコルです。元はMIDIのような音用のプロトコルなんだけど、対応ソフトウェア、環境の多さや、URLに似たデータ構造の汎用性の高さから、こーゆーデータのやりとりだけにも使われるよーです。(のわりには解説のページとか少ないような。。)

GlovePIEは↓な感じにスクリプトでいろいろ制御します。OSCにも対応しているのでクライアントになって、入力機器から取得したデータを他のアプリにデータ通信させます。

// 取得する間隔を設定
pie.FrameRate = 20 Hz

// OSCクライアントの設定
OSC.ip = "127.0.0.1"
OSC.port = 21588
OSC.broadcast = false

// FreeTrackのデータをOSCパケットで送信
OSC.data = TrackIR.Yaw + "" + TrackIR.Roll + "" + TrackIR.Pitch + "" + TrackIR.X + "" + TrackIR.Y + "" + TrackIR.Z

UDP→TCP。通信用サーバーを用意する

ほい3つ目w。例のごとくFlashはデータを直接受けとれないんで、Socket通信で送り出してあげるゲートウェイをこしらえます。なんでもいいんだけどやっぱりJavaで。データはUDPからOSCパケットが送信されるので、OSCを扱うJavaのライブラリ「NetUtil」を使います。FlashはUDPをサポートしてませんのでUDP→TCPも実装。

UDPでOSCパケットを受けるスレッドと、TCPでFlashに送信するスレッドを作って、スレッド間でデータをシンクロ。という感じにしてみた。もひとつスマートじゃないね。。まー動くからいいか。

Papervision3Dって便利。はちゅね召還。動かす。

080711_hatsune4.gif

出力ねー。FlashでPapervision3Dを使って、3Dのキャラクターを表示させます。モデリングはさっぱりわからんので、PV3Dの解説をされている「note.x」のrectさん作、はちゅねミクのColladaファイル(モデル作者はズサさん)を拝借しました。このモデルをBlenderで、頭部と身体を分解。別々にして、Colladaファイルで保存にしときます。

んでColladaファイルのオブジェクトをPapervision3Dに読み込みまーす。

package 
{
  /** 
   * MikuPv3dObject..as
   *
   * はちゅねみくの外部ファイルを読み出して配置したり動かす。
   *
   */
   
  import flash.display.*;
  import flash.events.*;
  
  import org.papervision3d.objects.parsers.Collada;
  import org.papervision3d.view.BasicView;

  public class MikuPv3dObject extends BasicView
  {
    
    private var _rotationX:Number;
    private var _rotationY:Number;
    private var _rotationZ:Number;
    private var _x:Number;
    private var _y:Number;
    private var _z:Number;
    
    private var _cmodel_head:Collada;
    private var _cmodel_body:Collada;
    
    public function MikuPv3dObject()
    {
      //viewportの定義とカメラタイプ定義
      super (0,0,true,false,"CAMERA3D");
      init();
      init3D();
    }
    
    public function init():void 
    {
      this._rotationX = 0;
      this._rotationY = 0;
      this._rotationZ = 0;
      this._x = 0;
      this._y = 0;
      this._z = 0;  
    }
    
    public function init3D():void
    {
      //カメラ設定
      camera.z = -300;
      camera.focus = 570;
      camera.zoom = 2;
      
      // はちゅね召還
      this._cmodel_head = new Collada("negimiku_head.dae");
      this._cmodel_body = new Collada("negimiku_body.dae");
      this._cmodel_head.scale = 0.15;
      this._cmodel_body.scale = 0.15;
      this.scene.addChild(_cmodel_head);
      this.scene.addChild(_cmodel_body);

      
      //レンダリング開始
      startRendering();
    }
    
    // ヘッドトラッキング情報を設定
    public function setTracking(vYaw:Number, vRoll:Number, vPitch:Number, vx:Number, vy:Number, vz:Number):void 
    {
   
      if (vYaw > 70) { this._rotationY = 70; }
      else if (vYaw < -70) { this._rotationY = -70; }
      else { this._rotationY = vYaw; }
      
      if (vPitch > 35) { this._rotationX = 35; }
      else if (vPitch < -60) { this._rotationX = -60; }
      else { this._rotationX = -(vPitch); }
      
      if (vRoll > 20) { this._rotationZ = 20; }
      else if (vRoll < -20) { this._rotationZ = -20 }
      else { this._rotationZ = -(vRoll); }
      
      this._x = -(330 * vx);
      this._y = 250 * vy;
      this._z = -(200 * vz);
    }
    
    // オブジェクトの位置を更新
    override protected function onRenderTick(event:Event=null):void
    {
      this._cmodel_head.rotationX = _rotationX;
      this._cmodel_head.rotationY = _rotationY;
      this._cmodel_head.rotationZ = _rotationZ;
      this._cmodel_body.rotationX = _rotationX * 0.2;
      this._cmodel_body.rotationY = _rotationY * 0.35;
      this._cmodel_body.rotationZ = _rotationZ * 0.35;
      this._cmodel_head.x = this._x * 0.04;
      this._cmodel_head.y = this._y * 0.04;
      this._cmodel_head.z = this._z * 0.5;
      this._cmodel_body.x = this._x * 0.05;
      this._cmodel_body.y = this._y * 0.05;
      this._cmodel_body.z = this._z * 0.5;
      
      super.onRenderTick(event);
    }
  }
}

身体の上に頭部がくるように配置するわけですが、そのまま動かすとおかしな位置で頭が回ります。PV3Dはオブジェクトの大きさの中心が移動や回転の基準点となります(このモデルだと髪の真ん中あたり)。首のあたりで回転してほしいので、そこが中心になるように、ダミーのオブジェトで包むなどして中心点をずらしておかないとダメなようです。

あとはJava鯖からソケット通信で送られるデータを受けて、頭のオブジェトクトを動かします。PitchはrotationX、YawはrotationY、RollはrotationZ、x, y, zは座標の移動量に割り当てます。どーせなら身体も動かします。IKとか考えもせずあきらめますw。頭に合わせて少し身体が回ったり傾けたりして、追随して動いてるようにごまかしますw。あとは頭の可動範囲とか自然な動きになるよう地味ぃに微調整しましょう。

でーきたー。アクターの動きに合わせて、はちゅねが動くよーになりました。

てきすとつーすぴーちでしゃべらせる

もひとつパっとしなかったんで、しゃべる(?)機能を足してみます。

クライアントからの入力に、何かアクションを返す、話す。とかめんどくさそうなんで。テキストを入力したら読み上げる。くらいにしときます。

Flash上のテキストボックスでテキストを入力させ、先のゲートウェイ鯖のTCP側のスレッドで受けます。読み上げには、Java Speech APIを利用して音声合成ができる「FreeTTS」を使ってみます。FreeTTSは日本語に対応していないのですが、ひらがなを1文字ずつ切り、英語で近い発音のものに変換してAPIに投げると、一音ずつ読み上げてくれます。音声の再生は、とりあえずローカルで鳴らせてます。

package text2talk01;

import java.util.*;

import com.sun.speech.freetts.*;

public class TTS02 extends Thread {

  private static List<TTS02> threads = new ArrayList<TTS02>();
  private String message;
  private String talkDialog;
  
  public TTS02(String word) 
  {
    super();
    message = word;
    threads.add(this);
  }
  
  // ここから処理
  public void run() 
  {
    try {
      List<BeanWord> textAnalysis = MorphologicalAnalysis.execute(message);
      System.err.println(message);
      for(int i = 0; i < textAnalysis.size(); i++) {
        System.err.print(textAnalysis.get(i).getReading());
        talkDialog = talkDialog + textAnalysis.get(i).getReading();
      }
      System.err.print("\n");
    } catch (Exception e) {
      threads.remove(this);
      return;
    }
    
    System.err.println(talkDialog);

    String dst = translate(talkDialog);
    System.err.println(dst);

    VoiceManager vm = VoiceManager.getInstance();
    Voice v = vm.getVoice("kevin16");
    v.setVolume(1.0f); // 0 to 1.0
    v.allocate();
    v.speak(dst);
    v.deallocate();
    System.err.println("読み上げ終了");
    threads.remove(this);
  }

  public String translate(String s){

    StringBuffer dst = new StringBuffer();
    for(int i=0; i<s.length(); i++){
      char c = s.charAt(i);
      if(c < 128){
        // 英語扱い
        dst.append(c);
      }else{
        // 日本語扱い
        String d = m.get("" + c);
        if(d != null){
          dst.append(d);
        }
        dst.append(" ");
      }
    }
    return dst.toString();
  }

  private static final Map<String, String> m;

  static {
    m = new HashMap<String, String>();
    m.put("ょ", "yo");
    m.put("あ","ah");
    m.put("い","e");
    m.put("う","wool");
    m.put("え","eay");
    m.put("お","oh");
    m.put("か","car");
    m.put("き","key");
    m.put("く","ku");
    m.put("け","k");
    m.put("こ","koh");
    m.put("さ","sar");
    m.put("し","c");
    m.put("す","sue");
    m.put("せ","say");
    m.put("そ","so");
    m.put("た","tar");
    m.put("ち","tick");
    m.put("つ","two");
    m.put("て","tea");
    m.put("と","toe");
    m.put("な","na");
    m.put("に","need");
    m.put("ぬ","nue");
    m.put("ね","ney");
    m.put("の","no");
    m.put("は","ha");
    m.put("ひ","he");
    m.put("ふ","foo");
    m.put("へ","hey");
    m.put("ほ","ho");
    m.put("ま","ma");
    m.put("み","me");
    m.put("む","muh");
    m.put("め","may");
    m.put("も","mo");
    m.put("や","yah");
    m.put("ゆ","you");
    m.put("よ","yo");
    m.put("ら","lar");
    m.put("り","lee");
    m.put("る","lu");
    m.put("れ","ray");
    m.put("ろ","low");
    m.put("わ","were");
    m.put("ゐ","e");
    m.put("ゑ","eay");
    m.put("を","war");
    m.put("ん","unn");
    m.put("が","ga");
    m.put("ぎ","gee");
    m.put("ぐ","goo");
    m.put("げ","gay");
    m.put("ご","go");
    m.put("ざ","za");
    m.put("じ","zee");
    m.put("ず","zu");
    m.put("ぜ","zey");
    m.put("ぞ","zo");
    m.put("だ","da");
    m.put("ぢ","zi");
    m.put("づ","zu");
    m.put("で","dead");
    m.put("ど","doh");
    m.put("ば","ba");
    m.put("び","be");
    m.put("ぶ","boo");
    m.put("べ","bay");
    m.put("ぼ","bo");
    m.put("ぱ","pa");
    m.put("ぴ","pee");
    m.put("ぷ","pooh");
    m.put("ぺ","pay");
    m.put("ぽ","po");
  }  
}

Yahoo!日本語形態素解析APIで、漢字まじりの文章にも対応

これだと、ひらがなの文章しか読めません。漢字交じりの普通の文にも対応させましょう。漢字に対応させるには、日本語の語彙を理解させてあげないと、音読み訓読み、送り仮名や助詞など区別がつきません。形態素解析すると、自然言語の文章を、意味の持つ最小単位の列に分割し、それぞれの品詞を判別することができます。日本語の形態素解析エンジンはいくつかアプリが提供されているんですが、オンラインで簡単に利用できる「Yahoo!日本語形態素解析API」を使ってみます。

Yahoo!日本語形態素解析APIは、Yahoo!デベロッパーネットワークで開発者向けに提供されているWebサービスで、アプリケーションIDを登録するだけで無料で利用できます。ただし、利用制限があり、24時間以内で1つのIPアドレスにつき50000件のリクエスト。1リクエスト100KB以内に制限されています。指定されたURLにクエリーとして文章を送ると、文節ごとに解析されたXML形式のデータが返ってきます。その中から形態素の読みがなのノードを抜き出し、順番に繋げるとひらがなだけの文章ができあがります。これを先のと同じように、読み上げてあげればいいわけです。

package text2talk01;

import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;

public class MorphologicalAnalysis {

  static final String MAURL = "http://api.jlp.yahoo.co.jp/"
    + "MAService/V1/parse?appid=xxxxxx&results=ma"
    + "&sentence=";

  @SuppressWarnings("unchecked")
  public static List<BeanWord> execute(final String sentence) throws Exception 
  {

    final Document doc = new SAXBuilder().build(new URL(MAURL + URLEncoder.encode(sentence, "UTF-8")));
    final Element root = doc.getRootElement();
    final Namespace ns = root.getNamespace();
    final List<Element> children = root.getChild("ma_result", ns).getChild("word_list", ns).getChildren("word", ns);
    final List<BeanWord> result = new ArrayList<BeanWord>();

    for (Element child : children) {
      final BeanWord word = new BeanWord();

      word.setSurface(child.getChild("surface", ns).getTextTrim());
      word.setReading(child.getChild("reading", ns).getTextTrim());
      word.setPos(child.getChild("pos", ns).getTextTrim());

      if (child.getChild("baseform", ns) != null) {
        word.setBaseform(child.getChild("baseform", ns).getTextTrim());
      }
      result.add(word);
    }
    return result;
  }
}

動かしてみる。

あー動いた動いた(真夜中の変なテンションでの撮影は危険。水平反転させてたの忘れてるしw)。アイドルのくせに声が野太いのがあれですね。。FreeTTSは標準の声以外にも、「Mbrola」のライブラリを利用できます。ええ。インストールの仕方がわからんので断念したんですが。。ローカル環境で使うんだし、標準で使えるテキスト読み上げ機能とか使ってもよかったのかも。

今後の妄想とか雑感

ううう。長いクセにまとまってない。。
カンタンにできたらいいなーと、もりもり作ってたらFlash以外が増えすぎた。反省。Flash増量を目指す。AR、トラッキングとか調べるほどおもしろい。あと3次元変換は、理解が曖昧なんで改めてやります。はい。。

以下やりたいこと。

・ネギ振り実装。PV3Dでアニメーションてどうやるのん?
・ヘッドトラッキングはFlashだけでもできるはず。
・コントロールとオーディエンス側を分けたい。双方向でも可。
・IRなのに赤外線じゃない罠w。
・TTSは無くてもいい。使うなら声は変えよう。
・音声はFMS使うとかクライアント側で鳴らしたい。
・Flash10で音響合成は。。。。

そんな感じ。エントリーはパっと書ききらないとねー。ばた。

Comments:0

Comment Form

Home > Flash > Flash : バーチャルアイドルになる!(その1) - FreeTrack + GlovePIE + Papervision3D + TTS

Auther
hoehoe3 : おおさか方面でWebとかやってますよ。
What am I doing...
    Search
    Feeds

    Return to page top