- Blender Add-on -

Add-on集

ちょっとしたアドオンの作り方がわかってきたので、幾つか公開してみます。

アドオンのお作法などは見よう見まねなので、そこのとこころはご容赦を…

メッシュ(その他データ)の名前をオブジェクトの名前に合わせる

ランダムに選択解除しながら Extrude を繰り返す

1つのオブジェクト内の選択された頂点に対してMirrorをかける

Placeholder(空のファイル)を利用して、指定したフレームのレンダリングを省略する

メッシュ(その他データ)の名前をオブジェクトの名前に合わせる

Cube.001 やら Cylinder.002 など元の名前のまま残っているメッシュデータを、オブジェクトの名前に変更します。
2つ以上のオブジェクトで共用されているメッシュデータの場合は、確認用ポップアップが出てくるので、そこで再度押すと実行されます。

DataRenamer.py

# Blender内部のデータ構造にアクセスするために必要
import bpy
import mathutils

# プラグインに関する情報
bl_info = {
    "name" : "DataRenamer",             # プラグイン名
    "author" : "Q@StudioPotpourri",     # 作者
    "version" : (0,4),                  # プラグインのバージョン
    "blender" : (2, 7, 7),              # プラグインが動作するBlenderのバージョン
    "location" : "Properties > Object > DataRenamer",   # Blender内部でのプラグインの位置づけ
    "description" : "Rename mesh",   # プラグインの説明
    "warning" : "",
    "wiki_url" : "",                    # プラグインの説明が存在するWikiページのURL
    "tracker_url" : "",                 # Blender Developer OrgのスレッドURL
    "category" : "Object"                   # プラグインのカテゴリ名
    }

class RenamerForce(bpy.types.Operator):
    """RenamerForce"""
    bl_idname = "object.renamerforce"
    bl_label = "RenameForce"
    bl_options = {'UNDO'}

    def execute(self, context):
        object = context.object
        name = object.name
        object.data.name = name
        
        return {'FINISHED'}

class Renamer(bpy.types.Operator):
    """Renamer"""

    bl_idname = "object.renamer"
    bl_label = "DataRenamer"
    bl_options = {'REGISTER','UNDO'}

    def invoke(self, context, event):
        object = context.object
        name = object.name

        if (object.name == object.data.name):
            return {'CANCELLED'}

        if (object.data.users == 1):
            return self.execute(context)
        else:
            return context.window_manager.invoke_props_popup(self, event)
        
        return {'CANCELLED'}

    def execute(self, context):
        object = context.object
        name = object.name;
        if (object.data.users == 1):
            object.data.name = name;
        
        return {'FINISHED'}

    def draw(self, context):
        layout = self.layout
        object = context.object
        data = context.object.data

        if (object.name == data.name):
            return

        col = layout.column()
        col.label(text="Data \"" + object.data.name + "\" has " + str(object.data.users) + " users.")
        col.operator("object.renamerforce", text="Rename it to \"" + object.name + "\" even so")

class OBJECT_PT_DataRenamer(bpy.types.Panel):
    bl_label = "DataRenamer"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "object"

    @classmethod
    def poll(cls, context):
        return (context.object.type == 'MESH' or context.object.type == 'LAMP' or context.object.type == 'CAMERA' or context.object.type == "CURVE")

    def draw(self, context):
        
        layout = self.layout
        object = context.object
        data = context.object.data
        
        l_row = layout.row();
        l_row.label(text=object.name, icon='OBJECT_DATA')
        if (object.type == 'MESH'):
            l_row.label(text=data.name, icon='MESH_DATA')
        if (object.type == 'CAMERA'):
            l_row.label(text=data.name, icon='CAMERA_DATA')
        if (object.type == 'CURVE'):
            l_row.label(text=data.name, icon='CURVE_DATA')
        if (object.type == 'LAMP'):
            if (data.type == 'POINT'):
                l_row.label(text=data.name, icon='LAMP_POINT')
            if (data.type == 'SUN'):
                l_row.label(text=data.name, icon='LAMP_SUN')
            if (data.type == 'SPOT'):
                l_row.label(text=data.name, icon='LAMP_SPOT')
            if (data.type == 'HEMI'):
                l_row.label(text=data.name, icon='LAMP_HEMI')
            if (data.type == 'AREA'):
                l_row.label(text=data.name, icon='LAMP_AREA')
            
        if (object.name == data.name and data.users == 1):
            l_row.label(text="users=" + str(data.users), icon='FILE_TICK')
        else:
            l_row.label(text="users=" + str(data.users), icon='DOT')
            if (object.name != data.name):
                layout.operator("object.renamer", text="Rename Single User Data")

