mrubyがやってきた!

OpenCASCADE の勉強のために、仕事とは関係ないところで、自分なりの簡易三次元 CAD ビューアを作成しています。
形状をパラメトリックに定義したり、対話的に制御したり、一連の処理をモジュール化する上で必要になってくるのがマクロ環境。マクロと言ってしまうと、使い勝手の悪いバッチ処理的なイメージがそこはかとなくありますが、強力なインタプリタ環境を組み込みたいと思いました。

アプリケーションを組む上でもトライアンドエラーのデバッグ効率は上がりますし、CAD ソフトにおける純粋な三次元幾何演算をハード的な部分であるとすれば、「ある決まった工業製品の組み立てモデルをシミュレーションする」といったより論理的な、ソフト的な役割のライブラリ化にも役立つと思っています。

さて、OpenCASCADE にはこのブログでも何回か書いたとおり、DRAW TEST HARNESS というコマンド・ドリヴンな対話型デバッグシェルが含まれています。単機能評価だけで言うと、Tk による最低限の UI とシェルっていう組み合わせは別に捨てたもんじゃないのですが、ユーザに提供するマクロ環境として Tcl/tk を選択するのはいささか時代遅れな感じは否めません。シェルスクリプトや Perl、AWK に馴染みのある人ならば、あんまり気にせずに使い込なせると思うんですが、本質とは違うところでの労力を強いるのは極力避けたいところでもあります。

対話的にも動作することができ、モジュール化しても軽快に動いてくれるインタプリタ環境を探していました。
最低限、

  • 基本的な計算(四則演算、数学関数の呼び出し)
  • 変数の保持と解釈
  • 条件式、繰り返し構文

を備えているものだったら何でも良いと思っていました。ほぼ全ての言語にあるようなものばかりですね。もちろん、書き易さやメンテナンスのやり易さも条件のひとつです。

さて、最初に思いついたのは Python です。Python のコーディングは、私自身、累計でも1万行も書いていないと思いますが、欧米製の大手 CAD ソフトでは Python をマクロ環境として組み込むものが増えてきているようです。フランスのパリに Open CASADE の勉強会に行った時に開発メンバーが見せてくれたデモでも、OpenCASCADE の C++ コードを Python から呼んでいたようです。また、2011年から OpenCASCADE のコミュニティ・エディションをメンテナンスしている Thomas Paviot 氏も PythonOCC という Python ラッパーのプロジェクトを立ち上げています。

大手 CAD や OpenCASCADE との連結でも実績のある Python ですが、組み込み関連の情報を漁っていると、 mruby というものを見つけました。

mruby とは、Ruby の開発者 Matz 氏が最近精力的に開発を進めている組み込み向けの Ruby 実装らしいです。本家 CRuby に比べて依存関係が少なく、小さな端末でも動きやすいよう設計されているようで、Vim 関連の記事でよく参考にさせていただいている mattn 氏頻繁にコミットされている事がブログからも伺えます。

私はガチガチの関数手続き型の頭と、シェルスクリプトのようなゆるゆるのシェル展開ありきの頭の2ビットでコーディングをしていたので、Ruby のような「ちょうど良い」言語には向いていないんだろうなーと食わず嫌いをしていて手をつけなかったような気がします。いや、嫌っていたワケではないのですが、あれ楽しい!これ楽しい!と目移りしている間に、いつの間にか、ちゃんとやってみる機会を逃したというのが正しいかもしれません。少なくとも、ちゃんとやりたい言語の一つであった事は間違いありません。

# 余談ですが、Lisp もちゃんと時間をとってやりたいです。Emacs の設定ファイルのために elisp をごちょごちょ書いたような記憶しかありせん。
# オライリーの「初めての Ruby(オーム社刊、Yasugi 著、2008年)」で紹介されている Ruby の系図に、Ruby が Lisp の考え方も引き継いでいるような図があったので楽しみ。

これは好機とばかりに、mruby を試してみることにしました。

Hello, mruby!

mruby を make するのは簡単でした。他所でも紹介されているように、git で clone して make するだけです。Ruby の環境をまともに触るのも初めてでしたので、初めて知ることがたくさんありました。

まず、Makefile は存在しているのですが、Makefile の中で呼ばれているのは ruby minirake というコマンド。make から minirake が呼ばれて、最終的に gcc がコンパイルする形になってます。コンパイルの動作を処理している minirake は全て Ruby で実装してあり、Ruby 初心者が最初に見た時は「?」マークの連続でした。(でも、それほど時間をかけずに読んでいけました。Ruby すごい!)

頭に mini と付いているのは、きっと mruby のための小さな仕組みであり、本家 CRuby では Rake という Ruby のための Make 環境があるんだろうな、とも理解できました。

Ruby には gem というモジュール管理機構があるようです。Perl で言う CPAN のようなものだと思いますが、rake ファイルにターゲットを追記しておくだけで新しい gem を作ることができました。本家 Perl と軽量目的の mruby を比べるとアレかもしれませんが、分かりやすさ・透明性と簡単さは驚きです。(これなら自分もできるかも!と思わせてくれるのにはテンションが上がります)

さてはて、自分のアプリケーションに組み込む場合は Windows 環境で動かさなければならないので、Windows 環境、とりわけ Visual Studio でビルドできるかというのは大きな問題でした。そして、結果から言うと杞憂でした。
Windows では TortoiseGit と MSYSGIT を使っていますので、Windows に Ruby を入れて PATH を通したあと、MSYS 環境に clone して make するだけでした。途中、bison がないぞって怒られたので、bison.exe を入れたらすんなりと mruby.exe(インタプリタ本体)、mirb.exe(mruby.exeに対話シェル機能を入れたもの)、mrbc.exe(Rubyコードをバイトコードにコンパイルするやつ) が出来てしまいました。これらバイナリは libmruby.a をスタティックリンクしているようで、スタンドアロンで動作可能っていう嬉しさ。さすが組み込み向けです。

コーディングをほとんどしていないのに、こんな側面から mruby がどんどん好きになってしまいます。

Visual Studio で

自分のアプリケーションを Windows 以外のプラットフォームでも提供するんだったら、Qt なんかでチャキっと組んだ方がいいと思うんですが、私は Qt を使ったことがありません。Qt はすごくやりたいのですが、それはまた別の機会として、当面の目標である Windows でそこそこ動くものを作らねばなりません。

そういう理由で、ユーザ・フロントエンドを C#、OpenCASCADE や mruby のラッパー DLL を C++/CLI で実装しよう(※)と思いました。ラッパー DLL も C++/CLI 依存の部分とネイティブ C++ 準拠の部分を分けてコーディングする事により、最悪、別のプラットフォームになった場合、ネイティブ C++ の部分だけ抜き出してビルドし直せばいいと思いました。MFC を使った COM 実装に比べれば、C++/CLI の制約なんて目をつぶる事ができそうですし、そんな高度な事もする予定がないので、当面はこれで問題がないはずです。

※Microsoft の公式推薦の C++/CLI 入門書籍でも、「C++マネージド拡張の失敗という前例もあるし、特に理由がなければ C++/CLI なんて使うな」と前書きに書いてありましたね。あれは声出して笑いました。

さて、さっそく MSYS環境で make した mruby.dll を Visual Studio で作成した C++/CLI の wmruby.exe (デバッグの為、とりあえずEXEを作成)からリンクしてみて実行しました。コンパイル、リンクとも正常に完了しているものの、実行時には 0xc0000007b というエラーが出て、実行できません。調べてみると、どうもバイナリイメージが CIL とネイティブの間で上手く取りつげていないようです。

ここは mruby ではなく、MSYS と、Windows の目紛しく仕組みが変わるバイナリの取り扱いの不一致だろうって事で、あまり深く調べずに考え方を変えました。

mruby.dll を make するために rake ファイルを読み漁ってた時に vs2010 や vs2012 といった、いかにも Visual Studio で mruby をビルドするオプションっぽいのを発見していました。

openssl とか、gem が他のオープンソースライブラリを使う時、Visual Studio ではなく MSYS 環境のほうが色々と苦労が少なそうだなーと思っていたので、あえて MSYS で libmruby.a を make していましたが、そこが問題になりそうだったら選択肢がありません。

vs2010 を使って、ビルドし直してみると、Visual Studio によって libmruby.a ではなく libmruby.lib が生成されました。加えて、MSYS でビルドしていた自前の mruby.dll も Visual Studio で C++/CLI として libmruby.lib にリンクして再ビルドすると、拍子抜けするほど上手く行きました。

libmruby.lib さえビルドできてしまったら、あとはこっちのもんですね!

IIJ 版

これまで触っていた本家の mruby では、require やシェルコマンド実行のクオートなど、Kernel でサポートしているものもほとんどありませんでした。開発途上という側面と、最低限のものを提供するというスタンスがあると思われます。

本家 mruby のほか、IIJ さんも精力的に mruby の開発に手を出しているようで、本家から folk したリポジトリになっています。mattn 氏のブログを拝読していたら、先程述べた require やクオート、正規表現など「モバイル端末などの局所的な組み込みには要らなかもしれない!でもPCで実行する対話的アプリのモジュールなら要るよね!」的な機能を IIJ 版にどんどん追加されていっている様子が伺えました。記事にもありましたが、CRuby にも遜色がないほどの基本機能を提供できるようになっているようです。本当に凄いし、夢が広がります!

という事で早速、IIJ 版を clone して make してみました。linux では何ごともなくすんなり。
./mirb して

[ruby]
ls.each_line do |item|
puts item
end
[/ruby]

すると、ちゃんと実行できました。楽しい。

さて、Windows 環境ではどうかと言うと、ソケットを扱ってそうな gem があったり、コードを見てみると Windows 向けのコーディングがされていなかったりと、半ば予想はしていましたが、Visual Studio でビルドしようとしても失敗しました。当たり前だよネ。

Visual Studio っていう巨大なイレギュラー環境が立ちふさがって、作業がストップしてしまいましたが、丹念に乗り越えなければならない道だろうなぁと思います。今はビルド方法をまとめるくらいしか能力がないですが、Ruby をガリガリ使えるようになって、実装の事も理解できるようになったら issue 飛ばしたり、コーディング面でも協力できるようになれればなあ、と思いました。

OpenCASCADE の Draw Test Harness を活用する その3

前回までの記事では、OpenCASCADE の Draw Test Harness を利用し、独自コマンドを組み込むところまでを紹介しました。今回は、実例を用いながらコーディングしていきたいと思います。

目的

任意の半径を持つ球体を 1/8 にした形状を作成し、その容積を印字したいと思います。

想定しているコマンドの書式は次のとおり。

[bash]
sphere1p8 result_name radius_of_sphere
[/bash]

アプローチとしては、まず原点(0, 0, 0)に半径 r の sphere を作り、同じ位置に大きさ(r, r, r) の box を作成し、二つをコモン演算します。その結果に対して容積を取得しようと思います。

ソースコード

まず、前回のサンプルコードをベースに、プリミティブ、ソリッド、プロパティを扱うためのヘッダを追加してあげます。次に、hello() 関数を sphere1p8() 関数に書き換え、コマンド定義も変更してやります。

[cpp]

include <iostream>

include "/opt/occ660/ros/config.h"

include <Draw.hxx>

include <Draw_Interpretor.hxx>

include <DBRep.hxx>

/* コマンドで利用するヘッダ */

include <TopoDS_Solid.hxx>

include <BRepPrimAPI_MakeBox.hxx>

include <BRepPrimAPI_MakeSphere.hxx>

include <BRepAlgoAPI_Common.hxx>

include <BRepGProp.hxx>

include <GProp_GProps.hxx>

include <Standard_PrimitiveTypes.hxx>

