有限要素法シミュレーションの結果をBlenderでモデル化できるようにした

# はじめに

有限要素法というものがあります。
企業の設計の紹介とかで見るカラフルな色がついてどこに力がかかっているかわかるやつです。
それを見るツールはあるのですが、これもしかしたらBlenderでも見れるのでは,
と思い、プログラムを作りました。
幸いBlenderはPythonでの操作に対応しており、以下のようにできました。
今回はそのプログラムについて解説していきたいと思います。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/3580de74-7908-2cea-3f13-c05b2ac33a73.png)


入力ファイル群とプログラムは以下に置いておきます。


https://github.com/yuki-2000/Blender_FEM


# Blenderの準備

Blenderは無料で使えオープンソースで開発されている高機能3Dモデリングソフトです。

https://www.blender.org/

使い方に関しては、解説書を公開している方がいましたので、そちらで勉強しました。

https://forest.watch.impress.co.jp/docs/news/1302036.html

http://web.wakayama-u.ac.jp/~tokoi/cgpe2020.html

私はダウンロードのしやすさからSteamからBlenderをダウンロードしています。

https://store.steampowered.com/app/365670/Blender/?l=japanese



# ライブラリのインストール

Blenderには独自のPythonが入っており、普段使用しているAnaconda環境度は別に、Blender内で使用するライブラリをインストールする必要があります。
初期のBlenderにはライブラリがあまりインストールされていません。
そこで以下のサイトを参考に今回必要な`matplotlib`をインストールします。

https://www.kkaneko.jp/db/cg/bpypip.html


今回、私はGドライブ直下に`Steam`フォルダがあり、そこにソフトをダウンロードしています。
上記サイトを参考にすると、以下の位置にpython.exeが見つかりました。
（3.1の部分はBlenderのバージョンなので読み替えてください。）

```
G:\Steam\steamapps\common\Blender\3.1\python\bin\python.exe
```




そして順番に、コマンドプロンプトで以下を実行します。
上から、

* pipの有効化
* pipのアップグレード
* インストール済みライブラリ一覧の表示


となっています。

```
G:\Steam\steamapps\common\Blender\3.2\python\bin\python.exe -m ensurepip
G:\Steam\steamapps\common\Blender\3.2\python\bin\python.exe -m pip install --user --upgrade pip
G:\Steam\steamapps\common\Blender\3.2\python\bin\python.exe -m pip list
```


`pip list`で調べると、以下のように最低限しか入っていないことがわかります。

```
Package            Version
------------------ ---------
autopep8           1.6.0
certifi            2021.10.8
charset-normalizer 2.0.10
Cython             0.29.26
idna               3.3
numpy              1.22.0
pip                22.1.2
pycodestyle        2.8.0
requests           2.27.1
setuptools         58.1.0
toml               0.10.2
urllib3            1.26.8
zstandard          0.16.0
```


そして、今回のプログラムで必要な`matplotlib`をダウンロードします。


```
G:\Steam\steamapps\common\Blender\3.2\python\bin\python.exe -m pip install --user -U matplotlib
```


### バージョンを変えてしまった時の注意

SteamでBlenderをアップデートすると、インストールしたライブラリが使えなくなってしまいます。
あたらしいバージョンのほうでライブラリを再インストールしようとして、なぜか古いバージョンのフォルダにダウンロードされ、ライブラリが新しいほうに入らなくなるので注意です。

解決法としては、無理やりですが、
バージョンが3.1から3.2になったときは、

```
G:\Steam\steamapps\common\Blender\3.1\python\lib\site-packages
```

の中身を

```
G:\Steam\steamapps\common\Blender\3.2\python\lib\site-packages
```

にコピーすることで解決します。





# BlenderのPythonでの操作


`Blender Python`と検索すると色々やり方は出てきますが、一応説明します。






https://zenn.dev/hotcocoa/articles/5c5ab06c40862b

https://www.kkaneko.jp/db/cg/bpy.html

http://kutodatabase.com/kuto1970.shop/Works_BPYNyumon.html

https://styly.cc/ja/tips/data-visualization-with-python-and-blender/


まずBlenderを開いたら、上のほうにあるタブから、`Scripting`を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/00c74164-8bde-02f0-6a8a-026baaeaf803.png)


出てきた画面で左下にあるのが`IPythonシェル`で、対話モードでPythonを実行することにできます。
右上のところからは、普段の`.pyファイル`から実行することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/24fd68a8-90cd-4fdb-7e57-58bc94be3a36.png)

今回は、右上の部分から新規作成し、プログラムをコピペして三角矢印を押して実行します。

画像で出てくるタブが見つからないときは、隠れてしまっている可能性があるので、タブをマウスのホイールでクリックしながら左に動かして出しましょう。




# データの用意



今回は、**3次元8節点六面体要素** の可視化をしていきたいと思います。

2次元や3次元四面体要素、2次要素などいろいろありますが、今回紹介する方法を使用すればご自分でも実装できるかもしれません。


![yk_fem4_5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/54ccb017-5750-c559-c2b8-b002cdb13e01.jpeg)
>https://monoist.itmedia.co.jp/mn/articles/0909/02/news100_2.html



今回必要なファイルは、

* 節点番号、座標ファイル
* 要素番号、コネクティビティファイル
* 要素値ファイル

の3つです。


順番に解説していきます。




## 節点番号、座標ファイル

文字通り、節点の番号、座標（xyz）を記入したファイルです。
以下のように、各行に順番に節点の情報を書いていきます。

| 節点番号 | x座標 | y座標 | z座標 |
|:-:|:-:|:-:|:-:|
| 1  | 0  | 0  | 0 |
| 2  | 0.1  | 4  | 0 |
| 3  | 2  | 0. 05 | 2  |

ファイル形式は、Pythonで読み込んで配列に変換出来たらなんでもいいですが、
今回はスペース区切りのファイルにします。



## 要素番号、コネクティビティファイル

各要素を構成する節点番号をまとめたファイルです。


わかりやすく説明するため、2次元3節点要素で解説します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/4e9d51a2-7d56-01e0-2841-6b18a9625a1d.png)
>https://rhinohattan.com/python%E3%81%A7%E4%BD%9C%E3%82%8B%E6%9C%89%E9%99%90%E8%A6%81%E7%B4%A0%E6%B3%95%E4%B8%89%E8%A7%92%E5%BD%A2%E4%B8%80%E6%AC%A1%E8%A6%81%E7%B4%A0%E7%B7%A8/

上記のようなメッシュを切った場合、
要素0は0,1,4
要素1は1,2,3
要素2は1,3,4
要素3は0,4,5
という節点番号から構成されています。
これをまとめます。


今回の**3次元8節点六面体要素** では、要素の形は6面体で、節点8個から成り立っています。
そしてその要素を構成する節点をまとめたファイルです。

以下のように、要素番号、構成節点番号を順番に書いたものを1行として書いていっています。

| 要素番号 | 構成節点番号1 | 構成節点番号2 | 構成節点番号3 | 構成節点番号4 | 構成節点番号5 | 構成節点番号6 | 構成節点番号7 | 構成節点番号8 |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| 1  | 1  | 4  | 5  | 2  | 8  | 11  | 25  | 78  |
| 2  | 2  | 8  | 1  | 3  | 72  | 52  | 44  |  25 |
| 3  | 10  |  5 |  7 | 50  |  43 | 20 | 1  | 4  |





ファイル形式は、Pythonで読み込んで配列に変換出来たらなんでもいいですが、
今回はカンマ区切りのファイルにします。



## 要素値ファイル



メッシュのみを可視化するなら、上記2つの節点、要素だけでできます。
しかし、有限要素法では、よく要素における値（応力やひずみなど）を持っています。
今回は、この要素値によって色を付けたいと思います。
節点値を持っていることもありますが、今回は対応していないです。

ファイルは以下のように、要素番号、その要素における値、をまとめたものです。


| 要素番号 | 値 |
|:-:|:-:|
| 1  | 2.35  |
| 2  | 5.2  |
| 3  | 2.2  |

ファイル形式は、Pythonで読み込んで配列に変換出来たらなんでもいいですが、
今回はスペース区切りのファイルにします。













# Blenderでの表示プログラム

いよいよ、blenderを使用して可視化をしていきます。
まず完成したプログラムが以下で、ここについて解説していきます。



### 全プログラム

```python
import numpy as np
import matplotlib.pyplot as plt
import csv  




#条件　ファイルから読み込んでもよい

#節点数
num_node  = 1331
#要素数
num_eleme = 1000
#表示倍率
amp = 10


node        = np.empty((num_node,3), dtype=np.float64) #節点座標
eleme       = np.empty((num_eleme,8),dtype=np.int32) #各要素のコネクティビティ #つまりある六面体elementを構成する接点node番号(1スタートに注意)
eleme_value = np.empty((num_node), dtype=np.float64)  #要素における値　これで色を付ける  



#節点番号、座標ファイルの読み込み   
with open('H:/programing/blender/output_disp2.dat') as f: 
    reader = csv.reader(f, delimiter=' ')
    l = [row for row in reader]
    
    for i in range(num_node):
        node[i,0] = l[i][1].replace('d','e')
        node[i,1] = l[i][2].replace('d','e')
        node[i,2] = l[i][3].replace('d','e')


#要素番号、コネクティビティファイルの読み込み  
with open('H:/programing/blender/input_eleme.txt') as f:
    reader = csv.reader(f)
    l = [row for row in reader]

    for i in range(num_eleme):
        eleme[i] = l[i][1:9]

        
#要素値ファイルの読み込み        
with open('H:/programing/blender/output_ave_strain2.dat') as f:   
    reader = csv.reader(f, delimiter=' ')    
    #reader = csv.reader(f)
    l = [row for row in reader]

    for i in range(num_eleme):
        eleme_value[i] = l[i][1].replace('d','e')
    

        
#正規化する関数
#https://deepage.net/features/numpy-normalize.html
def min_max(x, axis=None):
    min = x.min(axis=axis, keepdims=True)
    max = x.max(axis=axis, keepdims=True)
    result = (x-min)/(max-min)
    return result
        
#0から1に正規化した要素値を0から255に変更        
eleme_value = min_max(eleme_value, axis=None) * 255
#要素の値からmatplotlibのカラーマップ機能を使用して(R, G, B, A)に変換
cm_color = [plt.cm.jet(int(value)) for value in eleme_value]        
        
 
import bpy     



for item in bpy.data.meshes:
    bpy.data.meshes.remove(item)
for item in bpy.data.materials:
    bpy.data.materials.remove(item)









verts = np.zeros((8,3), dtype=np.float64) #ある六面体要素を構成する6接点のxyz座標
for i in range(num_eleme):
    for j in range(8): #節点の8
        #eleme[i,j]つまり要素値が1始まりなので0始まりのPython配列に合わせて-1している
        verts[j,0] = node[eleme[i,j]-1,0]
        verts[j,1] = node[eleme[i,j]-1,1]
        verts[j,2] = node[eleme[i,j]-1,2]
    
    #表示倍率を適用    
    verts *= amp
    #面を構成する節点の場所
    faces = [[0,1,2,3], [0,4,5,1], [1,5,6,2], [2,6,7,3], [0,3,7,4], [4,7,6,5]]
    
    #メッシュの作成
    msh = bpy.data.meshes.new(name=f"cubemesh{i}")
    msh.from_pydata(verts, [], faces)
    msh.update()

    #オブジェクトの作成
    obj = bpy.data.objects.new(name=f"cube{i}", object_data=msh)
    scene = bpy.context.scene
    scene.collection.objects.link(obj)
    
    #マテリアルの作成
    material = bpy.data.materials.new(f"cubematerial{i}")
    obj.data.materials.append(material)        
    material.use_nodes = True    

    #マテリアルの設定
    color = cm_color[i]
    p_BSDF = material.node_tree.nodes["Principled BSDF"]
    p_BSDF.inputs[0].default_value = color
    p_BSDF.inputs[9].default_value = 0
    p_BSDF.inputs[17].default_value = 1


#環境光で全体を明るく
#環境色を(R, G, B, A)で指定、今回は白
bpy.data.worlds["World"].node_tree.nodes["Background"].inputs[0].default_value = (1, 1, 1, 1)
#強さ
bpy.data.worlds["World"].node_tree.nodes["Background"].inputs[1].default_value = 1
```



## 条件

プログラムを動かすのにあたリ、節点数、要素数、表示倍率
が必要です。
これがなくても動くようなプログラムを作ることもできそうですが、有限要素法を使っているならあるはずなので、今回は使用します。

外部から読み込むプログラムを作ることも可能です。


```python
#条件　ファイルから読み込んでもよい

#節点数
num_node  = 1331
#要素数
num_eleme = 1000
#表示倍率
amp = 10
```




## データの読み込み


先ほど用意した3ファイルを読み込んでいきます。
blenderのカレントディレクトリは`<built-in function dir>`と出て場所がわからないので、ファイルの場所を**絶対パス**でスラッシュを使ってパスを書きます。
今回は、カンマ区切り/スペース区切りのファイルですので、Python標準ライブラリの`csv`ライブラリを使用します。


https://note.nkmk.me/python-csv-reader-writer/



読み込んだものは、`numpy`ライブラリの`ndarray`オブジェクトに入れます。
ちなみにこれはFEMプログラムの名残だったりします。

また、節点座標に関しては、Fortranの倍精度浮動小数点を表した際の`d`を読み込めるように`e`に変えていますが、元からeでも、ただの小数でも、整数でも読み込めます。

