Inside of Stopmotion Robotics

2016/9/8 に行われたAutodesk University Japan で展示する為にMayaからロボットアームをコントロールしてコマ撮りアニメを作る Stopmotion Roboticsという仕組みを作りました。

こちらが展示しながらできた作品

制作風景のタイムラプス

Stopmotion Roboticsの狙いはロボットと人間がコラボレーションして映像作品を作る仕組みを作り、人間とロボットの可能性を提示する事、ロボットアームをMayaからコントロールする事により、実空間をMayaでオーサリングできるようにする事です。

システムの概要

Maya2016をWindows PCで走らせて、今回の為に開発をしたC++で書いたPluginを使っています。

ロボットアームはKUKA iiwa lbrをKUKA Roboticsさんからお借りしています。ロボットアームはWindowsベースのカスタムのコントローラーで制御されていて、そこではJavaで書かれたアプリが動いています。

Mac Book ProはCanon EOS5Dをコントロールする為、DragonFrameというストップモーションアニメーション制作に特化したアプリを使う為に使っています。

これらのマシンは全て有線LANでルーターに接続してOSCでメッセージのやり取りをしています。

Mayaではキューブのアニメーションとカメラのアニメーションを持っています。今回映像を作る部分のディレクションは岡崎智弘さんにお願いして、岡崎さんからキューブの動きのイメージが送られてきたので、それをPlane上にテクスチャーとして貼り付けて、トレースするようにモーションパスを作成しました。

キューブはぞれぞれの角で減速して直線で加速するように、モーションパスのUの値をエディットしました。

カメラのアニメーションではキーフレームアニメーションを使っています。カメラワークは日下部実さんに協力していただきました。

カメラの動きはキューブの動きのループに合わせて、カメラの位置と注視点がループするようになっています。

Maya上でカメラのエイムを設定してあり、注視点をアニメーションさせることにより、自動的にカメラの角度をMayaで計算してくれます。それをロボットアームに送ることにより、ロボットアームがカメラを動かします。

CGではよく使われるテクニックですが、人間が実世界でカメラを構えてやろうとすると、毎フレーム同じ動きをぴったりさせるのは至難の技です。

MayaのViewportでカメラを切り替えると、実際に撮影するカメラと同じ画角でレイアウトをチェックする事が出来、しかもアニメーションを再生して確認する事が出来ます。

MAYAの中ではCGの世界なのでカメラもキューブも制限なく動かす事が可能なのですが、ロボットアームの可動範囲があるので、実際にアニメーション作った後にロボットアームを動かしてみて、可動範囲に収まっているかのチェックが必要です。今回もカメラの動きはロボットアームに合わせて調整をかけました。

MayaのカスタムPluginについて

MayaのPluginでやっていることは、Open Sound Control(OSC)の送受信、フレームを進める、キューブとカメラの位置の取得、全体のシーケンスのコントロールです。

MPxThreadedDeviceNodeについて

MayaでOSCの通信を受け付けるにはMPxThreadedDeviceNodeを使います。MPxThreadedDeviceNodeを使うと独自のスレッドをMaya内で使う事が出来、Mayaがブロックされる事なくメッセージの受付待ちが可能です。

MPxThreadedDeviceNodeに関してはこちらのリファレンスが参考になります。

また、KinectのデータをMayaで表示させるこちらのサンプルのコードを読んで最初の実装イメージを固めました。

MPxThreadedDeviceNodeではVisual StudioでC++を使っているので、OSCをC++でVisual Studioで使えるものはないかと探してみるとこちらを発見しました。(中村さんありがとうございます!)
http://www.naturalsoftware.jp/entry/2012/11/13/091119

MPxThreadedDeviceNodeのスレッドハンドラーの構成に合うようにOSCのメッセージの初期化部分、メッセージの受けの部分、OSCの終了処理の部分を別々の関数をカスタムで作り、スレッドハンドラーは以下のようになりました。

void threadDeviceHand::threadHandler()
{
OSCPacketListenerHand listener;
MStatus status;
setDone(false);

UdpListeningReceiveSocket s(
IpEndpointName(IpEndpointName::ANY_ADDRESS, HAND_PORT),
&listener);

s.RunInit();

while (!isDone())
{
if (!isLive())
continue;

beginThreadLoop();
{

MGlobal::executeCommand("print(\"-- threadHandler called --\");");

s.WaitForPacket();

Sleep(100);
}
endThreadLoop();
}
setDone(true);

s.RunEnd();
}

メッセージを受けて処理をする部分は、OscPacketListenerを継承したクラスを作り、OSCのアドレスごとに処理をしています。

#pragma once
#include <iostream>

#include "osc/OscReceivedElements.h"
#include "osc/OscPacketListener.h"
#include "ip/UdpSocket.h"

typedef enum {
SMR_STATUS_PREPARING,
SMR_STATUS_WAITING_OK,
SMR_STATUS_DONE,
} SMR_STATUS;


class OSCPacketListenerHand : public osc::OscPacketListener {
protected:
double cubeT[3];
double cubeR[3];
double cameraT[3];
double cameraR[3];
char *transNames[3] = { "tx", "ty", "tz" };
char *rotNames[3] = { "rx", "ry", "rz" };
int frame = 0;
SMR_STATUS armStatusCamera;
SMR_STATUS armStatusHand;
SMR_STATUS shutterStatus;
bool waitingForCamera;
void getTransRotByMelCommand(char *nodeName, double(&trans)[3], double(&rot)[3]);
void createMelCommandStepKeyFrame(char(&command)[512]);
void sendNextCubePosition();
void sendNextCameraPosition();
void sendShutter();
void sendReady();
virtual void ProcessMessage(const osc::ReceivedMessage& m,const IpEndpointName& remoteEndpoint);
};

こちらがメッセージを受けている部分です。

void OSCPacketListenerHand::ProcessMessage(const osc::ReceivedMessage& m,
const IpEndpointName& remoteEndpoint)
{

try {
char buf[512];
sprintf_s(buf, 512, "print(\"-- address %s --\\n\");", m.AddressPattern());
MGlobal::executeCommand(buf);

if (strcmp(m.AddressPattern(), "/next") == 0) {
createMelCommandStepKeyFrame(buf);
MGlobal::executeCommandOnIdle(buf);
getTransRotByMelCommand("pCube1", cubeT, cubeR);
getTransRotByMelCommand("camera1", cameraT, cameraR);
sendNextCubePosition();
sendNextCameraPosition();

}else if (strcmp(m.AddressPattern(), "/move_cube_ok") == 0) {
armStatusHand = SMR_STATUS_DONE;
if (armStatusHand == SMR_STATUS_DONE && armStatusCamera == SMR_STATUS_DONE ) {
// send /shutter
sendShutter();
}
}else if (strcmp(m.AddressPattern(), "/move_camera_ok") == 0) {
armStatusCamera = SMR_STATUS_DONE;
if (armStatusHand == SMR_STATUS_DONE && armStatusCamera == SMR_STATUS_DONE ) {
// send /shutter
sendShutter();
}
}else if (strcmp(m.AddressPattern(), "/shutter_ok") == 0) {
shutterStatus = SMR_STATUS_DONE;
// send /ready
sendReady();
}

}
catch (osc::Exception& e) {
// any parsing errors such as unexpected argument types, or
// missing arguments get thrown as exceptions.
std::cout << "error while parsing message: "
<< m.AddressPattern() << ": " << e.what() << "\n";
}
}

OSCのメッセージの送信は普通にoscpackのライブラリを使うだけです。

void OSCPacketListenerHand::sendNextCubePosition() {
UdpTransmitSocket transmitSocket(IpEndpointName(HAND_OUT_ADDRESS, HAND_OUT_PORT));

char buffer[HAND_OUTPUT_BUFFER_SIZE];
osc::OutboundPacketStream p(buffer, HAND_OUTPUT_BUFFER_SIZE);

p << osc::BeginBundleImmediate
<< osc::BeginMessage("/move_cube")
<< (float)cubeT[0] * 10.0f << (float)cubeT[2] * -10.0f << (float)cubeT[1] * 10.0f << (float)cubeR[1] << (float)cubeR[0] << (float)cubeR[2] << osc::EndMessage
<< osc::EndBundle;

transmitSocket.Send(p.Data(), p.Size());
armStatusHand = SMR_STATUS_WAITING_OK;
}

C++のPluginとMEL

Mayaの内部をコントロールするにはC++のPluginからMELのコマンドの文字列を生成してMGlobal::executeCommand()を使いC++からMELを呼びました。

Mayaの内部をMPxThreadedDeviceNodeから書き換えるにはMGlobal::executeCommandOnIdle()を使います。MGlobal::executeCommand()を使うとMayaが落ちます。

MELを使ってノードのアトリビュートを取得できますが、MCommandResultを使うと、MELの実行結果をC++のPluginに持ってくることができます。

Stopmotion Roboticsではフレームを進ませて、CubeとCameraの位置情報を取得するのにMCommandResultを使っています。

void OSCPacketListenerHand::getTransRotByMelCommand(char *nodeName, double(&trans)[3], double(&rot)[3]) {
MCommandResult result;
char commandBuf[256];

for (int i = 0; i < 3; i++) {
sprintf_s(commandBuf, 256, "getAttr %s.%s", nodeName, transNames[i]);
if (MS::kSuccess == MGlobal::executeCommand(commandBuf, result, false, false))
{
if (MCommandResult::kDouble == result.resultType())
{
result.getResult(trans[i]);
}
}
}

for (int i = 0; i < 3; i++) {
sprintf_s(commandBuf, 256, "getAttr %s.%s", nodeName, rotNames[i]);
if (MS::kSuccess == MGlobal::executeCommand(commandBuf, result, false, false))
{
if (MCommandResult::kDouble == result.resultType())
{
result.getResult(rot[i]);
}
}
}
}