def register():
    bpy.utils.register_module(__name__)

def unregister():
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()


ランダムに選択解除しながら Extrude を繰り返す

選択されている面をランダムに選択解除しながら Extrude を繰り返し、このような構造を作ります。
メッシュ分割された平面に適応すれば、このように簡易的な都市のような構造になります。
3D View の左側、メッシュツール内に登録されます。繰り返し回数や、一ステップごとの選択解除の確率を設定します。

Type: の選択肢は、上のような形状ではNormal方向へ伸ばすタイプ、下の街のような構造では Z directional を選びます。
(街のような構造の場合Normalでも見た目は一緒なのですが、ブロック境界のポリゴンに無駄が生じます)

RandomExtrude.py

# Blender内部のデータ構造にアクセスするために必要
import bpy
import mathutils
# Tool Self でプロパティ変更
from bpy.props import * 

# プラグインに関する情報
bl_info = {
    "name" : "Random Extrude",             # プラグイン名
    "author" : "Q@StudioPotpourri",                  # 作者
    "version" : (0,3),                  # プラグインのバージョン
    "blender" : (2, 7, 7),              # プラグインが動作するBlenderのバージョン
    "location" : "Mesh > Random Extrude",   # Blender内部でのプラグインの位置づけ
    "description" : "Repeat random deselect and extrude",   # プラグインの説明
    "warning" : "",
    "wiki_url" : "",                    # プラグインの説明が存在するWikiページのURL
    "tracker_url" : "",                 # Blender Developer OrgのスレッドURL
    "category" : "Mesh"                   # プラグインのカテゴリ名
    }

# メニュー
class CRandomExtrude(bpy.types.Operator):

    bl_idname = "mesh.random_extrude"      # ID名
    bl_label = "Random Extrude"            # メニューに表示される文字列
    bl_description = "Random Extrude"      # メニューに表示される説明文
    bl_options = {'REGISTER', 'UNDO'}

    # ツールシェルフへ表示させる値
    repeat = IntProperty(
        name = "Repeat",             
        description = "repeat ...",   
        default = 3,                    
        min = 0,                     
        max = 99)  

    deselectFirst = IntProperty(
        name = "First",    
        description = "Deselect Percent ...", 
        default = 0,
        subtype = 'PERCENTAGE',
        min = 0,                  
        max = 100)                    

    deselect = IntProperty(
        name = "",    
        description = "Deselect Percent ...", 
        default = 40,         
        subtype = 'PERCENTAGE',
        min = 0,                  
        max = 100) 

    seed = IntProperty(
        name = "Random seed",           
        description = "Random Seed ...",
        default = 3,
        min = 0)

    height = FloatProperty(
        name = "Step height",
        description = "lineSkip ...",
        default = 1,
        min = 0)

    Types = [("0", "Normal", "Normal"),
             ("1", "Z directional", "Z directional")]
    
    NormalTypes = EnumProperty(name="Type",
                               description ="Type",
                               items=Types)

    def draw(self, context):
        layout = self.layout;

        layout.prop(self, "repeat")
        layout.label("Deselect Percent:")
        row = layout.row()
        row.prop(self, "deselectFirst")
        row.prop(self, "deselect")

        layout.prop(self, "seed")
        layout.prop(self, "height")
        layout.prop(self, "NormalTypes")
        layout.prop(self, "normal")

    # 実際にプラグインが処理を実施する処理
    def execute(self, context):

        for num in range(0, self.repeat):
            deselect = self.deselect
            if num == 0:
                deselect = self.deselectFirst
            bpy.ops.mesh.select_random(percent=deselect, seed=self.seed, action='DESELECT')
            if int(self.NormalTypes) == 1:
                bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={"value":(0, 0, self.height)})
            else:
                bpy.ops.mesh.extrude_faces_move(TRANSFORM_OT_shrink_fatten={"value":-self.height})

        return {'FINISHED'}             # 成功した場合はFINISHEDを返す

