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,
FloatProperty,
StringProperty,
CollectionProperty,
)
import bpy
bl_info = {
"name": "Autodesk 3DS format",
"author": "Bob Holcomb, Campbell Barton, Sebastian Schrand",
"version": (2, 4, 9),
"version": (2, 5, 0),
"blender": (4, 1, 0),
"location": "File > Import-Export",
"description": "3DS Import/Export meshes, UVs, materials, textures, "
@ -46,6 +47,9 @@ class Import3DS(bpy.types.Operator, ImportHelper):
filename_ext = ".3ds"
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(
name="Constrain Size",
@ -106,7 +110,6 @@ class Import3DS(bpy.types.Operator, ImportHelper):
def execute(self, context):
from . import import_3ds
keywords = self.as_keywords(ignore=("axis_forward",
"axis_up",
"filter_glob",
@ -123,6 +126,17 @@ class Import3DS(bpy.types.Operator, ImportHelper):
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):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
@ -346,6 +360,7 @@ def menu_func_import(self, context):
def register():
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_transform)
bpy.utils.register_class(Export3DS)
@ -357,6 +372,7 @@ def register():
def unregister():
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_transform)
bpy.utils.unregister_class(Export3DS)

View File

@ -713,7 +713,8 @@ def make_material_chunk(material, image):
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(MATSELFILPCT, wrap.emission_strength))
material_chunk.add_subchunk(make_percent_subchunk(MATREFBLUR, wrap.node_principled_bsdf.inputs['Coat Weight'].default_value))
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(shading)
primary_tex = False

View File

@ -1671,10 +1671,12 @@ def load_3ds(filepath, context, CONSTRAIN=10.0, UNITS=False, IMAGE_SEARCH=True,
object_dictionary.clear()
object_matrix.clear()
"""
if APPLY_MATRIX:
for ob in imported_objects:
if ob.type == 'MESH':
ob.data.transform(ob.matrix_local.inverted())
"""
if UNITS:
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()
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_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,
APPLY_MATRIX=use_apply_transform, CONVERSE=global_matrix, CURSOR=use_cursor, PIVOT=use_center_pivot,)

View File

@ -5,7 +5,7 @@
bl_info = {
'name': 'glTF 2.0 format',
'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),
'location': 'File > Import-Export',
'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
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:
@ -254,17 +275,12 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
export_format: EnumProperty(
name='Format',
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')),
items=get_format_items,
description=(
'Output format. Binary is most efficient, '
'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,
)
@ -584,6 +600,10 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
'Export actions (actives and on NLA tracks) as separate animations'),
('ACTIVE_ACTIONS', 'Active actions merged',
'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',
'Export individual NLA Tracks as separate animation'),
('SCENE', 'Scene',
@ -657,6 +677,15 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
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(
name='Negative Frames',
items=(('SLIDE', 'Slide',
@ -851,6 +880,15 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
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
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_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_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_reset_pose_bones'] = self.export_reset_pose_bones
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_negative_frames'] = self.export_negative_frame
export_settings['gltf_anim_slide_to_zero'] = self.export_anim_slide_to_zero
export_settings['gltf_export_extra_animations'] = self.export_extra_animations
else:
export_settings['gltf_frame_range'] = 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_keep_armature'] = 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_reset_pose_bones'] = False
export_settings['gltf_export_reset_sk_data'] = False
export_settings['gltf_export_extra_animations'] = False
export_settings['gltf_skins'] = self.export_skins
if self.export_skins:
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')
if operator.export_keep_originals is False:
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, 'will_save_settings')
@ -1161,7 +1205,7 @@ class GLTF_PT_export_gltfpack(bpy.types.Panel):
def poll(cls, context):
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
return False;
return False
sfile = context.space_data
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')
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')
if operator.export_animation_mode == "SCENE":
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')
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')
layout.prop(operator, 'export_anim_slide_to_zero')
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')
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):
sfile = context.space_data
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="")
def draw(self, context):
@ -1829,6 +1873,36 @@ class GLTF_PT_export_animation_optimize(bpy.types.Panel):
row = layout.row()
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):
bl_space_type = 'FILE_BROWSER'
@ -2112,6 +2186,12 @@ class GLTF_AddonPreferences(bpy.types.AddonPreferences):
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):
layout = self.layout
row = layout.row()
@ -2120,6 +2200,10 @@ class GLTF_AddonPreferences(bpy.types.AddonPreferences):
row.prop(self, "animation_ui", text="Animation UI")
row = layout.row()
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):
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_sampling,
GLTF_PT_export_animation_optimize,
GLTF_PT_export_animation_extra,
GLTF_PT_export_gltfpack,
GLTF_PT_export_user_extensions,
ImportGLTF2,

