本日は Blender の小ネタ枠です。
Bevel Curve Tools アドオンを使ってBlenderで髪の毛オブジェクトを作る手順を記事にします。
前回記事の続きです。
bluebirdofoz.hatenablog.com
曲線オブジェクトをメッシュに変更する
今回からは一旦 Bevel Curve Tools アドオンでの編集を確定して仕上げを行っていきます。
最初に、形状の編集を行えるようにするため、全ての曲線オブジェクトをメッシュに変更します。
アウトライナーウィンドウから全てのカーブオブジェクトを選択します。
[カーブ]タブから[To Mesh(es)]をクリックして、メッシュへの変換を行います。
これで全てのオブジェクトがメッシュオブジェクトに変換されました。
オブジェクトを左右対称にする
次に、各メッシュオブジェクトにミラーモディファイアを適用して左右対称にしていきます。
中央のメッシュオブジェクトを一例に手順を書き進めます。
オブジェクトを半分にカットする
前回作成した曲線オブジェクトは断面を簡略化したため、下部の中央に頂点がありません。
このため、まずは中央の部分に頂点を作成する必要があります。
今回は左右非対称な場合でも中央の部分に頂点を作成できるようブーリアンモディファイアを活用してみます。
最初にカット行う範囲を指定するためのキューブオブジェクトを作成します。
以下のように左半分を覆う形で立方体オブジェクトを配置しました。
このとき、中央の点にピッタリ重なるように立方体を置くと、ブーリアンの演算がエラーを起こすので僅かにずらしています。
再び、左右対称にしたいオブジェクトを選択して、[モディファイア]タブを開きます。
[モディファイアーの追加]から[ブーリアン]モディファイアを選択します。
[演算]に[差分]を選択し、[オブジェクト]に作成した[Cube]オブジェクトを指定します。
[重複のしきい値]は先ほどのずらしに合わせて調整します。
これでオブジェクトが半分にカットされました。
最後に、ブーリアンモディファイアを適用します。
立方体オブジェクトを非表示にすると、綺麗に分割されていることが分かります。
処理の自動化
上記の作業を全てのメッシュに実施します。
手作業で実施すると時間がかかる上、再加工が大変になってしまうのでスクリプト化しました。
以下の記事の python スクリプトの関数を全てのオブジェクトに対して処理を行います。
bluebirdofoz.hatenablog.com
# bpyインポート import bpy # メッシュ操作のため import bmesh # ブーリアンモディファイアを使って対象オブジェクトをX軸で分割する # その後、ミラーモディファイアでX軸対称のオブジェクトとする def apply_boolean_mirror(arg_objectname="Default") -> bool: """ブーリアンモディファイアを使って対象オブジェクトをX軸で分割する その後、ミラーモディファイアでX軸対称のオブジェクトとする Keyword Arguments: arg_objectname {str} -- 対象オブジェクト名 (default: {"Default"}) Returns: bool -- 実行正否 """ # 指定オブジェクトを取得する # (get関数は対象が存在しない場合 None が返る) selectob = bpy.data.objects.get(arg_objectname) # 指定オブジェクトが存在するか確認する if selectob == None: # 指定オブジェクトが存在しない場合は処理しない return False # オブジェクトがメッシュであるか確認する if selectob.type != 'MESH': # 指定オブジェクトがメッシュでない場合は処理しない return False # ブーリアン用モディファイアの名前を指定する boolean_objname = "Difference" # 中央頂点でのブーリアン演算のエラーを防ぐため # 調整用のずらしと各判定しきい値を設定する diff_shift = 0.0001 cleanup_threshold = diff_shift * 10 delzero_threshold = cleanup_threshold * 2 mirror_threshold = delzero_threshold * 2 # 対象オブジェクトのトランスフォームを適用する trans_result = transform_apply_target(arg_objectname=arg_objectname) if trans_result != True: # 適用に失敗した場合は処理しない return False # ブーリアンモディファイアの差分検出用Cubeを作成する make_result = make_cube_difference(arg_objectname=arg_objectname, arg_makename=boolean_objname, arg_shift=diff_shift) if make_result != True: # Cube作成に失敗した場合は処理しない return False # ブーリアンモディファイアの差分適用を実行する apply_result = apply_boolean_difference(arg_objectname=arg_objectname, arg_diffname=boolean_objname) if apply_result != True: # ブーリアン作成に失敗した場合は処理しない return False # オブジェクトの孤立を削除(どの面にもつながっていない辺や頂点を削除する)を行う cleanup_result = cleanup_mesh_object(arg_objectname=arg_objectname, arg_threshold=cleanup_threshold) if cleanup_result != True: # 削除に失敗した場合は処理しない return False # X軸ゼロにある面を削除する delzero_result = delete_face_xzero(arg_objectname=arg_objectname, arg_threshold=delzero_threshold) if delzero_result != True: # 削除に失敗した場合は処理しない return False # X軸のミラーモディファイアを適用する mirror_result = apply_mirror_Xclipping(arg_objectname=arg_objectname, arg_threshold=mirror_threshold) if mirror_result != True: # ミラーに失敗した場合は処理しない return False return True # 対象オブジェクトのトランスフォームを適用する def transform_apply_target(arg_objectname="Default") -> bool: """対象オブジェクトのトランスフォームを適用する Keyword Arguments: arg_objectname {str} -- 対象オブジェクト名 (default: {"Default"}) Returns: bool -- 実行の正否 """ # 指定オブジェクトを取得する # (get関数は対象が存在しない場合 None が返る) selectob = bpy.data.objects.get(arg_objectname) # 指定オブジェクトが存在するか確認する if selectob == None: # 指定オブジェクトが存在しない場合は処理しない return False # 不要なオブジェクトを選択しないように # 全てのオブジェクトを走査する for ob in bpy.context.scene.objects: # 非選択状態に設定する ob.select_set(False) # 指定のオブジェクトのみを選択状態にする selectob.select_set(True) # 対象オブジェクトをアクティブに変更する bpy.context.view_layer.objects.active = selectob # 全てのトランスフォームを適用する bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) return True # ブーリアンモディファイアの差分検出用Cubeを作成する def make_cube_difference(arg_objectname="Default", arg_makename="Difference", arg_shift=0.0) -> bool: """ブーリアンモディファイアの差分検出用Cubeを作成する Keyword Arguments: arg_objectname {str} -- 指定オブジェクト名 (default: {"Default"}) arg_makename {str} -- 作成オブジェクト名 (default: {"Difference"}) arg_shift {float} -- ずらし (default: {0.0}) Returns: bool -- 実行正否 """ # 指定オブジェクトを取得する # (get関数は対象が存在しない場合 None が返る) selectob = bpy.data.objects.get(arg_objectname) # 指定オブジェクトが存在するか確認する if selectob == None: # 指定オブジェクトが存在しない場合は処理しない return False # 指定オブジェクトの境界情報を取得する # オブジェクトのインタフェース # (https://docs.blender.org/api/current/bpy.types.Object.html) target_boundbox = selectob.bound_box # 変数の初期化 vertcount = 0 center_x = 0.0 center_y = 0.0 center_z = 0.0 # 頂点座標の加算 for vert_boundbox in target_boundbox: center_x += vert_boundbox[0] center_y += vert_boundbox[1] center_z += vert_boundbox[2] vertcount += 1 # 中点の計算 center_x = center_x / vertcount center_y = center_y / vertcount center_z = center_z / vertcount # 寸法の取得 dimension_x = selectob.dimensions[0] dimension_y = selectob.dimensions[1] dimension_z = selectob.dimensions[2] # もし作成予定と同名のメッシュオブジェクトが既に存在していれば新規作成しない if arg_makename in bpy.data.objects: return False # Cubeオブジェクトを作成する bpy.ops.mesh.primitive_cube_add( size=2.0, location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0) ) # 作成したオブジェクトの参照を取得する makeob = bpy.context.view_layer.objects.active # 作成オブジェクト名を変更する makeob.name = arg_makename # 作成オブジェクトの位置を中点位置に変更する makeob.location = (center_x, center_y, center_z) # 作成オブジェクトのスケールを設定する # (対象オブジェクトより少し大きめのサイズにする) makeob.scale = ((dimension_x + 1.0) / 2.0, (dimension_y + 1.0) / 2.0, (dimension_z + 1.0) / 2.0) # 作成オブジェクトをX軸のマイナス側に移動する makeob.location[0] = -1.0 * ((dimension_x + 1.0) / 2.0) # 中央頂点でのブーリアン演算のエラーを防ぐためのずらし量を設定する makeob.location[0] = makeob.location[0] + arg_shift return True # 対象オブジェクトにブーリアンモディファイアの差分を適用する # 差分に用いたオブジェクトは削除する def apply_boolean_difference(arg_objectname="Default", arg_diffname="Difference") -> bool: """対象オブジェクトにブーリアンモディファイアの差分を適用する 差分に用いたオブジェクトは削除する Keyword Arguments: arg_objectname {str} -- 対象オブジェクト名 (default: {"Default"}) arg_diffname {str} -- 差分オブジェクト名 (default: {"Difference"}) Returns: bool -- 実行正否 """ # 差分オブジェクトを取得する # (get関数は対象が存在しない場合 None が返る) diffob = bpy.data.objects.get(arg_diffname) # 差分オブジェクトが存在するか確認する if diffob == None: # 差分オブジェクトが存在しない場合は処理しない return False # 指定オブジェクトを取得する # (get関数は対象が存在しない場合 None が返る) selectob = bpy.data.objects.get(arg_objectname) # 指定オブジェクトが存在するか確認する if selectob == None: # 指定オブジェクトが存在しない場合は処理しない return False # 指定オブジェクトをアクティブに変更する bpy.context.view_layer.objects.active = selectob # 「ブーリアン」モディファイアを追加する # ブーリアンモディファイアのインタフェース # (https://docs.blender.org/api/current/bpy.types.BooleanModifier.html) bpy.ops.object.modifier_add(type='BOOLEAN') # 追加されたモディファイアを取得する boolean_modifier = selectob.modifiers[-1] # 演算方法に[差分]を指定する boolean_modifier.operation = 'DIFFERENCE' # 統合対象にオブジェクトを指定する boolean_modifier.object = diffob # 重複の敷居値を指定する boolean_modifier.double_threshold = 0.000001 # 「ブーリアン」モディファイアを適用する bpy.ops.object.modifier_apply(apply_as='DATA',modifier=boolean_modifier.name) # 統合したオブジェクトを削除する bpy.data.objects.remove(diffob) return True # オブジェクトに以下のクリーンアップを実行 # ・大きさ0を融解:面積が0の面を削除し、1つの頂点にまとめる # ・孤立を削除:どの面にもつながっていない辺や頂点を削除する # ・重複頂点を削除:重複している頂点を1つの頂点にまとめる def cleanup_mesh_object(arg_objectname="Default", arg_threshold=0.0001) -> bool: """オブジェクトにクリーンアップを実行 ・大きさ0を融解:面積が0の面を削除し、1つの頂点にまとめる ・孤立を削除:どの面にもつながっていない辺や頂点を削除する ・重複頂点を削除:重複している頂点を1つの頂点にまとめる Keyword Arguments: arg_objectname {str} -- 指定オブジェクト名 (default: {"Default"}) arg_threshold {float} -- 結合しきい値 (default: {0.0001}) Returns: bool -- 実行の正否 """ # 他のオブジェクトに操作を適用しないよう全てのオブジェクトを走査する for ob in bpy.context.scene.objects: # 非選択状態に設定する ob.select_set(False) # 指定オブジェクトを取得する # (get関数は対象が存在しない場合 None が返る) targetob = bpy.data.objects.get(arg_objectname) # 指定オブジェクトが存在するか確認する if targetob == None: # 指定オブジェクトが存在しない場合は処理しない return False # 変更オブジェクトをアクティブに変更する bpy.context.view_layer.objects.active = targetob # 編集モードに移行する bpy.ops.object.mode_set(mode='EDIT', toggle=False) # 頂点を全選択した状態とする bpy.ops.mesh.select_all(action='SELECT') # 大きさ0を融解(結合距離 0.01) bpy.ops.mesh.dissolve_degenerate(threshold=0.01) # 変更を反映するため再び頂点を全選択 bpy.ops.mesh.select_all(action='SELECT') # 孤立を削除(頂点、辺のみ) bpy.ops.mesh.delete_loose(use_verts=True, use_edges=True, use_faces=False) # 孤立を削除で全選択が解除されるので再び頂点を全選択 bpy.ops.mesh.select_all() # 重複頂点を削除(結合距離、非選択部の結合無効) bpy.ops.mesh.remove_doubles(threshold=arg_threshold, use_unselected=False) # オブジェクトモードに移行する bpy.ops.object.mode_set(mode='OBJECT', toggle=False) return True # X軸ゼロにある面を削除する def delete_face_xzero(arg_objectname="Default", arg_threshold=0.0001) -> bool: """X軸ゼロにある面を削除する Keyword Arguments: arg_objectname {str} -- 対象オブジェクト名 (default: {"Default"}) arg_threshold {float} -- 選択しきい値 (default: {0.0001}) Returns: bool -- 実行正否 """ # 指定オブジェクトを取得する # (get関数は対象が存在しない場合 None が返る) selectob = bpy.data.objects.get(arg_objectname) # 指定オブジェクトが存在するか確認する if selectob == None: # 指定オブジェクトが存在しない場合は処理しない return False # 不要なオブジェクトを選択しないように # 全てのオブジェクトを走査する for ob in bpy.context.scene.objects: # 非選択状態に設定する ob.select_set(False) # 指定のオブジェクトのみを選択状態にする selectob.select_set(True) # 対象オブジェクトをアクティブに変更する bpy.context.view_layer.objects.active = selectob # 編集モードに移行する # モード切替のマニュアル # (https://docs.blender.org/api/current/bpy.ops.object.html#bpy.ops.object.mode_set) bpy.ops.object.mode_set(mode='EDIT', toggle=False) # メッシュデータを取得する # メッシュアクセスのマニュアル # (https://docs.blender.org/api/current/bmesh.types.html?highlight=bmedge#bmesh.types.BMesh) meshdata = bmesh.from_edit_mesh(selectob.data) # 選択モードを面選択モードにする meshdata.select_mode = {'VERT'} # 一度選択を全てクリアする bpy.ops.mesh.select_all(action='DESELECT') # 頂点を走査する # 頂点アクセスのマニュアル # (https://docs.blender.org/api/current/bmesh.types.html?highlight=bmedge#bmesh.types.BMVert) for v in meshdata.verts: # X軸の値がしきい値以下の頂点を取得する if v.co.x < arg_threshold: # 頂点を選択状態にする v.select_set(True) else: # 頂点を非選択状態にする v.select_set(False) # 辺または面の選択状態を更新する # (頂点の選択による面選択を有効化するため) meshdata.select_flush_mode() # 断面の面を削除する bpy.ops.mesh.delete(type='FACE') # オブジェクトモードに移行する bpy.ops.object.mode_set(mode='OBJECT', toggle=False) return True # X軸のミラーモディファイアを適用する def apply_mirror_Xclipping(arg_objectname="Default", arg_threshold=0.0001) -> bool: """対象オブジェクトにブーリアンモディファイアの差分を適用する Keyword Arguments: arg_objectname {str} -- 対象オブジェクト名 (default: {"Default"}) arg_threshold {float} -- 結合しきい値 (default: {0.0001}) Returns: bool -- 実行正否 """ # 指定オブジェクトを取得する # (get関数は対象が存在しない場合 None が返る) selectob = bpy.data.objects.get(arg_objectname) # 指定オブジェクトが存在するか確認する if selectob == None: # 指定オブジェクトが存在しない場合は処理しない return False # 指定オブジェクトをアクティブに変更する bpy.context.view_layer.objects.active = selectob # 「ミラー」モディファイアを追加する # ミラーモディファイアのインタフェース # (https://docs.blender.org/api/current/bpy.types.MirrorModifier.html) bpy.ops.object.modifier_add(type='MIRROR') # 作成モディファイアを取得する mirror_modifier = selectob.modifiers[-1] # X軸でミラー mirror_modifier.use_axis[0] = True mirror_modifier.use_axis[1] = False mirror_modifier.use_axis[2] = False # オブション設定(結合,クリッピング,頂点グループ) mirror_modifier.use_mirror_merge = True mirror_modifier.use_clip = True mirror_modifier.use_mirror_vertex_groups = True # 結合距離 mirror_modifier.merge_threshold = arg_threshold # 「ミラー」モディファイアを適用する bpy.ops.object.modifier_apply(apply_as='DATA',modifier=mirror_modifier.name) return True # オブジェクトのバウンドボックスから中点を計算する def get_object_center(arg_objectname="Default") -> list: """オブジェクトのバウンドボックスから中点を計算する Keyword Arguments: arg_objectname {str} -- 対象オブジェクト名 (default: {"Default"}) Returns: list -- 中点座標 """ # 指定オブジェクトを取得する # (get関数は対象が存在しない場合 None が返る) selectob = bpy.data.objects.get(arg_objectname) # 指定オブジェクトが存在するか確認する if selectob == None: # 指定オブジェクトが存在しない場合は処理しない return [0.0, 0.0, 0.0] # 指定オブジェクトの境界情報を取得する # オブジェクトのインタフェース # (https://docs.blender.org/api/current/bpy.types.Object.html) target_boundbox = selectob.bound_box # 変数の初期化 vertcount = 0 center_x = 0.0 center_y = 0.0 center_z = 0.0 # 頂点座標の加算 for vert_boundbox in target_boundbox: center_x += vert_boundbox[0] center_y += vert_boundbox[1] center_z += vert_boundbox[2] vertcount += 1 # 中点の計算 center_x = center_x / vertcount center_y = center_y / vertcount center_z = center_z / vertcount return [center_x, center_y, center_z] for mesh in bpy.data.objects: if mesh.type == 'MESH': apply_boolean_mirror(arg_objectname=mesh.name)
これで左右対称なメッシュオブジェクトに加工することができました。
次はシャープの設定と辺分離を使って陰影を修正します。
bluebirdofoz.hatenablog.com