/* 1/8球を作成し、ボリュームを印字する */
static int sphere1p8(Draw_Interpretor& di, Standard_Integer argc, const char** argv)
{
if (argc != 3)
return 1;

// 半径
double r = atof(argv[2]);

// 球と箱を作る
BRepPrimAPI_MakeSphere sphere(gp_Pnt(0, 0, 0), r);
BRepPrimAPI_MakeBox box(gp_Pnt(0, 0, 0), r, r, r);

// ブール演算
BRepAlgoAPI_Common bo(box.Solid(), sphere.Solid());
bo.SetOperation(BOPAlgo_COMMON); // 6.6.0 から BOP_* が BOPAlgo_* になってる
bo.Build();

if (bo.ErrorStatus())
    return bo.ErrorStatus();

// 結果を取り出して、登録
TopoDS_Shape shape = bo.Shape();
DBRep::Set(argv[1], shape);

// 容積を印字
GProp_GProps gpr;
BRepGProp::VolumeProperties(shape, gpr);
std::cout &lt;&lt; std::endl &lt;&lt; &quot;Volume: &quot; &lt;&lt; gpr.Mass() &lt;&lt; std::endl;

return 0;

}

void Draw_InitAppli(Draw_Interpretor& di)
{
Draw::Commands(di);
di.Add("sphere1p8", "sphere1p8 result r", FILE, sphere1p8);
}

include <Draw_Main.hxx>

DRAW_MAIN
[/cpp]

sphere1p8() 内の処理は、コメントを参考にすれば説明は不要だと思います。1点だけ、補足をしておきますと、40行目の DBRep::Set() 関数で DBRep という Draw の幾何オブジェクト管理構造にオブジェクトを登録しています。この DBRep に登録する事により、AXO ビューアにオブジェクトが表示されることになります。また、Draw インタプリタ上からオブジェクトの名前をキーにして扱うことも出来るようになります。

Draw 内だけで使われるもので、有用な関数を次に示します。

  • Draw_Interpretor::Eval … 関数内から Draw コマンドを実行する
    例)

    di.Eval("pload ALL; axo");
  • Draw_Interpretor::EvalFile … 関数内からファイルパスを指定し、Tcl スクリプトファイルを実行する
    例)

    di.EvalFile("script.tcl");
  • DBRep::Set … オブジェクト名をキーにして、オブジェクトを管理構造に追加する
    例)

    DBRep::Set(objname, myObject);
  • DBRep::Get … オブジェクト名をキーにして、オブジェクトを管理構造から取得する
    例)

    TopoDS_Shape myObject = DBRep::Get(objname);

上記の例のように Eval() の文字列はコマンド単位ではなく、一連のスクリプトの内容として食わせることが可能です。(コマンドはセミコロン区切りで1行に連結できます。)
また、DBRep はオブジェクトを静的に管理しているようなので、static 呼び出しで使います。 コマンドの引数として指定されたオブジェクト名で、オブジェクトを取得・設定する際に利用します。
これだけ知っていれば充分ですが、さらに詳しい情報は、Draw_InterpretorDBRep のリファレンスを参考にしてください。

ビルドと実行

前回のサンプルファイルの時から、利用しているライブラリファイルが増えましたので、ビルドコマンドライン、とりわけ Makefile も書き換える必要があります。

[cpp]
g++ -I/usr/local/inc -lTKernel -lTKDraw -lTKMath -lTKBRep -lTKTopAlgo -lTKPrim -lTKBO -lTKG2d drawtest.cpp
[/cpp]

沢山増えましたね。リンカエラーで「クラス○○の参照が見つかりません!」と怒られた場合は、クラスのリファレンスマニュアルを参照すると、ライブラリ名を特定することができます。例えば、Draw_interpretor クラスの参照エラーとなった場合、リファレンスページの画面上部に「Open CASCADE Technology > Module Draw > Toolkit TKDraw > Package Draw」と表示されています。この Toolkit の部分がライブラリ名になるので、この場合ですと「TKDraw」というライブラリをリンクしてあげればいい事になります。
OpenCASCADE ライブラリは、ほとんどに接頭辞 TK が付いていますが、Toolkit の略です。Tcl/Tk の Tk とおんなじですね。

ビルドが成功したら、実行バイナリを起動して、pload ALL、axo の順にタイプします。その後、追加したコマンド sphere1p8 obj 10.2、 fit コマンドを実行すると、次のようになりました。

dth_volume_sample容積は、約 555 らしいです。ちゃんと動いている模様。

あとは、コンパイラに -g して GDB などに投げて、ガリガリとトライ&エラーが楽しめます。

OpenCASCADE の Draw Test Harness を活用する その2

前回の記事で、OpenCASCADE に同梱されているコマンド駆動型の効率的なデバッグ環境 Draw Test Harness の使い方を紹介しました。今回は、いよいよデバッグ&プレビュー環境として活躍させるための、独自コマンドを追加する方法を書きたいと思います。

Draw Test Harness に独自コマンドを追加する

Draw Test Harness の環境に自前のコマンド(コード)を追加する方法は簡単です。どちらかと言うと、自分のデバッグしたいコードを取り出して、Tcl ないし Draw インタプリタを組み込むイメージです。何はともあれ、まずはコードを見てみましょう。

[cpp]
// File: drawtest.cpp

include <iostream>

// このプログラムを Draw インタプリタプログラムにするために必要なヘッダ

include "/opt/occ660/ros/config.h"

include <Draw.hxx>

include <Draw_Interpretor.hxx>

include <DBRep.hxx>

static int hello(Draw_Interpretor& di, // 1. コマンド実装部
Standard_Integer argc, const char** argv) //
{ //
std::cout << "hello, draw!" << std::endl; //
return 0; //
} //

void Draw_InitAppli(Draw_Interpretor& di) // 2. コマンド定義部
{ //
Draw::Commands(di); //
di.Add("hello", "print hello string for test", //
FILE, hello, "Test command"); //
} //