View File

@ -5,11 +5,19 @@
def get_target_property_name(data_path: str) -> str:
"""Retrieve target property."""
return data_path.rsplit('.', 1)[-1]
if data_path.endswith("]"):
return None
else:
return data_path.rsplit('.', 1)[-1]
def get_target_object_path(data_path: str) -> str:
"""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)
self_targeting = len(path_split) < 2
if self_targeting:

View File

@ -564,7 +564,7 @@ class SCENE_PT_gltf2_action_filter(bpy.types.Panel):
def poll(self, context):
sfile = context.space_data
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):
sfile = context.space_data

View File

@ -16,7 +16,7 @@ def gather_animation_fcurves(
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(
channels=channels,
@ -27,12 +27,12 @@ def gather_animation_fcurves(
)
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
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,
export_settings

View File

@ -23,25 +23,40 @@ def gather_animation_fcurves_channels(
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
if blender_action.use_frame_range:
custom_range = (blender_action.frame_start, blender_action.frame_end)
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():
channel = __gather_animation_fcurve_channel(chan['obj_uuid'], channel_group, chan['bone'], custom_range, export_settings)
if channel is not None:
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_extra = {}
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
# object_path : blank for blender_object itself, key_blocks["<name>"] for SK, pose.bones["<name>"] for bones
if not object_path:
target = blender_object
type_ = "OBJECT"
if fcurve.data_path.startswith("["):
target = blender_object
type_ = "EXTRA"
else:
target = blender_object
type_ = "OBJECT"
else:
try:
target = get_object_from_datapath(blender_object, object_path)
if blender_object.type == "ARMATURE" and fcurve.data_path.startswith("pose.bones["):
type_ = "BONE"
if target_property is not None:
if get_target(target_property) is not None:
type_ = "BONE"
else:
type_ = "EXTRA"
else:
type_ = "EXTRA"
else:
type_ = "EXTRA"
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
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
target_data = targets.get(target, {})
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
new_properties = {}
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 :)
else:
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))
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):
@ -226,7 +273,7 @@ def __gather_animation_fcurve_channel(obj_uuid: str,
__target= __gather_target(obj_uuid, channel_group, bone, export_settings)
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:
# 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],
bone: typing.Optional[str],
custom_range: typing.Optional[set],
extra_mode: bool,
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,
channels: typing.Tuple[bpy.types.FCurve],

View File

@ -16,11 +16,12 @@ def gather_fcurve_keyframes(
channel_group: typing.Tuple[bpy.types.FCurve],
bone: typing.Optional[str],
custom_range: typing.Optional[set],
extra_mode: bool,
export_settings):
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
# 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]
# 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():
__complete_key(key, non_keyed_values)
complete_key(key, non_keyed_values)
# compute tangents for cubic spline interpolation
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
def __gather_non_keyed_values(
def gather_non_keyed_values(
obj_uuid: str,
channel_group: typing.Tuple[bpy.types.FCurve],
bone: typing.Optional[str],
extra_mode: bool,
export_settings
) -> 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
non_keyed_values = []
@ -132,7 +138,7 @@ def __gather_non_keyed_values(
if i in indices:
non_keyed_values.append(None)
else:
if bone is None is None:
if bone is None:
non_keyed_values.append({
"delta_location" : blender_object.delta_location,
"delta_rotation_euler" : blender_object.delta_rotation_euler,
@ -169,16 +175,16 @@ def __gather_non_keyed_values(
shapekeys_idx[cpt_sk] = sk.name
cpt_sk += 1
for idx_c, channel in enumerate(channel_group):
if channel is None:
non_keyed_values.append(blender_object.data.shape_keys.key_blocks[shapekeys_idx[idx_c]].value)
else:
non_keyed_values.append(None)
for idx_c, channel in enumerate(channel_group):
if channel is None:
non_keyed_values.append(blender_object.data.shape_keys.key_blocks[shapekeys_idx[idx_c]].value)
else:
non_keyed_values.append(None)
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
"""

