New Addon: Import Autodesk .max #105013

Closed
Sebastian Sille wants to merge 136 commits from (deleted):nrgsille-import_max into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
4 changed files with 205 additions and 11 deletions
Showing only changes of commit 8359b550f6 - Show all commits

View File

@ -18,7 +18,7 @@ import bpy
bl_info = { bl_info = {
"name": "Autodesk 3DS format", "name": "Autodesk 3DS format",
"author": "Bob Holcomb, Campbell Barton, Andreas Atteneder, Sebastian Schrand", "author": "Bob Holcomb, Campbell Barton, Andreas Atteneder, Sebastian Schrand",
"version": (2, 4, 3), "version": (2, 4, 4),
"blender": (3, 6, 0), "blender": (3, 6, 0),
"location": "File > Import-Export", "location": "File > Import-Export",
"description": "3DS Import/Export meshes, UVs, materials, textures, " "description": "3DS Import/Export meshes, UVs, materials, textures, "
@ -40,15 +40,15 @@ if "bpy" in locals():
@orientation_helper(axis_forward='Y', axis_up='Z') @orientation_helper(axis_forward='Y', axis_up='Z')
class Import3DS(bpy.types.Operator, ImportHelper): class Import3DS(bpy.types.Operator, ImportHelper):
"""Import from 3DS file format (.3ds)""" """Import from 3DS file format (.3ds)"""
bl_idname = "import_scene.autodesk_3ds" bl_idname = "import_scene.max3ds"
bl_label = 'Import 3DS' bl_label = 'Import 3DS'
bl_options = {'UNDO'} bl_options = {'PRESET', 'UNDO'}
filename_ext = ".3ds" filename_ext = ".3ds"
filter_glob: StringProperty(default="*.3ds", options={'HIDDEN'}) filter_glob: StringProperty(default="*.3ds", options={'HIDDEN'})
constrain_size: FloatProperty( constrain_size: FloatProperty(
name="Size Constraint", name="Constrain",
description="Scale the model by 10 until it reaches the " description="Scale the model by 10 until it reaches the "
"size constraint (0 to disable)", "size constraint (0 to disable)",
min=0.0, max=1000.0, min=0.0, max=1000.0,
@ -98,12 +98,70 @@ class Import3DS(bpy.types.Operator, ImportHelper):
return import_3ds.load(self, context, **keywords) return import_3ds.load(self, context, **keywords)
def draw(self, context):
pass
class MAX3DS_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_max3ds"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = True
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, "use_image_search")
layout.prop(operator, "read_keyframe")
class MAX3DS_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_max3ds"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, "constrain_size")
layout.prop(operator, "convert_measure")
layout.prop(operator, "use_apply_transform")
layout.prop(operator, "use_world_matrix")
layout.prop(operator, "axis_forward")
layout.prop(operator, "axis_up")
@orientation_helper(axis_forward='Y', axis_up='Z') @orientation_helper(axis_forward='Y', axis_up='Z')
class Export3DS(bpy.types.Operator, ExportHelper): class Export3DS(bpy.types.Operator, ExportHelper):
"""Export to 3DS file format (.3ds)""" """Export to 3DS file format (.3ds)"""
bl_idname = "export_scene.autodesk_3ds" bl_idname = "export_scene.max3ds"
bl_label = 'Export 3DS' bl_label = 'Export 3DS'
bl_options = {'PRESET', 'UNDO'}
filename_ext = ".3ds" filename_ext = ".3ds"
filter_glob: StringProperty( filter_glob: StringProperty(
@ -149,6 +207,61 @@ class Export3DS(bpy.types.Operator, ExportHelper):
return export_3ds.save(self, context, **keywords) return export_3ds.save(self, context, **keywords)
def draw(self, context):
pass
class MAX3DS_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_max3ds"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = True
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, "use_selection")
layout.prop(operator, "use_hierarchy")
layout.prop(operator, "write_keyframe")
class MAX3DS_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_max3ds"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
sfile = context.space_data
operator = sfile.active_operator
layout.prop(operator, "scale_factor")
layout.prop(operator, "axis_forward")
layout.prop(operator, "axis_up")
# Add to a menu # Add to a menu
def menu_func_export(self, context): def menu_func_export(self, context):
@ -161,19 +274,25 @@ def menu_func_import(self, context):
def register(): def register():
bpy.utils.register_class(Import3DS) bpy.utils.register_class(Import3DS)
bpy.utils.register_class(MAX3DS_PT_import_include)
bpy.utils.register_class(MAX3DS_PT_import_transform)
bpy.utils.register_class(Export3DS) bpy.utils.register_class(Export3DS)
bpy.utils.register_class(MAX3DS_PT_export_include)
bpy.utils.register_class(MAX3DS_PT_export_transform)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import) bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export) bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister(): def unregister():
bpy.utils.unregister_class(Import3DS) bpy.utils.unregister_class(Import3DS)
bpy.utils.unregister_class(MAX3DS_PT_import_include)
bpy.utils.unregister_class(MAX3DS_PT_import_transform)
bpy.utils.unregister_class(Export3DS) bpy.utils.unregister_class(Export3DS)
bpy.utils.unregister_class(MAX3DS_PT_export_include)
bpy.utils.unregister_class(MAX3DS_PT_export_transform)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
if __name__ == "__main__": if __name__ == "__main__":
register() register()

View File

@ -5,7 +5,7 @@
bl_info = { bl_info = {
"name": "FBX format", "name": "FBX format",
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem", "author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
"version": (5, 5, 0), "version": (5, 5, 1),
"blender": (3, 6, 0), "blender": (3, 6, 0),
"location": "File > Import-Export", "location": "File > Import-Export",
"description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions", "description": "FBX IO meshes, UVs, vertex colors, materials, textures, cameras, lamps and actions",

View File

@ -67,6 +67,9 @@ MAT_CONVERT_BONE = Matrix()
BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'} BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}
BLENDER_OBJECT_TYPES_MESHLIKE = {'MESH'} | BLENDER_OTHER_OBJECT_TYPES BLENDER_OBJECT_TYPES_MESHLIKE = {'MESH'} | BLENDER_OTHER_OBJECT_TYPES
SHAPE_KEY_SLIDER_HARD_MIN = bpy.types.ShapeKey.bl_rna.properties["slider_min"].hard_min
SHAPE_KEY_SLIDER_HARD_MAX = bpy.types.ShapeKey.bl_rna.properties["slider_max"].hard_max
# Lamps. # Lamps.
FBX_LIGHT_TYPES = { FBX_LIGHT_TYPES = {
@ -600,6 +603,49 @@ def ensure_object_not_in_edit_mode(context, obj):
return True return True
def expand_shape_key_range(shape_key, value_to_fit):
"""Attempt to expand the slider_min/slider_max of a shape key to fit `value_to_fit` within the slider range,
expanding slightly beyond `value_to_fit` if possible, so that the new slider_min/slider_max is not the same as
`value_to_fit`. Blender has a hard minimum and maximum for slider values, so it may not be possible to fit the value
within the slider range.
If `value_to_fit` is already within the slider range, no changes are made.
First tries setting slider_min/slider_max to double `value_to_fit`, otherwise, expands the range in the direction of
`value_to_fit` by double the distance to `value_to_fit`.
The new slider_min/slider_max is rounded down/up to the nearest whole number for a more visually pleasing result.
Returns whether it was possible to expand the slider range to fit `value_to_fit`."""
if value_to_fit < (slider_min := shape_key.slider_min):
if value_to_fit < 0.0:
# For the most common case, set slider_min to double value_to_fit.
target_slider_min = value_to_fit * 2.0
else:
# Doubling value_to_fit would make it larger, so instead decrease slider_min by double the distance between
# slider_min and value_to_fit.
target_slider_min = slider_min - (slider_min - value_to_fit) * 2.0
# Set slider_min to the first whole number less than or equal to target_slider_min.
shape_key.slider_min = math.floor(target_slider_min)
return value_to_fit >= SHAPE_KEY_SLIDER_HARD_MIN
elif value_to_fit > (slider_max := shape_key.slider_max):
if value_to_fit > 0.0:
# For the most common case, set slider_max to double value_to_fit.
target_slider_max = value_to_fit * 2.0
else:
# Doubling value_to_fit would make it smaller, so instead increase slider_max by double the distance between
# slider_max and value_to_fit.
target_slider_max = slider_max + (value_to_fit - slider_max) * 2.0
# Set slider_max to the first whole number greater than or equal to target_slider_max.
shape_key.slider_max = math.ceil(target_slider_max)
return value_to_fit <= SHAPE_KEY_SLIDER_HARD_MAX
else:
# Value is already within the range.
return True
# ##### Attribute utils. ##### # ##### Attribute utils. #####
AttributeDataTypeInfo = namedtuple("AttributeDataTypeInfo", ["dtype", "foreach_attribute", "item_size"]) AttributeDataTypeInfo = namedtuple("AttributeDataTypeInfo", ["dtype", "foreach_attribute", "item_size"])
_attribute_data_type_info_lookup = { _attribute_data_type_info_lookup = {

View File

@ -47,6 +47,7 @@ from .fbx_utils import (
MESH_ATTRIBUTE_CORNER_VERT, MESH_ATTRIBUTE_CORNER_VERT,
MESH_ATTRIBUTE_SHARP_FACE, MESH_ATTRIBUTE_SHARP_FACE,
MESH_ATTRIBUTE_SHARP_EDGE, MESH_ATTRIBUTE_SHARP_EDGE,
expand_shape_key_range,
) )
# global singleton, assign on execution # global singleton, assign on execution
@ -563,7 +564,7 @@ def blen_read_animations_curves_iter(fbx_curves, blen_start_offset, fbx_start_of
yield (curr_blenkframe, curr_values) yield (curr_blenkframe, curr_values)
def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, global_scale): def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, global_scale, shape_key_deforms):
""" """
'Bake' loc/rot/scale into the action, 'Bake' loc/rot/scale into the action,
taking any pre_ and post_ matrix into account to transform from fbx into blender space. taking any pre_ and post_ matrix into account to transform from fbx into blender space.
@ -635,12 +636,14 @@ def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset, glo
store_keyframe(fc, frame, v) store_keyframe(fc, frame, v)
elif isinstance(item, ShapeKey): elif isinstance(item, ShapeKey):
deform_values = shape_key_deforms.setdefault(item, [])
for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps): for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
value = 0.0 value = 0.0
for v, (fbxprop, channel, _fbx_acdata) in values: for v, (fbxprop, channel, _fbx_acdata) in values:
assert(fbxprop == b'DeformPercent') assert(fbxprop == b'DeformPercent')
assert(channel == 0) assert(channel == 0)
value = v / 100.0 value = v / 100.0
deform_values.append(value)
for fc, v in zip(blen_curves, (value,)): for fc, v in zip(blen_curves, (value,)):
store_keyframe(fc, frame, v) store_keyframe(fc, frame, v)
@ -741,6 +744,7 @@ def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_o
""" """
from bpy.types import ShapeKey, Material, Camera from bpy.types import ShapeKey, Material, Camera
shape_key_values = {}
actions = {} actions = {}
for as_uuid, ((fbx_asdata, _blen_data), alayers) in stacks.items(): for as_uuid, ((fbx_asdata, _blen_data), alayers) in stacks.items():
stack_name = elem_name_ensure_class(fbx_asdata, b'AnimStack') stack_name = elem_name_ensure_class(fbx_asdata, b'AnimStack')
@ -778,7 +782,22 @@ def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_o
if not id_data.animation_data.action: if not id_data.animation_data.action:
id_data.animation_data.action = action id_data.animation_data.action = action
# And actually populate the action! # And actually populate the action!
blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset, global_scale) blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset, global_scale,
shape_key_values)
# If the minimum/maximum animated value is outside the slider range of the shape key, attempt to expand the slider
# range until the animated range fits and has extra room to be decreased or increased further.
# Shape key slider_min and slider_max have hard min/max values, if an imported animation uses a value outside that
# range, a warning message will be printed to the console and the slider_min/slider_max values will end up clamped.
shape_key_values_in_range = True
for shape_key, deform_values in shape_key_values.items():
min_animated_deform = min(deform_values)
max_animated_deform = max(deform_values)
shape_key_values_in_range &= expand_shape_key_range(shape_key, min_animated_deform)
shape_key_values_in_range &= expand_shape_key_range(shape_key, max_animated_deform)
if not shape_key_values_in_range:
print("WARNING: The imported animated Value of a Shape Key is beyond the minimum/maximum allowed and will be"
" clamped during playback.")
# ---- # ----
@ -1599,6 +1618,9 @@ def blen_read_shapes(fbx_tmpl, fbx_data, objects, me, scene):
objects = list({node.bl_obj for node in objects}) objects = list({node.bl_obj for node in objects})
assert(objects) assert(objects)
# Blender has a hard minimum and maximum shape key Value. If an imported shape key has a value outside this range it
# will be clamped, and we'll print a warning message to the console.
shape_key_values_in_range = True
bc_uuid_to_keyblocks = {} bc_uuid_to_keyblocks = {}
for bc_uuid, fbx_sdata, fbx_bcdata in fbx_data: for bc_uuid, fbx_sdata, fbx_bcdata in fbx_data:
elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry') elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry')
@ -1644,6 +1666,8 @@ def blen_read_shapes(fbx_tmpl, fbx_data, objects, me, scene):
shape_cos[indices] += dvcos shape_cos[indices] += dvcos
kb.data.foreach_set("co", shape_cos.ravel()) kb.data.foreach_set("co", shape_cos.ravel())
shape_key_values_in_range &= expand_shape_key_range(kb, weight)
kb.value = weight kb.value = weight
# Add vgroup if necessary. # Add vgroup if necessary.
@ -1657,6 +1681,11 @@ def blen_read_shapes(fbx_tmpl, fbx_data, objects, me, scene):
kb.vertex_group = kb.name kb.vertex_group = kb.name
bc_uuid_to_keyblocks.setdefault(bc_uuid, []).append(kb) bc_uuid_to_keyblocks.setdefault(bc_uuid, []).append(kb)
if not shape_key_values_in_range:
print("WARNING: The imported Value of a Shape Key on the Mesh '%s' is beyond the minimum/maximum allowed and"
" has been clamped." % me.name)
return bc_uuid_to_keyblocks return bc_uuid_to_keyblocks