include <Draw_Main.hxx> // 3. アプリケーション

DRAW_MAIN // 実行部

[/cpp]

こちらの手順で OpenCASCADE を導入した場合を前提にして書いていますが、5行目の CASROOT ディレクトリ(ros ディレクトリ)の直下にある config.h のパス文字列を、お使いの環境での config.h のパスに変更するだけで正常に動くと思います。

このプログラムは、1. コマンド実装部、2. コマンド定義部、3. アプリケーション実行部に分かれています。一つ一つはすごく簡単なので、ちゃっちゃか解説しちゃいましょう。

1. コマンド実装部

この関数がコマンドの実際の処理となります。上のサンプルでは、文字列を標準出力に印字する hello() 関数として実装しています。Draw インタプリタとコマンドの実行引数を取る静的関数であれば、名前は何でも構いません。複数のコマンドを実装したい場合は、コマンド毎に、コマンド用インタフェイスを持つ関数を追加してあげます。

[cpp]
/* コマンド用インタフェイス */
static int // 戻り値: コマンド正常終了時は0。
funcname (
Draw_Interpretor&, // Draw インタプリタのアドレス
Standard_Integer, // コマンドの引数数
const char** // コマンドの引数文字列
)
[/cpp]

2. コマンド定義部

Draw_InitAppli() はアプリケーションが初期化される際に一度だけ自動的に呼ばれる関数です。ここで、Draw::Commands() を呼び出した後、Draw_Interpretor::Add() 関数を使って、上記で追加した関数とコマンドの関連付けを Draw インタプリタに宣言します。

[cpp]
Draw_Interpretor::Add(
const Standard_CString Command, // コマンド名
const Standard_CString Help, // コマンドのヘルプ文字列
const Standard_CString Filename, // コマンドの関数がコーディングされているファイルパス
const Draw_CommandFunction Function, // コマンドの関数ポインタ
const Standard_CString Group=&quot;User Commands&quot; // コマンドのグループ文字列
)
[/cpp]

コマンド名と関数さえしっかり定義できていれば、あとは適当で構いません。ヘルプ文字列は、Draw インタプリタで help commandname を実行した際に、表示されるテキストを指定します。普通はコマンドの使い方を書いておくためのものです。

3. アプリケーション実行部

DRAW_MAIN 定数により、アプリケーションに必要な main() 関数や Draw インタプリタのその他の機能の呼び出しなど、面倒なコーディングはプリプロセッサが全部やってくれます。最後にこの2行を書いておけば、とりあえずOK。

ビルドと実行

ビルドは次のとおりです。TKDraw ライブラリを忘れずにリンクしましょう。

[bash]
g++ -lTKernel -I/usr/local/inc -lTKDraw drawtest.cpp
[/bash]

ここで生成された a.out を実行し、「hello」とタイプすると、hello() 内で定義したコードが実行されます。※ a.out を実行した際に「the CASROOT variable is mandatory to Run OpenCascade」というメッセージが表示されたら、環境変数 CASROOT が上手く定義されていません。

「hello, draw!」と印字されれば成功です。「exit」 とタイプして終了させましょう。

今回のサンプルでは、hello() 関数内で OpenCASCADE を用いた処理は一切行なっていなかった為、a.out の実行直後に hello コマンドを実行しても大丈夫でしたが、今後、OpenCASCADE を用いた処理を書くようになれば、必ず「pload ALL」を実行しなければなりません。

次回は、もう少し意味のあるデバッグコマンドを実装しながら、コマンド内でよく使うであろう機能を紹介したいと思います。

onnanoko

本日のまとめ

  1. Draw,Draw_Interpretor,DBRep ヘッダを読み込み、Draw_InitAppli() と DRAW_MAIN を持つソースコードを準備する。
  2. コマンド用インタフェイスを持った関数を作り、Draw_InitAppli() 内で、コマンドと関連付けする。
  3. コンパイルし、libTKDraw にリンクして、実行する。

OpenCASCADE の Draw Test Harness を活用する その1

OpenCASCADE には、コマンド駆動型の効率的なデバッグ環境 Draw Test Harness というものが付属しています。 OpenCASCADE のコードを、単機能ごとに評価できるツールとして開発には外せないものであり、非常に有用なものです。快適なデバッグするのには、グラフィカルなユーザインタフェイスやイベント、コマンド実行の仕組み、簡単な繰りかえし処理や変数、数式の解釈など、かなり高次元なフレームワークを構築しないと、難しいものがあります。その「本質以外の部分」を担ってくれるのが Draw Test Harness です。

Draw Test Harness の簡単な説明

Draw Test Harness は Tcl 言語のインタプリタを内包しており、Tcl 言語のモジュールとして OpenCASCADE を利用された様々なコマンドを実行することができます。また、Viewer や簡単なマウス処理も提供されているので、対話的にコマンドを実行し、結果を三次元ビューで確認することができます。

Tcl 言語をご存知ない方にとっては、「今から新しい言語を覚えるのか…」と逃げ腰になる必要はありません。確かに Tcl はレトロな言語なのですが、インタプリタとしては使い古されており、あまり高度なことをしようとしない限り、充分に、クイックリーに動いてくれる環境です。基本的には、デバッグを自動化してくれるマクロという位置付けですので、バッチファイルやシェルスクリプトを書いたことがある方ならば、充分に使いこなせると思います。基本はコマンドの羅列です。

Draw Test Harness では GUI のメニューも提供されていますが、こちらは Tcl 言語の相棒といっても過言ではない GUI フレームワーク Tk で実装されています。こちらに関しては、Tk を用いて GUI を構築しようとしない限り、まったく知識は要りません。

Draw Test Harness を実行する

前項の手順で OpenCASCADE をインストールし、ros ディレクトリを指す環境変数 CASROOT が定義された状態を前提としています。X サーバが動いている端末または、Xを飛ばしたクライアント上から、次のコマンドをタイプします。

