PythonとOpenSCADで快適な3Dモデリング生活

# はじめに
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/06751538-4a5f-a8b1-53bb-7b083123f707.png)

[OpenSCAD](https://www.openscad.org/)はFusion360，SketchUp，FreeCADなどの3DCADとは異なり，プログラムから3Dモデルを作成するソフトウェアです．そのためパラメトリックなモデリングが可能です．

OpenSCADについては次のリンクが参考になります．
- [OpenSCAD](https://www.openscad.org/)
- [OpenSCAD User Manual](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual)
- [OpenSCADで始めるプログラマブルな3Dモデリング](https://qiita.com/iwtn/items/b984519ad24ac3c7d7d5)

OpenSCADではOpenSCAD独自のプログラミング言語（OpenSCAD Language）を使ってモデリングを行います．しかし，実際に使ってみるとわかるのですが，この言語はあまり使いやすいとはいえません．

もしこれがPythonとかでできたら最高なのに...と思って調べたら，[SolidPython](https://github.com/SolidCode/SolidPython#extrude_along_path)というライブラリを見つけました．PythonプログラムからOpenSCADプログラムを生成することで，Pythonでのモデリングを可能にしているようです．

この記事ではOpenSCAD，Python，SolidPythonでそこそこ快適に3Dモデリングするための方法について，調べたり試したことをまとめます．最終的には次の画像のような感じでモデリングしています．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/f6d6724c-2f90-7a44-20ae-13309102fc18.png)

# OpenSCADのインストール
ここでダウンロードできます．
https://www.openscad.org/downloads.html

# SolidPythonのインストール

仮想環境を作ってSolidPythonをインストールします．Numpyもあるとモデリングのときに便利なので一緒にインストールします．
※Python3.8.2，Windows

```
$ python -m venv venv
$ venv\Scripts\activate
$ pip install solidpython numpy
```

~~※今現在，普通にNumpyをインストールするとNumpy 1.19.4がインストールされるのですが，WindowsではNumpy 1.19.4が使えないという問題があります．これを回避するためにNumpy 1.19.3を指定しています．詳しくは[この記事](https://qiita.com/bear_montblanc/items/b4b75dfd77da98076da5)を見てください．~~
→ 現在この問題は発生しません．普通にNumpyをインストールできます．(2021/05/07）

※私はPython3.8で問題なくSolidPythonをインストールできましたが，Python3.9以降ではインストールできないようです．（2021/05/23）

# Pythonで3Dモデリング！
次のようなPythonファイルを作成します．

```py
from solid import *
from solid.utils import *

c = cube(10)

scad_render_to_file(c, "output.scad")
```

これを実行すると，`output.scad`というファイルが生成されます．`.scad`はOpenSCADプログラムのファイル形式です．

`output.scad`をOpenSCADで開くと，右側に１辺10mmの立方体が表示されます．
表示されなければF5キーを押すか，画像の赤く囲ったプレビューボタンを押してみてください．
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/c9005dfa-a791-6398-760d-11d86c465d86.png)

今見ているのはプレビューで，3Dモデルをエクスポートするためにはレンダリングという処理が必要になります．画像の青く囲ったボタンを押すとレンダリングが始まります．

右下のコンソールに

```
Rendering finished.
```
と出てくればレンダリング終了です．

青く囲ったボタンの右にある「STL」と書かれているボタンを押すと，3DモデルをSTLファイルとして保存できます．レンダリングしてからでないと3Dモデルを出力できないので注意です．



これでPythonを使って3Dモデリングできました！

一応プログラムの内容について軽く説明します．

```py
from solid import *
from solid.utils import *
```
ここでSolidPythonをインポートしています．`import *`を使うことで，OpenSCADと近い感覚でプログラムが書けます．

```py
c = cube(10)
```
ここでは10mmの立方体を作って`c`に代入しています．当たり前な感じがしますが，OpenSCAD言語では変数に対して3Dモデルを代入することができなかったのでこれは大きな変化です．3Dモデルをオブジェクトとして扱えることで，より直感的にプログラムが書けるようになります．

```py
scad_render_to_file(c, "output.scad")
```
最後に作成した3DモデルをOpenSCADプログラムに変換します．

ここで注意が必要なのは，SolidPythonはあくまでも「Pythonが行ったモデリングの操作をOpenSCADプログラムに変換しているだけ」ということです．
Pythonプログラムがそれほど複雑に見えなくても，それをOpenSCADプログラムに変換したときにとんでもないサイズになってしまうことがあります．プレビューに時間がかかったり，3DモデルをSTLで出力しようとしてもレンダリングが永遠に終わらなかったり，OpenSCADが落ちたりします．

# もっと快適に
一応PythonでOpenSCADを使ってモデリングできるようになりましたが，上で説明した方法だと毎回OpenSCADプログラムを生成してOpenSCADで開いてプレビューする必要があってめんどくさいです．OpenSCADの設定をいじってそこそこ快適にします．

「Design」の中の「Automatic Reload and Preview」をチェックします．これでOpenSCADプログラムに変更が加えられたときに自動でプレビューしてくれます．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/5bd1844c-453f-bb23-8237-c2b818bb45af.png)

次に「View」を見ます．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/625d09d3-110c-7a74-d38b-a2a74826776a.png)
一番下の４つのオプションにチェックを入れることでエディターやコンソールなどを非表示にできます．OpenSCADはプレビュー用に使いたいのでEditorは非表示にします．Customizerもここでは使わない（たぶん使えない）ので非表示です．それ以外は好みです．

これでOpenSCADでプレビューだけを見れるようにできました．VSCodeとならべるとこんな感じになります．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/67bbfd1d-0a80-b9eb-bef3-73f8971a3f8b.png)
例えばOpenSCADで`output.scad`を開いた状態で，Pythonを実行して`output.scad`に変更を加えると，右のOpenSCADのプレビューに表示されます．

毎回`scad_render_to_file`で`output.scad`に書き込むように注意しないといけませんが，Pythonを実行してすぐにプレビューが見れるのでそこそこ快適です．

さらに次のようなテンプレを作って`make_model`関数内でモデリングするようにしました．

```py
from solid import *
from solid.utils import *

def make_model():
    c = cube(5)
    
    return c

if __name__ == "__main__":
    scad_render_to_file(make_model(), "output.scad")
```

# Jupyter Notebook でモデリング
紹介するだけに留めますが，[ViewSCAD](https://github.com/nickc92/ViewSCAD)というPythonライブラリがあります．これを使うとJupyter Notebook内でプレビューできるようになります．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/4c81b125-ec70-ba5a-5852-a1017fbdd223.png)

# SolidPython入門
OpenSCAD言語の`if`や`for`などの基本的な制御はPythonを使って置き換えられますが，モデリングに関する操作はOpenSCADから引き継がれています．なのでOpenSCADの機能の多くは同じような使い方でPythonでも使えます．

ここではとりあえずこれだけ知っていればなんとかなる（？）ところだけ説明します．詳しくは[OpenSCAD User Manual](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual)を見てください．

## [3D Primitives](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Primitive_Solids)
[3D Primitives](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Primitive_Solids)とは，例えばキューブ（cube）や球（sphere），円筒（cylinder）などの基本的な3Dモデルのことを指します．引数を与えることである程度自由に形を決めることができます．これらを次に説明するTransformやBoolean Combinationと組み合わせて使って好きな形を作っていきます．

## [Transform](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Transformations)
[Transform](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Transformations)を使うと，3Dモデルの位置や形状，色などを変えることができます．

中でも頻繁に使うのは`translate`，`rotate`です．言葉で説明するよりも例を見たほうがわかりやすいです．

```py
from solid import *
from solid.utils import *

def make_model():
    
    # 1mmの立方体
    c1 = cube(1)
    
    # c1をx方向に3mm移動
    c1_t = translate([3, 0, 0])(c1)
    
    # c1をz軸で45°回転
    c1_r = rotate([0, 0, 45])(c1)
    
    # c1，c1_t，c1_rをまとめて返す
    return c1+c1_t+c1_r

if __name__ == "__main__":
    scad_render_to_file(make_model(), "output.scad", include_orig_code=False)
```
実行結果：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/792386fd-cde1-4050-6346-1ef44834b569.png)

`translate`と`rotate`のどちらの場合も，最初のカッコ内で移動や回転を指定して，次のカッコ内にそれを適用するオブジェクトを指定しています．

`return`の行の`c1 + c1_t + c1_r`という式では，3つの3Dオブジェクト`c1`，`c1_t`，`c1_r`を結合して1つの3Dオブジェクトにまとめています．これは`scad_render_to_file`関数が1つの3Dオブジェクトしか受け取らないためです．

結合と言っても，結合する3Dオブジェクトが必ずしも重なってる必要はありません．上の例のように空間的に離れていても，結合することで1つの3Dオブジェクトとして扱うことができます．

ちなみに`scad_render_to_file`の引数の`include_orig_code`は，生成されるOpenSCADプログラムのコメントとして「元のPythonプログラム」を含めるかどうかを指定できます．ただPythonプログラムに日本語のコメントがあると，SolidPythonで日本語を処理しようとしてエラーが出るので`include_orig_code=False`としています．

## [Boolean Combination](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/CSG_Modelling)
[Boolean Combination](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/CSG_Modelling)では，3Dオブジェクト同士で足し算や引き算ができます．User Manualにある次の画像がわかりやすいです．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/51571d49-b045-7ebe-acdc-9540dfcc4882.png)

