【Blender】ハイポリのシェイプキーをローポリに複製する

投稿者: | 2024-04-09

ハイポリメッシュのシェイプキーをローポリメッシュに複製してみました。アドオンをインストールして使用します。

概要

ローポリメッシュの頂点を走査し、指定の範囲内にあるハイポリメッシュの頂点リストを記憶します。ハイポリメッシュをシェイプキーで変形した後の頂点の平均値を新しい頂点位置として、ローポリメッシュにシェイプキーを作成します。

スクリプト

__init__.py

# -*- coding: utf-8 -*-

bl_info = {
    "name": "ShapeKey Transmuter",
    "description": "Transfers shape keys between meshes based on vertex proximity.",
    "author": "Your Name",
    "version": (1, 0),
    "blender": (2, 93, 0),
    "location": "View3D > Sidebar > Tools Tab",
    "category": "Object"
}


import bpy
from .main_logic import perform_action

def get_vertex_groups(self, context):
    items = [('NONE', "None", "")]
    if self.target_object:
        items.extend([(vg.name, vg.name, "") for vg in self.target_object.vertex_groups])
    return items

def get_shape_keys(self, context):
    items = []
    if self.source_object and self.source_object.data.shape_keys:
        for key in self.source_object.data.shape_keys.key_blocks:
            items.append((key.name, key.name, ""))
    return items

class ShapeKeyToolProperties(bpy.types.PropertyGroup):
    radius: bpy.props.FloatProperty(name="Radius")
    source_object : bpy.props.PointerProperty(
        name="Source Object",
        type=bpy.types.Object,
        poll=lambda self, object: object.type == 'MESH'
    )
    target_object : bpy.props.PointerProperty(
        name="Target Object",
        type=bpy.types.Object,
        poll=lambda self, object: object.type == 'MESH'
    )
    vertex_group: bpy.props.EnumProperty(name="Vertex Group", items=get_vertex_groups)
    shape_key: bpy.props.EnumProperty(name="Shape Key", items=get_shape_keys)


class SKT_ShapeKeyOperator(bpy.types.Operator):
    bl_idname = "object.skt_shape_key_operator"
    bl_label = "Apply Shape Key"

    @classmethod
    def poll(cls, context):
        props = context.scene.shape_key_tool_props
        # オブジェクト、シェイプキー、頂点グループが適切に選択されていることを確認
        return props.source_object and props.target_object and props.shape_key and props.vertex_group

    def execute(self, context):
        perform_action(context)
        return {'FINISHED'}


class SKT_SimplePanel(bpy.types.Panel):
    bl_label = "ShapeKey Transmuter Panel"
    bl_idname = "OBJECT_PT_custom"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Tools'

    def draw(self, context):
        layout = self.layout
        tool_props = context.scene.shape_key_tool_props
        layout.prop(tool_props, "source_object")
        if tool_props.source_object:
            layout.prop(tool_props, "shape_key")
        layout.prop(tool_props, "target_object")
        if tool_props.target_object:
            layout.prop(tool_props, "vertex_group")
        layout.prop(tool_props, "radius")
        layout.operator("object.skt_shape_key_operator")

# カスタムプロパティをシーンに追加
def register():
    bpy.utils.register_class(ShapeKeyToolProperties)
    bpy.types.Scene.shape_key_tool_props = bpy.props.PointerProperty(type=ShapeKeyToolProperties)
    bpy.utils.register_class(SKT_SimplePanel)
    bpy.utils.register_class(SKT_ShapeKeyOperator)
    # その他の登録処理

def unregister():
    del bpy.types.Scene.shape_key_tool_props
    bpy.utils.unregister_class(ShapeKeyToolProperties)
    bpy.utils.unregister_class(SKT_SimplePanel)
    bpy.utils.unregister_class(SKT_ShapeKeyOperator)
    # その他の解除処理

if __name__ == "__main__":
    register()

main_logic.py

import bpy
import bmesh
from mathutils import Vector
import numpy as np

def perform_action(context):
    # シェイプキー名をプロパティから取得
    shape_key_name = context.scene.shape_key_tool_props.shape_key

    if not shape_key_name:
        # 適切なシェイプキーが設定されていない場合のエラー処理
        raise Exception("シェイプキーが設定されていません。")

    # ターゲットメッシュ(変更対象)とソースメッシュ(変更元)を決定
    target_mesh = context.scene.shape_key_tool_props.target_object
    source_mesh = context.scene.shape_key_tool_props.source_object

    if not target_mesh or target_mesh.type != 'MESH':
        raise Exception("ターゲットオブジェクトが適切に設定されていないか、メッシュタイプではありません。")

    if not source_mesh or source_mesh.type != 'MESH':
        raise Exception("ソースオブジェクトが適切に設定されていないか、メッシュタイプではありません。")


    # メッシュデータを取得
    target_bmesh = bmesh.new()
    target_bmesh.from_mesh(target_mesh.data)
    source_bmesh = bmesh.new()
    source_bmesh.from_mesh(source_mesh.data)

    # 頂点の影響範囲を定義
    radius = context.scene.shape_key_tool_props.radius

    # 頂点グループを取得
    vertex_group_name = context.scene.shape_key_tool_props.vertex_group

    # 頂点グループが設定されていることを確認
    if not vertex_group_name:
        raise Exception("頂点グループが設定されていません。")

    # ユーザーが「None」を選択した場合は全頂点を対象とする
    if vertex_group_name != 'NONE':
        vertex_group = target_mesh.vertex_groups.get(vertex_group_name)
        if vertex_group:
            vgroup_indices = {v.index for v in target_mesh.data.vertices if vertex_group.index in [g.group for g in v.groups]}
    else:
        vgroup_indices = set(range(len(target_mesh.data.vertices)))


    # 全てのシェイプキーを0に設定
    for key_block in source_mesh.data.shape_keys.key_blocks:
        key_block.value = 0.0
        
    # 頂点グループを考慮して半径内の対応する頂点を探す
    vertex_groups = [[] for _ in range(len(target_bmesh.verts))]
    for i, target_vert in enumerate(target_bmesh.verts):
        if target_vert.index in vgroup_indices:
            closest_vert = None
            min_dist = float('inf')
            for source_vert in source_bmesh.verts:
                dist = (target_vert.co - source_vert.co).length
                if dist <= radius:
                    vertex_groups[i].append(source_vert.index)
                elif dist < min_dist:
                    min_dist = dist
                    closest_vert = source_vert
            if not vertex_groups[i]:
                vertex_groups[i].append(closest_vert.index)

    # 指定されたシェイプキーのみを1に設定
    source_mesh.data.shape_keys.key_blocks[shape_key_name].value = 1.0
    source_mesh.data.update()

    # 新しい頂点位置を計算
    new_vertex_positions = [None] * len(target_bmesh.verts)
    for target_vert, group in zip(target_bmesh.verts, vertex_groups):
        if target_vert.index in vgroup_indices and group:
            total_position = Vector((0.0, 0.0, 0.0))
            for vert_idx in group:
                shaped_vert = source_mesh.data.shape_keys.key_blocks[shape_key_name].data[vert_idx].co
                total_position += shaped_vert
            average_position = total_position / len(group)
            new_position = average_position
            new_vertex_positions[target_vert.index] = new_position
        elif target_vert.index in vgroup_indices:
            new_vertex_positions[target_vert.index] = target_vert.co

    # ターゲットメッシュのシェイプキーデータを確認し、必要に応じてBasisキーを作成
    if not target_mesh.data.shape_keys:
        target_mesh.shape_key_add(name='Basis')

    # 新しいシェイプキーを追加
    new_shape_key = target_mesh.shape_key_add(name=shape_key_name)


    # 新規シェイプキーに頂点位置を適用
    for vert_idx, new_pos in enumerate(new_vertex_positions):
        if new_pos:
            new_shape_key.data[vert_idx].co = new_pos

    # ソースメッシュのシェイプキーの値を0に設定
    source_mesh.data.shape_keys.key_blocks[shape_key_name].value = 0.0
    source_mesh.data.update()

    # 解放処理
    target_bmesh.free()
    source_bmesh.free()

    # すべてのオブジェクトの選択を解除
    bpy.ops.object.select_all(action='DESELECT')

    # ターゲットメッシュだけを選択
    target_mesh.select_set(True)
    context.view_layer.objects.active = target_mesh


設定

上記のpyファイルのみを含んだフォルダを作ります。

