CM3D2 Converter.model_export

   1import struct
   2import time
   3import math
   4import bpy
   5import bmesh
   6import mathutils
   7import numpy as np
   8from operator import itemgetter
   9from . import common
  10from . import compat
  11from . import cm3d2_data
  12from .translations.pgettext_functions import *
  13
  14
  15# メインオペレーター
  16@compat.BlRegister()
  17class CNV_OT_export_cm3d2_model(bpy.types.Operator):
  18    bl_idname = 'export_mesh.export_cm3d2_model'
  19    bl_label = "CM3D2モデル (.model)"
  20    bl_description = "カスタムメイド3D2のmodelファイルを書き出します"
  21    bl_options = {'REGISTER'}
  22
  23    filepath = bpy.props.StringProperty(subtype='FILE_PATH')
  24    filename_ext = ".model"
  25    filter_glob = bpy.props.StringProperty(default="*.model", options={'HIDDEN'})
  26
  27    scale = bpy.props.FloatProperty(name="倍率", default=0.2, min=0.01, max=100, soft_min=0.01, soft_max=100, step=10, precision=2, description="エクスポート時のメッシュ等の拡大率です")
  28
  29    is_backup = bpy.props.BoolProperty(name="ファイルをバックアップ", default=True, description="ファイルに上書きする場合にバックアップファイルを複製します")
  30
  31    version = bpy.props.EnumProperty(
  32        name="ファイルバージョン",
  33        items=[
  34            ('2001', '2001', 'model version 2001 (available only for com3d2)', 'NONE', 0),
  35            ('2000', '2000', 'model version 2000 (com3d2 version)', 'NONE', 1),
  36            ('1000', '1000', 'model version 1000 (available for cm3d2/com3d2)', 'NONE', 2),
  37        ], default='1000')
  38    model_name = bpy.props.StringProperty(name="model名", default="*")
  39    base_bone_name = bpy.props.StringProperty(name="基点ボーン名", default="*")
  40
  41    items = [
  42        ('ARMATURE'         , "アーマチュア", "", 'OUTLINER_OB_ARMATURE', 1),
  43        ('TEXT'             , "テキスト", "", 'FILE_TEXT', 2),
  44        ('OBJECT_PROPERTY'  , "オブジェクト内プロパティ", "", 'OBJECT_DATAMODE', 3),
  45        ('ARMATURE_PROPERTY', "アーマチュア内プロパティ", "", 'ARMATURE_DATA', 4),
  46    ]
  47    bone_info_mode = bpy.props.EnumProperty(items=items, name="ボーン情報元", default='OBJECT_PROPERTY', description="modelファイルに必要なボーン情報をどこから引っ張ってくるか選びます")
  48
  49    items = [
  50        ('TEXT', "テキスト", "", 'FILE_TEXT', 1),
  51        ('MATERIAL', "マテリアル", "", 'MATERIAL', 2),
  52    ]
  53    mate_info_mode = bpy.props.EnumProperty(items=items, name="マテリアル情報元", default='MATERIAL', description="modelファイルに必要なマテリアル情報をどこから引っ張ってくるか選びます")
  54
  55    is_arrange_name = bpy.props.BoolProperty(name="データ名の連番を削除", default=True, description="「○○.001」のような連番が付属したデータ名からこれらを削除します")
  56
  57    is_align_to_base_bone = bpy.props.BoolProperty(name="Align to Base Bone", default=True, description="Align the object to it's base bone")
  58    is_convert_tris = bpy.props.BoolProperty(name="四角面を三角面に", default=True, description="四角ポリゴンを三角ポリゴンに変換してから出力します、元のメッシュには影響ありません")
  59    is_split_sharp = bpy.props.BoolProperty(name="Split Sharp Edges", default=True, description="Split all edges marked as sharp.")
  60    is_normalize_weight = bpy.props.BoolProperty(name="ウェイトの合計を1.0に", default=True, description="4つのウェイトの合計値が1.0になるように正規化します")
  61    is_convert_bone_weight_names = bpy.props.BoolProperty(name="頂点グループ名をCM3D2用に変換", default=True, description="全ての頂点グループ名をCM3D2で使える名前にしてからエクスポートします")
  62    is_clean_vertex_groups = bpy.props.BoolProperty(name="クリーンな頂点グループ", default=True, description="重みがゼロの場合、頂点グループから頂点を削除します")
  63    
  64    is_batch = bpy.props.BoolProperty(name="バッチモード", default=False, description="モードの切替やエラー個所の選択を行いません")
  65
  66    export_tangent = bpy.props.BoolProperty(name="接空間情報出力", default=False, description="接空間情報(binormals, tangents)を出力する")
  67
  68    
  69    shapekey_threshold = bpy.props.FloatProperty(name="Shape Key Threshold", default=0.00100, min=0, soft_min=0.0005, max=0.01, soft_max=0.002, precision=5, description="Lower values increase accuracy and file size. Higher values truncate small changes and reduce file size.")
  70    export_shapekey_normals = bpy.props.BoolProperty(name="Export Shape Key Normals", default=True, description="Export custom normals for each shape key on export.")
  71    shapekey_normals_blend = bpy.props.FloatProperty(name="Shape Key Normals Blend", default=0.6, min=0, max=1, precision=3, description="Adjust the influence of shape keys on custom normals")
  72    use_shapekey_colors = bpy.props.BoolProperty(name="Use Shape Key Colors", default=True, description="Use the shape key normals stored in the vertex colors instead of calculating the normals on export. (Recommend disabling if geometry was customized)")
  73    
  74
  75    @classmethod
  76    def poll(cls, context):
  77        ob = context.active_object
  78        if ob:
  79            if ob.type == 'MESH':
  80                return True
  81        return False
  82
  83    def report_cancel(self, report_message, report_type={'ERROR'}, resobj={'CANCELLED'}):
  84        """エラーメッセージを出力してキャンセルオブジェクトを返す"""
  85        self.report(type=report_type, message=report_message)
  86        return resobj
  87
  88    def precheck(self, context):
  89        """データの成否チェック"""
  90        ob = context.active_object
  91        if not ob:
  92            return self.report_cancel("アクティブオブジェクトがありません")
  93        if ob.type != 'MESH':
  94            return self.report_cancel("メッシュオブジェクトを選択した状態で実行してください")
  95        if not len(ob.material_slots):
  96            return self.report_cancel("マテリアルがありません")
  97        for slot in ob.material_slots:
  98            if not slot.material:
  99                return self.report_cancel("空のマテリアルスロットを削除してください")
 100            try:
 101                slot.material['shader1']
 102                slot.material['shader2']
 103            except:
 104                return self.report_cancel("マテリアルに「shader1」と「shader2」という名前のカスタムプロパティを用意してください")
 105        me = ob.data
 106        if not me.uv_layers.active:
 107            return self.report_cancel("UVがありません")
 108        if 65535 < len(me.vertices):
 109            return self.report_cancel("エクスポート可能な頂点数を大幅に超えています、最低でも65535未満には削減してください")
 110        return None
 111
 112    def invoke(self, context, event):
 113        res = self.precheck(context)
 114        if res:
 115            return res
 116        ob = context.active_object
 117
 118        # model名とか
 119        ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.')
 120        self.model_name = ob_names[0]
 121        self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto'
 122
 123        # ボーン情報元のデフォルトオプションを取得
 124        arm_ob = ob.parent
 125        for mod in ob.modifiers:
 126            if mod.type == 'ARMATURE' and mod.object:
 127                arm_ob = mod.object
 128        if arm_ob and not arm_ob.type == 'ARMATURE':
 129            arm_ob = None
 130
 131        info_mode_was_armature = (self.bone_info_mode == 'ARMATURE')
 132        if "BoneData" in context.blend_data.texts:
 133            if "LocalBoneData" in context.blend_data.texts:
 134                self.bone_info_mode = 'TEXT'
 135        if "BoneData:0" in ob:
 136            ver = ob.get("ModelVersion")
 137            if ver and ver >= 1000:
 138                self.version = str(ver)
 139            if "LocalBoneData:0" in ob:
 140                self.bone_info_mode = 'OBJECT_PROPERTY'
 141        if arm_ob:
 142            if info_mode_was_armature:
 143                self.bone_info_mode = 'ARMATURE'
 144            else:
 145                self.bone_info_mode = 'ARMATURE_PROPERTY'
 146
 147        # エクスポート時のデフォルトパスを取得
 148        #if not self.filepath[-6:] == '.model':
 149        if common.preferences().model_default_path:
 150            self.filepath = common.default_cm3d2_dir(common.preferences().model_default_path, self.model_name, "model")
 151        else:
 152            self.filepath = common.default_cm3d2_dir(common.preferences().model_export_path, self.model_name, "model")
 153
 154        # バックアップ関係
 155        self.is_backup = bool(common.preferences().backup_ext)
 156
 157        self.scale = 1.0 / common.preferences().scale
 158        context.window_manager.fileselect_add(self)
 159        return {'RUNNING_MODAL'}
 160
 161    # 'is_batch' がオンなら非表示
 162    def draw(self, context):
 163        self.layout.prop(self, 'scale')
 164        row = self.layout.row()
 165        row.prop(self, 'is_backup', icon='FILE_BACKUP')
 166        if not common.preferences().backup_ext:
 167            row.enabled = False
 168        self.layout.prop(self, 'is_arrange_name', icon='FILE_TICK')
 169        box = self.layout.box()
 170        box.prop(self, 'version', icon='LINENUMBERS_ON')
 171        box.prop(self, 'model_name', icon='SORTALPHA')
 172
 173        row = box.row()
 174        row.prop(self, 'base_bone_name', icon='CONSTRAINT_BONE')
 175        if self.base_bone_name == 'Auto':
 176            row.enabled = False
 177
 178        prefs = common.preferences()
 179        
 180        box = self.layout.box()
 181        col = box.column(align=True)
 182        col.label(text="ボーン情報元", icon='BONE_DATA')
 183        col.prop(self, 'bone_info_mode', icon='BONE_DATA', expand=True)
 184        col = box.column(align=True)
 185        col.label(text="マテリアル情報元", icon='MATERIAL')
 186        col.prop(self, 'mate_info_mode', icon='MATERIAL', expand=True)
 187        
 188        box = self.layout.box()
 189        box.label(text="メッシュオプション")
 190        box.prop(self , 'is_align_to_base_bone', icon=compat.icon('OBJECT_ORIGIN'  ))
 191        box.prop(self , 'is_convert_tris'      , icon=compat.icon('MESH_DATA'      ))
 192        box.prop(self , 'is_split_sharp'       , icon=compat.icon('MOD_EDGESPLIT'  ))
 193        box.prop(self , 'export_tangent'       , icon=compat.icon('CURVE_BEZCIRCLE'))
 194        sub_box = box.box()
 195        sub_box.prop(self , 'shapekey_threshold'     , icon=compat.icon('SHAPEKEY_DATA'      ), slider=True)
 196        sub_box.prop(prefs, 'skip_shapekey'          , icon=compat.icon('SHAPEKEY_DATA'      ), toggle=1)
 197        sub_box.prop(self , 'export_shapekey_normals', icon=compat.icon('NORMALS_VERTEX_FACE'))
 198        row = sub_box.row()
 199        row    .prop(self , 'shapekey_normals_blend' , icon=compat.icon('MOD_NORMALEDIT'     ), slider=True)
 200        row.enabled = self.export_shapekey_normals
 201        row = sub_box.row()
 202        row    .prop(self , 'use_shapekey_colors'    , icon=compat.icon('GROUP_VCOL')         , toggle=0)
 203        row.enabled = self.export_shapekey_normals
 204        sub_box = box.box()
 205        sub_box.prop(self, 'is_normalize_weight', icon='MOD_VERTEX_WEIGHT')
 206        sub_box.prop(self, 'is_clean_vertex_groups', icon='MOD_VERTEX_WEIGHT')
 207        sub_box.prop(self, 'is_convert_bone_weight_names', icon_value=common.kiss_icon())
 208        sub_box
 209        sub_box = box.box()
 210        sub_box.prop(prefs, 'is_apply_modifiers', icon='MODIFIER')
 211        row = sub_box.row()
 212        row.prop(prefs, 'custom_normal_blend', icon='SNAP_NORMAL', slider=True)
 213        row.enabled = prefs.is_apply_modifiers
 214
 215    def copy_and_activate_ob(self, context, ob):
 216        new_ob = ob.copy()
 217        new_me = ob.data.copy()
 218        new_ob.data = new_me
 219        compat.link(context.scene, new_ob)
 220        compat.set_active(context, new_ob)
 221        compat.set_select(new_ob, True)
 222        return new_ob
 223
 224    def execute(self, context):
 225        start_time = time.time()
 226        prefs = common.preferences()
 227
 228        selected_objs = context.selected_objects
 229        source_objs = []
 230        prev_mode = None
 231        try:
 232            ob_source = context.active_object
 233            if ob_source not in selected_objs:
 234                selected_objs.append(ob_source) # luvoid : Fix error where object is active but not selected
 235            ob_name = ob_source.name
 236            ob_main = None
 237            if self.is_batch:
 238                # アクティブオブジェクトを1つコピーするだけでjoinしない
 239                source_objs.append(ob_source)
 240                compat.set_select(ob_source, False)
 241                ob_main = self.copy_and_activate_ob(context, ob_source)
 242
 243                if prefs.is_apply_modifiers and bpy.ops.object.forced_modifier_apply.poll(context):
 244                    bpy.ops.object.forced_modifier_apply(is_applies=[True for i in range(32)])
 245            else:
 246                selected_count = 0
 247                # 選択されたMESHオブジェクトをコピーしてjoin
 248                # 必要に応じて、モディファイアの強制適用を行う
 249                for selected in selected_objs:
 250                    source_objs.append(selected)
 251
 252                    compat.set_select(selected, False)
 253
 254                    if selected.type == 'MESH':
 255                        ob_created = self.copy_and_activate_ob(context, selected)
 256                        if selected == ob_source:
 257                            ob_main = ob_created
 258                        if prefs.is_apply_modifiers:
 259                            bpy.ops.object.forced_modifier_apply(apply_viewport_visible=True)
 260
 261                        selected_count += 1
 262
 263                mode = context.active_object.mode
 264                if mode != 'OBJECT':
 265                    prev_mode = mode
 266                    bpy.ops.object.mode_set(mode='OBJECT')
 267
 268                if selected_count > 1:
 269                    if ob_main:
 270                        compat.set_active(context, ob_main)
 271                    bpy.ops.object.join()
 272                    self.report(type={'INFO'}, message=f_tip_("{}個のオブジェクトをマージしました", selected_count))
 273
 274            ret = self.export(context, ob_main)
 275            if 'FINISHED' not in ret:
 276                return ret
 277
 278            context.window_manager.progress_update(10)
 279            diff_time = time.time() - start_time
 280            self.report(type={'INFO'}, message=f_tip_("modelのエクスポートが完了しました。{:.2f} 秒 file={}", diff_time, self.filepath))
 281            return ret
 282        finally:
 283            # 作業データの破棄(コピーデータを削除、選択状態の復元、アクティブオブジェクト、モードの復元)
 284            if ob_main:
 285                common.remove_data(ob_main)
 286                # me_copied = ob_main.data
 287                # context.blend_data.objects.remove(ob_main, do_unlink=True)
 288                # context.blend_data.meshes.remove(me_copied, do_unlink=True)
 289
 290            for obj in source_objs:
 291                compat.set_select(obj, True)
 292
 293            if ob_source:
 294                # TODO 元のオブジェクトをアクティブに戻す
 295                if ob_name in bpy.data.objects:
 296                    compat.set_active(context, ob_source)
 297
 298            if prev_mode:
 299                bpy.ops.object.mode_set(mode=prev_mode)
 300
 301    def export(self, context, ob):
 302        """モデルファイルを出力"""
 303        prefs = common.preferences()
 304
 305        if not self.is_batch:
 306            prefs.model_export_path = self.filepath
 307            prefs.scale = 1.0 / self.scale
 308
 309        context.window_manager.progress_begin(0, 10)
 310        context.window_manager.progress_update(0)
 311
 312        res = self.precheck(context)
 313        if res:
 314            return res
 315        me = ob.data
 316
 317        if ob.active_shape_key_index != 0:
 318            ob.active_shape_key_index = 0
 319            me.update()
 320
 321        # データの成否チェック
 322        if self.bone_info_mode == 'ARMATURE':
 323            arm_ob = ob.parent
 324            if arm_ob and arm_ob.type != 'ARMATURE':
 325                return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません")
 326            if not arm_ob:
 327                try:
 328                    arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object)
 329                except StopIteration:
 330                    return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい")
 331                arm_ob = arm_ob.object
 332        elif self.bone_info_mode == 'TEXT':
 333            if "BoneData" not in context.blend_data.texts:
 334                return self.report_cancel("テキスト「BoneData」が見つかりません、中止します")
 335            if "LocalBoneData" not in context.blend_data.texts:
 336                return self.report_cancel("テキスト「LocalBoneData」が見つかりません、中止します")
 337        elif self.bone_info_mode == 'OBJECT_PROPERTY':
 338            if "BoneData:0" not in ob:
 339                return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません")
 340            if "LocalBoneData:0" not in ob:
 341                return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません")
 342        elif self.bone_info_mode == 'ARMATURE_PROPERTY':
 343            arm_ob = ob.parent
 344            if arm_ob and arm_ob.type != 'ARMATURE':
 345                return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません")
 346            if not arm_ob:
 347                try:
 348                    arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object)
 349                except StopIteration:
 350                    return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい")
 351                arm_ob = arm_ob.object
 352            if "BoneData:0" not in arm_ob.data:
 353                return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません")
 354            if "LocalBoneData:0" not in arm_ob.data:
 355                return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません")
 356        else:
 357            return self.report_cancel("ボーン情報元のモードがおかしいです")
 358
 359        if self.mate_info_mode == 'TEXT':
 360            for index, slot in enumerate(ob.material_slots):
 361                if "Material:" + str(index) not in context.blend_data.texts:
 362                    return self.report_cancel("マテリアル情報元のテキストが足りません")
 363        context.window_manager.progress_update(1)
 364
 365        # model名とか
 366        ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.')
 367        if self.model_name == '*':
 368            self.model_name = ob_names[0]
 369        if self.base_bone_name == '*':
 370            self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto'
 371
 372        # BoneData情報読み込み
 373        base_bone_candidate = None
 374        bone_data = []
 375        if self.bone_info_mode == 'ARMATURE':
 376            bone_data = self.armature_bone_data_parser(context, arm_ob)
 377            base_bone_candidate = arm_ob.data['BaseBone']
 378        elif self.bone_info_mode == 'TEXT':
 379            bone_data_text = context.blend_data.texts["BoneData"]
 380            if 'BaseBone' in bone_data_text:
 381                base_bone_candidate = bone_data_text['BaseBone']
 382            bone_data = self.bone_data_parser(l.body for l in bone_data_text.lines)
 383        elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']:
 384            target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data
 385            if 'BaseBone' in target:
 386                base_bone_candidate = target['BaseBone']
 387            bone_data = self.bone_data_parser(self.indexed_data_generator(target, prefix="BoneData:"))
 388        if len(bone_data) <= 0:
 389            return self.report_cancel("テキスト「BoneData」に有効なデータがありません")
 390
 391        if self.base_bone_name not in (b['name'] for b in bone_data):
 392            if base_bone_candidate and self.base_bone_name == 'Auto':
 393                self.base_bone_name = base_bone_candidate
 394            else:
 395                return self.report_cancel("基点ボーンが存在しません")
 396        bone_name_indices = {bone['name']: index for index, bone in enumerate(bone_data)}
 397        context.window_manager.progress_update(2)
 398
 399        if self.is_align_to_base_bone:
 400            bpy.ops.object.align_to_cm3d2_base_bone(scale=1.0/self.scale, is_preserve_mesh=True, bone_info_mode=self.bone_info_mode)
 401            me.update()
 402
 403        if self.is_split_sharp:
 404            bpy.ops.object.mode_set(mode='EDIT')
 405            bpy.ops.mesh.split_sharp()
 406            bpy.ops.object.mode_set(mode='OBJECT')
 407
 408        # LocalBoneData情報読み込み
 409        local_bone_data = []
 410        if self.bone_info_mode == 'ARMATURE':
 411            local_bone_data = self.armature_local_bone_data_parser(arm_ob)
 412        elif self.bone_info_mode == 'TEXT':
 413            local_bone_data_text = context.blend_data.texts["LocalBoneData"]
 414            local_bone_data = self.local_bone_data_parser(l.body for l in local_bone_data_text.lines)
 415        elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']:
 416            target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data
 417            local_bone_data = self.local_bone_data_parser(self.indexed_data_generator(target, prefix="LocalBoneData:"))
 418        if len(local_bone_data) <= 0:
 419            return self.report_cancel("テキスト「LocalBoneData」に有効なデータがありません")
 420        local_bone_name_indices = {bone['name']: index for index, bone in enumerate(local_bone_data)}
 421        context.window_manager.progress_update(3)
 422        
 423        used_local_bone = {index: False for index, bone in enumerate(local_bone_data)}
 424        
 425        # ウェイト情報読み込み
 426        vertices = []
 427        is_over_one = 0
 428        is_under_one = 0
 429        is_in_too_many = 0
 430        for i, vert in enumerate(me.vertices):
 431            vgs = []
 432            for vg in vert.groups:
 433                if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group.
 434                    continue
 435                name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names)
 436                index = local_bone_name_indices.get(name, -1)
 437                if index >= 0 and (vg.weight > 0.0 or not self.is_clean_vertex_groups):
 438                    vgs.append([index, vg.weight])
 439                    # luvoid : track used bones
 440                    used_local_bone[index] = True
 441                    boneindex = bone_name_indices.get(name, -1)
 442                    while boneindex >= 0:
 443                        parent = bone_data[boneindex]
 444                        localindex = local_bone_name_indices.get(parent['name'], -1)
 445                        # could check for `localindex == -1` here, but it's prescence may be useful in determing if the local bones resolve back to some root
 446                        used_local_bone[localindex] = True
 447                        boneindex = parent['parent_index']
 448            if len(vgs) == 0:
 449                if not self.is_batch:
 450                    self.select_no_weight_vertices(context, local_bone_name_indices)
 451                return self.report_cancel("ウェイトが割り当てられていない頂点が見つかりました、中止します")
 452            if len(vgs) > 4:
 453                is_in_too_many += 1
 454            vgs = sorted(vgs, key=itemgetter(1), reverse=True)[0:4]
 455            total = sum(vg[1] for vg in vgs)
 456            if self.is_normalize_weight:
 457                for vg in vgs:
 458                    vg[1] /= total
 459            else:
 460                if 1.01 < total:
 461                    is_over_one += 1
 462                elif total < 0.99:
 463                    is_under_one += 1
 464            if len(vgs) < 4:
 465                vgs += [(0, 0.0)] * (4 - len(vgs))
 466            vertices.append({
 467                'index': vert.index,
 468                'face_indexs': list(map(itemgetter(0), vgs)),
 469                'weights': list(map(itemgetter(1), vgs)),
 470            })
 471        
 472        if 1 <= is_over_one:
 473            self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0を超えている頂点が見つかりました。正規化してください。超過している頂点の数:{}", is_over_one))
 474        if 1 <= is_under_one:
 475            self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0未満の頂点が見つかりました。正規化してください。不足している頂点の数:{}", is_under_one))
 476        
 477        # luvoid : warn that there are vertices in too many vertex groups
 478        if is_in_too_many > 0:
 479            self.report(type={'WARNING'}, message=f_tip_("4つを超える頂点グループにある頂点が見つかりました。頂点グループをクリーンアップしてください。不足している頂点の数:{}", is_in_too_many))
 480                
 481        # luvoid : check for unused local bones that the game will delete
 482        is_deleted = 0
 483        deleted_names = "The game will delete these local bones"
 484        for index, is_used in used_local_bone.items():
 485            print(index, is_used)
 486            if is_used == False:
 487                is_deleted += 1
 488                deleted_names = deleted_names + '\n' + local_bone_data[index]['name']
 489            elif is_used == True:
 490                pass
 491            else:
 492                print(f_tip_("Unexpected: used_local_bone[{key}] == {value} when len(used_local_bone) == {length}", key=index, value=is_used, length=len(used_local_bone)))
 493                self.report(type={'WARNING'}, message=f_tip_("Could not find whether bone with index {index} was used. See console for more info.", index=i))
 494        if is_deleted > 0:
 495            self.report(type={'WARNING'}, message=f_tip_("頂点が割り当てられていない{num}つのローカルボーンが見つかりました。 詳細については、ログを参照してください。", num=is_deleted))
 496            self.report(type={'INFO'}, message=deleted_names)
 497                
 498        context.window_manager.progress_update(4)
 499        
 500
 501        try:
 502            writer = common.open_temporary(self.filepath, 'wb', is_backup=self.is_backup)
 503        except:
 504            self.report(type={'ERROR'}, message=f_tip_("ファイルを開くのに失敗しました、アクセス不可かファイルが存在しません。file={}", self.filepath))
 505            return {'CANCELLED'}
 506
 507        model_datas = {
 508            'bone_data': bone_data,
 509            'local_bone_data': local_bone_data,
 510            'vertices': vertices,
 511        }
 512        try:
 513            with writer:
 514                self.write_model(context, ob, writer, **model_datas)
 515        except common.CM3D2ExportException as e:
 516            self.report(type={'ERROR'}, message=str(e))
 517            return {'CANCELLED'}
 518
 519        return {'FINISHED'}
 520
 521    def write_model(self, context, ob, writer, bone_data=[], local_bone_data=[], vertices=[]):
 522        """モデルデータをファイルオブジェクトに書き込む"""
 523        me = ob.data
 524        prefs = common.preferences()
 525
 526        # ファイル先頭
 527        common.write_str(writer, 'CM3D2_MESH')
 528        self.version_num = int(self.version)
 529        writer.write(struct.pack('<i', self.version_num))
 530
 531        common.write_str(writer, self.model_name)
 532        common.write_str(writer, self.base_bone_name)
 533
 534        # ボーン情報書き出し
 535        writer.write(struct.pack('<i', len(bone_data)))
 536        for bone in bone_data:
 537            common.write_str(writer, bone['name'])
 538            writer.write(struct.pack('<b', bone['scl']))
 539        context.window_manager.progress_update(3.3)
 540        for bone in bone_data:
 541            writer.write(struct.pack('<i', bone['parent_index']))
 542        context.window_manager.progress_update(3.7)
 543        for bone in bone_data:
 544            writer.write(struct.pack('<3f', bone['co'][0], bone['co'][1], bone['co'][2]))
 545            writer.write(struct.pack('<4f', bone['rot'][1], bone['rot'][2], bone['rot'][3], bone['rot'][0]))
 546            if self.version_num >= 2001:
 547                use_scale = ('scale' in bone)
 548                writer.write(struct.pack('<b', use_scale))
 549                if use_scale:
 550                    bone_scale = bone['scale']
 551                    writer.write(struct.pack('<3f', bone_scale[0], bone_scale[1], bone_scale[2]))
 552        context.window_manager.progress_update(4)
 553
 554        # 正しい頂点数などを取得
 555        bm = bmesh.new()
 556        bm.from_mesh(me)
 557        uv_lay = bm.loops.layers.uv.active
 558        vert_uvs = []
 559        vert_uvs_append = vert_uvs.append
 560        vert_iuv = {}
 561        vert_indices = {}
 562        vert_count = 0
 563        for vert in bm.verts:
 564            vert_uv = []
 565            vert_uvs_append(vert_uv)
 566            for loop in vert.link_loops:
 567                uv = loop[uv_lay].uv
 568                if uv not in vert_uv:
 569                    vert_uv.append(uv)
 570                    vert_iuv[hash((vert.index, uv.x, uv.y))] = vert_count
 571                    vert_indices[vert.index] = vert_count
 572                    vert_count += 1
 573        if 65535 < vert_count:
 574            raise common.CM3D2ExportException(f_tip_("頂点数がまだ多いです (現在{}頂点)。あと{}頂点以上減らしてください、中止します", vert_count, vert_count - 65535))
 575        context.window_manager.progress_update(5)
 576
 577        writer.write(struct.pack('<2i', vert_count, len(ob.material_slots)))
 578
 579        # ローカルボーン情報を書き出し
 580        writer.write(struct.pack('<i', len(local_bone_data)))
 581        for bone in local_bone_data:
 582            common.write_str(writer, bone['name'])
 583        context.window_manager.progress_update(5.3)
 584        for bone in local_bone_data:
 585            for f in bone['matrix']:
 586                writer.write(struct.pack('<f', f))
 587        context.window_manager.progress_update(5.7)
 588
 589        # カスタム法線情報を取得
 590        if me.has_custom_normals:
 591            custom_normals = [mathutils.Vector() for i in range(len(me.vertices))]
 592            me.calc_normals_split()
 593            for loop in me.loops:
 594                custom_normals[loop.vertex_index] += loop.normal
 595            for no in custom_normals:
 596                no.normalize()
 597        else:
 598            custom_normals = None
 599
 600        cm_verts = []
 601        cm_norms = []
 602        cm_uvs = []
 603        # 頂点情報を書き出し
 604        for i, vert in enumerate(bm.verts):
 605            co = compat.convert_bl_to_cm_space( vert.co * self.scale )
 606            if me.has_custom_normals:
 607                no = custom_normals[vert.index]
 608            else:
 609                no = vert.normal.copy()
 610            no = compat.convert_bl_to_cm_space( no )
 611            for uv in vert_uvs[i]:
 612                cm_verts.append(co)
 613                cm_norms.append(no)
 614                cm_uvs.append(uv)
 615                writer.write(struct.pack('<3f', co.x, co.y, co.z))
 616                writer.write(struct.pack('<3f', no.x, no.y, no.z))
 617                writer.write(struct.pack('<2f', uv.x, uv.y))
 618        context.window_manager.progress_update(6)
 619
 620        cm_tris = self.parse_triangles(bm, ob, uv_lay, vert_iuv, vert_indices)
 621
 622        # 接空間情報を書き出し
 623        if self.export_tangent:
 624            tangents = self.calc_tangents(cm_tris, cm_verts, cm_norms, cm_uvs)
 625            writer.write(struct.pack('<i', len(tangents)))
 626            for t in tangents:
 627                writer.write(struct.pack('<4f', *t))
 628        else:
 629            writer.write(struct.pack('<i', 0))
 630
 631        # ウェイト情報を書き出し
 632        for vert in vertices:
 633            for uv in vert_uvs[vert['index']]:
 634                writer.write(struct.pack('<4H', *vert['face_indexs']))
 635                writer.write(struct.pack('<4f', *vert['weights']))
 636        context.window_manager.progress_update(7)
 637
 638        # 面情報を書き出し
 639        for tri in cm_tris:
 640            writer.write(struct.pack('<i', len(tri)))
 641            for vert_index in tri:
 642                writer.write(struct.pack('<H', vert_index))
 643        context.window_manager.progress_update(8)
 644
 645        # マテリアルを書き出し
 646        writer.write(struct.pack('<i', len(ob.material_slots)))
 647        for slot_index, slot in enumerate(ob.material_slots):
 648            if self.mate_info_mode == 'MATERIAL':
 649                mat_data = cm3d2_data.MaterialHandler.parse_mate(slot.material, self.is_arrange_name)
 650                mat_data.write(writer, write_header=False)
 651
 652            elif self.mate_info_mode == 'TEXT':
 653                text = context.blend_data.texts["Material:" + str(slot_index)].as_string()
 654                mat_data = cm3d2_data.MaterialHandler.parse_text(slot.material, self.is_arrange_name)
 655                mat_data.write(writer, write_header=False)
 656
 657        context.window_manager.progress_update(9)
 658
 659        # モーフを書き出し
 660        if me.shape_keys and len(me.shape_keys.key_blocks) >= 2:
 661            try:
 662                self.write_shapekeys(context, ob, writer, vert_uvs, custom_normals)
 663            finally:
 664                print("FINISHED SHAPE KEYS WRITE")
 665                pass
 666        common.write_str(writer, 'end')
 667
 668    def write_shapekeys(self, context, ob, writer, vert_uvs, custom_normals=None):
 669        # モーフを書き出し
 670        me = ob.data
 671        prefs = common.preferences()
 672        
 673        is_use_attributes = (not compat.IS_LEGACY and bpy.app.version >= (2,92))
 674
 675        loops_vert_index = np.empty((len(me.loops)), dtype=int)
 676        me.loops.foreach_get('vertex_index', loops_vert_index.ravel())
 677
 678        def find_normals_attribute(name) -> (bpy.types.Attribute, bool):
 679            if is_use_attributes:
 680                normals_color = me.attributes[name] if name in me.attributes.keys() else None
 681                attribute_is_color = (not normals_color is None) and normals_color.data_type in {'BYTE_COLOR', 'FLOAT_COLOR'}
 682            else:
 683                normals_color = me.vertex_colors[name] if name in me.vertex_colors.keys() else None
 684                attribute_is_color = True
 685            return normals_color, attribute_is_color
 686
 687        if self.use_shapekey_colors:
 688            static_attribute_colors = np.empty((len(me.loops), 4), dtype=float)
 689            color_offset = np.array([[0.5,0.5,0.5]])
 690            loops_per_vertex = np.zeros((len(me.vertices)))
 691            for loop in me.loops:
 692                loops_per_vertex[loop.vertex_index] += 1
 693            loops_per_vertex_reciprocal = np.reciprocal(loops_per_vertex, out=loops_per_vertex).reshape((len(me.vertices), 1))
 694        def get_sk_delta_normals_from_attribute(attribute, is_color, out):
 695            if is_color:
 696                attribute.data.foreach_get('color', static_attribute_colors.ravel())
 697                loop_delta_normals = static_attribute_colors[:,:3]
 698                loop_delta_normals -= color_offset
 699                loop_delta_normals *= 2
 700            else:
 701                loop_delta_normals = static_attribute_colors[:,:3]
 702                attribute.data.foreach_get('vector', loop_delta_normals.ravel())
 703            
 704            vert_delta_normals = out
 705            vert_delta_normals.fill(0)
 706
 707            # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index]
 708            np.add.at(vert_delta_normals, loops_vert_index, loop_delta_normals) # XXX Slower but handles edge cases better
 709            #vert_delta_normals[loops_vert_index] += loop_delta_normals # XXX Only first loop's value will be kept
 710            
 711            # for delta_normal in vert_delta_normals: delta_normal /= loops_per_vertex[vert.index]
 712            vert_delta_normals *= loops_per_vertex_reciprocal
 713
 714            return out #.tolist()
 715
 716        if me.has_custom_normals:
 717            basis_custom_normals = np.array(custom_normals, dtype=float)
 718            static_loop_normals = np.empty((len(me.loops), 3), dtype=float)
 719            static_vert_lengths = np.empty((len(me.vertices), 1), dtype=float)
 720        def get_sk_delta_normals_from_custom_normals(shape_key, out):
 721            vert_custom_normals = out
 722            vert_custom_normals.fill(0)
 723            
 724            loop_custom_normals = static_loop_normals
 725            np.copyto(loop_custom_normals.ravel(), shape_key.normals_split_get())
 726            
 727            # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index]
 728            if not self.is_split_sharp:  
 729                # XXX Slower
 730                np.add.at(vert_custom_normals, loops_vert_index, loop_custom_normals)
 731                vert_len_sq = get_lengths_squared(vert_custom_normals, out=static_vert_lengths)
 732                vert_len = np.sqrt(vert_len_sq, out=vert_len_sq)
 733                np.reciprocal(vert_len, out=vert_len)
 734                vert_custom_normals *= vert_len #.reshape((*vert_len.shape, 1))
 735            else:
 736                # loop normals should be the same per-vertex unless there is a sharp edge 
 737                # or a flat shaded face, but all sharp edges were split, so this method is fine
 738                # (and Flat shaded faces just won't be supported)
 739                vert_custom_normals[loops_vert_index] += loop_custom_normals # Only first loop's value will be kept
 740
 741            vert_custom_normals -= basis_custom_normals
 742            return out
 743        
 744        if not me.has_custom_normals:
 745            basis_normals = np.empty((len(me.vertices), 3), dtype=float)
 746            me.vertices.foreach_get('normal', basis_normals.ravel())
 747        def get_sk_delta_normals_from_normals(shape_key, out):
 748            vert_normals = out
 749            np.copyto(vert_normals.ravel(), shape_key.normals_vertex_get())
 750            vert_delta_normals = np.subtract(vert_normals, basis_normals, out=out)
 751            return out
 752
 753        basis_co = np.empty((len(me.vertices), 3), dtype=float)
 754        me.vertices.foreach_get('co', basis_co.ravel())
 755        def get_sk_delta_coordinates(shape_key, out):
 756            delta_coordinates = out
 757            shape_key.data.foreach_get('co', delta_coordinates.ravel())
 758            delta_coordinates -= basis_co
 759            return out
 760
 761        static_array_sq = np.empty((len(me.vertices), 3), dtype=float)
 762        def get_lengths_squared(vectors, out):
 763            np.power(vectors, 2, out=static_array_sq)
 764            np.sum(static_array_sq, axis=1, out=out.ravel())
 765            return out
 766
 767        def write_morph(morph, name):
 768            common.write_str(writer, 'morph')
 769            common.write_str(writer, name)
 770            writer.write(struct.pack('<i', len(morph)))
 771            for v_index, vec, normal in morph:
 772                vec    = compat.convert_bl_to_cm_space(vec   )
 773                normal = compat.convert_bl_to_cm_space(normal)
 774                writer.write(struct.pack('<H', v_index))
 775                writer.write(struct.pack('<3f', *vec[:3]))
 776                writer.write(struct.pack('<3f', *normal[:3]))
 777        
 778        # accessing operator properties via "self.x" is SLOW, so store some here
 779        self__export_shapekey_normals = self.export_shapekey_normals
 780        self__use_shapekey_colors = self.use_shapekey_colors
 781        self__shapekey_normals_blend = self.shapekey_normals_blend
 782        self__scale = self.scale
 783        
 784        co_diff_threshold = self.shapekey_threshold / 5
 785        co_diff_threshold_squared = co_diff_threshold * co_diff_threshold
 786        no_diff_threshold = self.shapekey_threshold * 10
 787        no_diff_threshold_squared = no_diff_threshold * no_diff_threshold
 788        
 789        # shared arrays
 790        delta_coordinates  = np.empty((len(me.vertices), 3), dtype=float)
 791        vert_delta_normals = np.empty((len(me.vertices), 3), dtype=float)
 792        loop_delta_normals = np.empty((len(me.loops   ), 3), dtype=float)
 793
 794        delta_co_lensq = np.empty((len(me.vertices)), dtype=float)
 795        delta_no_lensq = np.empty((len(me.vertices)), dtype=float)
 796
 797        if not self.export_shapekey_normals:
 798            vert_delta_normals.fill(0)
 799            delta_no_lensq.fill(0)
 800
 801        # HEAVY LOOP
 802        for shape_key in me.shape_keys.key_blocks[1:]:
 803            morph = []
 804
 805            if self__export_shapekey_normals and self__use_shapekey_colors:
 806                normals_color, attrubute_is_color = find_normals_attribute(f'{shape_key.name}_delta_normals')
 807
 808            if self__export_shapekey_normals:
 809                if self__use_shapekey_colors and not normals_color is None:
 810                    sk_delta_normals = get_sk_delta_normals_from_attribute(normals_color, attrubute_is_color, out=vert_delta_normals)
 811                elif me.has_custom_normals:
 812                    sk_delta_normals = get_sk_delta_normals_from_custom_normals(shape_key, out=vert_delta_normals)
 813                    sk_delta_normals *= self__shapekey_normals_blend
 814                else:
 815                    sk_delta_normals = get_sk_delta_normals_from_normals(shape_key, out=vert_delta_normals)
 816                    sk_delta_normals *= self__shapekey_normals_blend
 817                
 818                sk_no_lensq = get_lengths_squared(sk_delta_normals, out=delta_no_lensq)
 819            else:
 820                sk_delta_normals = vert_delta_normals
 821                sk_no_lensq = delta_no_lensq
 822
 823            sk_co_diffs = get_sk_delta_coordinates(shape_key, out=delta_coordinates)
 824            sk_co_diffs *= self__scale # scale before getting lengths
 825            sk_co_lensq = get_lengths_squared(sk_co_diffs, out=delta_co_lensq)
 826
 827            # SUPER HEAVY LOOP
 828            outvert_index = 0
 829            for i in range(len(me.vertices)):
 830                if sk_co_lensq[i] >= co_diff_threshold_squared or sk_no_lensq[i] >= no_diff_threshold_squared:
 831                    morph += [ (outvert_index+j, sk_co_diffs[i], sk_delta_normals[i]) for j in range(len(vert_uvs[i])) ]
 832                else:
 833                    # ignore because change is too small (greatly lowers file size)
 834                    pass
 835                outvert_index += len(vert_uvs[i])
 836
 837            if prefs.skip_shapekey and not len(morph):
 838                continue
 839            else:
 840                write_morph(morph, shape_key.name)
 841
 842    def write_tangents(self, writer, me):
 843        if len(me.uv_layers) < 1:
 844            return
 845
 846        num_loops = len(me.loops)
 847
 848    def parse_triangles(self, bm, ob, uv_lay, vert_iuv, vert_indices):
 849        def vert_index_from_loops(loops):
 850            """vert_index generator"""
 851            for loop in loops:
 852                uv = loop[uv_lay].uv
 853                v_index = loop.vert.index
 854                vert_index = vert_iuv.get(hash((v_index, uv.x, uv.y)))
 855                if vert_index is None:
 856                    vert_index = vert_indices.get(v_index, 0)
 857                yield vert_index
 858
 859        triangles = []
 860        for mate_index, slot in enumerate(ob.material_slots):
 861            tris_faces = []
 862            for face in bm.faces:
 863                if face.material_index != mate_index:
 864                    continue
 865                if len(face.verts) == 3:
 866                    tris_faces.extend(vert_index_from_loops(reversed(face.loops)))
 867                elif len(face.verts) == 4 and self.is_convert_tris:
 868                    v1 = face.loops[0].vert.co - face.loops[2].vert.co
 869                    v2 = face.loops[1].vert.co - face.loops[3].vert.co
 870                    if v1.length < v2.length:
 871                        f1 = [0, 1, 2]
 872                        f2 = [0, 2, 3]
 873                    else:
 874                        f1 = [0, 1, 3]
 875                        f2 = [1, 2, 3]
 876                    faces, faces2 = [], []
 877                    for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))):
 878                        if i in f1:
 879                            faces.append(vert_index)
 880                        if i in f2:
 881                            faces2.append(vert_index)
 882                    tris_faces.extend(faces)
 883                    tris_faces.extend(faces2)
 884                elif 5 <= len(face.verts) and self.is_convert_tris:
 885                    face_count = len(face.verts) - 2
 886
 887                    tris = []
 888                    seek_min, seek_max = 0, len(face.verts) - 1
 889                    for i in range(face_count):
 890                        if not i % 2:
 891                            tris.append([seek_min, seek_min + 1, seek_max])
 892                            seek_min += 1
 893                        else:
 894                            tris.append([seek_min, seek_max - 1, seek_max])
 895                            seek_max -= 1
 896
 897                    tris_indexs = [[] for _ in range(len(tris))]
 898                    for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))):
 899                        for tris_index, points in enumerate(tris):
 900                            if i in points:
 901                                tris_indexs[tris_index].append(vert_index)
 902
 903                    tris_faces.extend(p for ps in tris_indexs for p in ps)
 904
 905            triangles.append(tris_faces)
 906        return triangles
 907
 908    def calc_tangents(self, cm_tris, cm_verts, cm_norms, cm_uvs):
 909        count = len(cm_verts)
 910        tan1 = [None] * count
 911        tan2 = [None] * count
 912        for i in range(0, count):
 913            tan1[i] = mathutils.Vector((0, 0, 0))
 914            tan2[i] = mathutils.Vector((0, 0, 0))
 915
 916        for tris in cm_tris:
 917            tri_len = len(tris)
 918            tri_idx = 0
 919            while tri_idx < tri_len:
 920                i1, i2, i3 = tris[tri_idx], tris[tri_idx + 1], tris[tri_idx + 2]
 921                v1, v2, v3 = cm_verts[i1], cm_verts[i2], cm_verts[i3]
 922                w1, w2, w3 = cm_uvs[i1], cm_uvs[i2], cm_uvs[i3]
 923
 924                a1 = v2 - v1
 925                a2 = v3 - v1
 926                s1 = w2 - w1
 927                s2 = w3 - w1
 928                
 929                r_inverse = (s1.x * s2.y - s2.x * s1.y)
 930
 931                if r_inverse != 0:
 932                    # print("i1 = {i1}   i2 = {i2}   i3 = {i3}".format(i1=i1, i2=i2, i3=i3))
 933                    # print("v1 = {v1}   v2 = {v2}   v3 = {v3}".format(v1=v1, v2=v2, v3=v3))
 934                    # print("w1 = {w1}   w2 = {w2}   w3 = {w3}".format(w1=w1, w2=w2, w3=w3))
 935
 936                    # print("a1 = {a1}   a2 = {a2}".format(a1=a1, a2=a2))
 937                    # print("s1 = {s1}   s2 = {s2}".format(s1=s1, s2=s2))
 938                    
 939                    # print("r_inverse = ({s1x} * {s2y} - {s2x} * {s1y}) = {r_inverse}".format(r_inverse=r_inverse, s1x=s1.x, s1y=s1.y, s2x=s2.x, s2y=s2.y))
 940                                                
 941                    r = 1.0 / r_inverse
 942                    sdir = mathutils.Vector(((s2.y * a1.x - s1.y * a2.x) * r, (s2.y * a1.y - s1.y * a2.y) * r, (s2.y * a1.z - s1.y * a2.z) * r))
 943                    tan1[i1] += sdir
 944                    tan1[i2] += sdir
 945                    tan1[i3] += sdir
 946
 947                    tdir = mathutils.Vector(((s1.x * a2.x - s2.x * a1.x) * r, (s1.x * a2.y - s2.x * a1.y) * r, (s1.x * a2.z - s2.x * a1.z) * r))
 948                    tan2[i1] += tdir
 949                    tan2[i2] += tdir
 950                    tan2[i3] += tdir
 951
 952                tri_idx += 3
 953
 954        tangents = [None] * count
 955        for i in range(0, count):
 956            n = cm_norms[i]
 957            ti = tan1[i]
 958            t = (ti - n * n.dot(ti)).normalized()
 959
 960            c = n.cross(ti)
 961            val = c.dot(tan2[i])
 962            w = 1.0 if val < 0 else -1.0
 963            tangents[i] = (-t.x, t.y, t.z, w)
 964
 965        return tangents
 966
 967    def select_no_weight_vertices(self, context, local_bone_name_indices):
 968        """ウェイトが割り当てられていない頂点を選択する"""
 969        ob = context.active_object
 970        me = ob.data
 971        bpy.ops.object.mode_set(mode='EDIT')
 972        bpy.ops.mesh.select_all(action='SELECT')
 973        #bpy.ops.object.mode_set(mode='OBJECT')
 974        context.tool_settings.mesh_select_mode = (True, False, False)
 975        for vert in me.vertices:
 976            for vg in vert.groups:
 977                if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group.
 978                    continue
 979                name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names)
 980                if name in local_bone_name_indices and 0.0 < vg.weight:
 981                    vert.select = False
 982                    break
 983        bpy.ops.object.mode_set(mode='EDIT')
 984
 985    def armature_bone_data_parser(self, context, ob):
 986        """アーマチュアを解析してBoneDataを返す"""
 987        arm = ob.data
 988        
 989        pre_active = compat.get_active(context)
 990        pre_mode = ob.mode
 991
 992        compat.set_active(context, ob)
 993        bpy.ops.object.mode_set(mode='EDIT')
 994
 995        bones = []
 996        bone_name_indices = {}
 997        already_bone_names = []
 998        bones_queue = arm.edit_bones[:]
 999        while len(bones_queue):
1000            bone = bones_queue.pop(0)
1001
1002            if not bone.parent:
1003                already_bone_names.append(bone.name)
1004                bones.append(bone)
1005                bone_name_indices[bone.name] = len(bone_name_indices)
1006                continue
1007            elif bone.parent.name in already_bone_names:
1008                already_bone_names.append(bone.name)
1009                bones.append(bone)
1010                bone_name_indices[bone.name] = len(bone_name_indices)
1011                continue
1012
1013            bones_queue.append(bone)
1014
1015        bone_data = []
1016        for bone in bones:
1017
1018            # Also check for UnknownFlag for backwards compatibility
1019            is_scl_bone = bone['cm3d2_scl_bone'] if 'cm3d2_scl_bone' in bone \
1020                     else bone['UnknownFlag']    if 'UnknownFlag'    in bone \
1021                     else 0 
1022            parent_index = bone_name_indices[bone.parent.name] if bone.parent else -1
1023
1024            mat = bone.matrix.copy()
1025            
1026            if bone.parent:
1027                mat = compat.convert_bl_to_cm_bone_rotation(mat)
1028                mat = compat.mul(bone.parent.matrix.inverted(), mat)
1029                mat = compat.convert_bl_to_cm_bone_space(mat)
1030            else:
1031                mat = compat.convert_bl_to_cm_bone_rotation(mat)
1032                mat = compat.convert_bl_to_cm_space(mat)
1033            
1034            co = mat.to_translation() * self.scale
1035            rot = mat.to_quaternion()
1036            
1037            #if bone.parent:
1038            #    co.x, co.y, co.z = -co.y, -co.x, co.z
1039            #    rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, rot.x, -rot.z
1040            #else:
1041            #    co.x, co.y, co.z = -co.x, co.z, -co.y
1042            #
1043            #    fix_quat  = compat.Z_UP_TO_Y_UP_QUAT    #mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
1044            #    fix_quat2 = compat.BLEND_TO_OPENGL_QUAT #mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion()
1045            #    rot = compat.mul3(rot, fix_quat, fix_quat2)
1046            #    #rot = compat.mul3(fix_quat2, rot, fix_quat)
1047            #
1048            #    rot.w, rot.x, rot.y, rot.z = -rot.y, -rot.z, -rot.x, rot.w
1049            
1050            # luvoid : I copied this from the Bone-Util Addon by trzr
1051            #if bone.parent:
1052            #    co.x, co.y, co.z = -co.y, co.z, co.x
1053            #    rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, -rot.z, -rot.x
1054            #else:
1055            #    co.x, co.y, co.z = -co.x, co.z, -co.y
1056            #    
1057            #    rot = compat.mul(rot, mathutils.Quaternion((0, 0, 1), math.radians(90)))
1058            #    rot.w, rot.x, rot.y, rot.z = -rot.w, -rot.x, rot.z, -rot.y
1059            
1060            #opengl_mat = compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.matrix)
1061            #
1062            #if bone.parent:
1063            #    opengl_mat = compat.mul(compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.parent.matrix).inverted(), opengl_mat)
1064            #
1065            #co = opengl_mat.to_translation() * self.scale
1066            #rot = opengl_mat.to_quaternion()
1067
1068            data = {
1069                'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names),
1070                'scl': is_scl_bone,
1071                'parent_index': parent_index,
1072                'co': co.copy(),
1073                'rot': rot.copy(),
1074            }
1075            scale = arm.edit_bones[bone.name].get('cm3d2_bone_scale')
1076            if scale:
1077                data['scale'] = scale
1078            bone_data.append(data)
1079        
1080        bpy.ops.object.mode_set(mode=pre_mode)
1081        compat.set_active(context, pre_active)
1082        return bone_data
1083
1084    @staticmethod
1085    def bone_data_parser(container):
1086        """BoneData テキストをパースして辞書を要素とするリストを返す"""
1087        bone_data = []
1088        bone_name_indices = {}
1089        for line in container:
1090            data = line.split(',')
1091            if len(data) < 5:
1092                continue
1093
1094            parent_name = data[2]
1095            if parent_name.isdigit():
1096                parent_index = int(parent_name)
1097            else:
1098                parent_index = bone_name_indices.get(parent_name, -1)
1099
1100            bone_datum = {
1101                'name': data[0],
1102                'scl': int(data[1]),
1103                'parent_index': parent_index,
1104                'co': list(map(float, data[3].split())),
1105                'rot': list(map(float, data[4].split())),
1106            }
1107            # scale info (for version 2001 or later)
1108            if len(data) >= 7:
1109                if data[5] == '1':
1110                    bone_scale = data[6]
1111                    bone_datum['scale'] = list(map(float, bone_scale.split()))
1112            bone_data.append(bone_datum)
1113            bone_name_indices[data[0]] = len(bone_name_indices)
1114        return bone_data
1115
1116    def armature_local_bone_data_parser(self, ob):
1117        """アーマチュアを解析してBoneDataを返す"""
1118        arm = ob.data
1119
1120        # XXX Instead of just adding all bones, only bones / bones-with-decendants 
1121        #     that have use_deform == True or mathcing vertex groups should be used
1122        bones = []
1123        bone_name_indices = {}
1124        already_bone_names = []
1125        bones_queue = arm.bones[:]
1126        while len(bones_queue):
1127            bone = bones_queue.pop(0)
1128
1129            if not bone.parent:
1130                already_bone_names.append(bone.name)
1131                bones.append(bone)
1132                bone_name_indices[bone.name] = len(bone_name_indices)
1133                continue
1134            elif bone.parent.name in already_bone_names:
1135                already_bone_names.append(bone.name)
1136                bones.append(bone)
1137                bone_name_indices[bone.name] = len(bone_name_indices)
1138                continue
1139
1140            bones_queue.append(bone)
1141
1142        local_bone_data = []
1143        for bone in bones:
1144            mat = bone.matrix_local.copy()
1145            mat = compat.mul(mathutils.Matrix.Scale(-1, 4, (1, 0, 0)), mat)
1146            mat = compat.convert_bl_to_cm_bone_rotation(mat)
1147            pos = mat.translation.copy()
1148            
1149            mat.transpose()
1150            mat.row[3] = (0.0, 0.0, 0.0, 1.0)
1151            pos = compat.mul(mat.to_3x3(), pos)
1152            pos *= -self.scale
1153            mat.translation = pos
1154            mat.transpose()
1155            
1156            #co = mat.to_translation() * self.scale
1157            #rot = mat.to_quaternion()
1158            #
1159            #co.rotate(rot.inverted())
1160            #co.x, co.y, co.z = co.y, co.x, -co.z
1161            #
1162            #fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
1163            #rot = compat.mul(rot, fix_quat)
1164            #rot.w, rot.x, rot.y, rot.z = -rot.z, -rot.y, -rot.x, rot.w
1165            #
1166            #co_mat = mathutils.Matrix.Translation(co)
1167            #rot_mat = rot.to_matrix().to_4x4()
1168            #mat = compat.mul(co_mat, rot_mat)
1169            #
1170            #copy_mat = mat.copy()
1171            #mat[0][0], mat[0][1], mat[0][2], mat[0][3] = copy_mat[0][0], copy_mat[1][0], copy_mat[2][0], copy_mat[3][0]
1172            #mat[1][0], mat[1][1], mat[1][2], mat[1][3] = copy_mat[0][1], copy_mat[1][1], copy_mat[2][1], copy_mat[3][1]
1173            #mat[2][0], mat[2][1], mat[2][2], mat[2][3] = copy_mat[0][2], copy_mat[1][2], copy_mat[2][2], copy_mat[3][2]
1174            #mat[3][0], mat[3][1], mat[3][2], mat[3][3] = copy_mat[0][3], copy_mat[1][3], copy_mat[2][3], copy_mat[3][3]
1175
1176            mat_array = []
1177            for vec in mat:
1178                mat_array.extend(vec[:])
1179            
1180            local_bone_data.append({
1181                'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names),
1182                'matrix': mat_array,
1183            })
1184        return local_bone_data
1185
1186    @staticmethod
1187    def local_bone_data_parser(container):
1188        """LocalBoneData テキストをパースして辞書を要素とするリストを返す"""
1189        local_bone_data = []
1190        for line in container:
1191            data = line.split(',')
1192            if len(data) != 2:
1193                continue
1194            local_bone_data.append({
1195                'name': data[0],
1196                'matrix': list(map(float, data[1].split())),
1197            })
1198        return local_bone_data
1199
1200    @staticmethod
1201    def indexed_data_generator(container, prefix='', max_index=9**9, max_pass=50):
1202        """コンテナ内の数値インデックスをキーに持つ要素を昇順に返すジェネレーター"""
1203        pass_count = 0
1204        for i in range(max_index):
1205            name = prefix + str(i)
1206            if name not in container:
1207                pass_count += 1
1208                if max_pass < pass_count:
1209                    return
1210                continue
1211            yield container[name]
1212
1213
1214# メニューを登録する関数
1215def menu_func(self, context):
1216    self.layout.operator(CNV_OT_export_cm3d2_model.bl_idname, icon_value=common.kiss_icon())
@compat.BlRegister()
class CNV_OT_export_cm3d2_model(bpy_types.Operator):
  17@compat.BlRegister()
  18class CNV_OT_export_cm3d2_model(bpy.types.Operator):
  19    bl_idname = 'export_mesh.export_cm3d2_model'
  20    bl_label = "CM3D2モデル (.model)"
  21    bl_description = "カスタムメイド3D2のmodelファイルを書き出します"
  22    bl_options = {'REGISTER'}
  23
  24    filepath = bpy.props.StringProperty(subtype='FILE_PATH')
  25    filename_ext = ".model"
  26    filter_glob = bpy.props.StringProperty(default="*.model", options={'HIDDEN'})
  27
  28    scale = bpy.props.FloatProperty(name="倍率", default=0.2, min=0.01, max=100, soft_min=0.01, soft_max=100, step=10, precision=2, description="エクスポート時のメッシュ等の拡大率です")
  29
  30    is_backup = bpy.props.BoolProperty(name="ファイルをバックアップ", default=True, description="ファイルに上書きする場合にバックアップファイルを複製します")
  31
  32    version = bpy.props.EnumProperty(
  33        name="ファイルバージョン",
  34        items=[
  35            ('2001', '2001', 'model version 2001 (available only for com3d2)', 'NONE', 0),
  36            ('2000', '2000', 'model version 2000 (com3d2 version)', 'NONE', 1),
  37            ('1000', '1000', 'model version 1000 (available for cm3d2/com3d2)', 'NONE', 2),
  38        ], default='1000')
  39    model_name = bpy.props.StringProperty(name="model名", default="*")
  40    base_bone_name = bpy.props.StringProperty(name="基点ボーン名", default="*")
  41
  42    items = [
  43        ('ARMATURE'         , "アーマチュア", "", 'OUTLINER_OB_ARMATURE', 1),
  44        ('TEXT'             , "テキスト", "", 'FILE_TEXT', 2),
  45        ('OBJECT_PROPERTY'  , "オブジェクト内プロパティ", "", 'OBJECT_DATAMODE', 3),
  46        ('ARMATURE_PROPERTY', "アーマチュア内プロパティ", "", 'ARMATURE_DATA', 4),
  47    ]
  48    bone_info_mode = bpy.props.EnumProperty(items=items, name="ボーン情報元", default='OBJECT_PROPERTY', description="modelファイルに必要なボーン情報をどこから引っ張ってくるか選びます")
  49
  50    items = [
  51        ('TEXT', "テキスト", "", 'FILE_TEXT', 1),
  52        ('MATERIAL', "マテリアル", "", 'MATERIAL', 2),
  53    ]
  54    mate_info_mode = bpy.props.EnumProperty(items=items, name="マテリアル情報元", default='MATERIAL', description="modelファイルに必要なマテリアル情報をどこから引っ張ってくるか選びます")
  55
  56    is_arrange_name = bpy.props.BoolProperty(name="データ名の連番を削除", default=True, description="「○○.001」のような連番が付属したデータ名からこれらを削除します")
  57
  58    is_align_to_base_bone = bpy.props.BoolProperty(name="Align to Base Bone", default=True, description="Align the object to it's base bone")
  59    is_convert_tris = bpy.props.BoolProperty(name="四角面を三角面に", default=True, description="四角ポリゴンを三角ポリゴンに変換してから出力します、元のメッシュには影響ありません")
  60    is_split_sharp = bpy.props.BoolProperty(name="Split Sharp Edges", default=True, description="Split all edges marked as sharp.")
  61    is_normalize_weight = bpy.props.BoolProperty(name="ウェイトの合計を1.0に", default=True, description="4つのウェイトの合計値が1.0になるように正規化します")
  62    is_convert_bone_weight_names = bpy.props.BoolProperty(name="頂点グループ名をCM3D2用に変換", default=True, description="全ての頂点グループ名をCM3D2で使える名前にしてからエクスポートします")
  63    is_clean_vertex_groups = bpy.props.BoolProperty(name="クリーンな頂点グループ", default=True, description="重みがゼロの場合、頂点グループから頂点を削除します")
  64    
  65    is_batch = bpy.props.BoolProperty(name="バッチモード", default=False, description="モードの切替やエラー個所の選択を行いません")
  66
  67    export_tangent = bpy.props.BoolProperty(name="接空間情報出力", default=False, description="接空間情報(binormals, tangents)を出力する")
  68
  69    
  70    shapekey_threshold = bpy.props.FloatProperty(name="Shape Key Threshold", default=0.00100, min=0, soft_min=0.0005, max=0.01, soft_max=0.002, precision=5, description="Lower values increase accuracy and file size. Higher values truncate small changes and reduce file size.")
  71    export_shapekey_normals = bpy.props.BoolProperty(name="Export Shape Key Normals", default=True, description="Export custom normals for each shape key on export.")
  72    shapekey_normals_blend = bpy.props.FloatProperty(name="Shape Key Normals Blend", default=0.6, min=0, max=1, precision=3, description="Adjust the influence of shape keys on custom normals")
  73    use_shapekey_colors = bpy.props.BoolProperty(name="Use Shape Key Colors", default=True, description="Use the shape key normals stored in the vertex colors instead of calculating the normals on export. (Recommend disabling if geometry was customized)")
  74    
  75
  76    @classmethod
  77    def poll(cls, context):
  78        ob = context.active_object
  79        if ob:
  80            if ob.type == 'MESH':
  81                return True
  82        return False
  83
  84    def report_cancel(self, report_message, report_type={'ERROR'}, resobj={'CANCELLED'}):
  85        """エラーメッセージを出力してキャンセルオブジェクトを返す"""
  86        self.report(type=report_type, message=report_message)
  87        return resobj
  88
  89    def precheck(self, context):
  90        """データの成否チェック"""
  91        ob = context.active_object
  92        if not ob:
  93            return self.report_cancel("アクティブオブジェクトがありません")
  94        if ob.type != 'MESH':
  95            return self.report_cancel("メッシュオブジェクトを選択した状態で実行してください")
  96        if not len(ob.material_slots):
  97            return self.report_cancel("マテリアルがありません")
  98        for slot in ob.material_slots:
  99            if not slot.material:
 100                return self.report_cancel("空のマテリアルスロットを削除してください")
 101            try:
 102                slot.material['shader1']
 103                slot.material['shader2']
 104            except:
 105                return self.report_cancel("マテリアルに「shader1」と「shader2」という名前のカスタムプロパティを用意してください")
 106        me = ob.data
 107        if not me.uv_layers.active:
 108            return self.report_cancel("UVがありません")
 109        if 65535 < len(me.vertices):
 110            return self.report_cancel("エクスポート可能な頂点数を大幅に超えています、最低でも65535未満には削減してください")
 111        return None
 112
 113    def invoke(self, context, event):
 114        res = self.precheck(context)
 115        if res:
 116            return res
 117        ob = context.active_object
 118
 119        # model名とか
 120        ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.')
 121        self.model_name = ob_names[0]
 122        self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto'
 123
 124        # ボーン情報元のデフォルトオプションを取得
 125        arm_ob = ob.parent
 126        for mod in ob.modifiers:
 127            if mod.type == 'ARMATURE' and mod.object:
 128                arm_ob = mod.object
 129        if arm_ob and not arm_ob.type == 'ARMATURE':
 130            arm_ob = None
 131
 132        info_mode_was_armature = (self.bone_info_mode == 'ARMATURE')
 133        if "BoneData" in context.blend_data.texts:
 134            if "LocalBoneData" in context.blend_data.texts:
 135                self.bone_info_mode = 'TEXT'
 136        if "BoneData:0" in ob:
 137            ver = ob.get("ModelVersion")
 138            if ver and ver >= 1000:
 139                self.version = str(ver)
 140            if "LocalBoneData:0" in ob:
 141                self.bone_info_mode = 'OBJECT_PROPERTY'
 142        if arm_ob:
 143            if info_mode_was_armature:
 144                self.bone_info_mode = 'ARMATURE'
 145            else:
 146                self.bone_info_mode = 'ARMATURE_PROPERTY'
 147
 148        # エクスポート時のデフォルトパスを取得
 149        #if not self.filepath[-6:] == '.model':
 150        if common.preferences().model_default_path:
 151            self.filepath = common.default_cm3d2_dir(common.preferences().model_default_path, self.model_name, "model")
 152        else:
 153            self.filepath = common.default_cm3d2_dir(common.preferences().model_export_path, self.model_name, "model")
 154
 155        # バックアップ関係
 156        self.is_backup = bool(common.preferences().backup_ext)
 157
 158        self.scale = 1.0 / common.preferences().scale
 159        context.window_manager.fileselect_add(self)
 160        return {'RUNNING_MODAL'}
 161
 162    # 'is_batch' がオンなら非表示
 163    def draw(self, context):
 164        self.layout.prop(self, 'scale')
 165        row = self.layout.row()
 166        row.prop(self, 'is_backup', icon='FILE_BACKUP')
 167        if not common.preferences().backup_ext:
 168            row.enabled = False
 169        self.layout.prop(self, 'is_arrange_name', icon='FILE_TICK')
 170        box = self.layout.box()
 171        box.prop(self, 'version', icon='LINENUMBERS_ON')
 172        box.prop(self, 'model_name', icon='SORTALPHA')
 173
 174        row = box.row()
 175        row.prop(self, 'base_bone_name', icon='CONSTRAINT_BONE')
 176        if self.base_bone_name == 'Auto':
 177            row.enabled = False
 178
 179        prefs = common.preferences()
 180        
 181        box = self.layout.box()
 182        col = box.column(align=True)
 183        col.label(text="ボーン情報元", icon='BONE_DATA')
 184        col.prop(self, 'bone_info_mode', icon='BONE_DATA', expand=True)
 185        col = box.column(align=True)
 186        col.label(text="マテリアル情報元", icon='MATERIAL')
 187        col.prop(self, 'mate_info_mode', icon='MATERIAL', expand=True)
 188        
 189        box = self.layout.box()
 190        box.label(text="メッシュオプション")
 191        box.prop(self , 'is_align_to_base_bone', icon=compat.icon('OBJECT_ORIGIN'  ))
 192        box.prop(self , 'is_convert_tris'      , icon=compat.icon('MESH_DATA'      ))
 193        box.prop(self , 'is_split_sharp'       , icon=compat.icon('MOD_EDGESPLIT'  ))
 194        box.prop(self , 'export_tangent'       , icon=compat.icon('CURVE_BEZCIRCLE'))
 195        sub_box = box.box()
 196        sub_box.prop(self , 'shapekey_threshold'     , icon=compat.icon('SHAPEKEY_DATA'      ), slider=True)
 197        sub_box.prop(prefs, 'skip_shapekey'          , icon=compat.icon('SHAPEKEY_DATA'      ), toggle=1)
 198        sub_box.prop(self , 'export_shapekey_normals', icon=compat.icon('NORMALS_VERTEX_FACE'))
 199        row = sub_box.row()
 200        row    .prop(self , 'shapekey_normals_blend' , icon=compat.icon('MOD_NORMALEDIT'     ), slider=True)
 201        row.enabled = self.export_shapekey_normals
 202        row = sub_box.row()
 203        row    .prop(self , 'use_shapekey_colors'    , icon=compat.icon('GROUP_VCOL')         , toggle=0)
 204        row.enabled = self.export_shapekey_normals
 205        sub_box = box.box()
 206        sub_box.prop(self, 'is_normalize_weight', icon='MOD_VERTEX_WEIGHT')
 207        sub_box.prop(self, 'is_clean_vertex_groups', icon='MOD_VERTEX_WEIGHT')
 208        sub_box.prop(self, 'is_convert_bone_weight_names', icon_value=common.kiss_icon())
 209        sub_box
 210        sub_box = box.box()
 211        sub_box.prop(prefs, 'is_apply_modifiers', icon='MODIFIER')
 212        row = sub_box.row()
 213        row.prop(prefs, 'custom_normal_blend', icon='SNAP_NORMAL', slider=True)
 214        row.enabled = prefs.is_apply_modifiers
 215
 216    def copy_and_activate_ob(self, context, ob):
 217        new_ob = ob.copy()
 218        new_me = ob.data.copy()
 219        new_ob.data = new_me
 220        compat.link(context.scene, new_ob)
 221        compat.set_active(context, new_ob)
 222        compat.set_select(new_ob, True)
 223        return new_ob
 224
 225    def execute(self, context):
 226        start_time = time.time()
 227        prefs = common.preferences()
 228
 229        selected_objs = context.selected_objects
 230        source_objs = []
 231        prev_mode = None
 232        try:
 233            ob_source = context.active_object
 234            if ob_source not in selected_objs:
 235                selected_objs.append(ob_source) # luvoid : Fix error where object is active but not selected
 236            ob_name = ob_source.name
 237            ob_main = None
 238            if self.is_batch:
 239                # アクティブオブジェクトを1つコピーするだけでjoinしない
 240                source_objs.append(ob_source)
 241                compat.set_select(ob_source, False)
 242                ob_main = self.copy_and_activate_ob(context, ob_source)
 243
 244                if prefs.is_apply_modifiers and bpy.ops.object.forced_modifier_apply.poll(context):
 245                    bpy.ops.object.forced_modifier_apply(is_applies=[True for i in range(32)])
 246            else:
 247                selected_count = 0
 248                # 選択されたMESHオブジェクトをコピーしてjoin
 249                # 必要に応じて、モディファイアの強制適用を行う
 250                for selected in selected_objs:
 251                    source_objs.append(selected)
 252
 253                    compat.set_select(selected, False)
 254
 255                    if selected.type == 'MESH':
 256                        ob_created = self.copy_and_activate_ob(context, selected)
 257                        if selected == ob_source:
 258                            ob_main = ob_created
 259                        if prefs.is_apply_modifiers:
 260                            bpy.ops.object.forced_modifier_apply(apply_viewport_visible=True)
 261
 262                        selected_count += 1
 263
 264                mode = context.active_object.mode
 265                if mode != 'OBJECT':
 266                    prev_mode = mode
 267                    bpy.ops.object.mode_set(mode='OBJECT')
 268
 269                if selected_count > 1:
 270                    if ob_main:
 271                        compat.set_active(context, ob_main)
 272                    bpy.ops.object.join()
 273                    self.report(type={'INFO'}, message=f_tip_("{}個のオブジェクトをマージしました", selected_count))
 274
 275            ret = self.export(context, ob_main)
 276            if 'FINISHED' not in ret:
 277                return ret
 278
 279            context.window_manager.progress_update(10)
 280            diff_time = time.time() - start_time
 281            self.report(type={'INFO'}, message=f_tip_("modelのエクスポートが完了しました。{:.2f} 秒 file={}", diff_time, self.filepath))
 282            return ret
 283        finally:
 284            # 作業データの破棄(コピーデータを削除、選択状態の復元、アクティブオブジェクト、モードの復元)
 285            if ob_main:
 286                common.remove_data(ob_main)
 287                # me_copied = ob_main.data
 288                # context.blend_data.objects.remove(ob_main, do_unlink=True)
 289                # context.blend_data.meshes.remove(me_copied, do_unlink=True)
 290
 291            for obj in source_objs:
 292                compat.set_select(obj, True)
 293
 294            if ob_source:
 295                # TODO 元のオブジェクトをアクティブに戻す
 296                if ob_name in bpy.data.objects:
 297                    compat.set_active(context, ob_source)
 298
 299            if prev_mode:
 300                bpy.ops.object.mode_set(mode=prev_mode)
 301
 302    def export(self, context, ob):
 303        """モデルファイルを出力"""
 304        prefs = common.preferences()
 305
 306        if not self.is_batch:
 307            prefs.model_export_path = self.filepath
 308            prefs.scale = 1.0 / self.scale
 309
 310        context.window_manager.progress_begin(0, 10)
 311        context.window_manager.progress_update(0)
 312
 313        res = self.precheck(context)
 314        if res:
 315            return res
 316        me = ob.data
 317
 318        if ob.active_shape_key_index != 0:
 319            ob.active_shape_key_index = 0
 320            me.update()
 321
 322        # データの成否チェック
 323        if self.bone_info_mode == 'ARMATURE':
 324            arm_ob = ob.parent
 325            if arm_ob and arm_ob.type != 'ARMATURE':
 326                return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません")
 327            if not arm_ob:
 328                try:
 329                    arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object)
 330                except StopIteration:
 331                    return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい")
 332                arm_ob = arm_ob.object
 333        elif self.bone_info_mode == 'TEXT':
 334            if "BoneData" not in context.blend_data.texts:
 335                return self.report_cancel("テキスト「BoneData」が見つかりません、中止します")
 336            if "LocalBoneData" not in context.blend_data.texts:
 337                return self.report_cancel("テキスト「LocalBoneData」が見つかりません、中止します")
 338        elif self.bone_info_mode == 'OBJECT_PROPERTY':
 339            if "BoneData:0" not in ob:
 340                return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません")
 341            if "LocalBoneData:0" not in ob:
 342                return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません")
 343        elif self.bone_info_mode == 'ARMATURE_PROPERTY':
 344            arm_ob = ob.parent
 345            if arm_ob and arm_ob.type != 'ARMATURE':
 346                return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません")
 347            if not arm_ob:
 348                try:
 349                    arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object)
 350                except StopIteration:
 351                    return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい")
 352                arm_ob = arm_ob.object
 353            if "BoneData:0" not in arm_ob.data:
 354                return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません")
 355            if "LocalBoneData:0" not in arm_ob.data:
 356                return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません")
 357        else:
 358            return self.report_cancel("ボーン情報元のモードがおかしいです")
 359
 360        if self.mate_info_mode == 'TEXT':
 361            for index, slot in enumerate(ob.material_slots):
 362                if "Material:" + str(index) not in context.blend_data.texts:
 363                    return self.report_cancel("マテリアル情報元のテキストが足りません")
 364        context.window_manager.progress_update(1)
 365
 366        # model名とか
 367        ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.')
 368        if self.model_name == '*':
 369            self.model_name = ob_names[0]
 370        if self.base_bone_name == '*':
 371            self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto'
 372
 373        # BoneData情報読み込み
 374        base_bone_candidate = None
 375        bone_data = []
 376        if self.bone_info_mode == 'ARMATURE':
 377            bone_data = self.armature_bone_data_parser(context, arm_ob)
 378            base_bone_candidate = arm_ob.data['BaseBone']
 379        elif self.bone_info_mode == 'TEXT':
 380            bone_data_text = context.blend_data.texts["BoneData"]
 381            if 'BaseBone' in bone_data_text:
 382                base_bone_candidate = bone_data_text['BaseBone']
 383            bone_data = self.bone_data_parser(l.body for l in bone_data_text.lines)
 384        elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']:
 385            target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data
 386            if 'BaseBone' in target:
 387                base_bone_candidate = target['BaseBone']
 388            bone_data = self.bone_data_parser(self.indexed_data_generator(target, prefix="BoneData:"))
 389        if len(bone_data) <= 0:
 390            return self.report_cancel("テキスト「BoneData」に有効なデータがありません")
 391
 392        if self.base_bone_name not in (b['name'] for b in bone_data):
 393            if base_bone_candidate and self.base_bone_name == 'Auto':
 394                self.base_bone_name = base_bone_candidate
 395            else:
 396                return self.report_cancel("基点ボーンが存在しません")
 397        bone_name_indices = {bone['name']: index for index, bone in enumerate(bone_data)}
 398        context.window_manager.progress_update(2)
 399
 400        if self.is_align_to_base_bone:
 401            bpy.ops.object.align_to_cm3d2_base_bone(scale=1.0/self.scale, is_preserve_mesh=True, bone_info_mode=self.bone_info_mode)
 402            me.update()
 403
 404        if self.is_split_sharp:
 405            bpy.ops.object.mode_set(mode='EDIT')
 406            bpy.ops.mesh.split_sharp()
 407            bpy.ops.object.mode_set(mode='OBJECT')
 408
 409        # LocalBoneData情報読み込み
 410        local_bone_data = []
 411        if self.bone_info_mode == 'ARMATURE':
 412            local_bone_data = self.armature_local_bone_data_parser(arm_ob)
 413        elif self.bone_info_mode == 'TEXT':
 414            local_bone_data_text = context.blend_data.texts["LocalBoneData"]
 415            local_bone_data = self.local_bone_data_parser(l.body for l in local_bone_data_text.lines)
 416        elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']:
 417            target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data
 418            local_bone_data = self.local_bone_data_parser(self.indexed_data_generator(target, prefix="LocalBoneData:"))
 419        if len(local_bone_data) <= 0:
 420            return self.report_cancel("テキスト「LocalBoneData」に有効なデータがありません")
 421        local_bone_name_indices = {bone['name']: index for index, bone in enumerate(local_bone_data)}
 422        context.window_manager.progress_update(3)
 423        
 424        used_local_bone = {index: False for index, bone in enumerate(local_bone_data)}
 425        
 426        # ウェイト情報読み込み
 427        vertices = []
 428        is_over_one = 0
 429        is_under_one = 0
 430        is_in_too_many = 0
 431        for i, vert in enumerate(me.vertices):
 432            vgs = []
 433            for vg in vert.groups:
 434                if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group.
 435                    continue
 436                name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names)
 437                index = local_bone_name_indices.get(name, -1)
 438                if index >= 0 and (vg.weight > 0.0 or not self.is_clean_vertex_groups):
 439                    vgs.append([index, vg.weight])
 440                    # luvoid : track used bones
 441                    used_local_bone[index] = True
 442                    boneindex = bone_name_indices.get(name, -1)
 443                    while boneindex >= 0:
 444                        parent = bone_data[boneindex]
 445                        localindex = local_bone_name_indices.get(parent['name'], -1)
 446                        # could check for `localindex == -1` here, but it's prescence may be useful in determing if the local bones resolve back to some root
 447                        used_local_bone[localindex] = True
 448                        boneindex = parent['parent_index']
 449            if len(vgs) == 0:
 450                if not self.is_batch:
 451                    self.select_no_weight_vertices(context, local_bone_name_indices)
 452                return self.report_cancel("ウェイトが割り当てられていない頂点が見つかりました、中止します")
 453            if len(vgs) > 4:
 454                is_in_too_many += 1
 455            vgs = sorted(vgs, key=itemgetter(1), reverse=True)[0:4]
 456            total = sum(vg[1] for vg in vgs)
 457            if self.is_normalize_weight:
 458                for vg in vgs:
 459                    vg[1] /= total
 460            else:
 461                if 1.01 < total:
 462                    is_over_one += 1
 463                elif total < 0.99:
 464                    is_under_one += 1
 465            if len(vgs) < 4:
 466                vgs += [(0, 0.0)] * (4 - len(vgs))
 467            vertices.append({
 468                'index': vert.index,
 469                'face_indexs': list(map(itemgetter(0), vgs)),
 470                'weights': list(map(itemgetter(1), vgs)),
 471            })
 472        
 473        if 1 <= is_over_one:
 474            self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0を超えている頂点が見つかりました。正規化してください。超過している頂点の数:{}", is_over_one))
 475        if 1 <= is_under_one:
 476            self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0未満の頂点が見つかりました。正規化してください。不足している頂点の数:{}", is_under_one))
 477        
 478        # luvoid : warn that there are vertices in too many vertex groups
 479        if is_in_too_many > 0:
 480            self.report(type={'WARNING'}, message=f_tip_("4つを超える頂点グループにある頂点が見つかりました。頂点グループをクリーンアップしてください。不足している頂点の数:{}", is_in_too_many))
 481                
 482        # luvoid : check for unused local bones that the game will delete
 483        is_deleted = 0
 484        deleted_names = "The game will delete these local bones"
 485        for index, is_used in used_local_bone.items():
 486            print(index, is_used)
 487            if is_used == False:
 488                is_deleted += 1
 489                deleted_names = deleted_names + '\n' + local_bone_data[index]['name']
 490            elif is_used == True:
 491                pass
 492            else:
 493                print(f_tip_("Unexpected: used_local_bone[{key}] == {value} when len(used_local_bone) == {length}", key=index, value=is_used, length=len(used_local_bone)))
 494                self.report(type={'WARNING'}, message=f_tip_("Could not find whether bone with index {index} was used. See console for more info.", index=i))
 495        if is_deleted > 0:
 496            self.report(type={'WARNING'}, message=f_tip_("頂点が割り当てられていない{num}つのローカルボーンが見つかりました。 詳細については、ログを参照してください。", num=is_deleted))
 497            self.report(type={'INFO'}, message=deleted_names)
 498                
 499        context.window_manager.progress_update(4)
 500        
 501
 502        try:
 503            writer = common.open_temporary(self.filepath, 'wb', is_backup=self.is_backup)
 504        except:
 505            self.report(type={'ERROR'}, message=f_tip_("ファイルを開くのに失敗しました、アクセス不可かファイルが存在しません。file={}", self.filepath))
 506            return {'CANCELLED'}
 507
 508        model_datas = {
 509            'bone_data': bone_data,
 510            'local_bone_data': local_bone_data,
 511            'vertices': vertices,
 512        }
 513        try:
 514            with writer:
 515                self.write_model(context, ob, writer, **model_datas)
 516        except common.CM3D2ExportException as e:
 517            self.report(type={'ERROR'}, message=str(e))
 518            return {'CANCELLED'}
 519
 520        return {'FINISHED'}
 521
 522    def write_model(self, context, ob, writer, bone_data=[], local_bone_data=[], vertices=[]):
 523        """モデルデータをファイルオブジェクトに書き込む"""
 524        me = ob.data
 525        prefs = common.preferences()
 526
 527        # ファイル先頭
 528        common.write_str(writer, 'CM3D2_MESH')
 529        self.version_num = int(self.version)
 530        writer.write(struct.pack('<i', self.version_num))
 531
 532        common.write_str(writer, self.model_name)
 533        common.write_str(writer, self.base_bone_name)
 534
 535        # ボーン情報書き出し
 536        writer.write(struct.pack('<i', len(bone_data)))
 537        for bone in bone_data:
 538            common.write_str(writer, bone['name'])
 539            writer.write(struct.pack('<b', bone['scl']))
 540        context.window_manager.progress_update(3.3)
 541        for bone in bone_data:
 542            writer.write(struct.pack('<i', bone['parent_index']))
 543        context.window_manager.progress_update(3.7)
 544        for bone in bone_data:
 545            writer.write(struct.pack('<3f', bone['co'][0], bone['co'][1], bone['co'][2]))
 546            writer.write(struct.pack('<4f', bone['rot'][1], bone['rot'][2], bone['rot'][3], bone['rot'][0]))
 547            if self.version_num >= 2001:
 548                use_scale = ('scale' in bone)
 549                writer.write(struct.pack('<b', use_scale))
 550                if use_scale:
 551                    bone_scale = bone['scale']
 552                    writer.write(struct.pack('<3f', bone_scale[0], bone_scale[1], bone_scale[2]))
 553        context.window_manager.progress_update(4)
 554
 555        # 正しい頂点数などを取得
 556        bm = bmesh.new()
 557        bm.from_mesh(me)
 558        uv_lay = bm.loops.layers.uv.active
 559        vert_uvs = []
 560        vert_uvs_append = vert_uvs.append
 561        vert_iuv = {}
 562        vert_indices = {}
 563        vert_count = 0
 564        for vert in bm.verts:
 565            vert_uv = []
 566            vert_uvs_append(vert_uv)
 567            for loop in vert.link_loops:
 568                uv = loop[uv_lay].uv
 569                if uv not in vert_uv:
 570                    vert_uv.append(uv)
 571                    vert_iuv[hash((vert.index, uv.x, uv.y))] = vert_count
 572                    vert_indices[vert.index] = vert_count
 573                    vert_count += 1
 574        if 65535 < vert_count:
 575            raise common.CM3D2ExportException(f_tip_("頂点数がまだ多いです (現在{}頂点)。あと{}頂点以上減らしてください、中止します", vert_count, vert_count - 65535))
 576        context.window_manager.progress_update(5)
 577
 578        writer.write(struct.pack('<2i', vert_count, len(ob.material_slots)))
 579
 580        # ローカルボーン情報を書き出し
 581        writer.write(struct.pack('<i', len(local_bone_data)))
 582        for bone in local_bone_data:
 583            common.write_str(writer, bone['name'])
 584        context.window_manager.progress_update(5.3)
 585        for bone in local_bone_data:
 586            for f in bone['matrix']:
 587                writer.write(struct.pack('<f', f))
 588        context.window_manager.progress_update(5.7)
 589
 590        # カスタム法線情報を取得
 591        if me.has_custom_normals:
 592            custom_normals = [mathutils.Vector() for i in range(len(me.vertices))]
 593            me.calc_normals_split()
 594            for loop in me.loops:
 595                custom_normals[loop.vertex_index] += loop.normal
 596            for no in custom_normals:
 597                no.normalize()
 598        else:
 599            custom_normals = None
 600
 601        cm_verts = []
 602        cm_norms = []
 603        cm_uvs = []
 604        # 頂点情報を書き出し
 605        for i, vert in enumerate(bm.verts):
 606            co = compat.convert_bl_to_cm_space( vert.co * self.scale )
 607            if me.has_custom_normals:
 608                no = custom_normals[vert.index]
 609            else:
 610                no = vert.normal.copy()
 611            no = compat.convert_bl_to_cm_space( no )
 612            for uv in vert_uvs[i]:
 613                cm_verts.append(co)
 614                cm_norms.append(no)
 615                cm_uvs.append(uv)
 616                writer.write(struct.pack('<3f', co.x, co.y, co.z))
 617                writer.write(struct.pack('<3f', no.x, no.y, no.z))
 618                writer.write(struct.pack('<2f', uv.x, uv.y))
 619        context.window_manager.progress_update(6)
 620
 621        cm_tris = self.parse_triangles(bm, ob, uv_lay, vert_iuv, vert_indices)
 622
 623        # 接空間情報を書き出し
 624        if self.export_tangent:
 625            tangents = self.calc_tangents(cm_tris, cm_verts, cm_norms, cm_uvs)
 626            writer.write(struct.pack('<i', len(tangents)))
 627            for t in tangents:
 628                writer.write(struct.pack('<4f', *t))
 629        else:
 630            writer.write(struct.pack('<i', 0))
 631
 632        # ウェイト情報を書き出し
 633        for vert in vertices:
 634            for uv in vert_uvs[vert['index']]:
 635                writer.write(struct.pack('<4H', *vert['face_indexs']))
 636                writer.write(struct.pack('<4f', *vert['weights']))
 637        context.window_manager.progress_update(7)
 638
 639        # 面情報を書き出し
 640        for tri in cm_tris:
 641            writer.write(struct.pack('<i', len(tri)))
 642            for vert_index in tri:
 643                writer.write(struct.pack('<H', vert_index))
 644        context.window_manager.progress_update(8)
 645
 646        # マテリアルを書き出し
 647        writer.write(struct.pack('<i', len(ob.material_slots)))
 648        for slot_index, slot in enumerate(ob.material_slots):
 649            if self.mate_info_mode == 'MATERIAL':
 650                mat_data = cm3d2_data.MaterialHandler.parse_mate(slot.material, self.is_arrange_name)
 651                mat_data.write(writer, write_header=False)
 652
 653            elif self.mate_info_mode == 'TEXT':
 654                text = context.blend_data.texts["Material:" + str(slot_index)].as_string()
 655                mat_data = cm3d2_data.MaterialHandler.parse_text(slot.material, self.is_arrange_name)
 656                mat_data.write(writer, write_header=False)
 657
 658        context.window_manager.progress_update(9)
 659
 660        # モーフを書き出し
 661        if me.shape_keys and len(me.shape_keys.key_blocks) >= 2:
 662            try:
 663                self.write_shapekeys(context, ob, writer, vert_uvs, custom_normals)
 664            finally:
 665                print("FINISHED SHAPE KEYS WRITE")
 666                pass
 667        common.write_str(writer, 'end')
 668
 669    def write_shapekeys(self, context, ob, writer, vert_uvs, custom_normals=None):
 670        # モーフを書き出し
 671        me = ob.data
 672        prefs = common.preferences()
 673        
 674        is_use_attributes = (not compat.IS_LEGACY and bpy.app.version >= (2,92))
 675
 676        loops_vert_index = np.empty((len(me.loops)), dtype=int)
 677        me.loops.foreach_get('vertex_index', loops_vert_index.ravel())
 678
 679        def find_normals_attribute(name) -> (bpy.types.Attribute, bool):
 680            if is_use_attributes:
 681                normals_color = me.attributes[name] if name in me.attributes.keys() else None
 682                attribute_is_color = (not normals_color is None) and normals_color.data_type in {'BYTE_COLOR', 'FLOAT_COLOR'}
 683            else:
 684                normals_color = me.vertex_colors[name] if name in me.vertex_colors.keys() else None
 685                attribute_is_color = True
 686            return normals_color, attribute_is_color
 687
 688        if self.use_shapekey_colors:
 689            static_attribute_colors = np.empty((len(me.loops), 4), dtype=float)
 690            color_offset = np.array([[0.5,0.5,0.5]])
 691            loops_per_vertex = np.zeros((len(me.vertices)))
 692            for loop in me.loops:
 693                loops_per_vertex[loop.vertex_index] += 1
 694            loops_per_vertex_reciprocal = np.reciprocal(loops_per_vertex, out=loops_per_vertex).reshape((len(me.vertices), 1))
 695        def get_sk_delta_normals_from_attribute(attribute, is_color, out):
 696            if is_color:
 697                attribute.data.foreach_get('color', static_attribute_colors.ravel())
 698                loop_delta_normals = static_attribute_colors[:,:3]
 699                loop_delta_normals -= color_offset
 700                loop_delta_normals *= 2
 701            else:
 702                loop_delta_normals = static_attribute_colors[:,:3]
 703                attribute.data.foreach_get('vector', loop_delta_normals.ravel())
 704            
 705            vert_delta_normals = out
 706            vert_delta_normals.fill(0)
 707
 708            # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index]
 709            np.add.at(vert_delta_normals, loops_vert_index, loop_delta_normals) # XXX Slower but handles edge cases better
 710            #vert_delta_normals[loops_vert_index] += loop_delta_normals # XXX Only first loop's value will be kept
 711            
 712            # for delta_normal in vert_delta_normals: delta_normal /= loops_per_vertex[vert.index]
 713            vert_delta_normals *= loops_per_vertex_reciprocal
 714
 715            return out #.tolist()
 716
 717        if me.has_custom_normals:
 718            basis_custom_normals = np.array(custom_normals, dtype=float)
 719            static_loop_normals = np.empty((len(me.loops), 3), dtype=float)
 720            static_vert_lengths = np.empty((len(me.vertices), 1), dtype=float)
 721        def get_sk_delta_normals_from_custom_normals(shape_key, out):
 722            vert_custom_normals = out
 723            vert_custom_normals.fill(0)
 724            
 725            loop_custom_normals = static_loop_normals
 726            np.copyto(loop_custom_normals.ravel(), shape_key.normals_split_get())
 727            
 728            # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index]
 729            if not self.is_split_sharp:  
 730                # XXX Slower
 731                np.add.at(vert_custom_normals, loops_vert_index, loop_custom_normals)
 732                vert_len_sq = get_lengths_squared(vert_custom_normals, out=static_vert_lengths)
 733                vert_len = np.sqrt(vert_len_sq, out=vert_len_sq)
 734                np.reciprocal(vert_len, out=vert_len)
 735                vert_custom_normals *= vert_len #.reshape((*vert_len.shape, 1))
 736            else:
 737                # loop normals should be the same per-vertex unless there is a sharp edge 
 738                # or a flat shaded face, but all sharp edges were split, so this method is fine
 739                # (and Flat shaded faces just won't be supported)
 740                vert_custom_normals[loops_vert_index] += loop_custom_normals # Only first loop's value will be kept
 741
 742            vert_custom_normals -= basis_custom_normals
 743            return out
 744        
 745        if not me.has_custom_normals:
 746            basis_normals = np.empty((len(me.vertices), 3), dtype=float)
 747            me.vertices.foreach_get('normal', basis_normals.ravel())
 748        def get_sk_delta_normals_from_normals(shape_key, out):
 749            vert_normals = out
 750            np.copyto(vert_normals.ravel(), shape_key.normals_vertex_get())
 751            vert_delta_normals = np.subtract(vert_normals, basis_normals, out=out)
 752            return out
 753
 754        basis_co = np.empty((len(me.vertices), 3), dtype=float)
 755        me.vertices.foreach_get('co', basis_co.ravel())
 756        def get_sk_delta_coordinates(shape_key, out):
 757            delta_coordinates = out
 758            shape_key.data.foreach_get('co', delta_coordinates.ravel())
 759            delta_coordinates -= basis_co
 760            return out
 761
 762        static_array_sq = np.empty((len(me.vertices), 3), dtype=float)
 763        def get_lengths_squared(vectors, out):
 764            np.power(vectors, 2, out=static_array_sq)
 765            np.sum(static_array_sq, axis=1, out=out.ravel())
 766            return out
 767
 768        def write_morph(morph, name):
 769            common.write_str(writer, 'morph')
 770            common.write_str(writer, name)
 771            writer.write(struct.pack('<i', len(morph)))
 772            for v_index, vec, normal in morph:
 773                vec    = compat.convert_bl_to_cm_space(vec   )
 774                normal = compat.convert_bl_to_cm_space(normal)
 775                writer.write(struct.pack('<H', v_index))
 776                writer.write(struct.pack('<3f', *vec[:3]))
 777                writer.write(struct.pack('<3f', *normal[:3]))
 778        
 779        # accessing operator properties via "self.x" is SLOW, so store some here
 780        self__export_shapekey_normals = self.export_shapekey_normals
 781        self__use_shapekey_colors = self.use_shapekey_colors
 782        self__shapekey_normals_blend = self.shapekey_normals_blend
 783        self__scale = self.scale
 784        
 785        co_diff_threshold = self.shapekey_threshold / 5
 786        co_diff_threshold_squared = co_diff_threshold * co_diff_threshold
 787        no_diff_threshold = self.shapekey_threshold * 10
 788        no_diff_threshold_squared = no_diff_threshold * no_diff_threshold
 789        
 790        # shared arrays
 791        delta_coordinates  = np.empty((len(me.vertices), 3), dtype=float)
 792        vert_delta_normals = np.empty((len(me.vertices), 3), dtype=float)
 793        loop_delta_normals = np.empty((len(me.loops   ), 3), dtype=float)
 794
 795        delta_co_lensq = np.empty((len(me.vertices)), dtype=float)
 796        delta_no_lensq = np.empty((len(me.vertices)), dtype=float)
 797
 798        if not self.export_shapekey_normals:
 799            vert_delta_normals.fill(0)
 800            delta_no_lensq.fill(0)
 801
 802        # HEAVY LOOP
 803        for shape_key in me.shape_keys.key_blocks[1:]:
 804            morph = []
 805
 806            if self__export_shapekey_normals and self__use_shapekey_colors:
 807                normals_color, attrubute_is_color = find_normals_attribute(f'{shape_key.name}_delta_normals')
 808
 809            if self__export_shapekey_normals:
 810                if self__use_shapekey_colors and not normals_color is None:
 811                    sk_delta_normals = get_sk_delta_normals_from_attribute(normals_color, attrubute_is_color, out=vert_delta_normals)
 812                elif me.has_custom_normals:
 813                    sk_delta_normals = get_sk_delta_normals_from_custom_normals(shape_key, out=vert_delta_normals)
 814                    sk_delta_normals *= self__shapekey_normals_blend
 815                else:
 816                    sk_delta_normals = get_sk_delta_normals_from_normals(shape_key, out=vert_delta_normals)
 817                    sk_delta_normals *= self__shapekey_normals_blend
 818                
 819                sk_no_lensq = get_lengths_squared(sk_delta_normals, out=delta_no_lensq)
 820            else:
 821                sk_delta_normals = vert_delta_normals
 822                sk_no_lensq = delta_no_lensq
 823
 824            sk_co_diffs = get_sk_delta_coordinates(shape_key, out=delta_coordinates)
 825            sk_co_diffs *= self__scale # scale before getting lengths
 826            sk_co_lensq = get_lengths_squared(sk_co_diffs, out=delta_co_lensq)
 827
 828            # SUPER HEAVY LOOP
 829            outvert_index = 0
 830            for i in range(len(me.vertices)):
 831                if sk_co_lensq[i] >= co_diff_threshold_squared or sk_no_lensq[i] >= no_diff_threshold_squared:
 832                    morph += [ (outvert_index+j, sk_co_diffs[i], sk_delta_normals[i]) for j in range(len(vert_uvs[i])) ]
 833                else:
 834                    # ignore because change is too small (greatly lowers file size)
 835                    pass
 836                outvert_index += len(vert_uvs[i])
 837
 838            if prefs.skip_shapekey and not len(morph):
 839                continue
 840            else:
 841                write_morph(morph, shape_key.name)
 842
 843    def write_tangents(self, writer, me):
 844        if len(me.uv_layers) < 1:
 845            return
 846
 847        num_loops = len(me.loops)
 848
 849    def parse_triangles(self, bm, ob, uv_lay, vert_iuv, vert_indices):
 850        def vert_index_from_loops(loops):
 851            """vert_index generator"""
 852            for loop in loops:
 853                uv = loop[uv_lay].uv
 854                v_index = loop.vert.index
 855                vert_index = vert_iuv.get(hash((v_index, uv.x, uv.y)))
 856                if vert_index is None:
 857                    vert_index = vert_indices.get(v_index, 0)
 858                yield vert_index
 859
 860        triangles = []
 861        for mate_index, slot in enumerate(ob.material_slots):
 862            tris_faces = []
 863            for face in bm.faces:
 864                if face.material_index != mate_index:
 865                    continue
 866                if len(face.verts) == 3:
 867                    tris_faces.extend(vert_index_from_loops(reversed(face.loops)))
 868                elif len(face.verts) == 4 and self.is_convert_tris:
 869                    v1 = face.loops[0].vert.co - face.loops[2].vert.co
 870                    v2 = face.loops[1].vert.co - face.loops[3].vert.co
 871                    if v1.length < v2.length:
 872                        f1 = [0, 1, 2]
 873                        f2 = [0, 2, 3]
 874                    else:
 875                        f1 = [0, 1, 3]
 876                        f2 = [1, 2, 3]
 877                    faces, faces2 = [], []
 878                    for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))):
 879                        if i in f1:
 880                            faces.append(vert_index)
 881                        if i in f2:
 882                            faces2.append(vert_index)
 883                    tris_faces.extend(faces)
 884                    tris_faces.extend(faces2)
 885                elif 5 <= len(face.verts) and self.is_convert_tris:
 886                    face_count = len(face.verts) - 2
 887
 888                    tris = []
 889                    seek_min, seek_max = 0, len(face.verts) - 1
 890                    for i in range(face_count):
 891                        if not i % 2:
 892                            tris.append([seek_min, seek_min + 1, seek_max])
 893                            seek_min += 1
 894                        else:
 895                            tris.append([seek_min, seek_max - 1, seek_max])
 896                            seek_max -= 1
 897
 898                    tris_indexs = [[] for _ in range(len(tris))]
 899                    for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))):
 900                        for tris_index, points in enumerate(tris):
 901                            if i in points:
 902                                tris_indexs[tris_index].append(vert_index)
 903
 904                    tris_faces.extend(p for ps in tris_indexs for p in ps)
 905
 906            triangles.append(tris_faces)
 907        return triangles
 908
 909    def calc_tangents(self, cm_tris, cm_verts, cm_norms, cm_uvs):
 910        count = len(cm_verts)
 911        tan1 = [None] * count
 912        tan2 = [None] * count
 913        for i in range(0, count):
 914            tan1[i] = mathutils.Vector((0, 0, 0))
 915            tan2[i] = mathutils.Vector((0, 0, 0))
 916
 917        for tris in cm_tris:
 918            tri_len = len(tris)
 919            tri_idx = 0
 920            while tri_idx < tri_len:
 921                i1, i2, i3 = tris[tri_idx], tris[tri_idx + 1], tris[tri_idx + 2]
 922                v1, v2, v3 = cm_verts[i1], cm_verts[i2], cm_verts[i3]
 923                w1, w2, w3 = cm_uvs[i1], cm_uvs[i2], cm_uvs[i3]
 924
 925                a1 = v2 - v1
 926                a2 = v3 - v1
 927                s1 = w2 - w1
 928                s2 = w3 - w1
 929                
 930                r_inverse = (s1.x * s2.y - s2.x * s1.y)
 931
 932                if r_inverse != 0:
 933                    # print("i1 = {i1}   i2 = {i2}   i3 = {i3}".format(i1=i1, i2=i2, i3=i3))
 934                    # print("v1 = {v1}   v2 = {v2}   v3 = {v3}".format(v1=v1, v2=v2, v3=v3))
 935                    # print("w1 = {w1}   w2 = {w2}   w3 = {w3}".format(w1=w1, w2=w2, w3=w3))
 936
 937                    # print("a1 = {a1}   a2 = {a2}".format(a1=a1, a2=a2))
 938                    # print("s1 = {s1}   s2 = {s2}".format(s1=s1, s2=s2))
 939                    
 940                    # print("r_inverse = ({s1x} * {s2y} - {s2x} * {s1y}) = {r_inverse}".format(r_inverse=r_inverse, s1x=s1.x, s1y=s1.y, s2x=s2.x, s2y=s2.y))
 941                                                
 942                    r = 1.0 / r_inverse
 943                    sdir = mathutils.Vector(((s2.y * a1.x - s1.y * a2.x) * r, (s2.y * a1.y - s1.y * a2.y) * r, (s2.y * a1.z - s1.y * a2.z) * r))
 944                    tan1[i1] += sdir
 945                    tan1[i2] += sdir
 946                    tan1[i3] += sdir
 947
 948                    tdir = mathutils.Vector(((s1.x * a2.x - s2.x * a1.x) * r, (s1.x * a2.y - s2.x * a1.y) * r, (s1.x * a2.z - s2.x * a1.z) * r))
 949                    tan2[i1] += tdir
 950                    tan2[i2] += tdir
 951                    tan2[i3] += tdir
 952
 953                tri_idx += 3
 954
 955        tangents = [None] * count
 956        for i in range(0, count):
 957            n = cm_norms[i]
 958            ti = tan1[i]
 959            t = (ti - n * n.dot(ti)).normalized()
 960
 961            c = n.cross(ti)
 962            val = c.dot(tan2[i])
 963            w = 1.0 if val < 0 else -1.0
 964            tangents[i] = (-t.x, t.y, t.z, w)
 965
 966        return tangents
 967
 968    def select_no_weight_vertices(self, context, local_bone_name_indices):
 969        """ウェイトが割り当てられていない頂点を選択する"""
 970        ob = context.active_object
 971        me = ob.data
 972        bpy.ops.object.mode_set(mode='EDIT')
 973        bpy.ops.mesh.select_all(action='SELECT')
 974        #bpy.ops.object.mode_set(mode='OBJECT')
 975        context.tool_settings.mesh_select_mode = (True, False, False)
 976        for vert in me.vertices:
 977            for vg in vert.groups:
 978                if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group.
 979                    continue
 980                name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names)
 981                if name in local_bone_name_indices and 0.0 < vg.weight:
 982                    vert.select = False
 983                    break
 984        bpy.ops.object.mode_set(mode='EDIT')
 985
 986    def armature_bone_data_parser(self, context, ob):
 987        """アーマチュアを解析してBoneDataを返す"""
 988        arm = ob.data
 989        
 990        pre_active = compat.get_active(context)
 991        pre_mode = ob.mode
 992
 993        compat.set_active(context, ob)
 994        bpy.ops.object.mode_set(mode='EDIT')
 995
 996        bones = []
 997        bone_name_indices = {}
 998        already_bone_names = []
 999        bones_queue = arm.edit_bones[:]