演算には`union`（和），`difference`（差），`intersection`（共通部分）の３つがありますが，SolidPythonではこれらの演算をPythonの演算子`+`，`-`，`*`でできるようになっています．
https://github.com/SolidCode/SolidPython#basic-operators

```py
from solid import *
from solid.utils import *

def make_model():
    
    c = cube(4, center=True)
    cy = cylinder(1.5, 6, segments=30, center=True)
    
    return c - (cy + rotate([90, 0, 0])(cy) + rotate([0, 90, 0])(cy))

if __name__ == "__main__":
    scad_render_to_file(make_model(), "output.scad", include_orig_code=False)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/79c5e1b0-b068-3ee4-c1bd-c7d95bf1bd7c.png)

`center`という引数を`True`にすると，原点が中心になるようにして3Dオブジェクト作成できます．また`segments`という引数で曲線部分の滑らかさを指定できます．

`+`演算子で3Dオブジェクトを結合できることを利用すると，`sum([obj1, obj2, ...])`というふうにして3Dオブジェクトを結合できて面白いです．


## [2D Primitives](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/2D_Primitives)
Fusion360やSolidWorksなどの3DCADを使ったことがあれば，平面に図形を書いてからそれを押し出すことで3Dオブジェクトを作るということに馴染みがあると思います．

OpenSCADでも[2D Primitives](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/2D_Primitives)を使うことで平面図形を作成することができます．

`square`，`circle`，`polygon`の３つがあり，その中でも一番使うのが`polygon`だと思います．`polygon`は引数に２次元の点のリストを与えると，順番につないで2Dオブジェクトを作ってくれます．次の例では`polygon`で六角形を作っています．

```py
from solid import *
from solid.utils import *
import numpy as np

def make_model():
    hexagon_points = np.array([[1, 0],
                    [0.5, -0.866],
                    [-0.5, -0.866],
                    [-1, 0],
                    [-0.5, 0.866],
                    [0.5, 0.866]])
    
    hexagon_points *= 10    
    
    hexagon = polygon(hexagon_points)
    
    return hexagon

if __name__ == "__main__":
    scad_render_to_file(make_model(), "output.scad", include_orig_code=False)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/fe85bb43-028b-de6b-2df3-51c43108ae8f.png)

プレビューをよく見ると，六角形に厚みがあって2Dオブジェクトのはずなのに3Dオブジェクトのように見えます．これは「2Dオブジェクトをプレビュー表示する場合，xy平面にある厚さ1mmの3Dオブジェクトとして表示する」というOpenSCADの仕様のためです．

レンダリングしてみると2Dオブジェクトであることがはっきりとわかります．上のメニューの「File > Export」からSVGなどの2D形式で出力することができます．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/b1fca483-c433-f115-7660-886a9eb8e248.png)

## [Extrude](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/2D_to_3D_Extrusion)
2Dオブジェクトができたら，それを押し出し（Extrude）て3Dオブジェクトにします．

`linear_extrude`を使った例です．（あまり押し出していませんが）

```py
from solid import *
from solid.utils import *
import numpy as np

def make_model():
    hexagon_points = [[1, 0],
                    [0.5, -0.866],
                    [-0.5, -0.866],
                    [-1, 0],
                    [-0.5, 0.866],
                    [0.5, 0.866]]
    
    hexagon = polygon(hexagon_points)
    
    hex_col = sum([translate([0, 2*i])(hexagon) for i in range(5)])
    hex_tile = sum([translate([1.73*i, 1*(i%2)])(hex_col) for i in range(5)])
    
    hex_outline = square([7, 7]) - hex_tile
    
    return color("yellow")(linear_extrude(0.1)(hex_outline))

if __name__=="__main__":
    o = make_model()
    scad_render_to_file(o, "output.scad")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/981d043d-224d-b1a0-c65f-b8cec658029c.png)

# おわりに
お手軽にそこそこ快適にPythonとOpenSCADで3Dモデリングできるようになりました．Pythonの豊富なライブラリと組み合わせれば面白いものがたくさん作れそうで楽しみです．