Blender × Pythonでお気楽3DCG!

Adways Advent Calendar 2017 4日目の記事です。

http://blog.engineer.adways.net/entry/advent_calendar_2017


こんにちは!

はじめまして。2017年新卒システムエンジニアの神戸です!

普段はアドテク関係のシステム開発に携わっています!

今回は、趣味で使ってみたBlenderをPythonで操作してみようと思います。

下記に内容の目次を示します。基本的な操作は、割愛させていただきます。

  1. Blenderの紹介と環境について
  2. 「板(plane)」を出してレンダリングしてみる
  3. オーディオを読み込んで、オーディオビジュアライザ―(audiovisualizer)を作成してみる

1.Blenderの紹介と環境について

f:id:AdwaysEngineerBlog:20171205194536p:plain

Blenderは、オープンソースで提供されている3DCG高機能モデリングソフトで、誰でもフリーで導入が可能で、モデリングからコンポジットまでこれ一つで可能です。

今回、Blender Python APIとして提供されたパッケージ(bpy)を利用してPythonによる操作を行っていきます。

PythonでもBlenderの操作のメリットとしては、ランダムなオブジェクトを大量に生成する場合やアドオンの作成などがあげられます。個人的には、Pythonと3DCGの学習が出来る事が一番大きなメリットだと考えています。

今回使用する環境は、

  • OS : Windows10
  • Python : 3.1
  • Blender version : 2.79

となっています。

2.「板(plane)」を出してレンダリングしてみる

早速Blenderを起動して、コーディングしてみましょう。 スクリーンのレイアウトを”Scripting”に変更して、作業のしやすい環境に変更します。

f:id:AdwaysEngineerBlog:20171205194633j:plain

f:id:AdwaysEngineerBlog:20171205194642j:plain

画面の左は、主にスクリプトを記述する部分となるテキストエディターで、右側がビューになっています。

下部にあるものが、Pythonコンソールとなります。

コンソール上では、対話型で操作を行うことが可能で、bpy.から始まるモジュールを指定して実行することができます。

今回は、実行時の動作を見やすくするために一度、画面上のオブジェクトを全て削除します。

コマンドライン上で”bpy.”と打ち込みctrl+Spaceを押下することで候補が表示されるので、参照しつつ実行が可能となっています。

例として、初期画面上にあるオブジェクトを削除してみます。

操作としては、ビューに現在表示されているもの選択→削除をおこなっていきます。

選択

bpy.ops.object.select_all(action='SELECT')

全て選択された状態になったら削除

bpy.ops.object.delete(True)

ビューや右上のOutlinerからオブジェクトが消えている事が確認できるとおもいます。

f:id:AdwaysEngineerBlog:20171205194718j:plain

次はスクリプトの記述を行い、Planeを出してみます。

準備と実行に関しては、

  1. スクリプトを記述する前に、テキストエディター下部にある「+」で新規テキストを追加。
  2. スクリプトを記述。
  3. スクリプトの記述が完了したら、テキストエディター下部にある「Run Script」か「(マウスポインタの位置をテキストエディター内にして)alt+P」で実行。

となっています。

ただ、オブジェクトを作成する方法はいくつかあるので、今回は、下記の二通りの方法を例に挙げます。

  1. 頂点を指定して、面を作成する

  2. プリミティブオブジェクトを呼び出す

a.頂点を指定して、面を作成する

import bpy

# initialize

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(True)

#create mesh

verts = [(1.0 , 1.0 , 0.0),
     (-1.0 , 1.0, 0.0),
     (-1.0, -1.0, 0.0),
     (1.0, -1.0 , 0.0)]

faces = [(0, 1, 2, 3)]

#add object

mesh = bpy.data.meshes.new('a_plane')
mesh.from_pydata(verts,[],faces)

#active selects

obj = bpy.data.objects.new('a_plane', mesh)
bpy.context.scene.objects.link(obj)

obj.select = True

b.の場合

import bpy

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(True)