1000        while len(bones_queue):
1001            bone = bones_queue.pop(0)
1002
1003            if not bone.parent:
1004                already_bone_names.append(bone.name)
1005                bones.append(bone)
1006                bone_name_indices[bone.name] = len(bone_name_indices)
1007                continue
1008            elif bone.parent.name in already_bone_names:
1009                already_bone_names.append(bone.name)
1010                bones.append(bone)
1011                bone_name_indices[bone.name] = len(bone_name_indices)
1012                continue
1013
1014            bones_queue.append(bone)
1015
1016        bone_data = []
1017        for bone in bones:
1018
1019            # Also check for UnknownFlag for backwards compatibility
1020            is_scl_bone = bone['cm3d2_scl_bone'] if 'cm3d2_scl_bone' in bone \
1021                     else bone['UnknownFlag']    if 'UnknownFlag'    in bone \
1022                     else 0 
1023            parent_index = bone_name_indices[bone.parent.name] if bone.parent else -1
1024
1025            mat = bone.matrix.copy()
1026            
1027            if bone.parent:
1028                mat = compat.convert_bl_to_cm_bone_rotation(mat)
1029                mat = compat.mul(bone.parent.matrix.inverted(), mat)
1030                mat = compat.convert_bl_to_cm_bone_space(mat)
1031            else:
1032                mat = compat.convert_bl_to_cm_bone_rotation(mat)
1033                mat = compat.convert_bl_to_cm_space(mat)
1034            
1035            co = mat.to_translation() * self.scale
1036            rot = mat.to_quaternion()
1037            
1038            #if bone.parent:
1039            #    co.x, co.y, co.z = -co.y, -co.x, co.z
1040            #    rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, rot.x, -rot.z
1041            #else:
1042            #    co.x, co.y, co.z = -co.x, co.z, -co.y
1043            #
1044            #    fix_quat  = compat.Z_UP_TO_Y_UP_QUAT    #mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
1045            #    fix_quat2 = compat.BLEND_TO_OPENGL_QUAT #mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion()
1046            #    rot = compat.mul3(rot, fix_quat, fix_quat2)
1047            #    #rot = compat.mul3(fix_quat2, rot, fix_quat)
1048            #
1049            #    rot.w, rot.x, rot.y, rot.z = -rot.y, -rot.z, -rot.x, rot.w
1050            
1051            # luvoid : I copied this from the Bone-Util Addon by trzr
1052            #if bone.parent:
1053            #    co.x, co.y, co.z = -co.y, co.z, co.x
1054            #    rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, -rot.z, -rot.x
1055            #else:
1056            #    co.x, co.y, co.z = -co.x, co.z, -co.y
1057            #    
1058            #    rot = compat.mul(rot, mathutils.Quaternion((0, 0, 1), math.radians(90)))
1059            #    rot.w, rot.x, rot.y, rot.z = -rot.w, -rot.x, rot.z, -rot.y
1060            
1061            #opengl_mat = compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.matrix)
1062            #
1063            #if bone.parent:
1064            #    opengl_mat = compat.mul(compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.parent.matrix).inverted(), opengl_mat)
1065            #
1066            #co = opengl_mat.to_translation() * self.scale
1067            #rot = opengl_mat.to_quaternion()
1068
1069            data = {
1070                'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names),
1071                'scl': is_scl_bone,
1072                'parent_index': parent_index,
1073                'co': co.copy(),
1074                'rot': rot.copy(),
1075            }
1076            scale = arm.edit_bones[bone.name].get('cm3d2_bone_scale')
1077            if scale:
1078                data['scale'] = scale
1079            bone_data.append(data)
1080        
1081        bpy.ops.object.mode_set(mode=pre_mode)
1082        compat.set_active(context, pre_active)
1083        return bone_data
1084
1085    @staticmethod
1086    def bone_data_parser(container):
1087        """BoneData テキストをパースして辞書を要素とするリストを返す"""
1088        bone_data = []
1089        bone_name_indices = {}
1090        for line in container:
1091            data = line.split(',')
1092            if len(data) < 5:
1093                continue
1094
1095            parent_name = data[2]
1096            if parent_name.isdigit():
1097                parent_index = int(parent_name)
1098            else:
1099                parent_index = bone_name_indices.get(parent_name, -1)
1100
1101            bone_datum = {
1102                'name': data[0],
1103                'scl': int(data[1]),
1104                'parent_index': parent_index,
1105                'co': list(map(float, data[3].split())),
1106                'rot': list(map(float, data[4].split())),
1107            }
1108            # scale info (for version 2001 or later)
1109            if len(data) >= 7:
1110                if data[5] == '1':
1111                    bone_scale = data[6]
1112                    bone_datum['scale'] = list(map(float, bone_scale.split()))
1113            bone_data.append(bone_datum)
1114            bone_name_indices[data[0]] = len(bone_name_indices)
1115        return bone_data
1116
1117    def armature_local_bone_data_parser(self, ob):
1118        """アーマチュアを解析してBoneDataを返す"""
1119        arm = ob.data
1120
1121        # XXX Instead of just adding all bones, only bones / bones-with-decendants 
1122        #     that have use_deform == True or mathcing vertex groups should be used
1123        bones = []
1124        bone_name_indices = {}
1125        already_bone_names = []
1126        bones_queue = arm.bones[:]
1127        while len(bones_queue):
1128            bone = bones_queue.pop(0)
1129
1130            if not bone.parent:
1131                already_bone_names.append(bone.name)
1132                bones.append(bone)
1133                bone_name_indices[bone.name] = len(bone_name_indices)
1134                continue
1135            elif bone.parent.name in already_bone_names:
1136                already_bone_names.append(bone.name)
1137                bones.append(bone)
1138                bone_name_indices[bone.name] = len(bone_name_indices)
1139                continue
1140
1141            bones_queue.append(bone)
1142
1143        local_bone_data = []
1144        for bone in bones:
1145            mat = bone.matrix_local.copy()
1146            mat = compat.mul(mathutils.Matrix.Scale(-1, 4, (1, 0, 0)), mat)
1147            mat = compat.convert_bl_to_cm_bone_rotation(mat)
1148            pos = mat.translation.copy()
1149            
1150            mat.transpose()
1151            mat.row[3] = (0.0, 0.0, 0.0, 1.0)
1152            pos = compat.mul(mat.to_3x3(), pos)
1153            pos *= -self.scale
1154            mat.translation = pos
1155            mat.transpose()
1156            
1157            #co = mat.to_translation() * self.scale
1158            #rot = mat.to_quaternion()
1159            #
1160            #co.rotate(rot.inverted())
1161            #co.x, co.y, co.z = co.y, co.x, -co.z
1162            #
1163            #fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
1164            #rot = compat.mul(rot, fix_quat)
1165            #rot.w, rot.x, rot.y, rot.z = -rot.z, -rot.y, -rot.x, rot.w
1166            #
1167            #co_mat = mathutils.Matrix.Translation(co)
1168            #rot_mat = rot.to_matrix().to_4x4()
1169            #mat = compat.mul(co_mat, rot_mat)
1170            #
1171            #copy_mat = mat.copy()
1172            #mat[0][0], mat[0][1], mat[0][2], mat[0][3] = copy_mat[0][0], copy_mat[1][0], copy_mat[2][0], copy_mat[3][0]
1173            #mat[1][0], mat[1][1], mat[1][2], mat[1][3] = copy_mat[0][1], copy_mat[1][1], copy_mat[2][1], copy_mat[3][1]
1174            #mat[2][0], mat[2][1], mat[2][2], mat[2][3] = copy_mat[0][2], copy_mat[1][2], copy_mat[2][2], copy_mat[3][2]
1175            #mat[3][0], mat[3][1], mat[3][2], mat[3][3] = copy_mat[0][3], copy_mat[1][3], copy_mat[2][3], copy_mat[3][3]
1176
1177            mat_array = []
1178            for vec in mat:
1179                mat_array.extend(vec[:])
1180            
1181            local_bone_data.append({
1182                'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names),
1183                'matrix': mat_array,
1184            })
1185        return local_bone_data
1186
1187    @staticmethod
1188    def local_bone_data_parser(container):
1189        """LocalBoneData テキストをパースして辞書を要素とするリストを返す"""
1190        local_bone_data = []
1191        for line in container:
1192            data = line.split(',')
1193            if len(data) != 2:
1194                continue
1195            local_bone_data.append({
1196                'name': data[0],
1197                'matrix': list(map(float, data[1].split())),
1198            })
1199        return local_bone_data
1200
1201    @staticmethod
1202    def indexed_data_generator(container, prefix='', max_index=9**9, max_pass=50):
1203        """コンテナ内の数値インデックスをキーに持つ要素を昇順に返すジェネレーター"""
1204        pass_count = 0
1205        for i in range(max_index):
1206            name = prefix + str(i)
1207            if name not in container:
1208                pass_count += 1
1209                if max_pass < pass_count:
1210                    return
1211                continue
1212            yield container[name]
bl_idname = 'export_mesh.export_cm3d2_model'
bl_label = 'CM3D2モデル (.model)'
bl_description = 'カスタムメイド3D2のmodelファイルを書き出します'
bl_options = {'REGISTER'}
filepath: <_PropertyDeferred, <built-in function StringProperty>, {'subtype': 'FILE_PATH', 'attr': 'filepath'}> = <_PropertyDeferred, <built-in function StringProperty>, {'subtype': 'FILE_PATH', 'attr': 'filepath'}>
filename_ext = '.model'
filter_glob: <_PropertyDeferred, <built-in function StringProperty>, {'default': '*.model', 'options': {'HIDDEN'}, 'attr': 'filter_glob'}> = <_PropertyDeferred, <built-in function StringProperty>, {'default': '*.model', 'options': {'HIDDEN'}, 'attr': 'filter_glob'}>
scale: <_PropertyDeferred, <built-in function FloatProperty>, {'name': '倍率', 'default': 0.2, 'min': 0.01, 'max': 100, 'soft_min': 0.01, 'soft_max': 100, 'step': 10, 'precision': 2, 'description': 'エクスポート時のメッシュ等の拡大率です', 'attr': 'scale'}> = <_PropertyDeferred, <built-in function FloatProperty>, {'name': '倍率', 'default': 0.2, 'min': 0.01, 'max': 100, 'soft_min': 0.01, 'soft_max': 100, 'step': 10, 'precision': 2, 'description': 'エクスポート時のメッシュ等の拡大率です', 'attr': 'scale'}>
is_backup: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ファイルをバックアップ', 'default': True, 'description': 'ファイルに上書きする場合にバックアップファイルを複製します', 'attr': 'is_backup'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ファイルをバックアップ', 'default': True, 'description': 'ファイルに上書きする場合にバックアップファイルを複製します', 'attr': 'is_backup'}>
version: <_PropertyDeferred, <built-in function EnumProperty>, {'name': 'ファイルバージョン', 'items': [('2001', '2001', 'model version 2001 (available only for com3d2)', 'NONE', 0), ('2000', '2000', 'model version 2000 (com3d2 version)', 'NONE', 1), ('1000', '1000', 'model version 1000 (available for cm3d2/com3d2)', 'NONE', 2)], 'default': '1000', 'attr': 'version'}> = <_PropertyDeferred, <built-in function EnumProperty>, {'name': 'ファイルバージョン', 'items': [('2001', '2001', 'model version 2001 (available only for com3d2)', 'NONE', 0), ('2000', '2000', 'model version 2000 (com3d2 version)', 'NONE', 1), ('1000', '1000', 'model version 1000 (available for cm3d2/com3d2)', 'NONE', 2)], 'default': '1000', 'attr': 'version'}>
model_name: <_PropertyDeferred, <built-in function StringProperty>, {'name': 'model名', 'default': '*', 'attr': 'model_name'}> = <_PropertyDeferred, <built-in function StringProperty>, {'name': 'model名', 'default': '*', 'attr': 'model_name'}>
base_bone_name: <_PropertyDeferred, <built-in function StringProperty>, {'name': '基点ボーン名', 'default': '*', 'attr': 'base_bone_name'}> = <_PropertyDeferred, <built-in function StringProperty>, {'name': '基点ボーン名', 'default': '*', 'attr': 'base_bone_name'}>
items = [('TEXT', 'テキスト', '', 'FILE_TEXT', 1), ('MATERIAL', 'マテリアル', '', 'MATERIAL', 2)]
bone_info_mode: <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('ARMATURE', 'アーマチュア', '', 'OUTLINER_OB_ARMATURE', 1), ('TEXT', 'テキスト', '', 'FILE_TEXT', 2), ('OBJECT_PROPERTY', 'オブジェクト内プロパティ', '', 'OBJECT_DATAMODE', 3), ('ARMATURE_PROPERTY', 'アーマチュア内プロパティ', '', 'ARMATURE_DATA', 4)], 'name': 'ボーン情報元', 'default': 'OBJECT_PROPERTY', 'description': 'modelファイルに必要なボーン情報をどこから引っ張ってくるか選びます', 'attr': 'bone_info_mode'}> = <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('ARMATURE', 'アーマチュア', '', 'OUTLINER_OB_ARMATURE', 1), ('TEXT', 'テキスト', '', 'FILE_TEXT', 2), ('OBJECT_PROPERTY', 'オブジェクト内プロパティ', '', 'OBJECT_DATAMODE', 3), ('ARMATURE_PROPERTY', 'アーマチュア内プロパティ', '', 'ARMATURE_DATA', 4)], 'name': 'ボーン情報元', 'default': 'OBJECT_PROPERTY', 'description': 'modelファイルに必要なボーン情報をどこから引っ張ってくるか選びます', 'attr': 'bone_info_mode'}>
mate_info_mode: <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('TEXT', 'テキスト', '', 'FILE_TEXT', 1), ('MATERIAL', 'マテリアル', '', 'MATERIAL', 2)], 'name': 'マテリアル情報元', 'default': 'MATERIAL', 'description': 'modelファイルに必要なマテリアル情報をどこから引っ張ってくるか選びます', 'attr': 'mate_info_mode'}> = <_PropertyDeferred, <built-in function EnumProperty>, {'items': [('TEXT', 'テキスト', '', 'FILE_TEXT', 1), ('MATERIAL', 'マテリアル', '', 'MATERIAL', 2)], 'name': 'マテリアル情報元', 'default': 'MATERIAL', 'description': 'modelファイルに必要なマテリアル情報をどこから引っ張ってくるか選びます', 'attr': 'mate_info_mode'}>
is_arrange_name: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'データ名の連番を削除', 'default': True, 'description': '「○○.001」のような連番が付属したデータ名からこれらを削除します', 'attr': 'is_arrange_name'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'データ名の連番を削除', 'default': True, 'description': '「○○.001」のような連番が付属したデータ名からこれらを削除します', 'attr': 'is_arrange_name'}>
is_align_to_base_bone: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Align to Base Bone', 'default': True, 'description': "Align the object to it's base bone", 'attr': 'is_align_to_base_bone'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Align to Base Bone', 'default': True, 'description': "Align the object to it's base bone", 'attr': 'is_align_to_base_bone'}>
is_convert_tris: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '四角面を三角面に', 'default': True, 'description': '四角ポリゴンを三角ポリゴンに変換してから出力します、元のメッシュには影響ありません', 'attr': 'is_convert_tris'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': '四角面を三角面に', 'default': True, 'description': '四角ポリゴンを三角ポリゴンに変換してから出力します、元のメッシュには影響ありません', 'attr': 'is_convert_tris'}>
is_split_sharp: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Split Sharp Edges', 'default': True, 'description': 'Split all edges marked as sharp.', 'attr': 'is_split_sharp'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Split Sharp Edges', 'default': True, 'description': 'Split all edges marked as sharp.', 'attr': 'is_split_sharp'}>
is_normalize_weight: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ウェイトの合計を1.0に', 'default': True, 'description': '4つのウェイトの合計値が1.0になるように正規化します', 'attr': 'is_normalize_weight'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'ウェイトの合計を1.0に', 'default': True, 'description': '4つのウェイトの合計値が1.0になるように正規化します', 'attr': 'is_normalize_weight'}>
is_convert_bone_weight_names: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '頂点グループ名をCM3D2用に変換', 'default': True, 'description': '全ての頂点グループ名をCM3D2で使える名前にしてからエクスポートします', 'attr': 'is_convert_bone_weight_names'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': '頂点グループ名をCM3D2用に変換', 'default': True, 'description': '全ての頂点グループ名をCM3D2で使える名前にしてからエクスポートします', 'attr': 'is_convert_bone_weight_names'}>
is_clean_vertex_groups: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'クリーンな頂点グループ', 'default': True, 'description': '重みがゼロの場合、頂点グループから頂点を削除します', 'attr': 'is_clean_vertex_groups'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'クリーンな頂点グループ', 'default': True, 'description': '重みがゼロの場合、頂点グループから頂点を削除します', 'attr': 'is_clean_vertex_groups'}>
is_batch: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'バッチモード', 'default': False, 'description': 'モードの切替やエラー個所の選択を行いません', 'attr': 'is_batch'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'バッチモード', 'default': False, 'description': 'モードの切替やエラー個所の選択を行いません', 'attr': 'is_batch'}>
export_tangent: <_PropertyDeferred, <built-in function BoolProperty>, {'name': '接空間情報出力', 'default': False, 'description': '接空間情報(binormals, tangents)を出力する', 'attr': 'export_tangent'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': '接空間情報出力', 'default': False, 'description': '接空間情報(binormals, tangents)を出力する', 'attr': 'export_tangent'}>
shapekey_threshold: <_PropertyDeferred, <built-in function FloatProperty>, {'name': 'Shape Key Threshold', 'default': 0.001, 'min': 0, 'soft_min': 0.0005, 'max': 0.01, 'soft_max': 0.002, 'precision': 5, 'description': 'Lower values increase accuracy and file size. Higher values truncate small changes and reduce file size.', 'attr': 'shapekey_threshold'}> = <_PropertyDeferred, <built-in function FloatProperty>, {'name': 'Shape Key Threshold', 'default': 0.001, 'min': 0, 'soft_min': 0.0005, 'max': 0.01, 'soft_max': 0.002, 'precision': 5, 'description': 'Lower values increase accuracy and file size. Higher values truncate small changes and reduce file size.', 'attr': 'shapekey_threshold'}>
export_shapekey_normals: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Export Shape Key Normals', 'default': True, 'description': 'Export custom normals for each shape key on export.', 'attr': 'export_shapekey_normals'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Export Shape Key Normals', 'default': True, 'description': 'Export custom normals for each shape key on export.', 'attr': 'export_shapekey_normals'}>
shapekey_normals_blend: <_PropertyDeferred, <built-in function FloatProperty>, {'name': 'Shape Key Normals Blend', 'default': 0.6, 'min': 0, 'max': 1, 'precision': 3, 'description': 'Adjust the influence of shape keys on custom normals', 'attr': 'shapekey_normals_blend'}> = <_PropertyDeferred, <built-in function FloatProperty>, {'name': 'Shape Key Normals Blend', 'default': 0.6, 'min': 0, 'max': 1, 'precision': 3, 'description': 'Adjust the influence of shape keys on custom normals', 'attr': 'shapekey_normals_blend'}>
use_shapekey_colors: <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Use Shape Key Colors', 'default': True, 'description': 'Use the shape key normals stored in the vertex colors instead of calculating the normals on export. (Recommend disabling if geometry was customized)', 'attr': 'use_shapekey_colors'}> = <_PropertyDeferred, <built-in function BoolProperty>, {'name': 'Use Shape Key Colors', 'default': True, 'description': 'Use the shape key normals stored in the vertex colors instead of calculating the normals on export. (Recommend disabling if geometry was customized)', 'attr': 'use_shapekey_colors'}>
@classmethod
def poll(cls, context):
76    @classmethod
77    def poll(cls, context):
78        ob = context.active_object
79        if ob:
80            if ob.type == 'MESH':
81                return True
82        return False
def report_cancel(self, report_message, report_type={'ERROR'}, resobj={'CANCELLED'}):
84    def report_cancel(self, report_message, report_type={'ERROR'}, resobj={'CANCELLED'}):
85        """エラーメッセージを出力してキャンセルオブジェクトを返す"""
86        self.report(type=report_type, message=report_message)
87        return resobj

