読者です 読者をやめる 読者になる 読者になる
pixiv insideは移転しました! ≫ http://inside.pixiv.blog/

BlenderとPythonで3Dモデルを動的生成してレンダリングする

ピクシブ株式会社 Advent Calendar 2016 - 7日目

本日は新卒エンジニアのhayaがインターネットにつながってなくてもできるお話をします。
具体的には、PythonでBlenderを操作し、入力データにしたがって3Dモデルを生成したり変形したりすることで、任意形状オブジェクトのレンダリングをGUIなしで実行します。

もちろんインターネットにつながっているとなお良く、ものづくりがもっと楽しくなるアイテム制作サービス - pixivFACTORYでは、Blenderと画像変換を駆使して、様々なオリジナルグッスの仕上がりイメージをユーザーに提供しています。

コードで3Dオブジェクトをつくる

基本的に、BlenderのUIで操作できることは後述のPythonコンソールからでも同様の操作を行うことができます。
レンダリング画像の見栄えを良くするために必要な要素はたくさんありますが、この記事ではオブジェクトの生成と形状調整に関連することを紹介します。

まずは一旦、Blenderの画面を見ながら順を追って。

Pythonコンソールを使用する

Blenderを起動した直後は3D Viewが表示されているはずです。
Blenderには様々な作業に特化したエディタ画面が用意されており、画面左下のボタンからエディタタイプ変更できます。

Python Consoleを選択するか、ショートカットShitf+F4でPython Consoleを表示、 戻るときは同様に3D Viewを選択するか、ショートカットShift+F5

Blenderの画面はこのような切り替え可能なエディタの組み合わせで構成されていますが、あらかじめ、いくつかのエディタの組み合わせたプリセットも用意されています。

コンソールを使用する場合、画面上部のレイアウト切り替えボタンからScriptingを選択すると、3D View、Python Consoleに加えて、InfoとText Editorのペインが表示されます。 PCの画面が大きいなら、こちらを使うのが良いかもしれません。

InfoペインにはGUIで操作した際のコマンドなどが流れるので、一度手動で行った作業を自動化する際の参考に。
また、どのレイアウトでも、GUIの各要素にカーソルをのせていれば、コンソール上での操作の仕方が表示されるようになっているので、GUIで自分ができる操作であれば、独学でもコンソールで同様の操作を再現しやすいと思います。

f:id:devpixiv:20161204144410j:plain

コンソール上でCtrl+Spaceを押すと自動補完してくれます。出てくる候補を見るだけでわかる情報も多いので、とにかく補完してみると良いでしょう。

f:id:devpixiv:20161206195115p:plain

※Macの場合、Ctrl+SpaceがSpotlight検索に割り当てられていると思われるので、Autocompleteのボタンを右クリックしてChange Shortcutから好きなキーに割り当て直すと良い。

3Dオブジェクトの生成

コンソールからオブジェクトを生成する方法はたくさんあるので、ここではそのうちいくつかを抜粋して紹介します。

1. 頂点データから任意のポリゴンを生成する

頂点をすべて指定すれば任意の形状を作ることができます。当たり前ですね。
それでも頂点が大量にある場合、これはむしろ手動では実現が難しくなりますから、コードで頂点データを生成してオブジェクトを生成したい場面は、結構あるのではないでしょうか。

例えば、フラクタル図形や数列に基づいた規則的な図形を生成する場合など、GUIではやりたくないような気分になります。

import mathutils
from mathutils import Vector

# 頂点
verts = []
verts.append(Vector((0,0,0)))
verts.append(Vector((1,0,0)))
verts.append(Vector((1,1,0)))
verts.append(Vector((0,1,0)))

# 面を構成する頂点は、vertsのインデックスで指定できる
faces = [[0, 1, 2, 3]]
edges = []

mesh = bpy.data.meshes.new(name='Mesh')
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new('Object', mesh)
obj.location = (0,0,0)
bpy.context.scene.objects.link(obj)

# 面を張らず、辺だけを追加する場合
# 面同様にvretsのインデックスを指定して辺を指定できる
faces = []
edges = [[0,1],[1,2],[2,3],[3,0]]

mesh = bpy.data.meshes.new(name='Mesh')
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new('Object', mesh)
obj.location = (0.5,0.5,0)
bpy.context.scene.objects.link(obj)

上記以外にもいくつかの方法で、頂点データからオブジェクトを生成できます。

参考: Dev:Py/Scripts/Cookbook/Code snippets/Three ways to create objects - BlenderWiki

2. ファイルからインポート

Blenderはいくつかの3Dモデル/シーンファイルのインポートに対応しているので、入力として与えるファイルの形式次第では、1行のコードでレンダリング内容の大半を準備することができます。

bpy.ops.import_ ...

f:id:devpixiv:20161204141916j:plain

SVGから2Dのオブジェクトを生成することもできます。SVGはCurveとしてインポートされるので、場合によってはMeshに変換する必要があります。
なお、Blender内の長さ単位1BUは1mに対応するので、SVGのwidth/heightを設定しておくと、指定したサイズでインポートしてくれます。

