2023年8月31日木曜日

Blender x Python でキューブを使ってマインクラフト風壁紙を自動で作る

 画像ファイルを読み込み1ピクセルを1つのキューブでブロックのように積み上げて壁画のようにするスクリプトになります。キューブのサイズ半分だけずらしているので真上から見ても真横から見ても同じ画像です。

 処理スピード感は、1ラインにあたるキューブ50コの処理時間が最初の1ライン目は0.1秒ですが、10ライン目2.5秒、50ライン目は16秒、100ライン目は30秒という感じでどんどん遅くなります。5000ピクセル程の小さな画像でも30分ほどかかりました。ということでほどほどのサイズでやってみてください。動画、静止画、スクリプトをご紹介します。どうすると早くなるんでしょうか。




#blender 3.0.0で動作確認
import cv2
import bpy

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

im = cv2.imread('c:/tmp/picture.jpg')
height, width, channels = im.shape[:3]

for x in range(width):
    for y in range(height):
        b,g,r = im[y,x]
        bpy.ops.mesh.primitive_cube_add(enter_editmode=False, align='WORLD', location=(-(height-y) , x, height-y), scale=(0.5, 0.5, 0.5))
        mat_name = str( hex(r*0x10000+g*0x100+b))
        mat = bpy.data.materials.new(mat_name)
        mat.diffuse_color = (r/256, g/256, b/256, 1.0)
        bpy.context.object.data.materials.append(mat)
       

2023年8月30日水曜日

Blender x Python で作るシンプルなドミノ倒し

物理演算のスゴクわかりやすいサンプルに仕立ててみました。 キーワードでググるとたいていのものは見つけらますが、見慣れない1命令があるとワクワクしますね。resizeで簡単に形を変えられるとは知りませんでした。

スタスタスタと気持ちよく倒れる100コのドミノを再生してみてください。planeのサイズとforカウンタの数を変えればいくらでも大きくできますよ。


#blender 3.0.0で動作確認
import bpy

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

bpy.ops.mesh.primitive_plane_add(size=400,  align='WORLD', location=(0, 0, -5), scale=(1, 1, 1))
bpy.context.object.rotation_euler[2] = 0.78

bpy.ops.rigidbody.object_add()
bpy.context.object.rigid_body.type = 'PASSIVE'

for i in range (0,100):
    bpy.ops.mesh.primitive_cube_add(location=(-200+i*4, 0, 0), size=1.0 )
    bpy.ops.rigidbody.object_add()
    bpy.ops.transform.resize(value=(1, 4, 10), constraint_axis=(False, True, True))

    if i==0:
        bpy.context.object.rotation_euler[1] = 0.3
        
こちらのサイトを参考にさせて頂きました。

Blender x Python アルキメデスの螺旋でドミノを並べるスクリプト

 アルキメデスの螺旋(Archimedes' spiral)と呼ばれる方程式  によって表される曲線(うずまき)があります。線同士の間隔が等しいのが特徴です。この方程式を利用して渦巻でドミノを並べてみました。

 グラフにプロットするだけなら計算式そのままで良いのですが、ドミノということでポイントが2つあります。1つは、等間隔となる必要があり、1つドミノ(キューブメッシュ)を配置したら一定距離になるまで次のドミノを配置しないこと。直前のドミノのx1,y1と配置しようとする新ドミノの位置x2,y2の距離を測り、距離が一定値以上となったら配置するようにしています。ドミノの高さ以下の距離であれば大丈夫です。 プロットする位置は計算で導き出せるようなのですが、何をしてもうまくいかなかったので、細かくループを回してその都度距離を測定する方法にしました。2点目はドミノの角度です。グラフにするだけなら角度という概念は出てきませんが、ドミノが前のドミノと次のドミノの中間の角度になる必要がありますので、プロットするタイミングでZ軸の角度を調整します。

 動画を見て頂けるとわかるのですが、ドミノを並べる個数とその間隔、うずまきの密集具合も指定することができ、どの場合でもドミノがキレイに倒れます。

2000個までやってみました。10分ぐらいかかります。

なんか気持ちよくないですか?(アニメーションのスピードは10倍に設定してあります)

2023年8月28日月曜日

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

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

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

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

How can I export my blender model as a python script


■■■Blenderの状態

■■■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)


2023年8月27日日曜日

Blender x Python 三角形のドミノ倒し

 一直線ばかりだったドミノを少し変化させていきたいと思います。倒すドミノを1枚づつ増やしていく配置としました。あまり面白味がないので、ドミノの配置に少し乱数を加え、マテリアルを2種類用意してランダムに割り当てました。
 そろそろ立体にしてみようかと思っているところです。

#blender 3.0.0
import bpy
import random

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

domino_distance = 10
domino_count = 50
domino_width = 4

mat_b = bpy.data.materials.new("Blue")
mat_b.diffuse_color = (0, 0, 1, 1.0)

mat_w = bpy.data.materials.new("White")
mat_w.diffuse_color = (1, 1, 1, 1.0)

for x in range(1,domino_count):
    for y in range(x):
        x_location = (x - 1)*domino_distance + random.random()
        y_location = (y-x/2)*domino_width *3
        bpy.ops.mesh.primitive_cube_add(location=(x_location, y_location, 0), scale=(1,domino_width,10),rotation=(0, 0, 0.0) )
        bpy.ops.rigidbody.object_add()
        bpy.context.object.rigid_body.friction = 1

        if random.random() > 0.6:
            bpy.context.object.data.materials.append(mat_w)
        else:
            bpy.context.object.data.materials.append(mat_b)

    if x== 1:
        bpy.context.object.rotation_euler[1] = 0.3

s=domino_distance * domino_count*1.2
bpy.ops.mesh.primitive_plane_add(size=s,  location=(s/2 - 10, 0, -10 ), scale=(1, 1, 1))
bpy.ops.rigidbody.object_add()
bpy.context.object.rigid_body.type = 'PASSIVE'





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.




#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'

2023年8月26日土曜日

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["円錐"]


2023年8月25日金曜日

動画をタイル状の動画に変換する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行目:
整数倍でよければフレームスキップが可能です。その場合最大フレーム数溢れのチェックを入れてください。


オリジナルの映像

5x5にした映像

ソースコード

#-*- 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()

2023年8月24日木曜日

Blender × Python で文字を書く方法

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

2023年8月22日火曜日

Blender × Blender ガンダムハンマーが破壊する世界その1

キューブを円柱状に積み上げ、物理の法則に従うガンダムハンマーが破壊する風景です。   ソースコードはこんな感じ。  衝突するメッシュはリジッドボディをメッシュにしないと、アニメーションをオンにすると同時にはじけ飛びます。円錐と球を使ってガンダムハンマーの先端を作成し、さらにクサリの1つと親子関係を設定しています。 クサリは一番最初のものだけリジッドボディをPassiveにすることで固定させることで、振り子のような動きにしてみました。 最初の衝突の時の挙動がすり抜けているような感じもしますが、どこをどうすると良くなるのかわからずこのままとしています。お分かりの方がいらしたらアドバイス頂戴したくよろしくお願いします。ソースコードは素のまま添付しておきます。実行させた後アニメーションをご確認頂けるかと思います。  
#blender 2.93.1で動作確認
import bpy
import math

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)

N = 36
RR1 = 10.0
for z in range(0, 20):
    for i in range(0, N):
        rad = 2 * math.pi * (i+z*.5) /N
        xx = RR1 * math.cos(rad)
        yy = RR1 * math.sin(rad)
        bpy.ops.mesh.primitive_cube_add(location=(xx, yy, z*1.01), size=1.0, rotation=(0, 0, rad))
        bpy.ops.rigidbody.object_add()

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'


2023年8月21日月曜日

Blender x Python で作るスカスカのピラミッド

Pythonスクリプトでキューブメッシュを並べてピラミッドを作ります、の第2弾。20段のピラミッドですが、1つ飛ばしで配置することでメッシュ数をザックリ半分にしています。添付しているソースでは設定はしていませんが、リジッドボディを設定しても問題なくこの形を維持しております。  
#blender 2.93.1で動作確認
import bpy

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

N = 20
for z in range (0,N):
    for x in range (0,N-z):
        for y in range (0,N-z):
            if (x+y) % 2 == 0:
                bpy.ops.mesh.primitive_cube_add(location=(x+z*0.5, y+z*0.5, z), size=1.0, rotation=(0, 0, 0))





pydubとMP3は相性が悪い

 作成した映像用のBGMを作成するためpydubを利用してmp3音声データを加工しています。簡単に説明しますと、1種類のBGM音源を複数結合して長尺にした後、いくつかのMP3効果音を必要なタイミングでoverrayして合成し、出来上がったものを完成版として保存します。効果音の必要なタイミングがプログラム的に割り出せているので映像加工ソフト使うよりこのほうが手間が無いんですよね。

 実際やってみてわかったことですが、メモリ消費と処理時間の2点問題があることがわかりました。
 まずはメモリ。pydubによる音声データの加工はとてつもなくメモリを消費することがわかりました。数分のBGMを結合して1時間程度にした後、効果音を適当に重ねるのですが、メモリ使用量が自分のマシンの搭載メモリ16Gまで一瞬で上昇し、ガベージコレクション的に開放される動きを処理が完了するまで繰り返します。とても1時間程度のMP3データを処理しているとは思えない消費っぷりです。ただし、それ以上の弊害はないのでメモリがあるだけ消費する仕様なんだと納得することにしました。

 もう1点は処理速度。MP3限定ですが、読み込み、書き込みに想像以上に時間がかかります。測定したサンプルは以下の通りです。Windows 10に標準で入っていた呼び出し音(10秒程度でWAVファイルでは800KByte)の読み込みと保存です。MP3の場合、読み込み、書き出し共に200msec程要していますが、wavに変換するだけで極端に高速化されます。処理速度にお困りの場合はwavに変換してから利用すると良いと思います。

from pydub import AudioSegment
from datetime import datetime

class Timer(object):
    def __enter__(self):
        self.st = datetime.now()
    def __exit__(self, *exc):
        print("{}ms".format((datetime.now() - self.st).microseconds / 1000))

with Timer(): # 400 - 500ms
    sound = AudioSegment.from_file("ring03.mp3", "mp3")
    sound.export("output.wav", format="mp3")

with Timer(): #   0 - 10ms
    sound = AudioSegment.from_file("ring03.wav", "wav")
    sound.export("output.wav", format="wav")

2023年8月17日木曜日

弾性パラメタの違い

 衝突映像の作成には欠かせない弾性パラメタですが、0~1.0以下の値が指定できるとは知りつつも、どのくらいが丁度いいのかな?という話。実際にUV球を配置して、弾性パラメタを変化させてどの程度跳ね返ることになるのか見てみました。結論から言いますと0.5以上にしておかないとあまり目立った反応無し、といったところです。

 ソースコードを少し解説しますと、まずは大き目の床板を作成。restitution(物理演算プロパティ→リジッドボディ→表面の反応→弾性 になります)を1としておきます。10個のUV球を高さ10に並べて、 restitutionを0.5から1.0まで0.05刻みとしました。テキストを入れてありますが、スクリプトでうまく表現できませんでしたのでこれは手で入れました。

 落としてみると映像の通り。0.6~0.9ぐらいを設定すると"跳ねる"感じが出ているように思います。

 blenderをインストールしてスクリプトをコピペするだけで再現できます。是非一度やってみてください。念のため手順をおさらい。

  1. Blenderを起動する
  2. クリックしてスプラッシュ(3.0.0だと女の子と小鳥)を閉じる
  3. 画面上部メニューの"scripting"を選択
  4. 新しく出てきたパネルの上部中央にある"+ 新規"をクリック
  5. 数字の1が赤く出たと思います。エディタになっているので以下のソースコードをコピペ。
  6. "+ 新規"のすぐ右にある右向き三角"▶"でスクリプトの実行
  7. 画面上部メニューの"Animation"を選択
  8. 画面下部にあるアニメーション再生ボタン(右向き三角"▶")をクリック
どうでしょう?同じ映像が再生されたと思います。面白いですよね、Blender。
 

