├── LICENSE ├── README.md ├── README_JP.md ├── image.jpg └── moderate_weight_reduction_tools.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 HoloLab Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moderate Weight Reduction Tools 2 | 3 | English | [Japanese](./README_JP.md) 4 | 5 | ## About 6 | 7 | This blender add-on generates a model from the original model with reduced polygon mesh and optimized textures. 8 | If you need more details about how to use this blender add-on, Please refer to this [Video](https://www.youtube.com/watch?v=8FJ84-j-Ijc) and [Article](https://blog.hololab.co.jp/entry/2024/11/22/120000). 9 | 10 | ![image](image.jpg) 11 | 12 | ## How To Install 13 | 14 | 1. Open preferences by push [Edit] > [Preferences] button from the top menu. 15 | 2. Open add-ons section by select [Add-ons] tab in the left column. 16 | 3. Open the file select dialog by push [Install from Disk] button from drop-down menu in top right. 17 | 4. Select the add-on file "moderate_weight_reduction_tools.py" in the file selection dialog. 18 | 5. If this add-on is not enabled, check the box next to "Moderate Weight Reduction Tools" in add-ons list. 19 | 20 | If you need more details, Please refer to this page about how to install add-on. 21 | https://docs.blender.org/manual/en/latest/editors/preferences/extensions.html 22 | 23 | ## How To Use 24 | 25 | 1. Select the model you want to reduction, and make a object active. 26 | 2. Open [HoloLab] tab in side panel of 3D Viewport. 27 | 3. Push [Start] button to output the model with reduced polygon mesh and optimized textures. 28 | 4. (option) To export the model as FBX format, push [Texture] button of "Export:" section to save the texture file. 29 | 5. (option) To export the model as USDZ format, push [Original Model] button of "Delete:" section to remove the unnecessary original model and tetextures from the project. 30 | 31 | ## Settings 32 | 33 | * Mesh Settigbs: 34 | * Mesh Integration ... If the mesh is split, overlapping vertices are joined. (Default is not apply.) 35 | * Rate of Polygon Left ... Ratio of polygon mesh left after reduction. (Default is 5%.) 36 | * Texture Settings: 37 | * Name ... Name of new texture. (Default is "texture".) 38 | * Resolution ... Resolution of new texture. (Default is 1024x1024.) 39 | 40 | ## Support Versions 41 | 42 | This add-on works with Blender 3.6, 4.0, 4.1, 4.2, and 4.3. 43 | 44 | ## License 45 | 46 | Copyright © 2024 [HoloLab Inc.](https://hololab.co.jp/) 47 | This add-on is distributed under the MIT license. 48 | 49 | ## Related Information 50 | 51 | * [Blog@HoloLabInc](https://blog.hololab.co.jp/entry/2024/11/22/120000) 52 | * [YouTube@HoloLabInc](https://www.youtube.com/watch?v=8FJ84-j-Ijc) 53 | -------------------------------------------------------------------------------- /README_JP.md: -------------------------------------------------------------------------------- 1 | # Moderate Weight Reduction Tools 2 | 3 | [English](./README.md) | Japanese 4 | 5 | ## 概要 6 | 7 | このBlenderアドオンはポリゴン削減とテクスチャ最適化によるモデルの軽量化を初心者でも手軽に使えるようにしたツールです。 8 | 詳細な使い方はこちらの[動画](https://www.youtube.com/watch?v=8FJ84-j-Ijc)や[記事](https://blog.hololab.co.jp/entry/2024/11/22/120000)で解説していますのでご参照ください。 9 | 10 | ![image](image.jpg) 11 | 12 | ## インストール方法 13 | 14 | 1. 上部メニューから[編集] > [プリファレンス]([Edit] > [Preferences])ボタンを押して設定画面を開きます。 15 | 2. 左カラムから[アドオン]([Add-ons])タブを選択してアドオンの設定画面を開きます。 16 | 3. 右上のドロップダウンから[ディスクからインストール]([Install from Disk])ボタンを押してファイル選択ダイアログを開きます。 17 | 4. ファイル選択ダイアログでアドオンのファイル「moderate_weight_reduction_tools.py」を選択、[ディスクからインストール]([Install from Disk])ボタンを押してアドオンをインストールします。 18 | 5. インストールされたアドオンが自動的に有効化されていない場合、アドオンのリストから「Moderate Weight Reduction Tools」のチェックボックスにチェックを入れて有効化します。 19 | 20 | アドオンのインストール方法についての詳細は以下のドキュメントをご参照ください。 21 | https://docs.blender.org/manual/ja/latest/editors/preferences/extensions.html 22 | 23 | ## 使用方法 24 | 25 | 1. 軽量化したいモデルのオブジェクトを選択してアクティブ状態にします。 26 | 2. 3Dビューポートのサイドパネルから[HoloLab]タブを開きます。 27 | 3. パラメータを設定して[Start]ボタンを押すと、自動的にポリゴンの削減やテクスチャの最適化が実行されます。 28 | 4. (オプション) モデルをFBXフォーマットで保存する場合、「Export:」セクションの[Texture]ボタンを押してテクスチャを保存してください。 29 | 5. (オプション) モデルをUSDZフォーマットで保存する場合、「Delete:」セクションの[Original Model]ボタンを押して不要な元のモデルやテクスチャのオブジェクトを削除してください。 30 | 31 | ## 設定 32 | 33 | * Mesh Settigbs: 34 | * Mesh Integration ... 大規模なポリゴン数のモデルや、スマホアプリで生成したスキャンモデルを処理する場合、重なり合った頂点の統合を適用します。(デフォルトは「無効」です。) 35 | * Rate of Polygon Left ... 元のモデルのポリゴンからどれくらいまで削減するかの割合です。0.1なら1/10のポリゴン数になります。(デフォルトは「5%」です。) 36 | * Texture Settings: 37 | * Name ... 最適化後のテクスチャの名前の指定します。(デフォルトは「texture」です。) 38 | * Resolution ... 最適化後のテクスチャの解像度を指定します。(デフォルトは「1024x1024」です。) 39 | 40 | ## サポートバージョン 41 | 42 | このアドオンはBlender 3.6、4.0、4.1、4.2、および4.3で動作します。 43 | 44 | ## ライセンス 45 | 46 | Copyright © 2024 [HoloLab Inc.](https://hololab.co.jp/) 47 | このアドオンはMITライセンスでライセンスされています。 48 | 49 | ## 関連情報 50 | 51 | * [Blog@HoloLabInc](https://blog.hololab.co.jp/entry/2024/11/22/120000) 52 | * [YouTube@HoloLabInc](https://www.youtube.com/watch?v=8FJ84-j-Ijc) 53 | -------------------------------------------------------------------------------- /image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HoloLabInc/ModerateWeightReductionTools/d9f08b2d6e4d73c9e1d2cabafb467768a1569739/image.jpg -------------------------------------------------------------------------------- /moderate_weight_reduction_tools.py: -------------------------------------------------------------------------------- 1 | # This Blender add-on is licensed under the MIT License. 2 | # 3 | # MIT License 4 | # 5 | # Copyright (c) 2024 HoloLab Inc. 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | import os 26 | import sys 27 | import math 28 | import bpy 29 | 30 | 31 | bl_info = { 32 | "name": "Moderate Weight Reduction Tools", 33 | "author": "HoloLab Inc.", 34 | "version": (1, 0, 0), 35 | "blender": (3, 6, 0), 36 | "location": "3D Viewport > Sidebar > HoloLab", 37 | "description": "The add-on generates a model from the original model with reduced polygon mesh and optimized textures.", 38 | "warning": "", 39 | "support": "COMMUNITY", 40 | "doc_url": "https://github.com/HoloLabInc/ModerateWeightReductionTools/blob/main/README.md", 41 | "tracker_url": "https://github.com/HoloLabInc/ModerateWeightReductionTools/issues", 42 | "category": "Tool" 43 | } 44 | 45 | class HOLOLAB_OT_ModerateWeightReduction(bpy.types.Operator): 46 | bl_idname = "hololab.moderate_weight_reduction" 47 | bl_label = "Moderate Weight Reduction Tools" 48 | bl_description = "Generates models with reduced polygon mesh and optimized textures from the original model." 49 | bl_options = {'REGISTER', 'UNDO'} 50 | 51 | remove_doubles: bpy.props.BoolProperty( 52 | name="remove_doubles", 53 | description="If the mesh is split, overlapping vertices are joined.", 54 | default=False 55 | ) 56 | 57 | decimate_rate: bpy.props.FloatProperty( 58 | name="decimate_rate", 59 | description="Ratio of polygon mesh left after reduction.", 60 | default=0.05, 61 | min=0.0, 62 | max=1.0 63 | ) 64 | 65 | texture_name: bpy.props.StringProperty( 66 | name="texture_name", 67 | description="Name of new texture.", 68 | default="texture" 69 | ) 70 | 71 | texture_resolution: bpy.props.EnumProperty( 72 | name="texture_resolution", 73 | description="Resolution of new texture.", 74 | default="1024", 75 | items=[ 76 | ("256", "256 x 256", "256 x 256"), 77 | ("512", "512 x 512", "512 x 512"), 78 | ("1024", "1024 x 1024", "1024 x 1024"), 79 | ("2048", "2048 x 2048", "2048 x 2048"), 80 | ("4096", "4096 x 4096", "4096 x 4096") 81 | ] 82 | ) 83 | 84 | def execute(self, context): 85 | self.report({'INFO'}, "execute auto decimation and bake function") 86 | 87 | self.report({'INFO'}, f"{self.remove_doubles=}") 88 | self.report({'INFO'}, f"{self.decimate_rate=}") 89 | self.report({'INFO'}, f"{self.texture_name=}") 90 | self.report({'INFO'}, f"{self.texture_resolution=}") 91 | 92 | previous_language = bpy.context.preferences.view.language 93 | bpy.context.preferences.view.language = 'en_US' 94 | 95 | try: 96 | self.clone_target_object() 97 | self.integration_polygon() 98 | self.reduction_polygon() 99 | self.set_material_and_texture() 100 | self.expand_uv() 101 | self.apply_auto_smooth() 102 | self.settings_bake_configurations() 103 | self.execute_bake() 104 | self.triangulate_faces() 105 | self.export_gltf() 106 | except Exception as e: 107 | self.report({'ERROR'}, f"{e}") 108 | return {'CANCELLED'} 109 | finally: 110 | bpy.context.preferences.view.language = previous_language 111 | 112 | return {'FINISHED'} 113 | 114 | # clone tartget object 115 | def clone_target_object(self): 116 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 117 | 118 | # select source object 119 | bpy.ops.object.select_all(action='DESELECT') 120 | object_source = bpy.context.active_object 121 | if object_source is None: 122 | self.report({'ERROR'}, "please turn active the source object in outliner or view port.") 123 | raise Exception("source object is not found.") 124 | object_source.select_set(True) 125 | bpy.context.view_layer.objects.active = object_source 126 | 127 | # rename source object 128 | object_source.name = "Original" 129 | 130 | # duplicate object 131 | bpy.ops.object.duplicate(linked=False) 132 | 133 | # rename target object 134 | object_target = bpy.context.active_object 135 | object_target.name = "Result" 136 | 137 | # integration polygon 138 | def integration_polygon(self): 139 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 140 | 141 | # select target object 142 | bpy.ops.object.select_all(action='DESELECT') 143 | object_target = bpy.data.objects.get("Result") 144 | object_target.select_set(True) 145 | bpy.context.view_layer.objects.active = object_target 146 | 147 | # apply remove doubles 148 | if self.remove_doubles: 149 | bpy.ops.object.mode_set(mode='EDIT') 150 | bpy.ops.mesh.select_all(action='SELECT') 151 | bpy.ops.mesh.remove_doubles(threshold=0.001) 152 | bpy.ops.object.mode_set(mode='OBJECT') 153 | 154 | # reduction polygon 155 | def reduction_polygon(self): 156 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 157 | 158 | # select target object 159 | bpy.ops.object.select_all(action='DESELECT') 160 | object_target = bpy.data.objects.get("Result") 161 | object_target.select_set(True) 162 | bpy.context.view_layer.objects.active = object_target 163 | 164 | # decimation 165 | decimate_modifier = object_target.modifiers.new(name="Decimate", type='DECIMATE') 166 | decimate_modifier.ratio = self.decimate_rate 167 | bpy.ops.object.modifier_apply(modifier=decimate_modifier.name) 168 | 169 | # settings material and texture 170 | def set_material_and_texture(self): 171 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 172 | 173 | # select source object 174 | bpy.ops.object.select_all(action='DESELECT') 175 | object_source = bpy.data.objects.get("Original") 176 | object_source.select_set(True) 177 | bpy.context.view_layer.objects.active = object_source 178 | 179 | # rename exist image texture with same name as specified name that are linked in source object material 180 | texture_name = self.texture_name if self.texture_name else "texture" 181 | for material in object_source.data.materials: 182 | principled_bsdf = [node for node in material.node_tree.nodes if node.bl_idname == 'ShaderNodeBsdfPrincipled'][0] 183 | if principled_bsdf.inputs['Base Color'].is_linked is False: 184 | continue 185 | image = principled_bsdf.inputs['Base Color'].links[0].from_socket.node.image 186 | if image.name == texture_name: 187 | image.name += ".original" 188 | 189 | # remove exist image textures that generated at last run  190 | exist_images = [image for image in bpy.data.images if image.name == texture_name] 191 | for exist_image in exist_images: 192 | bpy.data.images.remove(image=exist_image) 193 | 194 | # select target object 195 | bpy.ops.object.select_all(action='DESELECT') 196 | object_target = bpy.data.objects.get("Result") 197 | object_target.select_set(True) 198 | bpy.context.view_layer.objects.active = object_target 199 | 200 | # clear materials 201 | object_target.data.materials.clear() 202 | 203 | # add new material 204 | material = bpy.data.materials.new(name="Material") 205 | object_target.data.materials.append(material) 206 | 207 | # add image texture node 208 | material.use_nodes = True 209 | node_tree = material.node_tree 210 | image_texture = node_tree.nodes.new(type='ShaderNodeTexImage') 211 | image_texture.location = (-200, 0) 212 | 213 | # create image texture 214 | resolution = int(self.texture_resolution) 215 | image_texture.image = bpy.data.images.new(name=texture_name, width=resolution, height=resolution) 216 | 217 | # link color socket from image texture to principled bsdf 218 | principled_bsdf = [node for node in node_tree.nodes if node.bl_idname == 'ShaderNodeBsdfPrincipled'][0] 219 | node_tree.links.new(image_texture.outputs['Color'], principled_bsdf.inputs['Base Color']) 220 | 221 | # expand uv 222 | def expand_uv(self): 223 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 224 | 225 | # select target object 226 | bpy.ops.object.select_all(action='DESELECT') 227 | object_target = bpy.data.objects.get("Result") 228 | object_target.select_set(True) 229 | bpy.context.view_layer.objects.active = object_target 230 | 231 | # set smart uv project 232 | bpy.ops.object.mode_set(mode='EDIT') 233 | bpy.ops.mesh.select_all(action='SELECT') 234 | bpy.ops.uv.smart_project(angle_limit=0.349066, margin_method='FRACTION', rotate_method='AXIS_ALIGNED', island_margin=0.001, area_weight=0.0, correct_aspect=True, scale_to_bounds=False) 235 | bpy.ops.object.mode_set(mode='OBJECT') 236 | 237 | # apply auto smooth 238 | def apply_auto_smooth(self): 239 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 240 | 241 | # select target object 242 | bpy.ops.object.select_all(action='DESELECT') 243 | object_target = bpy.data.objects.get("Result") 244 | object_target.select_set(True) 245 | bpy.context.view_layer.objects.active = object_target 246 | 247 | # apply auto smooth 248 | if bpy.app.version < (4, 1, 0): 249 | bpy.context.object.data.use_auto_smooth = True 250 | bpy.context.object.data.auto_smooth_angle = math.radians(30) 251 | else: 252 | bpy.ops.object.shade_smooth_by_angle(angle=math.radians(30)) 253 | 254 | # settings bake configurations 255 | def settings_bake_configurations(self): 256 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 257 | 258 | # set render engine to cycles 259 | bpy.context.scene.render.engine = 'CYCLES' 260 | 261 | # disable pass direct and pass indirect 262 | bpy.context.scene.render.bake.use_pass_direct = False 263 | bpy.context.scene.render.bake.use_pass_indirect = False 264 | 265 | # enable selected to active 266 | bpy.context.scene.render.bake.use_selected_to_active = True 267 | 268 | # set cage extrusion to 0.1m 269 | bpy.context.scene.render.bake.cage_extrusion = 0.1 270 | 271 | # set margin type to extend 272 | bpy.context.scene.render.bake.margin_type = 'EXTEND' 273 | 274 | # set bake type to diffuse 275 | bpy.context.scene.cycles.bake_type = 'DIFFUSE' 276 | bpy.context.scene.render.bake.use_clear = True 277 | 278 | # execute bake 279 | def execute_bake(self): 280 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 281 | 282 | # deselect all object 283 | bpy.ops.object.select_all(action='DESELECT') 284 | 285 | # select source object 286 | object_source = bpy.data.objects.get("Original") 287 | object_source.select_set(True) 288 | 289 | # active tartget object 290 | object_target = bpy.data.objects.get("Result") 291 | bpy.context.view_layer.objects.active = object_target 292 | 293 | # store metallic settings 294 | metallic_socket = None 295 | metallic_value = 0.0 296 | for material in object_source.data.materials: 297 | principled_bsdf = [node for node in material.node_tree.nodes if node.bl_idname == 'ShaderNodeBsdfPrincipled'][0] 298 | if not ('Metallic' in principled_bsdf.inputs.keys()): 299 | continue 300 | if principled_bsdf.inputs['Metallic'].is_linked: 301 | metallic_socket = principled_bsdf.inputs['Metallic'].links[0].from_socket 302 | material.node_tree.links.remove(principled_bsdf.inputs['Metallic'].links[0]) 303 | else: 304 | metallic_value = principled_bsdf.inputs['Metallic'].default_value 305 | principled_bsdf.inputs['Metallic'].default_value = 0 306 | 307 | # execute bake 308 | bpy.ops.object.bake(type='DIFFUSE') 309 | 310 | # restore metallic settings 311 | for material in object_source.data.materials: 312 | principled_bsdf = [node for node in material.node_tree.nodes if node.bl_idname == 'ShaderNodeBsdfPrincipled'][0] 313 | if not ('Metallic' in principled_bsdf.inputs.keys()): 314 | continue 315 | if metallic_socket is not None: 316 | material.node_tree.links.new(metallic_socket, principled_bsdf.inputs['Metallic']) 317 | else: 318 | principled_bsdf.inputs['Metallic'].default_value = metallic_value 319 | 320 | def triangulate_faces(self): 321 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 322 | 323 | # select target object 324 | bpy.ops.object.select_all(action='DESELECT') 325 | object_target = bpy.data.objects.get("Result") 326 | object_target.select_set(True) 327 | bpy.context.view_layer.objects.active = object_target 328 | 329 | # traiangulate faces 330 | bpy.ops.object.mode_set(mode='EDIT') 331 | bpy.ops.mesh.select_all(action='DESELECT') 332 | bpy.ops.mesh.select_mode(type='FACE') 333 | bpy.ops.mesh.select_all(action='SELECT') 334 | bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY') 335 | bpy.ops.object.mode_set(mode='OBJECT') 336 | 337 | def export_gltf(self): 338 | # select target object 339 | bpy.ops.object.select_all(action='DESELECT') 340 | object_target = bpy.data.objects.get("Result") 341 | object_target.select_set(True) 342 | bpy.context.view_layer.objects.active = object_target 343 | 344 | # this is workaround for the issue that gltf 2.0 does not export correctly. 345 | temp_file = os.path.join(bpy.app.tempdir, "temp.glb") 346 | bpy.ops.export_scene.gltf(filepath=temp_file, use_selection=True) 347 | 348 | class HOLOLAB_OT_SaveBakedTexture(bpy.types.Operator): 349 | bl_idname = "hololab.save_baked_texture" 350 | bl_label = "Save Baked Texture" 351 | bl_description = "Save baked texture to PNG file for export the model as FBX file." 352 | bl_options = {'REGISTER', 'UNDO'} 353 | 354 | directory: bpy.props.StringProperty( 355 | name="directory_path", 356 | default="", 357 | maxlen=1024, 358 | subtype='DIR_PATH', 359 | description="", 360 | ) 361 | 362 | filter_folder: bpy.props.BoolProperty( 363 | default=True, 364 | options={'HIDDEN'}, 365 | ) 366 | 367 | def invoke(self, context, event): 368 | context.window_manager.fileselect_add(self) 369 | return {'RUNNING_MODAL'} 370 | 371 | def execute(self, context): 372 | self.report({'INFO'}, "execute save baked texture function") 373 | 374 | previous_language = bpy.context.preferences.view.language 375 | bpy.context.preferences.view.language = 'en_US' 376 | 377 | try: 378 | self.save_texture() 379 | except Exception as e: 380 | self.report({'ERROR'}, f"{e}") 381 | return {'CANCELLED'} 382 | finally: 383 | bpy.context.preferences.view.language = previous_language 384 | 385 | return {'FINISHED'} 386 | 387 | # save texture 388 | def save_texture(self): 389 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 390 | 391 | # select target object 392 | bpy.ops.object.select_all(action='DESELECT') 393 | object_target = bpy.data.objects.get("Result") 394 | if object_target is None: 395 | return 396 | 397 | # get baked texture name 398 | texture_name = "" 399 | for material in object_target.data.materials: 400 | principled_bsdf = [node for node in material.node_tree.nodes if node.bl_idname == 'ShaderNodeBsdfPrincipled'][0] 401 | if principled_bsdf.inputs['Base Color'].is_linked is False: 402 | continue 403 | texture_name = principled_bsdf.inputs['Base Color'].links[0].from_socket.node.image.name 404 | if not texture_name: 405 | return 406 | 407 | # get baked texture 408 | texture = bpy.data.images[texture_name] 409 | if texture is None: 410 | return 411 | 412 | # save baked texture 413 | for area in bpy.context.screen.areas: 414 | if area.type == 'VIEW_3D': 415 | area.type = 'IMAGE_EDITOR' 416 | for space in area.spaces: 417 | if space.type != 'IMAGE_EDITOR': 418 | continue 419 | space.image = texture 420 | filepath = os.path.join(self.directory, f"{texture_name}.png") 421 | self.report({'INFO'}, f"{filepath}") 422 | bpy.ops.image.save_as(filepath=filepath, relative_path=True) 423 | break 424 | area.type = 'VIEW_3D' 425 | 426 | class HOLOLAB_OT_DeleteOriginal(bpy.types.Operator): 427 | bl_idname = "hololab.delete_original" 428 | bl_label = "Delete Original" 429 | bl_description = "Delete unnecessary original object and tetextures from project for export the model as USDZ file." 430 | bl_options = {'REGISTER', 'UNDO'} 431 | 432 | def execute(self, context): 433 | self.report({'INFO'}, "execute delete Original object and texture function") 434 | 435 | previous_language = bpy.context.preferences.view.language 436 | bpy.context.preferences.view.language = 'en_US' 437 | 438 | try: 439 | self.delete_texture() 440 | self.delete_object() 441 | except Exception as e: 442 | self.report({'ERROR'}, f"{e}") 443 | return {'CANCELLED'} 444 | finally: 445 | bpy.context.preferences.view.language = previous_language 446 | 447 | return {'FINISHED'} 448 | 449 | # delete texture 450 | def delete_texture(self): 451 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 452 | 453 | # select target object 454 | bpy.ops.object.select_all(action='DESELECT') 455 | object_target = bpy.data.objects.get("Result") 456 | if object_target is None: 457 | return 458 | 459 | # get baked texture name 460 | texture_name = "" 461 | for material in object_target.data.materials: 462 | principled_bsdf = [node for node in material.node_tree.nodes if node.bl_idname == 'ShaderNodeBsdfPrincipled'][0] 463 | if principled_bsdf.inputs['Base Color'].is_linked is False: 464 | continue 465 | texture_name = principled_bsdf.inputs['Base Color'].links[0].from_socket.node.image.name 466 | if not texture_name: 467 | return 468 | 469 | # delete all textures without specified texture 470 | images = [image for image in bpy.data.images if image.name != texture_name] 471 | for image in images: 472 | bpy.data.images.remove(image=image) 473 | 474 | # delete object 475 | def delete_object(self): 476 | self.report({'INFO'}, f"{sys._getframe().f_code.co_name}") 477 | 478 | # select target object 479 | bpy.ops.object.select_all(action='DESELECT') 480 | object_source = bpy.data.objects.get("Original") 481 | if object_source is None: 482 | return 483 | 484 | # delete object 485 | with bpy.context.temp_override(selected_objects=[object_source]): 486 | bpy.ops.object.delete() 487 | 488 | class HOLOLAB_PT_SideBar(bpy.types.Panel): 489 | bl_idname = "HOLOLAB_PT_SideBar" 490 | bl_label = "HoloLab" 491 | bl_space_type = "VIEW_3D" 492 | bl_region_type = "UI" 493 | bl_category = "HoloLab" 494 | 495 | def draw_header(self, context): 496 | layout = self.layout 497 | layout.label(text="Moderate Weight Reduction Tools", icon='PLUGIN') 498 | 499 | def draw(self, context): 500 | layout = self.layout 501 | scene = context.scene 502 | 503 | box = layout.box() 504 | box.label(text="Mesh Setting:") 505 | box.prop(scene, "remove_doubles") 506 | box.prop(scene, "decimate_rate") 507 | 508 | box = layout.box() 509 | box.label(text="Texture Setting:") 510 | box.prop(scene, "texture_name") 511 | box.prop(scene, "texture_resolution") 512 | 513 | op = layout.operator(HOLOLAB_OT_ModerateWeightReduction.bl_idname, text='Start', icon='PLAY') 514 | op.remove_doubles = scene.remove_doubles 515 | op.decimate_rate = scene.decimate_rate 516 | op.texture_name = scene.texture_name 517 | op.texture_resolution = scene.texture_resolution 518 | 519 | layout.separator() 520 | layout.label(text="Export:") 521 | 522 | op = layout.operator(HOLOLAB_OT_SaveBakedTexture.bl_idname, text='Texture', icon='FILE_TICK') 523 | 524 | layout.label(text="Delete:") 525 | 526 | op = layout.operator(HOLOLAB_OT_DeleteOriginal.bl_idname, text='Original Model', icon='TRASH') 527 | 528 | def menu_draw(cls, context): 529 | cls.layout.menu(HOLOLAB_PT_SideBar.bl_idname) 530 | 531 | def register_properies(): 532 | scene = bpy.types.Scene 533 | 534 | scene.remove_doubles = bpy.props.BoolProperty( 535 | name="Mesh Integration", 536 | description="If the mesh is split, overlapping vertices are joined.", 537 | default=False 538 | ) 539 | 540 | scene.decimate_rate = bpy.props.FloatProperty( 541 | name="Rate of Polygon Left", 542 | description="Ratio of polygon mesh left after reduction.", 543 | default=0.05, 544 | min=0.0, 545 | max=1.0 546 | ) 547 | 548 | scene.texture_name = bpy.props.StringProperty( 549 | name="Name", 550 | description="Name of new texture.", 551 | default="texture" 552 | ) 553 | 554 | scene.texture_resolution = bpy.props.EnumProperty( 555 | name="Resolution", 556 | description="Resolution of new texture.", 557 | default="1024", 558 | items=[ 559 | ("256", "256 x 256", "256 x 256"), 560 | ("512", "512 x 512", "512 x 512"), 561 | ("1024", "1024 x 1024", "1024 x 1024"), 562 | ("2048", "2048 x 2048", "2048 x 2048"), 563 | ("4096", "4096 x 4096", "4096 x 4096") 564 | ] 565 | ) 566 | 567 | def unregister_properies(): 568 | scene = bpy.types.Scene 569 | 570 | del scene.remove_doubles 571 | del scene.decimate_rate 572 | del scene.texture_name 573 | del scene.texture_resolution 574 | 575 | classes = [ 576 | HOLOLAB_OT_ModerateWeightReduction, 577 | HOLOLAB_OT_SaveBakedTexture, 578 | HOLOLAB_OT_DeleteOriginal, 579 | HOLOLAB_PT_SideBar 580 | ] 581 | 582 | def register(): 583 | for c in classes: 584 | bpy.utils.register_class(c) 585 | register_properies() 586 | 587 | def unregister(): 588 | unregister_properies() 589 | for c in reversed(classes): 590 | bpy.utils.unregister_class(c) 591 | 592 | if __name__ == "__main__": 593 | register() 594 | --------------------------------------------------------------------------------