Blenderに外部モジュールをインストールする
Pythonでモジュールのインストールを行うときはpipを使うのが一般的ですけど、Blenderで動作するPythonにはpipが付属してませんから、それを入れる必要があります。get-pip.pyってスクリプトをBlenderのPythonから実行してやると適切な場所にpipが入ります。
まずはget-pip.pyをダウンロードします。適当なディレクトリで
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
とすると、そのディレクトリにget-pip.pyが出来上がります。で、BlenderのPythonでそれを実行します。
pathToBlender.app/Contents/Resources/[Version]/python/bin/[pythonのバイナリ] get-pip.py
例えば僕はBlender 3.4を /Applications/Blender\ 3.4 に置いているので
/Applications/Blender\ 3.4/Blender.app/Contents/Resources/3.4/python/bin/python3.10 get-pip.py
というコマンドになります。これで python3.10 があるのと同じ場所に pip や wheel が入ります。
ちなみに、このやり方だとBlenderのマイナーアップデートが出てそれを上書きで入れ替えると、当然その中のpipも消えるので諸々入れ直しです。
で、モジュールのインストールです。BlenderのPythonのパスを入れるのが面倒なんで、それがあるディレクトリに移動しておくのがいいでしょう。その上で、例えば、OpenCVとTriangleってモジュールを入れるなら、そこ(さっき入れたpipのあるディレクトリ)で
./pip install opencv-python
./pip install triangle
でOK。これらモジュールもBlender.app内部に入っちゃうので上書きでBlender.appを入れたら入れ直しになります。以上で Tesselate_texture_plane[GitHub] をBlender 3.4にインストールの準備ができるわけですな。3.4にも無事入りました。
カメラ方向の見た目をベイクする
Blender 3.4のリリースノート[Blender Developer Wiki] にこう書かれてます。
Baking: new option to bake specular effects from the active camera view, instead of above the surface. This may be useful when baking textures that will be viewed from a fixed position or with limited camera motion.
アクティブなカメラからの鏡面効果をベイクするためのオプションが追加されたわけです。
3.3と3.4のベイクの設定部分を並べると、View From ってオプションが3.4に追加されていることに気づきます。そこではActive Cameraってパラメータを選べ、それが新機能ということっすね。従来のはAbove Surfaceなのだと思います。
鏡面反射をリアルタイムに計算する場合にはこのオプションの意味はないのですけど、そうじゃない場合やそれができない描画システムではこれができるのとできないのとでは画面の説得力がだいぶ変わってくると思うのですよ。
上図一番左の位置でView FromをActive Cameraにしてベイクしたのが一番右、真ん中は従来のAbove Surfaceです。ハイライト、あるいは目玉の反射の入り方に注目です。Active Cameraの方はテクスチャ解像度が負けている以外はかなりいい感じに再現されているのに対し、Above Surfaceは強いハイライトの位置や目玉の反射の見え方にだいぶ差が生じており、ベイク元と同じ場所に設置した時の「そこにある感」が軽減されてしまうことが予想されます。一方でActive Cameraの方はベイク元と同じ場所に置いた時、カメラが大きく動かなければかなり説得力のある見た目になるでしょう。
ということで、これは嬉しい新機能なのでした(Softimage民としては、え?できなかったの?なんだけど。まあSIのはカメラから見えない部分が塗られないという面倒な仕様だった記憶があるけど)
カメラや光源が大きく変化すると、焼き付けたハイライトなどが単なる模様に見えてしまってよろしくないので、使い所は考えないとです。
選択したマテリアルスロット以外を消す
この図で選択しているのはMaterial.004が刺さっている2つ目のマテリアルスロットですけど、選択されていないマテリアルスロットを全部削除して、選択したマテリアルだけ残したいとします。
import bpy objs = bpy.context.selected_objects matSlots = objs[0].material_slots selectedMatIndex = objs[0].active_material_index j = 0 for i in range( len( matSlots ) ): objs[0].active_material_index = j if j != selectedMatIndex: bpy.ops.object.material_slot_remove() selectedMatIndex -= 1 else: j += 1
まあそんな作業が発生したんですわ。これをもうちょっとちゃんと書いてマテリアルプロパティのところから右クリックからとかで実行できるようにできていれば捗っただろうなぁ。(-ボタンをプチプチ押していくのは面倒、時間かかる)
Blenderの親子関係
Blender 3.2で作業しててちょっとびっくりしたののが、オブジェクトの親子関係を作るコマンドが増えていたことです。
3.1までになかった Object (Keep Transform Without Inverse) ってのが追加されている。
通常、親子関係を作るとき、Object (Keep Transform) を多用してたのですが、これですと、下の画像のように
子供の位置が親からの相対座標で示されません。上図の子供4の直接の親との相対的な位置関係は ( 0 , -5, 0 ) なのにも関わらず、親子関係を作る前の値が維持されてます。
一方で3.2で追加された?コマンドを実行すると
意図した値が反映されてます。個人的にはこっちの挙動の方が見えている値で状態を理解できて好きなので、これは大歓迎なのですけど、じゃあなんで Keep Transform だとそうならなかったというと、マニュアル読むと書いてありました。
表に出ていないパラメータ Parent Inverse Matrix に値を持たせて、あえて元の値を表に出していたってことですね。ちなみにこの値はOutlinerをData APIにしてやれば見ることができました。
まあともあれ、個人的には便利になったなと思うわけです。
顔の回転角度の推定
ってのをしばらくやっているのですけど、よくわからん。いや、数式的なところはとりあえず置いておいて(俺には理解できないと思うw)、openCVを使ってやってみたりとか云々。
花畑に座っている女性 · 無料の写真素材[Pexels] を使って、OpenCV の solvepnp()関数を使用して PnP の問題を解決する [Delft スタック] という解説をそのまま適用したら当然ちゃんとした結果が表示されました。
ちなみに画像上の特徴点はPhotoshopで手動で拾ってます。
2D上の特徴点に対応する元となる3Dのメッシュの頂点座標の値が重要なんだと思います。(つまり、顔の3D空間での形をできるだけ正しく数値で持っておく必要がある)
で、世の解説記事で見かける、無料で配布されている68点の特徴点で学習させた結果を使ってリアルタイムで顔の向きを推定させる記事に書いてる顔の形状の数値を使って、先の記事のコードの特徴点情報を書き換えたのが下です。
import cv2 import numpy as np img = cv2.imread('woman-portrait-meadow-dandelions-157604.jpeg') size = img.shape image_points = np.array( [[1066, 423], # 27 left brow left corner [1011,433], # 23 left brow right corner [977,436], # 22 right brow left corner [946,454], # 18 right brow right corner [1070,445], # 46 left eye left corner [1025,458], # 43 left eye right corner [983, 466], # 40 right eye left corner [955,478], # 37 right eye right corner [1021,496], # 36 nose left corner [982,506], # 32 nose right corner [1042,538], # 55 mouth left corner [982,549], # 49 mouth right corner [1006,563], # 58 mouth central bottom corner [1006,591] ] ,# 6 chin corner dtype=np.float32) # https://pyimagesearch.com/2017/04/03/facial-landmarks-dlib-opencv-python/ # https://myuon.github.io/posts/juniq/ object_points = np.array( [[6.825897, 6.760612, 4.402142], # 27 left brow left corner [1.330353, 7.122144, 6.903745], # 23 left brow right corner [-1.330353, 7.122144, 6.903745], # 22 right brow left corner [-6.825897, 6.760612, 4.402142], # 18 right brow right corner [5.311432, 5.485328, 3.987654], # 46 left eye left corner [1.789930, 5.393625, 4.413414], # 43 left eye right corner [-1.789930, 5.393625, 4.413414], # 40 right eye left corner [-5.311432, 5.485328, 3.987654], # 37 right eye right corner [2.005628, 1.409845, 6.165652], # 36 nose left corner [-2.005628, 1.409845, 6.165652], # 32 nose right corner [2.774015, -2.080775, 5.048531], # 55 mouth left corner [-2.774015, -2.080775, 5.048531], # 49 mouth right corner [0.000000, -3.116408, 6.097667], # 58 mouth central bottom corner [0.000000, -7.415691, 4.070434] ] , # 6 chin corner dtype=np.float32) object_points = 10 * object_points #スケールを10倍しないといい感じにならない。mm単位かな。 chainPos = 10* np.array( [0.000000, -7.415691, 4.070434] ) distortion_coeffs = np.zeros(( 4, 1 )) focal_length = size[1] camera_matrix = np.array([ [focal_length, 0, size[1]//2 ], [ 0, focal_length, size[0]//2 ], [ 0, 0, 1] ], dtype = 'double') success, rotation_vector, translation_vector = cv2.solvePnP( object_points, image_points, camera_matrix, distortion_coeffs, flags=cv2.SOLVEPNP_ITERATIVE ) zero , jacobian = cv2.projectPoints( chainPos , rotation_vector, translation_vector, camera_matrix, distortion_coeffs ) nose_end_point2D500 , jacobian = cv2.projectPoints( np.array( [ 0., 0. , 500.] ) + chainPos, rotation_vector, translation_vector, camera_matrix, distortion_coeffs ) i = 0 for p in image_points: cv2.circle( img, ( int( p[0] ) , int( p[1] ) ) , 7 , ( i , 0, 0 ) , -1 ) i += 20 p1 = ( int( zero[0][0][0] ) , int( zero[0][0][1] ) ) p3 = ( int( nose_end_point2D500[0][0][0] ) , int( nose_end_point2D500[0][0][1] ) ) cv2.line( img, p1, p3, (0, 255, 255 ), 2 ) cv2.imshow( "RESULT", img ) cv2.waitKey(0) cv2.destroyAllWindows()
なんとなくうまくいっているようにも見えます。アゴから顔の向きの棒が伸びるようにしていますが、ちょっと起点がずれてますね。アゴの位置を image_points から拾うのではなく、solvePnP で取得したrotation_vector, translation_vector でアゴの頂点座標を2D上にマッピングしているせいです(多分)。元となるメッシュの座標がこの女性の形状と一致していない、ということなのだと思います。けど、悪くはない、ような気がします。
カーブをメッシュに変換後のメッシュ内部を分割する
Blenderの閉じたカーブのFill Modeで面を張った時、あるいはそれをConvert→Meshした結果のポリゴンの分割結果はあまりよろしくありません。細長いポリゴンが作られがちです。そのため2.7x時代にはメッシュ内部を適当に分割して三角形が鋭角になりすぎないようにするアドオンがあったみたいだけど、3.x時代の今、簡単に駆動するそのようなアドオンを見つけることができませんでした。
でも現在のBlendernにはGeometry Nodesがあります。ってことで、それでなんとなく動くものをネット参考にしつつ組んでみた次第。
メッシュ表面にポイントを発生させて、そのポイント位置に点が一つのカーブを置き、それらカーブと元のカーブを結合し一つのカーブにしてFill Curveを通すとあら不思議、ってわけみたいです。
これはとっても点の数が多いSVGファイルを読み込んだカーブから作った新潟県。
まあまあこれで回避できるかもと思っております。ってか、このFill Curveに相当する機能を、Curveオブジェクトのプロパティに持たせておいてくれるだけでいいのにね。
アルファチャンネルをもとにポリゴンを作るアドオン
画像のアルファチャンネルでメッシュを切り抜くという機能を探してて、そんな機能のアドオンにkei2mってのがあったので入れてみました。このアドオンのページにリンク貼るとgooブログのエディタ上でなぜか不正な書式ってなるのでリンク貼ってません。
上図、右側のアルファチャンネルをもったテクスチャから左側のようなメッシュを作ってくれる。これはこれで素敵なのだけど、僕の目的には合致しませんでした。目的というのは、透明部分はアルファチャンネルで作りたいってことで、アルファチャンネルの形よりちょっと大きいポリゴンを作ってくれればいいってわけです。
まあその挙動をするアドオンは既に見つけてて、以前も書いたけど、Tesselate texture plane [github]ってやつです。検証した当時(半年ほど前)2.92以前のみって感じだったんですよね。だからBlenderの現行バージョンでも動くものを探してました。と思ってこちらのアドオンページ行ってみたら、三ヶ月前にPythonファイルが更新されてて、2.93と3.0に対応したようです。これは試してみないとね、と思ったのだけど、このアドオンはPythonのモジュールを二つインストールする必要があり、BlenderのPythonからpipを使ってインストールすると、そのうちの一つ、Triangleが失敗します。
失敗する場合、手動で入れろって上記 Tesselate texture plane のプロジェクトのページには書いてあります。けど、Python 3.10環境ではインストールできませんでした。<追記>どうも、M1 Macだと失敗するみたいでして、Intel Macでは3.1にも入れることができました。</追記>
あきらめて2.93を入れました…。2.93のPythonは3.9.2で以前やった方法で素直に入りました。
ちなみに Triangle ってモジュールを使って、ポリゴン内側の三角形分割をしてくれているみたいです。便利っすね、これ。
今更ながらTexTools
いやー、TexTools、入れとくべきでしたわ。UVアイランドをUV平面いっぱいに拡大したいとき、こいつで1発でしたわ。
半分に切った球のUVをいっぱいに拡大したいとき、この場合、一番上をセンターにしてV方向に二倍に拡大すればいいけど、こんなふうにわかりやすい形じゃない時、それなりに面倒なのですよね、Blender標準機能だけでやろうとした時。
そんな時はTexTools[GitHub]です。
UVを選択してFillボタンを押すとあっという間に、UV平面いっぱいに拡大されます。
このコマンドのためだけに入れる価値があると思いました。ちなみに隣にあるCropはアスペクトを変えずにUV平面にフィットさせる時に使えるみたいです。
<追記>…と思ったんですけどね。下図、左に対してFillを実行したら右になっちゃう。この場合、回ってほしくないのだけどねぇ。
</追記>
<追記>…というわけで、拙いスクリプトでカバーです。
import bpy import bmesh obj = bpy.context.active_object bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.active u = [] v = [] for face in bm.faces: for fLoop in face.loops: u.append( fLoop[uv_layer].uv[0] ) v.append( fLoop[uv_layer].uv[1] ) uScale = 1.0/( max( u ) - min( u ) ) vScale = 1.0/( max( v ) - min( v ) ) for face in bm.faces: for fLoop in face.loops: fLoop[uv_layer].uv = ( ( fLoop[uv_layer].uv[0] - min( u )) * uScale , ( fLoop[uv_layer].uv[1] - min(v) ) * vScale ) bmesh.update_edit_mesh(obj.data)
たぶん無駄な処理をしているけど、一応目的とするところはできている。速度は知らんw
</追記>
UV空間のUVアイランドの中心にポイントを集める
前書いたの、ポリゴンごとの中心にまとめる、だった。
で、欠点はありつつも、前書いたのを拡張する形でUV空間でのポリゴンアイランドとかUVシェルとか呼ばれる塊ごとにその中心にポイントを集約するのを描きました。
import bpy import bpy_extras.mesh_utils import mathutils me = bpy.context.object.data islands = bpy_extras.mesh_utils.mesh_linked_uv_islands(me) uv_layer = me.uv_layers.active.data for island in islands: v = mathutils.Vector([0.,0.]) pIndex = [] for i in island: for loop_index in range( me.polygons[i].loop_start , me.polygons[i].loop_start + me.polygons[i].loop_total ): pIndex.append( loop_index ) pIndex = list(set(pIndex)) for i in pIndex: v += uv_layer[i].uv v = v / len(pIndex) for i in pIndex: uv_layer[i].uv = v
bpy_extras.mesh_utils.mesh_linked_uv_islands ってのがbpy_extras.mesh_utilsに用意されているのでそれを利用した形。
欠点というのはUV空間内で別のアイランドに属するポイント位置が完全に重なっていると、そのポイントたちが所属するアイランドが一つと見做されちゃうっぽいことです(そのポイント同士は接続されていないのだけどね)。そんな状況を作らなければいいのだけど、使いたい場面はそんな状況だったのでやっぱり困ってる。
ネットを見ると、これより遥かに複雑な書き方で実現しているものを見るので、そっちの書き方を試すかなぁ
UVのポリゴンの中心に頂点を集める
UV空間内のポリゴンの中心に頂点を集める方法がわからなかったので簡単なスクリプトを書いた。
import bpy import mathutils #https://zenn.dev/kurogitsune/scraps/7f5e9484dd8585 #ほぼそのまんま me = bpy.context.object.data uv_layer = me.uv_layers.active.data for poly in me.polygons: v = mathutils.Vector([0.,0.]) for loop_index in range( poly.loop_start , poly.loop_start + poly.loop_total ): v += uv_layer[loop_index].uv v = v / poly.loop_total for loop_index in range( poly.loop_start , poly.loop_start + poly.loop_total ): uv_layer[loop_index].uv = v