エラーメッセージを出力してキャンセルオブジェクトを返す

def precheck(self, context):
 89    def precheck(self, context):
 90        """データの成否チェック"""
 91        ob = context.active_object
 92        if not ob:
 93            return self.report_cancel("アクティブオブジェクトがありません")
 94        if ob.type != 'MESH':
 95            return self.report_cancel("メッシュオブジェクトを選択した状態で実行してください")
 96        if not len(ob.material_slots):
 97            return self.report_cancel("マテリアルがありません")
 98        for slot in ob.material_slots:
 99            if not slot.material:
100                return self.report_cancel("空のマテリアルスロットを削除してください")
101            try:
102                slot.material['shader1']
103                slot.material['shader2']
104            except:
105                return self.report_cancel("マテリアルに「shader1」と「shader2」という名前のカスタムプロパティを用意してください")
106        me = ob.data
107        if not me.uv_layers.active:
108            return self.report_cancel("UVがありません")
109        if 65535 < len(me.vertices):
110            return self.report_cancel("エクスポート可能な頂点数を大幅に超えています、最低でも65535未満には削減してください")
111        return None

データの成否チェック

def invoke(self, context, event):
113    def invoke(self, context, event):
114        res = self.precheck(context)
115        if res:
116            return res
117        ob = context.active_object
118
119        # model名とか
120        ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.')
121        self.model_name = ob_names[0]
122        self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto'
123
124        # ボーン情報元のデフォルトオプションを取得
125        arm_ob = ob.parent
126        for mod in ob.modifiers:
127            if mod.type == 'ARMATURE' and mod.object:
128                arm_ob = mod.object
129        if arm_ob and not arm_ob.type == 'ARMATURE':
130            arm_ob = None
131
132        info_mode_was_armature = (self.bone_info_mode == 'ARMATURE')
133        if "BoneData" in context.blend_data.texts:
134            if "LocalBoneData" in context.blend_data.texts:
135                self.bone_info_mode = 'TEXT'
136        if "BoneData:0" in ob:
137            ver = ob.get("ModelVersion")
138            if ver and ver >= 1000:
139                self.version = str(ver)
140            if "LocalBoneData:0" in ob:
141                self.bone_info_mode = 'OBJECT_PROPERTY'
142        if arm_ob:
143            if info_mode_was_armature:
144                self.bone_info_mode = 'ARMATURE'
145            else:
146                self.bone_info_mode = 'ARMATURE_PROPERTY'
147
148        # エクスポート時のデフォルトパスを取得
149        #if not self.filepath[-6:] == '.model':
150        if common.preferences().model_default_path:
151            self.filepath = common.default_cm3d2_dir(common.preferences().model_default_path, self.model_name, "model")
152        else:
153            self.filepath = common.default_cm3d2_dir(common.preferences().model_export_path, self.model_name, "model")
154
155        # バックアップ関係
156        self.is_backup = bool(common.preferences().backup_ext)
157
158        self.scale = 1.0 / common.preferences().scale
159        context.window_manager.fileselect_add(self)
160        return {'RUNNING_MODAL'}
def draw(self, context):
163    def draw(self, context):
164        self.layout.prop(self, 'scale')
165        row = self.layout.row()
166        row.prop(self, 'is_backup', icon='FILE_BACKUP')
167        if not common.preferences().backup_ext:
168            row.enabled = False
169        self.layout.prop(self, 'is_arrange_name', icon='FILE_TICK')
170        box = self.layout.box()
171        box.prop(self, 'version', icon='LINENUMBERS_ON')
172        box.prop(self, 'model_name', icon='SORTALPHA')
173
174        row = box.row()
175        row.prop(self, 'base_bone_name', icon='CONSTRAINT_BONE')
176        if self.base_bone_name == 'Auto':
177            row.enabled = False
178
179        prefs = common.preferences()
180        
181        box = self.layout.box()
182        col = box.column(align=True)
183        col.label(text="ボーン情報元", icon='BONE_DATA')
184        col.prop(self, 'bone_info_mode', icon='BONE_DATA', expand=True)
185        col = box.column(align=True)
186        col.label(text="マテリアル情報元", icon='MATERIAL')
187        col.prop(self, 'mate_info_mode', icon='MATERIAL', expand=True)
188        
189        box = self.layout.box()
190        box.label(text="メッシュオプション")
191        box.prop(self , 'is_align_to_base_bone', icon=compat.icon('OBJECT_ORIGIN'  ))
192        box.prop(self , 'is_convert_tris'      , icon=compat.icon('MESH_DATA'      ))
193        box.prop(self , 'is_split_sharp'       , icon=compat.icon('MOD_EDGESPLIT'  ))
194        box.prop(self , 'export_tangent'       , icon=compat.icon('CURVE_BEZCIRCLE'))
195        sub_box = box.box()
196        sub_box.prop(self , 'shapekey_threshold'     , icon=compat.icon('SHAPEKEY_DATA'      ), slider=True)
197        sub_box.prop(prefs, 'skip_shapekey'          , icon=compat.icon('SHAPEKEY_DATA'      ), toggle=1)
198        sub_box.prop(self , 'export_shapekey_normals', icon=compat.icon('NORMALS_VERTEX_FACE'))
199        row = sub_box.row()
200        row    .prop(self , 'shapekey_normals_blend' , icon=compat.icon('MOD_NORMALEDIT'     ), slider=True)
201        row.enabled = self.export_shapekey_normals
202        row = sub_box.row()
203        row    .prop(self , 'use_shapekey_colors'    , icon=compat.icon('GROUP_VCOL')         , toggle=0)
204        row.enabled = self.export_shapekey_normals
205        sub_box = box.box()
206        sub_box.prop(self, 'is_normalize_weight', icon='MOD_VERTEX_WEIGHT')
207        sub_box.prop(self, 'is_clean_vertex_groups', icon='MOD_VERTEX_WEIGHT')
208        sub_box.prop(self, 'is_convert_bone_weight_names', icon_value=common.kiss_icon())
209        sub_box
210        sub_box = box.box()
211        sub_box.prop(prefs, 'is_apply_modifiers', icon='MODIFIER')
212        row = sub_box.row()
213        row.prop(prefs, 'custom_normal_blend', icon='SNAP_NORMAL', slider=True)
214        row.enabled = prefs.is_apply_modifiers
def copy_and_activate_ob(self, context, ob):
216    def copy_and_activate_ob(self, context, ob):
217        new_ob = ob.copy()
218        new_me = ob.data.copy()
219        new_ob.data = new_me
220        compat.link(context.scene, new_ob)
221        compat.set_active(context, new_ob)
222        compat.set_select(new_ob, True)
223        return new_ob
def execute(self, context):
225    def execute(self, context):
226        start_time = time.time()
227        prefs = common.preferences()
228
229        selected_objs = context.selected_objects
230        source_objs = []
231        prev_mode = None
232        try:
233            ob_source = context.active_object
234            if ob_source not in selected_objs:
235                selected_objs.append(ob_source) # luvoid : Fix error where object is active but not selected
236            ob_name = ob_source.name
237            ob_main = None
238            if self.is_batch:
239                # アクティブオブジェクトを1つコピーするだけでjoinしない
240                source_objs.append(ob_source)
241                compat.set_select(ob_source, False)
242                ob_main = self.copy_and_activate_ob(context, ob_source)
243
244                if prefs.is_apply_modifiers and bpy.ops.object.forced_modifier_apply.poll(context):
245                    bpy.ops.object.forced_modifier_apply(is_applies=[True for i in range(32)])
246            else:
247                selected_count = 0
248                # 選択されたMESHオブジェクトをコピーしてjoin
249                # 必要に応じて、モディファイアの強制適用を行う
250                for selected in selected_objs:
251                    source_objs.append(selected)
252
253                    compat.set_select(selected, False)
254
255                    if selected.type == 'MESH':
256                        ob_created = self.copy_and_activate_ob(context, selected)
257                        if selected == ob_source:
258                            ob_main = ob_created
259                        if prefs.is_apply_modifiers:
260                            bpy.ops.object.forced_modifier_apply(apply_viewport_visible=True)
261
262                        selected_count += 1
263
264                mode = context.active_object.mode
265                if mode != 'OBJECT':
266                    prev_mode = mode
267                    bpy.ops.object.mode_set(mode='OBJECT')
268
269                if selected_count > 1:
270                    if ob_main:
271                        compat.set_active(context, ob_main)
272                    bpy.ops.object.join()
273                    self.report(type={'INFO'}, message=f_tip_("{}個のオブジェクトをマージしました", selected_count))
274
275            ret = self.export(context, ob_main)
276            if 'FINISHED' not in ret:
277                return ret
278
279            context.window_manager.progress_update(10)
280            diff_time = time.time() - start_time
281            self.report(type={'INFO'}, message=f_tip_("modelのエクスポートが完了しました。{:.2f} 秒 file={}", diff_time, self.filepath))
282            return ret
283        finally:
284            # 作業データの破棄(コピーデータを削除、選択状態の復元、アクティブオブジェクト、モードの復元)
285            if ob_main:
286                common.remove_data(ob_main)
287                # me_copied = ob_main.data
288                # context.blend_data.objects.remove(ob_main, do_unlink=True)
289                # context.blend_data.meshes.remove(me_copied, do_unlink=True)
290
291            for obj in source_objs:
292                compat.set_select(obj, True)
293
294            if ob_source:
295                # TODO 元のオブジェクトをアクティブに戻す
296                if ob_name in bpy.data.objects:
297                    compat.set_active(context, ob_source)
298
299            if prev_mode:
300                bpy.ops.object.mode_set(mode=prev_mode)
def export(self, context, ob):
302    def export(self, context, ob):
303        """モデルファイルを出力"""
304        prefs = common.preferences()
305
306        if not self.is_batch:
307            prefs.model_export_path = self.filepath
308            prefs.scale = 1.0 / self.scale
309
310        context.window_manager.progress_begin(0, 10)
311        context.window_manager.progress_update(0)
312
313        res = self.precheck(context)
314        if res:
315            return res
316        me = ob.data
317
318        if ob.active_shape_key_index != 0:
319            ob.active_shape_key_index = 0
320            me.update()
321
322        # データの成否チェック
323        if self.bone_info_mode == 'ARMATURE':
324            arm_ob = ob.parent
325            if arm_ob and arm_ob.type != 'ARMATURE':
326                return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません")
327            if not arm_ob:
328                try:
329                    arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object)
330                except StopIteration:
331                    return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい")
332                arm_ob = arm_ob.object
333        elif self.bone_info_mode == 'TEXT':
334            if "BoneData" not in context.blend_data.texts:
335                return self.report_cancel("テキスト「BoneData」が見つかりません、中止します")
336            if "LocalBoneData" not in context.blend_data.texts:
337                return self.report_cancel("テキスト「LocalBoneData」が見つかりません、中止します")
338        elif self.bone_info_mode == 'OBJECT_PROPERTY':
339            if "BoneData:0" not in ob:
340                return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません")
341            if "LocalBoneData:0" not in ob:
342                return self.report_cancel("オブジェクトのカスタムプロパティにボーン情報がありません")
343        elif self.bone_info_mode == 'ARMATURE_PROPERTY':
344            arm_ob = ob.parent
345            if arm_ob and arm_ob.type != 'ARMATURE':
346                return self.report_cancel("メッシュオブジェクトの親がアーマチュアではありません")
347            if not arm_ob:
348                try:
349                    arm_ob = next(mod for mod in ob.modifiers if mod.type == 'ARMATURE' and mod.object)
350                except StopIteration:
351                    return self.report_cancel("アーマチュアが見つかりません、親にするかモディファイアにして下さい")
352                arm_ob = arm_ob.object
353            if "BoneData:0" not in arm_ob.data:
354                return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません")
355            if "LocalBoneData:0" not in arm_ob.data:
356                return self.report_cancel("アーマチュアのカスタムプロパティにボーン情報がありません")
357        else:
358            return self.report_cancel("ボーン情報元のモードがおかしいです")
359
360        if self.mate_info_mode == 'TEXT':
361            for index, slot in enumerate(ob.material_slots):
362                if "Material:" + str(index) not in context.blend_data.texts:
363                    return self.report_cancel("マテリアル情報元のテキストが足りません")
364        context.window_manager.progress_update(1)
365
366        # model名とか
367        ob_names = common.remove_serial_number(ob.name, self.is_arrange_name).split('.')
368        if self.model_name == '*':
369            self.model_name = ob_names[0]
370        if self.base_bone_name == '*':
371            self.base_bone_name = ob_names[1] if 2 <= len(ob_names) else 'Auto'
372
373        # BoneData情報読み込み
374        base_bone_candidate = None
375        bone_data = []
376        if self.bone_info_mode == 'ARMATURE':
377            bone_data = self.armature_bone_data_parser(context, arm_ob)
378            base_bone_candidate = arm_ob.data['BaseBone']
379        elif self.bone_info_mode == 'TEXT':
380            bone_data_text = context.blend_data.texts["BoneData"]
381            if 'BaseBone' in bone_data_text:
382                base_bone_candidate = bone_data_text['BaseBone']
383            bone_data = self.bone_data_parser(l.body for l in bone_data_text.lines)
384        elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']:
385            target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data
386            if 'BaseBone' in target:
387                base_bone_candidate = target['BaseBone']
388            bone_data = self.bone_data_parser(self.indexed_data_generator(target, prefix="BoneData:"))
389        if len(bone_data) <= 0:
390            return self.report_cancel("テキスト「BoneData」に有効なデータがありません")
391
392        if self.base_bone_name not in (b['name'] for b in bone_data):
393            if base_bone_candidate and self.base_bone_name == 'Auto':
394                self.base_bone_name = base_bone_candidate
395            else:
396                return self.report_cancel("基点ボーンが存在しません")
397        bone_name_indices = {bone['name']: index for index, bone in enumerate(bone_data)}
398        context.window_manager.progress_update(2)
399
400        if self.is_align_to_base_bone:
401            bpy.ops.object.align_to_cm3d2_base_bone(scale=1.0/self.scale, is_preserve_mesh=True, bone_info_mode=self.bone_info_mode)
402            me.update()
403
404        if self.is_split_sharp:
405            bpy.ops.object.mode_set(mode='EDIT')
406            bpy.ops.mesh.split_sharp()
407            bpy.ops.object.mode_set(mode='OBJECT')
408
409        # LocalBoneData情報読み込み
410        local_bone_data = []
411        if self.bone_info_mode == 'ARMATURE':
412            local_bone_data = self.armature_local_bone_data_parser(arm_ob)
413        elif self.bone_info_mode == 'TEXT':
414            local_bone_data_text = context.blend_data.texts["LocalBoneData"]
415            local_bone_data = self.local_bone_data_parser(l.body for l in local_bone_data_text.lines)
416        elif self.bone_info_mode in ['OBJECT_PROPERTY', 'ARMATURE_PROPERTY']:
417            target = ob if self.bone_info_mode == 'OBJECT_PROPERTY' else arm_ob.data
418            local_bone_data = self.local_bone_data_parser(self.indexed_data_generator(target, prefix="LocalBoneData:"))
419        if len(local_bone_data) <= 0:
420            return self.report_cancel("テキスト「LocalBoneData」に有効なデータがありません")
421        local_bone_name_indices = {bone['name']: index for index, bone in enumerate(local_bone_data)}
422        context.window_manager.progress_update(3)
423        
424        used_local_bone = {index: False for index, bone in enumerate(local_bone_data)}
425        
426        # ウェイト情報読み込み
427        vertices = []
428        is_over_one = 0
429        is_under_one = 0
430        is_in_too_many = 0
431        for i, vert in enumerate(me.vertices):
432            vgs = []
433            for vg in vert.groups:
434                if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group.
435                    continue
436                name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names)
437                index = local_bone_name_indices.get(name, -1)
438                if index >= 0 and (vg.weight > 0.0 or not self.is_clean_vertex_groups):
439                    vgs.append([index, vg.weight])
440                    # luvoid : track used bones
441                    used_local_bone[index] = True
442                    boneindex = bone_name_indices.get(name, -1)
443                    while boneindex >= 0:
444                        parent = bone_data[boneindex]
445                        localindex = local_bone_name_indices.get(parent['name'], -1)
446                        # could check for `localindex == -1` here, but it's prescence may be useful in determing if the local bones resolve back to some root
447                        used_local_bone[localindex] = True
448                        boneindex = parent['parent_index']
449            if len(vgs) == 0:
450                if not self.is_batch:
451                    self.select_no_weight_vertices(context, local_bone_name_indices)
452                return self.report_cancel("ウェイトが割り当てられていない頂点が見つかりました、中止します")
453            if len(vgs) > 4:
454                is_in_too_many += 1
455            vgs = sorted(vgs, key=itemgetter(1), reverse=True)[0:4]
456            total = sum(vg[1] for vg in vgs)
457            if self.is_normalize_weight:
458                for vg in vgs:
459                    vg[1] /= total
460            else:
461                if 1.01 < total:
462                    is_over_one += 1
463                elif total < 0.99:
464                    is_under_one += 1
465            if len(vgs) < 4:
466                vgs += [(0, 0.0)] * (4 - len(vgs))
467            vertices.append({
468                'index': vert.index,
469                'face_indexs': list(map(itemgetter(0), vgs)),
470                'weights': list(map(itemgetter(1), vgs)),
471            })
472        
473        if 1 <= is_over_one:
474            self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0を超えている頂点が見つかりました。正規化してください。超過している頂点の数:{}", is_over_one))
475        if 1 <= is_under_one:
476            self.report(type={'WARNING'}, message=f_tip_("ウェイトの合計が1.0未満の頂点が見つかりました。正規化してください。不足している頂点の数:{}", is_under_one))
477        
478        # luvoid : warn that there are vertices in too many vertex groups
479        if is_in_too_many > 0:
480            self.report(type={'WARNING'}, message=f_tip_("4つを超える頂点グループにある頂点が見つかりました。頂点グループをクリーンアップしてください。不足している頂点の数:{}", is_in_too_many))
481                
482        # luvoid : check for unused local bones that the game will delete
483        is_deleted = 0
484        deleted_names = "The game will delete these local bones"
485        for index, is_used in used_local_bone.items():
486            print(index, is_used)
487            if is_used == False:
488                is_deleted += 1
489                deleted_names = deleted_names + '\n' + local_bone_data[index]['name']
490            elif is_used == True:
491                pass
492            else:
493                print(f_tip_("Unexpected: used_local_bone[{key}] == {value} when len(used_local_bone) == {length}", key=index, value=is_used, length=len(used_local_bone)))
494                self.report(type={'WARNING'}, message=f_tip_("Could not find whether bone with index {index} was used. See console for more info.", index=i))
495        if is_deleted > 0:
496            self.report(type={'WARNING'}, message=f_tip_("頂点が割り当てられていない{num}つのローカルボーンが見つかりました。 詳細については、ログを参照してください。", num=is_deleted))
497            self.report(type={'INFO'}, message=deleted_names)
498                
499        context.window_manager.progress_update(4)
500        
501
502        try:
503            writer = common.open_temporary(self.filepath, 'wb', is_backup=self.is_backup)
504        except:
505            self.report(type={'ERROR'}, message=f_tip_("ファイルを開くのに失敗しました、アクセス不可かファイルが存在しません。file={}", self.filepath))
506            return {'CANCELLED'}
507
508        model_datas = {
509            'bone_data': bone_data,
510            'local_bone_data': local_bone_data,
511            'vertices': vertices,
512        }
513        try:
514            with writer:
515                self.write_model(context, ob, writer, **model_datas)
516        except common.CM3D2ExportException as e:
517            self.report(type={'ERROR'}, message=str(e))
518            return {'CANCELLED'}
519
520        return {'FINISHED'}