# メニューを登録する関数
def menu_func(self, context):
    self.layout.label("Random Extrude:")
    self.layout.operator(CRandomExtrude.bl_idname)     # 登録したいクラスの「bl_idname」を指定

# プラグインをインストールしたときの処理
def register():
    bpy.utils.register_module(__name__)
    bpy.types.VIEW3D_PT_tools_meshedit.append(menu_func)

# プラグインをアンインストールしたときの処理
def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.VIEW3D_PT_tools_meshedit.remove(menu_func)

# メイン関数
if __name__ == "__main__":
    register()

1つのオブジェクト内の選択された頂点に対してMirrorをかける

内部的に行っていることとしては、選択されている頂点を分離して Mirror モディファイアをかけて 元と合成するということになります。
選択の状態を維持したり、既にモディファイアがかかっている場合などでも機能するように幾つかの工夫があります。

一旦分離して合成するという内部の操作上、一続きのつながったメッシュの一部だけを Mirror するときには、形状が分離します。一体化させたいときには Remove Double などで重複した頂点を除去するといった操作が必要になります。
3D View の左側、メッシュツール内に登録されます。Mirrorをかける軸を設定できます。

MirrorSelectedVertices.py

# Blender内部のデータ構造にアクセスするために必要
import bpy
import mathutils
# Tool Self でプロパティ変更
from bpy.props import * 

# プラグインに関する情報
bl_info = {
    "name" : "Mirror Selected Vertices",# プラグイン名
    "author" : "Q@SturioPotpourri",     # 作者
    "version" : (0,8),                  # プラグインのバージョン
    "blender" : (2, 7, 8),              # プラグインが動作するBlenderのバージョン
    "location" : "Mesh > Mirror Selected",   # Blender内部でのプラグインの位置づけ
    "description" : "Mirror Selected Vertices",   # プラグインの説明
    "warning" : "",
    "wiki_url" : "",                    # プラグインの説明が存在するWikiページのURL
    "tracker_url" : "",                 # Blender Developer OrgのスレッドURL
    "category" : "Mesh"                   # プラグインのカテゴリ名
    }

def diff(first, second):
    second = set(second)
    return [item for item in first if item not in second]

# メニュー
class CMirrorSelectedVertices(bpy.types.Operator):

    bl_idname = "mesh.mirror_selected_vertices"           # ID名
    bl_label = "Mirror Selected"            # メニューに表示される文字列
    bl_description = "Mirror Selected"        # メニューに表示される説明文
    bl_options = {'REGISTER', 'UNDO'}

    Types = [("0", "X", "X axis mirror"),
             ("1", "Y", "Y axis mirror"),
	     ("2", "Z", "Z axis mirror")]

    AxisTypes = EnumProperty(name="Axis",
			     description ="Axis",
			     items=Types)

    def draw(self, context):
	    layout = self.layout;
	    layout.prop(self, "AxisTypes")

    # 実際にプラグインが処理を実施する処理
    def execute(self, context):
        
        obj = context.object;
        mesh = obj.data

        bpy.ops.object.mode_set(mode='OBJECT')

        # Active のみ Selected に & 現状復帰用
        selectedObj = [o for o in bpy.data.objects if o.select]
        for o in selectedObj:
            if (o != obj):
                o.select = False
        
        # 頂点数チェック & 選択されたもの復帰用
        selected = [v.index for v in mesh.vertices if v.select]
        selectedNum = len(selected)
        selectedFace = [f.index for f in mesh.polygons if f.select]
        selectedNumFace = len(selectedFace)
        selectedEdge = [e.index for e in mesh.edges if e.select]
        selectedNumEdge = len(selectedEdge)
        
        if (selectedNum == 0):
            print("Selected")
            bpy.ops.object.mode_set(mode='EDIT')
            return {'CANCELLED'}

        # モディファイアの順番調整用
        modifierlist = obj.modifiers[:]
        modifiershow = [mod.show_viewport for mod in modifierlist]
        for mod in modifierlist:
            mod.show_viewport = False

        meshlist = bpy.data.objects[:]

        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.separate();
        bpy.ops.object.mode_set(mode='OBJECT')

        meshlist2 = bpy.data.objects[:]

        differenceB = diff(meshlist2, meshlist)

        separated = differenceB[0]
        separatedMesh = separated.data

        bpy.context.scene.objects.active = separated

        modnum = len(modifierlist)
        
        modifierlist1 = separated.modifiers[:]
        bpy.ops.object.modifier_add(type="MIRROR")
        modifierlist2 = separated.modifiers[:]

        differenceC = diff(modifierlist2, modifierlist1);

        mirrorName = differenceC[0].name

        for index in range (0, modnum):
            bpy.ops.object.modifier_move_up(modifier=mirrorName)
            
        type = int(self.AxisTypes)

        if (type == 0):
            separated.modifiers[mirrorName].use_x = True
            separated.modifiers[mirrorName].use_y = False
            separated.modifiers[mirrorName].use_z = False
            
        if (type == 1):
            separated.modifiers[mirrorName].use_x = False
            separated.modifiers[mirrorName].use_y = True
            separated.modifiers[mirrorName].use_z = False

        if (type == 2):
            separated.modifiers[mirrorName].use_x = False
            separated.modifiers[mirrorName].use_y = False
            separated.modifiers[mirrorName].use_z = True

        separated.modifiers[mirrorName].use_mirror_merge = False
       
        bpy.ops.object.modifier_apply(modifier=mirrorName)

        bpy.context.scene.objects.active = obj

        bpy.ops.object.join()
        bpy.data.meshes.remove(separatedMesh)

        for vindex in range(0, selectedNum*2):
            mesh.vertices[vindex].select = True

        for eindex in range(0, selectedNumEdge*2):
            mesh.edges[eindex].select = True

        for findex in range(0, selectedNumFace*2):
            mesh.polygons[findex].select = True

        #print(selectedNum, selectedNumFace, selectedNumEdge)
            
        for (mod, show) in zip(modifierlist, modifiershow):
            mod.show_viewport = show

        for o in selectedObj:
            if (o != obj):
                o.select = True
                
        bpy.ops.object.mode_set(mode='EDIT')

        return {'FINISHED'}


# メニューを登録する関数
def menu_func(self, context):
    self.layout.operator(CMirrorSelectedVertices.bl_idname)     # 登録したいクラスの「bl_idname」を指定

# プラグインをインストールしたときの処理
def register():
    bpy.utils.register_module(__name__)
    bpy.types.VIEW3D_PT_tools_meshedit.append(menu_func)

# プラグインをアンインストールしたときの処理
def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.VIEW3D_PT_tools_meshedit.remove(menu_func)

# メイン関数
if __name__ == "__main__":
    register()

Placeholder(空のファイル)を利用して、指定したフレームのレンダリングを省略する

Set placeholders ボタンを押すと、レンダリングをしたくないフレームの出力ファイル名に相当する空のファイル(Placeholder)が作成されます。
レンダリング設定で Overwrite(上書き) が無効になっていれば、ファイルのあるフレームのレンダリングが省略されます。

