io_scene_3ds: Take scene units into account for export #104775
@ -18,7 +18,7 @@ import bpy
|
||||
bl_info = {
|
||||
"name": "Autodesk 3DS format",
|
||||
"author": "Bob Holcomb, Campbell Barton, Andreas Atteneder, Sebastian Schrand",
|
||||
"version": (2, 4, 3),
|
||||
"version": (2, 4, 4),
|
||||
"blender": (3, 6, 0),
|
||||
"location": "File > Import-Export",
|
||||
"description": "3DS Import/Export meshes, UVs, materials, textures, "
|
||||
@ -40,15 +40,15 @@ if "bpy" in locals():
|
||||
@orientation_helper(axis_forward='Y', axis_up='Z')
|
||||
class Import3DS(bpy.types.Operator, ImportHelper):
|
||||
"""Import from 3DS file format (.3ds)"""
|
||||
bl_idname = "import_scene.autodesk_3ds"
|
||||
bl_idname = "import_scene.max3ds"
|
||||
bl_label = 'Import 3DS'
|
||||
bl_options = {'UNDO'}
|
||||
bl_options = {'PRESET', 'UNDO'}
|
||||
|
||||
filename_ext = ".3ds"
|
||||
filter_glob: StringProperty(default="*.3ds", options={'HIDDEN'})
|
||||
|
||||
constrain_size: FloatProperty(
|
||||
name="Size Constraint",
|
||||
name="Constrain",
|
||||
description="Scale the model by 10 until it reaches the "
|
||||
"size constraint (0 to disable)",
|
||||
min=0.0, max=1000.0,
|
||||
@ -98,12 +98,70 @@ class Import3DS(bpy.types.Operator, ImportHelper):
|
||||
|
||||
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')
|
||||
class Export3DS(bpy.types.Operator, ExportHelper):
|
||||
"""Export to 3DS file format (.3ds)"""
|
||||
bl_idname = "export_scene.autodesk_3ds"
|
||||
bl_idname = "export_scene.max3ds"
|
||||
bl_label = 'Export 3DS'
|
||||
bl_options = {'PRESET', 'UNDO'}
|
||||
|
||||
filename_ext = ".3ds"
|
||||
filter_glob: StringProperty(
|
||||
@ -149,6 +207,61 @@ class Export3DS(bpy.types.Operator, ExportHelper):
|
||||
|
||||
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
|
||||
def menu_func_export(self, context):
|
||||
@ -161,16 +274,22 @@ def menu_func_import(self, context):
|
||||
|
||||
def register():
|
||||
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(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_export.append(menu_func_export)
|
||||
|
||||
|
||||
def unregister():
|
||||
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(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_export.remove(menu_func_export)
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
bl_info = {
|
||||
"name": "FBX format",
|
||||
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier, @Mysteryem",
|
||||
"version": (5, 5, 0),
|
||||
"version": (5, 5, 1),
|
||||
"blender": (3, 6, 0),
|
||||
"location": "File > Import-Export",
|
||||
"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_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.
|
||||
FBX_LIGHT_TYPES = {
|
||||
@ -600,6 +603,49 @@ def ensure_object_not_in_edit_mode(context, obj):
|
||||
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. #####
|
||||
AttributeDataTypeInfo = namedtuple("AttributeDataTypeInfo", ["dtype", "foreach_attribute", "item_size"])
|
||||
_attribute_data_type_info_lookup = {
|
||||
|
@ -47,6 +47,7 @@ from .fbx_utils import (
|
||||
MESH_ATTRIBUTE_CORNER_VERT,
|
||||
MESH_ATTRIBUTE_SHARP_FACE,
|
||||
MESH_ATTRIBUTE_SHARP_EDGE,
|
||||
expand_shape_key_range,
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
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,
|
||||
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)
|
||||
|
||||
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):
|
||||
value = 0.0
|
||||
for v, (fbxprop, channel, _fbx_acdata) in values:
|
||||
assert(fbxprop == b'DeformPercent')
|
||||
assert(channel == 0)
|
||||
value = v / 100.0
|
||||
deform_values.append(value)
|
||||
|
||||
for fc, v in zip(blen_curves, (value,)):
|
||||
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
|
||||
|
||||
shape_key_values = {}
|
||||
actions = {}
|
||||
for as_uuid, ((fbx_asdata, _blen_data), alayers) in stacks.items():
|
||||
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:
|
||||
id_data.animation_data.action = 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})
|
||||
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 = {}
|
||||
for bc_uuid, fbx_sdata, fbx_bcdata in fbx_data:
|
||||
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
|
||||
kb.data.foreach_set("co", shape_cos.ravel())
|
||||
|
||||
shape_key_values_in_range &= expand_shape_key_range(kb, weight)
|
||||
|
||||
kb.value = weight
|
||||
|
||||
# Add vgroup if necessary.
|
||||
@ -1657,6 +1681,11 @@ def blen_read_shapes(fbx_tmpl, fbx_data, objects, me, scene):
|
||||
kb.vertex_group = kb.name
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user