モデルファイルを出力

def write_model( self, context, ob, writer, bone_data=[], local_bone_data=[], vertices=[]):
522    def write_model(self, context, ob, writer, bone_data=[], local_bone_data=[], vertices=[]):
523        """モデルデータをファイルオブジェクトに書き込む"""
524        me = ob.data
525        prefs = common.preferences()
526
527        # ファイル先頭
528        common.write_str(writer, 'CM3D2_MESH')
529        self.version_num = int(self.version)
530        writer.write(struct.pack('<i', self.version_num))
531
532        common.write_str(writer, self.model_name)
533        common.write_str(writer, self.base_bone_name)
534
535        # ボーン情報書き出し
536        writer.write(struct.pack('<i', len(bone_data)))
537        for bone in bone_data:
538            common.write_str(writer, bone['name'])
539            writer.write(struct.pack('<b', bone['scl']))
540        context.window_manager.progress_update(3.3)
541        for bone in bone_data:
542            writer.write(struct.pack('<i', bone['parent_index']))
543        context.window_manager.progress_update(3.7)
544        for bone in bone_data:
545            writer.write(struct.pack('<3f', bone['co'][0], bone['co'][1], bone['co'][2]))
546            writer.write(struct.pack('<4f', bone['rot'][1], bone['rot'][2], bone['rot'][3], bone['rot'][0]))
547            if self.version_num >= 2001:
548                use_scale = ('scale' in bone)
549                writer.write(struct.pack('<b', use_scale))
550                if use_scale:
551                    bone_scale = bone['scale']
552                    writer.write(struct.pack('<3f', bone_scale[0], bone_scale[1], bone_scale[2]))
553        context.window_manager.progress_update(4)
554
555        # 正しい頂点数などを取得
556        bm = bmesh.new()
557        bm.from_mesh(me)
558        uv_lay = bm.loops.layers.uv.active
559        vert_uvs = []
560        vert_uvs_append = vert_uvs.append
561        vert_iuv = {}
562        vert_indices = {}
563        vert_count = 0
564        for vert in bm.verts:
565            vert_uv = []
566            vert_uvs_append(vert_uv)
567            for loop in vert.link_loops:
568                uv = loop[uv_lay].uv
569                if uv not in vert_uv:
570                    vert_uv.append(uv)
571                    vert_iuv[hash((vert.index, uv.x, uv.y))] = vert_count
572                    vert_indices[vert.index] = vert_count
573                    vert_count += 1
574        if 65535 < vert_count:
575            raise common.CM3D2ExportException(f_tip_("頂点数がまだ多いです (現在{}頂点)。あと{}頂点以上減らしてください、中止します", vert_count, vert_count - 65535))
576        context.window_manager.progress_update(5)
577
578        writer.write(struct.pack('<2i', vert_count, len(ob.material_slots)))
579
580        # ローカルボーン情報を書き出し
581        writer.write(struct.pack('<i', len(local_bone_data)))
582        for bone in local_bone_data:
583            common.write_str(writer, bone['name'])
584        context.window_manager.progress_update(5.3)
585        for bone in local_bone_data:
586            for f in bone['matrix']:
587                writer.write(struct.pack('<f', f))
588        context.window_manager.progress_update(5.7)
589
590        # カスタム法線情報を取得
591        if me.has_custom_normals:
592            custom_normals = [mathutils.Vector() for i in range(len(me.vertices))]
593            me.calc_normals_split()
594            for loop in me.loops:
595                custom_normals[loop.vertex_index] += loop.normal
596            for no in custom_normals:
597                no.normalize()
598        else:
599            custom_normals = None
600
601        cm_verts = []
602        cm_norms = []
603        cm_uvs = []
604        # 頂点情報を書き出し
605        for i, vert in enumerate(bm.verts):
606            co = compat.convert_bl_to_cm_space( vert.co * self.scale )
607            if me.has_custom_normals:
608                no = custom_normals[vert.index]
609            else:
610                no = vert.normal.copy()
611            no = compat.convert_bl_to_cm_space( no )
612            for uv in vert_uvs[i]:
613                cm_verts.append(co)
614                cm_norms.append(no)
615                cm_uvs.append(uv)
616                writer.write(struct.pack('<3f', co.x, co.y, co.z))
617                writer.write(struct.pack('<3f', no.x, no.y, no.z))
618                writer.write(struct.pack('<2f', uv.x, uv.y))
619        context.window_manager.progress_update(6)
620
621        cm_tris = self.parse_triangles(bm, ob, uv_lay, vert_iuv, vert_indices)
622
623        # 接空間情報を書き出し
624        if self.export_tangent:
625            tangents = self.calc_tangents(cm_tris, cm_verts, cm_norms, cm_uvs)
626            writer.write(struct.pack('<i', len(tangents)))
627            for t in tangents:
628                writer.write(struct.pack('<4f', *t))
629        else:
630            writer.write(struct.pack('<i', 0))
631
632        # ウェイト情報を書き出し
633        for vert in vertices:
634            for uv in vert_uvs[vert['index']]:
635                writer.write(struct.pack('<4H', *vert['face_indexs']))
636                writer.write(struct.pack('<4f', *vert['weights']))
637        context.window_manager.progress_update(7)
638
639        # 面情報を書き出し
640        for tri in cm_tris:
641            writer.write(struct.pack('<i', len(tri)))
642            for vert_index in tri:
643                writer.write(struct.pack('<H', vert_index))
644        context.window_manager.progress_update(8)
645
646        # マテリアルを書き出し
647        writer.write(struct.pack('<i', len(ob.material_slots)))
648        for slot_index, slot in enumerate(ob.material_slots):
649            if self.mate_info_mode == 'MATERIAL':
650                mat_data = cm3d2_data.MaterialHandler.parse_mate(slot.material, self.is_arrange_name)
651                mat_data.write(writer, write_header=False)
652
653            elif self.mate_info_mode == 'TEXT':
654                text = context.blend_data.texts["Material:" + str(slot_index)].as_string()
655                mat_data = cm3d2_data.MaterialHandler.parse_text(slot.material, self.is_arrange_name)
656                mat_data.write(writer, write_header=False)
657
658        context.window_manager.progress_update(9)
659
660        # モーフを書き出し
661        if me.shape_keys and len(me.shape_keys.key_blocks) >= 2:
662            try:
663                self.write_shapekeys(context, ob, writer, vert_uvs, custom_normals)
664            finally:
665                print("FINISHED SHAPE KEYS WRITE")
666                pass
667        common.write_str(writer, 'end')

モデルデータをファイルオブジェクトに書き込む

def write_shapekeys(self, context, ob, writer, vert_uvs, custom_normals=None):
669    def write_shapekeys(self, context, ob, writer, vert_uvs, custom_normals=None):
670        # モーフを書き出し
671        me = ob.data
672        prefs = common.preferences()
673        
674        is_use_attributes = (not compat.IS_LEGACY and bpy.app.version >= (2,92))
675
676        loops_vert_index = np.empty((len(me.loops)), dtype=int)
677        me.loops.foreach_get('vertex_index', loops_vert_index.ravel())
678
679        def find_normals_attribute(name) -> (bpy.types.Attribute, bool):
680            if is_use_attributes:
681                normals_color = me.attributes[name] if name in me.attributes.keys() else None
682                attribute_is_color = (not normals_color is None) and normals_color.data_type in {'BYTE_COLOR', 'FLOAT_COLOR'}
683            else:
684                normals_color = me.vertex_colors[name] if name in me.vertex_colors.keys() else None
685                attribute_is_color = True
686            return normals_color, attribute_is_color
687
688        if self.use_shapekey_colors:
689            static_attribute_colors = np.empty((len(me.loops), 4), dtype=float)
690            color_offset = np.array([[0.5,0.5,0.5]])
691            loops_per_vertex = np.zeros((len(me.vertices)))
692            for loop in me.loops:
693                loops_per_vertex[loop.vertex_index] += 1
694            loops_per_vertex_reciprocal = np.reciprocal(loops_per_vertex, out=loops_per_vertex).reshape((len(me.vertices), 1))
695        def get_sk_delta_normals_from_attribute(attribute, is_color, out):
696            if is_color:
697                attribute.data.foreach_get('color', static_attribute_colors.ravel())
698                loop_delta_normals = static_attribute_colors[:,:3]
699                loop_delta_normals -= color_offset
700                loop_delta_normals *= 2
701            else:
702                loop_delta_normals = static_attribute_colors[:,:3]
703                attribute.data.foreach_get('vector', loop_delta_normals.ravel())
704            
705            vert_delta_normals = out
706            vert_delta_normals.fill(0)
707
708            # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index]
709            np.add.at(vert_delta_normals, loops_vert_index, loop_delta_normals) # XXX Slower but handles edge cases better
710            #vert_delta_normals[loops_vert_index] += loop_delta_normals # XXX Only first loop's value will be kept
711            
712            # for delta_normal in vert_delta_normals: delta_normal /= loops_per_vertex[vert.index]
713            vert_delta_normals *= loops_per_vertex_reciprocal
714
715            return out #.tolist()
716
717        if me.has_custom_normals:
718            basis_custom_normals = np.array(custom_normals, dtype=float)
719            static_loop_normals = np.empty((len(me.loops), 3), dtype=float)
720            static_vert_lengths = np.empty((len(me.vertices), 1), dtype=float)
721        def get_sk_delta_normals_from_custom_normals(shape_key, out):
722            vert_custom_normals = out
723            vert_custom_normals.fill(0)
724            
725            loop_custom_normals = static_loop_normals
726            np.copyto(loop_custom_normals.ravel(), shape_key.normals_split_get())
727            
728            # for loop in me.loops: vert_delta_normals[loop.vertex_index] += loop_delta_normals[loop.index]
729            if not self.is_split_sharp:  
730                # XXX Slower
731                np.add.at(vert_custom_normals, loops_vert_index, loop_custom_normals)
732                vert_len_sq = get_lengths_squared(vert_custom_normals, out=static_vert_lengths)
733                vert_len = np.sqrt(vert_len_sq, out=vert_len_sq)
734                np.reciprocal(vert_len, out=vert_len)
735                vert_custom_normals *= vert_len #.reshape((*vert_len.shape, 1))
736            else:
737                # loop normals should be the same per-vertex unless there is a sharp edge 
738                # or a flat shaded face, but all sharp edges were split, so this method is fine
739                # (and Flat shaded faces just won't be supported)
740                vert_custom_normals[loops_vert_index] += loop_custom_normals # Only first loop's value will be kept
741
742            vert_custom_normals -= basis_custom_normals
743            return out
744        
745        if not me.has_custom_normals:
746            basis_normals = np.empty((len(me.vertices), 3), dtype=float)
747            me.vertices.foreach_get('normal', basis_normals.ravel())
748        def get_sk_delta_normals_from_normals(shape_key, out):
749            vert_normals = out
750            np.copyto(vert_normals.ravel(), shape_key.normals_vertex_get())
751            vert_delta_normals = np.subtract(vert_normals, basis_normals, out=out)
752            return out
753
754        basis_co = np.empty((len(me.vertices), 3), dtype=float)
755        me.vertices.foreach_get('co', basis_co.ravel())
756        def get_sk_delta_coordinates(shape_key, out):
757            delta_coordinates = out
758            shape_key.data.foreach_get('co', delta_coordinates.ravel())
759            delta_coordinates -= basis_co
760            return out
761
762        static_array_sq = np.empty((len(me.vertices), 3), dtype=float)
763        def get_lengths_squared(vectors, out):
764            np.power(vectors, 2, out=static_array_sq)
765            np.sum(static_array_sq, axis=1, out=out.ravel())
766            return out
767
768        def write_morph(morph, name):
769            common.write_str(writer, 'morph')
770            common.write_str(writer, name)
771            writer.write(struct.pack('<i', len(morph)))
772            for v_index, vec, normal in morph:
773                vec    = compat.convert_bl_to_cm_space(vec   )
774                normal = compat.convert_bl_to_cm_space(normal)
775                writer.write(struct.pack('<H', v_index))
776                writer.write(struct.pack('<3f', *vec[:3]))
777                writer.write(struct.pack('<3f', *normal[:3]))
778        
779        # accessing operator properties via "self.x" is SLOW, so store some here
780        self__export_shapekey_normals = self.export_shapekey_normals
781        self__use_shapekey_colors = self.use_shapekey_colors
782        self__shapekey_normals_blend = self.shapekey_normals_blend
783        self__scale = self.scale
784        
785        co_diff_threshold = self.shapekey_threshold / 5
786        co_diff_threshold_squared = co_diff_threshold * co_diff_threshold
787        no_diff_threshold = self.shapekey_threshold * 10
788        no_diff_threshold_squared = no_diff_threshold * no_diff_threshold
789        
790        # shared arrays
791        delta_coordinates  = np.empty((len(me.vertices), 3), dtype=float)
792        vert_delta_normals = np.empty((len(me.vertices), 3), dtype=float)
793        loop_delta_normals = np.empty((len(me.loops   ), 3), dtype=float)
794
795        delta_co_lensq = np.empty((len(me.vertices)), dtype=float)
796        delta_no_lensq = np.empty((len(me.vertices)), dtype=float)
797
798        if not self.export_shapekey_normals:
799            vert_delta_normals.fill(0)
800            delta_no_lensq.fill(0)
801
802        # HEAVY LOOP
803        for shape_key in me.shape_keys.key_blocks[1:]:
804            morph = []
805
806            if self__export_shapekey_normals and self__use_shapekey_colors:
807                normals_color, attrubute_is_color = find_normals_attribute(f'{shape_key.name}_delta_normals')
808
809            if self__export_shapekey_normals:
810                if self__use_shapekey_colors and not normals_color is None:
811                    sk_delta_normals = get_sk_delta_normals_from_attribute(normals_color, attrubute_is_color, out=vert_delta_normals)
812                elif me.has_custom_normals:
813                    sk_delta_normals = get_sk_delta_normals_from_custom_normals(shape_key, out=vert_delta_normals)
814                    sk_delta_normals *= self__shapekey_normals_blend
815                else:
816                    sk_delta_normals = get_sk_delta_normals_from_normals(shape_key, out=vert_delta_normals)
817                    sk_delta_normals *= self__shapekey_normals_blend
818                
819                sk_no_lensq = get_lengths_squared(sk_delta_normals, out=delta_no_lensq)
820            else:
821                sk_delta_normals = vert_delta_normals
822                sk_no_lensq = delta_no_lensq
823
824            sk_co_diffs = get_sk_delta_coordinates(shape_key, out=delta_coordinates)
825            sk_co_diffs *= self__scale # scale before getting lengths
826            sk_co_lensq = get_lengths_squared(sk_co_diffs, out=delta_co_lensq)
827
828            # SUPER HEAVY LOOP
829            outvert_index = 0
830            for i in range(len(me.vertices)):
831                if sk_co_lensq[i] >= co_diff_threshold_squared or sk_no_lensq[i] >= no_diff_threshold_squared:
832                    morph += [ (outvert_index+j, sk_co_diffs[i], sk_delta_normals[i]) for j in range(len(vert_uvs[i])) ]
833                else:
834                    # ignore because change is too small (greatly lowers file size)
835                    pass
836                outvert_index += len(vert_uvs[i])
837
838            if prefs.skip_shapekey and not len(morph):
839                continue
840            else:
841                write_morph(morph, shape_key.name)
def write_tangents(self, writer, me):
843    def write_tangents(self, writer, me):
844        if len(me.uv_layers) < 1:
845            return
846
847        num_loops = len(me.loops)
def parse_triangles(self, bm, ob, uv_lay, vert_iuv, vert_indices):
849    def parse_triangles(self, bm, ob, uv_lay, vert_iuv, vert_indices):
850        def vert_index_from_loops(loops):
851            """vert_index generator"""
852            for loop in loops:
853                uv = loop[uv_lay].uv
854                v_index = loop.vert.index
855                vert_index = vert_iuv.get(hash((v_index, uv.x, uv.y)))
856                if vert_index is None:
857                    vert_index = vert_indices.get(v_index, 0)
858                yield vert_index
859
860        triangles = []
861        for mate_index, slot in enumerate(ob.material_slots):
862            tris_faces = []
863            for face in bm.faces:
864                if face.material_index != mate_index:
865                    continue
866                if len(face.verts) == 3:
867                    tris_faces.extend(vert_index_from_loops(reversed(face.loops)))
868                elif len(face.verts) == 4 and self.is_convert_tris:
869                    v1 = face.loops[0].vert.co - face.loops[2].vert.co
870                    v2 = face.loops[1].vert.co - face.loops[3].vert.co
871                    if v1.length < v2.length:
872                        f1 = [0, 1, 2]
873                        f2 = [0, 2, 3]
874                    else:
875                        f1 = [0, 1, 3]
876                        f2 = [1, 2, 3]
877                    faces, faces2 = [], []
878                    for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))):
879                        if i in f1:
880                            faces.append(vert_index)
881                        if i in f2:
882                            faces2.append(vert_index)
883                    tris_faces.extend(faces)
884                    tris_faces.extend(faces2)
885                elif 5 <= len(face.verts) and self.is_convert_tris:
886                    face_count = len(face.verts) - 2
887
888                    tris = []
889                    seek_min, seek_max = 0, len(face.verts) - 1
890                    for i in range(face_count):
891                        if not i % 2:
892                            tris.append([seek_min, seek_min + 1, seek_max])
893                            seek_min += 1
894                        else:
895                            tris.append([seek_min, seek_max - 1, seek_max])
896                            seek_max -= 1
897
898                    tris_indexs = [[] for _ in range(len(tris))]
899                    for i, vert_index in enumerate(vert_index_from_loops(reversed(face.loops))):
900                        for tris_index, points in enumerate(tris):
901                            if i in points:
902                                tris_indexs[tris_index].append(vert_index)
903
904                    tris_faces.extend(p for ps in tris_indexs for p in ps)
905
906            triangles.append(tris_faces)
907        return triangles
def calc_tangents(self, cm_tris, cm_verts, cm_norms, cm_uvs):
909    def calc_tangents(self, cm_tris, cm_verts, cm_norms, cm_uvs):
910        count = len(cm_verts)
911        tan1 = [None] * count
912        tan2 = [None] * count
913        for i in range(0, count):
914            tan1[i] = mathutils.Vector((0, 0, 0))
915            tan2[i] = mathutils.Vector((0, 0, 0))
916
917        for tris in cm_tris:
918            tri_len = len(tris)
919            tri_idx = 0
920            while tri_idx < tri_len:
921                i1, i2, i3 = tris[tri_idx], tris[tri_idx + 1], tris[tri_idx + 2]
922                v1, v2, v3 = cm_verts[i1], cm_verts[i2], cm_verts[i3]
923                w1, w2, w3 = cm_uvs[i1], cm_uvs[i2], cm_uvs[i3]
924
925                a1 = v2 - v1
926                a2 = v3 - v1
927                s1 = w2 - w1
928                s2 = w3 - w1
929                
930                r_inverse = (s1.x * s2.y - s2.x * s1.y)
931
932                if r_inverse != 0:
933                    # print("i1 = {i1}   i2 = {i2}   i3 = {i3}".format(i1=i1, i2=i2, i3=i3))
934                    # print("v1 = {v1}   v2 = {v2}   v3 = {v3}".format(v1=v1, v2=v2, v3=v3))
935                    # print("w1 = {w1}   w2 = {w2}   w3 = {w3}".format(w1=w1, w2=w2, w3=w3))
936
937                    # print("a1 = {a1}   a2 = {a2}".format(a1=a1, a2=a2))
938                    # print("s1 = {s1}   s2 = {s2}".format(s1=s1, s2=s2))
939                    
940                    # print("r_inverse = ({s1x} * {s2y} - {s2x} * {s1y}) = {r_inverse}".format(r_inverse=r_inverse, s1x=s1.x, s1y=s1.y, s2x=s2.x, s2y=s2.y))
941                                                
942                    r = 1.0 / r_inverse
943                    sdir = mathutils.Vector(((s2.y * a1.x - s1.y * a2.x) * r, (s2.y * a1.y - s1.y * a2.y) * r, (s2.y * a1.z - s1.y * a2.z) * r))
944                    tan1[i1] += sdir
945                    tan1[i2] += sdir
946                    tan1[i3] += sdir
947
948                    tdir = mathutils.Vector(((s1.x * a2.x - s2.x * a1.x) * r, (s1.x * a2.y - s2.x * a1.y) * r, (s1.x * a2.z - s2.x * a1.z) * r))
949                    tan2[i1] += tdir
950                    tan2[i2] += tdir
951                    tan2[i3] += tdir
952
953                tri_idx += 3
954
955        tangents = [None] * count
956        for i in range(0, count):
957            n = cm_norms[i]
958            ti = tan1[i]
959            t = (ti - n * n.dot(ti)).normalized()
960
961            c = n.cross(ti)
962            val = c.dot(tan2[i])
963            w = 1.0 if val < 0 else -1.0
964            tangents[i] = (-t.x, t.y, t.z, w)
965
966        return tangents
def select_no_weight_vertices(self, context, local_bone_name_indices):
968    def select_no_weight_vertices(self, context, local_bone_name_indices):
969        """ウェイトが割り当てられていない頂点を選択する"""
970        ob = context.active_object
971        me = ob.data
972        bpy.ops.object.mode_set(mode='EDIT')
973        bpy.ops.mesh.select_all(action='SELECT')
974        #bpy.ops.object.mode_set(mode='OBJECT')
975        context.tool_settings.mesh_select_mode = (True, False, False)
976        for vert in me.vertices:
977            for vg in vert.groups:
978                if len(ob.vertex_groups) <= vg.group: # Apparently a vertex can be assigned to a non-existent group.
979                    continue
980                name = common.encode_bone_name(ob.vertex_groups[vg.group].name, self.is_convert_bone_weight_names)
981                if name in local_bone_name_indices and 0.0 < vg.weight:
982                    vert.select = False
983                    break
984        bpy.ops.object.mode_set(mode='EDIT')

ウェイトが割り当てられていない頂点を選択する

def armature_bone_data_parser(self, context, ob):
 986    def armature_bone_data_parser(self, context, ob):
 987        """アーマチュアを解析してBoneDataを返す"""
 988        arm = ob.data
 989        
 990        pre_active = compat.get_active(context)
 991        pre_mode = ob.mode
 992
 993        compat.set_active(context, ob)
 994        bpy.ops.object.mode_set(mode='EDIT')
 995
 996        bones = []
 997        bone_name_indices = {}
 998        already_bone_names = []
 999        bones_queue = arm.edit_bones[:]
1000        while len(bones_queue):
1001            bone = bones_queue.pop(0)
1002
1003            if not bone.parent:
1004                already_bone_names.append(bone.name)
1005                bones.append(bone)
1006                bone_name_indices[bone.name] = len(bone_name_indices)
1007                continue
1008            elif bone.parent.name in already_bone_names:
1009                already_bone_names.append(bone.name)
1010                bones.append(bone)
1011                bone_name_indices[bone.name] = len(bone_name_indices)
1012                continue
1013
1014            bones_queue.append(bone)
1015
1016        bone_data = []
1017        for bone in bones:
1018
1019            # Also check for UnknownFlag for backwards compatibility
1020            is_scl_bone = bone['cm3d2_scl_bone'] if 'cm3d2_scl_bone' in bone \
1021                     else bone['UnknownFlag']    if 'UnknownFlag'    in bone \
1022                     else 0 
1023            parent_index = bone_name_indices[bone.parent.name] if bone.parent else -1
1024
1025            mat = bone.matrix.copy()
1026            
1027            if bone.parent:
1028                mat = compat.convert_bl_to_cm_bone_rotation(mat)
1029                mat = compat.mul(bone.parent.matrix.inverted(), mat)
1030                mat = compat.convert_bl_to_cm_bone_space(mat)
1031            else:
1032                mat = compat.convert_bl_to_cm_bone_rotation(mat)
1033                mat = compat.convert_bl_to_cm_space(mat)
1034            
1035            co = mat.to_translation() * self.scale
1036            rot = mat.to_quaternion()
1037            
1038            #if bone.parent:
1039            #    co.x, co.y, co.z = -co.y, -co.x, co.z
1040            #    rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, rot.x, -rot.z
1041            #else:
1042            #    co.x, co.y, co.z = -co.x, co.z, -co.y
1043            #
1044            #    fix_quat  = compat.Z_UP_TO_Y_UP_QUAT    #mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
1045            #    fix_quat2 = compat.BLEND_TO_OPENGL_QUAT #mathutils.Euler((math.radians(-90), 0, 0), 'XYZ').to_quaternion()
1046            #    rot = compat.mul3(rot, fix_quat, fix_quat2)
1047            #    #rot = compat.mul3(fix_quat2, rot, fix_quat)
1048            #
1049            #    rot.w, rot.x, rot.y, rot.z = -rot.y, -rot.z, -rot.x, rot.w
1050            
1051            # luvoid : I copied this from the Bone-Util Addon by trzr
1052            #if bone.parent:
1053            #    co.x, co.y, co.z = -co.y, co.z, co.x
1054            #    rot.w, rot.x, rot.y, rot.z = rot.w, rot.y, -rot.z, -rot.x
1055            #else:
1056            #    co.x, co.y, co.z = -co.x, co.z, -co.y
1057            #    
1058            #    rot = compat.mul(rot, mathutils.Quaternion((0, 0, 1), math.radians(90)))
1059            #    rot.w, rot.x, rot.y, rot.z = -rot.w, -rot.x, rot.z, -rot.y
1060            
1061            #opengl_mat = compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.matrix)
1062            #
1063            #if bone.parent:
1064            #    opengl_mat = compat.mul(compat.convert_blend_z_up_to_opengl_y_up_mat4(bone.parent.matrix).inverted(), opengl_mat)
1065            #
1066            #co = opengl_mat.to_translation() * self.scale
1067            #rot = opengl_mat.to_quaternion()
1068
1069            data = {
1070                'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names),
1071                'scl': is_scl_bone,
1072                'parent_index': parent_index,
1073                'co': co.copy(),
1074                'rot': rot.copy(),
1075            }
1076            scale = arm.edit_bones[bone.name].get('cm3d2_bone_scale')
1077            if scale:
1078                data['scale'] = scale
1079            bone_data.append(data)
1080        
1081        bpy.ops.object.mode_set(mode=pre_mode)
1082        compat.set_active(context, pre_active)
1083        return bone_data