こちらがMayaでパスアニメーションさせた位置情報をロボットアームに送ってキューブを動かしている映像です。

ロボットアームのそれぞれのジョイントの角度をOSCでMayaに送り、それをMayaのノードのアトリビュートの角度を変えるとMayaの中でロボットアームの姿勢がモニターできます。

スタート用のMELコマンドについて

プラグインのロード、スレッドの起動の一連の作業を自動化するためにMELスクリプトを作っておきました。しかし一気に複数スレッドを一つのMELスクリプトの中で行うとうまく動かなかったので別々のMELスクリプトにして実行しています。

プラグインロードのMELスクリプト

loadPlugin "D:/work/Autodesk/VisualStudioProject/StopMotionRoboticsVS/x64/Release/StopMotionRoboticsVS.mll";

制御用スレッドスタートのMELスクリプト

createNode "threadDeviceHand";
setAttr "threadDeviceHand1.live" 1;

ハンド用ロボットアームの角度を受け取るスレッドスタートのMELスクリプト

createNode "threadDeviceReciveHandArmMovement";
setAttr "threadDeviceReciveHandArmMovement1.live" 1;

カメラ用ロボットアームの角度を受け取るスレッドスタートのMELスクリプト

createNode "threadDeviceReciveCameraArmMovement";
setAttr "threadDeviceReciveCameraArmMovement1.live" 1;

一眼レフカメラのコントロールについて

gphoto2について

今回は撮影にCanon EOS5Dを使用しました。EOS5DはPTPというプロトコルに対応していて、PCからシャッターを切る、シャッタースピードの調整など様々なコントロールが可能です。

ここを読むとgphoto2ができることが分かりやすいです。

gphoto2をMacにインストールするにはbrewを使いました。

今回使っているgphoto2のコマンドは以下です。

カメラを検出

gphoto2 — auto-detect

カメラのキャプチャーを開始

gphoto2 — set-config capture=on

シャッターを切って画像をMacに転送

gphoto2 — capture-image-and-download — filename test0001.jpg — force-overwrite

Macの場合はカメラをUSB接続すると自動的に写真のアプリが起動します、これがPTPを使っているのでPTPを使っているプロセスをkillします。

これでPTPを使っているプロセスのIDを見つけます。

ps axuwww | grep “Image” | grep -v grep

gphoto2でOSCのメッセージを使ってカメラのシャッターを切る

Stopmotion RoboticsではMayaがフローをコントロールしています。

MayaのPluginが動いているWindows PCからOSCのメッセージをMacに送ってgphoto2をコントロールできるようにopenFrameworksベースのアプリを作りました。gphoto2をインストールするとlibgphoto2というライブラリもインストトールされて、openFrameworksのプログラムに直接リンクできて使えるのですが、うまくいかなかったので今回はC++のsystem関数を使ってgphoto2を呼んでいます。

こちらがOSCのメッセージを受けてgphoto2を呼んでいる部分です。

/shutterというOSCのアドレスが来たらシャッターを切ります。

void ofApp::update(){
// check for waiting messages
while(receiver.hasWaitingMessages()){
// get the next message
ofxOscMessage m;
receiver.getNextMessage(m);

// check for mouse moved message
if(m.getAddress() == “/shutter”){
gPhoto2CaptureAndDownload();
printf( “capture done! “);

ofxOscMessage m;
m.setAddress(“/shutter_ok”);
sender.sendMessage(m, false);
}
}
}

こちらがgphoto2を呼んでいる関数です。

void ofApp::gPhoto2CaptureAndDownload(){
char cmdLine[1024];
sprintf( cmdLine, “/usr/local/bin/gphoto2 — capture-image-and-download — filename %s_%04d.jpg — force-overwrite”, outpath, photoCunter);
system(cmdLine);
photoCunter ++ ;
}

DragonFrameについて

その場でコマ撮りアニメを作っていく上において出来上がっていくアニメーションのシーケンスを表示させるのにDragon Frameというコマ撮りアニメ撮影用のアプリを使いました。

gphoto2で画像をダウンロードするフォルダをDragonFrameのフォルダウォッチのターゲットのフォルダに指定します。

指定方法はメニュー->撮影->撮影ソース->フォルダウォッチを選択します。

右上のアイコンからカメラ設定に切り替えてカメラ設定からフォルダをクリックするとフォルダが選択できるので、gphoto2がダウンロードしに行くフォルダを選択します。

この設定をすると、カメラで一枚撮影すると自動的にDragonFrameのシーケンスの最後に追加され今作っている映像をループで回して確認しながらアニメーションを作っていくことができます。

Show your support

Clapping shows how much you appreciated naoji.taniguchi’s story.