#blender 3.0.0 で 動作確認済
import bpy

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

f = 10

#大き目の床板。弾性1.0に指定
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'
bpy.context.object.rigid_body.restitution = 1

#10個の球を弾性0.5~1.0まで10個
for x in range (0,f):
    bpy.ops.mesh.primitive_uv_sphere_add(radius=0.5, enter_editmode=False, align='WORLD', location=(x*2-f, 0, 10), scale=(1, 1, 1))
    bpy.ops.rigidbody.object_add()
    bpy.context.object.rigid_body.restitution = 0.5 + x / 20 -0.01

2023年8月16日水曜日

Blender x Python で作るガンダムハンマー

 トーラス(ドーナッツみたいなやつね)、ICO球、円錐を組み合わせてガンダムハンマーを作ってみました。実物より少しトゲが多くなっていますが、このぐらいのほうがかっこいいかなということにしときます。

 まずクサリの部分はトーラスを隙間を開けて並べて、その隙間にちょうど収まるように、かつ90度(2π*90/360≒1.57)ずらしで配置します。今回は2mのクサリを2.6m間隔で配置し、90度回転+1.3m差としました。

 肝心のトゲトゲはインスタンス化機能を利用しています。球を親として円錐を各面に張り付けることでガンダムハンマーっぽい構造になります。円錐の大きさがそのままトゲトゲの大きさになりますので、高さを変えてみると面白いと思います。

 リッジドボディを設定すればこれでピラミッドとか壊せそうです。 

 

#blender 2.93.1で動作確認
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, 0), scale=(1, 1, 1))
bpy.ops.mesh.primitive_ico_sphere_add(enter_editmode=False, align='WORLD', location=(0, 55.5, 0), 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, 0), major_radius=1.0, minor_radius=0.2, rotation=(0, 0, 0))

for i in range(0,20):
    bpy.ops.mesh.primitive_torus_add(location=(0, i*2.6+1.3, 0), major_radius=1.0, minor_radius=0.2, rotation=(0, 1.57, 0))

2023年8月11日金曜日

プリミティブの作成には時間がかかる

 キューブメッシュの作成(bpy.ops.mesh.primitive_cube_add)にかなり時間がかかる印象でしたので測定してみました。作成数にキレイに比例するようです。2,000個あたりではキューブメッシュ1つ作成するのに0.1秒かかりました。これでも十分遅いように思いますが、10,000個になると0.5秒となります。API呼び出し1回で0.5秒はなかなかの処理時間かなと思います。CPU、メモリ等のリソースが不足しているようには見えませんので作りの問題でしょうか。

プリミティブ作成時間


2023年8月8日火曜日

BlenderでXPの壁紙を壁にしてみました

約20,000個のCubeでWindows XPの壁紙を再現してみました。画像ファイルを読み込み1ドットづつ色を判定してその色でキューブを配置するという単純なスクリプトで作成しています。何に時間がかかるのかわかりませんが12時間ほどかかりました。

bpy.ops.mesh.primitive_cube_add でキューブの作成、bpy.data.materials.new でマテリアルを作成し、1ドットづつ色を指定しています。このあたりが遅い理由かもしれません。

ライティングとかいろいろ変化させられるかなと思ったのですが、ソリッドモード以外にするとハングしてしまうのでこれ以上操作はできません。時間がかかる理由等もう少し調べてみようと思います。

ちなみにこの草原の風景、CGではなく実在する景色なんだそうです。


2023年8月5日土曜日

Blender x Python で作るピラミッド

Pythonスクリプトでキューブメッシュを並べてピラミッドを作ります。円柱に引き続き簡単なスクリプトで作成できます。  
#blender 2.93.1で動作確認
import bpy

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

f = 15     #フロア数

for z in range (0,f):
    for x in range (0,f-z):
        for y in range (0,f-z):
            bpy.ops.mesh.primitive_cube_add(location=(x+z*0.5, y+z*0.5, z), size=1.0, rotation=(0, 0, 0))

指数関数的にメッシュ数が増えるのでフロア数は10~20ぐらいにしておくことをお勧めします。

2023年8月3日木曜日

Pythonでアプリが再生する音を録音する方法

 ステレオミキサーを利用してPCが再生している音をプログラムから録音する方法のご紹介です。Windows 11をベースに説明していますが、基本的なところはWindows 10でも同じです。ステレオミキサーの設定と、音声出力デバイスが複数ある場合の注意をご案内します。

■はじめに

 接続されているサウンド入力デバイス、出力デバイスを最小限にしてから設定を行うことをお勧めします。ヘッドセットや外部スピーカーの他、ディスプレイケーブルだけが接続されているディスプレイもサウド出力デバイスになるので、極力外しておきましょう。

■Windows 11の設定

 スクリーン右下にあるスピーカーアイコンを右クリックして"サウンドの設定"を開きます。出力デバイスと入力デバイスを設定できるので、入力デバイスをステレオミキサーに変更し、画面からはみ出てますが、マイクボリュームを最大にしておきましょう。


 もしステレオミキサーが選択できないようなら、デバイスマネージャーで設定を確認します。"オーディオの入力および出力"のツリーを展開してください。ステレオミキサーが選択できない場合はツリー配下にステレオミキサーが無いと思います。メニューの表示→非表示のデバイスを表示をチェックすることで表示されたら、ドライバーの更新、有効化、インストールしなおし等順番に操作してみてください。ここでうまくステレオミキサーを有効にできない場合はPCそのものを変えてみるとか大胆な手を打ってください。

■デバイスIDを確認する

 自分が利用しているPCのステレオミキサーのインデックスを確認します。pythonのライブラリ"sounddevice"もしくは"pyaudio"を利用してステレオミキサーのインデックスを確認するサンプルを掲載しておきます。
import sounddevice as sd
print (sd.query_devices())
import pyaudio
p = pyaudio.PyAudio()
for index in range(0, p.get_device_count()):
    print("{index}:{name}".format(**p.get_device_info_by_index(index)))
こちらは"sounddevice"のライブラリを利用して筆者の利用しているPCのデバイスインデックスを確認した結果です。ステレオミキサーはインデックス1番であることがわかります。1点ポイントですが、このRealtekのステレオミキサーで録音できる音は、Realtekのサウンドデバイスから音が出ている必要があります。ステレオミキサーの設定が正しくても別のデバイスから音が出ていても録音できません。また、実際にマイクから音を拾っているわけではありませんが、ミュートしていると録音できません。この2点気づいていないと、"昨日は動いたのに・・・"となります。
 ちなみに、不等号がデフォルトデバイス、行末のin,outの前の数字がチャンネル数になります。マイクとスピーカーは1セットしか付いていませんが、マイクがやたら多く出てきているのは設定をいじくりすぎておかしくなっているのかもしれません。

■Pythonで録音する

ここまでくれば後は簡単です。入力デバイスに先ほどのインデックスを指定すればOK。"sounddevicve"ライブラリの場合は、このような感じで指定すれば良いです。ライブラリの説明を見るよりサンプルソースを探せば早いかと。
sd.default.channels = 1, 2  #Different values for input and output:

2023年8月1日火曜日

Blender x Python で作る塔

Pythonスクリプトでキューブメッシュを並べて円柱を作ります。塔というよりUFOキャッチャーによくある小箱お菓子が積みあがられているようなイメージですが、簡単なスクリプトで作成できます。将来的には物理演算の機能によって何か面白そうなことしてみたいと思っています。  
#blender 2.93.1で動作確認
import bpy
import math

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

n = 36     #頂点の数
r = 10.0   #半径
f = 20     #フロア数

for z in range(0, f):
    for i in range(0, n):
        rad = 2 * math.pi * (i+z*.5) /n
        xx = r * math.cos(rad)
        yy = r * math.sin(rad)
        bpy.ops.mesh.primitive_cube_add(location=(xx, yy, z), size=1.0, rotation=(0, 0, rad))
n,r,fを変更することで、1フロアのキューブの数、半径の大きさ、積み上げる数を変化できます。角度を半分ずらすことで互い違いになるようにしていますが、キューブ間の開きがキューブの大きさを超えないように調整が必要です。