[bash] DRAWEXE [/bash]

GUIのメニューが表示され、端末には「Draw[n]>」というプロンプトが表示されたと思います。これが Tcl コンソールです。プロンプトに続けて、

[bash] pload ALL axo box b 10 10 10 fit [/bash]

とタイプしていくと、平行投影ビューにボックスが表示され、ビューにフィットすると思います。dth

1行目の「pload ALL」は、モジュールファイルを全て読み込むコマンドです。このコマンドを最初に実行しておかないと、Tcl コンソールから OpenCASCADE モジュールをコールすることができません。(ちなみに、OpenCASCADE モジュールは、環境変数 CASROOT で定義された ros ディレクトリからパスを追っていき読み込んでいます。) 2行目の「axo」で平行投影ビューを表示して、3行目の「box b 10 10 10」で「(原点位置に)大きさ10,10,10のbというboxを作る」というコマンドです。最後の「fit」コマンドで、平行投影ビューいっぱいにboxの表示をフィットさせています。

最後に、「exit」とタイプすると Tcl コンソールが終了します。

このような具合で、コマンドを対話的にタイプしていって IGES を読み込んだり、オブジェクトの容積を出したりすることができるツールが Draw Test Harness です。 Tcl コンソール自体がインタプリタになっているので、上でタイプしたコマンドをテキストファイル box.tcl に保存しておき、

[bash] DRAWEXE < box.tcl [/bash]

することにより、一連の作業を自動的に処理させることもできます。 次の Tcl スクリプトは、ボックスと平面の交線(エッジを含むコンパウンド)を水平レベルごとに取得し、表示するサンプルです。

[perl]

!/usr/bin/env tclsh

pload ALL

ビューの作成

axo

ボックスの作成

box b 10 10 12 fit

ボックスを傾ける

trotate b 0 0 0 1 0 0 14 trotate b 0 0 0 0 1 0 -21 fit

水平面で交線を求める

for {set i 1} {$i <= 14} {incr i} { plane p$i 0 0 $i 0 0 1 psection c$i b p$i unset p$i } [/perl]

簡単な制御構文を用いています。私自信、Tcl 言語はそれほど書いたことあるわけではないのですが、以前のブログ記事を見てみるとこんなモノも書いていたようです。全然覚えてないな・・・。

Windows 環境では、CASROOT ディレクトリ(ros)直下にある draw.bat をダブルクリックすると、Tcl コンソールが開きます。

Draw Test Harness を使った応用

Draw Test Harness には、OpenCASCADE モジュールとして提供されているコマンドが数百種類もあります。ここでは、一つ一つに関しての説明は省きますが、これらのコマンドを組み合わせることで、ちょっとした非対話的な CAD ないし、三次元データコンバータくらいなら書けてしまいます。また、DRAWEXE の標準入出力をつないだアプリケーションを書けば、コマンド駆動型、かつしっかりとしたGUIを積んだアプリケーションを書けないことはありません。ちょっとした IGES ファイルの形状のチェックや、形状を反転・拡大縮小したい場合などにも力を発揮します。

また、提供されているコマンドの実装はソースコードレベルで公開されていますので、例えば、三次元オブジェクトの重心位置を取得するコードを書きたい場合、Draw コマンド vprops の実装を調べれば良いわけです。Draw インタプリタ内で「getsourcefile vprops」とタイプすると、ソースコードのファイル名を教えてくれます。そのファイル名を基に $CASROOT/ros/src から目的のソースコードを探し出すことができます。Draw コマンドは、基本的には単機能ごとに機能を提供しているものなので、Draw コマンドの実装方法が「最強のサンプル集」となることでしょう。

さらに、Draw Test Harness では、既存のコマンドに加え、ユーザが C++ を用いてコーディングした OpenCASCADE のコードも簡単に呼び出すことができます。OpenCASCADE を使う、つまり C++ で OpenCASCADE を使う場合、Draw Test Harness は最強のデバッグビューアとなるのです。

次回は、Draw Test Harness に自分で書いた OpenCASCADE コードを独自のコマンドとして登録し、実行してみます。

onnanoko本日のまとめ

  • Draw Test Harness は、コマンド駆動型の強力な OpenCASCADE インタプリタ。
  • 面倒な GUI 周りが最初から準備されているので、三次元幾何演算のトライアンドエラー、プレビューがかんたんにできる。
  • 対話的に利用するもよし、数百種類あるコマンドを組み合わせてバッチ処理をさせてもよし。
  • それぞれのコマンドは、source commandname によって C++ のソースコードを参照できる。

OpenCASCADE における幾何オブジェクトの管理

OpenCASCADE で、幾何オブジェクトを管理するにはいくつも方法があるようですが、付属のコマンド実行環境 Draw Test Harness では、幾何オブジェクトに対して文字列の名前を与え、それで管理できる仕組みを提供しています。
具体的には、Standard_CString 文字列をキーとする AIS コンテキストに登録された Shape との関連付けマップ構造を持っているようです。Draw Test Harness 上でコーディングする際、次のようなコードをしょっちゅう書きます。

[cpp]
{
// …

// オブジェクトを取得
TopoDS_Shape S = DBRep::Get(shapename);

// ...

// オブジェクトを追加
TopoDS_Shape SS = ...
DBRep::Set(SS, shapename2);

}
[/cpp]

DBRep クラス
のソースを追ってみると、名前文字列とインデックスキーの関連付けに Tcl のマップが利用されており、その後で Draw クラスの Set(), Get() がコールされており、Draw クラス内で Draw_VMap 型のマップが宣言されていました。さらに、Draw_VMap は TCollection_BasicMap クラスの派生クラスとなっており、このコレクション・マップを利用すれば幾何オブジェクトを管理することができるようです。

誘惑に負けて std::map を使いそうになってしまいますが、特にプログラミング上の制約の多い Windows で幾何オブジェクトを取り扱うには、TCollection_BasicMap を利用するのが無難なようです。

