2019年12月22日日曜日

Blenderでオブジェクト原点はあまり変更しない方がいいかも?という話

Blenderには移動や回転の基準になる「ピボットポイント」を指定する機能がいくつか用意されています

そんな中でオブジェクトの回転操作をするのに
オブジェクトの原点を移動させてピボットポイントとして利用する話を見かけたので
「それはマズい操作なんじゃないのかな?」と思い、検証をしてみました

先にBlenderのオブジェクトとデータ(ジオメトリ)の関係を説明しておきます
Blenderのオブジェクトはメッシュのデータを収める透明な器のようなもので
オブジェクトモードでの移動は”器”の移動のデータが変化するだけで
中身のデータは変化しません。

中身であるジオメトリは オブジェクトの原点の位置からどれくらいの距離にあるか?という情報で決まっています。

では、 原点を移動させる操作はどういうことなのでしょうか?
実はオブジェクトモードでのオレンジの点が移動すると同時に
中のデータの位置にも逆方向に移動する操作が加わっているのです。

平行移動する程度では値は変化しませんが
オブジェクトに回転が加わっていたりすると計算の誤差が重なって値が変わってくることが想像できます

検証にオブジェクトの原点を-100mから100mの範囲でランダムに移動させて最初の位置に戻すスクリプトを作って値を見てみました

import bpy
import random
cursor = bpy.context.scene.cursor
obj = bpy.context.active_object
print()
print(obj.data.vertices[10].co)
for i in range(1000):
    cursor.location = (random.uniform(-100,100),random.uniform(-100,100),random.uniform(-100,100))
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN')

cursor.location = (0,0,0)
bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN')
print(obj.data.vertices[10].co)

これを傾きをかけたオブジェクトに実行すると座標に誤差が出ていました
緑の文字が特定の頂点の座標で
上の2行が2000回 下2行が1000回移動されてた時の前後になります
1000回で 0.1mmの単位なので問題にする必要がない といえばないのですが
意図しない値になることはあるので 注意が必要ですね。

オブジェクトを任意の位置で移動回転拡縮操作をしたい時には3Dカーソルをピボットポイントとして使った方が無難です。

2019年12月20日金曜日

画像素材の自動更新アドオン

Blenderのテクスチャを他のソフトで作成している場合に
画像の更新のために手動で更新をするのも煩わしいので
データの更新を検出して再読み込みをさせるアドオンを作成しました
その使用法とPhotoshopの便利な機能「画像アセット」について解説したいと思います

Photoshopの「画像アセット」

Photoshopにはレイヤにつけた名前に従って自動的に画像を書き出す機能があります。

例えば グループレイヤに01.png 02.pngと名前をつけておくと
PSDの保存されている場所にPSDの名前に-assetsとついたフォルダが作られて
それぞれのグループを統合した画像で 01.png 02.png といった感じに出力されます

PSDのレイヤを編集するとその都度変更が自動出力されます
Web用の機能なので 3Dのテクスチャ用で出力できる種類はPNGのみですが
自動的に出力されるのは便利かと思います。
ファイルの名前の付け方によっては 画像を縮小したりもできます

Blenderの自動更新アドオン

さて、冒頭に話をした自動更新アドオンです。
Blenderに読み込んでいる画像ファイルの更新を検出して再読み込みします
他にも同様な機能なものはあるようですが、ミニマムな構成にしてみました。
長いデータなので boothにダウンロード用ファイルを登録しました
bl_info = {
    "name" : "AutoReloadImage",
    "author" : "Yukimituki",
    "description" : "",
    "blender" : (2, 80, 0),
    "version" : (0, 0, 1),
    "location" : "",
    "warning" : "",
    "category" : "Image"
}

import bpy
import os
from bpy.types import (
        PropertyGroup,
        AddonPreferences,
        )

class IMAGE_PT_AutoReload(bpy.types.Panel):
    bl_idname = "autoreload.panel"
    bl_label = "Reload Images"
    bl_space_type = "IMAGE_EDITOR"
    bl_region_type = "UI"
    bl_category = "Image"
    # 描画の定義
    def draw(self, context):
        layout = self.layout
        row = layout.row()
        row.prop(context.scene, "imageassets_autoreload_time")
        row = layout.row()
        row.prop(context.scene, "imageassets_autoreload_active")
        row = layout.row()
        row.operator("image.reloadbutton")


