見出し画像

【Blender】プロパティのゲッター・セッター関数の謎を掘り下げる



■公式の答え

まず、こちらが公式の答えです。

https://docs.blender.org/api/current/bpy.props.html#getter-setter-example

私が思うにこちらの例が一番わかり易いと思います。

https://blenderartists.org/t/how-to-prevent-recursion-with-property-get-

set/1447704

見てもいまいちわからないので、まずはシンプルな例を。

import bpy

def get_float(self):
    print('**** get_float')
    return self['test_float']  # self is 'scene'

def set_float(self, value):
    print('**** set_float')
    print('**** value:', value)
    
    # 別の値や変数を使用してプロパティの値を設定
    noice = value * 2
    print('**** noice:', noice)
    
    self['test_float'] = noice  # self is 'scene'

bpy.types.Scene.test_float = bpy.props.FloatProperty(get=get_float, set=set_float)

# プロパティに値を設定
bpy.context.scene.test_float = 1000
# プロパティの値を取得
print(bpy.context.scene.test_float)

・ゲットは最初(ないしは変更後に設定される値)に設定される値を返す関数。セットは変更した際に実行される関数


要するに、
ゲット関数はプロパティを呼び出した時(習得)するものです。
セット関数はプロパティをセット(設定)した時に実行される関数で、
非常に単純ですね。

しかし、実際のプログラムによっては一度経由した値なので、理解が難しいと思います。

・self['test_float']とかくと、bpy.context.scene['test_float']と同意義である。

この例ではゲットではself['test_float']で直接値を設定してます。

selfは自分自身ですので、ここではbpy.context.sceneを指してるので、

bpy.context.scene['test_float']と同様の意味です。

実際に、
print('**** get_float',self)
とプリントしてみると

**** get_float <bpy_struct, Scene("Scene") at 0x000001B319064088>

シーンのクラスであるということがわかります。

セット内で使用されるvalueという引数はプロパティの実際の値ですので、
プロパティを変更した際に実際に表示される値です。

実際にセットない関数の結果によっては最終的にユーザーが見る値が違ったりするので、感覚的に理解が難しいと思います。
利用して慣れていくしかありませんね。

■注意事項

ちなみ、関数の第一因数にselfがありますが、これを使用して値を渡したいところですが、それをすると無限再帰が起こります。つまり渡せません。
selfがあるから混乱しますが、これはあくまでもオブジェクトやシーンをしているだけに使われるようです。

・ではどうやってデータを渡すのか?

ということで、なにか値を保存したい場合は通常であれば外部に保存する必要があり、殆どの例においては違う保存用のプロパティを別途用意して関数の結果を外部へ渡すのが一般的です。
また、jsonファイルやXMLで渡すのも良いでしょう。


■具体的な例(シーンプロパティで)


・下記がシーンのプロパティを渡した例です。


セット関数を通してセットした値の2倍の値が外部である["dependent_prop"]という違うシーンプロパティへ渡されているのがわかると思います。





import bpy

def get_float(self):
    return self["test_float"]

def set_float(self, value):
    self["test_float"] = value
    # test_floatの値に応じてdependent_propの値を更新
    self["dependent_prop"] = value * 2

def get_dependent_prop(self):
    return self["dependent_prop"]

bpy.types.Scene.test_float = bpy.props.FloatProperty(get=get_float, set=set_float)
bpy.types.Scene.dependent_prop = bpy.props.FloatProperty(get=get_dependent_prop)

# テスト用のプロパティを持つシーンを作成
scene = bpy.context.scene
scene["test_float"] = 5.0

# test_floatの値を変更すると、dependent_propの値も更新される
scene.test_float = 10.0
print(scene["dependent_prop"])  # 出力は20.0


■オブジェクトプロパティの例

当然、オブジェクトのカスタムプロパティでも渡すことは可能です

・例1 オブジェクトの値を違った値を通じて更新してみる


下記が例です。

import bpy

def get_scale_factor(self):
    return self.get("scale_factor", 1.0)

def set_scale_factor(self, value):
    self["scale_factor"] = value
    # scale_factorの値に応じてscaled_valueの値を更新
    self["scaled_value"] = self["value"] * value

def get_scaled_value(self):
    return self.get("scaled_value", 0.0)

# オブジェクトに新しいプロパティを追加
bpy.types.Object.scale_factor = bpy.props.FloatProperty(get=get_scale_factor, set=set_scale_factor)
bpy.types.Object.scaled_value = bpy.props.FloatProperty(get=get_scaled_value)

# テスト用のオブジェクトを作成
obj = bpy.data.objects.new("MyObject", None)
# 現在のシーンを取得
scene = bpy.context.scene
# シーンコレクションにオブジェクトをリンク
scene.collection.objects.link(obj)
obj["value"] = 10.0
obj["scale_factor"] = 2.0

# scale_factorの値を変更すると、scaled_valueの値も更新される
obj.scale_factor = 3.0
print(obj["scaled_value"])  # 出力は30.0

scale_factorをセットすることにより、
その10倍(valuse)の値がscaled_valueへ渡されてる事がわかります。

・例2 パネルに表示してみる

次にパネルに応用した例です。

オブジェクトに登録したカスタムプロパティはパネルにも表示可能です。

このスクリプトはksyn_Bevelという名前のベベルモディファイアの値がパネルから変更出来ます。

※あらかじめオブジェクトにベベルモディファイアをつけて、名前をksyn_Bevelに変更しておいて下さい。

import bpy

# カスタムプロパティを保持するクラスを定義
class MyObjectProperties(bpy.types.PropertyGroup):
    # プロパティのゲット関数
    def get_bevel_width(self):
        obj = bpy.context.object
        if obj and obj.type == 'MESH':
            if obj.modifiers.get("ksyn_Bevel"):
                return obj.modifiers["ksyn_Bevel"].width
        return 0.0

    def get_bevel_segments(self):
        obj = bpy.context.object
        if obj and obj.type == 'MESH':
            if obj.modifiers.get("ksyn_Bevel"):
                return obj.modifiers["ksyn_Bevel"].segments
        return 0

    # プロパティのセット関数
    def set_bevel_width(self, value):
        obj = bpy.context.object
        if obj and obj.type == 'MESH':
            if obj.modifiers.get("ksyn_Bevel"):
                obj.modifiers["ksyn_Bevel"].width = value

    def set_bevel_segments(self, value):
        obj = bpy.context.object
        if obj and obj.type == 'MESH':
            if obj.modifiers.get("ksyn_Bevel"):
                obj.modifiers["ksyn_Bevel"].segments = value

    bevel_width: bpy.props.FloatProperty(
        name="Bevel Width",
        description="Width of the bevel modifier",
        default=0.0,
        min=0.0,
        max=1.0,
        get=get_bevel_width, # ゲット関数を指定
        set=set_bevel_width  # セット関数を指定
    )
    bevel_segments: bpy.props.IntProperty(
        name="Bevel Segments",
        description="Segments of the bevel modifier",
        default=0,
        min=0,
        max=10,
        get=get_bevel_segments, # ゲット関数を指定
        set=set_bevel_segments  # セット関数を指定
    )

# パネルを定義
class OBJECT_PT_CustomPanel(bpy.types.Panel):
    bl_label = "Custom Object Panel"
    bl_idname = "OBJECT_PT_custom_panel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Custom'

    def draw(self, context):
        layout = self.layout
        obj = context.object
        obj_props = context.object.my_object_properties

        if obj is not None and obj.type == 'MESH':
            layout.prop(obj_props, "bevel_width")
            layout.prop(obj_props, "bevel_segments")

# クラスを登録
classes = (
    MyObjectProperties,
    OBJECT_PT_CustomPanel
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.Object.my_object_properties = bpy.props.PointerProperty(type=MyObjectProperties)

def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)
    del bpy.types.Object.my_object_properties

if __name__ == "__main__":
    register()

ポインタープロパティで2つのプロパティをまとめてあります。
この方が一つの目的のプロパティ郡をまとめれるので便利です。

仕組みは単純で、
ゲット関数(読み込む値)はオブジェクトのモディファイアの値を戻り値で返します。

    # プロパティのゲット関数
    def get_bevel_width(self):
        obj = bpy.context.object
        if obj and obj.type == 'MESH':
            if obj.modifiers.get("ksyn_Bevel"):
                return obj.modifiers["ksyn_Bevel"].width
        return 0.0

セット関数(プロパティを変更した際に実行される関数)
はプロパティの値がモディファイアの値に入力(obj.modifiers["ksyn_Bevel"].width = value)されます。

    def set_bevel_width(self, value):
        obj = bpy.context.object
        if obj and obj.type == 'MESH':
            if obj.modifiers.get("ksyn_Bevel"):
                obj.modifiers["ksyn_Bevel"].width = value


・実行結果

無事、モディファイアの値とパネルの値が、オブジェクトのプロパティを通じてゲット関数とセット関数を利用して連動してることがわかります。

■セッターゲッター関数を用いた外部ファイル読み込みの例


・ファイルの保存先



custom_properties.jsonに関してはBlenderファイルの層に保存されているはずです。(お好みに合わせて調整してください。)

・動作

scale_factorが変更されるとvaluseの値の乗算が出力される形で、同時に
書き込みもjosonファイルに行われます。

読み込みのゲット関数を見てもらえばわかりますが、
読み込みはカスタムプロパティを返さずにjsonファイルで読み込まれてるのがわかると思います。

scaled_valueにはセット関数が使用されてませんので、オブジェクトのカスタムプロパティ上では変更は出来ません。

import bpy
import json
import os

def get_scale_factor(self):
    filepath = "custom_properties.json"
    if os.path.exists(filepath):
        with open(filepath, 'r') as file:
            data = json.load(file)
            return data.get("scale_factor", 1.0)
    else:
        print(f"JSON file '{filepath}' not found. Using default scale factor.")
        return 1.0

def set_scale_factor(self, value):
    self["scale_factor"] = value
    self["scaled_value"] = self["value"] * value
    save_custom_properties_to_json(self)

def get_scaled_value(self):
    filepath = "custom_properties.json"
    if os.path.exists(filepath):
        with open(filepath, 'r') as file:
            data = json.load(file)
            return data.get("scaled_value", 0.0)
    else:
        print(f"JSON file '{filepath}' not found. Using default scaled value.")
        return 0.0

def save_custom_properties_to_json(obj):
    filepath = "custom_properties.json"
    data = {
        "scale_factor": obj.get("scale_factor", 1.0),
        "scaled_value": obj.get("scaled_value", 0.0)
    }
    with open(filepath, 'w') as file:
        json.dump(data, file, indent=4)

# オブジェクトに新しいプロパティを追加
bpy.types.Object.scale_factor = bpy.props.FloatProperty(get=get_scale_factor, set=set_scale_factor)
bpy.types.Object.scaled_value = bpy.props.FloatProperty(get=get_scaled_value)

# テスト用のオブジェクトを作成
obj = bpy.data.objects.new("MyObject", None)
scene = bpy.context.scene
scene.collection.objects.link(obj)
obj["value"] = 10.0

# scale_factorの値を変更すると、scaled_valueの値も更新される
obj.scale_factor = 2.0
print(obj.scaled_value)  # 出力は20.0


セッター関数で保存したデータは辞書形式で保存するのが一般的かと思います。


■列挙型の場合

・列挙型は戻り地でアイテムのインデックス番号を返す


列挙型の場合は文字列方を返すのではなく、列挙型のインデックス番号がバリューの値となります。

    def get_bevel_limit_method(self):
        obj = bpy.context.object
        if obj and obj.type == 'MESH':
            if obj.modifiers.get("ksyn_Bevel"):
               if obj.modifiers.get("ksyn_Bevel"):
                if obj.modifiers["ksyn_Bevel"].limit_method=="NONE":
                    return 0
                elif obj.modifiers["ksyn_Bevel"].limit_method == "WEIGHT":
                    return 1
                else:
                    return 0
            return 0
        else:
            return 0

    def set_bevel_limit_method(self, value):
        obj = bpy.context.object
        if obj and obj.type == 'MESH':
            if obj.modifiers.get("ksyn_Bevel"):
                # print("###value",value)
                if value==0:
                    obj.modifiers["ksyn_Bevel"].limit_method = "NONE"
                elif value==1:
                    obj.modifiers["ksyn_Bevel"].limit_method = "WEIGHT"
                else:
                    pass



    bevel_limit_method: bpy.props.EnumProperty(
        name="Bevel Limit Method",
        description="Limit method of the bevel modifier",
        items=[('NONE', 'None', 'No limit method'),
               ('WEIGHT', 'Weight', 'Limit by vertex weights')],
        default='NONE',
        get=get_bevel_limit_method,
        set=set_bevel_limit_method
    ) # type: ignore

つまり、こんな感じセットの場合のバリューの習得は列挙型で指定したインデックス番号を(整数型)。

■まとめ

・GETは外部から値を入手する

ゲットは主に連想するであろう値を外部から入手するプロセスだと思えばわかりやすいと思います。

・SETは内部の値を外部へ出力する

セットはユーザーが変更したプロパティの値を別のなにかの値へ変更するプロセスがメインな目的の関数という事です。