OneonetのPyBE

Python,Blender,Excelのいろいろ

ブレンダーモデルをPythonスクリプトとしてエクスポートする

ブログの趣旨的になんでもPythonで表現しないといけないのですが、メッシュを操作してコンソールを確認してメモする繰り返しがあまりテンションが上がりません。ということで思う存分メッシュを配置して動かして大きくして小さくしたところで、出来上がった状態をいきなりPythonスクリプトに変換するスクリプトを作成しました。

ループでパラメタを確認して文字列に落とし、最後にファイルに保存するスクリプトです。メッシュは複数対応できるようになっていて、位置(location)、サイズ(scale)、回転(rotation_euler)については情報を収集しています。キューブ以外でも可能ですが、ポリゴンの数が桁違いなのでとても長いスクリプトになります。

こちらのサイトを参考にさせて頂きました。複数メッシュの対応と位置、サイズ、回転の情報収集を付与しております。
blender.stackexchange.com

■■■Blenderはこんな状態
f:id:oneonet:20220316160226p:plain

■■■Pythonスクリプトを作成するスクリプト

#blender 3.0.0で動作確認
import bpy

s = "import bpy\n"

for ob in bpy.data.objects:
    if ob and ob.type == "MESH":
        data = ob.data

        faces = ",\n         ".join(f'{p.vertices[:]}' for p in data.polygons)
        verts = ",\n         ".join(f"{v.co[:]}"  for v in data.vertices)
        location = "".join(f"{ob.location[:]}")
        scale = "".join(f"{ob.scale[:]}")
        rotation_euler = "".join(f"{ob.rotation_euler[:]}")

        s = s + f"""

verts = ({verts})

faces = ({faces})

scene = bpy.context.scene
me = bpy.data.meshes.new("{data.name}")
me.from_pydata(verts, [], faces)
ob = bpy.data.objects.new("{data.name}", me)
ob.location = {location}
ob.scale = {scale}
ob.rotation_euler = {rotation_euler}
scene.collection.objects.link(ob)

"""

with open('c:/tmp/make_primitive.py', 'a') as f:
    print(s, file=f) 

■■■生成されたスクリプト
Blenderを日本語化しているため最初からのキューブは"Cube"、追加すると"立方体"になるようです。

import bpy

verts = ((1.0, 1.0, 1.0),
         (1.0, 1.0, -1.0),
         (1.0, -1.0, 1.0),
         (1.0, -1.0, -1.0),
         (-1.0, 1.0, 1.0),
         (-1.0, 1.0, -1.0),
         (-1.0, -1.0, 1.0),
         (-1.0, -1.0, -1.0))

faces = ((0, 4, 6, 2),
         (3, 2, 6, 7),
         (7, 6, 4, 5),
         (5, 1, 3, 7),
         (1, 0, 2, 3),
         (5, 4, 0, 1))

scene = bpy.context.scene
me = bpy.data.meshes.new("Cube")
me.from_pydata(verts, [], faces)
ob = bpy.data.objects.new("Cube", me)
ob.location = (0.0, 0.0, 0.0)
ob.scale = (1.0, 1.0, 1.0)
ob.rotation_euler = (0.0, 0.0, 0.0)
scene.collection.objects.link(ob)

verts = ((-1.0, -1.0, -1.0),
         (-1.0, -1.0, 1.0),
         (-1.0, 1.0, -1.0),
         (-1.0, 1.0, 1.0),
         (1.0, -1.0, -1.0),
         (1.0, -1.0, 1.0),
         (1.0, 1.0, -1.0),
         (1.0, 1.0, 1.0))

faces = ((0, 1, 3, 2),
         (2, 3, 7, 6),
         (6, 7, 5, 4),
         (4, 5, 1, 0),
         (2, 6, 4, 0),
         (7, 3, 1, 5))

scene = bpy.context.scene
me = bpy.data.meshes.new("立方体")
me.from_pydata(verts, [], faces)
ob = bpy.data.objects.new("立方体", me)
ob.location = (1.4182631969451904, 4.194612503051758, 0.7980197072029114)
ob.scale = (1.6070706844329834, 1.6070706844329834, 1.6070706844329834)
ob.rotation_euler = (-0.6515693664550781, 0.14909927546977997, -0.39666253328323364)
scene.collection.objects.link(ob)

Python and Blender: A World Where the Gundam Hammer Doesn't Destroy Anything Part 1

The target of the attack is a single thin cylinder, like a pencil.
But by giving the pencil a weight of 1 ton, it becomes, "That's Gog, he's nothing."
I'll be fine.

Gundam Hammer: The Gundam Hammer is a physical melee weapon that takes the form of a flail, with a large spiked ball attached to the handle by a chain. It is useful in situations where beam weapons cannot be used. The chain is long enough that the weapon can be swung around the mobile suit to strike enemies at mid-range or thrown.

f:id:oneonet:20220316160045p:plain

#blender 3.0.0
import bpy

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

bpy.ops.mesh.primitive_cone_add(radius1=1, radius2=0, depth=4,enter_editmode=False, align='WORLD', location=(0, 55.5, 70), scale=(1, 1, 1))
bpy.ops.mesh.primitive_ico_sphere_add(enter_editmode=False, align='WORLD', location=(0, 55.5, 70), scale=(5, 5, 5))
bpy.context.object.instance_type = 'FACES'
bpy.data.objects['円錐'].select_set(True)
bpy.data.objects['ICO球'].select_set(True)
bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)

for i in range(0,20):
    bpy.ops.mesh.primitive_torus_add(location=(0, i*2.6, 70), major_radius=1.0, minor_radius=0.2, rotation=(0, 0, 0))
    bpy.ops.rigidbody.object_add()
    bpy.context.object.rigid_body.collision_shape = 'MESH'
    if i==0:
        bpy.context.object.rigid_body.type = 'PASSIVE'

for i in range(0,20):
    bpy.ops.mesh.primitive_torus_add(location=(0, i*2.6+1.3, 70), major_radius=1.0, minor_radius=0.2, rotation=(0, 1.57, 0))
    bpy.ops.rigidbody.object_add()
    bpy.context.object.rigid_body.collision_shape = 'MESH'

bpy.data.objects['ICO球'].select_set(True)
bpy.ops.rigidbody.constraint_add()
bpy.data.objects['トーラス.039'].select_set(True)
bpy.ops.object.parent_set(type='OBJECT', keep_transform=True)


bpy.ops.mesh.primitive_cylinder_add(enter_editmode=False, align='WORLD', location=(0, 0, 10), scale=(0.5, 0.5, 10))
bpy.ops.rigidbody.object_add()
bpy.context.object.rigid_body.mass = 1000

bpy.ops.mesh.primitive_plane_add(size=40, enter_editmode=False, align='WORLD', location=(0, 0, -1), scale=(1, 1, 1))
bpy.ops.rigidbody.object_add()
bpy.context.object.rigid_body.type = 'PASSIVE'

Blender x Python でカメラをカーブに沿わせて常に被写体を狙うサンプル

カメラの調整がなかなか難しいBlenderですが、オブジェクトコンストレイントのパラメタ”パスに追従”によって、カーブや円に沿って動かす設定が可能です。ただそれだけですとカメラが明後日の方向を向いてしまうので、重ねて"トラック"の設定を行うことで、常に指定したオブジェクトの方向を向くことができます。これによって、太陽を常に捉える衛星のようなイメージでアニメーションを作成することができます。

文章で説明するだけでややこしい印象ですが、画面を添えて手順を説明するとなるともっとややこしい感じになります。ということで、ただただpythonスクリプトをコピペするだけで↑の説明を皆様のPCで再生できるようにしてあります。Blenderの凄さを味わってください。念のためですが、Blenderのバージョンは3.0.0で確認しております。

1点ご注意頂きたい点としては、日本語化してあるBlenderを利用しているため、"ベジェ円"とか"円錐"とかベタな日本語表現となっております。blenderを日本語化せずご利用になられている方は日本語部分を変更頂き実行してみてください。

#blender 3.0.0 で 動作確認済
import bpy

#主役のコーン
bpy.ops.mesh.primitive_cone_add(radius1=1, radius2=0, depth=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))

#2か所に光源設置。回転が分かりやすいようドキツイ2色
bpy.ops.object.light_add(type='POINT', align='WORLD', location=(3, 3, 3), scale=(1, 1, 1))
bpy.context.object.data.color = (0, 1, 0)
bpy.context.object.data.energy = 3000
bpy.ops.object.light_add(type='POINT', align='WORLD', location=(-5, -5, -5), scale=(1, 1, 1))
bpy.context.object.data.color = (1, 0, 0)
bpy.context.object.data.energy = 3000

#カメラの軌道となる楕円カーブ
bpy.ops.curve.primitive_bezier_circle_add(enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(10, 8, 3))
bpy.ops.transform.resize(value=(10, 5, 3))
bpy.context.object.rotation_euler[1] = 0.523599

#カメラの設置
bpy.ops.object.camera_add(enter_editmode=False, align='VIEW', location=(0, 0, 0), rotation=(0, 0, 0), scale=(1, 1, 1))

#カメラがベジェ円に追従するよう設定
bpy.ops.object.constraint_add(type='FOLLOW_PATH')
bpy.context.object.constraints["パスに追従"].target = bpy.data.objects["ベジエ円"]
bpy.ops.constraint.followpath_path_animate(constraint="パスに追従", owner='OBJECT')

#カメラが常に円錐を狙うよう設定
bpy.ops.object.constraint_add(type='TRACK_TO')
bpy.context.object.constraints["トラック"].target = bpy.data.objects["円錐"]

f:id:oneonet:20220316155736p:plain

是非動画をミラーサイトでご覧になってください。
oneonet-blog.blogspot.com

動画をタイル状の動画に変換するPythonスクリプト

