3D Print Toolbox: Add hollow out #105194
@ -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)
|
||||
|
@ -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(MATXPFALL, wrap.transmission))
|
||||
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(shading)
|
||||
|
||||
|
@ -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,)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -5,11 +5,19 @@
|
||||
|
||||
def get_target_property_name(data_path: str) -> str:
|
||||
"""Retrieve target property."""
|
||||
|
||||
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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
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["):
|
||||
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],
|
||||
|
@ -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,
|
||||
@ -178,7 +184,7 @@ def __gather_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
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
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:
|
||||
# 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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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,6 +26,40 @@ 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
|
||||
|
||||
|
||||
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(
|
||||
@ -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])
|
||||
|
@ -106,6 +106,9 @@ def __create_buffer(exporter, export_settings):
|
||||
buffer = bytes()
|
||||
if export_settings['gltf_format'] == 'GLB':
|
||||
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:
|
||||
exporter.finalize_buffer(export_settings['gltf_filedirectory'],
|
||||
export_settings['gltf_binaryfilename'])
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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]:
|
||||
|
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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 -------------------------------- #
|
||||
|
@ -1,3 +1,5 @@
|
||||
# SPDX-FileCopyrightText: 2022-2023 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
|
@ -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 #
|
||||
|
@ -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) #
|
||||
|
@ -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 ------------------------------- #
|
||||
# #
|
||||
|
@ -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 ------------------------------- #
|
||||
# #
|
||||
|
@ -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 #
|
||||
|
@ -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
|
||||
|
@ -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 ------------------------------- #
|
||||
|
@ -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 ------------------------------- #
|
||||
|
@ -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 #
|
||||
|
@ -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 ------------------------------- #
|
||||
# #
|
||||
|
@ -1,3 +1,5 @@
|
||||
# SPDX-FileCopyrightText: 2019-2023 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy, bmesh
|
||||
|
@ -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)
|
||||
#
|
||||
|
@ -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 ----------------------------- #
|
||||
# #
|
||||
|
@ -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 #
|
||||
|
@ -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 #
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user