3D Print Toolbox: Add hollow out #105194

Merged
Mikhail Rachinskiy merged 9 commits from usfreitas/blender-addons:hollow into main 2024-03-18 12:24:30 +01:00
47 changed files with 771 additions and 407 deletions
Showing only changes of commit 8cd3ab9f94 - Show all commits

View File

@ -13,12 +13,13 @@ from bpy.props import (
EnumProperty, EnumProperty,
FloatProperty, FloatProperty,
StringProperty, StringProperty,
CollectionProperty,
) )
import bpy import bpy
bl_info = { bl_info = {
"name": "Autodesk 3DS format", "name": "Autodesk 3DS format",
"author": "Bob Holcomb, Campbell Barton, Sebastian Schrand", "author": "Bob Holcomb, Campbell Barton, Sebastian Schrand",
"version": (2, 4, 9), "version": (2, 5, 0),
"blender": (4, 1, 0), "blender": (4, 1, 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, "
@ -46,6 +47,9 @@ class Import3DS(bpy.types.Operator, ImportHelper):
filename_ext = ".3ds" filename_ext = ".3ds"
filter_glob: StringProperty(default="*.3ds", options={'HIDDEN'}) filter_glob: StringProperty(default="*.3ds", options={'HIDDEN'})
filepath: StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE'})
files: CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
directory: StringProperty(subtype='DIR_PATH')
constrain_size: FloatProperty( constrain_size: FloatProperty(
name="Constrain Size", name="Constrain Size",
@ -106,7 +110,6 @@ class Import3DS(bpy.types.Operator, ImportHelper):
def execute(self, context): def execute(self, context):
from . import import_3ds from . import import_3ds
keywords = self.as_keywords(ignore=("axis_forward", keywords = self.as_keywords(ignore=("axis_forward",
"axis_up", "axis_up",
"filter_glob", "filter_glob",
@ -123,6 +126,17 @@ class Import3DS(bpy.types.Operator, ImportHelper):
pass pass
class MAX3DS_FH_import(bpy.types.FileHandler):
bl_idname = "MAX3DS_FH_import"
bl_label = "File handler for 3ds import"
bl_import_operator = "import_scene.max3ds"
bl_file_extensions = ".3ds;.3DS"
@classmethod
def poll_drop(cls, context):
return (context.area and context.area.type == 'VIEW_3D')
class MAX3DS_PT_import_include(bpy.types.Panel): class MAX3DS_PT_import_include(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER' bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS' bl_region_type = 'TOOL_PROPS'
@ -346,6 +360,7 @@ 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_FH_import)
bpy.utils.register_class(MAX3DS_PT_import_include) bpy.utils.register_class(MAX3DS_PT_import_include)
bpy.utils.register_class(MAX3DS_PT_import_transform) bpy.utils.register_class(MAX3DS_PT_import_transform)
bpy.utils.register_class(Export3DS) bpy.utils.register_class(Export3DS)
@ -357,6 +372,7 @@ def register():
def unregister(): def unregister():
bpy.utils.unregister_class(Import3DS) bpy.utils.unregister_class(Import3DS)
bpy.utils.unregister_class(MAX3DS_FH_import)
bpy.utils.unregister_class(MAX3DS_PT_import_include) bpy.utils.unregister_class(MAX3DS_PT_import_include)
bpy.utils.unregister_class(MAX3DS_PT_import_transform) bpy.utils.unregister_class(MAX3DS_PT_import_transform)
bpy.utils.unregister_class(Export3DS) bpy.utils.unregister_class(Export3DS)

View File

@ -713,6 +713,7 @@ def make_material_chunk(material, image):
material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - wrap.alpha)) material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - wrap.alpha))
material_chunk.add_subchunk(make_percent_subchunk(MATXPFALL, wrap.transmission)) material_chunk.add_subchunk(make_percent_subchunk(MATXPFALL, wrap.transmission))
material_chunk.add_subchunk(make_percent_subchunk(MATSELFILPCT, wrap.emission_strength)) material_chunk.add_subchunk(make_percent_subchunk(MATSELFILPCT, wrap.emission_strength))
if wrap.node_principled_bsdf is not None:
material_chunk.add_subchunk(make_percent_subchunk(MATREFBLUR, wrap.node_principled_bsdf.inputs['Coat Weight'].default_value)) material_chunk.add_subchunk(make_percent_subchunk(MATREFBLUR, wrap.node_principled_bsdf.inputs['Coat Weight'].default_value))
material_chunk.add_subchunk(shading) material_chunk.add_subchunk(shading)

View File

@ -1671,10 +1671,12 @@ def load_3ds(filepath, context, CONSTRAIN=10.0, UNITS=False, IMAGE_SEARCH=True,
object_dictionary.clear() object_dictionary.clear()
object_matrix.clear() object_matrix.clear()
"""
if APPLY_MATRIX: if APPLY_MATRIX:
for ob in imported_objects: for ob in imported_objects:
if ob.type == 'MESH': if ob.type == 'MESH':
ob.data.transform(ob.matrix_local.inverted()) ob.data.transform(ob.matrix_local.inverted())
"""
if UNITS: if UNITS:
unit_mtx = mathutils.Matrix.Scale(MEASURE,4) unit_mtx = mathutils.Matrix.Scale(MEASURE,4)
@ -1768,11 +1770,12 @@ def load_3ds(filepath, context, CONSTRAIN=10.0, UNITS=False, IMAGE_SEARCH=True,
file.close() file.close()
def load(operator, context, filepath="", constrain_size=0.0, use_scene_unit=False, def load(operator, context, files=None, directory="", filepath="", constrain_size=0.0, use_scene_unit=False,
use_image_search=True, object_filter=None, use_world_matrix=False, use_keyframes=True, use_image_search=True, object_filter=None, use_world_matrix=False, use_keyframes=True,
use_apply_transform=True, global_matrix=None, use_cursor=False, use_center_pivot=False): use_apply_transform=True, global_matrix=None, use_cursor=False, use_center_pivot=False):
load_3ds(filepath, context, CONSTRAIN=constrain_size, UNITS=use_scene_unit, for f in files:
load_3ds(os.path.join(directory, f.name), context, CONSTRAIN=constrain_size, UNITS=use_scene_unit,
IMAGE_SEARCH=use_image_search, FILTER=object_filter, WORLD_MATRIX=use_world_matrix, KEYFRAME=use_keyframes, IMAGE_SEARCH=use_image_search, FILTER=object_filter, WORLD_MATRIX=use_world_matrix, KEYFRAME=use_keyframes,
APPLY_MATRIX=use_apply_transform, CONVERSE=global_matrix, CURSOR=use_cursor, PIVOT=use_center_pivot,) APPLY_MATRIX=use_apply_transform, CONVERSE=global_matrix, CURSOR=use_cursor, PIVOT=use_center_pivot,)

View File

@ -5,7 +5,7 @@
bl_info = { bl_info = {
'name': 'glTF 2.0 format', 'name': 'glTF 2.0 format',
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
"version": (4, 2, 0), "version": (4, 2, 5),
'blender': (4, 1, 0), 'blender': (4, 1, 0),
'location': 'File > Import-Export', 'location': 'File > Import-Export',
'description': 'Import-Export as glTF 2.0', 'description': 'Import-Export as glTF 2.0',
@ -121,6 +121,27 @@ def on_export_action_filter_changed(self, context):
del bpy.types.Scene.gltf_action_filter_active del bpy.types.Scene.gltf_action_filter_active
def get_format_items(scene, context):
items = (('GLB', 'glTF Binary (.glb)',
'Exports a single file, with all data packed in binary form. '
'Most efficient and portable, but more difficult to edit later'),
('GLTF_SEPARATE', 'glTF Separate (.gltf + .bin + textures)',
'Exports multiple files, with separate JSON, binary and texture data. '
'Easiest to edit later'))
if bpy.context.preferences.addons['io_scene_gltf2'].preferences \
and "allow_embedded_format" in bpy.context.preferences.addons['io_scene_gltf2'].preferences \
and bpy.context.preferences.addons['io_scene_gltf2'].preferences['allow_embedded_format']:
# At initialization, the preferences are not yet loaded
# The second line check is needed until the PR is merge in Blender, for github CI tests
items += (('GLTF_EMBEDDED', 'glTF Embedded (.gltf)',
'Exports a single file, with all data packed in JSON. '
'Less efficient than binary, but easier to edit later'
),)
return items
class ConvertGLTF2_Base: class ConvertGLTF2_Base:
@ -254,17 +275,12 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_format: EnumProperty( export_format: EnumProperty(
name='Format', name='Format',
items=(('GLB', 'glTF Binary (.glb)', items=get_format_items,
'Exports a single file, with all data packed in binary form. '
'Most efficient and portable, but more difficult to edit later'),
('GLTF_SEPARATE', 'glTF Separate (.gltf + .bin + textures)',
'Exports multiple files, with separate JSON, binary and texture data. '
'Easiest to edit later')),
description=( description=(
'Output format. Binary is most efficient, ' 'Output format. Binary is most efficient, '
'but JSON may be easier to edit later' 'but JSON may be easier to edit later'
), ),
default='GLB', #Warning => If you change the default, need to change the default filter too default=0, #Warning => If you change the default, need to change the default filter too
update=on_export_format_changed, update=on_export_format_changed,
) )
@ -584,6 +600,10 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
'Export actions (actives and on NLA tracks) as separate animations'), 'Export actions (actives and on NLA tracks) as separate animations'),
('ACTIVE_ACTIONS', 'Active actions merged', ('ACTIVE_ACTIONS', 'Active actions merged',
'All the currently assigned actions become one glTF animation'), 'All the currently assigned actions become one glTF animation'),
('BROADCAST', 'Broadcast actions',
'Broadcast all compatible actions to all objects. '
'Animated objects will get all actions compatible with them, '
'others will get no animation at all'),
('NLA_TRACKS', 'NLA Tracks', ('NLA_TRACKS', 'NLA Tracks',
'Export individual NLA Tracks as separate animation'), 'Export individual NLA Tracks as separate animation'),
('SCENE', 'Scene', ('SCENE', 'Scene',
@ -657,6 +677,15 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
default=False default=False
) )
export_optimize_armature_disable_viewport: BoolProperty(
name='Disable viewport if possible',
description=(
"When exporting armature, disable viewport for other objects, "
"for performance. Drivers on shape keys for skined meshes prevent this optimization for now"
),
default=False
)
export_negative_frame: EnumProperty( export_negative_frame: EnumProperty(
name='Negative Frames', name='Negative Frames',
items=(('SLIDE', 'Slide', items=(('SLIDE', 'Slide',
@ -851,6 +880,15 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
default=False default=False
) )
export_extra_animations: BoolProperty(
name='Prepare extra animations',
description=(
'Export additional animations'
'This feature is not standard and needs an external extension to be included in the glTF file'
),
default=False
)
# Custom scene property for saving settings # Custom scene property for saving settings
scene_key = "glTF2ExportSettings" scene_key = "glTF2ExportSettings"
@ -1015,12 +1053,14 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_settings['gltf_optimize_animation'] = self.export_optimize_animation_size export_settings['gltf_optimize_animation'] = self.export_optimize_animation_size
export_settings['gltf_optimize_animation_keep_armature'] = self.export_optimize_animation_keep_anim_armature export_settings['gltf_optimize_animation_keep_armature'] = self.export_optimize_animation_keep_anim_armature
export_settings['gltf_optimize_animation_keep_object'] = self.export_optimize_animation_keep_anim_object export_settings['gltf_optimize_animation_keep_object'] = self.export_optimize_animation_keep_anim_object
export_settings['gltf_optimize_armature_disable_viewport'] = self.export_optimize_armature_disable_viewport
export_settings['gltf_export_anim_single_armature'] = self.export_anim_single_armature export_settings['gltf_export_anim_single_armature'] = self.export_anim_single_armature
export_settings['gltf_export_reset_pose_bones'] = self.export_reset_pose_bones export_settings['gltf_export_reset_pose_bones'] = self.export_reset_pose_bones
export_settings['gltf_export_reset_sk_data'] = self.export_morph_reset_sk_data export_settings['gltf_export_reset_sk_data'] = self.export_morph_reset_sk_data
export_settings['gltf_bake_animation'] = self.export_bake_animation export_settings['gltf_bake_animation'] = self.export_bake_animation
export_settings['gltf_negative_frames'] = self.export_negative_frame export_settings['gltf_negative_frames'] = self.export_negative_frame
export_settings['gltf_anim_slide_to_zero'] = self.export_anim_slide_to_zero export_settings['gltf_anim_slide_to_zero'] = self.export_anim_slide_to_zero
export_settings['gltf_export_extra_animations'] = self.export_extra_animations
else: else:
export_settings['gltf_frame_range'] = False export_settings['gltf_frame_range'] = False
export_settings['gltf_force_sampling'] = False export_settings['gltf_force_sampling'] = False
@ -1028,9 +1068,11 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_settings['gltf_optimize_animation'] = False export_settings['gltf_optimize_animation'] = False
export_settings['gltf_optimize_animation_keep_armature'] = False export_settings['gltf_optimize_animation_keep_armature'] = False
export_settings['gltf_optimize_animation_keep_object'] = False export_settings['gltf_optimize_animation_keep_object'] = False
export_settings['gltf_optimize_armature_disable_viewport'] = False
export_settings['gltf_export_anim_single_armature'] = False export_settings['gltf_export_anim_single_armature'] = False
export_settings['gltf_export_reset_pose_bones'] = False export_settings['gltf_export_reset_pose_bones'] = False
export_settings['gltf_export_reset_sk_data'] = False export_settings['gltf_export_reset_sk_data'] = False
export_settings['gltf_export_extra_animations'] = False
export_settings['gltf_skins'] = self.export_skins export_settings['gltf_skins'] = self.export_skins
if self.export_skins: if self.export_skins:
export_settings['gltf_all_vertex_influences'] = self.export_all_influences export_settings['gltf_all_vertex_influences'] = self.export_all_influences
@ -1145,6 +1187,8 @@ class GLTF_PT_export_main(bpy.types.Panel):
layout.prop(operator, 'export_keep_originals') layout.prop(operator, 'export_keep_originals')
if operator.export_keep_originals is False: if operator.export_keep_originals is False:
layout.prop(operator, 'export_texture_dir', icon='FILE_FOLDER') layout.prop(operator, 'export_texture_dir', icon='FILE_FOLDER')
if operator.export_format == 'GLTF_EMBEDDED':
layout.label(text="This is the least efficient of the available forms, and should only be used when required.", icon='ERROR')
layout.prop(operator, 'export_copyright') layout.prop(operator, 'export_copyright')
layout.prop(operator, 'will_save_settings') layout.prop(operator, 'will_save_settings')
@ -1161,7 +1205,7 @@ class GLTF_PT_export_gltfpack(bpy.types.Panel):
def poll(cls, context): def poll(cls, context):
gltfpack_path = context.preferences.addons['io_scene_gltf2'].preferences.gltfpack_path_ui.strip() gltfpack_path = context.preferences.addons['io_scene_gltf2'].preferences.gltfpack_path_ui.strip()
if (gltfpack_path == ''): # gltfpack not setup in plugin preferences -> dont show any gltfpack relevant options in export dialog if (gltfpack_path == ''): # gltfpack not setup in plugin preferences -> dont show any gltfpack relevant options in export dialog
return False; return False
sfile = context.space_data sfile = context.space_data
operator = sfile.active_operator operator = sfile.active_operator
@ -1638,7 +1682,7 @@ class GLTF_PT_export_animation(bpy.types.Panel):
layout.prop(operator, 'export_nla_strips_merged_animation_name') layout.prop(operator, 'export_nla_strips_merged_animation_name')
row = layout.row() row = layout.row()
row.active = operator.export_force_sampling and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS'] row.active = operator.export_force_sampling and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'BROACAST']
row.prop(operator, 'export_bake_animation') row.prop(operator, 'export_bake_animation')
if operator.export_animation_mode == "SCENE": if operator.export_animation_mode == "SCENE":
layout.prop(operator, 'export_anim_scene_split_object') layout.prop(operator, 'export_anim_scene_split_object')
@ -1697,11 +1741,11 @@ class GLTF_PT_export_animation_ranges(bpy.types.Panel):
layout.prop(operator, 'export_current_frame') layout.prop(operator, 'export_current_frame')
row = layout.row() row = layout.row()
row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'NLA_TRACKS'] row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'BROADCAST', 'NLA_TRACKS']
row.prop(operator, 'export_frame_range') row.prop(operator, 'export_frame_range')
layout.prop(operator, 'export_anim_slide_to_zero') layout.prop(operator, 'export_anim_slide_to_zero')
row = layout.row() row = layout.row()
row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'NLA_TRACKS'] row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'BROADCAST', 'NLA_TRACKS']
layout.prop(operator, 'export_negative_frame') layout.prop(operator, 'export_negative_frame')
class GLTF_PT_export_animation_armature(bpy.types.Panel): class GLTF_PT_export_animation_armature(bpy.types.Panel):
@ -1781,7 +1825,7 @@ class GLTF_PT_export_animation_sampling(bpy.types.Panel):
def draw_header(self, context): def draw_header(self, context):
sfile = context.space_data sfile = context.space_data
operator = sfile.active_operator operator = sfile.active_operator
self.layout.active = operator.export_animations and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS'] self.layout.active = operator.export_animations and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'BROADCAST']
self.layout.prop(operator, "export_force_sampling", text="") self.layout.prop(operator, "export_force_sampling", text="")
def draw(self, context): def draw(self, context):
@ -1829,6 +1873,36 @@ class GLTF_PT_export_animation_optimize(bpy.types.Panel):
row = layout.row() row = layout.row()
row.prop(operator, 'export_optimize_animation_keep_anim_object') row.prop(operator, 'export_optimize_animation_keep_anim_object')
row = layout.row()
row.prop(operator, 'export_optimize_armature_disable_viewport')
class GLTF_PT_export_animation_extra(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
bl_label = "Extra Animations"
bl_parent_id = "GLTF_PT_export_animation"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
sfile = context.space_data
operator = sfile.active_operator
return operator.bl_idname == "EXPORT_SCENE_OT_gltf" and \
operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS']
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
sfile = context.space_data
operator = sfile.active_operator
layout.active = operator.export_animations
layout.prop(operator, 'export_extra_animations')
class GLTF_PT_export_user_extensions(bpy.types.Panel): class GLTF_PT_export_user_extensions(bpy.types.Panel):
bl_space_type = 'FILE_BROWSER' bl_space_type = 'FILE_BROWSER'
@ -2112,6 +2186,12 @@ class GLTF_AddonPreferences(bpy.types.AddonPreferences):
subtype='FILE_PATH' subtype='FILE_PATH'
) )
allow_embedded_format: bpy.props.BoolProperty(
default = False,
name='Allow glTF Embedded format',
description="Allow glTF Embedded format"
)
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
row = layout.row() row = layout.row()
@ -2120,6 +2200,10 @@ class GLTF_AddonPreferences(bpy.types.AddonPreferences):
row.prop(self, "animation_ui", text="Animation UI") row.prop(self, "animation_ui", text="Animation UI")
row = layout.row() row = layout.row()
row.prop(self, "gltfpack_path_ui", text="Path to gltfpack") row.prop(self, "gltfpack_path_ui", text="Path to gltfpack")
row = layout.row()
row.prop(self, "allow_embedded_format", text="Allow glTF Embedded format")
if self.allow_embedded_format:
layout.label(text="This is the least efficient of the available forms, and should only be used when required.", icon='ERROR')
def menu_func_import(self, context): def menu_func_import(self, context):
self.layout.operator(ImportGLTF2.bl_idname, text='glTF 2.0 (.glb/.gltf)') self.layout.operator(ImportGLTF2.bl_idname, text='glTF 2.0 (.glb/.gltf)')
@ -2148,6 +2232,7 @@ classes = (
GLTF_PT_export_animation_shapekeys, GLTF_PT_export_animation_shapekeys,
GLTF_PT_export_animation_sampling, GLTF_PT_export_animation_sampling,
GLTF_PT_export_animation_optimize, GLTF_PT_export_animation_optimize,
GLTF_PT_export_animation_extra,
GLTF_PT_export_gltfpack, GLTF_PT_export_gltfpack,
GLTF_PT_export_user_extensions, GLTF_PT_export_user_extensions,
ImportGLTF2, ImportGLTF2,

View File

@ -5,11 +5,19 @@
def get_target_property_name(data_path: str) -> str: def get_target_property_name(data_path: str) -> str:
"""Retrieve target property.""" """Retrieve target property."""
if data_path.endswith("]"):
return None
else:
return data_path.rsplit('.', 1)[-1] return data_path.rsplit('.', 1)[-1]
def get_target_object_path(data_path: str) -> str: def get_target_object_path(data_path: str) -> str:
"""Retrieve target object data path without property""" """Retrieve target object data path without property"""
if data_path.endswith("]"):
return data_path.rsplit('[', 1)[0]
elif data_path.startswith("pose.bones["):
return data_path[:data_path.find('"]')] + '"]'
path_split = data_path.rsplit('.', 1) path_split = data_path.rsplit('.', 1)
self_targeting = len(path_split) < 2 self_targeting = len(path_split) < 2
if self_targeting: if self_targeting:

View File

@ -564,7 +564,7 @@ class SCENE_PT_gltf2_action_filter(bpy.types.Panel):
def poll(self, context): def poll(self, context):
sfile = context.space_data sfile = context.space_data
operator = sfile.active_operator operator = sfile.active_operator
return operator.export_animation_mode in ["ACTIONS", "ACTIVE_ACTIONS"] return operator.export_animation_mode in ["ACTIONS", "ACTIVE_ACTIONS", "BROADCAST"]
def draw_header(self, context): def draw_header(self, context):
sfile = context.space_data sfile = context.space_data

View File

@ -16,7 +16,7 @@ def gather_animation_fcurves(
name = __gather_name(blender_action, export_settings) name = __gather_name(blender_action, export_settings)
channels, to_be_sampled = __gather_channels_fcurves(obj_uuid, blender_action, export_settings) channels, to_be_sampled, extra_samplers = __gather_channels_fcurves(obj_uuid, blender_action, export_settings)
animation = gltf2_io.Animation( animation = gltf2_io.Animation(
channels=channels, channels=channels,
@ -27,12 +27,12 @@ def gather_animation_fcurves(
) )
if not animation.channels: if not animation.channels:
return None, to_be_sampled return None, to_be_sampled, extra_samplers
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
export_user_extensions('animation_gather_fcurve', export_settings, blender_object, blender_action) export_user_extensions('animation_gather_fcurve', export_settings, blender_object, blender_action)
return animation, to_be_sampled return animation, to_be_sampled, extra_samplers
def __gather_name(blender_action: bpy.types.Action, def __gather_name(blender_action: bpy.types.Action,
export_settings export_settings

View File

@ -23,25 +23,40 @@ def gather_animation_fcurves_channels(
export_settings export_settings
): ):
channels_to_perform, to_be_sampled = get_channel_groups(obj_uuid, blender_action, export_settings) channels_to_perform, to_be_sampled, extra_channels_to_perform = get_channel_groups(obj_uuid, blender_action, export_settings)
custom_range = None custom_range = None
if blender_action.use_frame_range: if blender_action.use_frame_range:
custom_range = (blender_action.frame_start, blender_action.frame_end) custom_range = (blender_action.frame_start, blender_action.frame_end)
channels = [] channels = []
for chan in [chan for chan in channels_to_perform.values() if len(chan['properties']) != 0 and chan['type'] != "EXTRA"]: extra_samplers = []
for chan in [chan for chan in channels_to_perform.values() if len(chan['properties']) != 0]:
for channel_group in chan['properties'].values(): for channel_group in chan['properties'].values():
channel = __gather_animation_fcurve_channel(chan['obj_uuid'], channel_group, chan['bone'], custom_range, export_settings) channel = __gather_animation_fcurve_channel(chan['obj_uuid'], channel_group, chan['bone'], custom_range, export_settings)
if channel is not None: if channel is not None:
channels.append(channel) channels.append(channel)
return channels, to_be_sampled if export_settings['gltf_export_extra_animations']:
for chan in [chan for chan in extra_channels_to_perform.values() if len(chan['properties']) != 0]:
for channel_group_name, channel_group in chan['properties'].items():
# No glTF channel here, as we don't have any target
# Trying to retrieve sampler directly
sampler = __gather_sampler(obj_uuid, tuple(channel_group), None, custom_range, True, export_settings)
if sampler is not None:
extra_samplers.append((channel_group_name, sampler, "OBJECT", None))
def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_settings): return channels, to_be_sampled, extra_samplers
def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_settings, no_sample_option=False):
# no_sample_option is used when we want to retrieve all SK channels, to be evaluate.
targets = {} targets = {}
targets_extra = {}
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
@ -68,13 +83,26 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
# find the object affected by this action # find the object affected by this action
# object_path : blank for blender_object itself, key_blocks["<name>"] for SK, pose.bones["<name>"] for bones # object_path : blank for blender_object itself, key_blocks["<name>"] for SK, pose.bones["<name>"] for bones
if not object_path: if not object_path:
if fcurve.data_path.startswith("["):
target = blender_object
type_ = "EXTRA"
else:
target = blender_object target = blender_object
type_ = "OBJECT" type_ = "OBJECT"
else: else:
try: try:
target = get_object_from_datapath(blender_object, object_path) target = get_object_from_datapath(blender_object, object_path)
if blender_object.type == "ARMATURE" and fcurve.data_path.startswith("pose.bones["): if blender_object.type == "ARMATURE" and fcurve.data_path.startswith("pose.bones["):
if target_property is not None:
if get_target(target_property) is not None:
type_ = "BONE" type_ = "BONE"
else:
type_ = "EXTRA"
else:
type_ = "EXTRA"
else: else:
type_ = "EXTRA" type_ = "EXTRA"
if blender_object.type == "MESH" and object_path.startswith("key_blocks"): if blender_object.type == "MESH" and object_path.startswith("key_blocks"):
@ -108,6 +136,25 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
multiple_rotation_mode_detected[target] = True multiple_rotation_mode_detected[target] = True
continue continue
if type_ == "EXTRA":
# No group by property, because we are going to export fcurve separately
# We are going to evaluate fcurve, so no check if need to be sampled
if target_property is None:
target_property = fcurve.data_path
if not target_property.startswith("pose.bones["):
target_property = fcurve.data_path
target_data = targets_extra.get(target, {})
target_data['type'] = type_
target_data['bone'] = target.name
target_data['obj_uuid'] = obj_uuid
target_properties = target_data.get('properties', {})
channels = target_properties.get(target_property, [])
channels.append(fcurve)
target_properties[target_property] = channels
target_data['properties'] = target_properties
targets_extra[target] = target_data
continue
# group channels by target object and affected property of the target # group channels by target object and affected property of the target
target_data = targets.get(target, {}) target_data = targets.get(target, {})
target_data['type'] = type_ target_data['type'] = type_
@ -148,7 +195,7 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
# Check if the property can be exported without sampling # Check if the property can be exported without sampling
new_properties = {} new_properties = {}
for prop in target_data['properties'].keys(): for prop in target_data['properties'].keys():
if needs_baking(obj_uuid, target_data['properties'][prop], export_settings) is True: if no_sample_option is False and needs_baking(obj_uuid, target_data['properties'][prop], export_settings) is True:
to_be_sampled.append((obj_uuid, target_data['type'], get_channel_from_target(get_target(prop)), target_data['bone'])) # bone can be None if not a bone :) to_be_sampled.append((obj_uuid, target_data['type'], get_channel_from_target(get_target(prop)), target_data['bone'])) # bone can be None if not a bone :)
else: else:
new_properties[prop] = target_data['properties'][prop] new_properties[prop] = target_data['properties'][prop]
@ -165,7 +212,7 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
to_be_sampled = list(set(to_be_sampled)) to_be_sampled = list(set(to_be_sampled))
return targets, to_be_sampled return targets, to_be_sampled, targets_extra
def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object): def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object):
@ -226,7 +273,7 @@ def __gather_animation_fcurve_channel(obj_uuid: str,
__target= __gather_target(obj_uuid, channel_group, bone, export_settings) __target= __gather_target(obj_uuid, channel_group, bone, export_settings)
if __target.path is not None: if __target.path is not None:
sampler = __gather_sampler(obj_uuid, channel_group, bone, custom_range, export_settings) sampler = __gather_sampler(obj_uuid, channel_group, bone, custom_range, False, export_settings)
if sampler is None: if sampler is None:
# After check, no need to animate this node for this channel # After check, no need to animate this node for this channel
@ -261,9 +308,10 @@ def __gather_sampler(obj_uuid: str,
channel_group: typing.Tuple[bpy.types.FCurve], channel_group: typing.Tuple[bpy.types.FCurve],
bone: typing.Optional[str], bone: typing.Optional[str],
custom_range: typing.Optional[set], custom_range: typing.Optional[set],
extra_mode: bool,
export_settings) -> gltf2_io.AnimationSampler: export_settings) -> gltf2_io.AnimationSampler:
return gather_animation_fcurves_sampler(obj_uuid, channel_group, bone, custom_range, export_settings) return gather_animation_fcurves_sampler(obj_uuid, channel_group, bone, custom_range, extra_mode, export_settings)
def needs_baking(obj_uuid: str, def needs_baking(obj_uuid: str,
channels: typing.Tuple[bpy.types.FCurve], channels: typing.Tuple[bpy.types.FCurve],

View File

@ -16,11 +16,12 @@ def gather_fcurve_keyframes(
channel_group: typing.Tuple[bpy.types.FCurve], channel_group: typing.Tuple[bpy.types.FCurve],
bone: typing.Optional[str], bone: typing.Optional[str],
custom_range: typing.Optional[set], custom_range: typing.Optional[set],
extra_mode: bool,
export_settings): export_settings):
keyframes = [] keyframes = []
non_keyed_values = __gather_non_keyed_values(obj_uuid, channel_group, bone, export_settings) non_keyed_values = gather_non_keyed_values(obj_uuid, channel_group, bone, extra_mode, export_settings)
# Just use the keyframes as they are specified in blender # Just use the keyframes as they are specified in blender
# Note: channels has some None items only for SK if some SK are not animated # Note: channels has some None items only for SK if some SK are not animated
@ -45,7 +46,7 @@ def gather_fcurve_keyframes(
key.value = [c.evaluate(frame) for c in channel_group if c is not None] key.value = [c.evaluate(frame) for c in channel_group if c is not None]
# Complete key with non keyed values, if needed # Complete key with non keyed values, if needed
if len([c for c in channel_group if c is not None]) != key.get_target_len(): if len([c for c in channel_group if c is not None]) != key.get_target_len():
__complete_key(key, non_keyed_values) complete_key(key, non_keyed_values)
# compute tangents for cubic spline interpolation # compute tangents for cubic spline interpolation
if [c for c in channel_group if c is not None][0].keyframe_points[0].interpolation == "BEZIER": if [c for c in channel_group if c is not None][0].keyframe_points[0].interpolation == "BEZIER":
@ -87,13 +88,18 @@ def gather_fcurve_keyframes(
return keyframes return keyframes
def __gather_non_keyed_values( def gather_non_keyed_values(
obj_uuid: str, obj_uuid: str,
channel_group: typing.Tuple[bpy.types.FCurve], channel_group: typing.Tuple[bpy.types.FCurve],
bone: typing.Optional[str], bone: typing.Optional[str],
extra_mode: bool,
export_settings export_settings
) -> typing.Tuple[typing.Optional[float]]: ) -> typing.Tuple[typing.Optional[float]]:
if extra_mode is True:
# No need to check if there are non non keyed values, as we export fcurve independently
return [None]
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
non_keyed_values = [] non_keyed_values = []
@ -132,7 +138,7 @@ def __gather_non_keyed_values(
if i in indices: if i in indices:
non_keyed_values.append(None) non_keyed_values.append(None)
else: else:
if bone is None is None: if bone is None:
non_keyed_values.append({ non_keyed_values.append({
"delta_location" : blender_object.delta_location, "delta_location" : blender_object.delta_location,
"delta_rotation_euler" : blender_object.delta_rotation_euler, "delta_rotation_euler" : blender_object.delta_rotation_euler,
@ -178,7 +184,7 @@ def __gather_non_keyed_values(
return tuple(non_keyed_values) return tuple(non_keyed_values)
def __complete_key(key: Keyframe, non_keyed_values: typing.Tuple[typing.Optional[float]]): def complete_key(key: Keyframe, non_keyed_values: typing.Tuple[typing.Optional[float]]):
""" """
Complete keyframe with non keyed values Complete keyframe with non keyed values
""" """

View File

@ -23,6 +23,7 @@ def gather_animation_fcurves_sampler(
channel_group: typing.Tuple[bpy.types.FCurve], channel_group: typing.Tuple[bpy.types.FCurve],
bone: typing.Optional[str], bone: typing.Optional[str],
custom_range: typing.Optional[set], custom_range: typing.Optional[set],
extra_mode: bool,
export_settings export_settings
) -> gltf2_io.AnimationSampler: ) -> gltf2_io.AnimationSampler:
@ -33,6 +34,7 @@ def gather_animation_fcurves_sampler(
channel_group, channel_group,
bone, bone,
custom_range, custom_range,
extra_mode,
export_settings) export_settings)
if keyframes is None: if keyframes is None:
@ -40,7 +42,7 @@ def gather_animation_fcurves_sampler(
return None return None
# Now we are raw input/output, we need to convert to glTF data # Now we are raw input/output, we need to convert to glTF data
input, output = __convert_keyframes(obj_uuid, channel_group, bone, keyframes, export_settings) input, output = __convert_keyframes(obj_uuid, channel_group, bone, keyframes, extra_mode, export_settings)
sampler = gltf2_io.AnimationSampler( sampler = gltf2_io.AnimationSampler(
extensions=None, extensions=None,
@ -62,16 +64,18 @@ def __gather_keyframes(
channel_group: typing.Tuple[bpy.types.FCurve], channel_group: typing.Tuple[bpy.types.FCurve],
bone: typing.Optional[str], bone: typing.Optional[str],
custom_range: typing.Optional[set], custom_range: typing.Optional[set],
extra_mode: bool,
export_settings export_settings
): ):
return gather_fcurve_keyframes(obj_uuid, channel_group, bone, custom_range, export_settings) return gather_fcurve_keyframes(obj_uuid, channel_group, bone, custom_range, extra_mode, export_settings)
def __convert_keyframes( def __convert_keyframes(
obj_uuid: str, obj_uuid: str,
channel_group: typing.Tuple[bpy.types.FCurve], channel_group: typing.Tuple[bpy.types.FCurve],
bone_name: typing.Optional[str], bone_name: typing.Optional[str],
keyframes, keyframes,
extra_mode: bool,
export_settings): export_settings):
times = [k.seconds for k in keyframes] times = [k.seconds for k in keyframes]
@ -137,6 +141,17 @@ def __convert_keyframes(
values = [] values = []
fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base) fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
for keyframe in keyframes: for keyframe in keyframes:
if extra_mode is True:
# Export as is, without trying to convert
keyframe_value = keyframe.value
if keyframe.in_tangent is not None:
keyframe_value = keyframe.in_tangent + keyframe_value
if keyframe.out_tangent is not None:
keyframe_value = keyframe_value + keyframe.out_tangent
values += keyframe_value
continue
# Transform the data and build gltf control points # Transform the data and build gltf control points
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction) value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction)
if is_yup and bone_name is None: if is_yup and bone_name is None:

View File

@ -261,6 +261,25 @@ def gather_action_animations( obj_uuid: int,
current_use_nla = blender_object.animation_data.use_nla current_use_nla = blender_object.animation_data.use_nla
blender_object.animation_data.use_nla = False blender_object.animation_data.use_nla = False
# Try to disable all except armature in viewport, for performance
if export_settings['gltf_optimize_armature_disable_viewport'] \
and export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE":
# If the skinned mesh has driver(s), we can't disable it to bake armature.
need_to_enable_again = False
sk_drivers = get_sk_drivers(obj_uuid, export_settings)
if len(sk_drivers) == 0:
need_to_enable_again = True
# Before baking, disabling from viewport all meshes
for obj in [n.blender_object for n in export_settings['vtree'].nodes.values() if n.blender_type in
[VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]:
obj.hide_viewport = True
export_settings['vtree'].nodes[obj_uuid].blender_object.hide_viewport = False
else:
print_console("WARNING", "Can't disable viewport because of drivers")
export_settings['gltf_optimize_armature_disable_viewport'] = False # We changed the option here, so we don't need to re-check it later, during
export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, False) export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, False)
######## Export ######## Export
@ -296,9 +315,9 @@ def gather_action_animations( obj_uuid: int,
if export_settings['gltf_force_sampling'] is True: if export_settings['gltf_force_sampling'] is True:
if export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE": if export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE":
animation = gather_action_armature_sampled(obj_uuid, blender_action, None, export_settings) animation, extra_samplers = gather_action_armature_sampled(obj_uuid, blender_action, None, export_settings)
elif on_type == "OBJECT": elif on_type == "OBJECT":
animation = gather_action_object_sampled(obj_uuid, blender_action, None, export_settings) animation, extra_samplers = gather_action_object_sampled(obj_uuid, blender_action, None, export_settings)
else: else:
animation = gather_action_sk_sampled(obj_uuid, blender_action, None, export_settings) animation = gather_action_sk_sampled(obj_uuid, blender_action, None, export_settings)
else: else:
@ -307,7 +326,7 @@ def gather_action_animations( obj_uuid: int,
# - animation on fcurves # - animation on fcurves
# - fcurve that cannot be handled not sampled, to be sampled # - fcurve that cannot be handled not sampled, to be sampled
# to_be_sampled is : (object_uuid , type , prop, optional(bone.name) ) # to_be_sampled is : (object_uuid , type , prop, optional(bone.name) )
animation, to_be_sampled = gather_animation_fcurves(obj_uuid, blender_action, export_settings) animation, to_be_sampled, extra_samplers = gather_animation_fcurves(obj_uuid, blender_action, export_settings)
for (obj_uuid, type_, prop, bone) in to_be_sampled: for (obj_uuid, type_, prop, bone) in to_be_sampled:
if type_ == "BONE": if type_ == "BONE":
channel = gather_sampled_bone_channel(obj_uuid, bone, prop, blender_action.name, True, get_gltf_interpolation("LINEAR"), export_settings) channel = gather_sampled_bone_channel(obj_uuid, bone, prop, blender_action.name, True, get_gltf_interpolation("LINEAR"), export_settings)
@ -334,6 +353,11 @@ def gather_action_animations( obj_uuid: int,
if channel is not None: if channel is not None:
animation.channels.append(channel) animation.channels.append(channel)
# Add extra samplers
# Because this is not core glTF specification, you can add extra samplers using hook
if export_settings['gltf_export_extra_animations'] and len(extra_samplers) != 0:
export_user_extensions('extra_animation_manage', export_settings, extra_samplers, obj_uuid, blender_object, blender_action, animation)
# If we are in a SK animation, and we need to bake (if there also in TRS anim) # If we are in a SK animation, and we need to bake (if there also in TRS anim)
if len([a for a in blender_actions if a[2] == "OBJECT"]) == 0 and on_type == "SHAPEKEY": if len([a for a in blender_actions if a[2] == "OBJECT"]) == 0 and on_type == "SHAPEKEY":
if export_settings['gltf_bake_animation'] is True and export_settings['gltf_force_sampling'] is True: if export_settings['gltf_bake_animation'] is True and export_settings['gltf_force_sampling'] is True:
@ -343,7 +367,7 @@ def gather_action_animations( obj_uuid: int,
if obj_uuid not in export_settings['ranges'].keys(): if obj_uuid not in export_settings['ranges'].keys():
export_settings['ranges'][obj_uuid] = {} export_settings['ranges'][obj_uuid] = {}
export_settings['ranges'][obj_uuid][obj_uuid] = export_settings['ranges'][obj_uuid][blender_action.name] export_settings['ranges'][obj_uuid][obj_uuid] = export_settings['ranges'][obj_uuid][blender_action.name]
channels = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings) channels, _ = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings)
if channels is not None: if channels is not None:
if animation is None: if animation is None:
animation = gltf2_io.Animation( animation = gltf2_io.Animation(
@ -431,6 +455,15 @@ def gather_action_animations( obj_uuid: int,
if blender_object and current_world_matrix is not None: if blender_object and current_world_matrix is not None:
blender_object.matrix_world = current_world_matrix blender_object.matrix_world = current_world_matrix
if export_settings['gltf_optimize_armature_disable_viewport'] \
and export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE":
if need_to_enable_again is True:
# And now, restoring meshes in viewport
for node, obj in [(n, n.blender_object) for n in export_settings['vtree'].nodes.values() if n.blender_type in
[VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]:
obj.hide_viewport = node.default_hide_viewport
export_settings['vtree'].nodes[obj_uuid].blender_object.hide_viewport = export_settings['vtree'].nodes[obj_uuid].default_hide_viewport
export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, True) export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, True)
return animations, tracks return animations, tracks
@ -447,6 +480,9 @@ def __get_blender_actions(obj_uuid: str,
export_user_extensions('pre_gather_actions_hook', export_settings, blender_object) export_user_extensions('pre_gather_actions_hook', export_settings, blender_object)
if export_settings['gltf_animation_mode'] == "BROADCAST":
return __get_blender_actions_broadcast(obj_uuid, export_settings)
if blender_object and blender_object.animation_data is not None: if blender_object and blender_object.animation_data is not None:
# Collect active action. # Collect active action.
if blender_object.animation_data.action is not None: if blender_object.animation_data.action is not None:
@ -571,3 +607,72 @@ def __gather_extras(blender_action, export_settings):
if export_settings['gltf_extras']: if export_settings['gltf_extras']:
return generate_extras(blender_action) return generate_extras(blender_action)
return None return None
def __get_blender_actions_broadcast(obj_uuid, export_settings):
blender_actions = []
blender_tracks = {}
action_on_type = {}
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
# Note : Like in FBX exporter:
# - Object with animation data will get all actions
# - Object without animation will not get any action
# Collect all actions
for blender_action in bpy.data.actions:
if hasattr(bpy.data.scenes[0], "gltf_action_filter") \
and id(blender_action) in [id(item.action) for item in bpy.data.scenes[0].gltf_action_filter if item.keep is False]:
continue # We ignore this action
# Keep all actions on objects (no Shapekey animation, No armature animation (on bones))
if blender_action.id_root == "OBJECT": #TRS and Bone animations
if blender_object.animation_data is None:
continue
if blender_object and blender_object.type == "ARMATURE" and __is_armature_action(blender_action):
blender_actions.append(blender_action)
blender_tracks[blender_action.name] = None
action_on_type[blender_action.name] = "OBJECT"
elif blender_object.type == "MESH":
if not __is_armature_action(blender_action):
blender_actions.append(blender_action)
blender_tracks[blender_action.name] = None
action_on_type[blender_action.name] = "OBJECT"
elif blender_action.id_root == "KEY":
if blender_object.type != "MESH" or blender_object.data is None or blender_object.data.shape_keys is None or blender_object.data.shape_keys.animation_data is None:
continue
# Checking that the object has some SK and some animation on it
if blender_object is None:
continue
if blender_object.type != "MESH":
continue
if blender_object.data is None or blender_object.data.shape_keys is None:
continue
blender_actions.append(blender_action)
blender_tracks[blender_action.name] = None
action_on_type[blender_action.name] = "SHAPEKEY"
# Use a class to get parameters, to be able to modify them
class GatherActionHookParameters:
def __init__(self, blender_actions, blender_tracks, action_on_type):
self.blender_actions = blender_actions
self.blender_tracks = blender_tracks
self.action_on_type = action_on_type
gatheractionhookparams = GatherActionHookParameters(blender_actions, blender_tracks, action_on_type)
export_user_extensions('gather_actions_hook', export_settings, blender_object, gatheractionhookparams)
# Get params back from hooks
blender_actions = gatheractionhookparams.blender_actions
blender_tracks = gatheractionhookparams.blender_tracks
action_on_type = gatheractionhookparams.action_on_type
# Remove duplicate actions.
blender_actions = list(set(blender_actions))
# sort animations alphabetically (case insensitive) so they have a defined order and match Blender's Action list
blender_actions.sort(key = lambda a: a.name.lower())
return [(blender_action, blender_tracks[blender_action.name], action_on_type[blender_action.name]) for blender_action in blender_actions]

View File

@ -162,6 +162,9 @@ def merge_tracks_perform(merged_tracks, animations, export_settings):
def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None): def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None):
# Bake situation does not export any extra animation channels, as we bake TRS + weights on Track or scene level, without direct
# Access to fcurve and action data
# if there is no animation in file => no need to bake # if there is no animation in file => no need to bake
if len(bpy.data.actions) == 0: if len(bpy.data.actions) == 0:
return None return None
@ -180,8 +183,7 @@ def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None
# (skinned meshes TRS must be ignored, says glTF specification) # (skinned meshes TRS must be ignored, says glTF specification)
if export_settings['vtree'].nodes[obj_uuid].skin is None: if export_settings['vtree'].nodes[obj_uuid].skin is None:
if mode is None or mode == "OBJECT": if mode is None or mode == "OBJECT":
animation = gather_action_object_sampled(obj_uuid, None, animation_key, export_settings) animation, _ = gather_action_object_sampled(obj_uuid, None, animation_key, export_settings)
# Need to bake sk only if not linked to a driver sk by parent armature # Need to bake sk only if not linked to a driver sk by parent armature
# In case of NLA track export, no baking of SK # In case of NLA track export, no baking of SK
@ -227,7 +229,7 @@ def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None
# We need to bake all bones. Because some bone can have some constraints linking to # We need to bake all bones. Because some bone can have some constraints linking to
# some other armature bones, for example # some other armature bones, for example
animation = gather_action_armature_sampled(obj_uuid, None, animation_key, export_settings) animation, _ = gather_action_armature_sampled(obj_uuid, None, animation_key, export_settings)
link_samplers(animation, export_settings) link_samplers(animation, export_settings)
if animation is not None: if animation is not None:
return animation return animation

View File

@ -14,7 +14,7 @@ def gather_animations(export_settings):
export_settings['ranges'] = {} export_settings['ranges'] = {}
export_settings['slide'] = {} export_settings['slide'] = {}
if export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]: if export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
return gather_actions_animations(export_settings) return gather_actions_animations(export_settings)
elif export_settings['gltf_animation_mode'] == "SCENE": elif export_settings['gltf_animation_mode'] == "SCENE":
return gather_scene_animations(export_settings) return gather_scene_animations(export_settings)

View File

@ -15,12 +15,21 @@ class Keyframe:
self.__length_morph = 0 self.__length_morph = 0
# Note: channels has some None items only for SK if some SK are not animated # Note: channels has some None items only for SK if some SK are not animated
if bake_channel is None: if bake_channel is None:
if not all([c == None for c in channels]):
self.target = [c for c in channels if c is not None][0].data_path.split('.')[-1] self.target = [c for c in channels if c is not None][0].data_path.split('.')[-1]
if self.target != "value": if self.target != "value":
self.__indices = [c.array_index for c in channels] self.__indices = [c.array_index for c in channels]
else: else:
self.__indices = [i for i, c in enumerate(channels) if c is not None] self.__indices = [i for i, c in enumerate(channels) if c is not None]
self.__length_morph = len(channels) self.__length_morph = len(channels)
else:
# If all channels are None (baking evaluate SK case)
self.target = "value"
self.__indices = []
self.__length_morph = len(channels)
for i in range(self.get_target_len()):
self.__indices.append(i)
else: else:
if bake_channel == "value": if bake_channel == "value":
self.__length_morph = len(channels) self.__length_morph = len(channels)
@ -47,10 +56,7 @@ class Keyframe:
"rotation_quaternion": 4, "rotation_quaternion": 4,
"scale": 3, "scale": 3,
"value": self.__length_morph "value": self.__length_morph
}.get(self.target) }.get(self.target, 1)
if length is None:
raise RuntimeError("Animations with target type '{}' are not supported.".format(self.target))
return length return length

View File

@ -68,7 +68,7 @@ def gather_scene_animations(export_settings):
if blender_object and blender_object.type != "ARMATURE": if blender_object and blender_object.type != "ARMATURE":
# We have to check if this is a skinned mesh, because we don't have to force animation baking on this case # We have to check if this is a skinned mesh, because we don't have to force animation baking on this case
if export_settings['vtree'].nodes[obj_uuid].skin is None: if export_settings['vtree'].nodes[obj_uuid].skin is None:
channels = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings) channels, _ = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings)
if channels is not None: if channels is not None:
total_channels.extend(channels) total_channels.extend(channels)
if export_settings['gltf_morph_anim'] and blender_object.type == "MESH" \ if export_settings['gltf_morph_anim'] and blender_object.type == "MESH" \
@ -90,11 +90,11 @@ def gather_scene_animations(export_settings):
elif blender_object is None: elif blender_object is None:
# This is GN instances # This is GN instances
# Currently, not checking if this instance is skinned.... #TODO # Currently, not checking if this instance is skinned.... #TODO
channels = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings) channels, _ = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings)
if channels is not None: if channels is not None:
total_channels.extend(channels) total_channels.extend(channels)
else: else:
channels = gather_armature_sampled_channels(obj_uuid, obj_uuid, export_settings) channels, _ = gather_armature_sampled_channels(obj_uuid, obj_uuid, export_settings)
if channels is not None: if channels is not None:
total_channels.extend(channels) total_channels.extend(channels)

View File

@ -47,6 +47,9 @@ def gather_track_animations( obj_uuid: int,
animations = [] animations = []
# Bake situation does not export any extra animation channels, as we bake TRS + weights on Track or scene level, without direct
# Access to fcurve and action data
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
# Collect all tracks affecting this object. # Collect all tracks affecting this object.
blender_tracks = __get_blender_tracks(obj_uuid, export_settings) blender_tracks = __get_blender_tracks(obj_uuid, export_settings)

View File

@ -8,6 +8,7 @@ from ......io.exp.gltf2_io_user_extensions import export_user_extensions
from ......io.com.gltf2_io_debug import print_console from ......io.com.gltf2_io_debug import print_console
from ......io.com import gltf2_io from ......io.com import gltf2_io
from .....com.gltf2_blender_extras import generate_extras from .....com.gltf2_blender_extras import generate_extras
from ...fcurves.gltf2_blender_gather_fcurves_sampler import gather_animation_fcurves_sampler
from .armature_channels import gather_armature_sampled_channels from .armature_channels import gather_armature_sampled_channels
@ -22,8 +23,9 @@ def gather_action_armature_sampled(armature_uuid: str, blender_action: typing.Op
name = __gather_name(blender_action, armature_uuid, cache_key, export_settings) name = __gather_name(blender_action, armature_uuid, cache_key, export_settings)
try: try:
channels, extra_channels = __gather_channels(armature_uuid, blender_action.name if blender_action else cache_key, export_settings)
animation = gltf2_io.Animation( animation = gltf2_io.Animation(
channels=__gather_channels(armature_uuid, blender_action.name if blender_action else cache_key, export_settings), channels=channels,
extensions=None, extensions=None,
extras=__gather_extras(blender_action, export_settings), extras=__gather_extras(blender_action, export_settings),
name=name, name=name,
@ -35,15 +37,28 @@ def gather_action_armature_sampled(armature_uuid: str, blender_action: typing.Op
export_user_extensions('pre_gather_animation_hook', export_settings, animation, blender_action, blender_object) export_user_extensions('pre_gather_animation_hook', export_settings, animation, blender_action, blender_object)
extra_samplers = []
if export_settings['gltf_export_extra_animations']:
for chan in [chan for chan in extra_channels.values() if len(chan['properties']) != 0]:
for channel_group_name, channel_group in chan['properties'].items():
# No glTF channel here, as we don't have any target
# Trying to retrieve sampler directly
sampler = gather_animation_fcurves_sampler(armature_uuid, tuple(channel_group), None, None, True, export_settings)
if sampler is not None:
extra_samplers.append((channel_group_name, sampler))
if not animation.channels: if not animation.channels:
return None return None, extra_samplers
# To allow reuse of samplers in one animation : This will be done later, when we know all channels are here # To allow reuse of samplers in one animation : This will be done later, when we know all channels are here
export_user_extensions('gather_animation_hook', export_settings, animation, blender_action, blender_object) # For compatibility for older version export_user_extensions('gather_animation_hook', export_settings, animation, blender_action, blender_object) # For compatibility for older version
export_user_extensions('animation_action_armature_sampled', export_settings, animation, blender_object, blender_action, cache_key) export_user_extensions('animation_action_armature_sampled', export_settings, animation, blender_object, blender_action, cache_key)
return animation return animation, extra_samplers
def __gather_name(blender_action: bpy.types.Action, def __gather_name(blender_action: bpy.types.Action,
armature_uuid: str, armature_uuid: str,

View File

@ -18,6 +18,7 @@ from .armature_sampler import gather_bone_sampled_animation_sampler
def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_settings) -> typing.List[gltf2_io.AnimationChannel]: def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_settings) -> typing.List[gltf2_io.AnimationChannel]:
channels = [] channels = []
extra_channels = {}
# Then bake all bones # Then bake all bones
bones_to_be_animated = [] bones_to_be_animated = []
@ -28,7 +29,7 @@ def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_
list_of_animated_bone_channels = {} list_of_animated_bone_channels = {}
if armature_uuid != blender_action_name and blender_action_name in bpy.data.actions: if armature_uuid != blender_action_name and blender_action_name in bpy.data.actions:
# Not bake situation # Not bake situation
channels_animated, to_be_sampled = get_channel_groups(armature_uuid, bpy.data.actions[blender_action_name], export_settings) channels_animated, to_be_sampled, extra_channels = get_channel_groups(armature_uuid, bpy.data.actions[blender_action_name], export_settings)
for chan in [chan for chan in channels_animated.values() if chan['bone'] is not None]: for chan in [chan for chan in channels_animated.values() if chan['bone'] is not None]:
for prop in chan['properties'].keys(): for prop in chan['properties'].keys():
list_of_animated_bone_channels[ list_of_animated_bone_channels[
@ -88,7 +89,7 @@ def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_
if channel is not None: if channel is not None:
channels.append(channel) channels.append(channel)
return channels return channels, extra_channels
def gather_sampled_bone_channel( def gather_sampled_bone_channel(
armature_uuid: str, armature_uuid: str,
@ -152,7 +153,7 @@ def __gather_sampler(armature_uuid, bone, channel, action_name, node_channel_is_
def __gather_armature_object_channel(obj_uuid: str, blender_action, export_settings): def __gather_armature_object_channel(obj_uuid: str, blender_action, export_settings):
channels = [] channels = []
channels_animated, to_be_sampled = get_channel_groups(obj_uuid, blender_action, export_settings) channels_animated, to_be_sampled, extra_channels = get_channel_groups(obj_uuid, blender_action, export_settings)
# Remove all channel linked to bones, keep only directly object channels # Remove all channel linked to bones, keep only directly object channels
channels_animated = [c for c in channels_animated.values() if c['type'] == "OBJECT"] channels_animated = [c for c in channels_animated.values() if c['type'] == "OBJECT"]
to_be_sampled = [c for c in to_be_sampled if c[1] == "OBJECT"] to_be_sampled = [c for c in to_be_sampled if c[1] == "OBJECT"]

View File

@ -35,6 +35,27 @@ def get_cache_data(path: str,
if export_settings['gltf_animation_mode'] in "NLA_TRACKS": if export_settings['gltf_animation_mode'] in "NLA_TRACKS":
obj_uuids = [blender_obj_uuid] obj_uuids = [blender_obj_uuid]
# If there is only 1 object to cache, we can disable viewport for other objects (for performance)
# This can be on these cases:
# - TRACK mode
# - Only one object to cache (but here, no really useful for performance)
# - Action mode, where some object have multiple actions
# - For this case, on first call, we will cache active action for all objects
# - On next calls, we will cache only the action of current object, so we can disable viewport for others
# For armature : We already checked that we can disable viewport (in case of drivers, this is currently not possible)
need_to_enable_again = False
if export_settings['gltf_optimize_armature_disable_viewport'] is True and len(obj_uuids) == 1:
need_to_enable_again = True
# Before baking, disabling from viewport all meshes
for obj in [n.blender_object for n in export_settings['vtree'].nodes.values() if n.blender_type in
[VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]:
if obj is None:
continue
obj.hide_viewport = True
export_settings['vtree'].nodes[obj_uuids[0]].blender_object.hide_viewport = False
depsgraph = bpy.context.evaluated_depsgraph_get() depsgraph = bpy.context.evaluated_depsgraph_get()
frame = min_ frame = min_
@ -94,7 +115,7 @@ def get_cache_data(path: str,
if export_settings['vtree'].nodes[obj_uuid].blender_type != VExportNode.COLLECTION: if export_settings['vtree'].nodes[obj_uuid].blender_type != VExportNode.COLLECTION:
if blender_obj and blender_obj.animation_data and blender_obj.animation_data.action \ if blender_obj and blender_obj.animation_data and blender_obj.animation_data.action \
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]: and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
if blender_obj.animation_data.action.name not in data[obj_uuid].keys(): if blender_obj.animation_data.action.name not in data[obj_uuid].keys():
data[obj_uuid][blender_obj.animation_data.action.name] = {} data[obj_uuid][blender_obj.animation_data.action.name] = {}
data[obj_uuid][blender_obj.animation_data.action.name]['matrix'] = {} data[obj_uuid][blender_obj.animation_data.action.name]['matrix'] = {}
@ -125,7 +146,7 @@ def get_cache_data(path: str,
if blender_obj and blender_obj.type == "ARMATURE": if blender_obj and blender_obj.type == "ARMATURE":
bones = export_settings['vtree'].get_all_bones(obj_uuid) bones = export_settings['vtree'].get_all_bones(obj_uuid)
if blender_obj.animation_data and blender_obj.animation_data.action \ if blender_obj.animation_data and blender_obj.animation_data.action \
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]: and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
if 'bone' not in data[obj_uuid][blender_obj.animation_data.action.name].keys(): if 'bone' not in data[obj_uuid][blender_obj.animation_data.action.name].keys():
data[obj_uuid][blender_obj.animation_data.action.name]['bone'] = {} data[obj_uuid][blender_obj.animation_data.action.name]['bone'] = {}
elif blender_obj.animation_data \ elif blender_obj.animation_data \
@ -155,7 +176,7 @@ def get_cache_data(path: str,
matrix = matrix @ blender_obj.matrix_world matrix = matrix @ blender_obj.matrix_world
if blender_obj.animation_data and blender_obj.animation_data.action \ if blender_obj.animation_data and blender_obj.animation_data.action \
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]: and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
if blender_bone.name not in data[obj_uuid][blender_obj.animation_data.action.name]['bone'].keys(): if blender_bone.name not in data[obj_uuid][blender_obj.animation_data.action.name]['bone'].keys():
data[obj_uuid][blender_obj.animation_data.action.name]['bone'][blender_bone.name] = {} data[obj_uuid][blender_obj.animation_data.action.name]['bone'][blender_bone.name] = {}
data[obj_uuid][blender_obj.animation_data.action.name]['bone'][blender_bone.name][frame] = matrix data[obj_uuid][blender_obj.animation_data.action.name]['bone'][blender_bone.name][frame] = matrix
@ -187,7 +208,7 @@ def get_cache_data(path: str,
and blender_obj.data.shape_keys is not None \ and blender_obj.data.shape_keys is not None \
and blender_obj.data.shape_keys.animation_data is not None \ and blender_obj.data.shape_keys.animation_data is not None \
and blender_obj.data.shape_keys.animation_data.action is not None \ and blender_obj.data.shape_keys.animation_data.action is not None \
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]: and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
if blender_obj.data.shape_keys.animation_data.action.name not in data[obj_uuid].keys(): if blender_obj.data.shape_keys.animation_data.action.name not in data[obj_uuid].keys():
data[obj_uuid][blender_obj.data.shape_keys.animation_data.action.name] = {} data[obj_uuid][blender_obj.data.shape_keys.animation_data.action.name] = {}
@ -233,7 +254,7 @@ def get_cache_data(path: str,
if dr_obj not in data.keys(): if dr_obj not in data.keys():
data[dr_obj] = {} data[dr_obj] = {}
if blender_obj.animation_data and blender_obj.animation_data.action \ if blender_obj.animation_data and blender_obj.animation_data.action \
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]: and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
if obj_uuid + "_" + blender_obj.animation_data.action.name not in data[dr_obj]: # Using uuid of armature + armature animation name as animation name if obj_uuid + "_" + blender_obj.animation_data.action.name not in data[dr_obj]: # Using uuid of armature + armature animation name as animation name
data[dr_obj][obj_uuid + "_" + blender_obj.animation_data.action.name] = {} data[dr_obj][obj_uuid + "_" + blender_obj.animation_data.action.name] = {}
data[dr_obj][obj_uuid + "_" + blender_obj.animation_data.action.name]['sk'] = {} data[dr_obj][obj_uuid + "_" + blender_obj.animation_data.action.name]['sk'] = {}
@ -254,6 +275,14 @@ def get_cache_data(path: str,
data[dr_obj][obj_uuid + "_" + obj_uuid]['sk'][None][frame] = [k.value for k in get_sk_exported(driver_object.data.shape_keys.key_blocks)] data[dr_obj][obj_uuid + "_" + obj_uuid]['sk'][None][frame] = [k.value for k in get_sk_exported(driver_object.data.shape_keys.key_blocks)]
frame += step frame += step
# And now, restoring meshes in viewport
for node, obj in [(n, n.blender_object) for n in export_settings['vtree'].nodes.values() if n.blender_type in
[VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]:
obj.hide_viewport = node.default_hide_viewport
export_settings['vtree'].nodes[obj_uuids[0]].blender_object.hide_viewport = export_settings['vtree'].nodes[obj_uuids[0]].default_hide_viewport
return data return data
# For perf, we may be more precise, and get a list of ranges to be exported that include all needed frames # For perf, we may be more precise, and get a list of ranges to be exported that include all needed frames

View File

@ -7,29 +7,45 @@ import typing
from ......io.com import gltf2_io from ......io.com import gltf2_io
from ......io.exp.gltf2_io_user_extensions import export_user_extensions from ......io.exp.gltf2_io_user_extensions import export_user_extensions
from .....com.gltf2_blender_extras import generate_extras from .....com.gltf2_blender_extras import generate_extras
from ...fcurves.gltf2_blender_gather_fcurves_sampler import gather_animation_fcurves_sampler
from .gltf2_blender_gather_object_channels import gather_object_sampled_channels from .gltf2_blender_gather_object_channels import gather_object_sampled_channels
def gather_action_object_sampled(object_uuid: str, blender_action: typing.Optional[bpy.types.Action], cache_key: str, export_settings): def gather_action_object_sampled(object_uuid: str, blender_action: typing.Optional[bpy.types.Action], cache_key: str, export_settings):
extra_samplers = []
# If no animation in file, no need to bake # If no animation in file, no need to bake
if len(bpy.data.actions) == 0: if len(bpy.data.actions) == 0:
return None return None, extra_samplers
channels, extra_channels = __gather_channels(object_uuid, blender_action.name if blender_action else cache_key, export_settings)
animation = gltf2_io.Animation( animation = gltf2_io.Animation(
channels=__gather_channels(object_uuid, blender_action.name if blender_action else cache_key, export_settings), channels=channels,
extensions=None, extensions=None,
extras=__gather_extras(blender_action, export_settings), extras=__gather_extras(blender_action, export_settings),
name=__gather_name(object_uuid, blender_action, cache_key, export_settings), name=__gather_name(object_uuid, blender_action, cache_key, export_settings),
samplers=[] samplers=[]
) )
if export_settings['gltf_export_extra_animations']:
for chan in [chan for chan in extra_channels.values() if len(chan['properties']) != 0]:
for channel_group_name, channel_group in chan['properties'].items():
# No glTF channel here, as we don't have any target
# Trying to retrieve sampler directly
sampler = gather_animation_fcurves_sampler(object_uuid, tuple(channel_group), None, None, True, export_settings)
if sampler is not None:
extra_samplers.append((channel_group_name, sampler, "OBJECT", None))
if not animation.channels: if not animation.channels:
return None return None, extra_samplers
blender_object = export_settings['vtree'].nodes[object_uuid].blender_object blender_object = export_settings['vtree'].nodes[object_uuid].blender_object
export_user_extensions('animation_action_object_sampled', export_settings, animation, blender_object, blender_action, cache_key) export_user_extensions('animation_action_object_sampled', export_settings, animation, blender_object, blender_action, cache_key)
return animation return animation, extra_samplers
def __gather_name(object_uuid: str, blender_action: typing.Optional[bpy.types.Action], cache_key: str, export_settings): def __gather_name(object_uuid: str, blender_action: typing.Optional[bpy.types.Action], cache_key: str, export_settings):
if blender_action: if blender_action:

View File

@ -15,11 +15,15 @@ from .gltf2_blender_gather_object_channel_target import gather_object_sampled_ch
def gather_object_sampled_channels(object_uuid: str, blender_action_name: str, export_settings) -> typing.List[gltf2_io.AnimationChannel]: def gather_object_sampled_channels(object_uuid: str, blender_action_name: str, export_settings) -> typing.List[gltf2_io.AnimationChannel]:
channels = [] channels = []
extra_channels = {}
# Bake situation does not export any extra animation channels, as we bake TRS + weights on Track or scene level, without direct
# Access to fcurve and action data
list_of_animated_channels = {} list_of_animated_channels = {}
if object_uuid != blender_action_name and blender_action_name in bpy.data.actions: if object_uuid != blender_action_name and blender_action_name in bpy.data.actions:
# Not bake situation # Not bake situation
channels_animated, to_be_sampled = get_channel_groups(object_uuid, bpy.data.actions[blender_action_name], export_settings) channels_animated, to_be_sampled, extra_channels = get_channel_groups(object_uuid, bpy.data.actions[blender_action_name], export_settings)
for chan in [chan for chan in channels_animated.values() if chan['bone'] is None]: for chan in [chan for chan in channels_animated.values() if chan['bone'] is None]:
for prop in chan['properties'].keys(): for prop in chan['properties'].keys():
list_of_animated_channels[ list_of_animated_channels[
@ -45,7 +49,7 @@ def gather_object_sampled_channels(object_uuid: str, blender_action_name: str, e
export_user_extensions('animation_gather_object_channel', export_settings, blender_object, blender_action_name) export_user_extensions('animation_gather_object_channel', export_settings, blender_object, blender_action_name)
return channels if len(channels) > 0 else None return channels if len(channels) > 0 else None, extra_channels
@cached @cached
def gather_sampled_object_channel( def gather_sampled_object_channel(

View File

@ -2,10 +2,14 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import bpy
import typing
import numpy as np import numpy as np
from ......blender.com.gltf2_blender_data_path import get_sk_exported from ......blender.com.gltf2_blender_data_path import get_sk_exported
from ....gltf2_blender_gather_cache import cached from ....gltf2_blender_gather_cache import cached
from ...gltf2_blender_gather_keyframes import Keyframe from ...gltf2_blender_gather_keyframes import Keyframe
from ...fcurves.gltf2_blender_gather_fcurves_channels import get_channel_groups
from ...fcurves.gltf2_blender_gather_fcurves_keyframes import gather_non_keyed_values
from ..gltf2_blender_gather_animation_sampling_cache import get_cache_data from ..gltf2_blender_gather_animation_sampling_cache import get_cache_data
@ -22,6 +26,40 @@ def gather_sk_sampled_keyframes(obj_uuid,
frame = start_frame frame = start_frame
step = export_settings['gltf_frame_step'] step = export_settings['gltf_frame_step']
blender_obj = export_settings['vtree'].nodes[obj_uuid].blender_object blender_obj = export_settings['vtree'].nodes[obj_uuid].blender_object
if export_settings['gltf_optimize_armature_disable_viewport'] is True:
# Using this option, we miss the drivers :(
# No solution exists for now. In the future, we should be able to copy a driver
if action_name in bpy.data.actions:
channel_group, _ = get_channel_groups(obj_uuid, bpy.data.actions[action_name], export_settings, no_sample_option=True)
elif blender_obj.data.shape_keys.animation_data and blender_obj.data.shape_keys.animation_data.action:
channel_group, _ = get_channel_groups(obj_uuid, blender_obj.data.shape_keys.animation_data.action, export_settings, no_sample_option=True)
else:
channel_group = {}
channels = [None] * len(get_sk_exported(blender_obj.data.shape_keys.key_blocks))
# One day, if we will be able to bake drivers or evaluate it the right way, we can add here the driver fcurves
for chan in channel_group.values():
channels = chan['properties']['value']
break
non_keyed_values = gather_non_keyed_values(obj_uuid, channels, None, export_settings)
while frame <= end_frame:
key = Keyframe(channels, frame, None)
key.value = [c.evaluate(frame) for c in channels if c is not None]
# Complete key with non keyed values, if needed
if len([c for c in channels if c is not None]) != key.get_target_len():
complete_key(key, non_keyed_values)
keyframes.append(key)
frame += step
else:
# Full bake, we will go frame by frame. This can take time (more than using evaluate)
while frame <= end_frame: while frame <= end_frame:
key = Keyframe([None] * (len(get_sk_exported(blender_obj.data.shape_keys.key_blocks))), frame, 'value') key = Keyframe([None] * (len(get_sk_exported(blender_obj.data.shape_keys.key_blocks))), frame, 'value')
key.value_total = get_cache_data( key.value_total = get_cache_data(
@ -54,3 +92,13 @@ def gather_sk_sampled_keyframes(obj_uuid,
def fcurve_is_constant(keyframes): def fcurve_is_constant(keyframes):
return all([j < 0.0001 for j in np.ptp([[k.value[i] for i in range(len(keyframes[0].value))] for k in keyframes], axis=0)]) return all([j < 0.0001 for j in np.ptp([[k.value[i] for i in range(len(keyframes[0].value))] for k in keyframes], axis=0)])
#TODO de-duplicate, but import issue???
def complete_key(key: Keyframe, non_keyed_values: typing.Tuple[typing.Optional[float]]):
"""
Complete keyframe with non keyed values
"""
for i in range(0, key.get_target_len()):
if i in key.get_indices():
continue # this is a keyed array_index or a SK animated
key.set_value_index(i, non_keyed_values[i])

View File

@ -106,6 +106,9 @@ def __create_buffer(exporter, export_settings):
buffer = bytes() buffer = bytes()
if export_settings['gltf_format'] == 'GLB': if export_settings['gltf_format'] == 'GLB':
buffer = exporter.finalize_buffer(export_settings['gltf_filedirectory'], is_glb=True) buffer = exporter.finalize_buffer(export_settings['gltf_filedirectory'], is_glb=True)
else:
if export_settings['gltf_format'] == 'GLTF_EMBEDDED':
exporter.finalize_buffer(export_settings['gltf_filedirectory'])
else: else:
exporter.finalize_buffer(export_settings['gltf_filedirectory'], exporter.finalize_buffer(export_settings['gltf_filedirectory'],
export_settings['gltf_binaryfilename']) export_settings['gltf_binaryfilename'])

View File

@ -100,7 +100,7 @@ def datacache(func):
# Here are the key used: result[obj_uuid][action_name][path][bone][frame] # Here are the key used: result[obj_uuid][action_name][path][bone][frame]
return result[cache_key_args[1]][cache_key_args[3]][cache_key_args[0]][cache_key_args[2]][cache_key_args[4]] return result[cache_key_args[1]][cache_key_args[3]][cache_key_args[0]][cache_key_args[2]][cache_key_args[4]]
# object is in cache, but not this action # object is in cache, but not this action
# We need to keep other actions # We need to not erase other actions of this object
elif cache_key_args[3] not in func.__cache[cache_key_args[1]].keys(): elif cache_key_args[3] not in func.__cache[cache_key_args[1]].keys():
result = func(*args, only_gather_provided=True) result = func(*args, only_gather_provided=True)
# The result can contains multiples animations, in case this is an armature with drivers # The result can contains multiples animations, in case this is an armature with drivers

View File

@ -55,6 +55,8 @@ class VExportNode:
self.blender_object = None self.blender_object = None
self.blender_bone = None self.blender_bone = None
self.default_hide_viewport = False # Need to store the default value for meshes in case of animation baking on armature
self.force_as_empty = False # Used for instancer display self.force_as_empty = False # Used for instancer display
# Only for bone/bone and object parented to bone # Only for bone/bone and object parented to bone
@ -160,14 +162,17 @@ class VExportTree:
node.blender_type = VExportNode.COLLECTION node.blender_type = VExportNode.COLLECTION
elif blender_object.type == "ARMATURE": elif blender_object.type == "ARMATURE":
node.blender_type = VExportNode.ARMATURE node.blender_type = VExportNode.ARMATURE
node.default_hide_viewport = blender_object.hide_viewport
elif blender_object.type == "CAMERA": elif blender_object.type == "CAMERA":
node.blender_type = VExportNode.CAMERA node.blender_type = VExportNode.CAMERA
elif blender_object.type == "LIGHT": elif blender_object.type == "LIGHT":
node.blender_type = VExportNode.LIGHT node.blender_type = VExportNode.LIGHT
elif blender_object.instance_type == "COLLECTION": elif blender_object.instance_type == "COLLECTION":
node.blender_type = VExportNode.INST_COLLECTION node.blender_type = VExportNode.INST_COLLECTION
node.default_hide_viewport = blender_object.hide_viewport
else: else:
node.blender_type = VExportNode.OBJECT node.blender_type = VExportNode.OBJECT
node.default_hide_viewport = blender_object.hide_viewport
# For meshes with armature modifier (parent is armature), keep armature uuid # For meshes with armature modifier (parent is armature), keep armature uuid
if node.blender_type == VExportNode.OBJECT: if node.blender_type == VExportNode.OBJECT:

View File

@ -125,7 +125,7 @@ class GlTF2Exporter:
f.write(self.__buffer.to_bytes()) f.write(self.__buffer.to_bytes())
uri = buffer_name uri = buffer_name
else: else:
pass # This is no more possible, we don't export embedded buffers uri = self.__buffer.to_embed_string()
buffer = gltf2_io.Buffer( buffer = gltf2_io.Buffer(
byte_length=self.__buffer.byte_length, byte_length=self.__buffer.byte_length,

View File

@ -163,12 +163,13 @@ def __gather_normal_scale(primary_socket, export_settings):
def __gather_occlusion_strength(primary_socket, export_settings): def __gather_occlusion_strength(primary_socket, export_settings):
# Look for a MixRGB node that mixes with pure white in front of # Look for a MixRGB node that mixes with pure white in front of
# primary_socket. The mix factor gives the occlusion strength. # primary_socket. The mix factor gives the occlusion strength.
node = previous_node(primary_socket) nav = primary_socket.to_node_nav()
if node and node.node.type == 'MIX' and node.node.blend_type == 'MIX': nav.move_back()
fac = get_const_from_socket(NodeSocket(node.node.inputs['Factor'], node.group_path), kind='VALUE') if nav.moved and nav.node.type == 'MIX' and nav.node.blend_type == 'MIX':
col1 = get_const_from_socket(NodeSocket(node.node.inputs[6], node.group_path), kind='RGB') fac = nav.get_constant('Factor')
col2 = get_const_from_socket(NodeSocket(node.node.inputs[7], node.group_path), kind='RGB')
if fac is not None: if fac is not None:
col1 = nav.get_constant('#A_Color')
col2 = nav.get_constant('#B_Color')
if col1 == [1.0, 1.0, 1.0] and col2 is None: if col1 == [1.0, 1.0, 1.0] and col2 is None:
return fac return fac
if col1 is None and col2 == [1.0, 1.0, 1.0]: if col1 is None and col2 == [1.0, 1.0, 1.0]:

View File

@ -184,11 +184,210 @@ def get_socket_from_gltf_material_node(blender_material: bpy.types.Material, nam
return NodeSocket(None, None) return NodeSocket(None, None)
class NodeNav:
"""Helper for navigating through node trees."""
def __init__(self, node, in_socket=None, out_socket=None):
self.node = node # Current node
self.out_socket = out_socket # Socket through which we arrived at this node
self.in_socket = in_socket # Socket through which we will leave this node
self.stack = [] # Stack of (group node, socket) pairs descended through to get here
self.moved = False # Whether the last move_back call moved back or not
def copy(self):
new = NodeNav(self.node)
new.assign(self)
return new
def assign(self, other):
self.node = other.node
self.in_socket = other.in_socket
self.out_socket = other.out_socket
self.stack = other.stack.copy()
self.moved = other.moved
def select_input_socket(self, in_soc):
"""Selects an input socket.
Most operations that operate on the input socket can be passed an in_soc
parameter to select an input socket before running.
"""
if in_soc is None:
# Keep current selected input socket
return
elif isinstance(in_soc, bpy.types.NodeSocket):
assert in_soc.node == self.node
self.in_socket = in_soc
elif isinstance(in_soc, int):
self.in_socket = self.node.inputs[in_soc]
else:
assert isinstance(in_soc, str)
# An identifier like "#A_Color" selects a socket by
# identifier. This is useful for sockets that cannot be
# selected because of non-unique names.
if in_soc.startswith('#'):
ident = in_soc.removeprefix('#')
for socket in self.node.inputs:
if socket.identifier == ident:
self.in_socket = socket
return
# Select by regular name
self.in_socket = self.node.inputs[in_soc]
def get_out_socket_index(self):
assert self.out_socket
for i, soc in enumerate(self.node.outputs):
if soc == self.out_socket:
return i
assert False
def descend(self):
"""Descend into a group node."""
if self.node and self.node.type == 'GROUP' and self.node.node_tree and self.out_socket:
i = self.get_out_socket_index()
self.stack.append((self.node, self.out_socket))
self.node = next(node for node in self.node.node_tree.nodes if node.type == 'GROUP_OUTPUT')
self.in_socket = self.node.inputs[i]
self.out_socket = None
def ascend(self):
"""Ascend from a group input node back to the group node."""
if self.stack and self.node and self.node.type == 'GROUP_INPUT' and self.out_socket:
i = self.get_out_socket_index()
self.node, self.out_socket = self.stack.pop()
self.in_socket = self.node.inputs[i]
def move_back(self, in_soc=None):
"""Move backwards through an input socket to the next node."""
self.moved = False
self.select_input_socket(in_soc)
if not self.in_socket or not self.in_socket.is_linked:
return
# Warning, slow! socket.links is O(total number of links)!
link = self.in_socket.links[0]
self.node = link.from_node
self.out_socket = link.from_socket
self.in_socket = None
self.moved = True
# Continue moving
if self.node.type == 'REROUTE':
self.move_back(0)
elif self.node.type == 'GROUP':
self.descend()
self.move_back()
elif self.node.type == 'GROUP_INPUT':
self.ascend()
self.move_back()
def peek_back(self, in_soc=None):
"""Peeks backwards through an input socket without modifying self."""
s = self.copy()
s.select_input_socket(in_soc)
s.move_back()
return s
def get_constant(self, in_soc=None):
"""Gets a constant from an input socket. Returns None if non-constant."""
self.select_input_socket(in_soc)
if not self.in_socket:
return None
# Get constant from unlinked socket's default value
if not self.in_socket.is_linked:
if self.in_socket.type == 'RGBA':
color = list(self.in_socket.default_value)
color = color[:3] # drop unused alpha component (assumes shader tree)
return color
elif self.in_socket.type == 'SHADER':
# Treat unlinked shader sockets as black
return [0.0, 0.0, 0.0]
elif self.in_socket.type == 'VECTOR':
return list(self.in_socket.default_value)
elif self.in_socket.type == 'VALUE':
return self.in_socket.default_value
else:
return None
# Check for a constant in the next node
nav = self.peek_back()
if nav.moved:
if self.in_socket.type == 'RGBA':
if nav.node.type == 'RGB':
color = list(nav.out_socket.default_value)
color = color[:3] # drop unused alpha component (assumes shader tree)
return color
elif self.in_socket.type == 'VALUE':
if nav.node.type == 'VALUE':
return nav.node.out_socket.default_value
return None
def get_factor(self, in_soc=None):
"""Gets a factor, eg. metallicFactor. Either a constant or constant multiplier."""
self.select_input_socket(in_soc)
if not self.in_socket:
return None
# Constant
fac = self.get_constant()
if fac is not None:
return fac
# Multiplied by constant
nav = self.peek_back()
if nav.moved:
x1, x2 = None, None
if self.in_socket.type == 'RGBA':
is_mul = (
nav.node.type == 'MIX' and
nav.node.data_type == 'RGBA' and
nav.node.blend_type == 'MULTIPLY'
)
if is_mul:
# TODO: check factor is 1?
x1 = nav.get_constant('#A_Color')
x2 = nav.get_constant('#B_Color')
elif self.in_socket.type == 'VALUE':
if nav.node.type == 'MATH' and nav.node.operation == 'MULTIPLY':
x1 = nav.get_constant(0)
x2 = nav.get_constant(1)
if x1 is not None and x2 is None: return x1
if x2 is not None and x1 is None: return x2
return None
class NodeSocket: class NodeSocket:
def __init__(self, socket, group_path): def __init__(self, socket, group_path):
self.socket = socket self.socket = socket
self.group_path = group_path self.group_path = group_path
def to_node_nav(self):
assert self.socket
nav = NodeNav(
self.socket.node,
out_socket=self.socket if self.socket.is_output else None,
in_socket=self.socket if not self.socket.is_output else None,
)
# No output socket information
nav.stack = [(node, None) for node in self.group_path]
return nav
class ShNode: class ShNode:
def __init__(self, node, group_path): def __init__(self, node, group_path):
self.node = node self.node = node
@ -243,52 +442,15 @@ def get_socket(blender_material: bpy.types.Material, name: str, volume=False):
return NodeSocket(None, None) return NodeSocket(None, None)
# Old, prefer NodeNav.get_factor in new code
def get_factor_from_socket(socket, kind): def get_factor_from_socket(socket, kind):
""" return socket.to_node_nav().get_factor()
For baseColorFactor, metallicFactor, etc.
Get a constant value from a socket, or a constant value
from a MULTIPLY node just before the socket.
kind is either 'RGB' or 'VALUE'.
"""
fac = get_const_from_socket(socket, kind)
if fac is not None:
return fac
node = previous_node(socket)
if node.node is not None:
x1, x2 = None, None
if kind == 'RGB':
if node.node.type == 'MIX' and node.node.data_type == "RGBA" and node.node.blend_type == 'MULTIPLY':
# TODO: handle factor in inputs[0]?
x1 = get_const_from_socket(NodeSocket(node.node.inputs[6], node.group_path), kind)
x2 = get_const_from_socket(NodeSocket(node.node.inputs[7], node.group_path), kind)
if kind == 'VALUE':
if node.node.type == 'MATH' and node.node.operation == 'MULTIPLY':
x1 = get_const_from_socket(NodeSocket(node.node.inputs[0], node.group_path), kind)
x2 = get_const_from_socket(NodeSocket(node.node.inputs[1], node.group_path), kind)
if x1 is not None and x2 is None: return x1
if x2 is not None and x1 is None: return x2
return None
# Old, prefer NodeNav.get_constant in new code
def get_const_from_socket(socket, kind): def get_const_from_socket(socket, kind):
if not socket.socket.is_linked: return socket.to_node_nav().get_constant()
if kind == 'RGB':
if socket.socket.type != 'RGBA': return None
return list(socket.socket.default_value)[:3]
if kind == 'VALUE':
if socket.socket.type != 'VALUE': return None
return socket.socket.default_value
# Handle connection to a constant RGB/Value node
prev_node = previous_node(socket)
if prev_node.node is not None:
if kind == 'RGB' and prev_node.node.type == 'RGB':
return list(prev_node.node.outputs[0].default_value)[:3]
if kind == 'VALUE' and prev_node.node.type == 'VALUE':
return prev_node.node.outputs[0].default_value
return None
def previous_socket(socket: NodeSocket): def previous_socket(socket: NodeSocket):

View File

@ -49,3 +49,6 @@ class Buffer:
def clear(self): def clear(self):
self.__data = b"" self.__data = b""
def to_embed_string(self):
return 'data:application/octet-stream;base64,' + base64.b64encode(self.__data).decode('ascii')

View File

@ -1,21 +1,6 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# --------------------------------- TISSUE ----------------------------------- # # --------------------------------- TISSUE ----------------------------------- #
# ------------------------------- version 0.3 -------------------------------- # # ------------------------------- version 0.3 -------------------------------- #

View File

@ -1,3 +1,5 @@
# SPDX-FileCopyrightText: 2022-2023 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
import bpy import bpy

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------# #-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
# # # #
# Vertex Color to Vertex Group allow you to convert colors channles to weight # # Vertex Color to Vertex Group allow you to convert colors channles to weight #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# # # #
# (c) Alessandro Zomparelli # # (c) Alessandro Zomparelli #
# (2017) # # (2017) #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# --------------------------------- DUAL MESH -------------------------------- # # --------------------------------- DUAL MESH -------------------------------- #
# -------------------------------- version 0.3 ------------------------------- # # -------------------------------- version 0.3 ------------------------------- #
# # # #

View File

@ -1,20 +1,7 @@
# ##### BEGIN GPL LICENSE BLOCK ##### # SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
# #
# This program is free software; you can redistribute it and/or # SPDX-License-Identifier: GPL-2.0-or-later
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# --------------------------- LATTICE ALONG SURFACE -------------------------- # # --------------------------- LATTICE ALONG SURFACE -------------------------- #
# -------------------------------- version 0.3 ------------------------------- # # -------------------------------- version 0.3 ------------------------------- #
# # # #

View File

@ -1,20 +1,6 @@
# ##### BEGIN GPL LICENSE BLOCK ##### # SPDX-FileCopyrightText: 2020 Alessandro Zomparelli
# #
# This program is free software; you can redistribute it and/or # SPDX-License-Identifier: GPL-2.0-or-later
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# # # #
# (c) Alessandro Zomparelli # # (c) Alessandro Zomparelli #

View File

@ -1,20 +1,6 @@
# ##### BEGIN GPL LICENSE BLOCK ##### # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
# #
# This program is free software; you can redistribute it and/or # SPDX-License-Identifier: GPL-2.0-or-later
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
import numpy as np import numpy as np
import time import time

View File

@ -1,20 +1,6 @@
# ##### BEGIN GPL LICENSE BLOCK ##### # SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
# #
# This program is free software; you can redistribute it and/or # SPDX-License-Identifier: GPL-2.0-or-later
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- # # ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
# ------------------------------- version 0.84 ------------------------------- # # ------------------------------- version 0.84 ------------------------------- #

View File

@ -1,20 +1,6 @@
# ##### BEGIN GPL LICENSE BLOCK ##### # SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
# #
# This program is free software; you can redistribute it and/or # SPDX-License-Identifier: GPL-2.0-or-later
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- # # ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
# ------------------------------- version 0.84 ------------------------------- # # ------------------------------- version 0.84 ------------------------------- #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------# #-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
# # # #
# Vertex Color to Vertex Group allow you to convert colors channles to weight # # Vertex Color to Vertex Group allow you to convert colors channles to weight #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- # # ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
# ------------------------------- version 0.84 ------------------------------- # # ------------------------------- version 0.84 ------------------------------- #
# # # #

View File

@ -1,3 +1,5 @@
# SPDX-FileCopyrightText: 2019-2023 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
import bpy, bmesh import bpy, bmesh

View File

@ -1,25 +1,7 @@
# SPDX-FileCopyrightText: 2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
# ---------------------------------------------------------- # ----------------------------------------------------------
# Author: Stephen Leger (s-leger) # Author: Stephen Leger (s-leger)
# #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# --------------------------------- UV to MESH ------------------------------- # # --------------------------------- UV to MESH ------------------------------- #
# -------------------------------- version 0.1.1 ----------------------------- # # -------------------------------- version 0.1.1 ----------------------------- #
# # # #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------# #-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
# # # #
# Vertex Color to Vertex Group allow you to convert colors channles to weight # # Vertex Color to Vertex Group allow you to convert colors channles to weight #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2022-2023 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------# #-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
# # # #
# Vertex Color to Vertex Group allow you to convert colors channles to weight # # Vertex Color to Vertex Group allow you to convert colors channles to weight #

View File

@ -72,6 +72,11 @@ def write_mesh(context, report_cb):
obj = layer.objects.active obj = layer.objects.active
export_data_layers = print_3d.use_data_layers export_data_layers = print_3d.use_data_layers
# Make sure at least one object is selected.
if not context.selected_objects:
report_cb({'ERROR'}, "No objects selected")
return False
# Create name 'export_path/blendname-objname' # Create name 'export_path/blendname-objname'
# add the filename component # add the filename component
if bpy.data.is_saved: if bpy.data.is_saved: