はじめに
ND3Mの田川です。
ND3Mでは先日、「音標本箱」という3Dプリンタプロダクトのポップアップストアイベントを開催しました。このイベントではシンセサイザーの音から作成したテクスチャのフラワーベースを出品しています。また、制作にあたってはHoudiniを使って音の生成からG-codeの出力までを行いました。
今回は、このプロダクトでも用いているHoudini20でモデリングからスライス、G-codeの出力までを行う方法について紹介していきます。
動作環境
Houdini Indy Version 20.0.625
Windows11
前提
本記事は、以下の読者を想定しています。
- スライサーを使わずにG-codeを作成することに興味を持っている方
- Houdiniの使い方やVEXの基礎がある程度わかる方
- 3Dプリンタの使用経験があり、G-codeの存在を知っている方
Houdiniや3Dプリンタについてよく知らない方でも実装できるようにわかりやすい手順での紹介を心がけていますが、基本的な用語についての紹介は省いています。わからない単語などがあった場合は適宜検索していただけますと幸いです。
また、HoudiniやG-codeについて事前知識が必要になる場面では、参考として関連した記事やドキュメント、動画などを記載します。必要に合わせてお読みください。
G-codeとは?
G-codeは、CNCや3Dプリンタなどの工作機械の動きを指示するために使用される汎用コードの一種です。
それぞれの行がマシンの動きを制御するコマンドとなっており、移動する座標や速度、送り出すエクストルーダーの量などを指示することができます。
3DプリンタやG-codeに関しては、ND3Mの田住がこちらの動画で詳しく解説しております。興味がある方はこちらをご参照ください。
G-codeの仕組み
各行の先頭についている「G92」や「M104」などがコマンドを表しており、その後に続く文字列でそのコマンドの設定や動作を指定しています。
これらのコマンドを先頭から順に実行していくことでマシンの動きを制御するというのがG-codeの基本的な考え方です。
コマンドがたくさん並んでいて気圧されてしまうかもしれませんが、実際のG-codeで使用するコマンドはそこまで多くありません。また、本記事で使用するコマンドについては実装の際に詳しく紹介していきます。
各コマンドの詳細についてはこちらのドキュメントを参照してください。
G-codeの作成方法
通常3Dプリンタでモデルを印刷する際は、下記のようなスライサーと呼ばれるソフトを使用してモデルをG-codeに変換します。
スライサーを用いることにより、インフィルやサポートなど高度な設定を簡単に行うことができます。そのため、G-codeを作成するうえではスライサーを使うだけで十分な場面がほとんどです。
一方で、スライサーに実装されている機能のみしか使えないため複雑な形状の造形が難しい場面も存在します。そこで、Houdiniを使用してG-codeの出力までを行いより自由度の高い造形を実現します。
以下では、そのG-code作成の基本的な実装について解説していきます。
Houdiniでの実装
HoudiniでG-codeを作成するにあたり、以下の要素を実装していきます。なお、2、3および4がスライサーにあたる機能です。
- 3Dプリントするモデルの作成
- G-codeのベースとなるパスの作成
- パスに対するG-codeへの変換に必要となる情報の付与
- パスをもとにしたG-codeの出力
各実装内容の概要
1. 3Dプリントするモデルの作成
もととなる3Dプリントモデルを作成します。また造形したいものに合わせてモデルを変更することも可能です。ただし、今回紹介するG-codeの作成方法では正しく造形できない形状も存在するので注意が必要です。
2. G-codeのベースとなるパスの作成
モデルを連続した直線の集まりに分解してG-codeに変換が可能なパスを作成します。このPrimitiveの集まりであるパスがそのまま3Dプリンタのヘッドが動く経路となるため、パスが下から順に無理なく一筆書きできるようになっている必要があります。そこで、パスのPointやPrimitiveの順番の整理などを重点的に行っていきます。
3. パスに対するG-codeへの変換に必要となる情報の付与
ここでは、G-codeを作成するのに必要となる各種のパラメータをパスに付与します。具体的には、PrimitiveごとにG-codeで使用するコマンドを判定するとともに、吐出量の値を算出するために必要な情報を算出しPrimitiveにアトリビュートとして付与します。
実装にあたってG-codeや3Dプリンタについての基本的な知識が必要となるため、ここで改めてコマンドなどの解説も行います。
4. パスをもとにしたG-codeの出力
最後に、作成したパスをもとにHoudiniのHDA(Houdini Digital Asset)という機能を使い、Pythonを用いてG-codeの出力を行います。ここでは、パスのPrimitiveの情報を順番に読み取り、それをコマンドに変換していくことでG-codeを作成します。
なお、今回は基本的な機能しか利用しないためHDAの機能や一般的な使用方法については触れません。詳細については以下のドキュメントを参照してください。
最終的な造形物
本記事では、実装例として下記の写真のモデルのようなシンプルなモデルを造形します。
3Dプリントするモデルの作成
はじめに、花瓶形状のモデルを作成します。
まず、ベースとなる円柱形を作成します。
使用するスケールはミリメートル単位であることに注意してください。
この例では、半径10mm、高さ80mmの円柱を作成しています。
float offset = chramp("ramp", relbbox(0, @P).y) * chf("amp");
v@P += offset * v@N;
G-codeのベースとなるパスの作成
コンターの作成
次に、モデルを積層ピッチに合わせてコンター(等高線)に変換します。
ここでは、モデルと積層ピッチごとに配置された平面の交線を求めることでコンターを作成します。
はじめに、基準となるy軸方向の直線を用意します。このとき、直線の高さがモデルと同じになるようにしてください。また、直線上のポイントも間隔が積層ピッチと同じなるように数を調整してください。
今回は、高さが80mmで積層ピッチが0.2mmのためポイント数を401にしています。
さらに、モデルよりも大きいグリッドを用意しCopy to point SOPで直線上の点にコピーします。
Intersection Analysis SOPでコピーしたグリッドとモデルの交線を求めます。
このとき、Output Intersection Segmentsにチェックを入れるようにしてください。
パスの整理
ここまでで、基本的なパスを作成することができました。しかし、できたパスを観察してみると大きな問題があることがわかります。
この画像は、Pointの番号(ptnum)が大きくなるにつれて白から青に変化するようにPointに色をつけたものです。モデル下部から上部にかけて順に青くなっていますが、一部側面に白い(Pointの番号が小さい)点が見えます。
同様にPrimitiveの番号(primnum)に応じて白から赤になるよう色をつけた画像です。こちらでは、モデルの側面で大きく色が異なっておりPrimitiveの順番が変則的になっていることが見て取れます。
以上のように、単純にコンターを作成しただけではパスが下から順に一筆書きできるようになっていないことがわかりました。
そこで、印刷時にヘッドの干渉などが起きないようパスの整理を行っていきます。
Primitiveの順番の整理
はじめに、Primitiveの順番の整理から行います。
まず、Polypath SOPを用いて連続する直線を一本のポリラインにまとめソートをしやすくします。
さらに、Resample SOPでPointを等間隔に配置しなおします。このときの間隔が実際に印刷したときのメッシュの解像感やG-codeのファイルサイズに影響するので、Lengthの値は適宜調整してください。今回は0.1で設定しています。
Sort SOPでPrimitiveを下から順番に並び替えます。ここでは、Primitive Sortを「By Y」に設定してください。
最後に、PrimitiveとPointに下から何番目の層なのかの情報を記録します。
i@slice = @primnum;
int prims[] = pointprims(0, @ptnum);
i@slice = prim(0, "slice", prims[0]);
これでPrimitiveの順番の整理と次に向けた下準備が終わりました。先ほどと同様の方法で確認すると、Primitiveが実際に下から順番に並んでいることが確認できます。
各層のPointの順番の整理
次に、各層ごとにPointの順番を整理していきます。
現状、Pointの番号が大きくなる方向は層ごとに異なっておりきれいなパスとは言えません。また、ポリラインの始点が他の層とずれている箇所も存在します。そこで、次のような考え方でPointの順番を並び替えていきます。
上記の模式図は、ある層を上から見た図です。基準となる点(おおよそ円の中心)を用意し、基準となるベクトルと基準点から各点へのベクトルのなす角度をそれぞれ算出します。そして、その角度が小さいPointから順にソートすることによって始点と向きが揃ったパスを得ることができます。次に、これを実装していきます。
まず、For-Eachを使ってPointを1層ずつ取り出します。
このとき、Block End SOPのパラメータは画像のように設定します。
また。Block Begine SOPのMethodは「Extract Piece or Point」を選択してください。
Attribute Promote SOPで基準点として層に含まれる点の平均値を求め、さらに次のようなAttribute Wrangle(Points)で各点の角度を算出します。
// 基準点と基準方向ベクトルを取得・設定
vector center = detail(0, "center");
vector ref_dir = normalize(set(0, 0, 1));
// 各ポイントの基準点からの方向ベクトルを計算・正規化
vector pos = normalize(@P - center);
// 基準方向ベクトルとの角度を計算
float ang = acos(dot(ref_dir, pos));
// 外積のY成分をチェックし、時計回りの角度に変換
if (cross(ref_dir, pos).y <= 0) {
ang = $PI*2 - ang;
}
// 角度を度に変換し、アトリビュートとして設定
@ang = degrees(ang);
そして、Sort SOPを用いてPointを角度の順番でソートします。このとき、Point Sortで「By Attribute」を選択し、Attributeに「ang」を設定してください。
これにより、上記の上側の画像から下側の画像ようにPointの順番が大きくなる方向と始点の位置を揃えることができました。
また、念の為Attribute Delete SOPでここまでで作成した不要なアトリビュートを削除しています。
Primitiveの作成
For-Eachで各層のPointのみを取り出しているため、現状では点同士がつながっていません。そこで、最後に再度Primitiveを作成します。
Add SOPを使用してPointを繋ぎ、ポリラインを作成します。(Add SOPのパラメータは画像のようにし、Closedのチェックを外してください)
ここで作成したポリラインはPrimitiveが一つにまとまっているため、Convert Line SOPを用いて分離したPrimitiveに変換します。
また、Add SOPで作成したポリラインは始点と終点がつながっていません。そこで、Attribute Wrangle(Detail)を用いて繋がっていない部分のPrimitiveを作成します。
// ポイントとスライス番号の取得
vector pos0 = point(0, "P", 0);
int slice = point(0, "slice", 0);
// 新しいポイントの追加
int newpt = addpoint(0, pos0);
setpointattrib(0, "slice", newpt, slice);
// 新しいポリラインプリミティブの追加
int newprim = addprim(0, "polyline", @numpt - 1, newpt);
これにより、連続したパスを作成することができました。ここで、始点(Point番号0)と終点(Point番号630)が同じ位置にあり、かつ繋がっていないことに注意してください。これにより、パスを次の層につなげる際に処理が楽になります。
仕上げとして、Attribute Create SOPを用いて作成したパスのPrimitiveが下から何層目なのかの情報を再度付与します。
Valueにはdetail("../foreach_count1", "iteration", 0)
を設定しSliceにFor-Eachのイテレーション番号を設定しています。
以上の操作を通じて、Point、Primitiveのどちらも下から順番に連続して番号が大きくなるように整理することができました。
次からはここで作成したパスをG-codeへ変換するために必要となる情報を作成していきます。
パスに対するG-codeへの変換に必要となる情報の付与
G-codeで必要となる情報を整理するために、ここで一度今回の実装で使用するG-codeのコマンドについて簡単にまとめます。
使用する主要なコマンドは以下の3つです。
G0 / G1
これらのコマンドは移動に関する命令で、以下のように使用します。
G0 X80.0624 Y90.0617 Z0.2 F1500.0
G1 X84.1749 Y89.0842 Z0.0 E0.0033 F1000.0
G0やG1のあとの文字列はパラメータを表しており、X
やY
、Z
は現在位置から移動する先の座標値、E
はフィラメントを押し出す量(mm)、F
は移動する速さ(mm/min)を表しています。
G0とG1は基本的に同じ機能を持ったコマンドですが、一般的にはG1は吐出を伴う移動に使用し、G0は移動のみを行う場合に使用するといった使い分けがされています。
M83
上記のG0/G1コマンドでは、指定がない場合は絶対座標を使用し、フィラメントの押し出し量についてもプリントの起点からの累積値を用います。しかし、押し出し量については次の地点までの増分のみを算出する方が容易です。
そこで、M83コマンドを使用して押し出し量のみ「相対モード」へ変更します。これにより、G1で指定するEの値として現在から次の地点にかけての差分のみを指定できるようになります。
Start G-code / End G-code
実際の造形では、これら以外にも3Dプリンタの温度設定やヘッドを初期位置に動かすコマンド、終了時にヘッドを退避させるコマンドなどが必要になります。しかし、それらのG-codeは造形物によらず共通していることが多いため、Start G-codeやEnd G-codeとして3Dプリンタに応じて用意されていることが多いです。
今回の実装でも、Start G-codeとEnd G-codeはスライサーで用意されたものを流用します。コードは機種や設定によって変わるので、ご自身の環境に合わせて適切なものを使用してください。
確認方法などは下記のサイトなどを参照してください。
パスへの情報の付与
以上のコマンドの情報を踏まえ、G-codeのベースとなるパスには以下の3つの情報が必要であることがわかりました。
- パスが押し出しを伴うかどうか(G0 / G1)
- 移動先の絶対座標
- フィラメントの押し出し量
- 移動する速さ
移動先の絶対座標は前章で作成したパスから取得できます。また、移動する速さはG-codeへの変換の際に一律で設定するのでパスに情報を付与する必要はありません。
そこでここからは、パスが押し出しを伴うかの判定と、フィラメントの押し出し量を算出するために必要な情報の取得・付与を行っていきます。
押し出しを伴うかの判定
現状、印刷する部分のみをパスとして作成しています。しかし、層の間を移動するパスは作成できておらず、これらは吐出を伴わないパス(G0)として作成する必要があります。
そこで、Attrbute Wrangle(Points)を用いて以下のようにパスを作成し、G0のグループを付与します。
// ポイントのプリミティブとスライス番号を取得
int prims[] = pointprims(0, @ptnum);
int slice = prim(0, "slice", prims[0]);
// 現在のポイントのプリミティブを格納
i[]@prims = prims;
// 次のポイントのプリミティブを取得
int nextprims[] = pointprims(0, @ptnum + 1);
// 新しいプリミティブを追加するパスの端点の条件をチェック
if(@ptnum < @numpt-1 && len(prims) == 1 && len(nextprims) == 1){
// 新しいポリラインプリミティブを追加
int newprim = addprim(0, "polyline", @ptnum, @ptnum+1);
// 新しいプリミティブのスライス番号、グループを設定
setprimattrib(0, "slice", newprim, slice);
setprimgroup(0, "G0", newprim, 1);
}
Split SOPを使ってG0のグループを分離し、赤に着色したものが上記の図です。ほぼ同じ方向を向いた位置にパスの始点が集まり、きれいに上下の層が繋がっていることが確認できます。
また、新しくPrimitiveを作成したため再度Sort SOPを使って順番の整理を行います。ここでは、Point SortおよびPrimitive Sortを「By Attribute」、Attributeをどちらも「Slice」に設定します。
フィラメントの押し出し量を算出するために必要な情報の取得・付与
フィラメントの吐出量の算出についてはこちらのサイトが詳しく解説されているのでこちらをお読みください。
上記のサイトより、フィラメントの吐出量を算出するためには
- 押出幅
- 積層ピッチ
- 吐出ビートの長さ(パスの長さ)
- フィラメント径
- フローレート
が必要であることがわかります。
押出幅やフィラメント径は、使用するノズルの径やフィラメントによって決まります。また、積層ピッチはコンターを作成する際に決定しました。そのため、ここではパスの長さとフローレートのみを算出すれば良いことがわかります。
パスの長さはMeasure SOPを使うことで簡単に求まります。
PrimitiveのGeometry Spreadsheetを確認してみると、perimeterの名前でパスの長さがアトリビュートとして付与されていることが確認できます。
また、今回は吐出量を調整する必要がないため、アトリビュートとして付与するフローレートの値はAttribute Create SOPで1に固定します。
これで吐出量の算出に必要なパラメータを用意することができました。ただし、ノズル径やフィラメント径を設定する必要があるため、実際の吐出量の算出はG-codeへの変換を行う際に行います。
以上でG-code必要な情報を持ったパスの作成が完了しました。
次でいよいよ、このパスをもとにG-codeの出力を行います。
パスをもとにしたG-codeの出力
本記事では、パスの情報を読み取り、G-codeへと変換してファイルを保存するためにPythonを用います。
HoudiniでPythonを実行する環境はいくつかありますが、今回は流用や更新が容易であるためHDAのPythonモジュールを使用します1。
次からはHDAを用いて、ボタンを押すとG-codeへの変換処理が実行され指定したファイルにG-codeが出力される仕組みを実装していきます。
なお、HDAのより詳しい作り方やPhtonモジュールの使い方などは下記のチュートリアル動画やサイトなどを参照してください。
HDAの作成方法
Null SOPを作成し、選択した状態でAssets > New Digital Asset From Selection...
を選択します。
新しいウィンドウが開くので、Asset Labelに好きな名前を設定してください。
新しいHDAが作成できたら、次は入力用のパラメータ作成していきます。作成したHDAを右クリックしてType Propertiesウィンドウを開いてください。
Parametersタブに以下の必要なパラメータを設定していきます。
- Write File(Button)
- Nozzle Diameter(Float)
- Filament Diameter(Float)
- Leyer Height(Float)
- Print Speed(Float)
- Travel Speed(Float)
- outfile(File)
- Start Code(String)
- End Code(String)
(Start CodeとEnd Codeを設定する際は、Parameter Descriptionの「Multi-line String」にチェックを入れてください)
パラメータを設定し、必要な情報を入力すると上記の画像のようになります。これでHDAの基本的な設定は完了しました。(このパラメータの値はご自身の環境に合わせて適宜設定してください)
G-codeへの変換処理を行うPythonモジュールの設定
再度Type Propertiesウィンドウを開き、Scriptsタブに移動します。
Event HandlerからPython Moduleを選択すると、ScriptsにPython Moduleが追加され、ウィンドウ右側にスクリプトが入力できるようになります。そこに、以下のPythonスクリプトを入力してください。
def writeFile(kwargs):
node = kwargs['node']
geo = node.geometry()
# HDAパラメータの読み取り
# node.param()で指定するパラメータ名は
# TypePropertiesで設定したパラメータのNameの表記と一致するようにする
params = {
"nozzledia": node.parm("nozzledia").eval(),
"filamentdia": node.parm("filamentdia").eval(),
"layerheight": node.parm("layerheight").eval(),
"printspeed": str(node.parm("printspeed").eval()),
"travelspeed": str(node.parm("travelspeed").eval()),
"startcode": node.parm("startcode").eval(),
"endcode": node.parm("endcode").eval(),
"outfile": str(node.parm("outfile").eval())
}
gcode = params["startcode"]
gcode += "M83 ; Relative extrusion mode\n"
# 初期位置設定
loc0 = geo.prims()[0].vertices()[0].point().position()
gcode += f"G0 X{round(loc0[0], 4)} Y{round(loc0[2], 4)} Z{round(loc0[1], 4)} F{params['travelspeed']}\n"
# パスの情報の読み取りとG-codeへの変換
for prim in geo.prims():
if prim.type() == hou.primType.Polygon:
loc1 = prim.vertices()[1].point().position()
length = prim.attribValue("perimeter")
flow = prim.vertices()[1].point().attribValue("flow")
# 吐出量の算出
extrude = (4 * params["nozzledia"] * params["layerheight"] * flow * length) / (3.14 * params["filamentdia"] ** 2)
extrude = round(extrude, 4)
if prim.groups()[0].name() == "G1":
gcode += f"G1 X{round(loc1[0], 4)} Y{round(loc1[2], 4)} Z{round(loc1[1], 4)} E{extrude} F{params['printspeed']}\n"
if prim.groups()[0].name() == "G0":
gcode += f"G0 X{round(loc1[0], 4)} Y{round(loc1[2], 4)} Z{round(loc1[1], 4)} F{params['travelspeed']}\n"
# 終了コードの追加
gcode += "G1 X0 Y0\n"
gcode += params["endcode"]
# ファイルへの書き込み
with open(params["outfile"], "w") as myfile:
myfile.write(gcode)
このスクリプトでは、HDAに入力されたパラメータとパスの情報をもとにG-codeを作成しています。for文で各Primitiveのアトリビュートを読み取り、その情報をもとに1行ずつG-codeを作成しています。また、パスのG-codeを作成する前にM83コマンドを挿入することで、相対モードへの変更を行っています。
ここでは詳しい説明を省きますが、HoudiniでのPythonの扱い方をより詳しく知りたい方は以下のドキュメントを参照してください。
最後に、ボタンを押した際にこのスクリプトが呼び出されるようにします。
Parametersタブに戻り、Write File
と名前をつけたボタンの詳細を開きます。
Callback Scriptにkwargs['node'].hdaModule().writeFile(kwargs)
を入力することで、このボタンを押した際に先程のPythonモジュールが呼び出されるように設定できます。
以上で、G-codeの出力を行うためのHDAの作成が完了しました。
G-codeの書き出し
最後に、作成したHDAを使って実際にG-codeの書き出しを行ってみます。
まず、作成したパスは原点上にあるため3Dプリンタの印刷可能な範囲への移動を行います。
そして作成したHDAにTransform SOPの出力をつなぎ、Write Fileボタンを押します。数秒待つと処理が完了し、指定したファイルが出力されます。
G-codeを確認できるソフト(Repetia-host等)で出力されたファイルを確認してみると、実際にパスやG-codeが問題なく書き出されていることが確認できます。
このG-codeを3Dプリンタで出力することで、最終成果物として掲載した写真のような造形物を得ることができます。
最後に
本記事では、Houdiniを使ってモデル作成からG-codeの書き出しまでを一貫して行う方法を紹介しました。
アトリビュートを使ってパスの順番を変化させたり、パラメータを管理したりする方法は、はじめのうちは非常に複雑に感じるかもしれません。しかし、アトリビュートを使って自分でアルゴリズムを組むことにより、少ない手数で細かくパスの形状やパラメータを変化させることが可能となります。こうした自由度の高さや応用範囲の広さはHoudiniの大きな利点であると感じます。
今回はG-code作成の基礎的な部分のみの紹介となりましたが、より発展的な内容(wall数の調整、non-planarやスパイラル、変化する積層ピッチに合わせた吐出量の自動調整など)についても今後記事にしていければと考えています。
非常に長くなってしまいましたが、ここまでお付き合いいただきありがとうございました。この記事が皆様のHoudiniライフや3Dプリンタライフのお役に立てれば幸いです。
-
HDAのPythonモジュールを利用してパスからG-codeへ変換を行う手法に関しては、海外のウェブサイトを参考に改変を行い実装しました。
現在、そのウェブサイトがどこにあるのかを失念してしまいリンクを載せることができませんが、見つかり次第記載します。 ↩