アーマチュアを解析してBoneDataを返す

@staticmethod
def bone_data_parser(container):
1085    @staticmethod
1086    def bone_data_parser(container):
1087        """BoneData テキストをパースして辞書を要素とするリストを返す"""
1088        bone_data = []
1089        bone_name_indices = {}
1090        for line in container:
1091            data = line.split(',')
1092            if len(data) < 5:
1093                continue
1094
1095            parent_name = data[2]
1096            if parent_name.isdigit():
1097                parent_index = int(parent_name)
1098            else:
1099                parent_index = bone_name_indices.get(parent_name, -1)
1100
1101            bone_datum = {
1102                'name': data[0],
1103                'scl': int(data[1]),
1104                'parent_index': parent_index,
1105                'co': list(map(float, data[3].split())),
1106                'rot': list(map(float, data[4].split())),
1107            }
1108            # scale info (for version 2001 or later)
1109            if len(data) >= 7:
1110                if data[5] == '1':
1111                    bone_scale = data[6]
1112                    bone_datum['scale'] = list(map(float, bone_scale.split()))
1113            bone_data.append(bone_datum)
1114            bone_name_indices[data[0]] = len(bone_name_indices)
1115        return bone_data

BoneData テキストをパースして辞書を要素とするリストを返す

def armature_local_bone_data_parser(self, ob):
1117    def armature_local_bone_data_parser(self, ob):
1118        """アーマチュアを解析してBoneDataを返す"""
1119        arm = ob.data
1120
1121        # XXX Instead of just adding all bones, only bones / bones-with-decendants 
1122        #     that have use_deform == True or mathcing vertex groups should be used
1123        bones = []
1124        bone_name_indices = {}
1125        already_bone_names = []
1126        bones_queue = arm.bones[:]
1127        while len(bones_queue):
1128            bone = bones_queue.pop(0)
1129
1130            if not bone.parent:
1131                already_bone_names.append(bone.name)
1132                bones.append(bone)
1133                bone_name_indices[bone.name] = len(bone_name_indices)
1134                continue
1135            elif bone.parent.name in already_bone_names:
1136                already_bone_names.append(bone.name)
1137                bones.append(bone)
1138                bone_name_indices[bone.name] = len(bone_name_indices)
1139                continue
1140
1141            bones_queue.append(bone)
1142
1143        local_bone_data = []
1144        for bone in bones:
1145            mat = bone.matrix_local.copy()
1146            mat = compat.mul(mathutils.Matrix.Scale(-1, 4, (1, 0, 0)), mat)
1147            mat = compat.convert_bl_to_cm_bone_rotation(mat)
1148            pos = mat.translation.copy()
1149            
1150            mat.transpose()
1151            mat.row[3] = (0.0, 0.0, 0.0, 1.0)
1152            pos = compat.mul(mat.to_3x3(), pos)
1153            pos *= -self.scale
1154            mat.translation = pos
1155            mat.transpose()
1156            
1157            #co = mat.to_translation() * self.scale
1158            #rot = mat.to_quaternion()
1159            #
1160            #co.rotate(rot.inverted())
1161            #co.x, co.y, co.z = co.y, co.x, -co.z
1162            #
1163            #fix_quat = mathutils.Euler((0, 0, math.radians(-90)), 'XYZ').to_quaternion()
1164            #rot = compat.mul(rot, fix_quat)
1165            #rot.w, rot.x, rot.y, rot.z = -rot.z, -rot.y, -rot.x, rot.w
1166            #
1167            #co_mat = mathutils.Matrix.Translation(co)
1168            #rot_mat = rot.to_matrix().to_4x4()
1169            #mat = compat.mul(co_mat, rot_mat)
1170            #
1171            #copy_mat = mat.copy()
1172            #mat[0][0], mat[0][1], mat[0][2], mat[0][3] = copy_mat[0][0], copy_mat[1][0], copy_mat[2][0], copy_mat[3][0]
1173            #mat[1][0], mat[1][1], mat[1][2], mat[1][3] = copy_mat[0][1], copy_mat[1][1], copy_mat[2][1], copy_mat[3][1]
1174            #mat[2][0], mat[2][1], mat[2][2], mat[2][3] = copy_mat[0][2], copy_mat[1][2], copy_mat[2][2], copy_mat[3][2]
1175            #mat[3][0], mat[3][1], mat[3][2], mat[3][3] = copy_mat[0][3], copy_mat[1][3], copy_mat[2][3], copy_mat[3][3]
1176
1177            mat_array = []
1178            for vec in mat:
1179                mat_array.extend(vec[:])
1180            
1181            local_bone_data.append({
1182                'name': common.encode_bone_name(bone.name, self.is_convert_bone_weight_names),
1183                'matrix': mat_array,
1184            })
1185        return local_bone_data

アーマチュアを解析してBoneDataを返す

@staticmethod
def local_bone_data_parser(container):
1187    @staticmethod
1188    def local_bone_data_parser(container):
1189        """LocalBoneData テキストをパースして辞書を要素とするリストを返す"""
1190        local_bone_data = []
1191        for line in container:
1192            data = line.split(',')
1193            if len(data) != 2:
1194                continue
1195            local_bone_data.append({
1196                'name': data[0],
1197                'matrix': list(map(float, data[1].split())),
1198            })
1199        return local_bone_data

LocalBoneData テキストをパースして辞書を要素とするリストを返す

@staticmethod
def indexed_data_generator(container, prefix='', max_index=387420489, max_pass=50):
1201    @staticmethod
1202    def indexed_data_generator(container, prefix='', max_index=9**9, max_pass=50):
1203        """コンテナ内の数値インデックスをキーに持つ要素を昇順に返すジェネレーター"""
1204        pass_count = 0
1205        for i in range(max_index):
1206            name = prefix + str(i)
1207            if name not in container:
1208                pass_count += 1
1209                if max_pass < pass_count:
1210                    return
1211                continue
1212            yield container[name]

コンテナ内の数値インデックスをキーに持つ要素を昇順に返すジェネレーター

bl_rna = <bpy_struct, Struct("EXPORT_MESH_OT_export_cm3d2_model")>
Inherited Members
bpy_types.Operator
as_keywords
poll_message_set
builtins.bpy_struct
keys
values
get
pop
as_pointer
keyframe_insert
keyframe_delete
driver_add
driver_remove
is_property_set
property_unset
is_property_hidden
is_property_readonly
is_property_overridable_library
property_overridable_library_set
path_resolve
path_from_id
type_recast
bl_rna_get_subclass_py
bl_rna_get_subclass
id_properties_ensure
id_properties_clear
id_properties_ui
id_data