OpenCASCADE 6.6.0 をビルドする。

OpenCASCADE 6.6.0 がリリースされて数ヶ月経ちますが、ようやく自宅の環境でもビルドする暇が出来たので手順を書いておきたいと思います。
手順と言っても、ソースコードを落としてきて make するだけなので、すごく簡単です。

OpenCASCADE 6.6.0 のビルド

うちの環境は相変わらず Debian 7.0 の x86 環境ですので、その環境を前提として書いています。
まず、公式サイトのダウンロードページより、6.6.0 の樽玉を落としてきます。ここでは、/tmp/occ640 を作業ディレクトリにしますので、そこに落とします。ダウンロードの際は、公式サイトにユーザ登録する必要があります。

ダウンロード後、次のコマンドを実行します。

[bash]
cd /tmp/occ640
tar zxvf OpenCASCADE660.tgz
cd ros
./build_configure

configure が生成される

./configure –enable-data –enable-algo –enable-vis –enable-caf –enable-dxe –enable-draw \
–with-tbb-include=/usr/include/tbb –with-tbb-library=/usr/lib –with-freeimage=/usr/include \
–with-gl2ps=/usr/include
[/bash]

configure にビルドするコンポーネントと OpenCASCADE が利用しているライブラリのパスを指定しています。何も指定しなくても最低限の構成でビルドすることは出来ますが、最低限の構成だと IGES の読み書きすらまともにできないので、全ビルドするように指定しています。

./configure を実行する際、ログの最後のほうに出てくるレポートを参考にしてください。

[bash]

3rdparty mandatory products

freetype : yes
tcltk : yes

3rdparty optional products

gl2ps : yes
freeimage : yes
tbb includes : yes
tbb libraries : yes
qt : no (–with-qt=DIR option was not defined)

Component Build


FoundationClasses yes
ModelingData yes
ModelingAlgorithms yes
Visualization yes
ApplicationFramework yes
DataExchange yes
Draw yes
[/bash]

私の場合、サンプルとして付属する Qt プロジェクトでのみ利用されている「qt」の部分以外は、全て有効にしました。freetype、tcktk が欠落していると、Component の Visualization 以下のビルドができません。tbb は Intel の CPU で効率的にマルチスレッドを実現するためのライブラリです。

参考までに、私の Debian では次のパッケージをインストールしました。

[bash]
sudo aptitude install libxmu-dev libfreetype6-dev
sudo aptitude install tcl-dev tk-dev
sudo aptitude install libgl2ps-dev libfreeimage-dev libtbb-dev
[/bash]

それぞれの開発パッケージに含まれるヘッダファイル、ライブラリファイルは、sudo dpkg -L パッケージ名 で確認することができます。これを基に、configure オプションの –with-tbb-include= といったオプションを指定します。
どの Component が必要か取捨選択できるようになるまでは、とりあえず全部動くようにしておくのが無難でしょう。上の configure のレポートのようになったら、後は make するだけです。

[bash]
make
[/bash]

ちなみに、最後の make は非常に時間がかかります。私の環境(core i5/x86、SSD、シングルスレッドビルド)で、396分かかりました。

ビルド後のチェック

ビルドが完了したら、そのまま sudo make install してもいいのですが、コワイのでまずちゃんと使えるかチェックします。

正常にビルドができていれば、共有ライブラリは ros/adm/lin/amk//.libs/lib.so に生成されているはずです。また、ヘッダファイルは ros/inc に格納されているので、次のようにオプションを指定すれば、OpenCASCADE を利用したアプリケーションのビルドをすることが出来ます。

[bash]
cd ros
mkdir mysrc
cd mysrc
vim test.cpp
cat test.cpp

include <stdio.h>

include "Standard_Boolean.hxx"

// サンプルプログラム
int main()
{
Standard_Boolean n = Standard_True;
printf("%d\n", n);
return 0;
}
g++ -lTKernel -I/tmp/occ660/ros/inc -L/tmp/occ660/ros/adm/lin/amk//.libs test.cpp
LD_LIBRARY_PATH=/tmp/occ660/adm/lin/amk/
/.libs ./a.out
1
[/bash]

OpenCASCADE で利用される基本型 Standard_Boolean を用いたサンプルプログラムをビルドし、実行した模様です。最終行のように「1」が表示されれば、OpenCASCADE が利用できるようになっています。

インストール

正常に使えている事が確認できたら、次のコマンドでインストールします。

[bash]
make install
[/bash]

これでライブラリは /usr/local/lib、ヘッダファイルは /usr/local/inc に格納されます。また、DRAWEXE といった実行ファイルが /usr/local/bin に設置されます。

最後に、OpenCASCADE のサンプルプロジェクトなどで ros ディレクトリ以下のファイルを利用しますので、作業ディレクトリを

[bash]
mv /tmp/occ660 /opt/
[/bash]

しておき、$HOME/.profile に $CASROOT=/opt/occ660/ors を追記しておきます。

Windows でのビルド

Windows における OpenCASCADE は、標準ビルド環境が Microsoft Visual Studio のソリューションファイルとして提供されている為、前項のような開発者の環境に合わせた条件付きコンパイルがやりづらくなっています。基本的な手順は、6.5.4 の手順と変わりません。

Tiarra やめました。

長らく利用していた IRC プロキシソフト Tiarra の利用をやめました。理由は次のとおりです。

  • tscreen(GNU Screenみたいなの)上の riece で IRC には常駐しているので、端末ログインすればどこからでも繋がる。
    • 携帯電話なんかの専用クライアントから接続できる手軽さはあるけれど、果たしてサーバソフトを常駐させるまで使うかっていうと、そうでもなかった。
    • 携帯電話なんかの専用クライアントから接続する場合は、大人しく別USERとしてログインすればいい気がしてた。
  • 頼んでもない大量の WHO リストが1,2分おきに tiarra サーバから出力されるようになって、チャットどころじゃなくなる。
    • サーバメッセージをメインログじゃなくてチャンネルログに流して、かつアクティブを持っていっちゃうようなクライアント側にも問題があるはずなんだけど、なんだか気持ち悪い。XChatやLimechatで接続すると、WHOリストは「プロトコルログ」のような生ログ表示以外には表示されないので、特に問題ない。
    • tiarra を経由しなかったら、riece でも WHOリストの大量取得現象は見られなかったから、IRCサーバ側にリクエストを投げているのは tiarra のはず。ここ1年くらいで、現象が起きる日と起きないがある。設定はいじってないし、なんだろうな。
    • 次の理由により、詳しく調べる前にtiarraの利用をやめることにしました。
  • かなり高度かつ柔軟な設定が出来るわりに、得られる効果がほとんどない。
    • tiarra のメリットと言えば、同一ホストからのセッション数を減らすことができる点。
    • それ以外だと、メイン宛のPRIVMSGを外部から読める、チャンネルの入退室を最小限にできる、常駐ロガーとして活躍…くらいか。
      • 常駐ロガーという意味では、自分のように既にクライアントソフトが常駐しているユーザにとってまるで意味がないもの。
    • デメリット
      • 設定が高度な分、煩雑でもあり、接続ができなくなったり、DCCのルーティング設定で問題があった場合など、問題を複雑化してしまいそう。
      • 複数サーバーに対し、それぞれ違う nick でログインしている/したいのに、そこらへんの設定が考慮されずに設計されていそう。
      • 設定自体、ちょっとWindows寄り。起動スクリプトを書いてcronなどに登録したり、ユーザごとの設定ファイルに対応させたり、いろいろ改造して使ってました。

お世話になっていたのは確かなのですが、IRC を便利に手間なく使うための Tiarra を本気で改良するのも本末転倒になってしまいそうで、結果的に利用をやめてしまいました。

当初、自分が Tiarra を導入した頃はガラケーでしたので、CGI/HTMLベースのIRCクライアントと一緒に使う際に威力を発揮していたんですが、今じゃスマホからSSHで端末を開いて、screen セッション上で動いている riece on emacs を表示して、普通にチャットができますもんね。

なんだかマイナス意見ばかりになってしまいましたが(利用をやめた理由だから当たり前ですが…)、オープンソースでこれだけのものを作って公開してくださった方には感謝しています。

 

tr1の正規表現ライブラリを使う

Microsoft Visual Studio 2008 の SP1 からは C++ Technical Report 1 (TR1)をサポートしているので、Boost やPCRE などのライブラリを用いずに正規表現を扱うことができます。

[cpp]

include <iostream>

include <string>

include <regex>

// usage: a.exe pattern replace-to

using namespace std;

int main(int c, char** v)
{
if (c != 3) return 1;

tr1::regex ex(v[1]); // 正規表現オブジェクト
char buf[256]; // 読み取り用のバッファ

while (cin.getline(buf, sizeof(buf)))
cout << tr1::regex_replace(string(buf), ex, string(v[2])) << endl;

return 0;
}
[/cpp]

Boost のコードと比べれば、名前空間が boost から tr1 に変わったこと、regex ヘッダが標準パスに入ったこと、regex_replace() の第3引数が char* から std::string に変わったことくらいでしょうか。Boost からの移植ですので、互換性があって当然ですね。

C++11 では、名前空間がさらに tr1 から std に移されているようです。TR1 は C++ の言語自体の規格ではないものの、拡張ライブラリの標準規格ですので、名前空間などの移り変わりに注意しておけば、今後も安心して使っていけると思われます。

音声合成エンジン Open JTalk について

動画の生放送サイトを見ていると、リアルタイムに視聴者から来るコメントを合成音声によって読み上げている人が増えているようで、合成音声のジャンルも賑わってきたようです。ボーカロイドの初音ミクが登場するよりも前、AquesTalk をいじってみて調整次第ではかなり「聞ける」音声になるんだなあ、と実感した覚えがあります。 自分は合成音声を入れた動画を作成したり、配信したりするわけではないのですが、IRCのログやTwitterのタイムラインの読み上げ程度には使ってみたくなりました。 ということで、今更感が拭えませんが、Open JTalkを使ってみた時のメモを書いておきます。

Open JTalk とは

オープンソースの日本語音声合成エンジンです。音声合成エンジンには、入力文章の処理部と発音部に機能が分かれるんですが、文章処理にはオープンソースの形態素解析エンジン MeCab を利用しているようです。ChaSen や Kakasi に並んで有名なエンジンですね。発音部には、HMM-based Speech Synthesis System (HTS)というエンジンを利用しているようです。 Open JTalk のデモページでは、WWWブラウザ経由で音声合成を試すことができます。 試しに作ってみた音声を置いておきます。能登っぽい声ですね。 こちらのデモページでは、更新履歴を見る限り、Open JTalk のバージョン 1.06 を利用しているようです。

インストール

インストールした時点での私の環境は Ubuntu 13.04 x64 版です。2013年6月現在、Ubuntu のリポジトリに入っているのはバージョン 1.05 で、公式サイトにて公開されている最新版は 1.06 です。詳しい事は後述しますが、これらのバージョンによって利用できる音声データファイルの形式が違ってきます。

OpenCASCADE のオブジェクトを WebGL で表示する

OpenCASCADE の Python 実装である PythonOCC の人が、2011年の1月のブログ記事で OpenCASCADE のオブジェクトを WebGL 向けに出力する方法を紹介しています。2年以上経った今、ウェブブラウザも WebGL に対応してきましたし、OpenGL 3.x をサポートしたオンボードチップも一般化してきました。精密な曲線・曲面表現の問題を抜きにすると、WebGL という手軽に三次元モデルが表示できる環境は魅力的だと考えています。メタセコイアの MQO 形式も WebGL で表示ができるよう作っている方もいますし、夢は広がるばかりです。