また、区切り文字に関しては、カンマ区切りだけでなく、スペース区切りや任意の区切り文字に対応しています。
変更したいときは、`reader = csv.reader(f, delimiter=' ')`というように`delimiter`変数で変更します。

今回は試しとして、節点ファイル、要素値ファイルは`スペース区切り`、要素番号コネクティビティファイルは`カンマ区切り`で用紙して読み込んでみましょう。




```python
node        = np.empty((num_node,3), dtype=np.float64) #節点座標
eleme       = np.empty((num_eleme,8),dtype=np.int32) #各要素のコネクティビティ #つまりある六面体elementを構成する接点node番号(1スタートに注意)
eleme_value = np.empty((num_node), dtype=np.float64)  #要素における値　これで色を付ける  



#節点番号、座標ファイルの読み込み   
with open('H:/programing/blender/output_disp2.dat') as f: 
    #reader = csv.reader(f)      #カンマ区切り用    
    reader = csv.reader(f, delimiter=' ')
    l = [row for row in reader]
    
    for i in range(num_node):
        node[i,0] = l[i][1].replace('d','e')
        node[i,1] = l[i][2].replace('d','e')
        node[i,2] = l[i][3].replace('d','e')


#要素番号、コネクティビティファイルの読み込み  
with open('H:/programing/blender/input_eleme.txt') as f:
    reader = csv.reader(f)
    l = [row for row in reader]

    for i in range(num_eleme):
        eleme[i] = l[i][1:9]

        
#要素値ファイルの読み込み        
with open('H:/programing/blender/output_ave_strain2.dat') as f:   
    reader = csv.reader(f, delimiter=' ')    
    #reader = csv.reader(f)
    l = [row for row in reader]

    for i in range(num_eleme):
        eleme_value[i] = l[i][1].replace('d','e')
    

        
#正規化する関数
#https://deepage.net/features/numpy-normalize.html
def min_max(x, axis=None):
    min = x.min(axis=axis, keepdims=True)
    max = x.max(axis=axis, keepdims=True)
    result = (x-min)/(max-min)
    return result
        
#0から1に正規化した要素値を0から255に変更        
eleme_value = min_max(eleme_value, axis=None) * 255
#要素の値からmatplotlibのカラーバー機能を使用して色に変換
cm_color = [plt.cm.jet(int(value)) for value in eleme_value]     
```


## 色の作成


要素の値からグラデーションの色を作成します。
今回はmatplotlibのカラーマップを使用して要素の値から(R, G, B, A)に変換します。
カラーマップでは0から255の`整数`に応じて色が返ってくるため、0から1に正規化したのちに0から255の整数に変換します。

https://matplotlib.org/stable/tutorials/colors/colormaps.html

https://deepage.net/features/numpy-normalize.html


```python
#正規化する関数
def min_max(x, axis=None):
    min = x.min(axis=axis, keepdims=True)
    max = x.max(axis=axis, keepdims=True)
    result = (x-min)/(max-min)
    return result
        
#0から1に正規化した要素値を0から255に変更        
eleme_value = min_max(eleme_value, axis=None) * 255
#要素の値からmatplotlibのカラーマップ機能を使用して色に変換
cm_color = [plt.cm.jet(int(value)) for value in eleme_value]   
```



## もとからあるオブジェクトを削除

Blenderは最初には立方体が置いてあります。
なのでこれを削除しましょう。

```python
for item in bpy.data.meshes:
    bpy.data.meshes.remove(item)
for item in bpy.data.materials:
    bpy.data.materials.remove(item)
```

https://zenn.dev/hotcocoa/articles/5c5ab06c40862b













## メッシュ・オブジェクト・マテリアルの作成

要素ごとに順番にメッシュ・オブジェクト・マテリアルを作成していきます。

先にプログラムを出すと、このようになっています。

```python
verts = np.zeros((8,3), dtype=np.float64) #ある六面体要素を構成する6接点のxyz座標
for i in range(num_eleme):
    for j in range(8): #節点の8
        #eleme[i,j]つまり要素値が1始まりなので0始まりのPython配列に合わせて-1している
        verts[j,0] = node[eleme[i,j]-1,0]
        verts[j,1] = node[eleme[i,j]-1,1]
        verts[j,2] = node[eleme[i,j]-1,2]
    
    #表示倍率を適用    
    verts *= amp
    #面を構成する節点の場所
    faces = [[0,1,2,3], [0,4,5,1], [1,5,6,2], [2,6,7,3], [0,3,7,4], [4,7,6,5]]
    
    #メッシュの作成
    msh = bpy.data.meshes.new(name=f"cubemesh{i}")
    msh.from_pydata(verts, [], faces)
    msh.update()

    #オブジェクトの作成
    obj = bpy.data.objects.new(name=f"cube{i}", object_data=msh)
    scene = bpy.context.scene
    scene.collection.objects.link(obj)
    
    #マテリアルの作成
    material = bpy.data.materials.new(f"cubematerial{i}")
    obj.data.materials.append(material)        
    material.use_nodes = True    

    #マテリアルの設定
    color = cm_color[i]
    p_BSDF = material.node_tree.nodes["Principled BSDF"]
    p_BSDF.inputs[0].default_value = color
    p_BSDF.inputs[9].default_value = 0
    p_BSDF.inputs[17].default_value = 1
```



### メッシュの作成

まず6面体のメッシュを作成しましょう。
メッシュを作成するためには、メッシュを構成する頂点の座標`verts`と、面を構成する頂点をまとめた`faces`が必要です。6面体の節点の順番によっては、立方体が構成されなかったり、面が裏返っていることがあります。その時はうまくやってください。（丸投げですみません。）

https://qiita.com/kenyoshi17/items/b93bbba6451e3c6017e5

https://qiita.com/masterkeaton12/items/3d8245d1f301baf4c126

https://bluebirdofoz.hatenablog.com/entry/2019/12/10/090822



### オブジェクトの作成

オブジェクトを作成します。
オブジェクトの中にメッシュ、マテリアルなどの情報を含むようになります。



### マテリアルの作成

マテリアルを作成し、色、透明度、粗さなどを設定していきます。


https://qiita.com/masterkeaton12/items/21a7ddddb4304622403b

https://suzulang.com/blender-python-set-materials-per-planes/





`p_BSDF.inputs[i]`というのは、画像のようにBlender内の`Shading`にある`プリシンプルBSDF`の左に丸がある部分の上からの0からの番号です。
この番号ですが、どうもBlenderのバージョンによって変わってしまうので、ほかのサイトの記事と違っていたりすることもあるので注意してください。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/37fe0348-58eb-712e-1084-a0de26e8c88c.png)

すなわち、今回は
`p_BSDF.inputs[0]`で色を決め、
`p_BSDF.inputs[9]`で粗さを0、つまりつるつるにし、
`p_BSDF.inputs[17]`で伝搬を1に。つまり透明にしています。





## ライティング

光源については触らず、環境光で全体を明るくします。

https://blender3d.biz/knowledge_rendering_letsrender_ambientlight.html


```python
#環境光で全体を明るく
#環境色を(R, G, B, A)で指定、今回は白
bpy.data.worlds["World"].node_tree.nodes["Background"].inputs[0].default_value = (1, 1, 1, 1)
#強さ
bpy.data.worlds["World"].node_tree.nodes["Background"].inputs[1].default_value = 1
```








# 観察

ちゃんと色がついているか観察してみましょう。
`Shading`から見てもいいですが、今回は、`Layout`タブ内で見ていきます。

右上の丸いマークのうち、右から2番目を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/89878d4f-3437-2e3a-1763-83cae4f43a5f.png)


さらに右の下矢印をクリックすれば環境を変えることもできます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/fab8ff4a-a010-4c29-26f8-f641c638a447.png)


結果がこちらです。
ちゃんと色がついていますね


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/993303/041bf017-2cd7-7c32-2348-217abe0e641b.png)



https://www.kkaneko.jp/db/cg/shadereditor.html


# まとめ

以上のプログラムで、Blender内で有限要素法の結果を表示することができるようになりました。Blender内で観察するもよし、エクスポートするもよしです。
BlenderをPythonで操作するというのがそもそもニッチな世界で、解説記事を書いてくださった先人には感謝しかありません。

今後としては、アニメーションもできるようにしたり、任意の位置でスライスして中身を見るようにできるようにしてみたいです。



# 参考




http://kutodatabase.com/kuto1970.shop/Works_ImportVerts.html#2


https://qiita.com/tatsuruM/items/9d4222c6c7d96b4c5b3c

https://katsumi3.hatenablog.com/entry/2020/01/11/171615



# 有限要素法関連の記事

https://qiita.com/yuki_2020/items/87ce1e97772876f1b157

https://qiita.com/yuki_2020/items/1ba48481b930fc3998fc

https://qiita.com/yuki_2020/items/9dcfd88c8909300cc9a1

https://qiita.com/yuki_2020/items/1b603561bb3ff779ad58

https://qiita.com/yuki_2020/items/4a741014be1721e6e968