Camera visibility モードでは、アクティブなカメラがレンダリングから隠れている状態のフレームでレンダリングを省略するようにします。
Marked frames モードでは、マーカーの打たれたフレーム以外のレンダリングを省略します。

Remove placeholders は、レンダリング終了後に邪魔な Placeholder を削除します。

Remove all output は、すべての出力ファイルを削除します。設定上、出力ファイルの上書きができないので、再レンダリングを行なう際に必要になります。

Fill placeholders は、placeholder のままで残っているファイルを、直前の空ではない画像ファイルのコピーに置き換えます。これにより、動きが全くないフレームのレンダリングを省略した後、省略したフレームを直前の有効フレームの画像に置き換えることができます。
仕組み上、Overwrite(上書き)を無効にする必要があります。上書きが無効になっているときに、右上にチェック印が付くようになっています。

TweakPlaceHolder.py

# Blender内部のデータ構造にアクセスするために必要
import bpy
import os
import shutil
from pathlib import Path

# プラグインに関する情報
bl_info = {
    "name" : "TweakPlaceHolder",        # プラグイン名
    "author" : "Q@StudioPotpourri",     # 作者
    "version" : (0,4),                  # プラグインのバージョン
    "blender" : (2, 7, 9),              # プラグインが動作するBlenderのバージョン
    "location" : "Properties > Render > Object Scripter",   # Blender内部でのプラグインの位置づけ
    "description" : "Set or remove placeholders.",   # プラグインの説明
    "warning" : "",
    "wiki_url" : "",                    # プラグインの説明が存在するWikiページのURL
    "tracker_url" : "",                 # Blender Developer OrgのスレッドURL
    "category" : "Render"               # プラグインのカテゴリ名
    }

class TweakPlaceHolderModeProperties(bpy.types.PropertyGroup):
    Types = [("0", "Camera visibility", "Render for frames active camera is not hide_render"),
             ("1", "Marked frames", "Render for frames that marked")]
    
    mode = bpy.props.EnumProperty(name="TweakMode", description="Mode", items=Types, default="0")

def GetExtension(scene, type):
    if scene.render.use_file_extension == False:
        return ''
    if (type == 'PNG'):
        return '.png'
    if (type == 'JPEG'):
        return '.jpg'
    if (type == 'IRIS'):
        return '.rgb'
    if (type == 'JPEG2000'):
        return '.jp2'
    if (type == 'TARGA' or type == 'TARGA_RAW'):
        return '.tga'

# 1 = place holder を置く
def CheckRender(scene, hide_render, frame):
    if int(scene.tweakplaceholderprops.mode) == 0:
        if hide_render == None:
            return 0
        if hide_render.evaluate(frame) == 1:
            return 1

    if int(scene.tweakplaceholderprops.mode) == 1:
        for markers in scene.timeline_markers:
            if (markers.frame == frame):
                return 0
        return 1

    return 0

class SetPlaceHolder(bpy.types.Operator):
    """SetPlaceHolder"""

    bl_idname = "render.setplaceholder"
    bl_label = "SetPlaceHolder"
    bl_options = {'UNDO'}

    def execute(self, context):
        scene = context.scene

        if not hasattr(scene, "camera"):
            print ("not active camera")
            return {'CANCELLED'}

        if not hasattr(scene.camera, "animation_data"):
            print ("not animation_data")
            return {'CANCELLED'}

        if not hasattr(scene.camera.animation_data, "action"):
            print ("not action")
            return {'CANCELLED'}

        if hasattr(scene.camera.animation_data.action, "fcurves"):
            fc = scene.camera.animation_data.action.fcurves
            hide_render = fc.find('hide_render', index=0)
        else:
            print("not fcurves")
            return {'CANCELLED'}
        
        

        filepath = scene.render.filepath
        fileformat = scene.render.image_settings.file_format
        ext = GetExtension(scene, fileformat)
        
        ini = scene.frame_start
        fin = scene.frame_end

        for f in range(ini,fin+1):
            if CheckRender(scene, hide_render, f) == 1:
                fullpath = filepath + "%04d" % f + ext
                Path(fullpath).touch()

        return {'FINISHED'}