bpy.ops.mesh.primitive_plane_add(location=(0,0,0))

となります。

上記を実行すると、どちらも一枚の板(plane)が出てくるだけですね。

f:id:AdwaysEngineerBlog:20171205194744j:plain

なんとなく物足りなさを感じる上に、遠回りな方法だと思いますが、このままレンダリングまで進めますね。

上記b.パターンでカメラ、ランプを追加して、レンダリングしていきます。

上手くいけば、C:\ tmp 直下にイメージが保存されます。

f:id:AdwaysEngineerBlog:20171205194754j:plain

コードの流れとしては、

  1. カメラの方向を決定するため、mathパッケージをインポートを追加。
  2. ランプを追加。
  3. カメラを追加、調整。
  4. レンダリング設定(サイズ、解像度、カメラの結び付け等)の調整。
  5. レンダリング。

となります。

import bpy
import math

#reset objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(True)

#plane_add
bpy.ops.mesh.primitive_plane_add(location=(0,0,0))

#lamp add
bpy.ops.object.lamp_add(location=(0.0,0.0,2.0))

#camera add
bpy.ops.object.camera_add(location=(5.0,0.0,5.0))
bpy.data.objects['Camera'].rotation_euler = (math.pi*1/4, 0, math.pi*1/2)

#render
bpy.context.scene.render.resolution_x = 500
bpy.context.scene.render.resolution_y = 500
bpy.context.scene.render.resolution_percentage = 100
bpy.context.scene.camera = bpy.context.object
bpy.context.scene.render.image_settings.file_format = 'PNG'
bpy.data.scenes["Scene"].render.filepath = "tmp/plane.png"
bpy.ops.render.render(write_still=True)

上記のスクリプトを走らせると、planeを見下ろしたイメージが生成されます。

f:id:AdwaysEngineerBlog:20171205194814p:plain

以上です。

以上なんですが、簡単にコードを追加していきます。

BackGroundを黒くして、LampをTypeをHEMIにしてみましょう。

#world
bpy.context.scene.world.horizon_color=(0.0,0.0,0.0)
#lamp add
bpy.ops.object.lamp_add(type='HEMI',location=(0.0,0.0,2.0))

f:id:AdwaysEngineerBlog:20171205194830p:plain

メリハリがつくようになりました。

ワイヤフレームにすれば、何でもかっこよくなる気がするので実装してみます。

プレーンにmodifierを追加してワイヤフレームの設定を変えてみましょう。

#plane_add
bpy.ops.mesh.primitive_plane_add(location=(0,0,0))
bpy.context.scene.objects.active = bpy.data.objects['Plane']
bpy.ops.object.modifier_add(type='WIREFRAME')
bpy.context.object.modifiers['Wireframe'].thickness = 0.01

f:id:AdwaysEngineerBlog:20171205194839p:plain

forでループさせてプレーンを羅列・回転させてみます。

安全をとって、各オブジェクトにmodifierを追加するときは、オブジェクトの追加とは別に行っていきます。

#plane_add
for i in range(0,100):
    bpy.ops.mesh.primitive_plane_add(radius = (i/100),location=(0,0,0),rotation=(math.pi*1/2,0,math.pi*i*10/360))

#modifier
for item in bpy.context.scene.objects:
    if item.type == 'MESH':
        bpy.context.scene.objects.active = bpy.data.objects[item.name]
        bpy.ops.object.modifier_add(type='WIREFRAME')
        bpy.context.object.modifiers['Wireframe'].thickness = 0.02

を追加します。

ついでにカメラと描画サイズの変更しておきます。

#camera add
bpy.ops.object.camera_add(location=(5.0,0.0,0.0))
bpy.data.objects['Camera'].rotation_euler = (math.pi*1/2, 0, math.pi*1/2)

#render
bpy.context.scene.render.resolution_x = 1000
bpy.context.scene.render.resolution_y = 1000

出来た画像が以下の様になります。

f:id:AdwaysEngineerBlog:20171205194904p:plain

はい。

