io_scene_3ds: Take scene units into account for import #104774
@ -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()
|
@ -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",
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user