class RemovePlaceHolder(bpy.types.Operator):
    """RemovePlaceHolder"""

    bl_idname = "render.removeplaceholder"
    bl_label = "RemovePlaceHolder"
    bl_options = {'UNDO'}
    
    def execute(self, context):
        scene = context.scene

        filepath = scene.render.filepath
        fileformat = scene.render.image_settings.file_format
        ext = GetExtension(scene, fileformat)

        ini = scene.frame_start
        fin = scene.frame_end

        for f in range(ini,fin+1):
            fullpath = filepath + "%04d" % f + ext
            if (os.path.isfile(fullpath)):
                if (os.path.getsize(fullpath) == 0):
                    os.remove(fullpath)

        return {'FINISHED'}

class RemoveAllOutputForce(bpy.types.Operator):
    """RemoveAllOutputForce"""
    bl_idname = "render.removealloutputforce"
    bl_label = "RemoveAllOutputForce"
    bl_options = {'UNDO'}

    def execute(self, context):
        scene = context.scene

        filepath = scene.render.filepath
        fileformat = scene.render.image_settings.file_format
        ext = GetExtension(scene, fileformat)

        ini = scene.frame_start
        fin = scene.frame_end

        for f in range(ini,fin+1):
            fullpath = filepath + "%04d" % f + ext
            if (os.path.isfile(fullpath)):
                os.remove(fullpath)

        return {'FINISHED'}

class RemoveAllOutput(bpy.types.Operator):
    """RemoveAllOutput"""

    bl_idname = "render.removealloutput"
    bl_label = "RemoveAllOutput"
    bl_options = {'REGISTER','UNDO'}

    def invoke(self, context, event):
        return context.window_manager.invoke_props_popup(self, event)

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

    def draw(self, context):
        layout = self.layout;
        
        layout.operator("render.removealloutputforce", text="Remove files?")
        
class FillPlaceHolder(bpy.types.Operator):
    """FillPlaceHolders"""

    bl_idname = "render.fillplaceholders"
    bl_label = "FillPlaceHolders"
    bl_options = {'UNDO'}
    
    def execute(self, context):
        scene = context.scene

        filepath = scene.render.filepath
        fileformat = scene.render.image_settings.file_format
        ext = GetExtension(scene, fileformat)

        ini = scene.frame_start
        fin = scene.frame_end

        for f in range(ini,fin+1):
            fullpathPre = filepath + "%04d" % (f-1) + ext
            fullpath    = filepath + "%04d" % f + ext
            if (os.path.isfile(fullpath) and
                os.path.isfile(fullpathPre)):
                if (os.path.getsize(fullpath) == 0 and
                    os.path.getsize(fullpathPre) != 0):
                    shutil.copyfile(fullpathPre, fullpath)

        return {'FINISHED'}
        
                
class RENDER_PT_TweakPlaceHolder(bpy.types.Panel):
    bl_label = "TweakPlaceHolder"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "render"

    @classmethod
    def poll(cls, context):
        return True

    def draw(self, context):
        layout = self.layout;

        row = layout.row();
        row.prop(context.scene.tweakplaceholderprops, "mode", text='');
        if (context.scene.render.use_overwrite == False):
            row.label(icon = 'FILE_TICK', )
        else:
            row.label("Overwrite", icon = 'X',)

        layout.operator("render.setplaceholder", text="Set placeholders")
        
        row = layout.row()
        row.operator("render.removeplaceholder", text="Remove placeholders")
        row.operator("render.removealloutput", text="Reomve all output")

        layout.operator("render.fillplaceholders", text="Fill placeholders")

        

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.tweakplaceholderprops = bpy.props.PointerProperty(type=TweakPlaceHolderModeProperties)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.tweakplaceholderprops

if __name__ == "__main__":
    register()

inserted by FC2 system