少しY軸の回転を加えます。

ついでにWireframeを細くしておきます。

(オブジェクトの材質的な要素を持つマテリアルの設定で、透明度を操作して割り当てたほうが良い気がします)

#plane_add
for i in range(0,100):
    bpy.ops.mesh.primitive_plane_add(radius = (i*1.1/100),location=(0,0,0),rotation=(math.pi*1/2,math.pi*i*8.2/360,math.pi*i*10/360))

f:id:AdwaysEngineerBlog:20171205194917p:plain

はい。

もう少し加工します。Bledner内で、レンダリングされたイメージの編集が可能なので、適当に編集します。

f:id:AdwaysEngineerBlog:20171205194926p:plain

はい。

話が大きく逸れてしまいましたが、

1.の最終的なスクリプトはこちらになります。

import bpy
import math

#reset objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(True)

#world
bpy.context.scene.world.horizon_color=(0.0,0.0,0.0)

#plane_add
for i in range(0,100):
    bpy.ops.mesh.primitive_plane_add(radius = (i*1.1/100),location=(0,0,0),rotation=(math.pi*1/2,math.pi*i*8.2/360,math.pi*i*10/360))

for item in bpy.context.scene.objects:
    if item.type == 'MESH':
        bpy.context.scene.objects.active = bpy.data.objects[item.name]
        bpy.ops.object.modifier_add(type='WIREFRAME')
        bpy.context.object.modifiers['Wireframe'].thickness = 0.0025
        bpy.context.object.modifiers['Wireframe'].use_boundary = True

#lamp add
bpy.ops.object.lamp_add(type='HEMI',location=(0.0,0.0,2.0))

#camera add
bpy.ops.object.camera_add(location=(5.0,0.0,0.0))
bpy.data.objects['Camera'].rotation_euler = (math.pi*1/2, 0, math.pi*1/2)

#render
bpy.context.scene.render.resolution_x = 1000
bpy.context.scene.render.resolution_y = 1000
bpy.context.scene.render.resolution_percentage = 100
bpy.context.scene.camera = bpy.context.object
bpy.context.scene.render.image_settings.file_format = 'PNG'
bpy.data.scenes["Scene"].render.filepath = "tmp/plane.png"
bpy.ops.render.render(write_still=True)

上記が板(plane)を出してレンダリングするスクリプト+αとなります。

応用すれば幾何学模様の表現も簡易におこなうことができます。

3.オーディオを読み込んで、オーディオビジュアライザ―(audiovisualizer)を作成してみる

特定の周波数領域の波形を抽出し、その値をオブジェクトの何かの値に割り当てることによって表現していきます。

元々、Blenderでは、音声波形をグラフに変更することは出来ますが、膨大なオブジェクトに対して、単体ごとに異なる動作を実装する際は、スクリプトの強みを十分見出せるかと思います。

ただし、今回は申し訳ないのですがオブジェクト単位に簡易的に実装するだけで、マテリアルの割り当て、環境の設定、レンダリングは行いません。頂点単位で動作させることができればmodifier等で、後の変更が容易だったりしますが割愛させて頂きます。

とりあえず現段階で想定している完成としては、下記の図に示します。

f:id:AdwaysEngineerBlog:20171205195002p:plain

大体何かを作るか想定できたと思いますので、実際にスクリプトを記述していきます。

まず、六角形の配列から行います。

隙間なく詰めていくので、xyの位置に注意してください。

import bpy
import math

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(True)

for i in range(-5,5):
    for j in range(-5,5):
        bpy.ops.mesh.primitive_cylinder_add(vertices = 6,location=( math.sqrt(3)*j+(i%2)*math.sqrt(3)/2, 1.5*i, 1))

実行結果が以下の様になります。

f:id:AdwaysEngineerBlog:20171205195017j:plain

次に、オーディオを読み込んで、動きをつけていきます。

最初に読み込むオーディオの周波数領域をステップ(今回は、配列の数)ごとに指定し、キーフレームに変換しています。

今回の場合、中心座標を最も激しく表現したかったため、二次関数の円の方程式を用いて中心座標から遠いほど、周波数領域が高くなって行くようにしています。

途中、上記の変換に伴い、少し時間がかかる点とエリアタイプをテキストエディタからグラフエディタに変更している点にご注意ください。

また、ファイルへのパスは、適宜変更をよろしくお願いいたします。

import bpy
import math

lo = 1000
hi = 15000
step = (hi - lo) / count

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(True)

for i in range(-5,5):
    for j in range(-5,5):
        bpy.ops.mesh.primitive_cylinder_add(vertices = 6,location=( math.sqrt(3)*j+(i%2)*math.sqrt(3)/2, 1.5*i, 1))

        bpy.ops.anim.keyframe_insert_menu(type='Scaling')
        bpy.context.active_object.animation_data.action.fcurves[0].lock = True
        bpy.context.active_object.animation_data.action.fcurves[1].lock = True
        bpy.context.area.type = 'GRAPH_EDITOR'
        bpy.ops.graph.sound_bake(filepath=r'C:\sample.mp3', low = (step*round(math.sqrt(i**2+j**2))), high = (step*(round(math.sqrt(i**2+j**2))+1)))
        bpy.context.active_object.animation_data.action.fcurves[2].lock = True
bpy.context.area.type = 'TEXT_EDITOR'

上記の動作を確認するために、コンソールからタイムラインパネルに変更して、再生ボタン▶を押してみましょう。

f:id:AdwaysEngineerBlog:20171205195038j:plain

わずかに動作が確認できるとおもいます。

次に、3Dカーソルを用いて、オブジェクトの重心を定めて、スケールの変換時にカーソルによって定めた位置を中心に拡大するようにします。今回は、実行時にカーソル自体を指定したオブジェクトの下に来るようにして指定し、+Z方向にのみ拡大するようにします。

下記が最終版となります。

import bpy
import math

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(True)

count = 3
lo = 1000
hi = 15000
step = (hi - lo) / count

for i in range(-count,count):
    for j in range(-count,count):
        #object fixed
        bpy.ops.mesh.primitive_cylinder_add(vertices = 6,location=( math.sqrt(3)*j+(i%2)*math.sqrt(3)/2, 1.5*i, 1))
        bpy.context.scene.cursor_location = bpy.context.active_object.location
        bpy.context.scene.cursor_location.z -= 1
        bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
        bpy.context.active_object.scale.z = 10
        bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
        #scale changes lock xy
        bpy.ops.anim.keyframe_insert_menu(type='Scaling')
        bpy.context.active_object.animation_data.action.fcurves[0].lock = True
        bpy.context.active_object.animation_data.action.fcurves[1].lock = True
        #import sound & graph baking
        bpy.context.area.type = 'GRAPH_EDITOR'
        #radius(round(math.sqrt(i**2+j**2)))
        bpy.ops.graph.sound_bake(filepath=r'C:\HOGE.mp3', low = (step*round(math.sqrt(i**2+j**2))), high = (step*(round(math.sqrt(i**2+j**2))+1)))
        bpy.context.active_object.animation_data.action.fcurves[2].lock = True
bpy.context.area.type = 'TEXT_EDITOR'

f:id:AdwaysEngineerBlog:20171205195058g:plain

-Z方向に拡大しなければOKです。

一応、目的の表現は確保できました。

ここから、レンダリングしたい所ですが、品質を確保すると時間がかかる為、マテリアルの設定などの調整を含めてまた別の機会に実施します。

物足りなさはぬぐい切れませんが、Blender × Python は、以上となります。

最後までご覧いただきありがとうございました!

まとめ

Blender * Pythonを扱ってみたわけですが、スクリプトをかけてすっきりしました。

忘れている点も多く、いい復習になりました。

今後は、インフラ関係の記事を書けるようになりたいです。

参考

Blender

Blender Stack Exchange

Blender Api docs

Blender stack(sound bake)