Pose Library: Update to use the asset shelf (when enabled) #104546

Merged
Julian Eisel merged 33 commits from asset-shelf into main 2023-08-04 15:00:21 +02:00
13 changed files with 1006 additions and 339 deletions
Showing only changes of commit 8dd4186d2c - Show all commits

View File

@ -700,7 +700,7 @@ class torus_knot_plus(Operator, AddObjectHelper):
print("Approximate average TK length = %.2f" % avgTKLen)
# x N factor = control points per unit length
self.torus_res = max(3, avgTKLen / links * 8)
self.torus_res = max(3, int(avgTKLen / links) * 8)
# update align matrix
self.align_matrix = align_matrix(self, context)

View File

@ -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, 6),
"blender": (3, 6, 0),
"location": "File > Import-Export",
"description": "3DS Import/Export meshes, UVs, materials, textures, "
@ -40,35 +40,51 @@ 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 Size",
description="Scale the model by 10 until it reaches the "
"size constraint (0 to disable)",
min=0.0, max=1000.0,
soft_min=0.0, soft_max=1000.0,
default=10.0,
)
use_scene_unit: BoolProperty(
name="Scene Units",
description="Converts to scene unit length settings",
default=False,
)
use_image_search: BoolProperty(
name="Image Search",
description="Search subdirectories for any associated images "
"(Warning, may be slow)",
default=True,
)
object_filter: EnumProperty(
name="Object Filter", options={'ENUM_FLAG'},
items=(('WORLD', "World".rjust(11), "", 'WORLD_DATA', 0x1),
('MESH', "Mesh".rjust(11), "", 'MESH_DATA', 0x2),
('LIGHT', "Light".rjust(12), "", 'LIGHT_DATA', 0x4),
('CAMERA', "Camera".rjust(11), "", 'CAMERA_DATA', 0x8),
('EMPTY', "Empty".rjust(11), "", 'EMPTY_AXIS', 0x10),
),
description="Object types to import",
default={'WORLD', 'MESH', 'LIGHT', 'CAMERA', 'EMPTY'},
)
use_apply_transform: BoolProperty(
name="Apply Transform",
description="Workaround for object transformations "
"importing incorrectly",
default=True,
)
read_keyframe: BoolProperty(
name="Read Keyframe",
use_keyframes: BoolProperty(
name="Animation",
description="Read the keyframe data",
default=True,
)
@ -77,6 +93,11 @@ class Import3DS(bpy.types.Operator, ImportHelper):
description="Transform to matrix world",
default=False,
)
use_cursor: BoolProperty(
name="Cursor Origin",
description="Read the 3D cursor location",
default=False,
)
def execute(self, context):
from . import import_3ds
@ -93,12 +114,84 @@ 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
layrow = layout.row(align=True)
layrow.prop(operator, "use_image_search")
layrow.label(text="", icon='OUTLINER_OB_IMAGE' if operator.use_image_search else 'IMAGE_DATA')
layout.column().prop(operator, "object_filter")
layrow = layout.row(align=True)
layrow.prop(operator, "use_keyframes")
layrow.label(text="", icon='ANIM' if operator.use_keyframes else 'DECORATE_DRIVER')
layrow = layout.row(align=True)
layrow.prop(operator, "use_cursor")
layrow.label(text="", icon='PIVOT_CURSOR' if operator.use_cursor else 'CURSOR')
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")
layrow = layout.row(align=True)
layrow.prop(operator, "use_scene_unit")
layrow.label(text="", icon='EMPTY_ARROWS' if operator.use_scene_unit else 'EMPTY_DATA')
layrow = layout.row(align=True)
layrow.prop(operator, "use_apply_transform")
layrow.label(text="", icon='MESH_CUBE' if operator.use_apply_transform else 'MOD_SOLIDIFY')
layrow = layout.row(align=True)
layrow.prop(operator, "use_world_matrix")
layrow.label(text="", icon='WORLD' if operator.use_world_matrix else 'META_BALL')
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(
@ -106,21 +199,49 @@ class Export3DS(bpy.types.Operator, ExportHelper):
options={'HIDDEN'},
)
scale_factor: FloatProperty(
name="Scale Factor",
description="Master scale factor for all objects",
min=0.0, max=100000.0,
soft_min=0.0, soft_max=100000.0,
default=1.0,
)
use_scene_unit: BoolProperty(
name="Scene Units",
description="Take the scene unit length settings into account",
default=False,
)
use_selection: BoolProperty(
name="Selection Only",
name="Selection",
description="Export selected objects only",
default=False,
)
use_hierarchy: bpy.props.BoolProperty(
name="Export Hierarchy",
object_filter: EnumProperty(
name="Object Filter", options={'ENUM_FLAG'},
items=(('WORLD', "World".rjust(11), "", 'WORLD_DATA',0x1),
('MESH', "Mesh".rjust(11), "", 'MESH_DATA', 0x2),
('LIGHT', "Light".rjust(12), "", 'LIGHT_DATA',0x4),
('CAMERA', "Camera".rjust(11), "", 'CAMERA_DATA',0x8),
('EMPTY', "Empty".rjust(11), "", 'EMPTY_AXIS',0x10),
),
description="Object types to export",
default={'WORLD', 'MESH', 'LIGHT', 'CAMERA', 'EMPTY'},
)
use_hierarchy: BoolProperty(
name="Hierarchy",
description="Export hierarchy chunks",
default=False,
)
write_keyframe: BoolProperty(
name="Write Keyframe",
use_keyframes: BoolProperty(
name="Animation",
description="Write the keyframe data",
default=False,
)
use_cursor: BoolProperty(
name="Cursor Origin",
description="Save the 3D cursor location",
default=False,
)
def execute(self, context):
from . import export_3ds
@ -137,6 +258,74 @@ 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
layrow = layout.row(align=True)
layrow.prop(operator, "use_selection")
layrow.label(text="", icon='RESTRICT_SELECT_OFF' if operator.use_selection else 'RESTRICT_SELECT_ON')
layout.column().prop(operator, "object_filter")
layrow = layout.row(align=True)
layrow.prop(operator, "use_hierarchy")
layrow.label(text="", icon='OUTLINER' if operator.use_hierarchy else 'CON_CHILDOF')
layrow = layout.row(align=True)
layrow.prop(operator, "use_keyframes")
layrow.label(text="", icon='ANIM' if operator.use_keyframes else 'DECORATE_DRIVER')
layrow = layout.row(align=True)
layrow.prop(operator, "use_cursor")
layrow.label(text="", icon='PIVOT_CURSOR' if operator.use_cursor else 'CURSOR')
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")
layrow = layout.row(align=True)
layrow.prop(operator, "use_scene_unit")
layrow.label(text="", icon='EMPTY_ARROWS' if operator.use_scene_unit else 'EMPTY_DATA')
layout.prop(operator, "axis_forward")
layout.prop(operator, "axis_up")
# Add to a menu
def menu_func_export(self, context):
@ -149,16 +338,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)

View File

@ -24,13 +24,24 @@ from bpy_extras import node_shader_utils
PRIMARY = 0x4D4D
# >----- Main Chunks
OBJECTINFO = 0x3D3D # Main mesh object chunk before material and object information
MESHVERSION = 0x3D3E # This gives the version of the mesh
VERSION = 0x0002 # This gives the version of the .3ds file
KFDATA = 0xB000 # This is the header for all of the keyframe info
# >----- sub defines of OBJECTINFO
OBJECTINFO = 0x3D3D # Main mesh object chunk before material and object information
MESHVERSION = 0x3D3E # This gives the version of the mesh
BITMAP = 0x1100 # The background image name
USE_BITMAP = 0x1101 # The background image flag
SOLIDBACKGND = 0x1200 # The background color (RGB)
USE_SOLIDBGND = 0x1201 # The background color flag
VGRADIENT = 0x1300 # The background gradient colors
USE_VGRADIENT = 0x1301 # The background gradient flag
O_CONSTS = 0x1500 # The origin of the 3D cursor
AMBIENTLIGHT = 0x2100 # The color of the ambient light
FOG = 0x2200 # The fog atmosphere settings
USE_FOG = 0x2201 # The fog atmosphere flag
LAYER_FOG = 0x2302 # The fog layer atmosphere settings
USE_LAYER_FOG = 0x2303 # The fog layer atmosphere flag
MATERIAL = 45055 # 0xAFFF // This stored the texture info
OBJECT = 16384 # 0x4000 // This stores the faces, vertices, etc...
@ -563,6 +574,7 @@ def make_material_texture_chunk(chunk_id, texslots, pct):
def add_texslot(texslot):
image = texslot.image
socket = None
filename = bpy.path.basename(image.filepath)
mat_sub_file = _3ds_chunk(MAT_MAP_FILE)
@ -659,7 +671,7 @@ def make_material_chunk(material, image):
material_chunk.add_subchunk(make_material_subchunk(MATDIFFUSE, (0.8, 0.8, 0.8)))
material_chunk.add_subchunk(make_material_subchunk(MATSPECULAR, (1.0, 1.0, 1.0)))
material_chunk.add_subchunk(make_percent_subchunk(MATSHINESS, 0.8))
material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, 1))
material_chunk.add_subchunk(make_percent_subchunk(MATSHIN2, 0.5))
material_chunk.add_subchunk(shading)
elif material and material.use_nodes:
@ -1045,6 +1057,19 @@ def make_mesh_chunk(ob, mesh, matrix, materialDict, translation):
return mesh_chunk
def calc_target(posi, tilt=0.0, pan=0.0):
"""Calculate target position for cameras and spotlights."""
adjacent = math.radians(90)
turn = 0.0 if abs(pan) < adjacent else -0.0
lean = 0.0 if abs(tilt) > adjacent else -0.0
diagonal = math.sqrt(pow(posi.x ,2) + pow(posi.y ,2))
target_x = math.copysign(posi.x + (posi.y * math.tan(pan)), pan)
target_y = math.copysign(posi.y + (posi.x * math.tan(adjacent - pan)), turn)
target_z = math.copysign(posi.z + diagonal * math.tan(adjacent - tilt), lean)
return target_x, target_y, target_z
#################
# KEYFRAME DATA #
#################
@ -1070,6 +1095,7 @@ def make_kfdata(revision, start=0, stop=100, curtime=0):
kfdata.add_subchunk(kfcurtime)
return kfdata
def make_track_chunk(ID, ob, ob_pos, ob_rot, ob_size):
"""Make a chunk for track data. Depending on the ID, this will construct
a position, rotation, scale, roll, color, fov, hotspot or falloff track."""
@ -1097,9 +1123,10 @@ def make_track_chunk(ID, ob, ob_pos, ob_rot, ob_size):
pos_x = next((tc.evaluate(frame) for tc in pos_track if tc.array_index == 0), ob_pos.x)
pos_y = next((tc.evaluate(frame) for tc in pos_track if tc.array_index == 1), ob_pos.y)
pos_z = next((tc.evaluate(frame) for tc in pos_track if tc.array_index == 2), ob_pos.z)
pos = ob_size @ mathutils.Vector((pos_x, pos_y, pos_z))
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("position", _3ds_point_3d((pos_x, pos_y, pos_z)))
track_chunk.add_variable("position", _3ds_point_3d((pos.x, pos.y, pos.z)))
elif ID == ROT_TRACK_TAG: # Rotation
for i, frame in enumerate(kframes):
@ -1110,14 +1137,14 @@ def make_track_chunk(ID, ob, ob_pos, ob_rot, ob_size):
quat = mathutils.Euler((rot_x, rot_y, rot_z)).to_quaternion().inverted()
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("rotation", _3ds_point_4d((quat.angle, quat.axis[0], quat.axis[1], quat.axis[2])))
track_chunk.add_variable("rotation", _3ds_point_4d((quat.angle, quat.axis.x, quat.axis.y, quat.axis.z)))
elif ID == SCL_TRACK_TAG: # Scale
for i, frame in enumerate(kframes):
scale_track = [fc for fc in fcurves if fc is not None and fc.data_path == 'scale']
size_x = next((tc.evaluate(frame) for tc in scale_track if tc.array_index == 0), ob_size[0])
size_y = next((tc.evaluate(frame) for tc in scale_track if tc.array_index == 1), ob_size[1])
size_z = next((tc.evaluate(frame) for tc in scale_track if tc.array_index == 2), ob_size[2])
size_x = next((tc.evaluate(frame) for tc in scale_track if tc.array_index == 0), ob_size.x)
size_y = next((tc.evaluate(frame) for tc in scale_track if tc.array_index == 1), ob_size.y)
size_z = next((tc.evaluate(frame) for tc in scale_track if tc.array_index == 2), ob_size.z)
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("scale", _3ds_point_3d((size_x, size_y, size_z)))
@ -1194,7 +1221,7 @@ def make_track_chunk(ID, ob, ob_pos, ob_rot, ob_size):
elif ID == ROT_TRACK_TAG: # Rotation (angle first [radians], followed by axis)
quat = ob_rot.to_quaternion().inverted()
track_chunk.add_variable("rotation", _3ds_point_4d((quat.angle, quat.axis[0], quat.axis[1], quat.axis[2])))
track_chunk.add_variable("rotation", _3ds_point_4d((quat.angle, quat.axis.x, quat.axis.y, quat.axis.z)))
elif ID == SCL_TRACK_TAG: # Scale vector
track_chunk.add_variable("scale", _3ds_point_3d(ob_size))
@ -1302,18 +1329,19 @@ def make_object_node(ob, translation, rotation, scale, name_id):
obj_morph_smooth.add_variable("angle", _3ds_float(round(ob.data.auto_smooth_angle, 6)))
obj_node.add_subchunk(obj_morph_smooth)
# Add track chunks for color, position, rotation and scale
# Add track chunks for position, rotation, size
ob_scale = scale[name] # and collect masterscale
if parent is None or (parent.name not in name_id):
ob_pos = translation[name]
ob_rot = rotation[name]
ob_size = scale[name]
ob_size = ob.scale
else: # Calculate child position and rotation of the object center, no scale applied
ob_pos = translation[name] - translation[parent.name]
ob_rot = rotation[name].to_quaternion().cross(rotation[parent.name].to_quaternion().copy().inverted()).to_euler()
ob_size = (1.0, 1.0, 1.0)
ob_size = mathutils.Vector((1.0, 1.0, 1.0))
obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, ob, ob_pos, ob_rot, ob_scale))
if ob.type in {'MESH', 'EMPTY'}:
obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, ob, ob_pos, ob_rot, ob_size))
@ -1360,12 +1388,8 @@ def make_target_node(ob, translation, rotation, scale, name_id):
# Calculate target position
ob_pos = translation[name]
ob_rot = rotation[name]
ob_size = scale[name]
diagonal = math.copysign(math.sqrt(pow(ob_pos.x, 2) + pow(ob_pos.y, 2)), ob_pos.y)
target_x = -1 * math.copysign(ob_pos.x + (ob_pos.y * math.tan(ob_rot.z)), ob_rot.x)
target_y = -1 * math.copysign(ob_pos.y + (ob_pos.x * math.tan(math.radians(90) - ob_rot.z)), ob_rot.z)
target_z = -1 * math.copysign(diagonal * math.tan(math.radians(90) - ob_rot.x), ob_pos.z)
ob_scale = scale[name]
target_pos = calc_target(ob_pos, ob_rot.x, ob_rot.z)
# Add track chunks for target position
track_chunk = _3ds_chunk(POS_TRACK_TAG)
@ -1394,13 +1418,11 @@ def make_target_node(ob, translation, rotation, scale, name_id):
rot_target = [fc for fc in fcurves if fc is not None and fc.data_path == 'rotation_euler']
rot_x = next((tc.evaluate(frame) for tc in rot_target if tc.array_index == 0), ob_rot.x)
rot_z = next((tc.evaluate(frame) for tc in rot_target if tc.array_index == 2), ob_rot.z)
diagonal = math.copysign(math.sqrt(pow(loc_x, 2) + pow(loc_y, 2)), loc_y)
target_x = -1 * math.copysign(loc_x + (loc_y * math.tan(rot_z)), rot_x)
target_y = -1 * math.copysign(loc_y + (loc_x * math.tan(math.radians(90) - rot_z)), rot_z)
target_z = -1 * math.copysign(diagonal * math.tan(math.radians(90) - rot_x), loc_z)
target_distance = ob_scale @ mathutils.Vector((loc_x, loc_y, loc_z))
target_pos = calc_target(target_distance, rot_x, rot_z)
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("position", _3ds_point_3d((target_x, target_y, target_z)))
track_chunk.add_variable("position", _3ds_point_3d(target_pos))
else: # Track header
track_chunk.add_variable("track_flags", _3ds_ushort(0x40)) # Based on observation default flag is 0x40
@ -1410,7 +1432,7 @@ def make_target_node(ob, translation, rotation, scale, name_id):
# Keyframe header
track_chunk.add_variable("tcb_frame", _3ds_uint(0))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("position", _3ds_point_3d((target_x, target_y, target_z)))
track_chunk.add_variable("position", _3ds_point_3d(target_pos))
tar_node.add_subchunk(track_chunk)
@ -1437,7 +1459,38 @@ def make_ambient_node(world):
amb_node_header_chunk.add_variable("parent", _3ds_ushort(ROOT_OBJECT))
amb_node.add_subchunk(amb_node_header_chunk)
if world.animation_data.action:
if world.use_nodes and world.node_tree.animation_data.action:
ambioutput = 'EMISSION' ,'MIX_SHADER', 'WORLD_OUTPUT'
action = world.node_tree.animation_data.action
links = world.node_tree.links
ambilinks = [lk for lk in links if lk.from_node.type in {'EMISSION', 'RGB'} and lk.to_node.type in ambioutput]
if ambilinks and action.fcurves:
fcurves = action.fcurves
fcurves.update()
emission = next((lk.from_socket.node for lk in ambilinks if lk.to_node.type in ambioutput), False)
ambinode = next((lk.from_socket.node for lk in ambilinks if lk.to_node.type == 'EMISSION'), emission)
kframes = [kf.co[0] for kf in [fc for fc in fcurves if fc is not None][0].keyframe_points]
ambipath = ('nodes[\"RGB\"].outputs[0].default_value' if ambinode and ambinode.type == 'RGB' else
'nodes[\"Emission\"].inputs[0].default_value')
nkeys = len(kframes)
if not 0 in kframes:
kframes.append(0)
nkeys = nkeys + 1
kframes = sorted(set(kframes))
track_chunk.add_variable("track_flags", _3ds_ushort(0x40))
track_chunk.add_variable("frame_start", _3ds_uint(int(action.frame_start)))
track_chunk.add_variable("frame_total", _3ds_uint(int(action.frame_end)))
track_chunk.add_variable("nkeys", _3ds_uint(nkeys))
for i, frame in enumerate(kframes):
ambient = [fc.evaluate(frame) for fc in fcurves if fc is not None and fc.data_path == ambipath]
if not ambient:
ambient = amb_color
track_chunk.add_variable("tcb_frame", _3ds_uint(int(frame)))
track_chunk.add_variable("tcb_flags", _3ds_ushort())
track_chunk.add_variable("color", _3ds_float_color(ambient[:3]))
elif world.animation_data.action:
action = world.animation_data.action
if action.fcurves:
fcurves = action.fcurves
@ -1480,17 +1533,41 @@ def make_ambient_node(world):
# EXPORT #
##########
def save(operator, context, filepath="", use_selection=False, use_hierarchy=False, write_keyframe=False, global_matrix=None):
def save(operator, context, filepath="", scale_factor=1.0, use_scene_unit=False, use_selection=False,
object_filter=None, use_hierarchy=False, use_keyframes=False, global_matrix=None, use_cursor=False):
"""Save the Blender scene to a 3ds file."""
# Time the export
duration = time.time()
context.window.cursor_set('WAIT')
scene = context.scene
layer = context.view_layer
depsgraph = context.evaluated_depsgraph_get()
world = scene.world
unit_measure = 1.0
if use_scene_unit:
unit_length = scene.unit_settings.length_unit
if unit_length == 'MILES':
unit_measure = 0.000621371
elif unit_length == 'KILOMETERS':
unit_measure = 0.001
elif unit_length == 'FEET':
unit_measure = 3.280839895
elif unit_length == 'INCHES':
unit_measure = 39.37007874
elif unit_length == 'CENTIMETERS':
unit_measure = 100
elif unit_length == 'MILLIMETERS':
unit_measure = 1000
elif unit_length == 'THOU':
unit_measure = 39370.07874
elif unit_length == 'MICROMETERS':
unit_measure = 1000000
mtx_scale = mathutils.Matrix.Scale((scale_factor * unit_measure),4)
if global_matrix is None:
global_matrix = mathutils.Matrix()
@ -1516,8 +1593,14 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
mscale.add_variable("scale", _3ds_float(1.0))
object_info.add_subchunk(mscale)
# Add 3D cursor location
if use_cursor:
cursor_chunk = _3ds_chunk(O_CONSTS)
cursor_chunk.add_variable("cursor", _3ds_point_3d(scene.cursor.location))
object_info.add_subchunk(cursor_chunk)
# Init main keyframe data chunk
if write_keyframe:
if use_keyframes:
revision = 0x0005
stop = scene.frame_end
start = scene.frame_start
@ -1525,13 +1608,89 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
kfdata = make_kfdata(revision, start, stop, curtime)
# Add AMBIENT color
if world is not None:
if world is not None and 'WORLD' in object_filter:
ambient_chunk = _3ds_chunk(AMBIENTLIGHT)
ambient_light = _3ds_chunk(RGB)
ambient_light.add_variable("ambient", _3ds_float_color(world.color))
ambient_chunk.add_subchunk(ambient_light)
object_info.add_subchunk(ambient_chunk)
if write_keyframe and world.animation_data:
# Add BACKGROUND and BITMAP
if world.use_nodes:
bgtype = 'BACKGROUND'
ntree = world.node_tree.links
background_color_chunk = _3ds_chunk(RGB)
background_chunk = _3ds_chunk(SOLIDBACKGND)
background_flag = _3ds_chunk(USE_SOLIDBGND)
bgmixer = 'BACKGROUND', 'MIX', 'MIX_RGB'
bgshade = 'ADD_SHADER', 'MIX_SHADER', 'OUTPUT_WORLD'
bg_tex = 'TEX_IMAGE', 'TEX_ENVIRONMENT'
bg_color = next((lk.from_node.inputs[0].default_value[:3] for lk in ntree if lk.from_node.type == bgtype and lk.to_node.type in bgshade), world.color)
bg_mixer = next((lk.from_node.type for lk in ntree if lk.from_node.type in bgmixer and lk.to_node.type == bgtype), bgtype)
bg_image = next((lk.from_node.image.name for lk in ntree if lk.from_node.type in bg_tex and lk.to_node.type == bg_mixer), False)
gradient = next((lk.from_node.color_ramp.elements for lk in ntree if lk.from_node.type == 'VALTORGB' and lk.to_node.type in bgmixer), False)
background_color_chunk.add_variable("color", _3ds_float_color(bg_color))
background_chunk.add_subchunk(background_color_chunk)
if bg_image:
background_image = _3ds_chunk(BITMAP)
background_flag = _3ds_chunk(USE_BITMAP)
background_image.add_variable("image", _3ds_string(sane_name(bg_image)))
object_info.add_subchunk(background_image)
object_info.add_subchunk(background_chunk)
# Add VGRADIENT chunk
if gradient and len(gradient) >= 3:
gradient_chunk = _3ds_chunk(VGRADIENT)
background_flag = _3ds_chunk(USE_VGRADIENT)
gradient_chunk.add_variable("midpoint", _3ds_float(gradient[1].position))
gradient_topcolor_chunk = _3ds_chunk(RGB)
gradient_topcolor_chunk.add_variable("color", _3ds_float_color(gradient[2].color[:3]))
gradient_chunk.add_subchunk(gradient_topcolor_chunk)
gradient_midcolor_chunk = _3ds_chunk(RGB)
gradient_midcolor_chunk.add_variable("color", _3ds_float_color(gradient[1].color[:3]))
gradient_chunk.add_subchunk(gradient_midcolor_chunk)
gradient_lowcolor_chunk = _3ds_chunk(RGB)
gradient_lowcolor_chunk.add_variable("color", _3ds_float_color(gradient[0].color[:3]))
gradient_chunk.add_subchunk(gradient_lowcolor_chunk)
object_info.add_subchunk(gradient_chunk)
object_info.add_subchunk(background_flag)
# Add FOG
fognode = next((lk.from_socket.node for lk in ntree if lk.from_socket.node.type == 'VOLUME_ABSORPTION' and lk.to_socket.node.type in bgshade), False)
if fognode:
fog_chunk = _3ds_chunk(FOG)
fog_color_chunk = _3ds_chunk(RGB)
use_fog_flag = _3ds_chunk(USE_FOG)
fog_density = fognode.inputs['Density'].default_value * 100
fog_color_chunk.add_variable("color", _3ds_float_color(fognode.inputs[0].default_value[:3]))
fog_chunk.add_variable("nearplane", _3ds_float(world.mist_settings.start))
fog_chunk.add_variable("nearfog", _3ds_float(fog_density * 0.5))
fog_chunk.add_variable("farplane", _3ds_float(world.mist_settings.depth))
fog_chunk.add_variable("farfog", _3ds_float(fog_density + fog_density * 0.5))
fog_chunk.add_subchunk(fog_color_chunk)
object_info.add_subchunk(fog_chunk)
# Add LAYER FOG
foglayer = next((lk.from_socket.node for lk in ntree if lk.from_socket.node.type == 'VOLUME_SCATTER' and lk.to_socket.node.type in bgshade), False)
if foglayer:
layerfog_flag = 0
if world.mist_settings.falloff == 'QUADRATIC':
layerfog_flag |= 0x1
if world.mist_settings.falloff == 'INVERSE_QUADRATIC':
layerfog_flag |= 0x2
layerfog_chunk = _3ds_chunk(LAYER_FOG)
layerfog_color_chunk = _3ds_chunk(RGB)
use_fog_flag = _3ds_chunk(USE_LAYER_FOG)
layerfog_color_chunk.add_variable("color", _3ds_float_color(foglayer.inputs[0].default_value[:3]))
layerfog_chunk.add_variable("lowZ", _3ds_float(world.mist_settings.start))
layerfog_chunk.add_variable("highZ", _3ds_float(world.mist_settings.height))
layerfog_chunk.add_variable("density", _3ds_float(foglayer.inputs[1].default_value))
layerfog_chunk.add_variable("flags", _3ds_uint(layerfog_flag))
layerfog_chunk.add_subchunk(layerfog_color_chunk)
object_info.add_subchunk(layerfog_chunk)
if fognode or foglayer and layer.use_pass_mist:
object_info.add_subchunk(use_fog_flag)
if use_keyframes and world.animation_data:
kfdata.add_subchunk(make_ambient_node(world))
# Make a list of all materials used in the selected meshes (use dictionary, each material is added once)
@ -1539,9 +1698,9 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
mesh_objects = []
if use_selection:
objects = [ob for ob in scene.objects if ob.visible_get(view_layer=layer) and ob.select_get(view_layer=layer)]
objects = [ob for ob in scene.objects if ob.type in object_filter and ob.visible_get(view_layer=layer) and ob.select_get(view_layer=layer)]
else:
objects = [ob for ob in scene.objects if ob.visible_get(view_layer=layer)]
objects = [ob for ob in scene.objects if ob.type in object_filter and ob.visible_get(view_layer=layer)]
empty_objects = [ob for ob in objects if ob.type == 'EMPTY']
light_objects = [ob for ob in objects if ob.type == 'LIGHT']
@ -1567,6 +1726,7 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
if data:
matrix = global_matrix @ mtx
data.transform(matrix)
data.transform(mtx_scale)
mesh_objects.append((ob_derived, data, matrix))
ma_ls = data.materials
ma_ls_len = len(ma_ls)
@ -1615,29 +1775,29 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
name_id = {}
for ob, data, matrix in mesh_objects:
translation[ob.name] = ob.location
translation[ob.name] = mtx_scale @ ob.location
rotation[ob.name] = ob.rotation_euler
scale[ob.name] = ob.scale
scale[ob.name] = mtx_scale.copy()
name_id[ob.name] = len(name_id)
object_id[ob.name] = len(object_id)
for ob in empty_objects:
translation[ob.name] = ob.location
translation[ob.name] = mtx_scale @ ob.location
rotation[ob.name] = ob.rotation_euler
scale[ob.name] = ob.scale
scale[ob.name] = mtx_scale.copy()
name_id[ob.name] = len(name_id)
for ob in light_objects:
translation[ob.name] = ob.location
translation[ob.name] = mtx_scale @ ob.location
rotation[ob.name] = ob.rotation_euler
scale[ob.name] = ob.scale
scale[ob.name] = mtx_scale.copy()
name_id[ob.name] = len(name_id)
object_id[ob.name] = len(object_id)
for ob in camera_objects:
translation[ob.name] = ob.location
translation[ob.name] = mtx_scale @ ob.location
rotation[ob.name] = ob.rotation_euler
scale[ob.name] = ob.scale
scale[ob.name] = mtx_scale.copy()
name_id[ob.name] = len(name_id)
object_id[ob.name] = len(object_id)
@ -1672,13 +1832,13 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
operator.report({'WARNING'}, "Object %r can't be written into a 3DS file")
# Export object node
if write_keyframe:
if use_keyframes:
kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale, name_id))
i += i
# Create chunks for all empties - only requires a object node
if write_keyframe:
if use_keyframes:
for ob in empty_objects:
kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale, name_id))
@ -1687,9 +1847,10 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
object_chunk = _3ds_chunk(OBJECT)
obj_light_chunk = _3ds_chunk(OBJECT_LIGHT)
color_float_chunk = _3ds_chunk(RGB)
light_distance = translation[ob.name]
light_energy_factor = _3ds_chunk(LIGHT_MULTIPLIER)
object_chunk.add_variable("light", _3ds_string(sane_name(ob.name)))
obj_light_chunk.add_variable("location", _3ds_point_3d(ob.location))
obj_light_chunk.add_variable("location", _3ds_point_3d(light_distance))
color_float_chunk.add_variable("color", _3ds_float_color(ob.data.color))
light_energy_factor.add_variable("energy", _3ds_float(ob.data.energy * 0.001))
obj_light_chunk.add_subchunk(color_float_chunk)
@ -1697,17 +1858,14 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
if ob.data.type == 'SPOT':
cone_angle = math.degrees(ob.data.spot_size)
hotspot = cone_angle - (ob.data.spot_blend * math.floor(cone_angle))
hypo = math.copysign(math.sqrt(pow(ob.location.x, 2) + pow(ob.location.y, 2)), ob.location.y)
pos_x = -1 * math.copysign(ob.location.x + (ob.location.y * math.tan(ob.rotation_euler.z)), ob.rotation_euler.x)
pos_y = -1 * math.copysign(ob.location.y + (ob.location.x * math.tan(math.radians(90) - ob.rotation_euler.z)), ob.rotation_euler.z)
pos_z = -1 * math.copysign(hypo * math.tan(math.radians(90) - ob.rotation_euler.x), ob.location.z)
hot_spot = cone_angle - (ob.data.spot_blend * math.floor(cone_angle))
spot_pos = calc_target(light_distance, rotation[ob.name].x, rotation[ob.name].z)
spotlight_chunk = _3ds_chunk(LIGHT_SPOTLIGHT)
spot_roll_chunk = _3ds_chunk(LIGHT_SPOT_ROLL)
spotlight_chunk.add_variable("target", _3ds_point_3d((pos_x, pos_y, pos_z)))
spotlight_chunk.add_variable("hotspot", _3ds_float(round(hotspot, 4)))
spotlight_chunk.add_variable("target", _3ds_point_3d(spot_pos))
spotlight_chunk.add_variable("hotspot", _3ds_float(round(hot_spot, 4)))
spotlight_chunk.add_variable("angle", _3ds_float(round(cone_angle, 4)))
spot_roll_chunk.add_variable("roll", _3ds_float(round(ob.rotation_euler.y, 6)))
spot_roll_chunk.add_variable("roll", _3ds_float(round(rotation[ob.name].y, 6)))
spotlight_chunk.add_subchunk(spot_roll_chunk)
if ob.data.use_shadow:
spot_shadow_flag = _3ds_chunk(LIGHT_SPOT_SHADOWED)
@ -1744,7 +1902,7 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
object_info.add_subchunk(object_chunk)
# Export light and spotlight target node
if write_keyframe:
if use_keyframes:
kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale, name_id))
if ob.data.type == 'SPOT':
kfdata.add_subchunk(make_target_node(ob, translation, rotation, scale, name_id))
@ -1753,14 +1911,12 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
for ob in camera_objects:
object_chunk = _3ds_chunk(OBJECT)
camera_chunk = _3ds_chunk(OBJECT_CAMERA)
diagonal = math.copysign(math.sqrt(pow(ob.location.x, 2) + pow(ob.location.y, 2)), ob.location.y)
focus_x = -1 * math.copysign(ob.location.x + (ob.location.y * math.tan(ob.rotation_euler.z)), ob.rotation_euler.x)
focus_y = -1 * math.copysign(ob.location.y + (ob.location.x * math.tan(math.radians(90) - ob.rotation_euler.z)), ob.rotation_euler.z)
focus_z = -1 * math.copysign(diagonal * math.tan(math.radians(90) - ob.rotation_euler.x), ob.location.z)
camera_distance = translation[ob.name]
camera_target = calc_target(camera_distance, rotation[ob.name].x, rotation[ob.name].z)
object_chunk.add_variable("camera", _3ds_string(sane_name(ob.name)))
camera_chunk.add_variable("location", _3ds_point_3d(ob.location))
camera_chunk.add_variable("target", _3ds_point_3d((focus_x, focus_y, focus_z)))
camera_chunk.add_variable("roll", _3ds_float(round(ob.rotation_euler.y, 6)))
camera_chunk.add_variable("location", _3ds_point_3d(camera_distance))
camera_chunk.add_variable("target", _3ds_point_3d(camera_target))
camera_chunk.add_variable("roll", _3ds_float(round(rotation[ob.name].y, 6)))
camera_chunk.add_variable("lens", _3ds_float(ob.data.lens))
object_chunk.add_subchunk(camera_chunk)
@ -1780,7 +1936,7 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
object_info.add_subchunk(object_chunk)
# Export camera and target node
if write_keyframe:
if use_keyframes:
kfdata.add_subchunk(make_object_node(ob, translation, rotation, scale, name_id))
kfdata.add_subchunk(make_target_node(ob, translation, rotation, scale, name_id))
@ -1788,11 +1944,10 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
primary.add_subchunk(object_info)
# Add main keyframe data chunk to primary chunk
if write_keyframe:
if use_keyframes:
primary.add_subchunk(kfdata)
# At this point, the chunk hierarchy is completely built
# Check the size
# The chunk hierarchy is completely built, now check the size
primary.get_size()
# Open the file for writing
@ -1809,6 +1964,7 @@ def save(operator, context, filepath="", use_selection=False, use_hierarchy=Fals
name_mapping.clear()
# Debugging only: report the exporting time
context.window.cursor_set('DEFAULT')
print("3ds export time: %.2f" % (time.time() - duration))
# Debugging only: dump the chunk hierarchy

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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 = {

View File

@ -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

View File

@ -466,7 +466,7 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_optimize_animation_keep_anim_armature: BoolProperty(
name='Force keeping channels for bones',
description=(
"if all keyframes are identical in a rig, "
"If all keyframes are identical in a rig, "
"force keeping the minimal animation. "
"When off, all possible channels for "
"the bones will be exported, even if empty "

View File

@ -115,12 +115,11 @@ def get_bmesh_loosevert_array(bm):
class _Mesh_Arrays:
def __init__(self, obj, create_tris, create_edges, create_looseverts):
def __init__(self, depsgraph, obj, create_tris, create_edges, create_looseverts):
self.tri_verts = self.edge_verts = self.looseverts = ()
if obj.type == "MESH":
me = obj.data
if me.is_editmode:
bm = bmesh.from_edit_mesh(me)
if obj.data.is_editmode:
bm = bmesh.from_edit_mesh(obj.data)
bm.verts.ensure_lookup_table()
self.verts_co = get_bmesh_vert_co_array(bm)
@ -131,11 +130,9 @@ class _Mesh_Arrays:
self.edge_verts = get_bmesh_edge_verts_array(bm)
if create_looseverts:
self.looseverts = get_bmesh_loosevert_array(bm)
del bm
else:
import bpy
ob_eval = obj.evaluated_get(depsgraph)
me = ob_eval.data
self.verts_co = get_mesh_vert_co_array(me)
if create_tris:
@ -177,21 +174,20 @@ class GPU_Indices_Mesh:
"users",
)
_Hash = {}
_cache = {}
shader = None
UBO_data = None
UBO = None
@classmethod
def end_opengl(cls):
def gpu_data_free(cls):
del cls.shader
del cls.UBO
del cls.UBO_data
del cls
cls.shader = cls.UBO = cls.UBO_data = None
@staticmethod
def init_opengl():
def gpu_data_ensure():
cls = GPU_Indices_Mesh
# OpenGL was already initialized, nothing to do here.
if cls.shader is not None:
@ -200,8 +196,8 @@ class GPU_Indices_Mesh:
import atexit
# Make sure we only registered the callback once.
atexit.unregister(cls.end_opengl)
atexit.register(cls.end_opengl)
atexit.unregister(cls.gpu_data_free)
atexit.register(cls.gpu_data_free)
shader_info = gpu.types.GPUShaderCreateInfo()
@ -280,8 +276,8 @@ class GPU_Indices_Mesh:
def __init__(self, depsgraph, obj, draw_tris, draw_edges, draw_verts):
self.ob_data = obj.original.data
if self.ob_data in GPU_Indices_Mesh._Hash:
src = GPU_Indices_Mesh._Hash[self.ob_data]
if self.ob_data in GPU_Indices_Mesh._cache:
src = GPU_Indices_Mesh._cache[self.ob_data]
dst = self
dst.draw_tris = src.draw_tris
@ -298,10 +294,10 @@ class GPU_Indices_Mesh:
dst.users.append(self)
update = False
else:
GPU_Indices_Mesh._Hash[self.ob_data] = self
GPU_Indices_Mesh._cache[self.ob_data] = self
self.users = [self]
update = True
if update:
@ -309,12 +305,10 @@ class GPU_Indices_Mesh:
self.draw_edges = draw_edges
self.draw_verts = draw_verts
GPU_Indices_Mesh.init_opengl()
GPU_Indices_Mesh.gpu_data_ensure()
## Init Array ##
mesh_arrays = _Mesh_Arrays(
depsgraph.id_eval_get(obj), draw_tris, draw_edges, draw_verts
)
mesh_arrays = _Mesh_Arrays(depsgraph, obj, draw_tris, draw_edges, draw_verts)
if mesh_arrays.verts_co is None:
self.draw_tris = False
@ -479,13 +473,13 @@ class GPU_Indices_Mesh:
del self.tri_verts
del self.edge_verts
del self.looseverts
GPU_Indices_Mesh._Hash.pop(self.ob_data)
GPU_Indices_Mesh._cache.pop(self.ob_data)
# print('mesh_del', self.obj.name)
def gpu_Indices_enable_state(winmat, viewmat):
GPU_Indices_Mesh.init_opengl()
GPU_Indices_Mesh.gpu_data_ensure()
gpu.matrix.push()
gpu.matrix.push_projection()
gpu.matrix.load_projection_matrix(winmat)
@ -499,7 +493,7 @@ def gpu_Indices_restore_state():
def gpu_Indices_use_clip_planes(rv3d, value):
GPU_Indices_Mesh.init_opengl()
GPU_Indices_Mesh.gpu_data_ensure()
if value and rv3d.use_clip_planes:
GPU_Indices_Mesh.UBO_data.use_clip_planes = True
GPU_Indices_Mesh.UBO_data.WorldClipPlanes[0] = rv3d.clip_planes[0][:]
@ -513,4 +507,4 @@ def gpu_Indices_use_clip_planes(rv3d, value):
def gpu_Indices_mesh_cache_clear():
GPU_Indices_Mesh._Hash.clear()
GPU_Indices_Mesh._cache.clear()

View File

@ -76,11 +76,9 @@ def main_object(context, collection, obj, level, **kw):
# must apply after boolean.
if use_recenter:
bpy.ops.object.origin_set(
{"selected_editable_objects": objects},
type='ORIGIN_GEOMETRY',
center='MEDIAN',
)
context_override = {"selected_editable_objects": objects}
with bpy.context.temp_override(**context_override):
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
# ----------
# Recursion

View File

@ -98,7 +98,8 @@ else:
addon_prefs = context.preferences.addons[__package__].preferences
if addon_prefs.show_overlays and sun_props.show_north:
_north_handle = bpy.types.SpaceView3D.draw_handler_add(north_draw, (), 'WINDOW', 'POST_VIEW')
if _north_handle is None:
_north_handle = bpy.types.SpaceView3D.draw_handler_add(north_draw, (), 'WINDOW', 'POST_VIEW')
elif _north_handle is not None:
bpy.types.SpaceView3D.draw_handler_remove(_north_handle, 'WINDOW')
_north_handle = None

View File

@ -5,8 +5,8 @@
bl_info = {
"name": "Manage UI translations",
"author": "Bastien Montagne",
"version": (1, 3, 3),
"blender": (3, 6, 0),
"version": (1, 3, 4),
"blender": (4, 0, 0),
"location": "Main \"File\" menu, text editor, any UI control",
"description": "Allows managing UI translations directly from Blender "
"(update main .po files, update scripts' translations, etc.)",

View File

@ -210,7 +210,11 @@ class UI_OT_i18n_updatetranslation_svn_trunk(Operator):
if uid and uid not in stats:
po = utils_i18n.I18nMessages(uid=uid, kind='PO', src=po_path, settings=self.settings)
stats[uid] = po.nbr_trans_msgs / po.nbr_msgs if po.nbr_msgs > 0 else 0
utils_languages_menu.gen_menu_file(stats, self.settings)
languages_menu_lines = utils_languages_menu.gen_menu_file(stats, self.settings)
with open(os.path.join(self.settings.TRUNK_MO_DIR, self.settings.LANGUAGES_FILE), 'w', encoding="utf8") as f:
f.write("\n".join(languages_menu_lines))
with open(os.path.join(self.settings.GIT_I18N_ROOT, self.settings.LANGUAGES_FILE), 'w', encoding="utf8") as f:
f.write("\n".join(languages_menu_lines))
context.window_manager.progress_end()
return {'FINISHED'}