フォルダを圧縮してzipファイルにします。

Blenderで、Edit > Preferences... を開きます。

Add-onsで「Install...」ボタンから、zipファイルをインストールします。

チェックボックスを入れてアドオンを有効化します。

横の▷をクリックするとアドオンの情報が開きます。「Remove」ボタンでアドオンを削除できます。

3Dビューポートのサイドバーの「Tools」タブにパネルが表示されます。

使い方

BlenderでMonkeyを2つ追加しました。

片方はDecimateモディファイアが適用されていて、ポリゴン数が減っています。

ハイポリメッシュにシェイプキーを追加して、表情を作ります。

パネルの「Source Object」にハイポリメッシュを設定し、「Target Object」にローポリメッシュを設定します。

「Shape Key」で、追加したシェイプキーを選択します。

「Radius」を設定して、Applyボタンを押します。

ローポリメッシュにシェイプキーが追加されます。

シェイプキーのValueを1にすると、ハイポリメッシュに設定した表情になりました。

ローポリ
ハイポリ

頂点グループを適用

ローポリメッシュに頂点グループを追加します。

編集モードで一部の頂点のみを選択します。

頂点グループにウェイト1で割り当てます。

パネルの「Vertex Group」でこの頂点グループを選択します。

Applyボタンを押すと、再度シェイプキーが追加されます。頂点グループに適用した部分のみが変形します。

Key 1.001
Key 1

これでハイポリメッシュからローポリメッシュへシェイプキーを複製できました。

プロパティ

パネルで設定するオブジェクト参照や各値はプロパティに設定されます。オブジェクトのプロパティはメッシュタイプのオブジェクトだけを受け入れます。

class ShapeKeyToolProperties(bpy.types.PropertyGroup):
    radius: bpy.props.FloatProperty(name="Radius")
    source_object : bpy.props.PointerProperty(
        name="Source Object",
        type=bpy.types.Object,
        poll=lambda self, object: object.type == 'MESH'
    )
    target_object : bpy.props.PointerProperty(
        name="Target Object",
        type=bpy.types.Object,
        poll=lambda self, object: object.type == 'MESH'
    )
    vertex_group: bpy.props.EnumProperty(name="Vertex Group", items=get_vertex_groups)
    shape_key: bpy.props.EnumProperty(name="Shape Key", items=get_shape_keys)

# ...

def register():
    bpy.utils.register_class(ShapeKeyToolProperties)
    bpy.types.Scene.shape_key_tool_props = bpy.props.PointerProperty(type=ShapeKeyToolProperties)

現在のシーンのコンテキストを使用して、プロパティグループ内のプロパティの値を取得できます。

    target_mesh = context.scene.shape_key_tool_props.target_object
    source_mesh = context.scene.shape_key_tool_props.source_object

スクリプト

シェイプキーやオブジェクト、半径などのプロパティ値を取得します。オブジェクトからメッシュデータを作成します。

import bpy
import bmesh
from mathutils import Vector
import numpy as np

def perform_action(context):
    # シェイプキー名をプロパティから取得
    shape_key_name = context.scene.shape_key_tool_props.shape_key

    if not shape_key_name:
        # 適切なシェイプキーが設定されていない場合のエラー処理
        raise Exception("シェイプキーが設定されていません。")

    # ターゲットメッシュ(変更対象)とソースメッシュ(変更元)を決定
    target_mesh = context.scene.shape_key_tool_props.target_object
    source_mesh = context.scene.shape_key_tool_props.source_object

    if not target_mesh or target_mesh.type != 'MESH':
        raise Exception("ターゲットオブジェクトが適切に設定されていないか、メッシュタイプではありません。")

    if not source_mesh or source_mesh.type != 'MESH':
        raise Exception("ソースオブジェクトが適切に設定されていないか、メッシュタイプではありません。")


    # メッシュデータを取得
    target_bmesh = bmesh.new()
    target_bmesh.from_mesh(target_mesh.data)
    source_bmesh = bmesh.new()
    source_bmesh.from_mesh(source_mesh.data)

    # 頂点の影響範囲を定義
    radius = context.scene.shape_key_tool_props.radius