<!-- circle.svg -->
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     viewBox="0 0 10 10" width="1000mm" height="1000mm">
  <circle cx="5" cy="5" r="5" fill="#fff" />
</svg>
bpy.ops.import_curve.svg(filepath='circle.svg')

ちなみに、オブジェクト名を指定してインポートしたり、インポートしたオブジェクト名を取得する方法が見つからないため、 インポート前のオブジェクト一覧を記録しておくか、先に存在するオブジェクトを選択不可にしておくなりして、追加したオブジェクトだけを取得できるようにする必要がある。 (何か良い方法はないものか・・・)

# 一旦すべて選択不可能にする
bpy.ops.object.select_all(action='DESELECT')
for o in bpy.context.scene.objects:
  o.hide_select = True

bpy.ops.import_curve.svg(filepath='circle.svg')
# 選択不可能なもの以外すべて=先ほど追加したものだけ選択
bpy.ops.object.select_all(action='SELECT')
obj = bpy.context.selected_objects[0]
bpy.data.objects[obj.name]
obj.select = True
bpy.context.scene.objects.active = obj

# インポートしたオブジェクトに対して何か処理する
# 例えばメッシュに変換
bpy.ops.object.convert(target='MESH')

# すべて選択可能に戻す
for o in bpy.context.scene.objects:
  o.hide_select = False

3. プリミティブから生成

自由度は高くないのですが、基本図形は簡単に生成できます。
ブロックが積み上がっている、謎の球体がたくさん浮かんでいるといった表現、 マテリアルを工夫したプリミティブをたくさん生成して、爆発や魔法エフェクトなどの用途には使えそうだとか、 簡単に思い浮かぶ程度には実用性がありそうです。

bpy.ops.mesh.primitive_ ...

f:id:devpixiv:20161204150336j:plain

オブジェクトの変形

ここまでは、新しいオブジェクトを生成する方法を紹介してきましたが、ここからは既存のオブジェクトを変形して任意形状を作る方法を紹介します。
新しいオブジェクトを生成する場合も、オブジェクトの移動/回転を行うことは多いでしょう。

1. 移動・回転・拡大縮小

生成したオブジェクトを移動・回転・拡大縮小する。
Blender上では角度をラジアンで扱うことが多いので注意。

import math

obj = bpy.data.objects['Cube']
obj.location.x += 1
obj.rotation_euler = (math.radians(15), math.radians(30), math.radians(45))
obj.scale.x = 2

2. モディファイヤーで変形する

工夫次第ですが、モディファイヤーでできることはかなり多く、少しのパラメータ設定でも表現の幅を大きく広げることができます。
これといった工夫をしない場合でも、自動で生成したオブジェクトにSubsurf Divisionを適用するなど、わりと使用する印象。

bpy.ops.mesh.primitive_uv_sphere_add()
obj = bpy.context.selected_objects[0]

mods = obj.modifiers
mod = mods.new(name='subsurf', type='SUBSURF')
mod.render_levels = 4
mod.levels = 4 # 3D View用

f:id:devpixiv:20161204145536j:plain

3. シェイプキーで変形する

使いどころは限られてきますが、シェイプキーを使った変形も自動化に向いていると思います。
主にアニメーションの作成に使用する機能ですが、複数の頂点の移動からなるオブジェクトの移動・変形を、1つのパラメータを介して滑らかに調整できるため、 少ないパラメータでもなかなか複雑な形状を作ることができます。複数のシェイプキーを設定すれば、かなり複雑な変形も実現可能。

動的にシェイプキーを設定しようとすると変形前後の形状に関する頂点データなどを与える必要があり、わざわざシェイプキーを介する必要性が薄くなるので、 モデルにシェイプキーを設定したものを事前に用意することになるでしょうか。

# Cylinderにシェイプキーが設定されているとして
obj = bpy.data.objects['Cylinder']
# 0番目は通常Baseの形状になる。キー名で指定しても良い
obj.data.shape_keys.key_blocks[1].value = 0.2
obj.data.shape_keys.key_blocks['Key 1'].value = 0.8

f:id:devpixiv:20161206014448g:plain

コマンドラインからBlenderを動かす

BlenderはGUIなしで動作させることが可能です。
Blenderの実行ファイルへパスを通すか、実行ファイルパスを指定してコマンドラインから起動できます。 その際、任意であらかじめ作成しておいたblendファイルを読み込むことができ、 さらにpythonスクリプトを指定すれば、起動後に自動で行う操作を自由に設定できます。
いままで説明した内容を含むコンソール上での操作手順を外部から指定できるわけですから、入力データはもちろん、操作の内容もその都度自由に変更できます。

blender [blendファイル] --background --python [pythonスクリプト]

スクリプトにレンダリングを行う処理を書いておけば、レンダリング結果をファイルに書き出すことができるので、 あとは入力データを準備すれば、コマンド1行でシーンを構築してレンダリング結果を得ることができます。

Blender内で実行するpythonスクリプトに入力データを渡す

常に固定された入力データを使用するということはあまりないでしょうから、 入力データ文字列や入力データファイルのパスをスクリプトの引数に渡すことを考えます。

実行コマンドのオプションを終了したあとに続けて文字列でパラメータを指定する方法と、環境変数で指定する方法があり、 どちらの場合も、スクリプト内でパラメータを受け取る処理を自分で記述します。

オプションを抜けて文字列で渡す
# test_argv.py

import sys

offset = sys.argv.index('--') + 1
print(sys.argv[offset:])
blender --background --python test_argv.py -- args0 args1
環境変数で渡す
# test_env.py

import os
print(os.environ['BLENDER_PARAM'])
BLENDER_PARAM=parameter blender --background --python test_env.py

# Windowsではsetを使用できる
# set BLENDER_PARAM=parameter
# blender --background --python test_env.py

デモンストレーション

個人気に試してみたかったのでシェイプキーによる変形を使ったデモをやってみます。
(3Dプリンタ用データをインポートしてレンダリングするとかだと説明の必要もないですし・・・)

シェイプキーで再現しやすそうな、轆轤回しか旋盤加工っぽい何かで面白いものを探したところ、良さげなのを発見。

Personalized Jewelry. The Original Soundwave Bracelet / Necklace

音声データから柱状のアクセサリーを作ってくれるそうです。
テカテカのステンレス製なら、マテリアルはさほど凝ってなくても見栄えしますね。

準備

モデルはあらかじめ準備しておき、それを変形してお目当ての形状を得る方向で作業します。
(記事の都合上、ここではモデルの編集方法は詳しく説明しません。)

まず、ループカットで細かく輪切りに分割した円柱を作成し、 分割した各円周に対して、シェイプキーを割り当てておきます。
旋盤加工っぽいことをやりたいので、値が1に近いほど削れて細くなるように、 Value=0でそのまま、Value=1で半径がもとの10%になるようなシェイプキーをKey 1 ~ Key 32まで設定してみました。
ここままだとカクカクしているので、Subsurf Divisionモーディファイヤーを追加して滑らかにしておきます。 ワールド、光源、カメラ、マテリアル、レンダリング設定などもお好みで適宜設定。

f:id:devpixiv:20161206122259j:plain

pythonスクリプト

blendファイルとwaveファイルを読み込み、 あらかじめ用意した円柱に対して、wavの振幅をシェイプキーとして割り当ててみます。

# rendering.py

import bpy
import sys
import os
import wave
import numpy as np

def shape_key_values_from_wave(wave_file, step_num):
  data = wave.open(wave_file)
  nchannels, sampwidth, framerate, nframes, comptype, compname = data.getparams()
  step = int(nframes / step_num)
  shape_key_values = []

  for i in range(step_num):
    buff = data.readframes(step)
    buff = np.frombuffer(buff, 'int16')
    # あまり滑らかでないほうが見た目が良かったので、思い切ってざっくりとサンプリングする
    buff = [abs(v) for v in buff[:50]]
    shape_key_values.append(np.average(buff))

  data.close()

  # 正規化して見栄えを良くする
  max_amp = max(shape_key_values)
  return [1.0 - (float(v) / max_amp) for v in shape_key_values]

def main():
  # パラメータを取得する
  offset = sys.argv.index('--') + 1
  wave_file = sys.argv[offset:][0]

  # 開いた.blendファイルのファイル名とディレクトリの取得
  d_name, f_name = os.path.split(bpy.data.filepath)

  # 既存のシーンとオブジェクトを選択する
  scene = bpy.context.scene
  obj = bpy.data.objects['Cylinder']

  # waveからシェイプキーを生成してセットする
  shape_key_values = shape_key_values_from_wave(wave_file, 32)
  for i in range(32):
    obj.data.shape_keys.key_blocks[i+1].value = shape_key_values[i]

  # .blendファイルと同じディレクトリに出力するように設定
  scene.render.filepath = os.path.join(d_name, 'output.png')
  # レンダリングしてファイルに書きだす
  bpy.ops.render.render(write_still=True)

try:
  main()
except Exception:
  print(traceback.format_exc())
  sys.exit(1)

参考:
【python】波形の表示(モノラル・ステレオ)【サウンドプログラミング】 - すこしふしぎ.
PythonでWAVファイルを読み込む - 音楽プログラミングの超入門(仮)
Pythonで音の波形を表示(Wavファイル)

レンダリング

あらかじめ要したblendファイル(wave_cylinder.blend)と、上記pythonスクリプト、適当な音声ファイル(sound.wav)を指定してBlenderを起動すると、自動的にレンダリングが開始されます。
今回し要したスクリプトではblendファイルと同じディレクトリにレンダリング結果が出力されるようになっています。

blender wave_cylinder.blend --background --python rendering.py -- sound.wav

f:id:devpixiv:20161206174328p:plain

入力に使用したwavファイルによって、いい感じの形状になったり、イマイチだったり、ただの棒になったりすると思います。

あとは、お風呂の栓についているようなチェーンのモデルを追加して、背景設定を工夫すれば、オシャレアイテムの仕上がりイメージの完成です。
もう少し分割数に関しては、もう少し増やした方が良かったかもしれない。

お疲れ様でした。
引き続き、ピクシブ株式会社 Advent Calendar 2016をお楽しみください。