WebGL については、OpenGL ハードウェアにレンダリング作業を投げたり、HTML の img タグの data スキーム内のデータをリアルタイムに書き換えてアニメーションさせている、という程度の漠然とした知識しかありませんでした。綺麗なデモを見てみると、ちゃんと JavaScript を勉強してやってみたいなあ、花形的なジャンルだなあとは思いつつ…、言語自体は綺麗で好みなのですが、なんだかなあ。

先日、弟と呑みに行って WebGL の話が出て、PythonOCC の人が OpenCASCADE のオブジェクトを WebGL 向けに出力していることを思い出しました。

こちらのデモページのように、OpenCASCADE で作成したオブジェクトがウェブブラウザの中で、ぐりぐり動かせて三次元表示されています。ソースコードを見てみると、TopoDS_Shape オブジェクトを三角形メッシュ化して、WebGL で読み込み、表示ができるフォーマットに出力し直しているようです。

私自身は、PythonOCC を利用していないので、このコードを参考に C++ で実装し直そうと思いました。

#include <iostream>
#include <TopoDS.hxx>
#include <TopoDS_Face.hxx>
#include <BRepTools.hxx> // BRepTools::Read()
#include <BRep_Tool.hxx> // BRep_Tool::Triangulation()
#include <BRep_Builder.hxx>
#include <BRepMesh.hxx>
#include <TopExp_Explorer.hxx>
#include <Poly_Triangulation.hxx>

#define MESHPREC 1000.0

using namespace std;

int main(int argc, char** argv)
{
  // 引数チェック
  if (argc != 2) {
    cerr << "The brep2webgl convert an OpenCASCADE Brep file format\n"
      "to ECMAScript script for WebGL.\n"
      "Usage:\n"
      " brep2webgl infile.brep > outfile.js\n"
      << endl;
    return 1;
  }

  // 読み込み
  TopoDS_Shape shape;
  BRep_Builder bb;
  BRepTools::Read(shape, argv[1], bb);
  if (shape.IsNull()) {
    cerr << "Convertion incompleted." << endl;
    return 1;
  }

  cout <<
  "var Shape = function() {"
    "var scope = this;"
    "THREE.Geometry.call(this);";

    cout << endl;

    BRepMesh::Mesh(shape, MESHPREC);
    TopExp_Explorer ex(shape, TopAbs_FACE);
    double scale = 0.001;

    for (int vindex = 0; ex.More(); ex.Next()) {
      TopoDS_Face face = TopoDS::Face(ex.Current());
      TopLoc_Location loc;
      Handle(Poly_Triangulation) poly = BRep_Tool::Triangulation(face, loc); // 三角形パッチ化
      const Poly_Array1OfTriangle& tris = poly->Triangles();
      for (Standard_Integer i = tris.Lower(); i <= tris.Upper(); i++) {
        const Poly_Triangle& tri = tris.Value(i);
        Standard_Integer n1, n2, n3; // ノードインデックス
        if (face.Orientation() == TopAbs_REVERSED)
          tri.Get(n3, n2, n1);
        else
          tri.Get(n1, n2, n3);
        gp_Pnt p1 = poly->Nodes().Value(n1);
        gp_Pnt p2 = poly->Nodes().Value(n2);
        gp_Pnt p3 = poly->Nodes().Value(n3);

        cout <<
          "v(" << p1.X() * scale << "," << p1.Y() * scale << "," << p1.Z() * scale << ");"
          "v(" << p2.X() * scale << "," << p2.Y() * scale << "," << p2.Z() * scale << ");"
          "v(" << p3.X() * scale << "," << p3.Y() * scale << "," << p3.Z() * scale << ");"
          "f3(" << vindex + 0 << "," << vindex + 1 << "," << vindex + 2 << ","
          << p1.X() << "," << p1.Y() << "," << p1.Z() << ","
          << p2.X() << "," << p2.Y() << "," << p2.Z() << ","
          << p3.X() << "," << p3.Y() << "," << p3.Z() << ");";
        cout << endl;
        vindex += 3;
      }

    }

    cout
      << endl << "this.sortFacesByMaterial();"
      << endl << "function v(x,y,z){scope.vertices.push(new THREE.Vertex(new THREE.Vector3(x,y,z)));}"
      << endl << "function f3(a,b,c,x,y,z,x2,y2,z2,x3,y3,z3){"
      << endl << "scope.faces.push(new THREE.Face3(a,b,c,"
      << endl << "[new THREE.Vector3(x,y,z),new THREE.Vector3(x2,y2,z2),new THREE.Vector3(x3,y3,z3)]));}"
      << endl << "}"
      << endl << "Shape.prototype=new THREE.Geometry();"
      << endl << "Shape.prototype.constructor=Shape;";

  return 0;

}

ビルドは次のとおり。BRepToolとMeshを使っているので、それぞれライブラリを指定してやります。

$ g++ -g -lTKernel -lTKMath -lTKBRep -lTKMesh -L/usr/lib/opencas -I/usr/include/opencascade -o brep2webgl brep2webgl.cx

このプログラムは、引数に与えられた BRep ファイルを読み込んで、内容物を三角形メッシュ化して前述の環境 pythonocc_webgl.html および Three.js で読み込める JavaScript 形式のデータを標準出力に吐きます。 実際に実行するには、次のようにタイプします。

$ brep2webgl test.brep > shape.js

標準出力にスクリプトが印字されますので、shape.js にリダイレクトをして保存し、pythonocc_webgl.html で読み込めば三次元モデルデータが表示されます。

試しに制作したデモをこちらに置いておきます。結構大きめのIGESファイルをBRepに変換後、上記のプログラムを介してWebGLで表示できるようにしたものですので、読み込みに時間がかかるかもしれません。 ※平面の法線ベクトルの関係で、デモを開いた状態では、ほとんど何も表示されていないかもしれません。マウスの左ボタンで画面を軽くドラッグしてやると、物体が回転してものが表示されるようになるはずです。