頂点グループを取得します。パネルで「None」が選択されていると頂点グループを使用しません。

    # 頂点グループを取得
    vertex_group_name = context.scene.shape_key_tool_props.vertex_group

    # 頂点グループが設定されていることを確認
    if not vertex_group_name:
        raise Exception("頂点グループが設定されていません。")

    # ユーザーが「None」を選択した場合は全頂点を対象とする
    if vertex_group_name != 'NONE':
        vertex_group = target_mesh.vertex_groups.get(vertex_group_name)
        if vertex_group:
            vgroup_indices = {v.index for v in target_mesh.data.vertices if vertex_group.index in [g.group for g in v.groups]}
    else:
        vgroup_indices = set(range(len(target_mesh.data.vertices)))

ハイポリメッシュの全てのシェイプキーの値を0にします。

    # 全てのシェイプキーを0に設定
    for key_block in source_mesh.data.shape_keys.key_blocks:
        key_block.value = 0.0

ローポリメッシュの頂点ごとに頂点リストを保持します。ハイポリメッシュの頂点を走査し、距離が半径以下ならリストに追加します。範囲内になければもっとも近くの頂点を追加しています。シェイプキーでの変形を追跡できるようにインデックスを記憶しています。

    # 頂点グループを考慮して半径内の対応する頂点を探す
    vertex_groups = [[] for _ in range(len(target_bmesh.verts))]
    for i, target_vert in enumerate(target_bmesh.verts):
        if target_vert.index in vgroup_indices:
            closest_vert = None
            min_dist = float('inf')
            for source_vert in source_bmesh.verts:
                dist = (target_vert.co - source_vert.co).length
                if dist <= radius:
                    vertex_groups[i].append(source_vert.index)
                elif dist < min_dist:
                    min_dist = dist
                    closest_vert = source_vert
            if not vertex_groups[i]:
                vertex_groups[i].append(closest_vert.index)

ハイポリメッシュのシェイプキーの値を1にします。シェイプキーはパネルで設定されています。

    # 指定されたシェイプキーのみを1に設定
    source_mesh.data.shape_keys.key_blocks[shape_key_name].value = 1.0
    source_mesh.data.update()

ローポリメッシュの頂点を一つずつ見ていって、リスト内の現在の頂点位置の平均値を新しい頂点位置にします。リスト内の頂点の位置はシェイプキーの値によって移動されています。

    # 新しい頂点位置を計算
    new_vertex_positions = [None] * len(target_bmesh.verts)
    for target_vert, group in zip(target_bmesh.verts, vertex_groups):
        if target_vert.index in vgroup_indices and group:
            total_position = Vector((0.0, 0.0, 0.0))
            for vert_idx in group:
                shaped_vert = source_mesh.data.shape_keys.key_blocks[shape_key_name].data[vert_idx].co
                total_position += shaped_vert
            average_position = total_position / len(group)
            new_position = average_position
            new_vertex_positions[target_vert.index] = new_position
        elif target_vert.index in vgroup_indices:
            new_vertex_positions[target_vert.index] = target_vert.co

ローポリメッシュに、指定のシェイプキーと同じ名前のシェイプキーを追加します。Basisがなければ作成します。

    # ターゲットメッシュのシェイプキーデータを確認し、必要に応じてBasisキーを作成
    if not target_mesh.data.shape_keys:
        target_mesh.shape_key_add(name='Basis')

    # 新しいシェイプキーを追加
    new_shape_key = target_mesh.shape_key_add(name=shape_key_name)

作成した頂点をシェイプキーに適用します。

    # 新規シェイプキーに頂点位置を適用
    for vert_idx, new_pos in enumerate(new_vertex_positions):
        if new_pos:
            new_shape_key.data[vert_idx].co = new_pos

最後にシェイプキーの値を0に戻したり、メモリの解放、選択解除などを行っています。

    # ソースメッシュのシェイプキーの値を0に設定
    source_mesh.data.shape_keys.key_blocks[shape_key_name].value = 0.0
    source_mesh.data.update()

    # 解放処理
    target_bmesh.free()
    source_bmesh.free()

    # すべてのオブジェクトの選択を解除
    bpy.ops.object.select_all(action='DESELECT')

    # ターゲットメッシュだけを選択
    target_mesh.select_set(True)
    context.view_layer.objects.active = target_mesh

コメントを残す

メールアドレスが公開されることはありません。