Adways Advent Calendar 2017 4日目の記事です。
http://blog.engineer.adways.net/entry/advent_calendar_2017
こんにちは!
はじめまして。2017年新卒システムエンジニアの神戸です!
普段はアドテク関係のシステム開発に携わっています!
今回は、趣味で使ってみたBlenderをPythonで操作してみようと思います。
下記に内容の目次を示します。基本的な操作は、割愛させていただきます。
- Blenderの紹介と環境について
- 「板(plane)」を出してレンダリングしてみる
- オーディオを読み込んで、オーディオビジュアライザ―(audiovisualizer)を作成してみる
1.Blenderの紹介と環境について
Blenderは、オープンソースで提供されている3DCG高機能モデリングソフトで、誰でもフリーで導入が可能で、モデリングからコンポジットまでこれ一つで可能です。
今回、Blender Python APIとして提供されたパッケージ(bpy)を利用してPythonによる操作を行っていきます。
PythonでもBlenderの操作のメリットとしては、ランダムなオブジェクトを大量に生成する場合やアドオンの作成などがあげられます。個人的には、Pythonと3DCGの学習が出来る事が一番大きなメリットだと考えています。
今回使用する環境は、
- OS : Windows10
- Python : 3.1
- Blender version : 2.79
となっています。
2.「板(plane)」を出してレンダリングしてみる
早速Blenderを起動して、コーディングしてみましょう。 スクリーンのレイアウトを”Scripting”に変更して、作業のしやすい環境に変更します。
画面の左は、主にスクリプトを記述する部分となるテキストエディターで、右側がビューになっています。
下部にあるものが、Pythonコンソールとなります。
コンソール上では、対話型で操作を行うことが可能で、bpy.から始まるモジュールを指定して実行することができます。
今回は、実行時の動作を見やすくするために一度、画面上のオブジェクトを全て削除します。
コマンドライン上で”bpy.”と打ち込みctrl+Spaceを押下することで候補が表示されるので、参照しつつ実行が可能となっています。
例として、初期画面上にあるオブジェクトを削除してみます。
操作としては、ビューに現在表示されているもの選択→削除をおこなっていきます。
選択
bpy.ops.object.select_all(action='SELECT')
全て選択された状態になったら削除
bpy.ops.object.delete(True)
ビューや右上のOutlinerからオブジェクトが消えている事が確認できるとおもいます。
次はスクリプトの記述を行い、Planeを出してみます。
準備と実行に関しては、
- スクリプトを記述する前に、テキストエディター下部にある「+」で新規テキストを追加。
- スクリプトを記述。
- スクリプトの記述が完了したら、テキストエディター下部にある「Run Script」か「(マウスポインタの位置をテキストエディター内にして)alt+P」で実行。
となっています。
ただ、オブジェクトを作成する方法はいくつかあるので、今回は、下記の二通りの方法を例に挙げます。
頂点を指定して、面を作成する
プリミティブオブジェクトを呼び出す
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)が出てくるだけですね。
なんとなく物足りなさを感じる上に、遠回りな方法だと思いますが、このままレンダリングまで進めますね。
上記b.パターンでカメラ、ランプを追加して、レンダリングしていきます。
上手くいけば、C:\ tmp 直下にイメージが保存されます。
コードの流れとしては、
- カメラの方向を決定するため、mathパッケージをインポートを追加。
- ランプを追加。
- カメラを追加、調整。
- レンダリング設定(サイズ、解像度、カメラの結び付け等)の調整。
- レンダリング。
となります。
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を見下ろしたイメージが生成されます。
以上です。
以上なんですが、簡単にコードを追加していきます。
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))
メリハリがつくようになりました。
ワイヤフレームにすれば、何でもかっこよくなる気がするので実装してみます。
プレーンに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
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
出来た画像が以下の様になります。
はい。
少し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))
はい。
もう少し加工します。Bledner内で、レンダリングされたイメージの編集が可能なので、適当に編集します。
はい。
話が大きく逸れてしまいましたが、
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等で、後の変更が容易だったりしますが割愛させて頂きます。
とりあえず現段階で想定している完成としては、下記の図に示します。
大体何かを作るか想定できたと思いますので、実際にスクリプトを記述していきます。
まず、六角形の配列から行います。
隙間なく詰めていくので、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))
実行結果が以下の様になります。
次に、オーディオを読み込んで、動きをつけていきます。
最初に読み込むオーディオの周波数領域をステップ(今回は、配列の数)ごとに指定し、キーフレームに変換しています。
今回の場合、中心座標を最も激しく表現したかったため、二次関数の円の方程式を用いて中心座標から遠いほど、周波数領域が高くなって行くようにしています。
途中、上記の変換に伴い、少し時間がかかる点とエリアタイプをテキストエディタからグラフエディタに変更している点にご注意ください。
また、ファイルへのパスは、適宜変更をよろしくお願いいたします。
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'
上記の動作を確認するために、コンソールからタイムラインパネルに変更して、再生ボタン▶を押してみましょう。
わずかに動作が確認できるとおもいます。
次に、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'
-Z方向に拡大しなければOKです。
一応、目的の表現は確保できました。
ここから、レンダリングしたい所ですが、品質を確保すると時間がかかる為、マテリアルの設定などの調整を含めてまた別の機会に実施します。
物足りなさはぬぐい切れませんが、Blender × Python は、以上となります。
最後までご覧いただきありがとうございました!
まとめ
Blender * Pythonを扱ってみたわけですが、スクリプトをかけてすっきりしました。
忘れている点も多く、いい復習になりました。
今後は、インフラ関係の記事を書けるようになりたいです。