class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"

    _timer = None
    asset_dic = {}
    def __init__(self):
        asset_dic = self.asset_dic
        self.asset_dic = set_asset_dic(asset_dic)

    def modal(self, context, event):
        #check ImageEditor panel value
        if not context.scene.imageassets_autoreload_active:
            context.window_manager.event_timer_remove(self._timer)
            return {'FINISHED'}

        if event.type == 'TIMER':
            asset_dic = self.asset_dic
            reload_assets(asset_dic)
            self.asset_dic = set_asset_dic(asset_dic)
        return {'PASS_THROUGH'}

    def execute(self, context):
        wm = context.window_manager
        reload_cycle = context.scene.imageassets_autoreload_time
        self._timer = wm.event_timer_add(reload_cycle, window=context.window)
        wm.modal_handler_add(self)
        self.asset_dic = set_asset_dic({})
        return {'RUNNING_MODAL'}

    def cancel(self, context):
        wm = context.window_manager
        wm.event_timer_remove(self._timer)
#手動リロード処理
class OBJECT_OT_reloadbutton(bpy.types.Operator):
    bl_idname = "image.reloadbutton"
    bl_label = "Manually update"
    def execute(self, context):
        for img in bpy.data.images:
            #only single image file
            if img.type != 'IMAGE': continue
            img.reload()
            set_asset_dic({})
        return {'FINISHED'}
#自動リロード処理
def reload_assets(asset_dic):
    for image_obj in asset_dic:
        try:
            #only single image file
            if image_obj.type != 'IMAGE': continue
            image_path = bpy.path.abspath(image_obj.filepath)
            #チェック時にファイル保存が重なると一時的にファイルが認識できないため
            if os.path.lexists(image_path) == False: continue
            #更新時刻の取得 get modefied time
            mod_time = os.stat(image_path).st_mtime_ns
            if mod_time != asset_dic[image_obj]:
                image_obj.reload()
                asset_dic[image_obj] = mod_time
                print("reload:%s" % image_obj.name )
        except:pass
#ファイル内のイメージデータブロックの情報を記録
def set_asset_dic(asset_dic):
    if len(bpy.data.images) == len(asset_dic): return(asset_dic)
    for img in bpy.data.images:
        if img.type != 'IMAGE':
            image_path = bpy.path.abspath(img.filepath)
            #get modefied time as nanosec(int)
            asset_dic[img] = os.stat(image_path).st_mtime_ns
        else:asset_dic[img] = 0
    return(asset_dic)

def update_bool_func(self, context):
    if context.scene.imageassets_autoreload_active:
        bpy.ops.wm.modal_timer_operator()

classes = (
    ModalTimerOperator,
    IMAGE_PT_AutoReload,
    OBJECT_OT_reloadbutton
    )
#アドオンとして機能させるための情報の登録
def register():
    #properties
    Scene = bpy.types.Scene
    props = bpy.props
    Scene.imageassets_autoreload_active = props.BoolProperty(name="Auto Reload", description="", default=False, update = update_bool_func)
    Scene.imageassets_autoreload_time = props.IntProperty(name="Reload Cycle",description="", default=10 )
    #classes
    for cls in classes:
        bpy.utils.register_class(cls)
#アドオンを停止させた時に情報の削除
def unregister():
    #properties
    Scene = bpy.types.Scene
    props = bpy.props
    del Scene.imageassets_autoreload_active
    del Scene.imageassets_autoreload_time
    #classes
    for cls in classes:
        bpy.utils.unregister_class(cls)

if __name__ == "__main__":
    register()

(Blender2.8用ですが5行目の "blender" : (2, 80, 0),の数字を(2,29,0)に書き換えることで2.79でも動作します )
アドオンとして読み込むと
画像エディッタやUVエディッタの画像タブに「Reload Images」の項目が出ます
Auto ReloadのチェックボックスをオンにするとReload Cycleの秒数毎にファイルをチェックしますManually updateのボタンで手動で更新することもできます。

先にPhotoshopの話をしましたが 画像ファイルの更新日時を検出する動作ですので
画像を作成するソフトは何でも構いません。

ゲームのモデル等 テクスチャを描く作業をしている人お役に立てれば幸いです。