diff --git a/BlenderScripts/io_scene_mmobj/__init__.py b/BlenderScripts/io_scene_mmobj/__init__.py index 193070c..49eb968 100644 --- a/BlenderScripts/io_scene_mmobj/__init__.py +++ b/BlenderScripts/io_scene_mmobj/__init__.py @@ -1,35 +1,17 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# +# SPDX-License-Identifier: GPL-2.0-or-later bl_info = { "name": "ModelMod MMOBJ format", "author": "Campbell Barton, Bastien Montagne, John Quigley (ModelMod extensions only)", "version": (2, 0, 0), - "blender": (2, 58, 0), + "blender": (3, 0, 0), "location": "File > Import-Export", - "description": "Import-Export MMOBJ", + "description": "Import-Export MMOBJ, Import MMOBJ mesh, UVs, materials and textures", "warning": "", - "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" - "Scripts/Import-Export/Wavefront_OBJ", + "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/scene_obj.html", "support": 'COMMUNITY', - "category": "Import-Export"} + "category": "Import-Export", +} if "bpy" in locals(): import importlib @@ -40,18 +22,22 @@ import bpy -from bpy.props import (BoolProperty, - FloatProperty, - StringProperty, - EnumProperty, - ) -from bpy_extras.io_utils import (ImportHelper, - ExportHelper, - path_reference_mode, - axis_conversion, - ) - - +from bpy.props import ( + BoolProperty, + FloatProperty, + StringProperty, + EnumProperty, +) +from bpy_extras.io_utils import ( + ImportHelper, + ExportHelper, + orientation_helper, + path_reference_mode, + axis_conversion, +) + + +@orientation_helper(axis_forward='-Z', axis_up='Y') class ImportOBJ(bpy.types.Operator, ImportHelper): """Load a ModelMod MMOBJ File""" bl_idname = "import_scene.mmobj" @@ -59,92 +45,64 @@ class ImportOBJ(bpy.types.Operator, ImportHelper): bl_options = {'PRESET', 'UNDO'} filename_ext = ".mmobj" - filter_glob = StringProperty( - default="*.mmobj;", - options={'HIDDEN'}, - ) - - use_ngons = BoolProperty( - name="NGons", - description="Import faces with more than 4 verts as ngons", - default=True, - ) - use_edges = BoolProperty( - name="Lines", - description="Import lines and faces with 2 verts as edge", - default=True, - ) - use_smooth_groups = BoolProperty( - name="Smooth Groups", - description="Surround smooth groups by sharp edges", - default=True, - ) + filter_glob: StringProperty( + default="*.mmobj;*.mtl", + options={'HIDDEN'}, + ) + + use_edges: BoolProperty( + name="Lines", + description="Import lines and faces with 2 verts as edge", + default=True, + ) + use_smooth_groups: BoolProperty( + name="Smooth Groups", + description="Surround smooth groups by sharp edges", + default=True, + ) # MMObj: splitting is always disabled because it split_mesh doesn't properly handle the # weight groups stuff that I added # (and snapshot never generates more than one OBJ object and it doesn't generate groups) - use_split_objects = BoolProperty( - name="Object", - description="Import OBJ Objects into Blender Objects", - default=False, - ) - use_split_groups = BoolProperty( - name="Group", - description="Import OBJ Groups into Blender Objects", - default=False, - ) - - use_groups_as_vgroups = BoolProperty( - name="Poly Groups", - description="Import OBJ groups as vertex groups", - default=False, - ) - - use_image_search = BoolProperty( - name="Image Search", - description="Search subdirs for any associated images " - "(Warning, may be slow)", - default=True, - ) - - split_mode = EnumProperty( - name="Split", - items=( - ('OFF', "Keep Vert Order", "Keep vertex order from file"), # MMObj: reorder because we want this to be default (never split) - ('ON', "Split", "Split geometry, omits unused verts"), - ), - ) - - global_clamp_size = FloatProperty( - name="Clamp Size", - description="Clamp bounds under this value (zero to disable)", - min=0.0, max=1000.0, - soft_min=0.0, soft_max=1000.0, - default=0.0, - ) - axis_forward = EnumProperty( - name="Forward", - items=(('X', "X Forward", ""), - ('Y', "Y Forward", ""), - ('Z', "Z Forward", ""), - ('-X', "-X Forward", ""), - ('-Y', "-Y Forward", ""), - ('-Z', "-Z Forward", ""), - ), - default='-Z', - ) - - axis_up = EnumProperty( - name="Up", - items=(('X', "X Up", ""), - ('Y', "Y Up", ""), - ('Z', "Z Up", ""), - ('-X', "-X Up", ""), - ('-Y', "-Y Up", ""), - ('-Z', "-Z Up", ""), - ), - default='Y', - ) + use_split_objects: BoolProperty( + name="Object", + description="Import OBJ Objects into Blender Objects", + default=False, + ) + use_split_groups: BoolProperty( + name="Group", + description="Import OBJ Groups into Blender Objects", + default=False, + ) + + use_groups_as_vgroups: BoolProperty( + name="Poly Groups", + description="Import OBJ groups as vertex groups", + default=False, + ) + + use_image_search: BoolProperty( + name="Image Search", + description="Search subdirs for any associated images " + "(Warning, may be slow)", + default=True, + ) + + split_mode: EnumProperty( + name="Split", + items=( + ('OFF', "Keep Vert Order", "Keep vertex order from file"), # MMObj: reorder because we want this to be default (never split) + ('ON', "Split", "Split geometry, omits vertices unused by edges or faces"), + ), + ) + + global_clamp_size: FloatProperty( + name="Clamp Size", + description="Clamp bounds under this value (zero to disable)", + min=0.0, max=1000.0, + soft_min=0.0, soft_max=1000.0, + default=0.0, + ) def execute(self, context): # print("Selected: " + context.active_object.name) @@ -156,53 +114,117 @@ def execute(self, context): else: self.use_groups_as_vgroups = False - keywords = self.as_keywords(ignore=("axis_forward", - "axis_up", - "filter_glob", - "split_mode", - )) - - global_matrix = axis_conversion(from_forward=self.axis_forward, - from_up=self.axis_up, - ).to_4x4() + keywords = self.as_keywords( + ignore=( + "axis_forward", + "axis_up", + "filter_glob", + "split_mode", + ), + ) + + global_matrix = axis_conversion( + from_forward=self.axis_forward, + from_up=self.axis_up, + ).to_4x4() keywords["global_matrix"] = global_matrix - if bpy.data.is_saved and context.user_preferences.filepaths.use_relative_paths: + if bpy.data.is_saved and context.preferences.filepaths.use_relative_paths: import os - keywords["relpath"] = os.path.dirname((bpy.data.path_resolve("filepath", False).as_bytes())) + keywords["relpath"] = os.path.dirname(bpy.data.filepath) - return import_mmobj.load(self, context, **keywords) + return import_mmobj.load(context, **keywords) + + def draw(self, context): + pass + + +# class OBJ_PT_import_include(bpy.types.Panel): + # bl_space_type = 'FILE_BROWSER' + # bl_region_type = 'TOOL_PROPS' + # bl_label = "Include" + # bl_parent_id = "FILE_PT_operator" + + # @classmethod + # def poll(cls, context): + # sfile = context.space_data + # operator = sfile.active_operator + + # return operator.bl_idname == "IMPORT_SCENE_OT_obj" + + # def draw(self, context): + # layout = self.layout + # layout.use_property_split = True + # layout.use_property_decorate = False # No animation. + + # sfile = context.space_data + # operator = sfile.active_operator + + # layout.prop(operator, 'use_image_search') + # layout.prop(operator, 'use_smooth_groups') + # layout.prop(operator, 'use_edges') + + +class OBJ_PT_import_transform(bpy.types.Panel): + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'TOOL_PROPS' + bl_label = "Transform" + bl_parent_id = "FILE_PT_operator" + + @classmethod + def poll(cls, context): + sfile = context.space_data + operator = sfile.active_operator + + return operator.bl_idname == "IMPORT_SCENE_OT_mmobj" def draw(self, context): layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + sfile = context.space_data + operator = sfile.active_operator + + layout.prop(operator, "global_clamp_size") + layout.prop(operator, "axis_forward") + layout.prop(operator, "axis_up") + - row = layout.row(align=True) - row.prop(self, "use_ngons") - row.prop(self, "use_edges") +# class OBJ_PT_import_geometry(bpy.types.Panel): + # bl_space_type = 'FILE_BROWSER' + # bl_region_type = 'TOOL_PROPS' + # bl_label = "Geometry" + # bl_parent_id = "FILE_PT_operator" + # bl_options = {'DEFAULT_CLOSED'} - layout.prop(self, "use_smooth_groups") + # @classmethod + # def poll(cls, context): + # sfile = context.space_data + # operator = sfile.active_operator - # MMObj: don't show the split UI since it can mess up import - #box = layout.box() - #row = box.row() - #row.prop(self, "split_mode", expand=True) + # return operator.bl_idname == "IMPORT_SCENE_OT_obj" - #row = box.row() - #if self.split_mode == 'ON': - # row.label(text="Split by:") - # row.prop(self, "use_split_objects") - # row.prop(self, "use_split_groups") - #else: - # row.prop(self, "use_groups_as_vgroups") + # def draw(self, context): + # layout = self.layout - row = layout.split(percentage=0.67) - row.prop(self, "global_clamp_size") - layout.prop(self, "axis_forward") - layout.prop(self, "axis_up") + # sfile = context.space_data + # operator = sfile.active_operator - layout.prop(self, "use_image_search") + # layout.row().prop(operator, "split_mode", expand=True) + # layout.use_property_split = True + # layout.use_property_decorate = False # No animation. + # col = layout.column() + # if operator.split_mode == 'ON': + # col.prop(operator, "use_split_objects", text="Split by Object") + # col.prop(operator, "use_split_groups", text="Split by Group") + # else: + # col.prop(operator, "use_groups_as_vgroups") + + +@orientation_helper(axis_forward='-Z', axis_up='Y') class ExportOBJ(bpy.types.Operator, ExportHelper): """Save a ModelMod MMOBJ File""" @@ -211,140 +233,111 @@ class ExportOBJ(bpy.types.Operator, ExportHelper): bl_options = {'PRESET'} filename_ext = ".mmobj" - filter_glob = StringProperty( - default="*.mmobj;", - options={'HIDDEN'}, - ) + filter_glob: StringProperty( + default="*.mmobj;*.mtl", + options={'HIDDEN'}, + ) # context group - use_selection = BoolProperty( - name="Selection Only", - description="Export selected objects only", - default=False, - ) - # use_animation = BoolProperty( - # name="Animation", - # description="Write out an OBJ for each frame", - # default=False, - # ) + use_selection: BoolProperty( + name="Selection Only", + description="Export selected objects only", + default=True, + ) + # use_animation: BoolProperty( + # name="Animation", + # description="Write out an OBJ for each frame", + # default=False, + # ) # object group - use_mesh_modifiers = BoolProperty( - name="Apply Modifiers", - description="Apply modifiers (preview resolution)", - default=True, - ) - + use_mesh_modifiers: BoolProperty( + name="Apply Modifiers", + description="Apply modifiers", + default=True, + ) # extra data group - + # MMOBJ ignores these: -# use_edges = BoolProperty( -# name="Include Edges", -# description="", -# default=True, -# ) - # use_smooth_groups = BoolProperty( - # name="Smooth Groups", - # description="Write sharp edges as smooth groups", - # default=False, - # ) - # use_smooth_groups_bitflags = BoolProperty( - # name="Bitflag Smooth Groups", - # description="Same as 'Smooth Groups', but generate smooth groups IDs as bitflags " - # "(produces at most 32 different smooth groups, usually much less)", - # default=False, - # ) - - # MMOBJ requires these to be enabled - # use_normals = BoolProperty( - # name="Write Normals", - # description="Export one normal per vertex and per face, to represent flat faces and sharp edges", - # default=True, - # ) - # use_uvs = BoolProperty( - # name="Include UVs", - # description="Write out the active UV coordinates", - # default=True, - # ) - - # Currently MMOBJ doesn't do anything with this on load. Ultimately however we might want to support this, to support - # texture swapping. - use_materials = BoolProperty( - name="Write Materials", - description="Write out the MTL file", - default=False, - ) - + # use_edges: BoolProperty( + # name="Include Edges", + # description="", + # default=True, + # ) + # use_smooth_groups: BoolProperty( + # name="Smooth Groups", + # description="Write sharp edges as smooth groups", + # default=False, + # ) + # use_smooth_groups_bitflags: BoolProperty( + # name="Bitflag Smooth Groups", + # description="Same as 'Smooth Groups', but generate smooth groups IDs as bitflags " + # "(produces at most 32 different smooth groups, usually much less)", + # default=False, + # ) + # use_normals: BoolProperty( + # name="Write Normals", + # description="Export one normal per vertex and per face, to represent flat faces and sharp edges", + # default=True, + # ) + # use_uvs: BoolProperty( + # name="Include UVs", + # description="Write out the active UV coordinates", + # default=True, + # ) + use_materials: BoolProperty( + name="Write Materials", + description="Write out the MTL file", + default=False, + ) # MMOBJ requires triangles and doesn't understand the other stuff - # use_triangles = BoolProperty( - # name="Triangulate Faces", - # description="Convert all faces to triangles", - # default=True, - # ) - # use_nurbs = BoolProperty( - # name="Write Nurbs", - # description="Write nurbs curves as OBJ nurbs rather than " - # "converting to geometry", - # default=False, - # ) - # use_vertex_groups = BoolProperty( - # name="Polygroups", - # description="", - # default=False, - # ) + # use_triangles: BoolProperty( + # name="Triangulate Faces", + # description="Convert all faces to triangles", + # default=False, + # ) + # use_nurbs: BoolProperty( + # name="Write Nurbs", + # description="Write nurbs curves as OBJ nurbs rather than " + # "converting to geometry", + # default=False, + # ) + # use_vertex_groups: BoolProperty( + # name="Polygroups", + # description="", + # default=False, + # ) # grouping group - # use_blen_objects = BoolProperty( - # name="Objects as OBJ Objects", - # description="", - # default=True, - # ) - # group_by_object = BoolProperty( - # name="Objects as OBJ Groups ", - # description="", - # default=False, - # ) - # group_by_material = BoolProperty( - # name="Material Groups", - # description="", - # default=False, - # ) - # keep_vertex_order = BoolProperty( - # name="Keep Vertex Order", - # description="", - # default=True, - # ) - - axis_forward = EnumProperty( - name="Forward", - items=(('X', "X Forward", ""), - ('Y', "Y Forward", ""), - ('Z', "Z Forward", ""), - ('-X', "-X Forward", ""), - ('-Y', "-Y Forward", ""), - ('-Z', "-Z Forward", ""), - ), - default='-Z', - ) - axis_up = EnumProperty( - name="Up", - items=(('X', "X Up", ""), - ('Y', "Y Up", ""), - ('Z', "Z Up", ""), - ('-X', "-X Up", ""), - ('-Y', "-Y Up", ""), - ('-Z', "-Z Up", ""), - ), - default='Y', - ) - global_scale = FloatProperty( - name="Scale", - min=0.01, max=1000.0, - default=1.0, - ) - - path_mode = path_reference_mode + # use_blen_objects: BoolProperty( + # name="OBJ Objects", + # description="Export Blender objects as OBJ objects", + # default=True, + # ) + # group_by_object: BoolProperty( + # name="OBJ Groups", + # description="Export Blender objects as OBJ groups", + # default=False, + # ) + # group_by_material: BoolProperty( + # name="Material Groups", + # description="Generate an OBJ group for each part of a geometry using a different material", + # default=False, + # ) + # keep_vertex_order: BoolProperty( + # name="Keep Vertex Order", + # description="", + # default=False, + # ) + + global_scale: FloatProperty( + name="Scale", + min=0.01, max=1000.0, + default=1.0, + ) + + path_mode: path_reference_mode check_extension = True @@ -352,20 +345,125 @@ def execute(self, context): from . import export_mmobj from mathutils import Matrix - keywords = self.as_keywords(ignore=("axis_forward", - "axis_up", - "global_scale", - "check_existing", - "filter_glob", - )) - - global_matrix = (Matrix.Scale(self.global_scale, 4) * - axis_conversion(to_forward=self.axis_forward, - to_up=self.axis_up, - ).to_4x4()) - + keywords = self.as_keywords( + ignore=( + "axis_forward", + "axis_up", + "global_scale", + "check_existing", + "filter_glob", + ), + ) + + global_matrix = ( + Matrix.Scale(self.global_scale, 4) @ + axis_conversion( + to_forward=self.axis_forward, + to_up=self.axis_up, + ).to_4x4() + ) + + # Fros comment: Axis conversion is handled by other classes keywords["global_matrix"] = global_matrix - return export_mmobj.save(self, context, **keywords) + return export_mmobj.save(context, **keywords) + + def draw(self, context): + pass + + +# class OBJ_PT_export_include(bpy.types.Panel): + # bl_space_type = 'FILE_BROWSER' + # bl_region_type = 'TOOL_PROPS' + # bl_label = "Include" + # bl_parent_id = "FILE_PT_operator" + + # @classmethod + # def poll(cls, context): + # sfile = context.space_data + # operator = sfile.active_operator + + # return operator.bl_idname == "EXPORT_SCENE_OT_obj" + + # def draw(self, context): + # layout = self.layout + # layout.use_property_split = True + # layout.use_property_decorate = False # No animation. + + # sfile = context.space_data + # operator = sfile.active_operator + + # col = layout.column(heading="Limit to") + # col.prop(operator, 'use_selection') + + # col = layout.column(heading="Objects as", align=True) + # col.prop(operator, 'use_blen_objects') + # col.prop(operator, 'group_by_object') + # col.prop(operator, 'group_by_material') + + # layout.separator() + + # layout.prop(operator, 'use_animation') + + +class OBJ_PT_export_transform(bpy.types.Panel): + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'TOOL_PROPS' + bl_label = "Transform" + bl_parent_id = "FILE_PT_operator" + + @classmethod + def poll(cls, context): + sfile = context.space_data + operator = sfile.active_operator + + return operator.bl_idname == "EXPORT_SCENE_OT_mmobj" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + sfile = context.space_data + operator = sfile.active_operator + + layout.prop(operator, 'global_scale') + layout.prop(operator, 'path_mode') + layout.prop(operator, 'axis_forward') + layout.prop(operator, 'axis_up') + + +# class OBJ_PT_export_geometry(bpy.types.Panel): + # bl_space_type = 'FILE_BROWSER' + # bl_region_type = 'TOOL_PROPS' + # bl_label = "Geometry" + # bl_parent_id = "FILE_PT_operator" + # bl_options = {'DEFAULT_CLOSED'} + + # @classmethod + # def poll(cls, context): + # sfile = context.space_data + # operator = sfile.active_operator + + # return operator.bl_idname == "EXPORT_SCENE_OT_obj" + + # def draw(self, context): + # layout = self.layout + # layout.use_property_split = True + # layout.use_property_decorate = False # No animation. + + # sfile = context.space_data + # operator = sfile.active_operator + + # layout.prop(operator, 'use_mesh_modifiers') + # layout.prop(operator, 'use_smooth_groups') + # layout.prop(operator, 'use_smooth_groups_bitflags') + # layout.prop(operator, 'use_normals') + # layout.prop(operator, 'use_uvs') + # layout.prop(operator, 'use_materials') + # layout.prop(operator, 'use_triangles') + # layout.prop(operator, 'use_nurbs', text="Curves as NURBS") + # layout.prop(operator, 'use_vertex_groups') + # layout.prop(operator, 'keep_vertex_order') def menu_func_import(self, context): @@ -376,18 +474,33 @@ def menu_func_export(self, context): self.layout.operator(ExportOBJ.bl_idname, text="ModelMod obj (.mmobj)") +classes = ( + ImportOBJ, + # OBJ_PT_import_include, + OBJ_PT_import_transform, + # OBJ_PT_import_geometry, + ExportOBJ, + # OBJ_PT_export_include, + OBJ_PT_export_transform, + # OBJ_PT_export_geometry, +) + + def register(): - bpy.utils.register_module(__name__) + for cls in classes: + bpy.utils.register_class(cls) - bpy.types.INFO_MT_file_import.append(menu_func_import) - bpy.types.INFO_MT_file_export.append(menu_func_export) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) def unregister(): - bpy.utils.unregister_module(__name__) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + + for cls in classes: + bpy.utils.unregister_class(cls) - bpy.types.INFO_MT_file_import.remove(menu_func_import) - bpy.types.INFO_MT_file_export.remove(menu_func_export) if __name__ == "__main__": register() diff --git a/BlenderScripts/io_scene_mmobj/export_mmobj.py b/BlenderScripts/io_scene_mmobj/export_mmobj.py index 6e88a61..ebaa9ae 100644 --- a/BlenderScripts/io_scene_mmobj/export_mmobj.py +++ b/BlenderScripts/io_scene_mmobj/export_mmobj.py @@ -1,29 +1,15 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# +# SPDX-License-Identifier: GPL-2.0-or-later import os -import time import bpy -import mathutils -import bpy_extras.io_utils +from mathutils import Matrix, Vector, Color +from bpy_extras import io_utils, node_shader_utils + +from bpy_extras.wm_utils.progress_report import ( + ProgressReport, + ProgressReportSubstep, +) def name_compat(name): @@ -43,126 +29,115 @@ def mesh_triangulate(me): def write_mtl(scene, filepath, path_mode, copy_set, mtl_dict): - from mathutils import Color - - world = scene.world - if world: - world_amb = world.ambient_color - else: - world_amb = Color((0.0, 0.0, 0.0)) - source_dir = os.path.dirname(bpy.data.filepath) dest_dir = os.path.dirname(filepath) - file = open(filepath, "w", encoding="utf8", newline="\n") - fw = file.write + with open(filepath, "w", encoding="utf8", newline="\n") as f: + fw = f.write - fw('# Blender MTL File: %r\n' % (os.path.basename(bpy.data.filepath) or "None")) - fw('# Material Count: %i\n' % len(mtl_dict)) + fw('# Blender MTL File: %r\n' % (os.path.basename(bpy.data.filepath) or "None")) + fw('# Material Count: %i\n' % len(mtl_dict)) - mtl_dict_values = list(mtl_dict.values()) - mtl_dict_values.sort(key=lambda m: m[0]) + mtl_dict_values = list(mtl_dict.values()) + mtl_dict_values.sort(key=lambda m: m[0]) - # Write material/image combinations we have used. - # Using mtl_dict.values() directly gives un-predictable order. - for mtl_mat_name, mat, face_img in mtl_dict_values: + # Write material/image combinations we have used. + # Using mtl_dict.values() directly gives un-predictable order. + for mtl_mat_name, mat in mtl_dict_values: + # Get the Blender data for the material and the image. + # Having an image named None will make a bug, dont do it :) - # Get the Blender data for the material and the image. - # Having an image named None will make a bug, dont do it :) + fw('\nnewmtl %s\n' % mtl_mat_name) # Define a new material: matname_imgname - fw('\nnewmtl %s\n' % mtl_mat_name) # Define a new material: matname_imgname + mat_wrap = node_shader_utils.PrincipledBSDFWrapper(mat) if mat else None - if mat: - # convert from blenders spec to 0 - 1000 range. - if mat.specular_shader == 'WARDISO': - tspec = (0.4 - mat.specular_slope) / 0.0004 - else: - tspec = (mat.specular_hardness - 1) * 1.9607843137254901 - fw('Ns %.6f\n' % tspec) - del tspec - - fw('Ka %.6f %.6f %.6f\n' % (mat.ambient * world_amb)[:]) # Ambient, uses mirror color, - fw('Kd %.6f %.6f %.6f\n' % (mat.diffuse_intensity * mat.diffuse_color)[:]) # Diffuse - fw('Ks %.6f %.6f %.6f\n' % (mat.specular_intensity * mat.specular_color)[:]) # Specular - if hasattr(mat, "raytrace_transparency") and hasattr(mat.raytrace_transparency, "ior"): - fw('Ni %.6f\n' % mat.raytrace_transparency.ior) # Refraction index - else: - fw('Ni %.6f\n' % 1.0) - fw('d %.6f\n' % mat.alpha) # Alpha (obj uses 'd' for dissolve) - - # 0 to disable lighting, 1 for ambient & diffuse only (specular color set to black), 2 for full lighting. - if mat.use_shadeless: - fw('illum 0\n') # ignore lighting - elif mat.specular_intensity == 0: - fw('illum 1\n') # no specular. - else: - fw('illum 2\n') # light normaly + if mat_wrap: + use_mirror = mat_wrap.metallic != 0.0 + use_transparency = mat_wrap.alpha != 1.0 + + # XXX Totally empirical conversion, trying to adapt it + # (from 1.0 - 0.0 Principled BSDF range to 0.0 - 1000.0 OBJ specular exponent range): + # (1.0 - bsdf_roughness)^2 * 1000 + spec = (1.0 - mat_wrap.roughness) + spec *= spec * 1000 + fw('Ns %.6f\n' % spec) + + # Ambient + if use_mirror: + fw('Ka %.6f %.6f %.6f\n' % (mat_wrap.metallic, mat_wrap.metallic, mat_wrap.metallic)) + else: + fw('Ka %.6f %.6f %.6f\n' % (1.0, 1.0, 1.0)) + fw('Kd %.6f %.6f %.6f\n' % mat_wrap.base_color[:3]) # Diffuse + # XXX TODO Find a way to handle tint and diffuse color, in a consistent way with import... + fw('Ks %.6f %.6f %.6f\n' % (mat_wrap.specular, mat_wrap.specular, mat_wrap.specular)) # Specular + # Emission, not in original MTL standard but seems pretty common, see T45766. + emission_strength = mat_wrap.emission_strength + emission = [emission_strength * c for c in mat_wrap.emission_color[:3]] + fw('Ke %.6f %.6f %.6f\n' % tuple(emission)) + fw('Ni %.6f\n' % mat_wrap.ior) # Refraction index + fw('d %.6f\n' % mat_wrap.alpha) # Alpha (obj uses 'd' for dissolve) + + # See http://en.wikipedia.org/wiki/Wavefront_.obj_file for whole list of values... + # Note that mapping is rather fuzzy sometimes, trying to do our best here. + if mat_wrap.specular == 0: + fw('illum 1\n') # no specular. + elif use_mirror: + if use_transparency: + fw('illum 6\n') # Reflection, Transparency, Ray trace + else: + fw('illum 3\n') # Reflection and Ray trace + elif use_transparency: + fw('illum 9\n') # 'Glass' transparency and no Ray trace reflection... fuzzy matching, but... + else: + fw('illum 2\n') # light normally + + #### And now, the image textures... + image_map = { + "map_Kd": "base_color_texture", + "map_Ka": None, # ambient... + "map_Ks": "specular_texture", + "map_Ns": "roughness_texture", + "map_d": "alpha_texture", + "map_Tr": None, # transmission roughness? + "map_Bump": "normalmap_texture", + "disp": None, # displacement... + "refl": "metallic_texture", + "map_Ke": "emission_color_texture" if emission_strength != 0.0 else None, + } + + for key, mat_wrap_key in sorted(image_map.items()): + if mat_wrap_key is None: + continue + tex_wrap = getattr(mat_wrap, mat_wrap_key, None) + if tex_wrap is None: + continue + image = tex_wrap.image + if image is None: + continue + + filepath = io_utils.path_reference(image.filepath, source_dir, dest_dir, + path_mode, "", copy_set, image.library) + options = [] + if key == "map_Bump": + if mat_wrap.normalmap_strength != 1.0: + options.append('-bm %.6f' % mat_wrap.normalmap_strength) + if tex_wrap.translation != Vector((0.0, 0.0, 0.0)): + options.append('-o %.6f %.6f %.6f' % tex_wrap.translation[:]) + if tex_wrap.scale != Vector((1.0, 1.0, 1.0)): + options.append('-s %.6f %.6f %.6f' % tex_wrap.scale[:]) + if options: + fw('%s %s %s\n' % (key, " ".join(options), repr(filepath)[1:-1])) + else: + fw('%s %s\n' % (key, repr(filepath)[1:-1])) - else: - #write a dummy material here? - fw('Ns 0\n') - fw('Ka %.6f %.6f %.6f\n' % world_amb[:]) # Ambient, uses mirror color, - fw('Kd 0.8 0.8 0.8\n') - fw('Ks 0.8 0.8 0.8\n') - fw('d 1\n') # No alpha - fw('illum 2\n') # light normaly - - # Write images! - if face_img: # We have an image on the face! - filepath = face_img.filepath - if filepath: # may be '' for generated images - # write relative image path - filepath = bpy_extras.io_utils.path_reference(filepath, source_dir, dest_dir, - path_mode, "", copy_set, face_img.library) - fw('map_Kd %s\n' % filepath) # Diffuse mapping image - del filepath else: - # so we write the materials image. - face_img = None - - if mat: # No face image. if we havea material search for MTex image. - image_map = {} - # backwards so topmost are highest priority - for mtex in reversed(mat.texture_slots): - if mtex and mtex.texture and mtex.texture.type == 'IMAGE': - image = mtex.texture.image - if image: - # texface overrides others - if (mtex.use_map_color_diffuse and - (face_img is None) and - (mtex.use_map_warp is False) and - (mtex.texture_coords != 'REFLECTION')): - image_map["map_Kd"] = image - if mtex.use_map_ambient: - image_map["map_Ka"] = image - # this is the Spec intensity channel but Ks stands for specular Color - ''' - if mtex.use_map_specular: - image_map["map_Ks"] = image - ''' - if mtex.use_map_color_spec: # specular color - image_map["map_Ks"] = image - if mtex.use_map_hardness: # specular hardness/glossiness - image_map["map_Ns"] = image - if mtex.use_map_alpha: - image_map["map_d"] = image - if mtex.use_map_translucency: - image_map["map_Tr"] = image - if mtex.use_map_normal: - image_map["map_Bump"] = image - if mtex.use_map_displacement: - image_map["disp"] = image - if mtex.use_map_color_diffuse and (mtex.texture_coords == 'REFLECTION'): - image_map["refl"] = image - if mtex.use_map_emit: - image_map["map_Ke"] = image - - for key, image in sorted(image_map.items()): - filepath = bpy_extras.io_utils.path_reference(image.filepath, source_dir, dest_dir, - path_mode, "", copy_set, image.library) - fw('%s %s\n' % (key, repr(filepath)[1:-1])) - - file.close() + # Write a dummy material here? + fw('Ns 500\n') + fw('Ka 0.8 0.8 0.8\n') + fw('Kd 0.8 0.8 0.8\n') + fw('Ks 0.8 0.8 0.8\n') + fw('d 1\n') # No alpha + fw('illum 2\n') # light normally def test_nurbs_compat(ob): @@ -204,7 +179,7 @@ def write_nurb(fw, ob, ob_mat): do_endpoints = (do_closed == 0) and nu.use_endpoint_u for pt in nu.points: - fw('v %.6f %.6f %.6f\n' % (ob_mat * pt.co.to_3d())[:]) + fw('v %.6f %.6f %.6f\n' % (ob_mat @ pt.co.to_3d())[:]) pt_num += 1 tot_verts += pt_num @@ -242,7 +217,7 @@ def write_nurb(fw, ob, ob_mat): return tot_verts -def write_file(filepath, objects, scene, +def write_file(filepath, objects, depsgraph, scene, EXPORT_TRI=False, EXPORT_EDGES=False, EXPORT_SMOOTH_GROUPS=False, @@ -251,6 +226,7 @@ def write_file(filepath, objects, scene, EXPORT_UV=True, EXPORT_MTL=True, EXPORT_APPLY_MODIFIERS=True, + EXPORT_APPLY_MODIFIERS_RENDER=False, EXPORT_BLEN_OBS=True, EXPORT_GROUP_BY_OB=False, EXPORT_GROUP_BY_MAT=False, @@ -259,19 +235,19 @@ def write_file(filepath, objects, scene, EXPORT_CURVE_AS_NURBS=True, EXPORT_GLOBAL_MATRIX=None, EXPORT_PATH_MODE='AUTO', + progress=ProgressReport(), ): """ Basic write function. The context and options must be already set - This can be accessed externaly + This can be accessed externally eg. write( 'c:\\test\\foobar.obj', Blender.Object.GetSelected() ) # Using default options. """ - if EXPORT_GLOBAL_MATRIX is None: - EXPORT_GLOBAL_MATRIX = mathutils.Matrix() + EXPORT_GLOBAL_MATRIX = Matrix() def veckey3d(v): - return round(v.x, 4), round(v.y, 4), round(v.z, 4) + return round(v.x, 6), round(v.y, 6), round(v.z, 6) # Fros: Match rounding precision with reference mmobj def veckey2d(v): return round(v[0], 4), round(v[1], 4) @@ -279,7 +255,7 @@ def veckey2d(v): def findVertexGroupName(face, vWeightMap): """ Searches the vertexDict to see what groups is assigned to a given face. - We use a frequency system in order to sort out the name because a given vetex can + We use a frequency system in order to sort out the name because a given vertex can belong to two or more groups at the same time. To find the right name for the face we list all the possible vertex group names with their frequency and then sort by frequency in descend order. The top element is the one shared by the highest number @@ -296,551 +272,568 @@ def findVertexGroupName(face, vWeightMap): else: return '(null)' - print('OBJ Export path: %r' % filepath) - - time1 = time.time() - - file = open(filepath, "w", encoding="utf8", newline="\n") - fw = file.write - - # Write Header - fw('# Blender v%s OBJ File: %r\n' % (bpy.app.version_string, os.path.basename(bpy.data.filepath))) - fw('# www.blender.org\n') - - # Tell the obj file what material file to use. - if EXPORT_MTL: - mtlfilepath = os.path.splitext(filepath)[0] + ".mtl" - fw('mtllib %s\n' % repr(os.path.basename(mtlfilepath))[1:-1]) # filepath can contain non utf8 chars, use repr - - # Initialize totals, these are updated each object - totverts = totuvco = totno = 1 + with ProgressReportSubstep(progress, 2, "OBJ Export path: %r" % filepath, "OBJ Export Finished") as subprogress1: + with open(filepath, "w", encoding="utf8", newline="\n") as f: + fw = f.write - face_vert_index = 1 + # Write Header + fw('# Blender v%s OBJ File: %r\n' % (bpy.app.version_string, os.path.basename(bpy.data.filepath))) + fw('# www.blender.org\n') - # A Dict of Materials - # (material.name, image.name):matname_imagename # matname_imagename has gaps removed. - mtl_dict = {} - # Used to reduce the usage of matname_texname materials, which can become annoying in case of - # repeated exports/imports, yet keeping unique mat names per keys! - # mtl_name: (material.name, image.name) - mtl_rev_dict = {} + # Tell the obj file what material file to use. + if EXPORT_MTL: + mtlfilepath = os.path.splitext(filepath)[0] + ".mtl" + # filepath can contain non utf8 chars, use repr + fw('mtllib %s\n' % repr(os.path.basename(mtlfilepath))[1:-1]) - copy_set = set() + # Initialize totals, these are updated each object + totverts = totuvco = totno = 1 - # mappings of vertex groups to indices; shared between all objects - indexedGroupDict = {} - indexedGroupList = [] + face_vert_index = 1 - # Get all meshes - for ob_main in objects: + # A Dict of Materials + # (material.name, image.name):matname_imagename # matname_imagename has gaps removed. + mtl_dict = {} + # Used to reduce the usage of matname_texname materials, which can become annoying in case of + # repeated exports/imports, yet keeping unique mat names per keys! + # mtl_name: (material.name, image.name) + mtl_rev_dict = {} - # ignore dupli children - if ob_main.parent and ob_main.parent.dupli_type in {'VERTS', 'FACES'}: - # XXX - print(ob_main.name, 'is a dupli child - ignoring') - continue - - obs = [] - if ob_main.dupli_type != 'NONE': - # XXX - print('creating dupli_list on', ob_main.name) - ob_main.dupli_list_create(scene) - - obs = [(dob.object, dob.matrix) for dob in ob_main.dupli_list] - - # XXX debug print - print(ob_main.name, 'has', len(obs), 'dupli children') - else: - obs = [(ob_main, ob_main.matrix_world)] - - for ob, ob_mat in obs: - uv_unique_count = no_unique_count = 0 - - # Nurbs curve support - if EXPORT_CURVE_AS_NURBS and test_nurbs_compat(ob): - ob_mat = EXPORT_GLOBAL_MATRIX * ob_mat - totverts += write_nurb(fw, ob, ob_mat) - continue - # END NURBS - - try: - me = ob.to_mesh(scene, EXPORT_APPLY_MODIFIERS, 'PREVIEW', calc_tessface=False) - except RuntimeError: - me = None - - if me is None: - continue - - me.transform(EXPORT_GLOBAL_MATRIX * ob_mat) - - if EXPORT_TRI: - # _must_ do this first since it re-allocs arrays - mesh_triangulate(me) - - if EXPORT_UV: - faceuv = len(me.uv_textures) > 0 - if faceuv: - uv_texture = me.uv_textures.active.data[:] - uv_layer = me.uv_layers.active.data[:] - else: - faceuv = False - - me_verts = me.vertices[:] - - # Make our own list so it can be sorted to reduce context switching - face_index_pairs = [(face, index) for index, face in enumerate(me.polygons)] - # faces = [ f for f in me.tessfaces ] - - if EXPORT_EDGES: - edges = me.edges - else: - edges = [] - - if not (len(face_index_pairs) + len(edges) + len(me.vertices)): # Make sure there is somthing to write + copy_set = set() + + # ModelMod mappings of vertex groups to indices; shared between all objects + indexedGroupDict = {} + indexedGroupList = [] + + # Get all meshes + subprogress1.enter_substeps(len(objects)) + for i, ob_main in enumerate(objects): + # ignore dupli children + if ob_main.parent and ob_main.parent.instance_type in {'VERTS', 'FACES'}: + subprogress1.step("Ignoring %s, dupli child..." % ob_main.name) + continue + + obs = [(ob_main, ob_main.matrix_world)] + if ob_main.is_instancer: + obs += [(dup.instance_object.original, dup.matrix_world.copy()) + for dup in depsgraph.object_instances + if dup.parent and dup.parent.original == ob_main] + # ~ print(ob_main.name, 'has', len(obs) - 1, 'dupli children') + + subprogress1.enter_substeps(len(obs)) + for ob, ob_mat in obs: + with ProgressReportSubstep(subprogress1, 6) as subprogress2: + uv_unique_count = no_unique_count = 0 + + # Nurbs curve support + if EXPORT_CURVE_AS_NURBS and test_nurbs_compat(ob): + ob_mat = EXPORT_GLOBAL_MATRIX @ ob_mat + totverts += write_nurb(fw, ob, ob_mat) + continue + # END NURBS + + ob_for_convert = ob.evaluated_get(depsgraph) if EXPORT_APPLY_MODIFIERS else ob.original + + try: + me = ob_for_convert.to_mesh() + except RuntimeError: + me = None + + if me is None: + continue + + # _must_ do this before applying transformation, else tessellation may differ + if EXPORT_TRI: + # _must_ do this first since it re-allocs arrays + mesh_triangulate(me) + + me.transform(EXPORT_GLOBAL_MATRIX @ ob_mat) + # If negative scaling, we have to invert the normals... + if ob_mat.determinant() < 0.0: + me.flip_normals() + + if EXPORT_UV: + faceuv = len(me.uv_layers) > 0 + if faceuv: + uv_layer = me.uv_layers.active.data[:] + else: + faceuv = False - # clean up - bpy.data.meshes.remove(me) + me_verts = me.vertices[:] - continue # dont bother with this mesh. + # Make our own list so it can be sorted to reduce context switching + face_index_pairs = [(face, index) for index, face in enumerate(me.polygons)] - if EXPORT_NORMALS and face_index_pairs: - me.calc_normals_split() - # No need to call me.free_normals_split later, as this mesh is deleted anyway! - loops = me.loops - else: - loops = [] + if EXPORT_EDGES: + edges = me.edges + else: + edges = [] - if (EXPORT_SMOOTH_GROUPS or EXPORT_SMOOTH_GROUPS_BITFLAGS) and face_index_pairs: - smooth_groups, smooth_groups_tot = me.calc_smooth_groups(EXPORT_SMOOTH_GROUPS_BITFLAGS) - if smooth_groups_tot <= 1: - smooth_groups, smooth_groups_tot = (), 0 - else: - smooth_groups, smooth_groups_tot = (), 0 + if not (len(face_index_pairs) + len(edges) + len(me.vertices)): # Make sure there is something to write + # clean up + ob_for_convert.to_mesh_clear() + continue # dont bother with this mesh. - materials = me.materials[:] - material_names = [m.name if m else None for m in materials] + if EXPORT_NORMALS and face_index_pairs: + me.calc_normals_split() + # No need to call me.free_normals_split later, as this mesh is deleted anyway! - # avoid bad index errors - if not materials: - materials = [None] - material_names = [name_compat(None)] + loops = me.loops - # Sort by Material, then images - # so we dont over context switch in the obj file. - if EXPORT_KEEP_VERT_ORDER: - pass - else: - if faceuv: - if smooth_groups: - sort_func = lambda a: (a[0].material_index, - hash(uv_texture[a[1]].image), - smooth_groups[a[1]] if a[0].use_smooth else False) - else: - sort_func = lambda a: (a[0].material_index, - hash(uv_texture[a[1]].image), - a[0].use_smooth) - elif len(materials) > 1: - if smooth_groups: - sort_func = lambda a: (a[0].material_index, - smooth_groups[a[1]] if a[0].use_smooth else False) - else: - sort_func = lambda a: (a[0].material_index, - a[0].use_smooth) - else: - # no materials - if smooth_groups: - sort_func = lambda a: smooth_groups[a[1] if a[0].use_smooth else False] - else: - sort_func = lambda a: a[0].use_smooth - - face_index_pairs.sort(key=sort_func) - - del sort_func + if (EXPORT_SMOOTH_GROUPS or EXPORT_SMOOTH_GROUPS_BITFLAGS) and face_index_pairs: + smooth_groups, smooth_groups_tot = me.calc_smooth_groups(use_bitflags=EXPORT_SMOOTH_GROUPS_BITFLAGS) + if smooth_groups_tot <= 1: + smooth_groups, smooth_groups_tot = (), 0 + else: + smooth_groups, smooth_groups_tot = (), 0 - # Set the default mat to no material and no image. - contextMat = 0, 0 # Can never be this, so we will label a new material the first chance we get. - contextSmooth = None # Will either be true or false, set bad to force initialization switch. + materials = me.materials[:] + material_names = [m.name if m else None for m in materials] - if EXPORT_BLEN_OBS or EXPORT_GROUP_BY_OB: - name1 = ob.name - name2 = ob.data.name - if name1 == name2: - obnamestring = name_compat(name1) - else: - obnamestring = '%s_%s' % (name_compat(name1), name_compat(name2)) - - if EXPORT_BLEN_OBS: - fw('o %s\n' % obnamestring) # Write Object name - else: # if EXPORT_GROUP_BY_OB: - fw('g %s\n' % obnamestring) - - # Vert - for v in me_verts: - fw('v %.6f %.6f %.6f\n' % v.co[:]) - - # UV - if faceuv: - # in case removing some of these dont get defined. - uv = f_index = uv_index = uv_key = uv_val = uv_ls = None - - uv_face_mapping = [None] * len(face_index_pairs) - - uv_dict = {} - uv_get = uv_dict.get - for f, f_index in face_index_pairs: - uv_ls = uv_face_mapping[f_index] = [] - for uv_index, l_index in enumerate(f.loop_indices): - uv = uv_layer[l_index].uv - uv_key = veckey2d(uv) - uv_val = uv_get(uv_key) - if uv_val is None: - uv_val = uv_dict[uv_key] = uv_unique_count - fw('vt %.6f %.6f\n' % uv[:]) - uv_unique_count += 1 - uv_ls.append(uv_val) - - del uv_dict, uv, f_index, uv_index, uv_ls, uv_get, uv_key, uv_val - # Only need uv_unique_count and uv_face_mapping - - # NORMAL, Smooth/Non smoothed. - if EXPORT_NORMALS: - no_key = no_val = None - normals_to_idx = {} - no_get = normals_to_idx.get - loops_to_normals = [0] * len(loops) - for f, f_index in face_index_pairs: - for l_idx in f.loop_indices: - no_key = veckey3d(loops[l_idx].normal) - no_val = no_get(no_key) - if no_val is None: - no_val = normals_to_idx[no_key] = no_unique_count - fw('vn %.6f %.6f %.6f\n' % no_key) - no_unique_count += 1 - loops_to_normals[l_idx] = no_val - del normals_to_idx, no_get, no_key, no_val - else: - loops_to_normals = [] - - if not faceuv: - f_image = None - - # XXX - if EXPORT_POLYGROUPS: - # Retrieve the list of vertex groups - vertGroupNames = ob.vertex_groups.keys() - if vertGroupNames: - currentVGroup = '' - # Create a dictionary keyed by face id and listing, for each vertex, the vertex groups it belongs to - vgroupsMap = [[] for _i in range(len(me_verts))] - for v_idx, v_ls in enumerate(vgroupsMap): - v_ls[:] = [(vertGroupNames[g.group], g.weight) for g in me_verts[v_idx].groups] - - for f, f_index in face_index_pairs: - f_smooth = f.use_smooth - if f_smooth and smooth_groups: - f_smooth = smooth_groups[f_index] - f_mat = min(f.material_index, len(materials) - 1) - - if faceuv: - tface = uv_texture[f_index] - f_image = tface.image - - # MAKE KEY - if faceuv and f_image: # Object is always true. - key = material_names[f_mat], f_image.name - else: - key = material_names[f_mat], None # No image, use None instead. - - # Write the vertex group - if EXPORT_POLYGROUPS: - if vertGroupNames: - # find what vertext group the face belongs to - vgroup_of_face = findVertexGroupName(f, vgroupsMap) - if vgroup_of_face != currentVGroup: - currentVGroup = vgroup_of_face - fw('g %s\n' % vgroup_of_face) - - # CHECK FOR CONTEXT SWITCH - if key == contextMat: - pass # Context already switched, dont do anything - else: - if key[0] is None and key[1] is None: - # Write a null material, since we know the context has changed. - if EXPORT_GROUP_BY_MAT: - # can be mat_image or (null) - fw("g %s_%s\n" % (name_compat(ob.name), name_compat(ob.data.name))) # can be mat_image or (null) - if EXPORT_MTL: - fw("usemtl (null)\n") # mat, image + # avoid bad index errors + if not materials: + materials = [None] + material_names = [name_compat(None)] - else: - mat_data = mtl_dict.get(key) - if not mat_data: - # First add to global dict so we can export to mtl - # Then write mtl - - # Make a new names from the mat and image name, - # converting any spaces to underscores with name_compat. - - # If none image dont bother adding it to the name - # Try to avoid as much as possible adding texname (or other things) - # to the mtl name (see [#32102])... - mtl_name = "%s" % name_compat(key[0]) - if mtl_rev_dict.get(mtl_name, None) not in {key, None}: - if key[1] is None: - tmp_ext = "_NONE" + # Sort by Material, then images + # so we dont over context switch in the obj file. + if EXPORT_KEEP_VERT_ORDER: + pass + else: + if len(materials) > 1: + if smooth_groups: + sort_func = lambda a: (a[0].material_index, + smooth_groups[a[1]] if a[0].use_smooth else False) else: - tmp_ext = "_%s" % name_compat(key[1]) - i = 0 - while mtl_rev_dict.get(mtl_name + tmp_ext, None) not in {key, None}: - i += 1 - tmp_ext = "_%3d" % i - mtl_name += tmp_ext - mat_data = mtl_dict[key] = mtl_name, materials[f_mat], f_image - mtl_rev_dict[mtl_name] = key - - if EXPORT_GROUP_BY_MAT: - fw("g %s_%s_%s\n" % (name_compat(ob.name), name_compat(ob.data.name), mat_data[0])) # can be mat_image or (null) - if EXPORT_MTL: - fw("usemtl %s\n" % mat_data[0]) # can be mat_image or (null) - - contextMat = key - if f_smooth != contextSmooth: - if f_smooth: # on now off - if smooth_groups: - f_smooth = smooth_groups[f_index] - fw('s %d\n' % f_smooth) + sort_func = lambda a: (a[0].material_index, + a[0].use_smooth) + else: + # no materials + if smooth_groups: + sort_func = lambda a: smooth_groups[a[1] if a[0].use_smooth else False] + else: + sort_func = lambda a: a[0].use_smooth + + face_index_pairs.sort(key=sort_func) + + del sort_func + + # Set the default mat to no material and no image. + contextMat = 0, 0 # Can never be this, so we will label a new material the first chance we get. + contextSmooth = None # Will either be true or false, set bad to force initialization switch. + + if EXPORT_BLEN_OBS or EXPORT_GROUP_BY_OB: + name1 = ob.name + name2 = ob.data.name + if name1 == name2: + obnamestring = name_compat(name1) + else: + obnamestring = '%s_%s' % (name_compat(name1), name_compat(name2)) + + if EXPORT_BLEN_OBS: + fw('o %s\n' % obnamestring) # Write Object name + else: # if EXPORT_GROUP_BY_OB: + fw('g %s\n' % obnamestring) + + subprogress2.step() + + # Vert + for v in me_verts: + fw('v %.6f %.6f %.6f\n' % v.co[:]) + + subprogress2.step() + + # UV + if faceuv: + # in case removing some of these dont get defined. + uv = f_index = uv_index = uv_key = uv_val = uv_ls = None + + uv_face_mapping = [None] * len(face_index_pairs) + + uv_dict = {} + uv_get = uv_dict.get + for f, f_index in face_index_pairs: + uv_ls = uv_face_mapping[f_index] = [] + for uv_index, l_index in enumerate(f.loop_indices): + uv = uv_layer[l_index].uv + # include the vertex index in the key so we don't share UVs between vertices, + # allowed by the OBJ spec but can cause issues for other importers, see: T47010. + + # this works too, shared UVs for all verts + #~ uv_key = veckey2d(uv) + uv_key = loops[l_index].vertex_index, veckey2d(uv) + + uv_val = uv_get(uv_key) + if uv_val is None: + uv_val = uv_dict[uv_key] = uv_unique_count + fw('vt %.6f %.6f\n' % uv[:]) + uv_unique_count += 1 + uv_ls.append(uv_val) + + del uv_dict, uv, f_index, uv_index, uv_ls, uv_get, uv_key, uv_val + # Only need uv_unique_count and uv_face_mapping + + subprogress2.step() + + # NORMAL, Smooth/Non smoothed. + if EXPORT_NORMALS: # Fros comment: Simplify this, we can call the normals directly from the mesh vertices, guaranteeing index parity + # no_key = no_val = None + # normals_to_idx = {} + # no_get = normals_to_idx.get + loops_to_normals = [0] * len(loops) + + for vert in me.vertices: + fw('vn %.6f %.6f %.6f\n' % veckey3d(vert.normal)) + # for f, f_index in face_index_pairs: + # for l_idx in f.loop_indices: + # no_key = veckey3d(loops[l_idx].normal) + # no_val = no_get(no_key) + # if no_val is None: + # no_val = normals_to_idx[no_key] = no_unique_count + # fw('vn %.4f %.4f %.4f\n' % no_key) + # no_unique_count += 1 + # loops_to_normals[l_idx] = no_val + # del normals_to_idx, no_get, no_key, no_val else: - fw('s 1\n') - else: # was off now on - fw('s off\n') - contextSmooth = f_smooth - - #f_v = [(vi, me_verts[v_idx]) for vi, v_idx in enumerate(f.vertices)] - f_v = [(vi, me_verts[v_idx], l_idx) for vi, (v_idx, l_idx) in enumerate(zip(f.vertices, f.loop_indices))] - - fw('f') - if faceuv: - if EXPORT_NORMALS: - for vi, v, li in f_v: - fw(" %d/%d/%d" % - (totverts + v.index, - totuvco + uv_face_mapping[f_index][vi], - totno + loops_to_normals[li], - )) # vert, uv, normal - else: # No Normals - for vi, v, li in f_v: - fw(" %d/%d" % ( - totverts + v.index, - totuvco + uv_face_mapping[f_index][vi], - )) # vert, uv - - face_vert_index += len(f_v) - - else: # No UV's - if EXPORT_NORMALS: - for vi, v, li in f_v: - fw(" %d//%d" % (totverts + v.index, totno + loops_to_normals[li])) - else: # No Normals - for vi, v, li in f_v: - fw(" %d" % (totverts + v.index)) - - fw('\n') - - vertGroupNames = ob.vertex_groups.keys() - blendGroupPrefix = "Index." - posTransformPrefix = "PosTransform." - uvTransformPrefix = "UVTransform." - - if vertGroupNames: - pos_xforms = [] - uv_xforms = [] - for gname in vertGroupNames: - if gname.startswith(posTransformPrefix): - pos_xforms.append(gname.replace(posTransformPrefix, "")) - elif gname.startswith(uvTransformPrefix): - uv_xforms.append(gname.replace(uvTransformPrefix, "")) - - for gname in vertGroupNames: - if (not (gname in indexedGroupDict)): - indexedGroupList.append(gname) - indexedGroupDict[gname] = len(indexedGroupList) - 1 - print("adding group " + gname + " to dict with index " + str(indexedGroupDict[gname])) - - vertBlendLines = [] - vertIndexLines = [] - - for i,vert in enumerate(me_verts): - weightvals = [] - - grpIndices = [] - for g in vert.groups: - gname = vertGroupNames[g.group] - - grpIndices.append(indexedGroupDict[gname]) - - if gname.startswith(blendGroupPrefix): - # ignore zero weight groups - if float(g.weight) < 0.0001: - continue - # the actual index value could get out of sync with the blender group index, - # so extract the index from the group name - blendindex = gname.strip() - blendindex = blendindex[len(blendGroupPrefix):] - # remove optional group annotation suffix - dotIdx = blendindex.find(".") - if dotIdx != -1: - blendindex = blendindex[0:dotIdx] - # remove zeropad - blendindex = int(blendindex) - pair = (blendindex,g.weight) - weightvals.append(pair) - - if (len(grpIndices) == 0): - # ungrouped vert, but every vert needs to have a group to preserve the index ordering, so add a dummy - grpIndices.append(-1) - - gline = " ".join(map(str,grpIndices)) - vertIndexLines.append(gline) + loops_to_normals = [] + + subprogress2.step() + + # XXX + if EXPORT_POLYGROUPS: + # Retrieve the list of vertex groups + vertGroupNames = ob.vertex_groups.keys() + if vertGroupNames: + currentVGroup = '' + # Create a dictionary keyed by face id and listing, for each vertex, the vertex groups it belongs to + vgroupsMap = [[] for _i in range(len(me_verts))] + for v_idx, v_ls in enumerate(vgroupsMap): + v_ls[:] = [(vertGroupNames[g.group], g.weight) for g in me_verts[v_idx].groups] + + for f, f_index in face_index_pairs: + f_smooth = f.use_smooth + if f_smooth and smooth_groups: + f_smooth = smooth_groups[f_index] + f_mat = min(f.material_index, len(materials) - 1) + + # MAKE KEY + key = material_names[f_mat], None # No image, use None instead. + + # Write the vertex group + if EXPORT_POLYGROUPS: + if vertGroupNames: + # find what vertext group the face belongs to + vgroup_of_face = findVertexGroupName(f, vgroupsMap) + if vgroup_of_face != currentVGroup: + currentVGroup = vgroup_of_face + fw('g %s\n' % vgroup_of_face) + + # CHECK FOR CONTEXT SWITCH + if key == contextMat: + pass # Context already switched, dont do anything + else: + if key[0] is None and key[1] is None: + # Write a null material, since we know the context has changed. + if EXPORT_GROUP_BY_MAT: + # can be mat_image or (null) + fw("g %s_%s\n" % (name_compat(ob.name), name_compat(ob.data.name))) + if EXPORT_MTL: + fw("usemtl (null)\n") # mat, image - def sortByWeight(pair): - idx,weight = pair - return -weight + else: + mat_data = mtl_dict.get(key) + if not mat_data: + # First add to global dict so we can export to mtl + # Then write mtl + + # Make a new names from the mat and image name, + # converting any spaces to underscores with name_compat. + + # If none image dont bother adding it to the name + # Try to avoid as much as possible adding texname (or other things) + # to the mtl name (see [#32102])... + mtl_name = "%s" % name_compat(key[0]) + if mtl_rev_dict.get(mtl_name, None) not in {key, None}: + if key[1] is None: + tmp_ext = "_NONE" + else: + tmp_ext = "_%s" % name_compat(key[1]) + i = 0 + while mtl_rev_dict.get(mtl_name + tmp_ext, None) not in {key, None}: + i += 1 + tmp_ext = "_%3d" % i + mtl_name += tmp_ext + mat_data = mtl_dict[key] = mtl_name, materials[f_mat] + mtl_rev_dict[mtl_name] = key + + if EXPORT_GROUP_BY_MAT: + # can be mat_image or (null) + fw("g %s_%s_%s\n" % (name_compat(ob.name), name_compat(ob.data.name), mat_data[0])) + if EXPORT_MTL: + fw("usemtl %s\n" % mat_data[0]) # can be mat_image or (null) + + contextMat = key + if f_smooth != contextSmooth: + if f_smooth: # on now off + if smooth_groups: + f_smooth = smooth_groups[f_index] + fw('s %d\n' % f_smooth) + else: + fw('s 1\n') + else: # was off now on + fw('s off\n') + contextSmooth = f_smooth + + f_v = [(vi, me_verts[v_idx], l_idx) + for vi, (v_idx, l_idx) in enumerate(zip(f.vertices, f.loop_indices))] + + fw('f') + if faceuv: + if EXPORT_NORMALS: + for vi, v, li in f_v: + fw(" %d/%d/%d" % (totverts + v.index, + totuvco + uv_face_mapping[f_index][vi], + totno + loops_to_normals[li], + )) # vert, uv, normal + else: # No Normals + for vi, v, li in f_v: + fw(" %d/%d" % (totverts + v.index, + totuvco + uv_face_mapping[f_index][vi], + )) # vert, uv + + face_vert_index += len(f_v) + + else: # No UVs + if EXPORT_NORMALS: + for vi, v, li in f_v: + fw(" %d//%d" % (totverts + v.index, totno + loops_to_normals[li])) + else: # No Normals + for vi, v, li in f_v: + fw(" %d" % (totverts + v.index)) + + fw('\n') + + subprogress2.step() - weightvals = sorted(weightvals, key = sortByWeight) - # need 4 weights, so pad them out with dummy values if we have fewer. - # reuse one of the actually used indices, may help cache performance. - dummy = (0,0.0) - if len(weightvals) > 0: - fstidx,fstweight = weightvals[0] - dummy = (fstidx,0.0) - while len(weightvals) < 4: - weightvals.append(dummy) - line = "" - for pair in weightvals: - line += '%d/%0.6f ' % pair - line.strip() - vertBlendLines.append(line) - - for line in vertIndexLines: - fw('#vg ' + line + '\n') - for line in vertBlendLines: - fw('#vbld ' + line + '\n') - - if len(pos_xforms) > 0: - fw("#pos_xforms " + ' '.join(pos_xforms) + '\n') - if len(uv_xforms) > 0: - fw("#uv_xforms " + ' '.join(uv_xforms) + '\n') + # ModelMod write vertex groups + vertGroupNames = ob.vertex_groups.keys() + blendGroupPrefix = "Index." - # Write edges. - if EXPORT_EDGES: - for ed in edges: - if ed.is_loose: - fw('l %d %d\n' % (totverts + ed.vertices[0], totverts + ed.vertices[1])) - - # Make the indices global rather then per mesh - totverts += len(me_verts) - totuvco += uv_unique_count - totno += no_unique_count - - # clean up - bpy.data.meshes.remove(me) - - if ob_main.dupli_type != 'NONE': - ob_main.dupli_list_clear() - - # write named vertex groups last (just once) - for gname in indexedGroupList: - fw('#vgn ' + gname + '\n') - - file.close() - - # Now we have all our materials, save them - if EXPORT_MTL: - write_mtl(scene, mtlfilepath, EXPORT_PATH_MODE, copy_set, mtl_dict) + if vertGroupNames: + # Fros comment: All vertex groups besides those with the Index prefix are ignored. Useful if arbtirary extra groups have been added + # pos_xforms = [] + # uv_xforms = [] + # for gname in vertGroupNames: + # if gname.startswith(posTransformPrefix): + # pos_xforms.append(gname.replace(posTransformPrefix, "")) + # elif gname.startswith(uvTransformPrefix): + # uv_xforms.append(gname.replace(uvTransformPrefix, "")) + + for gname in vertGroupNames: + if (not (gname in indexedGroupDict)): + indexedGroupList.append(gname) + indexedGroupDict[gname] = len(indexedGroupList) - 1 + print("adding group " + gname + " to dict with index " + str(indexedGroupDict[gname])) + + vertBlendLines = [] + vertIndexLines = [] + + for i,vert in enumerate(me_verts): + weightvals = [] + + grpIndices = [] + for g in vert.groups: + gname = vertGroupNames[g.group] + + grpIndices.append(indexedGroupDict[gname]) + + if gname.startswith(blendGroupPrefix): + # ignore zero weight groups + if float(g.weight) < 0.0001: + continue + # the actual index value could get out of sync with the blender group index, + # so extract the index from the group name + blendindex = gname.strip() + blendindex = blendindex[len(blendGroupPrefix):] + # remove optional group annotation suffix + dotIdx = blendindex.find(".") + if dotIdx != -1: + blendindex = blendindex[0:dotIdx] + # remove zeropad + blendindex = int(blendindex) + pair = (blendindex,g.weight) + weightvals.append(pair) + + if (len(grpIndices) == 0): + # ungrouped vert, but every vert needs to have a group to preserve the index ordering, so add a dummy + grpIndices.append(-1) + + gline = " ".join(map(str,grpIndices)) + vertIndexLines.append(gline) + + def sortByWeight(pair): + idx,weight = pair + return -weight + + weightvals = sorted(weightvals, key = sortByWeight) + # need 4 weights, so pad them out with dummy values if we have fewer. + # reuse one of the actually used indices, may help cache performance. + dummy = (0,0.0) + if len(weightvals) > 0: + fstidx,fstweight = weightvals[0] + dummy = (fstidx,0.0) + while len(weightvals) < 4: + weightvals.append(dummy) + line = "" + for pair in weightvals: + line += '%d/%0.6f ' % pair + line.strip() + vertBlendLines.append(line) + + for line in vertIndexLines: + fw('#vg ' + line + '\n') + for line in vertBlendLines: + fw('#vbld ' + line + '\n') + + # if len(pos_xforms) > 0: + # fw("#pos_xforms " + ' '.join(pos_xforms) + '\n') + # if len(uv_xforms) > 0: + # fw("#uv_xforms " + ' '.join(uv_xforms) + '\n') + + # Write edges. + if EXPORT_EDGES: + for ed in edges: + if ed.is_loose: + fw('l %d %d\n' % (totverts + ed.vertices[0], totverts + ed.vertices[1])) + + # Make the indices global rather then per mesh + totverts += len(me_verts) + totuvco += uv_unique_count + totno += no_unique_count + + # clean up + ob_for_convert.to_mesh_clear() + + subprogress1.leave_substeps("Finished writing geometry of '%s'." % ob_main.name) + + # Fros comment: retrieve transform data from custom properties + posTransformPrefix = 'ModelMod_Pos_' + uvTransformPrefix = 'ModelMod_UV_' + customPropKeys = ob.data.keys() + + if posTransformPrefix + '0' in customPropKeys: + fw('#pos_xforms ') + for key in customPropKeys: + if key.startswith(posTransformPrefix): + fw(ob.data[key] + ' ') + fw('\n') + + if uvTransformPrefix + '0' in customPropKeys: + fw('#uv_xforms ') + for key in customPropKeys: + if key.startswith(uvTransformPrefix): + fw(ob.data[key] + ' ') + fw('\n') + + # ModelMod write index vertex groups last (just once) + for gname in indexedGroupList: + if gname.startswith(blendGroupPrefix): + fw('#vgn ' + gname + '\n') + + subprogress1.leave_substeps() + + subprogress1.step("Finished exporting geometry, now exporting materials") - # copy all collected files. - bpy_extras.io_utils.path_reference_copy(copy_set) + # Now we have all our materials, save them + if EXPORT_MTL: + write_mtl(scene, mtlfilepath, EXPORT_PATH_MODE, copy_set, mtl_dict) - print("OBJ Export time: %.2f" % (time.time() - time1)) + # copy all collected files. + io_utils.path_reference_copy(copy_set) def _write(context, filepath, - EXPORT_TRI, # ok - EXPORT_EDGES, - EXPORT_SMOOTH_GROUPS, - EXPORT_SMOOTH_GROUPS_BITFLAGS, - EXPORT_NORMALS, # not yet - EXPORT_UV, # ok - EXPORT_MTL, - EXPORT_APPLY_MODIFIERS, # ok - EXPORT_BLEN_OBS, - EXPORT_GROUP_BY_OB, - EXPORT_GROUP_BY_MAT, - EXPORT_KEEP_VERT_ORDER, - EXPORT_POLYGROUPS, - EXPORT_CURVE_AS_NURBS, - EXPORT_SEL_ONLY, # ok - EXPORT_ANIMATION, - EXPORT_GLOBAL_MATRIX, - EXPORT_PATH_MODE, - ): # Not used - - base_name, ext = os.path.splitext(filepath) - context_name = [base_name, '', '', ext] # Base name, scene name, frame number, extension - - scene = context.scene - - # Exit edit mode before exporting, so current object states are exported properly. - if bpy.ops.object.mode_set.poll(): - bpy.ops.object.mode_set(mode='OBJECT') - - orig_frame = scene.frame_current - - # Export an animation? - if EXPORT_ANIMATION: - scene_frames = range(scene.frame_start, scene.frame_end + 1) # Up to and including the end frame. - else: - scene_frames = [orig_frame] # Dont export an animation. + EXPORT_TRI, # ok + EXPORT_EDGES, + EXPORT_SMOOTH_GROUPS, + EXPORT_SMOOTH_GROUPS_BITFLAGS, + EXPORT_NORMALS, # ok + EXPORT_UV, # ok + EXPORT_MTL, + EXPORT_APPLY_MODIFIERS, # ok + EXPORT_APPLY_MODIFIERS_RENDER, # ok + EXPORT_BLEN_OBS, + EXPORT_GROUP_BY_OB, + EXPORT_GROUP_BY_MAT, + EXPORT_KEEP_VERT_ORDER, + EXPORT_POLYGROUPS, + EXPORT_CURVE_AS_NURBS, + EXPORT_SEL_ONLY, # ok + EXPORT_ANIMATION, + EXPORT_GLOBAL_MATRIX, + EXPORT_PATH_MODE, # Not used + ): + + with ProgressReport(context.window_manager) as progress: + base_name, ext = os.path.splitext(filepath) + context_name = [base_name, '', '', ext] # Base name, scene name, frame number, extension + + depsgraph = context.evaluated_depsgraph_get() + scene = context.scene + + # Exit edit mode before exporting, so current object states are exported properly. + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode='OBJECT') + + orig_frame = scene.frame_current + + # Export an animation? + if EXPORT_ANIMATION: + scene_frames = range(scene.frame_start, scene.frame_end + 1) # Up to and including the end frame. + else: + scene_frames = [orig_frame] # Dont export an animation. - # Loop through all frames in the scene and export. - for frame in scene_frames: - if EXPORT_ANIMATION: # Add frame to the filepath. - context_name[2] = '_%.6d' % frame + # Loop through all frames in the scene and export. + progress.enter_substeps(len(scene_frames)) + for frame in scene_frames: + if EXPORT_ANIMATION: # Add frame to the filepath. + context_name[2] = '_%.6d' % frame - scene.frame_set(frame, 0.0) - if EXPORT_SEL_ONLY: - objects = context.selected_objects - else: - objects = scene.objects - - full_path = ''.join(context_name) - - # erm... bit of a problem here, this can overwrite files when exporting frames. not too bad. - # EXPORT THE FILE. - write_file(full_path, objects, scene, - EXPORT_TRI, - EXPORT_EDGES, - EXPORT_SMOOTH_GROUPS, - EXPORT_SMOOTH_GROUPS_BITFLAGS, - EXPORT_NORMALS, - EXPORT_UV, - EXPORT_MTL, - EXPORT_APPLY_MODIFIERS, - EXPORT_BLEN_OBS, - EXPORT_GROUP_BY_OB, - EXPORT_GROUP_BY_MAT, - EXPORT_KEEP_VERT_ORDER, - EXPORT_POLYGROUPS, - EXPORT_CURVE_AS_NURBS, - EXPORT_GLOBAL_MATRIX, - EXPORT_PATH_MODE, - ) - - scene.frame_set(orig_frame, 0.0) - - # Restore old active scene. -# orig_scene.makeCurrent() -# Window.WaitCursor(0) + scene.frame_set(frame, subframe=0.0) + if EXPORT_SEL_ONLY: + objects = context.selected_objects + else: + objects = scene.objects + + full_path = ''.join(context_name) + + # erm... bit of a problem here, this can overwrite files when exporting frames. not too bad. + # EXPORT THE FILE. + progress.enter_substeps(1) + write_file(full_path, objects, depsgraph, scene, + EXPORT_TRI, + EXPORT_EDGES, + EXPORT_SMOOTH_GROUPS, + EXPORT_SMOOTH_GROUPS_BITFLAGS, + EXPORT_NORMALS, + EXPORT_UV, + EXPORT_MTL, + EXPORT_APPLY_MODIFIERS, + EXPORT_APPLY_MODIFIERS_RENDER, + EXPORT_BLEN_OBS, + EXPORT_GROUP_BY_OB, + EXPORT_GROUP_BY_MAT, + EXPORT_KEEP_VERT_ORDER, + EXPORT_POLYGROUPS, + EXPORT_CURVE_AS_NURBS, + EXPORT_GLOBAL_MATRIX, + EXPORT_PATH_MODE, + progress, + ) + progress.leave_substeps() + + scene.frame_set(orig_frame, subframe=0.0) + progress.leave_substeps() """ @@ -850,7 +843,9 @@ def _write(context, filepath, """ -def save(operator, context, filepath="", +def save(context, + filepath, + *, use_triangles=True, use_edges=False, use_normals=True, @@ -859,6 +854,7 @@ def save(operator, context, filepath="", use_uvs=True, use_materials=False, use_mesh_modifiers=True, + use_mesh_modifiers_render=False, use_blen_objects=True, group_by_object=False, group_by_material=False, @@ -880,6 +876,7 @@ def save(operator, context, filepath="", EXPORT_UV=use_uvs, EXPORT_MTL=use_materials, EXPORT_APPLY_MODIFIERS=use_mesh_modifiers, + EXPORT_APPLY_MODIFIERS_RENDER=use_mesh_modifiers_render, EXPORT_BLEN_OBS=use_blen_objects, EXPORT_GROUP_BY_OB=group_by_object, EXPORT_GROUP_BY_MAT=group_by_material, diff --git a/BlenderScripts/io_scene_mmobj/import_mmobj.py b/BlenderScripts/io_scene_mmobj/import_mmobj.py index 5ae8494..e612377 100644 --- a/BlenderScripts/io_scene_mmobj/import_mmobj.py +++ b/BlenderScripts/io_scene_mmobj/import_mmobj.py @@ -1,70 +1,33 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# +# SPDX-License-Identifier: GPL-2.0-or-later # Script copyright (C) Campbell Barton # Contributors: Campbell Barton, Jiri Hnidek, Paolo Ciccone """ -This script imports ModelMod OBJ files to Blender. +This script imports a Wavefront OBJ files to Blender. -ModelMod obj files are a superset of obj files, notably supporting blend weights, indices, -and some other things required by modelmod. The extensions are written with comment directives (such as # vbi) so that an obj loader can still read them, however, this will throw away -all the data required by ModelMod. Usage: -Run this script from "File->Import" menu and then load the desired MMOBJ file. +Run this script from "File->Import" menu and then load the desired OBJ file. +Note, This loads mesh objects and materials only, nurbs and curves are not supported. http://wiki.blender.org/index.php/Scripts/Manual/Import/wavefront_obj """ +import array import os import time import bpy import mathutils -from bpy_extras.io_utils import unpack_list, unpack_face_list + +from bpy_extras.io_utils import unpack_list from bpy_extras.image_utils import load_image -import bpy -from bpy import context - -def mesh_untessellate(me, fgon_edges): - import bmesh - bm = bmesh.new() - bm.from_mesh(me) - verts = bm.verts[:] - get = bm.edges.get - edges = [get((verts[key[0]], verts[key[1]])) for key in fgon_edges] - try: - bmesh.ops.dissolve_edges(bm, edges=edges, use_verts=False) - except: - # Possible dissolve fails for some edges - # but dont fail silently unless this is a real bug. - import traceback - traceback.print_exc() - - bm.to_mesh(me) - bm.free() +from bpy_extras.wm_utils.progress_report import ProgressReport def line_value(line_split): """ - Returns 1 string represneting the value for this line - None will be returned if theres only 1 word + Returns 1 string representing the value for this line + None will be returned if there's only 1 word """ length = len(line_split) if length == 1: @@ -77,132 +40,254 @@ def line_value(line_split): return b' '.join(line_split[1:]) -def obj_image_load(imagepath, DIR, recursive, relpath): +def filenames_group_by_ext(line, ext): + """ + Splits material libraries supporting spaces, so: + b'foo bar.mtl baz spam.MTL' -> (b'foo bar.mtl', b'baz spam.MTL') + Also handle " chars (some software use those to protect filenames with spaces, see T67266... sic). + """ + # Note that we assume that if there are some " in that line, + # then all filenames are properly enclosed within those... + start = line.find(b'"') + 1 + if start != 0: + while start != 0: + end = line.find(b'"', start) + if end != -1: + yield line[start:end] + start = line.find(b'"', end + 1) + 1 + else: + break + return + + line_lower = line.lower() + i_prev = 0 + while i_prev != -1 and i_prev < len(line): + i = line_lower.find(ext, i_prev) + if i != -1: + i += len(ext) + yield line[i_prev:i].strip() + i_prev = i + + +def obj_image_load(img_data, context_imagepath_map, line, DIR, recursive, relpath): """ Mainly uses comprehensiveImageLoad - but tries to replace '_' with ' ' for Max's exporter replaces spaces with underscores. + But we try all space-separated items from current line when file is not found with last one + (users keep generating/using image files with spaces in a format that does not support them, sigh...) + Also tries to replace '_' with ' ' for Max's exporter replaces spaces with underscores. + Also handle " chars (some software use those to protect filenames with spaces, see T67266... sic). + Also corrects img_data (in case filenames with spaces have been split up in multiple entries, see T72148). """ - if b'_' in imagepath: - image = load_image(imagepath.replace(b'_', b' '), DIR, recursive=recursive, relpath=relpath) - if image: - return image + filepath_parts = line.split(b' ') + + start = line.find(b'"') + 1 + if start != 0: + end = line.find(b'"', start) + if end != 0: + filepath_parts = (line[start:end],) + + image = None + for i in range(-1, -len(filepath_parts), -1): + imagepath = os.fsdecode(b" ".join(filepath_parts[i:])) + image = context_imagepath_map.get(imagepath, ...) + if image is ...: + image = load_image(imagepath, DIR, recursive=recursive, relpath=relpath) + if image is None and "_" in imagepath: + image = load_image(imagepath.replace("_", " "), DIR, recursive=recursive, relpath=relpath) + if image is not None: + context_imagepath_map[imagepath] = image + del img_data[i:] + img_data.append(imagepath) + break; + else: + del img_data[i:] + img_data.append(imagepath) + break; + + if image is None: + imagepath = os.fsdecode(filepath_parts[-1]) + image = load_image(imagepath, DIR, recursive=recursive, place_holder=True, relpath=relpath) + context_imagepath_map[imagepath] = image - return load_image(imagepath, DIR, recursive=recursive, place_holder=True, relpath=relpath) + return image def create_materials(filepath, relpath, - material_libs, unique_materials, unique_material_images, + material_libs, unique_materials, use_image_search, float_func): """ Create all the used materials in this obj, assign colors and images to the materials from all referenced material libs """ + from math import sqrt + from bpy_extras import node_shader_utils + DIR = os.path.dirname(filepath) context_material_vars = set() - #==================================================================================# - # This function sets textures defined in .mtl file # - #==================================================================================# - def load_material_image(blender_material, context_material_name, imagepath, type): + # Don't load the same image multiple times + context_imagepath_map = {} - texture = bpy.data.textures.new(name=type, type='IMAGE') + nodal_material_wrap_map = {} - # Absolute path - c:\.. etc would work here - image = obj_image_load(imagepath, DIR, use_image_search, relpath) + def load_material_image(blender_material, mat_wrap, context_material_name, img_data, line, type): + """ + Set textures defined in .mtl file. + """ + map_options = {} - if image is not None: - texture.image = image + # Absolute path - c:\.. etc would work here + image = obj_image_load(img_data, context_imagepath_map, line, DIR, use_image_search, relpath) + + curr_token = [] + for token in img_data[:-1]: + if token.startswith(b'-') and token[1:].isalpha(): + if curr_token: + map_options[curr_token[0]] = curr_token[1:] + curr_token[:] = [] + curr_token.append(token) + if curr_token: + map_options[curr_token[0]] = curr_token[1:] + + map_offset = map_options.get(b'-o') + map_scale = map_options.get(b'-s') + if map_offset is not None: + map_offset = tuple(map(float_func, map_offset)) + if map_scale is not None: + map_scale = tuple(map(float_func, map_scale)) + + def _generic_tex_set(nodetex, image, texcoords, translation, scale): + nodetex.image = image + nodetex.texcoords = texcoords + if translation is not None: + nodetex.translation = translation + if scale is not None: + nodetex.scale = scale # Adds textures for materials (rendering) if type == 'Kd': - mtex = blender_material.texture_slots.add() - mtex.texture = texture - mtex.texture_coords = 'UV' - mtex.use_map_color_diffuse = True - - # adds textures to faces (Textured/Alt-Z mode) - # Only apply the diffuse texture to the face if the image has not been set with the inline usemat func. - unique_material_images[context_material_name] = image # set the texface image + _generic_tex_set(mat_wrap.base_color_texture, image, 'UV', map_offset, map_scale) elif type == 'Ka': - mtex = blender_material.texture_slots.add() - mtex.use_map_color_diffuse = False - - mtex.texture = texture - mtex.texture_coords = 'UV' - mtex.use_map_ambient = True + # XXX Not supported? + print("WARNING, currently unsupported ambient texture, skipped.") elif type == 'Ks': - mtex = blender_material.texture_slots.add() - mtex.use_map_color_diffuse = False + _generic_tex_set(mat_wrap.specular_texture, image, 'UV', map_offset, map_scale) - mtex.texture = texture - mtex.texture_coords = 'UV' - mtex.use_map_specular = True + elif type == 'Ke': + _generic_tex_set(mat_wrap.emission_color_texture, image, 'UV', map_offset, map_scale) + mat_wrap.emission_strength = 1.0 elif type == 'Bump': - mtex = blender_material.texture_slots.add() - mtex.use_map_color_diffuse = False + bump_mult = map_options.get(b'-bm') + bump_mult = float(bump_mult[0]) if (bump_mult and len(bump_mult[0]) > 1) else 1.0 + mat_wrap.normalmap_strength_set(bump_mult) - mtex.texture = texture - mtex.texture_coords = 'UV' - mtex.use_map_normal = True + _generic_tex_set(mat_wrap.normalmap_texture, image, 'UV', map_offset, map_scale) elif type == 'D': - mtex = blender_material.texture_slots.add() - mtex.use_map_color_diffuse = False - - mtex.texture = texture - mtex.texture_coords = 'UV' - mtex.use_map_alpha = True - blender_material.use_transparency = True - blender_material.transparency_method = 'Z_TRANSPARENCY' - if "alpha" not in context_material_vars: - blender_material.alpha = 0.0 - # Todo, unset deffuse material alpha if it has an alpha channel + _generic_tex_set(mat_wrap.alpha_texture, image, 'UV', map_offset, map_scale) elif type == 'disp': - mtex = blender_material.texture_slots.add() - mtex.use_map_color_diffuse = False - - mtex.texture = texture - mtex.texture_coords = 'UV' - mtex.use_map_displacement = True + # XXX Not supported? + print("WARNING, currently unsupported displacement texture, skipped.") + # ~ mat_wrap.bump_image_set(image) + # ~ mat_wrap.bump_mapping_set(coords='UV', translation=map_offset, scale=map_scale) elif type == 'refl': - mtex = blender_material.texture_slots.add() - mtex.use_map_color_diffuse = False + map_type = map_options.get(b'-type') + if map_type and map_type != [b'sphere']: + print("WARNING, unsupported reflection type '%s', defaulting to 'sphere'" + "" % ' '.join(i.decode() for i in map_type)) + + _generic_tex_set(mat_wrap.base_color_texture, image, 'Reflection', map_offset, map_scale) + mat_wrap.base_color_texture.projection = 'SPHERE' - mtex.texture = texture - mtex.texture_coords = 'REFLECTION' - mtex.use_map_color_diffuse = True else: raise Exception("invalid type %r" % type) - # Add an MTL with the same name as the obj if no MTLs are spesified. - temp_mtl = os.path.splitext((os.path.basename(filepath)))[0] + b'.mtl' - - if os.path.exists(os.path.join(DIR, temp_mtl)) and temp_mtl not in material_libs: - material_libs.append(temp_mtl) + def finalize_material(context_material, context_material_vars, spec_colors, + do_highlight, do_reflection, do_transparency, do_glass): + # Finalize previous mat, if any. + if context_material: + if "specular" in context_material_vars: + # XXX This is highly approximated, not sure whether we can do better... + # TODO: Find a way to guesstimate best value from diffuse color... + # IDEA: Use standard deviation of both spec and diff colors (i.e. how far away they are + # from some grey), and apply the the proportion between those two as tint factor? + spec = sum(spec_colors) / 3.0 + # ~ spec_var = math.sqrt(sum((c - spec) ** 2 for c in spec_color) / 3.0) + # ~ diff = sum(context_mat_wrap.base_color) / 3.0 + # ~ diff_var = math.sqrt(sum((c - diff) ** 2 for c in context_mat_wrap.base_color) / 3.0) + # ~ tint = min(1.0, spec_var / diff_var) + context_mat_wrap.specular = spec + context_mat_wrap.specular_tint = 0.0 + if "roughness" not in context_material_vars: + context_mat_wrap.roughness = 0.0 + + # FIXME, how else to use this? + if do_highlight: + if "specular" not in context_material_vars: + context_mat_wrap.specular = 1.0 + if "roughness" not in context_material_vars: + context_mat_wrap.roughness = 0.0 + else: + if "specular" not in context_material_vars: + context_mat_wrap.specular = 0.0 + if "roughness" not in context_material_vars: + context_mat_wrap.roughness = 1.0 + + if do_reflection: + if "metallic" not in context_material_vars: + context_mat_wrap.metallic = 1.0 + else: + # since we are (ab)using ambient term for metallic (which can be non-zero) + context_mat_wrap.metallic = 0.0 + + if do_transparency: + if "ior" not in context_material_vars: + context_mat_wrap.ior = 1.0 + if "alpha" not in context_material_vars: + context_mat_wrap.alpha = 1.0 + # EEVEE only + context_material.blend_method = 'BLEND' + + if do_glass: + if "ior" not in context_material_vars: + context_mat_wrap.ior = 1.5 + + # Try to find a MTL with the same name as the OBJ if no MTLs are specified. + temp_mtl = os.path.splitext((os.path.basename(filepath)))[0] + ".mtl" + if os.path.exists(os.path.join(DIR, temp_mtl)): + material_libs.add(temp_mtl) del temp_mtl - #Create new materials + # Create new materials for name in unique_materials: # .keys() - if name is not None: - unique_materials[name] = bpy.data.materials.new(name.decode('utf-8', "replace")) - unique_material_images[name] = None # assign None to all material images to start with, add to later. + ma_name = "Default OBJ" if name is None else name.decode('utf-8', "replace") + ma = unique_materials[name] = bpy.data.materials.new(ma_name) + ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=False) + nodal_material_wrap_map[ma] = ma_wrap + ma_wrap.use_nodes = True - unique_materials[None] = None - unique_material_images[None] = None - - for libname in material_libs: + for libname in sorted(material_libs): # print(libname) mtlpath = os.path.join(DIR, libname) if not os.path.exists(mtlpath): - print ("\tMaterial not found MTL: %r" % mtlpath) + print("\tMaterial not found MTL: %r" % mtlpath) else: - #print('\t\tloading mtl: %e' % mtlpath) + # Note: with modern Principled BSDF shader, things like ambient, raytrace or fresnel are always 'ON' + # (i.e. automatically controlled by other parameters). + do_highlight = False + do_reflection = False + do_transparency = False + do_glass = False + spec_colors = [0.0, 0.0, 0.0] + + # print('\t\tloading mtl: %e' % mtlpath) context_material = None + context_mat_wrap = None mtl = open(mtlpath, 'rb') for line in mtl: # .readlines(): line = line.strip() @@ -213,48 +298,75 @@ def load_material_image(blender_material, context_material_name, imagepath, type line_id = line_split[0].lower() if line_id == b'newmtl': + # Finalize previous mat, if any. + finalize_material(context_material, context_material_vars, spec_colors, + do_highlight, do_reflection, do_transparency, do_glass) + context_material_name = line_value(line_split) context_material = unique_materials.get(context_material_name) + if context_material is not None: + context_mat_wrap = nodal_material_wrap_map[context_material] context_material_vars.clear() + spec_colors[:] = [0.0, 0.0, 0.0] + do_highlight = False + do_reflection = False + do_transparency = False + do_glass = False + + elif context_material: + def _get_colors(line_split): + # OBJ 'allows' one or two components values, treat single component as greyscale, and two as blue = 0.0. + ln = len(line_split) + if ln == 2: + return [float_func(line_split[1])] * 3 + elif ln == 3: + return [float_func(line_split[1]), float_func(line_split[2]), 0.0] + else: + return [float_func(line_split[1]), float_func(line_split[2]), float_func(line_split[3])] + # we need to make a material to assign properties to it. if line_id == b'ka': - context_material.mirror_color = float_func(line_split[1]), float_func(line_split[2]), float_func(line_split[3]) + refl = sum(_get_colors(line_split)) / 3.0 + context_mat_wrap.metallic = refl + context_material_vars.add("metallic") elif line_id == b'kd': - context_material.diffuse_color = float_func(line_split[1]), float_func(line_split[2]), float_func(line_split[3]) + context_mat_wrap.base_color = _get_colors(line_split) elif line_id == b'ks': - context_material.specular_color = float_func(line_split[1]), float_func(line_split[2]), float_func(line_split[3]) + spec_colors[:] = _get_colors(line_split) + context_material_vars.add("specular") + elif line_id == b'ke': + # We cannot set context_material.emit right now, we need final diffuse color as well for this. + # XXX Unsupported currently + context_mat_wrap.emission_color = _get_colors(line_split) + context_mat_wrap.emission_strength = 1.0 elif line_id == b'ns': - context_material.specular_hardness = int((float_func(line_split[1]) * 0.51)) - elif line_id == b'ni': # Refraction index - context_material.raytrace_transparency.ior = max(1, min(float_func(line_split[1]), 3)) # between 1 and 3 + # XXX Totally empirical conversion, trying to adapt it + # (from 0.0 - 1000.0 OBJ specular exponent range to 1.0 - 0.0 Principled BSDF range)... + val = max(0.0, min(1000.0, float_func(line_split[1]))) + context_mat_wrap.roughness = 1.0 - (sqrt(val / 1000)) + context_material_vars.add("roughness") + elif line_id == b'ni': # Refraction index (between 0.001 and 10). + context_mat_wrap.ior = float_func(line_split[1]) context_material_vars.add("ior") - elif line_id == b'd': # dissolve (trancparency) - context_material.alpha = float_func(line_split[1]) - context_material.use_transparency = True - context_material.transparency_method = 'Z_TRANSPARENCY' + elif line_id == b'd': # dissolve (transparency) + context_mat_wrap.alpha = float_func(line_split[1]) context_material_vars.add("alpha") - elif line_id == b'tr': # trancelucency - context_material.translucency = float_func(line_split[1]) + elif line_id == b'tr': # translucency + print("WARNING, currently unsupported 'tr' translucency option, skipped.") elif line_id == b'tf': # rgb, filter color, blender has no support for this. - pass + print("WARNING, currently unsupported 'tf' filter color option, skipped.") elif line_id == b'illum': - illum = int(line_split[1]) - - do_ambient = True - do_highlight = False - do_reflection = False - do_transparency = False - do_glass = False - do_fresnel = False - do_raytrace = False + # Some MTL files incorrectly use a float for this value, see T60135. + illum = any_number_as_int(line_split[1]) # inline comments are from the spec, v4.2 if illum == 0: # Color on and Ambient off - do_ambient = False + print("WARNING, Principled BSDF shader does not support illumination 0 mode " + "(colors with no ambient), skipped.") elif illum == 1: # Color on and Ambient on pass @@ -264,32 +376,25 @@ def load_material_image(blender_material, context_material_name, imagepath, type elif illum == 3: # Reflection on and Ray trace on do_reflection = True - do_raytrace = True elif illum == 4: # Transparency: Glass on # Reflection: Ray trace on do_transparency = True do_reflection = True do_glass = True - do_raytrace = True elif illum == 5: # Reflection: Fresnel on and Ray trace on do_reflection = True - do_fresnel = True - do_raytrace = True elif illum == 6: # Transparency: Refraction on # Reflection: Fresnel off and Ray trace on do_transparency = True do_reflection = True - do_raytrace = True elif illum == 7: # Transparency: Refraction on # Reflection: Fresnel on and Ray trace on do_transparency = True do_reflection = True - do_fresnel = True - do_raytrace = True elif illum == 8: # Reflection on and Ray trace off do_reflection = True @@ -297,85 +402,72 @@ def load_material_image(blender_material, context_material_name, imagepath, type # Transparency: Glass on # Reflection: Ray trace off do_transparency = True - do_reflection = True + do_reflection = False do_glass = True elif illum == 10: # Casts shadows onto invisible surfaces - - # blender cant do this + print("WARNING, Principled BSDF shader does not support illumination 10 mode " + "(cast shadows on invisible surfaces), skipped.") pass - if do_ambient: - context_material.ambient = 1.0 - else: - context_material.ambient = 0.0 - - if do_highlight: - # FIXME, how else to use this? - context_material.specular_intensity = 1.0 - - if do_reflection: - context_material.raytrace_mirror.use = True - context_material.raytrace_mirror.reflect_factor = 1.0 - - if do_transparency: - context_material.use_transparency = True - context_material.transparency_method = 'RAYTRACE' if do_raytrace else 'Z_TRANSPARENCY' - if "alpha" not in context_material_vars: - context_material.alpha = 0.0 - - if do_glass: - if "ior" not in context_material_vars: - context_material.raytrace_transparency.ior = 1.5 - - if do_fresnel: - context_material.raytrace_mirror.fresnel = 1.0 # could be any value for 'ON' - - """ - if do_raytrace: - context_material.use_raytrace = True - else: - context_material.use_raytrace = False - """ - # XXX, this is not following the OBJ spec, but this was - # written when raytracing wasnt default, annoying to disable for blender users. - context_material.use_raytrace = True - elif line_id == b'map_ka': - img_filepath = line_value(line.split()) - if img_filepath: - load_material_image(context_material, context_material_name, img_filepath, 'Ka') + img_data = line.split()[1:] + if img_data: + load_material_image(context_material, context_mat_wrap, + context_material_name, img_data, line, 'Ka') elif line_id == b'map_ks': - img_filepath = line_value(line.split()) - if img_filepath: - load_material_image(context_material, context_material_name, img_filepath, 'Ks') + img_data = line.split()[1:] + if img_data: + load_material_image(context_material, context_mat_wrap, + context_material_name, img_data, line, 'Ks') elif line_id == b'map_kd': - img_filepath = line_value(line.split()) - if img_filepath: - load_material_image(context_material, context_material_name, img_filepath, 'Kd') + img_data = line.split()[1:] + if img_data: + load_material_image(context_material, context_mat_wrap, + context_material_name, img_data, line, 'Kd') + elif line_id == b'map_ke': + img_data = line.split()[1:] + if img_data: + load_material_image(context_material, context_mat_wrap, + context_material_name, img_data, line, 'Ke') elif line_id in {b'map_bump', b'bump'}: # 'bump' is incorrect but some files use it. - img_filepath = line_value(line.split()) - if img_filepath: - load_material_image(context_material, context_material_name, img_filepath, 'Bump') + img_data = line.split()[1:] + if img_data: + load_material_image(context_material, context_mat_wrap, + context_material_name, img_data, line, 'Bump') elif line_id in {b'map_d', b'map_tr'}: # Alpha map - Dissolve - img_filepath = line_value(line.split()) - if img_filepath: - load_material_image(context_material, context_material_name, img_filepath, 'D') + img_data = line.split()[1:] + if img_data: + load_material_image(context_material, context_mat_wrap, + context_material_name, img_data, line, 'D') elif line_id in {b'map_disp', b'disp'}: # displacementmap - img_filepath = line_value(line.split()) - if img_filepath: - load_material_image(context_material, context_material_name, img_filepath, 'disp') + img_data = line.split()[1:] + if img_data: + load_material_image(context_material, context_mat_wrap, + context_material_name, img_data, line, 'disp') elif line_id in {b'map_refl', b'refl'}: # reflectionmap - img_filepath = line_value(line.split()) - if img_filepath: - load_material_image(context_material, context_material_name, img_filepath, 'refl') + img_data = line.split()[1:] + if img_data: + load_material_image(context_material, context_mat_wrap, + context_material_name, img_data, line, 'refl') else: - print("\t%r:%r (ignored)" % (filepath, line)) + print("WARNING: %r:%r (ignored)" % (filepath, line)) + + # Finalize last mat, if any. + finalize_material(context_material, context_material_vars, spec_colors, + do_highlight, do_reflection, do_transparency, do_glass) mtl.close() +def face_is_edge(face): + """Simple check to test whether given (temp, working) data is an edge, and not a real face.""" + face_vert_loc_indices = face[0] + face_vert_nor_indices = face[1] + return len(face_vert_nor_indices) == 1 or len(face_vert_loc_indices) == 2 + + def split_mesh(verts_loc, faces, unique_materials, filepath, SPLIT_OB_OR_GROUP): """ Takes vert_loc and faces, and separates into multiple sets of @@ -385,15 +477,19 @@ def split_mesh(verts_loc, faces, unique_materials, filepath, SPLIT_OB_OR_GROUP): filename = os.path.splitext((os.path.basename(filepath)))[0] if not SPLIT_OB_OR_GROUP or not faces: + use_verts_nor = any(f[1] for f in faces) + use_verts_tex = any(f[2] for f in faces) # use the filename for the object name since we aren't chopping up the mesh. - return [(verts_loc, faces, unique_materials, filename)] + return [(verts_loc, faces, unique_materials, filename, use_verts_nor, use_verts_tex)] def key_to_name(key): # if the key is a tuple, join it to make a string if not key: return filename # assume its a string. make sure this is true if the splitting code is changed + elif isinstance(key, bytes): + return key.decode('utf-8', 'replace') else: - return key + return "_".join(k.decode('utf-8', 'replace') for k in key) # Return a key that makes the faces unique. face_split_dict = {} @@ -401,55 +497,60 @@ def key_to_name(key): oldkey = -1 # initialize to a value that will never match the key for face in faces: - key = face[4] + (face_vert_loc_indices, + face_vert_nor_indices, + face_vert_tex_indices, + context_material, + _context_smooth_group, + context_object_key, + _face_invalid_blenpoly, + ) = face + key = context_object_key if oldkey != key: # Check the key has changed. - try: - verts_split, faces_split, unique_materials_split, vert_remap = face_split_dict[key] - except KeyError: - faces_split = [] - verts_split = [] - unique_materials_split = {} - vert_remap = {} - - face_split_dict[key] = (verts_split, faces_split, unique_materials_split, vert_remap) - + (verts_split, faces_split, unique_materials_split, vert_remap, + use_verts_nor, use_verts_tex) = face_split_dict.setdefault(key, ([], [], {}, {}, [], [])) oldkey = key - face_vert_loc_indices = face[0] + if not face_is_edge(face): + if not use_verts_nor and face_vert_nor_indices: + use_verts_nor.append(True) + + if not use_verts_tex and face_vert_tex_indices: + use_verts_tex.append(True) # Remap verts to new vert list and add where needed - for enum, i in enumerate(face_vert_loc_indices): - map_index = vert_remap.get(i) + for loop_idx, vert_idx in enumerate(face_vert_loc_indices): + map_index = vert_remap.get(vert_idx) if map_index is None: map_index = len(verts_split) - vert_remap[i] = map_index # set the new remapped index so we only add once and can reference next time. - verts_split.append(verts_loc[i]) # add the vert to the local verts + vert_remap[vert_idx] = map_index # set the new remapped index so we only add once and can reference next time. + verts_split.append(verts_loc[vert_idx]) # add the vert to the local verts - face_vert_loc_indices[enum] = map_index # remap to the local index + face_vert_loc_indices[loop_idx] = map_index # remap to the local index - matname = face[2] - if matname and matname not in unique_materials_split: - unique_materials_split[matname] = unique_materials[matname] + if context_material not in unique_materials_split: + unique_materials_split[context_material] = unique_materials[context_material] faces_split.append(face) - # remove one of the itemas and reorder - return [(value[0], value[1], value[2], key_to_name(key)) for key, value in list(face_split_dict.items())] + # remove one of the items and reorder + return [(verts_split, faces_split, unique_materials_split, key_to_name(key), bool(use_vnor), bool(use_vtex)) + for key, (verts_split, faces_split, unique_materials_split, _, use_vnor, use_vtex) + in face_split_dict.items()] def create_mesh(new_objects, - has_ngons, - use_ngons, use_edges, verts_loc, + verts_nor, verts_tex, faces, unique_materials, - unique_material_images, unique_smooth_groups, vertex_groups, + # ModelMod requires the next three extra input arguments weighted_groups, pos_xforms, uv_xforms, @@ -459,47 +560,44 @@ def create_mesh(new_objects, Takes all the data gathered and generates a mesh, adding the new object to new_objects deals with ngons, sharp edges and assigning materials """ - from bpy_extras.mesh_utils import ngon_tessellate - - if not has_ngons: - use_ngons = False if unique_smooth_groups: - sharp_edges = {} - smooth_group_users = {context_smooth_group: {} for context_smooth_group in list(unique_smooth_groups.keys())} + sharp_edges = set() + smooth_group_users = {context_smooth_group: {} for context_smooth_group in unique_smooth_groups.keys()} context_smooth_group_old = -1 - # Split ngons into tri's - fgon_edges = set() # Used for storing fgon keys - if use_edges: - edges = [] + fgon_edges = set() # Used for storing fgon keys when we need to tessellate/untessellate them (ngons with hole). + edges = [] + tot_loops = 0 - context_object = None + context_object_key = None # reverse loop through face indices for f_idx in range(len(faces) - 1, -1, -1): + face = faces[f_idx] (face_vert_loc_indices, + face_vert_nor_indices, face_vert_tex_indices, context_material, context_smooth_group, - context_object, - ) = faces[f_idx] + context_object_key, + face_invalid_blenpoly, + ) = face len_face_vert_loc_indices = len(face_vert_loc_indices) if len_face_vert_loc_indices == 1: - faces.pop(f_idx) # cant add single vert faces + faces.pop(f_idx) # can't add single vert faces - elif not face_vert_tex_indices or len_face_vert_loc_indices == 2: # faces that have no texture coords are lines + # Face with a single item in face_vert_nor_indices is actually a polyline! + elif face_is_edge(face): if use_edges: - # generators are better in python 2.4+ but can't be used in 2.3 - # edges.extend( (face_vert_loc_indices[i], face_vert_loc_indices[i+1]) for i in xrange(len_face_vert_loc_indices-1) ) - edges.extend([(face_vert_loc_indices[i], face_vert_loc_indices[i + 1]) for i in range(len_face_vert_loc_indices - 1)]) - + edges.extend((face_vert_loc_indices[i], face_vert_loc_indices[i + 1]) + for i in range(len_face_vert_loc_indices - 1)) faces.pop(f_idx) - else: + else: # Smooth Group if unique_smooth_groups and context_smooth_group: # Is a part of of a smooth group and is a face @@ -507,210 +605,165 @@ def create_mesh(new_objects, edge_dict = smooth_group_users[context_smooth_group] context_smooth_group_old = context_smooth_group - for i in range(len_face_vert_loc_indices): - i1 = face_vert_loc_indices[i] - i2 = face_vert_loc_indices[i - 1] - if i1 > i2: - i1, i2 = i2, i1 - - try: - edge_dict[i1, i2] += 1 - except KeyError: - edge_dict[i1, i2] = 1 + prev_vidx = face_vert_loc_indices[-1] + for vidx in face_vert_loc_indices: + edge_key = (prev_vidx, vidx) if (prev_vidx < vidx) else (vidx, prev_vidx) + prev_vidx = vidx + edge_dict[edge_key] = edge_dict.get(edge_key, 0) + 1 # NGons into triangles - if has_ngons and len_face_vert_loc_indices > 4: - - ngon_face_indices = ngon_tessellate(verts_loc, face_vert_loc_indices) - faces.extend([([face_vert_loc_indices[ngon[0]], - face_vert_loc_indices[ngon[1]], - face_vert_loc_indices[ngon[2]], - ], - [face_vert_tex_indices[ngon[0]], - face_vert_tex_indices[ngon[1]], - face_vert_tex_indices[ngon[2]], - ], - context_material, - context_smooth_group, - context_object, - ) - for ngon in ngon_face_indices] - ) - - # edges to make ngons - if use_ngons: - edge_users = {} - for ngon in ngon_face_indices: - for i in (0, 1, 2): - i1 = face_vert_loc_indices[ngon[i]] - i2 = face_vert_loc_indices[ngon[i - 1]] - if i1 > i2: - i1, i2 = i2, i1 - - try: - edge_users[i1, i2] += 1 - except KeyError: - edge_users[i1, i2] = 1 - - for key, users in edge_users.items(): - if users > 1: - fgon_edges.add(key) - - # remove all after 3, means we dont have to pop this one. + if face_invalid_blenpoly: + # ignore triangles with invalid indices + if len(face_vert_loc_indices) > 3: + from bpy_extras.mesh_utils import ngon_tessellate + ngon_face_indices = ngon_tessellate(verts_loc, face_vert_loc_indices, debug_print=bpy.app.debug) + faces.extend([([face_vert_loc_indices[ngon[0]], + face_vert_loc_indices[ngon[1]], + face_vert_loc_indices[ngon[2]], + ], + [face_vert_nor_indices[ngon[0]], + face_vert_nor_indices[ngon[1]], + face_vert_nor_indices[ngon[2]], + ] if face_vert_nor_indices else [], + [face_vert_tex_indices[ngon[0]], + face_vert_tex_indices[ngon[1]], + face_vert_tex_indices[ngon[2]], + ] if face_vert_tex_indices else [], + context_material, + context_smooth_group, + context_object_key, + [], + ) + for ngon in ngon_face_indices] + ) + tot_loops += 3 * len(ngon_face_indices) + + # edges to make ngons + if len(ngon_face_indices) > 1: + edge_users = set() + for ngon in ngon_face_indices: + prev_vidx = face_vert_loc_indices[ngon[-1]] + for ngidx in ngon: + vidx = face_vert_loc_indices[ngidx] + if vidx == prev_vidx: + continue # broken OBJ... Just skip. + edge_key = (prev_vidx, vidx) if (prev_vidx < vidx) else (vidx, prev_vidx) + prev_vidx = vidx + if edge_key in edge_users: + fgon_edges.add(edge_key) + else: + edge_users.add(edge_key) + faces.pop(f_idx) + else: + tot_loops += len_face_vert_loc_indices # Build sharp edges if unique_smooth_groups: - for edge_dict in list(smooth_group_users.values()): - for key, users in list(edge_dict.items()): - if users == 1: # This edge is on the boundry of a group - sharp_edges[key] = None + for edge_dict in smooth_group_users.values(): + for key, users in edge_dict.items(): + if users == 1: # This edge is on the boundary of a group + sharp_edges.add(key) # map the material names to an index material_mapping = {name: i for i, name in enumerate(unique_materials)} # enumerate over unique_materials keys() materials = [None] * len(unique_materials) - for name, index in list(material_mapping.items()): + for name, index in material_mapping.items(): materials[index] = unique_materials[name] - me = bpy.data.meshes.new(dataname.decode('utf-8', "replace")) + me = bpy.data.meshes.new(dataname) - # make sure the list isnt too big + # make sure the list isn't too big for material in materials: me.materials.append(material) me.vertices.add(len(verts_loc)) - me.tessfaces.add(len(faces)) + me.loops.add(tot_loops) + me.polygons.add(len(faces)) # verts_loc is a list of (x, y, z) tuples me.vertices.foreach_set("co", unpack_list(verts_loc)) - # faces is a list of (vert_indices, texco_indices, ...) tuples - # XXX faces should contain either 3 or 4 verts - # XXX no check for valid face indices - me.tessfaces.foreach_set("vertices_raw", unpack_face_list([f[0] for f in faces])) - - if verts_tex and me.tessfaces: - me.tessface_uv_textures.new() - - context_material_old = -1 # avoid a dict lookup - mat = 0 # rare case it may be un-initialized. - me_faces = me.tessfaces - - for i, face in enumerate(faces): - if len(face[0]) < 2: - pass # raise Exception("bad face") - elif len(face[0]) == 2: - if use_edges: - edges.append(face[0]) - else: - - blender_face = me.tessfaces[i] - - (face_vert_loc_indices, - face_vert_tex_indices, - context_material, - context_smooth_group, - context_object, - ) = face - - if context_smooth_group: - blender_face.use_smooth = True - - if context_material: - if context_material_old is not context_material: - mat = material_mapping[context_material] - context_material_old = context_material - - blender_face.material_index = mat -# blender_face.mat= mat - - if verts_tex: - - blender_tface = me.tessface_uv_textures[0].data[i] - - if context_material: - image = unique_material_images[context_material] - if image: # Can be none if the material dosnt have an image. - blender_tface.image = image - - # BUG - Evil eekadoodle problem where faces that have vert index 0 location at 3 or 4 are shuffled. - if len(face_vert_loc_indices) == 4: - if face_vert_loc_indices[2] == 0 or face_vert_loc_indices[3] == 0: - face_vert_tex_indices = face_vert_tex_indices[2], face_vert_tex_indices[3], face_vert_tex_indices[0], face_vert_tex_indices[1] - else: # length of 3 - if face_vert_loc_indices[2] == 0: - face_vert_tex_indices = face_vert_tex_indices[1], face_vert_tex_indices[2], face_vert_tex_indices[0] - # END EEEKADOODLE FIX - - # assign material, uv's and image - blender_tface.uv1 = verts_tex[face_vert_tex_indices[0]] - blender_tface.uv2 = verts_tex[face_vert_tex_indices[1]] - blender_tface.uv3 = verts_tex[face_vert_tex_indices[2]] - - if len(face_vert_loc_indices) == 4: - blender_tface.uv4 = verts_tex[face_vert_tex_indices[3]] - -# for ii, uv in enumerate(blender_face.uv): -# uv.x, uv.y= verts_tex[face_vert_tex_indices[ii]] - del me_faces -# del ALPHA - - if use_edges and not edges: - use_edges = False - + loops_vert_idx = tuple(vidx for (face_vert_loc_indices, _, _, _, _, _, _) in faces for vidx in face_vert_loc_indices) + faces_loop_start = [] + lidx = 0 + for f in faces: + face_vert_loc_indices = f[0] + nbr_vidx = len(face_vert_loc_indices) + faces_loop_start.append(lidx) + lidx += nbr_vidx + + me.loops.foreach_set("vertex_index", loops_vert_idx) + me.polygons.foreach_set("loop_start", faces_loop_start) + + faces_ma_index = tuple(material_mapping[context_material] for (_, _, _, context_material, _, _, _) in faces) + me.polygons.foreach_set("material_index", faces_ma_index) + + faces_use_smooth = tuple(bool(context_smooth_group) for (_, _, _, _, context_smooth_group, _, _) in faces) + me.polygons.foreach_set("use_smooth", faces_use_smooth) + + if verts_nor and me.loops: + # Note: we store 'temp' normals in loops, since validate() may alter final mesh, + # we can only set custom lnors *after* calling it. + me.create_normals_split() + loops_nor = tuple(no for (_, face_vert_nor_indices, _, _, _, _, _) in faces + for face_noidx in face_vert_nor_indices + for no in verts_nor[face_noidx]) + me.loops.foreach_set("normal", loops_nor) + + if verts_tex and me.polygons: + # Some files Do not explicitly write the 'v' value when it's 0.0, see T68249... + verts_tex = [uv if len(uv) == 2 else uv + [0.0] for uv in verts_tex] + me.uv_layers.new(do_init=False) + loops_uv = tuple(uv for (_, _, face_vert_tex_indices, _, _, _, _) in faces + for face_uvidx in face_vert_tex_indices + for uv in verts_tex[face_uvidx]) + me.uv_layers[0].data.foreach_set("uv", loops_uv) + + use_edges = use_edges and bool(edges) if use_edges: me.edges.add(len(edges)) - # edges should be a list of (a, b) tuples me.edges.foreach_set("vertices", unpack_list(edges)) -# me_edges.extend( edges ) - -# del me_edges - - # Add edge faces. -# me_edges= me.edges - def edges_match(e1, e2): - return (e1[0] == e2[0] and e1[1] == e2[1]) or (e1[0] == e2[1] and e1[1] == e2[0]) + me.validate(clean_customdata=False) # *Very* important to not remove lnors here! + me.update(calc_edges=use_edges, calc_edges_loose=use_edges) - me.validate() - me.update(calc_edges=use_edges) - - if unique_smooth_groups and sharp_edges: + # Un-tessellate as much as possible, in case we had to triangulate some ngons... + if fgon_edges: import bmesh bm = bmesh.new() bm.from_mesh(me) - # to avoid slow iterator lookups later / indexing verts is slow in bmesh - bm_verts = bm.verts[:] - - for sharp_edge in sharp_edges.keys(): - vert1 = bm_verts[sharp_edge[0]] - vert2 = bm_verts[sharp_edge[1]] - if vert1 != vert2: - edge = bm.edges.get((vert1, vert2)) - if edge is not None: - me.edges[edge.index].use_edge_sharp = True - + verts = bm.verts[:] + get = bm.edges.get + edges = [get((verts[vidx1], verts[vidx2])) for vidx1, vidx2 in fgon_edges] + try: + bmesh.ops.dissolve_edges(bm, edges=edges, use_verts=False) + except: + # Possible dissolve fails for some edges, but don't fail silently in case this is a real bug. + import traceback + traceback.print_exc() + + bm.to_mesh(me) bm.free() - del bm - mesh_untessellate(me, fgon_edges) + # XXX If validate changes the geometry, this is likely to be broken... + if unique_smooth_groups and sharp_edges: + for e in me.edges: + if e.key in sharp_edges: + e.use_edge_sharp = True - # XXX slow -# if unique_smooth_groups and sharp_edges: -# for sharp_edge in sharp_edges.keys(): -# for ed in me.edges: -# if edges_match(sharp_edge, ed.vertices): -# ed.use_edge_sharp = True + if verts_nor: + clnors = array.array('f', [0.0] * (len(me.loops) * 3)) + me.loops.foreach_get("normal", clnors) -# if unique_smooth_groups and sharp_edges: -# SHARP= Mesh.EdgeFlags.SHARP -# for ed in me.findEdges( sharp_edges.keys() ): -# if ed is not None: -# me_edges[ed].flag |= SHARP -# del SHARP + if not unique_smooth_groups: + me.polygons.foreach_set("use_smooth", [True] * len(me.polygons)) + + me.normals_split_custom_set(tuple(zip(*(iter(clnors),) * 3))) + me.use_auto_smooth = True ob = bpy.data.objects.new(me.name, me) new_objects.append(ob) @@ -719,28 +772,33 @@ def edges_match(e1, e2): # content of the vertex_groups. If the user selects to NOT have vertex groups saved then # the following test will never run for group_name, group_indices in vertex_groups.items(): - group = ob.vertex_groups.new(group_name.decode('utf-8', "replace")) + group = ob.vertex_groups.new(name=group_name.decode('utf-8', "replace")) group.add(group_indices, 1.0, 'REPLACE') + # ModelMod requires addition of weighted vertex groups for skinning sorted_groups = list(weighted_groups.keys()) sorted_groups.sort() for group_name in sorted_groups: group_verts = weighted_groups[group_name] - group = ob.vertex_groups.new(group_name.decode('utf-8', "replace")) + group = ob.vertex_groups.new(name=group_name.decode('utf-8', "replace")) for vidx, vweight in group_verts.items(): group.add([vidx], vweight, 'ADD') # create transforms as vertex groups so that its possible (albeit hacky) to edit/delete them in blender. # a key-value storage would be more appropriate but I don't think blender has anything like that. - for xform in pos_xforms: + # Fros comment: Transform strings are now attached to object via custom properties + for xform_index, xform in enumerate(pos_xforms): xform = xform.decode('utf-8', "replace") - xform = "PosTransform." + xform - group = ob.vertex_groups.new(xform) + # xform = "PosTransform." + xform + # group = ob.vertex_groups.new(name=xform) + ob.data['ModelMod_Pos_{}'.format(xform_index)] = xform - for xform in uv_xforms: + for xform_index, xform in enumerate(uv_xforms): xform = xform.decode('utf-8', "replace") - xform = "UVTransform." + xform - group = ob.vertex_groups.new(xform) + # xform = "UVTransform." + xform + # group = ob.vertex_groups.new(name=xform) + ob.data['ModelMod_UV_{}'.format(xform_index)] = xform + def create_nurbs(context_nurbs, vert_loc, new_objects): """ @@ -772,7 +830,7 @@ def create_nurbs(context_nurbs, vert_loc, new_objects): nu = cu.splines.new('NURBS') nu.points.add(len(curv_idx) - 1) # a point is added to start with - nu.points.foreach_set("co", [co_axis for vt_idx in curv_idx for co_axis in (vert_loc[vt_idx] + (1.0,))]) + nu.points.foreach_set("co", [co_axis for vt_idx in curv_idx for co_axis in (vert_loc[vt_idx] + [1.0])]) nu.order_u = deg[0] + 1 @@ -846,17 +904,24 @@ def get_float_func(filepath): return float -def load(operator, context, filepath, +def any_number_as_int(svalue): + if b',' in svalue: + svalue = svalue.replace(b',', b'.') + return int(float(svalue)) + + +def load(context, + filepath, + *, global_clamp_size=0.0, - use_ngons=True, use_smooth_groups=True, use_edges=True, use_split_objects=True, - use_split_groups=True, + use_split_groups=False, use_image_search=True, use_groups_as_vgroups=False, relpath=None, - global_matrix=None, + global_matrix=None ): """ Called by the user interface or another script. @@ -864,385 +929,456 @@ def load(operator, context, filepath, This function passes the file and sends the data off to be split into objects and then converted into mesh objects """ - print('\nimporting obj %r' % filepath) - - filepath = os.fsencode(filepath) - - if global_matrix is None: - global_matrix = mathutils.Matrix() - - if use_split_objects or use_split_groups: - use_groups_as_vgroups = False - - time_main = time.time() - - verts_loc = [] - verts_tex = [] - faces = [] # tuples of the faces - material_libs = [] # filanems to material libs this uses - vertex_groups = {} # when use_groups_as_vgroups is true - - # used by modelmod to create weighted-index vgroups: - verts_blenddata_idx = 0 - weighted_groups = {} - pos_xforms = [] - uv_xforms = [] - - # Get the string to float conversion func for this file- is 'float' for almost all files. - float_func = get_float_func(filepath) - - # Context variables - context_material = None - context_smooth_group = None - context_object = None - context_vgroup = None - - # Nurbs - context_nurbs = {} - nurbs = [] - context_parm = b'' # used by nurbs too but could be used elsewhere - - has_ngons = False - # has_smoothgroups= False - is explicit with len(unique_smooth_groups) being > 0 - - # Until we can use sets - unique_materials = {} - unique_material_images = {} - unique_smooth_groups = {} - # unique_obects= {} - no use for this variable since the objects are stored in the face. - - # when there are faces that end with \ - # it means they are multiline- - # since we use xreadline we cant skip to the next line - # so we need to know whether - context_multi_line = b'' - - print("\tparsing obj file...") - time_sub = time.time() - - def zeropad(s,n): - while (len(s) < n): - s = b'0' + s - return s -# time_sub= sys.time() - - file = open(filepath, 'rb') - for line in file: # .readlines(): - line_split = line.split() - - if not line_split: - continue - - line_start = line_split[0] # we compare with this a _lot_ - - if line_start == b'v': - verts_loc.append((float_func(line_split[1]), float_func(line_split[2]), float_func(line_split[3]))) - - elif line_start == b'#vbld' or context_multi_line == b'#vbld': - groups = line_split[1:] - vidx = verts_blenddata_idx - verts_blenddata_idx += 1 - for g in groups: - idx,weight = g.split(b'/') - weight = float_func(weight) - if (weight > 0.0): - gname = b'Index.' + zeropad(idx,2) - weighted_groups.setdefault(gname, {}) - weighted_groups[gname][vidx] = weight - - context_multi_line = b'' - elif line_start == b'#pos_xforms' or context_multi_line == b'#pos_xforms': - # don't interpret this, its just baggage that needs to be passed through by the exporter. - pos_xforms = line_split[1:] - context_multi_line = b'' - elif line_start == b'#uv_xforms' or context_multi_line == b'#uv_xforms': - # don't interpret this, its just baggage that needs to be passed through by the exporter. - uv_xforms = line_split[1:] - context_multi_line = b'' - - elif line_start == b'vn': - pass - - elif line_start == b'vt': - verts_tex.append((float_func(line_split[1]), float_func(line_split[2]))) - - # Handel faces lines (as faces) and the second+ lines of fa multiline face here - # use 'f' not 'f ' because some objs (very rare have 'fo ' for faces) - elif line_start == b'f' or context_multi_line == b'f': - if context_multi_line: - # use face_vert_loc_indices and face_vert_tex_indices previously defined and used the obj_face - pass - - else: - line_split = line_split[1:] - face_vert_loc_indices = [] - face_vert_tex_indices = [] - - # Instance a face - faces.append((face_vert_loc_indices, - face_vert_tex_indices, - context_material, - context_smooth_group, - context_object, - )) - - if strip_slash(line_split): - context_multi_line = b'f' - else: - context_multi_line = b'' - - for v in line_split: - obj_vert = v.split(b'/') - vert_loc_index = int(obj_vert[0]) - 1 - # Add the vertex to the current group - # *warning*, this wont work for files that have groups defined around verts - if use_groups_as_vgroups and context_vgroup: - vertex_groups[context_vgroup].append(vert_loc_index) - - # Make relative negative vert indices absolute - if vert_loc_index < 0: - vert_loc_index = len(verts_loc) + vert_loc_index + 1 - - face_vert_loc_indices.append(vert_loc_index) - - if len(obj_vert) > 1 and obj_vert[1]: - # formatting for faces with normals and textures us - # loc_index/tex_index/nor_index - - vert_tex_index = int(obj_vert[1]) - 1 - # Make relative negative vert indices absolute - if vert_tex_index < 0: - vert_tex_index = len(verts_tex) + vert_tex_index + 1 - - face_vert_tex_indices.append(vert_tex_index) - else: - # dummy - face_vert_tex_indices.append(0) + def unique_name(existing_names, name_orig): + i = 0 + if name_orig is None: + name_orig = b"ObjObject" + name = name_orig + while name in existing_names: + name = b"%s.%03d" % (name_orig, i) + i += 1 + existing_names.add(name) + return name + + def handle_vec(line_start, context_multi_line, line_split, tag, data, vec, vec_len): + ret_context_multi_line = tag if strip_slash(line_split) else b'' + if line_start == tag: + vec[:] = [float_func(v) for v in line_split[1:]] + elif context_multi_line == tag: + vec += [float_func(v) for v in line_split] + if not ret_context_multi_line: + data.append(tuple(vec[:vec_len])) + return ret_context_multi_line + + def create_face(context_material, context_smooth_group, context_object_key): + face_vert_loc_indices = [] + face_vert_nor_indices = [] + face_vert_tex_indices = [] + return ( + face_vert_loc_indices, + face_vert_nor_indices, + face_vert_tex_indices, + context_material, + context_smooth_group, + context_object_key, + [], # If non-empty, that face is a Blender-invalid ngon (holes...), need a mutable object for that... + ) + + with ProgressReport(context.window_manager) as progress: + progress.enter_substeps(1, "Importing OBJ %r..." % filepath) + + if global_matrix is None: + global_matrix = mathutils.Matrix() + + if use_split_objects or use_split_groups: + use_groups_as_vgroups = False + + verts_loc = [] + verts_nor = [] + verts_tex = [] + faces = [] # tuples of the faces + material_libs = set() # filenames to material libs this OBJ uses + vertex_groups = {} # when use_groups_as_vgroups is true + + # used by modelmod to create weighted-index vgroups: + verts_blenddata_idx = 0 + weighted_groups = {} + pos_xforms = [] + uv_xforms = [] + + # Get the string to float conversion func for this file- is 'float' for almost all files. + float_func = get_float_func(filepath) + + # Context variables + context_material = None + context_smooth_group = None + context_object_key = None + context_object_obpart = None + context_vgroup = None + + objects_names = set() + + # Nurbs + context_nurbs = {} + nurbs = [] + context_parm = b'' # used by nurbs too but could be used elsewhere + + # Until we can use sets + use_default_material = False + unique_materials = {} + unique_smooth_groups = {} + # unique_obects= {} - no use for this variable since the objects are stored in the face. + + # when there are faces that end with \ + # it means they are multiline- + # since we use xreadline we can't skip to the next line + # so we need to know whether + context_multi_line = b'' + + # Per-face handling data. + face_vert_loc_indices = None + face_vert_nor_indices = None + face_vert_tex_indices = None + verts_loc_len = verts_nor_len = verts_tex_len = 0 + face_items_usage = set() + face_invalid_blenpoly = None + prev_vidx = None + face = None + vec = [] + + quick_vert_failures = 0 + skip_quick_vert = False + + # ModelMod utility function + def zeropad(s,n): + while (len(s) < n): + s = b'0' + s + return s + + progress.enter_substeps(3, "Parsing OBJ file...") + with open(filepath, 'rb') as f: + for line in f: + line_split = line.split() - if len(face_vert_loc_indices) > 4: - has_ngons = True + if not line_split: + continue - elif use_edges and (line_start == b'l' or context_multi_line == b'l'): - # very similar to the face load function above with some parts removed + line_start = line_split[0] # we compare with this a _lot_ - if context_multi_line: - # use face_vert_loc_indices and face_vert_tex_indices previously defined and used the obj_face - pass + if len(line_split) == 1 and not context_multi_line and line_start != b'end': + print("WARNING, skipping malformatted line: %s" % line.decode('UTF-8', 'replace').rstrip()) + continue - else: - line_split = line_split[1:] - face_vert_loc_indices = [] - face_vert_tex_indices = [] - - # Instance a face - faces.append((face_vert_loc_indices, - face_vert_tex_indices, - context_material, - context_smooth_group, - context_object, - )) - - if strip_slash(line_split): - context_multi_line = b'l' - else: - context_multi_line = b'' - - # isline = line_start == b'l' # UNUSED - - for v in line_split: - obj_vert = v.split(b'/') - vert_loc_index = int(obj_vert[0]) - 1 - - # Make relative negative vert indices absolute - if vert_loc_index < 0: - vert_loc_index = len(verts_loc) + vert_loc_index + 1 - - face_vert_loc_indices.append(vert_loc_index) - - elif line_start == b's': - if use_smooth_groups: - context_smooth_group = line_value(line_split) - if context_smooth_group == b'off': - context_smooth_group = None - elif context_smooth_group: # is not None - unique_smooth_groups[context_smooth_group] = None - - elif line_start == b'o': - if use_split_objects: - context_object = line_value(line_split) - # unique_obects[context_object]= None - - elif line_start == b'g': - if use_split_groups: - context_object = line_value(line.split()) - # print 'context_object', context_object - # unique_obects[context_object]= None - elif use_groups_as_vgroups: - context_vgroup = line_value(line.split()) - if context_vgroup and context_vgroup != b'(null)': - vertex_groups.setdefault(context_vgroup, []) + # Handling vertex data are pretty similar, factorize that. + # Also, most OBJ files store all those on a single line, so try fast parsing for that first, + # and only fallback to full multi-line parsing when needed, this gives significant speed-up + # (~40% on affected code). + if line_start == b'v': + vdata, vdata_len, do_quick_vert = verts_loc, 3, not skip_quick_vert + elif line_start == b'vn': + vdata, vdata_len, do_quick_vert = verts_nor, 3, not skip_quick_vert + elif line_start == b'vt': + vdata, vdata_len, do_quick_vert = verts_tex, 2, not skip_quick_vert + + # ModelMod extra conditions + elif line_start == b'#vbld' or context_multi_line == b'#vbld': + groups = line_split[1:] + vidx = verts_blenddata_idx + verts_blenddata_idx += 1 + for g in groups: + idx,weight = g.split(b'/') + weight = float_func(weight) + if (weight > 0.0): + gname = b'Index.' + zeropad(idx,2) + weighted_groups.setdefault(gname, {}) + weighted_groups[gname][vidx] = weight + + context_multi_line = b'' + elif line_start == b'#pos_xforms' or context_multi_line == b'#pos_xforms': + # don't interpret this, its just baggage that needs to be passed through by the exporter. + pos_xforms = line_split[1:] + context_multi_line = b'' + elif line_start == b'#uv_xforms' or context_multi_line == b'#uv_xforms': + # don't interpret this, its just baggage that needs to be passed through by the exporter. + uv_xforms = line_split[1:] + context_multi_line = b'' + + elif context_multi_line == b'v': + vdata, vdata_len, do_quick_vert = verts_loc, 3, False + elif context_multi_line == b'vn': + vdata, vdata_len, do_quick_vert = verts_nor, 3, False + elif context_multi_line == b'vt': + vdata, vdata_len, do_quick_vert = verts_tex, 2, False else: - context_vgroup = None # dont assign a vgroup - - elif line_start == b'usemtl': - context_material = line_value(line.split()) - unique_materials[context_material] = None - elif line_start == b'mtllib': # usemap or usemat - material_libs = list(set(material_libs) | set(line.split()[1:])) # can have multiple mtllib filenames per line, mtllib can appear more than once, so make sure only occurance of material exists - - # Nurbs support - elif line_start == b'cstype': - context_nurbs[b'cstype'] = line_value(line.split()) # 'rat bspline' / 'bspline' - elif line_start == b'curv' or context_multi_line == b'curv': - curv_idx = context_nurbs[b'curv_idx'] = context_nurbs.get(b'curv_idx', []) # in case were multiline - - if not context_multi_line: - context_nurbs[b'curv_range'] = float_func(line_split[1]), float_func(line_split[2]) - line_split[0:3] = [] # remove first 3 items - - if strip_slash(line_split): - context_multi_line = b'curv' - else: - context_multi_line = b'' - - for i in line_split: - vert_loc_index = int(i) - 1 - - if vert_loc_index < 0: - vert_loc_index = len(verts_loc) + vert_loc_index + 1 - - curv_idx.append(vert_loc_index) + vdata_len = 0 + + if vdata_len: + if do_quick_vert: + try: + vdata.append(list(map(float_func, line_split[1:vdata_len + 1]))) + except: + do_quick_vert = False + # In case we get too many failures on quick parsing, force fallback to full multi-line one. + # Exception handling can become costly... + quick_vert_failures += 1 + if quick_vert_failures > 10000: + skip_quick_vert = True + if not do_quick_vert: + context_multi_line = handle_vec(line_start, context_multi_line, line_split, + context_multi_line or line_start, + vdata, vec, vdata_len) + + elif line_start == b'f' or context_multi_line == b'f': + if not context_multi_line: + line_split = line_split[1:] + # Instantiate a face + face = create_face(context_material, context_smooth_group, context_object_key) + (face_vert_loc_indices, face_vert_nor_indices, face_vert_tex_indices, + _1, _2, _3, face_invalid_blenpoly) = face + faces.append(face) + face_items_usage.clear() + verts_loc_len = len(verts_loc) + verts_nor_len = len(verts_nor) + verts_tex_len = len(verts_tex) + if context_material is None: + use_default_material = True + # Else, use face_vert_loc_indices and face_vert_tex_indices previously defined and used the obj_face + + context_multi_line = b'f' if strip_slash(line_split) else b'' + + for v in line_split: + obj_vert = v.split(b'/') + idx = int(obj_vert[0]) # Note that we assume here we cannot get OBJ invalid 0 index... + vert_loc_index = (idx + verts_loc_len) if (idx < 1) else idx - 1 + # Add the vertex to the current group + # *warning*, this wont work for files that have groups defined around verts + if use_groups_as_vgroups and context_vgroup: + vertex_groups[context_vgroup].append(vert_loc_index) + # This a first round to quick-detect ngons that *may* use a same edge more than once. + # Potential candidate will be re-checked once we have done parsing the whole face. + if not face_invalid_blenpoly: + # If we use more than once a same vertex, invalid ngon is suspected. + if vert_loc_index in face_items_usage: + face_invalid_blenpoly.append(True) + else: + face_items_usage.add(vert_loc_index) + face_vert_loc_indices.append(vert_loc_index) + + # formatting for faces with normals and textures is + # loc_index/tex_index/nor_index + if len(obj_vert) > 1 and obj_vert[1] and obj_vert[1] != b'0': + idx = int(obj_vert[1]) + face_vert_tex_indices.append((idx + verts_tex_len) if (idx < 1) else idx - 1) + else: + face_vert_tex_indices.append(0) - elif line_start == b'parm' or context_multi_line == b'parm': - if context_multi_line: - context_multi_line = b'' - else: - context_parm = line_split[1] - line_split[0:2] = [] # remove first 2 + if len(obj_vert) > 2 and obj_vert[2] and obj_vert[2] != b'0': + idx = int(obj_vert[2]) + face_vert_nor_indices.append((idx + verts_nor_len) if (idx < 1) else idx - 1) + else: + face_vert_nor_indices.append(0) + + if not context_multi_line: + # Means we have finished a face, we have to do final check if ngon is suspected to be blender-invalid... + if face_invalid_blenpoly: + face_invalid_blenpoly.clear() + face_items_usage.clear() + prev_vidx = face_vert_loc_indices[-1] + for vidx in face_vert_loc_indices: + edge_key = (prev_vidx, vidx) if (prev_vidx < vidx) else (vidx, prev_vidx) + if edge_key in face_items_usage: + face_invalid_blenpoly.append(True) + break + face_items_usage.add(edge_key) + prev_vidx = vidx + + elif use_edges and (line_start == b'l' or context_multi_line == b'l'): + # very similar to the face load function above with some parts removed + if not context_multi_line: + line_split = line_split[1:] + # Instantiate a face + face = create_face(context_material, context_smooth_group, context_object_key) + face_vert_loc_indices = face[0] + # XXX A bit hackish, we use special 'value' of face_vert_nor_indices (a single True item) to tag this + # as a polyline, and not a regular face... + face[1][:] = [True] + faces.append(face) + if context_material is None: + use_default_material = True + # Else, use face_vert_loc_indices previously defined and used the obj_face + + context_multi_line = b'l' if strip_slash(line_split) else b'' + + for v in line_split: + obj_vert = v.split(b'/') + idx = int(obj_vert[0]) - 1 + face_vert_loc_indices.append((idx + len(verts_loc) + 1) if (idx < 0) else idx) + + elif line_start == b's': + if use_smooth_groups: + context_smooth_group = line_value(line_split) + if context_smooth_group == b'off': + context_smooth_group = None + elif context_smooth_group: # is not None + unique_smooth_groups[context_smooth_group] = None + + elif line_start == b'o': + if use_split_objects: + context_object_key = unique_name(objects_names, line_value(line_split)) + context_object_obpart = context_object_key + # unique_objects[context_object_key]= None + + elif line_start == b'g': + if use_split_groups: + grppart = line_value(line_split) + context_object_key = (context_object_obpart, grppart) if context_object_obpart else grppart + # print 'context_object_key', context_object_key + # unique_objects[context_object_key]= None + elif use_groups_as_vgroups: + context_vgroup = line_value(line.split()) + if context_vgroup and context_vgroup != b'(null)': + vertex_groups.setdefault(context_vgroup, []) + else: + context_vgroup = None # dont assign a vgroup + + elif line_start == b'usemtl': + context_material = line_value(line.split()) + unique_materials[context_material] = None + elif line_start == b'mtllib': # usemap or usemat + # can have multiple mtllib filenames per line, mtllib can appear more than once, + # so make sure only occurrence of material exists + material_libs |= {os.fsdecode(f) for f in filenames_group_by_ext(line.lstrip()[7:].strip(), b'.mtl') + } + + # Nurbs support + elif line_start == b'cstype': + context_nurbs[b'cstype'] = line_value(line.split()) # 'rat bspline' / 'bspline' + elif line_start == b'curv' or context_multi_line == b'curv': + curv_idx = context_nurbs[b'curv_idx'] = context_nurbs.get(b'curv_idx', []) # in case were multiline + + if not context_multi_line: + context_nurbs[b'curv_range'] = float_func(line_split[1]), float_func(line_split[2]) + line_split[0:3] = [] # remove first 3 items + + if strip_slash(line_split): + context_multi_line = b'curv' + else: + context_multi_line = b'' - if strip_slash(line_split): - context_multi_line = b'parm' - else: - context_multi_line = b'' - - if context_parm.lower() == b'u': - context_nurbs.setdefault(b'parm_u', []).extend([float_func(f) for f in line_split]) - elif context_parm.lower() == b'v': # surfaces not supported yet - context_nurbs.setdefault(b'parm_v', []).extend([float_func(f) for f in line_split]) - # else: # may want to support other parm's ? - - elif line_start == b'deg': - context_nurbs[b'deg'] = [int(i) for i in line.split()[1:]] - elif line_start == b'end': - # Add the nurbs curve - if context_object: - context_nurbs[b'name'] = context_object - nurbs.append(context_nurbs) - context_nurbs = {} - context_parm = b'' - - ''' # How to use usemap? depricated? - elif line_start == b'usema': # usemap or usemat - context_image= line_value(line_split) - ''' + for i in line_split: + vert_loc_index = int(i) - 1 - file.close() - time_new = time.time() - print("%.4f sec" % (time_new - time_sub)) - time_sub = time_new + if vert_loc_index < 0: + vert_loc_index = len(verts_loc) + vert_loc_index + 1 - print('\tloading materials and images...') - create_materials(filepath, relpath, material_libs, unique_materials, unique_material_images, use_image_search, float_func) + curv_idx.append(vert_loc_index) - time_new = time.time() - print("%.4f sec" % (time_new - time_sub)) - time_sub = time_new + elif line_start == b'parm' or context_multi_line == b'parm': + if context_multi_line: + context_multi_line = b'' + else: + context_parm = line_split[1] + line_split[0:2] = [] # remove first 2 - # deselect all - if bpy.ops.object.select_all.poll(): - bpy.ops.object.select_all(action='DESELECT') + if strip_slash(line_split): + context_multi_line = b'parm' + else: + context_multi_line = b'' + + if context_parm.lower() == b'u': + context_nurbs.setdefault(b'parm_u', []).extend([float_func(f) for f in line_split]) + elif context_parm.lower() == b'v': # surfaces not supported yet + context_nurbs.setdefault(b'parm_v', []).extend([float_func(f) for f in line_split]) + # else: # may want to support other parm's ? + + elif line_start == b'deg': + context_nurbs[b'deg'] = [int(i) for i in line.split()[1:]] + elif line_start == b'end': + # Add the nurbs curve + if context_object_key: + context_nurbs[b'name'] = context_object_key + nurbs.append(context_nurbs) + context_nurbs = {} + context_parm = b'' + + ''' # How to use usemap? deprecated? + elif line_start == b'usema': # usemap or usemat + context_image= line_value(line_split) + ''' + + progress.step("Done, loading materials and images...") + + if use_default_material: + unique_materials[None] = None + create_materials(filepath, relpath, material_libs, unique_materials, + use_image_search, float_func) + + progress.step("Done, building geometries (verts:%i faces:%i materials: %i smoothgroups:%i) ..." % + (len(verts_loc), len(faces), len(unique_materials), len(unique_smooth_groups))) + + # deselect all + if bpy.ops.object.select_all.poll(): + bpy.ops.object.select_all(action='DESELECT') + + new_objects = [] # put new objects here + + # Split the mesh by objects/materials, may + SPLIT_OB_OR_GROUP = bool(use_split_objects or use_split_groups) + + for data in split_mesh(verts_loc, faces, unique_materials, filepath, SPLIT_OB_OR_GROUP): + verts_loc_split, faces_split, unique_materials_split, dataname, use_vnor, use_vtex = data + + # MMObj: split_mesh doesn't know how to split the weight groups, so if the array counts mismatch, then the groups will point at the wrong verts + if verts_blenddata_idx > 0 and len(verts_loc_split) != verts_blenddata_idx: + raise Exception("split changed vertex count and thus blend weights are invalid: retry with mesh split options disabled (orig vert count: %i, new count: %i)" % (verts_blenddata_idx, len(verts_loc_split))) + + # Create meshes from the data, warning 'vertex_groups' wont support splitting + #~ print(dataname, use_vnor, use_vtex) + create_mesh(new_objects, + use_edges, + verts_loc_split, + verts_nor if use_vnor else [], + verts_tex if use_vtex else [], + faces_split, + unique_materials_split, + unique_smooth_groups, + vertex_groups, + weighted_groups, + pos_xforms, + uv_xforms, + dataname, + ) + + # nurbs support + for context_nurbs in nurbs: + create_nurbs(context_nurbs, verts_loc, new_objects) + + view_layer = context.view_layer + collection = view_layer.active_layer_collection.collection + + # Create new obj + for obj in new_objects: + collection.objects.link(obj) + obj.select_set(True) - scene = context.scene -# scn.objects.selected = [] - new_objects = [] # put new objects here + # we could apply this anywhere before scaling. + obj.matrix_world = global_matrix - print('\tbuilding geometry...\n\tverts:%i faces:%i materials: %i smoothgroups:%i ...' % (len(verts_loc), len(faces), len(unique_materials), len(unique_smooth_groups))) - # Split the mesh by objects/materials, may - if use_split_objects or use_split_groups: - SPLIT_OB_OR_GROUP = True - else: - SPLIT_OB_OR_GROUP = False + # MMOBJ: always set smooth on new objects + # http://blender.stackexchange.com/questions/8983/python-apply-object-operator-to-dupli-objects + selected_editable_objects = set() + for obj in new_objects: + selected_editable_objects.add(obj) + context_py = context.copy() + context_py["selected_editable_objects"] = list(selected_editable_objects) + bpy.ops.object.shade_smooth(context_py) - for verts_loc_split, faces_split, unique_materials_split, dataname in split_mesh(verts_loc, faces, unique_materials, filepath, SPLIT_OB_OR_GROUP): - # MMObj: split_mesh doesn't know how to split the weight groups, so if the array counts mismatch, then the groups will point at the wrong verts - if verts_blenddata_idx > 0 and len(verts_loc_split) != verts_blenddata_idx: - raise Exception("split changed vertex count and thus blend weights are invalid: retry with mesh split options disabled (orig vert count: %i, new count: %i)" % (verts_blenddata_idx, len(verts_loc_split))) - - # Create meshes from the data, warning 'vertex_groups' wont support splitting - create_mesh(new_objects, - has_ngons, - use_ngons, - use_edges, - verts_loc_split, - verts_tex, - faces_split, - unique_materials_split, - unique_material_images, - unique_smooth_groups, - vertex_groups, - weighted_groups, - pos_xforms, - uv_xforms, - dataname, - ) - - # nurbs support - for context_nurbs in nurbs: - create_nurbs(context_nurbs, verts_loc, new_objects) - - # Create new obj - for obj in new_objects: - base = scene.objects.link(obj) - base.select = True - - # we could apply this anywhere before scaling. - obj.matrix_world = global_matrix - - # MMOBJ: always set smooth on new objects - # http://blender.stackexchange.com/questions/8983/python-apply-object-operator-to-dupli-objects - selected_editable_objects = set() - for obj in new_objects: - selected_editable_objects.add(obj) - context_py = context.copy() - context_py["selected_editable_objects"] = list(selected_editable_objects) - bpy.ops.object.shade_smooth(context_py) - - scene.update() + view_layer.update() - axis_min = [1000000000] * 3 - axis_max = [-1000000000] * 3 + axis_min = [1000000000] * 3 + axis_max = [-1000000000] * 3 - if global_clamp_size: - # Get all object bounds - for ob in new_objects: - for v in ob.bound_box: - for axis, value in enumerate(v): - if axis_min[axis] > value: - axis_min[axis] = value - if axis_max[axis] < value: - axis_max[axis] = value + if global_clamp_size: + # Get all object bounds + for ob in new_objects: + for v in ob.bound_box: + for axis, value in enumerate(v): + if axis_min[axis] > value: + axis_min[axis] = value + if axis_max[axis] < value: + axis_max[axis] = value - # Scale objects - max_axis = max(axis_max[0] - axis_min[0], axis_max[1] - axis_min[1], axis_max[2] - axis_min[2]) - scale = 1.0 + # Scale objects + max_axis = max(axis_max[0] - axis_min[0], axis_max[1] - axis_min[1], axis_max[2] - axis_min[2]) + scale = 1.0 - while global_clamp_size < max_axis * scale: - scale = scale / 10.0 + while global_clamp_size < max_axis * scale: + scale = scale / 10.0 - for obj in new_objects: - obj.scale = scale, scale, scale + for obj in new_objects: + obj.scale = scale, scale, scale - time_new = time.time() + progress.leave_substeps("Done.") + progress.leave_substeps("Finished importing: %r" % filepath) - print("finished importing: %r in %.4f sec." % (filepath, (time_new - time_main))) return {'FINISHED'}