はじめに
Blender 2.80以降のバージョンはそれ以前のバージョンと比べていろいろと変わったと巷で噂になっていますが、Blenderが内蔵しているPython3を使ってトーラスをモデリングし、ついでに動画を作ってみることにしました。
Blender 2.92.0のインストール
今回のモデリングはWindows 10にBlender 2.92.0をインストールして作業していきます。
さっそく、Blender 2.92.0をインストールします… といきたいところですが、インストールの前にそれまで使っていたBlenderの古いバージョンはスパッとアンインストールします。
古いバージョンのアンインストールが終わったら、本家BlenderのサイトからBlender 2.92.0をダウンロードします。
インストールはWindows 10のしきたりに則って行います。
トーラスとは?
作業を開始する前にここでトーラスについて簡単におさらいしておきます。
なお、以下の座標系についての記述は特に明示しない限りグローバル座標系によるものとします。
ここで、
こんな感じの円です↓
この円を
スポンサーリンク
と表せるので、その回転体であるトーラスは(
と表すことができます。
割と簡単な式になりますね。
Python3のプログラムを書きます。
トーラスが回転体であることを理解したところで、その回転角
なお、Blender 2.92.0に同梱されているPython3のバージョンは3.7.7のようです。
プログラムの全貌
Python3のプログラム及び動画作成用のバッチファイルはGitHubのリポジトリに置きました。
Python3のプログラムは以下の通りになります。
BlenderをPython3から操作する場合にはbpyモジュールというPython3のモジュールを使用しますが、bpyモジュールで使用できるAPIの仕様が2.79以前のものと2.80以降では一部変わっているようです(↑のプログラムは2.92.0でのみ動作確認を行っています)。
逐語訳のようなもの
前節のプログラムの概要を説明していきます。
10行目: 画像ファイルの出力先ディレクトリの設定
Blenderのテキストエディタから前節のプログラムを読み込んで実行した際に出力される画像ファイルの出力先のディレクトリが指定できないので、ここで無理矢理設定します。
13-15行目: 配置済みのオブジェクトをすべて消す
配置済みのオブジェクト(デフォルトの状態でBlenderを起動したときにすでに置いてある立方体を含みます。)をすべて消すための関数です。
18-24行目: 円を描く
以下のような処理の流れになっています。
- いったん
平面に円を描きます(20行目)。 軸回りに 回転させます(22行目)。- 原点を3Dカーソル(このプログラムでは3Dカーソルを移動させないので、グローバル座標系の原点に3Dカーソルがあります。)に移動させます(23行目)。
軸回りに回転させます(24行目)。
回転させるときにはfill_typeの設定が反映されないようなので、回転角が
27-54行目: トーラスを作る
トーラスを作ります。
- 円を回転させて「曲がった土管」の部分を作ります(28-41行目)。
- 円を2個追加します(43-45行目)。
- 手順2で追加した円を土管にフタができる位置まで回転します(47-48行目)。
- オブジェクトをメッシュに変換します(50-52行目)。
- 「曲がった土管」及びフタを1個のオブジェクトに変換します(54行目)。
コメントにも簡単に書きましたが、プリファレンスの「翻訳」の設定で「日本語」を指定した場合で、モディファイアの設定を行う際には、bpy.context.object.modifiersでアクセスできるdictのキー名にモディファイアの日本語名を設定する必要があるようです。
57-63行目: 色の指定
生成されるトーラスごとにその表面の色を指定します。
自然な感じの色が生成できるような実装としています。
66-87行目: 画像をサクサクと生成する
トーラスの中心角を変えながらレンダリングを行い、PNGフォーマットの連番画像を作成します。
90-92行目: レンダリングした画像を1枚だけ作る関数
テスト用の関数です。
ちょっと脱線: 途中までなら動くやつ
前節の関数を作る際にBlenderの3Dビューで様子を見ながらプログラムを書くのですが…
BMeshをつかって土管を作成するところまで作ってみたプログラムはこちらになります↓
#!/usr/bin/env python3 | |
# | |
# See https://pandanote.info/?p=7456 | |
# | |
import bpy | |
import bmesh | |
import math | |
def verts_to_ngon(edgeslist, verts): | |
converted = [] | |
verts.append(verts[0]) | |
pos = 0 | |
while pos < len(verts)-1: | |
vs = verts[pos] | |
ve = verts[pos+1] | |
for edge in edgeslist: | |
if (edge.verts[0] == vs and edge.verts[1] == ve) \ | |
or (edge.verts[0] == ve and edge.verts[1] == vs): | |
converted.append(edge) | |
pos = pos + 1 | |
return converted | |
for item in bpy.data.meshes: | |
bpy.data.meshes.remove(item) | |
bpy.ops.mesh.primitive_circle_add(location=(0.9, 0, 0), radius=0.3, | |
fill_type='NGON') | |
bpy.ops.transform.rotate(value=math.pi/2, orient_axis='X') | |
bpy.ops.object.origin_set(type='ORIGIN_CURSOR') | |
bpy.ops.object.modifier_add(type='SCREW') | |
scr = bpy.context.object.modifiers["スクリュー"] | |
scr.angle = math.pi*0.33333 | |
scr.axis = 'Y' | |
bpy.ops.object.convert(target='MESH') | |
bpy.ops.object.mode_set(mode='EDIT') | |
bm = bmesh.from_edit_mesh(bpy.context.object.data) | |
bm.verts.ensure_lookup_table() | |
bm.edges.ensure_lookup_table() | |
ccount = {} | |
for edge in bm.edges: | |
edge.verts[0].select = False | |
edge.verts[1].select = False | |
edge.select = False | |
if edge.verts[0] not in ccount: | |
ccount[edge.verts[0]] = [] | |
ccount[edge.verts[0]].append(edge.verts[1]) | |
if edge.verts[1] not in ccount: | |
ccount[edge.verts[1]] = [] | |
ccount[edge.verts[1]].append(edge.verts[0]) | |
candidatelist = {} | |
for (k, v) in ccount.items(): | |
if len(v) <= 3: | |
candidatelist[k] = v | |
cap_a = list(candidatelist.keys()) | |
cap_b = [] | |
isDone = False | |
cap_b.append(cap_a.pop()) | |
current = cap_b[0] | |
while isDone is False: | |
next = None | |
for vv in candidatelist[current]: | |
if vv in cap_a: | |
next = vv | |
if next is not None: | |
cap_b.append(next) | |
cap_a.remove(next) | |
current = next | |
else: | |
isDone = True | |
edge_a = verts_to_ngon(bm.edges, cap_a) | |
edge_aa = [] | |
lq = int(len(edge_a)/4) | |
lh = int(len(edge_a)/2) | |
for i in range(lq): | |
edge_a[i].select = True | |
edge_aa.append(edge_a[i]) | |
for i in range(lq): | |
edge_a[i+lh].select = True | |
edge_aa.append(edge_a[i+lh]) | |
# bmesh.ops.grid_fill(bm, edges=edge_aa) |
↑のプログラムをBlenderのテキストエディタに読み込ませて実行させると、↓のような曲がった土管みたいな立体ができます。
これでフタをすれば完成かな… と思い、↑のプログラムの94行目のコメントを外してBlenderのテキストエディタに読み込ませて実行させてから3Dビューの画面に切り替えると、Blenderが異常終了してしまいます(試しにLinux (Fedora 33)でも実行してみましたが、core dumpします。)…
実は、BlenderのGUIからは「面」→「グリッドフィル」を選択すると…
のようにフタを作ることができたりします。
なんでなんですかね。(´・ω・`)
動画にします。
動画の作成
ちょっと話が脇道に逸れましたが、前々節のプログラムで大量に作成したPNGファイルをffmpegでまとめて動画にします。
動画をまとめるためのバッチファイルは↓のような感じです。
動作確認
出来上がった動画は↓のような感じになります。
#blender 2.92.0を試しに使っているうちにできてしまった、トーラスが伸びたり縮んだり、表面の色が変化したりするだけの動画。
#lifeinyokohama pic.twitter.com/8Y5xM4ocaO
— pandanote.info (@Pandanote_info) April 18, 2021
まとめ
最初は選挙の開票速報とかで得票率とか獲得議席数を示すときによく使われている扇形の中心角が広がっていくタイプの3次元のパイチャートを作ろうと思っていたところ、「Blenderを使うのだったら、もっとクールな立体にしよう。」と考え、最終的に目をつけたのがトーラスだったってわけです。
Blenderはコマンドラインオプションを使うとGUIを起動せずにレンダリングができます。
この記事に掲載したプログラムについてももう少し汎用性を高めることができるかもしれませんが、その件はまたの機会にいたしとう存じます。(・ω・)
この記事は以上です。