1つの動画を指定した枚数のタイル状にして表示します。ニュース等で見るモニタを積み重ねて全てに同じ映像を映すアレです。使い道は見当たりませんが、見どころの多い動画を4分割程にして表示するフレームをずらすことで、いつ見ても見どころが映ってる、そんな感じです。ワチャワチャした感じの背景という感じでしょうか。自由にお使いになってください。もし動画作成されたりしたらコメント欄でご紹介頂けますと幸いです。

各タイルでの動画の開始位置はランダムにしてありますが、同じ位置にすることもできます。各タイルで動画を最後まで表示したらランダムな位置に巻き戻しますので短い動画から長時間タイル動画にすることもできます。音は消えてしまうので後から合成してください。ffmpeg等お使いになると楽ちんかと思います。

以下解説です。

6,7行目:
入力ファイルと出力ファイルを指定します。特に制約は無いと思います。

8行目:
write_secに出力動画ファイルの長さを秒で指定します。各タイル毎に最後まで再生したらランダムな位置まで巻き戻していますので、何秒でも指定できます。

9行目:
tileに縦横枚数を指定します。3を指定すれば、3x3の映像になります。値に上限はありませんが処理時間に大きく影響を与えます。3と10では10倍程処理時間がかかります。2ケタにするとハングしているように見えると思います。

10行目:
expにオリジナルの映像サイズのに対する拡大率を指定します。1を指定すれは、入力ファイルと出力ファイルの映像サイズは同じになります。

20行目:
出力動画の形式を変更したい場合はここを修正してください。

26行目:
frame_posのリストに、n×nコのタイル毎に最初の1コマ目のフレームをランダムに格納しています。初期値は0~framecountの範囲で任意です。全てのタイルを先頭から再生したければ全てゼロ、一定間隔ずらしにしければオフセットを指定して初期値を設定してください。

31行目:
デバッグ用です。削っても問題ありません。30行目のforにtqdmを利用したほうがクールかと思います。

36行目:
cap.readはエラーにならない前提です。

38行目:
最後まで再生したらランダムな位置まで巻き戻します。先頭まで巻き戻したい場合はランダムではなくゼロを入れるようにしてください。

40行目:
整数倍でよければフレームスキップが可能です。その場合最大フレーム数溢れのチェックを入れてください。

#-*- coding:utf-8 -*-
import cv2
import random

#入力動画、出力動画のパラメタ設定
src_file = 'domino1.mp4'
dst_file = 'domino25.mp4'
write_sec = 30  # 出力動画の秒数
tile = 5       # n × n の nを指定
exp =2          # オリジナルサイズの何倍にするか?

#内部で利用する値
cap = cv2.VideoCapture(src_file)
width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)            #動画幅
height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)          #動画高
fps = cap.get(cv2.CAP_PROP_FPS)                      #動画FPS
framecount = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))  #合計フレーム数

#出力映像の初期設定
fourcc = cv2.VideoWriter_fourcc('m','p','4', 'v')
video  = cv2.VideoWriter(dst_file, fourcc, fps, (int(width*exp), int(height*exp)))

#開始位置の設定
frame_pos = []
for _ in range(tile**2):
    frame_pos.append(random.randint(0,framecount))

tmp_frame_x = [0]*(tile)
tmp_frame_y = [0]*(tile)
for i in range(int(fps * write_sec)):
    print('{} / {}'.format(i, int(fps*write_sec)))
    for y in range(tile):      #縦に結合
        for x in range(tile):  #横一列を読み込む
            tile_pos = y*tile + x
            cap.set(cv2.CAP_PROP_POS_FRAMES, frame_pos[tile_pos]) 
            _, tmp_frame_x[x] = cap.read()
            if frame_pos[tile_pos] >= framecount-1:
               frame_pos[tile_pos] = random.randint(0,framecount)
            else:
               frame_pos[tile_pos] += 1
        tmp_frame_y[y] = cv2.hconcat(tmp_frame_x)  #横に結合

    total_frame = cv2.vconcat(tmp_frame_y)  #最後に縦に結合
    #expに指定された倍率にリサイズ
    resize_frame = cv2.resize(total_frame, dsize=None, fx=exp/tile, fy=exp/tile)
    video.write(resize_frame)

cap.release()
video.release()

是非動画をミラーサイトでご覧になってください。
oneonet-blog.blogspot.com

Blender × Python で文字を書く方法

Pythonスクリプトを使ってメッシュと同じように文字を追加することができます。タイトルだったりメモだったりと使い方は様々。ということでUV球を5つ並べて、そのUV球のサイズをテキストで書いてみました。こんな感じでカタログのように仕立てておくとパラメタの覚書になっていいんじゃないかなと思います。
特に文字列の部分はPythonで好きに加工できるので、デバッグにも使えるかもしれませんね。面白い使い方考えたいと思います。

f:id:oneonet:20220316155058p:plain

import bpy

for x in range (1,5):
    bpy.ops.mesh.primitive_uv_sphere_add(radius=0.5, enter_editmode=False, align='WORLD', location=(x, 0, 1), scale=(x, x, x))

    bpy.ops.object.text_add()
    ob=bpy.context.object
    ob.data.body = "size=" + str(x)
    bpy.context.object.data.offset_x = x
    bpy.context.object.data.offset_y = 0