View File

@ -23,6 +23,7 @@ def gather_animation_fcurves_sampler(
channel_group: typing.Tuple[bpy.types.FCurve],
bone: typing.Optional[str],
custom_range: typing.Optional[set],
extra_mode: bool,
export_settings
) -> gltf2_io.AnimationSampler:
@ -33,6 +34,7 @@ def gather_animation_fcurves_sampler(
channel_group,
bone,
custom_range,
extra_mode,
export_settings)
if keyframes is None:
@ -40,7 +42,7 @@ def gather_animation_fcurves_sampler(
return None
# 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(
extensions=None,
@ -62,16 +64,18 @@ def __gather_keyframes(
channel_group: typing.Tuple[bpy.types.FCurve],
bone: typing.Optional[str],
custom_range: typing.Optional[set],
extra_mode: bool,
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(
obj_uuid: str,
channel_group: typing.Tuple[bpy.types.FCurve],
bone_name: typing.Optional[str],
keyframes,
extra_mode: bool,
export_settings):
times = [k.seconds for k in keyframes]
@ -137,6 +141,17 @@ def __convert_keyframes(
values = []
fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
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
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction)
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
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
@ -296,9 +315,9 @@ def gather_action_animations( obj_uuid: int,
if export_settings['gltf_force_sampling'] is True:
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":
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:
animation = gather_action_sk_sampled(obj_uuid, blender_action, None, export_settings)
else:
@ -307,7 +326,7 @@ def gather_action_animations( obj_uuid: int,
# - animation on fcurves
# - fcurve that cannot be handled not sampled, to be sampled
# 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:
if type_ == "BONE":
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:
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 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:
@ -343,7 +367,7 @@ def gather_action_animations( obj_uuid: int,
if obj_uuid not in export_settings['ranges'].keys():
export_settings['ranges'][obj_uuid] = {}
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 animation is None:
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:
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)
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)
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:
# Collect active action.
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']:
return generate_extras(blender_action)
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):
# 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 len(bpy.data.actions) == 0:
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)
if export_settings['vtree'].nodes[obj_uuid].skin is None:
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
# 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
# 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)
if animation is not None:
return animation

View File

@ -14,7 +14,7 @@ def gather_animations(export_settings):
export_settings['ranges'] = {}
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)
elif export_settings['gltf_animation_mode'] == "SCENE":
return gather_scene_animations(export_settings)

View File

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

View File

@ -68,7 +68,7 @@ def gather_scene_animations(export_settings):
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
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:
total_channels.extend(channels)
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:
# This is GN instances
# 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:
total_channels.extend(channels)
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:
total_channels.extend(channels)

View File

@ -47,6 +47,9 @@ def gather_track_animations( obj_uuid: int,
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
# Collect all tracks affecting this object.
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 import gltf2_io
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
@ -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)
try:
channels, extra_channels = __gather_channels(armature_uuid, blender_action.name if blender_action else cache_key, export_settings)
animation = gltf2_io.Animation(
channels=__gather_channels(armature_uuid, blender_action.name if blender_action else cache_key, export_settings),
channels=channels,
extensions=None,
extras=__gather_extras(blender_action, export_settings),
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)
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:
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
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)
return animation
return animation, extra_samplers
def __gather_name(blender_action: bpy.types.Action,
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]:
channels = []
extra_channels = {}
# Then bake all bones
bones_to_be_animated = []
@ -28,7 +29,7 @@ def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_
list_of_animated_bone_channels = {}
if armature_uuid != blender_action_name and blender_action_name in bpy.data.actions:
# 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 prop in chan['properties'].keys():
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:
channels.append(channel)
return channels
return channels, extra_channels
def gather_sampled_bone_channel(
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):
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
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"]

View File

@ -35,6 +35,27 @@ def get_cache_data(path: str,
if export_settings['gltf_animation_mode'] in "NLA_TRACKS":
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()
frame = min_
@ -94,7 +115,7 @@ def get_cache_data(path: str,
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 \
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():
data[obj_uuid][blender_obj.animation_data.action.name] = {}
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":
bones = export_settings['vtree'].get_all_bones(obj_uuid)
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():
data[obj_uuid][blender_obj.animation_data.action.name]['bone'] = {}
elif blender_obj.animation_data \
@ -155,7 +176,7 @@ def get_cache_data(path: str,
matrix = matrix @ blender_obj.matrix_world
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():
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
@ -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.animation_data 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():
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():
data[dr_obj] = {}
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
data[dr_obj][obj_uuid + "_" + blender_obj.animation_data.action.name] = {}
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)]
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
# 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.exp.gltf2_io_user_extensions import export_user_extensions
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
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 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(
channels=__gather_channels(object_uuid, blender_action.name if blender_action else cache_key, export_settings),
channels=channels,
extensions=None,
extras=__gather_extras(blender_action, export_settings),
name=__gather_name(object_uuid, blender_action, cache_key, export_settings),
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:
return None
return None, extra_samplers
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)
return animation
return animation, extra_samplers
def __gather_name(object_uuid: str, blender_action: typing.Optional[bpy.types.Action], cache_key: str, export_settings):
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]:
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 = {}
if object_uuid != blender_action_name and blender_action_name in bpy.data.actions:
# 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 prop in chan['properties'].keys():
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)
return channels if len(channels) > 0 else None
return channels if len(channels) > 0 else None, extra_channels
@cached
def gather_sampled_object_channel(

View File

@ -2,10 +2,14 @@
#
# SPDX-License-Identifier: Apache-2.0
import bpy
import typing
import numpy as np
from ......blender.com.gltf2_blender_data_path import get_sk_exported
from ....gltf2_blender_gather_cache import cached
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
@ -22,20 +26,54 @@ def gather_sk_sampled_keyframes(obj_uuid,
frame = start_frame
step = export_settings['gltf_frame_step']
blender_obj = export_settings['vtree'].nodes[obj_uuid].blender_object
while frame <= end_frame:
key = Keyframe([None] * (len(get_sk_exported(blender_obj.data.shape_keys.key_blocks))), frame, 'value')
key.value_total = get_cache_data(
'sk',
obj_uuid,
None,
action_name,
frame,
step,
export_settings
)
keyframes.append(key)
frame += step
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:
key = Keyframe([None] * (len(get_sk_exported(blender_obj.data.shape_keys.key_blocks))), frame, 'value')
key.value_total = get_cache_data(
'sk',
obj_uuid,
None,
action_name,
frame,
step,
export_settings
)
keyframes.append(key)
frame += step
if len(keyframes) == 0:
# For example, option CROP negative frames, but all are negatives
@ -54,3 +92,13 @@ def gather_sk_sampled_keyframes(obj_uuid,
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)])
#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

@ -107,8 +107,11 @@ def __create_buffer(exporter, export_settings):
if export_settings['gltf_format'] == 'GLB':
buffer = exporter.finalize_buffer(export_settings['gltf_filedirectory'], is_glb=True)
else:
exporter.finalize_buffer(export_settings['gltf_filedirectory'],
export_settings['gltf_binaryfilename'])
if export_settings['gltf_format'] == 'GLTF_EMBEDDED':
exporter.finalize_buffer(export_settings['gltf_filedirectory'])
else:
exporter.finalize_buffer(export_settings['gltf_filedirectory'],
export_settings['gltf_binaryfilename'])
return buffer

View File

@ -100,7 +100,7 @@ def datacache(func):
# 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]]
# 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():
result = func(*args, only_gather_provided=True)
# 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_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
# Only for bone/bone and object parented to bone
@ -160,14 +162,17 @@ class VExportTree:
node.blender_type = VExportNode.COLLECTION
elif blender_object.type == "ARMATURE":
node.blender_type = VExportNode.ARMATURE
node.default_hide_viewport = blender_object.hide_viewport
elif blender_object.type == "CAMERA":
node.blender_type = VExportNode.CAMERA
elif blender_object.type == "LIGHT":
node.blender_type = VExportNode.LIGHT
elif blender_object.instance_type == "COLLECTION":
node.blender_type = VExportNode.INST_COLLECTION
node.default_hide_viewport = blender_object.hide_viewport
else:
node.blender_type = VExportNode.OBJECT
node.default_hide_viewport = blender_object.hide_viewport
# For meshes with armature modifier (parent is armature), keep armature uuid
if node.blender_type == VExportNode.OBJECT:

View File

@ -125,7 +125,7 @@ class GlTF2Exporter:
f.write(self.__buffer.to_bytes())
uri = buffer_name
else:
pass # This is no more possible, we don't export embedded buffers
uri = self.__buffer.to_embed_string()
buffer = gltf2_io.Buffer(
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):
# Look for a MixRGB node that mixes with pure white in front of
# primary_socket. The mix factor gives the occlusion strength.
node = previous_node(primary_socket)
if node and node.node.type == 'MIX' and node.node.blend_type == 'MIX':
fac = get_const_from_socket(NodeSocket(node.node.inputs['Factor'], node.group_path), kind='VALUE')
col1 = get_const_from_socket(NodeSocket(node.node.inputs[6], node.group_path), kind='RGB')
col2 = get_const_from_socket(NodeSocket(node.node.inputs[7], node.group_path), kind='RGB')
nav = primary_socket.to_node_nav()
nav.move_back()
if nav.moved and nav.node.type == 'MIX' and nav.node.blend_type == 'MIX':
fac = nav.get_constant('Factor')
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:
return fac
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)
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:
def __init__(self, socket, group_path):
self.socket = socket
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:
def __init__(self, node, group_path):
self.node = node
@ -243,52 +442,15 @@ def get_socket(blender_material: bpy.types.Material, name: str, volume=False):
return NodeSocket(None, None)
# Old, prefer NodeNav.get_factor in new code
def get_factor_from_socket(socket, kind):
"""
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
return socket.to_node_nav().get_factor()
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):
if not socket.socket.is_linked:
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
return socket.to_node_nav().get_constant()
def previous_socket(socket: NodeSocket):

View File

@ -49,3 +49,6 @@ class Buffer:
def clear(self):
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
# ##### 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 ----------------------------------- #
# ------------------------------- version 0.3 -------------------------------- #

View File

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

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# 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 -------------------------#
# #
# 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
# ##### 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 #
# (2017) #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# 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 -------------------------------- #
# -------------------------------- 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
# 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 #####
# SPDX-License-Identifier: GPL-2.0-or-later
# --------------------------- LATTICE ALONG SURFACE -------------------------- #
# -------------------------------- 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
# 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 #####
# SPDX-License-Identifier: GPL-2.0-or-later
# #
# (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
# 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 #####
# SPDX-License-Identifier: GPL-2.0-or-later
import numpy as np
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
# 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 #####
# SPDX-License-Identifier: GPL-2.0-or-later
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
# ------------------------------- 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
# 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 #####
# SPDX-License-Identifier: GPL-2.0-or-later
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
# ------------------------------- version 0.84 ------------------------------- #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# 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 -------------------------#
# #
# 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
# ##### 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 --------------------------- #
# ------------------------------- version 0.84 ------------------------------- #
# #

View File

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

View File

@ -1,25 +1,7 @@
# SPDX-FileCopyrightText: 2022 Blender Foundation
#
# 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)
#

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# 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 ------------------------------- #
# -------------------------------- version 0.1.1 ----------------------------- #
# #

View File

@ -1,23 +1,7 @@
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
#
# 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 -------------------------#
# #
# 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
# ##### 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 -------------------------#
# #
# 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
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'
# add the filename component
if bpy.data.is_saved: